@cjhyy/code-shell-core 0.6.0-rc.1 → 0.6.0-rc.11
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/dist/context/compaction.d.ts +30 -0
- package/dist/context/compaction.js +93 -0
- package/dist/context/manager.d.ts +18 -0
- package/dist/context/manager.js +156 -44
- package/dist/context/token-counter.js +13 -0
- package/dist/engine/engine.d.ts +22 -12
- package/dist/engine/engine.js +263 -81
- package/dist/engine/model-connections-pool.js +1 -0
- package/dist/engine/model-facade.js +2 -12
- package/dist/engine/query.js +2 -0
- package/dist/engine/runtime.d.ts +2 -0
- package/dist/engine/runtime.js +25 -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/turn-loop.d.ts +28 -2
- package/dist/engine/turn-loop.js +153 -26
- package/dist/git/utils.d.ts +12 -0
- package/dist/git/utils.js +33 -6
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/llm/capabilities/rules.js +1 -1
- package/dist/llm/model-pool.d.ts +7 -0
- package/dist/llm/model-pool.js +8 -1
- package/dist/model-catalog/builtin.js +6 -1
- package/dist/preset/index.d.ts +5 -1
- package/dist/preset/index.js +21 -2
- package/dist/prompt/composer.d.ts +5 -0
- package/dist/prompt/composer.js +10 -2
- package/dist/prompt/sections/base.md +1 -0
- package/dist/protocol/chat-session-manager.d.ts +1 -0
- package/dist/protocol/chat-session-manager.js +2 -0
- package/dist/protocol/chat-session.d.ts +4 -1
- package/dist/protocol/chat-session.js +9 -3
- package/dist/protocol/client.d.ts +5 -1
- package/dist/protocol/client.js +8 -2
- package/dist/protocol/server.d.ts +13 -12
- package/dist/protocol/server.js +199 -67
- package/dist/protocol/types.d.ts +14 -0
- package/dist/runtime/background-shell.js +14 -0
- package/dist/runtime/safe-spawn.js +89 -11
- package/dist/runtime/spawn-common.d.ts +15 -4
- package/dist/runtime/spawn-common.js +113 -12
- package/dist/session/session-manager.js +7 -1
- package/dist/session/transcript.d.ts +4 -0
- package/dist/session/transcript.js +21 -0
- package/dist/tool-system/builtin/bash.js +3 -2
- package/dist/tool-system/builtin/cron.js +10 -2
- package/dist/tool-system/builtin/edit-model-catalog.js +15 -5
- package/dist/tool-system/builtin/generate-video.js +3 -0
- package/dist/tool-system/builtin/grep.d.ts +9 -0
- package/dist/tool-system/builtin/grep.js +100 -3
- package/dist/tool-system/builtin/index.d.ts +3 -1
- package/dist/tool-system/builtin/index.js +5 -5
- package/dist/tool-system/builtin/powershell.js +4 -1
- package/dist/tool-system/builtin/sleep.js +5 -0
- package/dist/tool-system/context.d.ts +10 -0
- package/dist/tool-system/executor.js +25 -2
- package/dist/tool-system/mcp-manager.js +17 -0
- 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/permission.d.ts +3 -1
- package/dist/tool-system/permission.js +2 -1
- package/dist/tool-system/sandbox/off.js +7 -1
- package/dist/types.d.ts +35 -1
- package/dist/utils/exec.d.ts +8 -0
- package/dist/utils/exec.js +10 -0
- package/package.json +1 -1
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import { spawn } from "node:child_process";
|
|
37
37
|
import { StringDecoder } from "node:string_decoder";
|
|
38
|
-
import { resolveSpawnTarget, defaultShellBinary, killChildTree } from "./spawn-common.js";
|
|
38
|
+
import { resolveSpawnTarget, defaultShellBinary, killChildTree, killProcessGroup } from "./spawn-common.js";
|
|
39
39
|
export const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;
|
|
40
40
|
export const DEFAULT_IO_DRAIN_GRACE_MS = 100;
|
|
41
41
|
const TIMEOUT_SIGKILL_GRACE_MS = 2000;
|
|
@@ -50,6 +50,7 @@ export function safeSpawn(file, args, opts) {
|
|
|
50
50
|
args,
|
|
51
51
|
opts,
|
|
52
52
|
cleanup: undefined,
|
|
53
|
+
resolveMs: undefined,
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
@@ -60,6 +61,7 @@ export function safeSpawn(file, args, opts) {
|
|
|
60
61
|
* abort, timeout, pre-spawn-abort. Used by the Bash tool.
|
|
61
62
|
*/
|
|
62
63
|
export function safeSpawnShell(command, opts) {
|
|
64
|
+
const resolveStartedAt = spawnProfileEnabled() ? performance.now() : 0;
|
|
63
65
|
// Don't hardcode a POSIX default here — resolveSpawnTarget →
|
|
64
66
|
// resolveShellInvocation picks the platform shell (cmd.exe on Windows,
|
|
65
67
|
// $SHELL/bin/bash on POSIX) when opts.shell is omitted.
|
|
@@ -69,11 +71,24 @@ export function safeSpawnShell(command, opts) {
|
|
|
69
71
|
shell,
|
|
70
72
|
sandbox: opts.sandbox,
|
|
71
73
|
});
|
|
72
|
-
|
|
74
|
+
// Shell commands run free-form LLM strings that routinely background
|
|
75
|
+
// grandchildren (`pytest &`, `sh → npm → node`). Spawn as a process-group
|
|
76
|
+
// leader (detached) so a timeout/abort kill reaches the WHOLE subtree, not
|
|
77
|
+
// just the direct shell — otherwise an orphaned grandchild keeps the
|
|
78
|
+
// inherited stdout pipe open and Node's `close` never fires (the hang).
|
|
79
|
+
return runLifecycle({
|
|
80
|
+
file,
|
|
81
|
+
args,
|
|
82
|
+
opts,
|
|
83
|
+
cleanup,
|
|
84
|
+
resolveMs: elapsedMs(resolveStartedAt),
|
|
85
|
+
detached: true,
|
|
86
|
+
});
|
|
73
87
|
}
|
|
74
|
-
function runLifecycle({ file, args, opts, cleanup }) {
|
|
88
|
+
function runLifecycle({ file, args, opts, cleanup, resolveMs, detached }) {
|
|
75
89
|
const maxBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
76
90
|
const abortGrace = opts.ioDrainGraceMs ?? DEFAULT_IO_DRAIN_GRACE_MS;
|
|
91
|
+
const lifecycleStartedAt = spawnProfileEnabled() ? performance.now() : 0;
|
|
77
92
|
// Pre-spawn abort: don't pay spawn cost.
|
|
78
93
|
if (opts.signal?.aborted) {
|
|
79
94
|
safeCleanup(cleanup);
|
|
@@ -81,10 +96,20 @@ function runLifecycle({ file, args, opts, cleanup }) {
|
|
|
81
96
|
}
|
|
82
97
|
return new Promise((resolve) => {
|
|
83
98
|
let settled = false;
|
|
99
|
+
// Declared before finish() so the spawn-failed early-return (which calls
|
|
100
|
+
// finish before these are assigned) doesn't hit a TDZ reference.
|
|
101
|
+
let timer;
|
|
102
|
+
let settleTimer;
|
|
103
|
+
let onAbort;
|
|
84
104
|
const finish = (result) => {
|
|
85
105
|
if (settled)
|
|
86
106
|
return;
|
|
87
107
|
settled = true;
|
|
108
|
+
if (timer)
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
if (settleTimer)
|
|
111
|
+
clearTimeout(settleTimer);
|
|
112
|
+
logSpawnProfile(file, args, resolveMs, elapsedMs(lifecycleStartedAt), result.reason);
|
|
88
113
|
// Always release backend-allocated resources, regardless of exit path.
|
|
89
114
|
// cleanup is best-effort — see seatbelt backend for rationale.
|
|
90
115
|
safeCleanup(cleanup);
|
|
@@ -97,7 +122,7 @@ function runLifecycle({ file, args, opts, cleanup }) {
|
|
|
97
122
|
};
|
|
98
123
|
let child;
|
|
99
124
|
try {
|
|
100
|
-
child = spawn(file, args, { cwd: opts.cwd, env: opts.env });
|
|
125
|
+
child = spawn(file, args, { cwd: opts.cwd, env: opts.env, detached });
|
|
101
126
|
}
|
|
102
127
|
catch (err) {
|
|
103
128
|
finish(emptyResult({ reason: "spawn_failed", spawnFailed: true, error: err.message }));
|
|
@@ -111,21 +136,63 @@ function runLifecycle({ file, args, opts, cleanup }) {
|
|
|
111
136
|
let stderrTruncated = false;
|
|
112
137
|
let timedOut = false;
|
|
113
138
|
let aborted = false;
|
|
114
|
-
|
|
139
|
+
let lastExitCode = null;
|
|
140
|
+
let lastExitSignal = null;
|
|
141
|
+
// Terminate the child on timeout/abort. When detached (shell mode) the
|
|
142
|
+
// child leads its own process group, so kill the WHOLE group — this reaps
|
|
143
|
+
// backgrounded grandchildren (`pytest &`, `sh → npm → node`) that a bare
|
|
144
|
+
// child.kill() would orphan. After the kill, arm a settle deadline:
|
|
145
|
+
// Node's `close` waits for every inherited stdio pipe to close, and an
|
|
146
|
+
// orphaned grandchild can hold stdout open forever, so `close` may never
|
|
147
|
+
// fire. Force a resolve via the last-seen `exit` code once the kill grace
|
|
148
|
+
// has elapsed — the promise must never hang past terminate().
|
|
149
|
+
const terminate = (graceMs) => {
|
|
150
|
+
if (detached && typeof child.pid === "number") {
|
|
151
|
+
void killProcessGroup(child.pid, { graceMs });
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
killChildTree(child, graceMs);
|
|
155
|
+
}
|
|
156
|
+
if (settleTimer)
|
|
157
|
+
return;
|
|
158
|
+
settleTimer = setTimeout(() => {
|
|
159
|
+
finishFromExit();
|
|
160
|
+
}, graceMs + 500);
|
|
161
|
+
settleTimer.unref?.();
|
|
162
|
+
};
|
|
163
|
+
timer = setTimeout(() => {
|
|
115
164
|
timedOut = true;
|
|
116
|
-
|
|
117
|
-
// spawns node children that child.kill() alone would orphan). POSIX:
|
|
118
|
-
// SIGTERM → grace → SIGKILL. See killChildTree.
|
|
119
|
-
killChildTree(child, TIMEOUT_SIGKILL_GRACE_MS);
|
|
165
|
+
terminate(TIMEOUT_SIGKILL_GRACE_MS);
|
|
120
166
|
}, opts.timeoutMs);
|
|
121
|
-
let onAbort;
|
|
122
167
|
if (opts.signal) {
|
|
123
168
|
onAbort = () => {
|
|
124
169
|
aborted = true;
|
|
125
|
-
|
|
170
|
+
terminate(abortGrace);
|
|
126
171
|
};
|
|
127
172
|
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
128
173
|
}
|
|
174
|
+
// Fallback finish when `close` never arrives because a killed process's
|
|
175
|
+
// orphaned grandchild still holds a stdio pipe. Uses whatever `exit` code
|
|
176
|
+
// we saw (exit fires when the direct child dies, independent of stdio).
|
|
177
|
+
const finishFromExit = () => {
|
|
178
|
+
const reason = timedOut ? "timeout" : aborted ? "aborted" : "ok";
|
|
179
|
+
finish({
|
|
180
|
+
reason,
|
|
181
|
+
stdout,
|
|
182
|
+
stderr,
|
|
183
|
+
exitCode: lastExitCode,
|
|
184
|
+
signal: lastExitSignal,
|
|
185
|
+
stdoutTruncated,
|
|
186
|
+
stderrTruncated,
|
|
187
|
+
timedOut,
|
|
188
|
+
aborted,
|
|
189
|
+
spawnFailed: false,
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
child.on("exit", (code, sig) => {
|
|
193
|
+
lastExitCode = code;
|
|
194
|
+
lastExitSignal = sig;
|
|
195
|
+
});
|
|
129
196
|
child.stdout?.on("data", (chunk) => {
|
|
130
197
|
if (stdoutTruncated)
|
|
131
198
|
return;
|
|
@@ -221,6 +288,17 @@ function runLifecycle({ file, args, opts, cleanup }) {
|
|
|
221
288
|
});
|
|
222
289
|
});
|
|
223
290
|
}
|
|
291
|
+
function spawnProfileEnabled() {
|
|
292
|
+
return process.env.CODESHELL_SPAWN_PROFILE === "1";
|
|
293
|
+
}
|
|
294
|
+
function elapsedMs(startedAt) {
|
|
295
|
+
return startedAt > 0 ? Math.round(performance.now() - startedAt) : undefined;
|
|
296
|
+
}
|
|
297
|
+
function logSpawnProfile(file, args, resolveMs, lifecycleMs, reason) {
|
|
298
|
+
if (!spawnProfileEnabled())
|
|
299
|
+
return;
|
|
300
|
+
console.error(`[spawn-profile] shell=${JSON.stringify(file)} flag=${JSON.stringify(args[0] ?? "")} resolveMs=${resolveMs ?? "n/a"} lifecycleMs=${lifecycleMs ?? "n/a"} reason=${reason}`);
|
|
301
|
+
}
|
|
224
302
|
function safeCleanup(cleanup) {
|
|
225
303
|
if (!cleanup)
|
|
226
304
|
return;
|
|
@@ -72,9 +72,11 @@ export interface SpawnTarget {
|
|
|
72
72
|
* in five places (bash.ts, safe-spawn.ts, background-shell.ts, worktree.ts):
|
|
73
73
|
*
|
|
74
74
|
* - POSIX: `<explicit ?? $SHELL ?? /bin/bash> -c "<command>"`
|
|
75
|
-
* - Windows:
|
|
76
|
-
*
|
|
77
|
-
* `-Command`,
|
|
75
|
+
* - Windows: `<explicit shell ?? Git Bash ?? PowerShell ?? ComSpec ?? cmd.exe>` with the
|
|
76
|
+
* shell-appropriate flag: Git Bash/POSIX shells use `-c`, PowerShell uses
|
|
77
|
+
* `-Command`, and cmd.exe uses `/c` (`-c` would be taken as a filename and
|
|
78
|
+
* fail/hang). Git Bash is preferred because the Bash tool receives POSIX
|
|
79
|
+
* syntax from the model.
|
|
78
80
|
*
|
|
79
81
|
* `$SHELL` is ignored on Windows — it is virtually never set there, and when
|
|
80
82
|
* it is (e.g. a stray value from a Unix-y env) it points at a POSIX path that
|
|
@@ -85,8 +87,17 @@ export declare function resolveShellInvocation(command: string, shell?: string):
|
|
|
85
87
|
file: string;
|
|
86
88
|
args: string[];
|
|
87
89
|
};
|
|
90
|
+
export declare function resolveGitBash(): string | undefined;
|
|
91
|
+
/** Reset the Git Bash probe cache. Test-only (platform is stubbed per test). */
|
|
92
|
+
export declare function _resetGitBashCache(): void;
|
|
93
|
+
export declare function resolvePowerShell(): string | undefined;
|
|
94
|
+
/** Reset the PowerShell probe cache. Test-only (platform is stubbed per test). */
|
|
95
|
+
export declare function _resetPowerShellCache(): void;
|
|
88
96
|
/** The platform's default interactive shell binary, for spawning a bare shell
|
|
89
|
-
* (no `-c`/`/c` command). Windows →
|
|
97
|
+
* (no `-c`/`/c` command). Windows → Git Bash if present, else PowerShell,
|
|
98
|
+
* else ComSpec/cmd.exe;
|
|
99
|
+
* POSIX → $SHELL/bin/bash. Windows prefers Git Bash so the model's bash-syntax
|
|
100
|
+
* commands actually run (cmd.exe can't). */
|
|
90
101
|
export declare function defaultShellBinary(shell?: string): string;
|
|
91
102
|
/**
|
|
92
103
|
* Resolve the actual (file, args) for a shell `command` under an optional
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* spawn detached, so there's no separate process group to reap); it shares
|
|
21
21
|
* (1) and (2) here. The background manager uses all three.
|
|
22
22
|
*/
|
|
23
|
-
import { spawn } from "node:child_process";
|
|
23
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
24
|
+
import { existsSync } from "node:fs";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
24
26
|
/**
|
|
25
27
|
* Env vars that are always safe to forward into a sandboxed shell. Mirrors
|
|
26
28
|
* the allowlist that previously lived in bash.ts — kept here so foreground
|
|
@@ -103,9 +105,11 @@ export function mergeShellEnv(base, projectEnv) {
|
|
|
103
105
|
* in five places (bash.ts, safe-spawn.ts, background-shell.ts, worktree.ts):
|
|
104
106
|
*
|
|
105
107
|
* - POSIX: `<explicit ?? $SHELL ?? /bin/bash> -c "<command>"`
|
|
106
|
-
* - Windows:
|
|
107
|
-
*
|
|
108
|
-
* `-Command`,
|
|
108
|
+
* - Windows: `<explicit shell ?? Git Bash ?? PowerShell ?? ComSpec ?? cmd.exe>` with the
|
|
109
|
+
* shell-appropriate flag: Git Bash/POSIX shells use `-c`, PowerShell uses
|
|
110
|
+
* `-Command`, and cmd.exe uses `/c` (`-c` would be taken as a filename and
|
|
111
|
+
* fail/hang). Git Bash is preferred because the Bash tool receives POSIX
|
|
112
|
+
* syntax from the model.
|
|
109
113
|
*
|
|
110
114
|
* `$SHELL` is ignored on Windows — it is virtually never set there, and when
|
|
111
115
|
* it is (e.g. a stray value from a Unix-y env) it points at a POSIX path that
|
|
@@ -114,21 +118,116 @@ export function mergeShellEnv(base, projectEnv) {
|
|
|
114
118
|
*/
|
|
115
119
|
export function resolveShellInvocation(command, shell) {
|
|
116
120
|
if (process.platform === "win32") {
|
|
117
|
-
const file = shell ??
|
|
118
|
-
// PowerShell
|
|
121
|
+
const file = shell ?? defaultShellBinary();
|
|
122
|
+
// Flag form depends on the shell: PowerShell → -Command; a POSIX shell such
|
|
123
|
+
// as Git Bash's bash.exe / sh → -c (it does NOT understand cmd's /c); cmd.exe
|
|
124
|
+
// (and cmd-like) → /c. Detecting bash/sh matters now that defaultShellBinary
|
|
125
|
+
// prefers Git Bash on Windows — feeding it /c would break every command.
|
|
119
126
|
const isPwsh = /(^|[\\/])(pwsh|powershell)(\.exe)?$/i.test(file);
|
|
120
|
-
|
|
127
|
+
if (isPwsh)
|
|
128
|
+
return { file, args: ["-Command", command] };
|
|
129
|
+
const isPosixShell = /(^|[\\/])(bash|sh|zsh|dash)(\.exe)?$/i.test(file);
|
|
130
|
+
return { file, args: [isPosixShell ? "-c" : "/c", command] };
|
|
121
131
|
}
|
|
122
132
|
const file = shell ?? process.env.SHELL ?? "/bin/bash";
|
|
123
133
|
return { file, args: ["-c", command] };
|
|
124
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Best-effort locate Git Bash's `bash.exe` on Windows. Returns undefined on
|
|
137
|
+
* non-Windows, when git isn't installed, or when no bash.exe is found.
|
|
138
|
+
*
|
|
139
|
+
* WHY: The Bash tool and background shells feed the model's *bash* commands
|
|
140
|
+
* (`ls`, `&&`, `$(…)`, pipes, quoting) to the shell. On Windows the historical
|
|
141
|
+
* default is `cmd.exe`, which can't run any of that — so "Bash" was effectively
|
|
142
|
+
* broken on Windows. Git for Windows (which most devs already have — we detect
|
|
143
|
+
* it for repo ops anyway) ships a full bash at `<git>\bin\bash.exe`, so prefer
|
|
144
|
+
* it. Falls back to PowerShell before cmd.exe when Git Bash truly isn't present.
|
|
145
|
+
*
|
|
146
|
+
* Resolution order:
|
|
147
|
+
* 1. CODE_SHELL_GIT_BASH_PATH env override (explicit user config wins).
|
|
148
|
+
* 2. Reverse-engineer from the git binary's location: `where git` gives e.g.
|
|
149
|
+
* `C:\Program Files\Git\cmd\git.exe` → `…\Git\bin\bash.exe`.
|
|
150
|
+
* 3. The two default install locations (Program Files / Program Files (x86)).
|
|
151
|
+
* Cached after first probe (a spawn per command would be wasteful).
|
|
152
|
+
*/
|
|
153
|
+
let gitBashCache;
|
|
154
|
+
export function resolveGitBash() {
|
|
155
|
+
if (process.platform !== "win32")
|
|
156
|
+
return undefined;
|
|
157
|
+
if (gitBashCache !== undefined)
|
|
158
|
+
return gitBashCache ?? undefined;
|
|
159
|
+
const override = process.env.CODE_SHELL_GIT_BASH_PATH;
|
|
160
|
+
if (override && existsSync(override))
|
|
161
|
+
return (gitBashCache = override);
|
|
162
|
+
const candidates = [];
|
|
163
|
+
// (2) derive from `where git`. Git installs git.exe under either \cmd\ or
|
|
164
|
+
// \bin\; bash.exe lives under the sibling \bin\. Walk up to the Git root.
|
|
165
|
+
try {
|
|
166
|
+
const out = execFileSync("where", ["git"], { encoding: "utf-8", timeout: 3000 });
|
|
167
|
+
const gitExe = out.split(/\r?\n/).find((l) => l.trim().toLowerCase().endsWith("git.exe"));
|
|
168
|
+
if (gitExe) {
|
|
169
|
+
const gitRoot = dirname(dirname(gitExe.trim())); // …\Git\cmd\git.exe → …\Git
|
|
170
|
+
candidates.push(join(gitRoot, "bin", "bash.exe"));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// git not on PATH — fall through to the well-known locations.
|
|
175
|
+
}
|
|
176
|
+
// (3) default install locations.
|
|
177
|
+
const pf = process.env["ProgramFiles"] ?? "C:\\Program Files";
|
|
178
|
+
const pf86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
179
|
+
candidates.push(join(pf, "Git", "bin", "bash.exe"));
|
|
180
|
+
candidates.push(join(pf86, "Git", "bin", "bash.exe"));
|
|
181
|
+
const found = candidates.find((p) => existsSync(p));
|
|
182
|
+
return (gitBashCache = found ?? null) ?? undefined;
|
|
183
|
+
}
|
|
184
|
+
/** Reset the Git Bash probe cache. Test-only (platform is stubbed per test). */
|
|
185
|
+
export function _resetGitBashCache() {
|
|
186
|
+
gitBashCache = undefined;
|
|
187
|
+
}
|
|
188
|
+
let powerShellCache;
|
|
189
|
+
export function resolvePowerShell() {
|
|
190
|
+
if (process.platform !== "win32")
|
|
191
|
+
return undefined;
|
|
192
|
+
if (powerShellCache !== undefined)
|
|
193
|
+
return powerShellCache ?? undefined;
|
|
194
|
+
const override = process.env.CODE_SHELL_POWERSHELL_PATH;
|
|
195
|
+
if (override && existsSync(override))
|
|
196
|
+
return (powerShellCache = override);
|
|
197
|
+
const candidates = [];
|
|
198
|
+
for (const exe of ["pwsh", "powershell"]) {
|
|
199
|
+
try {
|
|
200
|
+
const out = execFileSync("where", [exe], { encoding: "utf-8", timeout: 3000 });
|
|
201
|
+
const found = out.split(/\r?\n/).find((l) => l.trim().toLowerCase().endsWith(`${exe}.exe`));
|
|
202
|
+
if (found)
|
|
203
|
+
candidates.push(found.trim());
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Not on PATH; try the next shell / well-known locations.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const pf = process.env["ProgramFiles"] ?? "C:\\Program Files";
|
|
210
|
+
const systemRoot = process.env.SystemRoot ?? "C:\\Windows";
|
|
211
|
+
candidates.push(join(pf, "PowerShell", "7", "pwsh.exe"));
|
|
212
|
+
candidates.push(join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"));
|
|
213
|
+
const found = candidates.find((p) => existsSync(p));
|
|
214
|
+
return (powerShellCache = found ?? null) ?? undefined;
|
|
215
|
+
}
|
|
216
|
+
/** Reset the PowerShell probe cache. Test-only (platform is stubbed per test). */
|
|
217
|
+
export function _resetPowerShellCache() {
|
|
218
|
+
powerShellCache = undefined;
|
|
219
|
+
}
|
|
125
220
|
/** The platform's default interactive shell binary, for spawning a bare shell
|
|
126
|
-
* (no `-c`/`/c` command). Windows →
|
|
221
|
+
* (no `-c`/`/c` command). Windows → Git Bash if present, else PowerShell,
|
|
222
|
+
* else ComSpec/cmd.exe;
|
|
223
|
+
* POSIX → $SHELL/bin/bash. Windows prefers Git Bash so the model's bash-syntax
|
|
224
|
+
* commands actually run (cmd.exe can't). */
|
|
127
225
|
export function defaultShellBinary(shell) {
|
|
128
226
|
if (shell)
|
|
129
227
|
return shell;
|
|
130
|
-
if (process.platform === "win32")
|
|
131
|
-
return process.env.ComSpec ?? "cmd.exe";
|
|
228
|
+
if (process.platform === "win32") {
|
|
229
|
+
return resolveGitBash() ?? resolvePowerShell() ?? process.env.ComSpec ?? "cmd.exe";
|
|
230
|
+
}
|
|
132
231
|
return process.env.SHELL ?? "/bin/bash";
|
|
133
232
|
}
|
|
134
233
|
/**
|
|
@@ -143,8 +242,10 @@ export function resolveSpawnTarget(command, opts) {
|
|
|
143
242
|
const wrapped = opts.sandbox.wrap(command, { cwd: opts.cwd, shell: opts.shell });
|
|
144
243
|
return { file: wrapped.file, args: wrapped.args, cleanup: wrapped.cleanup };
|
|
145
244
|
}
|
|
146
|
-
// No sandbox
|
|
147
|
-
//
|
|
245
|
+
// No sandbox configured: pick the shell + command-flag form for the
|
|
246
|
+
// platform instead of assuming POSIX `-c`. Note Bash always passes a
|
|
247
|
+
// backend (off at minimum), so it never reaches this line — the off
|
|
248
|
+
// backend's wrap() delegates to resolveShellInvocation itself.
|
|
148
249
|
return resolveShellInvocation(command, opts.shell);
|
|
149
250
|
}
|
|
150
251
|
/**
|
|
@@ -7,6 +7,7 @@ import { homedir } from "node:os";
|
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
import { Transcript } from "./transcript.js";
|
|
9
9
|
import { SessionError } from "../exceptions.js";
|
|
10
|
+
import { normalizeCumulativeUsageCounters } from "../engine/session-usage.js";
|
|
10
11
|
/**
|
|
11
12
|
* Validate a session ID before it is joined into a filesystem path.
|
|
12
13
|
*
|
|
@@ -83,6 +84,9 @@ export class SessionManager {
|
|
|
83
84
|
model,
|
|
84
85
|
provider,
|
|
85
86
|
tokenUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
87
|
+
cumulativePromptTokens: 0,
|
|
88
|
+
cumulativeCacheReadTokens: 0,
|
|
89
|
+
cumulativeCacheCreationTokens: 0,
|
|
86
90
|
turnCount: 0,
|
|
87
91
|
invokedSkills: [],
|
|
88
92
|
status: "active",
|
|
@@ -245,6 +249,7 @@ export class SessionManager {
|
|
|
245
249
|
const transcriptFile = join(sessionDir, "transcript.jsonl");
|
|
246
250
|
const transcript = Transcript.loadFromFile(transcriptFile);
|
|
247
251
|
state.status = "active";
|
|
252
|
+
Object.assign(state, normalizeCumulativeUsageCounters(state, state.tokenUsage));
|
|
248
253
|
return { state, transcript };
|
|
249
254
|
}
|
|
250
255
|
saveState(state) {
|
|
@@ -275,7 +280,8 @@ export class SessionManager {
|
|
|
275
280
|
newBundle.state.parentSessionId = sourceSessionId;
|
|
276
281
|
// Copy events up to the fork point
|
|
277
282
|
for (const event of events) {
|
|
278
|
-
if (event.type === "turn_boundary" &&
|
|
283
|
+
if (event.type === "turn_boundary" &&
|
|
284
|
+
(event.data.turnNumber ?? -1) > forkTurn) {
|
|
279
285
|
break;
|
|
280
286
|
}
|
|
281
287
|
newBundle.transcript.append(event.type, event.data);
|
|
@@ -22,7 +22,10 @@ export declare class Transcript {
|
|
|
22
22
|
*/
|
|
23
23
|
appendMessage(role: string, content: string | ContentBlock[], opts?: {
|
|
24
24
|
injected?: boolean;
|
|
25
|
+
steerId?: string;
|
|
26
|
+
clientMessageId?: string;
|
|
25
27
|
}): TranscriptEvent;
|
|
28
|
+
hasClientMessageId(clientMessageId: string): boolean;
|
|
26
29
|
appendToolUse(toolName: string, toolCallId: string, args: Record<string, unknown>): TranscriptEvent;
|
|
27
30
|
appendToolResult(toolCallId: string, toolName: string, result?: string, error?: string): TranscriptEvent;
|
|
28
31
|
/** Anchor for a spawned sub-agent (see TranscriptEventType "subagent").
|
|
@@ -52,6 +55,7 @@ export declare class Transcript {
|
|
|
52
55
|
getEvents(type?: TranscriptEventType): TranscriptEvent[];
|
|
53
56
|
get turnNumber(): number;
|
|
54
57
|
get eventCount(): number;
|
|
58
|
+
private findMessageByClientId;
|
|
55
59
|
private flush;
|
|
56
60
|
/**
|
|
57
61
|
* Repair tool_result pairing issues:
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { appendFileSync, readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname } from "node:path";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
|
+
import { logger } from "../logging/logger.js";
|
|
8
9
|
export class Transcript {
|
|
9
10
|
events = [];
|
|
10
11
|
filePath;
|
|
@@ -40,12 +41,28 @@ export class Transcript {
|
|
|
40
41
|
* step-gap steering messages are left unmarked so they render normally.
|
|
41
42
|
*/
|
|
42
43
|
appendMessage(role, content, opts) {
|
|
44
|
+
if (opts?.clientMessageId) {
|
|
45
|
+
const existing = this.findMessageByClientId(opts.clientMessageId);
|
|
46
|
+
if (existing) {
|
|
47
|
+
logger.info("steer.submit.duplicate_ignored", {
|
|
48
|
+
clientMessageId: opts.clientMessageId,
|
|
49
|
+
role,
|
|
50
|
+
transcript: this.filePath,
|
|
51
|
+
});
|
|
52
|
+
return existing;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
43
55
|
return this.append("message", {
|
|
44
56
|
role,
|
|
45
57
|
content,
|
|
46
58
|
...(opts?.injected ? { injected: true } : {}),
|
|
59
|
+
...(opts?.steerId ? { steerId: opts.steerId } : {}),
|
|
60
|
+
...(opts?.clientMessageId ? { clientMessageId: opts.clientMessageId } : {}),
|
|
47
61
|
});
|
|
48
62
|
}
|
|
63
|
+
hasClientMessageId(clientMessageId) {
|
|
64
|
+
return this.findMessageByClientId(clientMessageId) !== undefined;
|
|
65
|
+
}
|
|
49
66
|
appendToolUse(toolName, toolCallId, args) {
|
|
50
67
|
return this.append("tool_use", { toolName, toolCallId, args });
|
|
51
68
|
}
|
|
@@ -150,6 +167,10 @@ export class Transcript {
|
|
|
150
167
|
get eventCount() {
|
|
151
168
|
return this.events.length;
|
|
152
169
|
}
|
|
170
|
+
findMessageByClientId(clientMessageId) {
|
|
171
|
+
return this.events.find((event) => event.type === "message" &&
|
|
172
|
+
event.data.clientMessageId === clientMessageId);
|
|
173
|
+
}
|
|
153
174
|
flush(event) {
|
|
154
175
|
try {
|
|
155
176
|
appendFileSync(this.filePath, JSON.stringify(event) + "\n", "utf-8");
|
|
@@ -29,8 +29,9 @@ function sandboxMark(backend) {
|
|
|
29
29
|
export const bashToolDef = {
|
|
30
30
|
name: "Bash",
|
|
31
31
|
description: "Execute a shell command and return its output. " +
|
|
32
|
-
"
|
|
33
|
-
"
|
|
32
|
+
"Use for ordinary shell commands, system operations, git commands, " +
|
|
33
|
+
"package-manager commands, running tests, installing packages, etc. Prefer Bash " +
|
|
34
|
+
"over PowerShell unless the task needs PowerShell-specific syntax or APIs.",
|
|
34
35
|
inputSchema: {
|
|
35
36
|
type: "object",
|
|
36
37
|
properties: {
|
|
@@ -27,7 +27,14 @@ export const cronCreateToolDef = {
|
|
|
27
27
|
"'30 8 * * 1' = 8:30am every Monday. Day-of-week: 0=Sunday..6=Saturday.\n\n" +
|
|
28
28
|
"For calendar schedules, set `timezone` to the user's IANA zone (e.g. 'Asia/Shanghai', " +
|
|
29
29
|
"'America/New_York'); ask the user if unknown. Set `cwd` to the project the job operates on. " +
|
|
30
|
-
"Leave `permissionLevel` as 'read-only' unless the user explicitly wants the job to modify code
|
|
30
|
+
"Leave `permissionLevel` as 'read-only' unless the user explicitly wants the job to modify code.\n\n" +
|
|
31
|
+
"SELF-WAKEUP / background-task safety net: you can also schedule THIS job for yourself, with no user " +
|
|
32
|
+
"request, to re-check on a long-running background task (a download, a build, a background shell/agent) " +
|
|
33
|
+
"that might hang and never signal completion. Use a short interval + `once: true` + `continueInSession: true` " +
|
|
34
|
+
"and a `prompt` that reminds you what to check, e.g. schedule '5m', once true, continueInSession true, " +
|
|
35
|
+
"prompt 'check whether the yt-dlp download finished (BashOutput/ListShells); if still running, wait again'. " +
|
|
36
|
+
"You wake back in THIS conversation with full context, inspect the task, and either finish or reschedule. " +
|
|
37
|
+
"This is the right pattern for a simple poll-until-done loop or a hang safety net — prefer it over looping Sleep.",
|
|
31
38
|
inputSchema: {
|
|
32
39
|
type: "object",
|
|
33
40
|
properties: {
|
|
@@ -52,7 +59,8 @@ export const cronCreateToolDef = {
|
|
|
52
59
|
once: {
|
|
53
60
|
type: "boolean",
|
|
54
61
|
description: "true = one-shot: run once at the scheduled time, then auto-delete (for 'in N minutes / " +
|
|
55
|
-
"at <time>, do X once' reminders or
|
|
62
|
+
"at <time>, do X once' reminders, tasks, or a self-wakeup to re-check a background task). " +
|
|
63
|
+
"Default false = recurring per `schedule`. " +
|
|
56
64
|
"A one-shot still uses `schedule` for its time: interval '10m' = 10 minutes from now; " +
|
|
57
65
|
"cron '0 7 25 6 *' = once at 07:00 on June 25.",
|
|
58
66
|
},
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
import { randomBytes } from "node:crypto";
|
|
11
11
|
import { userCatalogPath } from "../../model-catalog/index.js";
|
|
12
12
|
import { saveCatalogEntry } from "../../model-catalog/save-entry.js";
|
|
13
|
+
import { catalogEntrySchema } from "../../model-catalog/types.js";
|
|
13
14
|
export const editModelCatalogToolDef = {
|
|
14
15
|
name: "EditModelCatalog",
|
|
15
16
|
description: "Add or update a provider/model template in the user model catalog so it " +
|
|
16
|
-
"appears in the connection page (text/image/video). Keyed by `id
|
|
17
|
-
"adds, an existing id
|
|
17
|
+
"appears in the connection page (text/image/video). Keyed by `id`: a new id " +
|
|
18
|
+
"adds a provider, an existing id replaces that user-catalog entry. Backs up " +
|
|
19
|
+
"the file before writing and " +
|
|
18
20
|
"validates the entry. Use after researching a model's real facts (id, context " +
|
|
19
21
|
"window, supported params, modalities) — do NOT guess; verify against the " +
|
|
20
22
|
"provider's official docs first (see the model-fact-finder skill). Does NOT set " +
|
|
@@ -31,7 +33,8 @@ export const editModelCatalogToolDef = {
|
|
|
31
33
|
"maxContextTokens?, maxOutputTokens?, supportsVision?, params?[]}). Each param: " +
|
|
32
34
|
"{name, label?, control (enum|number|toggle|text), options?[], min?, max?, default?, " +
|
|
33
35
|
"doc?, wire?{field}}. Fill maxContextTokens with the model's REAL window; declare " +
|
|
34
|
-
"params per-model (only what that model supports)."
|
|
36
|
+
"params per-model (only what that model supports). To keep a custom provider " +
|
|
37
|
+
"beside a built-in one, use a distinct id.",
|
|
35
38
|
},
|
|
36
39
|
},
|
|
37
40
|
required: ["entry"],
|
|
@@ -42,11 +45,18 @@ export async function editModelCatalogTool(args) {
|
|
|
42
45
|
if (!entry || typeof entry !== "object") {
|
|
43
46
|
return "Error: `entry` (a CatalogEntry object) is required.";
|
|
44
47
|
}
|
|
48
|
+
const parsed = catalogEntrySchema.safeParse(entry);
|
|
49
|
+
if (!parsed.success) {
|
|
50
|
+
return `Error: invalid catalog entry: ${parsed.error.issues.map((i) => i.message).join("; ")}`;
|
|
51
|
+
}
|
|
45
52
|
const stamp = `${Date.now()}-${randomBytes(3).toString("hex")}`;
|
|
46
|
-
const r = saveCatalogEntry(
|
|
53
|
+
const r = saveCatalogEntry(parsed.data, {
|
|
54
|
+
path: userCatalogPath(),
|
|
55
|
+
stamp,
|
|
56
|
+
});
|
|
47
57
|
if (!r.ok)
|
|
48
58
|
return `Error: ${r.error}`;
|
|
49
|
-
return summarizeWrite(
|
|
59
|
+
return summarizeWrite(parsed.data, r.action ?? "added", r.backup);
|
|
50
60
|
}
|
|
51
61
|
/**
|
|
52
62
|
* Build a structured completion summary so the user can SEE exactly what was
|
|
@@ -86,6 +86,7 @@ export const generateVideoToolDef = {
|
|
|
86
86
|
let injectedProvider = null;
|
|
87
87
|
export function __setVideoProviderForTests(p) {
|
|
88
88
|
injectedProvider = p;
|
|
89
|
+
availCache.clear();
|
|
89
90
|
}
|
|
90
91
|
/**
|
|
91
92
|
* Tool-visibility guard: GenerateVideo is hidden until a video provider is
|
|
@@ -97,6 +98,8 @@ export function __setVideoProviderForTests(p) {
|
|
|
97
98
|
const availCache = new Map();
|
|
98
99
|
const AVAIL_TTL_MS = 1000;
|
|
99
100
|
export function isGenerateVideoAvailable(cwd = process.cwd(), nowMs = Date.now()) {
|
|
101
|
+
if (injectedProvider)
|
|
102
|
+
return true;
|
|
100
103
|
const hit = availCache.get(cwd);
|
|
101
104
|
if (hit && nowMs - hit.at < AVAIL_TTL_MS)
|
|
102
105
|
return hit.value;
|
|
@@ -3,5 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ToolDefinition } from "../../types.js";
|
|
5
5
|
import type { ToolContext } from "../context.js";
|
|
6
|
+
type ExecFileForGrep = (file: string, args: readonly string[], options: {
|
|
7
|
+
maxBuffer: number;
|
|
8
|
+
timeout: number;
|
|
9
|
+
}) => Promise<{
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function _setGrepExecFileForTest(fn?: ExecFileForGrep): void;
|
|
6
14
|
export declare const grepToolDef: ToolDefinition;
|
|
7
15
|
export declare function grepTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
16
|
+
export {};
|