@cjhyy/code-shell-core 0.6.0-rc.8 → 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.
- package/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/automation/scheduler.d.ts +13 -7
- package/dist/automation/scheduler.js +116 -37
- package/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +7 -1
- package/dist/cc-orchestrator/codex-session-history.js +1 -1
- package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
- package/dist/cc-orchestrator/cwd-normalize.js +19 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +102 -51
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
- package/dist/cc-orchestrator/session-history.js +2 -2
- package/dist/cli/agent-server-stdio.js +9 -2
- package/dist/context/compaction.d.ts +38 -1
- package/dist/context/compaction.js +138 -0
- package/dist/context/manager.d.ts +34 -2
- package/dist/context/manager.js +254 -58
- package/dist/context/token-counter.js +13 -0
- package/dist/credentials/access.d.ts +56 -0
- package/dist/credentials/access.js +183 -0
- package/dist/credentials/index.d.ts +1 -0
- package/dist/credentials/index.js +1 -0
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +30 -11
- package/dist/credentials/types.d.ts +2 -2
- package/dist/credentials/use-credential-tool.d.ts +9 -1
- package/dist/credentials/use-credential-tool.js +58 -45
- package/dist/engine/engine.d.ts +38 -12
- package/dist/engine/engine.js +477 -163
- package/dist/engine/image-policy.d.ts +6 -0
- package/dist/engine/image-policy.js +17 -6
- package/dist/engine/input-attachments.d.ts +13 -0
- package/dist/engine/input-attachments.js +255 -0
- package/dist/engine/model-facade.d.ts +5 -2
- package/dist/engine/model-facade.js +6 -16
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- package/dist/engine/query.js +2 -0
- package/dist/engine/session-usage.d.ts +12 -0
- package/dist/engine/session-usage.js +56 -0
- package/dist/engine/steer-queue.d.ts +2 -1
- package/dist/engine/steer-queue.js +2 -2
- package/dist/engine/streaming-tool-queue.d.ts +11 -7
- package/dist/engine/streaming-tool-queue.js +11 -7
- package/dist/engine/turn-loop.d.ts +34 -2
- package/dist/engine/turn-loop.js +269 -52
- package/dist/engine/types.d.ts +8 -0
- package/dist/git/worktree/crud.d.ts +69 -0
- package/dist/git/worktree/crud.js +206 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +82 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +51 -0
- package/dist/git/worktree/index.d.ts +5 -0
- package/dist/git/worktree/index.js +5 -0
- package/dist/git/worktree/query.d.ts +42 -0
- package/dist/git/worktree/query.js +121 -0
- package/dist/git/worktree/slug.d.ts +11 -0
- package/dist/git/worktree/slug.js +58 -0
- package/dist/git/worktree.d.ts +1 -84
- package/dist/git/worktree.js +5 -230
- package/dist/index.d.ts +31 -28
- package/dist/index.js +27 -25
- package/dist/logging/logger.js +6 -6
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/plugins/installer/checkUpdate.d.ts +4 -1
- package/dist/plugins/installer/checkUpdate.js +4 -2
- package/dist/plugins/installer/install.js +2 -0
- package/dist/plugins/installer/installFromSource.js +9 -1
- package/dist/plugins/installer/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- package/dist/plugins/installer/sourcePath.d.ts +9 -0
- package/dist/plugins/installer/sourcePath.js +50 -0
- package/dist/plugins/installer/update.d.ts +4 -1
- package/dist/plugins/installer/update.js +5 -3
- package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
- package/dist/plugins/parseMarketplaceInput.js +4 -3
- package/dist/plugins/pluginInstaller.js +24 -22
- package/dist/preset/index.d.ts +1 -0
- package/dist/preset/index.js +16 -9
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.d.ts +2 -0
- package/dist/protocol/chat-session-manager.js +38 -2
- package/dist/protocol/chat-session.d.ts +5 -0
- package/dist/protocol/chat-session.js +2 -0
- package/dist/protocol/client.d.ts +13 -5
- package/dist/protocol/client.js +26 -3
- package/dist/protocol/server.d.ts +43 -12
- package/dist/protocol/server.js +326 -104
- package/dist/protocol/types.d.ts +51 -0
- package/dist/protocol/types.js +6 -0
- package/dist/run/FileRunStore.js +10 -1
- package/dist/run/Heartbeat.js +12 -0
- package/dist/run/RunApprovalBackend.d.ts +3 -0
- package/dist/run/RunApprovalBackend.js +41 -6
- package/dist/run/RunLock.js +2 -0
- package/dist/run/RunManager.d.ts +2 -0
- package/dist/run/RunManager.js +64 -24
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +23 -0
- package/dist/runtime/background-shell.d.ts +1 -0
- package/dist/runtime/background-shell.js +23 -13
- package/dist/runtime/safe-spawn.js +74 -11
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/services/auto-dream.d.ts +15 -4
- package/dist/services/auto-dream.js +20 -20
- package/dist/services/dream-consolidation.d.ts +2 -3
- package/dist/services/dream-consolidation.js +65 -15
- package/dist/services/extract-memories.d.ts +14 -5
- package/dist/services/extract-memories.js +20 -3
- package/dist/services/global-dream-promotion.d.ts +23 -0
- package/dist/services/global-dream-promotion.js +112 -0
- package/dist/services/memory-orchestrator.js +347 -34
- package/dist/session/memory.d.ts +61 -18
- package/dist/session/memory.js +342 -79
- package/dist/session/session-manager.d.ts +35 -1
- package/dist/session/session-manager.js +197 -4
- package/dist/session/transcript.d.ts +5 -1
- package/dist/session/transcript.js +38 -5
- package/dist/settings/manager.d.ts +1 -0
- package/dist/settings/manager.js +87 -37
- package/dist/settings/schema-export.d.ts +2 -3
- package/dist/settings/schema-export.js +2 -3
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/skills/scanner.d.ts +3 -2
- package/dist/skills/scanner.js +12 -9
- package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
- package/dist/tool-system/builtin/background-jobs.js +12 -1
- package/dist/tool-system/builtin/background-work.d.ts +17 -18
- package/dist/tool-system/builtin/background-work.js +51 -5
- package/dist/tool-system/builtin/config.d.ts +2 -1
- package/dist/tool-system/builtin/config.js +16 -11
- package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
- package/dist/tool-system/builtin/drive-claude-code.js +301 -47
- package/dist/tool-system/builtin/edit.js +5 -2
- package/dist/tool-system/builtin/generate-video.d.ts +1 -0
- package/dist/tool-system/builtin/generate-video.js +13 -4
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +37 -21
- package/dist/tool-system/builtin/lsp.d.ts +2 -1
- package/dist/tool-system/builtin/lsp.js +6 -3
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/notebook-edit.js +5 -2
- package/dist/tool-system/builtin/powershell.d.ts +5 -2
- package/dist/tool-system/builtin/powershell.js +11 -7
- package/dist/tool-system/builtin/read.js +118 -6
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/view-image.js +109 -19
- package/dist/tool-system/builtin/worktree.d.ts +4 -4
- package/dist/tool-system/builtin/worktree.js +297 -74
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +23 -1
- package/dist/tool-system/executor.d.ts +1 -5
- package/dist/tool-system/executor.js +94 -115
- package/dist/tool-system/mcp-manager.d.ts +18 -5
- package/dist/tool-system/mcp-manager.js +157 -74
- package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
- package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +41 -12
- package/dist/tool-system/permission.d.ts +28 -7
- package/dist/tool-system/permission.js +130 -49
- package/dist/tool-system/registry.js +11 -4
- package/dist/tool-system/tool-result-redaction.d.ts +7 -0
- package/dist/tool-system/tool-result-redaction.js +48 -0
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +86 -5
- package/dist/utils/toolDisplay.js +1 -1
- 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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 {
|
|
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
|
-
|
|
37
|
+
done(false);
|
|
38
|
+
}, 2_000);
|
|
39
|
+
child.stdout?.on("data", (chunk) => {
|
|
40
|
+
out += chunk.toString("utf8");
|
|
63
41
|
});
|
|
64
|
-
child.on("
|
|
65
|
-
|
|
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
|
|
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
|
|
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,12 +10,48 @@
|
|
|
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.
|
|
17
17
|
*/
|
|
18
18
|
export declare function estimateTokens(messages: Message[]): number;
|
|
19
|
+
export declare const IMAGE_HISTORY_PLACEHOLDER_PREFIX = "[image #";
|
|
20
|
+
export declare const IMAGE_HISTORY_PLACEHOLDER_SUFFIX = ", \u5DF2\u5904\u7406 / already provided earlier]";
|
|
21
|
+
interface ImagePreserveSet {
|
|
22
|
+
has(message: Message): boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface DowngradeImageHistoryOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Messages whose image payloads are being sent for their first model
|
|
27
|
+
* consumption in this request. They still count toward image numbering but
|
|
28
|
+
* keep their base64 until the caller clears the preserve set after a
|
|
29
|
+
* successful model response.
|
|
30
|
+
*/
|
|
31
|
+
preserveMessages?: ImagePreserveSet;
|
|
32
|
+
}
|
|
33
|
+
export interface DowngradeImageHistoryResult {
|
|
34
|
+
messages: Message[];
|
|
35
|
+
replacedCount: number;
|
|
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;
|
|
43
|
+
/**
|
|
44
|
+
* Replace already-consumed image payload blocks with compact text markers.
|
|
45
|
+
*
|
|
46
|
+
* The transcript may retain the full image bytes for rendering/resume, but the
|
|
47
|
+
* working message history sent to the model should not re-send base64 after the
|
|
48
|
+
* model has seen it once. This handles both our internal Anthropic-style image
|
|
49
|
+
* blocks and OpenAI-style data-url image blocks defensively, including images
|
|
50
|
+
* nested inside tool_result.content arrays (view_image / browser screenshots).
|
|
51
|
+
*/
|
|
52
|
+
export declare function downgradeImagePayloadsInHistory(messages: Message[], options?: DowngradeImageHistoryOptions): DowngradeImageHistoryResult;
|
|
53
|
+
export declare function messageHasBase64ImagePayload(message: Message): boolean;
|
|
54
|
+
export declare function isBase64ImageBlock(block: ContentBlock): boolean;
|
|
19
55
|
/**
|
|
20
56
|
* Reconcile user-supplied compaction ratios into a safe ordering.
|
|
21
57
|
*
|
|
@@ -170,3 +206,4 @@ export declare function dropOldestRounds(messages: Message[], roundsToDrop: numb
|
|
|
170
206
|
* messages. Returns paths only — the model uses Read tool on demand.
|
|
171
207
|
*/
|
|
172
208
|
export declare function extractReferencedFilePaths(messages: Message[]): string[];
|
|
209
|
+
export {};
|