@cjhyy/code-shell-core 0.7.0-beta.1 → 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/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +4 -0
- package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
- package/dist/cc-orchestrator/codex-session-history.js +64 -4
- package/dist/cc-orchestrator/external-agent-changes.js +22 -5
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -1
- package/dist/cc-orchestrator/external-agent-driver.js +202 -38
- package/dist/cc-orchestrator/session-history.d.ts +35 -0
- package/dist/cc-orchestrator/session-history.js +96 -13
- 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 +11 -1
- package/dist/credentials/access.js +77 -1
- package/dist/credentials/index.d.ts +3 -1
- package/dist/credentials/index.js +2 -0
- package/dist/credentials/oauth.d.ts +25 -0
- package/dist/credentials/oauth.js +179 -0
- package/dist/credentials/store.d.ts +2 -1
- package/dist/credentials/store.js +19 -8
- package/dist/credentials/types.d.ts +84 -1
- package/dist/credentials/types.js +16 -1
- package/dist/engine/engine.d.ts +67 -34
- package/dist/engine/engine.js +448 -247
- package/dist/engine/goal.d.ts +19 -0
- package/dist/engine/goal.js +16 -6
- package/dist/engine/input-attachments.js +156 -13
- package/dist/engine/run-image-input.d.ts +22 -0
- package/dist/engine/run-image-input.js +195 -0
- package/dist/engine/session-title.d.ts +2 -1
- package/dist/engine/session-title.js +4 -1
- package/dist/engine/steer-queue.d.ts +3 -1
- package/dist/engine/steer-queue.js +10 -2
- package/dist/engine/turn-loop.d.ts +48 -1
- package/dist/engine/turn-loop.js +307 -37
- 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 +44 -2
- package/dist/hooks/goal-stop-hook.js +775 -52
- 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/preset/index.js +14 -4
- 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 +291 -73
- package/dist/protocol/types.d.ts +36 -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 +396 -27
- package/dist/session/transcript.d.ts +30 -1
- package/dist/session/transcript.js +119 -4
- package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
- package/dist/tool-system/builtin/agent-notifications.js +19 -7
- 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 +28 -5
- package/dist/tool-system/builtin/background-jobs.js +109 -7
- 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/cron-list.definition.d.ts +3 -0
- package/dist/tool-system/builtin/cron-list.definition.js +6 -0
- package/dist/tool-system/builtin/cron.d.ts +1 -2
- package/dist/tool-system/builtin/cron.js +9 -7
- package/dist/tool-system/builtin/drive-claude-code.d.ts +7 -0
- package/dist/tool-system/builtin/drive-claude-code.js +307 -20
- 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 +60 -5
- package/dist/tool-system/builtin/sleep.d.ts +1 -2
- package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
- package/dist/tool-system/builtin/sleep.definition.js +28 -0
- package/dist/tool-system/builtin/sleep.js +1 -22
- 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 +35 -5
- package/dist/tool-system/executor.js +24 -8
- package/dist/tool-system/mcp-manager.d.ts +20 -2
- package/dist/tool-system/mcp-manager.js +111 -12
- 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 +38 -7
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sleep tool metadata.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the executor so registration and presets can consume the
|
|
5
|
+
* definition without importing implementation code.
|
|
6
|
+
*/
|
|
7
|
+
export const sleepToolDef = {
|
|
8
|
+
name: "Sleep",
|
|
9
|
+
description: "Pause execution for a brief, deterministic wait (e.g. letting a just-started service settle for a few seconds). " +
|
|
10
|
+
"Do NOT use Sleep to poll for or wait on background work (background shells, async sub-agents, video generation): " +
|
|
11
|
+
"the system wakes you automatically when that work completes — just end your turn instead of looping Sleep. " +
|
|
12
|
+
"If you want a safety net in case a background task hangs and never signals completion, do NOT loop Sleep either — " +
|
|
13
|
+
"instead end your turn and schedule a one-shot self-wakeup with CronCreate " +
|
|
14
|
+
"({ schedule: '5m', once: true, continueInSession: true, permissionLevel: 'read-only', " +
|
|
15
|
+
"prompt: 'check whether <that task> finished; if still running, wait again' }). " +
|
|
16
|
+
"That returns control to you at the interval without burning a turn spinning. " +
|
|
17
|
+
"Maximum duration is 300 seconds (5 minutes).",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
seconds: {
|
|
22
|
+
type: "number",
|
|
23
|
+
description: "Number of seconds to sleep (max 300)",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ["seconds"],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -1,28 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SleepTool — pause execution for a specified duration.
|
|
3
3
|
*/
|
|
4
|
-
export
|
|
5
|
-
name: "Sleep",
|
|
6
|
-
description: "Pause execution for a brief, deterministic wait (e.g. letting a just-started service settle for a few seconds). " +
|
|
7
|
-
"Do NOT use Sleep to poll for or wait on background work (background shells, async sub-agents, video generation): " +
|
|
8
|
-
"the system wakes you automatically when that work completes — just end your turn instead of looping Sleep. " +
|
|
9
|
-
"If you want a safety net in case a background task hangs and never signals completion, do NOT loop Sleep either — " +
|
|
10
|
-
"instead end your turn and schedule a one-shot self-wakeup with CronCreate " +
|
|
11
|
-
"({ schedule: '5m', once: true, continueInSession: true, permissionLevel: 'read-only', " +
|
|
12
|
-
"prompt: 'check whether <that task> finished; if still running, wait again' }). " +
|
|
13
|
-
"That returns control to you at the interval without burning a turn spinning. " +
|
|
14
|
-
"Maximum duration is 300 seconds (5 minutes).",
|
|
15
|
-
inputSchema: {
|
|
16
|
-
type: "object",
|
|
17
|
-
properties: {
|
|
18
|
-
seconds: {
|
|
19
|
-
type: "number",
|
|
20
|
-
description: "Number of seconds to sleep (max 300)",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
required: ["seconds"],
|
|
24
|
-
},
|
|
25
|
-
};
|
|
4
|
+
export { sleepToolDef } from "./sleep.definition.js";
|
|
26
5
|
export async function sleepTool(args) {
|
|
27
6
|
const seconds = Math.min(Math.max(Number(args.seconds) || 1, 0.1), 300);
|
|
28
7
|
const signal = args.__signal;
|
|
@@ -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>;
|
|
@@ -197,6 +197,7 @@ export class FalVideoProvider {
|
|
|
197
197
|
const videoUrl = j.video?.url;
|
|
198
198
|
if (!videoUrl)
|
|
199
199
|
return { ok: false, error: "fal result: no video.url" };
|
|
200
|
+
req.onUrl?.(videoUrl);
|
|
200
201
|
// hop 2: video bytes
|
|
201
202
|
const vr = await this.fetchImpl(videoUrl, { method: "GET", signal: req.signal });
|
|
202
203
|
if (!vr.ok) {
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { ToolDefinition } from "../../types.js";
|
|
15
15
|
import type { ToolContext } from "../context.js";
|
|
16
|
-
import type {
|
|
16
|
+
import type { BuiltinToolReturn } from "./index.js";
|
|
17
17
|
export declare const viewImageToolDef: ToolDefinition;
|
|
18
|
-
export declare function viewImageTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<
|
|
18
|
+
export declare function viewImageTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<BuiltinToolReturn>;
|
|
@@ -25,6 +25,7 @@ export const webFetchToolDef = {
|
|
|
25
25
|
};
|
|
26
26
|
const MAX_OUTPUT = 100_000;
|
|
27
27
|
const DEFAULT_MAX = 50_000;
|
|
28
|
+
const RAW_BYTE_BUDGET_MULTIPLIER = 4;
|
|
28
29
|
// Header names that must not be overridable via args.headers (SSRF / auth injection)
|
|
29
30
|
const BLOCKED_REQUEST_HEADERS = new Set([
|
|
30
31
|
"host", "authorization", "cookie", "proxy-authorization",
|
|
@@ -252,7 +253,46 @@ export async function webFetchTool(args) {
|
|
|
252
253
|
}
|
|
253
254
|
async function readAndTruncateBody(res, maxLength) {
|
|
254
255
|
const contentType = res.headers.get("content-type") ?? "";
|
|
255
|
-
const
|
|
256
|
+
const maxBytes = maxLength * RAW_BYTE_BUDGET_MULTIPLIER;
|
|
257
|
+
const reader = res.body?.getReader();
|
|
258
|
+
let body;
|
|
259
|
+
let rawTruncated = false;
|
|
260
|
+
if (!reader) {
|
|
261
|
+
body = await res.text();
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
const decoder = new TextDecoder("utf-8");
|
|
265
|
+
const chunks = [];
|
|
266
|
+
let bytesRead = 0;
|
|
267
|
+
try {
|
|
268
|
+
for (;;) {
|
|
269
|
+
const { done, value } = await reader.read();
|
|
270
|
+
if (done)
|
|
271
|
+
break;
|
|
272
|
+
const remaining = maxBytes - bytesRead;
|
|
273
|
+
if (value.byteLength >= remaining) {
|
|
274
|
+
chunks.push(decoder.decode(value.subarray(0, remaining), { stream: true }));
|
|
275
|
+
bytesRead += remaining;
|
|
276
|
+
rawTruncated = true;
|
|
277
|
+
await reader.cancel();
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
281
|
+
bytesRead += value.byteLength;
|
|
282
|
+
}
|
|
283
|
+
const tail = decoder.decode();
|
|
284
|
+
// When the byte budget cuts through a multi-byte code point, flushing
|
|
285
|
+
// the decoder would turn that intentionally incomplete suffix into �.
|
|
286
|
+
// All complete code points were already emitted by stream:true, so the
|
|
287
|
+
// truncated tail is safe to discard.
|
|
288
|
+
if (!rawTruncated)
|
|
289
|
+
chunks.push(tail);
|
|
290
|
+
body = chunks.join("");
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
reader.releaseLock();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
256
296
|
let text;
|
|
257
297
|
if (contentType.includes("text/html") || contentType.includes("application/xhtml")) {
|
|
258
298
|
text = extractTextFromHTML(body);
|
|
@@ -260,8 +300,9 @@ async function readAndTruncateBody(res, maxLength) {
|
|
|
260
300
|
else {
|
|
261
301
|
text = body;
|
|
262
302
|
}
|
|
263
|
-
if (text.length > maxLength) {
|
|
264
|
-
|
|
303
|
+
if (rawTruncated || text.length > maxLength) {
|
|
304
|
+
const shownChars = Math.min(text.length, maxLength);
|
|
305
|
+
text = text.slice(0, maxLength) + `\n\n... content truncated (showing first ${shownChars} chars)`;
|
|
265
306
|
}
|
|
266
307
|
return text || "(page returned empty content)";
|
|
267
308
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { isAbsolute, resolve } from "node:path";
|
|
6
|
-
import { createWorktree, removeWorktree, listWorktrees, validateWorktreeSlug, selectPlatformScript, runWorktreeSetup, worktreeHasUncommittedOrAheadChanges, currentBranch, } from "../../git/worktree.js";
|
|
6
|
+
import { createWorktree, removeWorktree, listWorktrees, validateWorktreeSlug, selectPlatformScript, runWorktreeSetup, worktreeHasUncommittedOrAheadChanges, currentBranch, cleanupAbortedWorktree, } from "../../git/worktree.js";
|
|
7
7
|
export const switchSessionWorkspaceToolDef = {
|
|
8
8
|
name: "SwitchSessionWorkspace",
|
|
9
9
|
description: "Switch this current conversation session into or out of a workspace through the host UI path. " +
|
|
@@ -44,7 +44,7 @@ export async function switchSessionWorkspaceTool(args, ctx) {
|
|
|
44
44
|
nextTurnNotice(workspace.root, ctx?.cwd ?? workspace.root));
|
|
45
45
|
}
|
|
46
46
|
catch (err) {
|
|
47
|
-
return `Error switching workspace: ${err.message}`;
|
|
47
|
+
return `Error: switching workspace failed: ${err.message}`;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
export const enterWorktreeToolDef = {
|
|
@@ -86,6 +86,7 @@ export async function enterWorktreeTool(args, ctx) {
|
|
|
86
86
|
const branchPrefix = ctx?.engine?.readWorktreeBranchPrefix?.(mainRoot);
|
|
87
87
|
try {
|
|
88
88
|
if (target === "main") {
|
|
89
|
+
ctx?.signal?.throwIfAborted();
|
|
89
90
|
const workspace = { root: mainRoot, kind: "main" };
|
|
90
91
|
persistSessionWorkspace(sessionManager, sessionId, workspace, ctx);
|
|
91
92
|
sessionManager.recordWorkspaceHandoff(sessionId, fromWorkspace, workspace);
|
|
@@ -101,13 +102,26 @@ export async function enterWorktreeTool(args, ctx) {
|
|
|
101
102
|
sessionId,
|
|
102
103
|
currentWorkspace: fromWorkspace,
|
|
103
104
|
branchPrefix,
|
|
105
|
+
signal: ctx?.signal,
|
|
104
106
|
});
|
|
107
|
+
if (ctx?.signal?.aborted) {
|
|
108
|
+
if (selected.created) {
|
|
109
|
+
await cleanupAbortedWorktree(mainRoot, selected.session.worktreePath, selected.session.worktreeBranch);
|
|
110
|
+
}
|
|
111
|
+
throw new Error("Worktree creation aborted");
|
|
112
|
+
}
|
|
105
113
|
const workspace = toSessionWorkspace(selected, fromWorkspace);
|
|
106
|
-
persistSessionWorkspace(sessionManager, sessionId, workspace, ctx);
|
|
107
|
-
sessionManager.recordWorkspaceHandoff(sessionId, fromWorkspace, workspace);
|
|
108
114
|
const setupNote = selected.created
|
|
109
115
|
? await runSetupIfConfigured(selected.session.worktreePath, mainRoot, ctx)
|
|
110
116
|
: "";
|
|
117
|
+
if (ctx?.signal?.aborted) {
|
|
118
|
+
if (selected.created) {
|
|
119
|
+
await cleanupAbortedWorktree(mainRoot, selected.session.worktreePath, selected.session.worktreeBranch);
|
|
120
|
+
}
|
|
121
|
+
throw new Error("Worktree creation aborted");
|
|
122
|
+
}
|
|
123
|
+
persistSessionWorkspace(sessionManager, sessionId, workspace, ctx);
|
|
124
|
+
sessionManager.recordWorkspaceHandoff(sessionId, fromWorkspace, workspace);
|
|
111
125
|
const verb = selected.created ? "Worktree created and switched" : "Switched to worktree";
|
|
112
126
|
return (`${verb}:\n` +
|
|
113
127
|
` Path: ${workspace.worktree.path}\n` +
|
|
@@ -118,7 +132,7 @@ export async function enterWorktreeTool(args, ctx) {
|
|
|
118
132
|
setupNote);
|
|
119
133
|
}
|
|
120
134
|
catch (err) {
|
|
121
|
-
return `Error switching worktree: ${err.message}`;
|
|
135
|
+
return `Error: switching worktree failed: ${err.message}`;
|
|
122
136
|
}
|
|
123
137
|
}
|
|
124
138
|
/** Keep setup output from bloating the tool result — head+tail-ish trim. */
|
|
@@ -229,7 +243,7 @@ export async function exitWorktreeTool(args, ctx) {
|
|
|
229
243
|
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
230
244
|
}
|
|
231
245
|
catch (err) {
|
|
232
|
-
return `Error exiting worktree: ${err.message}`;
|
|
246
|
+
return `Error: exiting worktree failed: ${err.message}`;
|
|
233
247
|
}
|
|
234
248
|
}
|
|
235
249
|
function sessionServices(ctx) {
|
|
@@ -261,6 +275,7 @@ function sharedWorktreeRemovalSkippedMessage(otherOwners, mainRoot, currentTurnR
|
|
|
261
275
|
}
|
|
262
276
|
async function resolveWorktreeTarget(opts) {
|
|
263
277
|
const entries = await listWorktrees(opts.mainRoot);
|
|
278
|
+
opts.signal?.throwIfAborted();
|
|
264
279
|
const pathTarget = pathLike(opts.target) ? resolvePathTarget(opts.target, opts.cwd) : undefined;
|
|
265
280
|
const branchTarget = normalizeBranchName(opts.target);
|
|
266
281
|
const match = entries.find((entry) => {
|
|
@@ -269,6 +284,8 @@ async function resolveWorktreeTarget(opts) {
|
|
|
269
284
|
return entry.branch === branchTarget;
|
|
270
285
|
});
|
|
271
286
|
if (match) {
|
|
287
|
+
const originalBranch = await currentBranch(opts.mainRoot);
|
|
288
|
+
opts.signal?.throwIfAborted();
|
|
272
289
|
return {
|
|
273
290
|
created: false,
|
|
274
291
|
session: {
|
|
@@ -276,7 +293,7 @@ async function resolveWorktreeTarget(opts) {
|
|
|
276
293
|
worktreePath: match.path,
|
|
277
294
|
worktreeName: match.path.split(/[\\/]/).pop() ?? match.branch,
|
|
278
295
|
worktreeBranch: match.branch,
|
|
279
|
-
originalBranch
|
|
296
|
+
originalBranch,
|
|
280
297
|
sessionId: opts.sessionId,
|
|
281
298
|
createdAt: Date.now(),
|
|
282
299
|
},
|
|
@@ -289,6 +306,7 @@ async function resolveWorktreeTarget(opts) {
|
|
|
289
306
|
validateWorktreeSlug(opts.target);
|
|
290
307
|
const created = await createWorktree(opts.mainRoot, opts.target, opts.sessionId, {
|
|
291
308
|
prefix: opts.branchPrefix,
|
|
309
|
+
signal: opts.signal,
|
|
292
310
|
});
|
|
293
311
|
return { created: true, session: created, from: created.originalBranch ?? "HEAD" };
|
|
294
312
|
}
|
|
@@ -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 writeToolDef: ToolDefinition;
|
|
7
|
-
export declare function writeTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
8
|
+
export declare function writeTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string | ToolFailure>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in Write file tool.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { mkdir } from "node:fs/promises";
|
|
5
5
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
6
6
|
import { fileCache } from "./file-cache.js";
|
|
7
|
+
import { getFinalWritePathSnapshot, revalidateFinalWritePath, writeFileNoFollow, } from "../path-policy.js";
|
|
7
8
|
export const writeToolDef = {
|
|
8
9
|
name: "Write",
|
|
9
10
|
description: "Write content to a file. Creates the file if it doesn't exist, " +
|
|
@@ -27,12 +28,21 @@ export async function writeTool(args, ctx) {
|
|
|
27
28
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
28
29
|
const filePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
29
30
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
const approvedPath = getFinalWritePathSnapshot(args, filePath, cwd);
|
|
32
|
+
const beforeMkdir = revalidateFinalWritePath(filePath, cwd, approvedPath);
|
|
33
|
+
if ("error" in beforeMkdir)
|
|
34
|
+
return { ok: false, error: beforeMkdir.error };
|
|
35
|
+
await mkdir(dirname(beforeMkdir.resolvedPath), { recursive: true });
|
|
36
|
+
// mkdir may have crossed an existing symlink in a missing parent chain;
|
|
37
|
+
// resolve again immediately before opening the final file.
|
|
38
|
+
const beforeWrite = revalidateFinalWritePath(filePath, cwd, approvedPath);
|
|
39
|
+
if ("error" in beforeWrite)
|
|
40
|
+
return { ok: false, error: beforeWrite.error };
|
|
41
|
+
await writeFileNoFollow(beforeWrite.resolvedPath, content);
|
|
32
42
|
fileCache.invalidate(filePath);
|
|
33
43
|
return `Successfully wrote to ${filePath}`;
|
|
34
44
|
}
|
|
35
45
|
catch (err) {
|
|
36
|
-
return `Error writing file: ${err.message}
|
|
46
|
+
return { ok: false, error: `Error writing file: ${err.message}` };
|
|
37
47
|
}
|
|
38
48
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Tools that don't need any context (Read/Write/Bash/...) just ignore
|
|
13
13
|
* the second argument; the type is purely additive.
|
|
14
14
|
*/
|
|
15
|
-
import type { LLMConfig, StreamCallback } from "../types.js";
|
|
15
|
+
import type { LLMConfig, StreamCallback, TokenUsage } from "../types.js";
|
|
16
16
|
import type { ModelPool } from "../llm/model-pool.js";
|
|
17
17
|
import type { ToolRegistry } from "./registry.js";
|
|
18
18
|
import type { AgentPresetName } from "../preset/index.js";
|
|
@@ -20,6 +20,7 @@ import type { SandboxBackend } from "./sandbox/index.js";
|
|
|
20
20
|
import type { HookRegistry } from "../hooks/registry.js";
|
|
21
21
|
import type { SessionManager } from "../session/session-manager.js";
|
|
22
22
|
import type { SessionWorkspace } from "../types.js";
|
|
23
|
+
import type { ApprovalRouter } from "./permission.js";
|
|
23
24
|
/**
|
|
24
25
|
* Narrow view of the owning Engine that tools are allowed to call back into.
|
|
25
26
|
* Defined here (in the low-level tool-system) rather than importing the
|
|
@@ -154,10 +155,7 @@ export interface SubAgentSpawner {
|
|
|
154
155
|
* (or `resumeSessionId` when resuming), so the parent can address follow-up
|
|
155
156
|
* input at the same session via AgentSendInput (transcript replay).
|
|
156
157
|
*/
|
|
157
|
-
spawn(req: SubAgentSpawnRequest): Promise<
|
|
158
|
-
text: string;
|
|
159
|
-
sessionId: string;
|
|
160
|
-
}>;
|
|
158
|
+
spawn(req: SubAgentSpawnRequest): Promise<SubAgentSpawnResult>;
|
|
161
159
|
/**
|
|
162
160
|
* Whether a child session id exists on disk. Lets AgentSendInput resume an
|
|
163
161
|
* agent across a process restart (when the in-memory registry is empty but
|
|
@@ -173,6 +171,12 @@ export interface SubAgentSpawner {
|
|
|
173
171
|
permissionMode: string;
|
|
174
172
|
};
|
|
175
173
|
}
|
|
174
|
+
export interface SubAgentSpawnResult {
|
|
175
|
+
text: string;
|
|
176
|
+
sessionId: string;
|
|
177
|
+
/** Complete billed usage returned by the child Engine run. */
|
|
178
|
+
usage?: TokenUsage;
|
|
179
|
+
}
|
|
176
180
|
/**
|
|
177
181
|
* The full context object handed to every tool invocation.
|
|
178
182
|
*
|
|
@@ -184,6 +188,15 @@ export interface ToolVisibilityContext {
|
|
|
184
188
|
hasGoal: boolean;
|
|
185
189
|
settingsScope?: import("../settings/manager.js").SettingsScope;
|
|
186
190
|
}
|
|
191
|
+
export interface ExternalFileChangesRecord {
|
|
192
|
+
jobId: string;
|
|
193
|
+
description: string;
|
|
194
|
+
cli: "claude" | "codex";
|
|
195
|
+
cwd: string;
|
|
196
|
+
status: "completed" | "failed" | "cancelled";
|
|
197
|
+
changedFiles: string[];
|
|
198
|
+
originClientMessageId?: string;
|
|
199
|
+
}
|
|
187
200
|
export interface ToolContext {
|
|
188
201
|
/** Active working directory for this Engine. */
|
|
189
202
|
cwd: string;
|
|
@@ -238,6 +251,14 @@ export interface ToolContext {
|
|
|
238
251
|
* headless/test mode; tools must tolerate absence.
|
|
239
252
|
*/
|
|
240
253
|
streamCallback?: StreamCallback;
|
|
254
|
+
/**
|
|
255
|
+
* Report provider usage produced by a tool or child Engine into the owning
|
|
256
|
+
* session and run-scoped Goal budget. This must not invoke the process-wide
|
|
257
|
+
* billing hook again; the originating provider client already did that.
|
|
258
|
+
*/
|
|
259
|
+
recordBilledUsage?: (usage: TokenUsage) => void;
|
|
260
|
+
/** Connection-scoped approval owner router supplied by the protocol host. */
|
|
261
|
+
approvalRouter?: ApprovalRouter;
|
|
241
262
|
/** Whether the owning Engine is currently in plan mode. Replaces the
|
|
242
263
|
* removed module-level `isInPlanMode()` singleton. */
|
|
243
264
|
planMode: boolean;
|
|
@@ -261,6 +282,15 @@ export interface ToolContext {
|
|
|
261
282
|
* narrow context, or standalone tool tests. (B2 — Gate 1, standard §S3.)
|
|
262
283
|
*/
|
|
263
284
|
sessionId?: string;
|
|
285
|
+
/** Stable id of the real user turn that owns async work launched here. */
|
|
286
|
+
originClientMessageId?: string;
|
|
287
|
+
/**
|
|
288
|
+
* Persist file attribution from an async external DriveAgent completion in
|
|
289
|
+
* the owning CodeShell transcript. The callback closes over the live parent
|
|
290
|
+
* transcript, so a background job can record its files after the run that
|
|
291
|
+
* launched it has already returned.
|
|
292
|
+
*/
|
|
293
|
+
recordExternalFileChanges?: (record: ExternalFileChangesRecord) => void;
|
|
264
294
|
/**
|
|
265
295
|
* Skill names the user has hidden from the LLM (full namespaced names
|
|
266
296
|
* for plugin skills, e.g. "docs:pdf"). The skill builtin tool uses
|
|
@@ -9,7 +9,7 @@ import { logger as rootLogger, getCurrentSid } from "../logging/logger.js";
|
|
|
9
9
|
import { recordToolCall, recordToolResult } from "../logging/session-recorder.js";
|
|
10
10
|
import { validateToolArgs } from "./validation.js";
|
|
11
11
|
import { PLAN_MODE_ALLOWED_TOOLS } from "./plan-mode-allowlist.js";
|
|
12
|
-
import { enforcePathPolicyWithApproval } from "./path-policy.js";
|
|
12
|
+
import { attachFinalWritePathSnapshot, enforcePathPolicyWithApproval, } from "./path-policy.js";
|
|
13
13
|
import { parsePatch } from "./builtin/apply-patch/parser.js";
|
|
14
14
|
import { BUILTIN_TOOL_GUARDS } from "./builtin/index.js";
|
|
15
15
|
import { COMPLETE_GOAL_TOOL_NAME } from "./builtin/complete-goal.js";
|
|
@@ -244,6 +244,7 @@ export class ToolExecutor {
|
|
|
244
244
|
toolName: call.toolName,
|
|
245
245
|
args: call.args,
|
|
246
246
|
toolCallId: call.id,
|
|
247
|
+
signal: this.signal ?? this.toolCtx?.signal,
|
|
247
248
|
});
|
|
248
249
|
if (hookResult.decision === "deny") {
|
|
249
250
|
return {
|
|
@@ -280,6 +281,16 @@ export class ToolExecutor {
|
|
|
280
281
|
// path surface on RegisteredTool.pathPolicy; the executor enforces it
|
|
281
282
|
// after hooks have had a chance to rewrite args, but before permission
|
|
282
283
|
// classification or the handler can touch the filesystem.
|
|
284
|
+
if ((call.toolName === "Write" || call.toolName === "Edit") &&
|
|
285
|
+
this.toolCtx?.cwd &&
|
|
286
|
+
typeof call.args.file_path === "string") {
|
|
287
|
+
const raw = call.args.file_path;
|
|
288
|
+
const target = isAbsolutePath(raw) ? raw : resolvePath(this.toolCtx.cwd, raw);
|
|
289
|
+
call = {
|
|
290
|
+
...call,
|
|
291
|
+
args: attachFinalWritePathSnapshot(call.args, target, this.toolCtx.cwd),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
283
294
|
const pathPolicyError = await this.enforceDeclaredPathPolicy(toolDef, call.args);
|
|
284
295
|
if (pathPolicyError) {
|
|
285
296
|
return {
|
|
@@ -325,6 +336,7 @@ export class ToolExecutor {
|
|
|
325
336
|
args: call.args,
|
|
326
337
|
toolCallId: call.id,
|
|
327
338
|
classifierDecision,
|
|
339
|
+
signal: this.signal ?? this.toolCtx?.signal,
|
|
328
340
|
});
|
|
329
341
|
const preClamped = clampHookDecision(classifierDecision, hookResult.decision);
|
|
330
342
|
let decision = preClamped.decision;
|
|
@@ -383,6 +395,7 @@ export class ToolExecutor {
|
|
|
383
395
|
toolName: call.toolName,
|
|
384
396
|
args: call.args,
|
|
385
397
|
toolCallId: call.id,
|
|
398
|
+
signal: this.signal ?? this.toolCtx?.signal,
|
|
386
399
|
});
|
|
387
400
|
// 3. Execute. Use a span so begin/end share one cat and end carries
|
|
388
401
|
// duration_ms; widen args truncation from 200→2000 so Edit/Write payloads
|
|
@@ -431,25 +444,25 @@ export class ToolExecutor {
|
|
|
431
444
|
result.id = call.id;
|
|
432
445
|
// Prepend any non-blocking guard reminder onto a successful result so the
|
|
433
446
|
// model sees it on its next turn alongside the content it just fetched.
|
|
434
|
-
if (guardDecision?.prepend && !result.
|
|
447
|
+
if (guardDecision?.prepend && !result.isError && result.result) {
|
|
435
448
|
result.result = `${guardDecision.prepend}\n${result.result}`;
|
|
436
449
|
}
|
|
437
450
|
const observerResult = toolResultDisplayText(result);
|
|
438
451
|
const payload = observerResult ?? result.error ?? "";
|
|
439
452
|
span.end({
|
|
440
|
-
ok: !result.
|
|
453
|
+
ok: !result.isError,
|
|
441
454
|
chars: payload.length,
|
|
442
455
|
// Snippet only on failure — successful tool output can be huge and is
|
|
443
456
|
// already on the transcript. Failures are rare and the first 500 chars
|
|
444
457
|
// usually contain the message we need.
|
|
445
|
-
...(result.
|
|
458
|
+
...(result.isError ? { errorSnippet: payload.slice(0, 500) } : {}),
|
|
446
459
|
});
|
|
447
460
|
recordToolResult(sid, {
|
|
448
461
|
id: call.id,
|
|
449
462
|
toolName: call.toolName,
|
|
450
|
-
ok: !result.
|
|
463
|
+
ok: !result.isError,
|
|
451
464
|
durationMs: Date.now() - toolStartedAt,
|
|
452
|
-
output: result.
|
|
465
|
+
output: result.isError ? undefined : observerResult,
|
|
453
466
|
error: result.error,
|
|
454
467
|
});
|
|
455
468
|
// 4. Post-tool hook
|
|
@@ -458,6 +471,7 @@ export class ToolExecutor {
|
|
|
458
471
|
toolCallId: call.id,
|
|
459
472
|
result: observerResult,
|
|
460
473
|
error: result.error,
|
|
474
|
+
signal: this.signal ?? this.toolCtx?.signal,
|
|
461
475
|
});
|
|
462
476
|
// 5. Post-tool-use hook (after execution, can observe/modify result)
|
|
463
477
|
const postHook = await this.hooks.emit("post_tool_use", {
|
|
@@ -465,13 +479,14 @@ export class ToolExecutor {
|
|
|
465
479
|
toolCallId: call.id,
|
|
466
480
|
result: observerResult,
|
|
467
481
|
error: result.error,
|
|
482
|
+
signal: this.signal ?? this.toolCtx?.signal,
|
|
468
483
|
});
|
|
469
484
|
// Append handler-supplied context (linter output, type-check result,
|
|
470
485
|
// etc.) onto the tool result so the model sees it on the next turn.
|
|
471
486
|
// Tagged with a separator so the model can tell hook output from
|
|
472
487
|
// tool output. Skip when the tool errored — additional context on a
|
|
473
488
|
// failed tool would be confusing.
|
|
474
|
-
if (postHook.additionalContext && !result.
|
|
489
|
+
if (postHook.additionalContext && !result.isError) {
|
|
475
490
|
const tag = "--- additional context from post_tool_use hook ---";
|
|
476
491
|
result.result = result.result
|
|
477
492
|
? `${result.result}\n\n${tag}\n${postHook.additionalContext}`
|
|
@@ -484,10 +499,11 @@ export class ToolExecutor {
|
|
|
484
499
|
});
|
|
485
500
|
}
|
|
486
501
|
// 6. file_changed hook for Write/Edit tools
|
|
487
|
-
if ((call.toolName === "Write" || call.toolName === "Edit") && !result.
|
|
502
|
+
if ((call.toolName === "Write" || call.toolName === "Edit") && !result.isError) {
|
|
488
503
|
await this.hooks.emit("file_changed", {
|
|
489
504
|
toolName: call.toolName,
|
|
490
505
|
filePath: call.args.file_path,
|
|
506
|
+
signal: this.signal ?? this.toolCtx?.signal,
|
|
491
507
|
});
|
|
492
508
|
}
|
|
493
509
|
return result;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js";
|
|
7
7
|
import type { MCPServerConfig, RegisteredTool } from "../types.js";
|
|
8
|
+
import type { CredentialType } from "../credentials/types.js";
|
|
8
9
|
import { ToolRegistry } from "./registry.js";
|
|
9
10
|
import { type CredentialAccess } from "../credentials/access.js";
|
|
10
11
|
interface MCPResourceInfo {
|
|
@@ -21,6 +22,16 @@ interface MCPResourceInfo {
|
|
|
21
22
|
* and the env var.
|
|
22
23
|
*/
|
|
23
24
|
export declare function readRequiredEnv(serverName: string, field: string, envName: string): string;
|
|
25
|
+
export interface ResolvedMcpCredential {
|
|
26
|
+
secret: string;
|
|
27
|
+
type?: CredentialType;
|
|
28
|
+
label?: string;
|
|
29
|
+
}
|
|
30
|
+
export type HttpCredentialResolverResult = string | ResolvedMcpCredential | undefined;
|
|
31
|
+
export interface BuildHttpHeadersOptions {
|
|
32
|
+
now?: () => number;
|
|
33
|
+
oauthRefreshSkewMs?: number;
|
|
34
|
+
}
|
|
24
35
|
/**
|
|
25
36
|
* Build the spawned stdio server's environment. Priority (lowest → highest):
|
|
26
37
|
* a minimal inherited allowlist < forwarded `envVars` (read from process.env by
|
|
@@ -34,8 +45,15 @@ export declare function buildStdioEnv(serverName: string, config: MCPServerConfi
|
|
|
34
45
|
* base; env-sourced secrets (`bearerTokenEnvVar`, `envHeaders`) layer on top
|
|
35
46
|
* and win on conflict. Pure + exported for unit testing.
|
|
36
47
|
*/
|
|
37
|
-
export declare function buildHttpHeaders(serverName: string, config: MCPServerConfig, resolveCredential?: (id: string) =>
|
|
38
|
-
export declare function
|
|
48
|
+
export declare function buildHttpHeaders(serverName: string, config: MCPServerConfig, resolveCredential?: (id: string) => HttpCredentialResolverResult, options?: BuildHttpHeadersOptions): Record<string, string>;
|
|
49
|
+
export declare function bearerTokenFromMcpCredential(serverName: string, credentialId: string, credential: ResolvedMcpCredential, options?: BuildHttpHeadersOptions): string;
|
|
50
|
+
export declare function buildHttpHeadersWithCredentialAccess(serverName: string, config: MCPServerConfig, access?: Pick<CredentialAccess, "resolveValue"> & Partial<Pick<CredentialAccess, "resolveMeta">>, options?: BuildHttpHeadersOptions): Promise<Record<string, string>>;
|
|
51
|
+
/**
|
|
52
|
+
* Fetch adapter for long-lived HTTP MCP transports. OAuth access tokens are
|
|
53
|
+
* resolved for every request, refreshed by the host inside the skew window,
|
|
54
|
+
* and force-refreshed once after a replayable 401.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createMcpAuthenticatedFetch(serverName: string, config: MCPServerConfig, access?: CredentialAccess, baseFetch?: typeof fetch): typeof fetch;
|
|
39
57
|
/**
|
|
40
58
|
* Infer the transport when the config doesn't name one: a url-only entry is
|
|
41
59
|
* HTTP, everything else stdio. This is the CC `.mcp.json` convention —
|