@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
@@ -81,7 +81,9 @@ export function deriveAddr(cwd: string, agentId: string): string {
81
81
 
82
82
  /** Validate a talk address before it becomes a storage key. */
83
83
  export function assertAddress(addr: string): void {
84
- if (!ADDRESS_PATTERN.test(addr)) throw new TypeError(`Invalid talk address: ${addr}`);
84
+ if (!ADDRESS_PATTERN.test(addr)) {
85
+ throw new TypeError(`Invalid talk address: ${addr}`);
86
+ }
85
87
  }
86
88
 
87
89
  // ── Storage namespaces ───────────────────────────────────────────────────
@@ -111,7 +113,9 @@ export async function writeRecord(storage: TalkStorage, record: AgentRecord): Pr
111
113
 
112
114
  export async function readRecord(storage: TalkStorage, addr: string): Promise<AgentRecord | null> {
113
115
  const raw = await storage.readJson(RECORDS_NS, recordKey(addr));
114
- if (Value.Check(AgentRecordSchema, raw)) return raw;
116
+ if (Value.Check(AgentRecordSchema, raw)) {
117
+ return raw;
118
+ }
115
119
  // Migrate legacy records in place of the `sessionId` → `agentId` rename.
116
120
  if (Value.Check(LegacyAgentRecordSchema, raw)) {
117
121
  // Value.Check 是类型守卫,raw 已收窄成 legacy 记录的形状,不需要再断言。
@@ -126,9 +130,13 @@ export async function listRecords(storage: TalkStorage): Promise<AgentRecord[]>
126
130
  const out: AgentRecord[] = [];
127
131
  for (const key of await storage.listKeys(RECORDS_NS)) {
128
132
  const addr = key.slice(0, -".json".length);
129
- if (!ADDRESS_PATTERN.test(addr)) continue;
133
+ if (!ADDRESS_PATTERN.test(addr)) {
134
+ continue;
135
+ }
130
136
  const record = await readRecord(storage, addr);
131
- if (record) out.push(record);
137
+ if (record) {
138
+ out.push(record);
139
+ }
132
140
  }
133
141
  return out.toSorted((a, b) => a.startedAt - b.startedAt);
134
142
  }
@@ -160,22 +168,30 @@ export function readStartTime(pid: number): number | undefined {
160
168
  * unreadable — we fall back to the bare pid check.
161
169
  */
162
170
  function pidAlive(pid: number, pidStart?: number): boolean {
163
- if (!Number.isSafeInteger(pid) || pid <= 0) return false;
171
+ if (!Number.isSafeInteger(pid) || pid <= 0) {
172
+ return false;
173
+ }
164
174
  try {
165
175
  process.kill(pid, 0);
166
176
  } catch (error) {
167
177
  // EPERM means the process exists but isn't ours — still alive
168
178
  return (error as NodeJS.ErrnoException).code === "EPERM";
169
179
  }
170
- if (pidStart === undefined) return true;
180
+ if (pidStart === undefined) {
181
+ return true;
182
+ }
171
183
  const start = readStartTime(pid);
172
184
  // 读不到启动时间时保守认为「pid 还活着」,只有两边都读到且不同才判定被复用
173
185
  return start === undefined || start === pidStart;
174
186
  }
175
187
 
176
188
  export function presenceOf(record: AgentRecord): Presence {
177
- if (record.offline) return "offline";
178
- if (!pidAlive(record.pid, record.pidStart)) return "offline";
189
+ if (record.offline) {
190
+ return "offline";
191
+ }
192
+ if (!pidAlive(record.pid, record.pidStart)) {
193
+ return "offline";
194
+ }
179
195
  return "live";
180
196
  }
181
197
 
@@ -196,12 +212,18 @@ export async function sweep(storage: TalkStorage, now: number = Date.now()): Pro
196
212
  for (const record of await listRecords(storage)) {
197
213
  // Without a heartbeat, lastSeenAt only tracks the last event, so an idle
198
214
  // live agent would look long-quiet — never reap a live process.
199
- if (pidAlive(record.pid, record.pidStart)) continue;
215
+ if (pidAlive(record.pid, record.pidStart)) {
216
+ continue;
217
+ }
200
218
  const quietFor = now - record.lastSeenAt;
201
- if (quietFor < SWEEP_OFFLINE_GRACE_MS) continue;
219
+ if (quietFor < SWEEP_OFFLINE_GRACE_MS) {
220
+ continue;
221
+ }
202
222
  const hasMail =
203
223
  (await storage.hasKeys(inboxNs(record.addr))) || (await storage.hasKeys(asksNs(record.addr)));
204
- if (hasMail && quietFor < SWEEP_MAIL_KEEP_MS) continue;
224
+ if (hasMail && quietFor < SWEEP_MAIL_KEEP_MS) {
225
+ continue;
226
+ }
205
227
  await storage.removeNamespace(inboxNs(record.addr));
206
228
  await storage.removeNamespace(asksNs(record.addr));
207
229
  await storage.removeKey(RECORDS_NS, recordKey(record.addr));
@@ -100,7 +100,9 @@ export class SqliteTalkStorage implements TalkStorage {
100
100
  const row = this.db
101
101
  .prepare("SELECT value FROM talk_kv WHERE namespace = ? AND key = ?")
102
102
  .get(namespace, key) as { value: string } | undefined;
103
- if (row === undefined) return Promise.resolve(null);
103
+ if (row === undefined) {
104
+ return Promise.resolve(null);
105
+ }
104
106
  try {
105
107
  return Promise.resolve(JSON.parse(row.value) as unknown);
106
108
  } catch {
@@ -120,7 +120,9 @@ export function resolveImagePaths(params: { path?: string | string[] }): string[
120
120
  }
121
121
 
122
122
  function toTrimmedList(value: string | string[] | undefined): string[] {
123
- if (value === undefined) return [];
123
+ if (value === undefined) {
124
+ return [];
125
+ }
124
126
  const list = Array.isArray(value) ? value : [value];
125
127
  return list.map((s) => s.trim()).filter((s) => s.length > 0);
126
128
  }
@@ -143,9 +145,13 @@ export function loadVisionConfig(settingsPath = SETTINGS_PATH): VisionConfigSett
143
145
  } catch {
144
146
  return undefined;
145
147
  }
146
- if (!Value.Check(visionSettingsSchema, parsed)) return undefined;
148
+ if (!Value.Check(visionSettingsSchema, parsed)) {
149
+ return undefined;
150
+ }
147
151
  const config = parsed.visionConfig;
148
- if (config === undefined) return undefined;
152
+ if (config === undefined) {
153
+ return undefined;
154
+ }
149
155
  const provider = config.provider?.trim() || undefined;
150
156
  const defaultProvider = parsed.defaultProvider?.trim() || undefined;
151
157
  return {
@@ -210,9 +216,15 @@ export function buildPendantMarkdown(params: {
210
216
 
211
217
  /** 按文件头识别真实图片类型,识别不出返回 undefined */
212
218
  function sniffMime(buf: Buffer): string | undefined {
213
- if (buf.length >= 8 && buf.readUInt32BE(0) === 0x89504e47) return "image/png";
214
- if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return "image/jpeg";
215
- if (buf.length >= 6 && buf.toString("ascii", 0, 4) === "GIF8") return "image/gif";
219
+ if (buf.length >= 8 && buf.readUInt32BE(0) === 0x89504e47) {
220
+ return "image/png";
221
+ }
222
+ if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
223
+ return "image/jpeg";
224
+ }
225
+ if (buf.length >= 6 && buf.toString("ascii", 0, 4) === "GIF8") {
226
+ return "image/gif";
227
+ }
216
228
  if (
217
229
  buf.length >= 12 &&
218
230
  buf.toString("ascii", 0, 4) === "RIFF" &&
@@ -220,14 +232,18 @@ function sniffMime(buf: Buffer): string | undefined {
220
232
  ) {
221
233
  return "image/webp";
222
234
  }
223
- if (buf.length >= 2 && buf[0] === 0x42 && buf[1] === 0x4d) return "image/bmp";
235
+ if (buf.length >= 2 && buf[0] === 0x42 && buf[1] === 0x4d) {
236
+ return "image/bmp";
237
+ }
224
238
  return undefined;
225
239
  }
226
240
 
227
241
  /** 读取图片文件 → base64 + mimeType + 展示名 */
228
242
  function loadImageBytes(path: string): { base64: string; mimeType: string; label: string } {
229
243
  const ext = extname(path).toLowerCase();
230
- if (!IMAGE_EXTS.has(ext)) throw new Error(`不支持的文件格式: ${ext || "(无扩展名)"}`);
244
+ if (!IMAGE_EXTS.has(ext)) {
245
+ throw new Error(`不支持的文件格式: ${ext || "(无扩展名)"}`);
246
+ }
231
247
 
232
248
  let stat;
233
249
  try {
@@ -238,8 +254,12 @@ function loadImageBytes(path: string): { base64: string; mimeType: string; label
238
254
  { cause: error },
239
255
  );
240
256
  }
241
- if (!stat.isFile()) throw new Error(`不是普通文件: ${path}`);
242
- if (stat.size === 0) throw new Error(`文件为空: ${path}`);
257
+ if (!stat.isFile()) {
258
+ throw new Error(`不是普通文件: ${path}`);
259
+ }
260
+ if (stat.size === 0) {
261
+ throw new Error(`文件为空: ${path}`);
262
+ }
243
263
  if (stat.size > MAX_IMAGE_BYTES) {
244
264
  throw new Error(`图片过大 (${(stat.size / 1024 / 1024).toFixed(1)}MB),上限 10MB`);
245
265
  }
@@ -293,7 +313,9 @@ export async function callVision(
293
313
  );
294
314
  } catch (error) {
295
315
  // 用户主动取消(signal abort)不是失败,转成 VisionAbortError 由调用方处理
296
- if (error instanceof Error && error.name === "AbortError") throw new VisionAbortError();
316
+ if (error instanceof Error && error.name === "AbortError") {
317
+ throw new VisionAbortError();
318
+ }
297
319
  throw error;
298
320
  }
299
321
  const text = contentText(result.content).trim();
@@ -325,11 +347,14 @@ export default function visionAgent(pi: ExtensionAPI) {
325
347
  const hasTool = active.includes(TOOL_NAME);
326
348
  if (hasTool && isMultimodal(model)) {
327
349
  pi.setActiveTools(active.filter((t) => t !== TOOL_NAME));
328
- if (notify) notify(`主模型 ${model?.id ?? "?"} 支持视觉 → 已隐藏 ${TOOL_NAME},图片原生透传`);
350
+ if (notify) {
351
+ notify(`主模型 ${model?.id ?? "?"} 支持视觉 → 已隐藏 ${TOOL_NAME},图片原生透传`);
352
+ }
329
353
  } else if (!hasTool && !isMultimodal(model)) {
330
354
  pi.setActiveTools([...active, TOOL_NAME]);
331
- if (notify)
355
+ if (notify) {
332
356
  notify(`主模型 ${model?.id ?? "?"} 不支持视觉 → 已启用 ${TOOL_NAME},由视觉模型代理识别`);
357
+ }
333
358
  }
334
359
  }
335
360
 
@@ -347,7 +372,9 @@ export default function visionAgent(pi: ExtensionAPI) {
347
372
  // baseUrl/apiKey 由 pi 的模型注册表在调用时解析(execute 里 find),
348
373
  // 配置好 settings.json / models.json 后重新加载(/reload)即可生效。
349
374
  const visionConfig = loadVisionConfig();
350
- if (!visionConfig?.model) return;
375
+ if (!visionConfig?.model) {
376
+ return;
377
+ }
351
378
 
352
379
  pi.registerTool({
353
380
  name: TOOL_NAME,
package/src/web/config.ts CHANGED
@@ -20,7 +20,9 @@ export function webSearchConfigPath(): string {
20
20
  /** ~/.pi/web-search.json 的 search1apiApiKey,或 SEARCH1API_KEY 环境变量。 */
21
21
  export async function loadSearch1ApiKey(path = webSearchConfigPath()): Promise<string | undefined> {
22
22
  const envKey = process.env.SEARCH1API_KEY;
23
- if (envKey) return envKey;
23
+ if (envKey) {
24
+ return envKey;
25
+ }
24
26
  try {
25
27
  const raw = await readFile(path, "utf8");
26
28
  const parsed = Value.Parse(webSearchSchema, JSON.parse(raw));
package/src/web/fetch.ts CHANGED
@@ -38,33 +38,65 @@ const MAX_MARKDOWN_BYTES = 100 * 1024;
38
38
 
39
39
  function isPrivateIpv4(ip: string): boolean {
40
40
  const parts = ip.split(".").map(Number);
41
- if (parts.length !== 4 || parts.some((p) => Number.isNaN(p))) return false;
41
+ if (parts.length !== 4 || parts.some((p) => Number.isNaN(p))) {
42
+ return false;
43
+ }
42
44
  const [a, b, c] = parts;
43
- if (a === 0) return true; // 0.0.0.0/8
44
- if (a === 10) return true; // 10.0.0.0/8
45
- if (a === 127) return true; // 127.0.0.0/8 loopback
46
- if (a === 169 && b === 254) return true; // 169.254.0.0/16 link-local
47
- if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12
48
- if (a === 192 && b === 168) return true; // 192.168.0.0/16
49
- if (a === 100 && b >= 64 && b <= 127) return true; // 100.64.0.0/10 CGNAT
50
- if (a === 198 && (b === 18 || b === 19)) return true; // 198.18.0.0/15 benchmarking
51
- if (a === 192 && b === 0 && c === 0) return true; // 192.0.0.0/24
45
+ if (a === 0) {
46
+ return true; // 0.0.0.0/8
47
+ }
48
+ if (a === 10) {
49
+ return true; // 10.0.0.0/8
50
+ }
51
+ if (a === 127) {
52
+ return true; // 127.0.0.0/8 loopback
53
+ }
54
+ if (a === 169 && b === 254) {
55
+ return true; // 169.254.0.0/16 link-local
56
+ }
57
+ if (a === 172 && b >= 16 && b <= 31) {
58
+ return true; // 172.16.0.0/12
59
+ }
60
+ if (a === 192 && b === 168) {
61
+ return true; // 192.168.0.0/16
62
+ }
63
+ if (a === 100 && b >= 64 && b <= 127) {
64
+ return true; // 100.64.0.0/10 CGNAT
65
+ }
66
+ if (a === 198 && (b === 18 || b === 19)) {
67
+ return true; // 198.18.0.0/15 benchmarking
68
+ }
69
+ if (a === 192 && b === 0 && c === 0) {
70
+ return true; // 192.0.0.0/24
71
+ }
52
72
  return a >= 224; // 224.0.0.0/3 multicast + reserved
53
73
  }
54
74
 
55
75
  function isPrivateIpv6(ip: string): boolean {
56
76
  const lower = ip.toLowerCase();
57
- if (lower === "::" || lower === "::1") return true; // unspecified / loopback
58
- if (lower.startsWith("fc") || lower.startsWith("fd")) return true; // fc00::/7 ULA
59
- if (/^fe[89ab]/.test(lower)) return true; // fe80::/10 link-local
60
- if (lower.startsWith("::ffff:")) return isPrivateIpv4(lower.slice(7));
77
+ if (lower === "::" || lower === "::1") {
78
+ return true; // unspecified / loopback
79
+ }
80
+ if (lower.startsWith("fc") || lower.startsWith("fd")) {
81
+ return true; // fc00::/7 ULA
82
+ }
83
+ if (/^fe[89ab]/.test(lower)) {
84
+ return true; // fe80::/10 link-local
85
+ }
86
+ if (lower.startsWith("::ffff:")) {
87
+ return isPrivateIpv4(lower.slice(7));
88
+ }
61
89
  return false;
62
90
  }
63
91
 
64
92
  export function isPrivateAddress(ip: string): boolean {
65
93
  const version = isIP(ip);
66
- if (version === 4) return isPrivateIpv4(ip);
67
- if (version === 6) return isPrivateIpv6(ip);
94
+ if (version === 4) {
95
+ return isPrivateIpv4(ip);
96
+ }
97
+ if (version === 6) {
98
+ return isPrivateIpv6(ip);
99
+ }
68
100
  return true; // 非 IP 一律拒绝
69
101
  }
70
102
 
@@ -181,7 +213,9 @@ export async function fetchPage(url: string, signal?: AbortSignal): Promise<Fetc
181
213
  const decoder = new TextDecoder();
182
214
  for (;;) {
183
215
  const chunk = (await reader.read()) as { done: boolean; value: Uint8Array };
184
- if (chunk.done) break;
216
+ if (chunk.done) {
217
+ break;
218
+ }
185
219
  body += decoder.decode(chunk.value, { stream: true });
186
220
  if (Buffer.byteLength(body, "utf8") > MAX_BYTES) {
187
221
  throw new Error(`页面过大,上限 ${MAX_BYTES} bytes`);
@@ -222,7 +256,9 @@ export async function saveUrlToFile(
222
256
  const reader = response.body.getReader();
223
257
  for (;;) {
224
258
  const chunk = (await reader.read()) as { done: boolean; value: Uint8Array };
225
- if (chunk.done) break;
259
+ if (chunk.done) {
260
+ break;
261
+ }
226
262
  bytes += chunk.value.byteLength;
227
263
  if (bytes > MAX_FILE_BYTES) {
228
264
  throw new Error(`文件过大,上限 ${MAX_FILE_BYTES} bytes`);
@@ -248,10 +284,18 @@ export async function saveUrlToFile(
248
284
  /** 按 mime 主体分类响应;html 走 readability,其余文本类原样返回 */
249
285
  function classifyContentType(contentType: string): "html" | "text" | null {
250
286
  const mime = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
251
- if (mime === "text/html" || mime === "application/xhtml+xml") return "html";
252
- if (mime.startsWith("text/")) return "text";
253
- if (mime === "application/json" || mime.endsWith("+json")) return "text";
254
- if (mime === "application/xml" || mime.endsWith("+xml")) return "text";
287
+ if (mime === "text/html" || mime === "application/xhtml+xml") {
288
+ return "html";
289
+ }
290
+ if (mime.startsWith("text/")) {
291
+ return "text";
292
+ }
293
+ if (mime === "application/json" || mime.endsWith("+json")) {
294
+ return "text";
295
+ }
296
+ if (mime === "application/xml" || mime.endsWith("+xml")) {
297
+ return "text";
298
+ }
255
299
  return null;
256
300
  }
257
301
 
@@ -272,7 +316,9 @@ interface ParsedDocument {
272
316
  */
273
317
  function unshadowReactStreaming(document: ParsedDocument): void {
274
318
  for (const el of document.querySelectorAll("[hidden]")) {
275
- if (/^S:\d+$/.test(el.id ?? "")) el.removeAttribute("hidden");
319
+ if (/^S:\d+$/.test(el.id ?? "")) {
320
+ el.removeAttribute("hidden");
321
+ }
276
322
  }
277
323
  }
278
324
 
@@ -285,7 +331,9 @@ export function extractMarkdown(html: string, sourceUrl: string): FetchedPage {
285
331
  // linkedom document 的兼容方法,cast 桥接即可
286
332
  const article = new Readability(parsed.document).parse();
287
333
  let title = article?.title ?? document.title?.trim() ?? sourceUrl;
288
- if (typeof title !== "string" || title.length === 0) title = sourceUrl;
334
+ if (typeof title !== "string" || title.length === 0) {
335
+ title = sourceUrl;
336
+ }
289
337
  let body = article?.content;
290
338
  if (!body || body.length === 0) {
291
339
  body = document.body?.textContent ?? "";
@@ -303,7 +351,9 @@ export function extractMarkdown(html: string, sourceUrl: string): FetchedPage {
303
351
  }
304
352
 
305
353
  function truncateMarkdown(text: string): { text: string; truncated: boolean } {
306
- if (Buffer.byteLength(text, "utf8") <= MAX_MARKDOWN_BYTES) return { text, truncated: false };
354
+ if (Buffer.byteLength(text, "utf8") <= MAX_MARKDOWN_BYTES) {
355
+ return { text, truncated: false };
356
+ }
307
357
  const bytes = Buffer.from(text, "utf8");
308
358
  const sliced = bytes.subarray(0, MAX_MARKDOWN_BYTES).toString("utf8");
309
359
  const cut = sliced.lastIndexOf("\n", sliced.length - 1);
package/src/web/search.ts CHANGED
@@ -51,13 +51,17 @@ export interface SearchOptions {
51
51
  }
52
52
 
53
53
  function clampNumResults(value: number | undefined): number {
54
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
54
+ if (typeof value !== "number" || !Number.isFinite(value)) {
55
+ return 5;
56
+ }
55
57
  return Math.max(1, Math.min(Math.floor(value), 50));
56
58
  }
57
59
 
58
60
  function normalizeDomain(raw: string): string | undefined {
59
61
  const input = raw.trim().toLowerCase().replace(/^-/, "").trim();
60
- if (!input) return undefined;
62
+ if (!input) {
63
+ return undefined;
64
+ }
61
65
  try {
62
66
  const host = input.includes("://")
63
67
  ? new URL(input).hostname
@@ -76,7 +80,9 @@ function splitDomainFilter(domainFilter: string[] | undefined): {
76
80
  const excludeSites: string[] = [];
77
81
  for (const raw of domainFilter ?? []) {
78
82
  const domain = normalizeDomain(raw);
79
- if (!domain) continue;
83
+ if (!domain) {
84
+ continue;
85
+ }
80
86
  (raw.trimStart().startsWith("-") ? excludeSites : includeSites).push(domain);
81
87
  }
82
88
  return { includeSites, excludeSites };
@@ -90,10 +96,18 @@ export function buildSearchBody(query: string, options: SearchOptions): Record<s
90
96
  max_results: numResults,
91
97
  crawl_results: options.includeContent ? Math.min(numResults, 5) : 0,
92
98
  };
93
- if (options.recencyFilter) body.time_range = options.recencyFilter;
94
- if (options.searchService) body.search_service = options.searchService;
95
- if (includeSites.length > 0) body.include_sites = includeSites;
96
- if (excludeSites.length > 0) body.exclude_sites = excludeSites;
99
+ if (options.recencyFilter) {
100
+ body.time_range = options.recencyFilter;
101
+ }
102
+ if (options.searchService) {
103
+ body.search_service = options.searchService;
104
+ }
105
+ if (includeSites.length > 0) {
106
+ body.include_sites = includeSites;
107
+ }
108
+ if (excludeSites.length > 0) {
109
+ body.exclude_sites = excludeSites;
110
+ }
97
111
  return body;
98
112
  }
99
113
 
@@ -108,14 +122,18 @@ function mapHits(
108
122
  const hits: SearchHit[] = [];
109
123
  for (const item of results) {
110
124
  const url = item.link?.trim();
111
- if (!url) continue;
125
+ if (!url) {
126
+ continue;
127
+ }
112
128
  const hit: SearchHit = {
113
129
  title: item.title?.trim() || url,
114
130
  url,
115
131
  snippet: item.snippet?.replaceAll(/\s+/g, " ").trim() || "",
116
132
  };
117
133
  const content = item.content?.trim();
118
- if (content) hit.content = content;
134
+ if (content) {
135
+ hit.content = content;
136
+ }
119
137
  hits.push(hit);
120
138
  }
121
139
  return hits;
@@ -232,7 +250,9 @@ export default function webSearchTool(pi: ExtensionAPI): void {
232
250
  const hits: SearchHit[] = [];
233
251
  for (const result of results) {
234
252
  for (const hit of result.hits) {
235
- if (seen.has(hit.url)) continue;
253
+ if (seen.has(hit.url)) {
254
+ continue;
255
+ }
236
256
  seen.add(hit.url);
237
257
  hits.push(hit);
238
258
  }