omp packages

@trim21/personal-pi-extensions0.1.20261019144232

Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more

GitHub Actions Published 21 hours ago MIT ~21.6K downloads/mo

Reported broken on omp

1 of 1 report says this package does not work on omp. Read the notes before you install it.

0 works1 broken

Did it work for you?

Result

One report per person. A new report replaces your old one.

Reports 1

  • brokenv0.1.20261019144232 · 1 hour ago

    Automated load test on omp 18.12.3: `omp plugin install` fails. Error: Export named 'generateUnifiedPatch' not found in module 'omp-legacy-pi-bundled:@oh-my-pi/pi-coding-agent'.

Readme

Some of my personal pi extensions

你可以参考本仓库的实现,但不要直接使用:这是我个人自用的扩展集,我会随意做 breaking change,不承诺向后兼容。

pi coding-agent 自定义扩展集合。

扩展概览

扩展 描述
opencode 工具集 小写 read/edit/write/grep/glob/bash/todowrite/question
Claude Code 工具集 大写 Read/Edit/Write/Grep/Glob/Bash/TodoWrite/AskUserQuestion
工具启用配置 personalExtensions:按模型选文件 IO 工具集、通配禁用/启用工具
codemode 模型写一段 JS,在 QuickJS 沙箱里批量/串行调用其它工具,只有脚本输出进上下文
bwrap 基于 bubblewrap 的 OS 级沙箱(内置两套工具集):文件系统隔离 + 多档网络策略
写保护(内置) 写工具内置:限制文件写入在 workspace 内,外部写入需审批
LSP(内置) 文件工具内置 LSP 诊断 + lsp-rename/lsp-inspect/lsp-find-definition/lsp-find-reference
aft AFT 只读代码感知:aft_outline/aft_zoom/aft_callgraph/aft_search
gh-readonly GitHub 只读工具集(issue / PR / CI / release),数据走 octokit REST
spawn-agent 把任务委派给独立上下文窗口的子代理
system-prompt 完全替换 pi 默认 system prompt
vision-agent 视觉代理:主模型不支持视觉时提供 describe_image,调用视觉模型识别图片
session-name 首个 user prompt 自动生成会话名,失败仅告警不命名
request-headers 给每个 provider 请求附加 Thread-Id: <会话 id> 请求头
talk session 间消息传递,SQLite 邮箱 + 双向 ask 时间戳仲裁
web web_search(Search1API 搜索)与 web_fetch(正文提取 / 原样落盘)
openai-cost OpenAI Chat Completions,费用取自响应 usage.cost

codemode

codemode 让模型写一段 JavaScript(作为 async 函数体,await 与 return 都可用),在 进程内的 worker 线程里用 QuickJS wasm VM 执行。VM 里没有 node、文件系统、网络、timer 或模块加载,脚本的出口只有 call(name, args)、fs.read / fs.write 两个文件原语与内建的 sleep。

  • 嵌套调用经工具总线:call("Bash", { command }) 最终执行的是 Bash 工具自己的 execute。脚本里不弹审批:需要人工授权的调用(写工作区外、Bash 申请沙箱外执行) 在 codemode 内直接以 CallFailedError 失败,错误信息提示改用直调;模型直调同一个工具时 审批照常出现。此外 codemode 不额外加确认层:不需要审批的调用不会多出任何确认框。
  • 可调用集合 = 声明了结构化输出的工具(有 structuredSchema 的才进),执行时再与当前 active 列表求交,所以 personalExtensions.disabledTools、pi 的 defaultTools / --tools、子代理的工具白名单都同样约束脚本。脚本拿到的返回值必须有确定的形状,因此没给 结构化结果的工具不进集合——这条规则自己维持一致,不需要维护一份会漂移的黑名单。由此天然 不在集合里的:codemode 自身与 spawn-agent、文件读写工具(脚本用 fs.read / fs.write)、搜索工具(脚本用 call("Bash", { command: "rg …" }),走同一个沙箱、拿得到 退出码,还能拼管道)、lsp-rename(写工具)以及 talk / 会话工具(会把执行时间交给外部 输入)。
  • 文件读写只有 fs 一条路:fs.read(path) 返回文件全文的原始 UTF-8 文本(不加行号、 不截断、不设大小上限,只有内容不是合法 UTF-8 时报错),fs.write(path, content) 整体写入并自动创建 父目录,相对路径相对当前 cwd。fs.write 与写类工具共用同一套保护:写前要求「已读且读后 未变」(记账与文件工具共享,工具读过的文件脚本可以直接写),工作区外写入走 write-guard 的 diff 审批,headless / Windows / /bwrap-deny-request 下直接拒绝。文件读写工具本身 不在可调用集合里,脚本要改一行内容也走 fs.read + fs.write(行级替换用 Edit 工具 直接改,不进脚本)。
  • 只有脚本输出进上下文:text(value) / console.log(...) 与 return 值进入工具结果, 中间的工具调用与它们的返回内容不会(也不在会话记录里留下工具调用条目)。
  • 返回值:工具的结果就是结构化载荷,call() 直接把它解包成对象给脚本——gh-readonly 的工具给 REST 原物或从响应解析出的结构,Bash / bash 给 { exitCode, output } (exitCode 为 null 表示命令被超时/中止杀掉;命令非零退出是正常结果,直接读 exitCode 分支即可)。{ ok: false, error } 会 reject 成 CallFailedError (脚本可按 instanceof CallFailedError 区分调用失败与自身运行期错误)。没有声明输出 结构的工具不进集合,call() 会报「not available in codemode」。
  • 脚本接口:call / CallFailedError / ALL_TOOLS / fs.read / fs.write / text / image / exit / sleep / console.* / store.set / store.get / store.list (会话内持久的键值表);首行可选 // @options: {"max_output_tokens": 10000, "timeout_ms": 600000}——选项留在脚本里而不是工具参数, 因为 pi 的 grammar 约束(constrainedSampling)只支持单个自由文本参数,多出来的参数在支持 grammar 的模型上会被静默忽略。不声明 timeout_ms 时脚本没有时间上限:死循环由调用方中止(Esc) 结束,等嵌套调用返回(含慢命令)多久都不算超时。声明了 timeout_ms 就有墙钟预算(含等待嵌套 调用的时间),到点时正在跑的调用被中止、结果以超时结束,已产生的输出与调用记录仍然返回。 await sleep(seconds)(秒,可为小数)是脚本唯一的等待手段,由 worker 线程自己的定时器驱动 (不经主线程、不产生进度帧),它自己也没有上限——但会被 timeout_ms 结束。
  • 工具描述里给出每个可调用工具的 declare function call(name, args): Promise<T> 重载, 参数与返回类型都取自工具自己的 schema,所以模型在写脚本前就知道返回值形状。
  • store 记在每次成功调用工具结果的 details.store 上(与 src/lib/file-reads.ts 的 已读记账同一套做法),下一次调用从当前分支的 toolResult 重放;输出超过 max_output_tokens 时头尾截断并把全文落到 $TMPDIR/pi-codemode-*.txt。
  • 构建:worker 入口是 esbuild 产物 src/codemode/worker.js(随仓库提交, pnpm run build:codemode-worker 重新生成,pre-commit 会自动跑);quickjs-wasi 的 wasm 在注册工具时编译一次,之后每次执行复用。产物必须自包含:它由 new Worker(url) 作为普通 Node 模块加载,不像主线程那样经 jiti 从 pi 的 node_modules 解析裸包名,所以 除了 quickjs-wasi(我们自己的 dependency)之外的依赖都要打包进去——否则用户装好包后 worker 会在启动时报 Cannot find package。

