@trim21/personal-pi-extensions 0.0.304 → 0.0.306
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 +1 -1
- package/src/aft/ast-edit.ts +35 -1
- package/src/aft/imports.ts +125 -0
- package/src/aft/index.ts +8 -2
- package/src/aft/refactor.ts +144 -0
- package/src/lib/write-guard.ts +6 -2
package/package.json
CHANGED
package/src/aft/ast-edit.ts
CHANGED
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
* 防呆不同,ast_edit 不要求先读)。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import
|
|
14
|
+
import {
|
|
15
|
+
type ExtensionAPI,
|
|
16
|
+
generateDiffString,
|
|
17
|
+
generateUnifiedPatch,
|
|
18
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
19
|
import { Type } from "typebox";
|
|
16
20
|
import { Value } from "typebox/value";
|
|
17
21
|
|
|
@@ -114,6 +118,35 @@ export function mapEditItems(
|
|
|
114
118
|
});
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
/**
|
|
122
|
+
* 参照 Edit 工具的结果输出:把 preview 的 before/after 转成行号 diff、
|
|
123
|
+
* unified patch 与首个变更行。多文件(glob 批量)时逐文件拼接 diff/patch,
|
|
124
|
+
* firstChangedLine 取首个文件的。
|
|
125
|
+
*/
|
|
126
|
+
export function buildEditDiffDetails(
|
|
127
|
+
previewFiles: PreviewFile[],
|
|
128
|
+
fallbackFile: string,
|
|
129
|
+
): { diff: string; patch: string; firstChangedLine: number | undefined } {
|
|
130
|
+
const parts = previewFiles.map((f) => {
|
|
131
|
+
const file = f.file || fallbackFile;
|
|
132
|
+
const diff = generateDiffString(f.before, f.after);
|
|
133
|
+
return {
|
|
134
|
+
file,
|
|
135
|
+
diff: diff.diff,
|
|
136
|
+
patch: generateUnifiedPatch(file, f.before, f.after),
|
|
137
|
+
firstChangedLine: diff.firstChangedLine,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
if (parts.length === 0) {
|
|
141
|
+
return { diff: "", patch: "", firstChangedLine: undefined };
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
diff: parts.map((p) => `--- ${p.file}\n${p.diff}`).join("\n"),
|
|
145
|
+
patch: parts.map((p) => `--- ${p.file}\n${p.patch}`).join("\n"),
|
|
146
|
+
firstChangedLine: parts[0]?.firstChangedLine,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
117
150
|
// ── 工具 ────────────────────────────────────────────────────────────────────
|
|
118
151
|
|
|
119
152
|
const EditItemParams = Type.Object({
|
|
@@ -262,6 +295,7 @@ export function registerAstEditTool(pi: ExtensionAPI, ctx: AftToolContext): void
|
|
|
262
295
|
content: [{ type: "text", text }],
|
|
263
296
|
details: {
|
|
264
297
|
files,
|
|
298
|
+
...buildEditDiffDetails(previewFiles, filePath),
|
|
265
299
|
pendant: {
|
|
266
300
|
markdown: buildPendantMarkdown({
|
|
267
301
|
title: "ast_edit",
|
|
@@ -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/lib/write-guard.ts
CHANGED
|
@@ -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
|
-
|
|
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` +
|