- TypeScript 95%
- JavaScript 4.8%
- Shell 0.2%
Un sous-agent est un processus pi neuf qui heritait de l'environnement du parent puis ecrasait le socket et le jeton par les siens : ses demandes de mot de passe restaient dans un pont sans interface, et pendaient. Il detecte desormais un pont herite vivant (un ping, jamais un prompt) et l'adopte : aucun serveur a lui, environnement intact, GNUPGHOME du parent conserve, rien de demoli a l'arret. La demande remonte donc a la session interactive, et la reponse redescend. Au passage : - le prompt affiche l'origine (« depuis le sous-agent « worker » »), posee par le runner dans PI_ASKPASS_ORIGIN, pour que la decision soit eclairee ; - le client a enfin un delai par defaut (330 s, juste au-dessus des 300 s du serveur) : une demande que personne ne peut satisfaire echoue au lieu de bloquer son appelant indefiniment ; - le serveur repond a __ping sans prompter, et refuse toujours un mauvais jeton. Verifie : adoption en sous-agent (aucun socket supplementaire, la demande atteint le serveur du parent), ping teste, 11 tests serveur + 7 tests du pont verts. |
||
|---|---|---|
| .githooks | ||
| agents | ||
| bin | ||
| extensions | ||
| plans | ||
| prompts | ||
| .gitignore | ||
| AGENTS.md | ||
| README.md | ||
my-pi-plugins
Personal plugins for the pi coding agent: extensions, custom agents and prompt templates, tracked so the work survives and can be shared.
This repository is ~/.pi/agent (the pi agent dir), so the files are used in
place. Everything that is local state or a secret is gitignored — see
.gitignore.
Layout
extensions/— TypeScript extensions loaded by pi:-
subagent/— delegate tasks to isolated subagent processes. Foreground (single / parallel / chain) and background runs, a live agent roster plus the available agent definitions (list_agents [agentScope]— running agents with their ids and the user/project agent definitions the tool can invoke), peer messaging (send_to_agent/ask_agent/answer_peer/check_messages), and a question bridge so subagents can ask the orchestrator or the user mid-task. Includes a TUI sidebar/widget showing active subagents and their latest output. Background subagents run with persistent pi sessions and are checkpointed on a runtime reload, then automatically resumed afterwards (per orchestrator session, so multiple concurrent pi sessions stay isolated). With thesidebar/extension loaded it contributes a Subagents panel; otherwise it falls back to the overlay/widget.ctrl+shift+o(or/agents-view) opens a subagent's live transcript — reasoning, text, tool calls and results — in a centered, scrollable overlay. -
sidebar/— a real right-column sidebar (pi has no sidebar API, so this uses the terminal-compositor technique: narrowterminal.columns, paint the freed columns inside pi's synchronized render frame). Robust repaint: the whole region is repainted every frame in the same sync block (no reliance on pi's erase sequences), stale rows are cleared, cursor is saved/restored, and wrap is disabled. Panels are contributed by other extensions through aglobalThis-shared registry (registry.ts); built-in Session panel plus the subagent Subagents panel. Control:/sidebar on|off|width <10-120>(persisted insidebar.json). Do not run it alongsidepi-sidebar-tui(two compositors fight over the same columns). The separator is drawn for the full terminal height; the sidebar columns are wiped before each pi render so scrollback never keeps sidebar text.Widget API —
extensions/sidebar/api.ts:import { addSidebarWidget } from "../sidebar/api.ts"; const dispose = addSidebarWidget({ id: "build", title: "Build", order: 30, render: (view) => `branch ${view.branch ?? "?"}`, // string | string[] | null });Other packages (separate module roots) use the same API via the global handle:
globalThis.piSidebar.addWidget({ id, title, render }).renderruns on every paint and may return a string, lines, or null to hide. -
openrouter-blacklist.ts— auto-avoids flaky OpenRouter upstream providers. On a transient provider error (429/5xx with aprovider_namein the error body, e.g. DeepInfra), it blacklists that(provider, model)pair and injects it into theprovider.ignorearray of later OpenRouter requests (merged with anyopenRouterRouting). Grace grows exponentially per repeated strike (60s, 2m, 4m, … capped at 2h) and strikes decay after 6h of quiet. State:~/.pi/agent/openrouter-blacklist.json; manage with/openrouter-blacklist [clear [provider]]. Logic inlib/openrouter-blacklist.ts. -
store/— shared agent store (store_put/get/list/search/delete/sync).db.tsis the storage engine (SQLite cache + authoritative project JSON);index.tswires the tools and memory discoverability. Project scope is git-friendly JSON at<repo>/.pi/store/<ns>/<key>.jsonwith a SQLite cache; user scope is SQLite-only. JSON is authoritative and external edits are imported live. Discoverability: a stable system-prompt pointer (sentinel<!-- pi-store-pointer -->) lists namespaces, andbefore_agent_startinjects the top keyword matches as a hidden<agent-store-recall>XML message — explicitly marked as data, not user input or instructions. Ranking is BM25 via SQLite FTS5 (bm25()with key/tags/body column weights,unicode61+ light stemming), plus a small-corpus bonus1/(3-N)for 1–2 entry stores (where FTS5's classic idf is exactly 0), and a fuzzy pass (prefix + edit distance over the FTS vocabulary) for typos. Results are capped at the top 5; when more entries clear the relevance bar, a virtual<omitted count="N"/>element reports how many were not shown. -
reload.ts— LLM-callable runtime reload with scoped consent (once / session / this project), a styled warning prompt, and an optionalresumeWithmessage so the agent can continue after reloading. -
auto-reserve.ts— model-derived compaction reserve: proactively compacts after a completed turn when context usage would leave less room thanmin(model.maxTokens, capFraction × contextWindow) + margin(default 25% cap), instead of pi's fixed 16384reserveTokens. Never fires on load./auto-reserve [on|off|margin <n>|cap <fraction>]inspects/adjusts it. -
ultracompact.ts— survive a context that grew far past the model window. When a compaction's own summarization input wouldn't fit (or on/ultracompact [instructions]), it summarizes the history in chunks (map-reduce) and returns the merged result viasession_before_compact, so no single LLM call overflows. The pure engine lives inlib/summarize.ts(unit-tested) and it also auto-activates for oversized automatic/overflow compactions. -
opencode-import/— load a session from the local opencode SQLite DB and convert it into a real pi session (text, thinking, tool calls/results, images, usage)./import-opencode [query] [--all] [--id <ses_…>] [--dir <path>] [--limit N] [--tail N]lists/imports, then asks whether to switch into the new session; animport_opencodetool does the same for the agent (without switching). -
sessions.ts—/sessions [query] [--here] [--limit N] [--plain]lists pi sessions from every project (unlike project-scoped/resume), newest first, and switches to the selected one. Thelist_sessionstool exposes the same listing to the agent. Shell counterpart:bin/pi-sessions. -
askpass/— turnsudoand SSH password/passphrase prompts into a masked prompt inside pi, including remotesudoover ssh. Env-based only (no built-in tool overridden, no command-string heuristic):SUDO_ASKPASS+ asudoshim (adds-A),SSH_ASKPASS+SSH_ASKPASS_REQUIRE=force, and ansshshim that (for remote commands) prepends a POSIXsudofunction emitting a control marker on stderr — the local shim answers it with the password on ssh's stdin (sudo -S), so the secret only travels encrypted (never argv/env/terminal/logs). Helpers:bin/pi-askpass,bin/pi-ssh-askpass, over a Unix socket. Secrets are cached in memory for the session (never persisted) perkind@host(ssh-key@<path>,ssh-password@user@host,sudo@<host>,pinentry@<keygrip>); a failed command drops the entry. Every tool result gets an[askpass] …note telling the agent what happened (cached / typed / cancelled) — never the secret./askpass [status|on|off|clear|forget <kind@host>].The bridge token is never put in pi's environment: every command pi runs inherits that environment, so a token there would be readable by any tool call (and copied into any transcript that prints the environment). The server writes it to a
0600file next to the socket and publishes only the path (PI_ASKPASS_TOKEN_FILE); the rawPI_ASKPASS_TOKENsurvives as a fallback for the case where no file can be written. The client refuses — before connecting — a socket that is not a socket, not owned by the current user, or writable by others, refuses a token file readable by others, and compares the token in constant time. The honest limit is written down where it belongs: a token cannot keep out a command running as the same user; what keeps that in check is visibility (the[askpass]note and the command itself) and/askpass forget.GPG: each session gets a private
GNUPGHOMEoverlay holding a read-only copy of the keyring, served by its own gpg-agent (distinct socket) whosepinentry-programisbin/pi-pinentry; the global agent is never touched. GPG signing/decryption work with the passphrase prompted in pi; keyring writes (--import,--gen-key,--edit-key, passphrase change) fail by design, and a hidden<gpg-overlay>note tells the agent so. Robust cleanup on shutdown plus a stale-overlay sweep (pid liveness). -
justai-auth/— corporate gateway provider (JustAI/Brio). Registers the one-api gateway (JUSTAI_GATEWAY, defaulthttps://llm-gw.justai.ovh) as an OpenAI-compatible provider and authenticates it through Keycloak (JUSTAI_ISSUERrealmJustAI, public clientJUSTAI_CLIENT_ID, defaultpi-clients)./login justairuns the OIDC device flow — including the PKCE challenge this client enforces — then exchanges the access token atPOST /api/pi/tokenfor a short-lived gateway key. The model catalog is discovered live from/v1/modelsthrough pi'srefreshModelshook, soJUSTAI_API_KEY(or an existing credential) lists models without a browser. Config:config.ts; flow:oauth.ts; mapping:models.ts. -
lsp/— language-server support (TypeScript preset, generic/configurable). Tools:lsp_diagnostics(file or whole project),lsp_hover,lsp_definition,lsp_references,lsp_symbols,lsp_workspace_symbols. Servers are resolved from the project'snode_modules/.bin(walking up) orPATH— nothing is auto-installed — and the TypeScript preset points the server at the workspace so it uses the project's owntypescript/tsconfig. Diagnostics are also appended towrite/edittool results (disable with"diagnosticsOnEdit": falsein~/.pi/agent/lsp.json). -
credits.ts— status bar showing the current provider's remaining credits (OpenRouter account balance, falling back to per-key limits) and the in/out price (USD per 1M) of the model that last answered — the actual routed model (responseModel) and the actual upstream provider. The real provider and cost are captured from OpenRouter's SSE chunks (seelib/openrouter-stream.ts), so the rates reflect whoever actually served the request (e.g.@DeepInfra), not the catalogue list price; before any answer the list price is shown instead. Prices are colour-graded blue → green → bold red (in: 0→0.20→5.0; out: 0→0.80→25.0), truecolor with a 256-colour fallback, styled via the current theme. Refreshes on model change, session start, after each run and every ~2 min;/creditsforces a refresh. -
lib/openrouter-stream.ts— wraps theopenai-completionsstream for OpenRouter providers to tee the SSE body and capture theproviderand real cost fields pi drops (responseId/responseModelonly). Installed onsession_startfor every registeredopenrouter*provider;openRouterStats(generationId)andeffectiveRates(stats)feedcredits.ts. -
lib/model-metrics.ts(barrel) — pick the cheapest model clearing a quality bar. Split intolib/metrics-types.ts(types + defaults),lib/metrics-sources.ts(Artificial Analysis + local JSON, id normalization, default store) andlib/metrics-rank.ts(profiles, cost, aliases). Pluggable metrics sources (Artificial Analysis free API + local JSON), named profiles (software-engineering,research,architecture,codebase-analysis), canonical model-id matching (…/Qwen3.8-27B-Uncensored-GGUF:IQ4_XS→qwen3.8-27b), andfindCheapestModel/resolveAlias. Cost rule: pi-cost 0 is a hard override (free), otherwise Artificial Analysis cost-per-task. Local providers are auto-detected and win ties. Alias-backed candidates are self-excluded to avoid recursion. Aliases are baked snapshots (2026-09-11) and can be overridden in~/.pi/agent/model-metrics.json. Subagents can target one via themodelparam or agent frontmatter (e.g.model: research); candidates are restricted toenabledModels. Aliases are also registered as selectable pi models under the provideraliases(e.g.--model aliases/research); selecting one immediately substitutes the concrete model it resolves to, so requests then take the native path (streaming, auth, caching) and the model is pinned for the session (and restored on resume). A fallback stream covers the brief window before substitution. If a chosen model fails to launch, the subagent retries the next-ranked candidate. -
question.ts,questionnaire.ts— ask the user structured questions. -
display-image/— show an image file inline in the user's terminal via thedisplay_imagetool (PNG/JPEG/GIF/WebP/BMP, optional caption, width/height in cells), without sending the pixels to the model — usereadwhen the model must see the image. pi does not detect Konsole/Yakuake as image-capable even though they implement the Kitty graphics protocol, and an extension cannot teach the app otherwise: pi bundles its own pi-tui, sosetCapabilityOverrides()from an extension only reaches the extension's module instance. The tool therefore decides the protocol from the environment (kitty/ghostty/wezterm/warp/iTerm2, plus Konsole ≥ KDE Gear 22.04) and emits the Kitty/iTerm2 escape sequence itself. It also avoids pi-tui'scalculateImageCellSize, which is declared in the.d.tsbut not re-exported by the package index (it isundefinedat runtime); the sizing math is inlined inimage-file.ts, andrender()never throws because an exception inside pi's render loop is anuncaughtExceptionthat kills the process. Non-PNG files are converted to PNG for the Kitty protocol withffmpegwhen available (iTerm2 carries them as-is; ffmpeg's stderr is captured so diagnostics never spray the TUI). Fullscreen guard: pi exposes no TUI-mode API to extensions (no mode field in the render context, nothing onctx.ui, noPI_*env), so the mode is resolved the way pi does —--tui-mode> project.pi/settings.json> globalsettings.json> regular — cached for 1s, with aforceModeoverride. Images are hidden in fullscreen by default because pi's alt-screen crops/repaints images through its own metadata registry, which never sees extension-rendered images. Configdisplay-image.json(enabled,protocolauto|kitty|iterm2|none,maxWidthCells,maxBytes,convertToPng,showInFullscreen,forceMode); control/display-image [status|on|off|protocol <p>|width <cells>|convert <on|off>|fullscreen <on|off>]. Pure path/sniffing/format/sizing/mode helpers live inimage-file.ts(unit-tested). -
slow-tools.ts— pi does not surface tool durations to the model, so this timestamps each tool call and, when one runs longer than a threshold (default 300s), appends a[slow tool] … took 5m42s (342s).note to its result so the model knows the operation was slow. Configslow-tools.json;/slow-tools [on|off|threshold <seconds>]. -
max-output.ts— clamp the max output tokens sent to every model (default 128k = 131072) viabefore_provider_request, only lowering an existing value. Handlesmax_tokens/max_completion_tokens/max_output_tokensand Google'sgenerationConfig.maxOutputTokens. Configmax-output.json;/max-output [on|off|tokens <n>]. -
background-tasks.ts,undo.ts,context-meter.ts,coach.ts,jina.ts,openrouter-multi/— task management, undo, context monitoring, coaching, web search/read, and multi-model routing.
-
agents/— custom subagent definitions (planner,reviewer,scout,web-researcher,worker).prompts/— reusable prompt templates (implement,implement-and-review,scout-and-plan).plans/— design docs written before building (e.g.openrouter-topup.md).bin/— standalone shell tools (~/.pi/agent/binis on$PATH):pi-sessions— list saved pi sessions from the terminal, the CLI counterpart of/sessions.pi-sessions [list] [--here] [--limit N] [--query S] [--sort modified|created|messages|title] [--json] [--paths], pluspi-sessions path <id>andpi-sessions resume <id> [--exec]. Reads the JSONL files directly (no dependency on pi internals).
Tests
Pure modules that do not touch pi's virtual modules have dependency-free unit tests using Node's built-in test runner (with native TypeScript type stripping):
node --experimental-strip-types --test \
extensions/askpass/tests/*.test.ts extensions/askpass/tests/*.test.cjs \
extensions/justai-auth/tests/*.test.ts \
extensions/display-image/tests/*.test.ts
They cover the askpass socket server (cache, token, token-file lifecycle and
permissions, forget, watchdog, activity events), the shared bridge client
(env/env-file resolution, token file, refusal of an unsafe socket, real-binary
lookup),
the GPG overlay (read-only keyring copy, generated gpg-agent.conf, cleanup),
and the JustAI provider (OIDC device flow with PKCE, token refresh, gateway
exchange, model mapping).
Pre-push hook
The same tests run automatically before every push (a dependency-free stand-in for Husky — this repo has no npm dependencies). Enable it once per clone:
git config core.hooksPath .githooks
The hook is .githooks/pre-push; bypass a single push with git push --no-verify.
Not in this repo
Secrets and runtime state are intentionally excluded: auth.json,
jina-api-key, openrouter-keys.yaml, models-store.json, settings.json,
trust.json, sessions/, store-cache/, and reload-runtime-*.json.