工具启用配置

所有工具由单一扩展入口 src/index.ts 注册(package.json 的 pi.extensions 只列 src/index.ts、src/request-headers.ts、src/session-name.ts、src/system-prompt/index.ts)。启用的工具由 ~/.pi/agent/settings.json 的 personalExtensions section 决定:

{
  "personalExtensions": {
    // 兜底的文件 IO 工具集:opencode(小写 read/edit/write/grep/glob/bash)
    // 或 claude-code(大写 Read/Edit/Write/Grep/Glob/Bash)
    "fileIo": "claude-code",
    // 按模型覆盖 fileIo,按顺序取第一条命中(工具名与模型名都是 minimatch 通配,
    // 模型名同时试 model.id 与 provider/model)
    "fileIoByModel": [{ "models": ["glm-*", "zhipu/*"], "fileIo": "opencode" }],
    // 工具可用性规则:条目是工具名模式,或 { tools, models }
    "disabledTools": ["talk-*", { "tools": ["web_*"], "models": ["gpt-*"] }],
    "enabledTools": [{ "tools": ["web_search"], "models": ["glm-*"] }],
    // 只进 codemode、不交给模型的工具(须声明结构化输出才会生效)
    "codemodeOnlyTools": [{ "tools": ["aft_*"], "models": ["deepseek-*"] }],
  },
}

规则:命中 disabledTools 且未命中 enabledTools 的工具不注册。带 models 的 条目只在当前模型命中时参与判定。未配置该 section 时用 fileIo: "claude-code" 且不 禁用任何工具。

codemodeOnlyTools 把工具从模型可见列表移进 codemode:命中它、未被禁用、且声明了 structuredSchema 的工具只登记在工具总线上、不交给 pi,模型只能在 codemode 脚本里用 call(...) 调用它(codemode 的工具描述里仍有它的参数与返回类型声明)。没有结构化输出 的工具进不了 codemode,因此会被忽略、保持直接可用,并在会话启动时给出一条 warning—— 想真正移除工具用 disabledTools。

有些工具由代码固定为 codemode-only(例如 gh-readonly 那一组,见 GitHub 只读工具),不需要这条配置;disabledTools 对它们同样生效。

判定时机是每个会话启动:pi 在启动 / /new / resume / /fork / /reload 时重建 扩展,注册就发生在 session_start 里(模型在扩展加载期读不到),因此新会话按自己的 模型重新判定;同一会话内 /model 切换不会改变工具集合,换模型请开新会话。 两个不同模型的会话可以同时用不同的工具集。

配置里的非法取值、以及匹配不到任何工具的模式都会在会话启动时以 warning 通知报出。

入口级排除项的迁移

旧的 packages[].extensions 排除写法针对的是扩展入口文件,入口收敛后那些文件不再是 扩展入口,改用 personalExtensions 表达:

旧写法(packages[].extensions) 新写法(personalExtensions)
!src/opencode/index.ts "fileIo": "claude-code"
!src/claude-code/index.ts "fileIo": "opencode"
!src/web/search.ts "disabledTools": ["web_search"]
!src/web/fetch.ts "disabledTools": ["web_fetch"]
!src/talk/index.ts "disabledTools": ["talk-*"]
!src/aft/index.ts "disabledTools": ["aft_*"]
!src/gh-readonly.ts "disabledTools": ["read-github-*", "list-github-*", "wait-github-*", ...]
!src/spawn-agent.ts "disabledTools": ["spawn-agent"]
!src/vision-agent.ts "disabledTools": ["describe_image"]

小技巧:可以用 enabledTools 做「全局禁用、某个模型才开」:

{
  "disabledTools": ["web_*"],
  "enabledTools": [{ "tools": ["web_search"], "models": ["glm-*"] }],
}

bwrap

基于 bubblewrap 的 OS 级沙箱,为所有 bash 命令提供文件系统和网络隔离。

前置条件: 安装 bubblewrap(apt install bubblewrap / pacman -S bubblewrap / dnf install bubblewrap)。network: limited 模式额外需要 mihomo 与 slirp4netns。Windows 没有 bubblewrap:fs 与 network 均 allow-all 时直接执行,其余组合下每条命令都走审批。

模式(两轴正交)

fs(文件系统)与 network(网络)各自独立取值,可任意组合:

轴 取值 含义
fs readonly 文件系统只读(忽略 writablePaths / extraWritablePaths)
fs workspace-write workspace + /tmp 可写,其余只读(.pi / .agent / .git 始终只读)
fs allow-all 完全可写(沙箱内整根可写,不做 .pi / .agent / .git 保护绑定)
network block 断网
network limited 仅白名单可达:deny-by-default 过滤,network.allowlist 之外全部拒绝
network allow-all 网络不受限

默认 fs: workspace-write + network: block。两者都 allow-all 时完全不经 bwrap、直接执行;其余组合都在 bwrap 里执行:block 用 --unshare-net 断网,limited 叠一层 mihomo TUN(白名单 fakeip DNS)+ slirp4netns egress NAT,只有 allowlist 里的域名 / IP / CIDR 可达:未允许域名在 DNS 层即解析失败,裸 IP 连接在连接层被拒(allowlist 为空 = 全部拒绝)。条目匹配语义(精确 / *. 子域名)见 src/bwrap/README.md,进程模型与设计约束见同一文档。

提权机制

bash 工具(opencode 风格 bash、Claude Code 风格 Bash)注册了 dangerouslyDisableSandbox 参数。模型需要全权限时置为 true 并说明原因(如需要网络、写入 workspace 外部路径)。

建议模型不确定时先尝试沙箱模式,若因沙箱限制失败,再以完整权限重试。

