@cjhyy/code-shell-core 0.6.0-rc.6 → 0.6.0-rc.7
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/engine/model-connections-pool.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/llm/capabilities/rules.js +1 -1
- package/dist/llm/model-pool.d.ts +5 -0
- package/dist/llm/model-pool.js +2 -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.js +1 -1
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/server.js +8 -3
- package/dist/runtime/background-shell.js +14 -0
- package/dist/runtime/safe-spawn.js +17 -2
- package/dist/runtime/spawn-common.d.ts +10 -4
- package/dist/runtime/spawn-common.js +44 -8
- package/dist/tool-system/builtin/bash.js +2 -3
- 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/powershell.js +2 -2
- package/package.json +1 -1
|
@@ -51,6 +51,7 @@ export function modelEntriesFromConnections(connections, credentials, catalog) {
|
|
|
51
51
|
const e = {
|
|
52
52
|
key: inst.id,
|
|
53
53
|
provider: clientProvider(entry),
|
|
54
|
+
providerKind: entry.adapterKind,
|
|
54
55
|
model: resolved.model,
|
|
55
56
|
baseUrl: resolved.baseUrl,
|
|
56
57
|
needsKey: resolved.needsKey,
|
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.
|
|
6
|
+
export declare const VERSION = "0.6.0-rc.7";
|
|
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";
|
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.
|
|
6
|
+
export const VERSION = "0.6.0-rc.7";
|
|
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) ────────────────────────────────────────
|
|
@@ -190,7 +190,7 @@ export const RULES = [
|
|
|
190
190
|
// Most modern frontier models on OpenRouter are multimodal; flag the
|
|
191
191
|
// big families explicitly. The catch-all below stays vision=false so
|
|
192
192
|
// older/text-only routes don't get a green checkmark they don't earn.
|
|
193
|
-
match:
|
|
193
|
+
match: /^~?(?:openai\/(?:gpt-4o|gpt-5|o[1-9])|anthropic\/claude-(?:3|4|opus|sonnet|haiku|fable)|google\/gemini-(?:[2-9]|pro)|x-ai\/grok-[4-9]|qwen\/qwen-?vl|meta\/llama-(?:3\.2|4))/i,
|
|
194
194
|
capability: {
|
|
195
195
|
supportsVision: true,
|
|
196
196
|
reasoning: { kind: "openrouter-reasoning" },
|
package/dist/llm/model-pool.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ export interface ModelEntry {
|
|
|
21
21
|
provider: string;
|
|
22
22
|
/** Full model path: "anthropic/claude-opus-4-6" */
|
|
23
23
|
model: string;
|
|
24
|
+
/**
|
|
25
|
+
* Catalog/provider kind used by capability rules. This can differ from
|
|
26
|
+
* `provider`, which only selects the client protocol family.
|
|
27
|
+
*/
|
|
28
|
+
providerKind?: string;
|
|
24
29
|
baseUrl?: string;
|
|
25
30
|
apiKey?: string;
|
|
26
31
|
/** Whether the catalog template requires a key (default true; false = local/
|
package/dist/llm/model-pool.js
CHANGED
|
@@ -221,6 +221,7 @@ export class ModelPool {
|
|
|
221
221
|
return "anthropic";
|
|
222
222
|
return "openai";
|
|
223
223
|
};
|
|
224
|
+
const providerKind = entry.providerKind ?? fromCat?.kind;
|
|
224
225
|
return {
|
|
225
226
|
provider: entry.provider ||
|
|
226
227
|
kindToClientProvider(fromCat?.kind) ||
|
|
@@ -250,7 +251,7 @@ export class ModelPool {
|
|
|
250
251
|
...(entry.reasoningSummary ? { reasoningSummary: entry.reasoningSummary } : {}),
|
|
251
252
|
// Carry the catalog kind through so the capability layer can pick
|
|
252
253
|
// per-(kind, model) request-shape rules.
|
|
253
|
-
...(
|
|
254
|
+
...(providerKind ? { providerKind } : {}),
|
|
254
255
|
...(entry.extraBody && Object.keys(entry.extraBody).length > 0
|
|
255
256
|
? { extraBody: entry.extraBody }
|
|
256
257
|
: {}),
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* ~/.code-shell/model-catalog.user.json) — no UI / adapter changes, as long as
|
|
9
9
|
* its adapterKind points at an already-wired adapter.
|
|
10
10
|
*/
|
|
11
|
+
import { capabilitiesFor } from "../llm/capabilities/index.js";
|
|
11
12
|
import { paramSpecsFromCapability } from "../llm/capabilities/param-specs.js";
|
|
12
13
|
/**
|
|
13
14
|
* Build a text model preset, projecting its params from the capability layer
|
|
@@ -15,11 +16,13 @@ import { paramSpecsFromCapability } from "../llm/capabilities/param-specs.js";
|
|
|
15
16
|
* when the model has none, matching the "absent → no knobs" contract.
|
|
16
17
|
*/
|
|
17
18
|
function textPreset(kind, value, label, ctx) {
|
|
19
|
+
const capability = capabilitiesFor(kind, value);
|
|
18
20
|
const params = paramSpecsFromCapability(kind, value);
|
|
19
21
|
return {
|
|
20
22
|
value,
|
|
21
23
|
...(label ? { label } : {}),
|
|
22
24
|
...(ctx !== undefined ? { maxContextTokens: ctx } : {}),
|
|
25
|
+
supportsVision: capability.supportsVision,
|
|
23
26
|
...(params.length > 0 ? { params } : {}),
|
|
24
27
|
};
|
|
25
28
|
}
|
|
@@ -97,14 +100,16 @@ const ZHIPU_PARAMS = [
|
|
|
97
100
|
];
|
|
98
101
|
/** Zhipu GLM preset — shared ZHIPU_PARAMS, with per-model context window. */
|
|
99
102
|
function glmPreset(value, label, ctx) {
|
|
100
|
-
return { value, label, maxContextTokens: ctx, params: ZHIPU_PARAMS };
|
|
103
|
+
return { value, label, maxContextTokens: ctx, supportsVision: false, params: ZHIPU_PARAMS };
|
|
101
104
|
}
|
|
102
105
|
/** OpenRouter preset with explicit params (no capability-layer projection). */
|
|
103
106
|
function orPreset(value, label, params, ctx) {
|
|
107
|
+
const capability = capabilitiesFor("openrouter", value);
|
|
104
108
|
return {
|
|
105
109
|
value,
|
|
106
110
|
label,
|
|
107
111
|
...(ctx !== undefined ? { maxContextTokens: ctx } : {}),
|
|
112
|
+
supportsVision: capability.supportsVision,
|
|
108
113
|
...(params && params.length > 0 ? { params } : {}),
|
|
109
114
|
};
|
|
110
115
|
}
|
package/dist/preset/index.d.ts
CHANGED
|
@@ -49,6 +49,10 @@ export declare function registerPreset(preset: AgentPreset): void;
|
|
|
49
49
|
/** List all available preset names (built-in + custom). */
|
|
50
50
|
export declare function listPresetNames(): string[];
|
|
51
51
|
export declare function resolveAgentPreset(name?: string): AgentPreset;
|
|
52
|
+
export interface BuildPresetSystemPromptOptions {
|
|
53
|
+
activeToolNames?: readonly string[];
|
|
54
|
+
platform?: NodeJS.Platform;
|
|
55
|
+
}
|
|
52
56
|
/**
|
|
53
57
|
* Build the full system prompt for a preset by loading and joining its sections.
|
|
54
58
|
*
|
|
@@ -58,7 +62,7 @@ export declare function resolveAgentPreset(name?: string): AgentPreset;
|
|
|
58
62
|
* (tools + usage instructions disappear together). Omit `activeToolNames` to
|
|
59
63
|
* include all sections (e.g. when assembling a generic/preview prompt).
|
|
60
64
|
*/
|
|
61
|
-
export declare function buildPresetSystemPrompt(preset: AgentPreset,
|
|
65
|
+
export declare function buildPresetSystemPrompt(preset: AgentPreset, optionsOrActiveToolNames?: BuildPresetSystemPromptOptions | readonly string[]): string;
|
|
62
66
|
export declare function resolveBuiltinToolNames(options?: {
|
|
63
67
|
preset?: string;
|
|
64
68
|
enabledBuiltinTools?: string[];
|
package/dist/preset/index.js
CHANGED
|
@@ -232,6 +232,19 @@ const BROWSER_SECTION_TOOLS = ["browser_observe", "browser_act", "browser_naviga
|
|
|
232
232
|
const TOOL_GATED_SECTIONS = {
|
|
233
233
|
browser: BROWSER_SECTION_TOOLS,
|
|
234
234
|
};
|
|
235
|
+
function isActiveToolNamesList(value) {
|
|
236
|
+
return Array.isArray(value);
|
|
237
|
+
}
|
|
238
|
+
function platformShellGuidance(platform) {
|
|
239
|
+
if (platform !== "win32")
|
|
240
|
+
return "";
|
|
241
|
+
return [
|
|
242
|
+
"# Windows Shell Guidance",
|
|
243
|
+
"- On Windows, Bash uses Git Bash when it is available. Prefer Bash for ordinary shell commands, git operations, package-manager commands, tests, and POSIX-style command lines.",
|
|
244
|
+
"- 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.",
|
|
245
|
+
"- When writing Bash commands on Windows, use Git Bash paths such as `/d/github/project` instead of PowerShell-only syntax or raw `D:\\...` paths inside `cd` commands.",
|
|
246
|
+
].join("\n");
|
|
247
|
+
}
|
|
235
248
|
/**
|
|
236
249
|
* Build the full system prompt for a preset by loading and joining its sections.
|
|
237
250
|
*
|
|
@@ -241,8 +254,12 @@ const TOOL_GATED_SECTIONS = {
|
|
|
241
254
|
* (tools + usage instructions disappear together). Omit `activeToolNames` to
|
|
242
255
|
* include all sections (e.g. when assembling a generic/preview prompt).
|
|
243
256
|
*/
|
|
244
|
-
export function buildPresetSystemPrompt(preset,
|
|
257
|
+
export function buildPresetSystemPrompt(preset, optionsOrActiveToolNames) {
|
|
245
258
|
let sections = preset.promptSections;
|
|
259
|
+
const options = isActiveToolNamesList(optionsOrActiveToolNames)
|
|
260
|
+
? { activeToolNames: optionsOrActiveToolNames }
|
|
261
|
+
: (optionsOrActiveToolNames ?? {});
|
|
262
|
+
const activeToolNames = options.activeToolNames;
|
|
246
263
|
if (activeToolNames) {
|
|
247
264
|
const active = new Set(activeToolNames);
|
|
248
265
|
sections = sections.filter((s) => {
|
|
@@ -250,7 +267,9 @@ export function buildPresetSystemPrompt(preset, activeToolNames) {
|
|
|
250
267
|
return !gate || gate.some((t) => active.has(t));
|
|
251
268
|
});
|
|
252
269
|
}
|
|
253
|
-
return loadSections(sections)
|
|
270
|
+
return [loadSections(sections), platformShellGuidance(options.platform ?? process.platform)]
|
|
271
|
+
.filter(Boolean)
|
|
272
|
+
.join("\n\n");
|
|
254
273
|
}
|
|
255
274
|
export function resolveBuiltinToolNames(options) {
|
|
256
275
|
const preset = resolveAgentPreset(options?.preset);
|
package/dist/prompt/composer.js
CHANGED
|
@@ -187,7 +187,7 @@ export class PromptComposer {
|
|
|
187
187
|
cacheBreak: true,
|
|
188
188
|
compute: () => {
|
|
189
189
|
const preset = this.options.preset ?? resolveAgentPreset();
|
|
190
|
-
return buildPresetSystemPrompt(preset, activeToolNames);
|
|
190
|
+
return buildPresetSystemPrompt(preset, { activeToolNames, platform: process.platform });
|
|
191
191
|
},
|
|
192
192
|
});
|
|
193
193
|
// Skills listing is intentionally NOT a system section: it changes when a
|
|
@@ -31,7 +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:
|
|
34
|
+
- Shell choice: use Bash for ordinary shell commands, git operations, package-manager commands, test/build scripts, and POSIX-style command lines. Use PowerShell only when the user explicitly asks for it or the task requires PowerShell-specific cmdlets, Windows APIs, registry access, or `.ps1` behavior.
|
|
35
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.
|
|
36
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.
|
|
37
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.
|
package/dist/protocol/server.js
CHANGED
|
@@ -997,12 +997,17 @@ export class AgentServer {
|
|
|
997
997
|
break;
|
|
998
998
|
}
|
|
999
999
|
case "compact": {
|
|
1000
|
-
|
|
1001
|
-
this.
|
|
1000
|
+
const compactEngine = this.chatManager && typeof params.sessionId === "string"
|
|
1001
|
+
? this.chatManager.get(params.sessionId)?.engine
|
|
1002
|
+
: engine;
|
|
1003
|
+
if (!compactEngine) {
|
|
1004
|
+
this.transport.send(createErrorResponse(req.id, this.chatManager && params.sessionId ? ErrorCodes.SessionClosed : ErrorCodes.InternalError, this.chatManager && params.sessionId
|
|
1005
|
+
? `No such live session: ${params.sessionId}`
|
|
1006
|
+
: "No engine available for compact query"));
|
|
1002
1007
|
return;
|
|
1003
1008
|
}
|
|
1004
1009
|
try {
|
|
1005
|
-
const result =
|
|
1010
|
+
const result = compactEngine.forceCompact();
|
|
1006
1011
|
this.transport.send(createResponse(req.id, {
|
|
1007
1012
|
type: "compact",
|
|
1008
1013
|
data: result,
|
|
@@ -89,6 +89,7 @@ export class BackgroundShellManager {
|
|
|
89
89
|
error: `Too many background shells for this session (max ${MAX_SHELLS_PER_SESSION}). KillShell some first.`,
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
|
+
const profileStartedAt = backgroundSpawnProfileEnabled() ? performance.now() : 0;
|
|
92
93
|
const shell = opts.shell ?? defaultShellBinary();
|
|
93
94
|
const { file, args } = resolveSpawnTarget(opts.command, {
|
|
94
95
|
cwd: opts.cwd,
|
|
@@ -113,8 +114,10 @@ export class BackgroundShellManager {
|
|
|
113
114
|
});
|
|
114
115
|
}
|
|
115
116
|
catch (err) {
|
|
117
|
+
logBackgroundSpawnProfile(file, args, elapsedProfileMs(profileStartedAt), "spawn_failed");
|
|
116
118
|
return { ok: false, error: `Failed to spawn background shell: ${err.message}` };
|
|
117
119
|
}
|
|
120
|
+
logBackgroundSpawnProfile(file, args, elapsedProfileMs(profileStartedAt), "started");
|
|
118
121
|
if (child.pid === undefined) {
|
|
119
122
|
return { ok: false, error: "Failed to spawn background shell: no pid" };
|
|
120
123
|
}
|
|
@@ -493,3 +496,14 @@ export class BackgroundShellManager {
|
|
|
493
496
|
}
|
|
494
497
|
/** Process-local singleton, mirroring asyncAgentRegistry's lifetime contract. */
|
|
495
498
|
export const backgroundShellManager = new BackgroundShellManager();
|
|
499
|
+
function backgroundSpawnProfileEnabled() {
|
|
500
|
+
return process.env.CODESHELL_SPAWN_PROFILE === "1";
|
|
501
|
+
}
|
|
502
|
+
function elapsedProfileMs(startedAt) {
|
|
503
|
+
return startedAt > 0 ? Math.round(performance.now() - startedAt) : undefined;
|
|
504
|
+
}
|
|
505
|
+
function logBackgroundSpawnProfile(file, args, elapsedMs, status) {
|
|
506
|
+
if (!backgroundSpawnProfileEnabled())
|
|
507
|
+
return;
|
|
508
|
+
console.error(`[spawn-profile] background shell=${JSON.stringify(file)} flag=${JSON.stringify(args[0] ?? "")} elapsedMs=${elapsedMs ?? "n/a"} status=${status}`);
|
|
509
|
+
}
|
|
@@ -50,6 +50,7 @@ export function safeSpawn(file, args, opts) {
|
|
|
50
50
|
args,
|
|
51
51
|
opts,
|
|
52
52
|
cleanup: undefined,
|
|
53
|
+
resolveMs: undefined,
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
@@ -60,6 +61,7 @@ export function safeSpawn(file, args, opts) {
|
|
|
60
61
|
* abort, timeout, pre-spawn-abort. Used by the Bash tool.
|
|
61
62
|
*/
|
|
62
63
|
export function safeSpawnShell(command, opts) {
|
|
64
|
+
const resolveStartedAt = spawnProfileEnabled() ? performance.now() : 0;
|
|
63
65
|
// Don't hardcode a POSIX default here — resolveSpawnTarget →
|
|
64
66
|
// resolveShellInvocation picks the platform shell (cmd.exe on Windows,
|
|
65
67
|
// $SHELL/bin/bash on POSIX) when opts.shell is omitted.
|
|
@@ -69,11 +71,12 @@ export function safeSpawnShell(command, opts) {
|
|
|
69
71
|
shell,
|
|
70
72
|
sandbox: opts.sandbox,
|
|
71
73
|
});
|
|
72
|
-
return runLifecycle({ file, args, opts, cleanup });
|
|
74
|
+
return runLifecycle({ file, args, opts, cleanup, resolveMs: elapsedMs(resolveStartedAt) });
|
|
73
75
|
}
|
|
74
|
-
function runLifecycle({ file, args, opts, cleanup }) {
|
|
76
|
+
function runLifecycle({ file, args, opts, cleanup, resolveMs }) {
|
|
75
77
|
const maxBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
76
78
|
const abortGrace = opts.ioDrainGraceMs ?? DEFAULT_IO_DRAIN_GRACE_MS;
|
|
79
|
+
const lifecycleStartedAt = spawnProfileEnabled() ? performance.now() : 0;
|
|
77
80
|
// Pre-spawn abort: don't pay spawn cost.
|
|
78
81
|
if (opts.signal?.aborted) {
|
|
79
82
|
safeCleanup(cleanup);
|
|
@@ -85,6 +88,7 @@ function runLifecycle({ file, args, opts, cleanup }) {
|
|
|
85
88
|
if (settled)
|
|
86
89
|
return;
|
|
87
90
|
settled = true;
|
|
91
|
+
logSpawnProfile(file, args, resolveMs, elapsedMs(lifecycleStartedAt), result.reason);
|
|
88
92
|
// Always release backend-allocated resources, regardless of exit path.
|
|
89
93
|
// cleanup is best-effort — see seatbelt backend for rationale.
|
|
90
94
|
safeCleanup(cleanup);
|
|
@@ -221,6 +225,17 @@ function runLifecycle({ file, args, opts, cleanup }) {
|
|
|
221
225
|
});
|
|
222
226
|
});
|
|
223
227
|
}
|
|
228
|
+
function spawnProfileEnabled() {
|
|
229
|
+
return process.env.CODESHELL_SPAWN_PROFILE === "1";
|
|
230
|
+
}
|
|
231
|
+
function elapsedMs(startedAt) {
|
|
232
|
+
return startedAt > 0 ? Math.round(performance.now() - startedAt) : undefined;
|
|
233
|
+
}
|
|
234
|
+
function logSpawnProfile(file, args, resolveMs, lifecycleMs, reason) {
|
|
235
|
+
if (!spawnProfileEnabled())
|
|
236
|
+
return;
|
|
237
|
+
console.error(`[spawn-profile] shell=${JSON.stringify(file)} flag=${JSON.stringify(args[0] ?? "")} resolveMs=${resolveMs ?? "n/a"} lifecycleMs=${lifecycleMs ?? "n/a"} reason=${reason}`);
|
|
238
|
+
}
|
|
224
239
|
function safeCleanup(cleanup) {
|
|
225
240
|
if (!cleanup)
|
|
226
241
|
return;
|
|
@@ -72,9 +72,11 @@ export interface SpawnTarget {
|
|
|
72
72
|
* in five places (bash.ts, safe-spawn.ts, background-shell.ts, worktree.ts):
|
|
73
73
|
*
|
|
74
74
|
* - POSIX: `<explicit ?? $SHELL ?? /bin/bash> -c "<command>"`
|
|
75
|
-
* - Windows:
|
|
76
|
-
*
|
|
77
|
-
* `-Command`,
|
|
75
|
+
* - Windows: `<explicit shell ?? Git Bash ?? PowerShell ?? ComSpec ?? cmd.exe>` with the
|
|
76
|
+
* shell-appropriate flag: Git Bash/POSIX shells use `-c`, PowerShell uses
|
|
77
|
+
* `-Command`, and cmd.exe uses `/c` (`-c` would be taken as a filename and
|
|
78
|
+
* fail/hang). Git Bash is preferred because the Bash tool receives POSIX
|
|
79
|
+
* syntax from the model.
|
|
78
80
|
*
|
|
79
81
|
* `$SHELL` is ignored on Windows — it is virtually never set there, and when
|
|
80
82
|
* it is (e.g. a stray value from a Unix-y env) it points at a POSIX path that
|
|
@@ -88,8 +90,12 @@ export declare function resolveShellInvocation(command: string, shell?: string):
|
|
|
88
90
|
export declare function resolveGitBash(): string | undefined;
|
|
89
91
|
/** Reset the Git Bash probe cache. Test-only (platform is stubbed per test). */
|
|
90
92
|
export declare function _resetGitBashCache(): void;
|
|
93
|
+
export declare function resolvePowerShell(): string | undefined;
|
|
94
|
+
/** Reset the PowerShell probe cache. Test-only (platform is stubbed per test). */
|
|
95
|
+
export declare function _resetPowerShellCache(): void;
|
|
91
96
|
/** The platform's default interactive shell binary, for spawning a bare shell
|
|
92
|
-
* (no `-c`/`/c` command). Windows → Git Bash if present, else
|
|
97
|
+
* (no `-c`/`/c` command). Windows → Git Bash if present, else PowerShell,
|
|
98
|
+
* else ComSpec/cmd.exe;
|
|
93
99
|
* POSIX → $SHELL/bin/bash. Windows prefers Git Bash so the model's bash-syntax
|
|
94
100
|
* commands actually run (cmd.exe can't). */
|
|
95
101
|
export declare function defaultShellBinary(shell?: string): string;
|
|
@@ -105,9 +105,11 @@ export function mergeShellEnv(base, projectEnv) {
|
|
|
105
105
|
* in five places (bash.ts, safe-spawn.ts, background-shell.ts, worktree.ts):
|
|
106
106
|
*
|
|
107
107
|
* - POSIX: `<explicit ?? $SHELL ?? /bin/bash> -c "<command>"`
|
|
108
|
-
* - Windows:
|
|
109
|
-
*
|
|
110
|
-
* `-Command`,
|
|
108
|
+
* - Windows: `<explicit shell ?? Git Bash ?? PowerShell ?? ComSpec ?? cmd.exe>` with the
|
|
109
|
+
* shell-appropriate flag: Git Bash/POSIX shells use `-c`, PowerShell uses
|
|
110
|
+
* `-Command`, and cmd.exe uses `/c` (`-c` would be taken as a filename and
|
|
111
|
+
* fail/hang). Git Bash is preferred because the Bash tool receives POSIX
|
|
112
|
+
* syntax from the model.
|
|
111
113
|
*
|
|
112
114
|
* `$SHELL` is ignored on Windows — it is virtually never set there, and when
|
|
113
115
|
* it is (e.g. a stray value from a Unix-y env) it points at a POSIX path that
|
|
@@ -116,7 +118,7 @@ export function mergeShellEnv(base, projectEnv) {
|
|
|
116
118
|
*/
|
|
117
119
|
export function resolveShellInvocation(command, shell) {
|
|
118
120
|
if (process.platform === "win32") {
|
|
119
|
-
const file = shell ??
|
|
121
|
+
const file = shell ?? defaultShellBinary();
|
|
120
122
|
// Flag form depends on the shell: PowerShell → -Command; a POSIX shell such
|
|
121
123
|
// as Git Bash's bash.exe / sh → -c (it does NOT understand cmd's /c); cmd.exe
|
|
122
124
|
// (and cmd-like) → /c. Detecting bash/sh matters now that defaultShellBinary
|
|
@@ -139,7 +141,7 @@ export function resolveShellInvocation(command, shell) {
|
|
|
139
141
|
* default is `cmd.exe`, which can't run any of that — so "Bash" was effectively
|
|
140
142
|
* broken on Windows. Git for Windows (which most devs already have — we detect
|
|
141
143
|
* it for repo ops anyway) ships a full bash at `<git>\bin\bash.exe`, so prefer
|
|
142
|
-
* it.
|
|
144
|
+
* it. Falls back to PowerShell before cmd.exe when Git Bash truly isn't present.
|
|
143
145
|
*
|
|
144
146
|
* Resolution order:
|
|
145
147
|
* 1. CODE_SHELL_GIT_BASH_PATH env override (explicit user config wins).
|
|
@@ -183,15 +185,49 @@ export function resolveGitBash() {
|
|
|
183
185
|
export function _resetGitBashCache() {
|
|
184
186
|
gitBashCache = undefined;
|
|
185
187
|
}
|
|
188
|
+
let powerShellCache;
|
|
189
|
+
export function resolvePowerShell() {
|
|
190
|
+
if (process.platform !== "win32")
|
|
191
|
+
return undefined;
|
|
192
|
+
if (powerShellCache !== undefined)
|
|
193
|
+
return powerShellCache ?? undefined;
|
|
194
|
+
const override = process.env.CODE_SHELL_POWERSHELL_PATH;
|
|
195
|
+
if (override && existsSync(override))
|
|
196
|
+
return (powerShellCache = override);
|
|
197
|
+
const candidates = [];
|
|
198
|
+
for (const exe of ["pwsh", "powershell"]) {
|
|
199
|
+
try {
|
|
200
|
+
const out = execFileSync("where", [exe], { encoding: "utf-8", timeout: 3000 });
|
|
201
|
+
const found = out.split(/\r?\n/).find((l) => l.trim().toLowerCase().endsWith(`${exe}.exe`));
|
|
202
|
+
if (found)
|
|
203
|
+
candidates.push(found.trim());
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Not on PATH; try the next shell / well-known locations.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const pf = process.env["ProgramFiles"] ?? "C:\\Program Files";
|
|
210
|
+
const systemRoot = process.env.SystemRoot ?? "C:\\Windows";
|
|
211
|
+
candidates.push(join(pf, "PowerShell", "7", "pwsh.exe"));
|
|
212
|
+
candidates.push(join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"));
|
|
213
|
+
const found = candidates.find((p) => existsSync(p));
|
|
214
|
+
return (powerShellCache = found ?? null) ?? undefined;
|
|
215
|
+
}
|
|
216
|
+
/** Reset the PowerShell probe cache. Test-only (platform is stubbed per test). */
|
|
217
|
+
export function _resetPowerShellCache() {
|
|
218
|
+
powerShellCache = undefined;
|
|
219
|
+
}
|
|
186
220
|
/** The platform's default interactive shell binary, for spawning a bare shell
|
|
187
|
-
* (no `-c`/`/c` command). Windows → Git Bash if present, else
|
|
221
|
+
* (no `-c`/`/c` command). Windows → Git Bash if present, else PowerShell,
|
|
222
|
+
* else ComSpec/cmd.exe;
|
|
188
223
|
* POSIX → $SHELL/bin/bash. Windows prefers Git Bash so the model's bash-syntax
|
|
189
224
|
* commands actually run (cmd.exe can't). */
|
|
190
225
|
export function defaultShellBinary(shell) {
|
|
191
226
|
if (shell)
|
|
192
227
|
return shell;
|
|
193
|
-
if (process.platform === "win32")
|
|
194
|
-
return resolveGitBash() ?? process.env.ComSpec ?? "cmd.exe";
|
|
228
|
+
if (process.platform === "win32") {
|
|
229
|
+
return resolveGitBash() ?? resolvePowerShell() ?? process.env.ComSpec ?? "cmd.exe";
|
|
230
|
+
}
|
|
195
231
|
return process.env.SHELL ?? "/bin/bash";
|
|
196
232
|
}
|
|
197
233
|
/**
|
|
@@ -30,9 +30,8 @@ export const bashToolDef = {
|
|
|
30
30
|
name: "Bash",
|
|
31
31
|
description: "Execute a shell command and return its output. " +
|
|
32
32
|
"Use for ordinary shell commands, system operations, git commands, " +
|
|
33
|
-
"package-manager commands, running tests, installing packages, etc. " +
|
|
34
|
-
"
|
|
35
|
-
"PowerShell unless the task needs PowerShell-specific syntax or APIs.",
|
|
33
|
+
"package-manager commands, running tests, installing packages, etc. Prefer Bash " +
|
|
34
|
+
"over PowerShell unless the task needs PowerShell-specific syntax or APIs.",
|
|
36
35
|
inputSchema: {
|
|
37
36
|
type: "object",
|
|
38
37
|
properties: {
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
import { randomBytes } from "node:crypto";
|
|
11
11
|
import { userCatalogPath } from "../../model-catalog/index.js";
|
|
12
12
|
import { saveCatalogEntry } from "../../model-catalog/save-entry.js";
|
|
13
|
+
import { catalogEntrySchema } from "../../model-catalog/types.js";
|
|
13
14
|
export const editModelCatalogToolDef = {
|
|
14
15
|
name: "EditModelCatalog",
|
|
15
16
|
description: "Add or update a provider/model template in the user model catalog so it " +
|
|
16
|
-
"appears in the connection page (text/image/video). Keyed by `id
|
|
17
|
-
"adds, an existing id
|
|
17
|
+
"appears in the connection page (text/image/video). Keyed by `id`: a new id " +
|
|
18
|
+
"adds a provider, an existing id replaces that user-catalog entry. Backs up " +
|
|
19
|
+
"the file before writing and " +
|
|
18
20
|
"validates the entry. Use after researching a model's real facts (id, context " +
|
|
19
21
|
"window, supported params, modalities) — do NOT guess; verify against the " +
|
|
20
22
|
"provider's official docs first (see the model-fact-finder skill). Does NOT set " +
|
|
@@ -31,7 +33,8 @@ export const editModelCatalogToolDef = {
|
|
|
31
33
|
"maxContextTokens?, maxOutputTokens?, supportsVision?, params?[]}). Each param: " +
|
|
32
34
|
"{name, label?, control (enum|number|toggle|text), options?[], min?, max?, default?, " +
|
|
33
35
|
"doc?, wire?{field}}. Fill maxContextTokens with the model's REAL window; declare " +
|
|
34
|
-
"params per-model (only what that model supports)."
|
|
36
|
+
"params per-model (only what that model supports). To keep a custom provider " +
|
|
37
|
+
"beside a built-in one, use a distinct id.",
|
|
35
38
|
},
|
|
36
39
|
},
|
|
37
40
|
required: ["entry"],
|
|
@@ -42,11 +45,18 @@ export async function editModelCatalogTool(args) {
|
|
|
42
45
|
if (!entry || typeof entry !== "object") {
|
|
43
46
|
return "Error: `entry` (a CatalogEntry object) is required.";
|
|
44
47
|
}
|
|
48
|
+
const parsed = catalogEntrySchema.safeParse(entry);
|
|
49
|
+
if (!parsed.success) {
|
|
50
|
+
return `Error: invalid catalog entry: ${parsed.error.issues.map((i) => i.message).join("; ")}`;
|
|
51
|
+
}
|
|
45
52
|
const stamp = `${Date.now()}-${randomBytes(3).toString("hex")}`;
|
|
46
|
-
const r = saveCatalogEntry(
|
|
53
|
+
const r = saveCatalogEntry(parsed.data, {
|
|
54
|
+
path: userCatalogPath(),
|
|
55
|
+
stamp,
|
|
56
|
+
});
|
|
47
57
|
if (!r.ok)
|
|
48
58
|
return `Error: ${r.error}`;
|
|
49
|
-
return summarizeWrite(
|
|
59
|
+
return summarizeWrite(parsed.data, r.action ?? "added", r.backup);
|
|
50
60
|
}
|
|
51
61
|
/**
|
|
52
62
|
* Build a structured completion summary so the user can SEE exactly what was
|
|
@@ -86,6 +86,7 @@ export const generateVideoToolDef = {
|
|
|
86
86
|
let injectedProvider = null;
|
|
87
87
|
export function __setVideoProviderForTests(p) {
|
|
88
88
|
injectedProvider = p;
|
|
89
|
+
availCache.clear();
|
|
89
90
|
}
|
|
90
91
|
/**
|
|
91
92
|
* Tool-visibility guard: GenerateVideo is hidden until a video provider is
|
|
@@ -97,6 +98,8 @@ export function __setVideoProviderForTests(p) {
|
|
|
97
98
|
const availCache = new Map();
|
|
98
99
|
const AVAIL_TTL_MS = 1000;
|
|
99
100
|
export function isGenerateVideoAvailable(cwd = process.cwd(), nowMs = Date.now()) {
|
|
101
|
+
if (injectedProvider)
|
|
102
|
+
return true;
|
|
100
103
|
const hit = availCache.get(cwd);
|
|
101
104
|
if (hit && nowMs - hit.at < AVAIL_TTL_MS)
|
|
102
105
|
return hit.value;
|
|
@@ -3,5 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ToolDefinition } from "../../types.js";
|
|
5
5
|
import type { ToolContext } from "../context.js";
|
|
6
|
+
type ExecFileForGrep = (file: string, args: readonly string[], options: {
|
|
7
|
+
maxBuffer: number;
|
|
8
|
+
timeout: number;
|
|
9
|
+
}) => Promise<{
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function _setGrepExecFileForTest(fn?: ExecFileForGrep): void;
|
|
6
14
|
export declare const grepToolDef: ToolDefinition;
|
|
7
15
|
export declare function grepTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
16
|
+
export {};
|
|
@@ -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
|
+
}
|
|
@@ -10,8 +10,8 @@ export const powershellToolDef = {
|
|
|
10
10
|
name: "PowerShell",
|
|
11
11
|
description: "Execute PowerShell commands. Use only when the user explicitly asks for PowerShell " +
|
|
12
12
|
"or the task requires PowerShell-specific cmdlets, Windows APIs, registry access, " +
|
|
13
|
-
"or .ps1 behavior.
|
|
14
|
-
"
|
|
13
|
+
"or .ps1 behavior. Do NOT use for ordinary file, git, package-manager, test, " +
|
|
14
|
+
"or POSIX-style shell commands; use Bash for those.",
|
|
15
15
|
inputSchema: {
|
|
16
16
|
type: "object",
|
|
17
17
|
properties: {
|
package/package.json
CHANGED