Files
pi-extensions/README.md
T
Joakim Persson b3dc979aba secret-guard: keep secret env values out of model context, session files and the palace feed
Measured 2026-10-12 on emb-7kj4vr4g. A new session ran `env | grep …` and
tried to redact the output itself with a sed. The sed only matched when TOKEN
came after the `=`, so MEMPALACE_REMOTE_TOKEN=<value> went through. That raw
value was found in 12 local session files back to 2026-08-16, and each of those
sessions had sent it to its model provider. The palace stayed clean only
because mempalace-toolkit's feeder scrubs transcripts before staging. Nothing
guarded the step from tool output to model context.

The new extension ports tier 1 of mempalace_redact.py exactly: the exact
values of env vars whose name says secret are replaced with <redacted:NAME>.
Matching a known value cannot hit anything that is not that secret.
- tool_result: content text, structuredContent (always returned when content
  changes, so it is not dropped) and details.
- context: the same pass over every request, for user `!` output, pasted text
  and resumed sessions that already hold a raw value.
- tool_call: block a write/edit whose new text contains a live secret's
  placeholder, i.e. writing a redacted .env back. The reason says to use bash
  and let the shell expand "$NAME".
PI_SECRET_GUARD=off makes it inert.

Tests (16, two-sided): selection, ordering, redaction, deep walk with
identity, every handler, the off switch. Two parity tests run
mempalace_redact.py on the same synthetic env and compare the selected names
and the output text. Thirteen mutations, one per rule, are each caught. Two
mutations initially survived and exposed fixture gaps, which are now fixed.
Full suite: 45/45.

End-to-end in real pi (erdc/deepseek-v4.1-flash). The secret was synthetic,
and a test-only probe on before_provider_request counted it in each payload.
- control without the guard: 1 raw copy sent to the provider, 1 in the
  session file;
- with the guard: 0 sent and 0 stored, placeholder in both;
- resumed control session: 1 sent without the guard, 0 with it;
- interactive `!echo $SECRET`: 0 sent, plus a TUI warning;
- a write of the placeholder: blocked, file sha unchanged.
Not covered, as documented: live progress (tool_execution_update), the stored
entry of a user `!` command, secrets not in env (no shape rules), and pi-task
children, which run --no-extensions.
2026-10-12 14:35:49 +02:00

28 KiB
Raw Blame History

pi-extensions

Custom and modified extensions for the pi coding-agent.

This repo is the single source of truth for extensions that aren't suitable for general publishing — personal workflow tweaks, modified versions of built-in examples, and extensions written for specific infrastructure. Symlinked into ~/.pi/agent/extensions/ so pi loads them automatically.

Part of the same family as pi-toolkit (bring-up) and skillset (agent skills).


Install

git clone ssh://git@gitea.jordbo.se:2222/joakimp/pi-extensions.git ~/src/src_local/pi-extensions
cd ~/src/src_local/pi-extensions
chmod +x install.sh
./install.sh

Each .ts file in extensions/ is symlinked into ~/.pi/agent/extensions/. Existing real files are backed up with a timestamp. Re-runs are idempotent.

Bundled skill

skill/SKILL.md is the agent skill that documents how to use these extensions (fork tier selection, recall discipline, task design, remote-pi mechanics). It lives here, co-located with the code it describes, so it travels with the package and can be vendored by downstream consumers (e.g. pi-devbox bakes it as a fallback skill so containers have it even without the skillset repo mounted). skill/evaluate-extension-usage.py is referenced by the skill and must stay alongside it. install.sh does not deploy the skill — skill deployment remains the skillset repo's job on a normal workstation.

