@trim21/personal-pi-extensions 0.0.297 → 0.0.298

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.297",
3
+ "version": "0.0.298",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -32,6 +32,7 @@
32
32
  "typebox": ">=1.3.1"
33
33
  },
34
34
  "devDependencies": {
35
+ "@earendil-works/pi-agent-core": ">=0.84.1",
35
36
  "@earendil-works/pi-ai": "^0.84.1",
36
37
  "@earendil-works/pi-coding-agent": "^0.84.1",
37
38
  "@eslint/js": "10.0.1",
@@ -56,6 +57,7 @@
56
57
  },
57
58
  "pi": {
58
59
  "extensions": [
60
+ "src/aft/index.ts",
59
61
  "src/opencode/index.ts",
60
62
  "src/claude-code/index.ts",
61
63
  "src/vision-agent.ts",
@@ -83,7 +85,12 @@
83
85
  "node": ">=24"
84
86
  },
85
87
  "dependencies": {
88
+ "@cortexkit/aft-bridge": "0.51.0",
86
89
  "@vscode/tree-sitter-wasm": "^0.3.1",
90
+ "jsonc-parser": "^3.3.1",
87
91
  "web-tree-sitter": "^0.26.12"
92
+ },
93
+ "optionalDependencies": {
94
+ "@cortexkit/aft-linux-x64": "0.51.0"
88
95
  }
89
96
  }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * AFT bridge 管理:二进制解析、transport pool 生命周期、工具调用封装。
