@cjhyy/code-shell-core 0.7.0 → 0.7.1

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 (118) hide show
  1. package/dist/arena/arena.d.ts +2 -0
  2. package/dist/arena/arena.js +30 -1
  3. package/dist/arena/phases/adjudication.d.ts +2 -1
  4. package/dist/arena/phases/adjudication.js +2 -1
  5. package/dist/arena/phases/build-consensus.d.ts +2 -1
  6. package/dist/arena/phases/build-consensus.js +3 -1
  7. package/dist/arena/phases/cross-review.d.ts +3 -1
  8. package/dist/arena/phases/cross-review.js +8 -2
  9. package/dist/arena/phases/debate-rounds.d.ts +2 -1
  10. package/dist/arena/phases/debate-rounds.js +4 -2
  11. package/dist/arena/phases/participant-research.d.ts +2 -1
  12. package/dist/arena/phases/participant-research.js +3 -1
  13. package/dist/arena/phases/planning-detail-expansion.d.ts +2 -1
  14. package/dist/arena/phases/planning-detail-expansion.js +2 -1
  15. package/dist/arena/planner.d.ts +2 -1
  16. package/dist/arena/planner.js +2 -1
  17. package/dist/arena/types.d.ts +5 -1
  18. package/dist/cli/agent-server-tcp.js +13 -2
  19. package/dist/context/manager.d.ts +3 -3
  20. package/dist/context/manager.js +6 -6
  21. package/dist/context/token-counter.js +5 -3
  22. package/dist/context/tool-result-storage.d.ts +6 -0
  23. package/dist/context/tool-result-storage.js +25 -4
  24. package/dist/credentials/access.d.ts +10 -1
  25. package/dist/credentials/access.js +76 -2
  26. package/dist/credentials/index.d.ts +3 -2
  27. package/dist/credentials/index.js +2 -1
  28. package/dist/credentials/oauth.d.ts +6 -1
  29. package/dist/credentials/oauth.js +66 -1
  30. package/dist/credentials/store.d.ts +1 -1
  31. package/dist/credentials/store.js +16 -7
  32. package/dist/credentials/types.d.ts +38 -1
  33. package/dist/credentials/types.js +16 -1
  34. package/dist/engine/engine.d.ts +61 -32
  35. package/dist/engine/engine.js +289 -55
  36. package/dist/engine/goal.d.ts +5 -3
  37. package/dist/engine/session-title.d.ts +2 -1
  38. package/dist/engine/session-title.js +4 -1
  39. package/dist/engine/turn-loop.d.ts +18 -0
  40. package/dist/engine/turn-loop.js +202 -27
  41. package/dist/engine/types.d.ts +6 -2
  42. package/dist/git/worktree/crud.d.ts +3 -0
  43. package/dist/git/worktree/crud.js +32 -3
  44. package/dist/git/worktree/git-exec.d.ts +2 -2
  45. package/dist/git/worktree/git-exec.js +47 -11
  46. package/dist/git/worktree/query.d.ts +8 -7
  47. package/dist/git/worktree/query.js +27 -20
  48. package/dist/hooks/events.d.ts +3 -0
  49. package/dist/hooks/events.js +0 -3
  50. package/dist/hooks/goal-stop-hook.d.ts +14 -4
  51. package/dist/hooks/goal-stop-hook.js +626 -71
  52. package/dist/hooks/registry.js +3 -0
  53. package/dist/hooks/shell-runner.d.ts +12 -1
  54. package/dist/hooks/shell-runner.js +160 -9
  55. package/dist/index.d.ts +7 -6
  56. package/dist/index.js +6 -5
  57. package/dist/llm/client-base.js +12 -10
  58. package/dist/llm/types.d.ts +12 -5
  59. package/dist/plugins/pluginCommandHook.d.ts +4 -4
  60. package/dist/plugins/pluginCommandHook.js +111 -13
  61. package/dist/protocol/chat-session-manager.d.ts +13 -2
  62. package/dist/protocol/chat-session-manager.js +90 -18
  63. package/dist/protocol/chat-session.d.ts +12 -0
  64. package/dist/protocol/chat-session.js +30 -5
  65. package/dist/protocol/client.d.ts +5 -2
  66. package/dist/protocol/client.js +22 -1
  67. package/dist/protocol/server.d.ts +25 -11
  68. package/dist/protocol/server.js +290 -72
  69. package/dist/protocol/types.d.ts +34 -2
  70. package/dist/protocol/types.js +2 -0
  71. package/dist/services/dream-consolidation.d.ts +3 -0
  72. package/dist/services/dream-consolidation.js +4 -1
  73. package/dist/services/index.d.ts +1 -1
  74. package/dist/services/index.js +1 -1
  75. package/dist/services/oauth.d.ts +34 -10
  76. package/dist/services/oauth.js +233 -98
  77. package/dist/session/session-manager.d.ts +35 -6
  78. package/dist/session/session-manager.js +362 -26
  79. package/dist/session/transcript.d.ts +30 -1
  80. package/dist/session/transcript.js +119 -4
  81. package/dist/tool-system/builtin/agent.js +5 -1
  82. package/dist/tool-system/builtin/arena.js +1 -0
  83. package/dist/tool-system/builtin/background-jobs.d.ts +8 -5
  84. package/dist/tool-system/builtin/background-jobs.js +15 -11
  85. package/dist/tool-system/builtin/background-work.d.ts +6 -1
  86. package/dist/tool-system/builtin/background-work.js +5 -1
  87. package/dist/tool-system/builtin/bash.d.ts +3 -5
  88. package/dist/tool-system/builtin/bash.js +10 -5
  89. package/dist/tool-system/builtin/browser-tools.d.ts +2 -2
  90. package/dist/tool-system/builtin/edit.d.ts +2 -1
  91. package/dist/tool-system/builtin/edit.js +12 -4
  92. package/dist/tool-system/builtin/generate-video.d.ts +4 -0
  93. package/dist/tool-system/builtin/generate-video.js +138 -21
  94. package/dist/tool-system/builtin/glob.d.ts +2 -1
  95. package/dist/tool-system/builtin/glob.js +28 -3
  96. package/dist/tool-system/builtin/grep.d.ts +1 -0
  97. package/dist/tool-system/builtin/grep.js +82 -17
  98. package/dist/tool-system/builtin/index.d.ts +25 -11
  99. package/dist/tool-system/builtin/index.js +45 -2
  100. package/dist/tool-system/builtin/video-providers.d.ts +12 -15
  101. package/dist/tool-system/builtin/video-providers.js +1 -0
  102. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  103. package/dist/tool-system/builtin/web-fetch.js +44 -3
  104. package/dist/tool-system/builtin/worktree.js +25 -7
  105. package/dist/tool-system/builtin/write.d.ts +2 -1
  106. package/dist/tool-system/builtin/write.js +14 -4
  107. package/dist/tool-system/context.d.ts +17 -5
  108. package/dist/tool-system/executor.js +24 -8
  109. package/dist/tool-system/mcp-manager.d.ts +6 -0
  110. package/dist/tool-system/mcp-manager.js +55 -5
  111. package/dist/tool-system/path-policy.d.ts +19 -0
  112. package/dist/tool-system/path-policy.js +62 -1
  113. package/dist/tool-system/permission.d.ts +43 -3
  114. package/dist/tool-system/permission.js +383 -30
  115. package/dist/tool-system/registry.d.ts +3 -2
  116. package/dist/tool-system/registry.js +52 -34
  117. package/dist/types.d.ts +15 -0
  118. package/package.json +1 -1