审批对话框的选项:Allow once 放行本次;Run this in sandbox 拒绝提权、命令降级为沙箱内执行(不持久化);Deny / Deny with reason 拒绝;Edit approval rules 进入子菜单,按 pattern 勾选持久化 allow/deny 规则。

另一条提权路径是编辑类工具(Edit / Write / lsp-rename / web_fetch)写工作区外路径,同样经审批框放行。

对话中执行 /bwrap-deny-request 后,上述两类提权请求都不再弹审批框,直接按用户无理由拒绝处理(bash 报 User denied unsandboxed execution.,编辑类工具报 user deny <tool>: blocked);/bwrap-allow-request 恢复审批。该开关只影响提权路径,沙箱内可执行的命令与工作区内写入不受影响,新会话开始时复位。

保护目录

.pi、.agent 即使在 fs: workspace-write 下也始终只读;.git 同样只读:工作区根是 git 仓库时保护根 .git,根不是 git 仓库时才递归扫描嵌套仓库(monorepo 子仓库,跳过 node_modules、.venv 等包目录)。fs: allow-all 不做这些保护绑定。

可写与保护路径均使用 bwrap 的 --*-bind-try 变体:路径不存在时自动忽略该项,而不是让整条命令失败。

运行时命令

  • /bwrap — 显示当前两轴模式和路径配置
  • /bwrap-fs-readonly / /bwrap-fs-workspace-write / /bwrap-fs-allow-all — 切换 fs 模式
  • /bwrap-network-block / /bwrap-network-limited / /bwrap-network-allow-all — 切换 network 模式
  • /bwrap-reload — 重载 bwrap 配置并重启网络栈
  • /bwrap-deny-request — 拒绝模型的非沙盒请求,不再弹审批框
  • /bwrap-allow-request — 恢复非沙盒请求的审批

配置

配置文件(项目优先于全局):

  • ~/.pi/agent/sandbox.json(全局)
  • .pi/sandbox.json(项目)
{
  // 文件系统策略:"readonly" | "workspace-write" | "allow-all"
  "fs": {
    "mode": "workspace-write",
    // 可写路径列表,~ 展开为 $HOME,覆盖默认值
    "writablePaths": [".", "/tmp", "~/my-projects"],
    // 额外可写路径,与默认值合并(ro-bind)
    "extraWritablePaths": ["~/.config"],
    // 沙箱内隐藏的路径:以 / 结尾的视为目录(挂空 tmpfs),否则按文件处理(--ro-bind-try /dev/null)
    "denyPaths": [],
  },
  // 网络策略:"block" | "limited" | "allow-all"
  "network": {
    "mode": "block",
    // limited 模式允许直连的域名 / IP / CIDR,可带 :port;空 = 全部拒绝
    // 域名精确匹配,需要子域名时加 "*." 前缀("*.github.com" 不含 github.com 本身)
    "allowlist": ["github.com", "*.github.com", "*.githubassets.com"],
    // mihomo / slirp4netns 可执行文件路径(可选,缺省走 PATH)
    "mihomoPath": "/usr/local/bin/mihomo",
    "slirp4netnsPath": "/usr/bin/slirp4netns",
  },
  // 自定义 bwrap 路径(可选)
  "bwrapPath": "/usr/local/bin/bwrap",
  // 额外 bwrap 参数
  "extraArgs": ["--die-with-parent"],
  // 全权限执行的自动审批规则:命中规则的命令不弹确认框
  // allow 直接放行,deny 直接拒绝;命令用 tree-sitter 解析,
  // 按 BashArity 生成模式(git checkout main → "git checkout *")
  "approvalRules": [
    { "action": "allow", "pattern": "git status *" },
    { "action": "deny", "pattern": "git push *" },
  ],
}

dangerouslyDisableSandbox: true 的审批流程:先按 approvalRules 匹配(含嵌套 $(...) 内的命令,规则后写优先),命中 allow/deny 直接放行/拒绝,未命中才弹确认框。含文件输出重定向(> / >> / &> 等)的命令即使命令规则全匹配也不会自动放行,避免 echo * 把 echo '' > file 带过;管道(echo | tail)和 fd 复制(2>&1)不受影响。

使用

bwrap 已集成进 bash 工具实现(opencode 风格 bash 位于 src/opencode/bash.ts,Claude Code 风格 Bash 位于 src/claude-code/shell.ts),随对应扩展一起加载,无需单独安装。bash 工具内置默认超时 120 秒,且只支持同步执行(不支持后台 / detach 运行)。


写保护(内置)

