@livx.cc/agentx 0.99.32 → 0.99.34

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 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
- async function walkFiles(fs, dir, signal, out = []) {
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
- fsCwd = (fs) => fs.getCwd();
489
- globTool = {
490
- name: "Glob",
491
- 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"). Prefer over `bash find` for locating files \u2014 one call, structured output.',
492
- parameters: {
493
- type: "object",
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) {