clixad 0.0.1-beta.13 → 0.0.1-beta.15
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/clixad.mjs +275 -102
- package/package.json +1 -1
package/dist/clixad.mjs
CHANGED
|
@@ -604,7 +604,12 @@ var init_client = __esm({
|
|
|
604
604
|
const calls = /* @__PURE__ */ new Map();
|
|
605
605
|
for await (const evt of parseSSE(res.body)) {
|
|
606
606
|
if (evt === "[DONE]") break;
|
|
607
|
-
|
|
607
|
+
let json;
|
|
608
|
+
try {
|
|
609
|
+
json = JSON.parse(evt);
|
|
610
|
+
} catch {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
608
613
|
const choice = json.choices?.[0];
|
|
609
614
|
const delta = choice?.delta?.content;
|
|
610
615
|
if (delta) {
|
|
@@ -1239,12 +1244,13 @@ function installExitHook(registry) {
|
|
|
1239
1244
|
});
|
|
1240
1245
|
}
|
|
1241
1246
|
}
|
|
1242
|
-
var MAX_SHELL_BUFFER, MAX_SHELLS, ShellError, ShellRegistry, shells, hookInstalled;
|
|
1247
|
+
var MAX_SHELL_BUFFER, MAX_SHELLS, MAX_FINISHED_SHELLS, ShellError, ShellRegistry, shells, hookInstalled;
|
|
1243
1248
|
var init_shells = __esm({
|
|
1244
1249
|
"src/shells.ts"() {
|
|
1245
1250
|
"use strict";
|
|
1246
1251
|
MAX_SHELL_BUFFER = 2e5;
|
|
1247
1252
|
MAX_SHELLS = 8;
|
|
1253
|
+
MAX_FINISHED_SHELLS = 8;
|
|
1248
1254
|
ShellError = class extends Error {
|
|
1249
1255
|
};
|
|
1250
1256
|
ShellRegistry = class {
|
|
@@ -1252,21 +1258,27 @@ var init_shells = __esm({
|
|
|
1252
1258
|
seq = 0;
|
|
1253
1259
|
/** Start a command in the background and return it before it has finished. */
|
|
1254
1260
|
start(root, command, now = Date.now()) {
|
|
1261
|
+
this.pruneFinished();
|
|
1255
1262
|
const live = [...this.entries.values()].filter((e) => e.status === "running");
|
|
1256
1263
|
if (live.length >= MAX_SHELLS) {
|
|
1257
1264
|
throw new ShellError(
|
|
1258
1265
|
`${MAX_SHELLS} background commands are already running (${live.map((e) => e.id).join(", ")}) \u2014 stop one with kill_shell before starting another`
|
|
1259
1266
|
);
|
|
1260
1267
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1268
|
+
let child;
|
|
1269
|
+
try {
|
|
1270
|
+
child = spawn2(command, {
|
|
1271
|
+
cwd: root,
|
|
1272
|
+
shell: true,
|
|
1273
|
+
windowsHide: true,
|
|
1274
|
+
detached: process.platform !== "win32",
|
|
1275
|
+
// Nothing types at a background command, and leaving stdin inherited
|
|
1276
|
+
// lets it steal the keystrokes meant for the REPL.
|
|
1277
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1278
|
+
});
|
|
1279
|
+
} catch (err) {
|
|
1280
|
+
throw new ShellError(`could not start that command: ${err.message}`);
|
|
1281
|
+
}
|
|
1270
1282
|
const entry = {
|
|
1271
1283
|
id: `bash_${++this.seq}`,
|
|
1272
1284
|
command,
|
|
@@ -1339,10 +1351,23 @@ var init_shells = __esm({
|
|
|
1339
1351
|
list(now = Date.now()) {
|
|
1340
1352
|
return [...this.entries.values()].map((e) => snapshot(e, now));
|
|
1341
1353
|
}
|
|
1342
|
-
/**
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1354
|
+
/**
|
|
1355
|
+
* Forget all but the most recent `MAX_FINISHED_SHELLS` finished commands.
|
|
1356
|
+
*
|
|
1357
|
+
* Run when a new one starts, which is the only moment the registry grows.
|
|
1358
|
+
* Running commands are never touched — those are `killAll`'s and
|
|
1359
|
+
* `kill`'s — and the Map's insertion order is the start order, so "oldest"
|
|
1360
|
+
* needs no timestamp.
|
|
1361
|
+
*
|
|
1362
|
+
* This replaces a `forget(id)` that nothing ever called: an explicit
|
|
1363
|
+
* single-entry drop is only useful to a caller who knows a shell is finished
|
|
1364
|
+
* with, and neither the tools nor the REPL is in a position to know that.
|
|
1365
|
+
*/
|
|
1366
|
+
pruneFinished() {
|
|
1367
|
+
const finished = [...this.entries.values()].filter((e) => e.status !== "running");
|
|
1368
|
+
for (const entry of finished.slice(0, Math.max(0, finished.length - MAX_FINISHED_SHELLS))) {
|
|
1369
|
+
this.entries.delete(entry.id);
|
|
1370
|
+
}
|
|
1346
1371
|
}
|
|
1347
1372
|
/** Stop everything. Called on exit; safe to call more than once. */
|
|
1348
1373
|
killAll() {
|
|
@@ -2189,6 +2214,64 @@ function passesThrough(err) {
|
|
|
2189
2214
|
if (err instanceof PaywallError) return true;
|
|
2190
2215
|
return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
2191
2216
|
}
|
|
2217
|
+
function fsCall(what, run) {
|
|
2218
|
+
try {
|
|
2219
|
+
return run();
|
|
2220
|
+
} catch (err) {
|
|
2221
|
+
if (passesThrough(err)) throw err;
|
|
2222
|
+
const code = err.code ?? "";
|
|
2223
|
+
throw new ToolError(`${what}: ${FS_REASON[code] ?? err.message}`);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
function countArg(raw, fallback) {
|
|
2227
|
+
const n = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : Number.NaN;
|
|
2228
|
+
return Number.isFinite(n) ? Math.max(1, Math.floor(n)) : fallback;
|
|
2229
|
+
}
|
|
2230
|
+
function requireString(value, tool, field) {
|
|
2231
|
+
const text = readString(value);
|
|
2232
|
+
if (text === void 0) {
|
|
2233
|
+
throw new ToolError(`${tool}: ${field} must be a string, got ${describeValue(value)}`);
|
|
2234
|
+
}
|
|
2235
|
+
return text;
|
|
2236
|
+
}
|
|
2237
|
+
function optionalString(value, tool, field) {
|
|
2238
|
+
if (value === void 0 || value === null) return void 0;
|
|
2239
|
+
return requireString(value, tool, field);
|
|
2240
|
+
}
|
|
2241
|
+
function readString(value) {
|
|
2242
|
+
if (typeof value === "string") return value;
|
|
2243
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : void 0;
|
|
2244
|
+
}
|
|
2245
|
+
function describeValue(value) {
|
|
2246
|
+
if (value === null) return "null";
|
|
2247
|
+
if (value === void 0) return "nothing";
|
|
2248
|
+
if (Array.isArray(value)) return "an array";
|
|
2249
|
+
if (typeof value === "number") return "an unusable number";
|
|
2250
|
+
return typeof value === "object" ? "an object" : `a ${typeof value}`;
|
|
2251
|
+
}
|
|
2252
|
+
function entryKind(entry, abs) {
|
|
2253
|
+
if (entry.isDirectory()) return "dir";
|
|
2254
|
+
if (entry.isFile()) return "file";
|
|
2255
|
+
if (!entry.isSymbolicLink()) return "other";
|
|
2256
|
+
try {
|
|
2257
|
+
const stat = statSync(abs);
|
|
2258
|
+
return stat.isDirectory() ? "dir" : stat.isFile() ? "file" : "other";
|
|
2259
|
+
} catch {
|
|
2260
|
+
return "other";
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
function isFilePath(abs) {
|
|
2264
|
+
try {
|
|
2265
|
+
return statSync(abs).isFile();
|
|
2266
|
+
} catch {
|
|
2267
|
+
return false;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
function searchBase(ctx, path) {
|
|
2271
|
+
const abs = safeResolve(ctx.root, path ?? ".");
|
|
2272
|
+
if (!existsSync2(abs)) throw new ToolError(`not found: ${path ?? "."}`);
|
|
2273
|
+
return abs;
|
|
2274
|
+
}
|
|
2192
2275
|
function commandSignature(command) {
|
|
2193
2276
|
return `run:${command.replace(/\s+/g, " ").trim()}`;
|
|
2194
2277
|
}
|
|
@@ -2299,13 +2382,19 @@ async function runMcpTool(ctx, name, args) {
|
|
|
2299
2382
|
}
|
|
2300
2383
|
function execute(ctx, command, timeout) {
|
|
2301
2384
|
return new Promise((resolvePromise) => {
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2385
|
+
let child;
|
|
2386
|
+
try {
|
|
2387
|
+
child = spawn4(command, {
|
|
2388
|
+
cwd: ctx.root,
|
|
2389
|
+
shell: true,
|
|
2390
|
+
windowsHide: true,
|
|
2391
|
+
// Own process group on POSIX so killTree can take the children with it.
|
|
2392
|
+
detached: process.platform !== "win32"
|
|
2393
|
+
});
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
return resolvePromise(`command failed to start: ${err.message}
|
|
2396
|
+
(no output)`);
|
|
2397
|
+
}
|
|
2309
2398
|
let out = "";
|
|
2310
2399
|
let total = 0;
|
|
2311
2400
|
let timedOut = false;
|
|
@@ -2352,6 +2441,10 @@ function listWorkspaceFiles(root, limit = 5e3) {
|
|
|
2352
2441
|
return files.sort();
|
|
2353
2442
|
}
|
|
2354
2443
|
function walk(dir, root, visit) {
|
|
2444
|
+
if (isFilePath(dir)) {
|
|
2445
|
+
visit(dir);
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2355
2448
|
let seen = 0;
|
|
2356
2449
|
const stack = [dir];
|
|
2357
2450
|
while (stack.length > 0) {
|
|
@@ -2364,12 +2457,13 @@ function walk(dir, root, visit) {
|
|
|
2364
2457
|
}
|
|
2365
2458
|
for (const entry of entries) {
|
|
2366
2459
|
const abs = join3(current, entry.name);
|
|
2367
|
-
|
|
2368
|
-
|
|
2460
|
+
const kind = entryKind(entry, abs);
|
|
2461
|
+
if (kind === "dir") {
|
|
2462
|
+
if (entry.isSymbolicLink() || SKIP_DIRS.has(entry.name)) continue;
|
|
2369
2463
|
stack.push(abs);
|
|
2370
2464
|
continue;
|
|
2371
2465
|
}
|
|
2372
|
-
if (
|
|
2466
|
+
if (kind !== "file") continue;
|
|
2373
2467
|
if (++seen > MAX_WALK_FILES) return;
|
|
2374
2468
|
if (!visit(abs)) return;
|
|
2375
2469
|
}
|
|
@@ -2377,8 +2471,9 @@ function walk(dir, root, visit) {
|
|
|
2377
2471
|
void root;
|
|
2378
2472
|
}
|
|
2379
2473
|
function globToRegExp(pattern) {
|
|
2380
|
-
const p = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2474
|
+
const p = requireString(pattern, "glob", "pattern").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2381
2475
|
let re = "";
|
|
2476
|
+
let depth = 0;
|
|
2382
2477
|
for (let i = 0; i < p.length; i++) {
|
|
2383
2478
|
const ch = p[i];
|
|
2384
2479
|
if (ch === "*") {
|
|
@@ -2394,14 +2489,23 @@ function globToRegExp(pattern) {
|
|
|
2394
2489
|
re += "[^/]*";
|
|
2395
2490
|
}
|
|
2396
2491
|
} else if (ch === "?") re += "[^/]";
|
|
2397
|
-
else if (ch === "{")
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
else if (
|
|
2492
|
+
else if (ch === "{") {
|
|
2493
|
+
re += "(?:";
|
|
2494
|
+
depth++;
|
|
2495
|
+
} else if (ch === "}" && depth > 0) {
|
|
2496
|
+
re += ")";
|
|
2497
|
+
depth--;
|
|
2498
|
+
} else if (ch === "," && depth > 0) re += "|";
|
|
2499
|
+
else if (".+^$()[]|\\{}".includes(ch)) re += "\\" + ch;
|
|
2401
2500
|
else re += ch;
|
|
2402
2501
|
}
|
|
2502
|
+
re += ")".repeat(depth);
|
|
2403
2503
|
const anchored = p.includes("/") ? `^${re}$` : `^(?:.*/)?${re}$`;
|
|
2404
|
-
|
|
2504
|
+
try {
|
|
2505
|
+
return new RegExp(anchored);
|
|
2506
|
+
} catch (err) {
|
|
2507
|
+
throw new ToolError(`not a usable glob pattern: ${pattern} \u2014 ${err.message}`);
|
|
2508
|
+
}
|
|
2405
2509
|
}
|
|
2406
2510
|
function toolSchemaFor(mode, mcpSchemas = [], available = {}) {
|
|
2407
2511
|
const usable = (t) => t.function.name !== "web_search" || available.webSearch === true;
|
|
@@ -2416,7 +2520,7 @@ function subagentToolSchema(available = {}) {
|
|
|
2416
2520
|
(t) => SUBAGENT_TOOLS.includes(t.function.name) && (t.function.name !== "web_search" || available.webSearch === true)
|
|
2417
2521
|
);
|
|
2418
2522
|
}
|
|
2419
|
-
var ToolError, PermissionDeniedError, MAX_FILE_BYTES, MAX_READ_LINES, MAX_READ_LINE_CHARS, MAX_OUTPUT_CHARS, MAX_GLOB_RESULTS, MAX_GREP_MATCHES, MAX_WALK_FILES, DEFAULT_COMMAND_TIMEOUT, MAX_IMAGE_BYTES, DEFAULT_SEARCH_RESULTS, MAX_SEARCH_RESULTS, MAX_ASK_OPTIONS, BACKGROUND_SETTLE_MS, SKIP_DIRS, TOOLS, READ_ONLY_TOOLS, EXIT_PLAN_MODE_SCHEMA, SUBAGENT_TOOLS, TOOL_SCHEMA;
|
|
2523
|
+
var ToolError, PermissionDeniedError, FS_REASON, MAX_FILE_BYTES, MAX_READ_LINES, MAX_READ_LINE_CHARS, MAX_OUTPUT_CHARS, MAX_GLOB_RESULTS, MAX_GREP_MATCHES, MAX_WALK_FILES, DEFAULT_COMMAND_TIMEOUT, MAX_IMAGE_BYTES, DEFAULT_SEARCH_RESULTS, MAX_SEARCH_RESULTS, MAX_ASK_OPTIONS, BACKGROUND_SETTLE_MS, SKIP_DIRS, TOOLS, READ_ONLY_TOOLS, EXIT_PLAN_MODE_SCHEMA, SUBAGENT_TOOLS, TOOL_SCHEMA;
|
|
2420
2524
|
var init_tools = __esm({
|
|
2421
2525
|
"src/tools.ts"() {
|
|
2422
2526
|
"use strict";
|
|
@@ -2436,6 +2540,19 @@ var init_tools = __esm({
|
|
|
2436
2540
|
this.notice = notice;
|
|
2437
2541
|
}
|
|
2438
2542
|
};
|
|
2543
|
+
FS_REASON = {
|
|
2544
|
+
ENOTDIR: "it is a file, not a directory \u2014 use read_file",
|
|
2545
|
+
EISDIR: "it is a directory, not a file \u2014 use list_dir, or edit a file inside it",
|
|
2546
|
+
ENOENT: "it does not exist (a symlink pointing at nothing looks like this too)",
|
|
2547
|
+
EACCES: "permission denied",
|
|
2548
|
+
EPERM: "permission denied",
|
|
2549
|
+
ELOOP: "the path goes through a symlink loop",
|
|
2550
|
+
ENAMETOOLONG: "the path is too long",
|
|
2551
|
+
EMFILE: "this process has too many files open \u2014 try again",
|
|
2552
|
+
ENFILE: "the system has too many files open \u2014 try again",
|
|
2553
|
+
ENOSPC: "the disk is full",
|
|
2554
|
+
EROFS: "the filesystem is read-only"
|
|
2555
|
+
};
|
|
2439
2556
|
MAX_FILE_BYTES = 4e5;
|
|
2440
2557
|
MAX_READ_LINES = 2e3;
|
|
2441
2558
|
MAX_READ_LINE_CHARS = 2e3;
|
|
@@ -2450,16 +2567,37 @@ var init_tools = __esm({
|
|
|
2450
2567
|
MAX_ASK_OPTIONS = 6;
|
|
2451
2568
|
BACKGROUND_SETTLE_MS = 500;
|
|
2452
2569
|
SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2570
|
+
// Version control.
|
|
2453
2571
|
".git",
|
|
2572
|
+
".hg",
|
|
2573
|
+
".svn",
|
|
2574
|
+
// Dependencies and their caches.
|
|
2454
2575
|
"node_modules",
|
|
2576
|
+
".yarn",
|
|
2577
|
+
".pnpm-store",
|
|
2578
|
+
".venv",
|
|
2579
|
+
".tox",
|
|
2580
|
+
// Build output.
|
|
2455
2581
|
"dist",
|
|
2456
2582
|
"build",
|
|
2457
2583
|
"coverage",
|
|
2458
2584
|
".next",
|
|
2585
|
+
".nuxt",
|
|
2586
|
+
".svelte-kit",
|
|
2587
|
+
".output",
|
|
2588
|
+
".angular",
|
|
2589
|
+
".dart_tool",
|
|
2590
|
+
// Tool caches.
|
|
2459
2591
|
".turbo",
|
|
2592
|
+
".nx",
|
|
2460
2593
|
".cache",
|
|
2594
|
+
".parcel-cache",
|
|
2595
|
+
".gradle",
|
|
2461
2596
|
"__pycache__",
|
|
2462
|
-
".
|
|
2597
|
+
".mypy_cache",
|
|
2598
|
+
".pytest_cache",
|
|
2599
|
+
".ruff_cache",
|
|
2600
|
+
".terraform"
|
|
2463
2601
|
]);
|
|
2464
2602
|
TOOLS = {
|
|
2465
2603
|
/**
|
|
@@ -2478,11 +2616,14 @@ var init_tools = __esm({
|
|
|
2478
2616
|
* is told precisely what went wrong instead of guessing.
|
|
2479
2617
|
*/
|
|
2480
2618
|
read_file(ctx, args) {
|
|
2481
|
-
const
|
|
2482
|
-
|
|
2483
|
-
if (
|
|
2484
|
-
const bytes = readFileSync3(abs);
|
|
2619
|
+
const path = requireString(args.path, "read_file", "path");
|
|
2620
|
+
const abs = safeResolve(ctx.root, path);
|
|
2621
|
+
if (!existsSync2(abs)) throw new ToolError(`not found: ${path}`);
|
|
2485
2622
|
const name = rel(ctx.root, abs);
|
|
2623
|
+
if (fsCall(`cannot read ${name}`, () => statSync(abs)).isDirectory()) {
|
|
2624
|
+
throw new ToolError(`${path} is a directory \u2014 use list_dir`);
|
|
2625
|
+
}
|
|
2626
|
+
const bytes = fsCall(`cannot read ${name}`, () => readFileSync3(abs));
|
|
2486
2627
|
const image = imageType(bytes);
|
|
2487
2628
|
if (image) return readImage(ctx, bytes, name, image);
|
|
2488
2629
|
if (looksLikePdf(bytes)) {
|
|
@@ -2503,11 +2644,11 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
2503
2644
|
const raw = bytes.toString("utf8");
|
|
2504
2645
|
const all = raw.split("\n");
|
|
2505
2646
|
const total = all.length > 1 && all[all.length - 1] === "" ? all.length - 1 : all.length;
|
|
2506
|
-
const first =
|
|
2507
|
-
const want =
|
|
2647
|
+
const first = countArg(args.offset, 1);
|
|
2648
|
+
const want = countArg(args.limit, MAX_READ_LINES);
|
|
2508
2649
|
const last = Math.min(total, first + want - 1);
|
|
2509
2650
|
if (first > total) {
|
|
2510
|
-
return `${
|
|
2651
|
+
return `${path} has ${total} lines; offset ${first} is past the end`;
|
|
2511
2652
|
}
|
|
2512
2653
|
const body = all.slice(first - 1, last).map((line2, i) => {
|
|
2513
2654
|
const n = String(first + i).padStart(5, " ");
|
|
@@ -2523,27 +2664,35 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
2523
2664
|
return body + note;
|
|
2524
2665
|
},
|
|
2525
2666
|
list_dir(ctx, args) {
|
|
2526
|
-
const
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2667
|
+
const path = optionalString(args.path, "list_dir", "path") ?? ".";
|
|
2668
|
+
const abs = safeResolve(ctx.root, path);
|
|
2669
|
+
if (!existsSync2(abs)) throw new ToolError(`not found: ${path}`);
|
|
2670
|
+
const name = rel(ctx.root, abs);
|
|
2671
|
+
if (!fsCall(`cannot list ${name}`, () => statSync(abs)).isDirectory()) {
|
|
2672
|
+
throw new ToolError(`${path} is a file \u2014 use read_file`);
|
|
2673
|
+
}
|
|
2674
|
+
const entries = fsCall(`cannot list ${name}`, () => readdirSync(abs, { withFileTypes: true })).map(
|
|
2675
|
+
(entry) => {
|
|
2676
|
+
const isDir = entryKind(entry, resolve(abs, entry.name)) === "dir";
|
|
2677
|
+
return { name: isDir ? `${entry.name}/` : entry.name, isDir };
|
|
2678
|
+
}
|
|
2679
|
+
);
|
|
2532
2680
|
entries.sort((a, b) => Number(b.isDir) - Number(a.isDir) || a.name.localeCompare(b.name));
|
|
2533
2681
|
return entries.map((e) => e.name).join("\n") || "(empty)";
|
|
2534
2682
|
},
|
|
2535
2683
|
/** Find files by glob, e.g. `src/**\/*.ts`. Cheap orientation before reading. */
|
|
2536
2684
|
glob(ctx, args) {
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
const
|
|
2685
|
+
const pattern = optionalString(args.pattern, "glob", "pattern");
|
|
2686
|
+
if (!pattern) throw new ToolError("glob needs a pattern");
|
|
2687
|
+
const base = searchBase(ctx, optionalString(args.path, "glob", "path"));
|
|
2688
|
+
const re = globToRegExp(pattern);
|
|
2540
2689
|
const hits = [];
|
|
2541
2690
|
walk(base, ctx.root, (abs) => {
|
|
2542
2691
|
const r = rel(ctx.root, abs);
|
|
2543
2692
|
if (re.test(r)) hits.push(r);
|
|
2544
2693
|
return hits.length < MAX_GLOB_RESULTS;
|
|
2545
2694
|
});
|
|
2546
|
-
if (hits.length === 0) return `no files match ${
|
|
2695
|
+
if (hits.length === 0) return `no files match ${pattern}`;
|
|
2547
2696
|
hits.sort();
|
|
2548
2697
|
const more = hits.length >= MAX_GLOB_RESULTS ? `
|
|
2549
2698
|
\u2026 [capped at ${MAX_GLOB_RESULTS}]` : "";
|
|
@@ -2551,15 +2700,17 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
2551
2700
|
},
|
|
2552
2701
|
/** Search file contents with a regular expression; returns path:line: text. */
|
|
2553
2702
|
grep(ctx, args) {
|
|
2554
|
-
|
|
2703
|
+
const pattern = optionalString(args.pattern, "grep", "pattern");
|
|
2704
|
+
if (!pattern) throw new ToolError("grep needs a pattern");
|
|
2555
2705
|
let re;
|
|
2556
2706
|
try {
|
|
2557
|
-
re = new RegExp(
|
|
2707
|
+
re = new RegExp(pattern);
|
|
2558
2708
|
} catch (err) {
|
|
2559
2709
|
throw new ToolError(`invalid regular expression: ${err.message}`);
|
|
2560
2710
|
}
|
|
2561
|
-
const base =
|
|
2562
|
-
const
|
|
2711
|
+
const base = searchBase(ctx, optionalString(args.path, "grep", "path"));
|
|
2712
|
+
const globArg = optionalString(args.glob, "grep", "glob");
|
|
2713
|
+
const filter = globArg ? globToRegExp(globArg) : void 0;
|
|
2563
2714
|
const out = [];
|
|
2564
2715
|
walk(base, ctx.root, (abs) => {
|
|
2565
2716
|
const r = rel(ctx.root, abs);
|
|
@@ -2580,24 +2731,28 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
2580
2731
|
}
|
|
2581
2732
|
return true;
|
|
2582
2733
|
});
|
|
2583
|
-
if (out.length === 0) return `no matches for ${
|
|
2734
|
+
if (out.length === 0) return `no matches for ${pattern}`;
|
|
2584
2735
|
const more = out.length >= MAX_GREP_MATCHES ? `
|
|
2585
2736
|
\u2026 [capped at ${MAX_GREP_MATCHES} matches]` : "";
|
|
2586
2737
|
return out.join("\n") + more;
|
|
2587
2738
|
},
|
|
2588
2739
|
async write_file(ctx, args) {
|
|
2589
|
-
const abs = safeResolve(ctx.root, args.path);
|
|
2590
|
-
const
|
|
2591
|
-
const
|
|
2740
|
+
const abs = safeResolve(ctx.root, requireString(args.path, "write_file", "path"));
|
|
2741
|
+
const content = requireString(args.content, "write_file", "content");
|
|
2742
|
+
const name = rel(ctx.root, abs);
|
|
2743
|
+
const before = existsSync2(abs) ? fsCall(`cannot write ${name}`, () => readFileSync3(abs, "utf8")) : "";
|
|
2744
|
+
const preview = renderDiff(before, content) || "(no change)";
|
|
2592
2745
|
await gate(ctx, {
|
|
2593
2746
|
tool: "write_file",
|
|
2594
|
-
signature: `write:${
|
|
2595
|
-
summary: `${before ? "overwrite" : "create"} ${
|
|
2747
|
+
signature: `write:${name}`,
|
|
2748
|
+
summary: `${before ? "overwrite" : "create"} ${name}`,
|
|
2596
2749
|
preview
|
|
2597
2750
|
});
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2751
|
+
fsCall(`cannot write ${name}`, () => {
|
|
2752
|
+
mkdirSync2(dirname2(abs), { recursive: true });
|
|
2753
|
+
writeFileSync2(abs, content, "utf8");
|
|
2754
|
+
});
|
|
2755
|
+
return `wrote ${content.length} bytes to ${name}`;
|
|
2601
2756
|
},
|
|
2602
2757
|
/**
|
|
2603
2758
|
* Replace an exact snippet in a file. Preferred over write_file: it keeps the
|
|
@@ -2605,41 +2760,42 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
2605
2760
|
* An ambiguous match is an error rather than a guess.
|
|
2606
2761
|
*/
|
|
2607
2762
|
async edit_file(ctx, args) {
|
|
2608
|
-
const
|
|
2609
|
-
|
|
2610
|
-
if (
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
if (
|
|
2614
|
-
const
|
|
2615
|
-
const
|
|
2763
|
+
const path = requireString(args.path, "edit_file", "path");
|
|
2764
|
+
const abs = safeResolve(ctx.root, path);
|
|
2765
|
+
if (!existsSync2(abs)) throw new ToolError(`not found: ${path}`);
|
|
2766
|
+
const oldString = requireString(args.old_string, "edit_file", "old_string");
|
|
2767
|
+
const newString = requireString(args.new_string, "edit_file", "new_string");
|
|
2768
|
+
if (oldString === newString) throw new ToolError("old_string and new_string are identical");
|
|
2769
|
+
const name = rel(ctx.root, abs);
|
|
2770
|
+
const before = fsCall(`cannot edit ${name}`, () => readFileSync3(abs, "utf8"));
|
|
2771
|
+
const count = occurrences(before, oldString);
|
|
2616
2772
|
if (count === 0) {
|
|
2617
2773
|
throw new ToolError(
|
|
2618
|
-
`old_string not found in ${
|
|
2774
|
+
`old_string not found in ${path} \u2014 read the file and copy the exact text, without the line numbers read_file puts in front of each line`
|
|
2619
2775
|
);
|
|
2620
2776
|
}
|
|
2621
2777
|
if (count > 1 && !args.replace_all) {
|
|
2622
2778
|
throw new ToolError(
|
|
2623
|
-
`old_string appears ${count} times in ${
|
|
2779
|
+
`old_string appears ${count} times in ${path} \u2014 add more surrounding context, or pass replace_all: true`
|
|
2624
2780
|
);
|
|
2625
2781
|
}
|
|
2626
|
-
const after = before.split(
|
|
2782
|
+
const after = before.split(oldString).join(newString);
|
|
2627
2783
|
const preview = renderDiff(before, after) || "(no change)";
|
|
2628
2784
|
await gate(ctx, {
|
|
2629
2785
|
tool: "edit_file",
|
|
2630
|
-
signature: `write:${
|
|
2631
|
-
summary: `edit ${
|
|
2786
|
+
signature: `write:${name}`,
|
|
2787
|
+
summary: `edit ${name}`,
|
|
2632
2788
|
preview
|
|
2633
2789
|
});
|
|
2634
|
-
writeFileSync2(abs, after, "utf8");
|
|
2635
|
-
return `edited ${
|
|
2790
|
+
fsCall(`cannot edit ${name}`, () => writeFileSync2(abs, after, "utf8"));
|
|
2791
|
+
return `edited ${name} (${count > 1 ? `${count} occurrences` : "1 occurrence"})`;
|
|
2636
2792
|
},
|
|
2637
2793
|
/**
|
|
2638
2794
|
* Run a shell command in the workspace. Streams output to `ctx.onOutput` as it
|
|
2639
2795
|
* arrives and dies with `ctx.signal`, so a runaway build can be interrupted.
|
|
2640
2796
|
*/
|
|
2641
2797
|
async run_command(ctx, args) {
|
|
2642
|
-
const command = (args.command ?? "").trim();
|
|
2798
|
+
const command = (optionalString(args.command, "run_command", "command") ?? "").trim();
|
|
2643
2799
|
if (!command) throw new ToolError("run_command needs a command");
|
|
2644
2800
|
const background = args.background === true;
|
|
2645
2801
|
await gate(ctx, {
|
|
@@ -2651,7 +2807,7 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
2651
2807
|
summary: `${background ? "run in the background" : "run"} ${command.length > 60 ? command.slice(0, 59) + "\u2026" : command}`,
|
|
2652
2808
|
preview: command
|
|
2653
2809
|
});
|
|
2654
|
-
if (!background) return execute(ctx, command, args.timeout
|
|
2810
|
+
if (!background) return execute(ctx, command, countArg(args.timeout, DEFAULT_COMMAND_TIMEOUT));
|
|
2655
2811
|
const registry = ctx.shells ?? shells;
|
|
2656
2812
|
let started;
|
|
2657
2813
|
try {
|
|
@@ -2671,7 +2827,7 @@ ${first.output.trim().slice(0, 4e3)}` : "");
|
|
|
2671
2827
|
/** Whatever a background command has printed since it was last read. */
|
|
2672
2828
|
read_output(ctx, args) {
|
|
2673
2829
|
const registry = ctx.shells ?? shells;
|
|
2674
|
-
const id = (args.shell_id ?? "").trim();
|
|
2830
|
+
const id = (optionalString(args.shell_id, "read_output", "shell_id") ?? "").trim();
|
|
2675
2831
|
if (!id) {
|
|
2676
2832
|
const running = registry.list();
|
|
2677
2833
|
throw new ToolError(
|
|
@@ -2680,7 +2836,8 @@ ${first.output.trim().slice(0, 4e3)}` : "");
|
|
|
2680
2836
|
}
|
|
2681
2837
|
let read;
|
|
2682
2838
|
try {
|
|
2683
|
-
|
|
2839
|
+
const filter = optionalString(args.filter, "read_output", "filter");
|
|
2840
|
+
read = registry.read(id, filter ? { filter } : {});
|
|
2684
2841
|
} catch (err) {
|
|
2685
2842
|
asToolError(err);
|
|
2686
2843
|
}
|
|
@@ -2696,7 +2853,7 @@ ${body || "(nothing new since the last read)"}`;
|
|
|
2696
2853
|
},
|
|
2697
2854
|
async kill_shell(ctx, args) {
|
|
2698
2855
|
const registry = ctx.shells ?? shells;
|
|
2699
|
-
const id = (args.shell_id ?? "").trim();
|
|
2856
|
+
const id = (optionalString(args.shell_id, "kill_shell", "shell_id") ?? "").trim();
|
|
2700
2857
|
if (!id) throw new ToolError("kill_shell needs a shell_id");
|
|
2701
2858
|
await gate(ctx, {
|
|
2702
2859
|
tool: "kill_shell",
|
|
@@ -2719,14 +2876,14 @@ ${body || "(nothing new since the last read)"}`;
|
|
|
2719
2876
|
* a context that is billed by the token.
|
|
2720
2877
|
*/
|
|
2721
2878
|
async web_search(ctx, args) {
|
|
2722
|
-
const query = (args.query ?? "").trim();
|
|
2879
|
+
const query = (optionalString(args.query, "web_search", "query") ?? "").trim();
|
|
2723
2880
|
if (!query) throw new ToolError("web_search needs a query");
|
|
2724
2881
|
if (!ctx.webSearch) {
|
|
2725
2882
|
throw new ToolError(
|
|
2726
2883
|
"web search is not available in this run \u2014 use web_fetch on a URL you already know instead"
|
|
2727
2884
|
);
|
|
2728
2885
|
}
|
|
2729
|
-
const count = Math.
|
|
2886
|
+
const count = Math.min(MAX_SEARCH_RESULTS, countArg(args.count, DEFAULT_SEARCH_RESULTS));
|
|
2730
2887
|
let results;
|
|
2731
2888
|
try {
|
|
2732
2889
|
results = await ctx.webSearch(query, { count, ...ctx.signal ? { signal: ctx.signal } : {} });
|
|
@@ -2754,9 +2911,9 @@ ${body || "(nothing new since the last read)"}`;
|
|
|
2754
2911
|
async web_fetch(ctx, args) {
|
|
2755
2912
|
let page;
|
|
2756
2913
|
try {
|
|
2757
|
-
page = await fetchPage(args.url ?? "", {
|
|
2914
|
+
page = await fetchPage(optionalString(args.url, "web_fetch", "url") ?? "", {
|
|
2758
2915
|
...ctx.signal ? { signal: ctx.signal } : {},
|
|
2759
|
-
...
|
|
2916
|
+
...args.max_chars === void 0 ? {} : { maxChars: Math.max(1e3, countArg(args.max_chars, MAX_FILE_BYTES)) }
|
|
2760
2917
|
});
|
|
2761
2918
|
} catch (err) {
|
|
2762
2919
|
networkToolError(err);
|
|
@@ -2783,8 +2940,8 @@ ${page.text}`;
|
|
|
2783
2940
|
* the credits it saves do not pay for that.
|
|
2784
2941
|
*/
|
|
2785
2942
|
async task(ctx, args) {
|
|
2786
|
-
const prompt = (args.prompt ?? "").trim();
|
|
2787
|
-
const description = (args.description ?? "").trim() || "subagent";
|
|
2943
|
+
const prompt = (optionalString(args.prompt, "task", "prompt") ?? "").trim();
|
|
2944
|
+
const description = (optionalString(args.description, "task", "description") ?? "").trim() || "subagent";
|
|
2788
2945
|
if (!prompt) throw new ToolError("task needs a prompt describing what to find out");
|
|
2789
2946
|
if (!ctx.spawn) {
|
|
2790
2947
|
throw new ToolError(
|
|
@@ -2821,7 +2978,7 @@ ${page.text}`;
|
|
|
2821
2978
|
* be told something that was not on the list.
|
|
2822
2979
|
*/
|
|
2823
2980
|
async ask_user(ctx, args) {
|
|
2824
|
-
const question = (args.question ?? "").trim();
|
|
2981
|
+
const question = (optionalString(args.question, "ask_user", "question") ?? "").trim();
|
|
2825
2982
|
if (!question) throw new ToolError("ask_user needs a question");
|
|
2826
2983
|
if (!ctx.askUser) {
|
|
2827
2984
|
throw new ToolError(
|
|
@@ -2846,7 +3003,7 @@ ${page.text}`;
|
|
|
2846
3003
|
* able to grant itself write access.
|
|
2847
3004
|
*/
|
|
2848
3005
|
async exit_plan_mode(ctx, args) {
|
|
2849
|
-
const plan = (args.plan ?? "").trim();
|
|
3006
|
+
const plan = (optionalString(args.plan, "exit_plan_mode", "plan") ?? "").trim();
|
|
2850
3007
|
if (!plan) throw new ToolError("exit_plan_mode needs a plan");
|
|
2851
3008
|
if (!ctx.approvePlan) {
|
|
2852
3009
|
throw new ToolError(
|
|
@@ -3337,8 +3494,6 @@ var init_agent = __esm({
|
|
|
3337
3494
|
"use strict";
|
|
3338
3495
|
init_tools();
|
|
3339
3496
|
init_mcp();
|
|
3340
|
-
init_shells();
|
|
3341
|
-
init_web();
|
|
3342
3497
|
init_client();
|
|
3343
3498
|
SUBAGENT_MAX_STEPS = 12;
|
|
3344
3499
|
SYSTEM_PROMPT = "You are Clixad, a terminal coding agent. You can read, search, write and edit files and run shell commands, all confined to the user's workspace. Find your way around with glob and grep before reading whole files. Prefer edit_file over write_file for existing files. Make minimal correct edits, verify them when a test or build command is available, and stop when the task is complete. Keep your final message short: say what you did and why.\n\nNot every message is a task. If the user asks a question, or asks you to say something, answer in plain text and call no tools at all. Reach for a file or a command only when the request is actually about one, and when a message is ambiguous, prefer the reading of it that changes nothing.\n\nA tool call that was refused did not happen. Never describe it as done, and do not repeat it on a later message unless the user asks for it again.\n\nReach outside the workspace when the answer is outside it: web_search for current documentation, releases and error messages, then web_fetch to read the page. Do not guess at an API you could look up.\n\nStart anything that does not exit on its own \u2014 dev servers, watchers, tailed logs \u2014 with run_command background: true, and read it with read_output. A foreground command is killed after its timeout.\n\nWhen finding something out means opening many files, give it to task instead: the sub-agent reads them in its own context and reports back, which keeps this conversation short. Use todo_write for work with several distinct steps, and keep it current as you go.";
|
|
@@ -4831,7 +4986,9 @@ function App({ client, config, wallet, session, initialTask, sponsorServe }) {
|
|
|
4831
4986
|
const busyAbortRef = useRef(null);
|
|
4832
4987
|
const filesRef = useRef(null);
|
|
4833
4988
|
const ctrlCRef = useRef(0);
|
|
4834
|
-
const sessionRef = useRef(
|
|
4989
|
+
const sessionRef = useRef(
|
|
4990
|
+
session ?? { id: newSessionId(), started: (/* @__PURE__ */ new Date()).toISOString() }
|
|
4991
|
+
);
|
|
4835
4992
|
const runStartedAtRef = useRef(Date.now());
|
|
4836
4993
|
const contextRef = useRef(collectContext(root));
|
|
4837
4994
|
const catalogRef = useRef([]);
|
|
@@ -5185,16 +5342,14 @@ ${NO_SEARCH_INSTRUCTION}`),
|
|
|
5185
5342
|
});
|
|
5186
5343
|
const next = result.messages.slice(1);
|
|
5187
5344
|
setMessages(next);
|
|
5345
|
+
sessionRef.current.title ??= (task.split("\n")[0] || task).slice(0, 80);
|
|
5188
5346
|
saveSession({
|
|
5189
5347
|
id: sessionRef.current.id,
|
|
5190
5348
|
started: sessionRef.current.started,
|
|
5191
5349
|
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5192
5350
|
cwd: root,
|
|
5193
5351
|
model,
|
|
5194
|
-
|
|
5195
|
-
// a blank line, and a session titled with a file's contents is not a
|
|
5196
|
-
// title. What the user typed is always the first line.
|
|
5197
|
-
title: (session?.title || task.split("\n")[0] || task).slice(0, 80),
|
|
5352
|
+
title: sessionRef.current.title,
|
|
5198
5353
|
messages: next
|
|
5199
5354
|
});
|
|
5200
5355
|
if (result.stopped === "aborted") {
|
|
@@ -5248,8 +5403,7 @@ ${NO_SEARCH_INSTRUCTION}`),
|
|
|
5248
5403
|
nextSponsor,
|
|
5249
5404
|
permit,
|
|
5250
5405
|
push,
|
|
5251
|
-
root
|
|
5252
|
-
session?.title
|
|
5406
|
+
root
|
|
5253
5407
|
]
|
|
5254
5408
|
);
|
|
5255
5409
|
const stopCurrent = useCallback(() => {
|
|
@@ -5515,7 +5669,7 @@ ${NO_SEARCH_INSTRUCTION}`),
|
|
|
5515
5669
|
return;
|
|
5516
5670
|
}
|
|
5517
5671
|
setMessages(found.messages);
|
|
5518
|
-
sessionRef.current = { id: found.id, started: found.started };
|
|
5672
|
+
sessionRef.current = { id: found.id, started: found.started, title: found.title };
|
|
5519
5673
|
push({
|
|
5520
5674
|
kind: "notice",
|
|
5521
5675
|
tone: "good",
|
|
@@ -5722,9 +5876,27 @@ ${output}` }
|
|
|
5722
5876
|
setEditor((state) => insert(state, text2));
|
|
5723
5877
|
setHistIdx(-1);
|
|
5724
5878
|
});
|
|
5879
|
+
const dismissModals = useCallback(() => {
|
|
5880
|
+
if (plan) {
|
|
5881
|
+
setPlan(null);
|
|
5882
|
+
plan.resolve("keepPlanning");
|
|
5883
|
+
}
|
|
5884
|
+
if (ask2) {
|
|
5885
|
+
setAsk(null);
|
|
5886
|
+
ask2.resolve("deny");
|
|
5887
|
+
}
|
|
5888
|
+
if (picker) {
|
|
5889
|
+
const cancel = picker.onCancel;
|
|
5890
|
+
setPicker(null);
|
|
5891
|
+
cancel?.();
|
|
5892
|
+
}
|
|
5893
|
+
}, [ask2, picker, plan]);
|
|
5725
5894
|
useInput((ch, key) => {
|
|
5726
5895
|
if (key.ctrl && ch === "c") {
|
|
5727
|
-
if (busy)
|
|
5896
|
+
if (busy) {
|
|
5897
|
+
dismissModals();
|
|
5898
|
+
return stopCurrent();
|
|
5899
|
+
}
|
|
5728
5900
|
if (!isEmpty(editor)) return setEditor(EMPTY);
|
|
5729
5901
|
if (Date.now() - ctrlCRef.current < 2e3) return exit();
|
|
5730
5902
|
ctrlCRef.current = Date.now();
|
|
@@ -6419,6 +6591,7 @@ function logout(config) {
|
|
|
6419
6591
|
delete config.token;
|
|
6420
6592
|
delete config.userId;
|
|
6421
6593
|
delete config.email;
|
|
6594
|
+
delete config.login;
|
|
6422
6595
|
saveConfig(config);
|
|
6423
6596
|
console.log(c.green("\u2713 logged out"));
|
|
6424
6597
|
}
|