@trim21/personal-pi-extensions 0.1.460 → 0.1.463

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.
@@ -25,6 +25,7 @@ import type { ExtensionAPI, ExtensionUIContext } from "@earendil-works/pi-coding
25
25
  import { minimatch } from "minimatch";
26
26
  import { type Static, Type } from "typebox";
27
27
  import { Value } from "typebox/value";
28
+ import type { WorkspaceEdit } from "vscode-languageserver-types";
28
29
 
29
30
  import { type LspServerAdapter } from "./adapter.js";
30
31
  import {
@@ -32,6 +33,7 @@ import {
32
33
  type CreateInput,
33
34
  type Diagnostic,
34
35
  type Info as LspClient,
36
+ RenameNotPossibleError,
35
37
  WATCH_KIND_CHANGE,
36
38
  WATCH_KIND_CREATE,
37
39
  WATCH_KIND_DELETE,
@@ -273,6 +275,19 @@ export interface LspService {
273
275
  cwd: string,
274
276
  options?: LspRequestOptions,
275
277
  ): Promise<{ text: string; errorCount: number; warningCount: number }>;
278
+ /**
279
+ * 符号重命名:只面向 kind 为 "language" 的服务器(linter 不参与符号级
280
+ * 功能);多 client 按配置顺序取第一个成功结果,全部失败时抛聚合错误。
281
+ * line / character 为 0-based LSP position。
282
+ */
283
+ rename(request: {
284
+ file: string;
285
+ cwd: string;
286
+ line: number;
287
+ character: number;
288
+ newName: string;
289
+ options?: LspRequestOptions;
290
+ }): Promise<{ serverID: string; edit: WorkspaceEdit; placeholder?: string }>;
276
291
  shutdownAll(): Promise<void>;
277
292
  /** 停止全部服务器并禁用 LSP:之后工具调用不再 spawn,直到 start/reload。 */
278
293
  stop(): Promise<void>;
@@ -450,6 +465,7 @@ export function createLspService(
450
465
  file: string,
451
466
  cwd: string,
452
467
  notify?: ExtensionUIContext["notify"],
468
+ adapterFilter?: (adapter: LspServerAdapter) => boolean,
453
469
  ): Promise<LspClient[]> {
454
470
  if (state.closing || state.disabled) return [];
455
471
  if (!containsPath(file, cwd)) return [];
@@ -466,6 +482,7 @@ export function createLspService(
466
482
  }
467
483
 
468
484
  for (const adapter of active) {
485
+ if (adapterFilter && !adapterFilter(adapter)) continue;
469
486
  if (adapter.extensions.length > 0 && !adapter.extensions.includes(extension)) continue;
470
487
  const root = await adapter.findRoot(file, cwd);
471
488
  if (!root) continue;
@@ -626,6 +643,53 @@ export function createLspService(
626
643
  return { text: report(normalized, issues), errorCount, warningCount };
627
644
  }
628
645
 
646
+ /** 符号重命名:只面向 language 类服务器;按配置顺序取第一个成功结果。 */
647
+ async function rename(request: {
648
+ file: string;
649
+ cwd: string;
650
+ line: number;
651
+ character: number;
652
+ newName: string;
653
+ options?: LspRequestOptions;
654
+ }): Promise<{ serverID: string; edit: WorkspaceEdit; placeholder?: string }> {
655
+ const clients = await getClients(
656
+ request.file,
657
+ request.cwd,
658
+ request.options?.notify,
659
+ (adapter) => adapter.kind !== "linter",
660
+ );
661
+ if (clients.length === 0) {
662
+ throw new RenameNotPossibleError(
663
+ `no LSP language server available for ${request.file} (check lsp.json servers and kind)`,
664
+ );
665
+ }
666
+ const failures: { serverID: string; error: unknown }[] = [];
667
+ for (const client of clients) {
668
+ try {
669
+ const result = await client.renameSymbol({
670
+ path: request.file,
671
+ line: request.line,
672
+ character: request.character,
673
+ newName: request.newName,
674
+ });
675
+ return {
676
+ serverID: client.serverID,
677
+ edit: result.edit,
678
+ ...(result.placeholder !== undefined && { placeholder: result.placeholder }),
679
+ };
680
+ } catch (error) {
681
+ failures.push({ serverID: client.serverID, error });
682
+ }
683
+ }
684
+ const allNotRenameable = failures.every((f) => f.error instanceof RenameNotPossibleError);
685
+ const detail = failures
686
+ .map((f) => `${f.serverID}: ${f.error instanceof Error ? f.error.message : String(f.error)}`)
687
+ .join("; ");
688
+ throw allNotRenameable
689
+ ? new RenameNotPossibleError(detail)
690
+ : new Error(`LSP rename failed on all servers — ${detail}`);
691
+ }
692
+
629
693
  /** 关闭全部 client 并清空缓存;closing 置 true 让 in-flight spawn 自行退出。 */
630
694
  async function closeAll(): Promise<void> {
631
695
  state.closing = true;
@@ -687,6 +751,7 @@ export function createLspService(
687
751
  notifyFile,
688
752
  diagnostics,
689
753
  lspDiagnosticsForFile,
754
+ rename,
690
755
  shutdownAll,
691
756
  stop,
692
757
  start,
@@ -0,0 +1,224 @@
1
+ /**
2
+ * LSP WorkspaceEdit 应用与 rename 定位辅助。
3
+ *
4
+ * - expandWorkspaceEdit:把 rename 返回的 WorkspaceEdit 展开成每文件的
5
+ * old/new 文本(纯内存计算,写盘由调用方决定时机与方式);
6
+ * - lineCandidates:character 缺省时在一行内按词边界枚举候选位置,
7
+ * 供逐候选探测消歧;
8
+ * - canonicalizeEdit:把两次 rename 的结果归一化成稳定字符串,
9
+ * 比较它们是否指向同一个符号(同名歧义消解)。
10
+ *
11
+ * 位置语义按 LSP 规范:0-based 行列,character 为 UTF-16 code unit,
12
+ * 行内容不含行结束符(CRLF 的 \r 不计入列号)。
13
+ */
14
+
15
+ import { normalize } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ import type { TextEdit, WorkspaceEdit } from "vscode-languageserver-types";
19
+
20
+ /** 单个文件展开后的编辑结果。 */
21
+ export interface AppliedFileEdit {
22
+ readonly path: string;
23
+ readonly oldText: string;
24
+ readonly newText: string;
25
+ readonly changeCount: number;
26
+ }
27
+
28
+ export interface LspPosition {
29
+ readonly line: number;
30
+ readonly character: number;
31
+ }
32
+
33
+ /** 应用层只关心 range + newText(TextDocumentEdit 的编辑联合的公共形状)。 */
34
+ interface RangeLike {
35
+ readonly range: { readonly start: LspPosition; readonly end: LspPosition };
36
+ readonly newText: string;
37
+ }
38
+
39
+ /** 编辑联合里 SnippetTextEdit 没有 newText,应用层不支持且无法静默处理。 */
40
+ function isRangeLike(edit: unknown): edit is RangeLike {
41
+ return (
42
+ typeof edit === "object" &&
43
+ edit !== null &&
44
+ "range" in edit &&
45
+ "newText" in edit &&
46
+ typeof (edit as { newText: unknown }).newText === "string"
47
+ );
48
+ }
49
+
50
+ /** file:// URI → 规范化本地路径;非 file scheme 是服务器的意外行为,直接报错。 */
51
+ function toPath(uri: string): string {
52
+ if (!uri.startsWith("file:")) {
53
+ throw new Error(`lsp-rename: unsupported uri scheme: ${uri}`);
54
+ }
55
+ return normalize(fileURLToPath(uri));
56
+ }
57
+
58
+ /** 每行起始偏移(含第 0 行的 0);换行符是 \n,\r 属于行内容之外由调用侧处理。 */
59
+ function lineStartOffsets(text: string): number[] {
60
+ const starts = [0];
61
+ for (let i = 0; i < text.length; i++) {
62
+ if (text.codePointAt(i) === 10) starts.push(i + 1);
63
+ }
64
+ return starts;
65
+ }
66
+
67
+ /** 行内容长度(不含行结束符,CRLF 时连 \r 一起排除)。 */
68
+ function lineContentLength(text: string, starts: readonly number[], line: number): number {
69
+ const base = starts[line];
70
+ let end = line + 1 < starts.length ? starts[line + 1] - 1 : text.length;
71
+ if (end > base && text.codePointAt(end - 1) === 13) end--;
72
+ return end - base;
73
+ }
74
+
75
+ /**
76
+ * position → 字符串偏移。character 超出行长视为服务器返回了非法位置,
77
+ * 抛错而不是静默钳制(避免把编辑应用到错误位置)。
78
+ */
79
+ function positionToOffset(
80
+ text: string,
81
+ starts: readonly number[],
82
+ position: LspPosition,
83
+ path: string,
84
+ ): number {
85
+ if (position.line < 0 || position.line >= starts.length) {
86
+ throw new Error(
87
+ `lsp-rename: edit position out of range in ${path} (line ${position.line + 1})`,
88
+ );
89
+ }
90
+ const base = starts[position.line];
91
+ const length = lineContentLength(text, starts, position.line);
92
+ if (position.character < 0 || position.character > length) {
93
+ throw new Error(
94
+ `lsp-rename: edit position out of range in ${path} (character ${position.character + 1} on line ${position.line + 1})`,
95
+ );
96
+ }
97
+ return base + position.character;
98
+ }
99
+
100
+ /** 按起始位置降序应用(后文先改,避免前文编辑使后续偏移失效)。 */
101
+ function applyTextEdits(path: string, text: string, edits: readonly RangeLike[]): string {
102
+ const starts = lineStartOffsets(text);
103
+ let result = text;
104
+ const sorted = edits.toSorted(
105
+ (a, b) =>
106
+ b.range.start.line - a.range.start.line || b.range.start.character - a.range.start.character,
107
+ );
108
+ for (const edit of sorted) {
109
+ const start = positionToOffset(text, starts, edit.range.start, path);
110
+ const end = positionToOffset(text, starts, edit.range.end, path);
111
+ if (end < start) {
112
+ throw new Error(`lsp-rename: invalid edit range in ${path} (end before start)`);
113
+ }
114
+ result = result.slice(0, start) + edit.newText + result.slice(end);
115
+ }
116
+ return result;
117
+ }
118
+
119
+ /** 把 changes 与 documentChanges 里的 text edit 按(规范化)路径收集;文件级操作报不支持。 */
120
+ function collectTextEdits(edit: WorkspaceEdit): Map<string, RangeLike[]> {
121
+ const byPath = new Map<string, RangeLike[]>();
122
+ const push = (path: string, edits: readonly RangeLike[]): void => {
123
+ const existing = byPath.get(path);
124
+ if (existing) existing.push(...edits);
125
+ else byPath.set(path, [...edits]);
126
+ };
127
+ for (const [uri, edits] of Object.entries(edit.changes ?? {})) {
128
+ push(toPath(uri), edits.filter(isRangeLike));
129
+ }
130
+ for (const change of edit.documentChanges ?? []) {
131
+ if ("kind" in change || !("textDocument" in change) || !("edits" in change)) {
132
+ throw new Error(
133
+ "lsp-rename: file-level document changes (create/rename/delete) are not supported",
134
+ );
135
+ }
136
+ push(toPath(change.textDocument.uri), change.edits.filter(isRangeLike));
137
+ }
138
+ return byPath;
139
+ }
140
+
141
+ /**
142
+ * 展开成每文件的新旧文本。readText 由调用方提供(工具层传磁盘读取),
143
+ * 任一文件读取失败即整体失败,调用方可以安全地"全部算好再写盘"。
144
+ */
145
+ export async function expandWorkspaceEdit(
146
+ edit: WorkspaceEdit,
147
+ readText: (path: string) => Promise<string>,
148
+ ): Promise<AppliedFileEdit[]> {
149
+ const applied: AppliedFileEdit[] = [];
150
+ for (const [path, edits] of collectTextEdits(edit)) {
151
+ const oldText = await readText(path);
152
+ applied.push({
153
+ path,
154
+ oldText,
155
+ newText: applyTextEdits(path, oldText, edits),
156
+ changeCount: edits.length,
157
+ });
158
+ }
159
+ return applied;
160
+ }
161
+
162
+ /**
163
+ * 归一化 WorkspaceEdit 为稳定字符串(URI → 规范路径、结构化字段),供比较
164
+ * 两次 rename 的编辑集合是否一致(同一符号的多次出现 vs 不同符号)。
165
+ */
166
+ export function canonicalizeEdit(edit: WorkspaceEdit): string {
167
+ const changes: Record<string, TextEdit[]> = {};
168
+ const documentChanges: unknown[] = [];
169
+ for (const [uri, edits] of Object.entries(edit.changes ?? {})) {
170
+ changes[toPath(uri)] = edits;
171
+ }
172
+ for (const change of edit.documentChanges ?? []) {
173
+ if ("kind" in change || !("textDocument" in change) || !("edits" in change)) {
174
+ documentChanges.push(change);
175
+ continue;
176
+ }
177
+ documentChanges.push({
178
+ textDocument: { uri: toPath(change.textDocument.uri) },
179
+ edits: change.edits,
180
+ });
181
+ }
182
+ return JSON.stringify({ changes, documentChanges });
183
+ }
184
+
185
+ const WORD_PATTERN = /[\p{L}\p{N}_$]+/gu;
186
+
187
+ /**
188
+ * 一行内 `symbol` 的候选位置(0-based,列相对行首):
189
+ * - `character` 缺省:枚举行内与 `symbol` 相同的词出现位置;
190
+ * - `character` 给定:该列必须落在与 `symbol` 相同的词内(消歧时防改错目标),
191
+ * 否则抛参数错误。
192
+ * 该行找不到 `symbol` 时返回空数组。
193
+ */
194
+ export function symbolCandidates(
195
+ text: string,
196
+ line: number,
197
+ symbol: string,
198
+ character?: number,
199
+ ): LspPosition[] {
200
+ const starts = lineStartOffsets(text);
201
+ if (line < 0 || line >= starts.length) return [];
202
+ const base = starts[line];
203
+ const length = lineContentLength(text, starts, line);
204
+ const matches = [...text.slice(base, base + length).matchAll(WORD_PATTERN)];
205
+ if (character !== undefined) {
206
+ const target = matches.find((match) => {
207
+ const start = match.index;
208
+ return character >= start && character < start + match[0].length;
209
+ });
210
+ if (!target || target[0] !== symbol) {
211
+ const actual = target ? target[0] : "";
212
+ throw new Error(
213
+ `character ${character + 1} on line ${line + 1} does not point at symbol '${symbol}'` +
214
+ (actual === "" ? "" : ` (points at '${actual}')`),
215
+ );
216
+ }
217
+ return [{ line, character: target.index }];
218
+ }
219
+ const candidates: LspPosition[] = [];
220
+ for (const match of matches) {
221
+ if (match[0] === symbol) candidates.push({ line, character: match.index });
222
+ }
223
+ return candidates;
224
+ }
@@ -18,13 +18,20 @@ import { isAbsolute, join, relative, sep } from "node:path";
18
18
  import { minimatch } from "minimatch";
19
19
  import { type Static, Type } from "typebox";
20
20
 
21
- import { type LspServerAdapter, type LspServerHandle, nearestRoot } from "./adapter.js";
21
+ import {
22
+ type LspServerAdapter,
23
+ type LspServerHandle,
24
+ nearestRoot,
25
+ type ServerKind,
26
+ } from "./adapter.js";
22
27
  import { exists, findBinaryInWorkspace, which } from "./bin.js";
23
28
  import { spawnProcess } from "./launch.js";
24
29
 
25
30
  export const serverConfigSchema = Type.Object({
26
31
  /** 文件 glob(相对项目根或调用 cwd,任一命中即可);缺省匹配所有文件。 */
27
32
  include: Type.Optional(Type.Array(Type.String())),
33
+ /** 服务器类型:language(真语言服务器,缺省)或 linter(只实现 LSP 协议的 lint)。 */
34
+ kind: Type.Optional(Type.Union([Type.Literal("language"), Type.Literal("linter")])),
28
35
  /** 项目根标记文件(从文件目录向上查找);缺省用调用 cwd 作为根。 */
29
36
  rootMarkers: Type.Optional(Type.Array(Type.String())),
30
37
  /** 可执行文件:绝对路径、相对调用 cwd 的路径,或名字(项目工作区优先,PATH 兜底)。 */
@@ -111,6 +118,7 @@ function matchesInclude(
111
118
  /** 由配置构建的通用 adapter;include 精确过滤在 findRoot 完成,extensions 不设扩展名过滤。 */
112
119
  export class ConfigAdapter implements LspServerAdapter {
113
120
  readonly id: string;
121
+ readonly kind: ServerKind;
114
122
  readonly extensions: readonly string[] = [];
115
123
  readonly startupTimeoutMs: number | undefined;
116
124
  readonly diagnosticsWaitMs: number | undefined;
@@ -119,6 +127,7 @@ export class ConfigAdapter implements LspServerAdapter {
119
127
  constructor(id: string, config: ServerConfig) {
120
128
  this.id = id;
121
129
  this.config = config;
130
+ this.kind = config.kind ?? "language";
122
131
  this.startupTimeoutMs = config.startupTimeoutMs;
123
132
  this.diagnosticsWaitMs = config.diagnosticsWaitMs;
124
133
  }
package/src/lib/path.ts CHANGED
@@ -47,6 +47,14 @@ export async function resolveWorkdir(workdir: string, baseDir: string): Promise<
47
47
  return target;
48
48
  }
49
49
 
50
+ /** 解析 `~` 前缀与相对路径(相对调用 cwd);绝对路径原样返回。 */
51
+ export function resolvePathArg(cwd: string, input: string): string {
52
+ if (input === "~" || input.startsWith("~/")) {
53
+ return join(homedir(), input.slice(1));
54
+ }
55
+ return isAbsolute(input) ? input : resolve(cwd, input);
56
+ }
57
+
50
58
  /** 人类可读的显示路径:cwd 内用 `./…`,home 内用 `~/…`,否则原样绝对路径。 */
51
59
  export function formatDisplayPath(cwd: string, filePath: string): string {
52
60
  const relToCwd = relative(cwd, filePath);
@@ -113,8 +113,7 @@ export interface WriteGuardOptions {
113
113
  /** The resolved absolute target path (caller has already parsed its args). */
114
114
  absolutePath: string;
115
115
  /**
116
- * 待审批的变更内容;缺省时(如 aft_refactor/aft_import 无 preview diff)
117
- * 审批对话框不展示 diff 预览,仅按路径审批。
116
+ * 待审批的变更内容;缺省时审批对话框不展示 diff 预览,仅按路径审批。
118
117
  */
119
118
  change?: PendingChange;
120
119
  }
package/src/aft/import.md DELETED
@@ -1,12 +0,0 @@
1
- ### aft_import tool
2
-
3
- 语言感知的 import 管理:add / remove。
4
-
5
- 支持 TS, JS, TSX, Python, Rust, Go, Solidity, Java, C#, PHP, Kotlin, Scala, Swift, Ruby, Lua, C, C++, Perl, Vue。
6
-
7
- - `add`:添加默认导入(`default_import`)、具名导入(`names`,支持按名 `as` 别名,如 `['useState']`、`['ERC20', 'IERC20 as IToken']`)、命名空间导入(`namespace`,如 `import * as ns from 'mod'`)。
8
- - `remove`:移除整个 import(只给 `module`),或只移除其中某个具名导入(`remove_name`)。
9
- - 语言相关修饰符用 `modifiers`(如 Java/C# `static`、Swift `@testable`);符号类导入用 `import_kind`(如 PHP `function`/`const`、Swift `struct`/`enum`);仅类型导入用 `type_only`(仅 TS)。
10
- - `validate`:编辑后校验级别,默认 `syntax`,`full` 做完整校验。
11
-
12
- import 排序整理交给 lint(如 import/order + --fix),本工具不负责排序。写操作走路径级审批。
@@ -1,141 +0,0 @@
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 { readFileSync } from "node:fs";
10
- import { fileURLToPath } from "node:url";
11
-
12
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
- import { Type } from "typebox";
14
-
15
- import { type ToolPendant } from "../lib/pendant.js";
16
- import { guardWriteAccess } from "../lib/write-guard.js";
17
- import { callAftTool } from "./bridge.js";
18
- import {
19
- type AftToolContext,
20
- bridgeFor,
21
- buildPendantMarkdown,
22
- compactArgs,
23
- resolvePathArg,
24
- } from "./tools.js";
25
-
26
- /** 工具使用指南,以 markdown 形式维护,读起来像文档。 */
27
- const IMPORT_PROMPT = readFileSync(
28
- fileURLToPath(new URL("import.md", import.meta.url)),
29
- "utf8",
30
- ).trim();
31
-
32
- const IMPORT_OPS = ["add", "remove"] as const;
33
- const VALIDATE_LEVELS = ["syntax", "full"] as const;
34
-
35
- const ImportParams = Type.Object(
36
- {
37
- op: Type.Union(
38
- IMPORT_OPS.map((op) => Type.Literal(op)),
39
- { description: "import 操作" },
40
- ),
41
- path: Type.String({ description: "目标文件路径(绝对或相对项目根)" }),
42
- module: Type.Optional(
43
- Type.String({ description: "模块路径(add/remove 必填),如 'react'、'./utils'" }),
44
- ),
45
- names: Type.Optional(
46
- Type.Array(Type.String(), {
47
- description:
48
- "要添加的具名导入,用语言原生的具名导入写法,支持按名 `as` 别名,如 ['useState']、Solidity ['ERC20', 'IERC20 as IToken']",
49
- }),
50
- ),
51
- default_import: Type.Optional(Type.String({ description: "默认导入名(仅 ES),如 'React'" })),
52
- namespace: Type.Optional(
53
- Type.String({
54
- description:
55
- "命名空间绑定:`import * as ns from 'mod'`(ES)、`* as N from \"./X.sol\"`(Solidity)",
56
- }),
57
- ),
58
- alias: Type.Optional(
59
- Type.String({ description: '整模块别名。Solidity:`import "./X.sol" as X`' }),
60
- ),
61
- modifiers: Type.Optional(
62
- Type.Array(Type.String(), {
63
- description:
64
- "语句级修饰符,按语言校验:Java/C# 'static'、C# 'global'/'unsafe'、Java/Kotlin/Scala 'wildcard'、Swift '@testable'",
65
- }),
66
- ),
67
- import_kind: Type.Optional(
68
- Type.String({
69
- description:
70
- "符号类导入:PHP 'function'/'const'、Swift 'struct'/'class'/'enum'、Scala 'given'",
71
- }),
72
- ),
73
- remove_name: Type.Optional(
74
- Type.String({ description: "要移除的具名导入;缺省移除整个 import" }),
75
- ),
76
- type_only: Type.Optional(Type.Boolean({ description: "仅类型导入(仅 TS)" })),
77
- validate: Type.Optional(
78
- Type.Union(
79
- VALIDATE_LEVELS.map((level) => Type.Literal(level)),
80
- {
81
- description: "编辑后校验级别(默认 syntax)",
82
- },
83
- ),
84
- ),
85
- },
86
- { additionalProperties: false },
87
- );
88
-
89
- export function registerImportTool(pi: ExtensionAPI, ctx: AftToolContext): void {
90
- pi.registerTool({
91
- name: "aft_import",
92
- label: "aft_import",
93
- description: [
94
- "语言感知的 import 管理:add / remove。",
95
- "支持 TS, JS, TSX, Python, Rust, Go, Solidity, Java, C#, PHP, Kotlin, Scala, Swift, Ruby, Lua, C, C++, Perl, Vue。",
96
- "add:添加默认/具名/命名空间导入;remove:移除具名导入或整个 import。",
97
- "import 排序整理交给 lint(如 import/order + --fix),本工具不负责。",
98
- ].join("\n"),
99
- promptSnippet: "Language-aware import add / remove",
100
- promptGuidelines: [IMPORT_PROMPT],
101
- parameters: ImportParams,
102
- async execute(_id, params, _signal, _onUpdate, extCtx) {
103
- if (params.module === undefined || params.module.trim() === "") {
104
- throw new Error(`'module' is required for '${params.op}' op`);
105
- }
106
-
107
- const filePath = resolvePathArg(extCtx.cwd, params.path);
108
- await guardWriteAccess(extCtx, { toolName: "aft_import", absolutePath: filePath });
109
-
110
- const rawArgs = compactArgs({
111
- op: params.op,
112
- path: filePath,
113
- module: params.module,
114
- names: params.names,
115
- defaultImport: params.default_import,
116
- namespace: params.namespace,
117
- alias: params.alias,
118
- modifiers: params.modifiers,
119
- importKind: params.import_kind,
120
- removeName: params.remove_name,
121
- typeOnly: params.type_only,
122
- validate: params.validate,
123
- });
124
-
125
- const { text } = await callAftTool(bridgeFor(ctx), "import", rawArgs, extCtx);
126
- return {
127
- content: [{ type: "text", text }],
128
- details: {
129
- files: [filePath],
130
- pendant: {
131
- markdown: buildPendantMarkdown({
132
- title: "aft_import",
133
- input: params,
134
- output: text,
135
- }),
136
- } satisfies ToolPendant,
137
- },
138
- };
139
- },
140
- });
141
- }
@@ -1,53 +0,0 @@
1
- ### aft_refactor tool
2
-
3
- workspace-wide 重构:跨文件移动符号、抽函数、内联,自动更新引用与 import。
4
-
5
- - `move`:把顶层符号(非嵌套函数/类方法)移到另一个文件,全 workspace 重写 import 与引用。需要 `symbol` + `destination`;同名符号歧义时用 `scope` 消歧。
6
- - `extract`:把行区间抽成新函数(TS/JS/TSX、Python)。需要 `name` + `start_line` + `end_line`(1 起,含端点)。
7
- - `inline`:把调用点替换为函数体。需要 `symbol` + `call_site_line`(1 起)。
8
-
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
- 只支持顶层符号;嵌套函数与类方法不适用。