写保护直接内置在各写工具(opencode 风格 write/edit、Claude Code 风格 Write/Edit)内部,通过 src/lib/write-guard.ts 的 guardWriteAccess 实现。读取工具(read、ls、find、grep)不受限制。

  • workspace 内或 /tmp 下的路径自动放行
  • 外部路径需通过确认对话框由用户审批(Approve once / Block / Block with reason),对话框内以 ```diff 代码块展示将要发生的变更;预览由「将要写入的内容」直接算出,批准后写的就是这份内容(批准前若文件被外部改动则拒绝写入)
  • headless(无 UI)会话直接拒绝外部写入;Windows 上不提供审批路径,工作区外写入一律拒绝
  • /bwrap-deny-request 生效期间外部写入按无理由拒绝处理(user deny <tool>: blocked)
  • 无需配置,随各写工具自动生效

LSP(内置)

文件工具(read/edit/write 与 Read/Edit/Write)内置 LSP 诊断(写文件后等待并报告 ERROR 级诊断),并注册 lsp-rename / lsp-inspect / lsp-find-definition / lsp-find-reference 四个工具(两套工具集共用同一实现;lsp-rename 只面向 kind: "language" 的服务器,linter 类只参与诊断)。LSP protocol 是统一的,因此服务器不需要为每个语言写 adapter:用一份 JSON 配置声明如何启动即可。

配置文件(项目优先于全局):顶层字段(enabled/disabled、超时等)本地覆盖全局;servers 按服务器 id 合并——同名 id 本地整体覆盖、新增 id,全局其余服务器保留。

  • ~/.pi/agent/lsp.json(全局)
  • .pi/lsp.json(项目)
{
  "version": 1,
  "servers": {
    "gopls": {
      "include": ["**/*.go"],
      // 服务器类型:"language"(真语言服务器,缺省)或 "linter"(只实现 LSP 协议的 lint)
      "kind": "language",
      "rootMarkers": ["go.mod"],
      // 或者用固定 workingDir(与 rootMarkers 互斥,同时配置报错)
      "bin": "gopls",
      "args": [],
      "cwd": "{root}", // 支持 {root} / {cwd} 模板
      "env": { "VIRTUAL_ENV": "{root}/.venv", "GITHUB_TOKEN": { "sh": ["gh", "auth", "token"] } }, // 追加到子进程环境变量;string 值支持 {root} / {cwd} 模板与 ${VAR} 引用,{sh} 启动时执行命令取 stdout(失败则服务器启动失败)
      "languageIdByExtension": { ".go": "go" },
      "startupTimeoutMs": 45000,
      "diagnosticsWaitMs": 1500,
      "initializationOptions": { "pythonPath": "${VIRTUAL_ENV:-/opt/venv}/bin/python" }, // → initialize 请求;字符串值支持 ${VAR} / ${VAR:-default} 插值
      "settings": {}, // → didChangeConfiguration / workspace/configuration 请求
    },
  },
  "maxOpenDocuments": 32, // 驻留文档上限(LRU 容量),缺省 32
  "watch": {
    "enabled": true, // 工作区文件监听,缺省 true
    "debounceMs": "300ms", // 事件去抖,缺省 300ms;也支持 "5s" / "1m"
    "maxBatch": 500, // 单批事件上限,缺省 500,超出截断并提示一次
    "ignore": [], // 追加忽略 glob(相对工作区根)
  },
}

字段说明:

  • include:文件 glob,相对项目根或调用 cwd,任一命中即启用;支持 ! 否定排除,如 ["**/*.go", "!**/*_test.go"]
  • rootMarkers:项目根标记(精确文件名,目录名亦可):从调用 cwd 沿文件路径向下逐级查找,第一个含任一标记的目录即 root(取最外层命中;cwd 自身命中即 cwd),未命中回退 cwd;与 workingDir 互斥
  • workingDir:服务器工作目录(即 LSP root):绝对路径或相对调用 cwd 的路径,缺省即 cwd;文件必须位于该目录内才会由本服务器处理,spawn 工作目录与 rootUri 均用它;与 rootMarkers 互斥
  • bin:可执行文件——绝对路径、相对调用 cwd 的路径,或名字(先在项目内 node_modules/.bin、.venv/bin、venv/bin 找,再走 PATH)
  • languageIdByExtension:扩展名 → LSP languageId(didOpen 用);缺省回退内置映射表
  • startupTimeoutMs / diagnosticsWaitMs:per-server 超时,覆盖全局配置与默认值
  • env:追加到 LSP 子进程的环境变量(在 process.env 之上合并)。string 值支持 {root} / {cwd} 模板与 ${VAR} 环境变量引用;{ "sh": [...] } 在服务器启动时执行命令(argv 直接执行、不经 shell,需要 shell 特性时自行包 ["bash", "-c", "..."]),stdout trim 后作为值,命令失败(非零退出或输出为空)时该服务器启动失败并报错
  • initializationOptions 与 settings 按 LSP 语义分离:前者进 initialize 请求,后者进 didChangeConfiguration / workspace/configuration 请求;initializationOptions 的字符串值(含嵌套对象/数组)在启动时做 ${VAR} 插值,${VAR:-default} 在变量未定义或为空时用 default,未定义且无 default 替换为空字符串;插值时可引用 env 里配置的变量
  • initializationOptionsCommand:启动时执行命令计算 initializationOptions(argv 直接执行、不经 shell,需要 shell 特性时自行包 ["bash", "-c", "..."];参数项支持 {root} / {cwd} 模板与 ${VAR} 插值;cwd 为该服务器的项目根,环境含上面 env 解析出的变量)。stdout 必须是 JSON 对象,与静态 initializationOptions 深合并(命令输出优先,嵌套对象逐层递归);非零退出、输出为空或不是 JSON 对象时该服务器启动失败并报错

env 的 {sh} 命令与 initializationOptions 插值可以组合使用,例如用 gh auth token 给服务器的 initialize 请求提供 session token:

{
  "servers": {
    "github-lsp": {
      "bin": "github-lsp",
      "args": ["--stdio"],
      "env": {
        // 启动时执行 gh auth token,stdout(trim 后)成为环境变量 GITHUB_TOKEN
        "GITHUB_TOKEN": { "sh": ["gh", "auth", "token"] },
      },
      "initializationOptions": {
        // 插值引用上面命令的输出,随 initialize 请求发给服务器
        "sessionToken": "${GITHUB_TOKEN}",
      },
    },
  },
}

命令失败(gh 未登录 / 不在 PATH)时该服务器启动失败并报错。

启动时才能算出的值(例如项目把 typescript alias 成 @typescript/typescript6 时,要去 pnpm store 或 @typescript/* 里找真实的 tsserver.js)用 initializationOptionsCommand,避免把绝对路径写死在配置里。这类现算脚本随包提供了一个:bin/typescript-options.mjs(复制到 ~/.pi/agent/lsp/ 后由下面这行调用,标准 typescript 依赖的项目输出空对象、不做覆盖):

{
  "servers": {
    "typescript": {
      "bin": "typescript-language-server",
      "args": ["--stdio"],
      "initializationOptions": { "tsserver": { "logVerbosity": "verbose" } },
      // 脚本 stdout 的 JSON 对象与上面的静态值深合并(命令优先)
      "initializationOptionsCommand": ["node", "/home/<user>/.pi/agent/lsp/typescript-options.mjs"],
    },
  },
}

没有内置默认服务器:servers 的 key 就是服务器 id,全部来自你的配置,未定义 servers 时不启动任何语言服务器。executable 的发现逻辑(如 tsserver 路径、venv 里的 python)不内置,需要时用 bin / args / settings 自行表达。

启用控制只有顶层两处:enabled(白名单)与 disabled(排除),按服务器 id 生效。enabled 里的 id 必须是已配置服务器,否则视为配置错误;disabled 里未注册的 id 直接忽略。全局超时字段(initializeTimeoutMs 等)同样配在顶层。

顶层 watch 段控制工作区文件监听:事件源是 @parcel/watcher(递归监听会话 cwd),非本 agent 写入的改动——如 git checkout、外部格式化——也会以 workspace/didChangeWatchedFiles 批量通知服务器。内置忽略 node_modules、.git、dist、build、.venv、venv、target、coverage,ignore 可追加。maxOpenDocuments 是保持 open 的文档上限(LRU):超过时最久未使用的文档会被 didClose,服务器回落到读磁盘。watch.enabled: false 可整体关闭监听,回到仅工具触发同步的现状。

运行时命令:/lsp-start 重新启用 LSP(下次工具调用时启动服务器)、/lsp-stop 停掉全部服务器并禁用 LSP、/lsp-reload 重读配置并重启全部服务器。配置在 session 启动时预读缓存,仅 cwd 变化或 /lsp-reload 时重读。

服务器记录里不认识的键会被忽略并逐个告警(<file> (server "id"): unknown field "x" ignored):历史配置中残留的 "clangd": { "enabled": false } 已无任何效果,要禁用某个已配置的服务器请改用顶层 disabled。


opencode 工具集

opencode 风格工具集:read / edit / write / grep / glob / bash / todowrite / question,以及共享 LSP 工具。grep 替换 pi 内置 grep,glob 与 pi 内置 find 并存。

启用方式:personalExtensions.fileIo 设为 "opencode"(或按模型在 fileIoByModel 里指定,见工具启用配置)。它与 Claude Code 工具集只能启用一套——两套都注册会让同名命令(如 /bwrap)重复注册、系统提示重复注入。各单工具文件(src/opencode/grep.ts 等)仍可单独 -e 加载。

与 pi 内置工具重名的注意点:pi 的工具表里有同名的内置项(read / edit / write / bash / grep…),而默认启用的内置工具只有 read / bash / edit / write。同名时该名字的启用状态由内置项决定,所以 defaultTools: [](关闭内置工具)会把这一套里的 read/edit/write/bash/grep 一起挡住,grep 在默认选择下也不可见。这些工具本身注册正常(--tools read,grep 显式列出即可用),要绕开这个限制请用 Claude Code 风格命名(Read/Grep… 无重名)。

edit

edit 使用 opencode 的 schema 和模糊匹配引擎。核心 replacer 和 replace() 函数复制自 opencode,匹配引擎位于 src/opencode/edit-engine.ts。唯一的有意差异是去掉了原版的转义规范化(EscapeNormalizedReplacer):它会把源码里合法的 \n、\t 等转义序列当成模型多转义的产物做启发式反转义,可能改坏内容。

支持的匹配策略:

  • 精确匹配(SimpleReplacer)
  • 行尾空白容差(LineTrimmedReplacer)
  • 块首尾锚定(BlockAnchorReplacer)
  • 空白规范化(WhitespaceNormalizedReplacer)
  • 缩进灵活匹配(IndentationFlexibleReplacer)
  • 首尾空白修剪(TrimmedBoundaryReplacer)
  • 上下文感知匹配(ContextAwareReplacer)
  • 多次出现替换(MultiOccurrenceReplacer)

所有匹配策略按顺序尝试,第一个匹配成功即返回。同时自动处理 BOM、CRLF/LF 行尾转换和文件写入队列。filePath 接受绝对路径或相对工作目录的路径。

edit 要求目标文件已被 read 读过且内容未变(内容指纹比对,与 Claude Code 风格 Edit 同一套语义):没读过报 File has not been read yet. Read it first before writing to it.,读后文件被外部改动报 File has been modified since read...,两种情况都要重新 read。

write 不要求先读——没读过的文件可以直接写;但如果该文件已被读过、之后又被外部改动(或删除),write 会与 edit 一样要求重新 read,不让读取记录过期的内容被盲覆盖。read/edit/write/lsp-rename 都会刷新记账并随工具结果持久化,session 恢复 / fork / rewind 后依然有效;写完也会刷新,紧随其后的 edit 不必重新读。

grep

opencode 风格的 grep 工具,替换 pi 内置 grep。参数与输出格式对齐 opencode 的 grep 工具:pattern / path / include,输出以 Found N matches 开头,按文件分组(<绝对路径>: + Line N: <文本>)。隐藏文件参与搜索、.git 排除、结果上限 100 条(触顶时提示 (more matches available) 并附截断说明)。

执行层在 src/opencode/ripgrep.ts:rg 子进程流式读取 stdout,读满 100 条即终止进程,宽泛 pattern 不会把整个结果集读进内存;退出码语义与上游一致(1 = 无匹配,2 = 部分文件读失败仍返回已有结果,正则语法错误单独报错)。

与上游的三处有意差异:行文本去掉 rg JSON 带出的行尾换行(否则每条匹配后面会多一个空行);path 指向文件时只搜该文件(上游按目录搜索);path 不存在时报错(含同目录相近名字提示),而不是静默返回 No files found。

glob

opencode 风格的 glob 工具(与 pi 内置 find 并存)。参数 pattern / path,内部是 rg --files 语义:尊重 .gitignore、不列隐藏文件、不按修改时间排序、排除 .git,输出绝对路径,上限 100 条(截断时附 (Results are truncated: ...))。与 Claude Code 风格 Glob 的差异(--no-ignore / --hidden / --sort=modified)是各自跟随上游的结果。

path 不存在或指向文件时报错(含同目录相近名字提示)。

todowrite

opencode 风格的任务列表工具,参数与语义和 opencode 的 todowrite 工具一致,用 details.pendant.markdown 渲染(pendant 约定)。

  • 完整列表替换语义:模型每次调用都传完整的 todo 列表,工具整体替换当前列表;没有单条增删改动作
  • 参数:todos: Array<{ content, status, priority }>
    • status:pending | in_progress | completed | cancelled
    • priority:high | medium | low
  • 持久化:列表存进工具结果 details.todos,跟随会话分支自动恢复
  • 渲染:每次调用用完整 markdown 列表输出对应的任务

question

opencode 风格的提问工具,参数与语义和 opencode 的 question 工具一致。阻塞式执行:工具调用挂起,等用户作答后才把答案返回给模型。

  • 参数:questions: Array<{ question, header, options, multiple? }>
    • options 每项为 { label, description }
    • multiple 缺省为单选,true 时循环用 ui.select 逐个勾选直到「✓ Done」
  • 自定义答案:每个问题自动追加 Type your own answer. 选项,选中后走 ui.input 自由输入
  • 返回值:每个问题一个 label 数组(Answer = string[]),跳过的为空数组
  • 输出:与 opencode 一致 —— User has answered your questions: "q"="a", "q2"="Unanswered"...

交互全部走 pi 内置的 ctx.ui.select / ctx.ui.input,不写自定义 TUI 渲染;option.description 不显示在对话框里,仅保留在 details 中。

使用

pi -e ./src/opencode/index.ts

Claude Code 工具集

Claude Code 风格工具集:Read / Edit / Write / Grep / Glob / Bash / TodoWrite / AskUserQuestion,以及共享 LSP 工具(lsp-rename / lsp-inspect / lsp-find-definition / lsp-find-reference)。这是默认启用的文件 IO 工具集(personalExtensions.fileIo 缺省 "claude-code",见工具启用配置);它与 opencode 工具集只能启用一套。各单工具文件(src/claude-code/grep.ts 等)仍可单独 -e 加载。精确行为(输出格式、分页语义、匹配规则)见 .agents/skills/claude-code-tools/SKILL.md。

  • Read / Edit / Write:与 opencode 风格共享 read-before-write 记账与文案(File has not been read yet... / File has been modified since read...),差异是三者都要求先 Read(Edit 用空 old_string 创建新文件、Write 创建新文件例外)。Read 支持 offset / limit 分页与 PDF pages
  • Grep / Glob:ripgrep 实现,行为跟随 Claude Code(Glob 带 --no-ignore / --hidden / --sort=modified,与 opencode 风格 glob 的差异是各自跟随上游)
  • Bash:与 opencode 风格 bash 共享 bwrap 运行时与 dangerouslyDisableSandbox 提权,默认超时 120 秒,只支持同步执行。结构化结果给 { exitCode, output }:output 是命令的完整输出(文本该截断还截断,载荷里是全文),exitCode 为 null 表示被超时/中止杀掉;命令非零退出不是失败,脚本直接读 exitCode 分支
  • TodoWrite:任务列表工具(merge 语义),与 opencode 风格 todowrite 的完整替换语义不同,不要混用
  • AskUserQuestion:阻塞式向用户提问

使用

pi -e ./src/claude-code/index.ts

aft

AFT 只读代码感知工具:aft_outline(文件/目录结构大纲)、aft_zoom(命名符号完整源码)、aft_callgraph(调用关系导航)、aft_search(语义 + 精确搜索)。全部只读,不触碰本包 read/write/edit/bash 工具及其安全机制。

  • 二进制解析:session 启动时解析 aft 二进制(npm 平台包 @cortexkit/aft-<platform>、cargo install agent-file-tools 或 PATH,含 GitHub release 自动下载兜底);找不到则告警且不注册任何工具
  • aft_search 条件注册:仅当用户级 aft.jsonc 开启 semantic_search 且配好外部 embedding 后端(semantic.backend 为 openai_compatible / ollama 且有 base_url)时注册;aft 默认的本地 ONNX fastembed 后端不使用,只开开关不配后端会告警说明缺什么
  • 生命周期:常驻 aft 子进程与日志(tmp/{sessionId}/aft-plugin.log)随 session 创建与释放

使用

pi -e ./src/aft/index.ts

gh-readonly

GitHub 只读工具集。取数全部走 octokit REST(src/lib/github-reads.ts 的读取层 + src/lib/github.ts 的 accessor);只有「当前仓库」解析(gh repo view)与 token 获取(gh auth token)还用系统 gh,因此 gh 不在 PATH 时整组不注册并在 session_start 报错;Windows 禁用。一个会话只 spawn 一次 gh auth token:三个子客户端(search / checks / reads)共用同一个 accessor。

这一组工具只经 codemode 曝光、模型不能直接调用:它们注册在工具总线上但不交给 pi,只能在脚本里 call("read-github-issue", …)。工具的返回值就是结构化载荷(REST 原物,或从响应解析出的统计 / 判定),没有面向模型的文本输出。想在某些环境或模型下彻底关掉它们,用 disabledTools(可带 models)。

  • Issue / PR:read-github-issue、list-github-issues、read-github-issue-comments、read-github-pr、list-github-prs、read-github-pr-diff、read-github-pr-status、read-github-pr-comments
  • CI:read-github-ci-logs、list-github-workflow-runs、get-github-workflow-jobs、wait-github-pr-checks、wait-github-commit-checks、watch-github-run
  • 仓库 / 发布:read-github-repo、list-github-releases、read-github-release、download-github-release-assets

list-github-issues / list-github-prs 带 keywords 时走 GitHub search API,不带 keywords 时走 REST 列表端点(state 缺省只搜 open,state: "all" 不加 state qualifier 覆盖 open + closed),载荷给全部命中。wait-github-pr-checks / wait-github-commit-checks 30 秒轮询、600 秒截止:任一 fail 即返回,全部 pass / skipped 才算通过,超时返回快照不抛错;检查快照还包含「已结束但没有 check run」的 check suite——fork PR 上等待维护者批准的 workflow 只存在于这一层,遇到它立即返回 awaiting_approval(不会自行开始,轮询无意义),条目名字取自对应的 workflow run。read-github-ci-logs 把日志落盘并只返回 step 索引,日志正文用脚本的 fs.read(log_file) 读。

出网代理:~/.pi/agent/proxy.json({ "proxy": "http://127.0.0.1:7890", "noProxy": "localhost" }),缺省字段回退 HTTPS_PROXY / HTTP_PROXY / ALL_PROXY / NO_PROXY 环境变量。

使用

pi -e ./src/gh/index.ts

spawn-agent

spawn-agent 工具把任务委派给子代理:子代理是独立 session、独立上下文窗口,但在同一个 pi 进程内运行。调用阻塞到子代理 turn 结束,最终输出作为工具结果返回(上限 50KB);进度经 onUpdate 实时滚动(tool: / text: 行 + 思考字数状态行)。

子代理定义在 ~/.pi/agent/agents/*.md(YAML frontmatter + system prompt 正文):

---
name: scout
description: Fast codebase recon
tools:
  - read
  - grep
  - find
  - ls
provider: openai # 可选,覆盖全局默认
model: claude-haiku-4-5 # 可选,覆盖全局默认
thinkingLevel: high # 可选,off/minimal/low/medium/high/xhigh
sandbox: # 可选,bash 工具的固定沙箱配置(sandbox.json 同构)
  fs:
    mode: readonly
    extraWritablePaths:
      - /tmp
---
System prompt for the agent goes here.
  • 默认只读:frontmatter 不声明 tools 时只有 read/grep/find/ls,没有 bash/write/edit
  • sandbox:完整沙箱配置直接作为该子代理 bash 的沙箱(不读用户 sandbox.json),非沙盒请求直接拒绝,/bwrap-* 命令不注册;不声明则用子代理默认沙箱 fs: readonly + network: block(同样不继承用户 sandbox.json,需要写工作区或联网须显式声明)
  • 校验跳过:frontmatter 校验失败(缺 name/description、字段类型错)的文件直接跳过
  • 全局默认:~/.pi/agent/spawn-agent.json(provider / model / thinkingLevel),frontmatter 优先于它,二者都优先于 settings.json 的默认值
  • 可见子代理列表通过工具的 promptGuidelines 注入 system prompt;改 agents/*.md 或 spawn-agent.json 后 /reload 生效
  • Windows 禁用

使用

pi -e ./src/spawn-agent.ts

system-prompt

完全替换 pi 默认 system prompt 的扩展(before_agent_start 钩子接管,SYSTEM.md / --system-prompt 内容会被完全覆盖)。

  • 静态主体来自同目录 prompt.md(手写行为准则,衍生自 Claude Code 的 system prompt,剥离了 tool 相关说明)
  • 动态部分(工具列表、工具 guideline、AGENTS.md 上下文、skills、日期、cwd、--append-system-prompt 内容)用 event.systemPromptOptions 程序化拼装,渲染格式与 pi 默认 buildSystemPrompt 保持一致
  • prompt.md 中的 {{tools}} {{guidelines}} {{project_context}} {{skills}} {{append}} {{date}} {{cwd}} 占位符决定每个动态块的位置;占位符被删掉时对应块追加到末尾

使用

pi -e ./src/system-prompt/index.ts

vision-agent

视觉代理扩展。主模型不支持视觉(如 DeepSeek)时自动启用 describe_image 工具;主模型支持视觉时自动隐藏,图片由 pi 原生透传。

describe_image 工具只接收本地图片路径(path,单个或数组,一次可识别多张),图片直接以 base64 data URL 放进请求体,由视觉模型按顺序逐张描述,中间不经过任何 read 工具或 agent。内置默认 system prompt(图像识别助手),并支持 prompt 参数追加具体描述要求(如「图中验证码是什么」「逐字翻译图中的文字」),缺省时自动生成通用描述指令。功能与 pi-vlm-proxy 一致,但配置不单独维护。

配置

不需要独立配置文件,直接复用 pi 已有的配置:

// ~/.pi/agent/settings.json —— 指定视觉模型
{
  "defaultProvider": "axonhub",
  "visionConfig": {
    "provider": "axonhub", // 可选,缺省回退到 defaultProvider
    "model": "mimo-v2.5",
  },
}

provider 的 baseUrl / apiKey 从 ~/.pi/agent/models.json(pi 自定义 provider 配置)解析,认证、代理、网络全部复用 pi 自身配置。

未配置 visionConfig(或 provider 缺失)时扩展不注册 describe_image 工具,agent 看不到也调不到,避免一个必然失败的僵尸工具;配置好后 /reload 即可生效。

是否注册 describe_image 在每个会话启动时按该会话的主模型判定一次:主模型支持视觉(input 含 image)就不注册,图片由 pi 原生透传;不支持才注册。判定不走 pi 的 active 工具列表,也不会在会话过程中重新判定。

使用

pi -e ./src/vision-agent.ts

注意: 本扩展与 pi-vlm-proxy 都注册同名 describe_image 工具,启用前请先从 ~/.pi/agent/settings.json 的 packages 中移除 pi-vlm-proxy,避免工具注册冲突。


session-name

根据会话的第一个 user prompt 自动生成显示名,在 /resume 和 pi -r 里更易区分会话。

  • 模型命名:配置了 sessionName.model 时经 pi 的模型注册表调用命名模型(复用 ~/.pi/agent/models.json 的 provider 配置与 AI SDK,不手写 HTTP 请求)把 prompt 概括成短名,输出截断到 maxLength。
  • 失败即告警:未配置 sessionName、模型不可解析或调用失败时都不设置名字,仅以 warning 通知,方便排查。
  • 不覆盖已有名字:--name、/name 设置过名字的会话不会被改;恢复的已命名会话同样跳过。
  • 恢复无名会话:resume/fork 恢复且无名字的会话,从历史第一条 user 消息生成名字。
  • 非阻塞:命名在后台进行,不拖慢首轮回复;中途切换会话也不会把名字写到错误的 session。

配置

// ~/.pi/agent/settings.json
{
  "sessionName": {
    "provider": "axonhub", // 可选,缺省回退 defaultProvider
    "model": "deepseek-v4-flash", // 命名模型;不配置则不做自动命名
    "maxLength": 30, // 可选,名字最大长度,默认 30
  },
}

使用

pi -e ./src/session-name.ts

request-headers

给每个发往 provider 的请求附加 Thread-Id 请求头,值为当前会话 id,便于网关按会话追踪与归属请求。已存在的 Thread-Id 会被覆盖;会话 id 在每次请求时读取,resume、fork 或 /new 后自动跟随。

该扩展不注册工具,作为独立入口随包加载,不经过 src/index.ts 的工具总线。

使用

pi -e ./src/request-headers.ts

talk

session 间消息传递:不同 pi session(同一台机器)通过一个共享的 SQLite 邮箱互相发送消息、提问并等待回复。

架构(三层)

storage.ts   —— 存储层:TalkStorage 接口 + SqliteTalkStorage 实现(node:sqlite,零 npm 依赖)
core.ts      —— talk 核心:TalkCore 协调器 + mailbox / registry / group / policy / format 子模块,
               只依赖存储层,通过回调 yield 投递/通知
index.ts     —— pi adapter:把 core 接到 pi 的 sendMessage / 生命周期事件 / 工具注册

存储层抽象成接口是为了后续可换成 HTTP / remote 后端,talk 核心无需改动。

工具(LLM 可见)

工具 作用
talk-list-agents 列出会话,返回 JSON 数组(status / work_dir / id / name,自己带 self: true);只列出同组成员(未入组时只有自己),status 区分 live(idle / working / waiting-talk-message)与 offline
talk-ask 向某个 session 提问并阻塞等待回应(默认 30 分钟超时);对方发来任何 talk-send 消息都会解除等待
talk-send 发送纯文本消息到单个 session(to 只接受明确的 session id,不支持广播)

对端消息自动投递(无需主动拉取):投递方式由 talk.deliver 配置,steer 在模型工作过程中打断/唤醒,queue 排队到 session 下一轮自然 turn 时注入。

定位只认 session id:talk-send / talk-ask 的 to 只接受 talk-list-agents 返回的 id(pi 的 session uuid)精确匹配,不做 name/路径/前缀匹配。

标记废弃 session:/talk-dead 给 session 打 offline 标志并把 lastSeenAt 置 0(列表显示为 offline,下次 sweep 无 mail 即回收):无参标记当前 session,/talk-dead <sessionId> 标记指定 session,/talk-dead --all 标记所有其他可见 session(同组成员)。

关键设计

  • presence 不靠心跳:presence 由 offline 标志 + 进程 pid 存活判定;pid 存活时还校验进程启动时间(/proc/<pid>/stat),排除 pid 回卷复用造成的误判。未标记 offline 且进程存活即 live,否则 offline;没有心跳,进程挂死(wedged)与健康空闲不可区分。status 在 live 时显示 working / waiting-talk-message(talk-ask 阻塞等待回应中)/ idle。
  • 定期清理:进程仍存活的记录永不回收;进程已死且最后活跃超过 24h 且无未投递 mail 的记录会被定期 sweep(30 分钟一次)回收;有 mail 的保留 30 天。resume 后 session 会自动重新注册,无 mail 即无损失。
  • 投递成功才消费:信件只在成功交给 sendMessage 后才从 inbox 删除,投递失败留在 inbox 下次重试——不会因 sendMessage 吞异常而静默丢信。
  • 双向 ask 仲裁:talk-ask 发起前先检查收件箱(有对方消息就先读/先回);阻塞等待期间若收到对方的 ask,按两个 ask 的 ts 字段仲裁——先 ask 者主导继续等,后 ask 者让位并先回复对方。ts 是信件内固定字段,双方读到同一对值,结论天然对称;同毫秒碰撞用 session dir + session id 字符串比较兜底。
  • typebox runtime 验证:所有从存储读出的值经 TypeBox schema 校验,损坏/伪造数据被拒绝,不做 as T 强转。
  • 安全:纯文本 ≤32KB;10s 去重 / 30s 限速 8 条 / 50 积压上限(防环);每条投递标注来源(来自另一个 pi session,非用户)。
  • group 可见性:可见性完全由 group 决定——组内 session 只能看到同组成员,不在任何 group 的 session 只能看到自己。用 /talk-group-* 命令建组/入组,见下方「group 可见性」。

配置

sqlite 文件路径按优先级取第一个可用值:

  1. 环境变量 PI_TALK_DB
  2. global ~/.pi/agent/settings.json 里的 talk.db_path
  3. 默认 ~/.pi/agent/talk.db

db_path 支持 ~ 展开(~/… → 用户主目录),相对路径相对 ~/.pi/agent 解析;绝对路径原样使用。

// ~/.pi/agent/settings.json
{
  "talk": { "db_path": "~/data/talk.db", "deliver": "queue" },
}

talk.deliver 控制对端消息的投递方式:

  • "steer":消息到达时打断当前工作(工具调用间隙注入),空闲 session 被唤醒;
  • "queue":消息排队,在 session 下一轮自然 turn(如用户发消息)时注入,不主动唤醒。

默认 "queue"。

group 可见性

可见性完全由 group 决定,不再有路径/workspace 配置:

  • 在某个 group 里的 session 只能看到同组成员;不在任何 group 的 session 只能看到自己。
  • group 是带 uuid 的私有房间:任何 session 都可以凭 uuid 加入任意 group,也可以自由离开,没有 owner。
  • 一个 session 只能属于一个 group:加入新 group 自动离开旧 group。
  • group 成员关系存在共享的 talk DB 里,每次 list/发送实时读取,加入/离开立即对所有 session 生效(无需重启)。

通过 /talk-group-* 命令操作(TUI):

/talk-group-join              # 无参:自动创建一个新 group(uuid 作为组名)并加入
/talk-group-join <name>       # 加入名为 name 的 group;不存在则创建(名字允许字母/数字/-/_)
/talk-group-join-last         # 加入最近创建的 group(方便新开 session 快速归队);支持 --name <alias> 设置显示名
/talk-group-leave             # 离开当前 group(组空了自动删除)
/talk-group-list              # 列出所有 group 及其成员,最新创建的在前
/talk-group-del <name>        # 删除指定 group(成员随之变为未入组)
/talk-group-clear             # 删除所有 group

典型用法:在 A session 里 /talk-group-join(或 /talk-group-join mytask)建组,把组名复制到 B、C session 里 /talk-group-join <组名>,此后 A/B/C 互相可见且只见彼此。

TUI 命令:/talk 列出可见 session(与 talk-list-agents 同一视图)、/talk-dead 标记废弃 session、/talk-group-* 管理 group。

变量 默认 含义
PI_TALK_DB settings 或 ~/.pi/agent/talk.db SQLite 邮箱数据库路径
PI_TALK_INBOUND accept refuse 时丢弃所有 peer 消息
talk.deliver queue 消息投递方式:steer / queue

使用

pi -e ./src/talk/index.ts

web

web_search

Search1API 网页搜索。key 读 ~/.pi/web-search.json 的 search1apiApiKey 或 SEARCH1API_KEY 环境变量。参数 query / numResults(1-50,缺省 5)/ recencyFilter / domainFilter / searchService / includeContent(crawl_results,内联前几个结果的正文,可省去后续 web_fetch)。搜索响应不做 AI 预消化,直接返回整理后的结构化结果(title / url / snippet),零额外模型调用。

web_fetch

抓取 URL 并提取正文为 markdown(readability 主内容算法 + turndown),或按 output_path 原样落盘(附件、镜像、release 资产等,上限 200MB)。

  • SSRF 防护:DNS 预解析 + 拒绝私有/保留地址 + 每跳重定向重新校验
  • 代理:出网走 src/lib/proxy.ts 代理层(~/.pi/agent/proxy.json,回退 HTTPS_PROXY 等环境变量)
  • 写保护:output_path 落盘经 guardWriteAccess,工作区外写入需审批
  • 重定向上限 5,超时 30 秒

使用

pi -e ./src/web/search.ts
pi -e ./src/web/fetch.ts

openai-cost

OpenAI Chat Completions 兼容 provider。流式协议复用 pi 内置 openai-completions,费用不按模型单价估算,而是读取响应 usage.cost(number 或 { total },也认 Moonshot 的 choice.usage)写入 message.usage.cost.total。未上报 usage.cost 时保留默认 calculateCost。

配置文件:~/.pi/agent/openai-cost.json。文件缺失或校验失败时扩展不注册 provider。

{
  "id": "openai-cost", // 可选,默认 openai-cost
  "name": "OpenAI Cost", // 可选
  "baseUrl": "https://api.example.com/v1",
  "apiKeyEnv": "OPENAI_COST_API_KEY", // 可选;也支持 /login
  "models": [
    {
      "id": "my-model",
      "name": "My Model", // 可选,默认用 id
      "reasoning": false,
      "input": ["text"],
      "contextWindow": 128000,
      "maxTokens": 8192,
    },
  ],
}

省略 models 时启动后会请求 GET {baseUrl}/models,默认 reasoning: false、input: ["text"]、contextWindow 128000、maxTokens 8192。需要视觉 / reasoning / 准确窗口时把模型写进配置。API key 优先 stored credential,否则读 apiKeyEnv。

使用

openai-cost 不在 package.json 的默认注册列表(pi.extensions)里,装包后不会自动加载,需要手动指定入口:

pi -e ./src/openai-cost/index.ts

安装

通过 npm/git 包

// ~/.pi/agent/settings.json
{
  "packages": ["github:trim21/pi-extensions"], // 或 npm 包 "@trim21/personal-pi-extensions"
}

命令行加载单个扩展

pi -e ./src/opencode/index.ts

开发

pnpm install        # 安装依赖
pnpm run check      # tsc --noEmit + prettier --check
pnpm run lint       # eslint
pnpm run test       # vitest
pnpm run format     # prettier --write

新增扩展

  1. 在 src/ 下创建扩展文件
  2. 在 package.json 的 pi.extensions 数组中注册(skills 注册在 pi.skills)