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

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 (57) hide show
  1. package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
  2. package/dist/cc-orchestrator/agent-adapter.js +4 -0
  3. package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
  4. package/dist/cc-orchestrator/codex-session-history.js +64 -4
  5. package/dist/cc-orchestrator/external-agent-changes.js +22 -5
  6. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -1
  7. package/dist/cc-orchestrator/external-agent-driver.js +202 -38
  8. package/dist/cc-orchestrator/session-history.d.ts +35 -0
  9. package/dist/cc-orchestrator/session-history.js +96 -13
  10. package/dist/credentials/access.d.ts +1 -0
  11. package/dist/credentials/access.js +2 -0
  12. package/dist/credentials/index.d.ts +2 -1
  13. package/dist/credentials/index.js +1 -0
  14. package/dist/credentials/oauth.d.ts +20 -0
  15. package/dist/credentials/oauth.js +114 -0
  16. package/dist/credentials/store.d.ts +1 -0
  17. package/dist/credentials/store.js +3 -1
  18. package/dist/credentials/types.d.ts +47 -1
  19. package/dist/engine/engine.d.ts +6 -2
  20. package/dist/engine/engine.js +159 -192
  21. package/dist/engine/goal.d.ts +17 -0
  22. package/dist/engine/goal.js +16 -6
  23. package/dist/engine/input-attachments.js +156 -13
  24. package/dist/engine/run-image-input.d.ts +22 -0
  25. package/dist/engine/run-image-input.js +195 -0
  26. package/dist/engine/steer-queue.d.ts +3 -1
  27. package/dist/engine/steer-queue.js +10 -2
  28. package/dist/engine/turn-loop.d.ts +30 -1
  29. package/dist/engine/turn-loop.js +112 -17
  30. package/dist/hooks/goal-stop-hook.d.ts +33 -1
  31. package/dist/hooks/goal-stop-hook.js +202 -34
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +2 -2
  34. package/dist/preset/index.js +14 -4
  35. package/dist/protocol/server.js +1 -1
  36. package/dist/protocol/types.d.ts +2 -0
  37. package/dist/session/session-manager.js +34 -1
  38. package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
  39. package/dist/tool-system/builtin/agent-notifications.js +19 -7
  40. package/dist/tool-system/builtin/background-jobs.d.ts +25 -5
  41. package/dist/tool-system/builtin/background-jobs.js +105 -7
  42. package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
  43. package/dist/tool-system/builtin/cron-list.definition.js +6 -0
  44. package/dist/tool-system/builtin/cron.d.ts +1 -2
  45. package/dist/tool-system/builtin/cron.js +9 -7
  46. package/dist/tool-system/builtin/drive-claude-code.d.ts +7 -0
  47. package/dist/tool-system/builtin/drive-claude-code.js +307 -20
  48. package/dist/tool-system/builtin/index.js +15 -3
  49. package/dist/tool-system/builtin/sleep.d.ts +1 -2
  50. package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
  51. package/dist/tool-system/builtin/sleep.definition.js +28 -0
  52. package/dist/tool-system/builtin/sleep.js +1 -22
  53. package/dist/tool-system/context.d.ts +18 -0
  54. package/dist/tool-system/mcp-manager.d.ts +14 -2
  55. package/dist/tool-system/mcp-manager.js +56 -7
  56. package/dist/types.d.ts +23 -7
  57. package/package.json +1 -1
@@ -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
  }
@@ -20,12 +20,14 @@ import { enterPlanModeToolDef, enterPlanModeTool, exitPlanModeToolDef, exitPlanM
20
20
  import { toolSearchToolDef, toolSearchTool } from "./tool-search.js";
21
21
  import { todoWriteToolDef, todoWriteTool } from "./task.js";
22
22
  import { enterWorktreeToolDef, enterWorktreeTool, exitWorktreeToolDef, exitWorktreeTool, switchSessionWorkspaceToolDef, switchSessionWorkspaceTool, } from "./worktree.js";
23
- import { sleepToolDef, sleepTool } from "./sleep.js";
23
+ import { sleepToolDef } from "./sleep.definition.js";
24
+ import { sleepTool } from "./sleep.js";
24
25
  import { configToolDef, configTool } from "./config.js";
25
26
  import { notebookEditToolDef, notebookEditTool } from "./notebook-edit.js";
26
27
  import { lspToolDef, lspTool } from "./lsp.js";
27
- import { cronCreateToolDef, cronCreateTool, cronDeleteToolDef, cronDeleteTool, cronListToolDef, cronListTool, } from "./cron.js";
28
- import { driveClaudeCodeToolDef, driveClaudeCodeTool, driveAgentToolDef, driveAgentTool, DRIVE_AGENT_TOOL_TIMEOUT_MS, } from "./drive-claude-code.js";
28
+ import { cronCreateToolDef, cronCreateTool, cronDeleteToolDef, cronDeleteTool, cronListTool, } from "./cron.js";
29
+ import { cronListToolDef } from "./cron-list.definition.js";
30
+ import { driveClaudeCodeToolDef, driveClaudeCodeTool, driveAgentToolDef, driveAgentTool, driveAgentJobsToolDef, driveAgentJobsTool, DRIVE_AGENT_TOOL_TIMEOUT_MS, } from "./drive-claude-code.js";
29
31
  import { checkQuotaToolDef, checkQuotaTool } from "./check-quota.js";
30
32
  import { skillToolDef, skillTool } from "./skill.js";
31
33
  import { mcpToolDef, mcpToolExecute, listMcpResourcesToolDef, listMcpResourcesTool, readMcpResourceToolDef, readMcpResourceTool, } from "./mcp-tools.js";
@@ -448,6 +450,16 @@ export const BUILTIN_TOOLS = [
448
450
  },
449
451
  execute: driveAgentTool,
450
452
  },
