@trim21/personal-pi-extensions 0.1.612 → 0.1.613

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.
Files changed (75) hide show
  1. package/package.json +1 -1
  2. package/src/aft/config.ts +9 -3
  3. package/src/aft/index.ts +6 -2
  4. package/src/aft/logger.ts +9 -3
  5. package/src/aft/tools.ts +3 -1
  6. package/src/bwrap/approval-rules.ts +42 -14
  7. package/src/bwrap/approval-suggest.ts +12 -4
  8. package/src/bwrap/core.ts +39 -13
  9. package/src/bwrap/dcg-scan.ts +16 -6
  10. package/src/bwrap/holder.ts +3 -1
  11. package/src/bwrap/mihomo-config.ts +18 -6
  12. package/src/bwrap/network-stack.ts +30 -10
  13. package/src/bwrap/runtime.ts +30 -10
  14. package/src/claude-code/common.ts +6 -2
  15. package/src/claude-code/edit-utils.ts +3 -1
  16. package/src/claude-code/files.ts +40 -14
  17. package/src/claude-code/glob.ts +12 -4
  18. package/src/claude-code/grep.ts +41 -14
  19. package/src/claude-code/session-tools.ts +18 -6
  20. package/src/claude-code/shell.ts +9 -3
  21. package/src/gh/base.ts +72 -24
  22. package/src/gh/tools/download-release-assets.ts +12 -4
  23. package/src/gh/tools/list-releases.ts +3 -1
  24. package/src/gh/tools/list-workflow-runs.ts +9 -3
  25. package/src/gh/tools/read-ci-logs.ts +21 -7
  26. package/src/gh/tools/read-repo.ts +3 -1
  27. package/src/lib/cli-args.ts +6 -2
  28. package/src/lib/cli.ts +48 -16
  29. package/src/lib/file-reads.ts +24 -8
  30. package/src/lib/github.ts +35 -12
  31. package/src/lib/lsp/adapter.ts +6 -2
  32. package/src/lib/lsp/bin.ts +12 -4
  33. package/src/lib/lsp/client.ts +205 -69
  34. package/src/lib/lsp/diagnostic.ts +6 -2
  35. package/src/lib/lsp/inspect-tool.ts +25 -9
  36. package/src/lib/lsp/lsp.ts +138 -46
  37. package/src/lib/lsp/rename-tool.ts +11 -4
  38. package/src/lib/lsp/rename.ts +25 -8
  39. package/src/lib/lsp/server-config.ts +41 -14
  40. package/src/lib/lsp/watcher.ts +51 -17
  41. package/src/lib/path.ts +18 -6
  42. package/src/lib/proc.ts +3 -1
  43. package/src/lib/proxy.ts +9 -3
  44. package/src/lib/request-policy.ts +3 -1
  45. package/src/lib/seq-state.ts +3 -1
  46. package/src/lib/ui.ts +23 -8
  47. package/src/lib/write-guard.ts +21 -7
  48. package/src/openai-cost/config.ts +3 -1
  49. package/src/openai-cost/cost.ts +54 -18
  50. package/src/openai-cost/index.ts +3 -1
  51. package/src/openai-cost/provider.ts +6 -2
  52. package/src/opencode/bash.ts +9 -3
  53. package/src/opencode/edit-engine.ts +24 -8
  54. package/src/opencode/files.ts +30 -10
  55. package/src/opencode/glob.ts +3 -1
  56. package/src/opencode/grep.ts +12 -4
  57. package/src/opencode/question.ts +3 -1
  58. package/src/opencode/ripgrep.ts +18 -6
  59. package/src/opencode/todo.ts +3 -1
  60. package/src/session-name.ts +39 -13
  61. package/src/spawn-agent-agents.ts +18 -6
  62. package/src/spawn-agent.ts +53 -18
  63. package/src/system-prompt/index.ts +9 -3
  64. package/src/talk/core.ts +143 -48
  65. package/src/talk/format.ts +12 -4
  66. package/src/talk/group.ts +9 -3
  67. package/src/talk/index.ts +30 -10
  68. package/src/talk/mailbox.ts +30 -10
  69. package/src/talk/policy.ts +6 -2
  70. package/src/talk/registry.ts +33 -11
  71. package/src/talk/storage.ts +3 -1
  72. package/src/vision-agent.ts +41 -14
  73. package/src/web/config.ts +3 -1
  74. package/src/web/fetch.ts +75 -25
  75. package/src/web/search.ts +30 -10
