@cjhyy/code-shell-core 0.6.0-rc.9 → 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 (193) hide show
  1. package/THIRD_PARTY_NOTICES.md +206 -0
  2. package/dist/automation/scheduler.d.ts +13 -7
  3. package/dist/automation/scheduler.js +116 -37
  4. package/dist/capability-control/service.d.ts +2 -0
  5. package/dist/capability-control/service.js +4 -2
  6. package/dist/cc-orchestrator/agent-adapter.d.ts +4 -0
  7. package/dist/cc-orchestrator/agent-adapter.js +11 -1
  8. package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
  9. package/dist/cc-orchestrator/codex-session-history.js +65 -5
  10. package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
  11. package/dist/cc-orchestrator/cwd-normalize.js +19 -0
  12. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  13. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  14. package/dist/cc-orchestrator/external-agent-changes.js +22 -5
  15. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
  16. package/dist/cc-orchestrator/external-agent-driver.js +265 -50
  17. package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
  18. package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
  19. package/dist/cc-orchestrator/session-history.d.ts +35 -0
  20. package/dist/cc-orchestrator/session-history.js +98 -15
  21. package/dist/cli/agent-server-stdio.js +9 -2
  22. package/dist/context/compaction.d.ts +8 -1
  23. package/dist/context/compaction.js +49 -4
  24. package/dist/context/manager.d.ts +16 -2
  25. package/dist/context/manager.js +103 -19
  26. package/dist/credentials/access.d.ts +57 -0
  27. package/dist/credentials/access.js +185 -0
  28. package/dist/credentials/index.d.ts +3 -1
  29. package/dist/credentials/index.js +2 -0
  30. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  31. package/dist/credentials/inject-credential-tool.js +30 -11
  32. package/dist/credentials/oauth.d.ts +20 -0
  33. package/dist/credentials/oauth.js +114 -0
  34. package/dist/credentials/store.d.ts +1 -0
  35. package/dist/credentials/store.js +3 -1
  36. package/dist/credentials/types.d.ts +49 -3
  37. package/dist/credentials/use-credential-tool.d.ts +9 -1
  38. package/dist/credentials/use-credential-tool.js +58 -45
  39. package/dist/engine/engine.d.ts +23 -3
  40. package/dist/engine/engine.js +380 -255
  41. package/dist/engine/goal.d.ts +17 -0
  42. package/dist/engine/goal.js +16 -6
  43. package/dist/engine/image-policy.d.ts +6 -0
  44. package/dist/engine/image-policy.js +17 -6
  45. package/dist/engine/input-attachments.d.ts +13 -0
  46. package/dist/engine/input-attachments.js +398 -0
  47. package/dist/engine/model-facade.d.ts +5 -2
  48. package/dist/engine/model-facade.js +4 -4
  49. package/dist/engine/parse-task.d.ts +10 -0
  50. package/dist/engine/parse-task.js +5 -0
  51. package/dist/engine/run-image-input.d.ts +22 -0
  52. package/dist/engine/run-image-input.js +195 -0
  53. package/dist/engine/steer-queue.d.ts +3 -1
  54. package/dist/engine/steer-queue.js +10 -2
  55. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  56. package/dist/engine/streaming-tool-queue.js +11 -7
  57. package/dist/engine/turn-loop.d.ts +37 -2
  58. package/dist/engine/turn-loop.js +228 -43
  59. package/dist/engine/types.d.ts +8 -0
  60. package/dist/git/worktree/crud.d.ts +69 -0
  61. package/dist/git/worktree/crud.js +206 -0
  62. package/dist/git/worktree/diff.d.ts +14 -0
  63. package/dist/git/worktree/diff.js +82 -0
  64. package/dist/git/worktree/git-exec.d.ts +7 -0
  65. package/dist/git/worktree/git-exec.js +51 -0
  66. package/dist/git/worktree/index.d.ts +5 -0
  67. package/dist/git/worktree/index.js +5 -0
  68. package/dist/git/worktree/query.d.ts +42 -0
  69. package/dist/git/worktree/query.js +121 -0
  70. package/dist/git/worktree/slug.d.ts +11 -0
  71. package/dist/git/worktree/slug.js +58 -0
  72. package/dist/git/worktree.d.ts +1 -84
  73. package/dist/git/worktree.js +5 -230
  74. package/dist/hooks/goal-stop-hook.d.ts +33 -1
  75. package/dist/hooks/goal-stop-hook.js +202 -34
  76. package/dist/index.d.ts +31 -28
  77. package/dist/index.js +27 -25
  78. package/dist/logging/logger.js +6 -6
  79. package/dist/logging/sanitize-messages.d.ts +10 -2
  80. package/dist/logging/sanitize-messages.js +21 -6
  81. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  82. package/dist/plugins/installer/checkUpdate.js +4 -2
  83. package/dist/plugins/installer/install.js +2 -0
  84. package/dist/plugins/installer/installFromSource.js +9 -1
  85. package/dist/plugins/installer/parseSource.d.ts +4 -1
  86. package/dist/plugins/installer/parseSource.js +28 -10
  87. package/dist/plugins/installer/sourcePath.d.ts +9 -0
  88. package/dist/plugins/installer/sourcePath.js +50 -0
  89. package/dist/plugins/installer/update.d.ts +4 -1
  90. package/dist/plugins/installer/update.js +5 -3
  91. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  92. package/dist/plugins/parseMarketplaceInput.js +4 -3
  93. package/dist/plugins/pluginInstaller.js +24 -22
  94. package/dist/preset/index.d.ts +1 -0
  95. package/dist/preset/index.js +30 -13
  96. package/dist/prompt/sections/base.md +1 -1
  97. package/dist/protocol/chat-session-manager.d.ts +2 -0
  98. package/dist/protocol/chat-session-manager.js +38 -2
  99. package/dist/protocol/chat-session.d.ts +3 -0
  100. package/dist/protocol/chat-session.js +1 -0
  101. package/dist/protocol/client.d.ts +9 -4
  102. package/dist/protocol/client.js +18 -1
  103. package/dist/protocol/server.d.ts +30 -0
  104. package/dist/protocol/server.js +247 -47
  105. package/dist/protocol/types.d.ts +49 -0
  106. package/dist/protocol/types.js +6 -0
  107. package/dist/run/FileRunStore.js +10 -1
  108. package/dist/run/Heartbeat.js +12 -0
  109. package/dist/run/RunApprovalBackend.d.ts +3 -0
  110. package/dist/run/RunApprovalBackend.js +41 -6
  111. package/dist/run/RunLock.js +2 -0
  112. package/dist/run/RunManager.d.ts +2 -0
  113. package/dist/run/RunManager.js +64 -24
  114. package/dist/run/ids.d.ts +2 -0
  115. package/dist/run/ids.js +23 -0
  116. package/dist/runtime/background-shell.d.ts +1 -0
  117. package/dist/runtime/background-shell.js +23 -13
  118. package/dist/runtime/spawn-common.js +10 -0
  119. package/dist/services/auto-dream.d.ts +15 -4
  120. package/dist/services/auto-dream.js +20 -20
  121. package/dist/services/dream-consolidation.d.ts +2 -3
  122. package/dist/services/dream-consolidation.js +65 -15
  123. package/dist/services/extract-memories.d.ts +14 -5
  124. package/dist/services/extract-memories.js +20 -3
  125. package/dist/services/global-dream-promotion.d.ts +23 -0
  126. package/dist/services/global-dream-promotion.js +112 -0
  127. package/dist/services/memory-orchestrator.js +347 -34
  128. package/dist/session/memory.d.ts +61 -18
  129. package/dist/session/memory.js +342 -79
  130. package/dist/session/session-manager.d.ts +35 -1
  131. package/dist/session/session-manager.js +224 -4
  132. package/dist/session/transcript.d.ts +1 -1
  133. package/dist/session/transcript.js +17 -5
  134. package/dist/settings/manager.d.ts +1 -0
  135. package/dist/settings/manager.js +87 -37
  136. package/dist/settings/schema-export.d.ts +2 -3
  137. package/dist/settings/schema-export.js +2 -3
  138. package/dist/settings/schema.d.ts +21 -0
  139. package/dist/settings/schema.js +12 -0
  140. package/dist/skills/scanner.d.ts +3 -2
  141. package/dist/skills/scanner.js +12 -9
  142. package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
  143. package/dist/tool-system/builtin/agent-notifications.js +19 -7
  144. package/dist/tool-system/builtin/background-jobs.d.ts +35 -6
  145. package/dist/tool-system/builtin/background-jobs.js +116 -7
  146. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  147. package/dist/tool-system/builtin/background-work.js +51 -5
  148. package/dist/tool-system/builtin/config.d.ts +2 -1
  149. package/dist/tool-system/builtin/config.js +16 -11
  150. package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
  151. package/dist/tool-system/builtin/cron-list.definition.js +6 -0
  152. package/dist/tool-system/builtin/cron.d.ts +1 -2
  153. package/dist/tool-system/builtin/cron.js +9 -7
  154. package/dist/tool-system/builtin/drive-claude-code.d.ts +23 -2
  155. package/dist/tool-system/builtin/drive-claude-code.js +589 -48
  156. package/dist/tool-system/builtin/edit.js +5 -2
  157. package/dist/tool-system/builtin/generate-video.d.ts +1 -0
  158. package/dist/tool-system/builtin/generate-video.js +13 -4
  159. package/dist/tool-system/builtin/index.d.ts +8 -3
  160. package/dist/tool-system/builtin/index.js +50 -22
  161. package/dist/tool-system/builtin/lsp.d.ts +2 -1
  162. package/dist/tool-system/builtin/lsp.js +6 -3
  163. package/dist/tool-system/builtin/memory.js +40 -8
  164. package/dist/tool-system/builtin/notebook-edit.js +5 -2
  165. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  166. package/dist/tool-system/builtin/powershell.js +11 -7
  167. package/dist/tool-system/builtin/read.js +118 -6
  168. package/dist/tool-system/builtin/sleep.d.ts +1 -2
  169. package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
  170. package/dist/tool-system/builtin/sleep.definition.js +28 -0
  171. package/dist/tool-system/builtin/sleep.js +1 -22
  172. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  173. package/dist/tool-system/builtin/view-image.js +109 -19
  174. package/dist/tool-system/builtin/worktree.d.ts +4 -4
  175. package/dist/tool-system/builtin/worktree.js +297 -74
  176. package/dist/tool-system/builtin/write.js +5 -3
  177. package/dist/tool-system/context.d.ts +41 -1
  178. package/dist/tool-system/executor.d.ts +1 -5
  179. package/dist/tool-system/executor.js +94 -115
  180. package/dist/tool-system/mcp-manager.d.ts +31 -6
  181. package/dist/tool-system/mcp-manager.js +193 -78
  182. package/dist/tool-system/path-policy.d.ts +2 -0
  183. package/dist/tool-system/path-policy.js +41 -12
  184. package/dist/tool-system/permission.d.ts +28 -7
  185. package/dist/tool-system/permission.js +130 -49
  186. package/dist/tool-system/registry.js +11 -4
  187. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  188. package/dist/tool-system/tool-result-redaction.js +48 -0
  189. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  190. package/dist/tool-system/workspace-bridge.js +1 -0
  191. package/dist/types.d.ts +78 -11
  192. package/dist/utils/toolDisplay.js +1 -1
  193. package/package.json +4 -3
