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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/THIRD_PARTY_NOTICES.md +206 -0
  2. package/dist/automation/scheduler.d.ts +13 -7
  3. package/dist/automation/scheduler.js +116 -37
  4. package/dist/capability-control/service.d.ts +2 -0
  5. package/dist/capability-control/service.js +4 -2
  6. package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
  7. package/dist/cc-orchestrator/agent-adapter.js +7 -1
  8. package/dist/cc-orchestrator/codex-session-history.js +1 -1
  9. package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
  10. package/dist/cc-orchestrator/cwd-normalize.js +19 -0
  11. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  12. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  13. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
  14. package/dist/cc-orchestrator/external-agent-driver.js +102 -51
  15. package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
  16. package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
  17. package/dist/cc-orchestrator/session-history.js +2 -2
  18. package/dist/cli/agent-server-stdio.js +9 -2
  19. package/dist/context/compaction.d.ts +8 -1
  20. package/dist/context/compaction.js +49 -4
  21. package/dist/context/manager.d.ts +16 -2
  22. package/dist/context/manager.js +103 -19
  23. package/dist/credentials/access.d.ts +56 -0
  24. package/dist/credentials/access.js +183 -0
  25. package/dist/credentials/index.d.ts +1 -0
  26. package/dist/credentials/index.js +1 -0
  27. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  28. package/dist/credentials/inject-credential-tool.js +30 -11
  29. package/dist/credentials/types.d.ts +2 -2
  30. package/dist/credentials/use-credential-tool.d.ts +9 -1
  31. package/dist/credentials/use-credential-tool.js +58 -45
  32. package/dist/engine/engine.d.ts +17 -1
  33. package/dist/engine/engine.js +247 -89
  34. package/dist/engine/image-policy.d.ts +6 -0
  35. package/dist/engine/image-policy.js +17 -6
  36. package/dist/engine/input-attachments.d.ts +13 -0
  37. package/dist/engine/input-attachments.js +255 -0
  38. package/dist/engine/model-facade.d.ts +5 -2
  39. package/dist/engine/model-facade.js +4 -4
  40. package/dist/engine/parse-task.d.ts +10 -0
  41. package/dist/engine/parse-task.js +5 -0
  42. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  43. package/dist/engine/streaming-tool-queue.js +11 -7
  44. package/dist/engine/turn-loop.d.ts +7 -1
  45. package/dist/engine/turn-loop.js +117 -27
  46. package/dist/engine/types.d.ts +8 -0
  47. package/dist/git/worktree/crud.d.ts +69 -0
  48. package/dist/git/worktree/crud.js +206 -0
  49. package/dist/git/worktree/diff.d.ts +14 -0
  50. package/dist/git/worktree/diff.js +82 -0
  51. package/dist/git/worktree/git-exec.d.ts +7 -0
  52. package/dist/git/worktree/git-exec.js +51 -0
  53. package/dist/git/worktree/index.d.ts +5 -0
  54. package/dist/git/worktree/index.js +5 -0
  55. package/dist/git/worktree/query.d.ts +42 -0
  56. package/dist/git/worktree/query.js +121 -0
  57. package/dist/git/worktree/slug.d.ts +11 -0
  58. package/dist/git/worktree/slug.js +58 -0
  59. package/dist/git/worktree.d.ts +1 -84
  60. package/dist/git/worktree.js +5 -230
  61. package/dist/index.d.ts +31 -28
  62. package/dist/index.js +27 -25
  63. package/dist/logging/logger.js +6 -6
  64. package/dist/logging/sanitize-messages.d.ts +10 -2
  65. package/dist/logging/sanitize-messages.js +21 -6
  66. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  67. package/dist/plugins/installer/checkUpdate.js +4 -2
  68. package/dist/plugins/installer/install.js +2 -0
  69. package/dist/plugins/installer/installFromSource.js +9 -1
  70. package/dist/plugins/installer/parseSource.d.ts +4 -1
  71. package/dist/plugins/installer/parseSource.js +28 -10
  72. package/dist/plugins/installer/sourcePath.d.ts +9 -0
  73. package/dist/plugins/installer/sourcePath.js +50 -0
  74. package/dist/plugins/installer/update.d.ts +4 -1
  75. package/dist/plugins/installer/update.js +5 -3
  76. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  77. package/dist/plugins/parseMarketplaceInput.js +4 -3
  78. package/dist/plugins/pluginInstaller.js +24 -22
  79. package/dist/preset/index.d.ts +1 -0
  80. package/dist/preset/index.js +16 -9
  81. package/dist/prompt/sections/base.md +1 -1
  82. package/dist/protocol/chat-session-manager.d.ts +2 -0
  83. package/dist/protocol/chat-session-manager.js +38 -2
  84. package/dist/protocol/chat-session.d.ts +3 -0
  85. package/dist/protocol/chat-session.js +1 -0
  86. package/dist/protocol/client.d.ts +9 -4
  87. package/dist/protocol/client.js +18 -1
  88. package/dist/protocol/server.d.ts +30 -0
  89. package/dist/protocol/server.js +246 -46
  90. package/dist/protocol/types.d.ts +47 -0
  91. package/dist/protocol/types.js +6 -0
  92. package/dist/run/FileRunStore.js +10 -1
  93. package/dist/run/Heartbeat.js +12 -0
  94. package/dist/run/RunApprovalBackend.d.ts +3 -0
  95. package/dist/run/RunApprovalBackend.js +41 -6
  96. package/dist/run/RunLock.js +2 -0
  97. package/dist/run/RunManager.d.ts +2 -0
  98. package/dist/run/RunManager.js +64 -24
  99. package/dist/run/ids.d.ts +2 -0
  100. package/dist/run/ids.js +23 -0
  101. package/dist/runtime/background-shell.d.ts +1 -0
  102. package/dist/runtime/background-shell.js +23 -13
  103. package/dist/runtime/spawn-common.js +10 -0
  104. package/dist/services/auto-dream.d.ts +15 -4
  105. package/dist/services/auto-dream.js +20 -20
  106. package/dist/services/dream-consolidation.d.ts +2 -3
  107. package/dist/services/dream-consolidation.js +65 -15
  108. package/dist/services/extract-memories.d.ts +14 -5
  109. package/dist/services/extract-memories.js +20 -3
  110. package/dist/services/global-dream-promotion.d.ts +23 -0
  111. package/dist/services/global-dream-promotion.js +112 -0
  112. package/dist/services/memory-orchestrator.js +347 -34
  113. package/dist/session/memory.d.ts +61 -18
  114. package/dist/session/memory.js +342 -79
  115. package/dist/session/session-manager.d.ts +35 -1
  116. package/dist/session/session-manager.js +190 -3
  117. package/dist/session/transcript.d.ts +1 -1
  118. package/dist/session/transcript.js +17 -5
  119. package/dist/settings/manager.d.ts +1 -0
  120. package/dist/settings/manager.js +87 -37
  121. package/dist/settings/schema-export.d.ts +2 -3
  122. package/dist/settings/schema-export.js +2 -3
  123. package/dist/settings/schema.d.ts +21 -0
  124. package/dist/settings/schema.js +12 -0
  125. package/dist/skills/scanner.d.ts +3 -2
  126. package/dist/skills/scanner.js +12 -9
  127. package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
  128. package/dist/tool-system/builtin/background-jobs.js +12 -1
  129. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  130. package/dist/tool-system/builtin/background-work.js +51 -5
  131. package/dist/tool-system/builtin/config.d.ts +2 -1
  132. package/dist/tool-system/builtin/config.js +16 -11
  133. package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
  134. package/dist/tool-system/builtin/drive-claude-code.js +301 -47
  135. package/dist/tool-system/builtin/edit.js +5 -2
  136. package/dist/tool-system/builtin/generate-video.d.ts +1 -0
  137. package/dist/tool-system/builtin/generate-video.js +13 -4
  138. package/dist/tool-system/builtin/index.d.ts +8 -3
  139. package/dist/tool-system/builtin/index.js +37 -21
  140. package/dist/tool-system/builtin/lsp.d.ts +2 -1
  141. package/dist/tool-system/builtin/lsp.js +6 -3
  142. package/dist/tool-system/builtin/memory.js +40 -8
  143. package/dist/tool-system/builtin/notebook-edit.js +5 -2
  144. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  145. package/dist/tool-system/builtin/powershell.js +11 -7
  146. package/dist/tool-system/builtin/read.js +118 -6
  147. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  148. package/dist/tool-system/builtin/view-image.js +109 -19
  149. package/dist/tool-system/builtin/worktree.d.ts +4 -4
  150. package/dist/tool-system/builtin/worktree.js +297 -74
  151. package/dist/tool-system/builtin/write.js +5 -3
  152. package/dist/tool-system/context.d.ts +23 -1
  153. package/dist/tool-system/executor.d.ts +1 -5
  154. package/dist/tool-system/executor.js +94 -115
  155. package/dist/tool-system/mcp-manager.d.ts +18 -5
  156. package/dist/tool-system/mcp-manager.js +140 -74
  157. package/dist/tool-system/path-policy.d.ts +2 -0
  158. package/dist/tool-system/path-policy.js +41 -12
  159. package/dist/tool-system/permission.d.ts +28 -7
  160. package/dist/tool-system/permission.js +130 -49
  161. package/dist/tool-system/registry.js +11 -4
  162. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  163. package/dist/tool-system/tool-result-redaction.js +48 -0
  164. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  165. package/dist/tool-system/workspace-bridge.js +1 -0
  166. package/dist/types.d.ts +55 -4
  167. package/dist/utils/toolDisplay.js +1 -1
  168. package/package.json +4 -3
