@livx.cc/agentx 0.99.32 → 0.99.33
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.js +170 -76
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +30 -2
- package/dist/index.js +174 -76
- package/dist/index.js.map +1 -1
- package/dist/tools.shell.js +191 -0
- package/dist/tools.shell.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -302,7 +302,24 @@ var init_redact = __esm({
|
|
|
302
302
|
function ckAbort(signal) {
|
|
303
303
|
if (signal?.aborted) throw new Error("aborted");
|
|
304
304
|
}
|
|
305
|
-
|
|
305
|
+
function realOf(fs, p) {
|
|
306
|
+
const rp = fs.canonicalPath;
|
|
307
|
+
if (typeof rp !== "function") return p;
|
|
308
|
+
try {
|
|
309
|
+
return rp.call(fs, p) || p;
|
|
310
|
+
} catch {
|
|
311
|
+
return p;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function walkFiles(fs, dir, signal, out = [], st, depth = 0) {
|
|
315
|
+
const state = st ?? { skip: new Set(DEFAULT_SKIP_DIRS), maxDepth: DEFAULT_MAX_DEPTH, seen: /* @__PURE__ */ new Set(), prunedDirs: /* @__PURE__ */ new Set(), depthCuts: 0 };
|
|
316
|
+
if (depth > state.maxDepth) {
|
|
317
|
+
state.depthCuts++;
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
const real = realOf(fs, dir);
|
|
321
|
+
if (state.seen.has(real)) return out;
|
|
322
|
+
state.seen.add(real);
|
|
306
323
|
let entries;
|
|
307
324
|
try {
|
|
308
325
|
entries = await fs.readDir(dir);
|
|
@@ -311,12 +328,57 @@ async function walkFiles(fs, dir, signal, out = []) {
|
|
|
311
328
|
}
|
|
312
329
|
for (const name of entries.sort()) {
|
|
313
330
|
ckAbort(signal);
|
|
331
|
+
if (state.skip.has(name)) {
|
|
332
|
+
state.prunedDirs.add(name);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
314
335
|
const p = dir === "/" ? `/${name}` : `${dir}/${name}`;
|
|
315
|
-
if (await fs.isDirectory(p)) await walkFiles(fs, p, signal, out);
|
|
336
|
+
if (await fs.isDirectory(p)) await walkFiles(fs, p, signal, out, state, depth + 1);
|
|
316
337
|
else out.push(p);
|
|
317
338
|
}
|
|
318
339
|
return out;
|
|
319
340
|
}
|
|
341
|
+
function scanRoots(fs, path, opts) {
|
|
342
|
+
const asked = path == null ? [] : (Array.isArray(path) ? path : [path]).map((p) => String(p)).filter(Boolean);
|
|
343
|
+
const roots = (asked.length ? asked : opts?.roots?.length ? opts.roots : [fsCwd(fs)]).map((r) => r.length > 1 && r.endsWith("/") ? r.slice(0, -1) : r);
|
|
344
|
+
return [...new Set(roots)].filter((r, _i, all) => !all.some((other) => other !== r && r.startsWith(`${other}/`)));
|
|
345
|
+
}
|
|
346
|
+
function scanNote(states) {
|
|
347
|
+
const pruned = [...new Set(states.flatMap((st) => [...st.prunedDirs]))].sort();
|
|
348
|
+
const cuts = states.reduce((n, st) => n + st.depthCuts, 0);
|
|
349
|
+
const parts = [];
|
|
350
|
+
if (pruned.length) parts.push(`skipped ${pruned.join(", ")} (pass skipDirs to include them)`);
|
|
351
|
+
if (cuts) parts.push(`stopped at depth ${states[0]?.maxDepth} in ${cuts} place${cuts === 1 ? "" : "s"} (pass maxDepth to go deeper)`);
|
|
352
|
+
return parts.length ? `
|
|
353
|
+
[scan: ${parts.join("; ")}]` : "";
|
|
354
|
+
}
|
|
355
|
+
async function walkRoots(fs, roots, signal, opts, call) {
|
|
356
|
+
const skip = new Set(call?.skipDirs ?? opts?.skipDirs ?? DEFAULT_SKIP_DIRS);
|
|
357
|
+
const maxDepth = call?.maxDepth ?? opts?.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
358
|
+
const out = [];
|
|
359
|
+
const states = [];
|
|
360
|
+
for (const root of roots) {
|
|
361
|
+
const state = { skip, maxDepth, seen: /* @__PURE__ */ new Set(), prunedDirs: /* @__PURE__ */ new Set(), depthCuts: 0 };
|
|
362
|
+
states.push(state);
|
|
363
|
+
await walkFiles(fs, root, signal, out, state, 0);
|
|
364
|
+
}
|
|
365
|
+
return { files: out, note: scanNote(states) };
|
|
366
|
+
}
|
|
367
|
+
function callScan(args) {
|
|
368
|
+
const sd = args.skipDirs;
|
|
369
|
+
return {
|
|
370
|
+
skipDirs: Array.isArray(sd) ? sd.map(String) : typeof sd === "string" ? sd.split(",").map((x) => x.trim()).filter(Boolean) : void 0,
|
|
371
|
+
maxDepth: args.maxDepth == null ? void 0 : Number(args.maxDepth)
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function anchoredGlobAt(root, glob) {
|
|
375
|
+
if (glob.startsWith("/")) return globToRegExp(glob);
|
|
376
|
+
return globToRegExp(`${root === "/" ? "" : root}/${glob}`);
|
|
377
|
+
}
|
|
378
|
+
function globMatcher(roots, glob) {
|
|
379
|
+
const res = roots.map((r) => anchoredGlobAt(r, glob));
|
|
380
|
+
return (p) => res.some((re) => re.test(p));
|
|
381
|
+
}
|
|
320
382
|
function globToRegExp(glob, caseInsensitive = false) {
|
|
321
383
|
const g = glob.startsWith("/") ? glob : `/${glob}`;
|
|
322
384
|
let re = "";
|
|
@@ -338,6 +400,88 @@ function anchoredGlob(fs, glob) {
|
|
|
338
400
|
const base = cwd === "/" ? "" : cwd;
|
|
339
401
|
return globToRegExp(glob.startsWith("/") ? glob : `${base}/${glob}`);
|
|
340
402
|
}
|
|
403
|
+
function makeGlobTool(opts) {
|
|
404
|
+
return {
|
|
405
|
+
name: "Glob",
|
|
406
|
+
description: 'Find files by glob pattern (e.g. "**/*.ts", "src/**/*.test.ts"). Returns sorted paths, one per line. Space-separated patterns combine; `!`-prefix excludes (e.g. "**/*.ts !**/*.test.ts"). Optional `path` to search a specific directory. Prefer over `bash find` for locating files \u2014 one call, structured output.',
|
|
407
|
+
parameters: {
|
|
408
|
+
type: "object",
|
|
409
|
+
required: ["pattern"],
|
|
410
|
+
properties: {
|
|
411
|
+
pattern: { type: "string", description: "glob pattern(s); ** matches across directories; prefix a pattern with ! to exclude" },
|
|
412
|
+
...SCAN_PARAMS
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
async run({ pattern, path, skipDirs, maxDepth }, ctx) {
|
|
416
|
+
const roots = scanRoots(ctx.fs, path, opts);
|
|
417
|
+
const pats = String(pattern ?? "").trim().split(/\s+/).filter(Boolean);
|
|
418
|
+
const include = pats.filter((p) => !p.startsWith("!")).map((p) => globMatcher(roots, p));
|
|
419
|
+
const exclude = pats.filter((p) => p.startsWith("!")).map((p) => globMatcher(roots, p.slice(1)));
|
|
420
|
+
const includes = include.length ? include : [globMatcher(roots, "**")];
|
|
421
|
+
const { files, note } = await walkRoots(ctx.fs, roots, ctx.signal, opts, callScan({ skipDirs, maxDepth }));
|
|
422
|
+
const hits = files.filter((p) => includes.some((m) => m(p)) && !exclude.some((m) => m(p)));
|
|
423
|
+
return (hits.length ? [...new Set(hits)].sort().join("\n") : "(no matches)") + note;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function makeGrepTool(opts) {
|
|
428
|
+
return {
|
|
429
|
+
name: "Grep",
|
|
430
|
+
description: "Search file contents by regex. Returns `path:line: text` hits. Optional `glob` to scope files, `path` to search a specific directory, `context` for surrounding lines, `filesOnly` for matching paths only. Prefer over `bash grep` for file content search \u2014 structured results, no re-parse needed. Use `bash` instead for running commands, tests, or piped workflows.",
|
|
431
|
+
parameters: {
|
|
432
|
+
type: "object",
|
|
433
|
+
required: ["pattern"],
|
|
434
|
+
properties: {
|
|
435
|
+
pattern: { type: "string", description: "JS regular expression" },
|
|
436
|
+
glob: { type: "string", description: "optional file glob to restrict the search" },
|
|
437
|
+
...SCAN_PARAMS,
|
|
438
|
+
context: { type: "number", description: "lines of context before/after each hit" },
|
|
439
|
+
filesOnly: { type: "boolean", description: "only list matching file paths" }
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
async run({ pattern, glob, path, skipDirs, maxDepth, context, filesOnly }, ctx) {
|
|
443
|
+
let re;
|
|
444
|
+
try {
|
|
445
|
+
re = new RegExp(String(pattern ?? ""));
|
|
446
|
+
} catch (e) {
|
|
447
|
+
throw new Error(`invalid regex: ${String(e)}`);
|
|
448
|
+
}
|
|
449
|
+
const roots = scanRoots(ctx.fs, path, opts);
|
|
450
|
+
const scope = glob ? globMatcher(roots, String(glob)) : null;
|
|
451
|
+
const walked = await walkRoots(ctx.fs, roots, ctx.signal, opts, callScan({ skipDirs, maxDepth }));
|
|
452
|
+
const files = [...new Set(walked.files)].sort().filter((p) => !scope || scope(p));
|
|
453
|
+
const ctxN = Math.max(0, Number(context ?? 0));
|
|
454
|
+
const out = [];
|
|
455
|
+
const matched = [];
|
|
456
|
+
let skipped = 0;
|
|
457
|
+
for (const path2 of files) {
|
|
458
|
+
ckAbort(ctx.signal);
|
|
459
|
+
let content;
|
|
460
|
+
try {
|
|
461
|
+
content = await ctx.fs.readFile(path2);
|
|
462
|
+
} catch {
|
|
463
|
+
skipped++;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const lines = content.split("\n");
|
|
467
|
+
const mask = CONFIG_FILE_RE.test(path2);
|
|
468
|
+
let fileHit = false;
|
|
469
|
+
for (let i = 0; i < lines.length; i++) {
|
|
470
|
+
if (!re.test(lines[i])) continue;
|
|
471
|
+
fileHit = true;
|
|
472
|
+
if (filesOnly) break;
|
|
473
|
+
const lo = Math.max(0, i - ctxN), hi = Math.min(lines.length - 1, i + ctxN);
|
|
474
|
+
for (let j = lo; j <= hi; j++) out.push(`${path2}:${j + 1}: ${mask ? redactSecrets(lines[j]) : lines[j]}`);
|
|
475
|
+
}
|
|
476
|
+
if (fileHit) matched.push(path2);
|
|
477
|
+
}
|
|
478
|
+
const note = (skipped ? `
|
|
479
|
+
[skipped ${skipped} unreadable file${skipped === 1 ? "" : "s"}]` : "") + walked.note;
|
|
480
|
+
if (filesOnly) return (matched.length ? matched.join("\n") : "(no matches)") + note;
|
|
481
|
+
return (out.length ? out.join("\n") : "(no matches)") + note;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
341
485
|
function docOutlineOf(content, cap = 20) {
|
|
342
486
|
const out = [];
|
|
343
487
|
const lines = content.split("\n");
|
|
@@ -480,84 +624,21 @@ Reply with a short numbered list of concrete, actionable issues. If the changes
|
|
|
480
624
|
}
|
|
481
625
|
};
|
|
482
626
|
}
|
|
483
|
-
var fsCwd, globTool, grepTool, SIG_RE, isCode, isDoc, repoMapTool, writeTool, multiEditTool, applyEditsTool;
|
|
627
|
+
var DEFAULT_SKIP_DIRS, DEFAULT_MAX_DEPTH, SCAN_PARAMS, fsCwd, globTool, grepTool, SIG_RE, isCode, isDoc, repoMapTool, writeTool, multiEditTool, applyEditsTool;
|
|
484
628
|
var init_tools_structured = __esm({
|
|
485
629
|
"src/tools.structured.ts"() {
|
|
486
630
|
"use strict";
|
|
487
631
|
init_redact();
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
required: ["pattern"],
|
|
495
|
-
properties: { pattern: { type: "string", description: "glob pattern(s); ** matches across directories; prefix a pattern with ! to exclude" } }
|
|
496
|
-
},
|
|
497
|
-
async run({ pattern }, ctx) {
|
|
498
|
-
const pats = String(pattern ?? "").trim().split(/\s+/).filter(Boolean);
|
|
499
|
-
const include = pats.filter((p) => !p.startsWith("!")).map((p) => anchoredGlob(ctx.fs, p));
|
|
500
|
-
const exclude = pats.filter((p) => p.startsWith("!")).map((p) => anchoredGlob(ctx.fs, p.slice(1)));
|
|
501
|
-
const includes = include.length ? include : [anchoredGlob(ctx.fs, "**")];
|
|
502
|
-
const hits = (await walkFiles(ctx.fs, fsCwd(ctx.fs), ctx.signal)).filter(
|
|
503
|
-
(p) => includes.some((re) => re.test(p)) && !exclude.some((re) => re.test(p))
|
|
504
|
-
);
|
|
505
|
-
return hits.length ? hits.join("\n") : "(no matches)";
|
|
506
|
-
}
|
|
507
|
-
};
|
|
508
|
-
grepTool = {
|
|
509
|
-
name: "Grep",
|
|
510
|
-
description: "Search file contents by regex. Returns `path:line: text` hits. Optional `glob` to scope files, `context` for surrounding lines, `filesOnly` for matching paths only. Prefer over `bash grep` for file content search \u2014 structured results, no re-parse needed. Use `bash` instead for running commands, tests, or piped workflows.",
|
|
511
|
-
parameters: {
|
|
512
|
-
type: "object",
|
|
513
|
-
required: ["pattern"],
|
|
514
|
-
properties: {
|
|
515
|
-
pattern: { type: "string", description: "JS regular expression" },
|
|
516
|
-
glob: { type: "string", description: "optional file glob to restrict the search" },
|
|
517
|
-
context: { type: "number", description: "lines of context before/after each hit" },
|
|
518
|
-
filesOnly: { type: "boolean", description: "only list matching file paths" }
|
|
519
|
-
}
|
|
520
|
-
},
|
|
521
|
-
async run({ pattern, glob, context, filesOnly }, ctx) {
|
|
522
|
-
let re;
|
|
523
|
-
try {
|
|
524
|
-
re = new RegExp(String(pattern ?? ""));
|
|
525
|
-
} catch (e) {
|
|
526
|
-
throw new Error(`invalid regex: ${String(e)}`);
|
|
527
|
-
}
|
|
528
|
-
const scope = glob ? anchoredGlob(ctx.fs, String(glob)) : null;
|
|
529
|
-
const files = (await walkFiles(ctx.fs, fsCwd(ctx.fs), ctx.signal)).filter((p) => !scope || scope.test(p));
|
|
530
|
-
const ctxN = Math.max(0, Number(context ?? 0));
|
|
531
|
-
const out = [];
|
|
532
|
-
const matched = [];
|
|
533
|
-
let skipped = 0;
|
|
534
|
-
for (const path of files) {
|
|
535
|
-
ckAbort(ctx.signal);
|
|
536
|
-
let content;
|
|
537
|
-
try {
|
|
538
|
-
content = await ctx.fs.readFile(path);
|
|
539
|
-
} catch {
|
|
540
|
-
skipped++;
|
|
541
|
-
continue;
|
|
542
|
-
}
|
|
543
|
-
const lines = content.split("\n");
|
|
544
|
-
const mask = CONFIG_FILE_RE.test(path);
|
|
545
|
-
let fileHit = false;
|
|
546
|
-
for (let i = 0; i < lines.length; i++) {
|
|
547
|
-
if (!re.test(lines[i])) continue;
|
|
548
|
-
fileHit = true;
|
|
549
|
-
if (filesOnly) break;
|
|
550
|
-
const lo = Math.max(0, i - ctxN), hi = Math.min(lines.length - 1, i + ctxN);
|
|
551
|
-
for (let j = lo; j <= hi; j++) out.push(`${path}:${j + 1}: ${mask ? redactSecrets(lines[j]) : lines[j]}`);
|
|
552
|
-
}
|
|
553
|
-
if (fileHit) matched.push(path);
|
|
554
|
-
}
|
|
555
|
-
const note = skipped ? `
|
|
556
|
-
[skipped ${skipped} unreadable file${skipped === 1 ? "" : "s"}]` : "";
|
|
557
|
-
if (filesOnly) return (matched.length ? matched.join("\n") : "(no matches)") + note;
|
|
558
|
-
return (out.length ? out.join("\n") : "(no matches)") + note;
|
|
559
|
-
}
|
|
632
|
+
DEFAULT_SKIP_DIRS = ["node_modules", ".git", "dist", ".tmp"];
|
|
633
|
+
DEFAULT_MAX_DEPTH = 24;
|
|
634
|
+
SCAN_PARAMS = {
|
|
635
|
+
path: { type: "string", description: "optional directory (or list) to search instead of the default roots; absolute VFS path" },
|
|
636
|
+
skipDirs: { type: "string", description: `comma-separated directory names to prune (default: ${DEFAULT_SKIP_DIRS.join(", ")}); pass an empty string to prune nothing` },
|
|
637
|
+
maxDepth: { type: "number", description: `max directory depth below each root (default ${DEFAULT_MAX_DEPTH})` }
|
|
560
638
|
};
|
|
639
|
+
fsCwd = (fs) => fs.getCwd();
|
|
640
|
+
globTool = makeGlobTool();
|
|
641
|
+
grepTool = makeGrepTool();
|
|
561
642
|
SIG_RE = /^\s*(export\b|(?:export\s+)?(?:async\s+)?function\s+\*?\w|(?:export\s+)?(?:abstract\s+)?class\s+\w|(?:export\s+)?interface\s+\w|(?:export\s+)?type\s+\w|(?:export\s+)?enum\s+\w)/;
|
|
562
643
|
isCode = (p) => /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(p);
|
|
563
644
|
isDoc = (p) => /\.(md|mdx|txt)$/.test(p);
|
|
@@ -1587,7 +1668,7 @@ var NodeDiskFilesystem_exports = {};
|
|
|
1587
1668
|
__export(NodeDiskFilesystem_exports, {
|
|
1588
1669
|
NodeDiskFilesystem: () => NodeDiskFilesystem
|
|
1589
1670
|
});
|
|
1590
|
-
import { promises as fsp } from "fs";
|
|
1671
|
+
import { promises as fsp, realpathSync as realpathSync2 } from "fs";
|
|
1591
1672
|
import * as np from "path";
|
|
1592
1673
|
import { PathResolver } from "@livx.cc/wcli/core";
|
|
1593
1674
|
var NodeDiskFilesystem;
|
|
@@ -1666,6 +1747,19 @@ var init_NodeDiskFilesystem = __esm({
|
|
|
1666
1747
|
realPath(path) {
|
|
1667
1748
|
return this.real(this.resolvePath(path));
|
|
1668
1749
|
}
|
|
1750
|
+
/** The CANONICAL on-disk path — symlinks resolved. `realPath` is path arithmetic (baseDir + vpath)
|
|
1751
|
+
* and therefore reports a symlink and its target as different places, which is no use as an
|
|
1752
|
+
* identity: a tree walk that dedupes on it still re-enters a loop (`a/b/loop -> a` re-walked 11
|
|
1753
|
+
* times before a depth cap stopped it). Optional capability, duck-typed by the file-scanning
|
|
1754
|
+
* tools; falls back to the arithmetic path for anything unresolvable (a broken/racing link). */
|
|
1755
|
+
canonicalPath(path) {
|
|
1756
|
+
const p = this.realPath(path);
|
|
1757
|
+
try {
|
|
1758
|
+
return realpathSync2(p);
|
|
1759
|
+
} catch {
|
|
1760
|
+
return p;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1669
1763
|
/** Read raw bytes (for binary files like images). Not on the base IFilesystem (which is utf8-only) — an
|
|
1670
1764
|
* optional capability the Read tool duck-types to return image blocks. Same symlink/jail guards as readFile. */
|
|
1671
1765
|
async readFileBytes(path) {
|