@trim21/personal-pi-extensions 0.1.650 → 0.1.652

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.
Files changed (52) hide show
  1. package/README.md +67 -9
  2. package/package.json +5 -13
  3. package/src/aft/index.ts +40 -23
  4. package/src/aft/tools.ts +10 -9
  5. package/src/claude-code/files.ts +99 -54
  6. package/src/claude-code/glob.ts +8 -5
  7. package/src/claude-code/grep.ts +8 -5
  8. package/src/claude-code/index.ts +56 -10
  9. package/src/claude-code/search.ts +4 -3
  10. package/src/claude-code/session-tools.ts +4 -3
  11. package/src/claude-code/shell.ts +3 -8
  12. package/src/gh/index.ts +48 -25
  13. package/src/gh/tools/download-release-assets.ts +3 -3
  14. package/src/gh/tools/get-workflow-jobs.ts +3 -3
  15. package/src/gh/tools/list-issues.ts +3 -3
  16. package/src/gh/tools/list-prs.ts +3 -3
  17. package/src/gh/tools/list-releases.ts +3 -3
  18. package/src/gh/tools/list-workflow-runs.ts +3 -3
  19. package/src/gh/tools/read-ci-logs.ts +3 -4
  20. package/src/gh/tools/read-issue-comments.ts +3 -3
  21. package/src/gh/tools/read-issue.ts +3 -3
  22. package/src/gh/tools/read-pr-comments.ts +3 -3
  23. package/src/gh/tools/read-pr-diff.ts +3 -3
  24. package/src/gh/tools/read-pr-status.ts +3 -3
  25. package/src/gh/tools/read-pr.ts +3 -3
  26. package/src/gh/tools/read-release.ts +3 -3
  27. package/src/gh/tools/read-repo.ts +3 -3
  28. package/src/gh/tools/wait-commit-checks.ts +3 -3
  29. package/src/gh/tools/wait-pr-checks.ts +3 -3
  30. package/src/gh/tools/watch-run.ts +3 -3
  31. package/src/index.ts +120 -0
  32. package/src/lib/lsp/inspect-tool.ts +5 -5
  33. package/src/lib/lsp/rename-tool.ts +4 -7
  34. package/src/lib/tool-bus.ts +126 -0
  35. package/src/lib/tool-registration.ts +83 -0
  36. package/src/lib/tool-services.ts +65 -0
  37. package/src/lib/tool-units.ts +102 -0
  38. package/src/lib/tools-config.ts +277 -0
  39. package/src/opencode/bash.ts +7 -2
  40. package/src/opencode/files.ts +90 -43
  41. package/src/opencode/glob.ts +7 -1
  42. package/src/opencode/grep.ts +7 -1
  43. package/src/opencode/index.ts +66 -21
  44. package/src/opencode/question.ts +8 -2
  45. package/src/opencode/todo.ts +8 -2
  46. package/src/orphan-worker-cleaner.ts +38 -0
  47. package/src/spawn-agent.ts +160 -175
  48. package/src/talk/index.ts +92 -66
  49. package/src/vision-agent.ts +31 -7
  50. package/src/web/fetch.ts +89 -72
  51. package/src/web/search.ts +14 -2
  52. package/src/gh-readonly.ts +0 -10
package/README.md CHANGED
@@ -10,6 +10,7 @@
10
10
  | ----------------------------------------- | --------------------------------------------------------------------------------------------- |