@@ -0,0 +1,150 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { execFileSync } from "node:child_process";
4
+ import { codeShellHome, SessionManager } from "../session/session-manager.js";
5
+ export function externalAgentBindingsPath(home = codeShellHome()) {
6
+ return join(home, "external-agents", "bindings.json");
7
+ }
8
+ export class ExternalAgentBindingStore {
9
+ filePath;
10
+ constructor(filePath) {
11
+ this.filePath = filePath;
12
+ }
13
+ get(externalSessionId) {
14
+ return this.read({ failOnCorrupt: true }).bindings[externalSessionId];
15
+ }
16
+ upsert(next) {
17
+ const data = this.read({ failOnCorrupt: false });
18
+ const existing = data.bindings[next.externalSessionId];
19
+ const now = Date.now();
20
+ const binding = {
21
+ ...next,
22
+ codeShellSessionId: next.codeShellSessionId || existing?.codeShellSessionId || "",
23
+ createdAt: existing?.createdAt ?? now,
24
+ lastUsedAt: now,
25
+ };
26
+ data.bindings[next.externalSessionId] = binding;
27
+ this.write(data);
28
+ return binding;
29
+ }
30
+ read(opts) {
31
+ const file = this.file();
32
+ if (!existsSync(file))
33
+ return { bindings: {} };
34
+ try {
35
+ const parsed = JSON.parse(readFileSync(file, "utf-8"));
36
+ if (parsed && typeof parsed.bindings === "object" && parsed.bindings) {
37
+ return { bindings: parsed.bindings };
38
+ }
39
+ if (opts.failOnCorrupt) {
40
+ throw new Error(`external agent bindings file is corrupt: expected object with bindings`);
41
+ }
42
+ return { bindings: {} };
43
+ }
44
+ catch (err) {
45
+ if (opts.failOnCorrupt) {
46
+ throw new Error(`external agent bindings file is corrupt or unreadable: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
47
+ }
48
+ return { bindings: {} };
49
+ }
50
+ }
51
+ write(data) {
52
+ const file = this.file();
53
+ mkdirSync(dirname(file), { recursive: true });
54
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
55
+ writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
56
+ renameSync(tmp, file);
57
+ }
58
+ file() {
59
+ return this.filePath ?? externalAgentBindingsPath();
60
+ }
61
+ }
62
+ export function detectExternalAgentWorktree(cwd) {
63
+ if (!existsSync(cwd))
64
+ return {};
65
+ try {
66
+ const root = git(cwd, ["rev-parse", "--show-toplevel"]);
67
+ const branch = git(root, ["branch", "--show-current"]);
68
+ const entries = parseWorktreeList(git(root, ["worktree", "list", "--porcelain"]));
69
+ const current = entries.find((entry) => resolve(entry.path) === resolve(root));
70
+ const main = entries[0];
71
+ if (!current || !branch || !main || resolve(current.path) === resolve(main.path))
72
+ return {};
73
+ return { worktreePath: root, worktreeBranch: branch };
74
+ }
75
+ catch {
76
+ return {};
77
+ }
78
+ }
79
+ export function externalAgentResumeCwdError(binding) {
80
+ if (existsSync(binding.cwd))
81
+ return undefined;
82
+ const branch = binding.worktreeBranch;
83
+ if (branch && branchExistsForBinding(binding)) {
84
+ return (`Error: external ${binding.cli} session ${binding.externalSessionId} is bound to cwd ` +
85
+ `${binding.cwd}, but that directory no longer exists. Worktree branch ${branch} still ` +
86
+ `exists; recreate the worktree at ${binding.worktreePath ?? binding.cwd} before resuming.`);
87
+ }
88
+ return (`Error: external ${binding.cli} session ${binding.externalSessionId} is bound to cwd ` +
89
+ `${binding.cwd}, but the workspace deleted${branch ? ` and branch ${branch} is gone` : ""}. ` +
90
+ `Start a new external session.`);
91
+ }
92
+ function branchExistsForBinding(binding) {
93
+ if (!binding.worktreeBranch)
94
+ return false;
95
+ for (const cwd of candidateGitCwds(binding)) {
96
+ if (!cwd || !existsSync(cwd))
97
+ continue;
98
+ try {
99
+ const out = git(cwd, ["branch", "--list", binding.worktreeBranch]);
100
+ if (out.length > 0)
101
+ return true;
102
+ }
103
+ catch {
104
+ // Try the next candidate.
105
+ }
106
+ }
107
+ return false;
108
+ }
109
+ function candidateGitCwds(binding) {
110
+ const candidates = new Set();
111
+ if (existsSync(binding.cwd))
112
+ candidates.add(binding.cwd);
113
+ if (binding.codeShellSessionId) {
114
+ const sessionCwd = new SessionManager().readCwd(binding.codeShellSessionId);
115
+ if (sessionCwd)
116
+ candidates.add(sessionCwd);
117
+ }
118
+ candidates.add(process.cwd());
119
+ candidates.add(dirname(binding.cwd));
120
+ candidates.add(dirname(dirname(binding.cwd)));
121
+ return [...candidates];
122
+ }
123
+ function parseWorktreeList(raw) {
124
+ if (!raw.trim())
125
+ return [];
126
+ const entries = [];
127
+ let current = { path: "", branch: "" };
128
+ for (const line of raw.split("\n")) {
129
+ if (line.startsWith("worktree ")) {
130
+ if (current.path)
131
+ entries.push(current);
132
+ current = { path: line.slice(9), branch: "" };
133
+ }
134
+ else if (line.startsWith("branch ")) {
135
+ current.branch = line.slice(7).replace("refs/heads/", "");
136
+ }
137
+ }
138
+ if (current.path)
139
+ entries.push(current);
140
+ return entries;
141
+ }
142
+ function git(cwd, args) {
143
+ return execFileSync("git", args, {
144
+ cwd,
145
+ encoding: "utf-8",
146
+ timeout: 10000,
147
+ stdio: ["ignore", "pipe", "ignore"],
148
+ }).trim();
149
+ }
150
+ export const externalAgentBindingStore = new ExternalAgentBindingStore();
@@ -11,6 +11,7 @@ export declare function runWithLines(adapter: AgentAdapter, lines: string[], exi
11
11
  export interface DriverRunOpts extends Omit<BuildArgsOpts, "permissionMode"> {
12
12
  permissionMode?: PermissionMode;
13
13
  }
14
+ export declare function detectCodexImageInput(command: string, cwd: string): Promise<boolean>;
14
15
  /** Spawn ONE headless agent run, collect stream-json to exit, return result.
15
16
  * No time concept — a single turn. Honors AbortSignal (kills the child). */
16
17
  export declare function runAgentOnce(adapter: AgentAdapter, opts: DriverRunOpts & {
@@ -6,64 +6,115 @@ export function runWithLines(adapter, lines, exitCode) {
6
6
  const parsed = adapter.parseResult(lines);
7
7
  return { ...parsed, exitCode, lines };
8
8
  }
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,
9
+ const codexImageFlagCache = new Map();
10
+ export function detectCodexImageInput(command, cwd) {
11
+ const cached = codexImageFlagCache.get(command);
12
+ if (cached)
13
+ return cached;
14
+ const probe = new Promise((resolve) => {
15
+ const child = spawn(command, ["exec", "--help"], {
16
+ cwd,
26
17
  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"],
18
+ windowsHide: true,
19
+ stdio: ["ignore", "pipe", "pipe"],
34
20
  });
35
- if (viaStdin && child.stdin) {
36
- child.stdin.end(opts.prompt);
37
- }
38
- const lines = [];
39
- if (child.stdout) {
40
- const rl = createInterface({ input: child.stdout });
41
- rl.on("line", (line) => lines.push(line));
42
- }
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 = () => {
21
+ let out = "";
22
+ let settled = false;
23
+ const done = (value) => {
24
+ if (settled)
25
+ return;
26
+ settled = true;
27
+ clearTimeout(timer);
28
+ resolve(value);
29
+ };
30
+ const timer = setTimeout(() => {
47
31
  try {
48
32
  child.kill("SIGTERM");
49
33
  }
50
- catch { /* already gone */ }
51
- };
52
- signal?.addEventListener("abort", onAbort, { once: true });
53
- child.on("error", (err) => {
54
- 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"})。`));
60
- return;
34
+ catch {
35
+ // already gone
61
36
  }
62
- reject(err);
37
+ done(false);
38
+ }, 2_000);
39
+ child.stdout?.on("data", (chunk) => {
40
+ out += chunk.toString("utf8");
63
41
  });
64
- child.on("exit", (code) => {
65
- signal?.removeEventListener("abort", onAbort);
66
- resolve(runWithLines(adapter, lines, code));
42
+ child.stderr?.on("data", (chunk) => {
43
+ out += chunk.toString("utf8");
67
44
  });
45
+ child.on("error", () => done(false));
46
+ child.on("exit", () => done(/(?:^|\s)-i(?:,|\s)|--image\b/.test(out)));
47
+ });
48
+ codexImageFlagCache.set(command, probe);
49
+ return probe;
50
+ }
51
+ /** Spawn ONE headless agent run, collect stream-json to exit, return result.
52
+ * No time concept — a single turn. Honors AbortSignal (kills the child). */
53
+ export function runAgentOnce(adapter, opts, signal) {
54
+ return new Promise((resolve, reject) => {
55
+ void (async () => {
56
+ const codexImageInputSupported = adapter.kind === "codex" && (opts.imagePaths?.length ?? 0) > 0
57
+ ? await detectCodexImageInput(opts.command, opts.cwd).catch(() => false)
58
+ : false;
59
+ const args = adapter.buildArgs({
60
+ prompt: opts.prompt,
61
+ resumeSessionId: opts.resumeSessionId,
62
+ permissionMode: opts.permissionMode ?? "default",
63
+ cwd: opts.cwd,
64
+ imagePaths: opts.imagePaths,
65
+ codexImageInputSupported,
66
+ });
67
+ // claude takes the prompt in argv (`-p <prompt>`) and wants stdin closed
68
+ // (verified: avoids a 3s wait). codex `exec` reads the prompt from stdin
69
+ // (argv ends with `-`), so adapters that set promptViaStdin get a piped
70
+ // stdin we write the prompt to.
71
+ const viaStdin = adapter.promptViaStdin === true;
72
+ const child = spawn(opts.command, args, {
73
+ cwd: opts.cwd,
74
+ env: { ...process.env, PATH: pathWithCommonBins() },
75
+ // NOT detached: this child is owned by the (long-lived) worker that reads
76
+ // its stdout. Detaching orphaned it across a worker/app restart — the
77
+ // reader promise then never resolved and the completion notification never
78
+ // fired (the "后台任务没返回" bug). Bound to the worker, it lives or dies
79
+ // with the process that's actually listening for its result.
80
+ detached: false,
81
+ stdio: [viaStdin ? "pipe" : "ignore", "pipe", "pipe"],
82
+ });
83
+ if (viaStdin && child.stdin) {
84
+ child.stdin.end(opts.prompt);
85
+ }
86
+ const lines = [];
87
+ if (child.stdout) {
88
+ const rl = createInterface({ input: child.stdout });
89
+ rl.on("line", (line) => lines.push(line));
90
+ }
91
+ // Not detached → no own process group, so kill the child directly (a
92
+ // negative-pid group kill would target the worker's group). claude has no
93
+ // long-lived child tree of its own here, so a direct SIGTERM is sufficient.
94
+ const onAbort = () => {
95
+ try {
96
+ child.kill("SIGTERM");
97
+ }
98
+ catch {
99
+ /* already gone */
100
+ }
101
+ };
102
+ signal?.addEventListener("abort", onAbort, { once: true });
103
+ child.on("error", (err) => {
104
+ signal?.removeEventListener("abort", onAbort);
105
+ // A missing binary is the most common failure (user hasn't installed the
106
+ // CLI, or GUI-launched Electron's PATH misses it). Turn the cryptic
107
+ // "spawn codex ENOENT" into something actionable that names the command.
108
+ if (err.code === "ENOENT") {
109
+ reject(new Error(`未找到命令 "${opts.command}"。请先安装该 CLI 并确保它在 PATH 中(${adapter.kind === "codex" ? "Codex CLI" : "Claude Code CLI"})。`));
110
+ return;
111
+ }
112
+ reject(err);
113
+ });
114
+ child.on("exit", (code) => {
115
+ signal?.removeEventListener("abort", onAbort);
116
+ resolve(runWithLines(adapter, lines, code));
117
+ });
118
+ })().catch(reject);
68
119
  });
69
120
  }
