token-goat 2.6.18 → 2.6.19

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.
@@ -49,7 +49,7 @@ var init_define_import_meta_env = __esm({
49
49
  import { createRequire } from "node:module";
50
50
  function resolveVersion() {
51
51
  if (true) {
52
- return "2.6.18";
52
+ return "2.6.19";
53
53
  }
54
54
  const require2 = createRequire(import.meta.url);
55
55
  const pkg = require2("../package.json");
@@ -3212,7 +3212,7 @@ function markerExists(current, marker) {
3212
3212
  }
3213
3213
  const resolved = fs7.realpathSync(markerPath);
3214
3214
  const rel = path8.relative(path8.resolve(current), path8.resolve(resolved));
3215
- return !rel.startsWith("..");
3215
+ return !rel.startsWith("..") && !path8.isAbsolute(rel);
3216
3216
  } catch {
3217
3217
  return false;
3218
3218
  }
@@ -4561,6 +4561,33 @@ CREATE TABLE IF NOT EXISTS hint_suppression_probes (
4561
4561
  PRIMARY KEY (category, harness)
4562
4562
  );
4563
4563
 
4564
+ -- Free-text architecture/rationale notes (the "why" layer -- see notes.ts), attached either to
4565
+ -- a whole file (symbol = '') or to one specific indexed symbol within it (symbol = that
4566
+ -- symbol's name). '' rather than NULL for the whole-file case because SQLite's UNIQUE treats
4567
+ -- NULLs as pairwise-distinct (never conflicting with each other), which would let note-add
4568
+ -- accumulate unlimited duplicate whole-file notes for the same file instead of upserting one;
4569
+ -- '' is a real, comparable value so UNIQUE(file_path, symbol) enforces "at most one note per
4570
+ -- attachment point" for both cases identically. 'fingerprint' is a SHA-256 digest (see
4571
+ -- fingerprintContent in fingerprint.ts) captured at write time of exactly what the note
4572
+ -- describes -- the resolved symbol's current body text for a symbol-scoped note, or a stable
4573
+ -- digest of the file's current top-level symbol manifest (name:kind:line-range per symbol,
4574
+ -- sorted) for a file-scoped note -- so 'token-goat note-list --stale-only' can recompute the
4575
+ -- same fingerprint against the live index later and flag a mismatch (see notes.ts's
4576
+ -- isNoteStale). Staleness detection is purely advisory: nothing here ever auto-rewrites or
4577
+ -- deletes a note's content, only flags that the code it describes has moved since it was
4578
+ -- written -- a human/agent re-review decides what to do with a stale note.
4579
+ CREATE TABLE IF NOT EXISTS notes (
4580
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
4581
+ file_path TEXT NOT NULL,
4582
+ symbol TEXT NOT NULL DEFAULT '',
4583
+ content TEXT NOT NULL,
4584
+ fingerprint TEXT NOT NULL,
4585
+ created_at REAL NOT NULL,
4586
+ updated_at REAL NOT NULL,
4587
+ UNIQUE(file_path, symbol)
4588
+ );
4589
+ CREATE INDEX IF NOT EXISTS idx_notes_file_folded ON notes(TG_LOWER(file_path));
4590
+
4564
4591
  -- Baseline for skill_version_drift.ts's one-shot nudge: the token-goat CLI version (and its
4565
4592
  -- flat command-name set, JSON-encoded) active the moment the token-goat skill's body was
4566
4593
  -- last (re)loaded into this session -- see hooks_skill.ts's postSkillHandler. A session that
@@ -4626,7 +4653,7 @@ CREATE TRIGGER IF NOT EXISTS cache_recall_au AFTER UPDATE ON cache_recall BEGIN
4626
4653
  VALUES (new.row_id, new.label, new.content);
4627
4654
  END;
4628
4655
  `;
4629
- SCHEMA_VERSION = 7;
4656
+ SCHEMA_VERSION = 8;
4630
4657
  MIGRATIONS = {
4631
4658
  // v1 -> v2: adds files.embed_sha, tracked separately from files.sha so embedding freshness can be gated independently of parse freshness (see makeIndexer in worker.ts). A pre-existing v1 database's `files` table predates the column, so it needs an explicit ALTER TABLE here; a brand-new database already has the column from SCHEMA_SQL's CREATE TABLE above, so the ALTER TABLE would fail with "duplicate column name" there -- swallow exactly that error and rethrow anything else, so a genuine ALTER TABLE failure is never silently lost.
4632
4659
  1: (conn) => alterTableIdempotent(conn, "ALTER TABLE files ADD COLUMN embed_sha TEXT"),
@@ -5108,7 +5135,7 @@ function _renderByDaySection(stats) {
5108
5135
  function share(d) {
5109
5136
  return _tokenOrByteShare(d.tokens, d.bytes, stats.totals.tokens, stats.totals.bytes);
5110
5137
  }
5111
- for (const d of [...stats.by_day].sort((a, b) => b.date.localeCompare(a.date))) {
5138
+ for (const d of [...stats.by_day].sort((a, b) => b.date < a.date ? -1 : b.date > a.date ? 1 : 0)) {
5112
5139
  const s = share(d);
5113
5140
  lines2.push(
5114
5141
  _tableRow({
@@ -5718,6 +5745,7 @@ var init_stats = __esm({
5718
5745
  docx_text: SOURCE_READ,
5719
5746
  transcript_outline: SOURCE_READ,
5720
5747
  transcript: SOURCE_READ,
5748
+ video_chapters: SOURCE_READ,
5721
5749
  coverage_report_gaps: SOURCE_READ,
5722
5750
  json_query: SOURCE_READ,
5723
5751
  json_outline: SOURCE_READ,
@@ -5735,6 +5763,14 @@ var init_stats = __esm({
5735
5763
  session_slice: SOURCE_READ,
5736
5764
  gdrive_sections: SOURCE_READ,
5737
5765
  pr_slice: SOURCE_READ,
5766
+ note_read: SOURCE_READ,
5767
+ note_list: SOURCE_READ,
5768
+ // note-add is a write (like insert-section/replace, which record no stat at all -- neither
5769
+ // has a "full source it replaces" savings concept). It still gets an event-only entry here
5770
+ // (no bytesSaved/tokensSaved argument, same as skill_load) purely so `token-goat note-add`
5771
+ // usage is visible in `token-goat stats --full` at all -- SOURCE_OTHER, not SOURCE_READ,
5772
+ // since it is not a token-savings substitute for a read.
5773
+ note_write: SOURCE_OTHER,
5738
5774
  web_fetch: SOURCE_WEB,
5739
5775
  injection_detected: SOURCE_WEB,
5740
5776
  skill_load: SOURCE_SKILL,
@@ -5779,6 +5815,7 @@ var init_stats = __esm({
5779
5815
  "docx-text": /* @__PURE__ */ new Set(["docx_text"]),
5780
5816
  "transcript-outline": /* @__PURE__ */ new Set(["transcript_outline"]),
5781
5817
  transcript: /* @__PURE__ */ new Set(["transcript"]),
5818
+ "video-chapters": /* @__PURE__ */ new Set(["video_chapters"]),
5782
5819
  "coverage-report-gaps": /* @__PURE__ */ new Set(["coverage_report_gaps"]),
5783
5820
  "json-query": /* @__PURE__ */ new Set(["json_query"]),
5784
5821
  "json-outline": /* @__PURE__ */ new Set(["json_outline"]),
@@ -5796,6 +5833,9 @@ var init_stats = __esm({
5796
5833
  "session-slice": /* @__PURE__ */ new Set(["session_slice"]),
5797
5834
  "gdrive-sections": /* @__PURE__ */ new Set(["gdrive_sections"]),
5798
5835
  "pr-slice": /* @__PURE__ */ new Set(["pr_slice"]),
5836
+ "note-add": /* @__PURE__ */ new Set(["note_write"]),
5837
+ "note-get": /* @__PURE__ */ new Set(["note_read"]),
5838
+ "note-list": /* @__PURE__ */ new Set(["note_list"]),
5799
5839
  npm: /* @__PURE__ */ new Set([
5800
5840
  "bash_compress:npm_install",
5801
5841
  "bash_compress:npm_ci",
@@ -8237,6 +8277,7 @@ function formatHeadingTree(headings, filePath) {
8237
8277
  const lines2 = [];
8238
8278
  lines2.push(`Large markdown file (${headings.length} headings). Use token-goat section to read a specific section:`);
8239
8279
  lines2.push(` token-goat section "${filePath}::Heading Name"`);
8280
+ lines2.push(` Tip: an unambiguous heading prefix also resolves (e.g. "Lesson 16" instead of the full heading text) \u2014 shorter to type and avoids shell-quoting issues with punctuation in long headings.`);
8240
8281
  lines2.push(``);
8241
8282
  lines2.push(`Sections:`);
8242
8283
  let headingsAdded = 0;
@@ -11051,12 +11092,12 @@ function parseSectionOrdinal(heading) {
11051
11092
  const match2 = heading.match(/^(.*?)(?:#(\d+))?$/);
11052
11093
  if (!match2) return [heading, 1];
11053
11094
  const baseHeading = match2[1] || heading;
11054
- const ordinal = match2[2] ? Math.max(1, parseInt(match2[2], 10)) : 1;
11055
- return [baseHeading, ordinal];
11095
+ const ordinal2 = match2[2] ? Math.max(1, parseInt(match2[2], 10)) : 1;
11096
+ return [baseHeading, ordinal2];
11056
11097
  }
11057
11098
  function extractNamedSection(body, heading) {
11058
11099
  if (!body || !heading) return null;
11059
- const [baseHeading, ordinal] = parseSectionOrdinal(heading);
11100
+ const [baseHeading, ordinal2] = parseSectionOrdinal(heading);
11060
11101
  const headingLower = stripLower(baseHeading);
11061
11102
  const lines2 = body.split("\n");
11062
11103
  let matchCount = 0;
@@ -11066,7 +11107,7 @@ function extractNamedSection(body, heading) {
11066
11107
  const headingText = stripLower(stripped.slice(3));
11067
11108
  if (headingText === headingLower) {
11068
11109
  matchCount++;
11069
- if (matchCount === ordinal) {
11110
+ if (matchCount === ordinal2) {
11070
11111
  startIdx = i + 1;
11071
11112
  break;
11072
11113
  }
@@ -12326,6 +12367,11 @@ function isNodeModulesPath(p) {
12326
12367
  const check2 = foldPath(p);
12327
12368
  return check2.includes("/node_modules/") || check2.includes("\\node_modules\\");
12328
12369
  }
12370
+ function relPathWithinRoot(root, target) {
12371
+ const rel = path20.relative(root, target).replace(/\\/g, "/");
12372
+ if (rel.startsWith("..") || path20.isAbsolute(rel)) return null;
12373
+ return rel;
12374
+ }
12329
12375
  function _isDocFile(filePath) {
12330
12376
  const lower = filePath.toLowerCase();
12331
12377
  return lower.endsWith(".md") || lower.endsWith(".mdx") || lower.endsWith(".markdown") || lower.endsWith(".rst");
@@ -12490,7 +12536,8 @@ function loadSnapshotDiff(sessionId, normalized, basename19) {
12490
12536
  }
12491
12537
  function scanCrossSessionManifests(projectRoot, projectHash2, filePath, ttlSecs) {
12492
12538
  try {
12493
- const relPath = path20.relative(projectRoot, filePath).replace(/\\/g, "/");
12539
+ const relPath = relPathWithinRoot(projectRoot, filePath);
12540
+ if (relPath === null) return false;
12494
12541
  const foldedRelPath = foldPath(relPath);
12495
12542
  const manifests = readAllSessionManifests(projectHash2, ttlSecs);
12496
12543
  for (const data of manifests) {
@@ -12534,7 +12581,7 @@ function isProtectedRecentRead(normalized, n) {
12534
12581
  if (n <= 0) return false;
12535
12582
  const ranked = Array.from(getSessionFiles().entries()).sort((a, b) => {
12536
12583
  const byRecency = b[1].lastReadAt - a[1].lastReadAt;
12537
- return byRecency !== 0 ? byRecency : a[0].localeCompare(b[0]);
12584
+ return byRecency !== 0 ? byRecency : a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
12538
12585
  });
12539
12586
  const rank = ranked.findIndex(([filePath]) => filePath === normalized);
12540
12587
  return rank !== -1 && rank < n;
@@ -12778,8 +12825,7 @@ function preReadHandlerInner(event) {
12778
12825
  if (!project) {
12779
12826
  project = makeProjectAt(cwd);
12780
12827
  }
12781
- const relPath = path20.relative(project.root, normalized).replace(/\\/g, "/");
12782
- if (!relPath.startsWith("..")) {
12828
+ if (relPathWithinRoot(project.root, normalized) !== null) {
12783
12829
  const ttlSecs = config2.hints.cross_session_read_dedup_ttl_secs;
12784
12830
  if (scanCrossSessionManifests(project.root, project.hash, normalized, ttlSecs)) {
12785
12831
  recordActualRead(event, normalized);
@@ -12916,7 +12962,7 @@ function estimateTruncatedLineCount(normalized) {
12916
12962
  return Infinity;
12917
12963
  }
12918
12964
  function editAnywayHint(normalized) {
12919
- return 'To edit it anyway, use `token-goat replace "' + normalized + '" --old-from <oldfile> --new-from <newfile>` for a snippet edit, or `token-goat write-file "' + normalized + "\" --from <newfile>` to rewrite the whole file \u2014 Read/Edit's own precondition can't be satisfied after this deny.";
12965
+ return 'To edit it anyway, use `token-goat replace "' + normalized + '" --old-b64 <base64> --new-b64 <base64>` (preferred \u2014 no temp files needed) or `--old-from <oldfile> --new-from <newfile>` for a snippet edit, or `token-goat write-file "' + normalized + "\" --b64 <base64>` (or `--from <newfile>`) to rewrite the whole file \u2014 Read/Edit's own precondition can't be satisfied after this deny.";
12920
12966
  }
