@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.
- package/dist/arena/arena.d.ts +2 -0
- package/dist/arena/arena.js +30 -1
- package/dist/arena/phases/adjudication.d.ts +2 -1
- package/dist/arena/phases/adjudication.js +2 -1
- package/dist/arena/phases/build-consensus.d.ts +2 -1
- package/dist/arena/phases/build-consensus.js +3 -1
- package/dist/arena/phases/cross-review.d.ts +3 -1
- package/dist/arena/phases/cross-review.js +8 -2
- package/dist/arena/phases/debate-rounds.d.ts +2 -1
- package/dist/arena/phases/debate-rounds.js +4 -2
- package/dist/arena/phases/participant-research.d.ts +2 -1
- package/dist/arena/phases/participant-research.js +3 -1
- package/dist/arena/phases/planning-detail-expansion.d.ts +2 -1
- package/dist/arena/phases/planning-detail-expansion.js +2 -1
- package/dist/arena/planner.d.ts +2 -1
- package/dist/arena/planner.js +2 -1
- package/dist/arena/types.d.ts +5 -1
- package/dist/cli/agent-server-tcp.js +13 -2
- package/dist/context/manager.d.ts +3 -3
- package/dist/context/manager.js +6 -6
- package/dist/context/token-counter.js +5 -3
- package/dist/context/tool-result-storage.d.ts +6 -0
- package/dist/context/tool-result-storage.js +25 -4
- package/dist/credentials/access.d.ts +10 -1
- package/dist/credentials/access.js +76 -2
- package/dist/credentials/index.d.ts +3 -2
- package/dist/credentials/index.js +2 -1
- package/dist/credentials/oauth.d.ts +6 -1
- package/dist/credentials/oauth.js +66 -1
- package/dist/credentials/store.d.ts +1 -1
- package/dist/credentials/store.js +16 -7
- package/dist/credentials/types.d.ts +38 -1
- package/dist/credentials/types.js +16 -1
- package/dist/engine/engine.d.ts +61 -32
- package/dist/engine/engine.js +289 -55
- package/dist/engine/goal.d.ts +5 -3
- package/dist/engine/session-title.d.ts +2 -1
- package/dist/engine/session-title.js +4 -1
- package/dist/engine/turn-loop.d.ts +18 -0
- package/dist/engine/turn-loop.js +202 -27
- package/dist/engine/types.d.ts +6 -2
- package/dist/git/worktree/crud.d.ts +3 -0
- package/dist/git/worktree/crud.js +32 -3
- package/dist/git/worktree/git-exec.d.ts +2 -2
- package/dist/git/worktree/git-exec.js +47 -11
- package/dist/git/worktree/query.d.ts +8 -7
- package/dist/git/worktree/query.js +27 -20
- package/dist/hooks/events.d.ts +3 -0
- package/dist/hooks/events.js +0 -3
- package/dist/hooks/goal-stop-hook.d.ts +14 -4
- package/dist/hooks/goal-stop-hook.js +626 -71
- package/dist/hooks/registry.js +3 -0
- package/dist/hooks/shell-runner.d.ts +12 -1
- package/dist/hooks/shell-runner.js +160 -9
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -5
- package/dist/llm/client-base.js +12 -10
- package/dist/llm/types.d.ts +12 -5
- package/dist/plugins/pluginCommandHook.d.ts +4 -4
- package/dist/plugins/pluginCommandHook.js +111 -13
- package/dist/protocol/chat-session-manager.d.ts +13 -2
- package/dist/protocol/chat-session-manager.js +90 -18
- package/dist/protocol/chat-session.d.ts +12 -0
- package/dist/protocol/chat-session.js +30 -5
- package/dist/protocol/client.d.ts +5 -2
- package/dist/protocol/client.js +22 -1
- package/dist/protocol/server.d.ts +25 -11
- package/dist/protocol/server.js +290 -72
- package/dist/protocol/types.d.ts +34 -2
- package/dist/protocol/types.js +2 -0
- package/dist/services/dream-consolidation.d.ts +3 -0
- package/dist/services/dream-consolidation.js +4 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/oauth.d.ts +34 -10
- package/dist/services/oauth.js +233 -98
- package/dist/session/session-manager.d.ts +35 -6
- package/dist/session/session-manager.js +362 -26
- package/dist/session/transcript.d.ts +30 -1
- package/dist/session/transcript.js +119 -4
- package/dist/tool-system/builtin/agent.js +5 -1
- package/dist/tool-system/builtin/arena.js +1 -0
- package/dist/tool-system/builtin/background-jobs.d.ts +8 -5
- package/dist/tool-system/builtin/background-jobs.js +15 -11
- package/dist/tool-system/builtin/background-work.d.ts +6 -1
- package/dist/tool-system/builtin/background-work.js +5 -1
- package/dist/tool-system/builtin/bash.d.ts +3 -5
- package/dist/tool-system/builtin/bash.js +10 -5
- package/dist/tool-system/builtin/browser-tools.d.ts +2 -2
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +12 -4
- package/dist/tool-system/builtin/generate-video.d.ts +4 -0
- package/dist/tool-system/builtin/generate-video.js +138 -21
- package/dist/tool-system/builtin/glob.d.ts +2 -1
- package/dist/tool-system/builtin/glob.js +28 -3
- package/dist/tool-system/builtin/grep.d.ts +1 -0
- package/dist/tool-system/builtin/grep.js +82 -17
- package/dist/tool-system/builtin/index.d.ts +25 -11
- package/dist/tool-system/builtin/index.js +45 -2
- package/dist/tool-system/builtin/video-providers.d.ts +12 -15
- package/dist/tool-system/builtin/video-providers.js +1 -0
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/web-fetch.js +44 -3
- package/dist/tool-system/builtin/worktree.js +25 -7
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +14 -4
- package/dist/tool-system/context.d.ts +17 -5
- package/dist/tool-system/executor.js +24 -8
- package/dist/tool-system/mcp-manager.d.ts +6 -0
- package/dist/tool-system/mcp-manager.js +55 -5
- package/dist/tool-system/path-policy.d.ts +19 -0
- package/dist/tool-system/path-policy.js +62 -1
- package/dist/tool-system/permission.d.ts +43 -3
- package/dist/tool-system/permission.js +383 -30
- package/dist/tool-system/registry.d.ts +3 -2
- package/dist/tool-system/registry.js +52 -34
- package/dist/types.d.ts +15 -0
- 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
|
|
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
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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 (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
386
|
+
}
|
|
387
|
+
await abortableDelay(pollIntervalMs, signal);
|
|
334
388
|
}
|
|
335
|
-
const dl = await adapter.download({
|
|
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,
|
|
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
|
-
?
|
|
367
|
-
|
|
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"
|
|
370
|
-
|
|
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 {
|
|
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
|
|
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
|
}
|
|
@@ -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
|
-
|
|
9
|
-
|
|
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 ??
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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: [
|
|
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>;
|