@@ -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();
@@ -51,14 +51,14 @@ export function readRecentHistory(cwd, sessionId, limit, claudeHome = join(homed
51
51
  const t = textOf(d.message?.content).trim();
52
52
  if (!t || NOISE.some((n) => t.startsWith(n)))
53
53
  continue;
54
- all.push({ role: "user", text: t.slice(0, 4000) });
54
+ all.push({ role: "user", text: t });
55
55
  }
56
56
  else if (d.type === "assistant") {
57
57
  const t = textOf(d.message?.content).trim();
58
58
  const tools = toolsOf(d.message?.content);
59
59
  if (!t && tools.length === 0)
60
60
  continue;
61
- all.push({ role: "assistant", text: t.slice(0, 4000), tools: tools.length ? tools : undefined });
61
+ all.push({ role: "assistant", text: t, tools: tools.length ? tools : undefined });
62
62
  }
63
63
  }
64
64
  const lim = limit > 0 ? limit : 20;
@@ -50,6 +50,7 @@ import { logger } from "../logging/logger.js";
50
50
  import { cronScheduler } from "../automation/scheduler.js";
51
51
  import { CronStore, defaultCronStorePath } from "../automation/store.js";
52
52
  import { resolveLLMConfigForTag } from "../engine/resolve-llm-config.js";
