@cjhyy/code-shell-core 0.6.0-rc.9 → 0.7.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/THIRD_PARTY_NOTICES.md +206 -0
  2. package/dist/automation/scheduler.d.ts +13 -7
  3. package/dist/automation/scheduler.js +116 -37
  4. package/dist/capability-control/service.d.ts +2 -0
  5. package/dist/capability-control/service.js +4 -2
  6. package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
  7. package/dist/cc-orchestrator/agent-adapter.js +7 -1
  8. package/dist/cc-orchestrator/codex-session-history.js +1 -1
  9. package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
  10. package/dist/cc-orchestrator/cwd-normalize.js +19 -0
  11. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  12. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  13. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
  14. package/dist/cc-orchestrator/external-agent-driver.js +102 -51
  15. package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
  16. package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
  17. package/dist/cc-orchestrator/session-history.js +2 -2
  18. package/dist/cli/agent-server-stdio.js +9 -2
  19. package/dist/context/compaction.d.ts +8 -1
  20. package/dist/context/compaction.js +49 -4
  21. package/dist/context/manager.d.ts +16 -2
  22. package/dist/context/manager.js +103 -19
  23. package/dist/credentials/access.d.ts +56 -0
  24. package/dist/credentials/access.js +183 -0
  25. package/dist/credentials/index.d.ts +1 -0
  26. package/dist/credentials/index.js +1 -0
  27. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  28. package/dist/credentials/inject-credential-tool.js +30 -11
  29. package/dist/credentials/types.d.ts +2 -2
  30. package/dist/credentials/use-credential-tool.d.ts +9 -1
  31. package/dist/credentials/use-credential-tool.js +58 -45
  32. package/dist/engine/engine.d.ts +17 -1
  33. package/dist/engine/engine.js +247 -89
  34. package/dist/engine/image-policy.d.ts +6 -0
  35. package/dist/engine/image-policy.js +17 -6
  36. package/dist/engine/input-attachments.d.ts +13 -0
  37. package/dist/engine/input-attachments.js +255 -0
  38. package/dist/engine/model-facade.d.ts +5 -2
  39. package/dist/engine/model-facade.js +4 -4
  40. package/dist/engine/parse-task.d.ts +10 -0
  41. package/dist/engine/parse-task.js +5 -0
  42. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  43. package/dist/engine/streaming-tool-queue.js +11 -7
  44. package/dist/engine/turn-loop.d.ts +7 -1
  45. package/dist/engine/turn-loop.js +117 -27
  46. package/dist/engine/types.d.ts +8 -0
  47. package/dist/git/worktree/crud.d.ts +69 -0
  48. package/dist/git/worktree/crud.js +206 -0
  49. package/dist/git/worktree/diff.d.ts +14 -0
  50. package/dist/git/worktree/diff.js +82 -0
  51. package/dist/git/worktree/git-exec.d.ts +7 -0
  52. package/dist/git/worktree/git-exec.js +51 -0
  53. package/dist/git/worktree/index.d.ts +5 -0
  54. package/dist/git/worktree/index.js +5 -0
  55. package/dist/git/worktree/query.d.ts +42 -0
  56. package/dist/git/worktree/query.js +121 -0
  57. package/dist/git/worktree/slug.d.ts +11 -0
  58. package/dist/git/worktree/slug.js +58 -0
  59. package/dist/git/worktree.d.ts +1 -84
  60. package/dist/git/worktree.js +5 -230
  61. package/dist/index.d.ts +31 -28
  62. package/dist/index.js +27 -25
  63. package/dist/logging/logger.js +6 -6
  64. package/dist/logging/sanitize-messages.d.ts +10 -2
  65. package/dist/logging/sanitize-messages.js +21 -6
  66. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  67. package/dist/plugins/installer/checkUpdate.js +4 -2
  68. package/dist/plugins/installer/install.js +2 -0
  69. package/dist/plugins/installer/installFromSource.js +9 -1
  70. package/dist/plugins/installer/parseSource.d.ts +4 -1
  71. package/dist/plugins/installer/parseSource.js +28 -10
  72. package/dist/plugins/installer/sourcePath.d.ts +9 -0
  73. package/dist/plugins/installer/sourcePath.js +50 -0
  74. package/dist/plugins/installer/update.d.ts +4 -1
  75. package/dist/plugins/installer/update.js +5 -3
  76. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  77. package/dist/plugins/parseMarketplaceInput.js +4 -3
  78. package/dist/plugins/pluginInstaller.js +24 -22
  79. package/dist/preset/index.d.ts +1 -0
  80. package/dist/preset/index.js +16 -9
  81. package/dist/prompt/sections/base.md +1 -1
  82. package/dist/protocol/chat-session-manager.d.ts +2 -0
  83. package/dist/protocol/chat-session-manager.js +38 -2
  84. package/dist/protocol/chat-session.d.ts +3 -0
  85. package/dist/protocol/chat-session.js +1 -0
  86. package/dist/protocol/client.d.ts +9 -4
  87. package/dist/protocol/client.js +18 -1
  88. package/dist/protocol/server.d.ts +30 -0
  89. package/dist/protocol/server.js +246 -46
  90. package/dist/protocol/types.d.ts +47 -0
  91. package/dist/protocol/types.js +6 -0
  92. package/dist/run/FileRunStore.js +10 -1
  93. package/dist/run/Heartbeat.js +12 -0
  94. package/dist/run/RunApprovalBackend.d.ts +3 -0
  95. package/dist/run/RunApprovalBackend.js +41 -6
  96. package/dist/run/RunLock.js +2 -0
  97. package/dist/run/RunManager.d.ts +2 -0
  98. package/dist/run/RunManager.js +64 -24
  99. package/dist/run/ids.d.ts +2 -0
  100. package/dist/run/ids.js +23 -0
  101. package/dist/runtime/background-shell.d.ts +1 -0
  102. package/dist/runtime/background-shell.js +23 -13
  103. package/dist/runtime/spawn-common.js +10 -0
  104. package/dist/services/auto-dream.d.ts +15 -4
  105. package/dist/services/auto-dream.js +20 -20
  106. package/dist/services/dream-consolidation.d.ts +2 -3
  107. package/dist/services/dream-consolidation.js +65 -15
  108. package/dist/services/extract-memories.d.ts +14 -5
  109. package/dist/services/extract-memories.js +20 -3
  110. package/dist/services/global-dream-promotion.d.ts +23 -0
  111. package/dist/services/global-dream-promotion.js +112 -0
  112. package/dist/services/memory-orchestrator.js +347 -34
  113. package/dist/session/memory.d.ts +61 -18
  114. package/dist/session/memory.js +342 -79
  115. package/dist/session/session-manager.d.ts +35 -1
  116. package/dist/session/session-manager.js +190 -3
  117. package/dist/session/transcript.d.ts +1 -1
  118. package/dist/session/transcript.js +17 -5
  119. package/dist/settings/manager.d.ts +1 -0
  120. package/dist/settings/manager.js +87 -37
  121. package/dist/settings/schema-export.d.ts +2 -3
  122. package/dist/settings/schema-export.js +2 -3
  123. package/dist/settings/schema.d.ts +21 -0
  124. package/dist/settings/schema.js +12 -0
  125. package/dist/skills/scanner.d.ts +3 -2
  126. package/dist/skills/scanner.js +12 -9
  127. package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
  128. package/dist/tool-system/builtin/background-jobs.js +12 -1
  129. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  130. package/dist/tool-system/builtin/background-work.js +51 -5
  131. package/dist/tool-system/builtin/config.d.ts +2 -1
  132. package/dist/tool-system/builtin/config.js +16 -11
  133. package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
  134. package/dist/tool-system/builtin/drive-claude-code.js +301 -47
  135. package/dist/tool-system/builtin/edit.js +5 -2
  136. package/dist/tool-system/builtin/generate-video.d.ts +1 -0
  137. package/dist/tool-system/builtin/generate-video.js +13 -4
  138. package/dist/tool-system/builtin/index.d.ts +8 -3
  139. package/dist/tool-system/builtin/index.js +37 -21
  140. package/dist/tool-system/builtin/lsp.d.ts +2 -1
  141. package/dist/tool-system/builtin/lsp.js +6 -3
  142. package/dist/tool-system/builtin/memory.js +40 -8
  143. package/dist/tool-system/builtin/notebook-edit.js +5 -2
  144. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  145. package/dist/tool-system/builtin/powershell.js +11 -7
  146. package/dist/tool-system/builtin/read.js +118 -6
  147. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  148. package/dist/tool-system/builtin/view-image.js +109 -19
  149. package/dist/tool-system/builtin/worktree.d.ts +4 -4
  150. package/dist/tool-system/builtin/worktree.js +297 -74
  151. package/dist/tool-system/builtin/write.js +5 -3
  152. package/dist/tool-system/context.d.ts +23 -1
  153. package/dist/tool-system/executor.d.ts +1 -5
  154. package/dist/tool-system/executor.js +94 -115
  155. package/dist/tool-system/mcp-manager.d.ts +18 -5
  156. package/dist/tool-system/mcp-manager.js +140 -74
  157. package/dist/tool-system/path-policy.d.ts +2 -0
  158. package/dist/tool-system/path-policy.js +41 -12
  159. package/dist/tool-system/permission.d.ts +28 -7
  160. package/dist/tool-system/permission.js +130 -49
  161. package/dist/tool-system/registry.js +11 -4
  162. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  163. package/dist/tool-system/tool-result-redaction.js +48 -0
  164. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  165. package/dist/tool-system/workspace-bridge.js +1 -0
  166. package/dist/types.d.ts +55 -4
  167. package/dist/utils/toolDisplay.js +1 -1
  168. package/package.json +4 -3
