@trim21/personal-pi-extensions 0.1.690 → 0.1.691

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 (40) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/src/bwrap/dcg-scan.ts +2 -8
  4. package/src/bwrap/html.ts +13 -0
  5. package/src/bwrap/runtime.ts +1 -8
  6. package/src/claude-code/shell.ts +12 -42
  7. package/src/codemode/declarations.ts +3 -8
  8. package/src/gh/base.ts +91 -23
  9. package/src/gh/index.ts +8 -5
  10. package/src/gh/tools/download-release-assets.ts +6 -3
  11. package/src/gh/tools/get-workflow-jobs.ts +4 -7
  12. package/src/gh/tools/list-issues.ts +6 -39
  13. package/src/gh/tools/list-prs.ts +6 -39
  14. package/src/gh/tools/list-releases.ts +6 -9
  15. package/src/gh/tools/list-workflow-runs.ts +6 -9
  16. package/src/gh/tools/read-ci-logs.ts +8 -19
  17. package/src/gh/tools/read-issue-comments.ts +4 -12
  18. package/src/gh/tools/read-issue.ts +4 -12
  19. package/src/gh/tools/read-pr-comments.ts +6 -14
  20. package/src/gh/tools/read-pr-diff.ts +8 -8
  21. package/src/gh/tools/read-pr-status.ts +6 -4
  22. package/src/gh/tools/read-pr.ts +4 -12
  23. package/src/gh/tools/read-release.ts +5 -8
  24. package/src/gh/tools/read-repo.ts +5 -9
  25. package/src/gh/tools/wait-commit-checks.ts +2 -4
  26. package/src/gh/tools/wait-pr-checks.ts +2 -4
  27. package/src/gh/tools/watch-run.ts +7 -4
  28. package/src/lib/abort.ts +10 -0
  29. package/src/lib/bash-tool.ts +48 -0
  30. package/src/lib/github-reads.ts +3 -23
  31. package/src/lib/github.ts +81 -41
  32. package/src/lib/model-call.ts +3 -7
  33. package/src/lib/tools-config.ts +2 -4
  34. package/src/opencode/bash.ts +11 -42
  35. package/src/opencode/glob.ts +1 -6
  36. package/src/opencode/grep.ts +1 -6
  37. package/src/opencode/ripgrep.ts +5 -0
  38. package/src/web/fetch.ts +2 -6
  39. package/src/web/search.ts +2 -6
  40. package/src/orphan-worker-cleaner.ts +0 -38
package/src/lib/github.ts CHANGED
@@ -270,21 +270,41 @@ export function normalizeRestList(items: unknown, repo: string): SearchHit[] {
270
270
  }
271
271
 
272
272
  /**
273
- * Error thrown when the GitHub search API rejects the request. Carries the
274
- * original toolcall params so the model can see the exact input.
273
+ * GitHub 请求失败:octokit 调用与非 JSON 的原始下载共用这一个类型,调用方据此区分
274
+ * 「不存在」(`isNotFound`)与真失败,不用再各自去剥错误对象上的 status。
275
+ *
276
+ * `input` 是触发该请求的工具参数;带上它,模型才能在报错里看到自己给的输入。
275
277
  */