53
+ import { createIpcCredentialAccess, setDefaultCredentialAccess } from "../credentials/access.js";
53
54
  /**
54
55
  * Resolve per-session agent config: protocol slice overrides win, else fall
55
56
  * back to disk settings.agent.*. Fixes the bug where settings.agent.* never
@@ -106,8 +107,7 @@ catch (err) {
106
107
  // knob; imageDetail from settings.images.detail).
107
108
  const seedLlm = resolveLLMConfigForTag(settings, "text", settings.defaults?.text);
108
109
  if (!seedLlm) {
109
- console.error(`[agent-server] 没有可用的文本模型连接(defaults.text=${settings.defaults?.text ?? "未设置"})。` +
110
- `请在「连接」页添加并填写凭证。`);
110
+ console.error(`[agent-server] 没有可用的文本模型连接(defaults.text=${settings.defaults?.text ?? "未设置"})。` + `请在「连接」页添加并填写凭证。`);
111
111
  process.exit(1);
112
112
  }
113
113
  const llmConfig = seedLlm;
@@ -115,6 +115,10 @@ const llmConfig = seedLlm;
115
115
  const seedEngine = new Engine({
116
116
  llm: llmConfig,
117
117
  cwd,
118
+ preset: settings.agent.preset,
119
+ enabledBuiltinTools: settings.agent.enabledBuiltinTools,
120
+ disabledBuiltinTools: settings.agent.disabledBuiltinTools,
121
+ builtinToolHost: "desktop",
118
122
  settingsScope: "full",
119
123
  // No runtime — Engine.populateModelPoolFromSettings() runs in ctor.
120
124
  });
@@ -217,6 +221,7 @@ const chatManager = new ChatSessionManager({
217
221
  // This stdio worker exists only to serve the desktop app, so every
218
222
  // session it creates is a desktop-origin session.
219
223
  origin: "desktop",
224
+ builtinToolHost: "desktop",
220
225
  // Inherit full scope so spawned subagents read user config too.
221
226
  settingsScope: "full",
222
227
  // MCP servers from settings — the worker reads the full disk
@@ -270,6 +275,7 @@ cronScheduler.setExecutionEnabled(false);
270
275
  cronScheduler.loadJobs();
271
276
  // ─── Step 5: AgentServer over stdio ──────────────────────────────
272
277
  const stdioTransport = new StdioTransport(process.stdin, process.stdout);
278
+ setDefaultCredentialAccess(createIpcCredentialAccess(stdioTransport));
273
279
  // Cron jobs are persisted by this worker but only main arms/executes their
274
280
  // timers (this worker keeps setExecutionEnabled(false) above). When an AI tool
275
281
  // creates/deletes a cron job here, notify main over stdio so it reloads the
@@ -286,6 +292,7 @@ const goalDiskReader = new SessionManager();
286
292
  const agentServer = new AgentServer({
287
293
  chatManager,
288
294
  transport: stdioTransport,
295
+ workspaceBridge: true,
289
296
  // Config hot-reload (layer 2) reads disk through the SAME closure the
290
297
  // engineFactory uses for new sessions, so a reloaded running session and a
291
298
  // newly-created session converge on identical disk config (no divergence).
@@ -10,7 +10,7 @@
10
10
  * All slicing functions use adjustIndexToPreserveAPIInvariants()
11
11
  * to prevent splitting tool_use / tool_result pairs.
12
12
  */
