@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.
Files changed (69) hide show
  1. package/dist/context/compaction.d.ts +30 -0
  2. package/dist/context/compaction.js +93 -0
  3. package/dist/context/manager.d.ts +18 -0
  4. package/dist/context/manager.js +156 -44
  5. package/dist/context/token-counter.js +13 -0
  6. package/dist/engine/engine.d.ts +22 -12
  7. package/dist/engine/engine.js +263 -81
  8. package/dist/engine/model-connections-pool.js +1 -0
  9. package/dist/engine/model-facade.js +2 -12
  10. package/dist/engine/query.js +2 -0
  11. package/dist/engine/runtime.d.ts +2 -0
  12. package/dist/engine/runtime.js +25 -0
  13. package/dist/engine/session-usage.d.ts +12 -0
  14. package/dist/engine/session-usage.js +56 -0
  15. package/dist/engine/steer-queue.d.ts +2 -1
  16. package/dist/engine/steer-queue.js +2 -2
  17. package/dist/engine/turn-loop.d.ts +28 -2
  18. package/dist/engine/turn-loop.js +153 -26
  19. package/dist/git/utils.d.ts +12 -0
  20. package/dist/git/utils.js +33 -6
  21. package/dist/index.d.ts +4 -3
  22. package/dist/index.js +4 -3
  23. package/dist/llm/capabilities/rules.js +1 -1
  24. package/dist/llm/model-pool.d.ts +7 -0
  25. package/dist/llm/model-pool.js +8 -1
  26. package/dist/model-catalog/builtin.js +6 -1
  27. package/dist/preset/index.d.ts +5 -1
  28. package/dist/preset/index.js +21 -2
  29. package/dist/prompt/composer.d.ts +5 -0
  30. package/dist/prompt/composer.js +10 -2
  31. package/dist/prompt/sections/base.md +1 -0
  32. package/dist/protocol/chat-session-manager.d.ts +1 -0
  33. package/dist/protocol/chat-session-manager.js +2 -0
  34. package/dist/protocol/chat-session.d.ts +4 -1
  35. package/dist/protocol/chat-session.js +9 -3
  36. package/dist/protocol/client.d.ts +5 -1
  37. package/dist/protocol/client.js +8 -2
  38. package/dist/protocol/server.d.ts +13 -12
  39. package/dist/protocol/server.js +199 -67
  40. package/dist/protocol/types.d.ts +14 -0
  41. package/dist/runtime/background-shell.js +14 -0
  42. package/dist/runtime/safe-spawn.js +89 -11
  43. package/dist/runtime/spawn-common.d.ts +15 -4
  44. package/dist/runtime/spawn-common.js +113 -12
  45. package/dist/session/session-manager.js +7 -1
  46. package/dist/session/transcript.d.ts +4 -0
  47. package/dist/session/transcript.js +21 -0
  48. package/dist/tool-system/builtin/bash.js +3 -2
  49. package/dist/tool-system/builtin/cron.js +10 -2
  50. package/dist/tool-system/builtin/edit-model-catalog.js +15 -5
  51. package/dist/tool-system/builtin/generate-video.js +3 -0
  52. package/dist/tool-system/builtin/grep.d.ts +9 -0
  53. package/dist/tool-system/builtin/grep.js +100 -3
  54. package/dist/tool-system/builtin/index.d.ts +3 -1
  55. package/dist/tool-system/builtin/index.js +5 -5
  56. package/dist/tool-system/builtin/powershell.js +4 -1
  57. package/dist/tool-system/builtin/sleep.js +5 -0
  58. package/dist/tool-system/context.d.ts +10 -0
  59. package/dist/tool-system/executor.js +25 -2
  60. package/dist/tool-system/mcp-manager.js +17 -0
  61. package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
  62. package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
  63. package/dist/tool-system/permission.d.ts +3 -1
  64. package/dist/tool-system/permission.js +2 -1
  65. package/dist/tool-system/sandbox/off.js +7 -1
  66. package/dist/types.d.ts +35 -1
  67. package/dist/utils/exec.d.ts +8 -0
  68. package/dist/utils/exec.js +10 -0
  69. 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 execFileAsync("rg", args, {
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 execFileAsync("grep", args, {
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, (cwd: string) => boolean>;
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. Available on Windows and cross-platform where PowerShell Core is installed.",
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): Promise<boolean>;
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
- return { file: opts.shell, args: ["-c", command] };
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
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-core",
3
- "version": "0.6.0-rc.1",
3
+ "version": "0.6.0-rc.11",
4
4
  "description": "Core engine for code-shell — agent orchestration, tool execution, hooks, protocol. UI-agnostic.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",