@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
@@ -88,7 +88,9 @@ export function mergeServerRecords(
88
88
  ): Record<string, ServerConfig> | undefined {
89
89
  const merged: Record<string, ServerConfig> = {};
90
90
  for (const record of records) {
91
- if (!record) continue;
91
+ if (!record) {
92
+ continue;
93
+ }
92
94
  Object.assign(merged, record);
93
95
  }
94
96
  return Object.keys(merged).length > 0 ? merged : undefined;
@@ -118,8 +120,12 @@ function interpolateEnvVars(value: string, env: NodeJS.ProcessEnv): string {
118
120
 
119
121
  /** 深遍历配置值,对所有字符串做环境变量插值。 */
120
122
  function interpolateEnvDeep(value: unknown, env: NodeJS.ProcessEnv): unknown {
121
- if (typeof value === "string") return interpolateEnvVars(value, env);
122
- if (Array.isArray(value)) return value.map((item) => interpolateEnvDeep(item, env));
123
+ if (typeof value === "string") {
124
+ return interpolateEnvVars(value, env);
125
+ }
126
+ if (Array.isArray(value)) {
127
+ return value.map((item) => interpolateEnvDeep(item, env));
128
+ }
123
129
  if (value !== null && typeof value === "object") {
124
130
  return Object.fromEntries(
125
131
  Object.entries(value).map(([key, item]) => [key, interpolateEnvDeep(item, env)]),
@@ -135,7 +141,9 @@ async function resolveEnv(
135
141
  cwd: string,
136
142
  ): Promise<NodeJS.ProcessEnv | undefined> {
137
143
  const entries = Object.entries(config ?? {});
138
- if (entries.length === 0) return undefined;
144
+ if (entries.length === 0) {
145
+ return undefined;
146
+ }
139
147
  const resolved = await Promise.all(
140
148
  entries.map(async ([key, value]): Promise<[string, string]> => {
141
149
  if (typeof value === "string") {
@@ -226,8 +234,12 @@ function parseInitializationOptionsOutput(
226
234
 
227
235
  /** 非法输出的类型描述(typebox 的 ParseError 只有 "Parse",自己给出可读文案)。 */
228
236
  function describeValue(value: unknown): string {
229
- if (value === null) return "null";
230
- if (Array.isArray(value)) return "array";
237
+ if (value === null) {
238
+ return "null";
239
+ }
240
+ if (Array.isArray(value)) {
241
+ return "array";
242
+ }
231
243
  return typeof value;
232
244
  }
233
245
 
@@ -246,7 +258,9 @@ async function resolveInitializationOptions(
246
258
  const staticOptions = interpolateEnvDeep(config.initializationOptions, variables) as
247
259
  Record<string, unknown> | undefined;
248
260
  const command = config.initializationOptionsCommand;
249
- if (!command) return staticOptions;
261
+ if (!command) {
262
+ return staticOptions;
263
+ }
250
264
  const argv = command.map((arg) => interpolateEnvVars(resolveTemplate(arg, root, cwd), variables));
251
265
  const output = await runConfigCommand(argv, {
252
266
  cwd: root,
@@ -258,7 +272,9 @@ async function resolveInitializationOptions(
258
272
 
259
273
  /** 解析可执行文件:绝对/相对路径直接用;名字走项目工作区(node_modules/.bin 等)→ PATH。 */
260
274
  async function resolveBinary(bin: string, root: string, cwd: string): Promise<string | undefined> {
261
- if (isAbsolute(bin)) return existsSync(bin) ? bin : undefined;
275
+ if (isAbsolute(bin)) {
276
+ return existsSync(bin) ? bin : undefined;
277
+ }
262
278
  if (bin.includes("/") || bin.includes("\\")) {
263
279
  const relativePath = join(cwd, bin);
264
280
  return exists(relativePath) ? relativePath : undefined;
@@ -277,14 +293,21 @@ export function matchesInclude(
277
293
  root: string,
278
294
  cwd: string,
279
295
  ): boolean {
280
- if (patterns.length === 0) return true;
296
+ if (patterns.length === 0) {
297
+ return true;
298
+ }
281
299
  const positives: string[] = [];
282
300
  const negatives: string[] = [];
283
301
  for (const pattern of patterns) {
284
- if (pattern.startsWith("!")) negatives.push(pattern.slice(1));
285
- else positives.push(pattern);
302
+ if (pattern.startsWith("!")) {
303
+ negatives.push(pattern.slice(1));
304
+ } else {
305
+ positives.push(pattern);
306
+ }
307
+ }
308
+ if (positives.length === 0) {
309
+ positives.push("**");
286
310
  }
287
- if (positives.length === 0) positives.push("**");
288
311
  const candidates = [relative(root, file), relative(cwd, file)]
289
312
  .map((p) => p.split(sep).join("/"))
290
313
  .filter((p) => !p.startsWith(".."));
@@ -319,9 +342,13 @@ export class ConfigAdapter implements LspServerAdapter {
319
342
 
320
343
  async spawn(root: string, cwd: string): Promise<LspServerHandle | undefined> {
321
344
  const bin = this.config.bin;
322
- if (!bin) return undefined;
345
+ if (!bin) {
346
+ return undefined;
347
+ }
323
348
  const resolved = await resolveBinary(bin, root, cwd);
324
- if (!resolved) return undefined;
349
+ if (!resolved) {
350
+ return undefined;
351
+ }
325
352
  const env = await resolveEnv(this.config.env, root, cwd);
326
353
  const initialization = await resolveInitializationOptions(this.config, root, cwd, env);
327
354
  return {
@@ -68,7 +68,9 @@ export interface WorkspaceWatcher {
68
68
 
69
69
  function isIgnored(path: string, dir: string, patterns: string[]): boolean {
70
70
  const candidate = relative(dir, path).split(sep).join("/");
71
- if (candidate.startsWith("..")) return false;
71
+ if (candidate.startsWith("..")) {
72
+ return false;
73
+ }
72
74
  return patterns.some((pattern) => minimatch(candidate, pattern));
73
75
  }
74
76
 
@@ -98,15 +100,21 @@ export function watchWorkspace(
98
100
  let maxTimer: ReturnType<typeof setTimeout> | undefined;
99
101
 
100
102
  const clearTimers = (): void => {
101
- if (flushTimer) clearTimeout(flushTimer);
102
- if (maxTimer) clearTimeout(maxTimer);
103
+ if (flushTimer) {
104
+ clearTimeout(flushTimer);
105
+ }
106
+ if (maxTimer) {
107
+ clearTimeout(maxTimer);
108
+ }
103
109
  flushTimer = undefined;
104
110
  maxTimer = undefined;
105
111
  };
106
112
 
107
113
  const flush = (): void => {
108
114
  clearTimers();
109
- if (pending.length === 0) return;
115
+ if (pending.length === 0) {
116
+ return;
117
+ }
110
118
  let batch = pending;
111
119
  pending = [];
112
120
  if (batch.length > maxBatch) {
@@ -117,34 +125,54 @@ export function watchWorkspace(
117
125
  };
118
126
 
119
127
  const push = (change: FileChange): void => {
120
- if (stopped) return;
121
- if (pending.length === 0) maxTimer = setTimeout(flush, flushMs);
128
+ if (stopped) {
129
+ return;
130
+ }
131
+ if (pending.length === 0) {
132
+ maxTimer = setTimeout(flush, flushMs);
133
+ }
122
134
  pending.push(change);
123
- if (flushTimer) clearTimeout(flushTimer);
135
+ if (flushTimer) {
136
+ clearTimeout(flushTimer);
137
+ }
124
138
  flushTimer = setTimeout(flush, debounceMs);
125
139
  };
126
140
 
127
141
  const classify = async (path: string, type: FileChangeType): Promise<FileChange | undefined> => {
128
- if (path === dir) return undefined;
142
+ if (path === dir) {
143
+ return undefined;
144
+ }
129
145
  let isDirectory = false;
130
146
  let exists = true;
131
147
  try {
132
148
  const info = await lstat(path);
133
149
  isDirectory = info.isDirectory();
134
- if (isDirectory) seenDirectories.add(path);
150
+ if (isDirectory) {
151
+ seenDirectories.add(path);
152
+ }
135
153
  } catch {
136
154
  exists = false;
137
- if (seenDirectories.delete(path)) isDirectory = true;
155
+ if (seenDirectories.delete(path)) {
156
+ isDirectory = true;
157
+ }
158
+ }
159
+ if (type === "deleted") {
160
+ return { path, type, isDirectory };
138
161
  }
139
- if (type === "deleted") return { path, type, isDirectory };
140
162
  // 事件与 lstat 之间的竞态:目标已消失按删除处理
141
- if (!exists) return { path, type: "deleted", isDirectory };
142
- if (isDirectory) return undefined;
163
+ if (!exists) {
164
+ return { path, type: "deleted", isDirectory };
165
+ }
166
+ if (isDirectory) {
167
+ return undefined;
168
+ }
143
169
  return { path, type, isDirectory };
144
170
  };
145
171
 
146
172
  const handleError = (error: unknown): void => {
147
- if (stopped) return;
173
+ if (stopped) {
174
+ return;
175
+ }
148
176
  options?.onError?.(`workspace watcher failed for ${dir}: ${String(error)}`);
149
177
  };
150
178
 
@@ -155,13 +183,19 @@ export function watchWorkspace(
155
183
  }
156
184
  void (async () => {
157
185
  for (const event of events) {
158
- if (stopped) return;
186
+ if (stopped) {
187
+ return;
188
+ }
159
189
  const path = normalize(event.path);
160
190
  const type: FileChangeType =
161
191
  event.type === "create" ? "created" : event.type === "update" ? "changed" : "deleted";
162
192
  const change = await classify(path, type);
163
- if (!change) continue;
164
- if (isIgnored(change.path, dir, ignorePatterns)) continue;
193
+ if (!change) {
194
+ continue;
195
+ }
196
+ if (isIgnored(change.path, dir, ignorePatterns)) {
197
+ continue;
198
+ }
165
199
  push(change);
166
200
  }
167
201
  })();
package/src/lib/path.ts CHANGED
@@ -13,8 +13,12 @@ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:pat
13
13
  * - anything else (including `~user`) is left untouched.
14
14
  */
15
15
  export function expandHome(p: string): string {
16
- if (p === "~") return homedir();
17
- if (p.startsWith("~/")) return join(homedir(), p.slice(2));
16
+ if (p === "~") {
17
+ return homedir();
18
+ }
19
+ if (p.startsWith("~/")) {
20
+ return join(homedir(), p.slice(2));
21
+ }
18
22
  return p;
19
23
  }
20
24
 
@@ -72,18 +76,26 @@ export function formatDisplayPath(cwd: string, filePath: string): string {
72
76
  export const MAX_SUBTITLE_PATH_LENGTH = 30;
73
77
 
74
78
  function shortenSubtitlePath(filePath: string, display: string): string {
75
- if (display.length <= MAX_SUBTITLE_PATH_LENGTH) return display;
79
+ if (display.length <= MAX_SUBTITLE_PATH_LENGTH) {
80
+ return display;
81
+ }
76
82
  const name = basename(filePath);
77
83
  const parentDir = dirname(filePath);
78
84
  const parentName = basename(parentDir);
79
- if (parentName === "" || parentName === "." || parentDir === parentName) return name;
85
+ if (parentName === "" || parentName === "." || parentDir === parentName) {
86
+ return name;
87
+ }
80
88
  return join(parentName, name);
81
89
  }
82
90
 
83
91
  function formatSubtitleCounts(errorCount?: number, warningCount?: number): string {
84
92
  const parts: string[] = [];
85
- if (errorCount) parts.push(`ⓧ ${errorCount}`);
86
- if (warningCount) parts.push(`⚠ ${warningCount}`);
93
+ if (errorCount) {
94
+ parts.push(`ⓧ ${errorCount}`);
95
+ }
96
+ if (warningCount) {
97
+ parts.push(`⚠ ${warningCount}`);
98
+ }
87
99
  return parts.join(" ");
88
100
  }
89
101
 
package/src/lib/proc.ts CHANGED
@@ -14,7 +14,9 @@ export async function forEachLine(
14
14
  while ((newline = buffer.indexOf("\n")) !== -1) {
15
15
  const line = buffer.slice(0, newline);
16
16
  buffer = buffer.slice(newline + 1);
17
- if (callback(line) === false) return;
17
+ if (callback(line) === false) {
18
+ return;
19
+ }
18
20
  }
19
21
  }
20
22
  }
package/src/lib/proxy.ts CHANGED
@@ -63,7 +63,9 @@ const NO_PROXY_ENV_NAMES = ["NO_PROXY", "no_proxy"];
63
63
  function firstEnv(names: readonly string[], env: NodeJS.ProcessEnv): string | undefined {
64
64
  for (const name of names) {
65
65
  const value = env[name]?.trim();
66
- if (value) return value;
66
+ if (value) {
67
+ return value;
68
+ }
67
69
  }
68
70
  return undefined;
69
71
  }
@@ -124,7 +126,9 @@ export function readProxySettings(
124
126
  /** 要注入 gh 子进程的代理环境变量;未配置代理时为空对象(子进程继承父进程环境)。 */
125
127
  export function proxyEnvVars(settings: HttpProxySettings): NodeJS.ProcessEnv {
126
128
  const { proxy, noProxy } = settings;
127
- if (!proxy) return {};
129
+ if (!proxy) {
130
+ return {};
131
+ }
128
132
  return {
129
133
  // gh 是 Go 程序,https 目标只认 HTTPS_PROXY;统一填全部变量,避免用户只设了
130
134
  // HTTP_PROXY 时 https 请求直连。小写变体给 curl 系的子进程用。
@@ -179,7 +183,9 @@ export function createHttpProxy(
179
183
  let dispatcher: EnvHttpProxyAgent | undefined;
180
184
 
181
185
  const fetch: typeof undiciFetch = (input, init) => {
182
- if (!proxy) return undiciFetch(input, init);
186
+ if (!proxy) {
187
+ return undiciFetch(input, init);
188
+ }
183
189
  dispatcher ??= createProxyDispatcher(proxy, noProxy);
184
190
  return undiciFetch(input, { ...init, dispatcher });
185
191
  };
@@ -33,7 +33,9 @@ export function createRequestPolicy(events?: EventBus): RequestPolicy {
33
33
  let denyRequests = false;
34
34
 
35
35
  events?.on(REQUEST_POLICY_CHANNEL, (data) => {
36
- if (Value.Check(requestPolicyMessageSchema, data)) denyRequests = data.denyRequests;
36
+ if (Value.Check(requestPolicyMessageSchema, data)) {
37
+ denyRequests = data.denyRequests;
38
+ }
37
39
  });
38
40
 
39
41
  return {
@@ -25,7 +25,9 @@ export function createSeqState(): SeqState {
25
25
 
26
26
  tails.set(key, tail);
27
27
  void tail.finally(() => {
28
- if (tails.get(key) === tail) tails.delete(key);
28
+ if (tails.get(key) === tail) {
29
+ tails.delete(key);
30
+ }
29
31
  });
30
32
 
31
33
  return run;
package/src/lib/ui.ts CHANGED
@@ -42,9 +42,13 @@ export async function selectWithOptionalInput(
42
42
  actions.map((action) => action.label),
43
43
  { signal },
44
44
  );
45
- if (choice === undefined) return undefined;
45
+ if (choice === undefined) {
46
+ return undefined;
47
+ }
46
48
  const action = actions.find((candidate) => candidate.label === choice);
47
- if (action?.inputPrompt === undefined) return { label: choice, prompted: false };
49
+ if (action?.inputPrompt === undefined) {
50
+ return { label: choice, prompted: false };
51
+ }
48
52
  const answer = await ui.input(title, action.inputPrompt, { signal });
49
53
  return {
50
54
  label: choice,
@@ -87,7 +91,9 @@ export async function selectMultiple(
87
91
  }
88
92
  // 经 inputPrompt 输入的自定义答案不是固定条目:单独列出,同样可反选
89
93
  for (const label of selected) {
90
- if (entries.some((entry) => entry.label === label)) continue;
94
+ if (entries.some((entry) => entry.label === label)) {
95
+ continue;
96
+ }
91
97
  const display = `${CHECKED_PREFIX}${label}`;
92
98
  displayToLabel.set(display, label);
93
99
  round.push({ label: display });
@@ -98,16 +104,25 @@ export async function selectMultiple(
98
104
  ui,
99
105
  opts,
100
106
  );
101
- if (result === undefined || result.label === opts.doneLabel) break;
107
+ if (result === undefined || result.label === opts.doneLabel) {
108
+ break;
109
+ }
102
110
  if (result.prompted) {
103
111
  // 自定义答案进入已选并继续循环:可反选或继续勾选,最终手动提交
104
- if (result.input && !selectedSet.has(result.input)) selected.push(result.input);
112
+ if (result.input && !selectedSet.has(result.input)) {
113
+ selected.push(result.input);
114
+ }
105
115
  continue;
106
116
  }
107
117
  const label = displayToLabel.get(result.label);
108
- if (label === undefined) continue;
109
- if (selectedSet.has(label)) selected.splice(selected.indexOf(label), 1);
110
- else selected.push(label);
118
+ if (label === undefined) {
119
+ continue;
120
+ }
121
+ if (selectedSet.has(label)) {
122
+ selected.splice(selected.indexOf(label), 1);
123
+ } else {
124
+ selected.push(label);
125
+ }
111
126
  }
112
127
  return selected;
113
128
  }
@@ -33,10 +33,14 @@ function isInside(dir: string, filePath: string): boolean {
33
33
  }
34
34
 
35
35
  function isPathAllowed(absolutePath: string, cwd: string): boolean {
36
- if (isInside(cwd, absolutePath)) return true;
36
+ if (isInside(cwd, absolutePath)) {
37
+ return true;
38
+ }
37
39
 
38
40
  for (const allowed of ALWAYS_ALLOW) {
39
- if (isInside(allowed, absolutePath)) return true;
41
+ if (isInside(allowed, absolutePath)) {
42
+ return true;
43
+ }
40
44
  }
41
45
 
42
46
  return false;
@@ -144,9 +148,13 @@ export async function guardWriteAccess(
144
148
  ctx: WriteGuardContext | undefined,
145
149
  opts: WriteGuardOptions,
146
150
  ): Promise<void> {
147
- if (!ctx) return;
151
+ if (!ctx) {
152
+ return;
153
+ }
148
154
  const { absolutePath } = opts;
149
- if (isPathAllowed(absolutePath, ctx.cwd)) return;
155
+ if (isPathAllowed(absolutePath, ctx.cwd)) {
156
+ return;
157
+ }
150
158
 
151
159
  // 非沙盒请求策略生效时不弹审批框:工作区外写入按用户点 "Block"(无理由)处理。
152
160
  // 放在 win32 / 无 UI 分支之前,策略优先级高于各平台的降级路径。
@@ -182,12 +190,18 @@ export async function guardWriteAccess(
182
190
  ctx.abort?.();
183
191
  throw new Error(`user deny ${opts.toolName}: cancelled`);
184
192
  }
185
- if (choice === "Approve once") return;
186
- if (choice === "Block") throw new Error(`user deny ${opts.toolName}: blocked`);
193
+ if (choice === "Approve once") {
194
+ return;
195
+ }
196
+ if (choice === "Block") {
197
+ throw new Error(`user deny ${opts.toolName}: blocked`);
198
+ }
187
199
  const feedback = await ctx.ui.input("Why was this write denied?", undefined, {
188
200
  signal: opts.signal,
189
201
  });
190
- if (feedback === undefined) continue;
202
+ if (feedback === undefined) {
203
+ continue;
204
+ }
191
205
  throw new Error(
192
206
  feedback ? `user deny ${opts.toolName}: ${feedback}` : `user deny ${opts.toolName}: blocked`,
193
207
  );
@@ -65,7 +65,9 @@ function nonEmpty(value: string | undefined): string | undefined {
65
65
 
66
66
  function normalizeModel(model: Static<typeof modelSchema>): OpenaiCostModelConfig | undefined {
67
67
  const id = nonEmpty(model.id);
68
- if (!id) return undefined;
68
+ if (!id) {
69
+ return undefined;
70
+ }
69
71
  const input = model.input ?? [];
70
72
  return {
71
73
  id,
@@ -48,10 +48,16 @@ const chunkSchema = Type.Object(
48
48
  function costFromUsage(
49
49
  usage: { cost?: number | { total?: number } } | undefined,
50
50
  ): number | undefined {
51
- if (!usage) return undefined;
51
+ if (!usage) {
52
+ return undefined;
53
+ }
52
54
  const asNumber = finiteNumber(usage.cost);
53
- if (asNumber !== undefined) return asNumber;
54
- if (usage.cost && typeof usage.cost === "object") return finiteNumber(usage.cost.total);
55
+ if (asNumber !== undefined) {
56
+ return asNumber;
57
+ }
58
+ if (usage.cost && typeof usage.cost === "object") {
59
+ return finiteNumber(usage.cost.total);
60
+ }
55
61
  return undefined;
56
62
  }
57
63
 
@@ -64,19 +70,27 @@ export function extractReportedCost(value: unknown): number | undefined {
64
70
  return undefined;
65
71
  }
66
72
  const fromUsage = costFromUsage(chunk.usage);
67
- if (fromUsage !== undefined) return fromUsage;
73
+ if (fromUsage !== undefined) {
74
+ return fromUsage;
75
+ }
68
76
  for (const choice of chunk.choices ?? []) {
69
77
  const fromChoice = costFromUsage(choice.usage);
70
- if (fromChoice !== undefined) return fromChoice;
78
+ if (fromChoice !== undefined) {
79
+ return fromChoice;
80
+ }
71
81
  }
72
82
  return undefined;
73
83
  }
74
84
 
75
85
  export function costFromSseLine(line: string): number | undefined {
76
86
  const trimmed = line.trim();
77
- if (!trimmed.startsWith("data:")) return undefined;
87
+ if (!trimmed.startsWith("data:")) {
88
+ return undefined;
89
+ }
78
90
  const data = trimmed.slice("data:".length).trim();
79
- if (!data || data === "[DONE]") return undefined;
91
+ if (!data || data === "[DONE]") {
92
+ return undefined;
93
+ }
80
94
  try {
81
95
  return extractReportedCost(JSON.parse(data) as unknown);
82
96
  } catch {
@@ -92,18 +106,24 @@ export async function scanSseCost(body: ReadableStream<Uint8Array>): Promise<num
92
106
  try {
93
107
  for (;;) {
94
108
  const { done, value } = await reader.read();
95
- if (done) break;
109
+ if (done) {
110
+ break;
111
+ }
96
112
  buffer += decoder.decode(value, { stream: true });
97
113
  const lines = buffer.split("\n");
98
114
  buffer = lines.pop() ?? "";
99
115
  for (const line of lines) {
100
116
  const cost = costFromSseLine(line);
101
- if (cost !== undefined) last = cost;
117
+ if (cost !== undefined) {
118
+ last = cost;
119
+ }
102
120
  }
103
121
  }
104
122
  buffer += decoder.decode();
105
123
  const cost = costFromSseLine(buffer);
106
- if (cost !== undefined) last = cost;
124
+ if (cost !== undefined) {
125
+ last = cost;
126
+ }
107
127
  return last;
108
128
  } catch {
109
129
  return last;
@@ -115,14 +135,22 @@ export async function scanSseCost(body: ReadableStream<Uint8Array>): Promise<num
115
135
  type FetchInput = Parameters<typeof globalThis.fetch>[0];
116
136
 
117
137
  function requestUrl(input: FetchInput): string {
118
- if (typeof input === "string") return input;
119
- if (input instanceof URL) return input.href;
138
+ if (typeof input === "string") {
139
+ return input;
140
+ }
141
+ if (input instanceof URL) {
142
+ return input.href;
143
+ }
120
144
  return input.url;
121
145
  }
122
146
 
123
147
  function shouldScan(input: FetchInput, response: Response): boolean {
124
- if (!response.ok || !response.body) return false;
125
- if (requestUrl(input).includes("/chat/completions")) return true;
148
+ if (!response.ok || !response.body) {
149
+ return false;
150
+ }
151
+ if (requestUrl(input).includes("/chat/completions")) {
152
+ return true;
153
+ }
126
154
  const contentType = response.headers.get("content-type") ?? "";
127
155
  return contentType.includes("text/event-stream");
128
156
  }
@@ -133,7 +161,9 @@ export function createReportedCostCapture(
133
161
  let scan: Promise<number | undefined> | undefined;
134
162
  const fetch: typeof globalThis.fetch = async (input, init) => {
135
163
  const response = await innerFetch(input, init);
136
- if (!shouldScan(input, response) || !response.body) return response;
164
+ if (!shouldScan(input, response) || !response.body) {
165
+ return response;
166
+ }
137
167
  const [forSdk, forScan] = response.body.tee();
138
168
  scan = scanSseCost(forScan);
139
169
  return new Response(forSdk, {
@@ -157,8 +187,12 @@ export function applyReportedCost(usage: Usage, reported: number): void {
157
187
  }
158
188
 
159
189
  function messageOf(event: AssistantMessageEvent): AssistantMessage {
160
- if (event.type === "done") return event.message;
161
- if (event.type === "error") return event.error;
190
+ if (event.type === "done") {
191
+ return event.message;
192
+ }
193
+ if (event.type === "error") {
194
+ return event.error;
195
+ }
162
196
  return event.partial;
163
197
  }
164
198
 
@@ -166,7 +200,9 @@ export function applyCostToEvent(
166
200
  event: AssistantMessageEvent,
167
201
  reported: number | undefined,
168
202
  ): AssistantMessageEvent {
169
- if (reported === undefined) return event;
203
+ if (reported === undefined) {
204
+ return event;
205
+ }
170
206
  applyReportedCost(messageOf(event).usage, reported);
171
207
  return event;
172
208
  }
@@ -9,6 +9,8 @@ import { createOpenaiCostProvider } from "./provider.js";
9
9
 
10
10
  export default async function openaiCostExtension(pi: ExtensionAPI) {
11
11
  const config = await loadOpenaiCostConfig();
12
- if (!config) return;
12
+ if (!config) {
13
+ return;
14
+ }
13
15
  pi.registerProvider(createOpenaiCostProvider(config));
14
16
  }
@@ -69,7 +69,9 @@ export function mapRemoteModels(
69
69
  const models: Model<"openai-completions">[] = [];
70
70
  for (const item of payload.data) {
71
71
  const id = item.id.trim();
72
- if (!id) continue;
72
+ if (!id) {
73
+ continue;
74
+ }
73
75
  models.push(
74
76
  toPiModel(config, {
75
77
  id,
@@ -93,7 +95,9 @@ export async function fetchRemoteModels(
93
95
  ): Promise<Model<"openai-completions">[]> {
94
96
  const fetchImpl = options.fetch ?? globalThis.fetch;
95
97
  const headers: Record<string, string> = { Accept: "application/json" };
96
- if (options.apiKey) headers.Authorization = `Bearer ${options.apiKey}`;
98
+ if (options.apiKey) {
99
+ headers.Authorization = `Bearer ${options.apiKey}`;
100
+ }
97
101
  const response = await fetchImpl(modelsUrl(config.baseUrl), { headers, signal: options.signal });
98
102
  const raw = await response.text();
99
103
  if (!response.ok) {
@@ -29,9 +29,13 @@ function appendTruncationNotice(
29
29
  truncation: TruncationResult,
30
30
  fullOutputPath: string | undefined,
31
31
  ): string {
32
- if (!truncation.truncated) return text;
32
+ if (!truncation.truncated) {
33
+ return text;
34
+ }
33
35
  let out = `${text}\n\n${CAPTURE_TRUNCATED_NOTICE}`;
34
- if (fullOutputPath) out += `\nFull output: ${fullOutputPath}`;
36
+ if (fullOutputPath) {
37
+ out += `\nFull output: ${fullOutputPath}`;
38
+ }
35
39
  return out;
36
40
  }
37
41
 
@@ -106,7 +110,9 @@ export default function opencodeBash(
106
110
  onUpdate,
107
111
  });
108
112
  } catch (error) {
109
- if (!(error instanceof Error)) throw error;
113
+ if (!(error instanceof Error)) {
114
+ throw error;
115
+ }
110
116
  if (error instanceof BashInterruptedError) {
111
117
  const text = appendTruncationNotice(
112
118
  error.partial.output || "",