@@ -1,69 +1,284 @@
1
- import { spawn } from "node:child_process";
1
+ import { execFile, spawn } from "node:child_process";
2
2
  import { createInterface } from "node:readline";
3
3
  import { pathWithCommonBins } from "./cc-capability.js";
4
+ import { killProcessGroup } from "../runtime/spawn-common.js";
4
5
  /** Pure: reduce collected output lines + exit code to a result. Unit-testable. */
5
6
  export function runWithLines(adapter, lines, exitCode) {
6
7
  const parsed = adapter.parseResult(lines);
7
8
  return { ...parsed, exitCode, lines };
8
9
  }
9
- /** Spawn ONE headless agent run, collect stream-json to exit, return result.
10
- * No time concept — a single turn. Honors AbortSignal (kills the child). */
11
- export function runAgentOnce(adapter, opts, signal) {
12
- return new Promise((resolve, reject) => {
13
- const args = adapter.buildArgs({
14
- prompt: opts.prompt,
15
- resumeSessionId: opts.resumeSessionId,
16
- permissionMode: opts.permissionMode ?? "default",
17
- cwd: opts.cwd,
18
- });
19
- // claude takes the prompt in argv (`-p <prompt>`) and wants stdin closed
20
- // (verified: avoids a 3s wait). codex `exec` reads the prompt from stdin
21
- // (argv ends with `-`), so adapters that set promptViaStdin get a piped
22
- // stdin we write the prompt to.
23
- const viaStdin = adapter.promptViaStdin === true;
24
- const child = spawn(opts.command, args, {
25
- cwd: opts.cwd,
26
- env: { ...process.env, PATH: pathWithCommonBins() },
27
- // NOT detached: this child is owned by the (long-lived) worker that reads
28
- // its stdout. Detaching orphaned it across a worker/app restart — the
29
- // reader promise then never resolved and the completion notification never
30
- // fired (the "后台任务没返回" bug). Bound to the worker, it lives or dies
31
- // with the process that's actually listening for its result.
32
- detached: false,
33
- stdio: [viaStdin ? "pipe" : "ignore", "pipe", "pipe"],
10
+ const codexImageFlagCache = new Map();
11
+ const CODEX_IMAGE_PROBE_TIMEOUT_MS = 5_000;
12
+ const AGENT_TERMINATE_GRACE_MS = 500;
13
+ function listPosixProcessTree(rootPid) {
14
+ return new Promise((resolve) => {
15
+ execFile("ps", ["-axo", "pid=,ppid="], { encoding: "utf8" }, (err, stdout) => {
16
+ if (err) {
17
+ resolve([rootPid]);
18
+ return;
19
+ }
20
+ const childrenByParent = new Map();
21
+ for (const line of stdout.split("\n")) {
22
+ const [pidText, parentText] = line.trim().split(/\s+/, 2);
23
+ const pid = Number(pidText);
24
+ const parentPid = Number(parentText);
25
+ if (!Number.isInteger(pid) || !Number.isInteger(parentPid))
26
+ continue;
27
+ const children = childrenByParent.get(parentPid) ?? [];
28
+ children.push(pid);
29
+ childrenByParent.set(parentPid, children);
30
+ }
31
+ const pids = [];
32
+ const visit = (pid) => {
33
+ if (pids.includes(pid))
34
+ return;
35
+ pids.push(pid);
36
+ for (const childPid of childrenByParent.get(pid) ?? [])
37
+ visit(childPid);
38
+ };
39
+ visit(rootPid);
40
+ resolve(pids);
34
41
  });
35
- if (viaStdin && child.stdin) {
36
- child.stdin.end(opts.prompt);
42
+ });
43
+ }
44
+ function isPidAlive(pid) {
45
+ try {
46
+ process.kill(pid, 0);
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ function delay(ms) {
54
+ return new Promise((resolve) => setTimeout(resolve, ms));
55
+ }
56
+ async function waitForPidsToExit(pids, timeoutMs) {
57
+ const deadline = Date.now() + timeoutMs;
58
+ while ([...pids].some(isPidAlive)) {
59
+ if (Date.now() >= deadline)
60
+ return false;
61
+ await delay(25);
62
+ }
63
+ return true;
64
+ }
65
+ function signalPids(pids, killSignal) {
66
+ for (const pid of pids) {
67
+ try {
68
+ process.kill(pid, killSignal);
37
69
  }
38
- const lines = [];
39
- if (child.stdout) {
40
- const rl = createInterface({ input: child.stdout });
41
- rl.on("line", (line) => lines.push(line));
70
+ catch {
71
+ // already gone
42
72
  }
43
- // Not detached → no own process group, so kill the child directly (a
44
- // negative-pid group kill would target the worker's group). claude has no
45
- // long-lived child tree of its own here, so a direct SIGTERM is sufficient.
46
- const onAbort = () => {
73
+ }
74
+ }
75
+ async function terminateAttachedProcessTree(child) {
76
+ const pid = child.pid;
77
+ if (!pid) {
78
+ try {
79
+ child.kill("SIGKILL");
80
+ }
81
+ catch {
82
+ // already gone
83
+ }
84
+ return;
85
+ }
86
+ if (process.platform === "win32") {
87
+ // Windows has no POSIX SIGTERM semantics or negative-pid groups. Reap the
88
+ // tree via taskkill, then use positive-pid process.kill as the fallback the
89
+ // platform supports when taskkill is unavailable/races.
90
+ await killProcessGroup(pid, { graceMs: AGENT_TERMINATE_GRACE_MS });
91
+ if (isPidAlive(pid)) {
47
92
  try {
48
- child.kill("SIGTERM");
93
+ process.kill(pid);
49
94
  }
50
- catch { /* already gone */ }
51
- };
52
- signal?.addEventListener("abort", onAbort, { once: true });
53
- child.on("error", (err) => {
95
+ catch {
96
+ // already gone
97
+ }
98
+ }
99
+ return;
100
+ }
101
+ const trackedPids = new Set(await listPosixProcessTree(pid));
102
+ signalPids(trackedPids, "SIGTERM");
103
+ if (await waitForPidsToExit(trackedPids, AGENT_TERMINATE_GRACE_MS))
104
+ return;
105
+ // Capture descendants created during the grace window while the leader is
106
+ // still addressable, but retain the first snapshot so reparented stubborn
107
+ // descendants cannot disappear from the escalation set.
108
+ for (const treePid of await listPosixProcessTree(pid))
109
+ trackedPids.add(treePid);
110
+ signalPids(trackedPids, "SIGKILL");
111
+ await waitForPidsToExit(trackedPids, 1_000);
112
+ }
113
+ function abortError() {
114
+ const err = new Error("Agent run aborted");
115
+ err.name = "AbortError";
116
+ return err;
117
+ }
118
+ function throwIfAborted(signal) {
119
+ if (signal?.aborted)
120
+ throw abortError();
121
+ }
122
+ function createCodexImageProbe(command, cwd, signal) {
123
+ return new Promise((resolve, reject) => {
124
+ if (signal?.aborted) {
125
+ reject(abortError());
126
+ return;
127
+ }
128
+ const child = spawn(command, ["exec", "--help"], {
129
+ cwd,
130
+ env: { ...process.env, PATH: pathWithCommonBins() },
131
+ windowsHide: true,
132
+ stdio: ["ignore", "pipe", "pipe"],
133
+ });
134
+ let out = "";
135
+ let settled = false;
136
+ let terminating = false;
137
+ const cleanup = () => {
138
+ clearTimeout(timer);
54
139
  signal?.removeEventListener("abort", onAbort);
55
- // A missing binary is the most common failure (user hasn't installed the
56
- // CLI, or GUI-launched Electron's PATH misses it). Turn the cryptic
57
- // "spawn codex ENOENT" into something actionable that names the command.
58
- if (err.code === "ENOENT") {
59
- reject(new Error(`未找到命令 "${opts.command}"。请先安装该 CLI 并确保它在 PATH 中(${adapter.kind === "codex" ? "Codex CLI" : "Claude Code CLI"})。`));
140
+ };
141
+ const done = (value) => {
142
+ if (settled)
60
143
  return;
61
- }
144
+ settled = true;
145
+ cleanup();
146
+ resolve(value);
147
+ };
148
+ const fail = (err) => {
149
+ if (settled)
150
+ return;
151
+ settled = true;
152
+ cleanup();
62
153
  reject(err);
154
+ };
155
+ const onAbort = () => {
156
+ if (settled || terminating)
157
+ return;
158
+ terminating = true;
159
+ cleanup();
160
+ void terminateAttachedProcessTree(child).then(() => fail(abortError()));
161
+ };
162
+ const timer = setTimeout(() => {
163
+ if (settled || terminating)
164
+ return;
165
+ terminating = true;
166
+ cleanup();
167
+ void terminateAttachedProcessTree(child).then(() => done(false));
168
+ }, CODEX_IMAGE_PROBE_TIMEOUT_MS);
169
+ signal?.addEventListener("abort", onAbort, { once: true });
170
+ if (signal?.aborted)
171
+ onAbort();
172
+ child.stdout?.on("data", (chunk) => {
173
+ out += chunk.toString("utf8");
63
174
  });
64
- child.on("exit", (code) => {
65
- signal?.removeEventListener("abort", onAbort);
66
- resolve(runWithLines(adapter, lines, code));
175
+ child.stderr?.on("data", (chunk) => {
176
+ out += chunk.toString("utf8");
177
+ });
178
+ child.on("error", () => {
179
+ if (!terminating)
180
+ done(false);
181
+ });
182
+ child.on("exit", () => {
183
+ if (!terminating)
184
+ done(/(?:^|\s)-i(?:,|\s)|--image\b/.test(out));
67
185
  });
68
186
  });
69
187
  }
188
+ export function detectCodexImageInput(command, cwd, signal) {
189
+ if (signal)
190
+ return createCodexImageProbe(command, cwd, signal);
191
+ const cached = codexImageFlagCache.get(command);
192
+ if (cached)
193
+ return cached;
194
+ const probe = createCodexImageProbe(command, cwd);
195
+ codexImageFlagCache.set(command, probe);
196
+ return probe;
197
+ }
198
+ /** Spawn ONE headless agent run, collect stream-json to exit, return result.
199
+ * No time concept — a single turn. Honors AbortSignal (kills the child). */
200
+ export function runAgentOnce(adapter, opts, signal) {
201
+ return new Promise((resolve, reject) => {
202
+ void (async () => {
203
+ throwIfAborted(signal);
204
+ const codexImageInputSupported = adapter.kind === "codex" && (opts.imagePaths?.length ?? 0) > 0
205
+ ? await detectCodexImageInput(opts.command, opts.cwd, signal).catch((err) => {
206
+ if (signal?.aborted || err?.name === "AbortError")
207
+ throw err;
208
+ return false;
209
+ })
210
+ : false;
211
+ throwIfAborted(signal);
212
+ const args = adapter.buildArgs({
213
+ prompt: opts.prompt,
214
+ resumeSessionId: opts.resumeSessionId,
215
+ model: opts.model,
216
+ permissionMode: opts.permissionMode ?? "default",
217
+ cwd: opts.cwd,
218
+ imagePaths: opts.imagePaths,
219
+ codexImageInputSupported,
220
+ });
221
+ throwIfAborted(signal);
222
+ // claude takes the prompt in argv (`-p <prompt>`) and wants stdin closed
223
+ // (verified: avoids a 3s wait). codex `exec` reads the prompt from stdin
224
+ // (argv ends with `-`), so adapters that set promptViaStdin get a piped
225
+ // stdin we write the prompt to.
226
+ const viaStdin = adapter.promptViaStdin === true;
227
+ const child = spawn(opts.command, args, {
228
+ cwd: opts.cwd,
229
+ env: { ...process.env, PATH: pathWithCommonBins() },
230
+ // Keep the agent in the owning worker/app process group. If that owner
231
+ // is force-terminated, the CLI and its descendants must not escape into
232
+ // a detached group that can keep editing the workspace unseen.
233
+ detached: false,
234
+ stdio: [viaStdin ? "pipe" : "ignore", "pipe", "pipe"],
235
+ });
236
+ let settled = false;
237
+ let abortRequested = false;
238
+ let termination;
239
+ const cleanup = () => {
240
+ signal?.removeEventListener("abort", onAbort);
241
+ };
242
+ const onAbort = () => {
243
+ if (abortRequested)
244
+ return;
245
+ abortRequested = true;
246
+ termination = terminateAttachedProcessTree(child);
247
+ };
248
+ signal?.addEventListener("abort", onAbort, { once: true });
249
+ if (signal?.aborted)
250
+ onAbort();
251
+ if (viaStdin && child.stdin) {
252
+ child.stdin.end(opts.prompt);
253
+ }
254
+ const lines = [];
255
+ if (child.stdout) {
256
+ const rl = createInterface({ input: child.stdout });
257
+ rl.on("line", (line) => lines.push(line));
258
+ }
259
+ child.on("error", (err) => {
260
+ if (settled)
261
+ return;
262
+ settled = true;
263
+ cleanup();
264
+ // A missing binary is the most common failure (user hasn't installed the
265
+ // CLI, or GUI-launched Electron's PATH misses it). Turn the cryptic
266
+ // "spawn codex ENOENT" into something actionable that names the command.
267
+ if (err.code === "ENOENT") {
268
+ reject(new Error(`未找到命令 "${opts.command}"。请先安装该 CLI 并确保它在 PATH 中(${adapter.kind === "codex" ? "Codex CLI" : "Claude Code CLI"})。`));
269
+ return;
270
+ }
271
+ reject(err);
272
+ });
273
+ child.on("exit", (code) => {
274
+ if (settled)
275
+ return;
276
+ settled = true;
277
+ cleanup();
278
+ void (termination ?? Promise.resolve()).then(() => {
279
+ resolve(runWithLines(adapter, lines, code));
280
+ });
281
+ });
282
+ })().catch(reject);
283
+ });
284
+ }
@@ -0,0 +1,23 @@
1
+ export type ExternalAgentCli = "claude" | "codex";
2
+ export interface ExternalAgentSessionBinding {
3
+ cli: ExternalAgentCli;
4
+ sessionId: string;
5
+ cwd: string;
6
+ worktreePath?: string;
7
+ worktreeBranch?: string;
8
+ updatedAt: number;
9
+ }
10
+ export type ExternalAgentSessionRecord = Omit<ExternalAgentSessionBinding, "updatedAt"> & {
11
+ updatedAt?: number;
12
+ };
13
+ export declare function defaultExternalAgentSessionStorePath(): string;
14
+ export declare class ExternalAgentSessionStore {
15
+ private readonly file;
16
+ constructor(file?: string);
17
+ get(cli: ExternalAgentCli, sessionId: string): ExternalAgentSessionBinding | undefined;
18
+ record(binding: ExternalAgentSessionRecord): void;
19
+ private load;
20
+ private save;
21
+ private withLock;
22
+ }
23
+ export declare const externalAgentSessionStore: ExternalAgentSessionStore;
@@ -0,0 +1,146 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { codeShellHome } from "../session/session-manager.js";
4
+ import { logger } from "../logging/logger.js";
5
+ import { normalizeCwdPath } from "./cwd-normalize.js";
6
+ const LOCK_WAIT_MS = 5_000;
7
+ const LOCK_STALE_MS = 30_000;
8
+ const LOCK_POLL_MS = 10;
9
+ export function defaultExternalAgentSessionStorePath() {
10
+ return join(codeShellHome(), "external-agent-sessions.json");
11
+ }
12
+ export class ExternalAgentSessionStore {
13
+ file;
14
+ constructor(file = defaultExternalAgentSessionStorePath()) {
15
+ this.file = file;
16
+ }
17
+ get(cli, sessionId) {
18
+ if (!sessionId)
19
+ return undefined;
20
+ return this.load().find((s) => s.cli === cli && s.sessionId === sessionId);
21
+ }
22
+ record(binding) {
23
+ if (!binding.sessionId || !binding.cwd)
24
+ return;
25
+ const next = {
26
+ cli: binding.cli,
27
+ sessionId: binding.sessionId,
28
+ cwd: normalizeCwdPath(binding.cwd),
29
+ ...(binding.worktreePath ? { worktreePath: binding.worktreePath } : {}),
30
+ ...(binding.worktreeBranch ? { worktreeBranch: binding.worktreeBranch } : {}),
31
+ updatedAt: binding.updatedAt ?? Date.now(),
32
+ };
33
+ this.withLock(() => {
34
+ const sessions = this.load().filter((s) => !(s.cli === binding.cli && s.sessionId === binding.sessionId));
35
+ sessions.push(next);
36
+ this.save(sessions);
37
+ });
38
+ }
39
+ load() {
40
+ if (!existsSync(this.file))
41
+ return [];
42
+ try {
43
+ const raw = readFileSync(this.file, "utf-8");
44
+ const parsed = JSON.parse(raw);
45
+ if (!parsed || !Array.isArray(parsed.sessions))
46
+ return [];
47
+ return parsed.sessions.filter(isBinding).map(normalizeBinding);
48
+ }
49
+ catch (err) {
50
+ logger.warn("external_agent_session_store.load_failed", {
51
+ cat: "cc",
52
+ file: this.file,
53
+ error: err instanceof Error ? err.message : String(err),
54
+ });
55
+ return [];
56
+ }
57
+ }
58
+ save(sessions) {
59
+ const dir = dirname(this.file);
60
+ if (!existsSync(dir))
61
+ mkdirSync(dir, { recursive: true });
62
+ const snapshot = { version: 1, sessions };
63
+ const tmp = `${this.file}.${process.pid}.${Date.now()}.tmp`;
64
+ try {
65
+ writeFileSync(tmp, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
66
+ renameSync(tmp, this.file);
67
+ }
68
+ catch (err) {
69
+ rmSync(tmp, { force: true });
70
+ throw err;
71
+ }
72
+ }
73
+ withLock(fn) {
74
+ const dir = dirname(this.file);
75
+ if (!existsSync(dir))
76
+ mkdirSync(dir, { recursive: true });
77
+ // TODO: move this sync polling lock to an async write queue; callers can
78
+ // otherwise block the event loop for up to LOCK_WAIT_MS under contention.
79
+ const lockDir = `${this.file}.lock`;
80
+ const deadline = Date.now() + LOCK_WAIT_MS;
81
+ while (true) {
82
+ try {
83
+ mkdirSync(lockDir);
84
+ break;
85
+ }
86
+ catch (err) {
87
+ const code = err.code;
88
+ if (code !== "EEXIST")
89
+ throw err;
90
+ if (removeStaleLock(lockDir))
91
+ continue;
92
+ if (Date.now() >= deadline) {
93
+ throw new Error(`timed out waiting for external agent session store lock: ${lockDir}`, {
94
+ cause: err,
95
+ });
96
+ }
97
+ sleepSync(LOCK_POLL_MS);
98
+ }
99
+ }
100
+ try {
101
+ return fn();
102
+ }
103
+ finally {
104
+ rmSync(lockDir, { recursive: true, force: true });
105
+ }
106
+ }
107
+ }
108
+ function normalizeBinding(binding) {
109
+ return { ...binding, cwd: normalizeCwdPath(binding.cwd) };
110
+ }
111
+ function removeStaleLock(lockDir) {
112
+ try {
113
+ if (Date.now() - statSync(lockDir).mtimeMs <= LOCK_STALE_MS)
114
+ return false;
115
+ rmSync(lockDir, { recursive: true, force: true });
116
+ return true;
117
+ }
118
+ catch (err) {
119
+ if (err.code === "ENOENT")
120
+ return true;
121
+ return false;
122
+ }
123
+ }
124
+ function sleepSync(ms) {
125
+ try {
126
+ const view = new Int32Array(new SharedArrayBuffer(4));
127
+ Atomics.wait(view, 0, 0, ms);
128
+ }
129
+ catch {
130
+ const until = Date.now() + ms;
131
+ while (Date.now() < until) {
132
+ // fallback for runtimes where Atomics.wait is unavailable
133
+ }
134
+ }
135
+ }
136
+ function isBinding(value) {
137
+ if (!value || typeof value !== "object")
138
+ return false;
139
+ const v = value;
140
+ return ((v.cli === "claude" || v.cli === "codex") &&
141
+ typeof v.sessionId === "string" &&
142
+ v.sessionId.length > 0 &&
143
+ typeof v.cwd === "string" &&
144
+ v.cwd.length > 0);
145
+ }
146
+ export const externalAgentSessionStore = new ExternalAgentSessionStore();
@@ -11,6 +11,41 @@ export interface HistoryMessage {
11
11
  }[];
12
12
  ts?: number;
13
13
  }
14
+ /** One render-relevant event parsed from an appended external-CLI transcript
15
+ * line. Kept CLI-neutral so the desktop room follower can feed Claude Code and
16
+ * Codex through the same RoomManager push path. */
17
+ export type SessionTailEvent = {
18
+ type: "user";
19
+ text: string;
20
+ } | {
21
+ type: "assistant";
22
+ text: string;
23
+ } | {
24
+ type: "tool";
25
+ id?: string;
26
+ name: string;
27
+ summary: string;
28
+ args?: Record<string, unknown>;
29
+ } | {
30
+ type: "tool_result";
31
+ id?: string;
32
+ result: string;
33
+ isError: boolean;
34
+ } | {
35
+ type: "turn_end";
36
+ reason: string;
37
+ };
38
+ /** Parse a bounded/raw Claude Code transcript snapshot. Exported so a live
39
+ * follower can take its initial snapshot and EOF cursor from the same read,
40
+ * eliminating the snapshot→subscribe race. */
41
+ export declare function parseRecentHistory(raw: string, limit: number): {
42
+ messages: HistoryMessage[];
43
+ hasMore: boolean;
44
+ totalCount: number;
45
+ };
46
+ /** Parse one newly-appended Claude Code transcript JSONL line into the compact
47
+ * event vocabulary consumed by the desktop room follower. */
48
+ export declare function parseClaudeTranscriptLine(line: string): SessionTailEvent[];
14
49
  /** Read the last `limit` user/assistant messages from a claude session jsonl. */
15
50
  export declare function readRecentHistory(cwd: string, sessionId: string, limit: number, claudeHome?: string): {
16
51
  messages: HistoryMessage[];