@trim21/personal-pi-extensions 0.0.303 → 0.0.305

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.303",
3
+ "version": "0.0.305",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -16,9 +16,10 @@ import { Type } from "typebox";
16
16
  import { Value } from "typebox/value";
17
17
 
18
18
  import { requireAbsolutePath } from "../claude-code/common.js";
19
+ import { type ToolPendant } from "../lib/pendant.js";
19
20
  import { guardWriteAccess } from "../lib/write-guard.js";
20
21
  import { callAftTool } from "./bridge.js";
21
- import { type AftToolContext, bridgeFor } from "./tools.js";
22
+ import { type AftToolContext, bridgeFor, buildPendantMarkdown } from "./tools.js";
22
23
 
23
24
  // ── 纯函数(可测) ──────────────────────────────────────────────────────────
24
25
 
@@ -256,9 +257,20 @@ export function registerAstEditTool(pi: ExtensionAPI, ctx: AftToolContext): void
256
257
 
257
258
  // 第三步:AFT 原子写盘(备份 + 格式化 + undo),结果由 Rust 格式化返回。
258
259
  const { text } = await callAftTool(bridgeFor(ctx), "edit", rawArgs, extCtx);
260
+ const files = previewFiles.map((f) => f.file || filePath);
259
261
  return {
260
262
  content: [{ type: "text", text }],
261
- details: { input: params, files: previewFiles.map((f) => f.file || filePath) },
263
+ details: {
264
+ files,
265
+ pendant: {
266
+ markdown: buildPendantMarkdown({
267
+ title: "ast_edit",
268
+ input: params,
269
+ output: text,
270
+ }),
271
+ expanded: true,
272
+ } satisfies ToolPendant,
273
+ },
262
274
  };
263
275
  },
264
276
  });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * aft_import —— language-aware import add / remove。