Editing skill/ obliges you to refresh the mirror. skillset keeps a copy at skills/pi-extensions/, and that copy — not this one — is what most consumers read: every Mac's host-side ~/.agents/skills/pi-extensions symlinks into it, and a workstation with only skillset cloned has no other copy. The refresh has been forgotten on two consecutive edits (the mirror drifted 4,890 B behind, then 9,579 B), so hooks/pre-push now warns at push time when a pushed skill/ change is not yet mirrored, printing the exact cp. It warns rather than blocks — the refresh is a commit in another repo and chronologically comes after this push. Enforcement lives downstream: skillset's own pre-commit gate refuses a commit that leaves the mirror stale. ./install.sh activates the hook (core.hooksPath=hooks, per-clone config that cannot be tracked); it is silent when no skillset clone is on disk, since a reminder you cannot act on is noise.

Install a subset:

./install.sh --only ssh-controlmaster          # just this one
./install.sh --only "ssh-controlmaster,other"  # explicit list
./install.sh --skip "git-checkpoint"           # all except these

--only and --skip accept comma-separated names without the .ts suffix. --only takes precedence if both are given.

Alternative: pi install (local path)

Because package.json declares a pi manifest, you can also register this repo as a pi package:

pi install ~/src/src_local/pi-extensions

This makes pi manage the extension loading directly. The install.sh approach (symlinks) and pi install are mutually exclusive for the same extension — pick one per machine.

Uninstall

./install.sh --uninstall

Removes symlinks that point into this repo. Your own files in ~/.pi/agent/extensions/ are never touched.


Extensions

ssh-controlmaster.ts

Transparent SSH remote execution via a persistent ControlMaster socket.

When launched with --ssh user@host, all of pi's native file and shell tools (read, write, edit, bash) are transparently redirected to execute on the remote machine. One SSH connection is established at session start; all subsequent tool calls multiplex over it via a Unix socket. Much faster than the plain ssh.ts example which opens a new connection per tool call.

Use cases:

  • Diagnose and fix issues on a remote server without installing pi there
  • Work on Proxmox hosts, LXC containers, or ephemeral VMs
  • Any machine you have SSH key access to but don't own

Usage:

# Key-based auth (normal)
pi --ssh user@192.168.1.10

# Explicit remote path (skips the initial pwd call)
pi --ssh root@proxmox-node:/etc/pve

# Password auth — prompts before connecting
pi --ssh user@host --ssh-ask-pass

# Try without modifying your global install
pi -e ~/src/src_local/pi-extensions/extensions/ssh-controlmaster.ts --ssh user@host

Requirements:

  • SSH key-based auth (preferred), or password auth via --ssh-ask-pass (see below)
  • bash available on the remote

Note on --ssh-ask-pass: The password is prompted via pi's input dialog before the SSH connection is opened. Input is not masked — the password is visible while typing. It is passed to SSH via a temporary SSH_ASKPASS script (/tmp/pi-askpass-<pid>.sh, chmod 700) which is deleted immediately after the master is established.