453
+ {
454
+ definition: {
455
+ ...driveAgentJobsToolDef,
456
+ source: "builtin",
457
+ permissionDefault: "allow",
458
+ isReadOnly: false,
459
+ isConcurrencySafe: false,
460
+ },
461
+ execute: driveAgentJobsTool,
462
+ },
451
463
  // Back-compat alias: DriveClaudeCode = DriveAgent pinned to cli:claude. Kept
452
464
  // registered so old prompts/memories that name DriveClaudeCode still resolve.
453
465
  {
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * SleepTool — pause execution for a specified duration.
3
3
  */
4
- import type { ToolDefinition } from "../../types.js";
5
- export declare const sleepToolDef: ToolDefinition;
4
+ export { sleepToolDef } from "./sleep.definition.js";
6
5
  export declare function sleepTool(args: Record<string, unknown>): Promise<string>;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Sleep tool metadata.
3
+ *
4
+ * Kept separate from the executor so registration and presets can consume the
5
+ * definition without importing implementation code.
6
+ */
7
+ import type { ToolDefinition } from "../../types.js";
8
+ export declare const sleepToolDef: ToolDefinition;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Sleep tool metadata.
3
+ *
4
+ * Kept separate from the executor so registration and presets can consume the
5
+ * definition without importing implementation code.
6
+ */
7
+ export const sleepToolDef = {
8
+ name: "Sleep",
9
+ description: "Pause execution for a brief, deterministic wait (e.g. letting a just-started service settle for a few seconds). " +
10
+ "Do NOT use Sleep to poll for or wait on background work (background shells, async sub-agents, video generation): " +
11
+ "the system wakes you automatically when that work completes — just end your turn instead of looping Sleep. " +
12
+ "If you want a safety net in case a background task hangs and never signals completion, do NOT loop Sleep either — " +
13
+ "instead end your turn and schedule a one-shot self-wakeup with CronCreate " +
14
+ "({ schedule: '5m', once: true, continueInSession: true, permissionLevel: 'read-only', " +
15
+ "prompt: 'check whether <that task> finished; if still running, wait again' }). " +
16
+ "That returns control to you at the interval without burning a turn spinning. " +
17
+ "Maximum duration is 300 seconds (5 minutes).",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ seconds: {
22
+ type: "number",
23
+ description: "Number of seconds to sleep (max 300)",
24
+ },
25
+ },
26
+ required: ["seconds"],
27
+ },
28
+ };
@@ -1,28 +1,7 @@
1
1
  /**
2
2
  * SleepTool — pause execution for a specified duration.
3
3
  */
