@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
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Registry of NON-agent background jobs (GenerateVideo poll loops, DriveAgent
3
3
  * external CLI runs). Mirrors the role `asyncAgentRegistry` plays for background
4
- * sub-agents, but a job is not an agent — it has no transcript, no abort(), and
5
- * must NOT show up in AgentStatus.
4
+ * sub-agents, but a job is not an agent — it has no transcript and must NOT
5
+ * show up in AgentStatus.
6
6
  *
7
7
  * The engine's wait-for-background loop parks the turn until a session has no
8
8
  * more RUNNING jobs, so the goal-stop-hook doesn't force the model to busy-loop
@@ -21,11 +21,14 @@
21
21
  * loop observe the same instance.
22
22
  */
23
23
  type Listener = () => void;
24
- export type BackgroundJobStatus = "running" | "completed" | "failed";
24
+ export type BackgroundJobStatus = "running" | "cancelling" | "completed" | "failed" | "cancelled";
25
+ export type BackgroundJobKind = "drive-agent" | "video" | "job";
25
26
  /** A background job, running or finished. */
26
27
  export interface BackgroundJobEntry {
27
28
  jobId: string;
28
29
  sessionId: string;
30
+ /** Machine-readable kind for tools that need to target a subset of jobs. */
31
+ kind?: BackgroundJobKind;
29
32
  /** Human description (e.g. "Generating video: <prompt>"). Shown to the goal
30
33
  * judge (running) and in the background panel (all). */
31
34
  description: string;
@@ -42,16 +45,29 @@ export interface BackgroundJobEntry {
42
45
  changedFiles?: string[];
43
46
  /** Working directory for jobs that operate on the filesystem, e.g. DriveAgent. */
44
47
  cwd?: string;
48
+ /** DriveAgent prompt summary, separate from the UI-oriented description. */
49
+ promptSummary?: string;
50
+ /** External CLI kind for DriveAgent jobs. */
51
+ cli?: string;
52
+ /** Client message that launched this external job. */
53
+ originClientMessageId?: string;
54
+ /** Optional cancellation hook for jobs backed by a live process. */
55
+ abort?: () => void | Promise<void>;
45
56
  }
46
57
  /** Outcome passed to finish() to record how a job ended. */
47
58
  export interface BackgroundJobOutcome {
48
- status?: "completed" | "failed";
59
+ status?: "completed" | "failed" | "cancelled";
49
60
  finalText?: string;
50
61
  ccSessionId?: string;
51
62
  changedFiles?: string[];
52
63
  }
53
64
  export interface BackgroundJobStartOptions {
65
+ kind?: BackgroundJobKind;
54
66
  cwd?: string;
67
+ promptSummary?: string;
68
+ cli?: string;
69
+ originClientMessageId?: string;
70
+ abort?: () => void | Promise<void>;
55
71
  }
56
72
  declare class BackgroundJobRegistry {
57
73
  private jobs;
@@ -61,7 +77,11 @@ declare class BackgroundJobRegistry {
61
77
  /** Mark a job terminal (retained, not deleted). Unknown id is a no-op (no
62
78
  * notify) so a double-finish or a finish after reset stays quiet. */
63
79
  finish(jobId: string, outcome?: BackgroundJobOutcome): void;
64
- /** True while any RUNNING job spawned by `sessionId` remains. */
80
+ get(jobId: string): BackgroundJobEntry | undefined;
81
+ /** Persist artifacts discovered while a running job is winding down. */
82
+ recordArtifacts(jobId: string, artifacts: Pick<BackgroundJobOutcome, "ccSessionId" | "changedFiles">): void;
83
+ cancel(jobId: string, outcome?: Omit<BackgroundJobOutcome, "status">): Promise<boolean>;
84
+ /** True while any running/cancelling job spawned by `sessionId` remains. */
65
85
  hasRunningForSession(sessionId: string): boolean;
66
86
  /** Running jobs spawned by `sessionId`. Feeds the goal judge's task list. */
67
87
  listRunningForSession(sessionId: string): BackgroundJobEntry[];
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Registry of NON-agent background jobs (GenerateVideo poll loops, DriveAgent
3
3
  * external CLI runs). Mirrors the role `asyncAgentRegistry` plays for background
4
- * sub-agents, but a job is not an agent — it has no transcript, no abort(), and
5
- * must NOT show up in AgentStatus.
4
+ * sub-agents, but a job is not an agent — it has no transcript and must NOT
5
+ * show up in AgentStatus.
6
6
  *
7
7
  * The engine's wait-for-background loop parks the turn until a session has no
8
8
  * more RUNNING jobs, so the goal-stop-hook doesn't force the model to busy-loop
@@ -27,6 +27,29 @@ function isValidSessionId(sid) {
27
27
  /** Generous per-session cap on retained terminal jobs — a leak backstop, not a
28
28
  * UX limit; a human never spawns this many background jobs in one session. */
29
29
  const MAX_TERMINAL_JOBS_PER_SESSION = 50;
30
+ const CANCEL_WAIT_TIMEOUT_MS = 5_000;
31
+ function isActiveStatus(status) {
32
+ return status === "running" || status === "cancelling";
33
+ }
34
+ async function waitForAbort(abort) {
35
+ let timer;
36
+ try {
37
+ await Promise.race([
38
+ Promise.resolve().then(abort),
39
+ new Promise((resolve) => {
40
+ timer = setTimeout(resolve, CANCEL_WAIT_TIMEOUT_MS);
41
+ }),
42
+ ]);
43
+ }
44
+ catch {
45
+ // Cancellation is best-effort; the terminal state still closes after the
46
+ // abort hook settles or the hard deadline expires.
47
+ }
48
+ finally {
49
+ if (timer)
50
+ clearTimeout(timer);
51
+ }
52
+ }
30
53
  class BackgroundJobRegistry {
31
54
  jobs = new Map(); // jobId -> entry (insertion-ordered)
32
55
  listeners = new Set();
@@ -37,10 +60,17 @@ class BackgroundJobRegistry {
37
60
  this.jobs.set(jobId, {
38
61
  jobId,
39
62
  sessionId,
63
+ ...(options?.kind ? { kind: options.kind } : {}),
40
64
  description,
41
65
  status: "running",
42
66
  startedAt: Date.now(),
43
67
  ...(options?.cwd ? { cwd: normalizeCwdPath(options.cwd) } : {}),
68
+ ...(options?.promptSummary ? { promptSummary: options.promptSummary } : {}),
69
+ ...(options?.cli ? { cli: options.cli } : {}),
70
+ ...(options?.originClientMessageId
71
+ ? { originClientMessageId: options.originClientMessageId }
72
+ : {}),
73
+ ...(options?.abort ? { abort: options.abort } : {}),
44
74
  });
45
75
  this.notify();
46
76
  }
@@ -50,6 +80,8 @@ class BackgroundJobRegistry {
50
80
  const entry = this.jobs.get(jobId);
51
81
  if (!entry)
52
82
  return;
83
+ if (entry.status !== "running")
84
+ return;
53
85
  entry.status = outcome?.status ?? "completed";
54
86
  entry.finishedAt = Date.now();
55
87
  if (outcome?.finalText !== undefined)
@@ -61,22 +93,65 @@ class BackgroundJobRegistry {
61
93
  this.evictTerminalOverCap(entry.sessionId);
62
94
  this.notify();
63
95
  }
64
- /** True while any RUNNING job spawned by `sessionId` remains. */
96
+ get(jobId) {
97
+ return this.jobs.get(jobId);
98
+ }
99
+ /** Persist artifacts discovered while a running job is winding down. */
100
+ recordArtifacts(jobId, artifacts) {
101
+ const entry = this.jobs.get(jobId);
102
+ if (!entry || !isActiveStatus(entry.status))
103
+ return;
104
+ if (artifacts.ccSessionId !== undefined)
105
+ entry.ccSessionId = artifacts.ccSessionId;
106
+ if (artifacts.changedFiles !== undefined)
107
+ entry.changedFiles = artifacts.changedFiles;
108
+ }
109
+ async cancel(jobId, outcome) {
110
+ const entry = this.jobs.get(jobId);
111
+ if (!entry)
112
+ return false;
113
+ if (entry.status !== "running")
114
+ return false;
115
+ // Publish a non-terminal guard before invoking external code. A completion
116
+ // callback may synchronously re-enter finish(); it must not publish
117
+ // completed/failed, while cwd/session conflict checks must still see the
118
+ // process as active until the abort hook confirms exit.
119
+ entry.status = "cancelling";
120
+ this.notify();
121
+ if (entry.abort)
122
+ await waitForAbort(entry.abort);
123
+ // Session teardown/reset may have removed the entry while termination was
124
+ // in flight. Do not resurrect or notify for a closed session.
125
+ if (this.jobs.get(jobId) !== entry || entry.status !== "cancelling")
126
+ return false;
127
+ entry.status = "cancelled";
128
+ entry.finishedAt = Date.now();
129
+ if (outcome?.finalText !== undefined)
130
+ entry.finalText = outcome.finalText;
131
+ if (outcome?.ccSessionId !== undefined)
132
+ entry.ccSessionId = outcome.ccSessionId;
133
+ if (outcome?.changedFiles !== undefined)
134
+ entry.changedFiles = outcome.changedFiles;
135
+ this.evictTerminalOverCap(entry.sessionId);
136
+ this.notify();
137
+ return true;
138
+ }
139
+ /** True while any running/cancelling job spawned by `sessionId` remains. */
65
140
  hasRunningForSession(sessionId) {
66
141
  for (const e of this.jobs.values()) {
67
- if (e.sessionId === sessionId && e.status === "running")
142
+ if (e.sessionId === sessionId && isActiveStatus(e.status))
68
143
  return true;
69
144
  }
70
145
  return false;
71
146
  }
72
147
  /** Running jobs spawned by `sessionId`. Feeds the goal judge's task list. */
73
148
  listRunningForSession(sessionId) {
74
- return [...this.jobs.values()].filter((e) => e.sessionId === sessionId && e.status === "running");
149
+ return [...this.jobs.values()].filter((e) => e.sessionId === sessionId && isActiveStatus(e.status));
75
150
  }
76
151
  /** Running jobs, across all sessions, that are operating in the same cwd. */
77
152
  listRunningByCwd(cwd) {
78
153
  const normalized = normalizeCwdPath(cwd);
79
- return [...this.jobs.values()].filter((e) => e.status === "running" && e.cwd === normalized);
154
+ return [...this.jobs.values()].filter((e) => isActiveStatus(e.status) && e.cwd === normalized);
80
155
  }
81
156
  /** All jobs (running + retained terminal) for `sessionId`. Feeds the panel. */
82
157
  listForSession(sessionId) {
@@ -89,12 +164,26 @@ class BackgroundJobRegistry {
89
164
  /** Drop every job of a session — called when the session is deleted/closed. */
90
165
  dropForSession(sessionId) {
91
166
  let removed = false;
167
+ const aborts = [];
92
168
  for (const [id, e] of this.jobs) {
93
169
  if (e.sessionId === sessionId) {
170
+ // Delete first so a synchronous/queued completion caused by abort sees
171
+ // no live registry entry and cannot publish a late result for a closed
172
+ // session. Terminal jobs have nothing left to stop.
94
173
  this.jobs.delete(id);
174
+ if (isActiveStatus(e.status) && e.abort)
175
+ aborts.push(e.abort);
95
176
  removed = true;
96
177
  }
97
178
  }
179
+ for (const abort of aborts) {
180
+ try {
181
+ void Promise.resolve(abort()).catch(() => undefined);
182
+ }
183
+ catch {
184
+ // teardown is best-effort across jobs; continue reaping the rest
185
+ }
186
+ }
98
187
  if (removed)
99
188
  this.notify();
100
189
  }
@@ -106,12 +195,21 @@ class BackgroundJobRegistry {
106
195
  };
107
196
  /** Test helper: drop all tracked jobs. */
108
197
  reset() {
198
+ const aborts = [...this.jobs.values()].flatMap((entry) => isActiveStatus(entry.status) && entry.abort ? [entry.abort] : []);
109
199
  this.jobs.clear();
200
+ for (const abort of aborts) {
201
+ try {
202
+ void Promise.resolve(abort()).catch(() => undefined);
203
+ }
204
+ catch {
205
+ // ignore
206
+ }
207
+ }
110
208
  }
111
209
  /** Backstop: keep at most MAX_TERMINAL_JOBS_PER_SESSION terminal jobs per
112
210
  * session, evicting the oldest (insertion order). Running jobs never evicted. */
113
211
  evictTerminalOverCap(sessionId) {
114
- const terminal = [...this.jobs.values()].filter((e) => e.sessionId === sessionId && e.status !== "running");
212
+ const terminal = [...this.jobs.values()].filter((e) => e.sessionId === sessionId && !isActiveStatus(e.status));
115
213
  const over = terminal.length - MAX_TERMINAL_JOBS_PER_SESSION;
116
214
  if (over <= 0)
117
215
  return;
@@ -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;