3
+ *
4
+ * 参数经 bridge.toolCall 以 agent 工具名 "import" 分派,Rust 侧 subc 翻译层
5
+ * 按 op 转成内部命令(add_import / remove_import)。
6
+ * 与 aft_refactor 相同:不支持 preview,写保护为路径级审批。
7
+ */
8
+
9
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+ import { Type } from "typebox";
11
+
12
+ import { type ToolPendant } from "../lib/pendant.js";
13
+ import { guardWriteAccess } from "../lib/write-guard.js";
14
+ import { callAftTool } from "./bridge.js";
15
+ import { type AftToolContext, bridgeFor, buildPendantMarkdown, resolvePathArg } from "./tools.js";
16
+
17
+ const IMPORT_OPS = ["add", "remove"] as const;
18
+ const VALIDATE_LEVELS = ["syntax", "full"] as const;
19
+
20
+ const ImportParams = Type.Object(
21
+ {
22
+ op: Type.Union(
23
+ IMPORT_OPS.map((op) => Type.Literal(op)),
24
+ { description: "import 操作" },
25
+ ),
26
+ path: Type.String({ description: "目标文件路径(绝对或相对项目根)" }),
27
+ module: Type.Optional(
28
+ Type.String({ description: "模块路径(add/remove 必填),如 'react'、'./utils'" }),
29
+ ),
30
+ names: Type.Optional(
31
+ Type.Array(Type.String(), {
32
+ description:
33
+ "要添加的具名导入,用语言原生的具名导入写法,支持按名 `as` 别名,如 ['useState']、Solidity ['ERC20', 'IERC20 as IToken']",
34
+ }),
35
+ ),
36
+ default_import: Type.Optional(Type.String({ description: "默认导入名(仅 ES),如 'React'" })),
37
+ namespace: Type.Optional(
38
+ Type.String({
39
+ description:
40
+ "命名空间绑定:`import * as ns from 'mod'`(ES)、`* as N from \"./X.sol\"`(Solidity)",
41
+ }),
42
+ ),
43
+ alias: Type.Optional(
44
+ Type.String({ description: '整模块别名。Solidity:`import "./X.sol" as X`' }),
45
+ ),
46
+ modifiers: Type.Optional(
47
+ Type.Array(Type.String(), {
48
+ description:
49
+ "语句级修饰符,按语言校验:Java/C# 'static'、C# 'global'/'unsafe'、Java/Kotlin/Scala 'wildcard'、Swift '@testable'",
50
+ }),
51
+ ),
52
+ import_kind: Type.Optional(
53
+ Type.String({
54
+ description:
55
+ "符号类导入:PHP 'function'/'const'、Swift 'struct'/'class'/'enum'、Scala 'given'",
56
+ }),
57
+ ),
58
+ remove_name: Type.Optional(
59
+ Type.String({ description: "要移除的具名导入;缺省移除整个 import" }),
60
+ ),
61
+ type_only: Type.Optional(Type.Boolean({ description: "仅类型导入(仅 TS)" })),
62
+ validate: Type.Optional(
63
+ Type.Union(
64
+ VALIDATE_LEVELS.map((level) => Type.Literal(level)),
65
+ {
66
+ description: "编辑后校验级别(默认 syntax)",
67
+ },
68
+ ),
69
+ ),
70
+ },
71
+ { additionalProperties: false },
72
+ );
73
+
74
+ export function registerImportTool(pi: ExtensionAPI, ctx: AftToolContext): void {
75
+ pi.registerTool({
76
+ name: "aft_import",
77
+ label: "aft_import",
78
+ description: [
79
+ "语言感知的 import 管理:add / remove。",
80
+ "支持 TS, JS, TSX, Python, Rust, Go, Solidity, Java, C#, PHP, Kotlin, Scala, Swift, Ruby, Lua, C, C++, Perl, Vue。",
81
+ "add:添加默认/具名/命名空间导入;remove:移除具名导入或整个 import。",
82
+ "import 排序整理交给 lint(如 import/order + --fix),本工具不负责。",
83
+ ].join("\n"),
84
+ promptSnippet: "Language-aware import add / remove",
85
+ parameters: ImportParams,
86
+ async execute(_id, params, _signal, _onUpdate, extCtx) {
87
+ if (params.module === undefined || params.module.trim() === "") {
88
+ throw new Error(`'module' is required for '${params.op}' op`);
89
+ }
90
+
91
+ const filePath = resolvePathArg(extCtx.cwd, params.path);
92
+ await guardWriteAccess(extCtx, { toolName: "aft_import", absolutePath: filePath });
93
+
94
+ const rawArgs: Record<string, unknown> = { op: params.op, path: filePath };
95
+ if (params.module !== undefined && params.module.trim() !== "") {
96
+ rawArgs.module = params.module;
97
+ }
98
+ if (params.names !== undefined) rawArgs.names = params.names;
99
+ if (params.default_import !== undefined) rawArgs.defaultImport = params.default_import;
100
+ if (params.namespace !== undefined) rawArgs.namespace = params.namespace;
101
+ if (params.alias !== undefined) rawArgs.alias = params.alias;
102
+ if (params.modifiers !== undefined) rawArgs.modifiers = params.modifiers;
103
+ if (params.import_kind !== undefined) rawArgs.importKind = params.import_kind;
104
+ if (params.remove_name !== undefined) rawArgs.removeName = params.remove_name;
105
+ if (params.type_only !== undefined) rawArgs.typeOnly = params.type_only;
106
+ if (params.validate !== undefined) rawArgs.validate = params.validate;
107
+
108
+ const { text } = await callAftTool(bridgeFor(ctx), "import", rawArgs, extCtx);
109
+ return {
110
+ content: [{ type: "text", text }],
111
+ details: {
112
+ files: [filePath],
113
+ pendant: {
114
+ markdown: buildPendantMarkdown({
115
+ title: "aft_import",
116
+ input: params,
117
+ output: text,
118
+ }),
119
+ expanded: true,
120
+ } satisfies ToolPendant,
121
+ },
122
+ };
123
+ },
124
+ });
125
+ }
package/src/aft/index.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * AFT 扩展入口:感知工具(aft_outline / aft_zoom / aft_callgraph / aft_search)
3
- * + ast_edit(符号级编辑,套用本仓库写保护机制)。
3
+ * + ast_edit(符号级编辑,套用本仓库写保护机制)+ aft_refactor / aft_import
4
+ * (workspace-wide 重构与 import 管理,路径级写保护)。
4
5
  *