@@ -166,7 +166,9 @@ const BASH_TAIL_LIMIT_BYTES = 1024 * 1024;
166
166
  function countNewlines(data: Buffer): number {
167
167
  let count = 0;
168
168
  for (const byte of data) {
169
- if (byte === 0x0a) count++;
169
+ if (byte === 0x0a) {
170
+ count++;
171
+ }
170
172
  }
171
173
  return count;
172
174
  }
@@ -219,7 +221,9 @@ class BashOutput {
219
221
  }
220
222
 
221
223
  close(): Promise<void> {
222
- if (!this.stream) return Promise.resolve();
224
+ if (!this.stream) {
225
+ return Promise.resolve();
226
+ }
223
227
  const stream = this.stream;
224
228
  this.stream = undefined;
225
229
  return new Promise((resolve) => {
@@ -286,8 +290,12 @@ function notifyChange(
286
290
  ctx: { ui: { notify: (message: string, type?: "info" | "warning" | "error") => void } },
287
291
  change: SandboxChange,
288
292
  ): void {
289
- if (change.fs !== undefined) ctx.ui.notify(FS_LABELS[change.fs], "info");
290
- if (change.network !== undefined) ctx.ui.notify(NETWORK_LABELS[change.network], "info");
293
+ if (change.fs !== undefined) {
294
+ ctx.ui.notify(FS_LABELS[change.fs], "info");
295
+ }
296
+ if (change.network !== undefined) {
297
+ ctx.ui.notify(NETWORK_LABELS[change.network], "info");
298
+ }
291
299
  }
292
300
 
293
301
  /** 沙箱默认写边界:根只读、工作区与 /tmp 可写、.git 只读。不展开用户配置的额外可写路径。 */
@@ -342,7 +350,9 @@ export function describeSandbox(
342
350
  unsandboxed: boolean,
343
351
  fixed = false,
344
352
  ): string | undefined {
345
- if (unsandboxed) return undefined;
353
+ if (unsandboxed) {
354
+ return undefined;
355
+ }
346
356
  return [
347
357
  "<system-reminder>",
348
358
  `This command ran in a sandbox: ${describeLimits(resolved)}.`,
@@ -464,7 +474,9 @@ export class BwrapRuntime {
464
474
 
465
475
  // 固定沙箱(创建时显式传入配置)不注册 /bwrap-* 命令:切模式与恢复提权审批
466
476
  // 都可能放宽声明的沙箱
467
- if (this.config === undefined) this.registerCommands(pi);
477
+ if (this.config === undefined) {
478
+ this.registerCommands(pi);
479
+ }
468
480
  }
469
481
 
470
482
  setMode(cwd: string, change: SandboxChange): ResolvedBwrap {
@@ -663,7 +675,9 @@ export class BwrapRuntime {
663
675
  }
664
676
 
665
677
  private resolve(ctx: Pick<ExtensionContext, "cwd">): ResolvedBwrap {
666
- if (!this.resolved) this.resolved = resolveBwrap(this.config ?? loadBwrapConfig(ctx.cwd));
678
+ if (!this.resolved) {
679
+ this.resolved = resolveBwrap(this.config ?? loadBwrapConfig(ctx.cwd));
680
+ }
667
681
  return this.resolved;
668
682
  }
669
683
 
@@ -674,7 +688,9 @@ export class BwrapRuntime {
674
688
  execCwd: string,
675
689
  ): Promise<FullAccessGrant> {
676
690
  // hasUI 判定推迟到审批时刻:无 UI 会话弹不了审批框,按用户点 Deny 的标准文案拒绝
677
- if (!ctx.hasUI) throw new Error(UNSANDBOXED_DENIED);
691
+ if (!ctx.hasUI) {
692
+ throw new Error(UNSANDBOXED_DENIED);
693
+ }
678
694
  const decision = await this.approveFullAccessUI(ctx, command, reason, execCwd);
679
695
  // 关闭对话框 = 中断并拒绝,不循环重问
680
696
  if (decision === undefined) {
@@ -802,7 +818,9 @@ export class BwrapRuntime {
802
818
  const verdict = await selectWithOptionalInput(description + pending, actions, ctx.ui, {
803
819
  signal: ctx.signal,
804
820
  });
805
- if (verdict === undefined) return undefined;
821
+ if (verdict === undefined) {
822
+ return undefined;
823
+ }
806
824
  if (verdict.label !== EDIT_RULES) {
807
825
  return {
808
826
  result: verdict.label,
@@ -826,7 +844,9 @@ export class BwrapRuntime {
826
844
  patterns?: string[],
827
845
  ): Promise<void> {
828
846
  const rulePatterns = patterns ?? (await commandPatternsFor(command));
829
- if (rulePatterns.length === 0) return; // 解析失败:本次处理,不写规则
847
+ if (rulePatterns.length === 0) {
848
+ return; // 解析失败:本次处理,不写规则
849
+ }
830
850
  const newRules: ApprovalRule[] = rulePatterns.map((pattern) => ({ action: "allow", pattern }));
831
851
  const { project } = getBwrapConfigPaths(ctx.cwd);
832
852
  let config: Record<string, unknown> = {};
@@ -14,7 +14,9 @@ export function resolveToolFilePath(filePath: string, cwd: string): string {
14
14
 
15
15
  /** Resolve a search root path against the working directory. */
16
16
  export function searchRoot(path: string | undefined, cwd: string): string {
17
- if (!path) return cwd;
17
+ if (!path) {
18
+ return cwd;
19
+ }
18
20
  return isAbsolute(path) ? path : resolve(cwd, path);
19
21
  }
20
22
 
@@ -82,6 +84,8 @@ export async function suggestPathUnderCwd(
82
84
  /** "Did you mean" 建议:优先 cwd 重定位,其次同名不同扩展(对齐 Claude Code)。 */
83
85
  export async function didYouMean(filePath: string, cwd: string): Promise<string | undefined> {
84
86
  const cwdSuggestion = await suggestPathUnderCwd(filePath, cwd);
85
- if (cwdSuggestion) return cwdSuggestion;
87
+ if (cwdSuggestion) {
88
+ return cwdSuggestion;
89
+ }
86
90
  return findSimilarFile(filePath);
87
91
  }
@@ -12,6 +12,8 @@
12
12
  * convertLeadingTabsToSpaces)。仅用于 details 里展示的 diff,不影响写盘内容。
13
13
  */
14
14
  export function convertLeadingTabsToSpaces(content: string): string {
15
- if (!content.includes("\t")) return content;
15
+ if (!content.includes("\t")) {
16
+ return content;
17
+ }
16
18
  return content.replaceAll(/^\t+/gm, (tabs) => " ".repeat(tabs.length));
17
19
  }
@@ -44,9 +44,15 @@ const MAX_READ_TOKENS = 25_000;
44
44
  const MAX_EDIT_FILE_SIZE = 1024 * 1024 * 1024; // 1 GiB
45
45
 
46
46
  function formatFileSize(bytes: number): string {
47
- if (bytes < 1024) return `${bytes} B`;
48
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
49
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
47
+ if (bytes < 1024) {
48
+ return `${bytes} B`;
49
+ }
50
+ if (bytes < 1024 * 1024) {
51
+ return `${(bytes / 1024).toFixed(1)} KB`;
52
+ }
53
+ if (bytes < 1024 * 1024 * 1024) {
54
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
55
+ }
50
56
  return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
51
57
  }
52
58
 
@@ -81,7 +87,9 @@ async function assertReadableFile(filePath: string): Promise<Stats> {
81
87
  if (value.isDirectory()) {
82
88
  throw new Error(`EISDIR: illegal operation on a directory, read '${filePath}'`);
83
89
  }
84
- if (!value.isFile()) throw new Error(`File not found: ${filePath}`);
90
+ if (!value.isFile()) {
91
+ throw new Error(`File not found: ${filePath}`);
92
+ }
85
93
  await access(filePath, constants.R_OK);
86
94
  return value;
87
95
  }
@@ -89,8 +97,12 @@ async function assertReadableFile(filePath: string): Promise<Stats> {
89
97
  function isBinary(sample: Uint8Array): boolean {
90
98
  let suspicious = 0;
91
99
  for (const byte of sample) {
92
- if (byte === 0) return true;
93
- if (byte < 9 || (byte > 13 && byte < 32)) suspicious++;
100
+ if (byte === 0) {
101
+ return true;
102
+ }
103
+ if (byte < 9 || (byte > 13 && byte < 32)) {
104
+ suspicious++;
105
+ }
94
106
  }
95
107
  return sample.length > 0 && suspicious / sample.length > 0.3;
96
108
  }
@@ -104,7 +116,9 @@ function isBinary(sample: Uint8Array): boolean {
104
116
  */
105
117
  function splitFileLines(content: string): string[] {
106
118
  const text = content.replace(/^\uFEFF/, "");
107
- if (text === "") return [];
119
+ if (text === "") {
120
+ return [];
121
+ }
108
122
  return (text.endsWith("\n") ? text : `${text}\n`)
109
123
  .split("\n")
110
124
  .map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line));
@@ -157,7 +171,9 @@ export function formatReadOutput(
157
171
  }
158
172
 
159
173
  function countMatches(content: string, needle: string): number {
160
- if (needle === "") return 0;
174
+ if (needle === "") {
175
+ return 0;
176
+ }
161
177
  let count = 0;
162
178
  let index = 0;
163
179
  while ((index = content.indexOf(needle, index)) !== -1) {
@@ -173,17 +189,24 @@ export function exactReplace(
173
189
  newString: string,
174
190
  replaceAll = false,
175
191
  ): string {
176
- if (oldString === newString)
192
+ if (oldString === newString) {
177
193
  throw new Error("No changes to apply: old_string and new_string are identical.");
178
- if (oldString === "") throw new Error("old_string must not be empty.");
194
+ }
195
+ if (oldString === "") {
196
+ throw new Error("old_string must not be empty.");
197
+ }
179
198
  const matches = countMatches(content, oldString);
180
- if (matches === 0) throw new Error("String to replace not found in file.");
199
+ if (matches === 0) {
200
+ throw new Error("String to replace not found in file.");
201
+ }
181
202
  if (!replaceAll && matches > 1) {
182
203
  throw new Error(
183
204
  `Found ${matches} matches of the string to replace, but replace_all is false. To replace all occurrences, set replace_all to true. To replace only one occurrence, provide more context to make old_string unique.`,
184
205
  );
185
206
  }
186
- if (replaceAll) return content.split(oldString).join(newString);
207
+ if (replaceAll) {
208
+ return content.split(oldString).join(newString);
209
+ }
187
210
  const index = content.indexOf(oldString);
188
211
  return content.slice(0, index) + newString + content.slice(index + oldString.length);
189
212
  }
@@ -284,8 +307,9 @@ export function registerFileTools(
284
307
  const key = await readStateKey(filePath);
285
308
  const buffer = await readFile(filePath);
286
309
 
287
- if (isBinary(buffer.subarray(0, SAMPLE_BYTES)))
310
+ if (isBinary(buffer.subarray(0, SAMPLE_BYTES))) {
288
311
  throw new Error(`Cannot read binary file: ${filePath}`);
312
+ }
289
313
  // 全读(limit 未传)时受字节上限约束(对齐 Claude Code)
290
314
  if (params.limit === undefined && buffer.length > MAX_READ_SIZE_BYTES) {
291
315
  throw new Error(
@@ -398,7 +422,9 @@ export function registerFileTools(
398
422
  throw error;
399
423
  }
400
424
  }
401
- if (!exists) await mkdir(dirname(filePath), { recursive: true });
425
+ if (!exists) {
426
+ await mkdir(dirname(filePath), { recursive: true });
427
+ }
402
428
  await writeFile(filePath, newString, "utf8");
403
429
  const snapshot = snapshotOf(newString);
404
430
  const key = await readStateKey(filePath);
@@ -40,11 +40,15 @@ function extractGlobBaseDirectory(pattern: string): {
40
40
  }
41
41
  const staticPrefix = pattern.slice(0, match.index);
42
42
  const lastSepIndex = Math.max(staticPrefix.lastIndexOf("/"), staticPrefix.lastIndexOf(sep));
43
- if (lastSepIndex === -1) return { baseDir: "", relativePattern: pattern };
43
+ if (lastSepIndex === -1) {
44
+ return { baseDir: "", relativePattern: pattern };
45
+ }
44
46
  let baseDir = staticPrefix.slice(0, lastSepIndex);
45
47
  const relativePattern = pattern.slice(lastSepIndex + 1);
46
48
  // 根目录 pattern(如 /*.txt):baseDir 为空但应使用 "/"
47
- if (baseDir === "" && lastSepIndex === 0) baseDir = "/";
49
+ if (baseDir === "" && lastSepIndex === 0) {
50
+ baseDir = "/";
51
+ }
48
52
  return { baseDir, relativePattern };
49
53
  }
50
54
 
@@ -89,7 +93,9 @@ export async function globFiles(
89
93
  } catch (error) {
90
94
  // rg exit code 1 = 搜索完成但无匹配,对齐 Claude Code 的 ripGrep(正常空结果)
91
95
  const code = (error as { code?: unknown }).code;
92
- if (code === 1) return { files: [], truncated: false };
96
+ if (code === 1) {
97
+ return { files: [], truncated: false };
98
+ }
93
99
  const detail =
94
100
  (error as { stderr?: string }).stderr?.trim() ||
95
101
  (error instanceof Error ? error.message : String(error));
@@ -143,7 +149,9 @@ export function registerGlobTool(pi: ExtensionAPI): void {
143
149
  }
144
150
  throw error;
145
151
  }
146
- if (!stats.isDirectory()) throw new Error(`Path is not a directory: ${params.path}`);
152
+ if (!stats.isDirectory()) {
153
+ throw new Error(`Path is not a directory: ${params.path}`);
154
+ }
147
155
  }
148
156
  const { files, truncated } = await globFiles(params.pattern, root, signal);
149
157
  const filenames = files.map((filePath) => toRelativePath(filePath, ctx.cwd));
@@ -35,7 +35,9 @@ const VCS_DIRECTORIES_TO_EXCLUDE = [".git", ".svn", ".hg", ".bzr", ".jj", ".sl"]
35
35
  const DEFAULT_HEAD_LIMIT = 250;
36
36
 
37
37
  function truncateOutput(output: string, maxCharacters = 30_000): string {
38
- if (output.length <= maxCharacters) return output;
38
+ if (output.length <= maxCharacters) {
39
+ return output;
40
+ }
39
41
  return `${output.slice(0, maxCharacters)}\n\n[Output truncated at ${maxCharacters} characters]`;
40
42
  }
41
43
 
@@ -102,22 +104,36 @@ export function buildGrepArguments(params: GrepParameters, cwd: string): string[
102
104
  case "content": {
103
105
  args.push("--no-heading", "--with-filename");
104
106
  const showLineNumbers = params["-n"] === true || params["-n"] === undefined;
105
- if (showLineNumbers) args.push("--line-number");
107
+ if (showLineNumbers) {
108
+ args.push("--line-number");
109
+ }
106
110
  const before = params["-B"];
107
111
  const after = params["-A"];
108
112
  const around = params.context ?? params["-C"];
109
113
  if (around === undefined) {
110
- if (before !== undefined) args.push("--before-context", String(before));
111
- if (after !== undefined) args.push("--after-context", String(after));
112
- } else args.push("--context", String(around));
114
+ if (before !== undefined) {
115
+ args.push("--before-context", String(before));
116
+ }
117
+ if (after !== undefined) {
118
+ args.push("--after-context", String(after));
119
+ }
120
+ } else {
121
+ args.push("--context", String(around));
122
+ }
113
123
 
114
124
  break;
115
125
  }
116
126
  // No default
117
127
  }
118
- if (params["-i"] === true) args.push("--ignore-case");
119
- if (params.type) args.push("--type", params.type);
120
- if (params.multiline === true) args.push("--multiline", "--multiline-dotall");
128
+ if (params["-i"] === true) {
129
+ args.push("--ignore-case");
130
+ }
131
+ if (params.type) {
132
+ args.push("--type", params.type);
133
+ }
134
+ if (params.multiline === true) {
135
+ args.push("--multiline", "--multiline-dotall");
136
+ }
121
137
  // glob 按逗号/空格拆分(花括号模式不拆),对齐 Claude Code
122
138
  if (params.glob) {
123
139
  const globPatterns: string[] = [];
@@ -129,12 +145,17 @@ export function buildGrepArguments(params: GrepParameters, cwd: string): string[
129
145
  }
130
146
  }
131
147
  for (const globPattern of globPatterns) {
132
- if (globPattern) args.push("--glob", globPattern);
148
+ if (globPattern) {
149
+ args.push("--glob", globPattern);
150
+ }
133
151
  }
134
152
  }
135
153
  // 以 - 开头的 pattern 用 -e 显式声明,防止被 rg 当作选项
136
- if (params.pattern.startsWith("-")) args.push("-e", params.pattern);
137
- else args.push(params.pattern);
154
+ if (params.pattern.startsWith("-")) {
155
+ args.push("-e", params.pattern);
156
+ } else {
157
+ args.push(params.pattern);
158
+ }
138
159
  args.push(searchRoot(params.path, cwd));
139
160
  return args;
140
161
  }
@@ -147,7 +168,9 @@ export function buildGrepArguments(params: GrepParameters, cwd: string): string[
147
168
  */
148
169
  export async function sortFilesByMtime(output: string): Promise<string> {
149
170
  const paths = output.replace(/\n$/, "").split("\n").filter(Boolean);
150
- if (paths.length <= 1) return output;
171
+ if (paths.length <= 1) {
172
+ return output;
173
+ }
151
174
  const stats = await Promise.allSettled(paths.map((path) => stat(path)));
152
175
  const sorted = paths
153
176
  .map((path, index) => ({
@@ -208,8 +231,12 @@ function formatLimitInfo(
208
231
  appliedOffset: number | undefined,
209
232
  ): string {
210
233
  const parts: string[] = [];
211
- if (appliedLimit !== undefined) parts.push(`limit: ${appliedLimit}`);
212
- if (appliedOffset) parts.push(`offset: ${appliedOffset}`);
234
+ if (appliedLimit !== undefined) {
235
+ parts.push(`limit: ${appliedLimit}`);
236
+ }
237
+ if (appliedOffset) {
238
+ parts.push(`offset: ${appliedOffset}`);
239
+ }
213
240
  return parts.join(", ");
214
241
  }
215
242
 
@@ -63,7 +63,9 @@ const questionSchema = Type.Object(
63
63
  type QuestionInput = Static<typeof questionSchema>;
64
64
 
65
65
  function formatTodos(todos: readonly ClaudeCodeTodo[]): string[] | undefined {
66
- if (todos.length === 0) return undefined;
66
+ if (todos.length === 0) {
67
+ return undefined;
68
+ }
67
69
  const markers: Record<TodoStatus, string> = {
68
70
  pending: " ",
69
71
  in_progress: ">",
@@ -95,7 +97,9 @@ async function askSingle(
95
97
  ctx.ui,
96
98
  { signal },
97
99
  );
98
- if (result === undefined) return "Unanswered";
100
+ if (result === undefined) {
101
+ return "Unanswered";
102
+ }
99
103
  return result.prompted ? result.input || "Unanswered" : result.label;
100
104
  }
101
105
 
@@ -123,8 +127,12 @@ export function registerSessionTools(pi: ExtensionAPI): void {
123
127
  // 的列表重新渲染(完整列表替换语义,后出现的覆盖前面的)。
124
128
  pi.on("session_start", (_event, ctx) => {
125
129
  for (const entry of ctx.sessionManager.getBranch()) {
126
- if (entry.type !== "message" || entry.message.role !== "toolResult") continue;
127
- if (entry.message.toolName !== "TodoWrite") continue;
130
+ if (entry.type !== "message" || entry.message.role !== "toolResult") {
131
+ continue;
132
+ }
133
+ if (entry.message.toolName !== "TodoWrite") {
134
+ continue;
135
+ }
128
136
  if (Value.Check(todoSchema, entry.message.details)) {
129
137
  ctx.ui.setWidget("claude-code-todos", formatTodos(entry.message.details.todos));
130
138
  }
@@ -152,7 +160,9 @@ export function registerSessionTools(pi: ExtensionAPI): void {
152
160
  throw new Error("Todo content and activeForm must not be blank.");
153
161
  }
154
162
  const inProgress = todos.filter((todo) => todo.status === "in_progress");
155
- if (inProgress.length > 1) throw new Error("Only one todo may be in_progress at a time.");
163
+ if (inProgress.length > 1) {
164
+ throw new Error("Only one todo may be in_progress at a time.");
165
+ }
156
166
  ctx.ui.setWidget("claude-code-todos", formatTodos(todos));
157
167
  return Promise.resolve({
158
168
  content: [
@@ -190,7 +200,9 @@ export function registerSessionTools(pi: ExtensionAPI): void {
190
200
  ),
191
201
  executionMode: "sequential",
192
202
  async execute(_id, params, signal, _onUpdate, ctx) {
193
- if (!ctx.hasUI) throw new Error("Cannot ask questions: interactive UI is not available");
203
+ if (!ctx.hasUI) {
204
+ throw new Error("Cannot ask questions: interactive UI is not available");
205
+ }
194
206
  const answers: Record<string, string> = {};
195
207
  for (const question of params.questions) {
196
208
  signal?.throwIfAborted();
@@ -35,7 +35,9 @@ const BASH_PROMPT = readFileSync(fileURLToPath(new URL("bash.md", import.meta.ur
35
35
  */
36
36
  function formatBashError(exitCode: number | null, output: string): string {
37
37
  const full = [`Exit code ${exitCode ?? 1}`, output].filter(Boolean).join("\n");
38
- if (full.length <= MAX_ERROR_CHARS) return full;
38
+ if (full.length <= MAX_ERROR_CHARS) {
39
+ return full;
40
+ }
39
41
  const half = MAX_ERROR_CHARS / 2;
40
42
  return (
41
43
  full.slice(0, half) +
@@ -50,7 +52,9 @@ function appendTruncationNotice(
50
52
  truncation: TruncationResult,
51
53
  fullOutputPath: string | undefined,
52
54
  ): string {
53
- if (!fullOutputPath || !truncation.truncated) return text;
55
+ if (!fullOutputPath || !truncation.truncated) {
56
+ return text;
57
+ }
54
58
  const startLine = truncation.totalLines - truncation.outputLines + 1;
55
59
  const endLine = truncation.totalLines;
56
60
  if (truncation.lastLinePartial) {
@@ -145,7 +149,9 @@ export function registerShellTools(
145
149
  onUpdate,
146
150
  });
147
151
  } catch (error) {
148
- if (!(error instanceof Error)) throw error;
152
+ if (!(error instanceof Error)) {
153
+ throw error;
154
+ }
149
155
  if (error instanceof BashInterruptedError) {
150
156
  // 输出在前(必要时带截断提示),状态文本在最后
151
157
  const text = appendTruncationNotice(
package/src/gh/base.ts CHANGED
@@ -66,10 +66,14 @@ export interface GhResult {
66
66
  export function isGhAvailable(): boolean {
67
67
  const pathEnv = process.env.PATH ?? "";
68
68
  for (const directory of pathEnv.split(delimiter)) {
69
- if (existsSync(join(directory, "gh"))) return true;
69
+ if (existsSync(join(directory, "gh"))) {
70
+ return true;
71
+ }
70
72
  }
71
73
  for (const candidate of ["/usr/bin/gh", "/usr/local/bin/gh", "/run/current-system/sw/bin/gh"]) {
72
- if (existsSync(candidate)) return true;
74
+ if (existsSync(candidate)) {
75
+ return true;
76
+ }
73
77
  }
74
78
  return false;
75
79
  }
@@ -110,7 +114,9 @@ export function runGh(
110
114
  killReason = reason;
111
115
  proc.kill("SIGTERM");
112
116
  setTimeout(() => {
113
- if (!proc.killed) proc.kill("SIGKILL");
117
+ if (!proc.killed) {
118
+ proc.kill("SIGKILL");
119
+ }
114
120
  }, 5000);
115
121
  };
116
122
 
@@ -144,7 +150,9 @@ export function runGh(
144
150
  });
145
151
 
146
152
  proc.on("close", (code) => {
147
- if (timeoutId) clearTimeout(timeoutId);
153
+ if (timeoutId) {
154
+ clearTimeout(timeoutId);
155
+ }
148
156
  if (onAbort && ctx.signal) {
149
157
  ctx.signal.removeEventListener("abort", onAbort);
150
158
  }
@@ -163,7 +171,9 @@ export function runGh(
163
171
  });
164
172
 
165
173
  proc.on("error", (err: Error) => {
166
- if (timeoutId) clearTimeout(timeoutId);
174
+ if (timeoutId) {
175
+ clearTimeout(timeoutId);
176
+ }
167
177
  if (onAbort && ctx.signal) {
168
178
  ctx.signal.removeEventListener("abort", onAbort);
169
179
  }
@@ -284,7 +294,9 @@ export async function resolveRepo(
284
294
  cwd: string | undefined,
285
295
  input?: unknown,
286
296
  ): Promise<string> {
287
- if (repo) return repo;
297
+ if (repo) {
298
+ return repo;
299
+ }
288
300
  const stdout = await ghExec(["repo", "view", "--json", "nameWithOwner"], { cwd, signal, input });
289
301
  const { nameWithOwner } = Value.Parse(repoViewSchema, JSON.parse(stdout));
290
302
  return nameWithOwner;
@@ -303,13 +315,19 @@ export function truncate(
303
315
  const out: string[] = [];
304
316
  let bytes = 0;
305
317
  for (const line of lines) {
306
- if (out.length >= maxLines) break;
318
+ if (out.length >= maxLines) {
319
+ break;
320
+ }
307
321
  const lineBytes = Buffer.byteLength(line + "\n", "utf8");
308
- if (bytes + lineBytes > maxBytes) break;
322
+ if (bytes + lineBytes > maxBytes) {
323
+ break;
324
+ }
309
325
  out.push(line);
310
326
  bytes += lineBytes;
311
327
  }
312
- if (out.length > 0) return { text: out.join("\n"), truncated: true };
328
+ if (out.length > 0) {
329
+ return { text: out.join("\n"), truncated: true };
330
+ }
313
331
 
314
332
  // 首行自己就超过 maxBytes(如单行超长文本):保底保留字节前缀,
315
333
  // 不能把非空输出截成空字符串。
@@ -361,10 +379,16 @@ export function subtitlePendant<IdKey extends string = never>(
361
379
  idKey?: IdKey,
362
380
  ): ToolPendant | undefined {
363
381
  const parts: string[] = [];
364
- if (params.repo) parts.push(`repo=${params.repo}`);
382
+ if (params.repo) {
383
+ parts.push(`repo=${params.repo}`);
384
+ }
365
385
  const id = idKey === undefined ? undefined : params[idKey];
366
- if (typeof id === "string" || typeof id === "number") parts.push(`${idKey}=${id}`);
367
- if (parts.length === 0) return undefined;
386
+ if (typeof id === "string" || typeof id === "number") {
387
+ parts.push(`${idKey}=${id}`);
388
+ }
389
+ if (parts.length === 0) {
390
+ return undefined;
391
+ }
368
392
  return { subtitle: parts.join(" ") };
369
393
  }
370
394
 
@@ -394,12 +418,24 @@ export function listGithubArgs(kind: "issue" | "pr", params: ListFilters): strin
394
418
  const { repo, state, label, author, assignee, milestone, limit } = params;
395
419
 
396
420
  const args = [kind, "list", ...repoArgs(repo)];
397
- if (state) args.push("--state", state);
398
- if (label) args.push("--label", label);
399
- if (author) args.push("--author", author);
400
- if (assignee) args.push("--assignee", assignee);
401
- if (milestone) args.push("--milestone", milestone);
402
- if (limit) args.push("--limit", String(limit));
421
+ if (state) {
422
+ args.push("--state", state);
423
+ }
424
+ if (label) {
425
+ args.push("--label", label);
426
+ }
427
+ if (author) {
428
+ args.push("--author", author);
429
+ }
430
+ if (assignee) {
431
+ args.push("--assignee", assignee);
432
+ }
433
+ if (milestone) {
434
+ args.push("--milestone", milestone);
435
+ }
436
+ if (limit) {
437
+ args.push("--limit", String(limit));
438
+ }
403
439
  return args;
404
440
  }
405
441
 
@@ -472,14 +508,20 @@ export interface MergedCheck {
472
508
  }
473
509
 
474
510
  function statusBucket(state: string): CheckBucket {
475
- if (state === "success") return "pass";
476
- if (state === "failure" || state === "error") return "fail";
511
+ if (state === "success") {
512
+ return "pass";
513
+ }
514
+ if (state === "failure" || state === "error") {
515
+ return "fail";
516
+ }
477
517
  // pending, expected, and anything unknown must not end the wait
478
518
  return "pending";
479
519
  }
480
520
 
481
521
  function checkRunBucket(run: CheckRun): CheckBucket {
482
- if (run.status !== "completed" || run.conclusion === null) return "pending";
522
+ if (run.status !== "completed" || run.conclusion === null) {
523
+ return "pending";
524
+ }
483
525
  switch (run.conclusion) {
484
526
  case "success": {
485
527
  return "pass";
@@ -640,7 +682,9 @@ export async function pollPrChecks(options: PollPrChecksOptions): Promise<Checks
640
682
  let everSucceeded = false;
641
683
 
642
684
  for (let round = 1; ; round++) {
643
- if (signal.aborted) throw new Error("PR checks polling was aborted");
685
+ if (signal.aborted) {
686
+ throw new Error("PR checks polling was aborted");
687
+ }
644
688
  try {
645
689
  const [statuses, runs] = await Promise.all([
646
690
  checks.statuses(owner, repo, headSha, signal),
@@ -782,8 +826,12 @@ export function renderChecksVerdict(options: {
782
826
  ` - workflow: [${j.runName} (#${j.runId})](${j.runUrl})`,
783
827
  );
784
828
  }
785
- if (enrichmentError) lines.push(` - _Actions job details unavailable: ${enrichmentError}_`);
786
- if (pending.length > 0) lines.push(`\n_${pending.length} other check(s) still in flight._`);
829
+ if (enrichmentError) {
830
+ lines.push(` - _Actions job details unavailable: ${enrichmentError}_`);
831
+ }
832
+ if (pending.length > 0) {
833
+ lines.push(`\n_${pending.length} other check(s) still in flight._`);
834
+ }
787
835
 
788
836
  return {
789
837
  status: "failure",