@trim21/personal-pi-extensions 0.1.612 → 0.1.619

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 (77) 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/README.md +19 -10
  7. package/src/bwrap/approval-rules.ts +42 -14
  8. package/src/bwrap/approval-suggest.ts +12 -4
  9. package/src/bwrap/core.ts +39 -13
  10. package/src/bwrap/dcg-scan.ts +16 -6
  11. package/src/bwrap/holder.ts +3 -1
  12. package/src/bwrap/mihomo-config.ts +18 -6
  13. package/src/bwrap/network-stack.ts +69 -24
  14. package/src/bwrap/runtime.ts +30 -10
  15. package/src/bwrap/sandbox.ts +1 -1
  16. package/src/claude-code/common.ts +6 -2
  17. package/src/claude-code/edit-utils.ts +3 -1
  18. package/src/claude-code/files.ts +40 -14
  19. package/src/claude-code/glob.ts +12 -4
  20. package/src/claude-code/grep.ts +41 -14
  21. package/src/claude-code/session-tools.ts +18 -6
  22. package/src/claude-code/shell.ts +9 -3
  23. package/src/gh/base.ts +72 -24
  24. package/src/gh/tools/download-release-assets.ts +12 -4
  25. package/src/gh/tools/list-releases.ts +3 -1
  26. package/src/gh/tools/list-workflow-runs.ts +9 -3
  27. package/src/gh/tools/read-ci-logs.ts +21 -7
  28. package/src/gh/tools/read-repo.ts +3 -1
  29. package/src/lib/cli-args.ts +6 -2
  30. package/src/lib/cli.ts +48 -16
  31. package/src/lib/file-reads.ts +24 -8
  32. package/src/lib/github.ts +35 -12
  33. package/src/lib/lsp/adapter.ts +6 -2
  34. package/src/lib/lsp/bin.ts +12 -4
  35. package/src/lib/lsp/client.ts +205 -69
  36. package/src/lib/lsp/diagnostic.ts +6 -2
  37. package/src/lib/lsp/inspect-tool.ts +25 -9
  38. package/src/lib/lsp/lsp.ts +138 -46
  39. package/src/lib/lsp/rename-tool.ts +11 -4
  40. package/src/lib/lsp/rename.ts +25 -8
  41. package/src/lib/lsp/server-config.ts +41 -14
  42. package/src/lib/lsp/watcher.ts +51 -17
  43. package/src/lib/path.ts +18 -6
  44. package/src/lib/proc.ts +3 -1
  45. package/src/lib/proxy.ts +9 -3
  46. package/src/lib/request-policy.ts +3 -1
  47. package/src/lib/seq-state.ts +3 -1
  48. package/src/lib/ui.ts +23 -8
  49. package/src/lib/write-guard.ts +21 -7
  50. package/src/openai-cost/config.ts +3 -1
  51. package/src/openai-cost/cost.ts +54 -18
  52. package/src/openai-cost/index.ts +3 -1
  53. package/src/openai-cost/provider.ts +6 -2
  54. package/src/opencode/bash.ts +9 -3
  55. package/src/opencode/edit-engine.ts +24 -8
  56. package/src/opencode/files.ts +30 -10
  57. package/src/opencode/glob.ts +3 -1
  58. package/src/opencode/grep.ts +12 -4
  59. package/src/opencode/question.ts +3 -1
  60. package/src/opencode/ripgrep.ts +18 -6
  61. package/src/opencode/todo.ts +3 -1
  62. package/src/session-name.ts +39 -13
  63. package/src/spawn-agent-agents.ts +18 -6
  64. package/src/spawn-agent.ts +53 -18
  65. package/src/system-prompt/index.ts +9 -3
  66. package/src/talk/core.ts +143 -48
  67. package/src/talk/format.ts +12 -4
  68. package/src/talk/group.ts +9 -3
  69. package/src/talk/index.ts +30 -10
  70. package/src/talk/mailbox.ts +30 -10
  71. package/src/talk/policy.ts +6 -2
  72. package/src/talk/registry.ts +33 -11
  73. package/src/talk/storage.ts +3 -1
  74. package/src/vision-agent.ts +41 -14
  75. package/src/web/config.ts +3 -1
  76. package/src/web/fetch.ts +75 -25
  77. package/src/web/search.ts +30 -10