@@ -55,7 +55,14 @@ export async function __normalizeImagesForTests(images, image, uploader, creds,
55
55
  const VIDEO_PROVIDER_KINDS = ["fal"];
56
56
  const DEFAULT_POLL_INTERVAL_MS = 5_000;
57
57
  /** Safety cap so a stuck job's background loop can't poll forever. */
58
- const MAX_POLL_MS = 15 * 60 * 1000;
58
+ const DEFAULT_MAX_POLL_MS = 15 * 60 * 1000;
59
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
60
+ let maxPollMs = DEFAULT_MAX_POLL_MS;
61
+ let requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
62
+ export function __setVideoPollingLimitsForTests(limits) {
63
+ maxPollMs = limits?.maxPollMs ?? DEFAULT_MAX_POLL_MS;
64
+ requestTimeoutMs = limits?.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
65
+ }
59
66
  export const generateVideoToolDef = {
60
67
  name: "GenerateVideo",
61
68
  description: "Generate a video from a text prompt. The job runs in the background (video generation is " +
@@ -297,10 +304,35 @@ export async function generateVideoTool(args, ctx) {
297
304
  // two providers can't collide.
298
305
  const jobKey = `video-${jobId}`;
299
306
  const promptPreview = prompt.length > 80 ? `${prompt.slice(0, 80)}…` : prompt;
300
- backgroundJobRegistry.start(jobKey, sessionId ?? "", `生成视频中:${promptPreview}`);
301
- void pollToCompletion(adapter, jobId, creds, cwd, sessionId, prompt, pollIntervalMs)
307
+ const controller = new AbortController();
308
+ let abortKind;
309
+ const abortTask = (kind) => {
310
+ if (controller.signal.aborted)
311
+ return;
312
+ abortKind = kind;
313
+ controller.abort();
314
+ };
315
+ const onTurnAbort = () => abortTask("cancelled");
316
+ if (ctx?.signal?.aborted)
317
+ onTurnAbort();
318
+ else
319
+ ctx?.signal?.addEventListener("abort", onTurnAbort, { once: true });
320
+ const totalTimer = setTimeout(() => abortTask("timeout"), maxPollMs);
321
+ const polling = pollToCompletion(adapter, jobId, creds, cwd, sessionId, prompt, pollIntervalMs, controller.signal, () => abortKind, maxPollMs, requestTimeoutMs);
322
+ backgroundJobRegistry.start(jobKey, sessionId ?? "", `生成视频中:${promptPreview}`, {
323
+ kind: "video",
324
+ abort: async () => {
325
+ abortTask("cancelled");
326
+ return await polling;
327
+ },
328
+ });
329
+ void polling
302
330
  .then((outcome) => backgroundJobRegistry.finish(jobKey, outcome))
303
- .catch((err) => backgroundJobRegistry.finish(jobKey, { status: "failed", finalText: err.message }));
331
+ .catch((err) => backgroundJobRegistry.finish(jobKey, { status: "failed", finalText: err.message }))
332
+ .finally(() => {
333
+ clearTimeout(totalTimer);
334
+ ctx?.signal?.removeEventListener("abort", onTurnAbort);
335
+ });
304
336
  return [
305
337
  `Video generation started in the background.`,
306
338
  `job_id: ${jobId} (internal — do not show to user)`,
@@ -310,16 +342,36 @@ export async function generateVideoTool(args, ctx) {
310
342
  `If you have no other work right now, END YOUR TURN — the system wakes you when the video is ready. NEVER run \`sleep\` or poll the provider yourself.`,
311
343
  ].join("\n");
312
344
  }
313
- async function pollToCompletion(adapter, jobId, creds, cwd, sessionId, prompt, pollIntervalMs) {
314
- const started = Date.now();
345
+ async function pollToCompletion(adapter, jobId, creds, cwd, sessionId, prompt, pollIntervalMs, signal, getAbortKind, totalTimeoutMs, perRequestTimeoutMs) {
346
+ let knownVideoUrl;
347
+ const retainedUrlOutcome = () => {
348
+ if (!knownVideoUrl)
349
+ return undefined;
350
+ const msg = `Video rendered successfully, but the local download did not finish. Download it from ${knownVideoUrl}`;
351
+ notifyVideo(sessionId, "completed", prompt, undefined, undefined, knownVideoUrl);
352
+ return { status: "completed", finalText: msg };
353
+ };
354
+ const abortOutcome = () => {
355
+ const retained = retainedUrlOutcome();
356
+ if (retained) {
357
+ return retained;
358
+ }
359
+ if (getAbortKind() === "timeout") {
360
+ const msg = `video job ${jobId} timed out after ${Math.round(totalTimeoutMs / 1000)}s`;
361
+ notifyVideo(sessionId, "failed", prompt, undefined, msg);
362
+ return { status: "failed", finalText: msg };
363
+ }
364
+ const msg = "Local waiting stopped; the remote service may still be rendering the video.";
365
+ notifyVideo(sessionId, "cancelled", prompt, undefined, msg);
366
+ return { status: "cancelled", finalText: msg };
367
+ };
315
368
  try {
316
369
  for (;;) {
317
- if (Date.now() - started > MAX_POLL_MS) {
318
- const msg = `video job ${jobId} timed out after ${Math.round(MAX_POLL_MS / 1000)}s`;
319
- notifyVideo(sessionId, "failed", prompt, undefined, msg);
320
- return { status: "failed", finalText: msg };
321
- }
322
- const res = await adapter.poll({ jobId, creds });
370
+ if (signal.aborted)
371
+ return abortOutcome();
372
+ const res = await runVideoRequest((requestSignal) => adapter.poll({ jobId, creds, signal: requestSignal }), signal, perRequestTimeoutMs);
373
+ if (signal.aborted)
374
+ return abortOutcome();
323
375
  if (!res.ok) {
324
376
  notifyVideo(sessionId, "failed", prompt, undefined, res.error);
325
377
  return { status: "failed", finalText: res.error };
@@ -328,27 +380,84 @@ async function pollToCompletion(adapter, jobId, creds, cwd, sessionId, prompt, p
328
380
  notifyVideo(sessionId, "failed", prompt, undefined, res.error);
329
381
  return { status: "failed", finalText: res.error };
330
382
  }
331
- if (res.status === "succeeded")
383
+ if (res.status === "succeeded") {
384
+ knownVideoUrl = res.url;
332
385
  break;
333
- await new Promise((r) => setTimeout(r, pollIntervalMs));
386
+ }
387
+ await abortableDelay(pollIntervalMs, signal);
334
388
  }
335
- const dl = await adapter.download({ jobId, creds });
389
+ const dl = await runVideoRequest((requestSignal) => adapter.download({
390
+ jobId,
391
+ creds,
392
+ signal: requestSignal,
393
+ onUrl: (url) => {
394
+ knownVideoUrl = url;
395
+ },
396
+ }), signal, perRequestTimeoutMs);
397
+ if (signal.aborted)
398
+ return abortOutcome();
336
399
  if (!dl.ok) {
400
+ const retained = retainedUrlOutcome();
401
+ if (retained)
402
+ return retained;
337
403
  notifyVideo(sessionId, "failed", prompt, undefined, dl.error);
338
404
  return { status: "failed", finalText: dl.error };
339
405
  }
406
+ knownVideoUrl = dl.url ?? knownVideoUrl;
340
407
  const dir = join(cwd, ".code-shell", "generated_videos");
341
408
  await mkdir(dir, { recursive: true });
409
+ if (signal.aborted)
410
+ return abortOutcome();
342
411
  const path = join(dir, `${Date.now()}.${dl.ext || "mp4"}`);
343
- await writeFile(path, dl.bytes);
344
- notifyVideo(sessionId, "completed", prompt, path, undefined, dl.url);
412
+ await writeFile(path, dl.bytes, { signal });
413
+ notifyVideo(sessionId, "completed", prompt, path, undefined, knownVideoUrl);
345
414
  return { status: "completed", finalText: path };
346
415
  }
347
416
  catch (err) {
417
+ const retained = retainedUrlOutcome();
418
+ if (retained)
419
+ return retained;
420
+ if (signal.aborted)
421
+ return abortOutcome();
348
422
  notifyVideo(sessionId, "failed", prompt, undefined, err.message);
349
423
  return { status: "failed", finalText: err.message };
350
424
  }
351
425
  }
426
+ async function runVideoRequest(request, taskSignal, timeoutMs) {
427
+ const requestSignal = AbortSignal.any([taskSignal, AbortSignal.timeout(timeoutMs)]);
428
+ const promise = request(requestSignal);
429
+ if (requestSignal.aborted)
430
+ throw requestSignal.reason;
431
+ return await new Promise((resolve, reject) => {
432
+ const onAbort = () => reject(requestSignal.reason);
433
+ const cleanup = () => requestSignal.removeEventListener("abort", onAbort);
434
+ requestSignal.addEventListener("abort", onAbort, { once: true });
435
+ promise.then((value) => {
436
+ cleanup();
437
+ resolve(value);
438
+ }, (error) => {
439
+ cleanup();
440
+ reject(error);
441
+ });
442
+ });
443
+ }
444
+ async function abortableDelay(ms, signal) {
445
+ if (signal.aborted)
446
+ throw signal.reason;
447
+ await new Promise((resolve, reject) => {
448
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
449
+ const timer = setTimeout(() => {
450
+ cleanup();
451
+ resolve();
452
+ }, ms);
453
+ const onAbort = () => {
454
+ clearTimeout(timer);
455
+ cleanup();
456
+ reject(signal.reason);
457
+ };
458
+ signal.addEventListener("abort", onAbort, { once: true });
459
+ });
460
+ }
352
461
  function notifyVideo(sessionId, status, prompt, path, error, url) {
353
462
  if (!sessionId) {
354
463
  logger.warn("video_completion_without_session", { status });
@@ -363,11 +472,19 @@ function notifyVideo(sessionId, status, prompt, path, error, url) {
363
472
  agentId: `video-${Date.now()}`,
364
473
  name: "video generation",
365
474
  description: status === "completed"
366
- ? `Video generated: ${path} (prompt: ${prompt})`
367
- : `Video generation failed (prompt: ${prompt})`,
475
+ ? path
476
+ ? `Video generated: ${path} (prompt: ${prompt})`
477
+ : `Video rendered: ${url} (local download stopped; prompt: ${prompt})`
478
+ : status === "cancelled"
479
+ ? `Stopped waiting for video generation (prompt: ${prompt})`
480
+ : `Video generation failed (prompt: ${prompt})`,
368
481
  status,
369
- finalText: status === "completed" ? `Video saved to ${path}.${extendHint}` : undefined,
370
- error: status === "failed" ? error : undefined,
482
+ finalText: status === "completed"
483
+ ? path
484
+ ? `Video saved to ${path}.${extendHint}`
485
+ : `Video rendered successfully. Download it from ${url}.${extendHint}`
486
+ : undefined,
487
+ error: status === "failed" || status === "cancelled" ? error : undefined,
371
488
  enqueuedAt: Date.now(),
372
489
  }, sessionId);
373
490
  }
@@ -3,5 +3,6 @@
3
3
  */
4
4
  import type { ToolDefinition } from "../../types.js";
5
5
  import type { ToolContext } from "../context.js";
6
+ import type { ToolFailure } from "./index.js";
6
7
  export declare const globToolDef: ToolDefinition;
7
- export declare function globTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
8
+ export declare function globTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string | ToolFailure>;
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Built-in Glob file search tool.
3
3
  */
4
- import { glob } from "glob";
4
+ import { Glob } from "glob";
5
5
  import { stat } from "node:fs/promises";
6
6
  import * as path from "node:path";
7
+ import { classifyPath } from "../path-policy.js";
7
8
  export const globToolDef = {
8
9
  name: "Glob",
9
10
  description: "Fast file pattern matching. Returns matching file paths sorted by modification time. " +
@@ -32,13 +33,37 @@ export async function globTool(args, ctx) {
32
33
  : path.resolve(baseDir, argPath)
33
34
  : baseDir;
34
35
  try {
35
- const matches = await glob(pattern, {
36
+ const matcher = new Glob(pattern, {
36
37
  cwd,
37
38
  absolute: true,
38
39
  nodir: true,
39
40
  dot: false,
41
+ follow: false,
40
42
  ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/.next/**", "**/coverage/**"],
41
43
  });
44
+ for (const parsedPattern of matcher.patterns) {
45
+ const expandedPattern = parsedPattern.globString();
46
+ if (parsedPattern.isAbsolute() ||
47
+ path.posix.isAbsolute(expandedPattern) ||
48
+ path.win32.isAbsolute(expandedPattern)) {
49
+ return "Error: Glob pattern must be relative to the search path.";
50
+ }
51
+ if (expandedPattern.split(/[\\/]/).includes("..")) {
52
+ return "Error: Glob pattern cannot contain a '..' path segment.";
53
+ }
54
+ }
55
+ const searchRoot = classifyPath(cwd, { workspaceRoot: cwd, operation: "read" }).resolvedPath;
56
+ const matches = (await matcher.walk()).filter((filePath) => {
57
+ const resolved = classifyPath(filePath, {
58
+ workspaceRoot: cwd,
59
+ operation: "read",
60
+ }).resolvedPath;
61
+ const relativeToRoot = path.relative(searchRoot, resolved);
62
+ return (relativeToRoot === "" ||
63
+ (relativeToRoot !== ".." &&
64
+ !relativeToRoot.startsWith(`..${path.sep}`) &&
65
+ !path.isAbsolute(relativeToRoot)));
66
+ });
42
67
  if (matches.length === 0) {
43
68
  return "No files matched the pattern.";
44
69
  }
@@ -71,6 +96,6 @@ export async function globTool(args, ctx) {
71
96
  return result;
72
97
  }
73
98
  catch (err) {
74
- return `Error in glob: ${err.message}`;
99
+ return { ok: false, error: `Error in glob: ${err.message}` };
75
100
  }
76
101
  }
@@ -6,6 +6,7 @@ import type { ToolContext } from "../context.js";
6
6
  type ExecFileForGrep = (file: string, args: readonly string[], options: {
7
7
  maxBuffer: number;
8
8
  timeout: number;
9
+ signal?: AbortSignal;
9
10
  }) => Promise<{
10
11
  stdout: string;
11
12
  stderr: string;
@@ -3,12 +3,56 @@
3
3
  */
4
4
  import { execFile } from "node:child_process";
5
5
  import { readdir, readFile, stat } from "node:fs/promises";
6
- import { promisify } from "node:util";
7
6
  import { basename, join, relative, resolve, sep, isAbsolute } from "node:path";
8
- const execFileAsync = promisify(execFile);
9
- let execFileForTest = execFileAsync;
7
+ import { killChildTree } from "../../runtime/spawn-common.js";
8
+ function abortError() {
9
+ return Object.assign(new Error("Search aborted"), { name: "AbortError" });
10
+ }
11
+ const execFileAbortable = (file, args, options) => new Promise((resolvePromise, reject) => {
12
+ if (options.signal?.aborted) {
13
+ reject(abortError());
14
+ return;
15
+ }
16
+ let settled = false;
17
+ const child = execFile(file, [...args], { encoding: "utf8", maxBuffer: options.maxBuffer }, (error, stdout, stderr) => {
18
+ if (settled)
19
+ return;
20
+ settled = true;
21
+ cleanup();
22
+ if (error) {
23
+ Object.assign(error, { stdout, stderr });
24
+ reject(error);
25
+ }
26
+ else {
27
+ resolvePromise({ stdout, stderr });
28
+ }
29
+ });
30
+ const finishWithKill = (error) => {
31
+ if (settled)
32
+ return;
33
+ settled = true;
34
+ cleanup();
35
+ killChildTree(child, 1000);
36
+ reject(error);
37
+ };
38
+ const onAbort = () => finishWithKill(abortError());
39
+ const timer = setTimeout(() => {
40
+ const error = Object.assign(new Error(`Search timed out after ${options.timeout}ms`), {
41
+ code: "ETIMEDOUT",
42
+ });
43
+ finishWithKill(error);
44
+ }, options.timeout);
45
+ const cleanup = () => {
46
+ clearTimeout(timer);
47
+ options.signal?.removeEventListener("abort", onAbort);
48
+ };
49
+ options.signal?.addEventListener("abort", onAbort, { once: true });
50
+ if (options.signal?.aborted)
51
+ onAbort();
52
+ });
53
+ let execFileForTest = execFileAbortable;
10
54
  export function _setGrepExecFileForTest(fn) {
11
- execFileForTest = fn ?? execFileAsync;
55
+ execFileForTest = fn ?? execFileAbortable;
12
56
  }
13
57
  /**
14
58
  * Strip the cwd prefix from each line so results display as relative
@@ -39,7 +83,10 @@ export const grepToolDef = {
39
83
  pattern: { type: "string", description: "Regular expression pattern to search for" },
40
84
  path: { type: "string", description: "File or directory to search in (default: cwd)" },
41
85
  glob: { type: "string", description: 'Glob pattern to filter files (e.g. "*.ts")' },
42
- context: { type: "number", description: "Lines of context around matches (only with output_mode content)" },
86
+ context: {
87
+ type: "number",
88
+ description: "Lines of context around matches (only with output_mode content)",
89
+ },
43
90
  max_results: { type: "number", description: "Maximum number of results (default: 50)" },
44
91
  output_mode: {
45
92
  type: "string",
@@ -69,29 +116,36 @@ export async function grepTool(args, ctx) {
69
116
  const outputMode = args.output_mode || "files_with_matches";
70
117
  const caseInsensitive = args.case_insensitive || false;
71
118
  try {
72
- return await runRipgrep(pattern, searchPath, fileGlob, context, maxResults, outputMode, caseInsensitive);
119
+ return await runRipgrep(pattern, searchPath, fileGlob, context, maxResults, outputMode, caseInsensitive, ctx?.signal);
73
120
  }
74
121
  catch (rgErr) {
122
+ if (isAbort(rgErr, ctx?.signal))
123
+ throw rgErr;
75
124
  // rg/grep exit code 1 = no matches (not an error)
76
125
  if (isNoMatchExit(rgErr))
77
126
  return "No matches found.";
78
127
  try {
79
- return await runGrep(pattern, searchPath, fileGlob, context, maxResults, outputMode, caseInsensitive);
128
+ return await runGrep(pattern, searchPath, fileGlob, context, maxResults, outputMode, caseInsensitive, ctx?.signal);
80
129
  }
81
130
  catch (grepErr) {
131
+ if (isAbort(grepErr, ctx?.signal))
132
+ throw grepErr;
82
133
  if (isNoMatchExit(grepErr))
83
134
  return "No matches found.";
84
135
  if (isCommandNotFound(rgErr) || isCommandNotFound(grepErr)) {
85
- return await runNodeGrep(pattern, searchPath, fileGlob, context, maxResults, outputMode, caseInsensitive);
136
+ return await runNodeGrep(pattern, searchPath, fileGlob, context, maxResults, outputMode, caseInsensitive, ctx?.signal);
86
137
  }
87
- return `Error in search: ${grepErr.message}`;
138
+ throw new Error(`Error in search: ${grepErr.message}`);
88
139
  }
89
140
  }
90
141
  }
142
+ function isAbort(error, signal) {
143
+ return signal?.aborted === true || error?.name === "AbortError";
144
+ }
91
145
  function isCommandNotFound(err) {
92
146
  return err?.code === "ENOENT";
93
147
  }
94
- async function runRipgrep(pattern, path, fileGlob, context, maxResults, outputMode, caseInsensitive) {
148
+ async function runRipgrep(pattern, path, fileGlob, context, maxResults, outputMode, caseInsensitive, signal) {
95
149
  const args = ["--color=never"];
96
150
  if (caseInsensitive)
97
151
  args.push("-i");
@@ -116,6 +170,7 @@ async function runRipgrep(pattern, path, fileGlob, context, maxResults, outputMo
116
170
  const { stdout } = await execFileForTest("rg", args, {
117
171
  maxBuffer: 10 * 1024 * 1024,
118
172
  timeout: 30_000,
173
+ signal,
119
174
  });
120
175
  const result = relativizeOutput(stdout, path).trim();
121
176
  if (!result)
@@ -127,7 +182,7 @@ async function runRipgrep(pattern, path, fileGlob, context, maxResults, outputMo
127
182
  }
128
183
  return result;
129
184
  }
130
- async function runGrep(pattern, path, fileGlob, context, maxResults, outputMode, caseInsensitive) {
185
+ async function runGrep(pattern, path, fileGlob, context, maxResults, outputMode, caseInsensitive, signal) {
131
186
  const args = ["-r", "--color=never"];
132
187
  if (caseInsensitive)
133
188
  args.push("-i");
@@ -153,6 +208,7 @@ async function runGrep(pattern, path, fileGlob, context, maxResults, outputMode,
153
208
  const { stdout } = await execFileForTest("grep", args, {
154
209
  maxBuffer: 10 * 1024 * 1024,
155
210
  timeout: 30_000,
211
+ signal,
156
212
  });
157
213
  const result = relativizeOutput(stdout, path).trim();
158
214
  if (!result)
@@ -164,16 +220,18 @@ async function runGrep(pattern, path, fileGlob, context, maxResults, outputMode,
164
220
  return result;
165
221
  }
166
222
  const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "coverage"]);
167
- async function runNodeGrep(pattern, path, fileGlob, context, maxResults, outputMode, caseInsensitive) {
223
+ async function runNodeGrep(pattern, path, fileGlob, context, maxResults, outputMode, caseInsensitive, signal) {
224
+ signal?.throwIfAborted();
168
225
  let regex;
169
226
  try {
170
227
  regex = new RegExp(pattern, caseInsensitive ? "i" : "");
171
228
  }
172
229
  catch (err) {
173
- return `Error in search: ${err.message}`;
230
+ throw new Error(`Error in search: ${err.message}`);
174
231
  }
175
232
  const matches = [];
176
233
  await walkTextFiles(path, fileGlob, async (file) => {
234
+ signal?.throwIfAborted();
177
235
  if (matches.length >= maxResults)
178
236
  return;
179
237
  let text;
@@ -214,15 +272,17 @@ async function runNodeGrep(pattern, path, fileGlob, context, maxResults, outputM
214
272
  }
215
273
  }
216
274
  }
217
- });
275
+ }, signal);
218
276
  if (matches.length === 0)
219
277
  return "No matches found.";
220
278
  if (matches.length > 200)
221
279
  return matches.slice(0, 200).join("\n") + `\n\n... ${matches.length - 200} more results`;
222
280
  return matches.join("\n");
223
281
  }
224
- async function walkTextFiles(root, fileGlob, visit) {
282
+ async function walkTextFiles(root, fileGlob, visit, signal) {
283
+ signal?.throwIfAborted();
225
284
  const info = await stat(root).catch(() => null);
285
+ signal?.throwIfAborted();
226
286
  if (!info)
227
287
  return;
228
288
  if (info.isFile()) {
@@ -233,11 +293,13 @@ async function walkTextFiles(root, fileGlob, visit) {
233
293
  if (!info.isDirectory())
234
294
  return;
235
295
  const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
296
+ signal?.throwIfAborted();
236
297
  for (const entry of entries) {
298
+ signal?.throwIfAborted();
237
299
  if (entry.isDirectory()) {
238
300
  if (IGNORED_DIRS.has(entry.name))
239
301
  continue;
240
- await walkTextFiles(join(root, entry.name), fileGlob, visit);
302
+ await walkTextFiles(join(root, entry.name), fileGlob, visit, signal);
241
303
  }
242
304
  else if (entry.isFile()) {
243
305
  if (!fileGlob || matchesSimpleGlob(entry.name, fileGlob))
@@ -246,6 +308,9 @@ async function walkTextFiles(root, fileGlob, visit) {
246
308
  }
247
309
  }
248
310
  function matchesSimpleGlob(name, glob) {
249
- const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
311
+ const escaped = glob
312
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
313
+ .replace(/\*/g, ".*")
314
+ .replace(/\?/g, ".");
250
315
  return new RegExp(`^${escaped}$`).test(name);
251
316
  }
@@ -13,16 +13,28 @@ import type { ToolVisibilityContext } from "../context.js";
13
13
  * ctx is optional in the signature so legacy call sites (and tests) that
14
14
  * pass only args still type-check; ToolRegistry always passes ctx at runtime.
15
15
  */
16
- /**
17
- * 内置工具返回值:大多数返回纯文本字符串。需要回传图片(或其它结构化
18
- * 内容块)的工具(view_image)可改为返回 `{ contentBlocks }`;此时
19
- * registry 会把它放进 ToolResult.contentBlocks。可选的 `result` 字段是给
20
- * transcript / 摘要用的纯文本镜像。沙箱执行类工具(Bash 等)可返回
21
- * `{ result, sandbox }`,registry 把 sandbox 透传到 ToolResult.sandbox 供 UI 显示。
22
- * 敏感结果可返回 `{ result, sensitive, displayResult, transcriptResult }`;
23
- * `result` 仅供当前模型轮使用,显示/持久化路径必须使用占位字段。
24
- */
25
- export type BuiltinToolResult = string | {
16
+ /** Canonical tool-handler protocol. Only ToolSuccess is recorded as ok:true. */
17
+ export interface ToolSuccess {
18
+ ok: true;
19
+ result?: string;
20
+ contentBlocks?: import("../../types.js").ContentBlock[];
21
+ sandbox?: import("../../types.js").ToolResult["sandbox"];
22
+ sensitive?: boolean;
23
+ displayResult?: string;
24
+ transcriptResult?: string;
25
+ }
26
+ export interface ToolFailure {
27
+ ok: false;
28
+ error: string;
29
+ sandbox?: import("../../types.js").ToolResult["sandbox"];
30
+ exitCode?: number | null;
31
+ signal?: NodeJS.Signals | null;
32
+ stderr?: string;
33
+ }
34
+ export type ToolExecutionResult = ToolSuccess | ToolFailure;
35
+ export type BuiltinToolResult = ToolExecutionResult;
36
+ /** Direct implementation shape, normalized before entering the registry. */
37
+ export type BuiltinToolReturn = string | BuiltinToolResult | {
26
38
  contentBlocks: import("../../types.js").ContentBlock[];
27
39
  result?: string;
28
40
  } | {
@@ -32,7 +44,9 @@ export type BuiltinToolResult = string | {
32
44
  displayResult?: string;
33
45
  transcriptResult?: string;
34
46
  };
35
- export type BuiltinToolFn = (args: Record<string, unknown>, ctx?: import("../context.js").ToolContext) => Promise<BuiltinToolResult>;
47
+ export type BuiltinToolFn = (args: Record<string, unknown>, ctx?: import("../context.js").ToolContext) => Promise<string | BuiltinToolResult>;
48
+ /** Convert every builtin implementation into the closed discriminated protocol. */
49
+ export declare function toToolExecutionResult(value: unknown): ToolExecutionResult;
36
50
  export type BuiltinToolGuard = (ctx: ToolVisibilityContext) => boolean;
37
51
  export interface BuiltinTool {
38
52
  definition: RegisteredTool;
@@ -44,7 +44,43 @@ import { browserObserveToolDef, browserObserveTool, browserActToolDef, browserAc
44
44
  import { useCredentialToolDef, useCredentialBuiltinTool, } from "../../credentials/use-credential-tool.js";
45
45
  import { injectCredentialToolDef, injectCredentialTool, isInjectCredentialAvailable, } from "../../credentials/inject-credential-tool.js";
46
46
  import { credentialAccessScope, getCredentialAccess } from "../../credentials/access.js";
47
- export const BUILTIN_TOOLS = [
47
+ const FAILURE_TEXT = /^(?:error\b|failed\b|failure\b|skill\b.*(?:not found\b|disabled\b|not available\b|allowlist\b|denied\b)|(?:[a-z][\w-]*\s+){1,3}(?:error\b|failed\b|failure\b|aborted\b|timed out\b))/i;
48
+ function failureMessage(result) {
49
+ if (typeof result.error === "string" && result.error)
50
+ return result.error;
51
+ if (typeof result.result === "string" && result.result)
52
+ return result.result;
53
+ return "Tool execution failed";
54
+ }
55
+ /** Convert every builtin implementation into the closed discriminated protocol. */
56
+ export function toToolExecutionResult(value) {
57
+ if (typeof value === "string") {
58
+ return FAILURE_TEXT.test(value) ? { ok: false, error: value } : { ok: true, result: value };
59
+ }
60
+ if (!value || typeof value !== "object") {
61
+ return { ok: false, error: `Invalid tool result: ${String(value)}` };
62
+ }
63
+ const result = value;
64
+ if (result.ok === false || result.isError === true || "error" in result) {
65
+ return {
66
+ ...result,
67
+ ok: false,
68
+ error: failureMessage(result),
69
+ };
70
+ }
71
+ if (result.ok === true)
72
+ return result;
73
+ const text = typeof result.result === "string" ? result.result : undefined;
74
+ if (text && FAILURE_TEXT.test(text)) {
75
+ return {
76
+ ok: false,
77
+ error: text,
78
+ ...(result.sandbox ? { sandbox: result.sandbox } : {}),
79
+ };
80
+ }
81
+ return { ok: true, ...result };
82
+ }
83
+ const BUILTIN_IMPLEMENTATIONS = [
48
84
  {
49
85
  definition: {
50
86
  ...readToolDef,
@@ -155,7 +191,10 @@ export const BUILTIN_TOOLS = [
155
191
  permissionDefault: "allow",
156
192
  isReadOnly: true,
157
193
  isConcurrencySafe: true,
158
- pathPolicy: [{ kind: "arg", arg: "path", operation: "read", defaultToCwd: true }],
194
+ pathPolicy: [
195
+ { kind: "arg", arg: "path", operation: "read", defaultToCwd: true },
196
+ { kind: "arg", arg: "pattern", operation: "read" },
197
+ ],
159
198
  },
160
199
  execute: globTool,
161
200
  },
@@ -730,6 +769,10 @@ export const BUILTIN_TOOLS = [
730
769
  execute: injectCredentialTool,
731
770
  },
732
771
  ];
772
+ export const BUILTIN_TOOLS = BUILTIN_IMPLEMENTATIONS.map(({ definition, execute }) => ({
773
+ definition,
774
+ execute: async (args, ctx) => toToolExecutionResult(await execute(args, ctx)),
775
+ }));
733
776
  /**
734
777
  * Per-tool availability predicates. A tool listed here is filtered OUT of the
735
778
  * exposed toolDefs when its predicate returns false for the active cwd (see
@@ -40,6 +40,7 @@ export type VideoPollResult = {
40
40
  } | {
41
41
  ok: true;
42
42
  status: "succeeded";
43
+ url?: string;
43
44
  } | {
44
45
  ok: true;
45
46
  status: "failed";
@@ -57,6 +58,14 @@ export type VideoDownloadResult = {
57
58
  ok: false;
58
59
  error: string;
59
60
  };
61
+ export interface VideoDownloadRequest {
62
+ jobId: string;
63
+ creds: VideoProviderCreds;
64
+ signal?: AbortSignal;
65
+ /** Called as soon as the provider exposes the remote result URL, before the
66
+ * potentially long byte download begins. */
67
+ onUrl?: (url: string) => void;
68
+ }
60
69
  export interface VideoProvider {
61
70
  readonly kind: string;
62
71
  submit(req: VideoSubmitRequest): Promise<VideoSubmitResult>;
@@ -65,11 +74,7 @@ export interface VideoProvider {
65
74
  creds: VideoProviderCreds;
66
75
  signal?: AbortSignal;
67
76
  }): Promise<VideoPollResult>;
68
- download(req: {
69
- jobId: string;
70
- creds: VideoProviderCreds;
71
- signal?: AbortSignal;
72
- }): Promise<VideoDownloadResult>;
77
+ download(req: VideoDownloadRequest): Promise<VideoDownloadResult>;
73
78
  }
74
79
  /**
75
80
  * Deterministic in-memory adapter for tests and local dev — no network. Models
@@ -95,11 +100,7 @@ export declare class FakeVideoProvider implements VideoProvider {
95
100
  creds?: VideoProviderCreds;
96
101
  signal?: AbortSignal;
97
102
  }): Promise<VideoPollResult>;
98
- download(_req: {
99
- jobId: string;
100
- creds?: VideoProviderCreds;
101
- signal?: AbortSignal;
102
- }): Promise<VideoDownloadResult>;
103
+ download(_req: VideoDownloadRequest): Promise<VideoDownloadResult>;
103
104
  }
104
105
  /**
105
106
  * fal.ai video adapter — submit/poll/download against the fal queue API
@@ -138,11 +139,7 @@ export declare class FalVideoProvider implements VideoProvider {
138
139
  creds: VideoProviderCreds;
139
140
  signal?: AbortSignal;
140
141
  }): Promise<VideoPollResult>;
141
- download(req: {
142
- jobId: string;
143
- creds: VideoProviderCreds;
144
- signal?: AbortSignal;
145
- }): Promise<VideoDownloadResult>;
142
+ download(req: VideoDownloadRequest): Promise<VideoDownloadResult>;
146
143
  }
147
144
  /** Default model per video provider kind (filled in with real adapters). */
148
145
  export declare const DEFAULT_VIDEO_MODEL: Record<string, string>;