@trim21/personal-pi-extensions 0.1.540 → 0.1.541

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.540",
3
+ "version": "0.1.541",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -4,6 +4,7 @@ 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.
7
8
  - 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.
8
9
  - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
9
10
  - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
@@ -199,9 +199,8 @@ async function readStateKey(filePath: string): Promise<string> {
199
199
  }
200
200
 
201
201
  /**
202
- * 校验「若曾读过则内容未变」。key 与 currentContent 由调用方提供:调用方每次
203
- * 工具调用只 realpath / readFile 一次,避免重复 IO。从未读过时直接放行,
204
- * 不强制先 Read;存在已读快照时校验文本可编辑且 digest 一致。
202
+ * 校验「已读且未变」。key 与 currentContent 由调用方提供:调用方每次工具调用
203
+ * realpath / readFile 一次,避免重复 IO
205
204
  */
206
205
  function requireCurrentRead(
207
206
  state: ClaudeCodeState,
@@ -210,7 +209,9 @@ function requireCurrentRead(
210
209
  currentContent: Uint8Array,
211
210
  ): void {
212
211
  const readSnapshot = state.reads.get(key);
213
- if (!readSnapshot) return;
212
+ if (!readSnapshot) {
213
+ throw new Error("File has not been read yet. Read it first before writing to it.");
214
+ }
214
215
  if (!readSnapshot.textEditable) {
215
216
  throw new Error(`Cannot edit or overwrite a binary file with a text tool: ${filePath}`);
216
217
  }
@@ -331,18 +332,25 @@ export function registerFileTools(
331
332
  }
332
333
  const snapshot = snapshotOf(buffer);
333
334
  state.reads.set(key, snapshot);
334
- // LSP 文件事件通知是后台任务,失败不影响读取(read 不驻留文档)
335
- void getService()
336
- .notifyFile(filePath, ctx.cwd)
337
- .catch(() => {
338
- // 后台通知失败不影响读取
339
- });
335
+ // Edit / Write 同一条驻留路径:didOpen 后等待该文件的诊断并报告
336
+ const {
337
+ text: diagnosticText,
338
+ errorCount,
339
+ warningCount,
340
+ } = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
341
+ notify: (message, level) => ctx.ui.notify(message, level),
342
+ });
340
343
  return {
341
- content: [{ type: "text", text: formatted.text }],
344
+ content: [
345
+ {
346
+ type: "text",
347
+ text: appendLspDiagnosticText(formatted.text, diagnosticText, errorCount),
348
+ },
349
+ ],
342
350
  details: {
343
351
  reads: { [key]: snapshot },
344
352
  pendant: {
345
- subtitle: formatSubtitlePath(ctx.cwd, filePath),
353
+ subtitle: formatSubtitlePath(ctx.cwd, filePath, errorCount, warningCount),
346
354
  title: "Read",
347
355
  } satisfies ToolPendant,
348
356
  },
@@ -355,6 +363,7 @@ export function registerFileTools(
355
363
  label: "Edit",
356
364
  description: [
357
365
  "Performs exact string replacements in files.",
366
+ "You must use Read on the file before editing it.",
358
367
  "old_string must match exactly and must be unique unless replace_all is true.",
359
368
  "This tool does not use regular expressions or fuzzy matching.",
360
369
  ].join("\n"),
@@ -570,7 +579,7 @@ export function registerFileTools(
570
579
  description: [
571
580
  "Writes a file to the local filesystem.",
572
581
  "This tool overwrites an existing file with the full content provided.",
573
- "Prefer Edit for partial changes.",
582
+ "If the file exists, you must use Read first. Prefer Edit for partial changes.",
574
583
  ].join("\n"),
575
584
  promptSnippet: "Create or overwrite files",
576
585
  promptGuidelines: [WRITE_PROMPT],
@@ -693,8 +702,8 @@ export default function claudeCodeFileTools(pi: ExtensionAPI, options?: LspServi
693
702
  const state = createClaudeCodeState();
694
703
 
695
704
  // LSP 专属工具(lsp-rename / inspect 族)仅在 lsp.json 存在 enabled 服务器时
696
- // 注册(session_start 校验后);本工具集维护 reads 记账,rename 落盘的文件
697
- // 要标记为已读并随 details 持久化(restoreFileReads 依赖 details.reads)。
705
+ // 注册(session_start 校验后);本工具集跟踪 read-before-write 状态,rename
706
+ // 落盘的文件要标记为已读并随 details 持久化(restoreFileReads 依赖 details.reads)。
698
707
  const manager = createLspManager(
699
708
  pi,
700
709
  {
@@ -5,6 +5,7 @@ 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.
8
9
  - 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.
9
10
  - NEVER create documentation files (*.md) or README files unless explicitly requested by the User.
10
11
  - Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.
@@ -16,7 +16,7 @@
16
16
  * 的 id 直接忽略;
17
17
  * - client 按 (root, serverID) 缓存,并发 spawn 去重,启动失败记入
18
18
  * broken(冷却期内跳过,冷却过后下次触碰自动重试)并主动 notify;
19
- * - 工具只与 touchFile / notifyFile / diagnostics / lspDiagnosticsForFile 四个方法打交道;通知回调按请求传入。
19
+ * - 工具只与 touchFile / diagnostics / lspDiagnosticsForFile 三个方法打交道;通知回调按请求传入。
20
20
  */
21
21
 
22
22
  import { readFileSync } from "node:fs";
@@ -397,9 +397,8 @@ export interface LspService {
397
397
  diagnostics?: "document" | "full",
398
398
  options?: LspRequestOptions,
399
399
  ): Promise<void>;
400
- /** read 用:只发文件事件通知服务器磁盘上有该文件,不驻留、不等诊断。 */
401
- notifyFile(file: string, cwd: string, options?: LspRequestOptions): Promise<void>;
402
400
  diagnostics(): Promise<Record<string, Diagnostic[]>>;
401
+ /** read / edit / write 用:等待文档诊断并返回该文件的 ERROR / WARN 报告与数量。 */
403
402
  lspDiagnosticsForFile(
404
403
  file: string,
405
404
  cwd: string,
@@ -736,7 +735,7 @@ export function createLspService(
736
735
  /**
737
736
  * 记录一次启动失败:进入 broken(冷却期内跳过)、渲染 status,并按节流
738
737
  * 间隔主动 notify。错误上报优先走会话级 sessionNotify——不依赖触发请求
739
- * 恰好携带 notify(否则 Read warm-up 等静默通道会把失败吞掉);未注入
738
+ * 恰好携带 notify(否则 opencode 工具集等不带请求级 notify 的通道会把失败吞掉);未注入
740
739
  * 会话通知时退回请求级 notify 兜底。
741
740
  */
742
741
  function reportStartupFailure(
@@ -881,7 +880,7 @@ export function createLspService(
881
880
 
882
881
  /**
883
882
  * 打开文档让服务器索引 / 产出诊断。diagnostics 传 "document" 时最多等 5s,
884
- * "full" 最多等 10s;不传则只通知不等待(read warm-up 用)。
883
+ * "full" 最多等 10s;不传则只 didOpen 不等待。
885
884
  */
886
885
  async function touchFile(
887
886
  file: string,
@@ -923,23 +922,8 @@ export function createLspService(
923
922
  }
924
923
 
925
924
  /**
926
- * read warm-up:通知服务器磁盘上有这个文件(D5),不 didOpen 驻留、不等诊断。
927
- * 驻留文档若磁盘已被外部改写会顺带触发退场。
928
- */
929
- async function notifyFile(file: string, cwd: string, options?: LspRequestOptions): Promise<void> {
930
- const clients = await getClients(file, cwd, options?.notify);
931
- await Promise.all(
932
- clients.map((client) =>
933
- client.notify.watchedFiles([{ path: file, type: "changed", isDirectory: false }]),
934
- ),
935
- ).catch(() => {
936
- // 文件事件通知失败不影响读取
937
- });
938
- }
939
-
940
- /**
941
- * edit/write 用:等待文档诊断并返回该文件的 ERROR / WARN 报告(text 空串表示无此类诊断)
942
- * 与数量。内部所有 LSP 失败都会被吞掉,不干扰写操作本身。
925
+ * read / edit / write 用:等待文档诊断并返回该文件的 ERROR / WARN 报告(text 空串表示无此类诊断)
926
+ * 与数量。内部所有 LSP 失败都会被吞掉,不干扰读取或写操作本身。
943
927
  */
944
928
  async function lspDiagnosticsForFile(
945
929
  file: string,
@@ -1184,7 +1168,6 @@ export function createLspService(
1184
1168
 
1185
1169
  return {
1186
1170
  touchFile,
1187
- notifyFile,
1188
1171
  diagnostics,
1189
1172
  lspDiagnosticsForFile,
1190
1173
  rename,
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * 对齐官方 v1(packages/opencode/src/tool/{read,edit,write}.ts):
9
9
  * - read:流式分行(LF / CRLF / CR)、每行 `N: ` 行号前缀、单行 2000
10
- * 字符截断、1 起始 offset、目录排序;读取后后台 LSP warm-up。
10
+ * 字符截断、1 起始 offset、目录排序;读取后等待并报告该文档的诊断。
11
11
  * 不接 PDF、不接 <system-reminder>;图片 magic 检测保留。
12
12
  * - edit:匹配引擎 + 把 old/new 转到文件换行后再替换;写后等待文档诊断。
13
13
  * - write:BOM 保留(source.bom || next.bom);写后同 edit 的诊断输出。
@@ -431,20 +431,26 @@ function registerReadTool(pi: ExtensionAPI, getService: () => LspService): void
431
431
  outputText = `${header}${numbered}\n\n(End of file - total ${page.count} lines)${footer}`;
432
432
  }
433
433
 
434
- content = [{ type: "text", text: outputText }];
435
-
436
- // opencode: LSP 文件事件通知是后台任务,失败不影响读取(read 不驻留文档)
437
- void getService()
438
- .notifyFile(absolutePath, ctx.cwd)
439
- .catch(() => {
440
- // 后台通知失败不影响读取
441
- });
434
+ // opencode: edit / write 同一条驻留路径:didOpen 后等待该文件的诊断并报告
435
+ const {
436
+ text: diagnosticText,
437
+ errorCount,
438
+ warningCount,
439
+ } = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, { signal });
440
+ content = [
441
+ {
442
+ type: "text",
443
+ text: appendLspDiagnosticText(outputText, diagnosticText, errorCount),
444
+ },
445
+ ];
442
446
 
443
447
  return {
444
448
  content,
445
449
  details: {
446
450
  ...details,
447
- pendant: { subtitle: formatSubtitlePath(ctx.cwd, absolutePath) },
451
+ pendant: {
452
+ subtitle: formatSubtitlePath(ctx.cwd, absolutePath, errorCount, warningCount),
453
+ },
448
454
  },
449
455
  };
450
456
  },