13
- import type { Message } from "../types.js";
13
+ import type { ContentBlock, Message } from "../types.js";
14
14
  /**
15
15
  * Estimate token count from messages.
16
16
  * Uses per-block-type estimation with 33% overhead padding.
@@ -34,6 +34,12 @@ export interface DowngradeImageHistoryResult {
34
34
  messages: Message[];
35
35
  replacedCount: number;
36
36
  }
37
+ export interface Base64ImageHistoryEntry {
38
+ imageNumber: number;
39
+ block: ContentBlock;
40
+ }
41
+ export declare function collectBase64Images(messages: Message[]): Base64ImageHistoryEntry[];
42
+ export declare function findImageByNumber(messages: Message[], imageNumber: number): Base64ImageHistoryEntry | undefined;
37
43
  /**
38
44
  * Replace already-consumed image payload blocks with compact text markers.
39
45
  *
@@ -45,6 +51,7 @@ export interface DowngradeImageHistoryResult {
45
51
  */
46
52
  export declare function downgradeImagePayloadsInHistory(messages: Message[], options?: DowngradeImageHistoryOptions): DowngradeImageHistoryResult;
47
53
  export declare function messageHasBase64ImagePayload(message: Message): boolean;
54
+ export declare function isBase64ImageBlock(block: ContentBlock): boolean;
48
55
  /**
49
56
  * Reconcile user-supplied compaction ratios into a safe ordering.
50
57
  *