@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
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
* Built-in Grep content search tool.
|
|
3
3
|
*/
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
5
6
|
import { promisify } from "node:util";
|
|
6
|
-
import { resolve, sep, isAbsolute } from "node:path";
|
|
7
|
+
import { basename, join, relative, resolve, sep, isAbsolute } from "node:path";
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
9
|
+
let execFileForTest = execFileAsync;
|
|
10
|
+
export function _setGrepExecFileForTest(fn) {
|
|
11
|
+
execFileForTest = fn ?? execFileAsync;
|
|
12
|
+
}
|
|
8
13
|
/**
|
|
9
14
|
* Strip the cwd prefix from each line so results display as relative
|
|
10
15
|
* paths (e.g. "src/ui/App.tsx" instead of an absolute path). Lines that
|
|
@@ -76,10 +81,16 @@ export async function grepTool(args, ctx) {
|
|
|
76
81
|
catch (grepErr) {
|
|
77
82
|
if (isNoMatchExit(grepErr))
|
|
78
83
|
return "No matches found.";
|
|
84
|
+
if (isCommandNotFound(rgErr) || isCommandNotFound(grepErr)) {
|
|
85
|
+
return await runNodeGrep(pattern, searchPath, fileGlob, context, maxResults, outputMode, caseInsensitive);
|
|
86
|
+
}
|
|
79
87
|
return `Error in search: ${grepErr.message}`;
|
|
80
88
|
}
|
|
81
89
|
}
|
|
82
90
|
}
|
|
91
|
+
function isCommandNotFound(err) {
|
|
92
|
+
return err?.code === "ENOENT";
|
|
93
|
+
}
|
|
83
94
|
async function runRipgrep(pattern, path, fileGlob, context, maxResults, outputMode, caseInsensitive) {
|
|
84
95
|
const args = ["--color=never"];
|
|
85
96
|
if (caseInsensitive)
|
|
@@ -102,7 +113,7 @@ async function runRipgrep(pattern, path, fileGlob, context, maxResults, outputMo
|
|
|
102
113
|
// Ignore common non-source dirs
|
|
103
114
|
args.push("--glob", "!node_modules", "--glob", "!.git", "--glob", "!dist", "--glob", "!coverage");
|
|
104
115
|
args.push("--", pattern, path);
|
|
105
|
-
const { stdout } = await
|
|
116
|
+
const { stdout } = await execFileForTest("rg", args, {
|
|
106
117
|
maxBuffer: 10 * 1024 * 1024,
|
|
107
118
|
timeout: 30_000,
|
|
108
119
|
});
|
|
@@ -139,7 +150,7 @@ async function runGrep(pattern, path, fileGlob, context, maxResults, outputMode,
|
|
|
139
150
|
args.push("-E", pattern);
|
|
140
151
|
args.push("--exclude-dir=node_modules", "--exclude-dir=.git", "--exclude-dir=dist");
|
|
141
152
|
args.push(path);
|
|
142
|
-
const { stdout } = await
|
|
153
|
+
const { stdout } = await execFileForTest("grep", args, {
|
|
143
154
|
maxBuffer: 10 * 1024 * 1024,
|
|
144
155
|
timeout: 30_000,
|
|
145
156
|
});
|
|
@@ -152,3 +163,89 @@ async function runGrep(pattern, path, fileGlob, context, maxResults, outputMode,
|
|
|
152
163
|
}
|
|
153
164
|
return result;
|
|
154
165
|
}
|
|
166
|
+
const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "coverage"]);
|
|
167
|
+
async function runNodeGrep(pattern, path, fileGlob, context, maxResults, outputMode, caseInsensitive) {
|
|
168
|
+
let regex;
|
|
169
|
+
try {
|
|
170
|
+
regex = new RegExp(pattern, caseInsensitive ? "i" : "");
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
return `Error in search: ${err.message}`;
|
|
174
|
+
}
|
|
175
|
+
const matches = [];
|
|
176
|
+
await walkTextFiles(path, fileGlob, async (file) => {
|
|
177
|
+
if (matches.length >= maxResults)
|
|
178
|
+
return;
|
|
179
|
+
let text;
|
|
180
|
+
try {
|
|
181
|
+
text = await readFile(file, "utf8");
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const lines = text.split(/\r?\n/);
|
|
187
|
+
const matchingLines = [];
|
|
188
|
+
for (let i = 0; i < lines.length; i++) {
|
|
189
|
+
regex.lastIndex = 0;
|
|
190
|
+
if (regex.test(lines[i]))
|
|
191
|
+
matchingLines.push(i);
|
|
192
|
+
}
|
|
193
|
+
if (matchingLines.length === 0)
|
|
194
|
+
return;
|
|
195
|
+
const rel = relative(path, file) || basename(file);
|
|
196
|
+
if (outputMode === "files_with_matches") {
|
|
197
|
+
matches.push(rel);
|
|
198
|
+
}
|
|
199
|
+
else if (outputMode === "count") {
|
|
200
|
+
matches.push(`${rel}:${matchingLines.length}`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const emitted = new Set();
|
|
204
|
+
for (const lineIndex of matchingLines) {
|
|
205
|
+
const start = Math.max(0, lineIndex - context);
|
|
206
|
+
const end = Math.min(lines.length - 1, lineIndex + context);
|
|
207
|
+
for (let i = start; i <= end; i++) {
|
|
208
|
+
if (emitted.has(i))
|
|
209
|
+
continue;
|
|
210
|
+
emitted.add(i);
|
|
211
|
+
matches.push(`${rel}:${i + 1}:${lines[i]}`);
|
|
212
|
+
if (matches.length >= maxResults)
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
if (matches.length === 0)
|
|
219
|
+
return "No matches found.";
|
|
220
|
+
if (matches.length > 200)
|
|
221
|
+
return matches.slice(0, 200).join("\n") + `\n\n... ${matches.length - 200} more results`;
|
|
222
|
+
return matches.join("\n");
|
|
223
|
+
}
|
|
224
|
+
async function walkTextFiles(root, fileGlob, visit) {
|
|
225
|
+
const info = await stat(root).catch(() => null);
|
|
226
|
+
if (!info)
|
|
227
|
+
return;
|
|
228
|
+
if (info.isFile()) {
|
|
229
|
+
if (!fileGlob || matchesSimpleGlob(basename(root), fileGlob))
|
|
230
|
+
await visit(root);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (!info.isDirectory())
|
|
234
|
+
return;
|
|
235
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
236
|
+
for (const entry of entries) {
|
|
237
|
+
if (entry.isDirectory()) {
|
|
238
|
+
if (IGNORED_DIRS.has(entry.name))
|
|
239
|
+
continue;
|
|
240
|
+
await walkTextFiles(join(root, entry.name), fileGlob, visit);
|
|
241
|
+
}
|
|
242
|
+
else if (entry.isFile()) {
|
|
243
|
+
if (!fileGlob || matchesSimpleGlob(entry.name, fileGlob))
|
|
244
|
+
await visit(join(root, entry.name));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function matchesSimpleGlob(name, glob) {
|
|
249
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
250
|
+
return new RegExp(`^${escaped}$`).test(name);
|
|
251
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Built-in tool registration.
|
|
3
3
|
*/
|
|
4
4
|
import type { RegisteredTool } from "../../types.js";
|
|
5
|
+
import type { ToolVisibilityContext } from "../context.js";
|
|
5
6
|
/**
|
|
6
7
|
* Tool executor signature.
|
|
7
8
|
*
|
|
@@ -27,6 +28,7 @@ export type BuiltinToolResult = string | {
|
|
|
27
28
|
sandbox: import("../../types.js").ToolResult["sandbox"];
|
|
28
29
|
};
|
|
29
30
|
export type BuiltinToolFn = (args: Record<string, unknown>, ctx?: import("../context.js").ToolContext) => Promise<BuiltinToolResult>;
|
|
31
|
+
export type BuiltinToolGuard = (ctx: ToolVisibilityContext) => boolean;
|
|
30
32
|
export interface BuiltinTool {
|
|
31
33
|
definition: RegisteredTool;
|
|
32
34
|
execute: BuiltinToolFn;
|
|
@@ -38,6 +40,6 @@ export declare const BUILTIN_TOOLS: BuiltinTool[];
|
|
|
38
40
|
* engine.ts toolDefs assembly). Tools NOT listed here are always visible.
|
|
39
41
|
* Keyed by the tool's `name` (must match the toolDef name).
|
|
40
42
|
*/
|
|
41
|
-
export declare const BUILTIN_TOOL_GUARDS: Map<string,
|
|
43
|
+
export declare const BUILTIN_TOOL_GUARDS: Map<string, BuiltinToolGuard>;
|
|
42
44
|
/** UseCredential is available when the cwd's CredentialStore has ≥1 credential. */
|
|
43
45
|
export declare function isUseCredentialAvailable(cwd: string): boolean;
|
|
@@ -711,17 +711,17 @@ export const BUILTIN_TOOLS = [
|
|
|
711
711
|
* Keyed by the tool's `name` (must match the toolDef name).
|
|
712
712
|
*/
|
|
713
713
|
export const BUILTIN_TOOL_GUARDS = new Map([
|
|
714
|
-
[webSearchToolDef.name, isWebSearchAvailable],
|
|
715
|
-
[generateImageToolDef.name, isGenerateImageAvailable],
|
|
716
|
-
[generateVideoToolDef.name, isGenerateVideoAvailable],
|
|
714
|
+
[webSearchToolDef.name, (ctx) => isWebSearchAvailable(ctx.cwd)],
|
|
715
|
+
[generateImageToolDef.name, (ctx) => isGenerateImageAvailable(ctx.cwd)],
|
|
716
|
+
[generateVideoToolDef.name, (ctx) => isGenerateVideoAvailable(ctx.cwd)],
|
|
717
717
|
// UseCredential is hidden until at least one credential exists — keeps it out
|
|
718
718
|
// of the tool list (and the context) for the common no-credentials case,
|
|
719
719
|
// matching the spec's "quiet when empty" intent (true ToolSearch-deferral for
|
|
720
720
|
// builtins isn't wired in the engine).
|
|
721
|
-
[useCredentialToolDef.name, isUseCredentialAvailable],
|
|
721
|
+
[useCredentialToolDef.name, (ctx) => isUseCredentialAvailable(ctx.cwd)],
|
|
722
722
|
// InjectCredential hidden until ≥1 cookie credential exists (browser injection
|
|
723
723
|
// is cookie-only). Also degrades at call time if no browser bridge is wired.
|
|
724
|
-
[injectCredentialToolDef.name, isInjectCredentialAvailable],
|
|
724
|
+
[injectCredentialToolDef.name, (ctx) => isInjectCredentialAvailable(ctx.cwd)],
|
|
725
725
|
]);
|
|
726
726
|
/** UseCredential is available when the cwd's CredentialStore has ≥1 credential. */
|
|
727
727
|
export function isUseCredentialAvailable(cwd) {
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
import { safeSpawn } from "../../runtime/safe-spawn.js";
|
|
9
9
|
export const powershellToolDef = {
|
|
10
10
|
name: "PowerShell",
|
|
11
|
-
description: "Execute PowerShell commands.
|
|
11
|
+
description: "Execute PowerShell commands. Use only when the user explicitly asks for PowerShell " +
|
|
12
|
+
"or the task requires PowerShell-specific cmdlets, Windows APIs, registry access, " +
|
|
13
|
+
"or .ps1 behavior. Do NOT use for ordinary file, git, package-manager, test, " +
|
|
14
|
+
"or POSIX-style shell commands; use Bash for those.",
|
|
12
15
|
inputSchema: {
|
|
13
16
|
type: "object",
|
|
14
17
|
properties: {
|
|
@@ -6,6 +6,11 @@ export const sleepToolDef = {
|
|
|
6
6
|
description: "Pause execution for a brief, deterministic wait (e.g. letting a just-started service settle for a few seconds). " +
|
|
7
7
|
"Do NOT use Sleep to poll for or wait on background work (background shells, async sub-agents, video generation): " +
|
|
8
8
|
"the system wakes you automatically when that work completes — just end your turn instead of looping Sleep. " +
|
|
9
|
+
"If you want a safety net in case a background task hangs and never signals completion, do NOT loop Sleep either — " +
|
|
10
|
+
"instead end your turn and schedule a one-shot self-wakeup with CronCreate " +
|
|
11
|
+
"({ schedule: '5m', once: true, continueInSession: true, permissionLevel: 'read-only', " +
|
|
12
|
+
"prompt: 'check whether <that task> finished; if still running, wait again' }). " +
|
|
13
|
+
"That returns control to you at the interval without burning a turn spinning. " +
|
|
9
14
|
"Maximum duration is 300 seconds (5 minutes).",
|
|
10
15
|
inputSchema: {
|
|
11
16
|
type: "object",
|
|
@@ -169,6 +169,10 @@ export interface SubAgentSpawner {
|
|
|
169
169
|
* Optional fields are filled in by Engine.run(); some headless paths may
|
|
170
170
|
* leave them undefined (e.g. running without UI → no askUser).
|
|
171
171
|
*/
|
|
172
|
+
export interface ToolVisibilityContext {
|
|
173
|
+
cwd: string;
|
|
174
|
+
hasGoal: boolean;
|
|
175
|
+
}
|
|
172
176
|
export interface ToolContext {
|
|
173
177
|
/** Active working directory for this Engine. */
|
|
174
178
|
cwd: string;
|
|
@@ -290,6 +294,12 @@ export interface ToolContext {
|
|
|
290
294
|
* sub-agents and no-cwd contexts (same as readBuiltinOverride).
|
|
291
295
|
*/
|
|
292
296
|
disabledBuiltins?: Set<string>;
|
|
297
|
+
/**
|
|
298
|
+
* Per-turn context used by builtin availability guards. Engine.run() uses the
|
|
299
|
+
* same object to hide tools from the model; ToolExecutor reuses it to reject
|
|
300
|
+
* direct calls to tools that are not available in the current runtime state.
|
|
301
|
+
*/
|
|
302
|
+
toolVisibility?: ToolVisibilityContext;
|
|
293
303
|
/**
|
|
294
304
|
* MCP servers THIS session's merged config enables (keys of
|
|
295
305
|
* config.mcpServers, enabled!==false). The pool + registry are
|
|
@@ -11,6 +11,9 @@ import { validateToolArgs } from "./validation.js";
|
|
|
11
11
|
import { PLAN_MODE_ALLOWED_TOOLS } from "./plan-mode-allowlist.js";
|
|
12
12
|
import { enforcePathPolicyWithApproval } from "./path-policy.js";
|
|
13
13
|
import { parsePatch } from "./builtin/apply-patch/parser.js";
|
|
14
|
+
import { BUILTIN_TOOL_GUARDS } from "./builtin/index.js";
|
|
15
|
+
import { COMPLETE_GOAL_TOOL_NAME } from "./builtin/complete-goal.js";
|
|
16
|
+
import { CANCEL_GOAL_TOOL_NAME } from "./builtin/cancel-goal.js";
|
|
14
17
|
// A1 hardening: hooks must never promote a non-`allow` classifier
|
|
15
18
|
// decision to `allow`. They may otherwise adjust the decision freely
|
|
16
19
|
// (e.g. tighten `allow` to `deny`/`ask`, or relax `deny` to `ask` to
|
|
@@ -119,6 +122,24 @@ export class ToolExecutor {
|
|
|
119
122
|
isError: true,
|
|
120
123
|
};
|
|
121
124
|
}
|
|
125
|
+
if ((call.toolName === COMPLETE_GOAL_TOOL_NAME || call.toolName === CANCEL_GOAL_TOOL_NAME) &&
|
|
126
|
+
this.toolCtx?.toolVisibility?.hasGoal !== true) {
|
|
127
|
+
return {
|
|
128
|
+
id: call.id,
|
|
129
|
+
toolName: call.toolName,
|
|
130
|
+
error: `Tool ${call.toolName} is only available while an active goal is present. Do NOT retry this tool call unless a goal is active.`,
|
|
131
|
+
isError: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const visibilityGuard = BUILTIN_TOOL_GUARDS.get(call.toolName);
|
|
135
|
+
if (visibilityGuard && this.toolCtx?.toolVisibility && !visibilityGuard(this.toolCtx.toolVisibility)) {
|
|
136
|
+
return {
|
|
137
|
+
id: call.id,
|
|
138
|
+
toolName: call.toolName,
|
|
139
|
+
error: `Tool ${call.toolName} is not available in the current session context. Do NOT retry this tool call unless the relevant context changes.`,
|
|
140
|
+
isError: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
122
143
|
// Same gate for MCP tools: the registry is worker-shared, so it can hold
|
|
123
144
|
// tools from servers OTHER sessions enabled. Visibility filtering hides
|
|
124
145
|
// them from this session's tool list; this rejects a direct call anyway.
|
|
@@ -274,7 +295,7 @@ export class ToolExecutor {
|
|
|
274
295
|
// the hook and the user together have decided.
|
|
275
296
|
if (hookResult.decision === "ask") {
|
|
276
297
|
const reason = hookResult.messages?.join("\n") ?? undefined;
|
|
277
|
-
const approved = await this.permission.handleAsk(call.toolName, call.args, reason);
|
|
298
|
+
const approved = await this.permission.handleAsk(call.toolName, call.args, reason, { sessionId: this.toolCtx?.sessionId });
|
|
278
299
|
if (!approved) {
|
|
279
300
|
return {
|
|
280
301
|
id: call.id,
|
|
@@ -347,7 +368,9 @@ export class ToolExecutor {
|
|
|
347
368
|
}
|
|
348
369
|
if (decision === "ask") {
|
|
349
370
|
const reason = permHook.messages?.join("\n");
|
|
350
|
-
const approved = await this.permission.handleAsk(call.toolName, call.args, reason
|
|
371
|
+
const approved = await this.permission.handleAsk(call.toolName, call.args, reason, {
|
|
372
|
+
sessionId: this.toolCtx?.sessionId,
|
|
373
|
+
});
|
|
351
374
|
if (!approved) {
|
|
352
375
|
return {
|
|
353
376
|
id: call.id,
|
|
@@ -11,6 +11,7 @@ import { CredentialStore } from "../credentials/index.js";
|
|
|
11
11
|
import { ENV_ALLOWLIST } from "../runtime/spawn-common.js";
|
|
12
12
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
+
import { diagnoseMcpStdioMissingCommand, previewPath } from "./mcp-stdio-diagnostics.js";
|
|
14
15
|
/**
|
|
15
16
|
* Read a required secret from `process.env` by NAME (Codex-style env-secret
|
|
16
17
|
* handling — the value is never persisted in MCP config). A referenced env var
|
|
@@ -393,6 +394,22 @@ export class MCPManager {
|
|
|
393
394
|
catch {
|
|
394
395
|
// ignore cleanup errors
|
|
395
396
|
}
|
|
397
|
+
if (transportType === "stdio" && config.command) {
|
|
398
|
+
const diagnostic = await diagnoseMcpStdioMissingCommand(config.command, err);
|
|
399
|
+
if (diagnostic) {
|
|
400
|
+
logger.warn("mcp.stdio_command_missing", {
|
|
401
|
+
server: name,
|
|
402
|
+
command: config.command,
|
|
403
|
+
foundPaths: diagnostic.foundPaths,
|
|
404
|
+
path: previewPath(),
|
|
405
|
+
message: diagnostic.message,
|
|
406
|
+
});
|
|
407
|
+
const original = err instanceof Error ? err.message : String(err);
|
|
408
|
+
const enhanced = new Error(`${original}\n${diagnostic.message}`);
|
|
409
|
+
enhanced.cause = err;
|
|
410
|
+
throw enhanced;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
396
413
|
throw err;
|
|
397
414
|
}
|
|
398
415
|
finally {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function isBareCommand(command: string): boolean;
|
|
2
|
+
export declare function isMissingCommandError(error: unknown): boolean;
|
|
3
|
+
export declare function probeCommonExecutableLocations(command: string, env?: NodeJS.ProcessEnv): Promise<string[]>;
|
|
4
|
+
export declare function classifyMcpStdioMissingCommand(command: string, foundPaths: readonly string[]): string;
|
|
5
|
+
export declare function diagnoseMcpStdioMissingCommand(command: string, error: unknown, env?: NodeJS.ProcessEnv): Promise<{
|
|
6
|
+
message: string;
|
|
7
|
+
foundPaths: string[];
|
|
8
|
+
} | null>;
|
|
9
|
+
export declare function previewPath(env?: NodeJS.ProcessEnv): string;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import { delimiter, isAbsolute, join } from "node:path";
|
|
4
|
+
const COMMON_POSIX_BIN_DIRS = [
|
|
5
|
+
"/opt/homebrew/bin",
|
|
6
|
+
"/usr/local/bin",
|
|
7
|
+
"/home/linuxbrew/.linuxbrew/bin",
|
|
8
|
+
"/usr/bin",
|
|
9
|
+
"/bin",
|
|
10
|
+
];
|
|
11
|
+
function unique(entries) {
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const trimmed = entry.trim();
|
|
16
|
+
if (!trimmed || seen.has(trimmed))
|
|
17
|
+
continue;
|
|
18
|
+
seen.add(trimmed);
|
|
19
|
+
out.push(trimmed);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
function commonExecutableDirs(env = process.env) {
|
|
24
|
+
const home = env.HOME?.trim();
|
|
25
|
+
return unique([
|
|
26
|
+
...COMMON_POSIX_BIN_DIRS,
|
|
27
|
+
...(home
|
|
28
|
+
? [join(home, ".bun", "bin"), join(home, ".local", "bin"), join(home, ".npm-global", "bin")]
|
|
29
|
+
: []),
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
export function isBareCommand(command) {
|
|
33
|
+
const trimmed = command.trim();
|
|
34
|
+
return !!trimmed && !trimmed.includes("/") && !trimmed.includes("\\") && !isAbsolute(trimmed);
|
|
35
|
+
}
|
|
36
|
+
export function isMissingCommandError(error) {
|
|
37
|
+
const err = error;
|
|
38
|
+
const message = typeof err.message === "string" ? err.message : String(error);
|
|
39
|
+
return err.code === "ENOENT" || /\bENOENT\b/i.test(message) || /command not found/i.test(message);
|
|
40
|
+
}
|
|
41
|
+
async function isExecutable(filePath) {
|
|
42
|
+
try {
|
|
43
|
+
await access(filePath, constants.X_OK);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function probeCommonExecutableLocations(command, env = process.env) {
|
|
51
|
+
if (!isBareCommand(command) || process.platform === "win32")
|
|
52
|
+
return [];
|
|
53
|
+
const found = [];
|
|
54
|
+
for (const dir of commonExecutableDirs(env)) {
|
|
55
|
+
const candidate = join(dir, command);
|
|
56
|
+
if (await isExecutable(candidate))
|
|
57
|
+
found.push(candidate);
|
|
58
|
+
}
|
|
59
|
+
return found;
|
|
60
|
+
}
|
|
61
|
+
function installGuidance(command) {
|
|
62
|
+
if (command === "node" || command === "npx")
|
|
63
|
+
return "Please install Node.js and restart CodeShell.";
|
|
64
|
+
if (command === "bun" || command === "bunx")
|
|
65
|
+
return "Please install Bun and restart CodeShell.";
|
|
66
|
+
return `Please install "${command}" and restart CodeShell.`;
|
|
67
|
+
}
|
|
68
|
+
export function classifyMcpStdioMissingCommand(command, foundPaths) {
|
|
69
|
+
const trimmed = command.trim();
|
|
70
|
+
if (foundPaths.length > 0) {
|
|
71
|
+
return [
|
|
72
|
+
`MCP stdio command "${trimmed}" failed to start: detected ${trimmed} at ${foundPaths[0]},`,
|
|
73
|
+
"but that directory was not available on PATH.",
|
|
74
|
+
"Login-shell PATH injection may have failed; restart CodeShell or configure this MCP command as an absolute path.",
|
|
75
|
+
].join(" ");
|
|
76
|
+
}
|
|
77
|
+
return [
|
|
78
|
+
`MCP stdio command "${trimmed}" failed to start: ${trimmed} was not found on PATH or in common install locations.`,
|
|
79
|
+
installGuidance(trimmed),
|
|
80
|
+
].join(" ");
|
|
81
|
+
}
|
|
82
|
+
export async function diagnoseMcpStdioMissingCommand(command, error, env = process.env) {
|
|
83
|
+
if (!isBareCommand(command) || !isMissingCommandError(error))
|
|
84
|
+
return null;
|
|
85
|
+
const foundPaths = await probeCommonExecutableLocations(command, env);
|
|
86
|
+
return {
|
|
87
|
+
message: classifyMcpStdioMissingCommand(command, foundPaths),
|
|
88
|
+
foundPaths,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function previewPath(env = process.env) {
|
|
92
|
+
return (env.PATH ?? "").split(delimiter).filter(Boolean).join(delimiter);
|
|
93
|
+
}
|
|
@@ -130,7 +130,9 @@ export declare class PermissionClassifier {
|
|
|
130
130
|
reconfigure(mode: PermissionMode, approvalBackend: ApprovalBackend, rules?: PermissionRule[]): void;
|
|
131
131
|
getMode(): PermissionMode;
|
|
132
132
|
classify(toolName: string, args: Record<string, unknown>): PermissionDecision;
|
|
133
|
-
handleAsk(toolName: string, args: Record<string, unknown>, reason?: string
|
|
133
|
+
handleAsk(toolName: string, args: Record<string, unknown>, reason?: string, opts?: {
|
|
134
|
+
sessionId?: string;
|
|
135
|
+
}): Promise<boolean>;
|
|
134
136
|
/** Get denial warning message if the model keeps getting denied. */
|
|
135
137
|
getDenialWarning(toolName: string): string | undefined;
|
|
136
138
|
private matchesRule;
|
|
@@ -861,7 +861,7 @@ export class PermissionClassifier {
|
|
|
861
861
|
return "ask";
|
|
862
862
|
}
|
|
863
863
|
}
|
|
864
|
-
async handleAsk(toolName, args, reason) {
|
|
864
|
+
async handleAsk(toolName, args, reason, opts) {
|
|
865
865
|
if (this.defaultMode === "dontAsk") {
|
|
866
866
|
this.log.info("permission.auto_deny", {
|
|
867
867
|
cat: "permission",
|
|
@@ -900,6 +900,7 @@ export class PermissionClassifier {
|
|
|
900
900
|
? `${baseDescription}\n\nReason (from pre_tool_use hook): ${reason}`
|
|
901
901
|
: baseDescription;
|
|
902
902
|
result = await this.approvalBackend.requestApproval({
|
|
903
|
+
...(opts?.sessionId ? { sessionId: opts.sessionId } : {}),
|
|
903
904
|
toolName,
|
|
904
905
|
args,
|
|
905
906
|
description,
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { resolveShellInvocation } from "../../runtime/spawn-common.js";
|
|
1
2
|
export function createOffBackend() {
|
|
2
3
|
return {
|
|
3
4
|
name: "off",
|
|
4
5
|
wrap(command, opts) {
|
|
5
|
-
|
|
6
|
+
// "off" = no sandboxing; just run the command through the shell. Delegate
|
|
7
|
+
// to resolveShellInvocation for the PLATFORM-CORRECT flag instead of a
|
|
8
|
+
// hardcoded POSIX `-c`: on Windows cmd.exe needs `/c` (a bare `-c` is taken
|
|
9
|
+
// as a filename and cmd hangs in interactive mode until timeout — the
|
|
10
|
+
// "Bash never runs on Windows" beta regression). POSIX still gets `-c`.
|
|
11
|
+
return resolveShellInvocation(command, opts.shell);
|
|
6
12
|
},
|
|
7
13
|
};
|
|
8
14
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -145,7 +145,19 @@ export interface SessionState {
|
|
|
145
145
|
startedAt: number;
|
|
146
146
|
model: string;
|
|
147
147
|
provider: string;
|
|
148
|
+
/**
|
|
149
|
+
* Legacy/model-scoped usage summary. Some flows reset this accounting window
|
|
150
|
+
* (for example model switches), so it must not drive whole-session metrics.
|
|
151
|
+
*/
|
|
148
152
|
tokenUsage: TokenUsage;
|
|
153
|
+
/**
|
|
154
|
+
* Monotonic prompt-cache counters for the whole session. These only increase
|
|
155
|
+
* from session start and are separate from the resettable tokenUsage window.
|
|
156
|
+
* Optional on legacy state.json files; SessionManager normalizes them on load.
|
|
157
|
+
*/
|
|
158
|
+
cumulativePromptTokens?: number;
|
|
159
|
+
cumulativeCacheReadTokens?: number;
|
|
160
|
+
cumulativeCacheCreationTokens?: number;
|
|
149
161
|
turnCount: number;
|
|
150
162
|
/**
|
|
151
163
|
* Conversation-turn counter where ONE user message = one turn (incremented in
|
|
@@ -229,6 +241,8 @@ export interface PermissionRule {
|
|
|
229
241
|
reason?: string;
|
|
230
242
|
}
|
|
231
243
|
export interface ApprovalRequest {
|
|
244
|
+
/** Originating engine session. Hosts use this only to route the prompt UI. */
|
|
245
|
+
sessionId?: string;
|
|
232
246
|
toolName: string;
|
|
233
247
|
args: Record<string, unknown>;
|
|
234
248
|
description: string;
|
|
@@ -389,7 +403,7 @@ export type StreamEvent = {
|
|
|
389
403
|
summary: string;
|
|
390
404
|
} | {
|
|
391
405
|
type: "context_compact";
|
|
392
|
-
strategy: "micro" | "summary" | "window" | "snip" | "emergency";
|
|
406
|
+
strategy: "micro" | "summary" | "window" | "snip" | "emergency" | "compacted";
|
|
393
407
|
before: number;
|
|
394
408
|
after: number;
|
|
395
409
|
agentId?: string;
|
|
@@ -405,6 +419,24 @@ export type StreamEvent = {
|
|
|
405
419
|
*/
|
|
406
420
|
cacheReadTokens?: number;
|
|
407
421
|
cacheCreationTokens?: number;
|
|
422
|
+
/**
|
|
423
|
+
* Prompt-cache metrics for the current turn only. These are reset at the
|
|
424
|
+
* start of each turn-loop iteration and sum every LLM response in that
|
|
425
|
+
* turn, including max-token continuations.
|
|
426
|
+
*/
|
|
427
|
+
singleTurnPromptTokens?: number;
|
|
428
|
+
singleTurnCacheReadTokens?: number;
|
|
429
|
+
singleTurnCacheCreationTokens?: number;
|
|
430
|
+
singleTurnCacheHitRate?: number;
|
|
431
|
+
/**
|
|
432
|
+
* Whole-session monotonic prompt-cache counters/rate. Unlike the legacy
|
|
433
|
+
* session* fields below, these are not derived from the resettable
|
|
434
|
+
* tokenUsage accounting window.
|
|
435
|
+
*/
|
|
436
|
+
cumulativePromptTokens?: number;
|
|
437
|
+
cumulativeCacheReadTokens?: number;
|
|
438
|
+
cumulativeCacheCreationTokens?: number;
|
|
439
|
+
cumulativeCacheHitRate?: number;
|
|
408
440
|
/**
|
|
409
441
|
* SESSION-CUMULATIVE cache counts (sum across every LLM response this
|
|
410
442
|
* session, across runs and turns), emitted from the engine's turn
|
|
@@ -464,6 +496,8 @@ export interface LLMConfig {
|
|
|
464
496
|
apiKey?: string;
|
|
465
497
|
baseUrl?: string;
|
|
466
498
|
maxTokens?: number;
|
|
499
|
+
/** Per-model context window size used by hosts to seed Engine.maxContextTokens. */
|
|
500
|
+
maxContextTokens?: number;
|
|
467
501
|
/**
|
|
468
502
|
* Shell command whose stdout is the auth token (TODO 7.2). Resolved at
|
|
469
503
|
* client-build time when `apiKey` is absent. The trimmed first line of
|
package/dist/utils/exec.d.ts
CHANGED
|
@@ -38,3 +38,11 @@ export declare function setGitPathOverride(path: string | null | undefined): voi
|
|
|
38
38
|
export declare function resolveGit(env?: NodeJS.ProcessEnv): string;
|
|
39
39
|
/** Is a usable git binary available (override path, or git on PATH)? */
|
|
40
40
|
export declare function isGitAvailable(env?: NodeJS.ProcessEnv): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The RESOLVED absolute path of the usable git binary (override path, or git
|
|
43
|
+
* found on PATH), or null if none. Lets the settings UI auto-fill the git.path
|
|
44
|
+
* field after a successful detection instead of only reporting available:true
|
|
45
|
+
* with no path (the "检测到了但没回填 path" complaint). Returns the real path so
|
|
46
|
+
* the user can see/keep exactly what was found.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveGitPath(env?: NodeJS.ProcessEnv): string | null;
|
package/dist/utils/exec.js
CHANGED
|
@@ -120,6 +120,16 @@ export function resolveGit(env = process.env) {
|
|
|
120
120
|
export function isGitAvailable(env = process.env) {
|
|
121
121
|
return findExecutable(gitPathOverride ?? "git", env) !== null;
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* The RESOLVED absolute path of the usable git binary (override path, or git
|
|
125
|
+
* found on PATH), or null if none. Lets the settings UI auto-fill the git.path
|
|
126
|
+
* field after a successful detection instead of only reporting available:true
|
|
127
|
+
* with no path (the "检测到了但没回填 path" complaint). Returns the real path so
|
|
128
|
+
* the user can see/keep exactly what was found.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveGitPath(env = process.env) {
|
|
131
|
+
return findExecutable(gitPathOverride ?? "git", env);
|
|
132
|
+
}
|
|
123
133
|
function resolveExecutableUncached(trimmed, env) {
|
|
124
134
|
const command = trimmed;
|
|
125
135
|
// Already a path (absolute or relative with a separator): resolve extension.
|
package/package.json
CHANGED