@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.
Files changed (146) hide show
  1. package/dist/arena/arena.d.ts +2 -0
  2. package/dist/arena/arena.js +30 -1
  3. package/dist/arena/phases/adjudication.d.ts +2 -1
  4. package/dist/arena/phases/adjudication.js +2 -1
  5. package/dist/arena/phases/build-consensus.d.ts +2 -1
  6. package/dist/arena/phases/build-consensus.js +3 -1
  7. package/dist/arena/phases/cross-review.d.ts +3 -1
  8. package/dist/arena/phases/cross-review.js +8 -2
  9. package/dist/arena/phases/debate-rounds.d.ts +2 -1
  10. package/dist/arena/phases/debate-rounds.js +4 -2
  11. package/dist/arena/phases/participant-research.d.ts +2 -1
  12. package/dist/arena/phases/participant-research.js +3 -1
  13. package/dist/arena/phases/planning-detail-expansion.d.ts +2 -1
  14. package/dist/arena/phases/planning-detail-expansion.js +2 -1
  15. package/dist/arena/planner.d.ts +2 -1
  16. package/dist/arena/planner.js +2 -1
  17. package/dist/arena/types.d.ts +5 -1
  18. package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
  19. package/dist/cc-orchestrator/agent-adapter.js +4 -0
  20. package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
  21. package/dist/cc-orchestrator/codex-session-history.js +64 -4
  22. package/dist/cc-orchestrator/external-agent-changes.js +22 -5
  23. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -1
  24. package/dist/cc-orchestrator/external-agent-driver.js +202 -38
  25. package/dist/cc-orchestrator/session-history.d.ts +35 -0
  26. package/dist/cc-orchestrator/session-history.js +96 -13
  27. package/dist/cli/agent-server-tcp.js +13 -2
  28. package/dist/context/manager.d.ts +3 -3
  29. package/dist/context/manager.js +6 -6
  30. package/dist/context/token-counter.js +5 -3
  31. package/dist/context/tool-result-storage.d.ts +6 -0
  32. package/dist/context/tool-result-storage.js +25 -4
  33. package/dist/credentials/access.d.ts +11 -1
  34. package/dist/credentials/access.js +77 -1
  35. package/dist/credentials/index.d.ts +3 -1
  36. package/dist/credentials/index.js +2 -0
  37. package/dist/credentials/oauth.d.ts +25 -0
  38. package/dist/credentials/oauth.js +179 -0
  39. package/dist/credentials/store.d.ts +2 -1
  40. package/dist/credentials/store.js +19 -8
  41. package/dist/credentials/types.d.ts +84 -1
  42. package/dist/credentials/types.js +16 -1
  43. package/dist/engine/engine.d.ts +67 -34
  44. package/dist/engine/engine.js +448 -247
  45. package/dist/engine/goal.d.ts +19 -0
  46. package/dist/engine/goal.js +16 -6
  47. package/dist/engine/input-attachments.js +156 -13
  48. package/dist/engine/run-image-input.d.ts +22 -0
  49. package/dist/engine/run-image-input.js +195 -0
  50. package/dist/engine/session-title.d.ts +2 -1
  51. package/dist/engine/session-title.js +4 -1
  52. package/dist/engine/steer-queue.d.ts +3 -1
  53. package/dist/engine/steer-queue.js +10 -2
  54. package/dist/engine/turn-loop.d.ts +48 -1
  55. package/dist/engine/turn-loop.js +307 -37
  56. package/dist/engine/types.d.ts +6 -2
  57. package/dist/git/worktree/crud.d.ts +3 -0
  58. package/dist/git/worktree/crud.js +32 -3
  59. package/dist/git/worktree/git-exec.d.ts +2 -2
  60. package/dist/git/worktree/git-exec.js +47 -11
  61. package/dist/git/worktree/query.d.ts +8 -7
  62. package/dist/git/worktree/query.js +27 -20
  63. package/dist/hooks/events.d.ts +3 -0
  64. package/dist/hooks/events.js +0 -3
  65. package/dist/hooks/goal-stop-hook.d.ts +44 -2
  66. package/dist/hooks/goal-stop-hook.js +775 -52
  67. package/dist/hooks/registry.js +3 -0
  68. package/dist/hooks/shell-runner.d.ts +12 -1
  69. package/dist/hooks/shell-runner.js +160 -9
  70. package/dist/index.d.ts +7 -6
  71. package/dist/index.js +6 -5
  72. package/dist/llm/client-base.js +12 -10
  73. package/dist/llm/types.d.ts +12 -5
  74. package/dist/plugins/pluginCommandHook.d.ts +4 -4
  75. package/dist/plugins/pluginCommandHook.js +111 -13
  76. package/dist/preset/index.js +14 -4
  77. package/dist/protocol/chat-session-manager.d.ts +13 -2
  78. package/dist/protocol/chat-session-manager.js +90 -18
  79. package/dist/protocol/chat-session.d.ts +12 -0
  80. package/dist/protocol/chat-session.js +30 -5
  81. package/dist/protocol/client.d.ts +5 -2
  82. package/dist/protocol/client.js +22 -1
  83. package/dist/protocol/server.d.ts +25 -11
  84. package/dist/protocol/server.js +291 -73
  85. package/dist/protocol/types.d.ts +36 -2
  86. package/dist/protocol/types.js +2 -0
  87. package/dist/services/dream-consolidation.d.ts +3 -0
  88. package/dist/services/dream-consolidation.js +4 -1
  89. package/dist/services/index.d.ts +1 -1
  90. package/dist/services/index.js +1 -1
  91. package/dist/services/oauth.d.ts +34 -10
  92. package/dist/services/oauth.js +233 -98
  93. package/dist/session/session-manager.d.ts +35 -6
  94. package/dist/session/session-manager.js +396 -27
  95. package/dist/session/transcript.d.ts +30 -1
  96. package/dist/session/transcript.js +119 -4
  97. package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
  98. package/dist/tool-system/builtin/agent-notifications.js +19 -7
  99. package/dist/tool-system/builtin/agent.js +5 -1
  100. package/dist/tool-system/builtin/arena.js +1 -0
  101. package/dist/tool-system/builtin/background-jobs.d.ts +28 -5
  102. package/dist/tool-system/builtin/background-jobs.js +109 -7
  103. package/dist/tool-system/builtin/background-work.d.ts +6 -1
  104. package/dist/tool-system/builtin/background-work.js +5 -1
  105. package/dist/tool-system/builtin/bash.d.ts +3 -5
  106. package/dist/tool-system/builtin/bash.js +10 -5
  107. package/dist/tool-system/builtin/browser-tools.d.ts +2 -2
  108. package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
  109. package/dist/tool-system/builtin/cron-list.definition.js +6 -0
  110. package/dist/tool-system/builtin/cron.d.ts +1 -2
  111. package/dist/tool-system/builtin/cron.js +9 -7
  112. package/dist/tool-system/builtin/drive-claude-code.d.ts +7 -0
  113. package/dist/tool-system/builtin/drive-claude-code.js +307 -20
  114. package/dist/tool-system/builtin/edit.d.ts +2 -1
  115. package/dist/tool-system/builtin/edit.js +12 -4
  116. package/dist/tool-system/builtin/generate-video.d.ts +4 -0
  117. package/dist/tool-system/builtin/generate-video.js +138 -21
  118. package/dist/tool-system/builtin/glob.d.ts +2 -1
  119. package/dist/tool-system/builtin/glob.js +28 -3
  120. package/dist/tool-system/builtin/grep.d.ts +1 -0
  121. package/dist/tool-system/builtin/grep.js +82 -17
  122. package/dist/tool-system/builtin/index.d.ts +25 -11
  123. package/dist/tool-system/builtin/index.js +60 -5
  124. package/dist/tool-system/builtin/sleep.d.ts +1 -2
  125. package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
  126. package/dist/tool-system/builtin/sleep.definition.js +28 -0
  127. package/dist/tool-system/builtin/sleep.js +1 -22
  128. package/dist/tool-system/builtin/video-providers.d.ts +12 -15
  129. package/dist/tool-system/builtin/video-providers.js +1 -0
  130. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  131. package/dist/tool-system/builtin/web-fetch.js +44 -3
  132. package/dist/tool-system/builtin/worktree.js +25 -7
  133. package/dist/tool-system/builtin/write.d.ts +2 -1
  134. package/dist/tool-system/builtin/write.js +14 -4
  135. package/dist/tool-system/context.d.ts +35 -5
  136. package/dist/tool-system/executor.js +24 -8
  137. package/dist/tool-system/mcp-manager.d.ts +20 -2
  138. package/dist/tool-system/mcp-manager.js +111 -12
  139. package/dist/tool-system/path-policy.d.ts +19 -0
  140. package/dist/tool-system/path-policy.js +62 -1
  141. package/dist/tool-system/permission.d.ts +43 -3
  142. package/dist/tool-system/permission.js +383 -30
  143. package/dist/tool-system/registry.d.ts +3 -2
  144. package/dist/tool-system/registry.js +52 -34
  145. package/dist/types.d.ts +38 -7
  146. package/package.json +1 -1
