@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.
- package/README.md +67 -9
- package/package.json +5 -13
- package/src/aft/index.ts +40 -23
- package/src/aft/tools.ts +10 -9
- package/src/claude-code/files.ts +99 -54
- package/src/claude-code/glob.ts +8 -5
- package/src/claude-code/grep.ts +8 -5
- package/src/claude-code/index.ts +56 -10
- package/src/claude-code/search.ts +4 -3
- package/src/claude-code/session-tools.ts +4 -3
- package/src/claude-code/shell.ts +3 -8
- package/src/gh/index.ts +48 -25
- package/src/gh/tools/download-release-assets.ts +3 -3
- package/src/gh/tools/get-workflow-jobs.ts +3 -3
- package/src/gh/tools/list-issues.ts +3 -3
- package/src/gh/tools/list-prs.ts +3 -3
- package/src/gh/tools/list-releases.ts +3 -3
- package/src/gh/tools/list-workflow-runs.ts +3 -3
- package/src/gh/tools/read-ci-logs.ts +3 -4
- package/src/gh/tools/read-issue-comments.ts +3 -3
- package/src/gh/tools/read-issue.ts +3 -3
- package/src/gh/tools/read-pr-comments.ts +3 -3
- package/src/gh/tools/read-pr-diff.ts +3 -3
- package/src/gh/tools/read-pr-status.ts +3 -3
- package/src/gh/tools/read-pr.ts +3 -3
- package/src/gh/tools/read-release.ts +3 -3
- package/src/gh/tools/read-repo.ts +3 -3
- package/src/gh/tools/wait-commit-checks.ts +3 -3
- package/src/gh/tools/wait-pr-checks.ts +3 -3
- package/src/gh/tools/watch-run.ts +3 -3
- package/src/index.ts +120 -0
- package/src/lib/lsp/inspect-tool.ts +5 -5
- package/src/lib/lsp/rename-tool.ts +4 -7
- package/src/lib/tool-bus.ts +126 -0
- package/src/lib/tool-registration.ts +83 -0
- package/src/lib/tool-services.ts +65 -0
- package/src/lib/tool-units.ts +102 -0
- package/src/lib/tools-config.ts +277 -0
- package/src/opencode/bash.ts +7 -2
- package/src/opencode/files.ts +90 -43
- package/src/opencode/glob.ts +7 -1
- package/src/opencode/grep.ts +7 -1
- package/src/opencode/index.ts +66 -21
- package/src/opencode/question.ts +8 -2
- package/src/opencode/todo.ts +8 -2
- package/src/orphan-worker-cleaner.ts +38 -0
- package/src/spawn-agent.ts +160 -175
- package/src/talk/index.ts +92 -66
- package/src/vision-agent.ts +31 -7
- package/src/web/fetch.ts +89 -72
- package/src/web/search.ts +14 -2
- package/src/gh-readonly.ts +0 -10
package/src/index.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本仓库的统一扩展入口:所有工具都在这里注册。
|
|
3
|
+
*
|
|
4
|
+
* 为什么是单一入口:pi 给每个扩展入口单独的模块图,模块级状态不跨入口共享。
|
|
5
|
+
* 工具集中在一处之后,同一入口内的模块(codemode 等)才能看到并直接调用
|
|
6
|
+
* 全部工具;`personalExtensions` 配置(`fileIo` / `fileIoByModel` /
|
|
7
|
+
* `disabledTools` / `enabledTools`)也才有唯一的求值点。
|
|
8
|
+
*
|
|
9
|
+
* 注册时机:pi 在每次会话启动(启动、/new、resume、fork、/reload)时重建扩展,
|
|
10
|
+
* 所以在 `session_start` 里注册即「每个会话按自己的模型判定一次」。模型在扩展
|
|
11
|
+
* 加载期读不到(`ctx.model` 只在事件里),这是注册放在 session_start 的原因。
|
|
12
|
+
*
|
|
13
|
+
* 模块划分:两套文件 IO 工具集(claude-code / opencode)由 `ToolUnit` 表描述,
|
|
14
|
+
* 其余工具模块暴露 `createXxx(pi)`;各模块的默认导出保留给「按路径单独加载」
|
|
15
|
+
* (手工 `-e`),本入口直接调用 createXxx,因此一个实例里只有一份模块状态。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
|
|
20
|
+
import { createAftTools } from "./aft/index.js";
|
|
21
|
+
import { createClaudeCodeFileTools } from "./claude-code/files.js";
|
|
22
|
+
import { createGithubTools } from "./gh/index.js";
|
|
23
|
+
import type { ToolBus } from "./lib/tool-bus.js";
|
|
24
|
+
import { createToolRegistration } from "./lib/tool-registration.js";
|
|
25
|
+
import { createToolServices } from "./lib/tool-services.js";
|
|
26
|
+
import { TOOL_UNITS, type ToolUnitDeps } from "./lib/tool-units.js";
|
|
27
|
+
import { resolveFileIoToolset } from "./lib/tools-config.js";
|
|
28
|
+
import { createOpencodeFileTools } from "./opencode/files.js";
|
|
29
|
+
import { createSpawnAgentTool } from "./spawn-agent.js";
|
|
30
|
+
import { createTalkTools } from "./talk/index.js";
|
|
31
|
+
import { createVisionTools } from "./vision-agent.js";
|
|
32
|
+
import { createWebFetch } from "./web/fetch.js";
|
|
33
|
+
import { createWebSearchTool } from "./web/search.js";
|
|
34
|
+
|
|
35
|
+
/** 只做工具注册、以及自己那些与会话无关的钩子的模块。 */
|
|
36
|
+
interface ToolModule {
|
|
37
|
+
register(bus: ToolBus): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function personalExtensions(pi: ExtensionAPI): void {
|
|
41
|
+
const registration = createToolRegistration(pi);
|
|
42
|
+
// 共享服务要在 registration 之后创建:LSP manager 的 session_start handler 得
|
|
43
|
+
// 排在「记录本会话模型」的 handler 之后。
|
|
44
|
+
const services = createToolServices(pi);
|
|
45
|
+
|
|
46
|
+
// 与文件工具集无关的模块(各自在 create 里注册自己的会话钩子与提示)。
|
|
47
|
+
const modules: { name: string; module: ToolModule }[] = [];
|
|
48
|
+
const addModule = (name: string, module: ToolModule | undefined): void => {
|
|
49
|
+
if (module) {
|
|
50
|
+
modules.push({ name, module });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
addModule("github", createGithubTools());
|
|
54
|
+
addModule("vision", createVisionTools());
|
|
55
|
+
addModule("talk", createTalkTools(pi));
|
|
56
|
+
addModule("spawn-agent", createSpawnAgentTool());
|
|
57
|
+
addModule("web_search", createWebSearchTool());
|
|
58
|
+
addModule("web_fetch", createWebFetch(pi));
|
|
59
|
+
const aft = createAftTools(pi);
|
|
60
|
+
|
|
61
|
+
// 注册期的失败攒起来,在会话启动时一次性上报(那里才有 UI)。
|
|
62
|
+
const warnings: string[] = [];
|
|
63
|
+
|
|
64
|
+
/** 单个模块/单元注册失败不影响其余:记下失败,继续注册。 */
|
|
65
|
+
function runModule<T>(name: string, run: () => T): T | undefined {
|
|
66
|
+
try {
|
|
67
|
+
return run();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
warnings.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
registration.onSessionStart(async (bus, ctx) => {
|
|
75
|
+
// 配置解析期的警告是稳定的,注册期的失败每个会话重新收集(避免跨会话重复上报)。
|
|
76
|
+
warnings.length = 0;
|
|
77
|
+
warnings.push(...registration.config.warnings);
|
|
78
|
+
|
|
79
|
+
// 文件工具集跟着本会话的模型走(会话内切换模型不重建扩展,因此不重判)。
|
|
80
|
+
const kind = resolveFileIoToolset(registration.config, ctx.model);
|
|
81
|
+
const fileToolset =
|
|
82
|
+
kind === "opencode"
|
|
83
|
+
? createOpencodeFileTools(pi, {
|
|
84
|
+
policy: services.policy,
|
|
85
|
+
bus,
|
|
86
|
+
manager: services.manager,
|
|
87
|
+
})
|
|
88
|
+
: createClaudeCodeFileTools(pi, {
|
|
89
|
+
policy: services.policy,
|
|
90
|
+
bus,
|
|
91
|
+
manager: services.manager,
|
|
92
|
+
});
|
|
93
|
+
services.setLspEnabledHandler((service) => fileToolset.onLspEnabled(bus, service));
|
|
94
|
+
runModule(kind, () => fileToolset.restoreReads(ctx));
|
|
95
|
+
|
|
96
|
+
const deps: ToolUnitDeps = {
|
|
97
|
+
pi,
|
|
98
|
+
bus,
|
|
99
|
+
policy: services.policy,
|
|
100
|
+
runtime: services.runtime,
|
|
101
|
+
fileToolset,
|
|
102
|
+
};
|
|
103
|
+
for (const unit of TOOL_UNITS[kind]) {
|
|
104
|
+
runModule(unit.tools.join("/"), () => unit.register(deps));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// aft 的二进制探测、bridge 状态与工具注册绑在一起(找不到二进制就不注册)。
|
|
108
|
+
await runModule("aft", () => aft.registerForSession(bus, ctx));
|
|
109
|
+
for (const { name, module } of modules) {
|
|
110
|
+
runModule(name, () => module.register(bus));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const warning of warnings) {
|
|
114
|
+
ctx.ui.notify(warning, "warning");
|
|
115
|
+
}
|
|
116
|
+
for (const { field, pattern } of registration.unmatchedPatterns()) {
|
|
117
|
+
ctx.ui.notify(`personalExtensions.${field}: pattern "${pattern}" matches no tool`, "warning");
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -14,11 +14,11 @@ import { readFileSync } from "node:fs";
|
|
|
14
14
|
import { readFile, stat } from "node:fs/promises";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
|
|
17
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
17
|
import { Type } from "typebox";
|
|
19
18
|
import type { Hover } from "vscode-languageserver-types";
|
|
20
19
|
|
|
21
20
|
import { resolvePathArg } from "../path.js";
|
|
21
|
+
import type { ToolBus } from "../tool-bus.js";
|
|
22
22
|
import type { InspectLocation } from "./client.js";
|
|
23
23
|
import type { LspService } from "./lsp.js";
|
|
24
24
|
import { type LspPosition, symbolCandidates } from "./rename.js";
|
|
@@ -255,8 +255,8 @@ async function probeSymbolCandidates(options: {
|
|
|
255
255
|
return first.output;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
export function registerLspInspectTools(
|
|
259
|
-
|
|
258
|
+
export function registerLspInspectTools(bus: ToolBus, service: LspService): void {
|
|
259
|
+
bus.register({
|
|
260
260
|
name: "lsp-find-definition",
|
|
261
261
|
label: "Lsp Find Definition",
|
|
262
262
|
description:
|
|
@@ -299,7 +299,7 @@ export function registerLspInspectTools(pi: ExtensionAPI, service: LspService):
|
|
|
299
299
|
},
|
|
300
300
|
});
|
|
301
301
|
|
|
302
|
-
|
|
302
|
+
bus.register({
|
|
303
303
|
name: "lsp-find-reference",
|
|
304
304
|
label: "Lsp Find Reference",
|
|
305
305
|
description:
|
|
@@ -342,7 +342,7 @@ export function registerLspInspectTools(pi: ExtensionAPI, service: LspService):
|
|
|
342
342
|
},
|
|
343
343
|
});
|
|
344
344
|
|
|
345
|
-
|
|
345
|
+
bus.register({
|
|
346
346
|
name: "lsp-inspect",
|
|
347
347
|
label: "Lsp Inspect",
|
|
348
348
|
description:
|
|
@@ -14,16 +14,13 @@ import { readFileSync } from "node:fs";
|
|
|
14
14
|
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
type ExtensionAPI,
|
|
19
|
-
generateDiffString,
|
|
20
|
-
withFileMutationQueue,
|
|
21
|
-
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { generateDiffString, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
22
18
|
import { Type } from "typebox";
|
|
23
19
|
|
|
24
20
|
import { resolvePathArg } from "../path.js";
|
|
25
21
|
import type { ToolPendant } from "../pendant.ts";
|
|
26
22
|
import type { RequestPolicy } from "../request-policy.js";
|
|
23
|
+
import type { ToolBus } from "../tool-bus.js";
|
|
27
24
|
import { guardWriteAccess } from "../write-guard.js";
|
|
28
25
|
import { RenameNotPossibleError } from "./client.js";
|
|
29
26
|
import type { LspService } from "./lsp.js";
|
|
@@ -51,11 +48,11 @@ const LSP_RENAME_PROMPT = readFileSync(
|
|
|
51
48
|
).trim();
|
|
52
49
|
|
|
53
50
|
export function registerLspRenameTool(
|
|
54
|
-
|
|
51
|
+
bus: ToolBus,
|
|
55
52
|
service: LspService,
|
|
56
53
|
options: LspRenameOptions,
|
|
57
54
|
): void {
|
|
58
|
-
|
|
55
|
+
bus.register({
|
|
59
56
|
name: "lsp-rename",
|
|
60
57
|
label: "Lsp Rename",
|
|
61
58
|
description: "Rename a code symbol and update all references across the workspace via LSP",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具总线:本仓库全部工具的唯一注册通道,也是同入口模块(codemode 等)直接调用
|
|
3
|
+
* 工具的通道。
|
|
4
|
+
*
|
|
5
|
+
* - `register`:按配置过滤(被禁用时跳过并返回 false)后交给 `pi.registerTool`,
|
|
6
|
+
* 并保留通过的定义供 `list` / `get` 查询。
|
|
7
|
+
* - `executeTool`:先用工具自身的参数 schema 校验参数,再调用定义的 `execute`;
|
|
8
|
+
* 参数校验失败与工具抛出的异常都归一化成错误结果返回,不向调用方抛异常。
|
|
9
|
+
*
|
|
10
|
+
* 状态由闭包持有(工厂 + 闭包),不使用模块级可变状态。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
|
|
14
|
+
import type {
|
|
15
|
+
ExtensionAPI,
|
|
16
|
+
ExtensionContext,
|
|
17
|
+
ToolDefinition,
|
|
18
|
+
} from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import type { Static, TSchema } from "typebox";
|
|
20
|
+
|
|
21
|
+
import { parseWithSchema } from "./parse-with-schema.js";
|
|
22
|
+
|
|
23
|
+
/** 一次工具执行的结果:`isError` 标出失败(工具不存在、参数不合法、工具抛错)。 */
|
|
24
|
+
export type ToolExecutionResult = AgentToolResult<unknown> & { isError: boolean };
|
|
25
|
+
|
|
26
|
+
export interface ExecuteToolOptions {
|
|
27
|
+
/** 工具 `execute` 需要的宿主上下文:调用方把自己拿到的那个 ctx 传进来。 */
|
|
28
|
+
ctx: ExtensionContext;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
onUpdate?: AgentToolUpdateCallback<unknown>;
|
|
31
|
+
/** 日志与临时文件命名用的调用 id;缺省由总线生成。 */
|
|
32
|
+
toolCallId?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ToolBus {
|
|
36
|
+
/** 注册一个工具;被配置禁用时跳过并返回 false。 */
|
|
37
|
+
register<TParams extends TSchema, TDetails>(
|
|
38
|
+
definition: ToolDefinition<TParams, TDetails, unknown>,
|
|
39
|
+
): boolean;
|
|
40
|
+
/** 本次实际注册的工具定义。 */
|
|
41
|
+
list(): readonly ToolDefinition[];
|
|
42
|
+
/** 所有尝试注册过的工具名(含被禁用而跳过的),用于校验配置里的模式是否有效。 */
|
|
43
|
+
declaredNames(): readonly string[];
|
|
44
|
+
get(name: string): ToolDefinition | undefined;
|
|
45
|
+
/** 按名执行一个已注册工具;任何失败都以 `isError` 结果返回,不抛异常。 */
|
|
46
|
+
executeTool(
|
|
47
|
+
name: string,
|
|
48
|
+
args: unknown,
|
|
49
|
+
options: ExecuteToolOptions,
|
|
50
|
+
): Promise<ToolExecutionResult>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ToolBusOptions {
|
|
54
|
+
/** 返回 true 表示该工具被禁用、不注册。 */
|
|
55
|
+
isDisabled?: (name: string) => boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type AnyToolDefinition = ToolDefinition<TSchema, unknown, unknown>;
|
|
59
|
+
|
|
60
|
+
function errorResult(text: string): ToolExecutionResult {
|
|
61
|
+
return { content: [{ type: "text", text }], details: { error: text }, isError: true };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function errorMessage(error: unknown): string {
|
|
65
|
+
return error instanceof Error ? error.message : String(error);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createToolBus(pi: ExtensionAPI, options: ToolBusOptions = {}): ToolBus {
|
|
69
|
+
const registered = new Map<string, AnyToolDefinition>();
|
|
70
|
+
const declared = new Set<string>();
|
|
71
|
+
let callCounter = 0;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
register(definition) {
|
|
75
|
+
declared.add(definition.name);
|
|
76
|
+
if (options.isDisabled?.(definition.name) === true) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const stored = definition as unknown as AnyToolDefinition;
|
|
80
|
+
pi.registerTool(stored);
|
|
81
|
+
registered.set(stored.name, stored);
|
|
82
|
+
return true;
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
list() {
|
|
86
|
+
return [...registered.values()];
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
declaredNames() {
|
|
90
|
+
return [...declared];
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
get(name) {
|
|
94
|
+
return registered.get(name);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async executeTool(name, args, options) {
|
|
98
|
+
const definition = registered.get(name);
|
|
99
|
+
if (!definition) {
|
|
100
|
+
return errorResult(`Tool "${name}" is not available.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const toolCallId = options.toolCallId ?? `tool-bus-${++callCounter}`;
|
|
104
|
+
let params: Static<TSchema>;
|
|
105
|
+
try {
|
|
106
|
+
const prepared = definition.prepareArguments ? definition.prepareArguments(args) : args;
|
|
107
|
+
params = parseWithSchema(definition.parameters, prepared);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return errorResult(`Invalid arguments for tool "${name}": ${errorMessage(error)}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const result = await definition.execute(
|
|
114
|
+
toolCallId,
|
|
115
|
+
params,
|
|
116
|
+
options.signal,
|
|
117
|
+
options.onUpdate,
|
|
118
|
+
options.ctx,
|
|
119
|
+
);
|
|
120
|
+
return { ...result, isError: false };
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return errorResult(`Tool "${name}" failed: ${errorMessage(error)}`);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具注册上下文:把「配置 + 当前会话模型 → 总线 → 注册」这条链路收在一处。
|
|
3
|
+
*
|
|
4
|
+
* 约定:模块里的 registerXxx(bus, ...) 只收发工具的总线,需要宿主 API(pi.exec、
|
|
5
|
+
* pi.on、runtime.setup 等)时把 `pi` 作为后续参数显式传入;扩展入口/默认导出
|
|
6
|
+
* 用本文件的 createToolRegistration 把「当前会话模型」接上,并在 session_start
|
|
7
|
+
* 里调用这些 registerXxx。
|
|
8
|
+
*
|
|
9
|
+
* 为什么总线要先建、注册要晚于 `session_start`:
|
|
10
|
+
* - 禁用规则可以带 `models`,只有在 `session_start` 才能拿到本会话的模型,
|
|
11
|
+
* 因此注册发生在 `session_start` 里(总线在加载期先建好,过滤在注册时求值)。
|
|
12
|
+
* - LSP manager 在加载期创建、并在它自己的 `session_start` handler 里启用服务器
|
|
13
|
+
* 后回调注册 lsp 工具,所以它必须在加载期就能拿到同一个总线;模型由本文件
|
|
14
|
+
* 注册的 handler 更新,且它排在所有注册 handler 之前。
|
|
15
|
+
*
|
|
16
|
+
* 注册即启用:`pi.registerTool` 后工具默认进入 active 列表(pi 的 `defaultActive`
|
|
17
|
+
* 语义),本文件不碰 active 列表——工具可见性完全由「注册与否」决定,禁用工具就是
|
|
18
|
+
* 不注册。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
22
|
+
|
|
23
|
+
import { createToolBus, type ToolBus } from "./tool-bus.js";
|
|
24
|
+
import {
|
|
25
|
+
type ModelIdentity,
|
|
26
|
+
readToolsConfig,
|
|
27
|
+
resolveToolAvailability,
|
|
28
|
+
type ToolsConfig,
|
|
29
|
+
type UnmatchedPattern,
|
|
30
|
+
} from "./tools-config.js";
|
|
31
|
+
|
|
32
|
+
export interface ToolRegistration {
|
|
33
|
+
/** 与同入口其他模块共享的总线。 */
|
|
34
|
+
readonly bus: ToolBus;
|
|
35
|
+
/** 本次配置解析结果(含解析期警告)。 */
|
|
36
|
+
readonly config: ToolsConfig;
|
|
37
|
+
/**
|
|
38
|
+
* 在每次会话启动时注册工具;注册时总线的过滤已按本会话模型生效。回调拿到
|
|
39
|
+
* 本会话的 ctx(reads 记账恢复等需要 sessionManager)。
|
|
40
|
+
*/
|
|
41
|
+
onSessionStart(register: (bus: ToolBus, ctx: ExtensionContext) => void | Promise<void>): void;
|
|
42
|
+
/** 哪些配置模式没命中过任何工具(在注册完成后调用)。 */
|
|
43
|
+
unmatchedPatterns(): UnmatchedPattern[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 独立入口(按路径加载的模块、或只有一组工具的入口)的注册接线:
|
|
48
|
+
* 每次会话启动时用本会话模型建好的总线注册工具。
|
|
49
|
+
*/
|
|
50
|
+
export function registerToolsOnSessionStart(
|
|
51
|
+
pi: ExtensionAPI,
|
|
52
|
+
register: (bus: ToolBus, ctx: ExtensionContext) => void | Promise<void>,
|
|
53
|
+
): void {
|
|
54
|
+
createToolRegistration(pi).onSessionStart(register);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createToolRegistration(pi: ExtensionAPI, settingsPath?: string): ToolRegistration {
|
|
58
|
+
const config = readToolsConfig(settingsPath);
|
|
59
|
+
let model: ModelIdentity | undefined;
|
|
60
|
+
|
|
61
|
+
const bus = createToolBus(pi, {
|
|
62
|
+
// 每次注册都按当前模型重新判定:工具在 session_start 里注册,模型由下面
|
|
63
|
+
// 的 handler 更新,二者在同一个会话内保持一致。
|
|
64
|
+
isDisabled: (name) => resolveToolAvailability(config, model).isDisabled(name),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
pi.on("session_start", (_event, ctx) => {
|
|
68
|
+
model = ctx.model;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
bus,
|
|
73
|
+
config,
|
|
74
|
+
onSessionStart(register) {
|
|
75
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
76
|
+
await register(bus, ctx);
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
unmatchedPatterns() {
|
|
80
|
+
return resolveToolAvailability(config, model).unmatchedPatterns(bus.declaredNames());
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 共享工具服务:同一个扩展实例里的所有工具共用一份请求策略、bwrap runtime 与
|
|
3
|
+
* LSP manager。
|
|
4
|
+
*
|
|
5
|
+
* 为什么必须共享:两套文件 IO 工具集各自创建 LSP manager 会重复注册 `/lsp-*`
|
|
6
|
+
* 命令(命令名冲突);各自创建 bwrap runtime 会重复注册 `/bwrap*` 命令。因此
|
|
7
|
+
* 由入口创建一份、注入给选中的工具集。
|
|
8
|
+
*
|
|
9
|
+
* LSP 专属工具(lsp-rename / inspect 族)由「当前会话选中的工具集」注册,
|
|
10
|
+
* 通过 `setLspEnabledHandler` 交给本模块——manager 在 session_start 之后才
|
|
11
|
+
* 触发该回调,那时入口已完成工具集选择。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
|
|
16
|
+
import type { BwrapConfig } from "../bwrap/core.js";
|
|
17
|
+
import { type BwrapRuntime, createBwrapRuntime } from "../bwrap/runtime.js";
|
|
18
|
+
import {
|
|
19
|
+
createLspManager,
|
|
20
|
+
type LspManager,
|
|
21
|
+
type LspService,
|
|
22
|
+
type LspServiceOptions,
|
|
23
|
+
} from "./lsp/lsp.js";
|
|
24
|
+
import { createRequestPolicy, type RequestPolicy } from "./request-policy.js";
|
|
25
|
+
|
|
26
|
+
export interface ToolServices {
|
|
27
|
+
policy: RequestPolicy;
|
|
28
|
+
runtime: BwrapRuntime;
|
|
29
|
+
manager: LspManager;
|
|
30
|
+
/** 由入口在选出本会话的工具集后设置。 */
|
|
31
|
+
setLspEnabledHandler(handler: ((service: LspService) => void) | undefined): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ToolServicesOptions extends LspServiceOptions {
|
|
35
|
+
/** 固定沙箱配置(子代理用);不传则跟随用户配置与 /bwrap-* 命令。 */
|
|
36
|
+
sandbox?: BwrapConfig;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createToolServices(pi: ExtensionAPI, options?: ToolServicesOptions): ToolServices {
|
|
40
|
+
const policy = createRequestPolicy(pi.events);
|
|
41
|
+
const runtime = createBwrapRuntime(policy, options?.sandbox);
|
|
42
|
+
// 注册 /bwrap* 命令、沙箱系统提示与 session 生命周期钩子;固定沙箱
|
|
43
|
+
// (子代理)不注册命令,见 BwrapRuntime.setup。
|
|
44
|
+
runtime.setup(pi);
|
|
45
|
+
|
|
46
|
+
let delegate: ((service: LspService) => void) | undefined;
|
|
47
|
+
const manager = createLspManager(
|
|
48
|
+
pi,
|
|
49
|
+
{
|
|
50
|
+
onEnabled: (_pi, service) => {
|
|
51
|
+
delegate?.(service);
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
options,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
policy,
|
|
59
|
+
runtime,
|
|
60
|
+
manager,
|
|
61
|
+
setLspEnabledHandler(handler) {
|
|
62
|
+
delegate = handler;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具单元表:本仓库工具按「一套文件 IO 工具集 + 若干单元」组织,主入口与
|
|
3
|
+
* spawn-agent 的子代理共用同一张表,因此两处的工具实现、参数与审批完全一致。
|
|
4
|
+
*
|
|
5
|
+
* - 主入口按配置选中的工具集注册全部单元(`fileIo` / `fileIoByModel`)。
|
|
6
|
+
* - 子代理按 frontmatter 声明的工具名注册覆盖到的单元;可见性再由
|
|
7
|
+
* `createAgentSession({ tools })` 的白名单收敛。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
|
|
12
|
+
import type { BwrapRuntime } from "../bwrap/runtime.js";
|
|
13
|
+
import type { FileToolset } from "../claude-code/files.js";
|
|
14
|
+
import { registerSearchTools } from "../claude-code/search.js";
|
|
15
|
+
import { registerSessionTools } from "../claude-code/session-tools.js";
|
|
16
|
+
import { registerShellTools } from "../claude-code/shell.js";
|
|
17
|
+
import { registerBashTool } from "../opencode/bash.js";
|
|
18
|
+
import type { OpencodeFileToolset } from "../opencode/files.js";
|
|
19
|
+
import { registerGlobTool } from "../opencode/glob.js";
|
|
20
|
+
import { registerGrepTool } from "../opencode/grep.js";
|
|
21
|
+
import { registerQuestionTool } from "../opencode/question.js";
|
|
22
|
+
import { registerTodoTool } from "../opencode/todo.js";
|
|
23
|
+
import type { RequestPolicy } from "./request-policy.js";
|
|
24
|
+
import type { ToolBus } from "./tool-bus.js";
|
|
25
|
+
import type { FileIoToolset } from "./tools-config.js";
|
|
26
|
+
|
|
27
|
+
export interface ToolUnitDeps {
|
|
28
|
+
pi: ExtensionAPI;
|
|
29
|
+
bus: ToolBus;
|
|
30
|
+
policy: RequestPolicy;
|
|
31
|
+
runtime: BwrapRuntime;
|
|
32
|
+
/** 选中工具集的文件工具(Read/Edit/Write 或 read/edit/write)。 */
|
|
33
|
+
fileToolset: FileToolset | OpencodeFileToolset;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ToolUnit {
|
|
37
|
+
/** 该单元提供的工具名,用于子代理按声明挑选单元。 */
|
|
38
|
+
tools: readonly string[];
|
|
39
|
+
register(deps: ToolUnitDeps): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const TOOL_UNITS: Record<FileIoToolset, readonly ToolUnit[]> = {
|
|
43
|
+
"claude-code": [
|
|
44
|
+
{
|
|
45
|
+
tools: ["Read", "Edit", "Write"],
|
|
46
|
+
register: ({ bus, fileToolset }) => fileToolset.register(bus),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
tools: ["Glob", "Grep"],
|
|
50
|
+
register: ({ bus, pi }) => registerSearchTools(bus, pi),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
tools: ["Bash"],
|
|
54
|
+
register: ({ bus, pi, runtime }) => registerShellTools(bus, pi, runtime),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
tools: ["TodoWrite", "AskUserQuestion"],
|
|
58
|
+
register: ({ bus, pi }) => registerSessionTools(bus, pi),
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
opencode: [
|
|
62
|
+
{
|
|
63
|
+
tools: ["read", "edit", "write"],
|
|
64
|
+
register: ({ bus, fileToolset }) => fileToolset.register(bus),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
tools: ["glob", "grep"],
|
|
68
|
+
register: ({ bus }) => {
|
|
69
|
+
registerGlobTool(bus);
|
|
70
|
+
registerGrepTool(bus);
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
tools: ["bash"],
|
|
75
|
+
register: ({ bus, pi, runtime }) => registerBashTool(bus, pi, runtime),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
tools: ["todowrite", "question"],
|
|
79
|
+
register: ({ bus }) => {
|
|
80
|
+
registerTodoTool(bus);
|
|
81
|
+
registerQuestionTool(bus);
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** 子代理声明的工具名对应哪一套文件 IO 工具集:出现哪一套的名字就用哪一套。 */
|
|
88
|
+
export function toolsetsForToolNames(toolNames: readonly string[]): FileIoToolset | undefined {
|
|
89
|
+
const declared = new Set(toolNames);
|
|
90
|
+
for (const kind of ["claude-code", "opencode"] as const) {
|
|
91
|
+
if (TOOL_UNITS[kind].some((unit) => unit.tools.some((name) => declared.has(name)))) {
|
|
92
|
+
return kind;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 挑出覆盖到声明工具名的单元。 */
|
|
99
|
+
export function unitsForToolNames(kind: FileIoToolset, toolNames: readonly string[]): ToolUnit[] {
|
|
100
|
+
const declared = new Set(toolNames);
|
|
101
|
+
return TOOL_UNITS[kind].filter((unit) => unit.tools.some((name) => declared.has(name)));
|
|
102
|
+
}
|