How it works:

  1. On session_start, runs ssh -G <host> to read the effective config for that host
  2. If ~/.ssh/config already configures ControlMaster auto or yes for the host and its ControlPath directory is writable, the existing system socket is reused — no second connection is opened and pi does not tear down the master on exit (it was the system's to manage)
  3. Otherwise (no system master, or its ControlPath is on a read-only mount — e.g. ~/.ssh/cm when ~/.ssh is bind-mounted read-only) pi establishes its own master: ssh -fN -o ControlMaster=yes -o ControlPersist=yes -o ControlPath=/tmp/pi-cm-<pid>.sock <remote> and shuts it down cleanly on exit. The command-line -o ControlPath overrides the user's unwritable path.
  4. The remote pwd is resolved with a direct connection (-o ControlPath=none -o ControlMaster=no) so a read-only system ControlPath can't make the initial probe fail
  5. All tool calls multiplex over the socket with -o ControlMaster=no -o ControlPath=<socket> — near-zero per-call overhead
  6. The system prompt is patched to tell the LLM it's operating on <remoteCwd> (via SSH ControlMaster: <remote>)
  7. User ! shell commands are also routed over SSH

Hang-proofing: every SSH invocation carries ConnectTimeout=8 + ServerAlive* keepalives, and the startup probe / master start are additionally bounded by a 15 s wall-clock timeout. Key-auth calls also set BatchMode=yes so ssh can never wait silently on a /dev/tty password/passphrase or host-key prompt behind pi's TUI (--ssh-ask-pass omits BatchMode so the SSH_ASKPASS path still works). If the host can't be reached — e.g. a LAN target with no route from inside a container — the probe fails fast with an ✗ unreachable status and an error toast instead of hanging startup and silently swallowing your prompts.

LAN reachability from inside a container (-F config detection): the extension threads an -F <config> into every ssh call when one is available, so pi --ssh <peer> can reach hosts that require a ProxyJump exactly like the dssh alias does. Resolution order: PI_SSH_CONFIG=/path/to/config if set (leading ~ expanded), else ~/.ssh-local/config if it exists (the pi-devbox's setup-lan-access.sh regenerates it on every container start), else none. No hostnames are baked into the image — the LAN-jump list lives only in the host-owned, read-only-mounted ~/.config/devbox-shell/ssh-lan.conf; on the host (native pi) ~/.ssh-local/config doesn't exist so this is a no-op. The status/notify shows [config: <path>] when a non-default config is in use. To reach a new LAN peer from the container, add it to the host's ssh-lan.conf ProxyJump host line (it must already be a Host block in the host's ~/.ssh/config).

The status bar shows ⚡ own master or ⚡ system master so you can see which path was taken.

Status bar: Shows SSH ⚡ user@host:/path when the master is ready, ⟳ connecting… during setup, and an error state if the master fails to start.

Path mapping: Paths are rewritten by replacing the local cwd with the remote cwd. This means pi should be started from a directory that maps cleanly to a path on the remote. Use the user@host:/explicit/path form when the remote path differs significantly from your local working directory.


confirm-destructive.ts

Confirmation gates for dangerous bash commands and destructive session actions. Always-on — no flag needed.

Bash commands intercepted:

  • Recursive removes (rm -rf, rm -r, etc.)
  • Any sudo command
  • chmod/chown 777
  • dd if= (disk operations)
  • mkfs (format filesystem)
  • git push --force / git push -f
  • Writes to /dev/*
  • truncate --size 0

In non-interactive mode (e.g. pi -p) dangerous commands are blocked outright rather than prompted.

Session actions gated:

  • /new — confirms before clearing the session
  • /resume — confirms before switching away if the current session has messages
  • /fork — always confirms

git-checkpoint.ts

Creates a git stash checkpoint at the start of each turn, keyed to the session entry ID. If you /fork from a past entry, you're offered the option to restore the code to that point.

Silently skips when the working directory isn't inside a git repo, or when there are no changes to stash. Status bar shows ⎇ N checkpoints during active sessions.

Notes:

  • Uses git stash create — non-destructive, doesn't touch your working tree
  • Stash objects persist in the git repo even after pi exits, so you can apply them manually with git stash apply <ref> if needed
  • Checkpoints are in-memory per session — the entry→ref mapping is lost on restart, but the underlying stash objects remain

notify.ts

Sends a native terminal notification when the agent finishes and is waiting for input. Only fires when the agent ran for longer than the threshold (default 8 seconds) — quick responses are silently skipped.

Terminal support:

  • Kitty (KITTY_WINDOW_ID) → OSC 99
  • Windows Terminal / WSL (WT_SESSION) → Windows toast
  • Everything else (iTerm2, WezTerm, Ghostty) → OSC 777

Flag:

pi --notify-min-secs 15   # only notify for tasks over 15 seconds
pi --notify-min-secs 0    # notify on every agent completion

ext-toggle.ts

Registers /ext — a slash command that lists extensions in ~/.pi/agent/extensions/ and toggles individual ones on/off without leaving the TUI.

How it works: pi auto-discovers *.ts only. Toggling renames a file (or symlink) between name.ts and name.ts.off, so a disabled extension is invisible to the loader. After a toggle, the extension calls ctx.reload() so the change takes effect immediately — no restart needed.

Usage:

/ext                # opens the multi-toggle overlay
  • ↑ / ↓ — navigate
  • space — stage a toggle (visual ● / ○ flip; not yet applied)
  • enter — commit all staged changes and reload pi
  • esc — cancel, no changes

A footer line shows pending changes (e.g. pending: notify→off, foo→on) so you can see exactly what enter will apply. Guard rejections appear there too (⊘ ssh-controlmaster: …).

Notes:

  • Subdirectory-style extensions (name/index.ts) are listed read-only — v1 doesn't toggle them. Move the directory aside manually if needed.
  • install.sh --uninstall cleans up both .ts and .ts.off symlinks pointing into this repo, so a disabled extension won't be left behind.
  • Re-running ./install.sh respects a prior /ext disable: if <name>.ts.off already exists, the installer leaves it alone instead of silently re-enabling.
  • ssh-controlmaster cannot be disabled via /ext while pi was launched with --ssh — disabling mid-session would silently revert tool calls to the local filesystem. Exit pi and relaunch without --ssh instead.

Adding a new extension

  1. Drop a .ts file into extensions/
  2. Re-run ./install.sh — it picks up the new file and symlinks it
  3. In a running pi session, /reload is enough; no restart needed
  4. (or, with ext-toggle installed: /ext to disable noisy ones at runtime)

task.ts

Registers the pi-task runner as the task tool: a delegated task runs in an isolated child agent that sees only the spec (context ladder L0–L2), returns a machine-checked PASS/FAIL envelope, has every roots[] entry diffed before and after (a change outside write_allowed FAILS), and leaves an audit dir under ~/.pi/agent/pi-task/. Compare fork, whose child inherits the entire parent branch (L4) and returns prose.

Why a tool and not just the CLI: fork is a tool with a self-recommending description in the model's face every turn; the CLI had to be remembered, and the prose rule that said "use pi-task for briefs with prohibitions" lost to the tool list for months. The rule now lives in the tool description and in promptGuidelines, which pi appends to the system prompt — the one place compaction cannot remove it from.

Beyond wrapping the CLI the tool:

  • rejects, before a model run, the two spec errors that make a boundary violation certain — write_allowed not an exact subset of roots, and a writable root nested inside a watched-only root (the parent's porcelain would change every time);
  • serialises sibling task calls whose roots overlap (parallel siblings see each other's writes as violations — measured 2026-09-17);
  • returns the CLI's parent-facing report as the tool result (verdict, problems, deliverable, evidence pointers, audit dir) and the parsed result.json as details. A FAIL verdict is a result; only the CLI refusing to run is an error.

Finds the runner at $PI_TASK_BIN, /opt/pi-toolkit/bin/pi-task, ~/src/pi-toolkit/bin/pi-task, ~/src/src_local/pi-toolkit/bin/pi-task, /workspace/pi-toolkit/bin/pi-task, then $PATH. Effort tiers resolve through pi-fork.effortProfiles in settings.json, same as fork.

fork-gate.ts

A tool_call hook that blocks a fork whose brief contains a prohibition (do not / must not / never / only …), a write boundary (only touch, nothing else, read-only, stay within …) or a clause-initial file-changing imperative (Edit …, Commit …, Fix …, Implement …), and returns — as the block reason the model reads — the task(...) call to make instead, plus the CLI fallback.

Measured motivation: on 2026-09-17 all five fork briefs in one session carried "do not"; one returned confident verbatim quotes that did not exist in the source, and the four migration briefs had disjoint write boundaries that fork cannot enforce (they were re-run as pi-task and passed). Running the shipped classifier over those five real briefs redirects 5/5.

The gate matches wording, not intent, and says so: a genuinely read-only exploration brief that trips it is rephrased without the prohibition; one that cannot be rephrased needed task. Two-sided tests in test/fork-gate.test.mjs pin both the must-block and must-pass sets (the latter includes "Write a summary of…", "Report which files were modified…", "Give me an update on…" — verbs a naive list would misfire on).

PI_FORK_GATE=off makes it log to stderr instead of blocking; /ext disables it.

secret-guard.ts

Keeps the values of this process's secret env vars out of what the model sees, what the session file stores, and what the palace feeder stages. A secret var is one whose name says secret (…TOKEN, …SECRET, …API_KEY, …PASSWORD, _PAT, …) and whose value looks like a credential rather than a path, knob or placeholder. Each exact value is replaced with <redacted:NAME>, the same placeholder mempalace-toolkit's palace redactor writes.

Measured motivation, 2026-10-12: a new session ran an env | grep … with its own sed redaction, and the sed missed MEMPALACE_REMOTE_TOKEN=<value>. That value turned out to be in 12 local session files going back two months, and each of those sessions had sent it to its model provider. The palace stayed clean only because the feeder scrubs transcripts before upload; nothing guarded the step from tool output to model context.

Three hooks:

hook what it does
tool_result rewrites content (text parts), structuredContent and details of every tool result, MCP tools included. The model, the session file and therefore the palace feed only ever get the placeholder.
context the same pass over everything sent to the model, per request. Covers user ! command output, pasted text, and resumed sessions that already hold a raw value. Request-local: the session file is not changed.
tool_call blocks a write/edit whose new text contains the placeholder of a live secret. That is what an agent writes back after reading a redacted .env, and it would replace the secret with the placeholder. The reason says to write the file with bash and let the shell expand "$NAME".

Commands keep working: $NAME still expands in bash; only the output that comes back is rewritten. Matching a known value cannot hit anything that is not that secret, so there is no shape or entropy heuristic to misfire. The cost is 36 ms per request on a 53 MB session and under 1 ms on a normal one.

Not covered:

  • secrets that are not in pi's environment (key files, another host's decrypted .env);
  • the live streaming view while a tool runs (only the final result is rewritten);
  • the stored entry of a user ! command;
  • pi-task children, which run --no-extensions.

Tests: test/secret-guard.test.mjs is two-sided. It also runs mempalace_redact.py (from /opt/mempalace-toolkit/bin, or $MEMPALACE_REDACT_PY) on the same synthetic environment, and fails if the two pick different variables or produce different text. Every rule was mutation-checked.

PI_SECRET_GUARD=off makes it inert; /ext disables it.

todo.ts

Gives the agent a todo tool (actions: list / add / toggle / clear) so it can externalize a multi-step plan and tick items off as it works. Also registers /todos so you can inspect the current list at any time.

State lives in the session's tool result details, not an external file. So:

  • pi --continue / --resume brings the todos back with the conversation.
  • /fork forks the todo list along with the branch — each branch has its own state.

This is a verbatim copy of the upstream examples/extensions/todo.ts shipped with pi-coding-agent. Refresh from upstream when desired (see AGENTS.md).

mcp-loader.ts

Generic MCP server loader. Reads an mcp block from ~/.pi/agent/settings.json (same shape as opencode and Claude Desktop) and connects to each declared server, exposing all of their tools to pi as native tools — namespaced as <server-name>_<tool-name> to avoid collisions, with non-[A-Za-z0-9_] characters replaced by _ so the names pass the strictest provider tool-name regex (e.g. AWS Bedrock).

Settings.json shape:

{
  // … existing pi settings …
  "mcp": {
    "searxng": {
      "type": "local",
      "command": ["uvx", "mcp-searxng"],
      "env": { "SEARXNG_URL": "https://searxng.your-host.lan" }
    },
    "gitea": {
      "type": "local",
      "command": ["gitea-mcp", "-t", "stdio"],
      "enabled": false,
      "env": { "GITEA_ACCESS_TOKEN": "...", "GITEA_HOST": "https://gitea.example.com" }
    },
    "context7": {
      "type": "remote",
      "url": "https://mcp.context7.com/mcp"
    }
  }
}

Per-server keys:

Key Description
type "local" (stdio subprocess) or "remote" (streamable-http). Default "local".
command Argv array. First element is the executable, rest are args. Local servers only.
url Remote MCP endpoint URL. Remote servers only.
headers Optional object of HTTP headers (e.g. Authorization, X-API-Key) sent with every request. Remote servers only.
enabled Default true. Set false to disable a server without removing the entry.
env Optional object of env vars injected into the subprocess. Inherits parent env first, then overlays these keys. Local servers only.

Transports:

  • local — stdio JSON-RPC subprocess (mcp-searxng, gitea-mcp, mcp-server-time…).
  • remote — streamable-HTTP per MCP spec 2025-03-26: POST JSON-RPC, server replies either application/json or text/event-stream. Optional Mcp-Session-Id round-trip if the server issues one. No GET subscription stream (server-initiated notifications are not consumed).

Limitations:

  • No stdio reconnect if a subprocess dies mid-session — those tools become unavailable until /reload (same as mempalace.ts).
  • Remote sessions self-heal on 404. If a streamable-HTTP server forgets our session id (e.g. server restart), the client transparently re-initializes and retries the request once.
  • No OAuth flow. Remote servers requiring OAuth must be accessed with a pre-issued bearer token via headers.
  • Coexists with mempalace.ts but does not replace it. The mempalace bridge has bespoke handling (agent identity injection) that's worth keeping. Don't list mempalace in the mcp block too — you'd get duplicate tool registrations.

Debug: set PI_MCP_LOADER_DEBUG=1 in the environment to surface per-server stderr and connection logs.

Slash command: /mcp opens a multi-toggle overlay listing every server in the mcp block with its runtime status:

  • running · N tools — connected, tools registered
  • failed: <message> — start handshake threw
  • disabled in settings — enabled: false
  • invalid: <message> — malformed config (read-only row)

UX matches /ext: space stages a toggle, enter writes back to settings.json and reloads pi, esc cancels. Toggling re-enables a previously-disabled server by removing the explicit enabled key (the default is true).

Each extension is a TypeScript module loaded by jiti — no compilation step. See the pi extensions docs and the built-in examples for the API surface.


aws-sso.ts

Setup guide, modes per device type, log reference and troubleshooting: docs/aws-sso.md.

Keeps a Bedrock AWS SSO session alive, like Claude Code's awsAuthRefresh: when the IAM Identity Center session ends it runs aws sso login for you, before a request fails. You still approve the login (or your browser does it silently while it is signed in). Off until you enable it.

Command Effect
/aws-sso on / off Enable/disable (persisted in ~/.pi/agent/aws-sso/state.json)
/aws-sso status Mode, push target, profile chain, session state, last check, last login
/aws-sso now Log in now
/aws-sso local Default. The machine running Docker has your browser: run aws sso login there over SSH (~/.ssh-local/config, host alias host or $PI_AWS_SSO_SSH_HOST). Silent while that browser is signed in.
/aws-sso remote No browser you can see there (headless server, ssh + tmux, an always-on Mac used as a server): run aws sso login --use-device-code --no-browser in the container, show link + code in pi and push them; approve on any device. AWS always asks for a click in this flow.
/aws-sso continue on|off After a request failed on an expired session, send continue once the renewal succeeds (default on)
/aws-sso push-test Send a test push

Settings in pi-devbox .env (reaches the container via env_file; recreate the container after editing):

Variable Meaning
PI_AWS_SSO_REFRESH=1 / 0 Force the keeper on / off regardless of /aws-sso on|off
PI_AWS_SSO_MODE=local|remote Mode when none was set by command (a command-set mode wins)
PI_AWS_SSO_NTFY=https://ntfy.sh/<topic> Push target: an ntfy topic your phone's ntfy app subscribes to. Treat the topic name as a password (long and random), or use a self-hosted ntfy.
PI_AWS_SSO_NTFY_TOKEN Access token for a protected ntfy topic (optional)
PI_AWS_SSO_SSH_HOST SSH alias of the Docker host for local (default host)
PI_AWS_SSO_LOCAL_CMD Command run on the host for local; {session} is replaced by the sso-session name. Default: exec "${SHELL:-/bin/sh}" -lc 'aws sso login --sso-session {session}'. Verified on a macOS host; a Linux or Windows host may need its own command so the login can reach the desktop browser from an SSH session.

Pushes (only when PI_AWS_SSO_NTFY is set): the approval link and code (remote, tap to open); "login waiting" when a local login has not finished after 60 s; a login that did not complete; and "renewed" after a login you were pushed about. Status and logs show only the ntfy server and a fingerprint of the topic.

How it detects the end. Per AWS ("Understanding authentication sessions"), the CLI's 1-hour token is refreshed "as long as the underlying user interactive session remains valid", meaning the browser's AWS sign-in. That sign-in is admin-capped and measured 8 h here (2026-10-10 from the portal cookie; 2026-10-12 from the token cap below). In the session's last hour AWS still issues new 1-hour tokens but caps them at the session's end; after the end it refuses. Inside the SDK's 5-minute pre-expiry window both are silent: the SDK keeps returning a still-valid token. So the keeper makes that refresh attempt itself as soon as the window opens, with pi's own @aws-sdk/token-providers and the same cache file (~/.aws/sso/cache/<sha1(sso-session)>.json), and reads the result from the file: a new full token = alive; a new token issued for under 55 min = capped, and its expiry is the session's end; nothing written = refused. On a cap the keeper logs in 30 s after that end. On a refusal it logs in at once while the old token still covers requests. Refusals also happened with time left (cause unknown): a probe at 00:13 local on 2026-10-12, 5 min before the end, and a failed request ~2 h before it; both times a login worked at once. Nothing is renewed on a clock, and never before a known end: a login while the browser sign-in is valid completes silently ("…can be used until your session expires") but inherits its end, so it gains nothing. Tokens are only ever logged as sha256[:8] fingerprints (~/.pi/agent/aws-sso/events.jsonl); the token the SDK returns is dropped unread.

Triggers: a 20 s timer that checks every 60 s inside the refresh window (every 10 min once the token has run out). At least 2 min pass between unprompted logins; after an unanswered one the keeper retries after 30 min, 1 h, 2 h, then every 4 h, so a login waiting for you does not pile up tabs or pushes overnight. before_agent_start checks first if the token is in its window and logs in (and waits) if the session is over. message_end is the fallback: if a request still fails on an expired token, the keeper renews and sends continue (once per 10 min), also when you approve hours later. Only interactive sessions act; print/JSON children (task, fork) stay inert. A lock file stops several pi processes from starting several logins.

Deploying on a new machine

# 1. Prerequisites: pi installed, SSH key auth working
pi --help           # creates ~/.pi/agent/ on first run

# 2. Clone and install
git clone ssh://git@gitea.jordbo.se:2222/joakimp/pi-extensions.git ~/src/src_local/pi-extensions
cd ~/src/src_local/pi-extensions && ./install.sh

# 3. Verify
ls -la ~/.pi/agent/extensions/   # should show symlinks into this repo

  • pi-toolkit — pi bring-up: settings template, keybindings, shell env loader
  • mempalace-toolkit — persistent memory layer for pi via MemPalace MCP
  • skillset — agent skills for pi, opencode, and Claude

License

MIT — see LICENSE.