reasonix 0.4.15 → 0.4.16
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/cli/index.js +740 -252
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +98 -2
- package/dist/index.js +248 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -762,6 +762,91 @@ interface FilesystemToolsOptions {
|
|
|
762
762
|
}
|
|
763
763
|
declare function registerFilesystemTools(registry: ToolRegistry, opts: FilesystemToolsOptions): ToolRegistry;
|
|
764
764
|
|
|
765
|
+
/**
|
|
766
|
+
* Native shell tool — lets the model run commands inside the sandbox
|
|
767
|
+
* root so it can actually verify its own work (run tests, check git
|
|
768
|
+
* status, inspect a lockfile, etc.). Without this the coding-mode
|
|
769
|
+
* loop is "write code, hope it works, ask the user to run it" —
|
|
770
|
+
* defeats the purpose.
|
|
771
|
+
*
|
|
772
|
+
* Safety model:
|
|
773
|
+
* - Commands run with `cwd` pinned to the registered root. No
|
|
774
|
+
* path traversal via the command itself is enforced (users can
|
|
775
|
+
* `cat ../outside.txt`); the trust boundary is the directory
|
|
776
|
+
* you opened Reasonix from.
|
|
777
|
+
* - Commands are matched against a read-only / testing allowlist.
|
|
778
|
+
* Allowlisted commands execute immediately and return stdout +
|
|
779
|
+
* stderr merged. Everything else throws with a clear message —
|
|
780
|
+
* the UI translates that into an `/apply`-style confirm gate so
|
|
781
|
+
* the user sees the exact command before it runs.
|
|
782
|
+
* - Default timeout: 60s. Output cap: matches tool-result budget.
|
|
783
|
+
* - Every command that DOES run is spawned with `shell: false` and
|
|
784
|
+
* a tokenized argv — no string-to-shell interpolation, so the
|
|
785
|
+
* model can't accidentally construct a chained `rm` via quoting.
|
|
786
|
+
*
|
|
787
|
+
* This is intentionally narrower than what Claude Code / Aider ship:
|
|
788
|
+
* we gate more commands behind confirmation by default. Users who
|
|
789
|
+
* trust the model can widen the allowlist by instantiating their
|
|
790
|
+
* own tool registry.
|
|
791
|
+
*/
|
|
792
|
+
|
|
793
|
+
interface ShellToolsOptions {
|
|
794
|
+
/** Directory to run commands in. Must be an absolute path. */
|
|
795
|
+
rootDir: string;
|
|
796
|
+
/** Seconds before an individual command is killed. Default: 60. */
|
|
797
|
+
timeoutSec?: number;
|
|
798
|
+
/**
|
|
799
|
+
* Per-command stdout+stderr cap in characters. Default: 32_000 to
|
|
800
|
+
* match the tool-result budget.
|
|
801
|
+
*/
|
|
802
|
+
maxOutputChars?: number;
|
|
803
|
+
/**
|
|
804
|
+
* Extra command-name prefixes the user explicitly trusts. Added on
|
|
805
|
+
* top of the built-in allowlist. Examples: `["my-ci-script", "lint"]`.
|
|
806
|
+
*/
|
|
807
|
+
extraAllowed?: string[];
|
|
808
|
+
/**
|
|
809
|
+
* When true, skip the allowlist entirely and auto-run every command.
|
|
810
|
+
* Off by default — this is an escape hatch for non-interactive use
|
|
811
|
+
* (CI, benchmarks) where a human can't be in the loop to confirm.
|
|
812
|
+
*/
|
|
813
|
+
allowAll?: boolean;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Tokenize a shell-ish command string into argv. Handles single/double
|
|
817
|
+
* quoting; rejects unclosed quotes. Does NOT expand env vars, globs,
|
|
818
|
+
* backticks, or `$(…)` — the goal is to prevent the model from
|
|
819
|
+
* accidentally (or not) sneaking arbitrary shells past the allowlist
|
|
820
|
+
* via concatenation. Exported for testing.
|
|
821
|
+
*/
|
|
822
|
+
declare function tokenizeCommand(cmd: string): string[];
|
|
823
|
+
/**
|
|
824
|
+
* Return true when `cmd` matches an allowlisted prefix. Exported for
|
|
825
|
+
* testing. Match is on the space-normalized leading tokens so
|
|
826
|
+
* `git status -s ` and `git status` both match `git status`.
|
|
827
|
+
*/
|
|
828
|
+
declare function isAllowed(cmd: string, extra?: readonly string[]): boolean;
|
|
829
|
+
interface RunCommandResult {
|
|
830
|
+
exitCode: number | null;
|
|
831
|
+
/** Combined stdout+stderr, truncated to `maxOutputChars` with a marker. */
|
|
832
|
+
output: string;
|
|
833
|
+
/** True when the process was killed for exceeding `timeoutSec`. */
|
|
834
|
+
timedOut: boolean;
|
|
835
|
+
}
|
|
836
|
+
declare function runCommand(cmd: string, opts: {
|
|
837
|
+
cwd: string;
|
|
838
|
+
timeoutSec?: number;
|
|
839
|
+
maxOutputChars?: number;
|
|
840
|
+
signal?: AbortSignal;
|
|
841
|
+
}): Promise<RunCommandResult>;
|
|
842
|
+
/** Error thrown by `run_command` when the command isn't allowlisted. */
|
|
843
|
+
declare class NeedsConfirmationError extends Error {
|
|
844
|
+
readonly command: string;
|
|
845
|
+
constructor(command: string);
|
|
846
|
+
}
|
|
847
|
+
declare function registerShellTools(registry: ToolRegistry, opts: ShellToolsOptions): ToolRegistry;
|
|
848
|
+
declare function formatCommandResult(cmd: string, r: RunCommandResult): string;
|
|
849
|
+
|
|
765
850
|
/**
|
|
766
851
|
* Built-in web search + fetch tools.
|
|
767
852
|
*
|
|
@@ -1844,6 +1929,17 @@ interface ReasonixConfig {
|
|
|
1844
1929
|
* endpoint). Set to `false` to keep the session offline.
|
|
1845
1930
|
*/
|
|
1846
1931
|
search?: boolean;
|
|
1932
|
+
/**
|
|
1933
|
+
* Per-project state keyed by absolute directory path. Written by the
|
|
1934
|
+
* "always allow" choice on a shell confirmation prompt; merged into
|
|
1935
|
+
* `registerShellTools({ extraAllowed })` when `reasonix code` runs
|
|
1936
|
+
* against that directory again.
|
|
1937
|
+
*/
|
|
1938
|
+
projects?: {
|
|
1939
|
+
[absoluteRootDir: string]: {
|
|
1940
|
+
shellAllowed?: string[];
|
|
1941
|
+
};
|
|
1942
|
+
};
|
|
1847
1943
|
}
|
|
1848
1944
|
declare function defaultConfigPath(): string;
|
|
1849
1945
|
declare function readConfig(path?: string): ReasonixConfig;
|
|
@@ -1857,6 +1953,6 @@ declare function redactKey(key: string): string;
|
|
|
1857
1953
|
|
|
1858
1954
|
/** Reasonix — DeepSeek-native agent framework. Library entry point. */
|
|
1859
1955
|
|
|
1860
|
-
declare const VERSION = "0.4.
|
|
1956
|
+
declare const VERSION = "0.4.16";
|
|
1861
1957
|
|
|
1862
|
-
export { AppendOnlyLog, type ApplyResult, type ApplyStatus, type BranchOptions, type BranchProgress, type BranchResult, type BranchSample, type BranchSelector, type BranchSummary, type BridgeOptions, type BridgeResult, CODE_SYSTEM_PROMPT, CacheFirstLoop, type CacheFirstLoopOptions, type CallToolResult, type ChatMessage, type ChatResponse, DEFAULT_MAX_RESULT_CHARS, DeepSeekClient, type DeepSeekClientOptions, type RenderOptions as DiffRenderOptions, type DiffReport, type DiffSide, type EditBlock, type EditSnapshot, type EventRole, type FilesystemToolsOptions, type FlattenDecision, type FlattenOptions, type GetPromptResult, type HarvestOptions, ImmutablePrefix, type ImmutablePrefixOptions, type InitializeResult, type InspectionReport, type JSONSchema, type JsonRpcMessage, type JsonRpcRequest, type JsonRpcResponse, type ListPromptsResult, type ListResourcesResult, type ListToolsResult, type LoopEvent, MCP_PROTOCOL_VERSION, McpClient, type McpClientOptions, type McpContentBlock, type McpProgressHandler, type McpProgressInfo, type McpPrompt, type McpPromptArgument, type McpPromptMessage, type McpPromptResourceBlock, type McpResource, type McpResourceContents, type McpResourceContentsBlob, type McpResourceContentsText, type McpSpec, type McpTool, type McpToolSchema, type McpTransport, type PageContent, type ProgressNotificationParams, type ReadResourceResult, type ReadTranscriptResult, type ReasonixConfig, type ReconfigurableOptions, type RepairReport, type ReplayStats, type RetryInfo, type RetryOptions, type Role, type ScavengeOptions, type ScavengeResult, type SearchResult, type SectionResult, type SessionInfo, SessionStats, type SessionSummary, type SseMcpSpec, SseTransport, type SseTransportOptions, type StdioMcpSpec, StdioTransport, type StdioTransportOptions, StormBreaker, type StreamChunk, type ToolCall, type ToolCallContext, ToolCallRepair, type ToolCallRepairOptions, type ToolDefinition, type ToolFunctionSpec, ToolRegistry, type ToolSpec, type TranscriptMeta, type TranscriptRecord, type TruncationRepairResult, type TurnPair, type TurnStats, type TypedPlanState, Usage, VERSION, VolatileScratch, type WebFetchOptions, type WebSearchOptions, type WebToolsOptions, aggregateBranchUsage, analyzeSchema, appendSessionMessage, applyEditBlock, applyEditBlocks, bridgeMcpTools, claudeEquivalentCost, codeSystemPrompt, computeReplayStats, costUsd, defaultConfigPath, defaultSelector, deleteSession, diffTranscripts, emptyPlanState, fetchWithRetry, flattenMcpResult, flattenSchema, formatLoopError, formatSearchResults, harvest, healLoadedMessages, htmlToText, inputCostUsd, inspectMcpServer, isJsonRpcError, isPlanStateEmpty, isPlausibleKey, listSessions, loadApiKey, loadDotenv, loadSessionMessages, nestArguments, openTranscriptFile, outputCostUsd, parseEditBlocks, parseMcpSpec, parseMojeekResults, parseTranscript, readConfig, readTranscript, recordFromLoopEvent, redactKey, registerFilesystemTools, registerWebTools, renderMarkdown as renderDiffMarkdown, renderSummaryTable as renderDiffSummary, repairTruncatedJson, replayFromFile, restoreSnapshots, runBranches, sanitizeName as sanitizeSessionName, saveApiKey, scavengeToolCalls, sessionPath, sessionsDir, similarity, snapshotBeforeEdits, stripHallucinatedToolMarkup, truncateForModel, webFetch, webSearch, writeConfig, writeMeta, writeRecord };
|
|
1958
|
+
export { AppendOnlyLog, type ApplyResult, type ApplyStatus, type BranchOptions, type BranchProgress, type BranchResult, type BranchSample, type BranchSelector, type BranchSummary, type BridgeOptions, type BridgeResult, CODE_SYSTEM_PROMPT, CacheFirstLoop, type CacheFirstLoopOptions, type CallToolResult, type ChatMessage, type ChatResponse, DEFAULT_MAX_RESULT_CHARS, DeepSeekClient, type DeepSeekClientOptions, type RenderOptions as DiffRenderOptions, type DiffReport, type DiffSide, type EditBlock, type EditSnapshot, type EventRole, type FilesystemToolsOptions, type FlattenDecision, type FlattenOptions, type GetPromptResult, type HarvestOptions, ImmutablePrefix, type ImmutablePrefixOptions, type InitializeResult, type InspectionReport, type JSONSchema, type JsonRpcMessage, type JsonRpcRequest, type JsonRpcResponse, type ListPromptsResult, type ListResourcesResult, type ListToolsResult, type LoopEvent, MCP_PROTOCOL_VERSION, McpClient, type McpClientOptions, type McpContentBlock, type McpProgressHandler, type McpProgressInfo, type McpPrompt, type McpPromptArgument, type McpPromptMessage, type McpPromptResourceBlock, type McpResource, type McpResourceContents, type McpResourceContentsBlob, type McpResourceContentsText, type McpSpec, type McpTool, type McpToolSchema, type McpTransport, NeedsConfirmationError, type PageContent, type ProgressNotificationParams, type ReadResourceResult, type ReadTranscriptResult, type ReasonixConfig, type ReconfigurableOptions, type RepairReport, type ReplayStats, type RetryInfo, type RetryOptions, type Role, type RunCommandResult, type ScavengeOptions, type ScavengeResult, type SearchResult, type SectionResult, type SessionInfo, SessionStats, type SessionSummary, type ShellToolsOptions, type SseMcpSpec, SseTransport, type SseTransportOptions, type StdioMcpSpec, StdioTransport, type StdioTransportOptions, StormBreaker, type StreamChunk, type ToolCall, type ToolCallContext, ToolCallRepair, type ToolCallRepairOptions, type ToolDefinition, type ToolFunctionSpec, ToolRegistry, type ToolSpec, type TranscriptMeta, type TranscriptRecord, type TruncationRepairResult, type TurnPair, type TurnStats, type TypedPlanState, Usage, VERSION, VolatileScratch, type WebFetchOptions, type WebSearchOptions, type WebToolsOptions, aggregateBranchUsage, analyzeSchema, appendSessionMessage, applyEditBlock, applyEditBlocks, bridgeMcpTools, claudeEquivalentCost, codeSystemPrompt, computeReplayStats, costUsd, defaultConfigPath, defaultSelector, deleteSession, diffTranscripts, emptyPlanState, fetchWithRetry, flattenMcpResult, flattenSchema, formatCommandResult, formatLoopError, formatSearchResults, harvest, healLoadedMessages, htmlToText, inputCostUsd, inspectMcpServer, isAllowed, isJsonRpcError, isPlanStateEmpty, isPlausibleKey, listSessions, loadApiKey, loadDotenv, loadSessionMessages, nestArguments, openTranscriptFile, outputCostUsd, parseEditBlocks, parseMcpSpec, parseMojeekResults, parseTranscript, readConfig, readTranscript, recordFromLoopEvent, redactKey, registerFilesystemTools, registerShellTools, registerWebTools, renderMarkdown as renderDiffMarkdown, renderSummaryTable as renderDiffSummary, repairTruncatedJson, replayFromFile, restoreSnapshots, runBranches, runCommand, sanitizeName as sanitizeSessionName, saveApiKey, scavengeToolCalls, sessionPath, sessionsDir, similarity, snapshotBeforeEdits, stripHallucinatedToolMarkup, tokenizeCommand, truncateForModel, webFetch, webSearch, writeConfig, writeMeta, writeRecord };
|
package/dist/index.js
CHANGED
|
@@ -47,8 +47,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
|
|
|
47
47
|
}
|
|
48
48
|
function sleep(ms, signal) {
|
|
49
49
|
if (ms <= 0) return Promise.resolve();
|
|
50
|
-
return new Promise((
|
|
51
|
-
const timer = setTimeout(
|
|
50
|
+
return new Promise((resolve5, reject) => {
|
|
51
|
+
const timer = setTimeout(resolve5, ms);
|
|
52
52
|
if (signal) {
|
|
53
53
|
const onAbort = () => {
|
|
54
54
|
clearTimeout(timer);
|
|
@@ -1488,8 +1488,8 @@ var CacheFirstLoop = class {
|
|
|
1488
1488
|
}
|
|
1489
1489
|
);
|
|
1490
1490
|
for (let k = 0; k < budget; k++) {
|
|
1491
|
-
const sample = queue.shift() ?? await new Promise((
|
|
1492
|
-
waiter =
|
|
1491
|
+
const sample = queue.shift() ?? await new Promise((resolve5) => {
|
|
1492
|
+
waiter = resolve5;
|
|
1493
1493
|
});
|
|
1494
1494
|
yield {
|
|
1495
1495
|
turn: this._turn,
|
|
@@ -2183,6 +2183,221 @@ function lineDiff(a, b) {
|
|
|
2183
2183
|
return out;
|
|
2184
2184
|
}
|
|
2185
2185
|
|
|
2186
|
+
// src/tools/shell.ts
|
|
2187
|
+
import { spawn } from "child_process";
|
|
2188
|
+
import * as pathMod2 from "path";
|
|
2189
|
+
var DEFAULT_TIMEOUT_SEC = 60;
|
|
2190
|
+
var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
|
|
2191
|
+
var BUILTIN_ALLOWLIST = [
|
|
2192
|
+
// Repo inspection
|
|
2193
|
+
"git status",
|
|
2194
|
+
"git diff",
|
|
2195
|
+
"git log",
|
|
2196
|
+
"git show",
|
|
2197
|
+
"git blame",
|
|
2198
|
+
"git branch",
|
|
2199
|
+
"git remote",
|
|
2200
|
+
"git rev-parse",
|
|
2201
|
+
"git config --get",
|
|
2202
|
+
// Filesystem inspection
|
|
2203
|
+
"ls",
|
|
2204
|
+
"pwd",
|
|
2205
|
+
"cat",
|
|
2206
|
+
"head",
|
|
2207
|
+
"tail",
|
|
2208
|
+
"wc",
|
|
2209
|
+
"file",
|
|
2210
|
+
"tree",
|
|
2211
|
+
"find",
|
|
2212
|
+
"grep",
|
|
2213
|
+
"rg",
|
|
2214
|
+
// Language version probes
|
|
2215
|
+
"node --version",
|
|
2216
|
+
"node -v",
|
|
2217
|
+
"npm --version",
|
|
2218
|
+
"npx --version",
|
|
2219
|
+
"python --version",
|
|
2220
|
+
"python3 --version",
|
|
2221
|
+
"cargo --version",
|
|
2222
|
+
"go version",
|
|
2223
|
+
"rustc --version",
|
|
2224
|
+
"deno --version",
|
|
2225
|
+
"bun --version",
|
|
2226
|
+
// Test runners (non-destructive by convention)
|
|
2227
|
+
"npm test",
|
|
2228
|
+
"npm run test",
|
|
2229
|
+
"npx vitest run",
|
|
2230
|
+
"npx vitest",
|
|
2231
|
+
"npx jest",
|
|
2232
|
+
"pytest",
|
|
2233
|
+
"python -m pytest",
|
|
2234
|
+
"cargo test",
|
|
2235
|
+
"cargo check",
|
|
2236
|
+
"cargo clippy",
|
|
2237
|
+
"go test",
|
|
2238
|
+
"go vet",
|
|
2239
|
+
"deno test",
|
|
2240
|
+
"bun test",
|
|
2241
|
+
// Linters / typecheckers (read-only by convention)
|
|
2242
|
+
"npm run lint",
|
|
2243
|
+
"npm run typecheck",
|
|
2244
|
+
"npx tsc --noEmit",
|
|
2245
|
+
"npx biome check",
|
|
2246
|
+
"npx eslint",
|
|
2247
|
+
"npx prettier --check",
|
|
2248
|
+
"ruff",
|
|
2249
|
+
"mypy"
|
|
2250
|
+
];
|
|
2251
|
+
function tokenizeCommand(cmd) {
|
|
2252
|
+
const out = [];
|
|
2253
|
+
let cur = "";
|
|
2254
|
+
let quote = null;
|
|
2255
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
2256
|
+
const ch = cmd[i];
|
|
2257
|
+
if (quote) {
|
|
2258
|
+
if (ch === quote) {
|
|
2259
|
+
quote = null;
|
|
2260
|
+
} else if (ch === "\\" && quote === '"' && i + 1 < cmd.length) {
|
|
2261
|
+
cur += cmd[++i];
|
|
2262
|
+
} else {
|
|
2263
|
+
cur += ch;
|
|
2264
|
+
}
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
if (ch === '"' || ch === "'") {
|
|
2268
|
+
quote = ch;
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
if (ch === " " || ch === " ") {
|
|
2272
|
+
if (cur.length > 0) {
|
|
2273
|
+
out.push(cur);
|
|
2274
|
+
cur = "";
|
|
2275
|
+
}
|
|
2276
|
+
continue;
|
|
2277
|
+
}
|
|
2278
|
+
cur += ch;
|
|
2279
|
+
}
|
|
2280
|
+
if (quote) throw new Error(`unclosed ${quote} in command`);
|
|
2281
|
+
if (cur.length > 0) out.push(cur);
|
|
2282
|
+
return out;
|
|
2283
|
+
}
|
|
2284
|
+
function isAllowed(cmd, extra = []) {
|
|
2285
|
+
const normalized = cmd.trim().replace(/\s+/g, " ");
|
|
2286
|
+
const allowlist = [...BUILTIN_ALLOWLIST, ...extra];
|
|
2287
|
+
for (const prefix of allowlist) {
|
|
2288
|
+
if (normalized === prefix) return true;
|
|
2289
|
+
if (normalized.startsWith(`${prefix} `)) return true;
|
|
2290
|
+
}
|
|
2291
|
+
return false;
|
|
2292
|
+
}
|
|
2293
|
+
async function runCommand(cmd, opts) {
|
|
2294
|
+
const argv = tokenizeCommand(cmd);
|
|
2295
|
+
if (argv.length === 0) throw new Error("run_command: empty command");
|
|
2296
|
+
const timeoutMs = (opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3;
|
|
2297
|
+
const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
2298
|
+
const spawnOpts = {
|
|
2299
|
+
cwd: opts.cwd,
|
|
2300
|
+
shell: false,
|
|
2301
|
+
// no shell-expansion — see header comment
|
|
2302
|
+
windowsHide: true,
|
|
2303
|
+
env: process.env
|
|
2304
|
+
};
|
|
2305
|
+
return await new Promise((resolve5, reject) => {
|
|
2306
|
+
let child;
|
|
2307
|
+
try {
|
|
2308
|
+
child = spawn(argv[0], argv.slice(1), spawnOpts);
|
|
2309
|
+
} catch (err) {
|
|
2310
|
+
reject(err);
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
let buf = "";
|
|
2314
|
+
let timedOut = false;
|
|
2315
|
+
const killTimer = setTimeout(() => {
|
|
2316
|
+
timedOut = true;
|
|
2317
|
+
child.kill("SIGKILL");
|
|
2318
|
+
}, timeoutMs);
|
|
2319
|
+
const onAbort = () => child.kill("SIGKILL");
|
|
2320
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2321
|
+
const onData = (chunk) => {
|
|
2322
|
+
buf += chunk.toString();
|
|
2323
|
+
if (buf.length > maxChars * 2) buf = `${buf.slice(0, maxChars * 2)}`;
|
|
2324
|
+
};
|
|
2325
|
+
child.stdout?.on("data", onData);
|
|
2326
|
+
child.stderr?.on("data", onData);
|
|
2327
|
+
child.on("error", (err) => {
|
|
2328
|
+
clearTimeout(killTimer);
|
|
2329
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
2330
|
+
reject(err);
|
|
2331
|
+
});
|
|
2332
|
+
child.on("close", (code) => {
|
|
2333
|
+
clearTimeout(killTimer);
|
|
2334
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
2335
|
+
const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
|
|
2336
|
+
|
|
2337
|
+
[\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
|
|
2338
|
+
resolve5({ exitCode: code, output, timedOut });
|
|
2339
|
+
});
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
var NeedsConfirmationError = class extends Error {
|
|
2343
|
+
command;
|
|
2344
|
+
constructor(command) {
|
|
2345
|
+
super(
|
|
2346
|
+
`run_command: "${command}" needs the user's approval before it runs. STOP calling tools now \u2014 the TUI has already prompted the user to press y (run) or n (deny). Wait for their next message; it will either be the command's output (if they approved) or an instruction to continue without it (if they denied). Don't retry the command or call other shell commands in the meantime.`
|
|
2347
|
+
);
|
|
2348
|
+
this.name = "NeedsConfirmationError";
|
|
2349
|
+
this.command = command;
|
|
2350
|
+
}
|
|
2351
|
+
};
|
|
2352
|
+
function registerShellTools(registry, opts) {
|
|
2353
|
+
const rootDir = pathMod2.resolve(opts.rootDir);
|
|
2354
|
+
const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
|
|
2355
|
+
const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
2356
|
+
const extraAllowed = opts.extraAllowed ?? [];
|
|
2357
|
+
const allowAll = opts.allowAll ?? false;
|
|
2358
|
+
registry.register({
|
|
2359
|
+
name: "run_command",
|
|
2360
|
+
description: "Run a shell command in the project root and return its combined stdout+stderr. Read-only and test commands (git status, ls, npm test, pytest, cargo test, grep, etc.) run immediately. Anything that could mutate state (npm install, git commit, rm, chmod) is refused and the user has to confirm in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
|
|
2361
|
+
parameters: {
|
|
2362
|
+
type: "object",
|
|
2363
|
+
properties: {
|
|
2364
|
+
command: {
|
|
2365
|
+
type: "string",
|
|
2366
|
+
description: "Full command line, e.g. 'npm test' or 'git diff src/foo.ts'. Tokenized with POSIX-ish quoting; no shell expansion, no pipes, no redirects."
|
|
2367
|
+
},
|
|
2368
|
+
timeoutSec: {
|
|
2369
|
+
type: "integer",
|
|
2370
|
+
description: `Override the default ${timeoutSec}s timeout for a single command.`
|
|
2371
|
+
}
|
|
2372
|
+
},
|
|
2373
|
+
required: ["command"]
|
|
2374
|
+
},
|
|
2375
|
+
fn: async (args, ctx) => {
|
|
2376
|
+
const cmd = args.command.trim();
|
|
2377
|
+
if (!cmd) throw new Error("run_command: empty command");
|
|
2378
|
+
if (!allowAll && !isAllowed(cmd, extraAllowed)) {
|
|
2379
|
+
throw new NeedsConfirmationError(cmd);
|
|
2380
|
+
}
|
|
2381
|
+
const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
|
|
2382
|
+
const result = await runCommand(cmd, {
|
|
2383
|
+
cwd: rootDir,
|
|
2384
|
+
timeoutSec: effectiveTimeout,
|
|
2385
|
+
maxOutputChars,
|
|
2386
|
+
signal: ctx?.signal
|
|
2387
|
+
});
|
|
2388
|
+
return formatCommandResult(cmd, result);
|
|
2389
|
+
}
|
|
2390
|
+
});
|
|
2391
|
+
return registry;
|
|
2392
|
+
}
|
|
2393
|
+
function formatCommandResult(cmd, r) {
|
|
2394
|
+
const header = r.timedOut ? `$ ${cmd}
|
|
2395
|
+
[killed after timeout]` : `$ ${cmd}
|
|
2396
|
+
[exit ${r.exitCode ?? "?"}]`;
|
|
2397
|
+
return r.output ? `${header}
|
|
2398
|
+
${r.output}` : header;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2186
2401
|
// src/tools/web.ts
|
|
2187
2402
|
var DEFAULT_FETCH_MAX_CHARS = 32e3;
|
|
2188
2403
|
var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
|
|
@@ -2366,11 +2581,11 @@ ${i + 1}. ${r.title}`);
|
|
|
2366
2581
|
|
|
2367
2582
|
// src/env.ts
|
|
2368
2583
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2369
|
-
import { resolve as
|
|
2584
|
+
import { resolve as resolve3 } from "path";
|
|
2370
2585
|
function loadDotenv(path = ".env") {
|
|
2371
2586
|
let raw;
|
|
2372
2587
|
try {
|
|
2373
|
-
raw = readFileSync2(
|
|
2588
|
+
raw = readFileSync2(resolve3(process.cwd(), path), "utf8");
|
|
2374
2589
|
} catch {
|
|
2375
2590
|
return;
|
|
2376
2591
|
}
|
|
@@ -3052,7 +3267,7 @@ var McpClient = class {
|
|
|
3052
3267
|
const id = this.nextId++;
|
|
3053
3268
|
const frame = { jsonrpc: "2.0", id, method, params };
|
|
3054
3269
|
let abortHandler = null;
|
|
3055
|
-
const promise = new Promise((
|
|
3270
|
+
const promise = new Promise((resolve5, reject) => {
|
|
3056
3271
|
const timeout = setTimeout(() => {
|
|
3057
3272
|
this.pending.delete(id);
|
|
3058
3273
|
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
@@ -3061,7 +3276,7 @@ var McpClient = class {
|
|
|
3061
3276
|
);
|
|
3062
3277
|
}, this.requestTimeoutMs);
|
|
3063
3278
|
this.pending.set(id, {
|
|
3064
|
-
resolve:
|
|
3279
|
+
resolve: resolve5,
|
|
3065
3280
|
reject,
|
|
3066
3281
|
timeout
|
|
3067
3282
|
});
|
|
@@ -3143,7 +3358,7 @@ var McpClient = class {
|
|
|
3143
3358
|
};
|
|
3144
3359
|
|
|
3145
3360
|
// src/mcp/stdio.ts
|
|
3146
|
-
import { spawn } from "child_process";
|
|
3361
|
+
import { spawn as spawn2 } from "child_process";
|
|
3147
3362
|
var StdioTransport = class {
|
|
3148
3363
|
child;
|
|
3149
3364
|
queue = [];
|
|
@@ -3158,14 +3373,14 @@ var StdioTransport = class {
|
|
|
3158
3373
|
opts.command,
|
|
3159
3374
|
...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
|
|
3160
3375
|
].join(" ");
|
|
3161
|
-
this.child =
|
|
3376
|
+
this.child = spawn2(line, [], {
|
|
3162
3377
|
env,
|
|
3163
3378
|
cwd: opts.cwd,
|
|
3164
3379
|
stdio: ["pipe", "pipe", "inherit"],
|
|
3165
3380
|
shell: true
|
|
3166
3381
|
});
|
|
3167
3382
|
} else {
|
|
3168
|
-
this.child =
|
|
3383
|
+
this.child = spawn2(opts.command, opts.args ?? [], {
|
|
3169
3384
|
env,
|
|
3170
3385
|
cwd: opts.cwd,
|
|
3171
3386
|
stdio: ["pipe", "pipe", "inherit"]
|
|
@@ -3184,12 +3399,12 @@ var StdioTransport = class {
|
|
|
3184
3399
|
}
|
|
3185
3400
|
async send(message) {
|
|
3186
3401
|
if (this.closed) throw new Error("MCP transport is closed");
|
|
3187
|
-
return new Promise((
|
|
3402
|
+
return new Promise((resolve5, reject) => {
|
|
3188
3403
|
const line = `${JSON.stringify(message)}
|
|
3189
3404
|
`;
|
|
3190
3405
|
this.child.stdin.write(line, "utf8", (err) => {
|
|
3191
3406
|
if (err) reject(err);
|
|
3192
|
-
else
|
|
3407
|
+
else resolve5();
|
|
3193
3408
|
});
|
|
3194
3409
|
});
|
|
3195
3410
|
}
|
|
@@ -3200,8 +3415,8 @@ var StdioTransport = class {
|
|
|
3200
3415
|
continue;
|
|
3201
3416
|
}
|
|
3202
3417
|
if (this.closed) return;
|
|
3203
|
-
const next = await new Promise((
|
|
3204
|
-
this.waiters.push(
|
|
3418
|
+
const next = await new Promise((resolve5) => {
|
|
3419
|
+
this.waiters.push(resolve5);
|
|
3205
3420
|
});
|
|
3206
3421
|
if (next === null) return;
|
|
3207
3422
|
yield next;
|
|
@@ -3267,8 +3482,8 @@ var SseTransport = class {
|
|
|
3267
3482
|
constructor(opts) {
|
|
3268
3483
|
this.url = opts.url;
|
|
3269
3484
|
this.headers = opts.headers ?? {};
|
|
3270
|
-
this.endpointReady = new Promise((
|
|
3271
|
-
this.resolveEndpoint =
|
|
3485
|
+
this.endpointReady = new Promise((resolve5, reject) => {
|
|
3486
|
+
this.resolveEndpoint = resolve5;
|
|
3272
3487
|
this.rejectEndpoint = reject;
|
|
3273
3488
|
});
|
|
3274
3489
|
this.endpointReady.catch(() => void 0);
|
|
@@ -3295,8 +3510,8 @@ var SseTransport = class {
|
|
|
3295
3510
|
continue;
|
|
3296
3511
|
}
|
|
3297
3512
|
if (this.closed) return;
|
|
3298
|
-
const next = await new Promise((
|
|
3299
|
-
this.waiters.push(
|
|
3513
|
+
const next = await new Promise((resolve5) => {
|
|
3514
|
+
this.waiters.push(resolve5);
|
|
3300
3515
|
});
|
|
3301
3516
|
if (next === null) return;
|
|
3302
3517
|
yield next;
|
|
@@ -3496,7 +3711,7 @@ async function trySection(load) {
|
|
|
3496
3711
|
|
|
3497
3712
|
// src/code/edit-blocks.ts
|
|
3498
3713
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3499
|
-
import { dirname as dirname3, resolve as
|
|
3714
|
+
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
3500
3715
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
3501
3716
|
function parseEditBlocks(text) {
|
|
3502
3717
|
const out = [];
|
|
@@ -3514,8 +3729,8 @@ function parseEditBlocks(text) {
|
|
|
3514
3729
|
return out;
|
|
3515
3730
|
}
|
|
3516
3731
|
function applyEditBlock(block, rootDir) {
|
|
3517
|
-
const absRoot =
|
|
3518
|
-
const absTarget =
|
|
3732
|
+
const absRoot = resolve4(rootDir);
|
|
3733
|
+
const absTarget = resolve4(absRoot, block.path);
|
|
3519
3734
|
if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
|
|
3520
3735
|
return {
|
|
3521
3736
|
path: block.path,
|
|
@@ -3565,13 +3780,13 @@ function applyEditBlocks(blocks, rootDir) {
|
|
|
3565
3780
|
return blocks.map((b) => applyEditBlock(b, rootDir));
|
|
3566
3781
|
}
|
|
3567
3782
|
function snapshotBeforeEdits(blocks, rootDir) {
|
|
3568
|
-
const absRoot =
|
|
3783
|
+
const absRoot = resolve4(rootDir);
|
|
3569
3784
|
const seen = /* @__PURE__ */ new Set();
|
|
3570
3785
|
const snapshots = [];
|
|
3571
3786
|
for (const b of blocks) {
|
|
3572
3787
|
if (seen.has(b.path)) continue;
|
|
3573
3788
|
seen.add(b.path);
|
|
3574
|
-
const abs =
|
|
3789
|
+
const abs = resolve4(absRoot, b.path);
|
|
3575
3790
|
if (!existsSync2(abs)) {
|
|
3576
3791
|
snapshots.push({ path: b.path, prevContent: null });
|
|
3577
3792
|
continue;
|
|
@@ -3585,9 +3800,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
3585
3800
|
return snapshots;
|
|
3586
3801
|
}
|
|
3587
3802
|
function restoreSnapshots(snapshots, rootDir) {
|
|
3588
|
-
const absRoot =
|
|
3803
|
+
const absRoot = resolve4(rootDir);
|
|
3589
3804
|
return snapshots.map((snap) => {
|
|
3590
|
-
const abs =
|
|
3805
|
+
const abs = resolve4(absRoot, snap.path);
|
|
3591
3806
|
if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
|
|
3592
3807
|
return {
|
|
3593
3808
|
path: snap.path,
|
|
@@ -3737,7 +3952,7 @@ function redactKey(key) {
|
|
|
3737
3952
|
}
|
|
3738
3953
|
|
|
3739
3954
|
// src/index.ts
|
|
3740
|
-
var VERSION = "0.4.
|
|
3955
|
+
var VERSION = "0.4.16";
|
|
3741
3956
|
export {
|
|
3742
3957
|
AppendOnlyLog,
|
|
3743
3958
|
CODE_SYSTEM_PROMPT,
|
|
@@ -3747,6 +3962,7 @@ export {
|
|
|
3747
3962
|
ImmutablePrefix,
|
|
3748
3963
|
MCP_PROTOCOL_VERSION,
|
|
3749
3964
|
McpClient,
|
|
3965
|
+
NeedsConfirmationError,
|
|
3750
3966
|
SessionStats,
|
|
3751
3967
|
SseTransport,
|
|
3752
3968
|
StdioTransport,
|
|
@@ -3774,6 +3990,7 @@ export {
|
|
|
3774
3990
|
fetchWithRetry,
|
|
3775
3991
|
flattenMcpResult,
|
|
3776
3992
|
flattenSchema,
|
|
3993
|
+
formatCommandResult,
|
|
3777
3994
|
formatLoopError,
|
|
3778
3995
|
formatSearchResults,
|
|
3779
3996
|
harvest,
|
|
@@ -3781,6 +3998,7 @@ export {
|
|
|
3781
3998
|
htmlToText,
|
|
3782
3999
|
inputCostUsd,
|
|
3783
4000
|
inspectMcpServer,
|
|
4001
|
+
isAllowed,
|
|
3784
4002
|
isJsonRpcError,
|
|
3785
4003
|
isPlanStateEmpty,
|
|
3786
4004
|
isPlausibleKey,
|
|
@@ -3800,6 +4018,7 @@ export {
|
|
|
3800
4018
|
recordFromLoopEvent,
|
|
3801
4019
|
redactKey,
|
|
3802
4020
|
registerFilesystemTools,
|
|
4021
|
+
registerShellTools,
|
|
3803
4022
|
registerWebTools,
|
|
3804
4023
|
renderMarkdown as renderDiffMarkdown,
|
|
3805
4024
|
renderSummaryTable as renderDiffSummary,
|
|
@@ -3807,6 +4026,7 @@ export {
|
|
|
3807
4026
|
replayFromFile,
|
|
3808
4027
|
restoreSnapshots,
|
|
3809
4028
|
runBranches,
|
|
4029
|
+
runCommand,
|
|
3810
4030
|
sanitizeName as sanitizeSessionName,
|
|
3811
4031
|
saveApiKey,
|
|
3812
4032
|
scavengeToolCalls,
|
|
@@ -3815,6 +4035,7 @@ export {
|
|
|
3815
4035
|
similarity,
|
|
3816
4036
|
snapshotBeforeEdits,
|
|
3817
4037
|
stripHallucinatedToolMarkup,
|
|
4038
|
+
tokenizeCommand,
|
|
3818
4039
|
truncateForModel,
|
|
3819
4040
|
webFetch,
|
|
3820
4041
|
webSearch,
|