@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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Cron tools — CronCreate, CronDelete, CronList.
|
|
3
3
|
*/
|
|
4
4
|
import type { ToolDefinition } from "../../types.js";
|
|
5
|
+
export { cronListToolDef } from "./cron-list.definition.js";
|
|
5
6
|
/** Sink notified after a cron job is created/deleted, so the host (Electron
|
|
6
7
|
* main) can reload+arm the scheduler that actually executes jobs. The worker
|
|
7
8
|
* process only persists cron jobs (executionEnabled=false); without this the
|
|
@@ -12,6 +13,4 @@ export declare const cronCreateToolDef: ToolDefinition;
|
|
|
12
13
|
export declare function cronCreateTool(args: Record<string, unknown>): Promise<string>;
|
|
13
14
|
export declare const cronDeleteToolDef: ToolDefinition;
|
|
14
15
|
export declare function cronDeleteTool(args: Record<string, unknown>): Promise<string>;
|
|
15
|
-
export declare const cronListToolDef: ToolDefinition;
|
|
16
16
|
export declare function cronListTool(_args: Record<string, unknown>): Promise<string>;
|
|
17
|
-
export {};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { cronScheduler } from "../../automation/scheduler.js";
|
|
5
5
|
import { getCurrentSid } from "../../logging/logger.js";
|
|
6
|
+
export { cronListToolDef } from "./cron-list.definition.js";
|
|
6
7
|
let cronChangedSink = null;
|
|
7
8
|
export function setCronChangedSink(sink) {
|
|
8
9
|
cronChangedSink = sink;
|
|
@@ -38,7 +39,10 @@ export const cronCreateToolDef = {
|
|
|
38
39
|
inputSchema: {
|
|
39
40
|
type: "object",
|
|
40
41
|
properties: {
|
|
41
|
-
name: {
|
|
42
|
+
name: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "Short human-readable name for the job (e.g. '工作日晨间简报')",
|
|
45
|
+
},
|
|
42
46
|
schedule: {
|
|
43
47
|
type: "string",
|
|
44
48
|
description: "Interval ('5m','1h','1d') or a 5-field cron expression ('0 9 * * 1-5'). " +
|
|
@@ -49,7 +53,10 @@ export const cronCreateToolDef = {
|
|
|
49
53
|
type: "string",
|
|
50
54
|
description: "IANA timezone for cron-expression schedules (e.g. 'Asia/Shanghai'). Optional; defaults to the host system's timezone.",
|
|
51
55
|
},
|
|
52
|
-
cwd: {
|
|
56
|
+
cwd: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "Working directory / project the job runs in. Optional.",
|
|
59
|
+
},
|
|
53
60
|
permissionLevel: {
|
|
54
61
|
type: "string",
|
|
55
62
|
enum: ["read-only", "workspace-write", "full"],
|
|
@@ -146,11 +153,6 @@ export async function cronDeleteTool(args) {
|
|
|
146
153
|
fireCronChanged();
|
|
147
154
|
return deleted ? `Cron job #${id} deleted.` : `Cron job #${id} not found.`;
|
|
148
155
|
}
|
|
149
|
-
export const cronListToolDef = {
|
|
150
|
-
name: "CronList",
|
|
151
|
-
description: "List all scheduled cron jobs.",
|
|
152
|
-
inputSchema: { type: "object", properties: {} },
|
|
153
|
-
};
|
|
154
156
|
export async function cronListTool(_args) {
|
|
155
157
|
const jobs = cronScheduler.list();
|
|
156
158
|
if (jobs.length === 0)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ToolDefinition } from "../../types.js";
|
|
2
2
|
import type { AgentRunResult } from "../../cc-orchestrator/external-agent-driver.js";
|
|
3
|
+
import { readExternalChangedFiles } from "../../cc-orchestrator/external-agent-changes.js";
|
|
3
4
|
import type { ToolContext } from "../context.js";
|
|
4
5
|
import { type ExternalAgentSessionBinding, type ExternalAgentSessionRecord } from "../../cc-orchestrator/external-agent-session-store.js";
|
|
5
6
|
export type DriveCli = "claude" | "codex";
|
|
@@ -11,6 +12,7 @@ type Runner = (opts: {
|
|
|
11
12
|
cli: DriveCli;
|
|
12
13
|
prompt: string;
|
|
13
14
|
resumeSessionId?: string;
|
|
15
|
+
model?: string;
|
|
14
16
|
cwd: string;
|
|
15
17
|
permissionMode?: PermMode;
|
|
16
18
|
signal?: AbortSignal;
|
|
@@ -23,11 +25,15 @@ type SessionStore = {
|
|
|
23
25
|
export interface DriveAgentToolOptions {
|
|
24
26
|
foregroundHandoffMs?: number;
|
|
25
27
|
sessionStore?: SessionStore;
|
|
28
|
+
/** Test seam for external transcript attribution. */
|
|
29
|
+
readChangedFiles?: typeof readExternalChangedFiles;
|
|
26
30
|
}
|
|
27
31
|
/** Factory so tests can inject a fake runner. `fixedCli` (back-compat) forces a
|
|
28
32
|
* cli and hides the `cli` arg — that's how DriveClaudeCode stays a thin alias. */
|
|
29
33
|
export declare function makeDriveAgentTool(runner?: Runner, fixedCli?: DriveCli, options?: DriveAgentToolOptions): (args: Record<string, unknown>, ctx?: ToolContext) => Promise<string>;
|
|
30
34
|
export declare const driveAgentTool: (args: Record<string, unknown>, ctx?: ToolContext) => Promise<string>;
|
|
35
|
+
export declare const driveAgentJobsToolDef: ToolDefinition;
|
|
36
|
+
export declare function driveAgentJobsTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
31
37
|
export declare const driveClaudeCodeToolDef: ToolDefinition;
|
|
32
38
|
/** Back-compat factory: a DriveAgent pinned to cli:"claude" with the `cli` arg
|
|
33
39
|
* hidden. The injected `runner` here keeps the old shape (no `cli` field); we
|
|
@@ -35,6 +41,7 @@ export declare const driveClaudeCodeToolDef: ToolDefinition;
|
|
|
35
41
|
type LegacyRunner = (opts: {
|
|
36
42
|
prompt: string;
|
|
37
43
|
resumeSessionId?: string;
|
|
44
|
+
model?: string;
|
|
38
45
|
cwd: string;
|
|
39
46
|
permissionMode?: PermMode;
|
|
40
47
|
signal?: AbortSignal;
|
|
@@ -22,7 +22,11 @@ export const driveAgentToolDef = {
|
|
|
22
22
|
"Runs in the BACKGROUND by default — these tasks are typically long (minutes to hours), so this " +
|
|
23
23
|
"returns immediately and the result is delivered to you later via a completion notification " +
|
|
24
24
|
"that wakes you. Do NOT sleep-poll for it; just continue or end your turn — you'll be woken " +
|
|
25
|
-
"with the result.
|
|
25
|
+
"with the result. " +
|
|
26
|
+
"Before launching writable work in a cwd, call DriveAgentJobs(action:'list', cwd) to see any " +
|
|
27
|
+
"already-running DriveAgent jobs there, their prompt summaries, owner sessions, CLI kind, and " +
|
|
28
|
+
"known changed files; use DriveAgentJobs(action:'cancel', jobId) if you must stop one. " +
|
|
29
|
+
"For a quick task where you want the answer inline, pass background:false. " +
|
|
26
30
|
"It has NO time concept of its own: for 'in N minutes' / 'every N' / looping, use CronCreate " +
|
|
27
31
|
"instead (never sleep). A scheduled CronCreate job runs one codeshell turn whose prompt can " +
|
|
28
32
|
"instruct it to call DriveAgent; to continue a prior session across runs, have that turn pass " +
|
|
@@ -55,6 +59,10 @@ export const driveAgentToolDef = {
|
|
|
55
59
|
type: "string",
|
|
56
60
|
description: "Existing session id to resume (keeps context). Must come from a prior run of the SAME cli. Omit for a fresh session.",
|
|
57
61
|
},
|
|
62
|
+
model: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Optional model override, passed through to `claude --model` / `codex exec --model`. Omit to use the CLI default; only pass when the user explicitly requests a model.",
|
|
65
|
+
},
|
|
58
66
|
cwd: { type: "string", description: "Working directory the run operates in." },
|
|
59
67
|
attachmentPaths: {
|
|
60
68
|
type: "array",
|
|
@@ -80,6 +88,7 @@ const defaultRunner = (opts) => {
|
|
|
80
88
|
command,
|
|
81
89
|
prompt: opts.prompt,
|
|
82
90
|
resumeSessionId: opts.resumeSessionId,
|
|
91
|
+
model: opts.model,
|
|
83
92
|
cwd: opts.cwd,
|
|
84
93
|
permissionMode: opts.permissionMode ?? "default",
|
|
85
94
|
imagePaths: opts.imagePaths,
|
|
@@ -88,6 +97,64 @@ const defaultRunner = (opts) => {
|
|
|
88
97
|
function newDriveJobId() {
|
|
89
98
|
return `cc-${process.hrtime.bigint().toString(36)}`;
|
|
90
99
|
}
|
|
100
|
+
function summarizePrompt(prompt, max = 120) {
|
|
101
|
+
const oneLine = prompt.replace(/\s+/g, " ").trim();
|
|
102
|
+
if (oneLine.length <= max)
|
|
103
|
+
return oneLine;
|
|
104
|
+
return `${oneLine.slice(0, Math.max(0, max - 3))}...`;
|
|
105
|
+
}
|
|
106
|
+
function isDriveAgentJob(job) {
|
|
107
|
+
return job.kind === "drive-agent" || job.description.startsWith("DriveAgent(");
|
|
108
|
+
}
|
|
109
|
+
function changedFilesSummary(job) {
|
|
110
|
+
const files = job.changedFiles ?? [];
|
|
111
|
+
if (files.length === 0)
|
|
112
|
+
return "unknown";
|
|
113
|
+
if (files.length <= 4)
|
|
114
|
+
return files.join(",");
|
|
115
|
+
return `${files.slice(0, 4).join(",")} (+${files.length - 4} more)`;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Canonicalize external transcript paths against the DriveAgent cwd. Claude
|
|
119
|
+
* normally records absolute paths while Codex apply_patch normally records
|
|
120
|
+
* relative paths; returning a cwd-relative display path for in-workspace files
|
|
121
|
+
* gives the renderer one stable identity and removes duplicate transcript hits.
|
|
122
|
+
*/
|
|
123
|
+
function normalizeChangedFiles(cwd, files) {
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
if (typeof file !== "string" || !file.trim())
|
|
128
|
+
continue;
|
|
129
|
+
const absolute = resolve(cwd, file.trim());
|
|
130
|
+
if (seen.has(absolute))
|
|
131
|
+
continue;
|
|
132
|
+
seen.add(absolute);
|
|
133
|
+
const rel = relative(cwd, absolute);
|
|
134
|
+
out.push(rel && rel !== ".." && !rel.startsWith(`..${sep}`) ? rel : absolute);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
function jobDurationSeconds(job) {
|
|
139
|
+
const end = job.finishedAt ?? Date.now();
|
|
140
|
+
return `${Math.max(0, (end - job.startedAt) / 1000).toFixed(1)}s`;
|
|
141
|
+
}
|
|
142
|
+
function formatDriveJobListLine(job) {
|
|
143
|
+
const prompt = job.promptSummary || job.description || "(no prompt summary)";
|
|
144
|
+
const cwd = job.cwd ?? "(unknown cwd)";
|
|
145
|
+
const cli = job.cli ?? "unknown";
|
|
146
|
+
return [
|
|
147
|
+
`${job.jobId}`,
|
|
148
|
+
`status=${job.status}`,
|
|
149
|
+
`cli=${cli}`,
|
|
150
|
+
`session=${job.sessionId}`,
|
|
151
|
+
`cwd=${cwd}`,
|
|
152
|
+
`startedAt=${new Date(job.startedAt).toISOString()}`,
|
|
153
|
+
`duration=${jobDurationSeconds(job)}`,
|
|
154
|
+
`changedFiles=${changedFilesSummary(job)}`,
|
|
155
|
+
`prompt="${prompt}"`,
|
|
156
|
+
].join(" ");
|
|
157
|
+
}
|
|
91
158
|
function isValidSessionId(sessionId) {
|
|
92
159
|
return typeof sessionId === "string" && sessionId.length > 0;
|
|
93
160
|
}
|
|
@@ -140,8 +207,8 @@ function appendAttachmentPrompt(prompt, paths) {
|
|
|
140
207
|
}
|
|
141
208
|
return `${prompt}\n${lines.join("\n")}`;
|
|
142
209
|
}
|
|
143
|
-
function recordSuccessfulSession(store, cli, cwd, result) {
|
|
144
|
-
if (result.isError || !result.sessionId)
|
|
210
|
+
function recordSuccessfulSession(store, cli, cwd, result, includeErroredSession = false) {
|
|
211
|
+
if ((!includeErroredSession && result.isError) || !result.sessionId)
|
|
145
212
|
return;
|
|
146
213
|
try {
|
|
147
214
|
store.record({ cli, sessionId: result.sessionId, cwd });
|
|
@@ -159,17 +226,60 @@ function recordSuccessfulSession(store, cli, cwd, result) {
|
|
|
159
226
|
function duplicateCwdWarning(cwd, writable) {
|
|
160
227
|
if (!writable)
|
|
161
228
|
return undefined;
|
|
162
|
-
const running = backgroundJobRegistry.listRunningByCwd(cwd);
|
|
229
|
+
const running = backgroundJobRegistry.listRunningByCwd(cwd).filter(isDriveAgentJob);
|
|
163
230
|
if (running.length === 0)
|
|
164
231
|
return undefined;
|
|
165
|
-
const
|
|
166
|
-
return `Warning: another DriveAgent job is already running in cwd ${cwd}
|
|
232
|
+
const jobs = running.map(formatDriveJobListLine).join("; ");
|
|
233
|
+
return (`Warning: another DriveAgent job is already running in cwd ${cwd}. ` +
|
|
234
|
+
"Concurrent writable agents in the same directory can overwrite each other's work. " +
|
|
235
|
+
`Run DriveAgentJobs(action:"list", cwd:"${cwd}") before dispatching parallel work for details/cancellation. ` +
|
|
236
|
+
`Running: ${jobs}`);
|
|
167
237
|
}
|
|
168
238
|
function attachDriveCompletion(params) {
|
|
169
|
-
const { jobId, sessionId, label, cli, cwd, run, sessionStore } = params;
|
|
239
|
+
const { jobId, sessionId, label, cli, cwd, run, sessionStore, readChangedFiles, recordExternalFileChanges, originClientMessageId, } = params;
|
|
170
240
|
void run
|
|
171
241
|
.then((r) => {
|
|
172
|
-
|
|
242
|
+
const jobStatus = backgroundJobRegistry.get(jobId)?.status;
|
|
243
|
+
if (jobStatus !== "running" && jobStatus !== "cancelling")
|
|
244
|
+
return;
|
|
245
|
+
const cancelling = jobStatus === "cancelling";
|
|
246
|
+
recordSuccessfulSession(sessionStore, cli, cwd, r, cancelling);
|
|
247
|
+
// Attribute external changes BEFORE publishing completion. Previously the
|
|
248
|
+
// notification event was emitted first and carried no files; changedFiles
|
|
249
|
+
// only reached the background-work registry/panel, so the chat turn card
|
|
250
|
+
// could never count DriveAgent edits.
|
|
251
|
+
const rawChangedFiles = r.sessionId ? readChangedFiles(cli, cwd, r.sessionId) : [];
|
|
252
|
+
const changedFiles = normalizeChangedFiles(cwd, rawChangedFiles);
|
|
253
|
+
if (changedFiles.length > 0) {
|
|
254
|
+
recordExternalFileChanges?.({
|
|
255
|
+
jobId,
|
|
256
|
+
description: label,
|
|
257
|
+
cli,
|
|
258
|
+
cwd,
|
|
259
|
+
status: cancelling ? "cancelled" : r.isError ? "failed" : "completed",
|
|
260
|
+
changedFiles,
|
|
261
|
+
...(originClientMessageId ? { originClientMessageId } : {}),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
logger.debug("changed_files.drive_completion", {
|
|
265
|
+
cat: "changed_files",
|
|
266
|
+
jobId,
|
|
267
|
+
sessionId,
|
|
268
|
+
externalSessionId: r.sessionId || undefined,
|
|
269
|
+
cli,
|
|
270
|
+
cwd,
|
|
271
|
+
originClientMessageId,
|
|
272
|
+
rawSize: rawChangedFiles.length,
|
|
273
|
+
size: changedFiles.length,
|
|
274
|
+
files: changedFiles,
|
|
275
|
+
});
|
|
276
|
+
if (cancelling) {
|
|
277
|
+
backgroundJobRegistry.recordArtifacts(jobId, {
|
|
278
|
+
ccSessionId: r.sessionId || undefined,
|
|
279
|
+
changedFiles,
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
173
283
|
// Deliver the result back so the woken agent actually sees the answer
|
|
174
284
|
// (not just "a job finished"). Mirrors the video/sub-agent completion
|
|
175
285
|
// path — enqueue lands in the same notificationQueue the wakeup drains.
|
|
@@ -181,6 +291,8 @@ function attachDriveCompletion(params) {
|
|
|
181
291
|
workKind: "cc",
|
|
182
292
|
error: r.finalText || "(no output)",
|
|
183
293
|
ccSessionId: r.sessionId || undefined,
|
|
294
|
+
...(changedFiles.length ? { changedFiles, cwd } : {}),
|
|
295
|
+
...(originClientMessageId ? { originClientMessageId } : {}),
|
|
184
296
|
enqueuedAt: Date.now(),
|
|
185
297
|
}
|
|
186
298
|
: {
|
|
@@ -190,12 +302,10 @@ function attachDriveCompletion(params) {
|
|
|
190
302
|
workKind: "cc",
|
|
191
303
|
finalText: r.finalText,
|
|
192
304
|
ccSessionId: r.sessionId || undefined,
|
|
305
|
+
...(changedFiles.length ? { changedFiles, cwd } : {}),
|
|
306
|
+
...(originClientMessageId ? { originClientMessageId } : {}),
|
|
193
307
|
enqueuedAt: Date.now(),
|
|
194
308
|
}, sessionId);
|
|
195
|
-
// Attribute the files the external agent changed by parsing its own
|
|
196
|
-
// transcript (#6) — those Edit/Write calls are invisible to the host's
|
|
197
|
-
// in-session aggregator. Best-effort; [] on any failure.
|
|
198
|
-
const changedFiles = r.sessionId ? readExternalChangedFiles(cli, cwd, r.sessionId) : [];
|
|
199
309
|
// Retain the job in the panel with its result + the external CLI
|
|
200
310
|
// session id + changed files.
|
|
201
311
|
backgroundJobRegistry.finish(jobId, {
|
|
@@ -206,6 +316,8 @@ function attachDriveCompletion(params) {
|
|
|
206
316
|
});
|
|
207
317
|
})
|
|
208
318
|
.catch((err) => {
|
|
319
|
+
if (backgroundJobRegistry.get(jobId)?.status !== "running")
|
|
320
|
+
return;
|
|
209
321
|
const msg = err?.message ?? String(err);
|
|
210
322
|
notificationQueue.enqueue({
|
|
211
323
|
agentId: jobId,
|
|
@@ -221,8 +333,19 @@ function attachDriveCompletion(params) {
|
|
|
221
333
|
function trackBackgroundRun(params) {
|
|
222
334
|
const warning = duplicateCwdWarning(params.cwd, params.writable);
|
|
223
335
|
const jobId = newDriveJobId();
|
|
224
|
-
|
|
225
|
-
|
|
336
|
+
const run = params.start();
|
|
337
|
+
backgroundJobRegistry.start(jobId, params.sessionId, params.label, {
|
|
338
|
+
kind: "drive-agent",
|
|
339
|
+
cwd: params.cwd,
|
|
340
|
+
cli: params.cli,
|
|
341
|
+
promptSummary: params.promptSummary,
|
|
342
|
+
originClientMessageId: params.originClientMessageId,
|
|
343
|
+
abort: async () => {
|
|
344
|
+
params.abort();
|
|
345
|
+
await run.catch(() => undefined);
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
attachDriveCompletion({ ...params, jobId, run });
|
|
226
349
|
return { jobId, ...(warning ? { warning } : {}) };
|
|
227
350
|
}
|
|
228
351
|
async function waitForForegroundOrHandoff(run, handoffMs) {
|
|
@@ -243,6 +366,18 @@ async function waitForForegroundOrHandoff(run, handoffMs) {
|
|
|
243
366
|
clearTimeout(timer);
|
|
244
367
|
}
|
|
245
368
|
}
|
|
369
|
+
function makeAbortController(parent, linkParent = false) {
|
|
370
|
+
const controller = new AbortController();
|
|
371
|
+
if (!parent)
|
|
372
|
+
return controller;
|
|
373
|
+
if (parent.aborted) {
|
|
374
|
+
controller.abort(parent.reason);
|
|
375
|
+
}
|
|
376
|
+
else if (linkParent) {
|
|
377
|
+
parent.addEventListener("abort", () => controller.abort(parent.reason), { once: true });
|
|
378
|
+
}
|
|
379
|
+
return controller;
|
|
380
|
+
}
|
|
246
381
|
/** Factory so tests can inject a fake runner. `fixedCli` (back-compat) forces a
|
|
247
382
|
* cli and hides the `cli` arg — that's how DriveClaudeCode stays a thin alias. */
|
|
248
383
|
export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {}) {
|
|
@@ -261,6 +396,7 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
261
396
|
if (cli === "invalid")
|
|
262
397
|
return `Error: unknown cli "${String(args.cli)}" (expected "claude" or "codex")`;
|
|
263
398
|
const resumeSessionId = typeof args.resumeSessionId === "string" ? args.resumeSessionId : undefined;
|
|
399
|
+
const model = typeof args.model === "string" && args.model.trim() ? args.model : undefined;
|
|
264
400
|
const sessionStore = options.sessionStore ?? externalAgentSessionStore;
|
|
265
401
|
let cwd = requestedCwd;
|
|
266
402
|
let resumeNote = "";
|
|
@@ -306,13 +442,15 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
306
442
|
: [];
|
|
307
443
|
const cliName = cli === "codex" ? "Codex" : "Claude Code";
|
|
308
444
|
const label = `DriveAgent(${cli}): ${prompt.slice(0, 40)}`;
|
|
309
|
-
const
|
|
445
|
+
const promptSummary = summarizePrompt(prompt);
|
|
446
|
+
const callerSignal = ctx?.signal ?? argSignal(args);
|
|
447
|
+
const runOptsBase = {
|
|
310
448
|
cli,
|
|
311
449
|
prompt: promptWithAttachments,
|
|
312
450
|
resumeSessionId,
|
|
451
|
+
model,
|
|
313
452
|
cwd,
|
|
314
453
|
permissionMode,
|
|
315
|
-
signal: ctx?.signal ?? argSignal(args),
|
|
316
454
|
imagePaths,
|
|
317
455
|
};
|
|
318
456
|
const foregroundHandoffMs = options.foregroundHandoffMs ?? DRIVE_AGENT_FOREGROUND_HANDOFF_MS;
|
|
@@ -329,14 +467,20 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
329
467
|
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
330
468
|
return `Error: cannot start a background ${cliName} job without a session — its result notification would be dropped. Retry with background:false, or ensure the tool runs inside a session.`;
|
|
331
469
|
}
|
|
470
|
+
const abortController = makeAbortController(callerSignal, false);
|
|
332
471
|
const tracked = trackBackgroundRun({
|
|
333
472
|
sessionId,
|
|
334
473
|
label,
|
|
335
474
|
cli,
|
|
336
475
|
cwd,
|
|
337
|
-
|
|
476
|
+
promptSummary,
|
|
477
|
+
start: () => startRun(runner, { ...runOptsBase, signal: abortController.signal }),
|
|
478
|
+
abort: () => abortController.abort(),
|
|
338
479
|
sessionStore,
|
|
339
480
|
writable: isWritableRun,
|
|
481
|
+
readChangedFiles: options.readChangedFiles ?? readExternalChangedFiles,
|
|
482
|
+
recordExternalFileChanges: ctx?.recordExternalFileChanges,
|
|
483
|
+
originClientMessageId: ctx?.originClientMessageId,
|
|
340
484
|
});
|
|
341
485
|
return [
|
|
342
486
|
resumeNote,
|
|
@@ -346,7 +490,8 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
346
490
|
.filter(Boolean)
|
|
347
491
|
.join("\n");
|
|
348
492
|
}
|
|
349
|
-
const
|
|
493
|
+
const foregroundAbort = makeAbortController(callerSignal, true);
|
|
494
|
+
const run = startRun(runner, { ...runOptsBase, signal: foregroundAbort.signal });
|
|
350
495
|
const result = await waitForForegroundOrHandoff(run, foregroundHandoffMs);
|
|
351
496
|
if (result.kind === "handoff" && isValidSessionId(ctx?.sessionId)) {
|
|
352
497
|
const tracked = trackBackgroundRun({
|
|
@@ -354,9 +499,14 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
354
499
|
label,
|
|
355
500
|
cli,
|
|
356
501
|
cwd,
|
|
357
|
-
|
|
502
|
+
promptSummary,
|
|
503
|
+
start: () => run,
|
|
504
|
+
abort: () => foregroundAbort.abort(),
|
|
358
505
|
sessionStore,
|
|
359
506
|
writable: isWritableRun,
|
|
507
|
+
readChangedFiles: options.readChangedFiles ?? readExternalChangedFiles,
|
|
508
|
+
recordExternalFileChanges: ctx?.recordExternalFileChanges,
|
|
509
|
+
originClientMessageId: ctx?.originClientMessageId,
|
|
360
510
|
});
|
|
361
511
|
return [
|
|
362
512
|
resumeNote,
|
|
@@ -375,6 +525,142 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
375
525
|
};
|
|
376
526
|
}
|
|
377
527
|
export const driveAgentTool = makeDriveAgentTool();
|
|
528
|
+
export const driveAgentJobsToolDef = {
|
|
529
|
+
name: "DriveAgentJobs",
|
|
530
|
+
description: "List, inspect, or cancel background DriveAgent jobs. Use action:'list' before launching " +
|
|
531
|
+
"writable DriveAgent work in a cwd to see already-running jobs there, including prompt " +
|
|
532
|
+
"summary, owner session, cwd, CLI kind, status, start time, and known changed files. " +
|
|
533
|
+
"Use action:'inspect' with jobId for full details, or action:'cancel' with jobId to abort " +
|
|
534
|
+
"a running DriveAgent external CLI process and deliver a cancellation notification.",
|
|
535
|
+
inputSchema: {
|
|
536
|
+
type: "object",
|
|
537
|
+
properties: {
|
|
538
|
+
action: {
|
|
539
|
+
type: "string",
|
|
540
|
+
enum: ["list", "inspect", "cancel"],
|
|
541
|
+
description: "What to do. Defaults to list.",
|
|
542
|
+
},
|
|
543
|
+
jobId: {
|
|
544
|
+
type: "string",
|
|
545
|
+
description: "DriveAgent jobId for inspect or cancel.",
|
|
546
|
+
},
|
|
547
|
+
cwd: {
|
|
548
|
+
type: "string",
|
|
549
|
+
description: "When listing, restrict to DriveAgent jobs in this cwd. This is cross-session so it can catch same-directory write conflicts before dispatch.",
|
|
550
|
+
},
|
|
551
|
+
status: {
|
|
552
|
+
type: "string",
|
|
553
|
+
enum: ["running", "all"],
|
|
554
|
+
description: "When listing: running (default) or all retained DriveAgent jobs.",
|
|
555
|
+
},
|
|
556
|
+
all: {
|
|
557
|
+
type: "boolean",
|
|
558
|
+
description: "When listing without cwd: true lists DriveAgent jobs from every session; default lists only this session when session context exists.",
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
function jobIdArg(args) {
|
|
564
|
+
const camel = typeof args.jobId === "string" ? args.jobId.trim() : "";
|
|
565
|
+
if (camel)
|
|
566
|
+
return camel;
|
|
567
|
+
const snake = typeof args.job_id === "string" ? args.job_id.trim() : "";
|
|
568
|
+
return snake || undefined;
|
|
569
|
+
}
|
|
570
|
+
function listDriveAgentJobs(args, ctx) {
|
|
571
|
+
const rawCwd = typeof args.cwd === "string" && args.cwd.trim() ? args.cwd : undefined;
|
|
572
|
+
const cwd = rawCwd ? normalizeCwdPath(rawCwd) : undefined;
|
|
573
|
+
const sessionId = ctx?.sessionId;
|
|
574
|
+
const listAllSessions = args.all === true || !!cwd || !sessionId;
|
|
575
|
+
const status = args.status === "all" ? "all" : "running";
|
|
576
|
+
let jobs = listAllSessions || !sessionId
|
|
577
|
+
? backgroundJobRegistry.list()
|
|
578
|
+
: backgroundJobRegistry.listForSession(sessionId);
|
|
579
|
+
jobs = jobs.filter(isDriveAgentJob);
|
|
580
|
+
if (cwd)
|
|
581
|
+
jobs = jobs.filter((job) => job.cwd === cwd);
|
|
582
|
+
if (status === "running") {
|
|
583
|
+
jobs = jobs.filter((job) => job.status === "running" || job.status === "cancelling");
|
|
584
|
+
}
|
|
585
|
+
if (jobs.length === 0) {
|
|
586
|
+
if (cwd)
|
|
587
|
+
return `No ${status} DriveAgent jobs in cwd ${cwd}.`;
|
|
588
|
+
if (listAllSessions)
|
|
589
|
+
return `No ${status} DriveAgent jobs in this process.`;
|
|
590
|
+
return `No ${status} DriveAgent jobs in this session.`;
|
|
591
|
+
}
|
|
592
|
+
return jobs.map(formatDriveJobListLine).join("\n");
|
|
593
|
+
}
|
|
594
|
+
function inspectDriveAgentJob(jobId) {
|
|
595
|
+
if (!jobId)
|
|
596
|
+
return "Error: jobId is required.";
|
|
597
|
+
const job = backgroundJobRegistry.get(jobId);
|
|
598
|
+
if (!job || !isDriveAgentJob(job))
|
|
599
|
+
return `Error: DriveAgent jobId "${jobId}" not found.`;
|
|
600
|
+
const lines = [
|
|
601
|
+
`jobId: ${job.jobId}`,
|
|
602
|
+
`status: ${job.status}`,
|
|
603
|
+
`cli: ${job.cli ?? "unknown"}`,
|
|
604
|
+
`session: ${job.sessionId}`,
|
|
605
|
+
`cwd: ${job.cwd ?? "(unknown cwd)"}`,
|
|
606
|
+
`startedAt: ${new Date(job.startedAt).toISOString()}`,
|
|
607
|
+
`duration: ${jobDurationSeconds(job)}`,
|
|
608
|
+
`prompt: ${job.promptSummary || job.description || "(no prompt summary)"}`,
|
|
609
|
+
`description: ${job.description}`,
|
|
610
|
+
];
|
|
611
|
+
if (job.finishedAt !== undefined)
|
|
612
|
+
lines.push(`finishedAt: ${new Date(job.finishedAt).toISOString()}`);
|
|
613
|
+
if (job.ccSessionId)
|
|
614
|
+
lines.push(`ccSessionId: ${job.ccSessionId}`);
|
|
615
|
+
if (job.changedFiles && job.changedFiles.length > 0) {
|
|
616
|
+
lines.push("changedFiles:", ...job.changedFiles.map((file) => `- ${file}`));
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
lines.push("changedFiles: unknown");
|
|
620
|
+
}
|
|
621
|
+
if (job.finalText)
|
|
622
|
+
lines.push("finalText:", job.finalText);
|
|
623
|
+
return lines.join("\n");
|
|
624
|
+
}
|
|
625
|
+
async function cancelDriveAgentJob(jobId) {
|
|
626
|
+
if (!jobId)
|
|
627
|
+
return "Error: jobId is required.";
|
|
628
|
+
const job = backgroundJobRegistry.get(jobId);
|
|
629
|
+
if (!job || !isDriveAgentJob(job))
|
|
630
|
+
return `Error: DriveAgent jobId "${jobId}" not found.`;
|
|
631
|
+
if (job.status !== "running") {
|
|
632
|
+
return `DriveAgent job ${jobId} is already ${job.status}; nothing to cancel.`;
|
|
633
|
+
}
|
|
634
|
+
if (!job.abort) {
|
|
635
|
+
return `Error: DriveAgent job ${jobId} has no cancellation handle recorded.`;
|
|
636
|
+
}
|
|
637
|
+
const finalText = `DriveAgent job ${jobId} cancelled by DriveAgentJobs.`;
|
|
638
|
+
const ok = await backgroundJobRegistry.cancel(jobId, { finalText });
|
|
639
|
+
if (!ok)
|
|
640
|
+
return `Failed to cancel DriveAgent job ${jobId}.`;
|
|
641
|
+
notificationQueue.enqueue({
|
|
642
|
+
agentId: jobId,
|
|
643
|
+
description: job.description,
|
|
644
|
+
status: "cancelled",
|
|
645
|
+
workKind: "cc",
|
|
646
|
+
error: finalText,
|
|
647
|
+
ccSessionId: job.ccSessionId,
|
|
648
|
+
...(job.changedFiles?.length ? { changedFiles: job.changedFiles, cwd: job.cwd } : {}),
|
|
649
|
+
...(job.originClientMessageId ? { originClientMessageId: job.originClientMessageId } : {}),
|
|
650
|
+
enqueuedAt: Date.now(),
|
|
651
|
+
}, job.sessionId);
|
|
652
|
+
return `DriveAgent job ${jobId} cancelled.`;
|
|
653
|
+
}
|
|
654
|
+
export async function driveAgentJobsTool(args, ctx) {
|
|
655
|
+
const action = args.action === "inspect" || args.action === "cancel" || args.action === "list"
|
|
656
|
+
? args.action
|
|
657
|
+
: "list";
|
|
658
|
+
if (action === "inspect")
|
|
659
|
+
return inspectDriveAgentJob(jobIdArg(args));
|
|
660
|
+
if (action === "cancel")
|
|
661
|
+
return await cancelDriveAgentJob(jobIdArg(args));
|
|
662
|
+
return listDriveAgentJobs(args, ctx);
|
|
663
|
+
}
|
|
378
664
|
// ── Back-compat: DriveClaudeCode = DriveAgent pinned to cli:"claude" ──────────
|
|
379
665
|
// Kept so old prompts / memories / call sites that reference DriveClaudeCode
|
|
380
666
|
// keep working. It's a thin alias over the same machinery (fixedCli "claude").
|
|
@@ -394,6 +680,7 @@ export const driveClaudeCodeToolDef = {
|
|
|
394
680
|
type: "string",
|
|
395
681
|
description: "Existing CC session id to resume (keeps context). Omit for a fresh session.",
|
|
396
682
|
},
|
|
683
|
+
model: driveAgentToolDef.inputSchema.properties.model,
|
|
397
684
|
cwd: driveAgentToolDef.inputSchema.properties.cwd,
|
|
398
685
|
attachmentPaths: driveAgentToolDef.inputSchema.properties.attachmentPaths,
|
|
399
686
|
permissionMode: driveAgentToolDef.inputSchema.properties.permissionMode,
|
|
@@ -404,7 +691,7 @@ export const driveClaudeCodeToolDef = {
|
|
|
404
691
|
};
|
|
405
692
|
export function makeDriveClaudeCodeTool(runner, options) {
|
|
406
693
|
const generic = runner
|
|
407
|
-
? ({ prompt, resumeSessionId, cwd, permissionMode, signal }) => runner({ prompt, resumeSessionId, cwd, permissionMode, signal })
|
|
694
|
+
? ({ prompt, resumeSessionId, model, cwd, permissionMode, signal }) => runner({ prompt, resumeSessionId, model, cwd, permissionMode, signal })
|
|
408
695
|
: undefined;
|
|
409
696
|
return makeDriveAgentTool(generic ?? defaultRunner, "claude", options);
|
|
410
697
|
}
|
|
@@ -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 editToolDef: ToolDefinition;
|
|
7
|
-
export declare function editTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
8
|
+
export declare function editTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string | ToolFailure>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in Edit file tool — exact string replacement.
|
|
3
3
|
*/
|
|
4
|
-
import { readFile
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import { isAbsolute, resolve } from "node:path";
|
|
7
7
|
import { fileCache } from "./file-cache.js";
|
|
8
8
|
import { detectEol, toLf, applyEol } from "./eol.js";
|
|
9
|
+
import { getFinalWritePathSnapshot, revalidateFinalWritePath, writeFileNoFollow, } from "../path-policy.js";
|
|
9
10
|
export const editToolDef = {
|
|
10
11
|
name: "Edit",
|
|
11
12
|
description: "Perform exact string replacements in a file. " +
|
|
@@ -50,7 +51,11 @@ export async function editTool(args, ctx) {
|
|
|
50
51
|
if (!existsSync(filePath))
|
|
51
52
|
return `Error: File not found: ${filePath}`;
|
|
52
53
|
try {
|
|
53
|
-
const
|
|
54
|
+
const approvedPath = getFinalWritePathSnapshot(args, filePath, cwd);
|
|
55
|
+
const beforeRead = revalidateFinalWritePath(filePath, cwd, approvedPath);
|
|
56
|
+
if ("error" in beforeRead)
|
|
57
|
+
return { ok: false, error: beforeRead.error };
|
|
58
|
+
const raw = await readFile(beforeRead.resolvedPath, "utf-8");
|
|
54
59
|
// CRLF handling: detect the file's line ending, then match/replace in LF
|
|
55
60
|
// space (the model emits LF, so a CRLF file would never match otherwise),
|
|
56
61
|
// and write back in the ORIGINAL EOL so a CRLF file stays CRLF (no whole-
|
|
@@ -72,7 +77,10 @@ export async function editTool(args, ctx) {
|
|
|
72
77
|
const updatedLf = replaceAll
|
|
73
78
|
? content.split(oldLf).join(newLf)
|
|
74
79
|
: content.replace(oldLf, newLf);
|
|
75
|
-
|
|
80
|
+
const beforeWrite = revalidateFinalWritePath(filePath, cwd, approvedPath);
|
|
81
|
+
if ("error" in beforeWrite)
|
|
82
|
+
return { ok: false, error: beforeWrite.error };
|
|
83
|
+
await writeFileNoFollow(beforeWrite.resolvedPath, applyEol(updatedLf, eol));
|
|
76
84
|
fileCache.invalidate(filePath);
|
|
77
85
|
const count = replaceAll
|
|
78
86
|
? (content.split(oldLf).length - 1)
|
|
@@ -84,7 +92,7 @@ export async function editTool(args, ctx) {
|
|
|
84
92
|
return `Successfully edited ${filePath} (${count} replacement${count > 1 ? "s" : ""})\n${diffSummary}`;
|
|
85
93
|
}
|
|
86
94
|
catch (err) {
|
|
87
|
-
return `Error editing file: ${err.message}
|
|
95
|
+
return { ok: false, error: `Error editing file: ${err.message}` };
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
function generateCompactDiff(oldLines, newLines, filePath) {
|
|
@@ -32,6 +32,10 @@ export declare function __normalizeImagesForTests(images: string[] | undefined,
|
|
|
32
32
|
ok: false;
|
|
33
33
|
error: string;
|
|
34
34
|
}>;
|
|
35
|
+
export declare function __setVideoPollingLimitsForTests(limits: {
|
|
36
|
+
maxPollMs: number;
|
|
37
|
+
requestTimeoutMs: number;
|
|
38
|
+
} | null): void;
|
|
35
39
|
export declare const generateVideoToolDef: ToolDefinition;
|
|
36
40
|
export declare function __setVideoProviderForTests(p: VideoProvider | null): void;
|
|
37
41
|
export declare function isGenerateVideoAvailable(cwd?: string, nowMs?: number): boolean;
|