@cryer/star-cli 0.3.9 → 0.3.11

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