reasonix 0.4.14 → 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/README.md +66 -0
- package/dist/cli/index.js +1098 -300
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +197 -2
- package/dist/index.js +435 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,13 +2183,409 @@ 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
|
+
|
|
2401
|
+
// src/tools/web.ts
|
|
2402
|
+
var DEFAULT_FETCH_MAX_CHARS = 32e3;
|
|
2403
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
|
|
2404
|
+
var DEFAULT_TOPK = 5;
|
|
2405
|
+
var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
2406
|
+
var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
|
|
2407
|
+
async function webSearch(query, opts = {}) {
|
|
2408
|
+
const topK = Math.max(1, Math.min(10, opts.topK ?? DEFAULT_TOPK));
|
|
2409
|
+
const resp = await fetch(`${MOJEEK_ENDPOINT}?q=${encodeURIComponent(query)}`, {
|
|
2410
|
+
headers: {
|
|
2411
|
+
"User-Agent": USER_AGENT,
|
|
2412
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9",
|
|
2413
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
2414
|
+
},
|
|
2415
|
+
signal: opts.signal,
|
|
2416
|
+
redirect: "follow"
|
|
2417
|
+
});
|
|
2418
|
+
if (!resp.ok) throw new Error(`web_search ${resp.status}`);
|
|
2419
|
+
const html = await resp.text();
|
|
2420
|
+
const results = parseMojeekResults(html).slice(0, topK);
|
|
2421
|
+
if (results.length === 0) {
|
|
2422
|
+
if (/no results found|did not match any documents/i.test(html)) return [];
|
|
2423
|
+
if (/captcha|verify you are human|access denied|forbidden/i.test(html)) {
|
|
2424
|
+
throw new Error("web_search: Mojeek anti-bot page \u2014 rate-limited or blocked");
|
|
2425
|
+
}
|
|
2426
|
+
throw new Error(
|
|
2427
|
+
`web_search: 0 results but response doesn't look like a real empty page (${html.length} chars, first 120: ${html.slice(0, 120).replace(/\s+/g, " ")})`
|
|
2428
|
+
);
|
|
2429
|
+
}
|
|
2430
|
+
return results;
|
|
2431
|
+
}
|
|
2432
|
+
function parseMojeekResults(html) {
|
|
2433
|
+
const titles = [];
|
|
2434
|
+
const titleAnchorRe = /<a\b[^>]*\bclass="title"[^>]*>[\s\S]*?<\/a>/g;
|
|
2435
|
+
let m;
|
|
2436
|
+
while (true) {
|
|
2437
|
+
m = titleAnchorRe.exec(html);
|
|
2438
|
+
if (m === null) break;
|
|
2439
|
+
titles.push(m[0]);
|
|
2440
|
+
}
|
|
2441
|
+
const snippets = [];
|
|
2442
|
+
const snippetRe = /<p\b[^>]*\bclass="s"[^>]*>([\s\S]*?)<\/p>/g;
|
|
2443
|
+
while (true) {
|
|
2444
|
+
m = snippetRe.exec(html);
|
|
2445
|
+
if (m === null) break;
|
|
2446
|
+
snippets.push(m[1] ?? "");
|
|
2447
|
+
}
|
|
2448
|
+
const hrefRe = /href="([^"]+)"/;
|
|
2449
|
+
const innerRe = /<a\b[^>]*>([\s\S]*?)<\/a>/;
|
|
2450
|
+
const results = [];
|
|
2451
|
+
for (let i = 0; i < titles.length; i++) {
|
|
2452
|
+
const anchor = titles[i];
|
|
2453
|
+
const hrefMatch = anchor.match(hrefRe);
|
|
2454
|
+
const innerMatch = anchor.match(innerRe);
|
|
2455
|
+
if (!hrefMatch?.[1]) continue;
|
|
2456
|
+
results.push({
|
|
2457
|
+
title: decodeHtmlEntities(stripHtml(innerMatch?.[1] ?? "")).trim(),
|
|
2458
|
+
url: hrefMatch[1],
|
|
2459
|
+
snippet: decodeHtmlEntities(stripHtml(snippets[i] ?? "")).replace(/\s+/g, " ").trim()
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
return results;
|
|
2463
|
+
}
|
|
2464
|
+
async function webFetch(url, opts = {}) {
|
|
2465
|
+
const maxChars = opts.maxChars ?? DEFAULT_FETCH_MAX_CHARS;
|
|
2466
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
2467
|
+
const ctl = new AbortController();
|
|
2468
|
+
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
2469
|
+
const cancel = () => ctl.abort();
|
|
2470
|
+
opts.signal?.addEventListener("abort", cancel, { once: true });
|
|
2471
|
+
let resp;
|
|
2472
|
+
try {
|
|
2473
|
+
resp = await fetch(url, {
|
|
2474
|
+
headers: { "User-Agent": USER_AGENT, Accept: "text/html,text/plain,*/*" },
|
|
2475
|
+
signal: ctl.signal,
|
|
2476
|
+
redirect: "follow"
|
|
2477
|
+
});
|
|
2478
|
+
} finally {
|
|
2479
|
+
clearTimeout(timer);
|
|
2480
|
+
opts.signal?.removeEventListener("abort", cancel);
|
|
2481
|
+
}
|
|
2482
|
+
if (!resp.ok) throw new Error(`web_fetch ${resp.status} for ${url}`);
|
|
2483
|
+
const contentType = resp.headers.get("content-type") ?? "";
|
|
2484
|
+
const raw = await resp.text();
|
|
2485
|
+
const title = extractTitle(raw);
|
|
2486
|
+
const text = contentType.includes("text/html") ? htmlToText(raw) : raw;
|
|
2487
|
+
const truncated = text.length > maxChars;
|
|
2488
|
+
const finalText = truncated ? `${text.slice(0, maxChars)}
|
|
2489
|
+
|
|
2490
|
+
[\u2026 truncated ${text.length - maxChars} chars \u2026]` : text;
|
|
2491
|
+
return { url, title, text: finalText, truncated };
|
|
2492
|
+
}
|
|
2493
|
+
function htmlToText(html) {
|
|
2494
|
+
let s = html;
|
|
2495
|
+
s = s.replace(/<script[\s\S]*?<\/script>/gi, "");
|
|
2496
|
+
s = s.replace(/<style[\s\S]*?<\/style>/gi, "");
|
|
2497
|
+
s = s.replace(/<noscript[\s\S]*?<\/noscript>/gi, "");
|
|
2498
|
+
s = s.replace(/<nav[\s\S]*?<\/nav>/gi, "");
|
|
2499
|
+
s = s.replace(/<footer[\s\S]*?<\/footer>/gi, "");
|
|
2500
|
+
s = s.replace(/<aside[\s\S]*?<\/aside>/gi, "");
|
|
2501
|
+
s = s.replace(/<svg[\s\S]*?<\/svg>/gi, "");
|
|
2502
|
+
s = s.replace(/<\/?(p|div|br|h[1-6]|li|tr|section|article)\b[^>]*>/gi, "\n");
|
|
2503
|
+
s = s.replace(/<[^>]+>/g, "");
|
|
2504
|
+
s = decodeHtmlEntities(s);
|
|
2505
|
+
s = s.replace(/[ \t]+/g, " ");
|
|
2506
|
+
s = s.replace(/\n[ \t]+/g, "\n");
|
|
2507
|
+
s = s.replace(/\n{3,}/g, "\n\n");
|
|
2508
|
+
return s.trim();
|
|
2509
|
+
}
|
|
2510
|
+
function stripHtml(s) {
|
|
2511
|
+
return s.replace(/<[^>]+>/g, "");
|
|
2512
|
+
}
|
|
2513
|
+
function decodeHtmlEntities(s) {
|
|
2514
|
+
return s.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
2515
|
+
}
|
|
2516
|
+
function extractTitle(html) {
|
|
2517
|
+
const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
2518
|
+
if (!m?.[1]) return void 0;
|
|
2519
|
+
return m[1].replace(/\s+/g, " ").trim() || void 0;
|
|
2520
|
+
}
|
|
2521
|
+
function registerWebTools(registry, opts = {}) {
|
|
2522
|
+
const defaultTopK = opts.defaultTopK ?? DEFAULT_TOPK;
|
|
2523
|
+
const maxFetchChars = opts.maxFetchChars ?? DEFAULT_FETCH_MAX_CHARS;
|
|
2524
|
+
registry.register({
|
|
2525
|
+
name: "web_search",
|
|
2526
|
+
description: "Search the public web. Returns ranked results with title, url, and snippet. Use this when the question needs information more current than your training data, when you're unsure of a factual detail, or when the user asks about a specific webpage/library/release you haven't seen.",
|
|
2527
|
+
parameters: {
|
|
2528
|
+
type: "object",
|
|
2529
|
+
properties: {
|
|
2530
|
+
query: { type: "string", description: "Natural-language search query." },
|
|
2531
|
+
topK: {
|
|
2532
|
+
type: "integer",
|
|
2533
|
+
description: `Number of results to return (1..10). Default ${defaultTopK}.`
|
|
2534
|
+
}
|
|
2535
|
+
},
|
|
2536
|
+
required: ["query"]
|
|
2537
|
+
},
|
|
2538
|
+
fn: async (args, ctx) => {
|
|
2539
|
+
const results = await webSearch(args.query, {
|
|
2540
|
+
topK: args.topK ?? defaultTopK,
|
|
2541
|
+
signal: ctx?.signal
|
|
2542
|
+
});
|
|
2543
|
+
return formatSearchResults(args.query, results);
|
|
2544
|
+
}
|
|
2545
|
+
});
|
|
2546
|
+
registry.register({
|
|
2547
|
+
name: "web_fetch",
|
|
2548
|
+
description: "Download a URL and return its visible text content (HTML pages get scripts/styles/nav stripped). Truncated at the tool-result cap. Use after web_search when a snippet isn't enough.",
|
|
2549
|
+
parameters: {
|
|
2550
|
+
type: "object",
|
|
2551
|
+
properties: {
|
|
2552
|
+
url: { type: "string", description: "Absolute http:// or https:// URL." }
|
|
2553
|
+
},
|
|
2554
|
+
required: ["url"]
|
|
2555
|
+
},
|
|
2556
|
+
fn: async (args, ctx) => {
|
|
2557
|
+
if (!/^https?:\/\//i.test(args.url)) {
|
|
2558
|
+
throw new Error("web_fetch: url must start with http:// or https://");
|
|
2559
|
+
}
|
|
2560
|
+
const page = await webFetch(args.url, { maxChars: maxFetchChars, signal: ctx?.signal });
|
|
2561
|
+
const header = page.title ? `${page.title}
|
|
2562
|
+
${page.url}` : page.url;
|
|
2563
|
+
return `${header}
|
|
2564
|
+
|
|
2565
|
+
${page.text}`;
|
|
2566
|
+
}
|
|
2567
|
+
});
|
|
2568
|
+
return registry;
|
|
2569
|
+
}
|
|
2570
|
+
function formatSearchResults(query, results) {
|
|
2571
|
+
const lines = [`query: ${query}`, `
|
|
2572
|
+
results (${results.length}):`];
|
|
2573
|
+
results.forEach((r, i) => {
|
|
2574
|
+
lines.push(`
|
|
2575
|
+
${i + 1}. ${r.title}`);
|
|
2576
|
+
lines.push(` ${r.url}`);
|
|
2577
|
+
if (r.snippet) lines.push(` ${r.snippet}`);
|
|
2578
|
+
});
|
|
2579
|
+
return lines.join("\n");
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2186
2582
|
// src/env.ts
|
|
2187
2583
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2188
|
-
import { resolve as
|
|
2584
|
+
import { resolve as resolve3 } from "path";
|
|
2189
2585
|
function loadDotenv(path = ".env") {
|
|
2190
2586
|
let raw;
|
|
2191
2587
|
try {
|
|
2192
|
-
raw = readFileSync2(
|
|
2588
|
+
raw = readFileSync2(resolve3(process.cwd(), path), "utf8");
|
|
2193
2589
|
} catch {
|
|
2194
2590
|
return;
|
|
2195
2591
|
}
|
|
@@ -2871,7 +3267,7 @@ var McpClient = class {
|
|
|
2871
3267
|
const id = this.nextId++;
|
|
2872
3268
|
const frame = { jsonrpc: "2.0", id, method, params };
|
|
2873
3269
|
let abortHandler = null;
|
|
2874
|
-
const promise = new Promise((
|
|
3270
|
+
const promise = new Promise((resolve5, reject) => {
|
|
2875
3271
|
const timeout = setTimeout(() => {
|
|
2876
3272
|
this.pending.delete(id);
|
|
2877
3273
|
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
@@ -2880,7 +3276,7 @@ var McpClient = class {
|
|
|
2880
3276
|
);
|
|
2881
3277
|
}, this.requestTimeoutMs);
|
|
2882
3278
|
this.pending.set(id, {
|
|
2883
|
-
resolve:
|
|
3279
|
+
resolve: resolve5,
|
|
2884
3280
|
reject,
|
|
2885
3281
|
timeout
|
|
2886
3282
|
});
|
|
@@ -2962,7 +3358,7 @@ var McpClient = class {
|
|
|
2962
3358
|
};
|
|
2963
3359
|
|
|
2964
3360
|
// src/mcp/stdio.ts
|
|
2965
|
-
import { spawn } from "child_process";
|
|
3361
|
+
import { spawn as spawn2 } from "child_process";
|
|
2966
3362
|
var StdioTransport = class {
|
|
2967
3363
|
child;
|
|
2968
3364
|
queue = [];
|
|
@@ -2977,14 +3373,14 @@ var StdioTransport = class {
|
|
|
2977
3373
|
opts.command,
|
|
2978
3374
|
...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
|
|
2979
3375
|
].join(" ");
|
|
2980
|
-
this.child =
|
|
3376
|
+
this.child = spawn2(line, [], {
|
|
2981
3377
|
env,
|
|
2982
3378
|
cwd: opts.cwd,
|
|
2983
3379
|
stdio: ["pipe", "pipe", "inherit"],
|
|
2984
3380
|
shell: true
|
|
2985
3381
|
});
|
|
2986
3382
|
} else {
|
|
2987
|
-
this.child =
|
|
3383
|
+
this.child = spawn2(opts.command, opts.args ?? [], {
|
|
2988
3384
|
env,
|
|
2989
3385
|
cwd: opts.cwd,
|
|
2990
3386
|
stdio: ["pipe", "pipe", "inherit"]
|
|
@@ -3003,12 +3399,12 @@ var StdioTransport = class {
|
|
|
3003
3399
|
}
|
|
3004
3400
|
async send(message) {
|
|
3005
3401
|
if (this.closed) throw new Error("MCP transport is closed");
|
|
3006
|
-
return new Promise((
|
|
3402
|
+
return new Promise((resolve5, reject) => {
|
|
3007
3403
|
const line = `${JSON.stringify(message)}
|
|
3008
3404
|
`;
|
|
3009
3405
|
this.child.stdin.write(line, "utf8", (err) => {
|
|
3010
3406
|
if (err) reject(err);
|
|
3011
|
-
else
|
|
3407
|
+
else resolve5();
|
|
3012
3408
|
});
|
|
3013
3409
|
});
|
|
3014
3410
|
}
|
|
@@ -3019,8 +3415,8 @@ var StdioTransport = class {
|
|
|
3019
3415
|
continue;
|
|
3020
3416
|
}
|
|
3021
3417
|
if (this.closed) return;
|
|
3022
|
-
const next = await new Promise((
|
|
3023
|
-
this.waiters.push(
|
|
3418
|
+
const next = await new Promise((resolve5) => {
|
|
3419
|
+
this.waiters.push(resolve5);
|
|
3024
3420
|
});
|
|
3025
3421
|
if (next === null) return;
|
|
3026
3422
|
yield next;
|
|
@@ -3086,8 +3482,8 @@ var SseTransport = class {
|
|
|
3086
3482
|
constructor(opts) {
|
|
3087
3483
|
this.url = opts.url;
|
|
3088
3484
|
this.headers = opts.headers ?? {};
|
|
3089
|
-
this.endpointReady = new Promise((
|
|
3090
|
-
this.resolveEndpoint =
|
|
3485
|
+
this.endpointReady = new Promise((resolve5, reject) => {
|
|
3486
|
+
this.resolveEndpoint = resolve5;
|
|
3091
3487
|
this.rejectEndpoint = reject;
|
|
3092
3488
|
});
|
|
3093
3489
|
this.endpointReady.catch(() => void 0);
|
|
@@ -3114,8 +3510,8 @@ var SseTransport = class {
|
|
|
3114
3510
|
continue;
|
|
3115
3511
|
}
|
|
3116
3512
|
if (this.closed) return;
|
|
3117
|
-
const next = await new Promise((
|
|
3118
|
-
this.waiters.push(
|
|
3513
|
+
const next = await new Promise((resolve5) => {
|
|
3514
|
+
this.waiters.push(resolve5);
|
|
3119
3515
|
});
|
|
3120
3516
|
if (next === null) return;
|
|
3121
3517
|
yield next;
|
|
@@ -3315,7 +3711,7 @@ async function trySection(load) {
|
|
|
3315
3711
|
|
|
3316
3712
|
// src/code/edit-blocks.ts
|
|
3317
3713
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3318
|
-
import { dirname as dirname3, resolve as
|
|
3714
|
+
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
3319
3715
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
3320
3716
|
function parseEditBlocks(text) {
|
|
3321
3717
|
const out = [];
|
|
@@ -3333,8 +3729,8 @@ function parseEditBlocks(text) {
|
|
|
3333
3729
|
return out;
|
|
3334
3730
|
}
|
|
3335
3731
|
function applyEditBlock(block, rootDir) {
|
|
3336
|
-
const absRoot =
|
|
3337
|
-
const absTarget =
|
|
3732
|
+
const absRoot = resolve4(rootDir);
|
|
3733
|
+
const absTarget = resolve4(absRoot, block.path);
|
|
3338
3734
|
if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
|
|
3339
3735
|
return {
|
|
3340
3736
|
path: block.path,
|
|
@@ -3384,13 +3780,13 @@ function applyEditBlocks(blocks, rootDir) {
|
|
|
3384
3780
|
return blocks.map((b) => applyEditBlock(b, rootDir));
|
|
3385
3781
|
}
|
|
3386
3782
|
function snapshotBeforeEdits(blocks, rootDir) {
|
|
3387
|
-
const absRoot =
|
|
3783
|
+
const absRoot = resolve4(rootDir);
|
|
3388
3784
|
const seen = /* @__PURE__ */ new Set();
|
|
3389
3785
|
const snapshots = [];
|
|
3390
3786
|
for (const b of blocks) {
|
|
3391
3787
|
if (seen.has(b.path)) continue;
|
|
3392
3788
|
seen.add(b.path);
|
|
3393
|
-
const abs =
|
|
3789
|
+
const abs = resolve4(absRoot, b.path);
|
|
3394
3790
|
if (!existsSync2(abs)) {
|
|
3395
3791
|
snapshots.push({ path: b.path, prevContent: null });
|
|
3396
3792
|
continue;
|
|
@@ -3404,9 +3800,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
3404
3800
|
return snapshots;
|
|
3405
3801
|
}
|
|
3406
3802
|
function restoreSnapshots(snapshots, rootDir) {
|
|
3407
|
-
const absRoot =
|
|
3803
|
+
const absRoot = resolve4(rootDir);
|
|
3408
3804
|
return snapshots.map((snap) => {
|
|
3409
|
-
const abs =
|
|
3805
|
+
const abs = resolve4(absRoot, snap.path);
|
|
3410
3806
|
if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
|
|
3411
3807
|
return {
|
|
3412
3808
|
path: snap.path,
|
|
@@ -3556,7 +3952,7 @@ function redactKey(key) {
|
|
|
3556
3952
|
}
|
|
3557
3953
|
|
|
3558
3954
|
// src/index.ts
|
|
3559
|
-
var VERSION = "0.4.
|
|
3955
|
+
var VERSION = "0.4.16";
|
|
3560
3956
|
export {
|
|
3561
3957
|
AppendOnlyLog,
|
|
3562
3958
|
CODE_SYSTEM_PROMPT,
|
|
@@ -3566,6 +3962,7 @@ export {
|
|
|
3566
3962
|
ImmutablePrefix,
|
|
3567
3963
|
MCP_PROTOCOL_VERSION,
|
|
3568
3964
|
McpClient,
|
|
3965
|
+
NeedsConfirmationError,
|
|
3569
3966
|
SessionStats,
|
|
3570
3967
|
SseTransport,
|
|
3571
3968
|
StdioTransport,
|
|
@@ -3593,11 +3990,15 @@ export {
|
|
|
3593
3990
|
fetchWithRetry,
|
|
3594
3991
|
flattenMcpResult,
|
|
3595
3992
|
flattenSchema,
|
|
3993
|
+
formatCommandResult,
|
|
3596
3994
|
formatLoopError,
|
|
3995
|
+
formatSearchResults,
|
|
3597
3996
|
harvest,
|
|
3598
3997
|
healLoadedMessages,
|
|
3998
|
+
htmlToText,
|
|
3599
3999
|
inputCostUsd,
|
|
3600
4000
|
inspectMcpServer,
|
|
4001
|
+
isAllowed,
|
|
3601
4002
|
isJsonRpcError,
|
|
3602
4003
|
isPlanStateEmpty,
|
|
3603
4004
|
isPlausibleKey,
|
|
@@ -3610,18 +4011,22 @@ export {
|
|
|
3610
4011
|
outputCostUsd,
|
|
3611
4012
|
parseEditBlocks,
|
|
3612
4013
|
parseMcpSpec,
|
|
4014
|
+
parseMojeekResults,
|
|
3613
4015
|
parseTranscript,
|
|
3614
4016
|
readConfig,
|
|
3615
4017
|
readTranscript,
|
|
3616
4018
|
recordFromLoopEvent,
|
|
3617
4019
|
redactKey,
|
|
3618
4020
|
registerFilesystemTools,
|
|
4021
|
+
registerShellTools,
|
|
4022
|
+
registerWebTools,
|
|
3619
4023
|
renderMarkdown as renderDiffMarkdown,
|
|
3620
4024
|
renderSummaryTable as renderDiffSummary,
|
|
3621
4025
|
repairTruncatedJson,
|
|
3622
4026
|
replayFromFile,
|
|
3623
4027
|
restoreSnapshots,
|
|
3624
4028
|
runBranches,
|
|
4029
|
+
runCommand,
|
|
3625
4030
|
sanitizeName as sanitizeSessionName,
|
|
3626
4031
|
saveApiKey,
|
|
3627
4032
|
scavengeToolCalls,
|
|
@@ -3630,7 +4035,10 @@ export {
|
|
|
3630
4035
|
similarity,
|
|
3631
4036
|
snapshotBeforeEdits,
|
|
3632
4037
|
stripHallucinatedToolMarkup,
|
|
4038
|
+
tokenizeCommand,
|
|
3633
4039
|
truncateForModel,
|
|
4040
|
+
webFetch,
|
|
4041
|
+
webSearch,
|
|
3634
4042
|
writeConfig,
|
|
3635
4043
|
writeMeta,
|
|
3636
4044
|
writeRecord
|