11
11
  | [opencode 工具集](#opencode-工具集) | 小写 `read`/`edit`/`write`/`grep`/`glob`/`bash`/`todowrite`/`question` |
12
12
  | [Claude Code 工具集](#claude-code-工具集) | 大写 `Read`/`Edit`/`Write`/`Grep`/`Glob`/`Bash`/`TodoWrite`/`AskUserQuestion` |
13
+ | [工具启用配置](#工具启用配置) | `personalExtensions`:按模型选文件 IO 工具集、通配禁用/启用工具 |
13
14
  | [bwrap](#bwrap) | 基于 bubblewrap 的 OS 级沙箱(内置两套工具集):文件系统隔离 + 多档网络策略 |
14
15
  | [写保护(内置)](#写保护内置) | 写工具内置:限制文件写入在 workspace 内,外部写入需审批 |
15
16
  | [LSP(内置)](#lsp内置) | 文件工具内置 LSP 诊断 + `lsp-rename`/`lsp-inspect`/`lsp-find-definition`/`lsp-find-reference` |
@@ -23,13 +24,66 @@
23
24
  | [web](#web) | `web_search`(Search1API 搜索)与 `web_fetch`(正文提取 / 原样落盘) |
24
25
  | [openai-cost](#openai-cost) | OpenAI Chat Completions,费用取自响应 `usage.cost` |
25
26
 
26
- > **两套工具风格,按预期只启用其中一套**:本包同时提供 opencode 风格
27
- > (小写 `read`/`edit`/`write`/`grep`/`glob`/`bash`/`todowrite`/`question`)与 Claude Code
28
- > 风格(大写 `Read`/`Edit`/`Write`/`Grep`/`Glob`/`Bash`/`TodoWrite`/
29
- > `AskUserQuestion`)两套工具集,二者共享 bwrap 沙箱、写保护与 LSP 实现。两套同时
30
- > 启用会带来预期外的冗余:同名命令重复注册(如 `/bwrap` 出现 `/bwrap:1`
31
- > 后缀)、系统提示重复注入。请只启用其中一套:在 `~/.pi/agent/settings.json`
32
- > 的 `defaultTools` 中只列出一套,或启动时用 `--exclude-tools` 排除另一套。
27
+ ---
28
+
29
+ ## 工具启用配置
30
+
31
+ 所有工具由单一扩展入口 `src/index.ts` 注册(`package.json` 的 `pi.extensions` 只列
32
+ `src/index.ts`、`src/session-name.ts`、`src/system-prompt/index.ts`)。启用的工具由
33
+ `~/.pi/agent/settings.json` 的 `personalExtensions` section 决定:
34
+
35
+ ```jsonc
36
+ {
37
+ "personalExtensions": {
38
+ // 兜底的文件 IO 工具集:opencode(小写 read/edit/write/grep/glob/bash)
39
+ // 或 claude-code(大写 Read/Edit/Write/Grep/Glob/Bash)
40
+ "fileIo": "claude-code",
41
+ // 按模型覆盖 fileIo,按顺序取第一条命中(工具名与模型名都是 minimatch 通配,
42
+ // 模型名同时试 model.id 与 provider/model)
43
+ "fileIoByModel": [{ "models": ["glm-*", "zhipu/*"], "fileIo": "opencode" }],
44
+ // 工具可用性规则:条目是工具名模式,或 { tools, models }
45
+ "disabledTools": ["talk-*", { "tools": ["web_*"], "models": ["gpt-*"] }],
46
+ "enabledTools": [{ "tools": ["web_search"], "models": ["glm-*"] }],
47
+ },
48
+ }
49
+ ```
50
+
51
+ 规则:**命中 `disabledTools` 且未命中 `enabledTools` 的工具不注册**。带 `models` 的
52
+ 条目只在当前模型命中时参与判定。未配置该 section 时用 `fileIo: "claude-code"` 且不
53
+ 禁用任何工具。
54
+
55
+ **判定时机是每个会话启动**:pi 在启动 / `/new` / `resume` / `/fork` / `/reload` 时重建
56
+ 扩展,注册就发生在 `session_start` 里(模型在扩展加载期读不到),因此新会话按自己的
57
+ 模型重新判定;**同一会话内 `/model` 切换不会改变工具集合**,换模型请开新会话。
58
+ 两个不同模型的会话可以同时用不同的工具集。
59
+
60
+ 配置里的非法取值、以及匹配不到任何工具的模式都会在会话启动时以 warning 通知报出。
61
+
62
+ ### 入口级排除项的迁移
63
+
64
+ 旧的 `packages[].extensions` 排除写法针对的是扩展入口文件,入口收敛后那些文件不再是
65
+ 扩展入口,改用 `personalExtensions` 表达:
66
+
67
+ | 旧写法(`packages[].extensions`) | 新写法(`personalExtensions`) |
68
+ | --------------------------------- | --------------------------------------------------------------------------- |
69
+ | `!src/opencode/index.ts` | `"fileIo": "claude-code"` |
70
+ | `!src/claude-code/index.ts` | `"fileIo": "opencode"` |
71
+ | `!src/web/search.ts` | `"disabledTools": ["web_search"]` |
72
+ | `!src/web/fetch.ts` | `"disabledTools": ["web_fetch"]` |
73
+ | `!src/talk/index.ts` | `"disabledTools": ["talk-*"]` |
74
+ | `!src/aft/index.ts` | `"disabledTools": ["aft_*"]` |
75
+ | `!src/gh-readonly.ts` | `"disabledTools": ["read-github-*", "list-github-*", "wait-github-*", ...]` |
76
+ | `!src/spawn-agent.ts` | `"disabledTools": ["spawn-agent"]` |
77
+ | `!src/vision-agent.ts` | `"disabledTools": ["describe_image"]` |
78
+
79
+ 小技巧:可以用 `enabledTools` 做「全局禁用、某个模型才开」:
80
+
81
+ ```jsonc
82
+ {
83
+ "disabledTools": ["web_*"],
84
+ "enabledTools": [{ "tools": ["web_search"], "models": ["glm-*"] }],
85
+ }
86
+ ```
33
87
 
34
88
  ---
35
89
 
@@ -249,7 +303,11 @@ bwrap 已集成进 bash 工具实现(opencode 风格 `bash` 位于 `src/openco
249
303
 
250
304
  ## opencode 工具集
251
305
 
252
- opencode 风格工具集,随 `src/opencode/index.ts` 一次加载:`read` / `edit` / `write` / `grep` / `glob` / `bash` / `todowrite` / `question`,以及共享 LSP 工具。`grep` 替换 pi 内置 `grep`,`glob` 与 pi 内置 `find` 并存。各单工具文件(`src/opencode/grep.ts` 等)也可独立加载。
306
+ opencode 风格工具集:`read` / `edit` / `write` / `grep` / `glob` / `bash` / `todowrite` / `question`,以及共享 LSP 工具。`grep` 替换 pi 内置 `grep`,`glob` 与 pi 内置 `find` 并存。
307
+
308
+ 启用方式:`personalExtensions.fileIo` 设为 `"opencode"`(或按模型在 `fileIoByModel` 里指定,见[工具启用配置](#工具启用配置))。它与 Claude Code 工具集**只能启用一套**——两套都注册会让同名命令(如 `/bwrap`)重复注册、系统提示重复注入。各单工具文件(`src/opencode/grep.ts` 等)仍可单独 `-e` 加载。
309
+
310
+ > **与 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`… 无重名)。
253
311
 
254
312
  ### edit
255
313
 
@@ -320,7 +378,7 @@ pi -e ./src/opencode/index.ts
320
378
 
321
379
  ## Claude Code 工具集
322
380
 
323
- Claude Code 风格工具集,随 `src/claude-code/index.ts` 一次加载:`Read` / `Edit` / `Write` / `Grep` / `Glob` / `Bash` / `TodoWrite` / `AskUserQuestion`,以及共享 LSP 工具(`lsp-rename` / `lsp-inspect` / `lsp-find-definition` / `lsp-find-reference`)。各单工具文件(`src/claude-code/grep.ts` 等)也可独立加载。精确行为(输出格式、分页语义、匹配规则)见 `.agents/skills/claude-code-tools/SKILL.md`。
381
+ 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`。
324
382
 
325
383
  - **`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`
326
384
  - **`Grep` / `Glob`**:ripgrep 实现,行为跟随 Claude Code(`Glob` 带 `--no-ignore` / `--hidden` / `--sort=modified`,与 opencode 风格 `glob` 的差异是各自跟随上游)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.650",
3
+ "version": "0.1.652",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -64,17 +64,9 @@
64
64
  },
65
65
  "pi": {
66
66
  "extensions": [
67
- "src/aft/index.ts",
68
- "src/opencode/index.ts",
69
- "src/claude-code/index.ts",
70
- "src/vision-agent.ts",
67
+ "src/index.ts",
71
68
  "src/session-name.ts",
72
- "src/gh-readonly.ts",
73
- "src/spawn-agent.ts",
74
- "src/system-prompt/index.ts",
75
- "src/talk/index.ts",
76
- "src/web/search.ts",
77
- "src/web/fetch.ts"
69
+ "src/system-prompt/index.ts"
78
70
  ],
79
71
  "skills": [
80
72
  "src/talk/skills",
@@ -95,7 +87,7 @@
95
87
  "node": ">=24.14"
96
88
  },
97
89
  "dependencies": {
98
- "@cortexkit/aft-bridge": "0.57.2",
90
+ "@cortexkit/aft-bridge": "0.58.0",
99
91
  "@mozilla/readability": "^0.6.0",
100
92
  "@parcel/watcher": "^2.6.0",
101
93
  "@vscode/tree-sitter-wasm": "^0.3.1",
@@ -111,6 +103,6 @@
111
103
  "web-tree-sitter": "^0.27.0"
112
104
  },
113
105
  "optionalDependencies": {
114
- "@cortexkit/aft-linux-x64": "0.57.2"
106
+ "@cortexkit/aft-linux-x64": "0.58.0"
115
107
  }
116
108
  }
package/src/aft/index.ts CHANGED
@@ -20,8 +20,10 @@
20
20
  */
21
21
 
22
22
  import { resolveCortexKitConfigPaths } from "@cortexkit/aft-bridge";
23
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
23
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
24
24
 
25
+ import type { ToolBus } from "../lib/tool-bus.js";
26
+ import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
25
27
  import { createAftState, findBinary, resolveSessionId, shutdownAftPool } from "./bridge.js";
26
28
  import { loadAftConfig } from "./config.js";
27
29
  import {
@@ -31,13 +33,16 @@ import {
31
33
  registerZoomTool,
32
34
  } from "./tools.js";
33
35
 
34
- export default function aftReadTools(pi: ExtensionAPI): void {
36
+ /**
37
+ * aft 工具集:入口用它。`registerForSession` 必须在会话启动时调用——二进制
38
+ * 探测、semantic_search 的提示、bridge 状态的创建都在里面;找不到二进制就不
39
+ * 注册任何工具(避免模型看到只会抛 "not initialized" 的死工具)。
40
+ */
41
+ export function createAftTools(pi: ExtensionAPI): {
42
+ registerForSession(bus: ToolBus, ctx: ExtensionContext): Promise<void>;
43
+ } {
35
44
  const cwd = process.cwd();
36
45
  const cfg = loadAftConfig(resolveCortexKitConfigPaths(cwd).userConfigPath);
37
- if (!cfg.enabled) {
38
- return;
39
- }
40
-
41
46
  // bridge 状态跟 session 生命周期走,作用域就是本工厂闭包,不落到模块级。
42
47
  let state: Awaited<ReturnType<typeof createAftState>> | null = null;
43
48
 
@@ -52,9 +57,11 @@ export default function aftReadTools(pi: ExtensionAPI): void {
52
57
 
53
58
  const toolCtx = { cwd, getState };
54
59
 
55
- // 工具注册延迟到 session_start:先确认二进制可用再决定注册面。pi 允许在
56
- // session_start 里 registerTool(工具表按 name 覆盖,跨 session 重复注册幂等)。
57
- pi.on("session_start", async (_event, ctx) => {
60
+ async function registerForSession(bus: ToolBus, ctx: ExtensionContext): Promise<void> {
61
+ if (!cfg.enabled) {
62
+ return;
63
+ }
64
+
58
65
  const binaryPath = await findBinary();
59
66
  if (!binaryPath) {
60
67
  ctx.ui.notify(
@@ -66,23 +73,26 @@ export default function aftReadTools(pi: ExtensionAPI): void {
66
73
  return;
67
74
  }
68
75
 
69
- registerOutlineTool(pi, toolCtx);
70
- registerZoomTool(pi, toolCtx);
71
- registerCallgraphTool(pi, toolCtx);
72
- if (cfg.semanticSearch) {
73
- if (cfg.semanticRemote) {
74
- registerSearchTool(pi, toolCtx);
75
- } else {
76
- // 只开了开关、没配外部 embedding 后端:与其静默不注册,不如说明缺什么。
77
- ctx.ui.notify(
78
- "aft_search is not registered: semantic_search needs an external embedding backend (aft.jsonc semantic.backend = openai_compatible | ollama, plus base_url). The local ONNX fastembed default is not used here.",
79
- "warning",
80
- );
81
- }
76
+ // 只开了 semantic_search 开关、没配外部 embedding 后端:与其静默不注册,
77
+ // 不如说明缺什么。
78
+ if (cfg.semanticSearch && !cfg.semanticRemote) {
79
+ ctx.ui.notify(
80
+ "aft_search is not registered: semantic_search needs an external embedding backend (aft.jsonc semantic.backend = openai_compatible | ollama, plus base_url). The local ONNX fastembed default is not used here.",
81
+ "warning",
82
+ );
83
+ }
84
+
85
+ // 先注册工具再建 bridge 状态:状态构建失败(如 bridge 起不来)时工具仍然
86
+ // 可见,错误按原样浮出给用户,而不是变成「工具消失」。
87
+ registerOutlineTool(bus, toolCtx);
88
+ registerZoomTool(bus, toolCtx);
89
+ registerCallgraphTool(bus, toolCtx);
90
+ if (cfg.semanticSearch && cfg.semanticRemote) {
91
+ registerSearchTool(bus, toolCtx);
82
92
  }
83
93
 
84
94
  state = await createAftState(cwd, resolveSessionId(ctx), binaryPath, cfg.semanticRemote);
85
- });
95
+ }
86
96
 
87
97
  // 释放当前 session 的 bridge 状态。session_shutdown 是 pi 的正常生命周期;
88
98
  // beforeExit 兜底进程自然退出(不能注册 SIGINT/SIGTERM——那会吞掉 pi 主进程
@@ -105,4 +115,11 @@ export default function aftReadTools(pi: ExtensionAPI): void {
105
115
  pi.on("session_shutdown", async () => {
106
116
  await shutdown();
107
117
  });
118
+
119
+ return { registerForSession };
120
+ }
121
+
122
+ export default function aftReadTools(pi: ExtensionAPI): void {
123
+ const tools = createAftTools(pi);
124
+ registerToolsOnSessionStart(pi, (bus, ctx) => tools.registerForSession(bus, ctx));
108
125
  }
package/src/aft/tools.ts CHANGED
@@ -19,12 +19,13 @@ import {
19
19
  PLAIN_CALLGRAPH_THEME,
20
20
  type StatusSnapshot,
21
21
  } from "@cortexkit/aft-bridge";
22
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
22
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
23
23
  import { type Static, Type } from "typebox";
24
24
  import { Value } from "typebox/value";
25
25
 
26
26
  import { formatDisplayPath, formatSubtitlePath, resolvePathArg } from "../lib/path.js";
27
27
  import { type ToolPendant } from "../lib/pendant.js";
28
+ import type { ToolBus } from "../lib/tool-bus.js";
28
29
  import {
29
30
  type AftState,
30
31
  callAftTool,
@@ -125,8 +126,8 @@ const OutlineParams = Type.Object(
125
126
  { additionalProperties: false },
126
127
  );
127
128
 
128
- export function registerOutlineTool(pi: ExtensionAPI, ctx: AftToolContext): void {
129
- pi.registerTool({
129
+ export function registerOutlineTool(bus: ToolBus, ctx: AftToolContext): void {
130
+ bus.register({
130
131
  name: "aft_outline",
131
132
  label: "aft_outline",
132
133
  description: [
@@ -209,8 +210,8 @@ const ZoomParams = Type.Object(
209
210
  { additionalProperties: false },
210
211
  );
211
212
 
212
- export function registerZoomTool(pi: ExtensionAPI, ctx: AftToolContext): void {
213
- pi.registerTool({
213
+ export function registerZoomTool(bus: ToolBus, ctx: AftToolContext): void {
214
+ bus.register({
214
215
  name: "aft_zoom",
215
216
  label: "aft_zoom",
216
217
  description: [
@@ -361,8 +362,8 @@ export async function callCallgraphWithBuildRetry(
361
362
  }
362
363
  }
363
364
 
364
- export function registerCallgraphTool(pi: ExtensionAPI, ctx: AftToolContext): void {
365
- pi.registerTool({
365
+ export function registerCallgraphTool(bus: ToolBus, ctx: AftToolContext): void {
366
+ bus.register({
366
367
  name: "aft_callgraph",
367
368
  label: "aft_callgraph",
368
369
  description: [
@@ -610,8 +611,8 @@ const SearchParams = Type.Object(
610
611
  { additionalProperties: false },
611
612
  );
612
613
 
613
- export function registerSearchTool(pi: ExtensionAPI, ctx: AftToolContext): void {
614
- pi.registerTool({
614
+ export function registerSearchTool(bus: ToolBus, ctx: AftToolContext): void {
615
+ bus.register({
615
616
  name: "aft_search",
616
617
  label: "aft_search",
617
618
  description: [
@@ -25,11 +25,18 @@ import {
25
25
  } from "../lib/file-reads.js";
26
26
  import { appendLspDiagnosticText, type DiagnosticReport } from "../lib/lsp/diagnostic.js";
27
27
  import { registerLspInspectTools } from "../lib/lsp/inspect-tool.js";
28
- import { createLspManager, type LspService, type LspServiceOptions } from "../lib/lsp/lsp.js";
28
+ import {
29
+ createLspManager,
30
+ type LspManager,
31
+ type LspService,
32
+ type LspServiceOptions,
33
+ } from "../lib/lsp/lsp.js";
29
34
  import { registerLspRenameTool } from "../lib/lsp/rename-tool.js";
30
35
  import { formatSubtitlePath } from "../lib/path.js";
31
36
  import type { ToolPendant } from "../lib/pendant.ts";
32
37
  import { createRequestPolicy, type RequestPolicy } from "../lib/request-policy.js";
38
+ import type { ToolBus } from "../lib/tool-bus.js";
39
+ import { createToolRegistration } from "../lib/tool-registration.js";
33
40
  import { guardWriteAccess } from "../lib/write-guard.js";
34
41
  import { didYouMean, resolveToolFilePath } from "./common.js";
35
42
  import { applyExactEdit } from "./edit-match.js";
@@ -173,12 +180,12 @@ export function formatReadOutput(
173
180
  }
174
181
 
175
182
  export function registerFileTools(
176
- pi: ExtensionAPI,
183
+ bus: ToolBus,
177
184
  state: ReadsState,
178
185
  getService: () => LspService,
179
186
  policy: RequestPolicy,
180
187
  ): void {
181
- pi.registerTool({
188
+ bus.register({
182
189
  name: "Read",
183
190
  label: "Read",
184
191
  description: [
@@ -312,7 +319,7 @@ export function registerFileTools(
312
319
  },
313
320
  });
314
321
 
315
- pi.registerTool({
322
+ bus.register({
316
323
  name: "Edit",
317
324
  label: "Edit",
318
325
  description: [
@@ -493,7 +500,7 @@ export function registerFileTools(
493
500
  },
494
501
  });
495
502
 
496
- pi.registerTool({
503
+ bus.register({
497
504
  name: "Write",
498
505
  label: "Write",
499
506
  description: [
@@ -591,72 +598,110 @@ export function registerFileTools(
591
598
  /** 会更新 reads state 并随 details 持久化快照的工具名。 */
592
599
  const FILE_TOOL_NAMES = new Set(["Read", "Edit", "Write", "lsp-rename"]);
593
600
 
594
- function restoreFileReads(
595
- state: ReadsState,
596
- sessionManager: ExtensionContext["sessionManager"],
597
- ): void {
598
- restoreReads(state, sessionManager, FILE_TOOL_NAMES);
599
- }
600
-
601
601
  export interface ClaudeCodeFileToolOptions extends LspServiceOptions {
602
602
  /** 与 bash runtime 共享的非沙盒请求策略;独立入口不传,自建一份。 */
603
603
  policy?: RequestPolicy;
604
+ /** 聚合入口注入的总线(与同入口其他模块共享);独立入口不传,自建一份。 */
605
+ bus?: ToolBus;
606
+ /**
607
+ * 聚合入口注入的共享 LSP manager(两套文件工具集在同一个入口里共享一份,
608
+ * 否则 /lsp-* 命令与 LSP 服务会重复注册);独立入口不传,自建一份。
609
+ */
610
+ manager?: LspManager;
604
611
  }
605
612
 
606
613
  /**
607
- * 独立扩展入口:files.ts 可单独经 `-e claude-code/files.ts` 加载(spawn-agent
608
- * 子代理把 Read/Edit/Write 工具名映射到本文件),无需经 index.ts。reads
609
- * state 归本文件所有:扩展实例内创建,并随 session 事件从历史分支恢复,
610
- * 与主进程 index.ts 聚合加载时的行为一致。
614
+ * 一套文件工具集:reads state 归实例所有,LSP 与请求策略可以由入口注入共享。
615
+ * 工具用 `register`、LSP 专属工具用 `onLspEnabled`(在 LSP 服务就绪时由
616
+ * manager 触发),已读记账恢复用 `restoreReads`。
611
617
  */
612
- export default function claudeCodeFileTools(
618
+ export interface FileToolset {
619
+ register(bus: ToolBus): void;
620
+ onLspEnabled(bus: ToolBus, service: LspService): void;
621
+ restoreReads(ctx: ExtensionContext): void;
622
+ }
623
+
624
+ /**
625
+ * 创建文件工具集。入口在会话启动、知道本会话模型之后再调用 `register`;
626
+ * `restoreReads` 与 `onLspEnabled` 分别由会话事件与 LSP manager 触发。
627
+ *
628
+ * 独立扩展入口见文件末尾的默认导出:spawn-agent 子代理按工具名把
629
+ * Read/Edit/Write 映射到本文件并经 `-e` 加载,形态与聚合入口一致。
630
+ */
631
+ export function createClaudeCodeFileTools(
613
632
  pi: ExtensionAPI,
614
633
  options?: ClaudeCodeFileToolOptions,
615
- ): void {
634
+ ): FileToolset {
616
635
  const state = createReadsState();
617
- // 聚合入口(index.ts)注入与 bash runtime 共享的那一份;独立入口(spawn-agent
618
- // 按工具名 `-e` 加载本文件)自建一份,靠 pi.events 跟随同一开关。
636
+ // 聚合入口注入与 bash runtime 共享的那一份;独立入口自建并订阅 pi.events。
619
637
  const policy = options?.policy ?? createRequestPolicy(pi.events);
638
+ // 聚合入口注入自己的总线(与同入口其他模块共享);独立入口自建一份。
639
+ const bus = options?.bus ?? createToolRegistration(pi).bus;
620
640
 
621
- // LSP 专属工具(lsp-rename / inspect 族)仅在 lsp.json 存在 enabled 服务器时
622
- // 注册(session_start 校验后);本工具集跟踪 read-before-write 状态,rename
623
- // 落盘的文件要标记为已读并随 details 持久化(restoreFileReads 依赖 details.reads)。
624
- const manager = createLspManager(
625
- pi,
626
- {
627
- onEnabled: (pi, service) => {
628
- registerLspRenameTool(pi, service, {
629
- policy,
630
- recordReads: (applied) =>
631
- recordReads(
632
- state,
633
- applied.map((fileEdit) => ({
634
- path: fileEdit.path,
635
- snapshot: snapshotOf(fileEdit.newText),
636
- })),
637
- ),
638
- });
639
- registerLspInspectTools(pi, service);
640
- },
641
+ const toolset: FileToolset = {
642
+ register(currentBus) {
643
+ registerFileTools(currentBus, state, () => manager.mustLazyGetService(), policy);
641
644
  },
642
- options,
643
- );
644
645
 
645
- // 扩展实例在进程启动 / /reload / /new / /resume / /fork 时重建,内存里的
646
- // 已读记账随之丢失。这里从当前分支的历史工具结果里恢复:digest 是当时的值,
647
- // 若文件在此期间被外部修改,Edit/Write 时的指纹对比仍会要求重新 Read,
648
- // 防呆语义不因重建而弱化。
646
+ // LSP 专属工具(lsp-rename / inspect 族)仅在 lsp.json 有 enabled 服务器时
647
+ // 注册;本工具集跟踪 read-before-write 状态,rename 落盘的文件要标记为已读
648
+ // 并随 details 持久化(restoreReads 依赖 details.reads)。
649
+ onLspEnabled(currentBus, service) {
650
+ registerLspRenameTool(currentBus, service, {
651
+ policy,
652
+ recordReads: (applied) =>
653
+ recordReads(
654
+ state,
655
+ applied.map((fileEdit) => ({
656
+ path: fileEdit.path,
657
+ snapshot: snapshotOf(fileEdit.newText),
658
+ })),
659
+ ),
660
+ });
661
+ registerLspInspectTools(currentBus, service);
662
+ },
663
+
664
+ // 扩展实例在进程启动 / /reload / /new / /resume / /fork 时重建,内存里的
665
+ // 已读记账随之丢失。这里从当前分支的历史工具结果里恢复:digest 是当时的值,
666
+ // 若文件在此期间被外部修改,Edit/Write 时的指纹对比仍会要求重新 Read,
667
+ // 防呆语义不因重建而弱化。
668
+ restoreReads(ctx) {
669
+ restoreReads(state, ctx.sessionManager, FILE_TOOL_NAMES);
670
+ },
671
+ };
672
+
673
+ // 独立入口没有注入 manager 时自建一份(入口共享的那份由入口创建)。
674
+ const manager =
675
+ options?.manager ??
676
+ createLspManager(
677
+ pi,
678
+ { onEnabled: (_pi, service) => toolset.onLspEnabled(bus, service) },
679
+ options,
680
+ );
681
+
682
+ return toolset;
683
+ }
684
+
685
+ /**
686
+ * 独立扩展入口:files.ts 可单独经 `-e claude-code/files.ts` 加载(spawn-agent
687
+ * 子代理把 Read/Edit/Write 工具名映射到本文件),无需经 index.ts。
688
+ */
689
+ export default function claudeCodeFileTools(
690
+ pi: ExtensionAPI,
691
+ options?: ClaudeCodeFileToolOptions,
692
+ ): void {
693
+ const registration = createToolRegistration(pi);
694
+ const toolset = createClaudeCodeFileTools(pi, { ...options, bus: registration.bus });
695
+
649
696
  pi.on("session_start", (_event, ctx) => {
650
- restoreFileReads(state, ctx.sessionManager);
697
+ toolset.restoreReads(ctx);
651
698
  });
652
-
653
699
  // rewind / 树内跳转走 navigateTree → branch(),只发 session_tree 不发
654
- // session_start,扩展实例也不重建。这里同样重放当前分支,丢弃被抛弃分支
655
- // 的记账,避免 state 与当前分支脱节。
700
+ // session_start,扩展实例也不重建。这里同样重放当前分支。
656
701
  pi.on("session_tree", (_event, ctx) => {
657
- restoreFileReads(state, ctx.sessionManager);
702
+ toolset.restoreReads(ctx);
703
+ });
704
+ registration.onSessionStart(() => {
705
+ toolset.register(registration.bus);
658
706
  });
659
-
660
- // 文件工具无条件注册;service 惰性获取,disabled 时为 no-op。
661
- registerFileTools(pi, state, () => manager.mustLazyGetService(), policy);
662
707
  }
@@ -15,6 +15,8 @@ import { promisify } from "node:util";
15
15
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
16
  import { Type } from "typebox";
17
17
 
18
+ import type { ToolBus } from "../lib/tool-bus.js";
19
+ import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
18
20
  import { searchRoot, suggestPathUnderCwd, toRelativePath } from "./common.js";
19
21
 
20
22
  const GLOB_RESULT_LIMIT = 100;
@@ -110,8 +112,8 @@ export async function globFiles(
110
112
  };
111
113
  }
112
114
 
113
- export function registerGlobTool(pi: ExtensionAPI): void {
114
- pi.registerTool({
115
+ export function registerGlobTool(bus: ToolBus): void {
116
+ bus.register({
115
117
  name: "Glob",
116
118
  label: "Glob",
117
119
  description: [
@@ -167,6 +169,7 @@ export function registerGlobTool(pi: ExtensionAPI): void {
167
169
  }
168
170
 
169
171
  // 独立扩展入口:spawn-agent 子代理声明 `Glob` 工具时按本文件 `-e` 加载,
170
- // 无需经 index.ts / search.ts 聚合。registerGlobTool 本身就是 (pi) => void,
171
- // 可直接作为扩展 factory。
172
- export default registerGlobTool;
172
+ // 无需经 index.ts / search.ts 聚合。
173
+ export default function claudeCodeGlobTool(pi: ExtensionAPI): void {
174
+ registerToolsOnSessionStart(pi, registerGlobTool);
175
+ }
@@ -17,6 +17,8 @@ import { StringEnum } from "@earendil-works/pi-ai";
17
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
18
  import { type Static, Type } from "typebox";
19
19
 
20
+ import type { ToolBus } from "../lib/tool-bus.js";
21
+ import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
20
22
  import { searchRoot, suggestPathUnderCwd, toRelativePath } from "./common.js";
21
23
 
22
24
  const GREP_OUTPUT_MODES = ["content", "files_with_matches", "count"] as const;
@@ -258,8 +260,8 @@ function relativizeCountLine(line: string, cwd: string): string {
258
260
  return line;
259
261
  }
260
262
 
261
- export function registerGrepTool(pi: ExtensionAPI): void {
262
- pi.registerTool({
263
+ export function registerGrepTool(bus: ToolBus, pi: ExtensionAPI): void {
264
+ bus.register({
263
265
  name: "Grep",
264
266
  label: "Grep",
265
267
  description: [
@@ -374,6 +376,7 @@ export function registerGrepTool(pi: ExtensionAPI): void {
374
376
  }
375
377
 
376
378
  // 独立扩展入口:spawn-agent 子代理声明 `Grep` 工具时按本文件 `-e` 加载,
377
- // 无需经 index.ts / search.ts 聚合。registerGrepTool 本身就是 (pi) => void,
378
- // 可直接作为扩展 factory。
379
- export default registerGrepTool;
379
+ // 无需经 index.ts / search.ts 聚合。
380
+ export default function claudeCodeGrepTool(pi: ExtensionAPI): void {
381
+ registerToolsOnSessionStart(pi, (bus) => registerGrepTool(bus, pi));
382
+ }