12921
12967
  function truncatedReadDenyMessage(normalized) {
12922
12968
  return 'File was truncated on last read (>33K tokens). Use `token-goat skeleton "' + normalized + '"` for structure or `token-goat read "' + normalized + '::SymbolName"` for one function. ' + editAnywayHint(normalized);
@@ -12951,8 +12997,8 @@ function postReadHandlerInner(event) {
12951
12997
  const sessionState = exportSessionState();
12952
12998
  const mappedFiles = [];
12953
12999
  for (const fileEntry of sessionState.files) {
12954
- const relPath = path20.relative(project.root, fileEntry.path).replace(/\\/g, "/");
12955
- if (!relPath.startsWith("..")) {
13000
+ const relPath = relPathWithinRoot(project.root, fileEntry.path);
13001
+ if (relPath !== null) {
12956
13002
  mappedFiles.push({
12957
13003
  rel_path: relPath,
12958
13004
  hit_count: fileEntry.readCount
@@ -13895,6 +13941,17 @@ function slideShapes(parsedSlide) {
13895
13941
  function shapeText(shape) {
13896
13942
  return collectTextRuns(shape, "a:t").join(" ").trim();
13897
13943
  }
13944
+ function tableRowBlocks(parsedSlide) {
13945
+ const blocks = [];
13946
+ for (const tbl of collectElements(parsedSlide, "a:tbl")) {
13947
+ for (const row of collectElements(tbl, "a:tr")) {
13948
+ const cellTexts = collectElements(row, "a:tc").map((cell) => collectTextRuns(cell, "a:t").join(" ").trim());
13949
+ const rowText = cellTexts.join(" | ").trim();
13950
+ if (rowText.length > 0) blocks.push(rowText);
13951
+ }
13952
+ }
13953
+ return blocks;
13954
+ }
13898
13955
  async function slidePathsInPresentationOrder(entries) {
13899
13956
  const presXml = decodeZipEntry(entries, "ppt/presentation.xml");
13900
13957
  const relsXml = decodeZipEntry(entries, "ppt/_rels/presentation.xml.rels");
@@ -13994,7 +14051,7 @@ async function pptxSlideText(filePath, slideNumber, includeNotes) {
13994
14051
  const path63 = slidePaths[slideNumber - 1];
13995
14052
  const parsed = await parseSlide(entries, path63);
13996
14053
  const shapes = slideShapes(parsed);
13997
- const blocks = shapes.map(shapeText).filter((t) => t.length > 0);
14054
+ const blocks = [...shapes.map(shapeText).filter((t) => t.length > 0), ...tableRowBlocks(parsed)];
13998
14055
  const lines2 = [`# Slide ${slideNumber}`, ...blocks];
13999
14056
  if (includeNotes) {
14000
14057
  const notes = await notesTextFor(entries, await notesPathFor(entries, path63));
@@ -14791,9 +14848,6 @@ function extractPhp(content, filePath) {
14791
14848
  let braceDepth = 0;
14792
14849
  let inComment = false;
14793
14850
  let mlState = null;
14794
- function currentClass() {
14795
- return contextStack.length > 0 ? contextStack[contextStack.length - 1]?.[0] ?? null : null;
14796
- }
14797
14851
  for (let i = 0; i < lines2.length; i++) {
14798
14852
  const rawLine = lines2[i] ?? "";
14799
14853
  const lineNum = i + 1;
@@ -14857,7 +14911,9 @@ function extractPhp(content, filePath) {
14857
14911
  if (clsM) {
14858
14912
  const kind = clsM[1] ?? "class";
14859
14913
  const name2 = clsM[2] ?? "";
14860
- const parent = currentClass();
14914
+ const preLineDepth = braceDepth - openB + closeB;
14915
+ const topFrame2 = contextStack.length > 0 ? contextStack[contextStack.length - 1] : void 0;
14916
+ const parent = topFrame2 !== void 0 && preLineDepth === topFrame2[1] + 1 ? topFrame2[0] : null;
14861
14917
  symbols.push(makeLineSymbol(filePath, name2, kind, lineNum, stripped.slice(0, 200), parent ?? void 0));
14862
14918
  contextStack.push([name2, braceDepth - openB + closeB, false]);
14863
14919
  if (openB > 0 && openB === closeB) {
@@ -15455,6 +15511,9 @@ function extractScala(content, filePath) {
15455
15511
  const parent = typeStack.length > 0 ? typeStack[typeStack.length - 1].name : void 0;
15456
15512
  symbols.push(makeLineSymbol(filePath, cname, "class", lineNum, stripped.slice(0, 200), parent));
15457
15513
  typeStack.push({ name: cname, startDepth: braceDepth, bodyEntered: false });
15514
+ if (/\bcase\s+class\b/.test(stripped) && !stripStringLiterals(line).includes("{")) {
15515
+ typeStack.pop();
15516
+ }
15458
15517
  matched = true;
15459
15518
  }
15460
15519
  const om = !matched && typeDetectionGateOk && (!isIndented || typeStack.length > 0) ? OBJECT_RE.exec(stripped) : null;
@@ -15463,6 +15522,9 @@ function extractScala(content, filePath) {
15463
15522
  const parent = typeStack.length > 0 ? typeStack[typeStack.length - 1].name : void 0;
15464
15523
  symbols.push(makeLineSymbol(filePath, oname, "object", lineNum, stripped.slice(0, 200), parent));
15465
15524
  typeStack.push({ name: oname, startDepth: braceDepth, bodyEntered: false });
15525
+ if (/\bcase\s+object\b/.test(stripped) && !stripStringLiterals(line).includes("{")) {
15526
+ typeStack.pop();
15527
+ }
15466
15528
  matched = true;
15467
15529
  }
15468
15530
  const tm = !matched && typeDetectionGateOk && (!isIndented || typeStack.length > 0) ? TRAIT_RE.exec(stripped) : null;
@@ -15550,13 +15612,13 @@ var init_scala = __esm({
15550
15612
  init_common();
15551
15613
  IMPORT_RE3 = /^import\s+([A-Za-z_][A-Za-z0-9_.]*(?:\._)?)/;
15552
15614
  BRACE_IMPORT_RE = /^import\s+([A-Za-z_][A-Za-z0-9_.]*)\.\{([^}]*)\}/;
15553
- CLASS_RE3 = /^\s*(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)?\s*class\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s|\[|\(|:|$)/;
15554
- OBJECT_RE = /^\s*(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)?\s*object\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s|:|$)/;
15555
- TRAIT_RE = /^\s*(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)?\s*trait\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s|\[|:|$)/;
15615
+ CLASS_RE3 = /^\s*(?:(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)\s+)*class\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s|\[|\(|:|$)/;
15616
+ OBJECT_RE = /^\s*(?:(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)\s+)*object\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s|:|$)/;
15617
+ TRAIT_RE = /^\s*(?:(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)\s+)*trait\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s|\[|:|$)/;
15556
15618
  ENUM_RE = /^\s*(?:private|protected)?\s*enum\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s|\[|\(|:|$)/;
15557
- FUNC_RE2 = /^\s*(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)?\s*def\s+([A-Za-z_][A-Za-z0-9_]*|[+\-*/%=!<>&|^~]+)(?:\s*\[|\s*\(|\s*:)/;
15558
- VAL_RE = /^\s*(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)?\s*val\s+([A-Za-z_][A-Za-z0-9_]*)/;
15559
- VAR_RE = /^\s*(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)?\s*var\s+([A-Za-z_][A-Za-z0-9_]*)/;
15619
+ FUNC_RE2 = /^\s*(?:(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)\s+)*def\s+([A-Za-z_][A-Za-z0-9_]*|[+\-*/%=!<>&|^~]+)(?:\s*\[|\s*\(|\s*:)/;
15620
+ VAL_RE = /^\s*(?:(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)\s+)*val\s+([A-Za-z_][A-Za-z0-9_]*)/;
15621
+ VAR_RE = /^\s*(?:(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)\s+)*var\s+([A-Za-z_][A-Za-z0-9_]*)/;
15560
15622
  }
15561
15623
  });
15562
15624
 
@@ -15646,8 +15708,9 @@ function extractLua(content, filePath) {
15646
15708
  funcStack.push({ name: "", endKeywordNeeded: true, isBlock: true });
15647
15709
  continue;
15648
15710
  }
15649
- if (stripped === "end" || /^end\s/.test(stripped) || /^end$/.test(stripped)) {
15650
- if (funcStack.length > 0) {
15711
+ if (/^(?:end(?:\s+|$))+$/.test(stripped)) {
15712
+ const popCount = (stripped.match(/\bend\b/g) ?? []).length;
15713
+ for (let k = 0; k < popCount && funcStack.length > 0; k++) {
15651
15714
  funcStack.pop();
15652
15715
  }
15653
15716
  }
@@ -15690,6 +15753,7 @@ function extractElixir(content, filePath) {
15690
15753
  continue;
15691
15754
  }
15692
15755
  const opensDoBlock = /\bdo\s*$/.test(stripped);
15756
+ const opensFnBlock = /\bfn\b/.test(stripped) && !/\bend\b/.test(stripped) && /(?:fn|->)\s*$/.test(stripped);
15693
15757
  const modM = MODULE_RE.exec(stripped);
15694
15758
  if (modM) {
15695
15759
  const modName = modM[1] ?? "";
@@ -15739,7 +15803,7 @@ function extractElixir(content, filePath) {
15739
15803
  }
15740
15804
  continue;
15741
15805
  }
15742
- if (opensDoBlock) {
15806
+ if (opensDoBlock || opensFnBlock) {
15743
15807
  moduleStack.push({ name: "", endKeywordNeeded: true, isBlock: true });
15744
15808
  continue;
15745
15809
  }
@@ -15815,6 +15879,14 @@ function extractDart(content, filePath) {
15815
15879
  typeStack.push({ name: mname, startDepth: braceDepth, bodyEntered: false });
15816
15880
  matched = true;
15817
15881
  }
15882
+ const etm = !matched ? EXTENSION_TYPE_RE.exec(stripped) : null;
15883
+ if (etm) {
15884
+ const etname = etm[1] ?? "";
15885
+ const parent = typeStack.length > 0 ? typeStack[typeStack.length - 1].name : void 0;
15886
+ symbols.push(makeLineSymbol(filePath, etname, "extension_type", lineNum, stripped.slice(0, 200), parent));
15887
+ typeStack.push({ name: etname, startDepth: braceDepth, bodyEntered: false });
15888
+ matched = true;
15889
+ }
15818
15890
  const extm = !matched ? EXTENSION_RE.exec(stripped) : null;
15819
15891
  if (extm) {
15820
15892
  const extname14 = extm[1] ?? "extension";
@@ -15865,7 +15937,7 @@ function extractDart(content, filePath) {
15865
15937
  }
15866
15938
  return { symbols, imports };
15867
15939
  }
15868
- var CLASS_RE4, ENUM_RE2, MIXIN_RE, EXTENSION_RE, FUNC_RE5;
15940
+ var CLASS_RE4, ENUM_RE2, MIXIN_RE, EXTENSION_RE, EXTENSION_TYPE_RE, FUNC_RE5;
15869
15941
  var init_dart = __esm({
15870
15942
  "src/languages/dart.ts"() {
15871
15943
  "use strict";
@@ -15875,6 +15947,7 @@ var init_dart = __esm({
15875
15947
  ENUM_RE2 = /^enum\s+([A-Za-z_][A-Za-z0-9_]*)/;
15876
15948
  MIXIN_RE = /^(?:base\s+)?mixin\s+([A-Za-z_][A-Za-z0-9_]*)/;
15877
15949
  EXTENSION_RE = /^extension\s+(?:([A-Za-z_][A-Za-z0-9_]*)\s+)?on\s+/;
15950
+ EXTENSION_TYPE_RE = /^extension\s+type\s+([A-Za-z_][A-Za-z0-9_]*)/;
15878
15951
  FUNC_RE5 = /(?:^|\s)(?:static\s+)?(?:(?:void|Future|Stream|async|external)\s+|[A-Za-z_][A-Za-z0-9_<>]*(?:\s*\?)?\s+)([A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]*>)?\s*\(/;
15879
15952
  }
15880
15953
  });
@@ -15901,33 +15974,39 @@ function extractZig(content, filePath) {
15901
15974
  }
15902
15975
  const isIndented = line[0] === " " || line[0] === " ";
15903
15976
  let matched = false;
15904
- if (!isIndented || scopeStack.length > 0) {
15977
+ const outerFrame = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
15978
+ const outerDepthInType = outerFrame !== null ? braceDepth - outerFrame.startDepth : 0;
15979
+ const typeDetectionGateOk = scopeStack.length === 0 || outerDepthInType === 1;
15980
+ if (typeDetectionGateOk && (!isIndented || scopeStack.length > 0)) {
15905
15981
  const sm = CONTAINER_RE.exec(stripped);
15906
15982
  if (sm) {
15907
15983
  const sname = sm[1] ?? "";
15908
15984
  const skind = sm[2] ?? "struct";
15909
15985
  if (sname) {
15910
- const parent = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].name : void 0;
15986
+ const parent = outerFrame !== null ? outerFrame.name : void 0;
15911
15987
  symbols.push(makeLineSymbol(filePath, sname, skind, lineNum, stripped.slice(0, 200), parent));
15912
15988
  scopeStack.push({ name: sname, startDepth: braceDepth, bodyEntered: false });
15913
15989
  matched = true;
15914
15990
  }
15915
15991
  }
15916
15992
  }
15917
- if (!matched && !isIndented) {
15993
+ const frame = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
15994
+ if (!matched && !isIndented && frame === null) {
15918
15995
  const fm = FUNC_RE6.exec(stripped);
15919
15996
  if (fm) {
15920
15997
  const fname = fm[2] ?? "";
15921
15998
  symbols.push(makeLineSymbol(filePath, fname, "function", lineNum, stripped.slice(0, 200)));
15922
15999
  matched = true;
15923
16000
  }
15924
- } else if (!matched && scopeStack.length > 0) {
15925
- const fm = FUNC_RE6.exec(stripped);
15926
- if (fm) {
15927
- const fname = fm[2] ?? "";
15928
- const parent = scopeStack[scopeStack.length - 1].name;
15929
- symbols.push(makeLineSymbol(filePath, fname, "function", lineNum, stripped.slice(0, 200), parent));
15930
- matched = true;
16001
+ } else if (!matched && frame !== null) {
16002
+ const depthInType = braceDepth - frame.startDepth;
16003
+ if (depthInType === 1) {
16004
+ const fm = FUNC_RE6.exec(stripped);
16005
+ if (fm) {
16006
+ const fname = fm[2] ?? "";
16007
+ symbols.push(makeLineSymbol(filePath, fname, "function", lineNum, stripped.slice(0, 200), frame.name));
16008
+ matched = true;
16009
+ }
15931
16010
  }
15932
16011
  }
15933
16012
  if (!matched && !isIndented) {
@@ -15948,9 +16027,9 @@ function extractZig(content, filePath) {
15948
16027
  if (ch === "{") {
15949
16028
  braceDepth++;
15950
16029
  if (scopeStack.length > 0) {
15951
- const frame = scopeStack[scopeStack.length - 1];
15952
- if (braceDepth > frame.startDepth) {
15953
- frame.bodyEntered = true;
16030
+ const frame2 = scopeStack[scopeStack.length - 1];
16031
+ if (braceDepth > frame2.startDepth) {
16032
+ frame2.bodyEntered = true;
15954
16033
  }
15955
16034
  }
15956
16035
  } else if (ch === "}") {
@@ -19365,7 +19444,18 @@ var init_parser = __esm({
19365
19444
  // `visit` already descends into `interface_body`/class bodies to find them.
19366
19445
  ["method_signature", "method"],
19367
19446
  ["property_signature", "var"],
19368
- ["abstract_method_signature", "method"]
19447
+ ["abstract_method_signature", "method"],
19448
+ // `namespace Foo { ... }` (and the legacy `module Foo { ... }` synonym) parses as
19449
+ // `internal_module`; `declare module "some-string" { ... }` (an ambient module declaration,
19450
+ // common in .d.ts files) parses as `module` -- a distinct node type from either. Neither had a
19451
+ // kind-map entry, so the namespace/module declaration itself was silently invisible to
19452
+ // `symbol`/`outline`/`skeleton`/`read`, even though everything nested inside it still indexed
19453
+ // fine (the walk recurses into every node's children regardless of the parent's kind-map
19454
+ // membership) -- the same container-drop shape already fixed for C++ `namespace_definition`
19455
+ // and Rust `mod_item`. Both node types expose their name on the standard `name` field
19456
+ // (an identifier, nested_identifier, or string), so `nodeName` resolves it without special-casing.
19457
+ ["internal_module", "namespace"],
19458
+ ["module", "namespace"]
19369
19459
  ]);
19370
19460
  TSJS_FN_SCOPE_TYPES = /* @__PURE__ */ new Set([
19371
19461
  "function_declaration",
@@ -20031,6 +20121,7 @@ function workerHealthCheckMarkerPath(dir) {
20031
20121
  return path28.join(dir, "worker-healthcheck.marker");
20032
20122
  }
20033
20123
  function ensureWorkerAlive(dir = dataDir()) {
20124
+ if (process.env["TOKEN_GOAT_NO_WORKER_SPAWN"] === "1") return;
20034
20125
  const markerPath = workerHealthCheckMarkerPath(dir);
20035
20126
  try {
20036
20127
  const stat2 = fs24.statSync(markerPath);
@@ -24183,7 +24274,7 @@ function tableSectionEndIndex(headers, headerPos, totalLines) {
24183
24274
  }
24184
24275
  return totalLines;
24185
24276
  }
24186
- function resolveHeaderPos(headers, base, ordinal) {
24277
+ function resolveHeaderPos(headers, base, ordinal2) {
24187
24278
  const target = base.toLowerCase();
24188
24279
  const normalizedTarget = normalizeHeading(base).toLowerCase();
24189
24280
  const strippedTarget = normalizeHeadingStrip(base).toLowerCase();
@@ -24204,12 +24295,12 @@ function resolveHeaderPos(headers, base, ordinal) {
24204
24295
  }
24205
24296
  const matches2 = exactMatches.length > 0 ? exactMatches : normalizedMatches.length > 0 ? normalizedMatches : strippedMatches;
24206
24297
  if (matches2.length > 0) {
24207
- const pick3 = ordinal === null ? 0 : ordinal - 1;
24298
+ const pick3 = ordinal2 === null ? 0 : ordinal2 - 1;
24208
24299
  const headerPos = matches2[pick3];
24209
24300
  if (headerPos === void 0) return null;
24210
24301
  return { headerPos, redirectedFrom: null };
24211
24302
  }
24212
- if (ordinal !== null || normalizedTarget.length === 0) return null;
24303
+ if (ordinal2 !== null || normalizedTarget.length === 0) return null;
24213
24304
  let prefixPos = -1;
24214
24305
  const distinct = /* @__PURE__ */ new Set();
24215
24306
  for (let i = 0; i < headers.length; i++) {
@@ -24245,10 +24336,10 @@ function buildSectionResult(headers, kind, lines2, headerPos, redirectedFrom) {
24245
24336
  }
24246
24337
  function resolveSectionFromText(text, headingSpec, language) {
24247
24338
  const { headers, kind } = findHeaders(text, language);
24248
- const { base, ordinal } = parseHeadingSpec(headingSpec, headers);
24339
+ const { base, ordinal: ordinal2 } = parseHeadingSpec(headingSpec, headers);
24249
24340
  if (base.length === 0) return null;
24250
24341
  const lines2 = text.split("\n");
24251
- const resolved = resolveHeaderPos(headers, base, ordinal);
24342
+ const resolved = resolveHeaderPos(headers, base, ordinal2);
24252
24343
  if (resolved === null) return null;
24253
24344
  return buildSectionResult(headers, kind, lines2, resolved.headerPos, resolved.redirectedFrom);
24254
24345
  }
@@ -25195,6 +25286,11 @@ var init_graph_commands = __esm({
25195
25286
  "opaque",
25196
25287
  "mixin",
25197
25288
  "extension",
25289
+ // 'extension_type' (Dart 3.3's zero-cost wrapper type, `extension type Meters(int value)`) is
25290
+ // a distinct declaration kind from a plain 'extension' -- languages/dart.ts's
25291
+ // EXTENSION_TYPE_RE emits it via the same makeLineSymbol code path as 'class'/'mixin'/
25292
+ // 'extension' above, so it needs the same TYPE_KINDS entry those already have.
25293
+ "extension_type",
25198
25294
  "actor",
25199
25295
  // Protocol Buffers (languages/proto_idx.ts's KIND_MAP) uses its own kind strings rather
25200
25296
  // than the generic 'enum'/'interface' -- a proto message is a struct-shaped type, a proto
@@ -25236,7 +25332,26 @@ var init_graph_commands = __esm({
25236
25332
  // excluded from `token-goat types` in its entirety, the same class of gap already fixed for
25237
25333
  // Rust union, Swift protocol/actor, Zig opaque, Dart mixin/extension, proto message/enum/
25238
25334
  // service, Apex class/interface/enum, and GraphQL type/interface/input/enum/union.
25239
- "object"
25335
+ "object",
25336
+ // languages/sfc_idx.ts (Vue/Svelte/Astro single-file components) emits 'sfc_script_class' for
25337
+ // a top-level class declaration in the component's script block, a distinct kind string rather
25338
+ // than the generic 'class' -- so unlike a plain class it never reaches the looksLikeTypeClass
25339
+ // fallback below either, which only ever queries kind === 'class' literally. Every SFC
25340
+ // top-level class was indexed but silently excluded from `token-goat types` in its entirety,
25341
+ // the same class of gap already fixed for Rust union, Swift protocol/actor, Zig opaque, Dart
25342
+ // mixin/extension, proto message/enum/service, Apex class/interface/enum, GraphQL
25343
+ // type/interface/input/enum/union, and Kotlin/Scala object.
25344
+ "sfc_script_class",
25345
+ // languages/graphql_idx.ts's TYPE_RE handler maps EVERY `extend type|interface|input|enum|
25346
+ // union|scalar Foo { ... }` declaration to this one shared kind regardless of which keyword
25347
+ // follows `extend` -- GraphQL's mechanism for adding fields to a type from another file/module,
25348
+ // ubiquitous in federation and schema-stitching -- but unlike the six non-extend KIND_MAP
25349
+ // entries already listed above, this kind was never added here, so every `extend` declaration
25350
+ // was indexed but silently excluded from `token-goat types` in its entirety, the same class of
25351
+ // gap already fixed for the other six GraphQL kinds, Rust union, Swift protocol/actor, Zig
25352
+ // opaque, Dart mixin/extension, proto message/enum/service, Apex class/interface/enum, and
25353
+ // Kotlin/Scala object.
25354
+ "graphql_extend"
25240
25355
  ];
25241
25356
  MAX_CYCLES = 200;
25242
25357
  }
@@ -27679,7 +27794,8 @@ function extractOperations(spec) {
27679
27794
  });
27680
27795
  }
27681
27796
  }
27682
- operations.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
27797
+ const ordinal2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
27798
+ operations.sort((a, b) => ordinal2(a.path, b.path) || ordinal2(a.method, b.method));
27683
27799
  return operations;
27684
27800
  }
27685
27801
  function formatOpenApiOutline(operations) {
@@ -27767,7 +27883,7 @@ function listZipEntries(data) {
27767
27883
  return false;
27768
27884
  }
27769
27885
  });
27770
- entries.sort((a, b) => a.path.localeCompare(b.path));
27886
+ entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
27771
27887
  return entries;
27772
27888
  }
27773
27889
  function formatZipList(entries) {
@@ -28250,7 +28366,11 @@ function hasGap(f) {
28250
28366
  }
28251
28367
  function rankAndFilter(files) {
28252
28368
  const withGaps = files.filter(hasGap);
28253
- withGaps.sort((a, b) => b.uncoveredLineCount - a.uncoveredLineCount || a.filePath.localeCompare(b.filePath));
28369
+ withGaps.sort((a, b) => {
28370
+ const byCount = b.uncoveredLineCount - a.uncoveredLineCount;
28371
+ if (byCount !== 0) return byCount;
28372
+ return a.filePath < b.filePath ? -1 : a.filePath > b.filePath ? 1 : 0;
28373
+ });
28254
28374
  return withGaps;
28255
28375
  }
28256
28376
  function detectCoverageFormat(text) {
@@ -28798,6 +28918,83 @@ var init_screenshot = __esm({
28798
28918
  }
28799
28919
  });
28800
28920
 
28921
+ // src/notes.ts
28922
+ function toNoteRow(row) {
28923
+ return {
28924
+ id: row.id,
28925
+ filePath: row.file_path,
28926
+ symbol: row.symbol,
28927
+ content: row.content,
28928
+ fingerprint: row.fingerprint,
28929
+ createdAt: row.created_at,
28930
+ updatedAt: row.updated_at
28931
+ };
28932
+ }
28933
+ function pickEarliest(matches2) {
28934
+ return matches2.reduce((best, s) => s.lineStart < best.lineStart ? s : best);
28935
+ }
28936
+ function resolveSymbolMatch(filePath, symbolName, dbPath = globalDbPath()) {
28937
+ const matches2 = querySymbols({ filePath, name: symbolName }, dbPath);
28938
+ return matches2.length === 0 ? null : pickEarliest(matches2);
28939
+ }
28940
+ function symbolNamesInFile(filePath, dbPath = globalDbPath()) {
28941
+ const symbols = querySymbols({ filePath, limit: 1e5 }, dbPath);
28942
+ return [...new Set(symbols.map((s) => s.name))].sort();
28943
+ }
28944
+ function computeFileFingerprint(filePath, dbPath = globalDbPath()) {
28945
+ const symbols = querySymbols({ filePath, limit: 1e6 }, dbPath);
28946
+ const manifest = symbols.map((s) => `${s.name}:${s.kind}:${s.lineStart}-${s.lineEnd}`).sort().join("\n");
28947
+ return fingerprintContent(manifest);
28948
+ }
28949
+ function computeSymbolFingerprint(filePath, symbolName, dbPath = globalDbPath()) {
28950
+ const match2 = resolveSymbolMatch(filePath, symbolName, dbPath);
28951
+ return match2 === null ? null : fingerprintContent(match2.body);
28952
+ }
28953
+ function upsertNote(filePath, symbol3, content, fingerprint, dbPath = globalDbPath()) {
28954
+ const db = getDb(dbPath);
28955
+ const now = Date.now() / 1e3;
28956
+ db.prepare(
28957
+ `INSERT INTO notes (file_path, symbol, content, fingerprint, created_at, updated_at)
28958
+ VALUES (?, ?, ?, ?, ?, ?)
28959
+ ON CONFLICT(file_path, symbol) DO UPDATE SET
28960
+ content = excluded.content,
28961
+ fingerprint = excluded.fingerprint,
28962
+ updated_at = excluded.updated_at`
28963
+ ).run(filePath, symbol3, content, fingerprint, now, now);
28964
+ }
28965
+ function getNote(filePath, symbol3, dbPath = globalDbPath()) {
28966
+ const db = getDb(dbPath);
28967
+ const row = db.prepare(
28968
+ `SELECT id, file_path, symbol, content, fingerprint, created_at, updated_at FROM notes WHERE ${pathEqClause("file_path")} AND symbol = ?`
28969
+ ).get(foldPath(filePath), symbol3);
28970
+ return row === void 0 ? null : toNoteRow(row);
28971
+ }
28972
+ function listNotes(dbPath = globalDbPath()) {
28973
+ const db = getDb(dbPath);
28974
+ const rows = db.prepare(
28975
+ "SELECT id, file_path, symbol, content, fingerprint, created_at, updated_at FROM notes ORDER BY file_path, symbol"
28976
+ ).all();
28977
+ return rows.map(toNoteRow);
28978
+ }
28979
+ function isNoteStale(note, dbPath = globalDbPath()) {
28980
+ const current = note.symbol === WHOLE_FILE_NOTE_SYMBOL ? computeFileFingerprint(note.filePath, dbPath) : computeSymbolFingerprint(note.filePath, note.symbol, dbPath);
28981
+ return current === null || current !== note.fingerprint;
28982
+ }
28983
+ var WHOLE_FILE_NOTE_SYMBOL;
28984
+ var init_notes = __esm({
28985
+ "src/notes.ts"() {
28986
+ "use strict";
28987
+ init_define_import_meta_env();
28988
+ init_constants();
28989
+ init_db();
28990
+ init_fingerprint();
28991
+ init_index_reader();
28992
+ init_sql_path();
28993
+ init_util2();
28994
+ WHOLE_FILE_NOTE_SYMBOL = "";
28995
+ }
28996
+ });
28997
+
28801
28998
  // src/ts_refs.ts
28802
28999
  import { createRequire as createRequire6 } from "node:module";
28803
29000
  import * as path45 from "node:path";
@@ -29350,6 +29547,9 @@ function runSection(opts) {
29350
29547
  const heading = opts.spec.slice(colonIdx + 2);
29351
29548
  const result = readSection(filePath, heading);
29352
29549
  if (result === null) {
29550
+ if (!fs36.existsSync(filePath)) {
29551
+ return { text: `File not found: '${filePath}'`, code: 1 };
29552
+ }
29353
29553
  const messages = [`Section '${heading}' not found in '${filePath}'`];
29354
29554
  const available = listSections(filePath);
29355
29555
  if (available.length > 0) messages.push(didYouMean(available));
@@ -29494,7 +29694,7 @@ function runRefsSingle(opts) {
29494
29694
  function groupRefsByFile(refs) {
29495
29695
  const byFile = /* @__PURE__ */ new Map();
29496
29696
  for (const ref2 of refs) byFile.set(ref2.filePath, (byFile.get(ref2.filePath) ?? 0) + 1);
29497
- return [...byFile.entries()].map(([file2, count]) => ({ file: file2, count })).sort((a, b) => b.count - a.count || a.file.localeCompare(b.file));
29697
+ return [...byFile.entries()].map(([file2, count]) => ({ file: file2, count })).sort((a, b) => b.count - a.count || (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
29498
29698
  }
29499
29699
  function renderTopFilesSummary(refs, topN) {
29500
29700
  const grouped = groupRefsByFile(refs);
@@ -31004,6 +31204,11 @@ function extractImports(text, ext2) {
31004
31204
  const m = /^\s*#\s*include\s+[<"]([^>"]+)[>"]/.exec(line);
31005
31205
  if (m) push(m[1]);
31006
31206
  }
31207
+ } else if ([".sh", ".bash"].includes(e)) {
31208
+ for (const line of lines2) {
31209
+ const m = /^\s*(?:source|\.)\s+['"]?([^\s'";]+)['"]?/.exec(line);
31210
+ if (m) push(m[1]);
31211
+ }
31007
31212
  } else if ([".ps1", ".psm1"].includes(e)) {
31008
31213
  for (const line of lines2) {
31009
31214
  const importMod = /^\s*Import-Module\s+(?:-Name\s+)?['"]?([^\s'";]+)/i.exec(line);
@@ -31244,6 +31449,59 @@ ${previewLines(s.body, 3)}`);
31244
31449
  recordReadStat("semantic_search", sumFileSizes(results.map((s) => s.filePath)), text, query);
31245
31450
  return { text, code: 0 };
31246
31451
  }
31452
+ function runNoteGet(opts) {
31453
+ const resolvedPath = resolveIndexPath(opts.file, opts.projectRoot ?? process.cwd());
31454
+ healStaleIndex(resolvedPath);
31455
+ const symbol3 = opts.symbol ?? WHOLE_FILE_NOTE_SYMBOL;
31456
+ const note = getNote(resolvedPath, symbol3);
31457
+ if (note === null) {
31458
+ const where = opts.symbol !== void 0 ? ` for symbol '${opts.symbol}'` : " (whole-file note)";
31459
+ return { text: `No note found for '${opts.file}'${where}`, code: 1 };
31460
+ }
31461
+ const stale = isNoteStale(note);
31462
+ if (opts.json === true) {
31463
+ const payload = {
31464
+ filePath: note.filePath,
31465
+ symbol: note.symbol === WHOLE_FILE_NOTE_SYMBOL ? null : note.symbol,
31466
+ content: note.content,
31467
+ stale,
31468
+ createdAt: note.createdAt,
31469
+ updatedAt: note.updatedAt
31470
+ };
31471
+ const text2 = JSON.stringify(payload, null, 2);
31472
+ recordStat("note_read");
31473
+ return { text: text2, code: 0 };
31474
+ }
31475
+ const target = note.symbol === WHOLE_FILE_NOTE_SYMBOL ? opts.file : `${opts.file}::${note.symbol}`;
31476
+ const staleTag = stale ? " [STALE \u2014 code changed since this note was written]" : "";
31477
+ const text = `# note \u2014 ${target}${staleTag}
31478
+ ${note.content}`;
31479
+ recordStat("note_read");
31480
+ return { text, code: 0 };
31481
+ }
31482
+ function runNoteList(opts = {}) {
31483
+ const withStale = listNotes().map((note) => ({ note, stale: isNoteStale(note) }));
31484
+ const filtered = opts.staleOnly === true ? withStale.filter((n) => n.stale) : withStale;
31485
+ if (opts.json === true) {
31486
+ const items = filtered.map(({ note, stale }) => ({
31487
+ filePath: note.filePath,
31488
+ symbol: note.symbol === WHOLE_FILE_NOTE_SYMBOL ? null : note.symbol,
31489
+ stale,
31490
+ updatedAt: note.updatedAt
31491
+ }));
31492
+ recordStat("note_list");
31493
+ return { text: JSON.stringify(items, null, 2), code: 0 };
31494
+ }
31495
+ recordStat("note_list");
31496
+ if (filtered.length === 0) {
31497
+ return { text: opts.staleOnly === true ? "No stale notes." : "No notes recorded.", code: 0 };
31498
+ }
31499
+ const lines2 = filtered.map(({ note, stale }) => {
31500
+ const target = note.symbol === WHOLE_FILE_NOTE_SYMBOL ? note.filePath : `${note.filePath}::${note.symbol}`;
31501
+ return `${stale ? "[STALE] " : ""}${target}`;
31502
+ });
31503
+ return { text: lines2.join("\n"), code: 0 };
31504
+ }
31247
31505
  var DIDYOUMEAN_LIMIT, GREP_MAX_LINES, FIND_SCAN_LIMIT, REFS_TOP_SCAN_LIMIT, STALE_WARNING, PARENT_IDENTIFIER_RE, SKELETON_SYMBOL_CAP, HUNK_HEADER_RE, DEFAULT_LOG_MAX_COUNT;
31248
31506
  var init_read_commands = __esm({
31249
31507
  "src/read_commands.ts"() {
@@ -31278,6 +31536,7 @@ var init_read_commands = __esm({
31278
31536
  init_pdf_extract();
31279
31537
  init_screenshot();
31280
31538
  init_stats();
31539
+ init_notes();
31281
31540
  init_ts_refs();
31282
31541
  DIDYOUMEAN_LIMIT = 5;
31283
31542
  GREP_MAX_LINES = 200;
@@ -47607,9 +47866,10 @@ async function storeBashOutput(command, output, exitCode, cwd = null) {
47607
47866
  const id = await commandHash(command, cwd);
47608
47867
  const fingerprints = computeBashFingerprints(command, cwd);
47609
47868
  const redactedOutput = redactSecrets(output).text;
47869
+ const redactedCommand = redactSecrets(command).text;
47610
47870
  const entry = {
47611
47871
  id,
47612
- command,
47872
+ command: redactedCommand,
47613
47873
  output: redactedOutput,
47614
47874
  exitCode,
47615
47875
  storedAt: Date.now(),
@@ -47618,7 +47878,7 @@ async function storeBashOutput(command, output, exitCode, cwd = null) {
47618
47878
  };
47619
47879
  _byId.set(id, entry);
47620
47880
  storeBlob(BASH_OUTPUT_SUBDIR, id, entry);
47621
- indexRecallEntry("bash", id, command, `${command}
47881
+ indexRecallEntry("bash", id, redactedCommand, `${redactedCommand}
47622
47882
  ${redactedOutput}`, entry.storedAt);
47623
47883
  return id;
47624
47884
  }
@@ -49142,7 +49402,8 @@ function storeWebOutput(url2, content, dedupKey = url2) {
49142
49402
  _byId2.set(cacheId, redactedContent);
49143
49403
  _urlIndex.set(url2, cacheId);
49144
49404
  storeBlob(WEB_OUTPUT_SUBDIR, cacheId, { url: url2, content: redactedContent });
49145
- indexRecallEntry("web", cacheId, url2, `${url2}
49405
+ const redactedUrl = redactSecrets(url2).text;
49406
+ indexRecallEntry("web", cacheId, redactedUrl, `${redactedUrl}
49146
49407
  ${redactedContent}`, Date.now());
49147
49408
  return cacheId;
49148
49409
  }
@@ -49590,8 +49851,17 @@ function pathStem(p) {
49590
49851
  const dot = name2.lastIndexOf(".");
49591
49852
  return dot > 0 ? name2.slice(0, dot) : name2;
49592
49853
  }
49593
- function positionalArgs(args) {
49594
- return args.filter((a) => !a.startsWith("-"));
49854
+ function positionalArgs(args, valueFlags) {
49855
+ const out2 = [];
49856
+ for (let i = 0; i < args.length; i++) {
49857
+ const a = args[i];
49858
+ if (a.startsWith("-")) {
49859
+ if (valueFlags?.has(a) === true && i + 1 < args.length) i++;
49860
+ continue;
49861
+ }
49862
+ out2.push(a);
49863
+ }
49864
+ return out2;
49595
49865
  }
49596
49866
  var SHORT_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set(["-n", "-c", "-u", "-e"]);
49597
49867
  function stripPrefixes(argv) {
@@ -49729,6 +49999,7 @@ function byteLength(s) {
49729
49999
 
49730
50000
  // src/tool_filters/base.ts
49731
50001
  init_define_import_meta_env();
50002
+ init_secret_redact();
49732
50003
  var CompressedOutput = class {
49733
50004
  constructor(text, originalBytes, compressedBytes, filterName, exitCode = 0, notes = []) {
49734
50005
  this.text = text;
@@ -49898,6 +50169,9 @@ ${stderr.replace(/\s+$/, "")}`;
49898
50169
  const lines2 = body.split("\n");
49899
50170
  if (lines2.length > maxLines) body = truncateMiddleSmart(lines2, maxLines).join("\n");
49900
50171
  body = capBytes(body, maxBytes);
50172
+ const redacted = redactSecrets(body);
50173
+ body = redacted.text;
50174
+ if (redacted.count > 0) notes.push(`redacted ${redacted.count} secret-shaped value(s)`);
49901
50175
  if (notes.length) body = `[${notes.join("; ")}]
49902
50176
  ${body}`;
49903
50177
  return new CompressedOutput(body, originalBytes, byteLength(body), this.name, exitCode, notes);
@@ -53148,7 +53422,9 @@ var _GIT_VALUE_FLAGS = /* @__PURE__ */ new Set([
53148
53422
  "-F",
53149
53423
  "--file",
53150
53424
  "--author",
53151
- "--date"
53425
+ "--date",
53426
+ "--git-dir",
53427
+ "--work-tree"
53152
53428
  ]);
53153
53429
  function gitPositionalArgs(args) {
53154
53430
  const out2 = [];
@@ -55676,6 +55952,7 @@ var _DOCKER_OLD_SHA_RE = /^ *---> (?:sha256:)?[0-9a-f]{12,}\s*$/;
55676
55952
  var _DOCKER_OLD_STEP_RE = /^Step \d+\/\d+ : /;
55677
55953
  var _DOCKER_OLD_SUCCESS_RE = /^Successfully built [0-9a-f]+/;
55678
55954
  var _DOCKER_OLD_INTERMEDIATE_RE = /^Removing intermediate container [0-9a-f]+/;
55955
+ var _DOCKER_OLD_STEP_ERROR_RE = /error|returned a non-zero code/i;
55679
55956
  var DockerFilter = class extends ToolFilter {
55680
55957
  name = "docker";
55681
55958
  binaries = /* @__PURE__ */ new Set(["docker", "buildah", "podman", "nerdctl"]);
@@ -55756,7 +56033,7 @@ var DockerFilter = class extends ToolFilter {
55756
56033
  oldStepErr = false;
55757
56034
  continue;
55758
56035
  }
55759
- if (ol.toLowerCase().includes("error")) oldStepErr = true;
56036
+ if (_DOCKER_OLD_STEP_ERROR_RE.test(ol)) oldStepErr = true;
55760
56037
  oldNew.push(ol);
55761
56038
  }
55762
56039
  if (oldStepHdr !== null) {
@@ -55993,12 +56270,26 @@ function _compressKubectlDescribe(text) {
55993
56270
  }
55994
56271
  return kept.join("\n");
55995
56272
  }
56273
+ var KUBECTL_GLOBAL_VALUE_FLAGS = /* @__PURE__ */ new Set([
56274
+ "-n",
56275
+ "--namespace",
56276
+ "--context",
56277
+ "--kubeconfig",
56278
+ "--cluster",
56279
+ "--user",
56280
+ "-s",
56281
+ "--server",
56282
+ "--token",
56283
+ "--as",
56284
+ "--as-group",
56285
+ "--request-timeout"
56286
+ ]);
55996
56287
  var KubectlFilter = class extends ToolFilter {
55997
56288
  name = "kubectl";
55998
56289
  binaries = /* @__PURE__ */ new Set(["kubectl", "k", "k9s", "oc"]);
55999
56290
  errorPassthrough = true;
56000
56291
  compressBody(stdout, stderr, _exitCode, argv) {
56001
- const pos = positionalArgs(argv.slice(1));
56292
+ const pos = positionalArgs(argv.slice(1), KUBECTL_GLOBAL_VALUE_FLAGS);
56002
56293
  const subcommand = pos[0] ?? "";
56003
56294
  let text = stdout;
56004
56295
  if (subcommand === "get" || subcommand === "top") {
@@ -56180,7 +56471,7 @@ var KubectlLogsFilter = class extends ToolFilter {
56180
56471
  const stem = pathStem(argv[0]).toLowerCase();
56181
56472
  const name2 = pathName(argv[0]).toLowerCase();
56182
56473
  if (!["kubectl", "k"].includes(stem) && !["kubectl", "k"].includes(name2)) return false;
56183
- const pos = positionalArgs(argv.slice(1));
56474
+ const pos = positionalArgs(argv.slice(1), KUBECTL_GLOBAL_VALUE_FLAGS);
56184
56475
  return pos.length > 0 && pos[0] === "logs";
56185
56476
  }
56186
56477
  compressBody(stdout, stderr, _exitCode, _argv) {
@@ -56626,6 +56917,19 @@ var awsFilter = new AwsFilter();
56626
56917
  var _AWS_UPLOAD_RE = /^upload:\s+\S+\s+to\s+s3:\/\//i;
56627
56918
  var _AWS_DOWNLOAD_RE = /^download:\s+s3:\/\//i;
56628
56919
  var _AWS_S3_PROGRESS_RE = /^(?:Completed\s+\d|\d+(?:\.\d+)?\s*(?:KiB|MiB|GiB|B)\/s|Calculating|upload\s+failed:|download\s+failed:)/i;
56920
+ var AWS_GLOBAL_VALUE_FLAGS = /* @__PURE__ */ new Set([
56921
+ "--profile",
56922
+ "--region",
56923
+ "--endpoint-url",
56924
+ "--output",
56925
+ "--query",
56926
+ "--color",
56927
+ "--ca-bundle",
56928
+ "--cli-read-timeout",
56929
+ "--cli-connect-timeout",
56930
+ "--cli-binary-format",
56931
+ "--cli-pager"
56932
+ ]);
56629
56933
  var AwsCliFilter = class extends ToolFilter {
56630
56934
  name = "aws-cli";
56631
56935
  binaries = /* @__PURE__ */ new Set(["aws", "aws2"]);
@@ -56633,7 +56937,7 @@ var AwsCliFilter = class extends ToolFilter {
56633
56937
  _JSON_ARRAY_THRESHOLD = 10;
56634
56938
  _JSON_ARRAY_KEEP = 3;
56635
56939
  compressBody(stdout, stderr, _exitCode, argv) {
56636
- const positionals = positionalArgs(argv.slice(1));
56940
+ const positionals = positionalArgs(argv.slice(1), AWS_GLOBAL_VALUE_FLAGS);
56637
56941
  const isS3Transfer = positionals.length >= 2 && positionals[0] === "s3" && (positionals[1] === "cp" || positionals[1] === "sync" || positionals[1] === "mv");
56638
56942
  const isCfnEvents = positionals.length >= 2 && positionals[0] === "cloudformation" && positionals[1] === "describe-stack-events";
56639
56943
  let text = stdout;
@@ -58024,6 +58328,7 @@ var _GH_RUN_PASS_STEP_RE = /^\s*[✓√]\s/;
58024
58328
  var _GH_RUN_FAIL_STEP_RE = /^\s*[X✗❌]\s|^\s*FAIL(:|ED|URE)\b|^\s*Error:\s/;
58025
58329
  var _GH_API_URL_SUFFIX = "_url";
58026
58330
  var _GH_API_URL_KEEP = /* @__PURE__ */ new Set(["html_url", "avatar_url", "clone_url", "ssh_url"]);
58331
+ var GH_GLOBAL_VALUE_FLAGS = /* @__PURE__ */ new Set(["-R", "--repo", "--hostname"]);
58027
58332
  var _GH_API_NOISE_KEYS = /* @__PURE__ */ new Set(["gravatar_id", "site_admin"]);
58028
58333
  var _GH_CONTENT_B64_RE = /^[A-Za-z0-9+/=\n]+$/;
58029
58334
  var _GH_BASE64_MIN_LEN = 200;
@@ -58308,7 +58613,7 @@ var GhFilter = class extends ToolFilter {
58308
58613
  binaries = /* @__PURE__ */ new Set(["gh"]);
58309
58614
  compress(stdout, stderr, _exitCode, argv) {
58310
58615
  const redactedStdout = redactGhBase64Content(stdout);
58311
- const positionals = positionalArgs(argv.slice(1));
58616
+ const positionals = positionalArgs(argv.slice(1), GH_GLOBAL_VALUE_FLAGS);
58312
58617
  const subcommand = positionals[0] ?? "";
58313
58618
  const action = positionals[1] ?? "";
58314
58619
  const merged = this.combineOutput(redactedStdout, stderr);
@@ -59113,9 +59418,38 @@ var LsFilter = class _LsFilter extends ToolFilter {
59113
59418
  var _EZA_PASSTHROUGH = 30;
59114
59419
  var _HEADER_KEYWORDS = /* @__PURE__ */ new Set(["permission", "size", "date", "user", "name"]);
59115
59420
  var _SUMMARY_KEYWORDS = ["director", "file", "total"];
59116
- var EzaFilter = class extends ToolFilter {
59421
+ var EzaFilter = class _EzaFilter extends ToolFilter {
59117
59422
  name = "eza";
59118
59423
  binaries = /* @__PURE__ */ new Set(["eza", "exa", "ls"]);
59424
+ // Flags eza/exa support that plain GNU/BSD `ls` does not -- used to disambiguate the shared
59425
+ // 'ls' binary claim below (a common `alias ls=eza` setup means the literal command text is
59426
+ // "ls ...", but token-goat only ever sees that raw text, never the shell's alias resolution).
59427
+ static _EZA_ONLY_FLAGS = /* @__PURE__ */ new Set([
59428
+ "--tree",
59429
+ "-T",
59430
+ "--icons",
59431
+ "--no-icons",
59432
+ "--git",
59433
+ "--git-repos",
59434
+ "--git-repos-no-status",
59435
+ "--level"
59436
+ ]);
59437
+ // LsFilter (registered before EzaFilter in SHELL_FILE_FILTERS) always wins a plain `ls`
59438
+ // invocation since binary-name matching alone can't tell a real GNU/BSD `ls` from an
59439
+ // `alias ls=eza` shell alias -- the literal command text is "ls ..." either way. Without this
59440
+ // gate, EzaFilter's own 'ls' binary claim was permanently unreachable dead code: LsFilter's
59441
+ // generic ls-format compressor (no awareness of eza's tree/column output) silently ran on
59442
+ // every aliased `ls --tree`/`ls --icons`/etc. invocation instead. Mirrors RgFilter's own
59443
+ // `_hasContextFlags` gate, which resolves the same kind of shared-binary ambiguity between
59444
+ // itself and GrepFilter.
59445
+ matches(argv) {
59446
+ if (!super.matches(argv)) return false;
59447
+ const first2 = argv[0];
59448
+ const stem = pathStem(first2).toLowerCase();
59449
+ const name2 = pathName(first2).toLowerCase();
59450
+ if (stem !== "ls" && name2 !== "ls") return true;
59451
+ return argv.slice(1).some((a) => _EzaFilter._EZA_ONLY_FLAGS.has(a));
59452
+ }
59119
59453
  compressBody(stdout, stderr, _exitCode, argv) {
59120
59454
  const merged = this.combineOutput(stdout, stderr);
59121
59455
  const text = normalise(merged);
@@ -59873,9 +60207,11 @@ var SHELL_FILE_FILTERS = [
59873
60207
  ffmpegFilter,
59874
60208
  // Diff tool (plain POSIX diff; git diff is handled by GitFilter)
59875
60209
  diffFilter,
59876
- // Directory listings — LsFilter (simple) before EzaFilter (richer tree/column-aware)
59877
- lsFilter,
60210
+ // Directory listings — EzaFilter before LsFilter: EzaFilter's matches() gate falls through to
60211
+ // LsFilter for a plain `ls` with no eza-only flag, but must run FIRST so it can actually claim
60212
+ // an aliased `ls --tree`/`ls --icons`/etc. invocation (see EzaFilter.matches doc comment).
59878
60213
  ezaFilter,
60214
+ lsFilter,
59879
60215
  fdFilter,
59880
60216
  wcFilter,
59881
60217
  treeFilter,
@@ -60385,7 +60721,7 @@ var flutterFilter = new FlutterFilter();
60385
60721
  var DART_ANALYZING_RE = /^Analyzing\s/;
60386
60722
  var DART_ANALYZE_RESULT_RE = /^(?:No issues found!|\d+ issue[s]? found\.|warning -|error -|info -|hint -)/;
60387
60723
  var DART_TEST_PROGRESS_RE = /^\d{2}:\d{2}\s+[+\d]|^[.]+$/;
60388
- var DART_COMPILE_DONE_RE = /^(?:Generated:\s|Compiling\s)/;
60724
+ var DART_COMPILE_DONE_RE = /^Generated:\s/;
60389
60725
  var DART_TEST_SUMMARY_RE = /(?:All tests passed\.?|\d+\s+test[s]?\s+(?:passed|failed))/;
60390
60726
  var PUB_KEEP_RE2 = /^(?:Resolving dependencies|Changed \d+|No dependencies changed|Got dependencies|Downloading packages|Building package executable)/;
60391
60727
  var PUB_PKG_LINE_RE2 = /^[+>!]\s+\S+\s+\S+/;
@@ -62727,6 +63063,7 @@ function recordSavings(result) {
62727
63063
 
62728
63064
  // src/cli.ts
62729
63065
  init_read_commands();
63066
+ init_notes();
62730
63067
 
62731
63068
  // src/bridges_status.ts
62732
63069
  init_define_import_meta_env();
@@ -63186,6 +63523,7 @@ function formatCues(cues) {
63186
63523
  init_graph_commands();
63187
63524
  init_skill_cache();
63188
63525
  init_hooks_read();
63526
+ init_section_reader();
63189
63527
  init_util2();
63190
63528
  init_ansi();
63191
63529
  init_config();
@@ -66533,6 +66871,9 @@ import * as fs43 from "node:fs";
66533
66871
  import * as path53 from "node:path";
66534
66872
  var MAX_ENTRIES = 30;
66535
66873
  var KEY_RE2 = /^[A-Za-z0-9_-]{1,80}$/;
66874
+ function ordinal(a, b) {
66875
+ return a < b ? -1 : a > b ? 1 : 0;
66876
+ }
66536
66877
  function memoryPath(projectHash2) {
66537
66878
  return path53.join(dataDir(), "projects", `${projectHash2}_memory.toml`);
66538
66879
  }
@@ -66587,7 +66928,7 @@ function loadRaw(filePath) {
66587
66928
  }
66588
66929
  function save(filePath, entries) {
66589
66930
  const lines2 = [];
66590
- const sorted = Object.entries(entries).sort(([a], [b]) => a.localeCompare(b));
66931
+ const sorted = Object.entries(entries).sort(([a], [b]) => ordinal(a, b));
66591
66932
  for (const [k, v] of sorted) {
66592
66933
  const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n");
66593
66934
  lines2.push(`${k} = "${escaped}"`);
@@ -66610,7 +66951,7 @@ function setEntry(projectHash2, key, value) {
66610
66951
  const isNewKey = !(key in entries);
66611
66952
  if (isNewKey && Object.keys(entries).length >= MAX_ENTRIES) {
66612
66953
  const keysToKeep = MAX_ENTRIES - 1;
66613
- const allKeys = Object.keys(entries).sort((a, b) => a.localeCompare(b));
66954
+ const allKeys = Object.keys(entries).sort((a, b) => ordinal(a, b));
66614
66955
  for (const k of allKeys.slice(keysToKeep)) {
66615
66956
  delete entries[k];
66616
66957
  }
@@ -67255,7 +67596,7 @@ function parseRequirementsTxt(content) {
67255
67596
  const deps = [];
67256
67597
  for (const raw of splitLines2(content)) {
67257
67598
  if (/^\s*#/.test(raw)) continue;
67258
- const eggMatch = /^\s*(?:git|hg|svn|bzr)\+.*#egg=([A-Za-z0-9_.-]+)/.exec(raw);
67599
+ const eggMatch = /^\s*(?:-e\s+|--editable[\s=]+)?(?:git|hg|svn|bzr)\+.*#egg=([A-Za-z0-9_.-]+)/.exec(raw);
67259
67600
  if (eggMatch !== null) {
67260
67601
  deps.push({ name: eggMatch[1] ?? "", version: "", kind: "unknown" });
67261
67602
  continue;
@@ -67823,8 +68164,11 @@ function resolveTypesLocation(pkgDir, pkgJson, nodeModulesDir, pkgName) {
67823
68164
  try {
67824
68165
  const typesPkgJson = JSON.parse(typesPkgJsonRaw);
67825
68166
  const entry = (typeof typesPkgJson["types"] === "string" ? typesPkgJson["types"] : void 0) ?? (typeof typesPkgJson["main"] === "string" ? typesPkgJson["main"] : void 0) ?? "index.d.ts";
67826
- const entryPath = path55.join(typesPkgDir, entry.endsWith(".d.ts") ? entry : `${entry}.d.ts`);
67827
- if (fileExists2(entryPath)) return { path: entryPath, source: "@types" };
68167
+ const entryCandidates = [entry, entry.endsWith(".d.ts") ? entry : `${entry}.d.ts`, entry.replace(/\.[cm]?[jt]s$/, ".d.ts")];
68168
+ for (const c of entryCandidates) {
68169
+ const entryPath = path55.join(typesPkgDir, c);
68170
+ if (fileExists2(entryPath)) return { path: entryPath, source: "@types" };
68171
+ }
67828
68172
  } catch {
67829
68173
  }
67830
68174
  const fallback = path55.join(typesPkgDir, "index.d.ts");
@@ -69475,8 +69819,11 @@ var FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "Noteboo
69475
69819
  function extractFilePath(name2, input) {
69476
69820
  if (!FILE_PATH_TOOLS.has(name2)) return null;
69477
69821
  if (input === null || typeof input !== "object") return null;
69478
- const fp = input["file_path"];
69479
- return typeof fp === "string" ? fp : null;
69822
+ const o = input;
69823
+ const fp = o["file_path"];
69824
+ if (typeof fp === "string") return fp;
69825
+ const notebookPath = o["notebook_path"];
69826
+ return typeof notebookPath === "string" ? notebookPath : null;
69480
69827
  }
69481
69828
  function extractCommand(name2, input) {
69482
69829
  if (name2 !== "Bash") return null;
@@ -70769,7 +71116,11 @@ function cmdVideoChapters(file2) {
70769
71116
  }
70770
71117
  lines2.push("(extract a subtitle stream to .vtt/.srt with ffmpeg, then use transcript/transcript-outline on it)");
70771
71118
  }
70772
- out(lines2.join("\n"));
71119
+ const text = lines2.join("\n");
71120
+ out(text);
71121
+ const fullSourceBytes = fileSizeOrZero(file2);
71122
+ const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(text, "utf8"));
71123
+ recordStat("video_chapters", bytesSaved, Math.round(bytesSaved / 4));
70773
71124
  }
70774
71125
  function formatVideoTimestamp(totalSeconds) {
70775
71126
  const hours = Math.floor(totalSeconds / 3600);
@@ -71406,6 +71757,50 @@ function decodeBase64Buffer(payload, label) {
71406
71757
  }
71407
71758
  return Buffer.from(normalized, "base64");
71408
71759
  }
71760
+ function cmdNoteAdd(file2, opts) {
71761
+ if (!file2 || !file2.trim()) {
71762
+ throw new CliError("file path cannot be empty");
71763
+ }
71764
+ const usingFrom = opts.contentFrom !== void 0;
71765
+ const usingB64 = opts.contentB64 !== void 0;
71766
+ if (usingFrom && usingB64) {
71767
+ throw new CliError("cannot mix --content-from with --content-b64");
71768
+ }
71769
+ if (!usingFrom && !usingB64) {
71770
+ throw new CliError("must provide either --content-from or --content-b64");
71771
+ }
71772
+ const contentBytes = usingFrom ? readFileBoundedRaw(opts.contentFrom, "--content-from") : decodeBase64Buffer(opts.contentB64, "--content-b64");
71773
+ if (contentBytes.length === 0) {
71774
+ throw new CliError("note content cannot be empty");
71775
+ }
71776
+ if (Buffer.compare(Buffer.from(contentBytes.toString("utf8"), "utf8"), contentBytes) !== 0) {
71777
+ throw new CliError("note content must be valid UTF-8 text");
71778
+ }
71779
+ const resolvedPath = resolveIndexPath(file2);
71780
+ if (!fs52.existsSync(resolvedPath)) {
71781
+ throw new CliError(`File not found: '${resolvedPath}'`);
71782
+ }
71783
+ healStaleIndex(resolvedPath);
71784
+ let symbol3 = WHOLE_FILE_NOTE_SYMBOL;
71785
+ let fingerprint;
71786
+ if (opts.symbol !== void 0) {
71787
+ const match2 = resolveSymbolMatch(resolvedPath, opts.symbol);
71788
+ if (match2 === null) {
71789
+ const messages = [`No symbol named '${opts.symbol}' is indexed in '${file2}'`];
71790
+ const available = symbolNamesInFile(resolvedPath);
71791
+ if (available.length > 0) messages.push(didYouMean(available));
71792
+ throw new CliError(messages.join("\n"));
71793
+ }
71794
+ symbol3 = opts.symbol;
71795
+ fingerprint = fingerprintContent(match2.body);
71796
+ } else {
71797
+ fingerprint = computeFileFingerprint(resolvedPath);
71798
+ }
71799
+ upsertNote(resolvedPath, symbol3, contentBytes.toString("utf8"), fingerprint);
71800
+ const target = opts.symbol !== void 0 ? `${file2}::${opts.symbol}` : file2;
71801
+ out(`Note saved: ${target} (fingerprint ${fingerprint.slice(0, 12)})`);
71802
+ recordStat("note_write");
71803
+ }
71409
71804
  function cmdWriteFile(dest, opts) {
71410
71805
  validateWritablePath(dest, "destination");
71411
71806
  if (opts.from !== void 0 && opts.b64 !== void 0) {
@@ -71511,6 +71906,56 @@ function diagnoseNearMiss(targetText, oldText) {
71511
71906
  }
71512
71907
  return void 0;
71513
71908
  }
71909
+ function detectDominantEol(buf) {
71910
+ let crlf = 0;
71911
+ let lfOnly = 0;
71912
+ for (let i = 0; i < buf.length; i++) {
71913
+ if (buf[i] === 10) {
71914
+ if (i > 0 && buf[i - 1] === 13) crlf++;
71915
+ else lfOnly++;
71916
+ }
71917
+ }
71918
+ return crlf > lfOnly ? "\r\n" : "\n";
71919
+ }
71920
+ function normalizeEolToMatch(source, reference) {
71921
+ const eol = detectDominantEol(reference);
71922
+ const CR = 13;
71923
+ const LF = 10;
71924
+ const collapsed = [];
71925
+ for (let i = 0; i < source.length; i++) {
71926
+ if (source[i] === CR && source[i + 1] === LF) continue;
71927
+ collapsed.push(source[i]);
71928
+ }
71929
+ if (eol === "\n") return Buffer.from(collapsed);
71930
+ const expanded = [];
71931
+ for (const b of collapsed) {
71932
+ if (b === LF) expanded.push(CR, LF);
71933
+ else expanded.push(b);
71934
+ }
71935
+ return Buffer.from(expanded);
71936
+ }
71937
+ var MAX_CLOSEST_MATCH_COMPARISONS = 2e6;
71938
+ function findClosestLineWindow(targetText, oldText) {
71939
+ const targetLines = targetText.split("\n");
71940
+ const oldLines = oldText.split("\n");
71941
+ const windowSize = oldLines.length;
71942
+ if (windowSize === 0 || windowSize > targetLines.length) return void 0;
71943
+ if ((targetLines.length - windowSize + 1) * windowSize > MAX_CLOSEST_MATCH_COMPARISONS) return void 0;
71944
+ let bestIdx = -1;
71945
+ let bestScore = 0;
71946
+ for (let i = 0; i <= targetLines.length - windowSize; i++) {
71947
+ let score = 0;
71948
+ for (let j = 0; j < windowSize; j++) {
71949
+ if (targetLines[i + j] === oldLines[j]) score++;
71950
+ }
71951
+ if (score > bestScore) {
71952
+ bestScore = score;
71953
+ bestIdx = i;
71954
+ }
71955
+ }
71956
+ if (bestIdx === -1) return void 0;
71957
+ return { lineStart: bestIdx + 1, region: targetLines.slice(bestIdx, bestIdx + windowSize).join("\n") };
71958
+ }
71514
71959
  function cmdReplace(file2, opts) {
71515
71960
  validateWritablePath(file2, "target file");
71516
71961
  const targetBuf = readFileBoundedRaw(file2, "target file", true);
@@ -71538,19 +71983,32 @@ function cmdReplace(file2, opts) {
71538
71983
  }
71539
71984
  const oldBytes = usingFrom ? readFileBoundedRaw(opts.oldFrom, "--old-from") : decodeBase64Buffer(opts.oldB64, "--old-b64");
71540
71985
  const newBytes = usingFrom ? readFileBoundedRaw(opts.newFrom, "--new-from") : decodeBase64Buffer(opts.newB64, "--new-b64");
71541
- if (oldBytes.length === 0) {
71986
+ const normalizedOldBytes = opts.normalizeNewlines === true ? normalizeEolToMatch(oldBytes, targetBuf) : oldBytes;
71987
+ const normalizedNewBytes = opts.normalizeNewlines === true ? normalizeEolToMatch(newBytes, targetBuf) : newBytes;
71988
+ if (normalizedOldBytes.length === 0) {
71542
71989
  throw new CliError("old string cannot be empty");
71543
71990
  }
71544
71991
  const matches2 = [];
71545
71992
  let cursor = 0;
71546
- while ((cursor = targetBuf.indexOf(oldBytes, cursor)) !== -1) {
71993
+ while ((cursor = targetBuf.indexOf(normalizedOldBytes, cursor)) !== -1) {
71547
71994
  matches2.push(cursor);
71548
- cursor += oldBytes.length;
71995
+ cursor += normalizedOldBytes.length;
71549
71996
  }
71550
71997
  const occurrences = matches2.length;
71551
71998
  if (occurrences === 0) {
71552
- const nearMiss = diagnoseNearMiss(targetBuf.toString("utf8"), oldBytes.toString("utf8"));
71553
- throw new CliError(nearMiss !== void 0 ? `old string not found in ${file2} \u2014 ${nearMiss}` : `old string not found in ${file2}`);
71999
+ const nearMiss = diagnoseNearMiss(targetBuf.toString("utf8"), normalizedOldBytes.toString("utf8"));
72000
+ if (nearMiss !== void 0) {
72001
+ throw new CliError(`old string not found in ${file2} \u2014 ${nearMiss}`);
72002
+ }
72003
+ const closest = findClosestLineWindow(targetBuf.toString("utf8"), normalizedOldBytes.toString("utf8"));
72004
+ if (closest !== void 0) {
72005
+ const diff = buildLineDiff(closest.region, normalizedOldBytes.toString("utf8"), file2);
72006
+ throw new CliError(
72007
+ `old string not found in ${file2} \u2014 closest match at line ${closest.lineStart} (showing: what's actually there vs. what --old-from/--old-b64 searched for):
72008
+ ${diff}`
72009
+ );
72010
+ }
72011
+ throw new CliError(`old string not found in ${file2}`);
71554
72012
  }
71555
72013
  if (occurrences > 1 && !opts.all) {
71556
72014
  throw new CliError(`old string appears ${occurrences} times in ${file2} \u2014 pass --all to replace every occurrence, or provide a more specific match`);
@@ -71559,8 +72017,8 @@ function cmdReplace(file2, opts) {
71559
72017
  let prevEnd = 0;
71560
72018
  for (const pos of matches2) {
71561
72019
  parts.push(targetBuf.subarray(prevEnd, pos));
71562
- parts.push(newBytes);
71563
- prevEnd = pos + oldBytes.length;
72020
+ parts.push(normalizedNewBytes);
72021
+ prevEnd = pos + normalizedOldBytes.length;
71564
72022
  }
71565
72023
  parts.push(targetBuf.subarray(prevEnd));
71566
72024
  const replacedBuf = Buffer.concat(parts);
@@ -71587,6 +72045,65 @@ function cmdReplace(file2, opts) {
71587
72045
  enqueueDirtyPathSafe(file2);
71588
72046
  out(`replaced ${occurrences} occurrence${occurrences === 1 ? "" : "s"} in ${file2}`);
71589
72047
  }
72048
+ function cmdInsertSection(file2, opts) {
72049
+ validateWritablePath(file2, "target file");
72050
+ const usingFrom = opts.contentFrom !== void 0;
72051
+ const usingB64 = opts.contentB64 !== void 0;
72052
+ if (usingFrom && usingB64) {
72053
+ throw new CliError("cannot mix --content-from with --content-b64");
72054
+ }
72055
+ if (!usingFrom && !usingB64) {
72056
+ throw new CliError("must provide either --content-from or --content-b64");
72057
+ }
72058
+ const contentBytes = usingFrom ? readFileBoundedRaw(opts.contentFrom, "--content-from") : decodeBase64Buffer(opts.contentB64, "--content-b64");
72059
+ if (contentBytes.length === 0) {
72060
+ throw new CliError("content to insert cannot be empty");
72061
+ }
72062
+ let preWriteStat;
72063
+ try {
72064
+ preWriteStat = fs52.statSync(file2);
72065
+ } catch {
72066
+ }
72067
+ const result = readSection(file2, opts.after);
72068
+ if (result === null) {
72069
+ const available = listSections(file2);
72070
+ const messages = [`Section '${opts.after}' not found in '${file2}'`];
72071
+ if (available.length > 0) messages.push(didYouMean(available));
72072
+ throw new CliError(messages.join("\n"));
72073
+ }
72074
+ let rawText;
72075
+ try {
72076
+ rawText = fs52.readFileSync(file2, "utf-8");
72077
+ } catch (e) {
72078
+ mapFsError(e, void 0, file2);
72079
+ }
72080
+ if (rawText.charCodeAt(0) === 65279) rawText = rawText.slice(1);
72081
+ const eol = detectDominantEol(Buffer.from(rawText, "utf8"));
72082
+ const lfLines = rawText.replace(/\r\n/g, "\n").split("\n");
72083
+ const insertAt = result.lineEnd;
72084
+ const insertedLines = contentBytes.toString("utf8").replace(/\r\n/g, "\n").split("\n");
72085
+ if (insertedLines.length > 0 && insertedLines[insertedLines.length - 1] === "") insertedLines.pop();
72086
+ const mergedLfText = [...lfLines.slice(0, insertAt), ...insertedLines, ...lfLines.slice(insertAt)].join("\n");
72087
+ const mergedText = eol === "\n" ? mergedLfText : mergedLfText.replace(/\n/g, "\r\n");
72088
+ if (preWriteStat !== void 0) {
72089
+ let preRenameStat;
72090
+ try {
72091
+ preRenameStat = fs52.statSync(file2);
72092
+ } catch {
72093
+ }
72094
+ if (preRenameStat !== void 0 && (preRenameStat.mtimeMs !== preWriteStat.mtimeMs || preRenameStat.size !== preWriteStat.size)) {
72095
+ throw new CliError(`${file2} changed on disk while insert-section was running -- the file was modified concurrently, so the insert was NOT applied. Retry.`);
72096
+ }
72097
+ }
72098
+ try {
72099
+ atomicWriteBuffer(file2, Buffer.from(mergedText, "utf8"));
72100
+ } catch (e) {
72101
+ mapFsError(e, void 0, file2);
72102
+ }
72103
+ enqueueDirtyPathSafe(file2);
72104
+ const redirectNote = result.redirectedFrom !== void 0 ? ` (redirected from: '${result.redirectedFrom}')` : "";
72105
+ out(`inserted after '${result.heading}'${redirectNote} in ${file2}`);
72106
+ }
71590
72107
  async function cmdGdriveSections(fileId, opts) {
71591
72108
  const fetchOpts = { fresh: opts.fresh === true };
71592
72109
  const text = await fetchDoc(fileId, fetchOpts);
@@ -71801,7 +72318,9 @@ function buildProgram() {
71801
72318
  })
71802
72319
  )
71803
72320
  );
71804
- program2.command("section <spec>").description("read one section from a file (spec: file::heading), or list all sections with --list").option("-j, --json", "output as JSON").option("--list", "list all section headings in the file instead of reading one").action(
72321
+ program2.command("section <spec>").description(
72322
+ 'read one section from a file (spec: file::heading, or file::<unambiguous heading prefix> \u2014 e.g. "Lesson 16" resolves a longer unique heading), or list all sections with --list'
72323
+ ).option("-j, --json", "output as JSON").option("--list", "list all section headings in the file instead of reading one").action(
71805
72324
  (spec, opts) => opts.list === true ? runExit(() => runListSections({ file: spec, ...opts.json === true ? { json: true } : {} })) : runExitText(() => runSection({ spec, ...opts.json === true ? { json: true } : {} }))
71806
72325
  );
71807
72326
  program2.command("semantic <query>").description("semantic search (falls back to full-text search)").option("-l, --limit <n>", "max results").action(guard(cmdSemantic));
@@ -72162,7 +72681,33 @@ function buildProgram() {
72162
72681
  ).option("--summary", "line ranges and ours/base/theirs labels only, omitting the conflict content").option("--json", "emit the results as JSON instead of text").action(guard(cmdConflicts));
72163
72682
  program2.command("screenshot <url> <destPath>").description("capture a local headless-browser screenshot, shrunk the same way local image reads are").option("--executable-path <path>", "Chrome/Chromium executable to launch (overrides config/auto-detect)").option("--width <n>", "viewport width in pixels (default: 1280)").option("--height <n>", "viewport height in pixels (default: 800)").option("--full-page", "capture the full scrollable page instead of just the viewport").action(guard(cmdScreenshot));
72164
72683
  program2.command("write-file <dest>").description("write exact bytes to a file \u2014 handles backticks, quotes, $vars, CRLF without escaping\n\nModes: --b64 PAYLOAD (base64), --from SOURCE (copy file), or piped stdin").option("--from <source>", "copy bytes from this source file instead of stdin/base64").option("--b64 <payload>", "decode base64 payload and write to dest").action(guard(cmdWriteFile));
72165
- program2.command("replace <file>").description("replace one string in a file; supply old/new text via --old-from/--new-from or --old-b64/--new-b64, and use --all to replace every occurrence").option("--old-from <source>", "read the old text from this source file").option("--new-from <source>", "read the new text from this source file").option("--old-b64 <payload>", "base64 payload for the old text").option("--new-b64 <payload>", "base64 payload for the new text").option("--all", "replace every occurrence instead of requiring a unique match").action(guard(cmdReplace));
72684
+ program2.command("replace <file>").description("replace one string in a file; supply old/new text via --old-from/--new-from or --old-b64/--new-b64, and use --all to replace every occurrence").option("--old-from <source>", "read the old text from this source file").option("--new-from <source>", "read the new text from this source file").option("--old-b64 <payload>", "base64 payload for the old text").option("--new-b64 <payload>", "base64 payload for the new text").option("--all", "replace every occurrence instead of requiring a unique match").option(
72685
+ "--normalize-newlines",
72686
+ "convert the old/new text's line endings (CRLF/LF) to match the target file's dominant line ending before matching, instead of requiring a byte-exact line-ending match"
72687
+ ).action(guard(cmdReplace));
72688
+ program2.command("insert-section <file>").description(
72689
+ "insert content immediately after a matched section (spec resolved the same way as `section`: exact heading, or an unambiguous prefix), avoiding a stale byte-exact anchor for append-to-a-running-log edits"
72690
+ ).requiredOption("--after <heading>", "heading text (or unambiguous prefix) to insert after").option("--content-from <source>", "read the content to insert from this source file").option("--content-b64 <payload>", "base64 payload for the content to insert").action(guard(cmdInsertSection));
72691
+ program2.command("note-add <file>").description(
72692
+ "attach a free-text architecture note to a file, or to one specific indexed symbol within it (--symbol NAME), fingerprinting what the note describes so `note-list --stale-only` can flag it once the code changes"
72693
+ ).option("--symbol <name>", "attach the note to one indexed symbol in the file instead of the whole file").option("--content-from <source>", "read the note content (Markdown) from this source file").option("--content-b64 <payload>", "base64 payload for the note content").action(guard(cmdNoteAdd));
72694
+ program2.command("note-get <file>").description("read back the note attached to a file, or to one indexed symbol within it (--symbol NAME); flags whether it has gone stale since it was written").option("--symbol <name>", "read the note attached to this indexed symbol instead of the whole-file note").option("-j, --json", "output as JSON").action(
72695
+ (file2, opts) => runExitText(
72696
+ () => runNoteGet({
72697
+ file: file2,
72698
+ ...opts.symbol !== void 0 ? { symbol: opts.symbol } : {},
72699
+ ...opts.json === true ? { json: true } : {}
72700
+ })
72701
+ )
72702
+ );
72703
+ program2.command("note-list").description("list every recorded architecture note; --stale-only shows just the notes whose attached file/symbol changed since they were written").option("--stale-only", "only list notes whose fingerprint no longer matches the current index").option("-j, --json", "output as JSON").action(
72704
+ (opts) => runExitText(
72705
+ () => runNoteList({
72706
+ ...opts.staleOnly === true ? { staleOnly: true } : {},
72707
+ ...opts.json === true ? { json: true } : {}
72708
+ })
72709
+ )
72710
+ );
72166
72711
  program2.command("gdrive-sections <file-id>").description("fetch and list sections from a public Google Doc").option("--heading <name>", "get content of one named section").option("--fresh", "skip the on-disk cache and force a live fetch").action(guard(cmdGdriveSections));
72167
72712
  program2.command("compress").description("run a shell command and emit a compressed view of its output").requiredOption("-c, --cmd <command>", "the shell command to run, as one string").option("-f, --filter <name>", "filter name (auto-detected from the command when omitted)").option("--timeout <seconds>", "wall-clock timeout in seconds (0 = built-in default)").option("--no-compress", "stream output raw without compression (debug the wrapper)").option("--profile <name>", "compression profile: aggressive | balanced | minimal").option("--max-tokens <n>", "post-compress token cap (0 = no cap)").action(cmdCompress);
72168
72713
  program2.command("version").description("print the token-goat version").action(
@@ -78322,7 +78867,7 @@ function preFetchHandler(event) {
78322
78867
  if (cached2 !== null) {
78323
78868
  const cachedBytes = Buffer.byteLength(cached2, "utf-8");
78324
78869
  if (cachedBytes >= loadConfig().hints.web_dedup_min_bytes) {
78325
- recordStat("webfetch:recall", cachedBytes, Math.round(cached2.length / 4));
78870
+ recordStat("webfetch:recall", cachedBytes, Math.round(cachedBytes / 4));
78326
78871
  return denyOutput(
78327
78872
  "Already fetched this URL with this prompt; the response is cached. Use `token-goat web-output " + cacheId + "` to recall it (append `--grep PATTERN` to filter or `--section Heading` for a markdown section) instead of re-fetching."
78328
78873
  );
@@ -79958,8 +80503,9 @@ function storeMcpOutput(sessionId, toolName, toolInput, resultText) {
79958
80503
  const rawSizeBytes = Buffer.byteLength(resultText, "utf-8");
79959
80504
  if (rawSizeBytes > MCP_MAX_CACHE_BYTES) return null;
79960
80505
  const id = mcpOutputId(sessionId, mcpHash(toolName, toolInput));
79961
- const label = `mcp:${toolName} ${mcpInputPreview(toolInput)}`.trim();
80506
+ const rawLabel = `mcp:${toolName} ${mcpInputPreview(toolInput)}`.trim();
79962
80507
  const redactedOutput = redactSecrets(resultText).text;
80508
+ const label = redactSecrets(rawLabel).text;
79963
80509
  const entry = {
79964
80510
  id,
79965
80511
  command: label,
@@ -80228,6 +80774,7 @@ function compressMcpResultWithPacks(toolName, resultText) {
80228
80774
  }
80229
80775
 
80230
80776
  // src/hooks_mcp.ts
80777
+ init_secret_redact();
80231
80778
  function isMcpErrorResponse(raw) {
80232
80779
  const tr = raw["tool_response"];
80233
80780
  if (!tr || typeof tr !== "object") return false;
@@ -80267,7 +80814,7 @@ function postMcpHandler(event) {
80267
80814
  return {
80268
80815
  hookType: "rewriteOutput",
80269
80816
  updatedOutput: `[token-goat: compressed, full via mcp-output ${id}]
80270
- ${compressed}`
80817
+ ${redactSecrets(compressed).text}`
80271
80818
  };
80272
80819
  }
80273
80820
  }