@andares/pi-toolkits 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,12 +9,12 @@
9
9
  [![npm version](https://img.shields.io/npm/v/@andares/pi-toolkits?label=npm&logo=npm)](https://www.npmjs.com/package/@andares/pi-toolkits)
10
10
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
11
11
  [![Package Manager](https://img.shields.io/badge/package%20manager-pnpm-orange?logo=pnpm)](https://pnpm.io)
12
- [![pi >= 0.87](https://img.shields.io/badge/pi-%3E%3D0.87.0%20%3C2.0.0-blueviolet)](https://github.com/earendil-works/pi)
12
+ [![pi >= 1.0](https://img.shields.io/badge/pi-%3E%3D1.0.0%20%3C1.12.0-blueviolet)](https://github.com/earendil-works/pi)
13
13
  [![GitHub](https://img.shields.io/badge/github-andares%2Fpi--toolkits-181717?logo=github)](https://github.com/andares/pi-toolkits)
14
14
 
15
15
  In a nutshell: six ready-to-use capabilities — **read-only consult mode** `ask`, **model favorites** `favorites`, **prompt stash** `stash`, **third-party code review** `third-review`, **dedicated compaction model** `compact-model` and **session startup automation** `autostart` — with more to come.
16
16
 
17
- **Requirements**: pi `>=0.87.0 <2.0.0` (covering the four most recently released minor lines — 0.87.x / 0.99.x / 1.0.x / 1.1.x; 0.88–0.98 were never released). Every one of the 11 published versions inside that range (0.87.0 → 1.1.0) was compared against the host `dist`: the extension APIs we use carry no breaking differences (1.0.4 → 1.1.0 only adds fields), and the two runtime patch targets used by favorites — `ModelSelectorComponent` / `AgentSession.cycleModel` — are byte-identical. `typecheck` plus the full test suite pass on each of 0.87.0, 0.87.1, 0.99.0, 0.99.2, 1.0.0, 1.0.4 and 1.1.0, and smoke-loading on real pi 1.1.0 produces no warnings. Note: pi ≥ 0.99 hosts require `@earendil-works/pi-tui` to be declared in `peerDependencies` as `"*"` (provided by the host); this package declares it as required.
17
+ **Requirements**: pi `>=1.0.0 <1.12.0` — the four most recently released minor lines (1.0.x / 1.1.x / 1.2.x / 1.3.x) are supported best-effort, and the upper bound encodes the forward probe: on every host release we check up to **8 minor lines** beyond the newest one (now 1.4 … 1.11) and never claim past that cap. **Measured natural compatibility: 1.3.0** (the newest release at the time of writing — nothing newer exists to probe yet). A single command, `pnpm check:compat`, derives that version set from the registry and runs, per version, in a sandbox under the system temp dir with the whole `@earendil-works/*` family pinned to the version under test: install → `tsc --noEmit` → full test suite → keybinding audit → hash-compare of the host `dist` files this package patches or mirrors. Results over 1.0.0 → 1.3.0 (all 8 published versions): typecheck, 289 tests and the keybinding audit pass on every one; the tracked host files drift only in `core/agent-session.js` (1.3.0 changed the tool-restore path; `cycleModel` untouched), `core/keybindings.js` and `core/compaction/utils.js` (both changed in 1.2.0, table semantics for our keys unchanged), and `extensions/types.d.ts` (additive) — `ModelSelectorComponent`, `core/compaction/compaction.js` and `core/usage-totals.js` are byte-identical across the whole window. Smoke-loading on real pi 1.3.0 produces no warnings. Note: pi ≥ 0.99 hosts require `@earendil-works/pi-tui` to be declared in `peerDependencies` as `"*"` (provided by the host); this package declares it as required.
18
18
 
19
19
  ---
20
20
 
@@ -65,7 +65,7 @@ After installing, verify in any pi session:
65
65
 
66
66
  | Feature | Entry point | One-line description |
67
67
  | --- | --- | --- |
68
- | 🛡️ **ask** read-only consult mode | `/ask` | enter read-only Q&A mode: `write`/`edit` hard-disabled, `bash` runs in a read-only sandbox |
68
+ | 🛡️ **ask** read-only consult mode | `/ask` | enter read-only Q&A mode: `write`/`edit`/`powershell` hard-disabled, `bash` runs in a read-only sandbox, `grep`/`find`/`ls` activated |
69
69
  | ⭐ **favorites** model favorites | `ctrl+A` / `ctrl+J` inside `/model` | favorite the models you use most (marked in bold bright yellow), `ctrl+p` cycles only among favorites |
70
70
  | 📥 **stash** prompt stash | `ctrl+alt+y` | a stash slot for one prompt: press to swap, press twice to stash and clear the input box |
71
71
  | 🔎 **review** third-party code review | `/third-review` | summon the configured review model to check + fix the task just finished, focusing on "is this round's requirement implemented / is anything missing / is anything wrong" |
@@ -82,12 +82,13 @@ Enter with `/ask` (press it again to exit). Meant for the case where you only wa
82
82
 
83
83
  **While active**:
84
84
 
85
- - **`write` / `edit` hard-disabled** — removed from the active tool set; pi rejects calls to inactive tools outright ("Tool not found"), so file modification becomes mechanically impossible
86
- - **`bash` kept, but sandboxed to read-only** — based on shell tokenization plus write-intent detection (blocks `curl -o/-O`, `wget -O`, `dd of=`, `tee`, redirections, and the like). Heuristic, not a security boundary
87
- - **System prompt appendix** — a mode description is appended so the model behaves along the read-only constraint
85
+ - **`write` / `edit` / `powershell` hard-disabled** — removed from the active tool set; pi rejects calls to inactive tools outright ("Tool not found"), so file modification becomes mechanically impossible
86
+ - **Read-only helper tools activated** — `grep` / `find` / `ls` (registered by pi but inactive by default) are switched on for the duration, so the model gets proper search tools instead of bash-only search; the entry snapshot is restored exactly on exit
87
+ - **`bash` kept, but sandboxed to read-only** — shell tokenization, per-segment validation plus write-intent detection. Allowed beyond the obvious readers: git archaeology (`blame`, `grep`, `rev-parse`, `describe`, `shortlog`, `reflog show`, `stash list`, `tag -l`, `cat-file`, ...), package metadata queries (`pnpm view`, `pip show/list`, `python -m json.tool`) and network reads through `curl -s <url>` / `wget -O - <url>`. Still blocked: redirections, `tee`, `dd`, `sed -i`, installs/builds, git mutations, command substitution, command runners (`find -exec`, `git grep -O`, `rg --pre`), and every write target (`curl -o`, `wget -O file`, `git diff --output=`, `tree -o`). Heuristic, not a security boundary
88
+ - **System prompt appendix names the alternatives** — the appended mode description states what is blocked *and what to use instead* (`read` for file contents, `curl -s` for URLs, `/ask` to exit). Rejected tool calls come back with a classified reason that points at the nearest allowed alternative, so the model does not retry blind variants
88
89
  - **Grey `ask` footer status** — a permanent reminder that read-only mode is on
89
90
 
90
- **Exit**: press `/ask` again; the tool-set snapshot taken on entry is fully restored.
91
+ **Exit**: press `/ask` again; the tool-set snapshot taken on entry is fully restored (read-only helpers included).
91
92
 
92
93
  ---
93
94
 
@@ -195,7 +196,7 @@ pi's `/compact` and auto-compact summarize the context **with the current sessio
195
196
  | auto-compact threshold reached | summarized by the compaction model |
196
197
  | context overflow recovery | summarized by the compaction model, retry behavior unchanged |
197
198
 
198
- **Parts that are identical to pi's built-in behavior after takeover** (the built-in implementation is mirrored verbatim — byte-identical since 0.87.1, verified up to 1.1.0; 0.87.0 shipped an earlier split-turn prompt, and the mirror carries the 0.87.1 fix for every host — and only the executing model changes):
199
+ **Parts that are identical to pi's built-in behavior after takeover** (the built-in implementation is mirrored verbatim — copied from 0.87.1, byte-identical across every published version of the supported window, 1.0.0 → 1.3.0 — and only the executing model changes; the conversation serializer itself is imported from the host, so it follows whatever the running host does, e.g. 1.2.0 stops feeding assistant thinking back into the summarization request):
199
200
 
200
201
  - **Summary format** — the structured Goal / Progress / Key Decisions checkpoint; when a summary already exists, the iterative-update prompt is used so information carries over instead of being lost
201
202
  - **Split behavior** — the `keepRecentTokens` retention policy, and `firstKeptEntryId` / `tokensBefore` passed back unchanged
@@ -262,7 +263,7 @@ No file = feature disabled, zero overhead, silently skipped.
262
263
 
263
264
  **Key constraint: extension commands only**. Commands registered via `registerCommand` such as `/add-dir` (pi-add-dir) and `/cd` (pi-cd) work; built-in commands (`/compact`, `/reload`, `/resume`, …) are dispatched in the TUI layer and cannot be triggered from the extension layer; skill commands (`/skill:name`) are an expansion mechanism rather than an execution mechanism. The target extension you depend on (e.g. pi-add-dir) must be installed first.
264
265
 
265
- > **Implementation notes**: execution uses `sendUserMessage(cmd, { expandPromptTemplates: true })` — that option routes `/cmd args` into command dispatch instead of the model prompt path (the wrapper defaults to `false`; verified against pi 0.87.0–1.1.0 sources, see the host-compatibility discipline in AGENTS.md). Pre-validation uses `pi.getCommands()` (accepting only `source: "extension"`). Depends on `smol-toml` (zero-dependency, ~10KB, TOML 1.0).
266
+ > **Implementation notes**: execution uses `sendUserMessage(cmd, { expandPromptTemplates: true })` — that option routes `/cmd args` into command dispatch instead of the model prompt path (the wrapper defaults to `false`; `sendUserMessage` and `_tryExecuteExtensionCommand` are byte-identical across 1.0.0–1.3.0, see the host-compatibility discipline in AGENTS.md). Pre-validation uses `pi.getCommands()` (accepting only `source: "extension"`). Depends on `smol-toml` (zero-dependency, ~10KB, TOML 1.0).
266
267
 
267
268
  ---
268
269
 
@@ -276,6 +277,9 @@ No file = feature disabled, zero overhead, silently skipped.
276
277
  pnpm install # install deps
277
278
  pnpm typecheck # tsc --noEmit
278
279
  pnpm test # vitest (all feature unit tests)
280
+
281
+ pnpm check:keybindings # audit this plugin's keys against the host's own dispatch table
282
+ pnpm check:compat # verify the declared pi range: last 4 minor lines + 8-line forward probe
279
283
  ```
280
284
 
281
285
  ### Project structure
@@ -309,9 +313,13 @@ pnpm release patch # 0.1.2 → 0.1.3
309
313
  pnpm release minor # 0.1.2 → 0.2.0 (patch zeroed)
310
314
  pnpm release major # 0.1.2 → 1.0.0 (minor + patch zeroed)
311
315
  pnpm release patch --dry-run # preview without changing anything
316
+ pnpm release:github # create/verify the GitHub Release for the current version
317
+ pnpm release:github --audit # list npm versions vs GitHub Releases vs remote tags
312
318
  ```
313
319
 
314
- `release` requires exactly one of `major | minor | patch`; incrementing a higher level zeroes the lower ones. The chain is: **the working tree must be clean (any uncommitted change aborts before anything happens, guaranteeing tag content == published content)** → `typecheck + test` gate → version bump → git commit + `vX.Y.Z` tag (self-check that tag == HEAD and the tree is clean) → `pnpm publish` (with `prepublishOnly` as a second gate) → push only the current branch + the current tag and verify the remote hashes match (if the remote tag differs, GitHub Release creation is skipped).
320
+ `release` requires exactly one of `major | minor | patch`; incrementing a higher level zeroes the lower ones. The chain is: **the working tree must be clean (any uncommitted change aborts before anything happens, guaranteeing tag content == published content)** → `typecheck + test` gate → version bump → git commit + `vX.Y.Z` tag (self-check that tag == HEAD and the tree is clean) → `pnpm publish` (with `prepublishOnly` as a second gate) → push the current branch → push the tag if the remote lacks it, verify the remote tag hash (with bounded retries, because GitHub's ref view lags right after a push) and create the GitHub Release.
321
+
322
+ If that last step is skipped (no `GITHUB_TOKEN`, a failed push, an HTTP error), the version would otherwise stay Release-less forever: the next release's auto-generated notes diff against the previous *published* Release, so the missing version's commits get folded into the next one and GitHub drifts away from npm. `pnpm release:github` is the resume path — it never bumps the version and never touches npm; `--audit` is the drift detector (`--allow-gaps` downgrades a historical gap to a warning).
315
323
 
316
324
  ---
317
325
 
@@ -327,6 +335,7 @@ PRs and issues welcome. A few conventions:
327
335
  - **Built-in patches** (favorites) must stay **version-guarded and idempotent** — verify the target prototype members exist before patching, guard against double-apply, degrade with a `console.warn` instead of breaking the extension
328
336
  - **Built-in prompt sync** (compact) — the summarization prompts in `src/features/compact/prompt.ts` are copied verbatim from pi's built-in compaction; when upgrading the pi dependency, diff `dist/core/compaction/compaction.js` (and `utils.js`) and update the strings + assembly helpers to match, keeping takeover output byte-compatible with stock
329
337
  - **Keybindings** — before picking a new shortcut, check pi defaults + reserved keys, terminal control chars, and OS/terminal-emulator grabs; prefer `ctrl+alt+<letter>` for cross-platform safety. Do not stop at "it works on my machine in the main screen": in `fullscreen` mode (pi's default since 1.0) the alt-screen layer consumes `tui.altScreen.*` **before** dialogs see the key, and on Windows/WSL several of those bindings differ from the Linux defaults. Run `pnpm check:keybindings [candidate...]` (and `pnpm check:keybindings --all` after a pi upgrade) — it probes the installed host's resolved keybinding table with the host's own `matchesKey`. A key free in the host table can still be taken at the machine layer: Windows Terminal binds `ctrl+v` (paste) and `ctrl+shift+f` (find), and third-party Windows tools commonly grab `ctrl+alt+<letter>` (a screenshot tool once took `ctrl+alt+f`) — so try the key for real on the target machine before shipping it.
338
+ - **Host compatibility** — the peer range is a measured statement, not a wish: the four most recently released pi minor lines are supported best-effort, the upper bound encodes the forward probe cap (newest line + 8 minor lines), and the README annotation must name the highest version actually verified. Re-run `pnpm check:compat` whenever the host dependency moves and read its verdict block (it fails if the declared range drifts from the policy). See the pi 宿主兼容 section of AGENTS.md; keybinding collisions found during an upgrade are reported to the maintainer for a decision, never re-keyed silently.
330
339
  - **Quality gate** — `pnpm typecheck && pnpm test` must pass before submitting
331
340
 
332
341
  ---
package/README.zh-CN.md CHANGED
@@ -9,12 +9,12 @@
9
9
  [![npm version](https://img.shields.io/npm/v/@andares/pi-toolkits?label=npm&logo=npm)](https://www.npmjs.com/package/@andares/pi-toolkits)
10
10
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
11
11
  [![Package Manager](https://img.shields.io/badge/package%20manager-pnpm-orange?logo=pnpm)](https://pnpm.io)
12
- [![pi >= 0.87](https://img.shields.io/badge/pi-%3E%3D0.87.0%20%3C2.0.0-blueviolet)](https://github.com/earendil-works/pi)
12
+ [![pi >= 1.0](https://img.shields.io/badge/pi-%3E%3D1.0.0%20%3C1.12.0-blueviolet)](https://github.com/earendil-works/pi)
13
13
  [![GitHub](https://img.shields.io/badge/github-andares%2Fpi--toolkits-181717?logo=github)](https://github.com/andares/pi-toolkits)
14
14
 
15
15
  一句话介绍:目前包含六个开箱即用的功能——**只读咨询模式** `ask`、**模型收藏** `favorites`、**提示词暂存** `stash`、**第三方代码评审** `third-review`、**独立压缩模型** `compact-model`、**会话启动自动化** `autostart`,后续持续追加。
16
16
 
17
- **版本要求**:pi `>=0.87.0 <2.0.0`(覆盖最近 4 个中版本线 0.87.x / 0.99.x / 1.0.x / 1.1.x —— 0.88–0.98 从未发布。区间内全部 11 个已发布版本(0.87.0 → 1.1.0)逐版本比对宿主 dist:用到的扩展 API 无破坏性差异(1.0.4 → 1.1.0 仅新增字段),favorites 两处运行时 patch 目标 `ModelSelectorComponent` / `AgentSession.cycleModel` 字节级一致;并在 0.87.0、0.87.1、0.99.0、0.99.2、1.0.0、1.0.4、1.1.0 上逐一通过 typecheck + 全量测试,在真实 pi 1.1.0 上冒烟加载无警告)。注:pi ≥ 0.99 的宿主要求 `@earendil-works/pi-tui` 声明为 peerDependencies `"*"`(由宿主提供),本包已按要求声明。
17
+ **版本要求**:pi `>=1.0.0 <1.12.0` —— 最近 4 个中版本线(1.0.x / 1.1.x / 1.2.x / 1.3.x)尽力支持,上界编码「向后探针」的硬上限:宿主每发一版,我们最多向后查 **8 条中版本线**(当前 1.4 … 1.11),再晚不宣称。**实测自然兼容最高 1.3.0**(写此文时的最新版本,更新的还没发布、无物可探)。一条命令 `pnpm check:compat` 从 registry 推导目标集合,逐版本在系统临时目录沙箱中(把整个 `@earendil-works/*` 家族钉在待测版本)跑:安装 → `tsc --noEmit` → 全量测试 → 键位审计 → 对 patch/mirror 的宿主 `dist` 文件做哈希比对。1.0.0 → 1.3.0 全部 8 个已发布版本:typecheck、289 个测试、键位审计全绿;被跟踪的宿主文件仅在 `core/agent-session.js`(1.3.0 改了 tool-restore 路径,`cycleModel` 未动)、`core/keybindings.js` 与 `core/compaction/utils.js`(二者都是 1.2.0 变的,我们按键的解析结果不变)、`extensions/types.d.ts`(纯增量)上有差异 —— `ModelSelectorComponent`、`core/compaction/compaction.js` 与 `core/usage-totals.js` 在窗口内字节级不变。真实 pi 1.3.0 冒烟加载无警告。注:pi ≥ 0.99 的宿主要求 `@earendil-works/pi-tui` 声明为 peerDependencies `"*"`(由宿主提供),本包已按要求声明。
18
18
 
19
19
  ---
20
20
 
@@ -65,7 +65,7 @@ pi -e ./src/index.ts
65
65
 
66
66
  | 功能 | 入口 | 一句话说明 |
67
67
  | --- | --- | --- |
68
- | 🛡️ **ask** 只读咨询模式 | `/ask` | 进入只读问答模式,`write`/`edit` 硬禁用,`bash` 只读沙箱 |
68
+ | 🛡️ **ask** 只读咨询模式 | `/ask` | 进入只读问答模式,`write`/`edit`/`powershell` 硬禁用,`bash` 只读沙箱,临时启用 `grep`/`find`/`ls` |
69
69
  | ⭐ **favorites** 模型收藏 | `/model` 内 `ctrl+A` / `ctrl+J` | 收藏常用模型(加粗亮黄标记),`ctrl+p` 仅在收藏间循环 |
70
70
  | 📥 **stash** 提示词暂存 | `ctrl+alt+y` | 一段提示词的暂存槽,按键交换 / 连按两次暂存并清空输入框 |
71
71
  | 🔎 **review** 第三方代码评审 | `/third-review` | 召唤配置的评审模型,对刚完成的任务做查+修,重点检查「本轮需求实现 / 是否有遗漏 / 是否有错误」 |
@@ -82,12 +82,13 @@ pi -e ./src/index.ts
82
82
 
83
83
  **进入后**:
84
84
 
85
- - **`write` / `edit` 硬禁用** —— 从活跃工具集中移除,pi 运行时对未激活工具的调用直接拒绝("Tool not found"),文件修改在机制上不可能发生
86
- - **`bash` 保留但只读沙箱** —— 基于 shell 分词 + 写盘意图检测(`curl -o/-O`、`wget -O`、`dd of=`、`tee`、重定向等一律拦截)。启发式,非安全边界
87
- - **系统提示词追加** —— 追加一段模式说明,模型会遵守只读约束行事
85
+ - **`write` / `edit` / `powershell` 硬禁用** —— 从活跃工具集中移除,pi 运行时对未激活工具的调用直接拒绝("Tool not found"),文件修改在机制上不可能发生
86
+ - **临时启用只读检索工具** —— `grep` / `find` / `ls`(宿主已注册但默认不激活)在 ask 期间打开,让模型有正规搜索工具而不是只能走 bash;退出时按进入前快照完整恢复
87
+ - **`bash` 保留但只读沙箱** —— shell 分词 + 逐段校验 + 写盘意图检测。除常见读取命令外,放行 git 考古族(`blame`、`grep`、`rev-parse`、`describe`、`shortlog`、`reflog show`、`stash list`、`tag -l`、`cat-file` 等)、包元数据查询(`pnpm view`、`pip show/list`、`python -m json.tool`),以及网络读取 `curl -s <url>` / `wget -O - <url>`。仍拦截:重定向、`tee`、`dd`、`sed -i`、安装/构建、git 变更类命令、命令替换、会启动其它程序的选项(`find -exec`、`git grep -O`、`rg --pre`),以及一切写盘目标(`curl -o`、`wget -O file`、`git diff --output=`、`tree -o`)。启发式,非安全边界
88
+ - **系统提示词给出替代方案** —— 追加的模式说明不仅列限制,还写明被拦后用什么替代(读文件用 `read`、取网页用 `curl -s`、要改动则 `/ask` 退出);被拒的工具调用返回分类原因,直接指向最近的可用替代,模型不会盲目重试
88
89
  - **灰色 `ask` footer 状态** —— 常驻提示当前处于只读模式
89
90
 
90
- **退出**:再按 `/ask`,完整恢复进入前的工具集快照。
91
+ **退出**:再按 `/ask`,完整恢复进入前的工具集快照(含临时启用的只读工具)。
91
92
 
92
93
  ---
93
94
 
@@ -195,7 +196,7 @@ pi 的 `/compact` 与 auto-compact 默认**用当前会话模型**做上下文
195
196
  | auto-compact 阈值触发 | 压缩模型总结 |
196
197
  | context overflow 恢复 | 压缩模型总结,重试行为不变 |
197
198
 
198
- **接管后与 pi 内建行为完全一致的部分**(逐字同步 pi 内建实现,0.87.1 起字节级不变、已核对至 1.1.0,只换执行模型;0.87.0 的分割轮次前缀 prompt 与 0.87.1 不同,本镜像对所有宿主采用 0.87.1 的修复文本):
199
+ **接管后与 pi 内建行为完全一致的部分**(逐字同步 pi 内建实现,复制自 0.87.1、在支持区间内全部已发布版本(1.0.0 → 1.3.0)字节级不变,只换执行模型;对话序列化函数(`serializeConversation`)是直接从宿主导入的,因此跟随宿主行为 —— 例如 1.2.0 起不再把 assistant thinking 回填进摘要请求):
199
200
 
200
201
  - **摘要格式** —— Goal / Progress / Key Decisions 结构化 checkpoint;已有摘要时用迭代更新版 prompt,信息延续不丢
201
202
  - **切分行为** —— 保留最近 `keepRecentTokens` 的策略、`firstKeptEntryId` / `tokensBefore` 原样回传,不动
@@ -262,7 +263,7 @@ run = [
262
263
 
263
264
  **关键约束:只支持扩展命令**。`/add-dir`(pi-add-dir)、`/cd`(pi-cd)等 `registerCommand` 注册的命令可以;内置命令(`/compact`、`/reload`、`/resume`…)在 TUI 层分发,扩展层触发不了;skill 命令(`/skill:name`)是展开机制而非执行机制。依赖的目标扩展(如 pi-add-dir)需要先安装。
264
265
 
265
- > **实现说明**:执行用 `sendUserMessage(cmd, { expandPromptTemplates: true })` —— 该选项把 `/cmd args` 路由到命令分发而不是模型 prompt 路径(包装层默认 `false`,已对照 pi 0.87.0–1.1.0 源码核实,见 AGENTS.md 宿主兼容纪律)。预校验用 `pi.getCommands()`(只认 `source: "extension"`)。依赖 `smol-toml`(零依赖、~10KB、TOML 1.0)。
266
+ > **实现说明**:执行用 `sendUserMessage(cmd, { expandPromptTemplates: true })` —— 该选项把 `/cmd args` 路由到命令分发而不是模型 prompt 路径(包装层默认 `false`,`sendUserMessage` 与 `_tryExecuteExtensionCommand` 在 1.0.0–1.3.0 全区间字节级一致,见 AGENTS.md 宿主兼容纪律)。预校验用 `pi.getCommands()`(只认 `source: "extension"`)。依赖 `smol-toml`(零依赖、~10KB、TOML 1.0)。
266
267
 
267
268
  ---
268
269
 
@@ -276,6 +277,9 @@ run = [
276
277
  pnpm install # 安装依赖
277
278
  pnpm typecheck # tsc --noEmit
278
279
  pnpm test # vitest(全部功能单元测试)
280
+
281
+ pnpm check:keybindings # 用宿主自己的分发表审计本插件的按键
282
+ pnpm check:compat # 校验声明的 pi 区间:最近 4 条中版本线 + 向后 8 条线探针
279
283
  ```
280
284
 
281
285
  ### 项目结构
@@ -308,9 +312,13 @@ pnpm release patch # 0.1.2 → 0.1.3
308
312
  pnpm release minor # 0.1.2 → 0.2.0 (patch 清零)
309
313
  pnpm release major # 0.1.2 → 1.0.0 (minor + patch 清零)
310
314
  pnpm release patch --dry-run # 预览,不做任何改动
315
+ pnpm release:github # 为当前版本创建/校验 GitHub Release(幂等)
316
+ pnpm release:github --audit # 列出 npm 版本 vs GitHub Release vs 远端 tag
311
317
  ```
312
318
 
313
- `release` 要求且仅要求 `major | minor | patch` 之一;上级递增清零下级。执行链:**工作区必须干净(有未提交变更直接中止,保证 tag 内容 == 发布内容)** → `typecheck + test` 门禁 → 改版本 → git commit + `vX.Y.Z` tag(自检 tag==HEAD 且工作区干净) → `pnpm publish`(`prepublishOnly` 二次门禁) → 只推当前分支 + 当前 tag 并校验远端哈希一致(远端 tag 不一致则跳过 Release 创建)。
319
+ `release` 要求且仅要求 `major | minor | patch` 之一;上级递增清零下级。执行链:**工作区必须干净(有未提交变更直接中止,保证 tag 内容 == 发布内容)** → `typecheck + test` 门禁 → 改版本 → git commit + `vX.Y.Z` tag(自检 tag==HEAD 且工作区干净) → `pnpm publish`(`prepublishOnly` 二次门禁) → 推当前分支 → tag 远端没有就推、有就校验哈希一致(带有限次重试——刚推完 GitHub 的 ref 可见性可能滞后)→ 创建 GitHub Release。
320
+
321
+ 若最后一步被跳过(无 `GITHUB_TOKEN`、push 失败、HTTP 报错),该版本会永远没有 Release:下一版自动生成的 release notes 是**对着上一个已发布的 Release** 算 diff 的,于是缺的那版提交被吞进下一版,GitHub 的版本线就与 npm 对不上。`pnpm release:github` 就是补建/续跑入口——不动版本号、不碰 npm;`--audit` 是差集探测器(`--allow-gaps` 把历史缺口降级为警告)。
314
322
 
315
323
  ---
316
324
 
@@ -326,6 +334,7 @@ pnpm release patch --dry-run # 预览,不做任何改动
326
334
  - **内建 patch**(favorites)必须保持**版本守卫 + 幂等** —— 打 patch 前先确认目标原型成员存在,防止重复应用,失败时用 `console.warn` 降级而不是把扩展搞挂
327
335
  - **内建提示词同步**(compact) —— `src/features/compact/prompt.ts` 里的总结 prompt 逐字抄自 pi 内建压缩;升级 pi 依赖时 diff `dist/core/compaction/compaction.js`(及 `utils.js`),同步字符串与组装辅助函数,保持接管输出与内建字节级一致
328
336
  - **键位** —— 定新快捷键前先查 pi 默认键位 + 保留键、终端控制字符、OS/终端模拟器占用;跨平台安全优先选 `ctrl+alt+<字母>`。**不得只验证「主屏下我这台机器能用」**:自 pi 1.0 起默认 `fullscreen`,alt-screen 层在对话框看到按键**之前**就消费 `tui.altScreen.*`,且 Windows/WSL 上多个绑定与 Linux 默认值不同。用 `pnpm check:keybindings [候选键...]` 核查(pi 升级后跑 `pnpm check:keybindings --all`),它用宿主自己的 `matchesKey` 探测已安装宿主的**已解析**键位表。**宿主表空闲 ≠ 机器层空闲**:Windows Terminal 默认占 `ctrl+v`(粘贴)与 `ctrl+shift+f`(查找),第三方 Windows 工具常绑 `ctrl+alt+<字母>`(截图工具占过 `ctrl+alt+f`)——定键后必须在真机试按,确认按键真的到得了应用。
337
+ - **宿主兼容** —— peer 区间是实测结论而非愿望:最近 4 个已发布中版本线尽力支持,上界编码「向后探 8 条中版本线」的上限,README 必须标注**实测支持到的最高版本**。宿主依赖一动就重跑 `pnpm check:compat`,读它的 verdict 段(区间与策略不一致会直接判失败)。详见 AGENTS.md「pi 宿主兼容」;升级中发现的键位冲突**上报崔总决策**,不得顺手换键。
329
338
  - **质量门禁** —— 提交前必须通过 `pnpm typecheck && pnpm test`
330
339
 
331
340
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andares/pi-toolkits",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Integrated toolkit extension for pi — ask mode (read-only Q&A), model favorites, prompt stash, third-party code review, and a dedicated compaction model",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  "autostart"
29
29
  ],
30
30
  "peerDependencies": {
31
- "@earendil-works/pi-coding-agent": ">=0.87.0 <2.0.0",
31
+ "@earendil-works/pi-coding-agent": ">=1.0.0 <1.12.0",
32
32
  "@earendil-works/pi-tui": "*"
33
33
  },
34
34
  "dependencies": {
@@ -36,9 +36,9 @@
36
36
  "smol-toml": "^1.9.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@earendil-works/pi-ai": "^1.1.0",
40
- "@earendil-works/pi-coding-agent": "^1.1.0",
41
- "@earendil-works/pi-tui": "^1.1.0",
39
+ "@earendil-works/pi-ai": "^1.3.0",
40
+ "@earendil-works/pi-coding-agent": "^1.3.0",
41
+ "@earendil-works/pi-tui": "^1.3.0",
42
42
  "@types/node": "^26.6.2",
43
43
  "typescript": "^5.6.0",
44
44
  "vitest": "^4.1.10"
@@ -52,6 +52,8 @@
52
52
  "typecheck": "tsc --noEmit",
53
53
  "test": "vitest run",
54
54
  "check:keybindings": "node ./scripts/check-keybindings.mjs",
55
+ "check:compat": "node ./scripts/check-compat.mjs",
56
+ "release:github": "node ./scripts/release-github.mjs",
55
57
  "release": "node ./scripts/publish.mjs",
56
58
  "tag-current": "node ./scripts/tag-current.mjs"
57
59
  }
@@ -2,7 +2,7 @@
2
2
  * Boundary tests for the ask-mode read-only bash sandbox.
3
3
  */
4
4
  import { describe, expect, it } from "vitest";
5
- import { isReadOnlyCommand } from "./bash-sandbox.js";
5
+ import { classifyBlockedCommand, isReadOnlyCommand } from "./bash-sandbox.js";
6
6
 
7
7
  describe("isReadOnlyCommand — allowed (read-only)", () => {
8
8
  const allowed = [
@@ -14,6 +14,10 @@ describe("isReadOnlyCommand — allowed (read-only)", () => {
14
14
  "git status",
15
15
  "git log --oneline -5",
16
16
  "git diff --stat",
17
+ "git show HEAD",
18
+ "git branch -a",
19
+ "git remote -v",
20
+ "git show HEAD 2>/dev/null",
17
21
  "grep -r foo src",
18
22
  "find . -name '*.ts'",
19
23
  "cd src && ls",
@@ -21,7 +25,12 @@ describe("isReadOnlyCommand — allowed (read-only)", () => {
21
25
  "npm list --depth=0",
22
26
  "curl -s https://example.com",
23
27
  "curl -o - https://example.com", // stdout, allowed
28
+ "curl --output - https://example.com", // stdout, allowed
29
+ "curl --output=- https://example.com", // stdout, allowed
24
30
  "wget -O - https://example.com", // stdout, allowed
31
+ "wget -qO- https://example.com", // stdout, allowed
32
+ "wget -O - --quiet https://example.com", // stdout, allowed
33
+ "wget --output-document=- https://example.com", // stdout, allowed
25
34
  "echo 'hello && world'", // quoted operator is data, not control
26
35
  ];
27
36
 
@@ -30,6 +39,61 @@ describe("isReadOnlyCommand — allowed (read-only)", () => {
30
39
  });
31
40
  });
32
41
 
42
+ describe("isReadOnlyCommand — allowed (relaxed read-only families)", () => {
43
+ const allowed = [
44
+ // git archaeology
45
+ "git blame -L 10,20 src/index.ts",
46
+ "git grep -n registerAsk -- src",
47
+ "git rev-parse HEAD",
48
+ "git rev-list --count HEAD",
49
+ "git describe --tags --abbrev=0",
50
+ "git shortlog -sn",
51
+ "git reflog show",
52
+ "git reflog",
53
+ "git stash list",
54
+ "git stash show -p",
55
+ "git worktree list",
56
+ "git tag -l",
57
+ "git tag --list 'v*'",
58
+ "git cat-file -p HEAD",
59
+ "git merge-base HEAD~5 HEAD",
60
+ "git check-ignore -v node_modules",
61
+ "git config --list",
62
+ // destructive-substring false positives in arguments
63
+ "git log --grep=rm --oneline",
64
+ "git blame src/rm-utils.ts",
65
+ "git log --grep=rm --oneline && ls src",
66
+ // search arguments that merely *mention* a guarded flag are data
67
+ "grep -rn 'find -exec' src",
68
+ "git grep -n 'find -exec' -- .",
69
+ "git log --grep='curl -so x' --oneline",
70
+ "rg 'sed -i' .",
71
+ // package metadata
72
+ "pnpm view vite version",
73
+ "pnpm info react",
74
+ "pnpm search eslint",
75
+ "pip show requests",
76
+ "pip list",
77
+ "pip3 freeze",
78
+ "uv pip list",
79
+ "python3 -m json.tool package.json",
80
+ // guards must not over-block plain readers
81
+ "fd -e ts",
82
+ "sort package.json",
83
+ "curl -so- https://example.com", // clustered short form, stdout
84
+ "git grep --textconv -n foo", // read path, not the -O pager
85
+ "env", // bare env prints the environment
86
+ "env -i",
87
+ "env FOO=bar", // assignment-only form, no command
88
+ "file README.md",
89
+ "sed -n '1p' README.md",
90
+ ];
91
+
92
+ it.each(allowed)("allows: %s", (command) => {
93
+ expect(isReadOnlyCommand(command)).toBe(true);
94
+ });
95
+ });
96
+
33
97
  describe("isReadOnlyCommand — blocked (mutating/destructive)", () => {
34
98
  const blocked = [
35
99
  "rm -rf /",
@@ -40,19 +104,67 @@ describe("isReadOnlyCommand — blocked (mutating/destructive)", () => {
40
104
  "touch foo",
41
105
  "npm install",
42
106
  "npm run build",
107
+ "pnpm install",
108
+ "pnpm add left-pad",
109
+ "pip install requests",
43
110
  "git commit -m x",
44
111
  "git push",
45
112
  "git checkout main",
113
+ "git reset --hard",
114
+ "git stash drop",
115
+ "git stash pop",
116
+ "git worktree remove x",
117
+ "git reflog expire --all",
118
+ "git tag v1",
119
+ "git tag -d v1",
120
+ "git branch new-branch",
121
+ "git branch -m old new",
122
+ "git branch -D old",
123
+ "git remote add origin git@example.com:x/y.git",
124
+ "git remote remove origin",
46
125
  "cat x > out.txt",
47
126
  "echo hi >> log",
48
127
  "echo $(rm -rf /)", // command substitution
128
+ "git log --grep=$(rm -rf /)", // substitution hidden in an allowed verb
49
129
  "ls | rm -rf /", // pipeline with destructive command
130
+ "git log --grep=rm --oneline && rm -rf /", // second segment is a write
131
+ "find . -name x -exec rm {} \\;", // find stays out of the read-only bypass
132
+ "find . -delete", // -delete is a write
133
+ "ls | find . -delete", // ... and the pre-check sees pipelines too
134
+ "find . -name '*.ts' -exec sh -c 'echo x > /tmp/y' \\;", // -exec runs commands
135
+ "ls | find . -name x -exec echo {} \\;", // exec intent outside the first segment
136
+ "fd . -x rm", // fd -x runs commands: never on the bypass list
137
+ "fd . -x touch y",
138
+ "fd --exec rm {}",
139
+ "sort -o out.txt file", // sort's output flag is a write
140
+ "sort file --output=out.txt",
141
+ "curl -so /tmp/x https://example.com", // clustered short form writes
142
+ "curl -o/tmp/x https://example.com", // attached short form writes
143
+ "curl -sLo /tmp/x https://example.com",
144
+ "curl --remote-name https://example.com",
145
+ "sed -n -i 's/a/b/' f", // -i sneaks past the `sed -n` allowlist
146
+ "tree -o /tmp/x",
147
+ "tree --output=/tmp/x",
148
+ "file -C -m magic",
149
+ "git grep -O 'rm -rf /' pattern", // -O runs a pager command
150
+ "git grep --open-files-in-pager=less pattern",
151
+ "git log --ext-diff -p", // external diff driver
152
+ "env curl -o out.txt https://example.com", // env forwards to a write
153
+ "env -i curl -o out.txt https://example.com",
154
+ "env FOO=1 curl -o out.txt https://example.com",
155
+ "ls && env curl -o out.txt https://example.com",
156
+ "env -S 'sh -c x'",
157
+ "env find . -delete",
158
+ "env sed -i 's/a/b/' f",
50
159
  "curl -o file https://example.com",
51
160
  "curl -O https://example.com/x",
52
161
  "curl --output file https://example.com",
53
162
  "wget -O file https://example.com",
54
163
  "wget --output-document file https://example.com",
55
- "wget -qO- https://example.com", // sandbox rejects (only wget -O - is safe); fine, be strict
164
+ "wget https://example.com",
165
+ "git diff --output=out.txt", // real write path on a read-only verb
166
+ "git log --output out.txt",
167
+ "git show -o out.txt",
56
168
  "dd if=/dev/zero of=/tmp/x bs=1 count=1",
57
169
  "echo x | tee file",
58
170
  "vim README.md",
@@ -64,3 +176,22 @@ describe("isReadOnlyCommand — blocked (mutating/destructive)", () => {
64
176
  expect(isReadOnlyCommand(command)).toBe(false);
65
177
  });
66
178
  });
179
+
180
+ describe("classifyBlockedCommand", () => {
181
+ it.each([
182
+ ["echo $(id)", "substitution"],
183
+ ["cat a > b", "redirect"],
184
+ ["git grep -O x", "exec"],
185
+ ["env curl -o out.txt https://example.com", "exec"],
186
+ ["find . -exec echo {} ;", "exec"],
187
+ ["rm -rf /", "write"],
188
+ ["find . -delete", "write"],
189
+ ["curl -O https://example.com", "write"],
190
+ ["wget https://example.com", "download"],
191
+ ["git commit -m x", "git"],
192
+ ["pnpm install", "install"],
193
+ ["vim README.md", "unlisted"],
194
+ ] as const)("classifies %s as %s", (command, kind) => {
195
+ expect(classifyBlockedCommand(command)).toBe(kind);
196
+ });
197
+ });