276
- class GithubSearchError extends Error {
277
- readonly params: SearchParams;
278
+ export class GithubApiError extends Error {
279
+ /** HTTP 状态;连响应都没拿到(网络错误)时为 undefined。 */
278
280
  readonly status: number | undefined;
279
-
280
- constructor(message: string, params: SearchParams, status?: number) {
281
- super(`${message} (input: ${JSON.stringify(params)})`);
282
- this.name = "GithubSearchError";
283
- this.params = params;
284
- this.status = status;
281
+ readonly input: unknown;
282
+
283
+ constructor(
284
+ message: string,
285
+ options: { status?: number; input?: unknown; cause?: unknown } = {},
286
+ ) {
287
+ const inputText =
288
+ options.input === undefined ? "" : ` (input: ${JSON.stringify(options.input)})`;
289
+ super(
290
+ `${message}${inputText}`,
291
+ options.cause === undefined ? undefined : { cause: options.cause },
292
+ );
293
+ this.name = "GithubApiError";
294
+ this.status = options.status;
295
+ this.input = options.input;
285
296
  }
286
297
  }
287
298
 
299
+ /** 404:资源不存在或 token 没有访问权限——与网络/限流错误分开处理。 */
300
+ export function isNotFound(error: unknown): boolean {
301
+ return error instanceof GithubApiError && error.status === 404;
302
+ }
303
+
304
+ const errorStatus = (error: unknown): number | undefined => (error as { status?: number }).status;
305
+ const errorMessage = (error: unknown): string =>
306
+ (error as { message?: string }).message ?? String(error);
307
+
288
308
  function describeHttpError(status: number | undefined): string {
289
309
  if (status === 401) {
290
310
  return 'GitHub auth failed (401): token invalid or expired — run "gh auth login"';
@@ -298,12 +318,35 @@ function describeHttpError(status: number | undefined): string {
298
318
  return `GitHub API error${status === undefined ? "" : ` (HTTP ${status})`}`;
299
319
  }
300
320
 
321
+ /** 非 2xx 的原始响应(二进制下载)转成错误:状态、URL 与 GitHub 的 message 一起给出。 */
322
+ async function ensureOk(response: Response): Promise<Response> {
323
+ if (response.ok) {
324
+ return response;
325
+ }
326
+ let detail: string | undefined;
327
+ try {
328
+ const body = await response.text();
329
+ detail = (JSON.parse(body) as { message?: string }).message ?? body.slice(0, 200);
330
+ } catch {
331
+ detail = undefined;
332
+ }
333
+ throw new GithubApiError(
334
+ `GitHub API error (HTTP ${response.status}) at ${response.url}${detail === undefined || detail === "" ? "" : `: ${detail}`}`,
335
+ { status: response.status },
336
+ );
337
+ }
338
+
301
339
  export interface GithubApi {
302
- /** Run an octokit request; retries once with a fresh token on 401. */
303
- call<T>(fn: (octokit: Octokit) => Promise<T>): Promise<T>;
340
+ /**
341
+ * Run an octokit request; retries once with a fresh token on 401. Failures
342
+ * are rethrown as `GithubApiError`; `input` (the toolcall params behind the
343
+ * request) is attached to that error when given.
344
+ */
345
+ call<T>(fn: (octokit: Octokit) => Promise<T>, input?: unknown): Promise<T>;
304
346
  /**
305
347
  * 用同一个 token 直接 fetch(二进制资产、源码归档这类要走原始响应体、不能经 octokit
306
- * 的 JSON 解析的请求)。重定向自动跟随,401 同样丢缓存重试一次。
348
+ * 的 JSON 解析的请求)。重定向自动跟随,401 同样丢缓存重试一次;非 2xx 抛
349
+ * `GithubApiError`(状态、URL 与 GitHub 的 message 都在里面)。
307
350
  */
308
351
  rawFetch(url: string, init?: RequestInit): Promise<Response>;
309
352
  }
@@ -361,18 +404,26 @@ export function createGithubApi(options: GithubClientOptions = {}): GithubApi {
361
404
  }
362
405
 
363
406
  return {
364
- async call(fn) {
407
+ async call(fn, input) {
365
408
  for (let attempt = 0; ; attempt += 1) {
366
409
  try {
367
410
  return await fn(await getClient());
368
411
  } catch (error) {
369
- const status = (error as { status?: number }).status;
412
+ const status = errorStatus(error);
370
413
  if (status === 401 && attempt === 0 && client) {
371
414
  token = undefined;
372
415
  client = undefined;
373
416
  continue;
374
417
  }
375
- throw error;
418
+ // 统一在这里成 GithubApiError:调用方(工具、诊断)拿到的是同一种错误形态,
419
+ // 不必各自去剥 status / message。
420
+ throw error instanceof GithubApiError
421
+ ? error
422
+ : new GithubApiError(`${describeHttpError(status)}: ${errorMessage(error)}`, {
423
+ status,
424
+ input,
425
+ cause: error,
426
+ });
376
427
  }
377
428
  }
378
429
  },
@@ -381,7 +432,7 @@ export function createGithubApi(options: GithubClientOptions = {}): GithubApi {
381
432
  for (let attempt = 0; ; attempt += 1) {
382
433
  const response = await rawFetchOnce(url, init);
383
434
  if (response.status !== 401 || attempt > 0) {
384
- return response;
435
+ return ensureOk(response);
385
436
  }
386
437
  await response.body?.cancel();
387
438
  }
@@ -394,31 +445,22 @@ export interface GithubSearch {
394
445
  }
395
446
 
396
447
  /**
397
- * Create a search client backed by a cached octokit instance.
448
+ * Create a search client on top of a shared API accessor (见 `createGithubApi`):
449
+ * 一个会话只应有一个 accessor,搜索 / checks / reads 共用同一份 octokit 与 token 缓存。
398
450
  */
399
- export function createGithubSearch(options: GithubClientOptions = {}): GithubSearch {
400
- const api = createGithubApi(options);
401
-
451
+ export function createGithubSearch(api: GithubApi): GithubSearch {
402
452
  return {
403
453
  async search(kind, params) {
404
454
  const limit = Math.min(Math.max(params.limit ?? 30, 1), 100);
405
-
406
- try {
407
- // `@me` 是 gh CLI 的简写,REST 搜索不认识:这里不展开,带关键词时它按字面量
408
- // 进入查询串(无关键词的列表走 gh CLI,由 gh 自己展开)。
409
- const q = buildSearchQuery(kind, { ...params, limit });
410
- return await api.call(async (client) => {
411
- const { data } = await client.rest.search.issuesAndPullRequests({
412
- q,
413
- per_page: limit,
414
- });
415
- return data.items.map((item) => normalize(parseWithSchema(searchItemSchema, item)));
455
+ // `@me` 是 gh CLI 的简写,REST 搜索不认识:这里不展开,它按字面量进入查询串。
456
+ const q = buildSearchQuery(kind, { ...params, limit });
457
+ return await api.call(async (client) => {
458
+ const { data } = await client.rest.search.issuesAndPullRequests({
459
+ q,
460
+ per_page: limit,
416
461
  });
417
- } catch (error) {
418
- const status = (error as { status?: number }).status;
419
- const message = (error as { message?: string }).message ?? String(error);
420
- throw new GithubSearchError(`${describeHttpError(status)}: ${message}`, params, status);
421
- }
462
+ return data.items.map((item) => normalize(parseWithSchema(searchItemSchema, item)));
463
+ }, params);
422
464
  },
423
465
  };
424
466
  }
@@ -522,7 +564,7 @@ export interface GithubChecksClient {
522
564
  }
523
565
 
524
566
  /**
525
- * Create a client for PR CI checks, backed by a cached octokit instance.
567
+ * Create a client for PR CI checks on top of a shared API accessor.
526
568
  * Covers both check sources GitHub exposes for a commit — classic commit
527
569
  * statuses (Azure DevOps, Jenkins, ...) and check runs (GitHub Actions,
528
570
  * GitHub Apps) — so external CI is visible to the caller.
@@ -553,9 +595,7 @@ function toRunJob(job: ApiJob): RunJob {
553
595
  };
554
596
  }
555
597
 
556
- export function createGithubChecks(options: GithubClientOptions = {}): GithubChecksClient {
557
- const api = createGithubApi(options);
558
-
598
+ export function createGithubChecks(api: GithubApi): GithubChecksClient {
559
599
  return {
560
600
  async statuses(owner, repo, ref, signal) {
561
601
  const { data } = await api.call((octokit) =>
@@ -25,6 +25,8 @@ import {
25
25
  type UserMessage,
26
26
  } from "@earendil-works/pi-ai";
27
27
 
28
+ import { withTimeoutSignal } from "./abort.js";
29
+
28
30
  /**
29
31
  * 模型注册表操作:扩展传 `ctx.modelRegistry`,测试传 mock。
30
32
  * 结构化类型(duck typing),只声明用到的两个方法。
@@ -58,12 +60,6 @@ export interface CompleteTextResult {
58
60
  usage: Usage;
59
61
  }
60
62
 
61
- /** 合并调用方 signal 与本地超时;调用方未传时仍然有超时兜底 */
62
- function withTimeout(signal: AbortSignal | undefined, ms: number): AbortSignal {
63
- const timeout = AbortSignal.timeout(ms);
64
- return signal ? AbortSignal.any([signal, timeout]) : timeout;
65
- }
66
-
67
63
  /**
68
64
  * 用给定模型生成正文。`registry.complete` 抛出的错误原样向上抛(含
69
65
  * `AbortError`),取消与失败的区分由调用方决定。
@@ -79,7 +75,7 @@ export async function completeText(options: CompleteTextOptions): Promise<Comple
79
75
  },
80
76
  {
81
77
  maxTokens: options.maxTokens ?? options.model.maxTokens,
82
- signal: withTimeout(options.signal, options.timeoutMs),
78
+ signal: withTimeoutSignal(options.signal, options.timeoutMs),
83
79
  },
84
80
  );
85
81
  const text = contentText(result.content).trim();
@@ -25,6 +25,8 @@ import { minimatch } from "minimatch";
25
25
  import { Type } from "typebox";
26
26
  import { Value } from "typebox/value";
27
27
 
28
+ import { isRecord } from "./narrow.js";
29
+
28
30
  // ── schema ───────────────────────────────────────────────────────────────────
29
31
 
30
32
  const fileIoSchema = Type.Union([Type.Literal("claude-code"), Type.Literal("opencode")]);
@@ -88,10 +90,6 @@ export const DEFAULT_FILE_IO: FileIoToolset = "claude-code";
88
90
 
89
91
  // ── 读取与解析 ───────────────────────────────────────────────────────────────
90
92
 
91
- function isRecord(value: unknown): value is Record<string, unknown> {
92
- return typeof value === "object" && value !== null && !Array.isArray(value);
93
- }
94
-
95
93
  function parseFileIo(value: unknown, field: string, warnings: string[]): FileIoToolset | undefined {
96
94
  if (value === undefined) {
97
95
  return undefined;
@@ -1,5 +1,4 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
2
  import { fileURLToPath } from "node:url";
4
3
 
5
4
  import type { ExtensionAPI, TruncationResult } from "@earendil-works/pi-coding-agent";
@@ -12,53 +11,23 @@ import {
12
11
  formatElapsedSeconds,
13
12
  sandboxHintBlock,
14
13
  } from "../bwrap/runtime.js";
14
+ import {
15
+ BASH_DEFAULT_TIMEOUT_MS,
16
+ BASH_MAX_TIMEOUT_MS,
17
+ bashStructuredSchema,
18
+ spilledOutput,
19
+ } from "../lib/bash-tool.js";
15
20
  import { resolveWorkdir } from "../lib/path.js";
16
21
  import { createRequestPolicy } from "../lib/request-policy.js";
17
22
  import { defineStructuredTool, type ToolBus } from "../lib/tool-bus.js";
18
23
  import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
19
24
 
20
- const DEFAULT_TIMEOUT_MS = 120_000;
21
- const MAX_TIMEOUT_MS = 7_200_000;
22
-
23
25
  /** 对齐上游 opencode 的截断提示文案(tools/BashTool/bash.ts)。 */
24
26
  const CAPTURE_TRUNCATED_NOTICE = "[output capture truncated at the in-memory safety limit]";
25
27
 
26
28
  /** Bash tool guidance, kept in markdown so it reads like documentation. */
27
29
  const BASH_PROMPT = readFileSync(fileURLToPath(new URL("bash.md", import.meta.url)), "utf8").trim();
28
30
 
29
- /**
30
- * `bash` 的结构化结果:只有退出码与完整输出——命令非零退出不是失败,所以载荷里没有成败
31
- * 标志,脚本直接读 `exitCode` 分支;超时/中止时 `exitCode` 为 `null`(原因在模型侧文本里)。
32
- */
33
- const bashStructuredSchema = Type.Object({
34
- exitCode: Type.Union([Type.Number(), Type.Null()], {
35
- description: "Exit code of the command; null when it was killed (timeout or abort)",
36
- }),
37
- output: Type.String({
38
- description:
39
- "Complete output of the command (stdout and stderr merged). Never truncated, never mixed with tool-added notices.",
40
- }),
41
- });
42
-
43
- /**
44
- * 载荷里的输出:文本被截断时读回落盘的完整输出(读不到就退回那份截断文本),
45
- * 因此脚本拿到的永远是命令真正输出的内容。
46
- */
47
- async function fullOutput(
48
- output: string,
49
- truncation: TruncationResult,
50
- spillPath: string | undefined,
51
- ): Promise<string> {
52
- if (spillPath === undefined || !truncation.truncated) {
53
- return output;
54
- }
55
- try {
56
- return await readFile(spillPath, "utf8");
57
- } catch {
58
- return output;
59
- }
60
- }
61
-
62
31
  /** opencode 风格截断提示(成功、超时、中断路径共用)。 */
63
32
  function appendTruncationNotice(
64
33
  text: string,
@@ -127,9 +96,9 @@ export function registerBashTool(bus: ToolBus, pi: ExtensionAPI, runtime: BwrapR
127
96
  { additionalProperties: false },
128
97
  ),
129
98
  async execute(id, params, signal, onUpdate, ctx) {
130
- const timeout = params.timeout ?? DEFAULT_TIMEOUT_MS;
131
- if (!Number.isFinite(timeout) || timeout <= 0 || timeout > MAX_TIMEOUT_MS) {
132
- throw new Error(`timeout must be between 1 and ${MAX_TIMEOUT_MS} milliseconds`);
99
+ const timeout = params.timeout ?? BASH_DEFAULT_TIMEOUT_MS;
100
+ if (!Number.isFinite(timeout) || timeout <= 0 || timeout > BASH_MAX_TIMEOUT_MS) {
101
+ throw new Error(`timeout must be between 1 and ${BASH_MAX_TIMEOUT_MS} milliseconds`);
133
102
  }
134
103
 
135
104
  const cwd = params.workdir ? await resolveWorkdir(params.workdir, ctx.cwd) : ctx.cwd;
@@ -175,7 +144,7 @@ export function registerBashTool(bus: ToolBus, pi: ExtensionAPI, runtime: BwrapR
175
144
  ok: true as const,
176
145
  value: {
177
146
  exitCode: null,
178
- output: await fullOutput(
147
+ output: await spilledOutput(
179
148
  error.partial.output,
180
149
  error.partial.truncation,
181
150
  error.partial.fullOutputPath,
@@ -208,7 +177,7 @@ export function registerBashTool(bus: ToolBus, pi: ExtensionAPI, runtime: BwrapR
208
177
  ok: true as const,
209
178
  value: {
210
179
  exitCode: result.exitCode,
211
- output: await fullOutput(result.output, result.truncation, result.fullOutputPath),
180
+ output: await spilledOutput(result.output, result.truncation, result.fullOutputPath),
212
181
  },
213
182
  },
214
183
  };
@@ -21,7 +21,7 @@ import { Type } from "typebox";
21
21
  import type { ToolBus } from "../lib/tool-bus.js";
22
22
  import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
23
23
  import { didYouMean } from "./files.js";
24
- import { RIPGREP_RESULT_LIMIT, runRipgrep } from "./ripgrep.js";
24
+ import { normalizeRipgrepPath, RIPGREP_RESULT_LIMIT, runRipgrep } from "./ripgrep.js";
25
25
 
26
26
  /** Tool guidance, kept in markdown so it reads like documentation. */
27
27
  const GLOB_PROMPT = readFileSync(fileURLToPath(new URL("glob.md", import.meta.url)), "utf8").trim();
@@ -30,11 +30,6 @@ export function buildGlobArgs(pattern: string): string[] {
30
30
  return ["--no-config", "--files", `--glob=${pattern}`, "--glob=!**/.git/**", "."];
31
31
  }
32
32
 
33
- /** rg --files 输出的路径:去掉前导 `./`,分隔符统一成 `/`。 */
34
- function normalizeRipgrepPath(text: string): string {
35
- return text.replace(/^(?:\.[\\/])+/u, "").replaceAll("\\", "/");
36
- }
37
-
38
33
  export function renderGlobOutput(files: readonly string[], truncated: boolean): string {
39
34
  if (files.length === 0) {
40
35
  return "No files found";
@@ -24,7 +24,7 @@ import { parseWithSchema } from "../lib/parse-with-schema.js";
24
24
  import type { ToolBus } from "../lib/tool-bus.js";
25
25
  import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
26
26
  import { didYouMean } from "./files.js";
27
- import { RIPGREP_RESULT_LIMIT, runRipgrep } from "./ripgrep.js";
27
+ import { normalizeRipgrepPath, RIPGREP_RESULT_LIMIT, runRipgrep } from "./ripgrep.js";
28
28
 
29
29
  /** Tool guidance, kept in markdown so it reads like documentation. */
30
30
  const GREP_PROMPT = readFileSync(fileURLToPath(new URL("grep.md", import.meta.url)), "utf8").trim();
@@ -51,11 +51,6 @@ export interface GrepMatch {
51
51
  text: string;
52
52
  }
53
53
 
54
- /** rg 输出的路径:去掉前导 `./`,分隔符统一成 `/`。 */
55
- function normalizeRipgrepPath(text: string): string {
56
- return text.replace(/^(?:\.[\\/])+/u, "").replaceAll("\\", "/");
57
- }
58
-
59
54
  /** 行文本:去掉行尾换行,超长截断(不留半个 surrogate pair)。 */
60
55
  function normalizeLineText(text: string): string {
61
56
  const line = text.replace(/[\r\n]+$/u, "");
@@ -12,6 +12,11 @@ import { spawn } from "node:child_process";
12
12
  /** 单次搜索最多返回的结果数(上游 limit = 100)。 */
13
13
  export const RIPGREP_RESULT_LIMIT = 100;
14
14
 
15
+ /** rg 输出的路径:去掉前导 `./`,分隔符统一成 `/`。 */
16
+ export function normalizeRipgrepPath(text: string): string {
17
+ return text.replace(/^(?:\.[\\/])+/u, "").replaceAll("\\", "/");
18
+ }
19
+
15
20
  /** stderr 最多保留的字节数(上游 ERROR_BYTES)。 */
16
21
  const ERROR_BYTES = 8 * 1024;
17
22
 
package/src/web/fetch.ts CHANGED
@@ -21,6 +21,7 @@ import { parseHTML } from "linkedom";
21
21
  import TurndownService from "turndown";
22
22
  import { Type } from "typebox";
23
23
 
24
+ import { withTimeoutSignal } from "../lib/abort.js";
24
25
  import { egress } from "../lib/egress.js";
25
26
  import { resolvePathArg } from "../lib/path.js";
26
27
  import { createRequestPolicy } from "../lib/request-policy.js";
@@ -120,11 +121,6 @@ export async function assertPublicHostname(
120
121
  }
121
122
  }
122
123
 
123
- function withTimeout(signal: AbortSignal | undefined, ms: number): AbortSignal {
124
- const timeout = AbortSignal.timeout(ms);
125
- return signal ? AbortSignal.any([signal, timeout]) : timeout;
126
- }
127
-
128
124
  interface FetchedPage {
129
125
  url: string;
130
126
  title: string;
@@ -150,7 +146,7 @@ async function fetchWithRedirects(
150
146
  await assertPublicHostname(current.hostname);
151
147
  const response = await fetchFn(current, {
152
148
  redirect: "manual",
153
- signal: withTimeout(signal, TIMEOUT_MS),
149
+ signal: withTimeoutSignal(signal, TIMEOUT_MS),
154
150
  headers: { "user-agent": "Mozilla/5.0 (compatible; pi-web-fetch/1.0)" },
155
151
  });
156
152
  const location = response.headers.get("location");
package/src/web/search.ts CHANGED
@@ -11,6 +11,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
11
  import { Type } from "typebox";
12
12
  import { Value } from "typebox/value";
13
13
 
14
+ import { withTimeoutSignal } from "../lib/abort.js";
14
15
  import { egress } from "../lib/egress.js";
15
16
  import type { ToolBus } from "../lib/tool-bus.js";
16
17
  import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
@@ -143,11 +144,6 @@ function mapHits(
143
144
  }
144
145
 
145
146
  /** 合并调用方 signal 与超时;调用方未传时仍有超时兜底 */
146
- function withTimeout(signal: AbortSignal | undefined, ms: number): AbortSignal {
147
- const timeout = AbortSignal.timeout(ms);
148
- return signal ? AbortSignal.any([signal, timeout]) : timeout;
149
- }
150
-
151
147
  export async function searchWeb(
152
148
  query: string,
153
149
  apiKey: string,
@@ -160,7 +156,7 @@ export async function searchWeb(
160
156
  "Content-Type": "application/json",
161
157
  },
162
158
  body: JSON.stringify(buildSearchBody(query, options)),
163
- signal: withTimeout(options.signal, TIMEOUT_MS),
159
+ signal: withTimeoutSignal(options.signal, TIMEOUT_MS),
164
160
  });
165
161
 
166
162
  const raw = await response.text();
@@ -1,38 +0,0 @@
1
- /**
2
- * orphan-worker-cleaner —— 宿主消失时让 pi SDK worker 自行退出
3
- *
4
- * VS Code 的 pi 扩展(Pendant)用 fork 启动 dist/pi-sdk-worker.cjs 承载 pi SDK。
5
- * 宿主进程消失时 worker 不会退出:worker 侧只把 IPC 断连当成可重试的传输错误
6
- * 上报,既不清理也不自杀,于是留下 ppid=1 的孤儿,连同它拉起的 language server
7
- * (实测单个 worker 身上挂 4 GiB 上下)一起常驻。
8
- *
9
- * 这里只做一件事:定时看一眼自己的父进程还在不在(父进程退出后会被 init 收养,
10
- * ppid 变成 1),不在就退出。worker 一退,它拉起的 language server 会因为 stdin
11
- * 关闭自行退出,language server 退出时(process.on("exit") → shutdown())又会
12
- * kill 掉自己 fork 的 tsserver,整棵树随之消失 —— 所以不需要遍历或清理别的进程。
13
- *
14
- * 只对 fork 出来的 SDK worker 生效(process.channel 存在):CLI 直跑、以及 pi
15
- * 内部 spawn 出来的进程都没有 IPC channel,不在此列,不会被误伤。
16
- */
17
-
18
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
-
20
- /** 检查父进程是否还在的间隔 */
21
- export const DEFAULT_CHECK_INTERVAL_MS = 5_000;
22
-
23
- export default function orphanWorkerCleaner(pi: ExtensionAPI): void {
24
- if (process.channel === undefined) {
25
- return;
26
- }
27
- const timer = setInterval(() => {
28
- if (process.ppid === 1) {
29
- // eslint-disable-next-line unicorn/no-process-exit -- 宿主已经没了,worker 必须整体退出
30
- process.exit(0);
31
- }
32
- }, DEFAULT_CHECK_INTERVAL_MS);
33
- timer.unref();
34
-
35
- pi.on("session_shutdown", () => {
36
- clearInterval(timer);
37
- });
38
- }