@@ -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",
@@ -54,7 +54,9 @@ export function releaseAssetDir(repo: string, tag: string): string {
54
54
 
55
55
  /** Split the comma-separated `pattern` toolcall parameter into gh pattern values. */
56
56
  export function releasePatterns(pattern: string | undefined): string[] {
57
- if (pattern === undefined) return [];
57
+ if (pattern === undefined) {
58
+ return [];
59
+ }
58
60
  return pattern
59
61
  .split(",")
60
62
  .map((value) => value.trim())
@@ -75,8 +77,12 @@ export function releaseDownloadArgs(options: {
75
77
  }): string[] {
76
78
  const { tag, repo, dir, patterns, archive } = options;
77
79
  const args = ["release", "download", tag, ...repoArgs(repo)];
78
- if (archive !== undefined) args.push("--archive", archive);
79
- for (const pattern of patterns) args.push("--pattern", pattern);
80
+ if (archive !== undefined) {
81
+ args.push("--archive", archive);
82
+ }
83
+ for (const pattern of patterns) {
84
+ args.push("--pattern", pattern);
85
+ }
80
86
  args.push("--dir", dir, "--skip-existing");
81
87
  return args;
82
88
  }
@@ -86,7 +92,9 @@ async function listReleaseFiles(dir: string): Promise<ReleaseFile[]> {
86
92
  const entries = await readdir(dir, { withFileTypes: true });
87
93
  const files: ReleaseFile[] = [];
88
94
  for (const entry of entries) {
89
- if (!entry.isFile()) continue;
95
+ if (!entry.isFile()) {
96
+ continue;
97
+ }
90
98
  const path = join(dir, entry.name);
91
99
  const info = await stat(path);
92
100
  files.push({ name: entry.name, path, bytes: info.size });
@@ -16,7 +16,9 @@ export function addListReleasesTool(_gh: GhClient, pi: ExtensionAPI) {
16
16
  async execute(_id, params, signal, _onUpdate, ctx) {
17
17
  const { repo, limit } = params;
18
18
  const args = ["release", "list", ...repoArgs(repo)];
19
- if (limit) args.push("--limit", String(limit));
19
+ if (limit) {
20
+ args.push("--limit", String(limit));
21
+ }
20
22
  const result = toToolResult(
21
23
  await ghExec(args, { cwd: ctx.cwd, signal, input: params }),
22
24
  params,
@@ -20,9 +20,15 @@ export function addListWorkflowRunsTool(_gh: GhClient, pi: ExtensionAPI) {
20
20
  async execute(_id, params, signal, _onUpdate, ctx) {
21
21
  const { repo, limit, status, workflow } = params;
22
22
  const args = ["run", "list", ...repoArgs(repo)];
23
- if (limit) args.push("--limit", String(limit));
24
- if (status) args.push("--status", status);
25
- if (workflow) args.push("--workflow", workflow);
23
+ if (limit) {
24
+ args.push("--limit", String(limit));
25
+ }
26
+ if (status) {
27
+ args.push("--status", status);
28
+ }
29
+ if (workflow) {
30
+ args.push("--workflow", workflow);
31
+ }
26
32
  const result = toToolResult(
27
33
  await ghExec(args, { cwd: ctx.cwd, signal, input: params }),
28
34
  params,
@@ -138,7 +138,9 @@ const TIME_MATCH_SCORE = 2;
138
138
 
139
139
  function headerTimestamp(line: string): number | null {
140
140
  const match = LOG_TIMESTAMP_RE.exec(line);
141
- if (match === null) return null;
141
+ if (match === null) {
142
+ return null;
143
+ }
142
144
  const ms = Date.parse(match[1]);
143
145
  return Number.isNaN(ms) ? null : ms;
144
146
  }
@@ -149,12 +151,18 @@ function stepHeaders(lines: string[]): StepHeader[] {
149
151
  let depth = 0;
150
152
  for (const [i, line] of lines.entries()) {
151
153
  if (line.includes("##[endgroup]")) {
152
- if (depth > 0) depth--;
154
+ if (depth > 0) {
155
+ depth--;
156
+ }
157
+ continue;
158
+ }
159
+ if (!line.includes("##[group]")) {
153
160
  continue;
154
161
  }
155
- if (!line.includes("##[group]")) continue;
156
162
  depth++;
157
- if (depth !== 1) continue;
163
+ if (depth !== 1) {
164
+ continue;
165
+ }
158
166
  const name = /##\[group\](.*)/.exec(line)?.[1].trim() ?? "";
159
167
  if (HEADER_PREFIX_RE.test(name)) {
160
168
  headers.push({
@@ -252,7 +260,9 @@ function alignStepsToHeaders(
252
260
 
253
261
  /** Exclusive end index with trailing blank lines dropped, so a span slices to real text. */
254
262
  function trimTrailingBlankLines(lines: string[], start: number, end: number): number {
255
- while (end > start && (lines[end - 1] ?? "").trim() === "") end--;
263
+ while (end > start && (lines[end - 1] ?? "").trim() === "") {
264
+ end--;
265
+ }
256
266
  return end;
257
267
  }
258
268
 
@@ -306,7 +316,9 @@ export function extractStepFromLog(
306
316
  apiSteps: readonly { number: number; name: string }[],
307
317
  ): string | null {
308
318
  const span = stepLineSpans(log, apiSteps).get(stepNumber);
309
- if (span === undefined) return null;
319
+ if (span === undefined) {
320
+ return null;
321
+ }
310
322
  return log.split("\n").slice(span.start, span.end).join("\n").trimEnd();
311
323
  }
312
324
 
@@ -382,7 +394,9 @@ async function ciLogs(gh: GhClient, call: ToolCall<JobIdParams>): Promise<ToolRe
382
394
  target = await gh.checks.job(owner, name, jobId, signal);
383
395
  } catch (error) {
384
396
  const status = (error as { status?: number }).status;
385
- if (status !== 404) throw error;
397
+ if (status !== 404) {
398
+ throw error;
399
+ }
386
400
  return failure(
387
401
  `Job ${jobId} not found in ${effectiveRepo} — job IDs come from \`get-github-workflow-jobs\`.`,
388
402
  );
@@ -15,7 +15,9 @@ export function addReadRepoTool(_gh: GhClient, pi: ExtensionAPI) {
15
15
  async execute(_id, params, signal, _onUpdate, ctx) {
16
16
  const { repo } = params;
17
17
  const args = ["repo", "view"];
18
- if (repo) args.push(repo);
18
+ if (repo) {
19
+ args.push(repo);
20
+ }
19
21
  const result = toToolResult(
20
22
  await ghExec(args, { cwd: ctx.cwd, signal, input: params }),
21
23
  params,
@@ -35,7 +35,9 @@ export function shlexSplit(raw: string): string[] {
35
35
  continue;
36
36
  }
37
37
  if (c === "\\") {
38
- if (i + 1 < raw.length) cur += raw[i + 1];
38
+ if (i + 1 < raw.length) {
39
+ cur += raw[i + 1];
40
+ }
39
41
  i += 2;
40
42
  continue;
41
43
  }
@@ -53,6 +55,8 @@ export function shlexSplit(raw: string): string[] {
53
55
  if (quote !== undefined) {
54
56
  throw new SyntaxError(`unterminated quote in command arguments: ${raw}`);
55
57
  }
56
- if (cur) tokens.push(cur);
58
+ if (cur) {
59
+ tokens.push(cur);
60
+ }
57
61
  return tokens;
58
62
  }
package/src/lib/cli.ts CHANGED
@@ -84,16 +84,28 @@ interface FlagInfo {
84
84
 
85
85
  /** schema 的种类名,仅用于报错文案。 */
86
86
  function schemaKindLabel(schema: TSchema): string {
87
- if ("~kind" in schema && typeof schema["~kind"] === "string") return schema["~kind"];
88
- if ("type" in schema && typeof schema.type === "string") return schema.type;
87
+ if ("~kind" in schema && typeof schema["~kind"] === "string") {
88
+ return schema["~kind"];
89
+ }
90
+ if ("type" in schema && typeof schema.type === "string") {
91
+ return schema.type;
92
+ }
89
93
  return "unknown";
90
94
  }
91
95
 
92
96
  function kindOf(key: string, schema: TSchema): FlagKind {
93
- if (IsKind(schema, "Boolean")) return "boolean";
94
- if (IsKind(schema, "String")) return "string";
95
- if (IsKind(schema, "Number") || IsKind(schema, "Integer")) return "number";
96
- if (IsKind(schema, "Union")) return "enum";
97
+ if (IsKind(schema, "Boolean")) {
98
+ return "boolean";
99
+ }
100
+ if (IsKind(schema, "String")) {
101
+ return "string";
102
+ }
103
+ if (IsKind(schema, "Number") || IsKind(schema, "Integer")) {
104
+ return "number";
105
+ }
106
+ if (IsKind(schema, "Union")) {
107
+ return "enum";
108
+ }
97
109
  throw new TypeError(
98
110
  `Unsupported flag type for '${key}': ${schemaKindLabel(schema)} ` +
99
111
  "(use Type.Boolean/String/Number/Integer or a string literal union)",
@@ -118,10 +130,14 @@ function flagDescription(schema: TSchema): string {
118
130
 
119
131
  /** Allowed values for a string-literal union flag, or undefined for mixed unions. */
120
132
  function enumValues(schema: TSchema): string[] | undefined {
121
- if (!("anyOf" in schema) || !isUnknownArray(schema.anyOf)) return undefined;
133
+ if (!("anyOf" in schema) || !isUnknownArray(schema.anyOf)) {
134
+ return undefined;
135
+ }
122
136
  const values: string[] = [];
123
137
  for (const variant of schema.anyOf) {
124
- if (!isRecord(variant) || typeof variant.const !== "string") return undefined;
138
+ if (!isRecord(variant) || typeof variant.const !== "string") {
139
+ return undefined;
140
+ }
125
141
  values.push(variant.const);
126
142
  }
127
143
  return values;
@@ -164,7 +180,9 @@ function helpResult<TFlags extends TObject>(
164
180
  autoHelp: boolean,
165
181
  ): CommandResult<TFlags> {
166
182
  const lines = [`Usage: /${spec.name} ${spec.usage}`];
167
- if (spec.description) lines.push("", spec.description);
183
+ if (spec.description) {
184
+ lines.push("", spec.description);
185
+ }
168
186
  const rows = flags.map((f) => ({
169
187
  rawName:
170
188
  (f.short ? `-${f.short}, ` : "") +
@@ -172,15 +190,21 @@ function helpResult<TFlags extends TObject>(
172
190
  (f.kind === "boolean" ? "" : ` ${f.placeholder}`),
173
191
  description: f.description,
174
192
  }));
175
- if (autoHelp) rows.push({ rawName: "-h, --help", description: "Display this message" });
193
+ if (autoHelp) {
194
+ rows.push({ rawName: "-h, --help", description: "Display this message" });
195
+ }
176
196
  if (rows.length > 0) {
177
197
  lines.push("", "Options:");
178
198
  const width = Math.max(...rows.map((r) => r.rawName.length));
179
- for (const r of rows) lines.push(` ${r.rawName.padEnd(width)} ${r.description}`);
199
+ for (const r of rows) {
200
+ lines.push(` ${r.rawName.padEnd(width)} ${r.description}`);
201
+ }
180
202
  }
181
203
  if (spec.examples?.length) {
182
204
  lines.push("", "Examples:");
183
- for (const e of spec.examples) lines.push(` ${e}`);
205
+ for (const e of spec.examples) {
206
+ lines.push(` ${e}`);
207
+ }
184
208
  }
185
209
  return { kind: "help", text: lines.join("\n") };
186
210
  }
@@ -233,9 +257,13 @@ export function parseCommand<TFlags extends TObject>(
233
257
  const body = token.slice(2);
234
258
  const eq = body.indexOf("=");
235
259
  const name = eq === -1 ? body : body.slice(0, eq);
236
- if (autoHelp && name === "help") return helpResult(spec, flags, autoHelp);
260
+ if (autoHelp && name === "help") {
261
+ return helpResult(spec, flags, autoHelp);
262
+ }
237
263
  const info = byLong.get(name);
238
- if (!info) return errorResult(spec, `Unknown option '--${name}'`);
264
+ if (!info) {
265
+ return errorResult(spec, `Unknown option '--${name}'`);
266
+ }
239
267
  const inline = eq === -1 ? undefined : body.slice(eq + 1);
240
268
  if (inline !== undefined) {
241
269
  rawFlags[info.key] = inline;
@@ -258,9 +286,13 @@ export function parseCommand<TFlags extends TObject>(
258
286
  const rest = token.slice(1);
259
287
  for (let j = 0; j < rest.length; j++) {
260
288
  const c = rest[j];
261
- if (autoHelp && c === "h") return helpResult(spec, flags, autoHelp);
289
+ if (autoHelp && c === "h") {
290
+ return helpResult(spec, flags, autoHelp);
291
+ }
262
292
  const info = byShort.get(c);
263
- if (!info) return errorResult(spec, `Unknown option '-${c}'`);
293
+ if (!info) {
294
+ return errorResult(spec, `Unknown option '-${c}'`);
295
+ }
264
296
  if (info.kind === "boolean") {
265
297
  rawFlags[info.key] = true;
266
298
  continue;
@@ -64,7 +64,9 @@ export async function digestIfExists(filePath: string): Promise<string | undefin
64
64
  try {
65
65
  return await fileDigest(filePath);
66
66
  } catch (error) {
67
- if (isMissingPath(error)) return undefined;
67
+ if (isMissingPath(error)) {
68
+ return undefined;
69
+ }
68
70
  throw error;
69
71
  }
70
72
  }
@@ -77,7 +79,9 @@ export async function readStateKey(filePath: string): Promise<string> {
77
79
  try {
78
80
  return await realpath(filePath);
79
81
  } catch (error) {
80
- if (isMissingPath(error)) return filePath;
82
+ if (isMissingPath(error)) {
83
+ return filePath;
84
+ }
81
85
  throw error;
82
86
  }
83
87
  }
@@ -120,7 +124,9 @@ export function requireUnchangedRead(
120
124
  currentDigest: string | undefined,
121
125
  ): void {
122
126
  const readSnapshot = state.reads.get(key);
123
- if (!readSnapshot) return;
127
+ if (!readSnapshot) {
128
+ return;
129
+ }
124
130
  if (currentDigest === undefined || readSnapshot.digest !== currentDigest) {
125
131
  throw new Error(
126
132
  "File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.",
@@ -135,9 +141,13 @@ export function requireUnchangedRead(
135
141
  */
136
142
  export function deserializeReads(data: unknown): Map<string, FileSnapshot> {
137
143
  const reads = new Map<string, FileSnapshot>();
138
- if (typeof data !== "object" || data === null || Array.isArray(data)) return reads;
144
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
145
+ return reads;
146
+ }
139
147
  for (const [filePath, snapshot] of Object.entries(data)) {
140
- if (Value.Check(fileSnapshotSchema, snapshot)) reads.set(filePath, snapshot);
148
+ if (Value.Check(fileSnapshotSchema, snapshot)) {
149
+ reads.set(filePath, snapshot);
150
+ }
141
151
  }
142
152
  return reads;
143
153
  }
@@ -153,10 +163,16 @@ export function restoreReads(
153
163
  ): void {
154
164
  state.reads.clear();
155
165
  for (const entry of sessionManager.getBranch()) {
156
- if (entry.type !== "message" || entry.message.role !== "toolResult") continue;
157
- if (!toolNames.has(entry.message.toolName)) continue;
166
+ if (entry.type !== "message" || entry.message.role !== "toolResult") {
167
+ continue;
168
+ }
169
+ if (!toolNames.has(entry.message.toolName)) {
170
+ continue;
171
+ }
158
172
  const details = entry.message.details;
159
- if (!isRecord(details) || !details.reads) continue;
173
+ if (!isRecord(details) || !details.reads) {
174
+ continue;
175
+ }
160
176
  for (const [filePath, snapshot] of deserializeReads(details.reads)) {
161
177
  state.reads.set(filePath, snapshot);
162
178
  }