5
6
  * 感知工具只读,不触碰本仓库自己的 read/write/edit/bash 工具及其安全机制
6
7
  * (bwrap 沙箱、write-guard、reads 记账)。aft_search 仅当用户级
7
8
  * aft.jsonc 开启 semantic_search 时注册(本地语义索引需 ONNX 运行时,
8
9
  * 内网默认关闭)。ast_edit 是写工具:用 AFT 的 preview 计算 diff,落盘走
9
- * 本仓库的 write-guard + reads 记账 + 写管线。
10
+ * 本仓库的 write-guard + reads 记账 + 写管线。aft_refactor / aft_import 的
11
+ * Rust 命令不支持 preview,写保护退化为路径级审批。
10
12
  *
11
13
  * 二进制缺失或 pool 创建失败时降级:不注册任何工具并在 session 开始时报
12
14
  * 一次错,而不是让每个工具调用失败。
@@ -21,6 +23,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
21
23
  import { registerAstEditTool } from "./ast-edit.js";
22
24
  import { type AftPool, createAftPool, shutdownAftPool } from "./bridge.js";
23
25
  import { loadAftConfig } from "./config.js";
26
+ import { registerImportTool } from "./imports.js";
27
+ import { registerRefactorTool } from "./refactor.js";
24
28
  import {
25
29
  registerCallgraphTool,
26
30
  registerOutlineTool,
@@ -52,6 +56,8 @@ export default async function aftReadTools(pi: ExtensionAPI): Promise<void> {
52
56
  registerZoomTool(pi, toolCtx);
53
57
  registerCallgraphTool(pi, toolCtx);
54
58
  registerAstEditTool(pi, toolCtx);
59
+ registerRefactorTool(pi, toolCtx);
60
+ registerImportTool(pi, toolCtx);
55
61
  if (cfg.semanticSearch) {
56
62
  registerSearchTool(pi, toolCtx);
57
63
  }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * aft_refactor —— workspace-wide refactoring(move / extract / inline)。
3
+ *
4
+ * 参数经 bridge.toolCall 以 agent 工具名 "refactor" 分派,Rust 侧 subc 翻译层
5
+ * 按 op 转成内部命令(move_symbol / extract_function / inline_symbol)。
6
+ * 与 ast_edit 不同,这些命令不支持 preview:写保护退化为路径级审批
7
+ * (workspace 内自动放行,外部路径经 write-guard 确认,无 diff 预览)。
8
+ */
9
+
10
+ import { coerceOptionalInt } from "@cortexkit/aft-bridge";
11
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
+ import { Type } from "typebox";
13
+
14
+ import { type ToolPendant } from "../lib/pendant.js";
15
+ import { guardWriteAccess } from "../lib/write-guard.js";
16
+ import { callAftTool } from "./bridge.js";
17
+ import { type AftToolContext, bridgeFor, buildPendantMarkdown, resolvePathArg } from "./tools.js";
18
+
19
+ const REFACTOR_OPS = ["move", "extract", "inline"] as const;
20
+
21
+ function requireField(value: unknown, name: string, op: string): void {
22
+ if (typeof value !== "string" || value.trim().length === 0) {
23
+ throw new Error(`'${name}' is required for '${op}' op`);
24
+ }
25
+ }
26
+
27
+ const RefactorParams = Type.Object(
28
+ {
29
+ op: Type.Union(
30
+ REFACTOR_OPS.map((op) => Type.Literal(op)),
31
+ { description: "重构操作" },
32
+ ),
33
+ path: Type.String({
34
+ description: "源文件路径(绝对或相对项目根;move 为符号当前所在文件)",
35
+ }),
36
+ symbol: Type.Optional(Type.String({ description: "符号名(move / inline 必填)" })),
37
+ destination: Type.Optional(Type.String({ description: "目标文件(move 必填)" })),
38
+ scope: Type.Optional(Type.String({ description: "move 消歧作用域" })),
39
+ name: Type.Optional(Type.String({ description: "新函数名(extract 必填)" })),
40
+ start_line: Type.Optional(
41
+ Type.Union([Type.Number(), Type.String()], {
42
+ description: "extract 起始行(1 起,必填)",
43
+ }),
44
+ ),
45
+ end_line: Type.Optional(
46
+ Type.Union([Type.Number(), Type.String()], {
47
+ description: "extract 结束行(含,必填)",
48
+ }),
49
+ ),
50
+ call_site_line: Type.Optional(
51
+ Type.Union([Type.Number(), Type.String()], {
52
+ description: "inline 调用点行号(1 起,必填)",
53
+ }),
54
+ ),
55
+ },
56
+ { additionalProperties: false },
57
+ );
58
+
59
+ export function registerRefactorTool(pi: ExtensionAPI, ctx: AftToolContext): void {
60
+ pi.registerTool({
61
+ name: "aft_refactor",
62
+ label: "aft_refactor",
63
+ description: [
64
+ "workspace-wide 重构:更新跨文件的 import 与引用。",
65
+ "move:把顶层符号(非嵌套函数/类方法)移到另一文件,全 workspace 重写 import;执行前自动创建 checkpoint。",
66
+ "extract:把行区间抽成新函数(TS/JS/TSX、Python)。",
67
+ "inline:把调用点替换为函数体。",
68
+ "move / rename 整个文件用 aft_move(OS 层操作,不更新引用);移动代码符号用本工具 op=move。",
69
+ ].join("\n"),
70
+ promptSnippet: "Workspace-wide symbol move / function extraction / inlining",
71
+ parameters: RefactorParams,
72
+ async execute(_id, params, _signal, _onUpdate, extCtx) {
73
+ const startLine = coerceOptionalInt(
74
+ params.start_line,
75
+ "start_line",
76
+ 1,
77
+ Number.MAX_SAFE_INTEGER,
78
+ );
79
+ const endLine = coerceOptionalInt(params.end_line, "end_line", 1, Number.MAX_SAFE_INTEGER);
80
+ const callSiteLine = coerceOptionalInt(
81
+ params.call_site_line,
82
+ "call_site_line",
83
+ 1,
84
+ Number.MAX_SAFE_INTEGER,
85
+ );
86
+
87
+ if (params.op === "move") {
88
+ requireField(params.symbol, "symbol", "move");
89
+ requireField(params.destination, "destination", "move");
90
+ } else if (params.op === "extract") {
91
+ requireField(params.name, "name", "extract");
92
+ if (startLine === undefined) throw new Error("'start_line' is required for 'extract' op");
93
+ if (endLine === undefined) throw new Error("'end_line' is required for 'extract' op");
94
+ } else {
95
+ requireField(params.symbol, "symbol", "inline");
96
+ if (callSiteLine === undefined) {
97
+ throw new Error("'call_site_line' is required for 'inline' op");
98
+ }
99
+ }
100
+
101
+ const filePath = resolvePathArg(extCtx.cwd, params.path);
102
+ const destination =
103
+ params.destination === undefined || params.destination.trim() === ""
104
+ ? undefined
105
+ : resolvePathArg(extCtx.cwd, params.destination);
106
+
107
+ const targets = destination === undefined ? [filePath] : [filePath, destination];
108
+ for (const target of targets) {
109
+ await guardWriteAccess(extCtx, { toolName: "aft_refactor", absolutePath: target });
110
+ }
111
+
112
+ const rawArgs: Record<string, unknown> = { op: params.op, path: filePath };
113
+ if (params.symbol !== undefined && params.symbol.trim() !== "") {
114
+ rawArgs.symbol = params.symbol;
115
+ }
116
+ if (destination !== undefined) rawArgs.destination = destination;
117
+ if (params.scope !== undefined && params.scope.trim() !== "") {
118
+ rawArgs.scope = params.scope;
119
+ }
120
+ if (params.name !== undefined && params.name.trim() !== "") {
121
+ rawArgs.name = params.name;
122
+ }
123
+ if (startLine !== undefined) rawArgs.startLine = startLine;
124
+ if (endLine !== undefined) rawArgs.endLine = endLine;
125
+ if (callSiteLine !== undefined) rawArgs.callSiteLine = callSiteLine;
126
+
127
+ const { text } = await callAftTool(bridgeFor(ctx), "refactor", rawArgs, extCtx);
128
+ return {
129
+ content: [{ type: "text", text }],
130
+ details: {
131
+ files: targets,
132
+ pendant: {
133
+ markdown: buildPendantMarkdown({
134
+ title: "aft_refactor",
135
+ input: params,
136
+ output: text,
137
+ }),
138
+ expanded: true,
139
+ } satisfies ToolPendant,
140
+ },
141
+ };
142
+ },
143
+ });
144
+ }
package/src/aft/tools.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
21
21
  import { Type } from "typebox";
22
22
 
23
+ import { type ToolPendant } from "../lib/pendant.js";
23
24
  import { callAftTool, SEMANTIC_INDEX_WAIT_TIMEOUT_MS } from "./bridge.js";
24
25
 
25
26
  /** 解析 `~` 前缀与相对路径(相对 session cwd)。URL 与绝对路径原样返回。 */
@@ -40,6 +41,32 @@ export function bridgeFor(ctx: AftToolContext): AftProjectTransport {
40
41
  return ctx.pool.getBridge(ctx.cwd);
41
42
  }
42
43
 
44
+ /** 人类视角的调用记录:input params + 与 LLM 相同的输出结果。 */
45
+ export function buildPendantMarkdown(params: {
46
+ title: string;
47
+ input: unknown;
48
+ output: string;
49
+ truncated?: boolean;
50
+ }): string {
51
+ const lines = [
52
+ `## ${params.title}`,
53
+ "",
54
+ "**Input**",
55
+ "",
56
+ "```json",
57
+ JSON.stringify(params.input, null, 2),
58
+ "```",
59
+ "",
60
+ "**Output**",
61
+ "",
62
+ params.output,
63
+ ];
64
+ if (params.truncated) {
65
+ lines.push("", "_输出已截断_");
66
+ }
67
+ return lines.join("\n").trim();
68
+ }
69
+
43
70
  const OutlineParams = Type.Object(
44
71
  {
45
72
  target: Type.String({
@@ -88,9 +115,21 @@ export function registerOutlineTool(pi: ExtensionAPI, ctx: AftToolContext): void
88
115
  if (params.includeTests !== undefined) rawArgs.includeTests = params.includeTests;
89
116
 
90
117
  const { text, response } = await callAftTool(bridgeFor(ctx), "outline", rawArgs, extCtx);
118
+ const truncated = response.truncated === true;
91
119
  return {
92
120
  content: [{ type: "text", text }],
93
- details: { input: params, truncated: response.truncated === true },
121
+ details: {
122
+ truncated,
123
+ pendant: {
124
+ markdown: buildPendantMarkdown({
125
+ title: "aft_outline",
126
+ input: params,
127
+ output: text,
128
+ truncated,
129
+ }),
130
+ expanded: true,
131
+ } satisfies ToolPendant,
132
+ },
94
133
  };
95
134
  },
96
135
  });
@@ -193,9 +232,21 @@ export function registerZoomTool(pi: ExtensionAPI, ctx: AftToolContext): void {
193
232
  if (coerceBoolean(params.callgraph)) rawArgs.callgraph = true;
194
233
 
195
234
  const { text, response } = await callAftTool(bridgeFor(ctx), "zoom", rawArgs, extCtx);
235
+ const truncated = response.truncated === true;
196
236
  return {
197
237
  content: [{ type: "text", text }],
198
- details: { input: params, truncated: response.truncated === true },
238
+ details: {
239
+ truncated,
240
+ pendant: {
241
+ markdown: buildPendantMarkdown({
242
+ title: "aft_zoom",
243
+ input: params,
244
+ output: text,
245
+ truncated,
246
+ }),
247
+ expanded: true,
248
+ } satisfies ToolPendant,
249
+ },
199
250
  };
200
251
  },
201
252
  });
@@ -291,9 +342,21 @@ export function registerCallgraphTool(pi: ExtensionAPI, ctx: AftToolContext): vo
291
342
  formatCallgraphSections(params.op, response, PLAIN_CALLGRAPH_THEME, {
292
343
  includeUnresolved: coerceBoolean(params.includeUnresolved),
293
344
  }).join("\n");
345
+ const truncated = response.truncated === true;
294
346
  return {
295
347
  content: [{ type: "text", text: out }],
296
- details: { input: params, truncated: response.truncated === true },
348
+ details: {
349
+ truncated,
350
+ pendant: {
351
+ markdown: buildPendantMarkdown({
352
+ title: "aft_callgraph",
353
+ input: params,
354
+ output: out,
355
+ truncated,
356
+ }),
357
+ expanded: true,
358
+ } satisfies ToolPendant,
359
+ },
297
360
  };
298
361
  },
