@trim21/personal-pi-extensions 0.1.446 → 0.1.450
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 +3 -3
- package/src/aft/bridge.ts +53 -12
- package/src/aft/callgraph.md +3 -1
- package/src/aft/config.ts +99 -26
- package/src/aft/index.ts +16 -5
- package/src/aft/outline.md +1 -1
- package/src/aft/refactor.md +46 -2
- package/src/aft/refactor.ts +3 -4
- package/src/aft/search.md +1 -1
- package/src/aft/tools.ts +1 -3
- package/src/aft/ast-edit.ts +0 -310
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trim21/personal-pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.450",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"node": ">=24"
|
|
95
95
|
},
|
|
96
96
|
"dependencies": {
|
|
97
|
-
"@cortexkit/aft-bridge": "0.
|
|
97
|
+
"@cortexkit/aft-bridge": "0.54.0",
|
|
98
98
|
"@mozilla/readability": "^0.6.0",
|
|
99
99
|
"@vscode/tree-sitter-wasm": "^0.3.1",
|
|
100
100
|
"jsonc-parser": "^3.3.1",
|
|
@@ -107,6 +107,6 @@
|
|
|
107
107
|
"web-tree-sitter": "^0.26.12"
|
|
108
108
|
},
|
|
109
109
|
"optionalDependencies": {
|
|
110
|
-
"@cortexkit/aft-linux-x64": "0.
|
|
110
|
+
"@cortexkit/aft-linux-x64": "0.54.0"
|
|
111
111
|
}
|
|
112
112
|
}
|
package/src/aft/bridge.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type BridgeRequestOptions,
|
|
14
14
|
createAftTransportPool,
|
|
15
15
|
findBinary,
|
|
16
|
+
inlineUserConfigTier,
|
|
16
17
|
readConfigTiers,
|
|
17
18
|
resolveCortexKitConfigPaths,
|
|
18
19
|
resolveCortexKitStorageRoot,
|
|
@@ -20,6 +21,8 @@ import {
|
|
|
20
21
|
} from "@cortexkit/aft-bridge";
|
|
21
22
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
22
23
|
|
|
24
|
+
import type { SemanticRemote } from "./config.js";
|
|
25
|
+
|
|
23
26
|
/** Pi 会话 ID:Rust 侧用它做 session 作用域(undo/checkpoint),感知工具可留空。 */
|
|
24
27
|
export function resolveSessionId(extCtx: ExtensionContext): string | undefined {
|
|
25
28
|
const manager = (extCtx as unknown as { sessionManager?: { getSessionId?: () => string } })
|
|
@@ -53,26 +56,64 @@ export interface AftPool {
|
|
|
53
56
|
/** semantic_search 等待索引构建完成的上限(与 Rust 侧 AFT_WAIT_FOR_SEMANTIC_READY_MS 一致)。 */
|
|
54
57
|
export const SEMANTIC_INDEX_WAIT_TIMEOUT_MS = 600_000;
|
|
55
58
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
59
|
+
/**
|
|
60
|
+
* 调用图存储未就绪时查询侧的内联等待窗口。Rust 侧默认 0(纯异步:立即返回
|
|
61
|
+
* callgraph_building),非零时查询会在窗口内加入同一个 single-flight 构建,
|
|
62
|
+
* 就绪后直接返回真实结果。窗口必须明显小于 aft-bridge 给 callgraph 的 60s
|
|
63
|
+
* transport 预算,否则客户端先超时并触发 bridge hang 升级。
|
|
64
|
+
*/
|
|
65
|
+
export const CALLGRAPH_BUILD_WAIT_MS = 30_000;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 本扩展替用户注入 embedding 密钥时使用的固定变量名。仅当用户只配了
|
|
69
|
+
* `semantic.api_key`(值)而没配 `semantic.api_key_env`(变量名)时生效。
|
|
70
|
+
*/
|
|
71
|
+
export const SEMANTIC_API_KEY_ENV = "AFT_SEMANTIC_API_KEY";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 创建 transport pool。每个项目根一个常驻 aft 进程,跨 session 共享。
|
|
75
|
+
*
|
|
76
|
+
* `semantic` 决定 embedding 密钥如何送达子进程:aft 只从配置里读 `api_key_env`
|
|
77
|
+
* 这个「变量名」,再自己 `env::var` 取值,所以提供值时必须连带把名字告诉它。
|
|
78
|
+
* 两者都缺省时不注入任何凭据(无鉴权端点直接可用)。
|
|
79
|
+
*/
|
|
80
|
+
export async function createAftPool(cwd: string, semantic?: SemanticRemote): Promise<AftPool> {
|
|
58
81
|
const binaryPath = await resolveAftBinary();
|
|
59
82
|
const paths = resolveCortexKitConfigPaths(cwd);
|
|
83
|
+
const childEnv: Record<string, string> = {
|
|
84
|
+
// Rust 侧 semantic_search 在索引 Building 时阻塞等待构建完成
|
|
85
|
+
// (main.rs wait_for_semantic_index_before_search),避免首次搜索拿到
|
|
86
|
+
// 词法 fallback 的部分结果。语义搜索未启用时该逻辑直接跳过。
|
|
87
|
+
AFT_WAIT_FOR_SEMANTIC_READY: "1",
|
|
88
|
+
AFT_WAIT_FOR_SEMANTIC_READY_MS: String(SEMANTIC_INDEX_WAIT_TIMEOUT_MS),
|
|
89
|
+
// 调用图冷构建与 watcher 重建时让查询内联等待就绪,而不是把
|
|
90
|
+
// callgraph_building 直接吐给模型(见 CALLGRAPH_BUILD_WAIT_MS 注释)。
|
|
91
|
+
AFT_CALLGRAPH_BUILD_WAIT_MS: String(CALLGRAPH_BUILD_WAIT_MS),
|
|
92
|
+
};
|
|
93
|
+
const config = [...readConfigTiers(paths)];
|
|
94
|
+
if (semantic?.apiKey !== undefined) {
|
|
95
|
+
// childEnv 叠加在继承的 process.env 之上,所以配置里的值优先于 shell 同名变量。
|
|
96
|
+
// 密钥只进子进程环境,不进日志与工具输出。
|
|
97
|
+
childEnv[semantic.apiKeyEnv ?? SEMANTIC_API_KEY_ENV] = semantic.apiKey;
|
|
98
|
+
if (semantic.apiKeyEnv === undefined) {
|
|
99
|
+
// 用户没指定变量名:追加一份 user tier(后应用的 doc 覆盖先应用的)让 aft
|
|
100
|
+
// 知道该读 SEMANTIC_API_KEY_ENV,否则它不知道去哪个变量取值。
|
|
101
|
+
config.push(
|
|
102
|
+
...inlineUserConfigTier(
|
|
103
|
+
{ semantic: { api_key_env: SEMANTIC_API_KEY_ENV } },
|
|
104
|
+
"pi:semantic-api-key",
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
60
109
|
const pool = await createAftTransportPool({
|
|
61
110
|
harness: "pi",
|
|
62
111
|
binaryPath,
|
|
63
|
-
poolOptions: {
|
|
64
|
-
childEnv: {
|
|
65
|
-
// Rust 侧 semantic_search 在索引 Building 时阻塞等待构建完成
|
|
66
|
-
// (main.rs wait_for_semantic_index_before_search),避免首次搜索拿到
|
|
67
|
-
// 词法 fallback 的部分结果。语义搜索未启用时该逻辑直接跳过。
|
|
68
|
-
AFT_WAIT_FOR_SEMANTIC_READY: "1",
|
|
69
|
-
AFT_WAIT_FOR_SEMANTIC_READY_MS: String(SEMANTIC_INDEX_WAIT_TIMEOUT_MS),
|
|
70
|
-
},
|
|
71
|
-
},
|
|
112
|
+
poolOptions: { childEnv },
|
|
72
113
|
configOverrides: {
|
|
73
114
|
storage_dir: resolveCortexKitStorageRoot(),
|
|
74
115
|
cortexkit_user_config_path: paths.userConfigPath,
|
|
75
|
-
config
|
|
116
|
+
config,
|
|
76
117
|
},
|
|
77
118
|
});
|
|
78
119
|
pool.setConfigureOverride("harness", "pi");
|
package/src/aft/callgraph.md
CHANGED
package/src/aft/config.ts
CHANGED
|
@@ -1,46 +1,119 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* aft.jsonc 最小读取:只关心 JS
|
|
3
|
-
*
|
|
2
|
+
* aft.jsonc 最小读取:只关心 JS 侧注册决策需要的字段(enabled / semantic_search /
|
|
3
|
+
* semantic.backend / semantic.base_url),其余字段由 Rust 侧从配置 tier 自行读取。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* 只读用户级配置:语义搜索涉及外部 embedding 端点与密钥,按 AFT 的信任边界只允许
|
|
6
|
+
* 用户级控制(项目级配置不应替用户开启语义搜索或注入 base_url)。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* 本仓库只接受外部 embedding 后端(openai_compatible / ollama):aft 默认后端
|
|
9
|
+
* fastembed 走本地 ONNX Runtime,内网镜像不提供该运行时,所以即便用户开了
|
|
10
|
+
* semantic_search,外部后端未就绪时也不注册 aft_search。
|
|
11
|
+
*
|
|
12
|
+
* JSONC 解析用微软 jsonc-parser(vscode 同源,处理注释与尾逗号),不手写解析器;
|
|
13
|
+
* 分两层:rawConfigSchema 只描述依赖的外部形状并做基础类型校验,toReadConfig 负责
|
|
14
|
+
* 补默认值与后端语义判定。
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
17
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
18
|
|
|
14
19
|
import { parse } from "jsonc-parser";
|
|
20
|
+
import { type Static, Type } from "typebox";
|
|
21
|
+
|
|
22
|
+
import { parseWithSchema } from "../lib/parse-with-schema.js";
|
|
23
|
+
|
|
24
|
+
/** aft 侧走 HTTP embeddings 端点的后端;fastembed(本地 ONNX)有意排除在外。 */
|
|
25
|
+
const REMOTE_SEMANTIC_BACKENDS = ["openai_compatible", "ollama"] as const;
|
|
26
|
+
|
|
27
|
+
export type RemoteSemanticBackend = (typeof REMOTE_SEMANTIC_BACKENDS)[number];
|
|
28
|
+
|
|
29
|
+
/** 就绪的外部 embedding 后端:类型受支持且配了 base_url。 */
|
|
30
|
+
export interface SemanticRemote {
|
|
31
|
+
backend: RemoteSemanticBackend;
|
|
32
|
+
baseUrl: string;
|
|
33
|
+
/**
|
|
34
|
+
* 用户自己指定的密钥环境变量名(可选覆盖)。配了就直接用该名,值由用户的
|
|
35
|
+
* shell 提供,或由 `apiKey` 注入到同名变量。
|
|
36
|
+
*/
|
|
37
|
+
apiKeyEnv: string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* 密钥值(可选)。aft 只认环境变量名,所以未配 `apiKeyEnv` 时由本扩展注入成
|
|
40
|
+
* 固定的内部变量名并告诉 aft 去读它。undefined = 无鉴权端点(自建网关、ollama
|
|
41
|
+
* 都可以不带 key)。
|
|
42
|
+
*/
|
|
43
|
+
apiKey: string | undefined;
|
|
44
|
+
}
|
|
15
45
|
|
|
16
46
|
export interface AftReadConfig {
|
|
17
47
|
enabled: boolean;
|
|
18
48
|
semanticSearch: boolean;
|
|
49
|
+
/** undefined = 外部后端未就绪(未配 backend、非远程后端、或缺 base_url)。 */
|
|
50
|
+
semanticRemote: SemanticRemote | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const DEFAULTS: AftReadConfig = {
|
|
54
|
+
enabled: true,
|
|
55
|
+
semanticSearch: false,
|
|
56
|
+
semanticRemote: undefined,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** 上游(aft)控制的形状:只声明本仓库真正读取的字段,未声明的键由解析层忽略。 */
|
|
60
|
+
const rawConfigSchema = Type.Object({
|
|
61
|
+
enabled: Type.Optional(Type.Boolean()),
|
|
62
|
+
semantic_search: Type.Optional(Type.Boolean()),
|
|
63
|
+
semantic: Type.Optional(
|
|
64
|
+
Type.Object({
|
|
65
|
+
backend: Type.Optional(Type.String()),
|
|
66
|
+
base_url: Type.Optional(Type.String()),
|
|
67
|
+
api_key_env: Type.Optional(Type.String()),
|
|
68
|
+
// 本扩展独有:aft 只读环境变量,密钥值由我们注入成 api_key_env 命名的变量。
|
|
69
|
+
api_key: Type.Optional(Type.String()),
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
type RawConfig = Static<typeof rawConfigSchema>;
|
|
75
|
+
|
|
76
|
+
function isRemoteBackend(value: string): value is RemoteSemanticBackend {
|
|
77
|
+
return (REMOTE_SEMANTIC_BACKENDS as readonly string[]).includes(value);
|
|
19
78
|
}
|
|
20
79
|
|
|
21
|
-
|
|
80
|
+
function trimmedOrUndefined(value: string | undefined): string | undefined {
|
|
81
|
+
const trimmed = value?.trim();
|
|
82
|
+
return trimmed === undefined || trimmed === "" ? undefined : trimmed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function remoteOf(semantic: RawConfig["semantic"]): SemanticRemote | undefined {
|
|
86
|
+
const backend = trimmedOrUndefined(semantic?.backend);
|
|
87
|
+
// 去掉尾斜杠:aft 侧按 base_url + /embeddings 拼请求。
|
|
88
|
+
const baseUrl = trimmedOrUndefined(semantic?.base_url)?.replace(/\/+$/, "");
|
|
89
|
+
if (backend === undefined || baseUrl === undefined) return undefined;
|
|
90
|
+
if (!isRemoteBackend(backend)) return undefined;
|
|
91
|
+
return {
|
|
92
|
+
backend,
|
|
93
|
+
baseUrl,
|
|
94
|
+
apiKeyEnv: trimmedOrUndefined(semantic?.api_key_env),
|
|
95
|
+
apiKey: trimmedOrUndefined(semantic?.api_key),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
22
98
|
|
|
23
|
-
function
|
|
99
|
+
function toReadConfig(raw: RawConfig): AftReadConfig {
|
|
100
|
+
return {
|
|
101
|
+
enabled: raw.enabled ?? DEFAULTS.enabled,
|
|
102
|
+
semanticSearch: raw.semantic_search ?? DEFAULTS.semanticSearch,
|
|
103
|
+
semanticRemote: remoteOf(raw.semantic),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 读用户级 aft.jsonc。文件缺失、非法 JSONC 或形状不符(含字段类型错误)时整体回退
|
|
109
|
+
* 默认值:注册决策宁可保守,不该因配置写错而让扩展加载失败。
|
|
110
|
+
*/
|
|
111
|
+
export function loadAftConfig(userConfigPath: string): AftReadConfig {
|
|
24
112
|
try {
|
|
25
|
-
if (!existsSync(
|
|
26
|
-
const parsed: unknown = parse(readFileSync(
|
|
27
|
-
|
|
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
|
-
};
|
|
113
|
+
if (!existsSync(userConfigPath)) return DEFAULTS;
|
|
114
|
+
const parsed: unknown = parse(readFileSync(userConfigPath, "utf8"));
|
|
115
|
+
return toReadConfig(parseWithSchema(rawConfigSchema, parsed ?? {}));
|
|
38
116
|
} catch {
|
|
39
117
|
return DEFAULTS;
|
|
40
118
|
}
|
|
41
119
|
}
|
|
42
|
-
|
|
43
|
-
/** 读用户级 aft.jsonc。解析失败或文件缺失时回退默认值(不阻塞扩展加载)。 */
|
|
44
|
-
export function loadAftConfig(userConfigPath: string): AftReadConfig {
|
|
45
|
-
return readOne(userConfigPath);
|
|
46
|
-
}
|
package/src/aft/index.ts
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* + aft_refactor / aft_import(workspace-wide 重构与 import 管理,路径级写保护)。
|
|
4
4
|
*
|
|
5
5
|
* 感知工具只读,不触碰本仓库自己的 read/write/edit/bash 工具及其安全机制
|
|
6
|
-
* (bwrap 沙箱、write-guard、reads 记账)。aft_search 仅当用户级
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* (bwrap 沙箱、write-guard、reads 记账)。aft_search 仅当用户级 aft.jsonc 开启
|
|
7
|
+
* semantic_search 且配好外部 embedding 后端(semantic.backend 为
|
|
8
|
+
* openai_compatible / ollama 且有 base_url)时注册;aft 默认的本地 ONNX
|
|
9
|
+
* fastembed 后端不使用。aft_refactor / aft_import 的 Rust 命令不支持 preview,
|
|
9
10
|
* 写保护退化为路径级审批。
|
|
10
11
|
*
|
|
11
12
|
* 二进制缺失或 pool 创建失败时降级:不注册任何工具并在 session 开始时报
|
|
@@ -36,7 +37,7 @@ export default async function aftReadTools(pi: ExtensionAPI): Promise<void> {
|
|
|
36
37
|
|
|
37
38
|
let pool: AftPool;
|
|
38
39
|
try {
|
|
39
|
-
pool = await createAftPool(cwd);
|
|
40
|
+
pool = await createAftPool(cwd, cfg.semanticRemote);
|
|
40
41
|
} catch (error) {
|
|
41
42
|
const message = error instanceof Error ? error.message : String(error);
|
|
42
43
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -55,7 +56,17 @@ export default async function aftReadTools(pi: ExtensionAPI): Promise<void> {
|
|
|
55
56
|
registerRefactorTool(pi, toolCtx);
|
|
56
57
|
registerImportTool(pi, toolCtx);
|
|
57
58
|
if (cfg.semanticSearch) {
|
|
58
|
-
|
|
59
|
+
if (cfg.semanticRemote) {
|
|
60
|
+
registerSearchTool(pi, toolCtx);
|
|
61
|
+
} else {
|
|
62
|
+
// 只开了开关、没配外部 embedding 后端:与其静默不注册,不如说明缺什么。
|
|
63
|
+
pi.on("session_start", (_event, ctx) => {
|
|
64
|
+
ctx.ui.notify(
|
|
65
|
+
"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.",
|
|
66
|
+
"warning",
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
// 关闭 bridge pool。session_shutdown 是 pi 的正常生命周期;beforeExit 兜底
|
package/src/aft/outline.md
CHANGED
package/src/aft/refactor.md
CHANGED
|
@@ -2,8 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
workspace-wide 重构:跨文件移动符号、抽函数、内联,自动更新引用与 import。
|
|
4
4
|
|
|
5
|
-
- `move`:把顶层符号(非嵌套函数/类方法)移到另一个文件,全 workspace 重写 import
|
|
5
|
+
- `move`:把顶层符号(非嵌套函数/类方法)移到另一个文件,全 workspace 重写 import 与引用。需要 `symbol` + `destination`;同名符号歧义时用 `scope` 消歧。
|
|
6
6
|
- `extract`:把行区间抽成新函数(TS/JS/TSX、Python)。需要 `name` + `start_line` + `end_line`(1 起,含端点)。
|
|
7
7
|
- `inline`:把调用点替换为函数体。需要 `symbol` + `call_site_line`(1 起)。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
重构前先看影响面:对目标符号跑 aft_callgraph `impact`,确认波及范围符合预期再动手。
|
|
10
|
+
|
|
11
|
+
#### 示例
|
|
12
|
+
|
|
13
|
+
`path` 相对项目根,行号 1 起且含端点。
|
|
14
|
+
|
|
15
|
+
跨文件移动顶层符号,自动重写全部 import 与引用:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"op": "move",
|
|
20
|
+
"path": "src/aft/tools.ts",
|
|
21
|
+
"symbol": "buildPendantMarkdown",
|
|
22
|
+
"destination": "src/lib/pendant.ts"
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
把行区间抽成新函数(先用 aft_outline 或 Read 定位区间):
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"op": "extract",
|
|
31
|
+
"path": "src/aft/refactor.ts",
|
|
32
|
+
"name": "buildRefactorArgs",
|
|
33
|
+
"start_line": 121,
|
|
34
|
+
"end_line": 134
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`start_line` / `end_line` 指 `path` 文件里被抽取的代码,不是新函数所在的行。
|
|
39
|
+
|
|
40
|
+
内联单个调用点:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"op": "inline",
|
|
45
|
+
"path": "src/aft/refactor.ts",
|
|
46
|
+
"symbol": "requireField",
|
|
47
|
+
"call_site_line": 98
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`call_site_line` 是调用发生的那一行,不是函数定义行;只替换指定那一处,内联掉全部调用点后原定义不会被自动删除,需要自己清理。
|
|
52
|
+
|
|
53
|
+
只支持顶层符号;嵌套函数与类方法不适用。
|
package/src/aft/refactor.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 参数经 bridge.toolCall 以 agent 工具名 "refactor" 分派,Rust 侧 subc 翻译层
|
|
5
5
|
* 按 op 转成内部命令(move_symbol / extract_function / inline_symbol)。
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* 这些命令不支持 preview,写保护退化为路径级审批(workspace 内自动放行,
|
|
7
|
+
* 外部路径经 write-guard 确认,无 diff 预览)。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
@@ -71,10 +71,9 @@ export function registerRefactorTool(pi: ExtensionAPI, ctx: AftToolContext): voi
|
|
|
71
71
|
label: "aft_refactor",
|
|
72
72
|
description: [
|
|
73
73
|
"workspace-wide 重构:更新跨文件的 import 与引用。",
|
|
74
|
-
"move:把顶层符号(非嵌套函数/类方法)移到另一文件,全 workspace 重写 import
|
|
74
|
+
"move:把顶层符号(非嵌套函数/类方法)移到另一文件,全 workspace 重写 import。",
|
|
75
75
|
"extract:把行区间抽成新函数(TS/JS/TSX、Python)。",
|
|
76
76
|
"inline:把调用点替换为函数体。",
|
|
77
|
-
"move / rename 整个文件用 aft_move(OS 层操作,不更新引用);移动代码符号用本工具 op=move。",
|
|
78
77
|
].join("\n"),
|
|
79
78
|
promptSnippet: "Workspace-wide symbol move / function extraction / inlining",
|
|
80
79
|
promptGuidelines: [REFACTOR_PROMPT],
|
package/src/aft/search.md
CHANGED
package/src/aft/tools.ts
CHANGED
|
@@ -389,9 +389,7 @@ export function registerSearchTool(pi: ExtensionAPI, ctx: AftToolContext): void
|
|
|
389
389
|
"一个工具完成代码搜索:概念、标识符、错误串、正则、字面量、文件名自动路由到合适的引擎并按相关度排序。",
|
|
390
390
|
"概念类查询('ORM 如何构建并执行查询')用自然语言整句——语义通道理解意图并匹配 docstring 和注释;",
|
|
391
391
|
"精确名字、字符串、正则保持简短('^export'、'Cargo.lock')。",
|
|
392
|
-
"
|
|
393
|
-
"否则退化为词法/正则匹配通道。",
|
|
394
|
-
"启用语义索引时,首次调用会阻塞到索引构建完成,避免返回部分结果。",
|
|
392
|
+
"索引首次构建时本调用会阻塞到构建完成,避免返回部分结果。",
|
|
395
393
|
].join("\n"),
|
|
396
394
|
promptSnippet: "Search code by meaning or exact text",
|
|
397
395
|
promptGuidelines: [SEARCH_PROMPT],
|
package/src/aft/ast-edit.ts
DELETED
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ast_edit —— 符号级/AST 感知编辑工具。
|
|
3
|
-
*
|
|
4
|
-
* 用 AFT 的 edit 命令做符号级替换 / 模糊匹配 / 批量编辑,流程:
|
|
5
|
-
* 1. preview(只计算不写盘)拿所有变动文件的 before/after
|
|
6
|
-
* 2. write-guard:工作区内自动放行,外部路径用真实 diff 审批
|
|
7
|
-
* 3. 再次调用 edit(不带 preview)→ AFT 自己原子写盘(备份 + 格式化 +
|
|
8
|
-
* undo),本工具不再直接写文件
|
|
9
|
-
*
|
|
10
|
-
* 不维护 reads 记账:写盘后模型重新 Read 目标文件即可(与本仓库 Edit 的
|
|
11
|
-
* 防呆不同,ast_edit 不要求先读)。
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
type ExtensionAPI,
|
|
16
|
-
generateDiffString,
|
|
17
|
-
generateUnifiedPatch,
|
|
18
|
-
} from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import { Type } from "typebox";
|
|
20
|
-
import { Value } from "typebox/value";
|
|
21
|
-
|
|
22
|
-
import { requireAbsolutePath } from "../claude-code/common.js";
|
|
23
|
-
import { type ToolPendant } from "../lib/pendant.js";
|
|
24
|
-
import { guardWriteAccess } from "../lib/write-guard.js";
|
|
25
|
-
import { callAftTool } from "./bridge.js";
|
|
26
|
-
import { type AftToolContext, bridgeFor, buildPendantMarkdown } from "./tools.js";
|
|
27
|
-
|
|
28
|
-
// ── 纯函数(可测) ──────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
export interface PreviewFile {
|
|
31
|
-
/** 绝对路径;symbol 模式(顶层 diff)时为调用方传入的 file_path 兜底。 */
|
|
32
|
-
file: string;
|
|
33
|
-
before: string;
|
|
34
|
-
after: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface PreviewExtract {
|
|
38
|
-
files: PreviewFile[];
|
|
39
|
-
/** 任一文件超过 512KB 未返回全文(diff.truncated)。 */
|
|
40
|
-
truncated: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** AFT preview diff 条目:preview 模式下默认带 before/after,>512KB 只带 truncated。 */
|
|
44
|
-
const previewDiffSchema = Type.Object({
|
|
45
|
-
before: Type.Optional(Type.String()),
|
|
46
|
-
after: Type.Optional(Type.String()),
|
|
47
|
-
truncated: Type.Optional(Type.Boolean()),
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
type PreviewDiff = { truncated: true } | { truncated: false; before: string; after: string };
|
|
51
|
-
|
|
52
|
-
function findDiffObject(value: unknown): PreviewDiff | undefined {
|
|
53
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
54
|
-
if (!Value.Check(previewDiffSchema, value)) return undefined;
|
|
55
|
-
if (value.truncated === true) return { truncated: true };
|
|
56
|
-
if (value.before === undefined || value.after === undefined) return undefined;
|
|
57
|
-
return { before: value.before, after: value.after, truncated: false };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 从 AFT edit preview 响应提取所有变动文件的 before/after,供写保护审批。
|
|
62
|
-
* 批量/单文件 edits/glob 模式在 `files[]`(每项带 `file` 与 `diff`);
|
|
63
|
-
* symbol 模式在顶层 `diff`(file 留空,由调用方兜底)。
|
|
64
|
-
*/
|
|
65
|
-
export function extractPreviewFiles(response: Record<string, unknown>): PreviewExtract {
|
|
66
|
-
const rawFiles = response.files;
|
|
67
|
-
if (Array.isArray(rawFiles) && rawFiles.length > 0) {
|
|
68
|
-
const files: PreviewFile[] = [];
|
|
69
|
-
let truncated = false;
|
|
70
|
-
for (const raw of rawFiles as unknown[]) {
|
|
71
|
-
if (typeof raw !== "object" || raw === null) continue;
|
|
72
|
-
const entry = raw as Record<string, unknown>;
|
|
73
|
-
const diff = findDiffObject(entry.diff);
|
|
74
|
-
if (!diff) continue;
|
|
75
|
-
if (diff.truncated) {
|
|
76
|
-
truncated = true;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
files.push({
|
|
80
|
-
file: typeof entry.file === "string" ? entry.file : "",
|
|
81
|
-
before: diff.before,
|
|
82
|
-
after: diff.after,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
if (truncated || files.length > 0) return { files, truncated };
|
|
86
|
-
}
|
|
87
|
-
const top = findDiffObject(response.diff);
|
|
88
|
-
if (top) {
|
|
89
|
-
if (top.truncated) return { files: [], truncated: true };
|
|
90
|
-
return {
|
|
91
|
-
files: [{ file: "", before: top.before, after: top.after }],
|
|
92
|
-
truncated: false,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
return { files: [], truncated: false };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** 把本工具的参数名映射到 AFT edit wire 格式。 */
|
|
99
|
-
export function mapEditItems(
|
|
100
|
-
items: {
|
|
101
|
-
old_string?: string;
|
|
102
|
-
new_string?: string;
|
|
103
|
-
start_line?: number | string;
|
|
104
|
-
end_line?: number | string;
|
|
105
|
-
content?: string;
|
|
106
|
-
occurrence?: number | string;
|
|
107
|
-
}[],
|
|
108
|
-
): Record<string, unknown>[] {
|
|
109
|
-
return items.map((item) => {
|
|
110
|
-
const out: Record<string, unknown> = {};
|
|
111
|
-
if (item.old_string !== undefined) out.oldString = item.old_string;
|
|
112
|
-
if (item.new_string !== undefined) out.newString = item.new_string;
|
|
113
|
-
if (item.start_line !== undefined) out.startLine = item.start_line;
|
|
114
|
-
if (item.end_line !== undefined) out.endLine = item.end_line;
|
|
115
|
-
if (item.content !== undefined) out.content = item.content;
|
|
116
|
-
if (item.occurrence !== undefined) out.occurrence = item.occurrence;
|
|
117
|
-
return out;
|
|
118
|
-
});
|
|
119
|
-
}
|
|
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
|
-
|
|
150
|
-
// ── 工具 ────────────────────────────────────────────────────────────────────
|
|
151
|
-
|
|
152
|
-
const EditItemParams = Type.Object({
|
|
153
|
-
old_string: Type.Optional(Type.String({ description: "要替换的文本" })),
|
|
154
|
-
new_string: Type.Optional(Type.String({ description: "替换后的文本" })),
|
|
155
|
-
start_line: Type.Optional(
|
|
156
|
-
Type.Union([Type.Number(), Type.String()], {
|
|
157
|
-
description: "行范围编辑:起始行(1 起)",
|
|
158
|
-
}),
|
|
159
|
-
),
|
|
160
|
-
end_line: Type.Optional(
|
|
161
|
-
Type.Union([Type.Number(), Type.String()], {
|
|
162
|
-
description: "行范围编辑:结束行",
|
|
163
|
-
}),
|
|
164
|
-
),
|
|
165
|
-
content: Type.Optional(Type.String({ description: "替换内容;空字符串删除这些行" })),
|
|
166
|
-
occurrence: Type.Optional(
|
|
167
|
-
Type.Union([Type.Number(), Type.String()], {
|
|
168
|
-
description: "第几次匹配(0 起);多匹配时指定",
|
|
169
|
-
}),
|
|
170
|
-
),
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const AstEditParams = Type.Object(
|
|
174
|
-
{
|
|
175
|
-
file_path: Type.String({
|
|
176
|
-
description:
|
|
177
|
-
"要修改的文件(绝对路径);配合 old_string+replace_all 可传绝对路径 glob(如 /path/src/**/*.ts)批量替换多个文件",
|
|
178
|
-
}),
|
|
179
|
-
old_string: Type.Optional(Type.String({ description: "精确/模糊匹配的旧文本" })),
|
|
180
|
-
new_string: Type.Optional(Type.String({ description: "替换后的新文本" })),
|
|
181
|
-
replace_all: Type.Optional(Type.Boolean({ description: "替换所有匹配(默认 false)" })),
|
|
182
|
-
occurrence: Type.Optional(
|
|
183
|
-
Type.Union([Type.Number(), Type.String()], {
|
|
184
|
-
description: "第几次匹配(0 起);多匹配时指定",
|
|
185
|
-
}),
|
|
186
|
-
),
|
|
187
|
-
symbol: Type.Optional(Type.String({ description: "要替换的符号名(函数/类等)" })),
|
|
188
|
-
content: Type.Optional(Type.String({ description: "符号的新实现内容" })),
|
|
189
|
-
edits: Type.Optional(
|
|
190
|
-
Type.Array(EditItemParams, {
|
|
191
|
-
description: "批量编辑(原子应用,全部成功或全部失败)",
|
|
192
|
-
}),
|
|
193
|
-
),
|
|
194
|
-
append_content: Type.Optional(Type.String({ description: "追加到文件末尾" })),
|
|
195
|
-
},
|
|
196
|
-
{ additionalProperties: false },
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
function buildWireArgs(
|
|
200
|
-
params: {
|
|
201
|
-
old_string?: string;
|
|
202
|
-
new_string?: string;
|
|
203
|
-
replace_all?: boolean;
|
|
204
|
-
occurrence?: number | string;
|
|
205
|
-
symbol?: string;
|
|
206
|
-
content?: string;
|
|
207
|
-
edits?: Record<string, unknown>[];
|
|
208
|
-
append_content?: string;
|
|
209
|
-
},
|
|
210
|
-
filePath: string,
|
|
211
|
-
): Record<string, unknown> {
|
|
212
|
-
const rawArgs: Record<string, unknown> = { path: filePath };
|
|
213
|
-
const modes = [
|
|
214
|
-
params.old_string !== undefined,
|
|
215
|
-
params.symbol !== undefined,
|
|
216
|
-
params.edits !== undefined,
|
|
217
|
-
params.append_content !== undefined,
|
|
218
|
-
].filter(Boolean).length;
|
|
219
|
-
if (modes !== 1) {
|
|
220
|
-
throw new Error(
|
|
221
|
-
"Provide exactly one mode: old_string+new_string, symbol+content, edits, or append_content",
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
if (params.old_string !== undefined) {
|
|
225
|
-
if (params.new_string === undefined) {
|
|
226
|
-
throw new Error("'new_string' is required with 'old_string'");
|
|
227
|
-
}
|
|
228
|
-
rawArgs.oldString = params.old_string;
|
|
229
|
-
rawArgs.newString = params.new_string;
|
|
230
|
-
if (params.replace_all) rawArgs.replaceAll = true;
|
|
231
|
-
if (params.occurrence !== undefined) rawArgs.occurrence = params.occurrence;
|
|
232
|
-
} else if (params.symbol !== undefined) {
|
|
233
|
-
if (params.content === undefined) {
|
|
234
|
-
throw new Error("'content' is required with 'symbol'");
|
|
235
|
-
}
|
|
236
|
-
rawArgs.symbol = params.symbol;
|
|
237
|
-
rawArgs.content = params.content;
|
|
238
|
-
} else if (params.edits !== undefined) {
|
|
239
|
-
rawArgs.edits = mapEditItems(params.edits);
|
|
240
|
-
} else if (params.append_content !== undefined) {
|
|
241
|
-
rawArgs.appendContent = params.append_content;
|
|
242
|
-
}
|
|
243
|
-
return rawArgs;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function registerAstEditTool(pi: ExtensionAPI, ctx: AftToolContext): void {
|
|
247
|
-
pi.registerTool({
|
|
248
|
-
name: "ast_edit",
|
|
249
|
-
label: "ast_edit",
|
|
250
|
-
description: [
|
|
251
|
-
"符号级/AST 感知编辑:按符号名替换函数/类实现,或做模糊匹配/批量/追加编辑。",
|
|
252
|
-
"与 Edit 的区别:Edit 是精确字符串替换;ast_edit 支持 symbol+content(符号级,包含装饰器/注释/属性)、",
|
|
253
|
-
"edits[] 批量(原子)、fuzzy 匹配(4 步容错)与 append_content 追加。",
|
|
254
|
-
"old_string + replace_all 且 file_path 为 glob 时批量替换多个文件(结果为所有变动文件)。",
|
|
255
|
-
"写盘由 AFT 原子完成(自动备份、格式化);调用前先以 preview 计算 diff 套用工作区写保护。",
|
|
256
|
-
"不要求先 Read(与本仓库 Edit 不同);写入后如需确认可重新 Read 文件。四种模式互斥:",
|
|
257
|
-
" • old_string + new_string(+ replace_all / occurrence;glob 批量需 replace_all)",
|
|
258
|
-
" • symbol + content",
|
|
259
|
-
" • edits[](每项 old_string+new_string 或 start_line+end_line+content)",
|
|
260
|
-
" • append_content",
|
|
261
|
-
].join("\n"),
|
|
262
|
-
promptSnippet: "Symbol-aware / fuzzy file edits via AFT",
|
|
263
|
-
parameters: AstEditParams,
|
|
264
|
-
async execute(_id, params, _signal, _onUpdate, extCtx) {
|
|
265
|
-
const filePath = requireAbsolutePath(params.file_path);
|
|
266
|
-
const rawArgs = buildWireArgs(params, filePath);
|
|
267
|
-
|
|
268
|
-
// 第一步:preview(不写盘)拿所有变动文件的 before/after,供写保护审批。
|
|
269
|
-
// preview 必须经 options 传入(bridge 会放到 tool_call 消息顶层),
|
|
270
|
-
// 放进 arguments 会被 AFT 忽略并真的写盘。
|
|
271
|
-
const { response } = await callAftTool(bridgeFor(ctx), "edit", rawArgs, extCtx, {
|
|
272
|
-
preview: true,
|
|
273
|
-
});
|
|
274
|
-
const { files: previewFiles, truncated } = extractPreviewFiles(response);
|
|
275
|
-
if (truncated) {
|
|
276
|
-
throw new Error("ast_edit: file too large for preview (over 512KB)");
|
|
277
|
-
}
|
|
278
|
-
if (previewFiles.length === 0) {
|
|
279
|
-
throw new Error("ast_edit: preview did not return before/after content");
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// 第二步:写保护——工作区内自动放行,外部路径用真实 diff 审批。
|
|
283
|
-
for (const preview of previewFiles) {
|
|
284
|
-
await guardWriteAccess(extCtx, {
|
|
285
|
-
toolName: "ast_edit",
|
|
286
|
-
absolutePath: preview.file || filePath,
|
|
287
|
-
change: { oldText: preview.before, newText: preview.after },
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// 第三步:AFT 原子写盘(备份 + 格式化 + undo),结果由 Rust 格式化返回。
|
|
292
|
-
const { text } = await callAftTool(bridgeFor(ctx), "edit", rawArgs, extCtx);
|
|
293
|
-
const files = previewFiles.map((f) => f.file || filePath);
|
|
294
|
-
return {
|
|
295
|
-
content: [{ type: "text", text }],
|
|
296
|
-
details: {
|
|
297
|
-
files,
|
|
298
|
-
...buildEditDiffDetails(previewFiles, filePath),
|
|
299
|
-
pendant: {
|
|
300
|
-
markdown: buildPendantMarkdown({
|
|
301
|
-
title: "ast_edit",
|
|
302
|
-
input: params,
|
|
303
|
-
output: text,
|
|
304
|
-
}),
|
|
305
|
-
} satisfies ToolPendant,
|
|
306
|
-
},
|
|
307
|
-
};
|
|
308
|
-
},
|
|
309
|
-
});
|
|
310
|
-
}
|