@@ -0,0 +1,3 @@
1
+ /** CronList tool metadata, shared by registration and presets. */
2
+ import type { ToolDefinition } from "../../types.js";
3
+ export declare const cronListToolDef: ToolDefinition;
@@ -0,0 +1,6 @@
1
+ /** CronList tool metadata, shared by registration and presets. */
2
+ export const cronListToolDef = {
3
+ name: "CronList",
4
+ description: "List all scheduled cron jobs.",
5
+ inputSchema: { type: "object", properties: {} },
6
+ };
@@ -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: { type: "string", description: "Short human-readable name for the job (e.g. '工作日晨间简报')" },
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: { type: "string", description: "Working directory / project the job runs in. Optional." },
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. For a quick task where you want the answer inline, pass background:false. " +
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 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.`;
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
- recordSuccessfulSession(sessionStore, cli, cwd, r);
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
- backgroundJobRegistry.start(jobId, params.sessionId, params.label, { cwd: params.cwd });
225
- attachDriveCompletion({ ...params, jobId });
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 runOpts = {
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
- run: startRun(runner, runOpts),
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 run = startRun(runner, runOpts);
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
- run,
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, writeFile } from "node:fs/promises";
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 raw = await readFile(filePath, "utf-8");
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
- await writeFile(filePath, applyEol(updatedLf, eol), "utf-8");
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;