@@ -2,7 +2,14 @@ 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 { existsSync, realpathSync, statSync } from "node:fs";
8
+ import { extname, isAbsolute, relative, resolve, sep } from "node:path";
9
+ import { externalAgentSessionStore, } from "../../cc-orchestrator/external-agent-session-store.js";
10
+ import { logger } from "../../logging/logger.js";
11
+ export const DRIVE_AGENT_FOREGROUND_HANDOFF_MS = 110_000;
12
+ export const DRIVE_AGENT_TOOL_TIMEOUT_MS = 1_800_000;
6
13
  const CLI_ADAPTERS = {
7
14
  claude: { adapter: claudeAdapter, command: "claude" },
8
15
  codex: { adapter: codexAdapter, command: "codex" },
@@ -35,32 +42,248 @@ export const driveAgentToolDef = {
35
42
  inputSchema: {
36
43
  type: "object",
37
44
  properties: {
38
- prompt: { type: "string", description: "The task for the agent. Make it COMPLETE and self-contained: state the goal, a concrete definition of done, and (for open-ended work) an explicit scope bound so it finishes end-to-end and verifies its own work rather than sprawling or stopping half-way." },
39
- cli: { type: "string", enum: ["claude", "codex"], description: "Which external CLI to drive. 'claude' = Claude Code (default), 'codex' = OpenAI Codex. resumeSessionId is only valid against the same cli that produced it." },
40
- resumeSessionId: { type: "string", description: "Existing session id to resume (keeps context). Must come from a prior run of the SAME cli. Omit for a fresh session." },
45
+ prompt: {
46
+ type: "string",
47
+ description: "The task for the agent. Make it COMPLETE and self-contained: state the goal, a concrete definition of done, and (for open-ended work) an explicit scope bound so it finishes end-to-end and verifies its own work rather than sprawling or stopping half-way.",
48
+ },
49
+ cli: {
50
+ type: "string",
51
+ enum: ["claude", "codex"],
52
+ description: "Which external CLI to drive. 'claude' = Claude Code (default), 'codex' = OpenAI Codex. resumeSessionId is only valid against the same cli that produced it.",
53
+ },
54
+ resumeSessionId: {
55
+ type: "string",
56
+ description: "Existing session id to resume (keeps context). Must come from a prior run of the SAME cli. Omit for a fresh session.",
57
+ },
41
58
  cwd: { type: "string", description: "Working directory the run operates in." },
42
- permissionMode: { type: "string", enum: ["default", "acceptEdits", "bypassPermissions"], description: "Permission/sandbox level. Defaults to 'bypassPermissions' (full auto — needed so the agent's tools run unattended; there is no interactive approval loop here). For codex: default→read-only sandbox, acceptEdits→workspace-write, bypassPermissions→no sandbox. Pass 'default'/'acceptEdits' to gate." },
43
- background: { type: "boolean", description: "Defaults to TRUE: run in the background and notify you on completion (right for long tasks). Pass false to run in the foreground and get the result inline (only for quick tasks)." },
59
+ attachmentPaths: {
60
+ type: "array",
61
+ items: { type: "string" },
62
+ description: "Optional local file paths to hand to the driven agent. Paths must resolve inside cwd. Images are also passed to Codex with -i when the installed Codex CLI supports it; otherwise all paths are listed in the prompt.",
63
+ },
64
+ permissionMode: {
65
+ type: "string",
66
+ enum: ["default", "acceptEdits", "bypassPermissions"],
67
+ description: "Permission/sandbox level. Defaults to 'bypassPermissions' (full auto — needed so the agent's tools run unattended; there is no interactive approval loop here). For codex: default→read-only sandbox, acceptEdits→workspace-write, bypassPermissions→no sandbox. Pass 'default'/'acceptEdits' to gate.",
68
+ },
69
+ background: {
70
+ type: "boolean",
71
+ description: "Defaults to TRUE: run in the background and notify you on completion (right for long tasks). Pass false to run in the foreground and get the result inline (only for quick tasks).",
72
+ },
44
73
  },
45
74
  required: ["prompt", "cwd"],
46
75
  },
47
76
  };
48
77
  const defaultRunner = (opts) => {
49
78
  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" });
79
+ return runAgentOnce(adapter, {
80
+ command,
81
+ prompt: opts.prompt,
82
+ resumeSessionId: opts.resumeSessionId,
83
+ cwd: opts.cwd,
84
+ permissionMode: opts.permissionMode ?? "default",
85
+ imagePaths: opts.imagePaths,
86
+ }, opts.signal);
51
87
  };
88
+ function newDriveJobId() {
89
+ return `cc-${process.hrtime.bigint().toString(36)}`;
90
+ }
91
+ function isValidSessionId(sessionId) {
92
+ return typeof sessionId === "string" && sessionId.length > 0;
93
+ }
94
+ function argSignal(args) {
95
+ const signal = args.__signal;
96
+ return signal &&
97
+ typeof signal === "object" &&
98
+ typeof signal.aborted === "boolean" &&
99
+ typeof signal.addEventListener === "function"
100
+ ? signal
101
+ : undefined;
102
+ }
103
+ function startRun(runner, opts) {
104
+ return Promise.resolve().then(() => runner(opts));
105
+ }
106
+ function resolveAttachmentPaths(raw, cwd) {
107
+ if (raw === undefined)
108
+ return { paths: [] };
109
+ if (!Array.isArray(raw))
110
+ return { paths: [], error: "attachmentPaths must be an array of strings" };
111
+ const cwdReal = realpathSync(cwd);
112
+ const paths = [];
113
+ for (const item of raw) {
114
+ if (typeof item !== "string" || !item.trim()) {
115
+ return { paths: [], error: "attachmentPaths must contain only non-empty strings" };
116
+ }
117
+ const candidate = isAbsolute(item) ? item : resolve(cwd, item);
118
+ if (!existsSync(candidate))
119
+ return { paths: [], error: `attachment path not found: ${item}` };
120
+ const real = realpathSync(candidate);
121
+ const rel = relative(cwdReal, real);
122
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`)) {
123
+ return { paths: [], error: `attachment path is outside cwd: ${item}` };
124
+ }
125
+ const info = statSync(real);
126
+ if (!info.isFile())
127
+ return { paths: [], error: `attachment path is not a file: ${item}` };
128
+ paths.push(real);
129
+ }
130
+ return { paths };
131
+ }
132
+ const DRIVE_IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
133
+ function appendAttachmentPrompt(prompt, paths) {
134
+ if (paths.length === 0)
135
+ return prompt;
136
+ const lines = ["", "Attached files:"];
137
+ for (const path of paths) {
138
+ const kind = DRIVE_IMAGE_EXTS.has(extname(path).toLowerCase()) ? "image" : "file";
139
+ lines.push(`- ${path} (${kind})`);
140
+ }
141
+ return `${prompt}\n${lines.join("\n")}`;
142
+ }
143
+ function recordSuccessfulSession(store, cli, cwd, result) {
144
+ if (result.isError || !result.sessionId)
145
+ return;
146
+ try {
147
+ store.record({ cli, sessionId: result.sessionId, cwd });
148
+ }
149
+ catch (err) {
150
+ logger.warn("drive_agent.session_binding_record_failed", {
151
+ cat: "cc",
152
+ cli,
153
+ sessionId: result.sessionId,
154
+ cwd,
155
+ error: err instanceof Error ? err.message : String(err),
156
+ });
157
+ }
158
+ }
159
+ function duplicateCwdWarning(cwd, writable) {
160
+ if (!writable)
161
+ return undefined;
162
+ const running = backgroundJobRegistry.listRunningByCwd(cwd);
163
+ if (running.length === 0)
164
+ return undefined;
165
+ const ids = running.map((j) => j.jobId).join(", ");
166
+ 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.`;
167
+ }
168
+ function attachDriveCompletion(params) {
169
+ const { jobId, sessionId, label, cli, cwd, run, sessionStore } = params;
170
+ void run
171
+ .then((r) => {
172
+ recordSuccessfulSession(sessionStore, cli, cwd, r);
173
+ // Deliver the result back so the woken agent actually sees the answer
174
+ // (not just "a job finished"). Mirrors the video/sub-agent completion
175
+ // path — enqueue lands in the same notificationQueue the wakeup drains.
176
+ notificationQueue.enqueue(r.isError
177
+ ? {
178
+ agentId: jobId,
179
+ description: label,
180
+ status: "failed",
181
+ workKind: "cc",
182
+ error: r.finalText || "(no output)",
183
+ ccSessionId: r.sessionId || undefined,
184
+ enqueuedAt: Date.now(),
185
+ }
186
+ : {
187
+ agentId: jobId,
188
+ description: label,
189
+ status: "completed",
190
+ workKind: "cc",
191
+ finalText: r.finalText,
192
+ ccSessionId: r.sessionId || undefined,
193
+ enqueuedAt: Date.now(),
194
+ }, 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
+ // Retain the job in the panel with its result + the external CLI
200
+ // session id + changed files.
201
+ backgroundJobRegistry.finish(jobId, {
202
+ status: r.isError ? "failed" : "completed",
203
+ finalText: r.finalText || undefined,
204
+ ccSessionId: r.sessionId || undefined,
205
+ ...(changedFiles.length ? { changedFiles } : {}),
206
+ });
207
+ })
208
+ .catch((err) => {
209
+ const msg = err?.message ?? String(err);
210
+ notificationQueue.enqueue({
211
+ agentId: jobId,
212
+ description: label,
213
+ status: "failed",
214
+ workKind: "cc",
215
+ error: msg,
216
+ enqueuedAt: Date.now(),
217
+ }, sessionId);
218
+ backgroundJobRegistry.finish(jobId, { status: "failed", finalText: msg });
219
+ });
220
+ }
221
+ function trackBackgroundRun(params) {
222
+ const warning = duplicateCwdWarning(params.cwd, params.writable);
223
+ const jobId = newDriveJobId();
224
+ backgroundJobRegistry.start(jobId, params.sessionId, params.label, { cwd: params.cwd });
225
+ attachDriveCompletion({ ...params, jobId });
226
+ return { jobId, ...(warning ? { warning } : {}) };
227
+ }
228
+ async function waitForForegroundOrHandoff(run, handoffMs) {
229
+ if (handoffMs < 0) {
230
+ return { kind: "completed", result: await run };
231
+ }
232
+ let timer;
233
+ try {
234
+ return await Promise.race([
235
+ run.then((result) => ({ kind: "completed", result })),
236
+ new Promise((resolve) => {
237
+ timer = setTimeout(() => resolve({ kind: "handoff" }), handoffMs);
238
+ }),
239
+ ]);
240
+ }
241
+ finally {
242
+ if (timer)
243
+ clearTimeout(timer);
244
+ }
245
+ }
52
246
  /** Factory so tests can inject a fake runner. `fixedCli` (back-compat) forces a
53
247
  * cli and hides the `cli` arg — that's how DriveClaudeCode stays a thin alias. */
54
- export function makeDriveAgentTool(runner = defaultRunner, fixedCli) {
248
+ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {}) {
55
249
  return async (args, ctx) => {
56
250
  const prompt = typeof args.prompt === "string" ? args.prompt : "";
57
- const cwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
251
+ const rawRequestedCwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
252
+ const requestedCwd = normalizeCwdPath(rawRequestedCwd);
58
253
  if (!prompt)
59
254
  return "Error: prompt is required";
60
- const cli = fixedCli ?? (args.cli === "codex" ? "codex" : args.cli === "claude" || args.cli === undefined ? "claude" : "invalid");
255
+ const cli = fixedCli ??
256
+ (args.cli === "codex"
257
+ ? "codex"
258
+ : args.cli === "claude" || args.cli === undefined
259
+ ? "claude"
260
+ : "invalid");
61
261
  if (cli === "invalid")
62
262
  return `Error: unknown cli "${String(args.cli)}" (expected "claude" or "codex")`;
63
263
  const resumeSessionId = typeof args.resumeSessionId === "string" ? args.resumeSessionId : undefined;
264
+ const sessionStore = options.sessionStore ?? externalAgentSessionStore;
265
+ let cwd = requestedCwd;
266
+ let resumeNote = "";
267
+ if (resumeSessionId) {
268
+ const binding = sessionStore.get(cli, resumeSessionId);
269
+ if (binding) {
270
+ const storedCwd = normalizeCwdPath(binding.cwd);
271
+ if (!isExistingDirectory(storedCwd)) {
272
+ return `Error: cannot resume ${cli} session ${resumeSessionId}: stored cwd no longer exists or is not a directory: ${storedCwd}`;
273
+ }
274
+ if (storedCwd !== requestedCwd) {
275
+ cwd = storedCwd;
276
+ resumeNote = `Note: resume session ${resumeSessionId} is bound to stored cwd ${storedCwd}; ignoring requested cwd ${requestedCwd}.`;
277
+ logger.info("drive_agent.resume_forced_stored_cwd", {
278
+ cat: "cc",
279
+ cli,
280
+ sessionId: resumeSessionId,
281
+ requestedCwd,
282
+ storedCwd,
283
+ });
284
+ }
285
+ }
286
+ }
64
287
  // Default to bypassPermissions: this tool is a fire-one-turn delegation to
65
288
  // an external CLI with nobody watching for approvals, and there is no
66
289
  // interactive approval loop here — so under "default" a tool that needs
@@ -73,8 +296,27 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli) {
73
296
  args.permissionMode === "bypassPermissions"
74
297
  ? args.permissionMode
75
298
  : "bypassPermissions";
299
+ const resolvedAttachmentPaths = resolveAttachmentPaths(args.attachmentPaths, cwd);
300
+ if (resolvedAttachmentPaths.error)
301
+ return `Error: ${resolvedAttachmentPaths.error}`;
302
+ const attachmentPaths = resolvedAttachmentPaths.paths;
303
+ const promptWithAttachments = appendAttachmentPrompt(prompt, attachmentPaths);
304
+ const imagePaths = cli === "codex"
305
+ ? attachmentPaths.filter((path) => DRIVE_IMAGE_EXTS.has(extname(path).toLowerCase()))
306
+ : [];
76
307
  const cliName = cli === "codex" ? "Codex" : "Claude Code";
77
308
  const label = `DriveAgent(${cli}): ${prompt.slice(0, 40)}`;
309
+ const runOpts = {
310
+ cli,
311
+ prompt: promptWithAttachments,
312
+ resumeSessionId,
313
+ cwd,
314
+ permissionMode,
315
+ signal: ctx?.signal ?? argSignal(args),
316
+ imagePaths,
317
+ };
318
+ const foregroundHandoffMs = options.foregroundHandoffMs ?? DRIVE_AGENT_FOREGROUND_HANDOFF_MS;
319
+ const isWritableRun = permissionMode !== "default";
78
320
  // Background by default (these tasks are typically long). Only an explicit
79
321
  // background:false runs in the foreground and returns the result inline.
80
322
  const background = args.background !== false;
@@ -87,42 +329,49 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli) {
87
329
  if (typeof sessionId !== "string" || sessionId.length === 0) {
88
330
  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
331
  }
90
- const jobId = `cc-${process.hrtime.bigint().toString(36)}`;
91
- backgroundJobRegistry.start(jobId, sessionId, label);
92
- void runner({ cli, prompt, resumeSessionId, cwd, permissionMode })
93
- .then((r) => {
94
- // Deliver the result back so the woken agent actually sees the answer
95
- // (not just "a job finished"). Mirrors the video/sub-agent completion
96
- // path — enqueue lands in the same notificationQueue the wakeup drains.
97
- notificationQueue.enqueue(r.isError
98
- ? { agentId: jobId, description: label, status: "failed", workKind: "cc", error: r.finalText || "(no output)", ccSessionId: r.sessionId || undefined, enqueuedAt: Date.now() }
99
- : { agentId: jobId, description: label, status: "completed", workKind: "cc", finalText: r.finalText, ccSessionId: r.sessionId || undefined, enqueuedAt: Date.now() }, sessionId);
100
- // Attribute the files the external agent changed by parsing its own
101
- // transcript (#6) — those Edit/Write calls are invisible to the host's
102
- // in-session aggregator. Best-effort; [] on any failure.
103
- const changedFiles = r.sessionId
104
- ? readExternalChangedFiles(cli, cwd, r.sessionId)
105
- : [];
106
- // Retain the job in the panel with its result + the external CLI
107
- // session id + changed files.
108
- backgroundJobRegistry.finish(jobId, {
109
- status: r.isError ? "failed" : "completed",
110
- finalText: r.finalText || undefined,
111
- ccSessionId: r.sessionId || undefined,
112
- ...(changedFiles.length ? { changedFiles } : {}),
113
- });
114
- })
115
- .catch((err) => {
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 });
332
+ const tracked = trackBackgroundRun({
333
+ sessionId,
334
+ label,
335
+ cli,
336
+ cwd,
337
+ run: startRun(runner, runOpts),
338
+ sessionStore,
339
+ writable: isWritableRun,
340
+ });
341
+ return [
342
+ resumeNote,
343
+ `已在后台启动 ${cliName}(jobId ${tracked.jobId})。完成后会通知你结果,无需轮询。`,
344
+ tracked.warning,
345
+ ]
346
+ .filter(Boolean)
347
+ .join("\n");
348
+ }
349
+ const run = startRun(runner, runOpts);
350
+ const result = await waitForForegroundOrHandoff(run, foregroundHandoffMs);
351
+ if (result.kind === "handoff" && isValidSessionId(ctx?.sessionId)) {
352
+ const tracked = trackBackgroundRun({
353
+ sessionId: ctx.sessionId,
354
+ label,
355
+ cli,
356
+ cwd,
357
+ run,
358
+ sessionStore,
359
+ writable: isWritableRun,
119
360
  });
120
- return `已在后台启动 ${cliName}(jobId ${jobId})。完成后会通知你结果,无需轮询。`;
361
+ return [
362
+ resumeNote,
363
+ `${cliName} foreground run exceeded ${foregroundHandoffMs}ms; moved it to background (jobId ${tracked.jobId}). Completion will notify this session, so do not poll.`,
364
+ tracked.warning,
365
+ ]
366
+ .filter(Boolean)
367
+ .join("\n");
121
368
  }
122
- const r = await runner({ cli, prompt, resumeSessionId, cwd, permissionMode });
369
+ const r = result.kind === "completed" ? result.result : await run;
370
+ recordSuccessfulSession(sessionStore, cli, cwd, r);
371
+ const prefix = resumeNote ? `${resumeNote}\n` : "";
123
372
  if (r.isError)
124
- return `${cliName} 运行出错(session ${r.sessionId}):\n${r.finalText}`;
125
- return `${cliName} 完成(session ${r.sessionId}):\n${r.finalText}`;
373
+ return `${prefix}${cliName} 运行出错(session ${r.sessionId}):\n${r.finalText}`;
374
+ return `${prefix}${cliName} 完成(session ${r.sessionId}):\n${r.finalText}`;
126
375
  };
127
376
  }
128
377
  export const driveAgentTool = makeDriveAgentTool();
@@ -134,24 +383,29 @@ export const driveClaudeCodeToolDef = {
134
383
  name: "DriveClaudeCode",
135
384
  description: "(Alias of DriveAgent with cli:claude.) Delegate a task to the external Claude Code CLI " +
136
385
  "(drives `claude` for one turn). Prefer DriveAgent for new calls; this name is kept for " +
137
- "compatibility. " + driveAgentToolDef.description,
386
+ "compatibility. " +
387
+ driveAgentToolDef.description,
138
388
  inputSchema: {
139
389
  type: "object",
140
390
  properties: {
141
391
  // intentionally omit `cli` — this alias is always claude
142
392
  prompt: driveAgentToolDef.inputSchema.properties.prompt,
143
- resumeSessionId: { type: "string", description: "Existing CC session id to resume (keeps context). Omit for a fresh session." },
393
+ resumeSessionId: {
394
+ type: "string",
395
+ description: "Existing CC session id to resume (keeps context). Omit for a fresh session.",
396
+ },
144
397
  cwd: driveAgentToolDef.inputSchema.properties.cwd,
398
+ attachmentPaths: driveAgentToolDef.inputSchema.properties.attachmentPaths,
145
399
  permissionMode: driveAgentToolDef.inputSchema.properties.permissionMode,
146
400
  background: driveAgentToolDef.inputSchema.properties.background,
147
401
  },
148
402
  required: ["prompt", "cwd"],
149
403
  },
150
404
  };
151
- export function makeDriveClaudeCodeTool(runner) {
405
+ export function makeDriveClaudeCodeTool(runner, options) {
152
406
  const generic = runner
153
- ? ({ prompt, resumeSessionId, cwd, permissionMode }) => runner({ prompt, resumeSessionId, cwd, permissionMode })
407
+ ? ({ prompt, resumeSessionId, cwd, permissionMode, signal }) => runner({ prompt, resumeSessionId, cwd, permissionMode, signal })
154
408
  : undefined;
155
- return makeDriveAgentTool(generic ?? defaultRunner, "claude");
409
+ return makeDriveAgentTool(generic ?? defaultRunner, "claude", options);
156
410
  }
157
411
  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 filePath = args.file_path;
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 (!filePath)
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 ? args.image : undefined;
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.filter((x) => typeof x === "string")
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)
@@ -19,13 +19,18 @@ import type { ToolVisibilityContext } from "../context.js";
19
19
  * registry 会把它放进 ToolResult.contentBlocks。可选的 `result` 字段是给
20
20
  * transcript / 摘要用的纯文本镜像。沙箱执行类工具(Bash 等)可返回
21
21
  * `{ result, sandbox }`,registry 把 sandbox 透传到 ToolResult.sandbox 供 UI 显示。
22
+ * 敏感结果可返回 `{ result, sensitive, displayResult, transcriptResult }`;
23
+ * `result` 仅供当前模型轮使用,显示/持久化路径必须使用占位字段。
22
24
  */
23
25
  export type BuiltinToolResult = string | {
24
26
  contentBlocks: import("../../types.js").ContentBlock[];
25
27
  result?: string;
26
28
  } | {
27
29
  result: string;
28
- sandbox: import("../../types.js").ToolResult["sandbox"];
30
+ sandbox?: import("../../types.js").ToolResult["sandbox"];
31
+ sensitive?: boolean;
32
+ displayResult?: string;
33
+ transcriptResult?: string;
29
34
  };
30
35
  export type BuiltinToolFn = (args: Record<string, unknown>, ctx?: import("../context.js").ToolContext) => Promise<BuiltinToolResult>;
31
36
  export type BuiltinToolGuard = (ctx: ToolVisibilityContext) => boolean;
@@ -41,5 +46,5 @@ export declare const BUILTIN_TOOLS: BuiltinTool[];
41
46
  * Keyed by the tool's `name` (must match the toolDef name).
42
47
  */
43
48
  export declare const BUILTIN_TOOL_GUARDS: Map<string, BuiltinToolGuard>;
44
- /** UseCredential is available when the cwd's CredentialStore has ≥1 credential. */
45
- export declare function isUseCredentialAvailable(cwd: string): boolean;
49
+ /** UseCredential is available when the cwd's credential metadata has ≥1 entry. */
50
+ export declare function isUseCredentialAvailable(cwd: string, settingsScope?: import("../../settings/manager.js").SettingsScope): boolean;