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.
@@ -3050,7 +3050,7 @@ var require_commander = __commonJS({
3050
3050
  import { createRequire } from "node:module";
3051
3051
  function resolveVersion() {
3052
3052
  if (true) {
3053
- return "2.6.18";
3053
+ return "2.6.19";
3054
3054
  }
3055
3055
  const require2 = createRequire(import.meta.url);
3056
3056
  const pkg = require2("../package.json");
@@ -4916,7 +4916,7 @@ function markerExists(current, marker) {
4916
4916
  }
4917
4917
  const resolved = fs3.realpathSync(markerPath);
4918
4918
  const rel = path4.relative(path4.resolve(current), path4.resolve(resolved));
4919
- return !rel.startsWith("..");
4919
+ return !rel.startsWith("..") && !path4.isAbsolute(rel);
4920
4920
  } catch {
4921
4921
  return false;
4922
4922
  }
@@ -6220,6 +6220,33 @@ CREATE TABLE IF NOT EXISTS hint_suppression_probes (
6220
6220
  PRIMARY KEY (category, harness)
6221
6221
  );
6222
6222
 
6223
+ -- Free-text architecture/rationale notes (the "why" layer -- see notes.ts), attached either to
6224
+ -- a whole file (symbol = '') or to one specific indexed symbol within it (symbol = that
6225
+ -- symbol's name). '' rather than NULL for the whole-file case because SQLite's UNIQUE treats
6226
+ -- NULLs as pairwise-distinct (never conflicting with each other), which would let note-add
6227
+ -- accumulate unlimited duplicate whole-file notes for the same file instead of upserting one;
6228
+ -- '' is a real, comparable value so UNIQUE(file_path, symbol) enforces "at most one note per
6229
+ -- attachment point" for both cases identically. 'fingerprint' is a SHA-256 digest (see
6230
+ -- fingerprintContent in fingerprint.ts) captured at write time of exactly what the note
6231
+ -- describes -- the resolved symbol's current body text for a symbol-scoped note, or a stable
6232
+ -- digest of the file's current top-level symbol manifest (name:kind:line-range per symbol,
6233
+ -- sorted) for a file-scoped note -- so 'token-goat note-list --stale-only' can recompute the
6234
+ -- same fingerprint against the live index later and flag a mismatch (see notes.ts's
6235
+ -- isNoteStale). Staleness detection is purely advisory: nothing here ever auto-rewrites or
6236
+ -- deletes a note's content, only flags that the code it describes has moved since it was
6237
+ -- written -- a human/agent re-review decides what to do with a stale note.
6238
+ CREATE TABLE IF NOT EXISTS notes (
6239
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6240
+ file_path TEXT NOT NULL,
6241
+ symbol TEXT NOT NULL DEFAULT '',
6242
+ content TEXT NOT NULL,
6243
+ fingerprint TEXT NOT NULL,
6244
+ created_at REAL NOT NULL,
6245
+ updated_at REAL NOT NULL,
6246
+ UNIQUE(file_path, symbol)
6247
+ );
6248
+ CREATE INDEX IF NOT EXISTS idx_notes_file_folded ON notes(TG_LOWER(file_path));
6249
+
6223
6250
  -- Baseline for skill_version_drift.ts's one-shot nudge: the token-goat CLI version (and its
6224
6251
  -- flat command-name set, JSON-encoded) active the moment the token-goat skill's body was
6225
6252
  -- last (re)loaded into this session -- see hooks_skill.ts's postSkillHandler. A session that
@@ -6285,7 +6312,7 @@ CREATE TRIGGER IF NOT EXISTS cache_recall_au AFTER UPDATE ON cache_recall BEGIN
6285
6312
  VALUES (new.row_id, new.label, new.content);
6286
6313
  END;
6287
6314
  `;
6288
- SCHEMA_VERSION = 7;
6315
+ SCHEMA_VERSION = 8;
6289
6316
  MIGRATIONS = {
6290
6317
  // 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.
6291
6318
  1: (conn) => alterTableIdempotent(conn, "ALTER TABLE files ADD COLUMN embed_sha TEXT"),
@@ -6778,6 +6805,17 @@ function slideShapes(parsedSlide) {
6778
6805
  function shapeText(shape) {
6779
6806
  return collectTextRuns(shape, "a:t").join(" ").trim();
6780
6807
  }
6808
+ function tableRowBlocks(parsedSlide) {
6809
+ const blocks = [];
6810
+ for (const tbl of collectElements(parsedSlide, "a:tbl")) {
6811
+ for (const row of collectElements(tbl, "a:tr")) {
6812
+ const cellTexts = collectElements(row, "a:tc").map((cell) => collectTextRuns(cell, "a:t").join(" ").trim());
6813
+ const rowText = cellTexts.join(" | ").trim();
6814
+ if (rowText.length > 0) blocks.push(rowText);
6815
+ }
6816
+ }
6817
+ return blocks;
6818
+ }
6781
6819
  async function slidePathsInPresentationOrder(entries) {
6782
6820
  const presXml = decodeZipEntry(entries, "ppt/presentation.xml");
6783
6821
  const relsXml = decodeZipEntry(entries, "ppt/_rels/presentation.xml.rels");
@@ -6877,7 +6915,7 @@ async function pptxSlideText(filePath, slideNumber, includeNotes) {
6877
6915
  const path63 = slidePaths[slideNumber - 1];
6878
6916
  const parsed = await parseSlide(entries, path63);
6879
6917
  const shapes = slideShapes(parsed);
6880
- const blocks = shapes.map(shapeText).filter((t) => t.length > 0);
6918
+ const blocks = [...shapes.map(shapeText).filter((t) => t.length > 0), ...tableRowBlocks(parsed)];
6881
6919
  const lines2 = [`# Slide ${slideNumber}`, ...blocks];