3
+ *
4
+ * 依赖 @cortexkit/aft-bridge(官方协议的 JS 客户端):findBinary 的解析顺序是
5
+ * 缓存 → npm 平台包(@cortexkit/aft-<platform>,随 npm 镜像分发,无运行时网络)
6
+ * → PATH → cargo → GitHub release 兜底;内网部署只要保证平台包版本与
7
+ * aft-bridge 锁一致就不会走到最后的网络下载。
8
+ */
9
+
10
+ import { createRequire } from "node:module";
11
+
12
+ import {
13
+ type AftProjectTransport,
14
+ type AftTransportPool,
15
+ type BridgeRequestOptions,
16
+ createAftTransportPool,
17
+ findBinary,
18
+ readConfigTiers,
19
+ resolveCortexKitConfigPaths,
20
+ resolveCortexKitStorageRoot,
21
+ timeoutForCommand,
22
+ } from "@cortexkit/aft-bridge";
23
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
24
+
25
+ /** aft-bridge 包自身版本,作为二进制版本匹配基准。 */
26
+ const BRIDGE_VERSION: string = (() => {
27
+ try {
28
+ const req = createRequire(import.meta.url);
29
+ return (req("@cortexkit/aft-bridge/package.json") as { version?: string }).version ?? "0.0.0";
30
+ } catch {
31
+ return "0.0.0";
32
+ }
33
+ })();
34
+
35
+ /** Pi 会话 ID:Rust 侧用它做 session 作用域(undo/checkpoint),感知工具可留空。 */
36
+ export function resolveSessionId(extCtx: ExtensionContext): string | undefined {
37
+ const manager = (extCtx as unknown as { sessionManager?: { getSessionId?: () => string } })
38
+ .sessionManager;
39
+ const id = manager?.getSessionId?.();
40
+ return typeof id === "string" && id.length > 0 ? id : undefined;
41
+ }
42
+
43
+ /** 解析 aft 二进制;失败时抛出(调用方决定是否降级不注册工具)。 */
44
+ export async function resolveAftBinary(): Promise<string> {
45
+ const path = await findBinary(BRIDGE_VERSION);
46
+ if (!path) {
47
+ throw new Error(
48
+ "AFT binary not found. Install via npm platform package (@cortexkit/aft-<platform>), cargo install agent-file-tools, or place `aft` on PATH.",
49
+ );
50
+ }
51
+ return path;
52
+ }
53
+
54
+ export interface AftPool {
55
+ pool: AftTransportPool;
56
+ /** 当前项目根(process.cwd),供 bridge 查询。 */
57
+ projectRoot: string;
58
+ }
59
+
60
+ /** 创建 transport pool。每个项目根一个常驻 aft 进程,跨 session 共享。 */
61
+ export async function createAftPool(cwd: string): Promise<AftPool> {
62
+ const binaryPath = await resolveAftBinary();
63
+ const paths = resolveCortexKitConfigPaths(cwd);
64
+ const pool = await createAftTransportPool({
65
+ harness: "pi",
66
+ binaryPath,
67
+ poolOptions: { minVersion: BRIDGE_VERSION },
68
+ configOverrides: {
69
+ storage_dir: resolveCortexKitStorageRoot(),
70
+ cortexkit_user_config_path: paths.userConfigPath,
71
+ config: readConfigTiers(paths),
72
+ },
73
+ });
74
+ pool.setConfigureOverride("harness", "pi");
75
+ return { pool, projectRoot: cwd };
76
+ }
77
+
78
+ /**
79
+ * 调用 AFT 工具命令并返回 Rust 格式化好的文本。
80
+ * 只读感知工具统一走 toolCall(server 侧 tool_call 分派)。`softCodes` 里的
81
+ * 错误码(如 symbol_not_found)是合法否定答案,不抛错、按文本返回。
82
+ */
83
+ export async function callAftTool(
84
+ bridge: AftProjectTransport,
85
+ command: string,
86
+ rawArgs: Record<string, unknown>,
87
+ extCtx: ExtensionContext,
88
+ options?: BridgeRequestOptions,
89
+ softCodes?: ReadonlySet<string>,
90
+ ): Promise<{ text: string; response: Record<string, unknown> }> {
91
+ const timeoutMs = timeoutForCommand(command);
92
+ const sessionId = resolveSessionId(extCtx);
93
+ const sendOptions = {
94
+ ...(timeoutMs !== undefined && { timeoutMs }),
95
+ ...options,
96
+ };
97
+ const response = await bridge.toolCall(
98
+ sessionId,
99
+ command,
100
+ rawArgs,
101
+ Object.keys(sendOptions).length > 0 ? sendOptions : undefined,
102
+ );
103
+ if (!response.success) {
104
+ const code = typeof response.code === "string" ? response.code : "";
105
+ if (softCodes?.has(code)) {
106
+ return {
107
+ text: response.text || response.message || "",
108
+ response: response as unknown as Record<string, unknown>,
109
+ };
110
+ }
111
+ throw new Error(response.text || response.message || `${command} failed`);
112
+ }
113
+ return {
114
+ text: typeof response.text === "string" ? response.text : "",
115
+ response: response as unknown as Record<string, unknown>,
116
+ };
117
+ }
118
+
119
+ /** 关闭 pool(session 结束 / 进程退出时调用)。 */
120
+ export async function shutdownAftPool(owner: AftPool): Promise<void> {
121
+ await owner.pool.shutdown();
122
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * aft.jsonc 最小读取:只关心 JS 侧注册决策需要的字段
3
+ * (enabled / semantic_search),完整配置由 Rust 侧从配置 tier 读取。
4
+ *
5
+ * 只读用户级配置:semantic_search 涉及外部 embedding 后端,按 AFT 的信任
6
+ * 边界语义只允许用户级控制(项目级配置不应替用户开启语义搜索)。
7
+ *
8
+ * JSONC 解析用微软 jsonc-parser(vscode 同源,处理注释与尾逗号),
9
+ * 不手写解析器。
10
+ */
11
+
12
+ import { existsSync, readFileSync } from "node:fs";
13
+
14
+ import { parse } from "jsonc-parser";
15
+
16
+ export interface AftReadConfig {
17
+ enabled: boolean;
18
+ semanticSearch: boolean;
19
+ }
20
+
21
+ const DEFAULTS: AftReadConfig = { enabled: true, semanticSearch: false };
22
+
23
+ function readOne(path: string): AftReadConfig {
24
+ try {
25
+ if (!existsSync(path)) return DEFAULTS;
26
+ const parsed: unknown = parse(readFileSync(path, "utf8"));
27
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
28
+ return DEFAULTS;
29
+ }
30
+ const value = parsed as Record<string, unknown>;
31
+ return {
32
+ enabled: typeof value.enabled === "boolean" ? value.enabled : DEFAULTS.enabled,
33
+ semanticSearch:
34
+ typeof value.semantic_search === "boolean"
35
+ ? value.semantic_search
36
+ : DEFAULTS.semanticSearch,
37
+ };
38
+ } catch {
39
+ return DEFAULTS;
40
+ }
41
+ }
42
+
43
+ /** 读用户级 aft.jsonc。解析失败或文件缺失时回退默认值(不阻塞扩展加载)。 */
44
+ export function loadAftConfig(userConfigPath: string): AftReadConfig {
45
+ return readOne(userConfigPath);
46
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * AFT 感知工具扩展入口:aft_outline / aft_zoom / aft_callgraph / aft_search。
3
+ *
4
+ * 只读接入 AFT 的结构感知能力(tree-sitter 符号表、trigram 索引、调用图、
5
+ * 语义搜索),不触碰本仓库自己的 read/write/edit/bash 工具及其安全机制
6
+ * (bwrap 沙箱、write-guard、reads 记账)。aft_search 仅当用户级
7
+ * aft.jsonc 开启 semantic_search 时注册(本地语义索引需 ONNX 运行时,
8
+ * 内网默认关闭)。
9
+ *
10
+ * 二进制缺失或 pool 创建失败时降级:不注册任何工具并在 session 开始时报
11
+ * 一次错,而不是让每个工具调用失败。
12
+ *
13
+ * Usage:
14
+ * pi -e ./aft/index.ts
15
+ */
16
+
17
+ import { resolveCortexKitConfigPaths } from "@cortexkit/aft-bridge";
18
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
+
20
+ import { type AftPool, createAftPool, shutdownAftPool } from "./bridge.js";
21
+ import { loadAftConfig } from "./config.js";
22
+ import {
23
+ registerCallgraphTool,
24
+ registerOutlineTool,
25
+ registerSearchTool,
26
+ registerZoomTool,
27
+ } from "./tools.js";
28
+
29
+ export default async function aftReadTools(pi: ExtensionAPI): Promise<void> {
30
+ const cwd = process.cwd();
31
+ const cfg = loadAftConfig(resolveCortexKitConfigPaths(cwd).userConfigPath);
32
+ if (!cfg.enabled) return;
33
+
34
+ let pool: AftPool;
35
+ try {
36
+ pool = await createAftPool(cwd);
37
+ } catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ pi.on("session_start", (_event, ctx) => {
40
+ ctx.ui.notify(
41
+ `AFT tools are disabled: ${message}. Install @cortexkit/aft-<platform> via your npm mirror (locked to the same version as @cortexkit/aft-bridge), then reload.`,
42
+ "error",
43
+ );
44
+ });
45
+ return;
46
+ }
47
+
48
+ const toolCtx = { cwd, pool: pool.pool };
49
+ registerOutlineTool(pi, toolCtx);
50
+ registerZoomTool(pi, toolCtx);
51
+ registerCallgraphTool(pi, toolCtx);
52
+ if (cfg.semanticSearch) {
53
+ registerSearchTool(pi, toolCtx);
54
+ }
55
+
56
+ // 关闭 bridge pool。session_shutdown 是 pi 的正常生命周期;beforeExit 兜底
57
+ // 进程自然退出(不能注册 SIGINT/SIGTERM——那会吞掉 pi 主进程自己的信号处理)。
58
+ let shuttingDown = false;
59
+ const shutdown = async (): Promise<void> => {
60
+ if (shuttingDown) return;
61
+ shuttingDown = true;
62
+ try {
63
+ await shutdownAftPool(pool);
64
+ } catch {
65
+ // 关闭失败不影响退出流程
66
+ }
67
+ };
68
+ process.once("beforeExit", () => void shutdown());
69
+
70
+ pi.on("session_shutdown", async () => {
71
+ await shutdown();
72
+ });
73
+ }
@@ -0,0 +1,360 @@
1
+ /**
2
+ * AFT 感知工具:aft_outline / aft_zoom / aft_callgraph / aft_search。
3
+ *
4
+ * 全部只读,不经过写路径;路径参数在本地解析后透传给 AFT bridge,
5
+ * 由 Rust 侧(tree-sitter 符号表 / trigram 索引 / 调用图)计算。
6
+ */
7
+
8
+ import { homedir } from "node:os";
9
+ import { isAbsolute, join, resolve } from "node:path";
10
+
11
+ import {
12
+ type AftProjectTransport,
13
+ type AftTransportPool,
14
+ coerceBoolean,
15
+ coerceOptionalInt,
16
+ coerceTargetParam,
17
+ formatCallgraphSections,
18
+ PLAIN_CALLGRAPH_THEME,
19
+ } from "@cortexkit/aft-bridge";
20
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
21
+ import { Type } from "typebox";
22
+
23
+ import { callAftTool } from "./bridge.js";
24
+
25
+ /** 解析 `~` 前缀与相对路径(相对 session cwd)。URL 与绝对路径原样返回。 */
26
+ export function resolvePathArg(cwd: string, input: string): string {
27
+ if (input === "~" || input.startsWith("~/")) {
28
+ return join(homedir(), input.slice(1));
29
+ }
30
+ if (input.startsWith("http://") || input.startsWith("https://")) return input;
31
+ return isAbsolute(input) ? input : resolve(cwd, input);
32
+ }
33
+
34
+ interface AftToolContext {
35
+ cwd: string;
36
+ pool: AftTransportPool;
37
+ }
38
+
39
+ function bridgeFor(ctx: AftToolContext): AftProjectTransport {
40
+ return ctx.pool.getBridge(ctx.cwd);
41
+ }
42
+
43
+ const OutlineParams = Type.Object(
44
+ {
45
+ target: Type.Union([Type.String(), Type.Array(Type.String())], {
46
+ description:
47
+ "要 outline 的对象:文件路径、目录路径、URL(http:// 或 https://),或文件路径数组。模式自动识别:URL 按前缀、目录按 stat、数组按多文件。目录递归上限 200 个文件。",
48
+ }),
49
+ files: Type.Optional(
50
+ Type.Boolean({
51
+ description:
52
+ "目录模式:为 true 时 target 必须是目录(或目录数组),返回带语言/符号数/字节大小的扁平文件树,而非符号大纲。",
53
+ }),
54
+ ),
55
+ includeTests: Type.Optional(
56
+ Type.Boolean({ description: "目录大纲:包含测试文件。默认 false。" }),
57
+ ),
58
+ },
59
+ { additionalProperties: false },
60
+ );
61
+
62
+ export function registerOutlineTool(pi: ExtensionAPI, ctx: AftToolContext): void {
63
+ pi.registerTool({
64
+ name: "aft_outline",
65
+ label: "aft_outline",
66
+ description: [
67
+ "输出代码文件、目录、URL 的结构化大纲:函数/类/类型等符号及其行号范围;Markdown/HTML 返回标题层级。",
68
+ "用它在读取具体内容之前先了解文件结构(比整文件 read 省 token)。",
69
+ "深入了解某个符号用 aft_zoom;看跨文件调用关系用 aft_callgraph。",
70
+ "target 支持:文件路径(带签名的符号大纲)、目录路径(递归最多 200 文件)、URL、文件路径数组。",
71
+ "files: true 且 target 为目录时返回扁平文件树(语言、顶层符号数、字节大小)。",
72
+ ].join("\n"),
73
+ promptSnippet: "Output structural outline of a file/directory/URL",
74
+ parameters: OutlineParams,
75
+ async execute(_id, params, _signal, _onUpdate, extCtx) {
76
+ const target = coerceTargetParam(params.target);
77
+ if (
78
+ (typeof target !== "string" || target.length === 0) &&
79
+ (!Array.isArray(target) || target.length === 0)
80
+ ) {
81
+ throw new Error("'target' must be a non-empty string or array of strings");
82
+ }
83
+ const filesMode = coerceBoolean(params.files);
84
+ const rawArgs: Record<string, unknown> = {
85
+ target: Array.isArray(target)
86
+ ? target.map((t) => resolvePathArg(extCtx.cwd, t))
87
+ : filesMode || target.startsWith("http://") || target.startsWith("https://")
88
+ ? target
89
+ : resolvePathArg(extCtx.cwd, target),
90
+ };
91
+ if (filesMode) rawArgs.files = true;
92
+ if (params.includeTests !== undefined) rawArgs.includeTests = params.includeTests;
93
+
94
+ const { text, response } = await callAftTool(bridgeFor(ctx), "outline", rawArgs, extCtx);
95
+ return {
96
+ content: [{ type: "text", text }],
97
+ details: { input: params, truncated: response.truncated === true },
98
+ };
99
+ },
100
+ });
101
+ }
102
+
103
+ const ZoomTarget = Type.Object({
104
+ path: Type.String({ description: "文件路径(绝对或相对项目根)" }),
105
+ symbol: Type.String({ description: "该文件中的符号名" }),
106
+ });
107
+
108
+ const ZoomParams = Type.Object(
109
+ {
110
+ path: Type.Optional(Type.String({ description: "文件路径(绝对或相对项目根)" })),
111
+ url: Type.Optional(Type.String({ description: "要 zoom 的 HTML/Markdown 文档 URL" })),
112
+ symbols: Type.Optional(
113
+ Type.Union([Type.String(), Type.Array(Type.String())], {
114
+ description: "符号名(代码)或标题文本(Markdown/HTML);字符串或数组(同文件批量查询)。",
115
+ }),
116
+ ),
117
+ targets: Type.Optional(
118
+ Type.Union([ZoomTarget, Type.Array(ZoomTarget)], {
119
+ description: "跨文件批量:`{ path, symbol }` 或数组。与 path/url/symbols 互斥。",
120
+ }),
121
+ ),
122
+ contextLines: Type.Optional(
123
+ Type.Union([Type.Number({ minimum: 1 }), Type.String()], {
124
+ description: "符号前后上下文行数(默认 3)",
125
+ }),
126
+ ),
127
+ callgraph: Type.Optional(
128
+ Type.Boolean({
129
+ description: "附带调用图标注(同文件内 calls-out / called-by)。默认 false 保持输出精简。",
130
+ }),
131
+ ),
132
+ },
133
+ { additionalProperties: false },
134
+ );
135
+
136
+ export function registerZoomTool(pi: ExtensionAPI, ctx: AftToolContext): void {
137
+ pi.registerTool({
138
+ name: "aft_zoom",
139
+ label: "aft_zoom",
140
+ description: [
141
+ "查看命名符号(函数/类/类型)的完整源码,或 Markdown/HTML 的标题段落内容。",
142
+ "需要理解某个具体符号时用它(读整个文件用 read)。",
143
+ "callgraph: true 时附带同文件内的调用关系标注。",
144
+ "三种模式互斥,用且仅用一种:`{ path, symbols }`、`{ url, symbols }`、`{ targets }`。",
145
+ ].join("\n"),
146
+ promptSnippet: "Inspect the full source of a named symbol",
147
+ parameters: ZoomParams,
148
+ async execute(_id, params, _signal, _onUpdate, extCtx) {
149
+ const isEmpty = (v: unknown): boolean =>
150
+ v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
151
+
152
+ const hasPath = !isEmpty(params.path);
153
+ const hasUrl = !isEmpty(params.url);
154
+ const hasSymbols = !isEmpty(params.symbols);
155
+ const hasTargets = !isEmpty(params.targets);
156
+
157
+ if (hasTargets && (hasPath || hasUrl || hasSymbols)) {
158
+ throw new Error("'targets' 与 'path'/'url'/'symbols' 互斥,只能提供一种模式");
159
+ }
160
+ if (hasPath && hasUrl) {
161
+ throw new Error("'path' 与 'url' 互斥,只能提供一种");
162
+ }
163
+ if (!hasTargets && !hasPath && !hasUrl) {
164
+ throw new Error("Provide exactly one of 'path', 'url', or 'targets'");
165
+ }
166
+
167
+ const rawArgs: Record<string, unknown> = {};
168
+ if (hasTargets) {
169
+ const targetList = params.targets;
170
+ if (!targetList) {
171
+ throw new Error("'targets' must be a non-empty object or array");
172
+ }
173
+ const list = Array.isArray(targetList) ? targetList : [targetList];
174
+ rawArgs.targets = list.map((t) => ({
175
+ filePath: resolvePathArg(extCtx.cwd, t.path),
176
+ symbol: t.symbol,
177
+ }));
178
+ } else if (hasUrl) {
179
+ rawArgs.url = params.url;
180
+ if (hasSymbols) rawArgs.symbols = params.symbols;
181
+ } else {
182
+ const filePath = params.path;
183
+ if (!filePath) {
184
+ throw new Error("'path' must be a non-empty string");
185
+ }
186
+ rawArgs.filePath = resolvePathArg(extCtx.cwd, filePath);
187
+ if (hasSymbols) rawArgs.symbols = params.symbols;
188
+ }
189
+
190
+ const contextLines = coerceOptionalInt(
191
+ params.contextLines,
192
+ "contextLines",
193
+ 1,
194
+ Number.MAX_SAFE_INTEGER,
195
+ );
196
+ if (contextLines !== undefined) rawArgs.contextLines = contextLines;
197
+ if (coerceBoolean(params.callgraph)) rawArgs.callgraph = true;
198
+
199
+ const { text, response } = await callAftTool(bridgeFor(ctx), "zoom", rawArgs, extCtx);
200
+ return {
201
+ content: [{ type: "text", text }],
202
+ details: { input: params, truncated: response.truncated === true },
203
+ };
204
+ },
205
+ });
206
+ }
207
+
208
+ const CALLGRAPH_OPS = [
209
+ "call_tree",
210
+ "callers",
211
+ "trace_to",
212
+ "trace_to_symbol",
213
+ "impact",
214
+ "trace_data",
215
+ ] as const;
216
+
217
+ const CallgraphParams = Type.Object(
218
+ {
219
+ op: Type.Union(
220
+ CALLGRAPH_OPS.map((op) => Type.Literal(op)),
221
+ { description: "导航操作" },
222
+ ),
223
+ path: Type.String({
224
+ description: "包含目标符号的源文件(绝对或相对项目根)",
225
+ }),
226
+ symbol: Type.String({ description: "要分析的符号名" }),
227
+ depth: Type.Optional(
228
+ Type.Union([Type.Number({ minimum: 1 }), Type.String()], {
229
+ description: "调用图最大遍历深度",
230
+ }),
231
+ ),
232
+ expression: Type.Optional(Type.String({ description: "要追踪的表达式(op=trace_data 必填)" })),
233
+ toSymbol: Type.Optional(Type.String({ description: "目标符号(op=trace_to_symbol 必填)" })),
234
+ toPath: Type.Optional(Type.String({ description: "目标文件(toSymbol 存在歧义时指定)" })),
235
+ includeTests: Type.Optional(
236
+ Type.Boolean({ description: "callers/路径中包含测试文件。默认 false。" }),
237
+ ),
238
+ includeUnresolved: Type.Optional(
239
+ Type.Boolean({
240
+ description: "逐个显示未解析的外部/stdlib 调用。默认折叠为每父节点一条摘要。",
241
+ }),
242
+ ),
243
+ },
244
+ { additionalProperties: false },
245
+ );
246
+
247
+ /** 只读导航的合法"否定答案":符号未定义或索引仍在构建——返回文本而非报错。 */
248
+ const CALLGRAPH_SOFT_CODES = new Set(["symbol_not_found", "callgraph_building"]);
249
+
250
+ export function registerCallgraphTool(pi: ExtensionAPI, ctx: AftToolContext): void {
251
+ pi.registerTool({
252
+ name: "aft_callgraph",
253
+ label: "aft_callgraph",
254
+ description: [
255
+ "基于真实调用图回答代码关系问题(谁调用我、影响面、调用链),替代 grep + read 的链条式排查。",
256
+ "op 语义:callers=调用点(改名/改签名前用);impact=影响面(改一个符号会波及谁);",
257
+ "call_tree=该函数调用了什么;trace_to=从入口如何执行到某符号;",
258
+ "trace_to_symbol=两符号间最短路径(需 toSymbol,歧义时需 toPath);trace_data=追踪值在参数/赋值间的流转(需 expression)。",
259
+ "标记:~ = 仅按名字解析的边(可能指向同名符号);[unresolved] = 未解析到定义的调用点。",
260
+ ].join("\n"),
261
+ promptSnippet: "Call graph and data-flow navigation",
262
+ parameters: CallgraphParams,
263
+ async execute(_id, params, _signal, _onUpdate, extCtx) {
264
+ const rawArgs: Record<string, unknown> = {
265
+ op: params.op,
266
+ filePath: resolvePathArg(extCtx.cwd, params.path),
267
+ symbol: params.symbol,
268
+ };
269
+ const depth = coerceOptionalInt(params.depth, "depth", 1, Number.MAX_SAFE_INTEGER);
270
+ if (depth !== undefined) rawArgs.depth = depth;
271
+ if (params.expression !== undefined && params.expression !== "") {
272
+ rawArgs.expression = params.expression;
273
+ }
274
+ if (params.toSymbol !== undefined && params.toSymbol !== "") {
275
+ rawArgs.toSymbol = params.toSymbol;
276
+ }
277
+ if (params.toPath !== undefined && params.toPath !== "") {
278
+ rawArgs.toFile = resolvePathArg(extCtx.cwd, params.toPath);
279
+ }
280
+ if (params.includeTests !== undefined) rawArgs.includeTests = params.includeTests;
281
+ if (params.includeUnresolved !== undefined) {
282
+ rawArgs.includeUnresolved = params.includeUnresolved;
283
+ }
284
+
285
+ const { text, response } = await callAftTool(
286
+ bridgeFor(ctx),
287
+ "callgraph",
288
+ rawArgs,
289
+ extCtx,
290
+ undefined,
291
+ CALLGRAPH_SOFT_CODES,
292
+ );
293
+ const out =
294
+ text ||
295
+ formatCallgraphSections(params.op, response, PLAIN_CALLGRAPH_THEME, {
296
+ includeUnresolved: coerceBoolean(params.includeUnresolved),
297
+ }).join("\n");
298
+ return {
299
+ content: [{ type: "text", text: out }],
300
+ details: { input: params, truncated: response.truncated === true },
301
+ };
302
+ },
303
+ });
304
+ }
305
+
306
+ const SearchParams = Type.Object(
307
+ {
308
+ query: Type.String({
309
+ description:
310
+ "搜索意图:概念、标识符、错误串、正则、字面量或文件名。概念类查询用完整自然语言句子,精确的名字/字符串/正则保持简短。",
311
+ }),
312
+ topK: Type.Optional(
313
+ Type.Integer({
314
+ description: "最大结果数(默认 10,最大 100)",
315
+ minimum: 1,
316
+ maximum: 100,
317
+ }),
318
+ ),
319
+ includeTests: Type.Optional(Type.Boolean({ description: "包含测试文件。默认 false。" })),
320
+ path: Type.Optional(
321
+ Type.String({
322
+ description: "仅当要搜索不同的 Git 项目时设置(绝对或 ~ 路径)。默认搜索当前项目。",
323
+ }),
324
+ ),
325
+ },
326
+ { additionalProperties: false },
327
+ );
328
+
329
+ export function registerSearchTool(pi: ExtensionAPI, ctx: AftToolContext): void {
330
+ pi.registerTool({
331
+ name: "aft_search",
332
+ label: "aft_search",
333
+ description: [
334
+ "一个工具完成代码搜索:概念、标识符、错误串、正则、字面量、文件名自动路由到合适的引擎并按相关度排序。",
335
+ "概念类查询('ORM 如何构建并执行查询')用自然语言整句——语义通道理解意图并匹配 docstring 和注释;",
336
+ "精确名字、字符串、正则保持简短('^export'、'Cargo.lock')。",
337
+ "需要语义索引可用(aft.jsonc 中 semantic_search: true 且 embedding 后端就绪);",
338
+ "否则退化为词法/正则匹配通道。",
339
+ ].join("\n"),
340
+ promptSnippet: "Search code by meaning or exact text",
341
+ parameters: SearchParams,
342
+ async execute(_id, params, _signal, _onUpdate, extCtx) {
343
+ if (typeof params.query !== "string" || params.query.trim().length === 0) {
344
+ throw new Error("'query' must be a non-empty string");
345
+ }
346
+ const rawArgs: Record<string, unknown> = { query: params.query };
347
+ if (params.topK !== undefined) rawArgs.topK = params.topK;
348
+ if (params.includeTests !== undefined) rawArgs.includeTests = params.includeTests;
349
+ if (params.path !== undefined && params.path !== "") {
350
+ rawArgs.path = resolvePathArg(extCtx.cwd, params.path);
351
+ }
352
+
353
+ const { text, response } = await callAftTool(bridgeFor(ctx), "search", rawArgs, extCtx);
354
+ return {
355
+ content: [{ type: "text", text }],
356
+ details: { input: params, truncated: response.truncated === true },
357
+ };
358
+ },
359
+ });
360
+ }