@trim21/personal-pi-extensions 0.1.500 → 0.1.502

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.1.500",
3
+ "version": "0.1.502",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
package/src/aft/bridge.ts CHANGED
@@ -150,6 +150,9 @@ export async function createAftPool(
150
150
  * 调用 AFT 工具命令并返回 Rust 格式化好的文本。
151
151
  * 只读感知工具统一走 toolCall(server 侧 tool_call 分派)。`softCodes` 里的
152
152
  * 错误码(如 symbol_not_found)是合法否定答案,不抛错、按文本返回。
153
+ *
154
+ * `signal`:宿主取消信号,透传给 bridge 的 abortSignal——standalone transport
155
+ * 会在 abort 时向 Rust 发 cancel_request(subc route 忽略,靠 route 关闭取消)。
153
156
  */
154
157
  export async function callAftTool(
155
158
  bridge: AftProjectTransport,
@@ -158,12 +161,14 @@ export async function callAftTool(
158
161
  extCtx: ExtensionContext,
159
162
  options?: BridgeRequestOptions & { preview?: boolean },
160
163
  softCodes?: ReadonlySet<string>,
164
+ signal?: AbortSignal,
161
165
  ): Promise<{ text: string; response: Record<string, unknown> }> {
162
166
  const timeoutMs = timeoutForCommand(command);
163
167
  const sessionId = resolveSessionId(extCtx);
164
168
  const sendOptions = {
165
169
  ...(timeoutMs !== undefined && { timeoutMs }),
166
170
  ...options,
171
+ ...(signal !== undefined && !options?.abortSignal && { abortSignal: signal }),
167
172
  };
168
173
  const response = await bridge.toolCall(
169
174
  sessionId,
package/src/aft/tools.ts CHANGED
@@ -126,7 +126,7 @@ export function registerOutlineTool(pi: ExtensionAPI, ctx: AftToolContext): void
126
126
  promptSnippet: "Output structural outline of a file/directory",
127
127
  promptGuidelines: [OUTLINE_PROMPT],
128
128
  parameters: OutlineParams,
129
- async execute(_id, params, _signal, _onUpdate, extCtx) {
129
+ async execute(_id, params, signal, _onUpdate, extCtx) {
130
130
  const target = coerceTargetParam(params.target);
131
131
  if (typeof target !== "string" || target.length === 0) {
132
132
  throw new Error("'target' must be a single path (array targets are not supported)");
@@ -145,7 +145,15 @@ export function registerOutlineTool(pi: ExtensionAPI, ctx: AftToolContext): void
145
145
 
146
146
  const subtitle = buildOutlineSubtitle(extCtx.cwd, target);
147
147
 
148
- const { text, response } = await callAftTool(bridgeFor(ctx), "outline", rawArgs, extCtx);
148
+ const { text, response } = await callAftTool(
149
+ bridgeFor(ctx),
150
+ "outline",
151
+ rawArgs,
152
+ extCtx,
153
+ undefined,
154
+ undefined,
155
+ signal,
156
+ );
149
157
  const truncated = response.truncated === true;
150
158
  return {
151
159
  content: [{ type: "text", text }],
@@ -201,7 +209,7 @@ export function registerZoomTool(pi: ExtensionAPI, ctx: AftToolContext): void {
201
209
  promptSnippet: "Inspect the full source of a named symbol",
202
210
  promptGuidelines: [ZOOM_PROMPT],
203
211
  parameters: ZoomParams,
204
- async execute(_id, params, _signal, _onUpdate, extCtx) {
212
+ async execute(_id, params, signal, _onUpdate, extCtx) {
205
213
  const rawArgs = compactArgs({
206
214
  filePath: resolvePathArg(extCtx.cwd, params.path),
207
215
  symbols: params.symbols,
@@ -216,7 +224,15 @@ export function registerZoomTool(pi: ExtensionAPI, ctx: AftToolContext): void {
216
224
 
217
225
  const subtitle = buildZoomSubtitle(extCtx.cwd, params);
218
226
 
219
- const { text, response } = await callAftTool(bridgeFor(ctx), "zoom", rawArgs, extCtx);
227
+ const { text, response } = await callAftTool(
228
+ bridgeFor(ctx),
229
+ "zoom",
230
+ rawArgs,
231
+ extCtx,
232
+ undefined,
233
+ undefined,
234
+ signal,
235
+ );
220
236
  const truncated = response.truncated === true;
221
237
  return {
222
238
  content: [{ type: "text", text }],
@@ -305,12 +321,16 @@ export async function callCallgraphWithBuildRetry(
305
321
  bridge: AftProjectTransport,
306
322
  rawArgs: Record<string, unknown>,
307
323
  extCtx: ExtensionContext,
324
+ signal?: AbortSignal,
308
325
  timing?: { budgetMs: number; intervalMs: number },
309
326
  ): Promise<{ text: string; response: Record<string, unknown> }> {
310
327
  const budgetMs = timing?.budgetMs ?? CALLGRAPH_BUILD_RETRY_BUDGET_MS;
311
328
  const intervalMs = timing?.intervalMs ?? CALLGRAPH_BUILD_RETRY_INTERVAL_MS;
312
329
  const deadline = Date.now() + budgetMs;
313
330
  for (;;) {
331
+ if (signal?.aborted) {
332
+ throw new Error("callgraph request aborted");
333
+ }
314
334
  const { text, response } = await callAftTool(
315
335
  bridge,
316
336
  "callgraph",
@@ -318,6 +338,7 @@ export async function callCallgraphWithBuildRetry(
318
338
  extCtx,
319
339
  undefined,
320
340
  CALLGRAPH_SOFT_CODES,
341
+ signal,
321
342
  );
322
343
  const code = typeof response.code === "string" ? response.code : "";
323
344
  if (code !== "callgraph_building" || Date.now() >= deadline) {
@@ -341,7 +362,7 @@ export function registerCallgraphTool(pi: ExtensionAPI, ctx: AftToolContext): vo
341
362
  promptSnippet: "Call graph and data-flow navigation",
342
363
  promptGuidelines: [CALLGRAPH_PROMPT],
343
364
  parameters: CallgraphParams,
344
- async execute(_id, params, _signal, _onUpdate, extCtx) {
365
+ async execute(_id, params, signal, _onUpdate, extCtx) {
345
366
  const rawArgs = compactArgs({
346
367
  op: params.op,
347
368
  filePath: resolvePathArg(extCtx.cwd, params.path),
@@ -354,7 +375,12 @@ export function registerCallgraphTool(pi: ExtensionAPI, ctx: AftToolContext): vo
354
375
  includeUnresolved: params.includeUnresolved,
355
376
  });
356
377
 
357
- const { text, response } = await callCallgraphWithBuildRetry(bridgeFor(ctx), rawArgs, extCtx);
378
+ const { text, response } = await callCallgraphWithBuildRetry(
379
+ bridgeFor(ctx),
380
+ rawArgs,
381
+ extCtx,
382
+ signal,
383
+ );
358
384
  const out =
359
385
  text ||
360
386
  formatCallgraphSections(params.op, response, PLAIN_CALLGRAPH_THEME, {
@@ -483,7 +509,7 @@ export function registerSearchTool(pi: ExtensionAPI, ctx: AftToolContext): void
483
509
  promptSnippet: "Search code by meaning or exact text",
484
510
  promptGuidelines: [SEARCH_PROMPT],
485
511
  parameters: SearchParams,
486
- async execute(_id, params, _signal, onUpdate, extCtx) {
512
+ async execute(_id, params, signal, onUpdate, extCtx) {
487
513
  if (typeof params.query !== "string" || params.query.trim().length === 0) {
488
514
  throw new Error("'query' must be a non-empty string");
489
515
  }
@@ -506,13 +532,21 @@ export function registerSearchTool(pi: ExtensionAPI, ctx: AftToolContext): void
506
532
  let response: Record<string, unknown>;
507
533
  let text: string;
508
534
  try {
509
- ({ text, response } = await callAftTool(bridge, "search", rawArgs, extCtx, {
510
- // 默认 search 传输超时仅 60s,会早于索引等待(600s)触发;覆盖为等待
511
- // 上限 + 常规执行预算。超时只说明响应被挤掉而非 bridge 挂死,保留
512
- // 常驻的语义索引/LSP 状态。
513
- transportTimeoutMs: SEMANTIC_INDEX_WAIT_TIMEOUT_MS + 60_000,
514
- keepBridgeOnTimeout: true,
515
- }));
535
+ ({ text, response } = await callAftTool(
536
+ bridge,
537
+ "search",
538
+ rawArgs,
539
+ extCtx,
540
+ {
541
+ // 默认 search 传输超时仅 60s,会早于索引等待(600s)触发;覆盖为等待
542
+ // 上限 + 常规执行预算。超时只说明响应被挤掉而非 bridge 挂死,保留
543
+ // 常驻的语义索引/LSP 状态。
544
+ transportTimeoutMs: SEMANTIC_INDEX_WAIT_TIMEOUT_MS + 60_000,
545
+ keepBridgeOnTimeout: true,
546
+ },
547
+ undefined,
548
+ signal,
549
+ ));
516
550
  } finally {
517
551
  stopProgress?.();
518
552
  }
@@ -4,7 +4,6 @@ Performs exact string replacements in files.
4
4
 
5
5
  Usage:
6
6
 
7
- - You must use your Read tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
8
7
  - When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + tab. Everything after that is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
9
8
  - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
10
9
  - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
@@ -199,8 +199,9 @@ async function readStateKey(filePath: string): Promise<string> {
199
199
  }
200
200
 
201
201
  /**
202
- * 校验「已读且未变」。key 与 currentContent 由调用方提供:调用方每次工具调用
203
- * realpath / readFile 一次,避免重复 IO
202
+ * 校验「若曾读过则内容未变」。key 与 currentContent 由调用方提供:调用方每次
203
+ * 工具调用只 realpath / readFile 一次,避免重复 IO。从未读过时直接放行,
204
+ * 不强制先 Read;存在已读快照时校验文本可编辑且 digest 一致。
204
205
  */
205
206
  function requireCurrentRead(
206
207
  state: ClaudeCodeState,
@@ -209,9 +210,7 @@ function requireCurrentRead(
209
210
  currentContent: Uint8Array,
210
211
  ): void {
211
212
  const readSnapshot = state.reads.get(key);
212
- if (!readSnapshot) {
213
- throw new Error("File has not been read yet. Read it first before writing to it.");
214
- }
213
+ if (!readSnapshot) return;
215
214
  if (!readSnapshot.textEditable) {
216
215
  throw new Error(`Cannot edit or overwrite a binary file with a text tool: ${filePath}`);
217
216
  }
@@ -356,7 +355,6 @@ export function registerFileTools(
356
355
  label: "Edit",
357
356
  description: [
358
357
  "Performs exact string replacements in files.",
359
- "You must use Read on the file before editing it.",
360
358
  "old_string must match exactly and must be unique unless replace_all is true.",
361
359
  "This tool does not use regular expressions or fuzzy matching.",
362
360
  ].join("\n"),
@@ -572,7 +570,7 @@ export function registerFileTools(
572
570
  description: [
573
571
  "Writes a file to the local filesystem.",
574
572
  "This tool overwrites an existing file with the full content provided.",
575
- "If the file exists, you must use Read first. Prefer Edit for partial changes.",
573
+ "Prefer Edit for partial changes.",
576
574
  ].join("\n"),
577
575
  promptSnippet: "Create or overwrite files",
578
576
  promptGuidelines: [WRITE_PROMPT],
@@ -695,8 +693,8 @@ export default function claudeCodeFileTools(pi: ExtensionAPI, options?: LspServi
695
693
  const state = createClaudeCodeState();
696
694
 
697
695
  // LSP 专属工具(lsp-rename / inspect 族)仅在 lsp.json 存在 enabled 服务器时
698
- // 注册(session_start 校验后);本工具集跟踪 read-before-write 状态,rename
699
- // 落盘的文件要标记为已读并随 details 持久化(restoreFileReads 依赖 details.reads)。
696
+ // 注册(session_start 校验后);本工具集维护 reads 记账,rename 落盘的文件
697
+ // 要标记为已读并随 details 持久化(restoreFileReads 依赖 details.reads)。
700
698
  const manager = createLspManager(
701
699
  pi,
702
700
  {
@@ -5,7 +5,6 @@ Writes a file to the local filesystem.
5
5
  Usage:
6
6
 
7
7
  - This tool will overwrite the existing file if there is one at the provided path.
8
- - If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.
9
8
  - Prefer the Edit tool for modifying existing files — it only sends the diff. Only use this tool to create new files or for complete rewrites.
10
9
  - NEVER create documentation files (*.md) or README files unless explicitly requested by the User.
11
10
  - Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.
@@ -5,9 +5,9 @@
5
5
  * 这里只负责工具注册与执行编排:按行内候选逐个探测 → canonicalizeEdit 分组
6
6
  * 消歧 → expandWorkspaceEdit 内存展开 → 审批 → 写盘 → 诊断。
7
7
  *
8
- * 两个工具集行为一致,唯一差异是 reads 记账:跟踪 read-before-write 状态的
9
- * 工具集通过 hooks.recordReads 把重命名文件标记为已读并随 details 持久化;
10
- * 不跟踪该状态的工具集不传 hook。
8
+ * 两个工具集行为一致,唯一差异是 reads 记账:跟踪已读快照的工具集通过
9
+ * hooks.recordReads 把重命名文件标记为已读并随 details 持久化;不跟踪
10
+ * 该状态的工具集不传 hook。
11
11
  */
12
12
 
13
13
  import { readFileSync } from "node:fs";