@cjhyy/code-shell-core 0.6.0-rc.12 → 0.6.0-rc.14
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/cc-orchestrator/cwd-normalize.d.ts +2 -0
- package/dist/cc-orchestrator/cwd-normalize.js +19 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +144 -0
- package/dist/context/compaction.d.ts +8 -1
- package/dist/context/compaction.js +49 -4
- package/dist/context/manager.d.ts +3 -1
- package/dist/context/manager.js +24 -15
- package/dist/credentials/types.d.ts +2 -2
- package/dist/engine/engine.d.ts +5 -1
- package/dist/engine/engine.js +92 -51
- package/dist/engine/turn-loop.js +2 -2
- package/dist/git/worktree.d.ts +49 -6
- package/dist/git/worktree.js +265 -31
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/logging/logger.js +6 -6
- package/dist/plugins/installer/installFromSource.js +9 -1
- package/dist/plugins/installer/sourcePath.d.ts +9 -0
- package/dist/plugins/installer/sourcePath.js +50 -0
- package/dist/plugins/pluginInstaller.js +24 -22
- package/dist/protocol/chat-session-manager.d.ts +1 -0
- package/dist/protocol/chat-session-manager.js +13 -0
- package/dist/protocol/server.d.ts +8 -0
- package/dist/protocol/server.js +45 -8
- package/dist/protocol/types.d.ts +4 -0
- package/dist/protocol/types.js +4 -0
- package/dist/run/FileRunStore.js +10 -1
- package/dist/run/Heartbeat.js +12 -0
- package/dist/run/RunApprovalBackend.d.ts +3 -0
- package/dist/run/RunApprovalBackend.js +41 -6
- package/dist/run/RunLock.js +2 -0
- package/dist/run/RunManager.d.ts +2 -0
- package/dist/run/RunManager.js +64 -24
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +23 -0
- package/dist/session/session-manager.d.ts +35 -1
- package/dist/session/session-manager.js +189 -2
- package/dist/session/transcript.d.ts +1 -1
- package/dist/session/transcript.js +17 -5
- package/dist/settings/manager.d.ts +1 -0
- package/dist/settings/manager.js +45 -26
- package/dist/settings/schema-export.d.ts +2 -3
- package/dist/settings/schema-export.js +2 -3
- package/dist/tool-system/builtin/background-jobs.d.ts +8 -1
- package/dist/tool-system/builtin/background-jobs.js +8 -1
- package/dist/tool-system/builtin/config.d.ts +2 -1
- package/dist/tool-system/builtin/config.js +16 -11
- package/dist/tool-system/builtin/drive-claude-code.d.ts +15 -2
- package/dist/tool-system/builtin/drive-claude-code.js +174 -39
- package/dist/tool-system/builtin/edit.js +5 -2
- package/dist/tool-system/builtin/generate-video.d.ts +1 -0
- package/dist/tool-system/builtin/generate-video.js +13 -4
- package/dist/tool-system/builtin/index.js +5 -1
- package/dist/tool-system/builtin/lsp.d.ts +2 -1
- package/dist/tool-system/builtin/lsp.js +6 -3
- package/dist/tool-system/builtin/notebook-edit.js +5 -2
- package/dist/tool-system/builtin/read.js +5 -2
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/view-image.js +100 -19
- package/dist/tool-system/builtin/worktree.d.ts +2 -4
- package/dist/tool-system/builtin/worktree.js +250 -75
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +12 -0
- package/dist/tool-system/mcp-manager.d.ts +8 -0
- package/dist/tool-system/mcp-manager.js +32 -11
- package/dist/types.d.ts +12 -0
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,12 @@ import { runAgentOnce } from "../../cc-orchestrator/external-agent-driver.js";
|
|
|
2
2
|
import { claudeAdapter, codexAdapter } from "../../cc-orchestrator/agent-adapter.js";
|
|
3
3
|
import { backgroundJobRegistry } from "./background-jobs.js";
|
|
4
4
|
import { readExternalChangedFiles } from "../../cc-orchestrator/external-agent-changes.js";
|
|
5
|
+
import { isExistingDirectory, normalizeCwdPath } from "../../cc-orchestrator/cwd-normalize.js";
|
|
5
6
|
import { notificationQueue } from "./agent-notifications.js";
|
|
7
|
+
import { externalAgentSessionStore, } from "../../cc-orchestrator/external-agent-session-store.js";
|
|
8
|
+
import { logger } from "../../logging/logger.js";
|
|
9
|
+
export const DRIVE_AGENT_FOREGROUND_HANDOFF_MS = 110_000;
|
|
10
|
+
export const DRIVE_AGENT_TOOL_TIMEOUT_MS = 1_800_000;
|
|
6
11
|
const CLI_ADAPTERS = {
|
|
7
12
|
claude: { adapter: claudeAdapter, command: "claude" },
|
|
8
13
|
codex: { adapter: codexAdapter, command: "codex" },
|
|
@@ -47,20 +52,144 @@ export const driveAgentToolDef = {
|
|
|
47
52
|
};
|
|
48
53
|
const defaultRunner = (opts) => {
|
|
49
54
|
const { adapter, command } = CLI_ADAPTERS[opts.cli];
|
|
50
|
-
return runAgentOnce(adapter, { command, prompt: opts.prompt, resumeSessionId: opts.resumeSessionId, cwd: opts.cwd, permissionMode: opts.permissionMode ?? "default" });
|
|
55
|
+
return runAgentOnce(adapter, { command, prompt: opts.prompt, resumeSessionId: opts.resumeSessionId, cwd: opts.cwd, permissionMode: opts.permissionMode ?? "default" }, opts.signal);
|
|
51
56
|
};
|
|
57
|
+
function newDriveJobId() {
|
|
58
|
+
return `cc-${process.hrtime.bigint().toString(36)}`;
|
|
59
|
+
}
|
|
60
|
+
function isValidSessionId(sessionId) {
|
|
61
|
+
return typeof sessionId === "string" && sessionId.length > 0;
|
|
62
|
+
}
|
|
63
|
+
function argSignal(args) {
|
|
64
|
+
const signal = args.__signal;
|
|
65
|
+
return signal &&
|
|
66
|
+
typeof signal === "object" &&
|
|
67
|
+
typeof signal.aborted === "boolean" &&
|
|
68
|
+
typeof signal.addEventListener === "function"
|
|
69
|
+
? signal
|
|
70
|
+
: undefined;
|
|
71
|
+
}
|
|
72
|
+
function startRun(runner, opts) {
|
|
73
|
+
return Promise.resolve().then(() => runner(opts));
|
|
74
|
+
}
|
|
75
|
+
function recordSuccessfulSession(store, cli, cwd, result) {
|
|
76
|
+
if (result.isError || !result.sessionId)
|
|
77
|
+
return;
|
|
78
|
+
try {
|
|
79
|
+
store.record({ cli, sessionId: result.sessionId, cwd });
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
logger.warn("drive_agent.session_binding_record_failed", {
|
|
83
|
+
cat: "cc",
|
|
84
|
+
cli,
|
|
85
|
+
sessionId: result.sessionId,
|
|
86
|
+
cwd,
|
|
87
|
+
error: err instanceof Error ? err.message : String(err),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function duplicateCwdWarning(cwd, writable) {
|
|
92
|
+
if (!writable)
|
|
93
|
+
return undefined;
|
|
94
|
+
const running = backgroundJobRegistry.listRunningByCwd(cwd);
|
|
95
|
+
if (running.length === 0)
|
|
96
|
+
return undefined;
|
|
97
|
+
const ids = running.map((j) => j.jobId).join(", ");
|
|
98
|
+
return `Warning: another DriveAgent job is already running in cwd ${cwd} (jobId ${ids}). Concurrent writable agents in the same directory can overwrite each other's work.`;
|
|
99
|
+
}
|
|
100
|
+
function attachDriveCompletion(params) {
|
|
101
|
+
const { jobId, sessionId, label, cli, cwd, run, sessionStore } = params;
|
|
102
|
+
void run
|
|
103
|
+
.then((r) => {
|
|
104
|
+
recordSuccessfulSession(sessionStore, cli, cwd, r);
|
|
105
|
+
// Deliver the result back so the woken agent actually sees the answer
|
|
106
|
+
// (not just "a job finished"). Mirrors the video/sub-agent completion
|
|
107
|
+
// path — enqueue lands in the same notificationQueue the wakeup drains.
|
|
108
|
+
notificationQueue.enqueue(r.isError
|
|
109
|
+
? { agentId: jobId, description: label, status: "failed", workKind: "cc", error: r.finalText || "(no output)", ccSessionId: r.sessionId || undefined, enqueuedAt: Date.now() }
|
|
110
|
+
: { agentId: jobId, description: label, status: "completed", workKind: "cc", finalText: r.finalText, ccSessionId: r.sessionId || undefined, enqueuedAt: Date.now() }, sessionId);
|
|
111
|
+
// Attribute the files the external agent changed by parsing its own
|
|
112
|
+
// transcript (#6) — those Edit/Write calls are invisible to the host's
|
|
113
|
+
// in-session aggregator. Best-effort; [] on any failure.
|
|
114
|
+
const changedFiles = r.sessionId
|
|
115
|
+
? readExternalChangedFiles(cli, cwd, r.sessionId)
|
|
116
|
+
: [];
|
|
117
|
+
// Retain the job in the panel with its result + the external CLI
|
|
118
|
+
// session id + changed files.
|
|
119
|
+
backgroundJobRegistry.finish(jobId, {
|
|
120
|
+
status: r.isError ? "failed" : "completed",
|
|
121
|
+
finalText: r.finalText || undefined,
|
|
122
|
+
ccSessionId: r.sessionId || undefined,
|
|
123
|
+
...(changedFiles.length ? { changedFiles } : {}),
|
|
124
|
+
});
|
|
125
|
+
})
|
|
126
|
+
.catch((err) => {
|
|
127
|
+
const msg = err?.message ?? String(err);
|
|
128
|
+
notificationQueue.enqueue({ agentId: jobId, description: label, status: "failed", workKind: "cc", error: msg, enqueuedAt: Date.now() }, sessionId);
|
|
129
|
+
backgroundJobRegistry.finish(jobId, { status: "failed", finalText: msg });
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function trackBackgroundRun(params) {
|
|
133
|
+
const warning = duplicateCwdWarning(params.cwd, params.writable);
|
|
134
|
+
const jobId = newDriveJobId();
|
|
135
|
+
backgroundJobRegistry.start(jobId, params.sessionId, params.label, { cwd: params.cwd });
|
|
136
|
+
attachDriveCompletion({ ...params, jobId });
|
|
137
|
+
return { jobId, ...(warning ? { warning } : {}) };
|
|
138
|
+
}
|
|
139
|
+
async function waitForForegroundOrHandoff(run, handoffMs) {
|
|
140
|
+
if (handoffMs < 0) {
|
|
141
|
+
return { kind: "completed", result: await run };
|
|
142
|
+
}
|
|
143
|
+
let timer;
|
|
144
|
+
try {
|
|
145
|
+
return await Promise.race([
|
|
146
|
+
run.then((result) => ({ kind: "completed", result })),
|
|
147
|
+
new Promise((resolve) => {
|
|
148
|
+
timer = setTimeout(() => resolve({ kind: "handoff" }), handoffMs);
|
|
149
|
+
}),
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
if (timer)
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
52
157
|
/** Factory so tests can inject a fake runner. `fixedCli` (back-compat) forces a
|
|
53
158
|
* cli and hides the `cli` arg — that's how DriveClaudeCode stays a thin alias. */
|
|
54
|
-
export function makeDriveAgentTool(runner = defaultRunner, fixedCli) {
|
|
159
|
+
export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {}) {
|
|
55
160
|
return async (args, ctx) => {
|
|
56
161
|
const prompt = typeof args.prompt === "string" ? args.prompt : "";
|
|
57
|
-
const
|
|
162
|
+
const rawRequestedCwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
|
|
163
|
+
const requestedCwd = normalizeCwdPath(rawRequestedCwd);
|
|
58
164
|
if (!prompt)
|
|
59
165
|
return "Error: prompt is required";
|
|
60
166
|
const cli = fixedCli ?? (args.cli === "codex" ? "codex" : args.cli === "claude" || args.cli === undefined ? "claude" : "invalid");
|
|
61
167
|
if (cli === "invalid")
|
|
62
168
|
return `Error: unknown cli "${String(args.cli)}" (expected "claude" or "codex")`;
|
|
63
169
|
const resumeSessionId = typeof args.resumeSessionId === "string" ? args.resumeSessionId : undefined;
|
|
170
|
+
const sessionStore = options.sessionStore ?? externalAgentSessionStore;
|
|
171
|
+
let cwd = requestedCwd;
|
|
172
|
+
let resumeNote = "";
|
|
173
|
+
if (resumeSessionId) {
|
|
174
|
+
const binding = sessionStore.get(cli, resumeSessionId);
|
|
175
|
+
if (binding) {
|
|
176
|
+
const storedCwd = normalizeCwdPath(binding.cwd);
|
|
177
|
+
if (!isExistingDirectory(storedCwd)) {
|
|
178
|
+
return `Error: cannot resume ${cli} session ${resumeSessionId}: stored cwd no longer exists or is not a directory: ${storedCwd}`;
|
|
179
|
+
}
|
|
180
|
+
if (storedCwd !== requestedCwd) {
|
|
181
|
+
cwd = storedCwd;
|
|
182
|
+
resumeNote = `Note: resume session ${resumeSessionId} is bound to stored cwd ${storedCwd}; ignoring requested cwd ${requestedCwd}.`;
|
|
183
|
+
logger.info("drive_agent.resume_forced_stored_cwd", {
|
|
184
|
+
cat: "cc",
|
|
185
|
+
cli,
|
|
186
|
+
sessionId: resumeSessionId,
|
|
187
|
+
requestedCwd,
|
|
188
|
+
storedCwd,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
64
193
|
// Default to bypassPermissions: this tool is a fire-one-turn delegation to
|
|
65
194
|
// an external CLI with nobody watching for approvals, and there is no
|
|
66
195
|
// interactive approval loop here — so under "default" a tool that needs
|
|
@@ -75,6 +204,9 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli) {
|
|
|
75
204
|
: "bypassPermissions";
|
|
76
205
|
const cliName = cli === "codex" ? "Codex" : "Claude Code";
|
|
77
206
|
const label = `DriveAgent(${cli}): ${prompt.slice(0, 40)}`;
|
|
207
|
+
const runOpts = { cli, prompt, resumeSessionId, cwd, permissionMode, signal: ctx?.signal ?? argSignal(args) };
|
|
208
|
+
const foregroundHandoffMs = options.foregroundHandoffMs ?? DRIVE_AGENT_FOREGROUND_HANDOFF_MS;
|
|
209
|
+
const isWritableRun = permissionMode !== "default";
|
|
78
210
|
// Background by default (these tasks are typically long). Only an explicit
|
|
79
211
|
// background:false runs in the foreground and returns the result inline.
|
|
80
212
|
const background = args.background !== false;
|
|
@@ -87,42 +219,45 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli) {
|
|
|
87
219
|
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
88
220
|
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.`;
|
|
89
221
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const msg = err?.message ?? String(err);
|
|
117
|
-
notificationQueue.enqueue({ agentId: jobId, description: label, status: "failed", workKind: "cc", error: msg, enqueuedAt: Date.now() }, sessionId);
|
|
118
|
-
backgroundJobRegistry.finish(jobId, { status: "failed", finalText: msg });
|
|
222
|
+
const tracked = trackBackgroundRun({
|
|
223
|
+
sessionId,
|
|
224
|
+
label,
|
|
225
|
+
cli,
|
|
226
|
+
cwd,
|
|
227
|
+
run: startRun(runner, runOpts),
|
|
228
|
+
sessionStore,
|
|
229
|
+
writable: isWritableRun,
|
|
230
|
+
});
|
|
231
|
+
return [
|
|
232
|
+
resumeNote,
|
|
233
|
+
`已在后台启动 ${cliName}(jobId ${tracked.jobId})。完成后会通知你结果,无需轮询。`,
|
|
234
|
+
tracked.warning,
|
|
235
|
+
].filter(Boolean).join("\n");
|
|
236
|
+
}
|
|
237
|
+
const run = startRun(runner, runOpts);
|
|
238
|
+
const result = await waitForForegroundOrHandoff(run, foregroundHandoffMs);
|
|
239
|
+
if (result.kind === "handoff" && isValidSessionId(ctx?.sessionId)) {
|
|
240
|
+
const tracked = trackBackgroundRun({
|
|
241
|
+
sessionId: ctx.sessionId,
|
|
242
|
+
label,
|
|
243
|
+
cli,
|
|
244
|
+
cwd,
|
|
245
|
+
run,
|
|
246
|
+
sessionStore,
|
|
247
|
+
writable: isWritableRun,
|
|
119
248
|
});
|
|
120
|
-
return
|
|
249
|
+
return [
|
|
250
|
+
resumeNote,
|
|
251
|
+
`${cliName} foreground run exceeded ${foregroundHandoffMs}ms; moved it to background (jobId ${tracked.jobId}). Completion will notify this session, so do not poll.`,
|
|
252
|
+
tracked.warning,
|
|
253
|
+
].filter(Boolean).join("\n");
|
|
121
254
|
}
|
|
122
|
-
const r =
|
|
255
|
+
const r = result.kind === "completed" ? result.result : await run;
|
|
256
|
+
recordSuccessfulSession(sessionStore, cli, cwd, r);
|
|
257
|
+
const prefix = resumeNote ? `${resumeNote}\n` : "";
|
|
123
258
|
if (r.isError)
|
|
124
|
-
return `${cliName} 运行出错(session ${r.sessionId}):\n${r.finalText}`;
|
|
125
|
-
return `${cliName} 完成(session ${r.sessionId}):\n${r.finalText}`;
|
|
259
|
+
return `${prefix}${cliName} 运行出错(session ${r.sessionId}):\n${r.finalText}`;
|
|
260
|
+
return `${prefix}${cliName} 完成(session ${r.sessionId}):\n${r.finalText}`;
|
|
126
261
|
};
|
|
127
262
|
}
|
|
128
263
|
export const driveAgentTool = makeDriveAgentTool();
|
|
@@ -148,10 +283,10 @@ export const driveClaudeCodeToolDef = {
|
|
|
148
283
|
required: ["prompt", "cwd"],
|
|
149
284
|
},
|
|
150
285
|
};
|
|
151
|
-
export function makeDriveClaudeCodeTool(runner) {
|
|
286
|
+
export function makeDriveClaudeCodeTool(runner, options) {
|
|
152
287
|
const generic = runner
|
|
153
|
-
? ({ prompt, resumeSessionId, cwd, permissionMode }) => runner({ prompt, resumeSessionId, cwd, permissionMode })
|
|
288
|
+
? ({ prompt, resumeSessionId, cwd, permissionMode, signal }) => runner({ prompt, resumeSessionId, cwd, permissionMode, signal })
|
|
154
289
|
: undefined;
|
|
155
|
-
return makeDriveAgentTool(generic ?? defaultRunner, "claude");
|
|
290
|
+
return makeDriveAgentTool(generic ?? defaultRunner, "claude", options);
|
|
156
291
|
}
|
|
157
292
|
export const driveClaudeCodeTool = makeDriveClaudeCodeTool();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
|
+
import { isAbsolute, resolve } from "node:path";
|
|
6
7
|
import { fileCache } from "./file-cache.js";
|
|
7
8
|
import { detectEol, toLf, applyEol } from "./eol.js";
|
|
8
9
|
export const editToolDef = {
|
|
@@ -32,11 +33,11 @@ export const editToolDef = {
|
|
|
32
33
|
},
|
|
33
34
|
};
|
|
34
35
|
export async function editTool(args, ctx) {
|
|
35
|
-
const
|
|
36
|
+
const rawPath = args.file_path;
|
|
36
37
|
const oldString = args.old_string;
|
|
37
38
|
const newString = args.new_string;
|
|
38
39
|
const replaceAll = args.replace_all ?? false;
|
|
39
|
-
if (!
|
|
40
|
+
if (!rawPath)
|
|
40
41
|
return "Error: file_path is required";
|
|
41
42
|
if (oldString === undefined)
|
|
42
43
|
return "Error: old_string is required";
|
|
@@ -44,6 +45,8 @@ export async function editTool(args, ctx) {
|
|
|
44
45
|
return "Error: new_string is required";
|
|
45
46
|
if (oldString === newString)
|
|
46
47
|
return "Error: old_string and new_string must be different";
|
|
48
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
49
|
+
const filePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
47
50
|
if (!existsSync(filePath))
|
|
48
51
|
return `Error: File not found: ${filePath}`;
|
|
49
52
|
try {
|
|
@@ -18,6 +18,7 @@ import type { ToolDefinition } from "../../types.js";
|
|
|
18
18
|
import type { ToolContext } from "../context.js";
|
|
19
19
|
import { type VideoProvider, type VideoProviderCreds } from "./video-providers.js";
|
|
20
20
|
import { type ImageUploader, type UploaderCreds } from "./image-uploader.js";
|
|
21
|
+
export declare function __resolveLocalImageInputForTests(pathOrUrl: string, cwd: string): string;
|
|
21
22
|
/**
|
|
22
23
|
* Normalize image inputs (URLs or local paths) into public URLs the provider
|
|
23
24
|
* can consume. `images[]` wins over the single `image`. Local paths are
|
|
@@ -15,17 +15,22 @@
|
|
|
15
15
|
* "no video provider configured" message) rather than half-working.
|
|
16
16
|
*/
|
|
17
17
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
18
|
-
import { join } from "node:path";
|
|
18
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
19
19
|
import { SettingsManager } from "../../settings/manager.js";
|
|
20
20
|
import { notificationQueue } from "./agent-notifications.js";
|
|
21
21
|
import { backgroundJobRegistry } from "./background-jobs.js";
|
|
22
22
|
import { logger } from "../../logging/logger.js";
|
|
23
23
|
import { getVideoProvider, DEFAULT_VIDEO_MODEL, } from "./video-providers.js";
|
|
24
|
-
import { getImageUploader } from "./image-uploader.js";
|
|
24
|
+
import { getImageUploader, isHttpUrl } from "./image-uploader.js";
|
|
25
25
|
import { effectiveApiKey } from "./generate-image.js";
|
|
26
26
|
import { getMergedCatalog, findCatalogEntry } from "../../model-catalog/index.js";
|
|
27
27
|
import { genInstancesFromConnections } from "../../model-catalog/gen-connections.js";
|
|
28
28
|
const MAX_IMAGES = 9;
|
|
29
|
+
export function __resolveLocalImageInputForTests(pathOrUrl, cwd) {
|
|
30
|
+
if (isHttpUrl(pathOrUrl))
|
|
31
|
+
return pathOrUrl;
|
|
32
|
+
return isAbsolute(pathOrUrl) ? pathOrUrl : resolve(cwd, pathOrUrl);
|
|
33
|
+
}
|
|
29
34
|
/**
|
|
30
35
|
* Normalize image inputs (URLs or local paths) into public URLs the provider
|
|
31
36
|
* can consume. `images[]` wins over the single `image`. Local paths are
|
|
@@ -230,9 +235,13 @@ export async function generateVideoTool(args, ctx) {
|
|
|
230
235
|
const sessionId = ctx?.sessionId;
|
|
231
236
|
const preferKind = typeof args.provider === "string" && args.provider ? args.provider : undefined;
|
|
232
237
|
const overrideModel = typeof args.model === "string" && args.model ? args.model : undefined;
|
|
233
|
-
const image = typeof args.image === "string" && args.image
|
|
238
|
+
const image = typeof args.image === "string" && args.image
|
|
239
|
+
? __resolveLocalImageInputForTests(args.image, cwd)
|
|
240
|
+
: undefined;
|
|
234
241
|
const imagesArg = Array.isArray(args.images)
|
|
235
|
-
? args.images
|
|
242
|
+
? args.images
|
|
243
|
+
.filter((x) => typeof x === "string")
|
|
244
|
+
.map((x) => __resolveLocalImageInputForTests(x, cwd))
|
|
236
245
|
: undefined;
|
|
237
246
|
// videos pass through verbatim to fal's video_urls (no upload) — http(s) only.
|
|
238
247
|
const videosArg = Array.isArray(args.videos)
|
|
@@ -25,7 +25,7 @@ import { configToolDef, configTool } from "./config.js";
|
|
|
25
25
|
import { notebookEditToolDef, notebookEditTool } from "./notebook-edit.js";
|
|
26
26
|
import { lspToolDef, lspTool } from "./lsp.js";
|
|
27
27
|
import { cronCreateToolDef, cronCreateTool, cronDeleteToolDef, cronDeleteTool, cronListToolDef, cronListTool } from "./cron.js";
|
|
28
|
-
import { driveClaudeCodeToolDef, driveClaudeCodeTool, driveAgentToolDef, driveAgentTool } from "./drive-claude-code.js";
|
|
28
|
+
import { driveClaudeCodeToolDef, driveClaudeCodeTool, driveAgentToolDef, driveAgentTool, DRIVE_AGENT_TOOL_TIMEOUT_MS } from "./drive-claude-code.js";
|
|
29
29
|
import { checkQuotaToolDef, checkQuotaTool } from "./check-quota.js";
|
|
30
30
|
import { skillToolDef, skillTool } from "./skill.js";
|
|
31
31
|
import { mcpToolDef, mcpToolExecute, listMcpResourcesToolDef, listMcpResourcesTool, readMcpResourceToolDef, readMcpResourceTool } from "./mcp-tools.js";
|
|
@@ -433,6 +433,7 @@ export const BUILTIN_TOOLS = [
|
|
|
433
433
|
permissionDefault: "ask",
|
|
434
434
|
isReadOnly: false,
|
|
435
435
|
isConcurrencySafe: false,
|
|
436
|
+
timeoutMs: DRIVE_AGENT_TOOL_TIMEOUT_MS,
|
|
436
437
|
},
|
|
437
438
|
execute: driveAgentTool,
|
|
438
439
|
},
|
|
@@ -445,6 +446,7 @@ export const BUILTIN_TOOLS = [
|
|
|
445
446
|
permissionDefault: "ask",
|
|
446
447
|
isReadOnly: false,
|
|
447
448
|
isConcurrencySafe: false,
|
|
449
|
+
timeoutMs: DRIVE_AGENT_TOOL_TIMEOUT_MS,
|
|
448
450
|
},
|
|
449
451
|
execute: driveClaudeCodeTool,
|
|
450
452
|
},
|
|
@@ -722,6 +724,8 @@ export const BUILTIN_TOOL_GUARDS = new Map([
|
|
|
722
724
|
// InjectCredential hidden until ≥1 cookie credential exists (browser injection
|
|
723
725
|
// is cookie-only). Also degrades at call time if no browser bridge is wired.
|
|
724
726
|
[injectCredentialToolDef.name, (ctx) => isInjectCredentialAvailable(ctx.cwd)],
|
|
727
|
+
[completeGoalToolDef.name, (ctx) => ctx.hasGoal === true],
|
|
728
|
+
[cancelGoalToolDef.name, (ctx) => ctx.hasGoal === true],
|
|
725
729
|
]);
|
|
726
730
|
/** UseCredential is available when the cwd's CredentialStore has ≥1 credential. */
|
|
727
731
|
export function isUseCredentialAvailable(cwd) {
|
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
* LSPTool — language server protocol operations for code intelligence.
|
|
3
3
|
*/
|
|
4
4
|
import type { ToolDefinition } from "../../types.js";
|
|
5
|
+
import type { ToolContext } from "../context.js";
|
|
5
6
|
export declare const lspToolDef: ToolDefinition;
|
|
6
|
-
export declare function lspTool(args: Record<string, unknown
|
|
7
|
+
export declare function lspTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { getLSPManager } from "../../lsp/manager.js";
|
|
5
5
|
import { detectLSPServer } from "../../lsp/servers.js";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { isAbsolute, resolve } from "node:path";
|
|
7
8
|
export const lspToolDef = {
|
|
8
9
|
name: "LSP",
|
|
9
10
|
description: "Use Language Server Protocol for code intelligence operations. " +
|
|
@@ -33,13 +34,15 @@ export const lspToolDef = {
|
|
|
33
34
|
required: ["action", "file_path"],
|
|
34
35
|
},
|
|
35
36
|
};
|
|
36
|
-
export async function lspTool(args) {
|
|
37
|
+
export async function lspTool(args, ctx) {
|
|
37
38
|
const action = args.action;
|
|
38
|
-
const
|
|
39
|
+
const rawPath = args.file_path;
|
|
39
40
|
const line = args.line ?? 0;
|
|
40
41
|
const character = args.character ?? 0;
|
|
41
|
-
if (!
|
|
42
|
+
if (!rawPath)
|
|
42
43
|
return "Error: file_path is required";
|
|
44
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
45
|
+
const filePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
43
46
|
const manager = getLSPManager();
|
|
44
47
|
if (!manager)
|
|
45
48
|
return "Error: LSP is not initialized. Language servers are not available.";
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* NotebookEditTool — read and edit Jupyter notebook (.ipynb) cells.
|
|
3
3
|
*/
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
5
|
+
import { isAbsolute, resolve } from "node:path";
|
|
5
6
|
export const notebookEditToolDef = {
|
|
6
7
|
name: "NotebookEdit",
|
|
7
8
|
description: "Edit a Jupyter notebook (.ipynb) file. Supports actions: " +
|
|
@@ -37,10 +38,12 @@ export const notebookEditToolDef = {
|
|
|
37
38
|
},
|
|
38
39
|
};
|
|
39
40
|
export async function notebookEditTool(args, ctx) {
|
|
40
|
-
const
|
|
41
|
+
const rawPath = args.file_path;
|
|
41
42
|
const action = args.action;
|
|
42
|
-
if (!
|
|
43
|
+
if (!rawPath)
|
|
43
44
|
return "Error: file_path is required";
|
|
45
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
46
|
+
const filePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
44
47
|
if (!filePath.endsWith(".ipynb"))
|
|
45
48
|
return "Error: file must be a .ipynb file";
|
|
46
49
|
if (action === "read") {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { readFile, stat } from "node:fs/promises";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
|
+
import { isAbsolute, resolve } from "node:path";
|
|
6
7
|
import { fileCache } from "./file-cache.js";
|
|
7
8
|
import { toLf } from "./eol.js";
|
|
8
9
|
export const readToolDef = {
|
|
@@ -28,9 +29,11 @@ export const readToolDef = {
|
|
|
28
29
|
};
|
|
29
30
|
const MAX_CONTENT_CHARS = 200_000;
|
|
30
31
|
export async function readTool(args, ctx) {
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
32
|
+
const rawPath = args.file_path;
|
|
33
|
+
if (!rawPath)
|
|
33
34
|
return "Error: file_path is required";
|
|
35
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
36
|
+
const filePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
34
37
|
if (!existsSync(filePath))
|
|
35
38
|
return `Error: File not found: ${filePath}`;
|
|
36
39
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in view_image tool —
|
|
3
|
-
* 回传进上下文,让 vision 模型「看」它(对照 codex 的 view_image)。
|
|
2
|
+
* Built-in view_image tool — 把一个本地图片文件或历史图片以 base64 image
|
|
3
|
+
* ContentBlock 回传进上下文,让 vision 模型「看」它(对照 codex 的 view_image)。
|
|
4
4
|
*
|
|
5
5
|
* 典型用法:模型先写 SVG/Mermaid 并用 shell 转成 PNG,再调 view_image(png)
|
|
6
6
|
* 检查图画对没有(标签是否重叠、文字是否溢出),不对就改源再重转。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in view_image tool —
|
|
3
|
-
* 回传进上下文,让 vision 模型「看」它(对照 codex 的 view_image)。
|
|
2
|
+
* Built-in view_image tool — 把一个本地图片文件或历史图片以 base64 image
|
|
3
|
+
* ContentBlock 回传进上下文,让 vision 模型「看」它(对照 codex 的 view_image)。
|
|
4
4
|
*
|
|
5
5
|
* 典型用法:模型先写 SVG/Mermaid 并用 shell 转成 PNG,再调 view_image(png)
|
|
6
6
|
* 检查图画对没有(标签是否重叠、文字是否溢出),不对就改源再重转。
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { readFile, stat } from "node:fs/promises";
|
|
15
15
|
import { extname, isAbsolute, resolve } from "node:path";
|
|
16
16
|
import { capabilitiesFor } from "../../llm/capabilities/index.js";
|
|
17
|
+
import { collectBase64Images, findImageByNumber } from "../../context/compaction.js";
|
|
17
18
|
const MAX_BYTES = 5 * 1024 * 1024; // 5MB —— vision 模型按 tile 计 token,大图无益且撑大请求
|
|
18
19
|
const MEDIA_TYPES = {
|
|
19
20
|
".png": "image/png",
|
|
@@ -24,11 +25,11 @@ const MEDIA_TYPES = {
|
|
|
24
25
|
};
|
|
25
26
|
export const viewImageToolDef = {
|
|
26
27
|
name: "view_image",
|
|
27
|
-
description: "Load
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
28
|
+
description: "Load an image into the conversation so you can SEE it (vision). Pass path to view a " +
|
|
29
|
+
"workspace image file, or pass imageNumber to retrieve the original image behind an " +
|
|
30
|
+
"earlier [image #N, already provided] history placeholder. Use exactly one of path or " +
|
|
31
|
+
"imageNumber. File paths support PNG/JPEG/GIF/WebP only; convert SVG/PDF to PNG first. " +
|
|
32
|
+
"Requires a vision-capable model; otherwise the image is skipped.",
|
|
32
33
|
inputSchema: {
|
|
33
34
|
type: "object",
|
|
34
35
|
properties: {
|
|
@@ -36,14 +37,31 @@ export const viewImageToolDef = {
|
|
|
36
37
|
type: "string",
|
|
37
38
|
description: "Path to the image file (absolute, or relative to the working directory).",
|
|
38
39
|
},
|
|
40
|
+
imageNumber: {
|
|
41
|
+
type: "number",
|
|
42
|
+
description: "Image history number N from an earlier [image #N, already provided] placeholder.",
|
|
43
|
+
},
|
|
39
44
|
},
|
|
40
|
-
required: ["path"],
|
|
41
45
|
},
|
|
42
46
|
};
|
|
43
47
|
export async function viewImageTool(args, ctx) {
|
|
44
48
|
const rawPath = args.path;
|
|
49
|
+
const rawImageNumber = args.imageNumber;
|
|
50
|
+
const hasPath = typeof rawPath === "string" && rawPath.trim().length > 0;
|
|
51
|
+
const hasImageNumber = rawImageNumber !== undefined && rawImageNumber !== null;
|
|
52
|
+
if (hasPath === hasImageNumber) {
|
|
53
|
+
return "Error: provide exactly one of path or imageNumber";
|
|
54
|
+
}
|
|
55
|
+
if (hasImageNumber) {
|
|
56
|
+
if (typeof rawImageNumber !== "number" ||
|
|
57
|
+
!Number.isSafeInteger(rawImageNumber) ||
|
|
58
|
+
rawImageNumber <= 0) {
|
|
59
|
+
return "Error: imageNumber must be a positive integer";
|
|
60
|
+
}
|
|
61
|
+
return viewHistoricalImage(rawImageNumber, ctx);
|
|
62
|
+
}
|
|
45
63
|
if (typeof rawPath !== "string" || !rawPath.trim()) {
|
|
46
|
-
return "Error: path
|
|
64
|
+
return "Error: path must be a non-empty string";
|
|
47
65
|
}
|
|
48
66
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
49
67
|
const abs = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
@@ -51,13 +69,7 @@ export async function viewImageTool(args, ctx) {
|
|
|
51
69
|
// 缺 llmConfig 时按「非视觉」处理(fail closed),对齐 DEFAULT_CAPABILITY
|
|
52
70
|
// 的保守姿态:运行时 ToolContext.llmConfig 必有,缺失只发生在测试 / 异常
|
|
53
71
|
// 装配下,此时绝不把 base64 读进上下文。
|
|
54
|
-
|
|
55
|
-
if (!ctx?.llmConfig)
|
|
56
|
-
return false;
|
|
57
|
-
const kind = (ctx.llmConfig.providerKind ?? ctx.llmConfig.provider);
|
|
58
|
-
return capabilitiesFor(kind, ctx.llmConfig.model).supportsVision;
|
|
59
|
-
})();
|
|
60
|
-
if (!supportsVision) {
|
|
72
|
+
if (!supportsVision(ctx)) {
|
|
61
73
|
return `[图片未加载: ${abs} —— 当前模型不支持视觉输入,已跳过。切换到 vision 模型后再 view_image。]`;
|
|
62
74
|
}
|
|
63
75
|
// 闸门 2:格式 gate
|
|
@@ -88,9 +100,78 @@ export async function viewImageTool(args, ctx) {
|
|
|
88
100
|
const data = buf.toString("base64");
|
|
89
101
|
const kb = Math.round(buf.length / 1024);
|
|
90
102
|
return {
|
|
91
|
-
contentBlocks: [
|
|
92
|
-
{ type: "image", source: { type: "base64", media_type: mediaType, data } },
|
|
93
|
-
],
|
|
103
|
+
contentBlocks: [{ type: "image", source: { type: "base64", media_type: mediaType, data } }],
|
|
94
104
|
result: `[已加载图片: ${abs} (${mediaType}, ${kb} KB)]`,
|
|
95
105
|
};
|
|
96
106
|
}
|
|
107
|
+
function supportsVision(ctx) {
|
|
108
|
+
if (!ctx?.llmConfig)
|
|
109
|
+
return false;
|
|
110
|
+
const kind = (ctx.llmConfig.providerKind ?? ctx.llmConfig.provider);
|
|
111
|
+
return capabilitiesFor(kind, ctx.llmConfig.model).supportsVision;
|
|
112
|
+
}
|
|
113
|
+
async function viewHistoricalImage(imageNumber, ctx) {
|
|
114
|
+
if (!supportsVision(ctx)) {
|
|
115
|
+
return `[图片未取回: image #${imageNumber} —— 当前模型不支持视觉输入,已跳过。切换到 vision 模型后再 view_image。]`;
|
|
116
|
+
}
|
|
117
|
+
const sessionManager = ctx?.engine?.getSessionManager?.();
|
|
118
|
+
const sessionId = ctx?.sessionId;
|
|
119
|
+
if (!sessionManager || !sessionId) {
|
|
120
|
+
return `Error: 无法取回 image #${imageNumber}: 当前工具上下文没有可读取的 session 历史。`;
|
|
121
|
+
}
|
|
122
|
+
let messages;
|
|
123
|
+
try {
|
|
124
|
+
messages = sessionManager.resume(sessionId).transcript.toMessages();
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
return `Error: 无法读取 session 历史以取回 image #${imageNumber}: ${err.message}`;
|
|
128
|
+
}
|
|
129
|
+
const images = collectBase64Images(messages);
|
|
130
|
+
const found = findImageByNumber(messages, imageNumber);
|
|
131
|
+
if (!found) {
|
|
132
|
+
return `Error: 未找到 image #${imageNumber}; 当前 session 历史中共有 ${images.length} 张可取回图片。`;
|
|
133
|
+
}
|
|
134
|
+
const block = normalizeImageBlockForReturn(found.block);
|
|
135
|
+
const size = decodedImageBlockBytes(block);
|
|
136
|
+
if (size !== undefined && size > MAX_BYTES) {
|
|
137
|
+
const mb = (size / 1024 / 1024).toFixed(1);
|
|
138
|
+
return `[图片未取回: image #${imageNumber} —— 图片过大 (${mb} MB > 5 MB),请先压缩或缩放原图。]`;
|
|
139
|
+
}
|
|
140
|
+
const kb = size === undefined ? "unknown" : String(Math.round(size / 1024));
|
|
141
|
+
const mediaType = mediaTypeOfImageBlock(block) ?? "base64 image";
|
|
142
|
+
return {
|
|
143
|
+
contentBlocks: [block],
|
|
144
|
+
result: `[已取回 image #${imageNumber}: ${mediaType}, ${kb} KB]`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function normalizeImageBlockForReturn(block) {
|
|
148
|
+
if (block.type === "image" && block.source?.type === "base64")
|
|
149
|
+
return block;
|
|
150
|
+
const source = openAIDataUrlImageSource(block);
|
|
151
|
+
return source ? { type: "image", source } : block;
|
|
152
|
+
}
|
|
153
|
+
function decodedImageBlockBytes(block) {
|
|
154
|
+
const data = base64DataOfImageBlock(block);
|
|
155
|
+
if (!data)
|
|
156
|
+
return undefined;
|
|
157
|
+
return Buffer.byteLength(data, "base64");
|
|
158
|
+
}
|
|
159
|
+
function base64DataOfImageBlock(block) {
|
|
160
|
+
if (block.type === "image" && block.source?.type === "base64") {
|
|
161
|
+
return block.source.data;
|
|
162
|
+
}
|
|
163
|
+
return openAIDataUrlImageSource(block)?.data;
|
|
164
|
+
}
|
|
165
|
+
function mediaTypeOfImageBlock(block) {
|
|
166
|
+
if (block.type === "image" && block.source?.type === "base64") {
|
|
167
|
+
return block.source.media_type;
|
|
168
|
+
}
|
|
169
|
+
return openAIDataUrlImageSource(block)?.media_type;
|
|
170
|
+
}
|
|
171
|
+
function openAIDataUrlImageSource(block) {
|
|
172
|
+
const maybeOpenAI = block;
|
|
173
|
+
const match = maybeOpenAI.image_url?.url?.match(/^data:(image\/[^;,]+);base64,(.+)$/i);
|
|
174
|
+
if (maybeOpenAI.type !== "image_url" || !match?.[1] || !match[2])
|
|
175
|
+
return undefined;
|
|
176
|
+
return { type: "base64", media_type: match[1], data: match[2] };
|
|
177
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EnterWorktree / ExitWorktree tools —
|
|
2
|
+
* EnterWorktree / ExitWorktree tools — switch a session between git workspaces.
|
|
3
3
|
*/
|
|
4
4
|
import type { ToolDefinition } from "../../types.js";
|
|
5
5
|
import type { ToolContext } from "../context.js";
|
|
6
|
-
import { type WorktreeSession } from "../../git/worktree.js";
|
|
7
|
-
export declare function getActiveWorktree(): WorktreeSession | undefined;
|
|
8
6
|
export declare const enterWorktreeToolDef: ToolDefinition;
|
|
9
7
|
export declare function enterWorktreeTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
10
8
|
export declare const exitWorktreeToolDef: ToolDefinition;
|
|
11
|
-
export declare function exitWorktreeTool(args: Record<string, unknown
|
|
9
|
+
export declare function exitWorktreeTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|