@cjhyy/code-shell-core 0.6.0-rc.3 → 0.6.0-rc.5

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.
@@ -1323,6 +1323,12 @@ export class Engine {
1323
1323
  disabledPlugins,
1324
1324
  skillAllowlist: this.config.skillAllowlist,
1325
1325
  memoriesMaxAgeDays: this.readMemoriesConfig()?.maxAge,
1326
+ goalToolState: {
1327
+ hasGoal: this.config.isSubAgent !== true &&
1328
+ (normalizeGoal(options?.goal) !== undefined ||
1329
+ session.state.activeGoal !== undefined ||
1330
+ normalizeGoal(this.config.goal) !== undefined),
1331
+ },
1326
1332
  });
1327
1333
  // Connect MCP servers (if configured and not already connected).
1328
1334
  // B1: prefer the Runtime-owned MCPManager so all sessions in a
@@ -1358,6 +1364,14 @@ export class Engine {
1358
1364
  // cwd. Recomputed every message, so configuring a key takes effect on the
1359
1365
  // NEXT message without a restart. Tools with no guard entry are always kept.
1360
1366
  const guardCwd = toolCtx.cwd;
1367
+ const toolVisibility = {
1368
+ cwd: guardCwd,
1369
+ hasGoal: this.config.isSubAgent !== true &&
1370
+ (normalizeGoal(options?.goal) !== undefined ||
1371
+ session.state.activeGoal !== undefined ||
1372
+ normalizeGoal(this.config.goal) !== undefined),
1373
+ };
1374
+ toolCtx.toolVisibility = toolVisibility;
1361
1375
  // #7: per-turn project builtin override. The toolRegistry's builtin tool
1362
1376
  // SET is ctor-frozen (and may be shared via runtime), so a mid-session
1363
1377
  // project override of a builtin can't rebuild the registry. But the tool
@@ -1407,7 +1421,7 @@ export class Engine {
1407
1421
  .filter((t) => mcpVisible(t.name))
1408
1422
  .filter((t) => {
1409
1423
  const guard = BUILTIN_TOOL_GUARDS.get(t.name);
1410
- return guard ? guard(guardCwd) : true;
1424
+ return guard ? guard(toolVisibility) : true;
1411
1425
  })
1412
1426
  .filter((t) => {
1413
1427
  const flag = TOOL_FEATURE_FLAGS.get(t.name);
@@ -23,6 +23,8 @@ export declare class EngineRuntime {
23
23
  readonly costTracker: CostTracker;
24
24
  private sandboxCache;
25
25
  constructor(opts: EngineRuntimeOptions);
26
+ clearModels(): void;
27
+ reloadModelsFromSettings(): void;
26
28
  /**
27
29
  * Lazily resolve and cache a sandbox backend for `(mode, cwd)`.
28
30
  *
@@ -1,3 +1,6 @@
1
+ import { getMergedCatalog } from "../model-catalog/index.js";
2
+ import { modelEntriesFromConnections } from "./model-connections-pool.js";
3
+ import { defaultCacheDir } from "../llm/model-cache.js";
1
4
  import { resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
2
5
  import { sandboxCacheKey } from "./sandbox-cache-key.js";
3
6
  /**
@@ -22,6 +25,28 @@ export class EngineRuntime {
22
25
  this.mcpPool = opts.mcpPool;
23
26
  this.costTracker = opts.costTracker;
24
27
  }
28
+ clearModels() {
29
+ this.modelPool.clear();
30
+ }
31
+ reloadModelsFromSettings() {
32
+ const settings = this.settings.load();
33
+ const connections = settings.modelConnections ?? [];
34
+ const credentials = settings.credentials ?? [];
35
+ const catalog = getMergedCatalog();
36
+ const entries = modelEntriesFromConnections(connections, credentials, catalog);
37
+ this.modelPool.clear();
38
+ if (entries.length === 0)
39
+ return;
40
+ for (const entry of entries)
41
+ this.modelPool.register(entry);
42
+ if (entries.length > 0) {
43
+ const defaultText = settings.defaults?.text;
44
+ const key = defaultText && entries.some((e) => e.key === defaultText) ? defaultText : entries[0].key;
45
+ this.modelPool.switch(key);
46
+ this.modelPool.setCacheDir(defaultCacheDir());
47
+ this.modelPool.reloadCachedContextWindows();
48
+ }
49
+ }
25
50
  /**
26
51
  * Lazily resolve and cache a sandbox backend for `(mode, cwd)`.
27
52
  *
@@ -14,6 +14,18 @@ export interface GitStatusEntry {
14
14
  }
15
15
  export type { GitLogEntry };
16
16
  export declare function isGitRepo(cwd: string): boolean;
17
+ /**
18
+ * Resolve a directory to its PROJECT ROOT: the enclosing git repository's
19
+ * top-level dir if `cwd` is inside a git repo, otherwise `cwd` unchanged.
20
+ *
21
+ * This is the project-boundary rule the desktop uses when adding/identifying a
22
+ * project: picking a SUBDIRECTORY of a git repo should belong to that one repo
23
+ * (its root), not spawn a separate project per subdir — mirrors how editors
24
+ * (and Claude Code) treat a repo as one workspace. A non-git folder is its own
25
+ * project (returned as-is). Never throws; on any git failure falls back to cwd.
26
+ * Returns the git-reported toplevel (already absolute, forward-slashed on win).
27
+ */
28
+ export declare function resolveProjectRoot(cwd: string): string;
17
29
  export declare function getCurrentBranch(cwd: string): string;
18
30
  export declare function getGitStatus(cwd: string): GitStatusEntry[];
19
31
  export declare function getGitDiff(cwd: string, opts?: {
package/dist/git/utils.js CHANGED
@@ -8,15 +8,15 @@
8
8
  * into the string form.
9
9
  */
10
10
  import { execFileSync } from "node:child_process";
11
+ import { realpathSync } from "node:fs";
11
12
  import { parseGitLog } from "./parse-log.js";
12
- import { resolveExecutable } from "../utils/exec.js";
13
+ import { resolveExecutable, resolveGit } from "../utils/exec.js";
13
14
  // Resolve git/gh through PATH×PATHEXT on Windows so a .cmd/.exe shim is found
14
15
  // (bare execFile doesn't walk PATHEXT). No-op on POSIX. See utils/exec.ts.
15
- const GIT_BIN = resolveExecutable("git");
16
16
  const GH_BIN = resolveExecutable("gh");
17
17
  /** Run git with an argv array and return its trimmed stdout. */
18
18
  function git(cwd, args, timeoutMs = 10000) {
19
- return execFileSync(GIT_BIN, args, { cwd, encoding: "utf-8", timeout: timeoutMs }).trim();
19
+ return execFileSync(resolveGit(), args, { cwd, encoding: "utf-8", timeout: timeoutMs }).trim();
20
20
  }
21
21
  /** Run gh with an argv array and return its trimmed stdout. */
22
22
  function gh(cwd, args, timeoutMs = 10000) {
@@ -31,6 +31,33 @@ export function isGitRepo(cwd) {
31
31
  return false;
32
32
  }
33
33
  }
34
+ /**
35
+ * Resolve a directory to its PROJECT ROOT: the enclosing git repository's
36
+ * top-level dir if `cwd` is inside a git repo, otherwise `cwd` unchanged.
37
+ *
38
+ * This is the project-boundary rule the desktop uses when adding/identifying a
39
+ * project: picking a SUBDIRECTORY of a git repo should belong to that one repo
40
+ * (its root), not spawn a separate project per subdir — mirrors how editors
41
+ * (and Claude Code) treat a repo as one workspace. A non-git folder is its own
42
+ * project (returned as-is). Never throws; on any git failure falls back to cwd.
43
+ * Returns the git-reported toplevel (already absolute, forward-slashed on win).
44
+ */
45
+ export function resolveProjectRoot(cwd) {
46
+ let realCwd = cwd;
47
+ try {
48
+ realCwd = realpathSync(cwd);
49
+ }
50
+ catch {
51
+ // Non-existent paths are allowed to fall back unchanged below.
52
+ }
53
+ try {
54
+ const top = git(realCwd, ["rev-parse", "--show-toplevel"], 5000);
55
+ return top ? realpathSync(top) : realCwd;
56
+ }
57
+ catch {
58
+ return realCwd;
59
+ }
60
+ }
34
61
  export function getCurrentBranch(cwd) {
35
62
  return git(cwd, ["branch", "--show-current"], 5000);
36
63
  }
@@ -82,13 +109,13 @@ export function gitAdd(cwd, files = ["."]) {
82
109
  // `--` ensures a path starting with `-` cannot be parsed as a flag.
83
110
  // Each file is its own argv token, so spaces / quotes / non-ASCII pass
84
111
  // through verbatim with no shell parsing.
85
- execFileSync(GIT_BIN, ["add", "--", ...files], { cwd, timeout: 10000 });
112
+ execFileSync(resolveGit(), ["add", "--", ...files], { cwd, timeout: 10000 });
86
113
  }
87
114
  export function gitCommit(cwd, message) {
88
115
  // Pre-fix this used `JSON.stringify(message)` which only happened to be
89
116
  // safe because JSON.stringify covers most shell metacharacters — but it's
90
117
  // not real escaping. The argv form is.
91
- return execFileSync(GIT_BIN, ["commit", "-m", message], {
118
+ return execFileSync(resolveGit(), ["commit", "-m", message], {
92
119
  cwd,
93
120
  encoding: "utf-8",
94
121
  timeout: 30000,
@@ -115,7 +142,7 @@ export function gitCheckout(cwd, branch, create = false) {
115
142
  throw new Error(`refusing branch name that starts with '-': ${branch}`);
116
143
  }
117
144
  const args = create ? ["checkout", "-b", branch] : ["checkout", branch];
118
- execFileSync(GIT_BIN, args, { cwd, timeout: 10000 });
145
+ execFileSync(resolveGit(), args, { cwd, timeout: 10000 });
119
146
  }
120
147
  export function ghAvailable() {
121
148
  try {
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Public API exports.
5
5
  */
6
- export declare const VERSION = "0.6.0-rc.3";
6
+ export declare const VERSION = "0.6.0-rc.5";
7
7
  export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
8
8
  export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
9
9
  export { Engine, loadAgentDefinitionsForCwd } from "./engine/engine.js";
@@ -116,7 +116,7 @@ export { getGraphemeSegmenter, firstGrapheme, lastGrapheme, getWordSegmenter, ge
116
116
  export { env } from "./utils/env.js";
117
117
  export { default as sliceAnsi } from "./utils/sliceAnsi.js";
118
118
  export { execFileNoThrow } from "./utils/execFileNoThrow.js";
119
- export { findExecutable, resolveExecutable, setGitPathOverride, resolveGit, isGitAvailable, } from "./utils/exec.js";
119
+ export { findExecutable, resolveExecutable, setGitPathOverride, resolveGit, isGitAvailable, resolveGitPath, } from "./utils/exec.js";
120
120
  export { gte } from "./utils/semver.js";
121
121
  export { lock, lockSync, unlock, check } from "./utils/lockfile.js";
122
122
  export { logForDebugging } from "./utils/debug.js";
@@ -134,7 +134,7 @@ export { CostTracker, costTracker, installCostTracking } from "./cost-tracker.js
134
134
  export { NOOP_COLORIZER, type Colorizer } from "./colorizer.js";
135
135
  export { hasApiKey, resolveApiKey, appendOnboardingResult, detectEnvKeys, saveArenaSettingsByKeys, type OnboardingResult, } from "./onboarding.js";
136
136
  export { getCurrentVersion, checkForUpdate, scheduleAutoInstallOnExit, getUpdateAvailable, getAutoUpdateDisabledReason, type UpdateInfo, } from "./updater.js";
137
- export { isGitRepo, getCurrentBranch, getGitStatus, getGitDiff, getGitDiffStat, getGitLog, gitAdd, gitCommit, gitListBranches, gitCheckout, ghAvailable, ghPrComments, } from "./git/utils.js";
137
+ export { isGitRepo, resolveProjectRoot, getCurrentBranch, getGitStatus, getGitDiff, getGitDiffStat, getGitLog, gitAdd, gitCommit, gitListBranches, gitCheckout, ghAvailable, ghPrComments, } from "./git/utils.js";
138
138
  export { buildReviewPrompt, parseDimensions, ALL_DIMENSIONS, type ReviewDimension, type ReviewPromptOptions, } from "./review/review-prompt.js";
139
139
  /**
140
140
  * @internal Exports below are consumed by the in-repo TUI/desktop hosts, not
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Public API exports.
5
5
  */
6
- export const VERSION = "0.6.0-rc.3";
6
+ export const VERSION = "0.6.0-rc.5";
7
7
  // ─── Exceptions ──────────────────────────────────────────────────
8
8
  export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
9
9
  // ─── Engine (primary API) ────────────────────────────────────────
@@ -136,7 +136,7 @@ export { getGraphemeSegmenter, firstGrapheme, lastGrapheme, getWordSegmenter, ge
136
136
  export { env } from "./utils/env.js";
137
137
  export { default as sliceAnsi } from "./utils/sliceAnsi.js";
138
138
  export { execFileNoThrow } from "./utils/execFileNoThrow.js";
139
- export { findExecutable, resolveExecutable, setGitPathOverride, resolveGit, isGitAvailable, } from "./utils/exec.js";
139
+ export { findExecutable, resolveExecutable, setGitPathOverride, resolveGit, isGitAvailable, resolveGitPath, } from "./utils/exec.js";
140
140
  export { gte } from "./utils/semver.js";
141
141
  // Cross-process file lock (proper-lockfile via createRequire, ESM-safe). Used by
142
142
  // RunLock / CronStore internally; exported so other writers of shared files
@@ -163,7 +163,7 @@ export { hasApiKey, resolveApiKey, appendOnboardingResult, detectEnvKeys, saveAr
163
163
  // ─── Updater ─────────────────────────────────────────────────────
164
164
  export { getCurrentVersion, checkForUpdate, scheduleAutoInstallOnExit, getUpdateAvailable, getAutoUpdateDisabledReason, } from "./updater.js";
165
165
  // ─── Git Utilities ───────────────────────────────────────────────
166
- export { isGitRepo, getCurrentBranch, getGitStatus, getGitDiff, getGitDiffStat, getGitLog, gitAdd, gitCommit, gitListBranches, gitCheckout, ghAvailable, ghPrComments, } from "./git/utils.js";
166
+ export { isGitRepo, resolveProjectRoot, getCurrentBranch, getGitStatus, getGitDiff, getGitDiffStat, getGitLog, gitAdd, gitCommit, gitListBranches, gitCheckout, ghAvailable, ghPrComments, } from "./git/utils.js";
167
167
  // ─── Code review (TODO 7.3) ─────────────────────────────────────
168
168
  export { buildReviewPrompt, parseDimensions, ALL_DIMENSIONS, } from "./review/review-prompt.js";
169
169
  // ─── Tool-system (extended for TUI) ─────────────────────────────
@@ -79,6 +79,8 @@ export declare class ModelPool {
79
79
  * The first entry (or the one matching `defaultKey`) becomes active.
80
80
  */
81
81
  constructor(entries?: ModelEntry[], defaultKey?: string);
82
+ /** Clear all registered models and active selection. */
83
+ clear(): void;
82
84
  /** Register a model at runtime. */
83
85
  register(entry: ModelEntry): void;
84
86
  /** Fill in known defaults (e.g. context window for DeepSeek V4) when the entry doesn't specify them. */
@@ -142,6 +142,11 @@ export class ModelPool {
142
142
  this.activeKey = entries[0].key;
143
143
  }
144
144
  }
145
+ /** Clear all registered models and active selection. */
146
+ clear() {
147
+ this.models.clear();
148
+ this.activeKey = undefined;
149
+ }
145
150
  /** Register a model at runtime. */
146
151
  register(entry) {
147
152
  this.models.set(entry.key, this.withBuiltinDefaults(entry));
@@ -228,6 +233,7 @@ export class ModelPool {
228
233
  // instead of fabricating 8192, which silently truncates long outputs and
229
234
  // masks the real per-model cap.
230
235
  maxTokens: entry.maxOutputTokens,
236
+ ...(entry.maxContextTokens !== undefined ? { maxContextTokens: entry.maxContextTokens } : {}),
231
237
  // reasoning: entry overrides catalog. No base fallback — see class doc.
232
238
  ...(entry.reasoning ?? fromCat?.reasoning
233
239
  ? { reasoning: entry.reasoning ?? fromCat?.reasoning }
@@ -41,6 +41,10 @@ export interface ComposerOptions {
41
41
  * dispatch gate.
42
42
  */
43
43
  skillAllowlist?: string[];
44
+ /** Volatile goal state used only in the trailing dynamic-context message. */
45
+ goalToolState?: {
46
+ hasGoal: boolean;
47
+ };
44
48
  /**
45
49
  * settings.memories.maxAge (days). When > 0, memories whose file mtime is
46
50
  * older than this are dropped from the injected memory context (TODO 8.1).
@@ -79,6 +83,7 @@ export declare class PromptComposer {
79
83
  * breakpoint — a change here never re-bills the history prefix.
80
84
  */
81
85
  buildDynamicContextMessage(): Promise<Message | null>;
86
+ private buildGoalToolContext;
82
87
  invalidateCache(sectionName?: string): void;
83
88
  private getSections;
84
89
  private getInstructions;
@@ -113,7 +113,8 @@ export class PromptComposer {
113
113
  // prefix — so a memory change (extraction / recall usage++ / approve) never
114
114
  // re-bills the cached prefix. See buildUserContextMessage for the rationale.
115
115
  const memoryContext = this.getMemoryContext();
116
- const parts = [skillsListing, gitStatus, memoryContext].filter(Boolean);
116
+ const goalToolContext = this.buildGoalToolContext();
117
+ const parts = [skillsListing, gitStatus, memoryContext, goalToolContext].filter(Boolean);
117
118
  if (parts.length === 0)
118
119
  return null;
119
120
  return {
@@ -121,6 +122,13 @@ export class PromptComposer {
121
122
  content: `<system-reminder>\n${parts.join("\n\n")}\n</system-reminder>`,
122
123
  };
123
124
  }
125
+ buildGoalToolContext() {
126
+ if (!this.options.goalToolState)
127
+ return "";
128
+ return this.options.goalToolState.hasGoal
129
+ ? "当前存在 active goal。只有在目标完全完成时才可调用 complete_goal;只有用户明确要求取消/停止/放弃该目标时才可调用 cancel_goal。"
130
+ : "当前没有 active goal。不要调用 complete_goal/cancel_goal;如果误调用,系统会拒绝。";
131
+ }
124
132
  invalidateCache(sectionName) {
125
133
  this.sectionCache.invalidate(sectionName);
126
134
  if (!sectionName) {
@@ -31,6 +31,7 @@ When you encounter an obstacle, do not use destructive actions as a shortcut to
31
31
  - To search for files use Glob instead of find or ls
32
32
  - To search the content of files, use Grep instead of grep or rg
33
33
  - Reserve using the Bash exclusively for system commands and terminal operations that require shell execution.
34
+ - Shell choice: when Bash is available, use it for ordinary shell commands, git operations, package-manager commands, test/build scripts, and POSIX-style command lines. On Windows, Bash uses Git Bash when it is available, so do not choose PowerShell merely because the OS is Windows. Use PowerShell only when the user explicitly asks for it or the task requires PowerShell-specific cmdlets, Windows APIs, registry access, or `.ps1` behavior.
34
35
  - For long-lived processes that don't exit on their own — a dev server (`npm run dev`, `vite`), a watcher, a tunnel — call Bash with `run_in_background: true`. It returns a `shell_id` immediately instead of blocking until a timeout. Then use `BashOutput(shell_id)` to read its logs (e.g. to confirm it started or to see an error), `ListShells()` to see what's running, and `KillShell(shell_id)` to stop it. Never run such a command in the foreground — it will just block until it's killed. Plain one-shot commands (build, test, git) stay foreground.
35
36
  - Break down and manage multi-step work with the TodoWrite tool. Pass the complete todo list each call; rewrite it as items move pending → in_progress → completed.
36
37
  - You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel.
@@ -17,6 +17,7 @@ export interface ChatSessionManagerOptions {
17
17
  }
18
18
  export declare class ChatSessionManager {
19
19
  private readonly sessions;
20
+ readonly runtime: EngineRuntime;
20
21
  private readonly factory;
21
22
  private readonly maxSessions;
22
23
  private readonly idleTtlMs;
@@ -3,11 +3,13 @@ import { backgroundShellManager } from "../runtime/background-shell.js";
3
3
  import { clearAgentOutputFiles } from "../tool-system/builtin/agent-output-file.js";
4
4
  export class ChatSessionManager {
5
5
  sessions = new Map();
6
+ runtime;
6
7
  factory;
7
8
  maxSessions;
8
9
  idleTtlMs;
9
10
  sweeper = null;
10
11
  constructor(opts) {
12
+ this.runtime = opts.runtime;
11
13
  this.factory = opts.engineFactory;
12
14
  this.maxSessions = opts.maxSessions ?? 16;
13
15
  this.idleTtlMs = opts.idleTtlMs ?? 30 * 60 * 1000;
@@ -1,4 +1,5 @@
1
1
  import type { Engine, EngineResult } from "../engine/engine.js";
2
+ import type { ModelEntry } from "../llm/model-pool.js";
2
3
  import type { StreamEvent } from "../types.js";
3
4
  export interface ChatSessionOptions {
4
5
  id: string;
@@ -116,7 +117,7 @@ export declare class ChatSession {
116
117
  * turn is in flight so a hot switch never mutates the model under a
117
118
  * running LLM client.
118
119
  */
119
- requestModelSwitch(key: string): void;
120
+ requestModelSwitch(key: string): ModelEntry;
120
121
  /**
121
122
  * Perform the actual model switch for this session: rotate the engine's model
122
123
  * and reset the session's cumulative usage on disk — a different model has a
@@ -126,10 +126,14 @@ export class ChatSession {
126
126
  */
127
127
  requestModelSwitch(key) {
128
128
  if (this.isBusy()) {
129
+ const pool = this.engine.getModelPool();
130
+ const entry = pool.get(key);
131
+ if (!entry)
132
+ throw new Error(`Model not found: ${key}`);
129
133
  this.pendingModel = key;
130
- return;
134
+ return entry;
131
135
  }
132
- this.applyModelSwitch(key);
136
+ return this.applyModelSwitch(key);
133
137
  }
134
138
  /**
135
139
  * Perform the actual model switch for this session: rotate the engine's model
@@ -140,8 +144,9 @@ export class ChatSession {
140
144
  * path and the deferred run-boundary path.
141
145
  */
142
146
  applyModelSwitch(key) {
143
- this.engine.switchModel(key);
147
+ const entry = this.engine.switchModel(key);
144
148
  this.engine.resetSessionUsage(this.id);
149
+ return entry;
145
150
  }
146
151
  queueDepth() {
147
152
  return this.queue.length;
@@ -23,6 +23,7 @@ export interface AgentRunOptions {
23
23
  cwd?: string;
24
24
  sessionId?: string;
25
25
  permissionMode?: PermissionMode;
26
+ model?: string;
26
27
  planMode?: boolean;
27
28
  }
28
29
  export type BackgroundAgentCompletedHandler = (sessionId: string, event: BackgroundAgentCompletedEvent) => void;
@@ -307,6 +307,19 @@ export class AgentServer {
307
307
  this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
308
308
  return;
309
309
  }
310
+ if (params.model !== undefined) {
311
+ if (typeof params.model !== "string" || params.model.length === 0) {
312
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
313
+ return;
314
+ }
315
+ try {
316
+ session.requestModelSwitch(params.model);
317
+ }
318
+ catch (err) {
319
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
320
+ return;
321
+ }
322
+ }
310
323
  if (typeof params.planMode === "boolean") {
311
324
  session.engine.setPlanMode(params.planMode);
312
325
  }
@@ -387,6 +400,19 @@ export class AgentServer {
387
400
  // Engine.setPermissionMode now keeps this.permissionMode + this.planMode
388
401
  // in sync and tools read them via ToolContext.permissionMode/planMode.
389
402
  }
403
+ if (params.model !== undefined) {
404
+ if (typeof params.model !== "string" || params.model.length === 0) {
405
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
406
+ return;
407
+ }
408
+ try {
409
+ this.legacyEngine.switchModel(params.model);
410
+ }
411
+ catch (err) {
412
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
413
+ return;
414
+ }
415
+ }
390
416
  this.running = true;
391
417
  this.abortController = new AbortController();
392
418
  this.notify(Methods.Status, { status: "running" });
@@ -720,7 +746,11 @@ export class AgentServer {
720
746
  const sid = params.sessionId;
721
747
  const s = this.chatManager.get(sid);
722
748
  if (!s) {
723
- this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${sid}`));
749
+ // Session not found (already cleaned by idle sweeper, or never created).
750
+ // Don't create one just for configure — let the subsequent run() do it
751
+ // with proper per-session config. Return OK since there's nothing to
752
+ // configure on a non-existent session.
753
+ this.transport.send(createResponse(req.id, { ok: true }));
724
754
  return;
725
755
  }
726
756
  if (typeof params.planMode === "boolean")
@@ -728,13 +758,32 @@ export class AgentServer {
728
758
  if (typeof params.permissionMode === "string") {
729
759
  s.engine.setPermissionMode(params.permissionMode);
730
760
  }
761
+ if (params.clearModels) {
762
+ this.chatManager.runtime.clearModels();
763
+ }
764
+ if (params.reloadModels) {
765
+ try {
766
+ this.chatManager.runtime.reloadModelsFromSettings();
767
+ }
768
+ catch (err) {
769
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
770
+ return;
771
+ }
772
+ }
731
773
  // Per-session model switch — the missing piece that made model changes
732
774
  // worker-global (session-isolation research §3). requestModelSwitch
733
775
  // applies immediately when idle, defers to the run boundary when busy
734
776
  // so it never swaps the model under a running LLM client.
735
777
  if (typeof params.model === "string") {
736
778
  try {
737
- s.requestModelSwitch(params.model);
779
+ const entry = s.requestModelSwitch(params.model);
780
+ this.transport.send(createResponse(req.id, {
781
+ ok: true,
782
+ model: entry.model,
783
+ key: entry.key,
784
+ maxContextTokens: entry.maxContextTokens,
785
+ }));
786
+ return;
738
787
  }
739
788
  catch (err) {
740
789
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
@@ -757,22 +806,33 @@ export class AgentServer {
757
806
  // Global configure — delegate to legacyEngine if available,
758
807
  // or to any session's engine from chatManager for settings ops
759
808
  const engine = this.legacyEngine ?? this.anyEngine();
809
+ if (params.clearModels) {
810
+ if (this.chatManager) {
811
+ this.chatManager.runtime.clearModels();
812
+ }
813
+ else {
814
+ this.legacyEngine?.getModelPool().clear();
815
+ this.globalQueryEngine?.getModelPool().clear();
816
+ }
817
+ }
760
818
  if (params.reloadModels) {
761
819
  try {
762
- const seen = new Set();
763
- const reload = (target) => {
764
- if (!target || seen.has(target))
765
- return;
766
- seen.add(target);
767
- target.reloadModelPool();
768
- };
769
- reload(this.legacyEngine);
770
- reload(this.globalQueryEngine);
771
820
  if (this.chatManager) {
772
- this.chatManager.forEachSession((s) => reload(s.engine));
821
+ this.chatManager.runtime.reloadModelsFromSettings();
822
+ }
823
+ else {
824
+ const seen = new Set();
825
+ const reload = (target) => {
826
+ if (!target || seen.has(target))
827
+ return;
828
+ seen.add(target);
829
+ target.reloadModelPool();
830
+ };
831
+ reload(this.legacyEngine);
832
+ reload(this.globalQueryEngine);
833
+ if (seen.size === 0)
834
+ reload(engine);
773
835
  }
774
- if (seen.size === 0)
775
- reload(engine);
776
836
  }
777
837
  catch (err) {
778
838
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
@@ -782,7 +842,12 @@ export class AgentServer {
782
842
  if (params.model !== undefined && engine) {
783
843
  try {
784
844
  const entry = engine.switchModel(params.model);
785
- this.transport.send(createResponse(req.id, { ok: true, model: entry.model, key: entry.key }));
845
+ this.transport.send(createResponse(req.id, {
846
+ ok: true,
847
+ model: entry.model,
848
+ key: entry.key,
849
+ maxContextTokens: entry.maxContextTokens,
850
+ }));
786
851
  return;
787
852
  }
788
853
  catch (err) {
@@ -61,6 +61,12 @@ export interface RunParams {
61
61
  * When omitted, the engine keeps its configured default.
62
62
  */
63
63
  permissionMode?: PermissionMode;
64
+ /**
65
+ * Per-run model pool key. Applied after the session exists and before the
66
+ * turn starts, so cold desktop runs don't need a separate pre-run configure
67
+ * request to a worker that has not been spawned yet.
68
+ */
69
+ model?: string;
64
70
  /**
65
71
  * Workspace trust for this run's project (`cwd`), asserted by the host
66
72
  * (desktop main from its trust-store) — never by the renderer. When false,
@@ -149,6 +155,8 @@ export interface ConfigureParams {
149
155
  * running engine picks them up without a process restart.
150
156
  */
151
157
  reloadModels?: boolean;
158
+ /** Clear the live model pool, used after logout removes saved credentials. */
159
+ clearModels?: boolean;
152
160
  /**
153
161
  * Re-read disk settings and hot-push the disk-default config fields (preset /
154
162
  * customSystemPrompt / appendSystemPrompt / personalization / mcpServers) +
@@ -85,8 +85,13 @@ export declare function resolveShellInvocation(command: string, shell?: string):
85
85
  file: string;
86
86
  args: string[];
87
87
  };
88
+ export declare function resolveGitBash(): string | undefined;
89
+ /** Reset the Git Bash probe cache. Test-only (platform is stubbed per test). */
90
+ export declare function _resetGitBashCache(): void;
88
91
  /** The platform's default interactive shell binary, for spawning a bare shell
89
- * (no `-c`/`/c` command). Windows → ComSpec/cmd.exe; POSIX → $SHELL/bin/bash. */
92
+ * (no `-c`/`/c` command). Windows → Git Bash if present, else ComSpec/cmd.exe;
93
+ * POSIX → $SHELL/bin/bash. Windows prefers Git Bash so the model's bash-syntax
94
+ * commands actually run (cmd.exe can't). */
90
95
  export declare function defaultShellBinary(shell?: string): string;
91
96
  /**
92
97
  * 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
@@ -115,20 +117,81 @@ export function mergeShellEnv(base, projectEnv) {
115
117
  export function resolveShellInvocation(command, shell) {
116
118
  if (process.platform === "win32") {
117
119
  const file = shell ?? process.env.ComSpec ?? "cmd.exe";
118
- // PowerShell variants take -Command; cmd.exe (and cmd-like) take /c.
120
+ // Flag form depends on the shell: PowerShell -Command; a POSIX shell such
121
+ // as Git Bash's bash.exe / sh → -c (it does NOT understand cmd's /c); cmd.exe
122
+ // (and cmd-like) → /c. Detecting bash/sh matters now that defaultShellBinary
123
+ // prefers Git Bash on Windows — feeding it /c would break every command.
119
124
  const isPwsh = /(^|[\\/])(pwsh|powershell)(\.exe)?$/i.test(file);
120
- return isPwsh ? { file, args: ["-Command", command] } : { file, args: ["/c", command] };
125
+ if (isPwsh)
126
+ return { file, args: ["-Command", command] };
127
+ const isPosixShell = /(^|[\\/])(bash|sh|zsh|dash)(\.exe)?$/i.test(file);
128
+ return { file, args: [isPosixShell ? "-c" : "/c", command] };
121
129
  }
122
130
  const file = shell ?? process.env.SHELL ?? "/bin/bash";
123
131
  return { file, args: ["-c", command] };
124
132
  }
133
+ /**
134
+ * Best-effort locate Git Bash's `bash.exe` on Windows. Returns undefined on
135
+ * non-Windows, when git isn't installed, or when no bash.exe is found.
136
+ *
137
+ * WHY: The Bash tool and background shells feed the model's *bash* commands
138
+ * (`ls`, `&&`, `$(…)`, pipes, quoting) to the shell. On Windows the historical
139
+ * default is `cmd.exe`, which can't run any of that — so "Bash" was effectively
140
+ * broken on Windows. Git for Windows (which most devs already have — we detect
141
+ * it for repo ops anyway) ships a full bash at `<git>\bin\bash.exe`, so prefer
142
+ * it. Only falls back to cmd.exe when Git Bash truly isn't present.
143
+ *
144
+ * Resolution order:
145
+ * 1. CODE_SHELL_GIT_BASH_PATH env override (explicit user config wins).
146
+ * 2. Reverse-engineer from the git binary's location: `where git` gives e.g.
147
+ * `C:\Program Files\Git\cmd\git.exe` → `…\Git\bin\bash.exe`.
148
+ * 3. The two default install locations (Program Files / Program Files (x86)).
149
+ * Cached after first probe (a spawn per command would be wasteful).
150
+ */
151
+ let gitBashCache;
152
+ export function resolveGitBash() {
153
+ if (process.platform !== "win32")
154
+ return undefined;
155
+ if (gitBashCache !== undefined)
156
+ return gitBashCache ?? undefined;
157
+ const override = process.env.CODE_SHELL_GIT_BASH_PATH;
158
+ if (override && existsSync(override))
159
+ return (gitBashCache = override);
160
+ const candidates = [];
161
+ // (2) derive from `where git`. Git installs git.exe under either \cmd\ or
162
+ // \bin\; bash.exe lives under the sibling \bin\. Walk up to the Git root.
163
+ try {
164
+ const out = execFileSync("where", ["git"], { encoding: "utf-8", timeout: 3000 });
165
+ const gitExe = out.split(/\r?\n/).find((l) => l.trim().toLowerCase().endsWith("git.exe"));
166
+ if (gitExe) {
167
+ const gitRoot = dirname(dirname(gitExe.trim())); // …\Git\cmd\git.exe → …\Git
168
+ candidates.push(join(gitRoot, "bin", "bash.exe"));
169
+ }
170
+ }
171
+ catch {
172
+ // git not on PATH — fall through to the well-known locations.
173
+ }
174
+ // (3) default install locations.
175
+ const pf = process.env["ProgramFiles"] ?? "C:\\Program Files";
176
+ const pf86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
177
+ candidates.push(join(pf, "Git", "bin", "bash.exe"));
178
+ candidates.push(join(pf86, "Git", "bin", "bash.exe"));
179
+ const found = candidates.find((p) => existsSync(p));
180
+ return (gitBashCache = found ?? null) ?? undefined;
181
+ }
182
+ /** Reset the Git Bash probe cache. Test-only (platform is stubbed per test). */
183
+ export function _resetGitBashCache() {
184
+ gitBashCache = undefined;
185
+ }
125
186
  /** The platform's default interactive shell binary, for spawning a bare shell
126
- * (no `-c`/`/c` command). Windows → ComSpec/cmd.exe; POSIX → $SHELL/bin/bash. */
187
+ * (no `-c`/`/c` command). Windows → Git Bash if present, else ComSpec/cmd.exe;
188
+ * POSIX → $SHELL/bin/bash. Windows prefers Git Bash so the model's bash-syntax
189
+ * commands actually run (cmd.exe can't). */
127
190
  export function defaultShellBinary(shell) {
128
191
  if (shell)
129
192
  return shell;
130
193
  if (process.platform === "win32")
131
- return process.env.ComSpec ?? "cmd.exe";
194
+ return resolveGitBash() ?? process.env.ComSpec ?? "cmd.exe";
132
195
  return process.env.SHELL ?? "/bin/bash";
133
196
  }
134
197
  /**
@@ -29,8 +29,10 @@ function sandboxMark(backend) {
29
29
  export const bashToolDef = {
30
30
  name: "Bash",
31
31
  description: "Execute a shell command and return its output. " +
32
- "Commands run in the user's default shell. Use for system operations, " +
33
- "git commands, running tests, installing packages, etc.",
32
+ "Use for ordinary shell commands, system operations, git commands, " +
33
+ "package-manager commands, running tests, installing packages, etc. " +
34
+ "On Windows this tool prefers Git Bash when available, so prefer Bash over " +
35
+ "PowerShell unless the task needs PowerShell-specific syntax or APIs.",
34
36
  inputSchema: {
35
37
  type: "object",
36
38
  properties: {
@@ -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. For ordinary shell/git/package/test commands, prefer Bash " +
14
+ "(Git Bash on Windows when available).",
12
15
  inputSchema: {
13
16
  type: "object",
14
17
  properties: {
@@ -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.
@@ -3,9 +3,11 @@ export function createOffBackend() {
3
3
  return {
4
4
  name: "off",
5
5
  wrap(command, opts) {
6
- // Platform-aware flag: cmd.exe takes /c, PowerShell -Command, POSIX -c.
7
- // Bash always passes a backend (off at minimum), so resolveSpawnTarget's
8
- // no-sandbox fallback never covers this path wrap() must handle it.
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`.
9
11
  return resolveShellInvocation(command, opts.shell);
10
12
  },
11
13
  };
package/dist/types.d.ts CHANGED
@@ -466,6 +466,8 @@ export interface LLMConfig {
466
466
  apiKey?: string;
467
467
  baseUrl?: string;
468
468
  maxTokens?: number;
469
+ /** Per-model context window size used by hosts to seed Engine.maxContextTokens. */
470
+ maxContextTokens?: number;
469
471
  /**
470
472
  * Shell command whose stdout is the auth token (TODO 7.2). Resolved at
471
473
  * 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.3",
3
+ "version": "0.6.0-rc.5",
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",