4
- export const sleepToolDef = {
5
- name: "Sleep",
6
- description: "Pause execution for a brief, deterministic wait (e.g. letting a just-started service settle for a few seconds). " +
7
- "Do NOT use Sleep to poll for or wait on background work (background shells, async sub-agents, video generation): " +
8
- "the system wakes you automatically when that work completes — just end your turn instead of looping Sleep. " +
9
- "If you want a safety net in case a background task hangs and never signals completion, do NOT loop Sleep either — " +
10
- "instead end your turn and schedule a one-shot self-wakeup with CronCreate " +
11
- "({ schedule: '5m', once: true, continueInSession: true, permissionLevel: 'read-only', " +
12
- "prompt: 'check whether <that task> finished; if still running, wait again' }). " +
13
- "That returns control to you at the interval without burning a turn spinning. " +
14
- "Maximum duration is 300 seconds (5 minutes).",
15
- inputSchema: {
16
- type: "object",
17
- properties: {
18
- seconds: {
19
- type: "number",
20
- description: "Number of seconds to sleep (max 300)",
21
- },
22
- },
23
- required: ["seconds"],
24
- },
25
- };
4
+ export { sleepToolDef } from "./sleep.definition.js";
26
5
  export async function sleepTool(args) {
27
6
  const seconds = Math.min(Math.max(Number(args.seconds) || 1, 0.1), 300);
28
7
  const signal = args.__signal;
@@ -184,6 +184,15 @@ export interface ToolVisibilityContext {
184
184
  hasGoal: boolean;
185
185
  settingsScope?: import("../settings/manager.js").SettingsScope;
186
186
  }
187
+ export interface ExternalFileChangesRecord {
188
+ jobId: string;
189
+ description: string;
190
+ cli: "claude" | "codex";
191
+ cwd: string;
192
+ status: "completed" | "failed" | "cancelled";
193
+ changedFiles: string[];
194
+ originClientMessageId?: string;
195
+ }
187
196
  export interface ToolContext {
188
197
  /** Active working directory for this Engine. */
189
198
  cwd: string;
@@ -261,6 +270,15 @@ export interface ToolContext {
261
270
  * narrow context, or standalone tool tests. (B2 — Gate 1, standard §S3.)
262
271
  */
263
272
  sessionId?: string;
273
+ /** Stable id of the real user turn that owns async work launched here. */
274
+ originClientMessageId?: string;
275
+ /**
276
+ * Persist file attribution from an async external DriveAgent completion in
277
+ * the owning CodeShell transcript. The callback closes over the live parent
278
+ * transcript, so a background job can record its files after the run that
279
+ * launched it has already returned.
280
+ */
281
+ recordExternalFileChanges?: (record: ExternalFileChangesRecord) => void;
264
282
  /**
265
283
  * Skill names the user has hidden from the LLM (full namespaced names
266
284
  * for plugin skills, e.g. "docs:pdf"). The skill builtin tool uses
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js";
7
7
  import type { MCPServerConfig, RegisteredTool } from "../types.js";
8
+ import type { CredentialType } from "../credentials/types.js";
8
9
  import { ToolRegistry } from "./registry.js";
9
10
  import { type CredentialAccess } from "../credentials/access.js";
10
11
  interface MCPResourceInfo {
@@ -21,6 +22,16 @@ interface MCPResourceInfo {
21
22
  * and the env var.
22
23
  */
23
24
  export declare function readRequiredEnv(serverName: string, field: string, envName: string): string;
25
+ export interface ResolvedMcpCredential {
26
+ secret: string;
27
+ type?: CredentialType;
28
+ label?: string;
29
+ }
30
+ export type HttpCredentialResolverResult = string | ResolvedMcpCredential | undefined;
31
+ export interface BuildHttpHeadersOptions {
32
+ now?: () => number;
33
+ oauthRefreshSkewMs?: number;
34
+ }
24
35
  /**
25
36
  * Build the spawned stdio server's environment. Priority (lowest → highest):
26
37
  * a minimal inherited allowlist < forwarded `envVars` (read from process.env by
@@ -34,8 +45,9 @@ export declare function buildStdioEnv(serverName: string, config: MCPServerConfi
34
45
  * base; env-sourced secrets (`bearerTokenEnvVar`, `envHeaders`) layer on top
35
46
  * and win on conflict. Pure + exported for unit testing.
36
47
  */
37
- export declare function buildHttpHeaders(serverName: string, config: MCPServerConfig, resolveCredential?: (id: string) => string | undefined): Record<string, string>;
38
- export declare function buildHttpHeadersWithCredentialAccess(serverName: string, config: MCPServerConfig, access?: Pick<CredentialAccess, "resolveValue">): Promise<Record<string, string>>;
48
+ export declare function buildHttpHeaders(serverName: string, config: MCPServerConfig, resolveCredential?: (id: string) => HttpCredentialResolverResult, options?: BuildHttpHeadersOptions): Record<string, string>;
49
+ export declare function bearerTokenFromMcpCredential(serverName: string, credentialId: string, credential: ResolvedMcpCredential, options?: BuildHttpHeadersOptions): string;
50
+ export declare function buildHttpHeadersWithCredentialAccess(serverName: string, config: MCPServerConfig, access?: Pick<CredentialAccess, "resolveValue"> & Partial<Pick<CredentialAccess, "resolveMeta">>, options?: BuildHttpHeadersOptions): Promise<Record<string, string>>;
39
51
  /**
40
52
  * Infer the transport when the config doesn't name one: a url-only entry is
41
53
  * HTTP, everything else stdio. This is the CC `.mcp.json` convention —