299
362
  });
@@ -354,9 +417,21 @@ export function registerSearchTool(pi: ExtensionAPI, ctx: AftToolContext): void
354
417
  transportTimeoutMs: SEMANTIC_INDEX_WAIT_TIMEOUT_MS + 60_000,
355
418
  keepBridgeOnTimeout: true,
356
419
  });
420
+ const truncated = response.truncated === true;
357
421
  return {
358
422
  content: [{ type: "text", text }],
359
- details: { input: params, truncated: response.truncated === true },
423
+ details: {
424
+ truncated,
425
+ pendant: {
426
+ markdown: buildPendantMarkdown({
427
+ title: "aft_search",
428
+ input: params,
429
+ output: text,
430
+ truncated,
431
+ }),
432
+ expanded: true,
433
+ } satisfies ToolPendant,
434
+ },
360
435
  };
361
436
  },
362
437
  });
@@ -108,7 +108,11 @@ export interface WriteGuardOptions {
108
108
  toolName: string;
109
109
  /** The resolved absolute target path (caller has already parsed its args). */
110
110
  absolutePath: string;
111
- change: PendingChange;
111
+ /**
112
+ * 待审批的变更内容;缺省时(如 aft_refactor/aft_import 无 preview diff)
113
+ * 审批对话框不展示 diff 预览,仅按路径审批。
114
+ */
115
+ change?: PendingChange;
112
116
  }
113
117
 
114
118
  /**
@@ -137,7 +141,7 @@ export async function guardWriteAccess(
137
141
  }
138
142
 
139
143
  while (true) {
140
- const diffPreview = await buildDiffPreview(absolutePath, opts.change);
144
+ const diffPreview = opts.change ? await buildDiffPreview(absolutePath, opts.change) : undefined;
141
145
  const title =
142
146
  `Model requests write access outside workspace:\n\n` +
143
147
  ` Tool: ${opts.toolName}\n` +