pi: extensions as a directory of links, so a machine can have its own ~/.pi/agent/extensions was a link to the repo's directory, so anything that installs a pi extension wrote into this checkout -- herdr's pi integration did, on one machine. It is now a real directory with the repo's tracked extensions linked in one by one, and a file a machine adds there stays that machine's. pi follows the links, and skips a dangling one silently, so an extension deleted from the repo leaves a harmless stale link. The old directory link is removed first: linking into it would write through to the repo and turn its files into links to themselves. Only tracked files are linked, so a machine's leftovers in the repo's directory are not linked back. The filed version put this loop in link.sh; it lives in link/pi.sh, since link.sh holds one-line links only and a part also obeys ~/.config/dot/skip. From change request 3f6b05.
Igor Soarez igor@soarez.org
Mon, 05 Oct 2026 13:53:09 +0300
3 files changed,
34 insertions(+),
3 deletions(-)
M
AGENTS.md
→
AGENTS.md
@@ -57,6 +57,7 @@ | `~/.config/claude/mcp-servers.d/*.json` | MCP servers, registered by `link/claude.sh` |
| `~/.claude/rules/*.md` | extra Claude Code user memory, loaded next to `config/claude/CLAUDE.md` | | `~/.config/pi/settings.d/*.json` | pi settings merged into the generated `~/.pi/agent/settings.json` by `link/pi.sh` | | `~/.config/pi/models.d/*.json` | pi providers merged into the generated `~/.pi/agent/models.json` by `link/pi.sh` | +| `~/.pi/agent/extensions/*`, real files | pi extensions only this machine has -- herdr's pi integration installs `herdr-agent-state.ts` here | pi itself, beside the repo's extensions, which `link/pi.sh` links in one by one | A credential in a pi provider -- `apiKey`, or anything under `headers` -- is a **reference**, not a name: `"$SILICONFLOW_API_KEY"`, not `"SILICONFLOW_API_KEY"`.
M
link.sh
→
link.sh
@@ -35,9 +35,8 @@ link_all "$config_dir/nvim" "$xdg_config_dir/nvim"
link_all "$config_dir/bat" "$xdg_config_dir/bat" link "$config_dir/aider.conf.yml" "$HOME/.aider.conf.yml" -link "$config_dir/pi/extensions" "$HOME/.pi/agent/extensions" link "$config_dir/pi/compact-tools.json" "$HOME/.pi/agent/compact-tools.json" -# settings.json and models.json are both generated by link/pi.sh; AGENTS.md and +# extensions/, settings.json and models.json are all link/pi.sh's; AGENTS.md and # mcp.json, if any, come from another layer. # Utils
M
link/pi.sh
→
link/pi.sh
@@ -1,6 +1,7 @@
#!/bin/bash -# pi's settings.json, generated. Dry run by default; pass `yes` to apply. +# pi's extensions directory, and its settings.json and models.json, generated. +# Dry run by default; pass `yes` to apply. set -euo pipefail@@ -38,6 +39,36 @@ pi_dir="$HOME/.pi/agent"
if [[ ! -d "$pi_dir" ]] then exit 0 +fi + +# pi's extensions: a real directory, with this repo's tracked extensions linked +# in one by one, so an extension only one machine has -- herdr's pi integration +# writes herdr-agent-state.ts here -- sits beside them instead of landing in this +# checkout. pi loads every *.ts and *.js there and follows links, and it skips a +# dangling one silently (resource-loader.js, pi 1.0.0), so the link left behind +# by an extension deleted from the repo does no harm. +# +# Machines used to link the whole directory to the repo. That link goes first: +# linking into it would write through to the repo and turn its files into links +# to themselves. Tracked files only, so nothing a machine left in the repo's +# directory is linked back in. +pi_extensions="$pi_dir/extensions" +if [[ -L "$pi_extensions" ]] +then + if [[ -n "${is_dry_run}" ]] + then + yellow "$pi_extensions is a link to the repo; will become a directory of links to its tracked files" + else + rm "$pi_extensions" + fi +fi +if [[ ! -L "$pi_extensions" ]] +then + [[ -n "${is_dry_run}" ]] || mkdir -p "$pi_extensions" + while IFS= read -r -d '' f + do + link "$dotfiles_dir/$f" "$pi_extensions/$(basename "$f")" + done < <(git -C "$dotfiles_dir" ls-files -z -- config/pi/extensions) fi pi_status="$(DRY_RUN="$is_dry_run" python3 "$dotfiles_dir/link/pi-settings.py" \