6882
6920
  if (includeNotes) {
6883
6921
  const notes = await notesTextFor(entries, await notesPathFor(entries, path63));
@@ -9621,7 +9659,7 @@ function _renderByDaySection(stats) {
9621
9659
  function share(d) {
9622
9660
  return _tokenOrByteShare(d.tokens, d.bytes, stats.totals.tokens, stats.totals.bytes);
9623
9661
  }
9624
- for (const d of [...stats.by_day].sort((a, b) => b.date.localeCompare(a.date))) {
9662
+ for (const d of [...stats.by_day].sort((a, b) => b.date < a.date ? -1 : b.date > a.date ? 1 : 0)) {
9625
9663
  const s = share(d);
9626
9664
  lines2.push(
9627
9665
  _tableRow({
@@ -10231,6 +10269,7 @@ var init_stats = __esm({
10231
10269
  docx_text: SOURCE_READ,
10232
10270
  transcript_outline: SOURCE_READ,
10233
10271
  transcript: SOURCE_READ,
10272
+ video_chapters: SOURCE_READ,
10234
10273
  coverage_report_gaps: SOURCE_READ,
10235
10274
  json_query: SOURCE_READ,
10236
10275
  json_outline: SOURCE_READ,
@@ -10248,6 +10287,14 @@ var init_stats = __esm({
10248
10287
  session_slice: SOURCE_READ,
10249
10288
  gdrive_sections: SOURCE_READ,
10250
10289
  pr_slice: SOURCE_READ,
10290
+ note_read: SOURCE_READ,
10291
+ note_list: SOURCE_READ,
10292
+ // note-add is a write (like insert-section/replace, which record no stat at all -- neither
10293
+ // has a "full source it replaces" savings concept). It still gets an event-only entry here
10294
+ // (no bytesSaved/tokensSaved argument, same as skill_load) purely so `token-goat note-add`
10295
+ // usage is visible in `token-goat stats --full` at all -- SOURCE_OTHER, not SOURCE_READ,
10296
+ // since it is not a token-savings substitute for a read.
10297
+ note_write: SOURCE_OTHER,
10251
10298
  web_fetch: SOURCE_WEB,
10252
10299
  injection_detected: SOURCE_WEB,
10253
10300
  skill_load: SOURCE_SKILL,
@@ -10292,6 +10339,7 @@ var init_stats = __esm({
10292
10339
  "docx-text": /* @__PURE__ */ new Set(["docx_text"]),
10293
10340
  "transcript-outline": /* @__PURE__ */ new Set(["transcript_outline"]),
10294
10341
  transcript: /* @__PURE__ */ new Set(["transcript"]),
10342
+ "video-chapters": /* @__PURE__ */ new Set(["video_chapters"]),
10295
10343
  "coverage-report-gaps": /* @__PURE__ */ new Set(["coverage_report_gaps"]),
10296
10344
  "json-query": /* @__PURE__ */ new Set(["json_query"]),
10297
10345
  "json-outline": /* @__PURE__ */ new Set(["json_outline"]),
@@ -10309,6 +10357,9 @@ var init_stats = __esm({
10309
10357
  "session-slice": /* @__PURE__ */ new Set(["session_slice"]),
10310
10358
  "gdrive-sections": /* @__PURE__ */ new Set(["gdrive_sections"]),
10311
10359
  "pr-slice": /* @__PURE__ */ new Set(["pr_slice"]),
10360
+ "note-add": /* @__PURE__ */ new Set(["note_write"]),
10361
+ "note-get": /* @__PURE__ */ new Set(["note_read"]),
10362
+ "note-list": /* @__PURE__ */ new Set(["note_list"]),
10312
10363
  npm: /* @__PURE__ */ new Set([
10313
10364
  "bash_compress:npm_install",
10314
10365
  "bash_compress:npm_ci",
@@ -14137,6 +14188,7 @@ function formatHeadingTree(headings, filePath) {
14137
14188
  const lines2 = [];
14138
14189
  lines2.push(`Large markdown file (${headings.length} headings). Use token-goat section to read a specific section:`);
14139
14190
  lines2.push(` token-goat section "${filePath}::Heading Name"`);
14191
+ 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.`);
14140
14192
  lines2.push(``);
14141
14193
  lines2.push(`Sections:`);
14142
14194
  let headingsAdded = 0;
@@ -15176,12 +15228,12 @@ function parseSectionOrdinal(heading) {
15176
15228
  const match2 = heading.match(/^(.*?)(?:#(\d+))?$/);
15177
15229
  if (!match2) return [heading, 1];
15178
15230
  const baseHeading = match2[1] || heading;
15179
- const ordinal = match2[2] ? Math.max(1, parseInt(match2[2], 10)) : 1;
15180
- return [baseHeading, ordinal];
15231
+ const ordinal2 = match2[2] ? Math.max(1, parseInt(match2[2], 10)) : 1;
15232
+ return [baseHeading, ordinal2];
15181
15233
  }
15182
15234
  function extractNamedSection(body, heading) {
15183
15235
  if (!body || !heading) return null;
15184
- const [baseHeading, ordinal] = parseSectionOrdinal(heading);
15236
+ const [baseHeading, ordinal2] = parseSectionOrdinal(heading);
15185
15237
  const headingLower = stripLower(baseHeading);
15186
15238
  const lines2 = body.split("\n");
15187
15239
  let matchCount = 0;
@@ -15191,7 +15243,7 @@ function extractNamedSection(body, heading) {
15191
15243
  const headingText = stripLower(stripped.slice(3));
15192
15244
  if (headingText === headingLower) {
15193
15245
  matchCount++;
15194
- if (matchCount === ordinal) {
15246
+ if (matchCount === ordinal2) {
15195
15247
  startIdx = i + 1;
15196
15248
  break;
15197
15249
  }
@@ -16265,6 +16317,11 @@ function isNodeModulesPath(p) {
16265
16317
  const check2 = foldPath(p);
16266
16318
  return check2.includes("/node_modules/") || check2.includes("\\node_modules\\");
16267
16319
  }
16320
+ function relPathWithinRoot(root, target) {
16321
+ const rel = path21.relative(root, target).replace(/\\/g, "/");
16322
+ if (rel.startsWith("..") || path21.isAbsolute(rel)) return null;
16323
+ return rel;
16324
+ }
16268
16325
  function _isDocFile(filePath) {
16269
16326
  const lower = filePath.toLowerCase();
16270
16327
  return lower.endsWith(".md") || lower.endsWith(".mdx") || lower.endsWith(".markdown") || lower.endsWith(".rst");
@@ -16429,7 +16486,8 @@ function loadSnapshotDiff(sessionId, normalized, basename19) {
16429
16486
  }
16430
16487
  function scanCrossSessionManifests(projectRoot, projectHash2, filePath, ttlSecs) {
16431
16488
  try {
16432
- const relPath = path21.relative(projectRoot, filePath).replace(/\\/g, "/");
16489
+ const relPath = relPathWithinRoot(projectRoot, filePath);
16490
+ if (relPath === null) return false;
16433
16491
  const foldedRelPath = foldPath(relPath);
16434
16492
  const manifests = readAllSessionManifests(projectHash2, ttlSecs);
16435
16493
  for (const data of manifests) {
@@ -16473,7 +16531,7 @@ function isProtectedRecentRead(normalized, n) {
16473
16531
  if (n <= 0) return false;
16474
16532
  const ranked = Array.from(getSessionFiles().entries()).sort((a, b) => {
16475
16533
  const byRecency = b[1].lastReadAt - a[1].lastReadAt;
16476
- return byRecency !== 0 ? byRecency : a[0].localeCompare(b[0]);
16534
+ return byRecency !== 0 ? byRecency : a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
16477
16535
  });
16478
16536
  const rank = ranked.findIndex(([filePath]) => filePath === normalized);
16479
16537
  return rank !== -1 && rank < n;
@@ -16717,8 +16775,7 @@ function preReadHandlerInner(event) {
16717
16775
  if (!project) {
16718
16776
  project = makeProjectAt(cwd);
16719
16777
  }
16720
- const relPath = path21.relative(project.root, normalized).replace(/\\/g, "/");
16721
- if (!relPath.startsWith("..")) {
16778
+ if (relPathWithinRoot(project.root, normalized) !== null) {
16722
16779
  const ttlSecs = config2.hints.cross_session_read_dedup_ttl_secs;
16723
16780
  if (scanCrossSessionManifests(project.root, project.hash, normalized, ttlSecs)) {
16724
16781
  recordActualRead(event, normalized);
@@ -16855,7 +16912,7 @@ function estimateTruncatedLineCount(normalized) {
16855
16912
  return Infinity;
16856
16913
  }
16857
16914
  function editAnywayHint(normalized) {
16858
- 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.";
16915
+ 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.";
16859
16916
  }
16860
16917
  function truncatedReadDenyMessage(normalized) {
16861
16918
  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);
@@ -16890,8 +16947,8 @@ function postReadHandlerInner(event) {
16890
16947
  const sessionState = exportSessionState();
16891
16948
  const mappedFiles = [];
16892
16949
  for (const fileEntry of sessionState.files) {
16893
- const relPath = path21.relative(project.root, fileEntry.path).replace(/\\/g, "/");
16894
- if (!relPath.startsWith("..")) {
16950
+ const relPath = relPathWithinRoot(project.root, fileEntry.path);
16951
+ if (relPath !== null) {
16895
16952
  mappedFiles.push({
16896
16953
  rel_path: relPath,
16897
16954
  hit_count: fileEntry.readCount
@@ -18616,9 +18673,6 @@ function extractPhp(content, filePath) {
18616
18673
  let braceDepth = 0;
18617
18674
  let inComment = false;
18618
18675
  let mlState = null;
18619
- function currentClass() {
18620
- return contextStack.length > 0 ? contextStack[contextStack.length - 1]?.[0] ?? null : null;
18621
- }
18622
18676
  for (let i = 0; i < lines2.length; i++) {
18623
18677
  const rawLine = lines2[i] ?? "";
18624
18678
  const lineNum = i + 1;
@@ -18682,7 +18736,9 @@ function extractPhp(content, filePath) {
18682
18736
  if (clsM) {
18683
18737
  const kind = clsM[1] ?? "class";
18684
18738
  const name2 = clsM[2] ?? "";
18685
- const parent = currentClass();
18739
+ const preLineDepth = braceDepth - openB + closeB;
18740
+ const topFrame2 = contextStack.length > 0 ? contextStack[contextStack.length - 1] : void 0;
18741
+ const parent = topFrame2 !== void 0 && preLineDepth === topFrame2[1] + 1 ? topFrame2[0] : null;
18686
18742
  symbols.push(makeLineSymbol(filePath, name2, kind, lineNum, stripped.slice(0, 200), parent ?? void 0));
18687
18743
  contextStack.push([name2, braceDepth - openB + closeB, false]);
18688
18744
  if (openB > 0 && openB === closeB) {
@@ -19280,6 +19336,9 @@ function extractScala(content, filePath) {
19280
19336
  const parent = typeStack.length > 0 ? typeStack[typeStack.length - 1].name : void 0;
19281
19337
  symbols.push(makeLineSymbol(filePath, cname, "class", lineNum, stripped.slice(0, 200), parent));
19282
19338
  typeStack.push({ name: cname, startDepth: braceDepth, bodyEntered: false });
19339
+ if (/\bcase\s+class\b/.test(stripped) && !stripStringLiterals(line).includes("{")) {
19340
+ typeStack.pop();
19341
+ }
19283
19342
  matched = true;
19284
19343
  }
19285
19344
  const om = !matched && typeDetectionGateOk && (!isIndented || typeStack.length > 0) ? OBJECT_RE.exec(stripped) : null;
@@ -19288,6 +19347,9 @@ function extractScala(content, filePath) {
19288
19347
  const parent = typeStack.length > 0 ? typeStack[typeStack.length - 1].name : void 0;
19289
19348
  symbols.push(makeLineSymbol(filePath, oname, "object", lineNum, stripped.slice(0, 200), parent));
19290
19349
  typeStack.push({ name: oname, startDepth: braceDepth, bodyEntered: false });
19350
+ if (/\bcase\s+object\b/.test(stripped) && !stripStringLiterals(line).includes("{")) {
19351
+ typeStack.pop();
19352
+ }
19291
19353
  matched = true;
19292
19354
  }
19293
19355
  const tm = !matched && typeDetectionGateOk && (!isIndented || typeStack.length > 0) ? TRAIT_RE.exec(stripped) : null;
@@ -19375,13 +19437,13 @@ var init_scala = __esm({
19375
19437
  init_common();
19376
19438
  IMPORT_RE3 = /^import\s+([A-Za-z_][A-Za-z0-9_.]*(?:\._)?)/;
19377
19439
  BRACE_IMPORT_RE = /^import\s+([A-Za-z_][A-Za-z0-9_.]*)\.\{([^}]*)\}/;
19378
- 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|\[|\(|:|$)/;
19379
- 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|:|$)/;
19380
- 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|\[|:|$)/;
19440
+ 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|\[|\(|:|$)/;
19441
+ 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|:|$)/;
19442
+ 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|\[|:|$)/;
19381
19443
  ENUM_RE = /^\s*(?:private|protected)?\s*enum\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s|\[|\(|:|$)/;
19382
- 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*:)/;
19383
- VAL_RE = /^\s*(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)?\s*val\s+([A-Za-z_][A-Za-z0-9_]*)/;
19384
- VAR_RE = /^\s*(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)?\s*var\s+([A-Za-z_][A-Za-z0-9_]*)/;
19444
+ 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*:)/;
19445
+ VAL_RE = /^\s*(?:(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)\s+)*val\s+([A-Za-z_][A-Za-z0-9_]*)/;
19446
+ VAR_RE = /^\s*(?:(?:implicit|lazy|sealed|abstract|final|private|protected|override|covariant|contravariant|case)\s+)*var\s+([A-Za-z_][A-Za-z0-9_]*)/;
19385
19447
  }
19386
19448
  });
19387
19449
 
@@ -19471,8 +19533,9 @@ function extractLua(content, filePath) {
19471
19533
  funcStack.push({ name: "", endKeywordNeeded: true, isBlock: true });
19472
19534
  continue;
19473
19535
  }
19474
- if (stripped === "end" || /^end\s/.test(stripped) || /^end$/.test(stripped)) {
19475
- if (funcStack.length > 0) {
19536
+ if (/^(?:end(?:\s+|$))+$/.test(stripped)) {
19537
+ const popCount = (stripped.match(/\bend\b/g) ?? []).length;
19538
+ for (let k = 0; k < popCount && funcStack.length > 0; k++) {
19476
19539
  funcStack.pop();
19477
19540
  }
19478
19541
  }
@@ -19515,6 +19578,7 @@ function extractElixir(content, filePath) {
19515
19578
  continue;
19516
19579
  }
19517
19580
  const opensDoBlock = /\bdo\s*$/.test(stripped);
19581
+ const opensFnBlock = /\bfn\b/.test(stripped) && !/\bend\b/.test(stripped) && /(?:fn|->)\s*$/.test(stripped);
19518
19582
  const modM = MODULE_RE.exec(stripped);
19519
19583
  if (modM) {
19520
19584
  const modName = modM[1] ?? "";
@@ -19564,7 +19628,7 @@ function extractElixir(content, filePath) {
19564
19628
  }
19565
19629
  continue;
19566
19630
  }
19567
- if (opensDoBlock) {
19631
+ if (opensDoBlock || opensFnBlock) {
19568
19632
  moduleStack.push({ name: "", endKeywordNeeded: true, isBlock: true });
19569
19633
  continue;
19570
19634
  }
@@ -19640,6 +19704,14 @@ function extractDart(content, filePath) {
19640
19704
  typeStack.push({ name: mname, startDepth: braceDepth, bodyEntered: false });
19641
19705
  matched = true;
19642
19706
  }
19707
+ const etm = !matched ? EXTENSION_TYPE_RE.exec(stripped) : null;
19708
+ if (etm) {
19709
+ const etname = etm[1] ?? "";
19710
+ const parent = typeStack.length > 0 ? typeStack[typeStack.length - 1].name : void 0;
19711
+ symbols.push(makeLineSymbol(filePath, etname, "extension_type", lineNum, stripped.slice(0, 200), parent));
19712
+ typeStack.push({ name: etname, startDepth: braceDepth, bodyEntered: false });
19713
+ matched = true;
19714
+ }
19643
19715
  const extm = !matched ? EXTENSION_RE.exec(stripped) : null;
19644
19716
  if (extm) {
19645
19717
  const extname14 = extm[1] ?? "extension";
@@ -19690,7 +19762,7 @@ function extractDart(content, filePath) {
19690
19762
  }
19691
19763
  return { symbols, imports };
19692
19764
  }
19693
- var CLASS_RE4, ENUM_RE2, MIXIN_RE, EXTENSION_RE, FUNC_RE5;
19765
+ var CLASS_RE4, ENUM_RE2, MIXIN_RE, EXTENSION_RE, EXTENSION_TYPE_RE, FUNC_RE5;
19694
19766
  var init_dart = __esm({
19695
19767
  "src/languages/dart.ts"() {
19696
19768
  "use strict";
@@ -19700,6 +19772,7 @@ var init_dart = __esm({
19700
19772
  ENUM_RE2 = /^enum\s+([A-Za-z_][A-Za-z0-9_]*)/;
19701
19773
  MIXIN_RE = /^(?:base\s+)?mixin\s+([A-Za-z_][A-Za-z0-9_]*)/;
19702
19774
  EXTENSION_RE = /^extension\s+(?:([A-Za-z_][A-Za-z0-9_]*)\s+)?on\s+/;
19775
+ EXTENSION_TYPE_RE = /^extension\s+type\s+([A-Za-z_][A-Za-z0-9_]*)/;
19703
19776
  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*\(/;
19704
19777
  }
19705
19778
  });
@@ -19726,33 +19799,39 @@ function extractZig(content, filePath) {
19726
19799
  }
19727
19800
  const isIndented = line[0] === " " || line[0] === " ";
19728
19801
  let matched = false;
19729
- if (!isIndented || scopeStack.length > 0) {
19802
+ const outerFrame = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
19803
+ const outerDepthInType = outerFrame !== null ? braceDepth - outerFrame.startDepth : 0;
19804
+ const typeDetectionGateOk = scopeStack.length === 0 || outerDepthInType === 1;
19805
+ if (typeDetectionGateOk && (!isIndented || scopeStack.length > 0)) {
19730
19806
  const sm = CONTAINER_RE.exec(stripped);
19731
19807
  if (sm) {
19732
19808
  const sname = sm[1] ?? "";
19733
19809
  const skind = sm[2] ?? "struct";
19734
19810
  if (sname) {
19735
- const parent = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].name : void 0;
19811
+ const parent = outerFrame !== null ? outerFrame.name : void 0;
19736
19812
  symbols.push(makeLineSymbol(filePath, sname, skind, lineNum, stripped.slice(0, 200), parent));
19737
19813
  scopeStack.push({ name: sname, startDepth: braceDepth, bodyEntered: false });
19738
19814
  matched = true;
19739
19815
  }
19740
19816
  }
19741
19817
  }
19742
- if (!matched && !isIndented) {
19818
+ const frame = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
19819
+ if (!matched && !isIndented && frame === null) {
19743
19820
  const fm = FUNC_RE6.exec(stripped);
19744
19821
  if (fm) {
19745
19822
  const fname = fm[2] ?? "";
19746
19823
  symbols.push(makeLineSymbol(filePath, fname, "function", lineNum, stripped.slice(0, 200)));
19747
19824
  matched = true;
19748
19825
  }
19749
- } else if (!matched && scopeStack.length > 0) {
19750
- const fm = FUNC_RE6.exec(stripped);
19751
- if (fm) {
19752
- const fname = fm[2] ?? "";
19753
- const parent = scopeStack[scopeStack.length - 1].name;
19754
- symbols.push(makeLineSymbol(filePath, fname, "function", lineNum, stripped.slice(0, 200), parent));
19755
- matched = true;
19826
+ } else if (!matched && frame !== null) {
19827
+ const depthInType = braceDepth - frame.startDepth;
19828
+ if (depthInType === 1) {
19829
+ const fm = FUNC_RE6.exec(stripped);
19830
+ if (fm) {
19831
+ const fname = fm[2] ?? "";
19832
+ symbols.push(makeLineSymbol(filePath, fname, "function", lineNum, stripped.slice(0, 200), frame.name));
19833
+ matched = true;
19834
+ }
19756
19835
  }
19757
19836
  }
19758
19837
  if (!matched && !isIndented) {
@@ -19773,9 +19852,9 @@ function extractZig(content, filePath) {
19773
19852
  if (ch === "{") {
19774
19853
  braceDepth++;
19775
19854
  if (scopeStack.length > 0) {
19776
- const frame = scopeStack[scopeStack.length - 1];
19777
- if (braceDepth > frame.startDepth) {
19778
- frame.bodyEntered = true;
19855
+ const frame2 = scopeStack[scopeStack.length - 1];
19856
+ if (braceDepth > frame2.startDepth) {
19857
+ frame2.bodyEntered = true;
19779
19858
  }
19780
19859
  }
19781
19860
  } else if (ch === "}") {
@@ -23190,7 +23269,18 @@ var init_parser = __esm({
23190
23269
  // `visit` already descends into `interface_body`/class bodies to find them.
23191
23270
  ["method_signature", "method"],
23192
23271
  ["property_signature", "var"],
23193
- ["abstract_method_signature", "method"]
23272
+ ["abstract_method_signature", "method"],
23273
+ // `namespace Foo { ... }` (and the legacy `module Foo { ... }` synonym) parses as
23274
+ // `internal_module`; `declare module "some-string" { ... }` (an ambient module declaration,
23275
+ // common in .d.ts files) parses as `module` -- a distinct node type from either. Neither had a
23276
+ // kind-map entry, so the namespace/module declaration itself was silently invisible to
23277
+ // `symbol`/`outline`/`skeleton`/`read`, even though everything nested inside it still indexed
23278
+ // fine (the walk recurses into every node's children regardless of the parent's kind-map
23279
+ // membership) -- the same container-drop shape already fixed for C++ `namespace_definition`
23280
+ // and Rust `mod_item`. Both node types expose their name on the standard `name` field
23281
+ // (an identifier, nested_identifier, or string), so `nodeName` resolves it without special-casing.
23282
+ ["internal_module", "namespace"],
23283
+ ["module", "namespace"]
23194
23284
  ]);
23195
23285
  TSJS_FN_SCOPE_TYPES = /* @__PURE__ */ new Set([
23196
23286
  "function_declaration",
@@ -24226,6 +24316,7 @@ function workerHealthCheckMarkerPath(dir) {
24226
24316
  return path33.join(dir, "worker-healthcheck.marker");
24227
24317
  }
24228
24318
  function ensureWorkerAlive(dir = dataDir()) {
24319
+ if (process.env["TOKEN_GOAT_NO_WORKER_SPAWN"] === "1") return;
24229
24320
  const markerPath = workerHealthCheckMarkerPath(dir);
24230
24321
  try {
24231
24322
  const stat2 = fs27.statSync(markerPath);
@@ -24337,6 +24428,7 @@ async function runWorkerLoop(dir, pollIntervalMs, shouldStop = () => false) {
24337
24428
  let lastSnapshotCleanupMs = 0;
24338
24429
  let lastKnownRootsSweepMs = 0;
24339
24430
  while (!shouldStop()) {
24431
+ if (!fs27.existsSync(dir)) break;
24340
24432
  try {
24341
24433
  drainOnce(dir);
24342
24434
  } catch {
@@ -24657,7 +24749,7 @@ function tableSectionEndIndex(headers, headerPos, totalLines) {
24657
24749
  }
24658
24750
  return totalLines;
24659
24751
  }
24660
- function resolveHeaderPos(headers, base, ordinal) {
24752
+ function resolveHeaderPos(headers, base, ordinal2) {
24661
24753
  const target = base.toLowerCase();
24662
24754
  const normalizedTarget = normalizeHeading(base).toLowerCase();
24663
24755
  const strippedTarget = normalizeHeadingStrip(base).toLowerCase();
@@ -24678,12 +24770,12 @@ function resolveHeaderPos(headers, base, ordinal) {
24678
24770
  }
24679
24771
  const matches2 = exactMatches.length > 0 ? exactMatches : normalizedMatches.length > 0 ? normalizedMatches : strippedMatches;
24680
24772
  if (matches2.length > 0) {
24681
- const pick3 = ordinal === null ? 0 : ordinal - 1;
24773
+ const pick3 = ordinal2 === null ? 0 : ordinal2 - 1;
24682
24774
  const headerPos = matches2[pick3];
24683
24775
  if (headerPos === void 0) return null;
24684
24776
  return { headerPos, redirectedFrom: null };
24685
24777
  }
24686
- if (ordinal !== null || normalizedTarget.length === 0) return null;
24778
+ if (ordinal2 !== null || normalizedTarget.length === 0) return null;
24687
24779
  let prefixPos = -1;
24688
24780
  const distinct = /* @__PURE__ */ new Set();
24689
24781
  for (let i = 0; i < headers.length; i++) {
@@ -24719,10 +24811,10 @@ function buildSectionResult(headers, kind, lines2, headerPos, redirectedFrom) {
24719
24811
  }
24720
24812
  function resolveSectionFromText(text, headingSpec, language) {
24721
24813
  const { headers, kind } = findHeaders(text, language);
24722
- const { base, ordinal } = parseHeadingSpec(headingSpec, headers);
24814
+ const { base, ordinal: ordinal2 } = parseHeadingSpec(headingSpec, headers);
24723
24815
  if (base.length === 0) return null;
24724
24816
  const lines2 = text.split("\n");
24725
- const resolved = resolveHeaderPos(headers, base, ordinal);
24817
+ const resolved = resolveHeaderPos(headers, base, ordinal2);
24726
24818
  if (resolved === null) return null;
24727
24819
  return buildSectionResult(headers, kind, lines2, resolved.headerPos, resolved.redirectedFrom);
24728
24820
  }
@@ -25669,6 +25761,11 @@ var init_graph_commands = __esm({
25669
25761
  "opaque",
25670
25762
  "mixin",
25671
25763
  "extension",
25764
+ // 'extension_type' (Dart 3.3's zero-cost wrapper type, `extension type Meters(int value)`) is
25765
+ // a distinct declaration kind from a plain 'extension' -- languages/dart.ts's
25766
+ // EXTENSION_TYPE_RE emits it via the same makeLineSymbol code path as 'class'/'mixin'/
25767
+ // 'extension' above, so it needs the same TYPE_KINDS entry those already have.
25768
+ "extension_type",
25672
25769
  "actor",
25673
25770
  // Protocol Buffers (languages/proto_idx.ts's KIND_MAP) uses its own kind strings rather
25674
25771
  // than the generic 'enum'/'interface' -- a proto message is a struct-shaped type, a proto
@@ -25710,7 +25807,26 @@ var init_graph_commands = __esm({
25710
25807
  // excluded from `token-goat types` in its entirety, the same class of gap already fixed for
25711
25808
  // Rust union, Swift protocol/actor, Zig opaque, Dart mixin/extension, proto message/enum/
25712
25809
  // service, Apex class/interface/enum, and GraphQL type/interface/input/enum/union.
25713
- "object"
25810
+ "object",
25811
+ // languages/sfc_idx.ts (Vue/Svelte/Astro single-file components) emits 'sfc_script_class' for
25812
+ // a top-level class declaration in the component's script block, a distinct kind string rather
25813
+ // than the generic 'class' -- so unlike a plain class it never reaches the looksLikeTypeClass
25814
+ // fallback below either, which only ever queries kind === 'class' literally. Every SFC
25815
+ // top-level class was indexed but silently excluded from `token-goat types` in its entirety,
25816
+ // the same class of gap already fixed for Rust union, Swift protocol/actor, Zig opaque, Dart
25817
+ // mixin/extension, proto message/enum/service, Apex class/interface/enum, GraphQL
25818
+ // type/interface/input/enum/union, and Kotlin/Scala object.
25819
+ "sfc_script_class",
25820
+ // languages/graphql_idx.ts's TYPE_RE handler maps EVERY `extend type|interface|input|enum|
25821
+ // union|scalar Foo { ... }` declaration to this one shared kind regardless of which keyword
25822
+ // follows `extend` -- GraphQL's mechanism for adding fields to a type from another file/module,
25823
+ // ubiquitous in federation and schema-stitching -- but unlike the six non-extend KIND_MAP
25824
+ // entries already listed above, this kind was never added here, so every `extend` declaration
25825
+ // was indexed but silently excluded from `token-goat types` in its entirety, the same class of
25826
+ // gap already fixed for the other six GraphQL kinds, Rust union, Swift protocol/actor, Zig
25827
+ // opaque, Dart mixin/extension, proto message/enum/service, Apex class/interface/enum, and
25828
+ // Kotlin/Scala object.
25829
+ "graphql_extend"
25714
25830
  ];
25715
25831
  MAX_CYCLES = 200;
25716
25832
  }
@@ -28153,7 +28269,8 @@ function extractOperations(spec) {
28153
28269
  });
28154
28270
  }
28155
28271
  }
28156
- operations.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
28272
+ const ordinal2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
28273
+ operations.sort((a, b) => ordinal2(a.path, b.path) || ordinal2(a.method, b.method));
28157
28274
  return operations;
28158
28275
  }
28159
28276
  function formatOpenApiOutline(operations) {
@@ -28241,7 +28358,7 @@ function listZipEntries(data) {
28241
28358
  return false;
28242
28359
  }
28243
28360
  });
28244
- entries.sort((a, b) => a.path.localeCompare(b.path));
28361
+ entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
28245
28362
  return entries;
28246
28363
  }
28247
28364
  function formatZipList(entries) {
@@ -28724,7 +28841,11 @@ function hasGap(f) {
28724
28841
  }
28725
28842
  function rankAndFilter(files) {
28726
28843
  const withGaps = files.filter(hasGap);
28727
- withGaps.sort((a, b) => b.uncoveredLineCount - a.uncoveredLineCount || a.filePath.localeCompare(b.filePath));
28844
+ withGaps.sort((a, b) => {
28845
+ const byCount = b.uncoveredLineCount - a.uncoveredLineCount;
28846
+ if (byCount !== 0) return byCount;
28847
+ return a.filePath < b.filePath ? -1 : a.filePath > b.filePath ? 1 : 0;
28848
+ });
28728
28849
  return withGaps;
28729
28850
  }
28730
28851
  function detectCoverageFormat(text) {
@@ -29272,6 +29393,83 @@ var init_screenshot = __esm({
29272
29393
  }
29273
29394
  });
29274
29395
 
29396
+ // src/notes.ts
29397
+ function toNoteRow(row) {
29398
+ return {
29399
+ id: row.id,
29400
+ filePath: row.file_path,
29401
+ symbol: row.symbol,
29402
+ content: row.content,
29403
+ fingerprint: row.fingerprint,
29404
+ createdAt: row.created_at,
29405
+ updatedAt: row.updated_at
29406
+ };
29407
+ }
29408
+ function pickEarliest(matches2) {
29409
+ return matches2.reduce((best, s) => s.lineStart < best.lineStart ? s : best);
29410
+ }
29411
+ function resolveSymbolMatch(filePath, symbolName, dbPath = globalDbPath()) {
29412
+ const matches2 = querySymbols({ filePath, name: symbolName }, dbPath);
29413
+ return matches2.length === 0 ? null : pickEarliest(matches2);
29414
+ }
29415
+ function symbolNamesInFile(filePath, dbPath = globalDbPath()) {
29416
+ const symbols = querySymbols({ filePath, limit: 1e5 }, dbPath);
29417
+ return [...new Set(symbols.map((s) => s.name))].sort();
29418
+ }
29419
+ function computeFileFingerprint(filePath, dbPath = globalDbPath()) {
29420
+ const symbols = querySymbols({ filePath, limit: 1e6 }, dbPath);
29421
+ const manifest = symbols.map((s) => `${s.name}:${s.kind}:${s.lineStart}-${s.lineEnd}`).sort().join("\n");
29422
+ return fingerprintContent(manifest);
29423
+ }
29424
+ function computeSymbolFingerprint(filePath, symbolName, dbPath = globalDbPath()) {
29425
+ const match2 = resolveSymbolMatch(filePath, symbolName, dbPath);
29426
+ return match2 === null ? null : fingerprintContent(match2.body);
29427
+ }
29428
+ function upsertNote(filePath, symbol3, content, fingerprint, dbPath = globalDbPath()) {
29429
+ const db = getDb(dbPath);
29430
+ const now = Date.now() / 1e3;
29431
+ db.prepare(
29432
+ `INSERT INTO notes (file_path, symbol, content, fingerprint, created_at, updated_at)
29433
+ VALUES (?, ?, ?, ?, ?, ?)
29434
+ ON CONFLICT(file_path, symbol) DO UPDATE SET
29435
+ content = excluded.content,
29436
+ fingerprint = excluded.fingerprint,
29437
+ updated_at = excluded.updated_at`
29438
+ ).run(filePath, symbol3, content, fingerprint, now, now);
29439
+ }
29440
+ function getNote(filePath, symbol3, dbPath = globalDbPath()) {
29441
+ const db = getDb(dbPath);
29442
+ const row = db.prepare(
29443
+ `SELECT id, file_path, symbol, content, fingerprint, created_at, updated_at FROM notes WHERE ${pathEqClause("file_path")} AND symbol = ?`
29444
+ ).get(foldPath(filePath), symbol3);
29445
+ return row === void 0 ? null : toNoteRow(row);
29446
+ }
29447
+ function listNotes(dbPath = globalDbPath()) {
29448
+ const db = getDb(dbPath);
29449
+ const rows = db.prepare(
29450
+ "SELECT id, file_path, symbol, content, fingerprint, created_at, updated_at FROM notes ORDER BY file_path, symbol"
29451
+ ).all();
29452
+ return rows.map(toNoteRow);
29453
+ }
29454
+ function isNoteStale(note, dbPath = globalDbPath()) {
29455
+ const current = note.symbol === WHOLE_FILE_NOTE_SYMBOL ? computeFileFingerprint(note.filePath, dbPath) : computeSymbolFingerprint(note.filePath, note.symbol, dbPath);
29456
+ return current === null || current !== note.fingerprint;
29457
+ }
29458
+ var WHOLE_FILE_NOTE_SYMBOL;
29459
+ var init_notes = __esm({
29460
+ "src/notes.ts"() {
29461
+ "use strict";
29462
+ init_define_import_meta_env();
29463
+ init_constants();
29464
+ init_db();
29465
+ init_fingerprint();
29466
+ init_index_reader();
29467
+ init_sql_path();
29468
+ init_util2();
29469
+ WHOLE_FILE_NOTE_SYMBOL = "";
29470
+ }
29471
+ });
29472
+
29275
29473
  // src/ts_refs.ts
29276
29474
  import { createRequire as createRequire6 } from "node:module";
29277
29475
  import * as path45 from "node:path";
@@ -29824,6 +30022,9 @@ function runSection(opts) {
29824
30022
  const heading = opts.spec.slice(colonIdx + 2);
29825
30023
  const result = readSection(filePath, heading);
29826
30024
  if (result === null) {
30025
+ if (!fs36.existsSync(filePath)) {
30026
+ return { text: `File not found: '${filePath}'`, code: 1 };
30027
+ }
29827
30028
  const messages = [`Section '${heading}' not found in '${filePath}'`];
29828
30029
  const available = listSections(filePath);
29829
30030
  if (available.length > 0) messages.push(didYouMean(available));
@@ -29968,7 +30169,7 @@ function runRefsSingle(opts) {
29968
30169
  function groupRefsByFile(refs) {
29969
30170
  const byFile = /* @__PURE__ */ new Map();
29970
30171
  for (const ref2 of refs) byFile.set(ref2.filePath, (byFile.get(ref2.filePath) ?? 0) + 1);
29971
- return [...byFile.entries()].map(([file2, count]) => ({ file: file2, count })).sort((a, b) => b.count - a.count || a.file.localeCompare(b.file));
30172
+ 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));
29972
30173
  }
29973
30174
  function renderTopFilesSummary(refs, topN) {
29974
30175
  const grouped = groupRefsByFile(refs);
@@ -31478,6 +31679,11 @@ function extractImports(text, ext2) {
31478
31679
  const m = /^\s*#\s*include\s+[<"]([^>"]+)[>"]/.exec(line);
31479
31680
  if (m) push(m[1]);
31480
31681
  }
31682
+ } else if ([".sh", ".bash"].includes(e)) {
31683
+ for (const line of lines2) {
31684
+ const m = /^\s*(?:source|\.)\s+['"]?([^\s'";]+)['"]?/.exec(line);
31685
+ if (m) push(m[1]);
31686
+ }
31481
31687
  } else if ([".ps1", ".psm1"].includes(e)) {
31482
31688
  for (const line of lines2) {
31483
31689
  const importMod = /^\s*Import-Module\s+(?:-Name\s+)?['"]?([^\s'";]+)/i.exec(line);
@@ -31718,6 +31924,59 @@ ${previewLines(s.body, 3)}`);
31718
31924
  recordReadStat("semantic_search", sumFileSizes(results.map((s) => s.filePath)), text, query);
31719
31925
  return { text, code: 0 };
31720
31926
  }
31927
+ function runNoteGet(opts) {
31928
+ const resolvedPath = resolveIndexPath(opts.file, opts.projectRoot ?? process.cwd());
31929
+ healStaleIndex(resolvedPath);
31930
+ const symbol3 = opts.symbol ?? WHOLE_FILE_NOTE_SYMBOL;
31931
+ const note = getNote(resolvedPath, symbol3);
31932
+ if (note === null) {
31933
+ const where = opts.symbol !== void 0 ? ` for symbol '${opts.symbol}'` : " (whole-file note)";
31934
+ return { text: `No note found for '${opts.file}'${where}`, code: 1 };
31935
+ }
31936
+ const stale = isNoteStale(note);
31937
+ if (opts.json === true) {
31938
+ const payload = {
31939
+ filePath: note.filePath,
31940
+ symbol: note.symbol === WHOLE_FILE_NOTE_SYMBOL ? null : note.symbol,
31941
+ content: note.content,
31942
+ stale,
31943
+ createdAt: note.createdAt,
31944
+ updatedAt: note.updatedAt
31945
+ };
31946
+ const text2 = JSON.stringify(payload, null, 2);
31947
+ recordStat("note_read");
31948
+ return { text: text2, code: 0 };
31949
+ }
31950
+ const target = note.symbol === WHOLE_FILE_NOTE_SYMBOL ? opts.file : `${opts.file}::${note.symbol}`;
31951
+ const staleTag = stale ? " [STALE \u2014 code changed since this note was written]" : "";
31952
+ const text = `# note \u2014 ${target}${staleTag}
31953
+ ${note.content}`;
31954
+ recordStat("note_read");
31955
+ return { text, code: 0 };
31956
+ }
31957
+ function runNoteList(opts = {}) {
31958
+ const withStale = listNotes().map((note) => ({ note, stale: isNoteStale(note) }));
31959
+ const filtered = opts.staleOnly === true ? withStale.filter((n) => n.stale) : withStale;
31960
+ if (opts.json === true) {
31961
+ const items = filtered.map(({ note, stale }) => ({
31962
+ filePath: note.filePath,
31963
+ symbol: note.symbol === WHOLE_FILE_NOTE_SYMBOL ? null : note.symbol,
31964
+ stale,
31965
+ updatedAt: note.updatedAt
31966
+ }));
31967
+ recordStat("note_list");
31968
+ return { text: JSON.stringify(items, null, 2), code: 0 };
31969
+ }
31970
+ recordStat("note_list");
31971
+ if (filtered.length === 0) {
31972
+ return { text: opts.staleOnly === true ? "No stale notes." : "No notes recorded.", code: 0 };
31973
+ }
31974
+ const lines2 = filtered.map(({ note, stale }) => {
31975
+ const target = note.symbol === WHOLE_FILE_NOTE_SYMBOL ? note.filePath : `${note.filePath}::${note.symbol}`;
31976
+ return `${stale ? "[STALE] " : ""}${target}`;
31977
+ });
31978
+ return { text: lines2.join("\n"), code: 0 };
31979
+ }
31721
31980
  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;
31722
31981
  var init_read_commands = __esm({
31723
31982
  "src/read_commands.ts"() {
@@ -31752,6 +32011,7 @@ var init_read_commands = __esm({
31752
32011
  init_pdf_extract();
31753
32012
  init_screenshot();
31754
32013
  init_stats();
32014
+ init_notes();
31755
32015
  init_ts_refs();
31756
32016
  DIDYOUMEAN_LIMIT = 5;
31757
32017
  GREP_MAX_LINES = 200;
@@ -48167,9 +48427,10 @@ async function storeBashOutput(command, output, exitCode, cwd = null) {
48167
48427
  const id = await commandHash(command, cwd);
48168
48428
  const fingerprints = computeBashFingerprints(command, cwd);
48169
48429
  const redactedOutput = redactSecrets(output).text;
48430
+ const redactedCommand = redactSecrets(command).text;
48170
48431
  const entry = {
48171
48432
  id,
48172
- command,
48433
+ command: redactedCommand,
48173
48434
  output: redactedOutput,
48174
48435
  exitCode,
48175
48436
  storedAt: Date.now(),
@@ -48178,7 +48439,7 @@ async function storeBashOutput(command, output, exitCode, cwd = null) {
48178
48439
  };
48179
48440
  _byId.set(id, entry);
48180
48441
  storeBlob(BASH_OUTPUT_SUBDIR, id, entry);
48181
- indexRecallEntry("bash", id, command, `${command}
48442
+ indexRecallEntry("bash", id, redactedCommand, `${redactedCommand}
48182
48443
  ${redactedOutput}`, entry.storedAt);
48183
48444
  return id;
48184
48445
  }
@@ -48608,7 +48869,8 @@ function storeWebOutput(url2, content, dedupKey = url2) {
48608
48869
  _byId2.set(cacheId, redactedContent);
48609
48870
  _urlIndex.set(url2, cacheId);
48610
48871
  storeBlob(WEB_OUTPUT_SUBDIR, cacheId, { url: url2, content: redactedContent });
48611
- indexRecallEntry("web", cacheId, url2, `${url2}
48872
+ const redactedUrl = redactSecrets(url2).text;
48873
+ indexRecallEntry("web", cacheId, redactedUrl, `${redactedUrl}
48612
48874
  ${redactedContent}`, Date.now());
48613
48875
  return cacheId;
48614
48876
  }
@@ -54653,7 +54915,7 @@ function preFetchHandler(event) {
54653
54915
  if (cached2 !== null) {
54654
54916
  const cachedBytes = Buffer.byteLength(cached2, "utf-8");
54655
54917
  if (cachedBytes >= loadConfig().hints.web_dedup_min_bytes) {
54656
- recordStat("webfetch:recall", cachedBytes, Math.round(cached2.length / 4));
54918
+ recordStat("webfetch:recall", cachedBytes, Math.round(cachedBytes / 4));
54657
54919
  return denyOutput(
54658
54920
  "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."
54659
54921
  );
@@ -55172,8 +55434,17 @@ function pathStem(p) {
55172
55434
  const dot = name2.lastIndexOf(".");
55173
55435
  return dot > 0 ? name2.slice(0, dot) : name2;
55174
55436
  }
55175
- function positionalArgs(args) {
55176
- return args.filter((a) => !a.startsWith("-"));
55437
+ function positionalArgs(args, valueFlags) {
55438
+ const out2 = [];
55439
+ for (let i = 0; i < args.length; i++) {
55440
+ const a = args[i];
55441
+ if (a.startsWith("-")) {
55442
+ if (valueFlags?.has(a) === true && i + 1 < args.length) i++;
55443
+ continue;
55444
+ }
55445
+ out2.push(a);
55446
+ }
55447
+ return out2;
55177
55448
  }
55178
55449
  var SHORT_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set(["-n", "-c", "-u", "-e"]);
55179
55450
  function stripPrefixes(argv) {
@@ -55311,6 +55582,7 @@ function byteLength(s) {
55311
55582
 
55312
55583
  // src/tool_filters/base.ts
55313
55584
  init_define_import_meta_env();
55585
+ init_secret_redact();
55314
55586
  var CompressedOutput = class {
55315
55587
  constructor(text, originalBytes, compressedBytes, filterName, exitCode = 0, notes = []) {
55316
55588
  this.text = text;
@@ -55480,6 +55752,9 @@ ${stderr.replace(/\s+$/, "")}`;
55480
55752
  const lines2 = body.split("\n");
55481
55753
  if (lines2.length > maxLines) body = truncateMiddleSmart(lines2, maxLines).join("\n");
55482
55754
  body = capBytes(body, maxBytes);
55755
+ const redacted = redactSecrets(body);
55756
+ body = redacted.text;
55757
+ if (redacted.count > 0) notes.push(`redacted ${redacted.count} secret-shaped value(s)`);
55483
55758
  if (notes.length) body = `[${notes.join("; ")}]
55484
55759
  ${body}`;
55485
55760
  return new CompressedOutput(body, originalBytes, byteLength(body), this.name, exitCode, notes);
@@ -58730,7 +59005,9 @@ var _GIT_VALUE_FLAGS = /* @__PURE__ */ new Set([
58730
59005
  "-F",
58731
59006
  "--file",
58732
59007
  "--author",
58733
- "--date"
59008
+ "--date",
59009
+ "--git-dir",
59010
+ "--work-tree"
58734
59011
  ]);
58735
59012
  function gitPositionalArgs(args) {
58736
59013
  const out2 = [];
@@ -61258,6 +61535,7 @@ var _DOCKER_OLD_SHA_RE = /^ *---> (?:sha256:)?[0-9a-f]{12,}\s*$/;
61258
61535
  var _DOCKER_OLD_STEP_RE = /^Step \d+\/\d+ : /;
61259
61536
  var _DOCKER_OLD_SUCCESS_RE = /^Successfully built [0-9a-f]+/;
61260
61537
  var _DOCKER_OLD_INTERMEDIATE_RE = /^Removing intermediate container [0-9a-f]+/;
61538
+ var _DOCKER_OLD_STEP_ERROR_RE = /error|returned a non-zero code/i;
61261
61539
  var DockerFilter = class extends ToolFilter {
61262
61540
  name = "docker";
61263
61541
  binaries = /* @__PURE__ */ new Set(["docker", "buildah", "podman", "nerdctl"]);
@@ -61338,7 +61616,7 @@ var DockerFilter = class extends ToolFilter {
61338
61616
  oldStepErr = false;
61339
61617
  continue;
61340
61618
  }
61341
- if (ol.toLowerCase().includes("error")) oldStepErr = true;
61619
+ if (_DOCKER_OLD_STEP_ERROR_RE.test(ol)) oldStepErr = true;
61342
61620
  oldNew.push(ol);
61343
61621
  }
61344
61622
  if (oldStepHdr !== null) {
@@ -61575,12 +61853,26 @@ function _compressKubectlDescribe(text) {
61575
61853
  }
61576
61854
  return kept.join("\n");
61577
61855
  }
61856
+ var KUBECTL_GLOBAL_VALUE_FLAGS = /* @__PURE__ */ new Set([
61857
+ "-n",
61858
+ "--namespace",
61859
+ "--context",
61860
+ "--kubeconfig",
61861
+ "--cluster",
61862
+ "--user",
61863
+ "-s",
61864
+ "--server",
61865
+ "--token",
61866
+ "--as",
61867
+ "--as-group",
61868
+ "--request-timeout"
61869
+ ]);
61578
61870
  var KubectlFilter = class extends ToolFilter {
61579
61871
  name = "kubectl";
61580
61872
  binaries = /* @__PURE__ */ new Set(["kubectl", "k", "k9s", "oc"]);
61581
61873
  errorPassthrough = true;
61582
61874
  compressBody(stdout, stderr, _exitCode, argv) {
61583
- const pos = positionalArgs(argv.slice(1));
61875
+ const pos = positionalArgs(argv.slice(1), KUBECTL_GLOBAL_VALUE_FLAGS);
61584
61876
  const subcommand = pos[0] ?? "";
61585
61877
  let text = stdout;
61586
61878
  if (subcommand === "get" || subcommand === "top") {
@@ -61762,7 +62054,7 @@ var KubectlLogsFilter = class extends ToolFilter {
61762
62054
  const stem = pathStem(argv[0]).toLowerCase();
61763
62055
  const name2 = pathName(argv[0]).toLowerCase();
61764
62056
  if (!["kubectl", "k"].includes(stem) && !["kubectl", "k"].includes(name2)) return false;
61765
- const pos = positionalArgs(argv.slice(1));
62057
+ const pos = positionalArgs(argv.slice(1), KUBECTL_GLOBAL_VALUE_FLAGS);
61766
62058
  return pos.length > 0 && pos[0] === "logs";
61767
62059
  }
61768
62060
  compressBody(stdout, stderr, _exitCode, _argv) {
@@ -62208,6 +62500,19 @@ var awsFilter = new AwsFilter();
62208
62500
  var _AWS_UPLOAD_RE = /^upload:\s+\S+\s+to\s+s3:\/\//i;
62209
62501
  var _AWS_DOWNLOAD_RE = /^download:\s+s3:\/\//i;
62210
62502
  var _AWS_S3_PROGRESS_RE = /^(?:Completed\s+\d|\d+(?:\.\d+)?\s*(?:KiB|MiB|GiB|B)\/s|Calculating|upload\s+failed:|download\s+failed:)/i;
62503
+ var AWS_GLOBAL_VALUE_FLAGS = /* @__PURE__ */ new Set([
62504
+ "--profile",
62505
+ "--region",
62506
+ "--endpoint-url",
62507
+ "--output",
62508
+ "--query",
62509
+ "--color",
62510
+ "--ca-bundle",
62511
+ "--cli-read-timeout",
62512
+ "--cli-connect-timeout",
62513
+ "--cli-binary-format",
62514
+ "--cli-pager"
62515
+ ]);
62211
62516
  var AwsCliFilter = class extends ToolFilter {
62212
62517
  name = "aws-cli";
62213
62518
  binaries = /* @__PURE__ */ new Set(["aws", "aws2"]);
@@ -62215,7 +62520,7 @@ var AwsCliFilter = class extends ToolFilter {
62215
62520
  _JSON_ARRAY_THRESHOLD = 10;
62216
62521
  _JSON_ARRAY_KEEP = 3;
62217
62522
  compressBody(stdout, stderr, _exitCode, argv) {
62218
- const positionals = positionalArgs(argv.slice(1));
62523
+ const positionals = positionalArgs(argv.slice(1), AWS_GLOBAL_VALUE_FLAGS);
62219
62524
  const isS3Transfer = positionals.length >= 2 && positionals[0] === "s3" && (positionals[1] === "cp" || positionals[1] === "sync" || positionals[1] === "mv");
62220
62525
  const isCfnEvents = positionals.length >= 2 && positionals[0] === "cloudformation" && positionals[1] === "describe-stack-events";
62221
62526
  let text = stdout;
@@ -63606,6 +63911,7 @@ var _GH_RUN_PASS_STEP_RE = /^\s*[✓√]\s/;
63606
63911
  var _GH_RUN_FAIL_STEP_RE = /^\s*[X✗❌]\s|^\s*FAIL(:|ED|URE)\b|^\s*Error:\s/;
63607
63912
  var _GH_API_URL_SUFFIX = "_url";
63608
63913
  var _GH_API_URL_KEEP = /* @__PURE__ */ new Set(["html_url", "avatar_url", "clone_url", "ssh_url"]);
63914
+ var GH_GLOBAL_VALUE_FLAGS = /* @__PURE__ */ new Set(["-R", "--repo", "--hostname"]);
63609
63915
  var _GH_API_NOISE_KEYS = /* @__PURE__ */ new Set(["gravatar_id", "site_admin"]);
63610
63916
  var _GH_CONTENT_B64_RE = /^[A-Za-z0-9+/=\n]+$/;
63611
63917
  var _GH_BASE64_MIN_LEN = 200;
@@ -63890,7 +64196,7 @@ var GhFilter = class extends ToolFilter {
63890
64196
  binaries = /* @__PURE__ */ new Set(["gh"]);
63891
64197
  compress(stdout, stderr, _exitCode, argv) {
63892
64198
  const redactedStdout = redactGhBase64Content(stdout);
63893
- const positionals = positionalArgs(argv.slice(1));
64199
+ const positionals = positionalArgs(argv.slice(1), GH_GLOBAL_VALUE_FLAGS);
63894
64200
  const subcommand = positionals[0] ?? "";
63895
64201
  const action = positionals[1] ?? "";
63896
64202
  const merged = this.combineOutput(redactedStdout, stderr);
@@ -64695,9 +65001,38 @@ var LsFilter = class _LsFilter extends ToolFilter {
64695
65001
  var _EZA_PASSTHROUGH = 30;
64696
65002
  var _HEADER_KEYWORDS = /* @__PURE__ */ new Set(["permission", "size", "date", "user", "name"]);
64697
65003
  var _SUMMARY_KEYWORDS = ["director", "file", "total"];
64698
- var EzaFilter = class extends ToolFilter {
65004
+ var EzaFilter = class _EzaFilter extends ToolFilter {
64699
65005
  name = "eza";
64700
65006
  binaries = /* @__PURE__ */ new Set(["eza", "exa", "ls"]);
65007
+ // Flags eza/exa support that plain GNU/BSD `ls` does not -- used to disambiguate the shared
65008
+ // 'ls' binary claim below (a common `alias ls=eza` setup means the literal command text is
65009
+ // "ls ...", but token-goat only ever sees that raw text, never the shell's alias resolution).
65010
+ static _EZA_ONLY_FLAGS = /* @__PURE__ */ new Set([
65011
+ "--tree",
65012
+ "-T",
65013
+ "--icons",
65014
+ "--no-icons",
65015
+ "--git",
65016
+ "--git-repos",
65017
+ "--git-repos-no-status",
65018
+ "--level"
65019
+ ]);
65020
+ // LsFilter (registered before EzaFilter in SHELL_FILE_FILTERS) always wins a plain `ls`
65021
+ // invocation since binary-name matching alone can't tell a real GNU/BSD `ls` from an
65022
+ // `alias ls=eza` shell alias -- the literal command text is "ls ..." either way. Without this
65023
+ // gate, EzaFilter's own 'ls' binary claim was permanently unreachable dead code: LsFilter's
65024
+ // generic ls-format compressor (no awareness of eza's tree/column output) silently ran on
65025
+ // every aliased `ls --tree`/`ls --icons`/etc. invocation instead. Mirrors RgFilter's own
65026
+ // `_hasContextFlags` gate, which resolves the same kind of shared-binary ambiguity between
65027
+ // itself and GrepFilter.
65028
+ matches(argv) {
65029
+ if (!super.matches(argv)) return false;
65030
+ const first2 = argv[0];
65031
+ const stem = pathStem(first2).toLowerCase();
65032
+ const name2 = pathName(first2).toLowerCase();
65033
+ if (stem !== "ls" && name2 !== "ls") return true;
65034
+ return argv.slice(1).some((a) => _EzaFilter._EZA_ONLY_FLAGS.has(a));
65035
+ }
64701
65036
  compressBody(stdout, stderr, _exitCode, argv) {
64702
65037
  const merged = this.combineOutput(stdout, stderr);
64703
65038
  const text = normalise(merged);
@@ -65455,9 +65790,11 @@ var SHELL_FILE_FILTERS = [
65455
65790
  ffmpegFilter,
65456
65791
  // Diff tool (plain POSIX diff; git diff is handled by GitFilter)
65457
65792
  diffFilter,
65458
- // Directory listings — LsFilter (simple) before EzaFilter (richer tree/column-aware)
65459
- lsFilter,
65793
+ // Directory listings — EzaFilter before LsFilter: EzaFilter's matches() gate falls through to
65794
+ // LsFilter for a plain `ls` with no eza-only flag, but must run FIRST so it can actually claim
65795
+ // an aliased `ls --tree`/`ls --icons`/etc. invocation (see EzaFilter.matches doc comment).
65460
65796
  ezaFilter,
65797
+ lsFilter,
65461
65798
  fdFilter,
65462
65799
  wcFilter,
65463
65800
  treeFilter,
@@ -65967,7 +66304,7 @@ var flutterFilter = new FlutterFilter();
65967
66304
  var DART_ANALYZING_RE = /^Analyzing\s/;
65968
66305
  var DART_ANALYZE_RESULT_RE = /^(?:No issues found!|\d+ issue[s]? found\.|warning -|error -|info -|hint -)/;
65969
66306
  var DART_TEST_PROGRESS_RE = /^\d{2}:\d{2}\s+[+\d]|^[.]+$/;
65970
- var DART_COMPILE_DONE_RE = /^(?:Generated:\s|Compiling\s)/;
66307
+ var DART_COMPILE_DONE_RE = /^Generated:\s/;
65971
66308
  var DART_TEST_SUMMARY_RE = /(?:All tests passed\.?|\d+\s+test[s]?\s+(?:passed|failed))/;
65972
66309
  var PUB_KEEP_RE2 = /^(?:Resolving dependencies|Changed \d+|No dependencies changed|Got dependencies|Downloading packages|Building package executable)/;
65973
66310
  var PUB_PKG_LINE_RE2 = /^[+>!]\s+\S+\s+\S+/;
@@ -69710,8 +70047,9 @@ function storeMcpOutput(sessionId, toolName, toolInput, resultText) {
69710
70047
  const rawSizeBytes = Buffer.byteLength(resultText, "utf-8");
69711
70048
  if (rawSizeBytes > MCP_MAX_CACHE_BYTES) return null;
69712
70049
  const id = mcpOutputId(sessionId, mcpHash(toolName, toolInput));
69713
- const label = `mcp:${toolName} ${mcpInputPreview(toolInput)}`.trim();
70050
+ const rawLabel = `mcp:${toolName} ${mcpInputPreview(toolInput)}`.trim();
69714
70051
  const redactedOutput = redactSecrets(resultText).text;
70052
+ const label = redactSecrets(rawLabel).text;
69715
70053
  const entry = {
69716
70054
  id,
69717
70055
  command: label,
@@ -69980,6 +70318,7 @@ function compressMcpResultWithPacks(toolName, resultText) {
69980
70318
  }
69981
70319
 
69982
70320
  // src/hooks_mcp.ts
70321
+ init_secret_redact();
69983
70322
  function isMcpErrorResponse(raw) {
69984
70323
  const tr = raw["tool_response"];
69985
70324
  if (!tr || typeof tr !== "object") return false;
@@ -70019,7 +70358,7 @@ function postMcpHandler(event) {
70019
70358
  return {
70020
70359
  hookType: "rewriteOutput",
70021
70360
  updatedOutput: `[token-goat: compressed, full via mcp-output ${id}]
70022
- ${compressed}`
70361
+ ${redactSecrets(compressed).text}`
70023
70362
  };
70024
70363
  }
70025
70364
  }
@@ -71753,6 +72092,7 @@ function recordSavings(result) {
71753
72092
 
71754
72093
  // src/cli.ts
71755
72094
  init_read_commands();
72095
+ init_notes();
71756
72096
 
71757
72097
  // src/bridges_status.ts
71758
72098
  init_define_import_meta_env();
@@ -72168,6 +72508,7 @@ function formatCues(cues) {
72168
72508
  init_graph_commands();
72169
72509
  init_skill_cache();
72170
72510
  init_hooks_read();
72511
+ init_section_reader();
72171
72512
  init_util2();
72172
72513
  init_ansi();
72173
72514
  init_config();
@@ -75515,6 +75856,9 @@ import * as fs43 from "node:fs";
75515
75856
  import * as path53 from "node:path";
75516
75857
  var MAX_ENTRIES = 30;
75517
75858
  var KEY_RE2 = /^[A-Za-z0-9_-]{1,80}$/;
75859
+ function ordinal(a, b) {
75860
+ return a < b ? -1 : a > b ? 1 : 0;
75861
+ }
75518
75862
  function memoryPath(projectHash2) {
75519
75863
  return path53.join(dataDir(), "projects", `${projectHash2}_memory.toml`);
75520
75864
  }
@@ -75569,7 +75913,7 @@ function loadRaw(filePath) {
75569
75913
  }
75570
75914
  function save(filePath, entries) {
75571
75915
  const lines2 = [];
75572
- const sorted = Object.entries(entries).sort(([a], [b]) => a.localeCompare(b));
75916
+ const sorted = Object.entries(entries).sort(([a], [b]) => ordinal(a, b));
75573
75917
  for (const [k, v] of sorted) {
75574
75918
  const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n");
75575
75919
  lines2.push(`${k} = "${escaped}"`);
@@ -75592,7 +75936,7 @@ function setEntry(projectHash2, key, value) {
75592
75936
  const isNewKey = !(key in entries);
75593
75937
  if (isNewKey && Object.keys(entries).length >= MAX_ENTRIES) {
75594
75938
  const keysToKeep = MAX_ENTRIES - 1;
75595
- const allKeys = Object.keys(entries).sort((a, b) => a.localeCompare(b));
75939
+ const allKeys = Object.keys(entries).sort((a, b) => ordinal(a, b));
75596
75940
  for (const k of allKeys.slice(keysToKeep)) {
75597
75941
  delete entries[k];
75598
75942
  }
@@ -76237,7 +76581,7 @@ function parseRequirementsTxt(content) {
76237
76581
  const deps = [];
76238
76582
  for (const raw of splitLines2(content)) {
76239
76583
  if (/^\s*#/.test(raw)) continue;
76240
- const eggMatch = /^\s*(?:git|hg|svn|bzr)\+.*#egg=([A-Za-z0-9_.-]+)/.exec(raw);
76584
+ const eggMatch = /^\s*(?:-e\s+|--editable[\s=]+)?(?:git|hg|svn|bzr)\+.*#egg=([A-Za-z0-9_.-]+)/.exec(raw);
76241
76585
  if (eggMatch !== null) {
76242
76586
  deps.push({ name: eggMatch[1] ?? "", version: "", kind: "unknown" });
76243
76587
  continue;
@@ -76805,8 +77149,11 @@ function resolveTypesLocation(pkgDir, pkgJson, nodeModulesDir, pkgName) {
76805
77149
  try {
76806
77150
  const typesPkgJson = JSON.parse(typesPkgJsonRaw);
76807
77151
  const entry = (typeof typesPkgJson["types"] === "string" ? typesPkgJson["types"] : void 0) ?? (typeof typesPkgJson["main"] === "string" ? typesPkgJson["main"] : void 0) ?? "index.d.ts";
76808
- const entryPath = path55.join(typesPkgDir, entry.endsWith(".d.ts") ? entry : `${entry}.d.ts`);
76809
- if (fileExists2(entryPath)) return { path: entryPath, source: "@types" };
77152
+ const entryCandidates = [entry, entry.endsWith(".d.ts") ? entry : `${entry}.d.ts`, entry.replace(/\.[cm]?[jt]s$/, ".d.ts")];
77153
+ for (const c of entryCandidates) {
77154
+ const entryPath = path55.join(typesPkgDir, c);
77155
+ if (fileExists2(entryPath)) return { path: entryPath, source: "@types" };
77156
+ }
76810
77157
  } catch {
76811
77158
  }
76812
77159
  const fallback = path55.join(typesPkgDir, "index.d.ts");
@@ -78457,8 +78804,11 @@ var FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "Noteboo
78457
78804
  function extractFilePath(name2, input) {
78458
78805
  if (!FILE_PATH_TOOLS.has(name2)) return null;
78459
78806
  if (input === null || typeof input !== "object") return null;
78460
- const fp = input["file_path"];
78461
- return typeof fp === "string" ? fp : null;
78807
+ const o = input;
78808
+ const fp = o["file_path"];
78809
+ if (typeof fp === "string") return fp;
78810
+ const notebookPath = o["notebook_path"];
78811
+ return typeof notebookPath === "string" ? notebookPath : null;
78462
78812
  }
78463
78813
  function extractCommand2(name2, input) {
78464
78814
  if (name2 !== "Bash") return null;
@@ -79751,7 +80101,11 @@ function cmdVideoChapters(file2) {
79751
80101
  }
79752
80102
  lines2.push("(extract a subtitle stream to .vtt/.srt with ffmpeg, then use transcript/transcript-outline on it)");
79753
80103
  }
79754
- out(lines2.join("\n"));
80104
+ const text = lines2.join("\n");
80105
+ out(text);
80106
+ const fullSourceBytes = fileSizeOrZero(file2);
80107
+ const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(text, "utf8"));
80108
+ recordStat("video_chapters", bytesSaved, Math.round(bytesSaved / 4));
79755
80109
  }
79756
80110
  function formatVideoTimestamp(totalSeconds) {
79757
80111
  const hours = Math.floor(totalSeconds / 3600);
@@ -80388,6 +80742,50 @@ function decodeBase64Buffer(payload, label) {
80388
80742
  }
80389
80743
  return Buffer.from(normalized, "base64");
80390
80744
  }
80745
+ function cmdNoteAdd(file2, opts) {
80746
+ if (!file2 || !file2.trim()) {
80747
+ throw new CliError("file path cannot be empty");
80748
+ }
80749
+ const usingFrom = opts.contentFrom !== void 0;
80750
+ const usingB64 = opts.contentB64 !== void 0;
80751
+ if (usingFrom && usingB64) {
80752
+ throw new CliError("cannot mix --content-from with --content-b64");
80753
+ }
80754
+ if (!usingFrom && !usingB64) {
80755
+ throw new CliError("must provide either --content-from or --content-b64");
80756
+ }
80757
+ const contentBytes = usingFrom ? readFileBoundedRaw(opts.contentFrom, "--content-from") : decodeBase64Buffer(opts.contentB64, "--content-b64");
80758
+ if (contentBytes.length === 0) {
80759
+ throw new CliError("note content cannot be empty");
80760
+ }
80761
+ if (Buffer.compare(Buffer.from(contentBytes.toString("utf8"), "utf8"), contentBytes) !== 0) {
80762
+ throw new CliError("note content must be valid UTF-8 text");
80763
+ }
80764
+ const resolvedPath = resolveIndexPath(file2);
80765
+ if (!fs52.existsSync(resolvedPath)) {
80766
+ throw new CliError(`File not found: '${resolvedPath}'`);
80767
+ }
80768
+ healStaleIndex(resolvedPath);
80769
+ let symbol3 = WHOLE_FILE_NOTE_SYMBOL;
80770
+ let fingerprint;
80771
+ if (opts.symbol !== void 0) {
80772
+ const match2 = resolveSymbolMatch(resolvedPath, opts.symbol);
80773
+ if (match2 === null) {
80774
+ const messages = [`No symbol named '${opts.symbol}' is indexed in '${file2}'`];
80775
+ const available = symbolNamesInFile(resolvedPath);
80776
+ if (available.length > 0) messages.push(didYouMean(available));
80777
+ throw new CliError(messages.join("\n"));
80778
+ }
80779
+ symbol3 = opts.symbol;
80780
+ fingerprint = fingerprintContent(match2.body);
80781
+ } else {
80782
+ fingerprint = computeFileFingerprint(resolvedPath);
80783
+ }
80784
+ upsertNote(resolvedPath, symbol3, contentBytes.toString("utf8"), fingerprint);
80785
+ const target = opts.symbol !== void 0 ? `${file2}::${opts.symbol}` : file2;
80786
+ out(`Note saved: ${target} (fingerprint ${fingerprint.slice(0, 12)})`);
80787
+ recordStat("note_write");
80788
+ }
80391
80789
  function cmdWriteFile(dest, opts) {
80392
80790
  validateWritablePath(dest, "destination");
80393
80791
  if (opts.from !== void 0 && opts.b64 !== void 0) {
@@ -80493,6 +80891,56 @@ function diagnoseNearMiss(targetText, oldText) {
80493
80891
  }
80494
80892
  return void 0;
80495
80893
  }
80894
+ function detectDominantEol(buf) {
80895
+ let crlf = 0;
80896
+ let lfOnly = 0;
80897
+ for (let i = 0; i < buf.length; i++) {
80898
+ if (buf[i] === 10) {
80899
+ if (i > 0 && buf[i - 1] === 13) crlf++;
80900
+ else lfOnly++;
80901
+ }
80902
+ }
80903
+ return crlf > lfOnly ? "\r\n" : "\n";
80904
+ }
80905
+ function normalizeEolToMatch(source, reference) {
80906
+ const eol = detectDominantEol(reference);
80907
+ const CR = 13;
80908
+ const LF = 10;
80909
+ const collapsed = [];
80910
+ for (let i = 0; i < source.length; i++) {
80911
+ if (source[i] === CR && source[i + 1] === LF) continue;
80912
+ collapsed.push(source[i]);
80913
+ }
80914
+ if (eol === "\n") return Buffer.from(collapsed);
80915
+ const expanded = [];
80916
+ for (const b of collapsed) {
80917
+ if (b === LF) expanded.push(CR, LF);
80918
+ else expanded.push(b);
80919
+ }
80920
+ return Buffer.from(expanded);
80921
+ }
80922
+ var MAX_CLOSEST_MATCH_COMPARISONS = 2e6;
80923
+ function findClosestLineWindow(targetText, oldText) {
80924
+ const targetLines = targetText.split("\n");
80925
+ const oldLines = oldText.split("\n");
80926
+ const windowSize = oldLines.length;
80927
+ if (windowSize === 0 || windowSize > targetLines.length) return void 0;
80928
+ if ((targetLines.length - windowSize + 1) * windowSize > MAX_CLOSEST_MATCH_COMPARISONS) return void 0;
80929
+ let bestIdx = -1;
80930
+ let bestScore = 0;
80931
+ for (let i = 0; i <= targetLines.length - windowSize; i++) {
80932
+ let score = 0;
80933
+ for (let j = 0; j < windowSize; j++) {
80934
+ if (targetLines[i + j] === oldLines[j]) score++;
80935
+ }
80936
+ if (score > bestScore) {
80937
+ bestScore = score;
80938
+ bestIdx = i;
80939
+ }
80940
+ }
80941
+ if (bestIdx === -1) return void 0;
80942
+ return { lineStart: bestIdx + 1, region: targetLines.slice(bestIdx, bestIdx + windowSize).join("\n") };
80943
+ }
80496
80944
  function cmdReplace(file2, opts) {
80497
80945
  validateWritablePath(file2, "target file");
80498
80946
  const targetBuf = readFileBoundedRaw(file2, "target file", true);
@@ -80520,19 +80968,32 @@ function cmdReplace(file2, opts) {
80520
80968
  }
80521
80969
  const oldBytes = usingFrom ? readFileBoundedRaw(opts.oldFrom, "--old-from") : decodeBase64Buffer(opts.oldB64, "--old-b64");
80522
80970
  const newBytes = usingFrom ? readFileBoundedRaw(opts.newFrom, "--new-from") : decodeBase64Buffer(opts.newB64, "--new-b64");
80523
- if (oldBytes.length === 0) {
80971
+ const normalizedOldBytes = opts.normalizeNewlines === true ? normalizeEolToMatch(oldBytes, targetBuf) : oldBytes;
80972
+ const normalizedNewBytes = opts.normalizeNewlines === true ? normalizeEolToMatch(newBytes, targetBuf) : newBytes;
80973
+ if (normalizedOldBytes.length === 0) {
80524
80974
  throw new CliError("old string cannot be empty");
80525
80975
  }
80526
80976
  const matches2 = [];
80527
80977
  let cursor = 0;
80528
- while ((cursor = targetBuf.indexOf(oldBytes, cursor)) !== -1) {
80978
+ while ((cursor = targetBuf.indexOf(normalizedOldBytes, cursor)) !== -1) {
80529
80979
  matches2.push(cursor);
80530
- cursor += oldBytes.length;
80980
+ cursor += normalizedOldBytes.length;
80531
80981
  }
80532
80982
  const occurrences = matches2.length;
80533
80983
  if (occurrences === 0) {
80534
- const nearMiss = diagnoseNearMiss(targetBuf.toString("utf8"), oldBytes.toString("utf8"));
80535
- throw new CliError(nearMiss !== void 0 ? `old string not found in ${file2} \u2014 ${nearMiss}` : `old string not found in ${file2}`);
80984
+ const nearMiss = diagnoseNearMiss(targetBuf.toString("utf8"), normalizedOldBytes.toString("utf8"));
80985
+ if (nearMiss !== void 0) {
80986
+ throw new CliError(`old string not found in ${file2} \u2014 ${nearMiss}`);
80987
+ }
80988
+ const closest = findClosestLineWindow(targetBuf.toString("utf8"), normalizedOldBytes.toString("utf8"));
80989
+ if (closest !== void 0) {
80990
+ const diff = buildLineDiff(closest.region, normalizedOldBytes.toString("utf8"), file2);
80991
+ throw new CliError(
80992
+ `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):
80993
+ ${diff}`
80994
+ );
80995
+ }
80996
+ throw new CliError(`old string not found in ${file2}`);
80536
80997
  }
80537
80998
  if (occurrences > 1 && !opts.all) {
80538
80999
  throw new CliError(`old string appears ${occurrences} times in ${file2} \u2014 pass --all to replace every occurrence, or provide a more specific match`);
@@ -80541,8 +81002,8 @@ function cmdReplace(file2, opts) {
80541
81002
  let prevEnd = 0;
80542
81003
  for (const pos of matches2) {
80543
81004
  parts.push(targetBuf.subarray(prevEnd, pos));
80544
- parts.push(newBytes);
80545
- prevEnd = pos + oldBytes.length;
81005
+ parts.push(normalizedNewBytes);
81006
+ prevEnd = pos + normalizedOldBytes.length;
80546
81007
  }
80547
81008
  parts.push(targetBuf.subarray(prevEnd));
80548
81009
  const replacedBuf = Buffer.concat(parts);
@@ -80569,6 +81030,65 @@ function cmdReplace(file2, opts) {
80569
81030
  enqueueDirtyPathSafe(file2);
80570
81031
  out(`replaced ${occurrences} occurrence${occurrences === 1 ? "" : "s"} in ${file2}`);
80571
81032
  }
81033
+ function cmdInsertSection(file2, opts) {
81034
+ validateWritablePath(file2, "target file");
81035
+ const usingFrom = opts.contentFrom !== void 0;
81036
+ const usingB64 = opts.contentB64 !== void 0;
81037
+ if (usingFrom && usingB64) {
81038
+ throw new CliError("cannot mix --content-from with --content-b64");
81039
+ }
81040
+ if (!usingFrom && !usingB64) {
81041
+ throw new CliError("must provide either --content-from or --content-b64");
81042
+ }
81043
+ const contentBytes = usingFrom ? readFileBoundedRaw(opts.contentFrom, "--content-from") : decodeBase64Buffer(opts.contentB64, "--content-b64");
81044
+ if (contentBytes.length === 0) {
81045
+ throw new CliError("content to insert cannot be empty");
81046
+ }
81047
+ let preWriteStat;
81048
+ try {
81049
+ preWriteStat = fs52.statSync(file2);
81050
+ } catch {
81051
+ }
81052
+ const result = readSection(file2, opts.after);
81053
+ if (result === null) {
81054
+ const available = listSections(file2);
81055
+ const messages = [`Section '${opts.after}' not found in '${file2}'`];
81056
+ if (available.length > 0) messages.push(didYouMean(available));
81057
+ throw new CliError(messages.join("\n"));
81058
+ }
81059
+ let rawText;
81060
+ try {
81061
+ rawText = fs52.readFileSync(file2, "utf-8");
81062
+ } catch (e) {
81063
+ mapFsError(e, void 0, file2);
81064
+ }
81065
+ if (rawText.charCodeAt(0) === 65279) rawText = rawText.slice(1);
81066
+ const eol = detectDominantEol(Buffer.from(rawText, "utf8"));
81067
+ const lfLines = rawText.replace(/\r\n/g, "\n").split("\n");
81068
+ const insertAt = result.lineEnd;
81069
+ const insertedLines = contentBytes.toString("utf8").replace(/\r\n/g, "\n").split("\n");
81070
+ if (insertedLines.length > 0 && insertedLines[insertedLines.length - 1] === "") insertedLines.pop();
81071
+ const mergedLfText = [...lfLines.slice(0, insertAt), ...insertedLines, ...lfLines.slice(insertAt)].join("\n");
81072
+ const mergedText = eol === "\n" ? mergedLfText : mergedLfText.replace(/\n/g, "\r\n");
81073
+ if (preWriteStat !== void 0) {
81074
+ let preRenameStat;
81075
+ try {
81076
+ preRenameStat = fs52.statSync(file2);
81077
+ } catch {
81078
+ }
81079
+ if (preRenameStat !== void 0 && (preRenameStat.mtimeMs !== preWriteStat.mtimeMs || preRenameStat.size !== preWriteStat.size)) {
81080
+ throw new CliError(`${file2} changed on disk while insert-section was running -- the file was modified concurrently, so the insert was NOT applied. Retry.`);
81081
+ }
81082
+ }
81083
+ try {
81084
+ atomicWriteBuffer(file2, Buffer.from(mergedText, "utf8"));
81085
+ } catch (e) {
81086
+ mapFsError(e, void 0, file2);
81087
+ }
81088
+ enqueueDirtyPathSafe(file2);
81089
+ const redirectNote = result.redirectedFrom !== void 0 ? ` (redirected from: '${result.redirectedFrom}')` : "";
81090
+ out(`inserted after '${result.heading}'${redirectNote} in ${file2}`);
81091
+ }
80572
81092
  async function cmdGdriveSections(fileId, opts) {
80573
81093
  const fetchOpts = { fresh: opts.fresh === true };
80574
81094
  const text = await fetchDoc(fileId, fetchOpts);
@@ -80783,7 +81303,9 @@ function buildProgram() {
80783
81303
  })
80784
81304
  )
80785
81305
  );
80786
- 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(
81306
+ program2.command("section <spec>").description(
81307
+ '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'
81308
+ ).option("-j, --json", "output as JSON").option("--list", "list all section headings in the file instead of reading one").action(
80787
81309
  (spec, opts) => opts.list === true ? runExit(() => runListSections({ file: spec, ...opts.json === true ? { json: true } : {} })) : runExitText(() => runSection({ spec, ...opts.json === true ? { json: true } : {} }))
80788
81310
  );
80789
81311
  program2.command("semantic <query>").description("semantic search (falls back to full-text search)").option("-l, --limit <n>", "max results").action(guard(cmdSemantic));
@@ -81144,7 +81666,33 @@ function buildProgram() {
81144
81666
  ).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));
81145
81667
  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));
81146
81668
  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));
81147
- 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));
81669
+ 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(
81670
+ "--normalize-newlines",
81671
+ "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"
81672
+ ).action(guard(cmdReplace));
81673
+ program2.command("insert-section <file>").description(
81674
+ "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"
81675
+ ).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));
81676
+ program2.command("note-add <file>").description(
81677
+ "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"
81678
+ ).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));
81679
+ 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(
81680
+ (file2, opts) => runExitText(
81681
+ () => runNoteGet({
81682
+ file: file2,
81683
+ ...opts.symbol !== void 0 ? { symbol: opts.symbol } : {},
81684
+ ...opts.json === true ? { json: true } : {}
81685
+ })
81686
+ )
81687
+ );
81688
+ 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(
81689
+ (opts) => runExitText(
81690
+ () => runNoteList({
81691
+ ...opts.staleOnly === true ? { staleOnly: true } : {},
81692
+ ...opts.json === true ? { json: true } : {}
81693
+ })
81694
+ )
81695
+ );
81148
81696
  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));
81149
81697
  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);
81150
81698
  program2.command("version").description("print the token-goat version").action(