@cryer/star-cli 0.3.9 → 0.3.10

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.
@@ -334,10 +334,250 @@ var grepTool = {
334
334
  }
335
335
  };
336
336
 
337
- // src/tools/fs/read.ts
338
- import { open, realpath as realpath2, stat as stat3 } from "fs/promises";
337
+ // src/tools/fs/outline.ts
338
+ import { readFile as readFile3, realpath as realpath2, stat as stat3 } from "fs/promises";
339
339
  import path4 from "path";
340
340
  import { z as z4 } from "zod";
341
+ var MAX_DIR_FILES = 60;
342
+ var MAX_FILE_BYTES2 = 2 * 1024 * 1024;
343
+ var MAX_SYMBOLS_PER_FILE = 150;
344
+ var MAX_LINE_CHARS2 = 200;
345
+ var MAX_OUT_CHARS = 30 * 1024;
346
+ var MAX_INDENT_CHARS = 8;
347
+ var KEYWORD_EXCLUSION = "(?!if\\b|for\\b|while\\b|switch\\b|catch\\b|return\\b|else\\b|do\\b|new\\b|throw\\b)";
348
+ var TS_MEMBER_MODIFIERS = "(?:public|private|protected|static|async|readonly|get|set|override|abstract)";
349
+ var TS_LIKE = [
350
+ /^(?:export\s+)?(?:default\s+)?(?:async\s+)?function\*?\s+[\w$]+/,
351
+ /^(?:export\s+)?(?:abstract\s+)?class\s+[\w$]+/,
352
+ /^(?:export\s+)?interface\s+[\w$]+/,
353
+ /^(?:export\s+)?type\s+[\w$]+\s*=/,
354
+ /^(?:export\s+)?(?:const\s+)?enum\s+[\w$]+/,
355
+ // const handler = (a, b) => / const handler = async (a) => — top level
356
+ // only; locals inside functions are navigation noise.
357
+ /^(?:export\s+)?(?:const|let|var)\s+[\w$]+\s*=\s*(?:async\s*)?(?:\([^)]*\)|[\w$]+)\s*=>/,
358
+ // Indented class/object members with a body: name<T>(args): ReturnType {
359
+ // — control flow excluded by the keyword lookahead, dotted calls
360
+ // (this.x()) fail the anchored name-then-paren shape.
361
+ new RegExp(
362
+ `^\\s+(?:${TS_MEMBER_MODIFIERS}\\s+)*${KEYWORD_EXCLUSION}[\\w$]+(?:<[^>]*>)?\\s*\\([^)]*\\)\\s*(?::\\s*[^{}=;]+?)?\\s*(?:\\{\\s*)?\\}?\\s*$`
363
+ ),
364
+ // Bodyless signatures (interface members, overloads): only when the
365
+ // parens carry type annotations or an explicit return type follows —
366
+ // otherwise indented calls like clearTimeout(timer); or finalize();
367
+ // would leak into the outline.
368
+ new RegExp(
369
+ `^\\s+(?:${TS_MEMBER_MODIFIERS}\\s+)*${KEYWORD_EXCLUSION}[\\w$]+(?:<[^>]*>)?\\s*(?:\\([^)]*:[^)]*\\)\\s*(?::\\s*[^{}=;]+?)?|\\([^)]*\\)\\s*:\\s*[^{}=;]+?)\\s*;\\s*$`
370
+ )
371
+ ];
372
+ var LANGUAGE_PATTERNS = {
373
+ ts: TS_LIKE,
374
+ python: [/^\s*(?:async\s+)?def\s+\w+\s*\(/, /^\s*class\s+\w+/],
375
+ go: [/^func\s+(?:\([^)]*\)\s*)?\w+\s*\(/, /^type\s+\w+\s+(?:struct|interface)\b/],
376
+ rust: [
377
+ /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+\w+/,
378
+ /^\s*(?:pub\s+)?(?:struct|enum|trait|union)\s+\w+/
379
+ ],
380
+ jvm: [
381
+ /^\s*(?:(?:public|private|protected|internal|static|final|abstract|sealed|open|suspend|inline|override|async|partial|virtual|extern)\s+)*(?:class|interface|enum|struct|record|object)\s+\w+/,
382
+ /^\s*(?:(?:public|private|protected|internal|open|suspend|inline|override)\s+)*fun\s+\w+\s*\(/,
383
+ new RegExp(
384
+ `^\\s+(?:(?:public|private|protected|internal|static|final|abstract|sealed|open|suspend|inline|override|async|partial|virtual)\\s+)*[\\w<>[\\],.?]+\\s+${KEYWORD_EXCLUSION}\\w+\\s*\\([^;{}]*\\)\\s*(?:\\{|;|=>|$)`
385
+ )
386
+ ],
387
+ cpp: [
388
+ /^\s*(?:class|struct|enum|union|namespace)\s+\w+/,
389
+ new RegExp(
390
+ `^\\s*(?:(?:static|inline|virtual|constexpr|extern|friend|explicit)\\s+)*[\\w:<>,~*&\\s]+?\\s+${KEYWORD_EXCLUSION}[\\w:~]+\\s*\\([^;]*\\)\\s*(?:const\\s*)?(?:noexcept\\s*)?(?:\\{|;|$)`
391
+ )
392
+ ],
393
+ ruby: [/^\s*def\s+[\w.=!?]+/, /^\s*(?:class|module)\s+[\w:]+/],
394
+ php: [
395
+ /^\s*(?:(?:public|private|protected|static|abstract|final)\s+)*function\s+\w+\s*\(/,
396
+ /^\s*(?:(?:abstract|final)\s+)?(?:class|interface|trait|enum)\s+\w+/
397
+ ],
398
+ shell: [/^\s*(?:function\s+)?[\w.-]+\s*\(\s*\)\s*(?:\{|$)/],
399
+ swift: [
400
+ /^\s*(?:(?:public|private|internal|open|fileprivate|static|class|override|mutating|final)\s+)*func\s+\w+\s*\(/,
401
+ /^\s*(?:(?:public|private|internal|open|final)\s+)*(?:class|struct|enum|protocol|extension)\s+\w+/
402
+ ],
403
+ scala: [
404
+ /^\s*(?:(?:private|protected|override|final|abstract|implicit)\s+)*def\s+\w+\s*[(\[]/,
405
+ /^\s*(?:(?:abstract|final|sealed)\s+)*(?:class|trait|object)\s+\w+/
406
+ ]
407
+ };
408
+ var EXT_TO_LANGUAGE = {
409
+ ts: "ts",
410
+ tsx: "ts",
411
+ mts: "ts",
412
+ cts: "ts",
413
+ js: "ts",
414
+ jsx: "ts",
415
+ mjs: "ts",
416
+ cjs: "ts",
417
+ py: "python",
418
+ pyi: "python",
419
+ go: "go",
420
+ rs: "rust",
421
+ java: "jvm",
422
+ kt: "jvm",
423
+ kts: "jvm",
424
+ cs: "jvm",
425
+ c: "cpp",
426
+ h: "cpp",
427
+ cc: "cpp",
428
+ cpp: "cpp",
429
+ cxx: "cpp",
430
+ hpp: "cpp",
431
+ hh: "cpp",
432
+ rb: "ruby",
433
+ php: "php",
434
+ sh: "shell",
435
+ bash: "shell",
436
+ zsh: "shell",
437
+ swift: "swift",
438
+ scala: "scala",
439
+ sc: "scala"
440
+ };
441
+ function languageOf(filePath) {
442
+ const name = path4.basename(filePath);
443
+ if (/\.(min|bundle|generated)\.[^.]+$/.test(name)) return void 0;
444
+ const ext = name.split(".").pop()?.toLowerCase() ?? "";
445
+ const lang = EXT_TO_LANGUAGE[ext];
446
+ return lang === void 0 ? void 0 : LANGUAGE_PATTERNS[lang];
447
+ }
448
+ function extractSymbols(text, patterns) {
449
+ const out = [];
450
+ const lines = text.split("\n");
451
+ for (let i = 0; i < lines.length; i++) {
452
+ const raw = (lines[i] ?? "").replace(/\r$/, "");
453
+ const trimmed = raw.trim();
454
+ if (trimmed === "") continue;
455
+ if (!patterns.some((re) => re.test(raw))) continue;
456
+ const indent = raw.slice(0, raw.length - raw.trimStart().length).slice(0, MAX_INDENT_CHARS);
457
+ const shown = trimmed.length > MAX_LINE_CHARS2 ? `${trimmed.slice(0, MAX_LINE_CHARS2)}\u2026` : trimmed;
458
+ out.push({ line: i + 1, indent, text: shown });
459
+ }
460
+ return out;
461
+ }
462
+ async function outlineFile(abs) {
463
+ const st = await stat3(abs).catch(() => null);
464
+ if (!st || st.size > MAX_FILE_BYTES2) return { unreadable: true };
465
+ const buf = await readFile3(abs).catch(() => null);
466
+ if (!buf) return { unreadable: true };
467
+ if (buf.includes(0)) return { binary: true };
468
+ const patterns = languageOf(abs);
469
+ if (!patterns) return { unreadable: true };
470
+ const all = extractSymbols(buf.toString("utf8"), patterns);
471
+ return { symbols: all.slice(0, MAX_SYMBOLS_PER_FILE), total: all.length };
472
+ }
473
+ function formatSymbols(symbols, total) {
474
+ const out = symbols.map((s) => `${s.line} ${s.indent}${s.text}`);
475
+ if (total > symbols.length) {
476
+ out.push(`... (${total - symbols.length} more declarations)`);
477
+ }
478
+ return out;
479
+ }
480
+ var schema4 = z4.object({
481
+ path: z4.string().optional().describe("Source file or directory to outline (default: the working directory)"),
482
+ maxFiles: z4.number().int().positive().optional().describe(`Directory mode: maximum files to include (default ${MAX_DIR_FILES})`)
483
+ });
484
+ var codeOutlineTool = {
485
+ name: "code_outline",
486
+ description: "List the declaration skeleton of a source file or directory (functions, classes, methods, types) with line numbers, without reading the bodies. Cheaper than read_file for navigating unfamiliar code: outline first, then read_file the exact line ranges you need. Supports TS/JS, Python, Go, Rust, Java/Kotlin/C#, C/C++, Ruby, PHP, Shell, Swift, Scala.",
487
+ permission: "read",
488
+ parameters: schema4,
489
+ async execute(args, ctx) {
490
+ const root = path4.resolve(ctx.cwd, args.path ?? ".");
491
+ const st = await stat3(root).catch(() => null);
492
+ if (!st) {
493
+ return { content: `Path not found: ${args.path ?? "."}`, isError: true };
494
+ }
495
+ if (st.isFile()) {
496
+ const resolved = await realpath2(root).catch(() => null);
497
+ if (isSensitivePath(root) || resolved !== null && isSensitivePath(resolved)) {
498
+ return { content: `Refused to outline sensitive file: ${args.path ?? "."}`, isError: true };
499
+ }
500
+ if (languageOf(root) === void 0) {
501
+ return {
502
+ content: `No supported source language for: ${args.path ?? "."} (supported: TS/JS, Python, Go, Rust, Java/Kotlin/C#, C/C++, Ruby, PHP, Shell, Swift, Scala)`
503
+ };
504
+ }
505
+ const result = await outlineFile(root);
506
+ if ("binary" in result) {
507
+ return { content: `Cannot outline binary file: ${args.path ?? "."}`, isError: true };
508
+ }
509
+ if ("unreadable" in result) {
510
+ return {
511
+ content: `Cannot outline ${args.path ?? "."}: unreadable or larger than 2MB`,
512
+ isError: true
513
+ };
514
+ }
515
+ if (result.symbols.length === 0) {
516
+ return { content: `(no recognizable declarations in ${args.path ?? "."})` };
517
+ }
518
+ return { content: formatSymbols(result.symbols, result.total).join("\n") };
519
+ }
520
+ const walked = await walkFiles(root, void 0, {
521
+ ignore: createIgnorePredicate(ctx.cwd),
522
+ withMtime: false,
523
+ signal: ctx.abortSignal,
524
+ symlinkBoundary: ctx.cwd,
525
+ nestedIgnore: true
526
+ });
527
+ const candidates = walked.files.filter(
528
+ (f) => languageOf(f.abs) !== void 0 && !isSensitivePath(f.abs) && (f.resolved === void 0 || !isSensitivePath(f.resolved))
529
+ );
530
+ const maxFiles = args.maxFiles ?? MAX_DIR_FILES;
531
+ const scan = async (file) => {
532
+ const result = await outlineFile(file.abs);
533
+ if ("symbols" in result && result.symbols.length > 0) {
534
+ const lines = formatSymbols(result.symbols, result.total);
535
+ const chars = lines.reduce((n, l) => n + l.length + 1, file.rel.length + 2);
536
+ return { rel: file.rel, lines, chars };
537
+ }
538
+ return null;
539
+ };
540
+ const out = [];
541
+ let size = 0;
542
+ let included = 0;
543
+ let hitLimit = false;
544
+ for (let i = 0; i < candidates.length && included < maxFiles; i += WALK_CONCURRENCY) {
545
+ if (ctx.abortSignal?.aborted) break;
546
+ const chunk = await Promise.all(
547
+ candidates.slice(i, i + WALK_CONCURRENCY).map((file) => scan(file))
548
+ );
549
+ for (const block of chunk) {
550
+ if (!block) continue;
551
+ if (size + block.chars > MAX_OUT_CHARS || included >= maxFiles) {
552
+ hitLimit = true;
553
+ break;
554
+ }
555
+ if (out.length > 0) out.push("");
556
+ out.push(`${block.rel}:`);
557
+ out.push(...block.lines);
558
+ size += block.chars;
559
+ included += 1;
560
+ }
561
+ if (hitLimit) break;
562
+ }
563
+ if (included === 0) {
564
+ return {
565
+ content: `No recognizable declarations under ${args.path ?? "."} (${candidates.length} source file(s) scanned)`
566
+ };
567
+ }
568
+ if (hitLimit) {
569
+ out.push("... (truncated: output limit reached, more files not outlined)");
570
+ } else if (walked.aborted || ctx.abortSignal?.aborted) {
571
+ out.push("... (interrupted: partial results)");
572
+ }
573
+ return { content: out.join("\n") };
574
+ }
575
+ };
576
+
577
+ // src/tools/fs/read.ts
578
+ import { open, realpath as realpath3, stat as stat4 } from "fs/promises";
579
+ import path5 from "path";
580
+ import { z as z5 } from "zod";
341
581
  var DEFAULT_LIMIT = 2e3;
342
582
  var MAX_CHARS = 100 * 1024;
343
583
  var MAX_READ_BYTES = 1024 * 1024;
@@ -363,10 +603,10 @@ function decodeTextBuffer(buf) {
363
603
  }
364
604
  return { text: buf.toString("utf8") };
365
605
  }
366
- var schema4 = z4.object({
367
- path: z4.string().describe("File path, absolute or relative to the working directory"),
368
- offset: z4.number().int().positive().optional().describe("1-based line number to start reading from"),
369
- limit: z4.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
606
+ var schema5 = z5.object({
607
+ path: z5.string().describe("File path, absolute or relative to the working directory"),
608
+ offset: z5.number().int().positive().optional().describe("1-based line number to start reading from"),
609
+ limit: z5.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
370
610
  });
371
611
  var READ_CACHE_MAX_ENTRIES = 200;
372
612
  function createReadFileTool() {
@@ -375,15 +615,15 @@ function createReadFileTool() {
375
615
  name: "read_file",
376
616
  description: "Read a text file and return its contents with line numbers. Output is truncated to 2000 lines or 100KB by default. Re-reading an unchanged file range returns a short note instead of the content.",
377
617
  permission: "read",
378
- parameters: schema4,
618
+ parameters: schema5,
379
619
  reset: () => cache.clear(),
380
620
  async execute(args, ctx) {
381
- const filePath = path4.resolve(ctx.cwd, args.path);
382
- const resolvedPath = await realpath2(filePath).catch(() => null);
621
+ const filePath = path5.resolve(ctx.cwd, args.path);
622
+ const resolvedPath = await realpath3(filePath).catch(() => null);
383
623
  if (isSensitivePath(filePath) || resolvedPath !== null && isSensitivePath(resolvedPath)) {
384
624
  return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
385
625
  }
386
- const st = await stat3(filePath).catch(() => null);
626
+ const st = await stat4(filePath).catch(() => null);
387
627
  if (!st) {
388
628
  return { content: `File not found: ${args.path}`, isError: true };
389
629
  }
@@ -472,9 +712,9 @@ function createReadFileTool() {
472
712
  var readFileTool = createReadFileTool();
473
713
 
474
714
  // src/tools/fs/read-image.ts
475
- import { readFile as readFile3, realpath as realpath3, stat as stat4 } from "fs/promises";
476
- import path5 from "path";
477
- import { z as z5 } from "zod";
715
+ import { readFile as readFile4, realpath as realpath4, stat as stat5 } from "fs/promises";
716
+ import path6 from "path";
717
+ import { z as z6 } from "zod";
478
718
  var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
479
719
  var IMAGE_MIME_TYPES = {
480
720
  ".png": "image/png",
@@ -483,8 +723,8 @@ var IMAGE_MIME_TYPES = {
483
723
  ".gif": "image/gif",
484
724
  ".webp": "image/webp"
485
725
  };
486
- var schema5 = z5.object({
487
- path: z5.string().describe(
726
+ var schema6 = z6.object({
727
+ path: z6.string().describe(
488
728
  "Image file path (png/jpg/jpeg/gif/webp), absolute or relative to the working directory"
489
729
  )
490
730
  });
@@ -492,7 +732,7 @@ var readImageTool = {
492
732
  name: "read_image",
493
733
  description: "Read an image file (png, jpg, gif, webp) and show it to the model. Use this whenever the user references a screenshot, diagram, or picture file you need to see. Images larger than 2000px are downsampled automatically.",
494
734
  permission: "read",
495
- parameters: schema5,
735
+ parameters: schema6,
496
736
  async execute(args, ctx) {
497
737
  if (ctx.visionEnabled === false) {
498
738
  return {
@@ -500,19 +740,19 @@ var readImageTool = {
500
740
  isError: true
501
741
  };
502
742
  }
503
- const filePath = path5.resolve(ctx.cwd, args.path);
504
- const resolvedPath = await realpath3(filePath).catch(() => null);
743
+ const filePath = path6.resolve(ctx.cwd, args.path);
744
+ const resolvedPath = await realpath4(filePath).catch(() => null);
505
745
  if (isSensitivePath(filePath) || resolvedPath !== null && isSensitivePath(resolvedPath)) {
506
746
  return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
507
747
  }
508
- const mimeType = IMAGE_MIME_TYPES[path5.extname(filePath).toLowerCase()];
748
+ const mimeType = IMAGE_MIME_TYPES[path6.extname(filePath).toLowerCase()];
509
749
  if (!mimeType) {
510
750
  return {
511
751
  content: `Not a supported image (png/jpg/jpeg/gif/webp): ${args.path}`,
512
752
  isError: true
513
753
  };
514
754
  }
515
- const st = await stat4(filePath).catch(() => null);
755
+ const st = await stat5(filePath).catch(() => null);
516
756
  if (!st || !st.isFile()) {
517
757
  return { content: `File not found: ${args.path}`, isError: true };
518
758
  }
@@ -522,7 +762,7 @@ var readImageTool = {
522
762
  isError: true
523
763
  };
524
764
  }
525
- const buf = await readFile3(filePath).catch(() => null);
765
+ const buf = await readFile4(filePath).catch(() => null);
526
766
  if (!buf || buf.length === 0) {
527
767
  return { content: `Failed to read ${args.path}`, isError: true };
528
768
  }
@@ -557,19 +797,19 @@ var readImageTool = {
557
797
 
558
798
  // src/tools/fs/write.ts
559
799
  import { mkdir, writeFile as writeFile2 } from "fs/promises";
560
- import path6 from "path";
561
- import { z as z6 } from "zod";
562
- var schema6 = z6.object({
563
- path: z6.string().describe("File path, absolute or relative to the working directory"),
564
- content: z6.string().describe("Full content to write to the file")
800
+ import path7 from "path";
801
+ import { z as z7 } from "zod";
802
+ var schema7 = z7.object({
803
+ path: z7.string().describe("File path, absolute or relative to the working directory"),
804
+ content: z7.string().describe("Full content to write to the file")
565
805
  });
566
806
  var writeFileTool = {
567
807
  name: "write_file",
568
808
  description: "Write content to a file, creating parent directories as needed. Overwrites existing files. Refuses content with NUL bytes (binary) or over 5MB.",
569
809
  permission: "write",
570
- parameters: schema6,
810
+ parameters: schema7,
571
811
  async execute(args, ctx) {
572
- const filePath = path6.resolve(ctx.cwd, args.path);
812
+ const filePath = path7.resolve(ctx.cwd, args.path);
573
813
  if (args.content.includes("\0")) {
574
814
  return {
575
815
  content: `Refused to write ${args.path}: content contains NUL bytes (write_file is text-only)`,
@@ -585,7 +825,7 @@ var writeFileTool = {
585
825
  }
586
826
  const snapshot = await captureSnapshot(filePath, "write_file", ctx.snapshotContext);
587
827
  try {
588
- await mkdir(path6.dirname(filePath), { recursive: true });
828
+ await mkdir(path7.dirname(filePath), { recursive: true });
589
829
  await writeFile2(filePath, args.content, "utf8");
590
830
  } catch (err) {
591
831
  return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
@@ -596,14 +836,14 @@ var writeFileTool = {
596
836
  };
597
837
 
598
838
  // src/tools/screenshot.ts
599
- import { z as z7 } from "zod";
839
+ import { z as z8 } from "zod";
600
840
 
601
841
  // src/core/screenshot.ts
602
842
  import { execFile } from "child_process";
603
843
  import { randomUUID } from "crypto";
604
- import { readFile as readFile4, unlink } from "fs/promises";
844
+ import { readFile as readFile5, unlink } from "fs/promises";
605
845
  import os from "os";
606
- import path7 from "path";
846
+ import path8 from "path";
607
847
  import { promisify } from "util";
608
848
  var SCREENSHOT_TIMEOUT_MS = 2e4;
609
849
  var execFileAsync = promisify(execFile);
@@ -676,7 +916,7 @@ async function commandExists(command) {
676
916
  }
677
917
  var LINUX_TOOL_NAMES = ["grim", "gnome-screenshot", "spectacle", "import", "scrot"];
678
918
  async function captureScreenshot() {
679
- const outputFile = path7.join(os.tmpdir(), `star-screenshot-${randomUUID()}.png`);
919
+ const outputFile = path8.join(os.tmpdir(), `star-screenshot-${randomUUID()}.png`);
680
920
  try {
681
921
  const tools = {};
682
922
  if (process.platform === "linux") {
@@ -695,7 +935,7 @@ async function captureScreenshot() {
695
935
  const detail = err instanceof Error && err.message ? ` (${err.message})` : "";
696
936
  return { ok: false, error: `${plan.command} failed to capture the screen${detail}` };
697
937
  }
698
- const buf = await readFile4(outputFile).catch(() => null);
938
+ const buf = await readFile5(outputFile).catch(() => null);
699
939
  if (!buf || buf.length === 0) {
700
940
  return { ok: false, error: `${plan.command} produced no image file` };
701
941
  }
@@ -721,13 +961,13 @@ async function captureScreenshot() {
721
961
  }
722
962
 
723
963
  // src/tools/screenshot.ts
724
- var schema7 = z7.object({});
964
+ var schema8 = z8.object({});
725
965
  function createScreenshotTool(capture = captureScreenshot) {
726
966
  return {
727
967
  name: "screenshot",
728
968
  description: "Capture a screenshot of the primary display and see it. Use this to check the actual visual result of a running program (web page, GUI) \u2014 e.g. after starting a dev server \u2014 instead of inferring from code. The capture shows exactly what is visible on screen, including overlapping windows: content hidden behind other windows cannot be seen, so if a specific window matters ask the user to bring it to the front first. Note the capture may include anything else currently on screen.",
729
969
  permission: "read",
730
- parameters: schema7,
970
+ parameters: schema8,
731
971
  async execute(_args, ctx) {
732
972
  if (ctx.visionEnabled === false) {
733
973
  return {
@@ -753,7 +993,7 @@ function createScreenshotTool(capture = captureScreenshot) {
753
993
  var screenshotTool = createScreenshotTool();
754
994
 
755
995
  // src/tools/tasks.ts
756
- import { z as z8 } from "zod";
996
+ import { z as z9 } from "zod";
757
997
 
758
998
  // src/tasks/format.ts
759
999
  function formatDuration(startedAt, endedAt) {
@@ -799,8 +1039,8 @@ function formatTaskFinished(task) {
799
1039
  }
800
1040
 
801
1041
  // src/tools/tasks.ts
802
- var idSchema = z8.object({
803
- id: z8.string().describe("Task id, e.g. task-1 or agent-1")
1042
+ var idSchema = z9.object({
1043
+ id: z9.string().describe("Task id, e.g. task-1 or agent-1")
804
1044
  });
805
1045
  function truncateOutput(s) {
806
1046
  if (s.length <= MAX_OUTPUT) return s;
@@ -826,7 +1066,7 @@ var taskListTool = {
826
1066
  name: "task_list",
827
1067
  description: "List background tasks (shell commands and subagents) with status and runtime.",
828
1068
  permission: "read",
829
- parameters: z8.object({}),
1069
+ parameters: z9.object({}),
830
1070
  execute(_args, ctx) {
831
1071
  const caller = callerId(ctx);
832
1072
  const shell = formatTaskList(
@@ -888,17 +1128,17 @@ var taskKillTool = {
888
1128
  };
889
1129
 
890
1130
  // src/tools/web/search.ts
891
- import { z as z9 } from "zod";
1131
+ import { z as z10 } from "zod";
892
1132
 
893
1133
  // src/llm/debug.ts
894
1134
  import { appendFileSync } from "fs";
895
- import path8 from "path";
1135
+ import path9 from "path";
896
1136
  function debugStreamLog(event, data) {
897
1137
  if (!process.env.STAR_DEBUG) return;
898
1138
  try {
899
1139
  const line = `${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), event, ...data })}
900
1140
  `;
901
- appendFileSync(path8.join(starHome(), "debug.log"), line);
1141
+ appendFileSync(path9.join(starHome(), "debug.log"), line);
902
1142
  } catch {
903
1143
  }
904
1144
  }
@@ -908,9 +1148,9 @@ var TIMEOUT_MS = 15e3;
908
1148
  var DEFAULT_MAX_RESULTS = 5;
909
1149
  var MAX_RESULTS_LIMIT = 10;
910
1150
  var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
911
- var schema8 = z9.object({
912
- query: z9.string().min(1).describe("The search query"),
913
- maxResults: z9.number().int().min(1).max(MAX_RESULTS_LIMIT).optional().describe(
1151
+ var schema9 = z10.object({
1152
+ query: z10.string().min(1).describe("The search query"),
1153
+ maxResults: z10.number().int().min(1).max(MAX_RESULTS_LIMIT).optional().describe(
914
1154
  `Maximum number of results to return (default ${DEFAULT_MAX_RESULTS}, max ${MAX_RESULTS_LIMIT})`
915
1155
  )
916
1156
  });
@@ -1031,7 +1271,7 @@ var webSearchTool = {
1031
1271
  name: "web_search",
1032
1272
  description: "Search the web and return a numbered list of results with title, URL and snippet. Uses DuckDuckGo with automatic fallback to DuckDuckGo Lite and Bing when a source fails or returns nothing. No API key required. Use web_fetch to read a result's full page.",
1033
1273
  permission: "read",
1034
- parameters: schema8,
1274
+ parameters: schema9,
1035
1275
  async execute(args, ctx) {
1036
1276
  const maxResults = Math.min(args.maxResults ?? DEFAULT_MAX_RESULTS, MAX_RESULTS_LIMIT);
1037
1277
  const failures = [];
@@ -1116,6 +1356,7 @@ var ToolRegistry = class {
1116
1356
  editFileTool,
1117
1357
  globTool,
1118
1358
  grepTool,
1359
+ codeOutlineTool,
1119
1360
  bashTool,
1120
1361
  webFetchTool,
1121
1362
  webSearchTool,
@@ -1130,6 +1371,16 @@ var ToolRegistry = class {
1130
1371
  register(tool) {
1131
1372
  this.tools.set(tool.name, tool);
1132
1373
  }
1374
+ // Drops every tool the predicate rejects. Used to restrict a read-only
1375
+ // subagent loop's tool set; the agent loop reads list() lazily per
1376
+ // request, so a retain right after loop construction still shapes the
1377
+ // first request. The permission gate stays as backstop for anything
1378
+ // attempted outside the model's tool map.
1379
+ retain(predicate) {
1380
+ for (const [name, tool] of this.tools) {
1381
+ if (!predicate(tool)) this.tools.delete(name);
1382
+ }
1383
+ }
1133
1384
  // Drops per-session volatile tool state after a wholesale history rewrite
1134
1385
  // (compaction, /undo, resume): anything a tool remembers as "already sent
1135
1386
  // to the model" may no longer be in the context.
@@ -1160,4 +1411,4 @@ export {
1160
1411
  ToolRegistry,
1161
1412
  createDefaultRegistry
1162
1413
  };
1163
- //# sourceMappingURL=chunk-OIJYNQFZ.js.map
1414
+ //# sourceMappingURL=chunk-URC7JXKX.js.map