codebase-snapshot 0.9.9 → 0.9.14

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.
@@ -1,6 +1,6 @@
1
- import { GENERATOR_VERSION, MIN_MAX_PART_BYTES, SCHEMA_VERSION, GENERATOR_NAME, ARTIFACT_NAMES, PRESETS, generateSnapshot, validateSnapshot, explainPath, renderExplainText, describeManifestShapeProblem, loadConfigFileDetailed, parseByteSize, resolveConfig } from './chunk-KUVFTJHK.js';
1
+ import { GENERATOR_VERSION, anchorForPath, displayPath, MIN_MAX_PART_BYTES, SCHEMA_VERSION, GENERATOR_NAME, ARTIFACT_NAMES, PRESETS, generateSnapshot, generateInWorker, validateSnapshot, buildImportResolver, resolveSpecifier, truncateToBytes, explainPath, renderExplainText, describeManifestShapeProblem, loadConfigFileDetailed, parseByteSize, resolveConfig } from './chunk-DUZK5VL2.js';
2
2
  import * as fs from 'fs';
3
- import * as path2 from 'path';
3
+ import * as path from 'path';
4
4
 
5
5
  // src/mcp/protocol.ts
6
6
  var LEGACY_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18"];
@@ -520,6 +520,8 @@ function createMcpServer(options) {
520
520
  // src/mcp/part.ts
521
521
  var FILE_HEADING = "### FILE: ";
522
522
  var CONTENT_MARKERS = /* @__PURE__ */ new Set(["#### Content", "#### Bounded Preview"]);
523
+ var ANCHOR_LINE = /^<a id="([^"]+)"><\/a>$/;
524
+ var INCLUSION_MODES = /* @__PURE__ */ new Set(["full", "preview", "signatures"]);
523
525
  function parsePartSections(partText) {
524
526
  const lines = partText.split("\n");
525
527
  const sections = [];
@@ -530,16 +532,26 @@ function parsePartSections(partText) {
530
532
  continue;
531
533
  }
532
534
  const heading = lines[index].slice(FILE_HEADING.length);
535
+ const anchorMatch = index > 0 ? ANCHOR_LINE.exec(lines[index - 1]) : null;
533
536
  let cursor = index + 1;
534
537
  let inclusion = null;
535
- let sha256 = null;
536
538
  let includedLines = null;
537
539
  while (cursor < lines.length && !CONTENT_MARKERS.has(lines[cursor])) {
538
540
  const line = lines[cursor];
539
541
  if (line.startsWith(FILE_HEADING)) break;
540
- if (line.startsWith("- Inclusion: ")) inclusion = line.slice("- Inclusion: ".length).trim();
541
- else if (line.startsWith("- SHA-256: ")) sha256 = line.slice("- SHA-256: ".length).trim();
542
- else if (line.startsWith("- Included Lines: ")) {
542
+ if (line.startsWith("- Meta: ")) {
543
+ for (const field of line.slice("- Meta: ".length).split("|")) {
544
+ const value = field.trim();
545
+ if (INCLUSION_MODES.has(value)) inclusion = value;
546
+ }
547
+ const counts = /(-?\d+) of (-?\d+) lines|(\d+) lines/.exec(line);
548
+ if (counts) {
549
+ const emitted = counts[1] ?? counts[3];
550
+ includedLines = Number.isFinite(Number(emitted)) ? Number(emitted) : null;
551
+ }
552
+ } else if (line.startsWith("- Inclusion: ")) {
553
+ inclusion = line.slice("- Inclusion: ".length).trim();
554
+ } else if (line.startsWith("- Included Lines: ")) {
543
555
  const value = Number(line.slice("- Included Lines: ".length).trim());
544
556
  includedLines = Number.isFinite(value) ? value : null;
545
557
  }
@@ -563,47 +575,49 @@ function parsePartSections(partText) {
563
575
  body.push(lines[bodyLine]);
564
576
  bodyLine += 1;
565
577
  }
566
- sections.push({ path: heading, body: body.join("\n"), inclusion, sha256, includedLines });
578
+ sections.push({ path: heading, anchor: anchorMatch?.[1] ?? null, body: body.join("\n"), inclusion, includedLines });
567
579
  index = bodyLine + 1;
568
580
  }
569
581
  return sections;
570
582
  }
571
- function findSection(sections, relativePath, sha256) {
572
- const exact = sections.find((section) => section.path === relativePath);
583
+ function findSection(sections, relativePath) {
584
+ const anchor = anchorForPath(relativePath);
585
+ const byAnchor = sections.find((section) => section.anchor === anchor);
586
+ if (byAnchor) return byAnchor;
587
+ const rendered = displayPath(relativePath);
588
+ const samePath = (section) => section.path === relativePath || section.path === rendered;
589
+ const exact = sections.find(samePath);
573
590
  if (exact) return exact;
574
- if (sha256) {
575
- const byHash = sections.filter((section) => section.sha256 === sha256);
576
- if (byHash.length === 1) return byHash[0];
577
- if (byHash.length > 1) {
578
- const byPath = byHash.find(
579
- (section) => section.path.toLowerCase() === relativePath.toLowerCase()
580
- );
581
- if (byPath) return byPath;
582
- }
583
- }
584
- const folded = sections.filter((section) => section.path.toLowerCase() === relativePath.toLowerCase());
591
+ const foldedPath = rendered.toLowerCase();
592
+ const folded = sections.filter((section) => {
593
+ const path4 = section.path.toLowerCase();
594
+ return path4 === relativePath.toLowerCase() || path4 === foldedPath;
595
+ });
585
596
  return folded.length === 1 ? folded[0] : null;
586
597
  }
587
598
  function windowText(body, fromLine, lineCount) {
588
599
  const lines = body.split("\n");
589
- const total = lines.length;
590
- const start = Math.min(Math.max(1, Math.trunc(fromLine)), Math.max(1, total));
600
+ const total = body.length === 0 ? 0 : lines.length;
601
+ const requested = Math.max(1, Math.trunc(fromLine));
602
+ const pastEnd = total > 0 && requested > total;
603
+ const start = pastEnd ? requested : Math.min(requested, Math.max(1, total));
591
604
  const end = Math.min(total, start + Math.max(1, Math.trunc(lineCount)) - 1);
592
- const slice = lines.slice(start - 1, end);
605
+ const slice = pastEnd ? [] : lines.slice(start - 1, Math.max(start - 1, end));
593
606
  return {
594
607
  text: slice.join("\n"),
595
608
  fromLine: start,
596
609
  returnedLines: slice.length,
597
610
  totalLines: total,
598
- truncated: end < total,
599
- nextFromLine: end < total ? end + 1 : null
611
+ truncated: !pastEnd && end < total,
612
+ nextFromLine: !pastEnd && end < total ? end + 1 : null,
613
+ pastEnd
600
614
  };
601
615
  }
602
616
  function foldForComparison(value) {
603
617
  return process.platform === "win32" || process.platform === "darwin" ? value.toLowerCase() : value;
604
618
  }
605
619
  function canonicalPath(value) {
606
- const absolute = path2.resolve(value);
620
+ const absolute = path.resolve(value);
607
621
  try {
608
622
  return fs.realpathSync(absolute);
609
623
  } catch {
@@ -613,14 +627,14 @@ function canonicalPath(value) {
613
627
  function isInside(parent, child) {
614
628
  const foldedParent = foldForComparison(parent);
615
629
  const foldedChild = foldForComparison(child);
616
- return foldedChild === foldedParent || foldedChild.startsWith(`${foldedParent}${path2.sep}`);
630
+ return foldedChild === foldedParent || foldedChild.startsWith(`${foldedParent}${path.sep}`);
617
631
  }
618
632
  function resolveRoot(requested, context) {
619
633
  if (requested !== void 0 && (typeof requested !== "string" || requested.trim().length === 0)) {
620
634
  return { ok: false, message: '"root" must be a non-empty string when provided.' };
621
635
  }
622
636
  const wanted = typeof requested === "string" ? requested.trim() : context.cwd;
623
- const absolute = path2.resolve(context.cwd, wanted);
637
+ const absolute = path.resolve(context.cwd, wanted);
624
638
  let stats;
625
639
  try {
626
640
  stats = fs.statSync(absolute);
@@ -659,26 +673,59 @@ async function openWorkspace(root, overrides = {}) {
659
673
  }
660
674
  cliOptions.maxPartBytes = parsed;
661
675
  }
676
+ if (overrides.maxPartTokens !== void 0) {
677
+ const parsed = parseByteSize(overrides.maxPartTokens);
678
+ if (parsed === void 0) {
679
+ return {
680
+ ok: false,
681
+ message: '"maxPartTokens" must be a token count, for example 32000 or "32k".'
682
+ };
683
+ }
684
+ cliOptions.maxPartTokens = parsed;
685
+ }
662
686
  if (overrides.outDir !== void 0) {
663
687
  if (typeof overrides.outDir !== "string" || overrides.outDir.trim().length === 0) {
664
688
  return { ok: false, message: '"outDir" must be a non-empty string.' };
665
689
  }
666
690
  cliOptions.outDir = overrides.outDir.trim();
667
691
  }
692
+ if (overrides.changedSince !== void 0) {
693
+ if (typeof overrides.changedSince !== "string" || overrides.changedSince.trim().length === 0) {
694
+ return {
695
+ ok: false,
696
+ message: '"changedSince" must be a git ref such as "main" or "HEAD~3".'
697
+ };
698
+ }
699
+ cliOptions.changedSince = overrides.changedSince.trim();
700
+ }
701
+ if (overrides.skeleton === true) cliOptions.skeleton = true;
702
+ if (overrides.sarif === true) cliOptions.sarif = true;
703
+ if (overrides.staged === true) cliOptions.staged = true;
704
+ if (overrides.withDependents === true) cliOptions.withDependents = true;
668
705
  try {
669
706
  const config = resolveConfig(loaded.config, cliOptions, root);
670
- return { ok: true, workspace: { root, config, configFile: loaded.file } };
707
+ return { ok: true, workspace: { root, config, configFile: loaded.file, cliOptions } };
671
708
  } catch (error) {
672
709
  return { ok: false, message: error instanceof Error ? error.message : String(error) };
673
710
  }
674
711
  }
675
712
  var manifestCache = /* @__PURE__ */ new Map();
676
713
  function manifestPathFor(workspace) {
677
- return path2.resolve(workspace.root, workspace.config.outDir, "manifest.json");
714
+ return path.resolve(workspace.root, workspace.config.outDir, "manifest.json");
715
+ }
716
+ function partPathFor(workspace, filename) {
717
+ if (filename.length === 0 || filename.includes("\0")) return null;
718
+ const partsDir = path.resolve(workspace.root, workspace.config.outDir, "parts");
719
+ const resolved = path.resolve(partsDir, filename);
720
+ const relative3 = path.relative(partsDir, resolved);
721
+ if (relative3.length === 0 || relative3.includes(path.sep) || relative3.includes("/") || relative3.includes("\\") || path.isAbsolute(relative3)) {
722
+ return null;
723
+ }
724
+ return resolved;
678
725
  }
679
726
  function readManifest(workspace) {
680
727
  const file = manifestPathFor(workspace);
681
- const shown = path2.relative(workspace.root, file).split(path2.sep).join("/") || file;
728
+ const shown = path.relative(workspace.root, file).split(path.sep).join("/") || file;
682
729
  let stats;
683
730
  try {
684
731
  stats = fs.statSync(file);
@@ -741,6 +788,49 @@ var ROOT_PROPERTY = {
741
788
  type: "string",
742
789
  description: "Repository root. Defaults to the directory this server was started in."
743
790
  };
791
+ var OUT_DIR_PROPERTY = {
792
+ type: "string",
793
+ description: "Snapshot directory, relative to the root. Defaults to the project configuration's outDir."
794
+ };
795
+ var outDirOverride = (args) => ({ outDir: args.outDir });
796
+ var SCOPE_PROPERTIES = {
797
+ maxPartTokens: {
798
+ type: "string",
799
+ description: 'Token ceiling per part, for example 32000 or "32k". Narrower than maxPartBytes; it converts at four bytes per token.'
800
+ },
801
+ skeleton: {
802
+ type: "boolean",
803
+ description: "Replace every function body with `{ /* ... */ }`, keeping signatures, imports and exports. Applies to TypeScript, JavaScript and JSX."
804
+ },
805
+ sarif: {
806
+ type: "boolean",
807
+ description: "Also write reports/audit.sarif beside reports/audit.md, for CI code scanning."
808
+ },
809
+ changedSince: {
810
+ type: "string",
811
+ description: 'Only files changed since this git ref, for example "main" or "HEAD~3".'
812
+ },
813
+ staged: {
814
+ type: "boolean",
815
+ description: "Only files staged in the git index."
816
+ },
817
+ withDependents: {
818
+ type: "boolean",
819
+ description: "With staged or changedSince, also include the files that import the changed ones (one hop)."
820
+ }
821
+ };
822
+ function workspaceOverrides(args) {
823
+ return {
824
+ outDir: args.outDir,
825
+ maxPartBytes: args.maxPartBytes,
826
+ maxPartTokens: args.maxPartTokens,
827
+ skeleton: args.skeleton,
828
+ sarif: args.sarif,
829
+ changedSince: args.changedSince,
830
+ staged: args.staged,
831
+ withDependents: args.withDependents
832
+ };
833
+ }
744
834
  function failure(message) {
745
835
  return { text: message, isError: true };
746
836
  }
@@ -753,7 +843,7 @@ function formatBytes(bytes) {
753
843
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
754
844
  }
755
845
  function relativeFor(workspace, absolute) {
756
- return path2.relative(workspace.root, absolute).split(path2.sep).join("/");
846
+ return path.relative(workspace.root, absolute).split(path.sep).join("/");
757
847
  }
758
848
  function capped(values, limit = 20) {
759
849
  return { count: values.length, sample: values.slice(0, limit) };
@@ -771,19 +861,50 @@ function summarizeDrift(drift) {
771
861
  }
772
862
  function toRelativePath(root, value) {
773
863
  const cleaned = value.trim();
774
- if (cleaned.length === 0) return null;
775
- if (!path2.isAbsolute(cleaned)) {
776
- return cleaned.replace(/\\/g, "/").replace(/^\.\//, "");
864
+ if (cleaned.length === 0 || cleaned.includes("\0")) return null;
865
+ const resolvedRoot = path.resolve(root);
866
+ const absolute = path.resolve(resolvedRoot, cleaned);
867
+ const relative3 = path.relative(resolvedRoot, absolute);
868
+ if (relative3 === "" || relative3 === ".." || relative3.startsWith(`..${path.sep}`) || path.isAbsolute(relative3)) {
869
+ return null;
777
870
  }
778
- const relative3 = path2.relative(root, cleaned);
779
- if (relative3.startsWith("..") || path2.isAbsolute(relative3)) return null;
780
- return relative3.split(path2.sep).join("/");
871
+ return relative3.split(path.sep).join("/");
781
872
  }
782
873
  function symbolsOf(entry) {
783
874
  const summary = entry.summary;
784
- if (!summary || typeof summary !== "object" || !("exports" in summary)) return [];
785
- const source = summary;
786
- return [...source.exports, ...source.reactComponents, ...source.routeHandlers];
875
+ if (!summary || typeof summary !== "object") return [];
876
+ const names = [];
877
+ const collect = (value) => {
878
+ if (!Array.isArray(value)) return;
879
+ for (const item of value) {
880
+ if (typeof item === "string") names.push(item);
881
+ }
882
+ };
883
+ const record = summary;
884
+ for (const field of [
885
+ "exports",
886
+ "reactComponents",
887
+ "routeHandlers",
888
+ "functions",
889
+ "classes",
890
+ "interfaces",
891
+ "types",
892
+ "enums",
893
+ "hooks",
894
+ "headings",
895
+ // The rest of the same records. `variables` is where a `const`, a Python constant, a Go
896
+ // `var`, a Rust `mod`, a C `#define` and a Terraform `variable` block all land, so leaving it
897
+ // out made `snapshot_locate` miss most of what a non-JavaScript file declares; `directives`
898
+ // carries `"use client"`-style markers, and `frontmatterKeys`/`keys` are the headings and
899
+ // top-level names of a Markdown or JSON file.
900
+ "variables",
901
+ "directives",
902
+ "frontmatterKeys",
903
+ "keys"
904
+ ]) {
905
+ collect(record[field]);
906
+ }
907
+ return names;
787
908
  }
788
909
  async function prepare(args, context, overrides = {}) {
789
910
  const rootOutcome = resolveRoot(args.root, context);
@@ -798,7 +919,7 @@ var statusTool = {
798
919
  description: "Report whether a snapshot exists for this repository, when it was built, and what it covers. Reads the manifest rather than the repository, so it is cheap. Call it before deciding whether a snapshot needs to be generated.",
799
920
  inputSchema: {
800
921
  type: "object",
801
- properties: { root: ROOT_PROPERTY },
922
+ properties: { root: ROOT_PROPERTY, outDir: OUT_DIR_PROPERTY },
802
923
  additionalProperties: false
803
924
  },
804
925
  outputSchema: {
@@ -819,7 +940,7 @@ var statusTool = {
819
940
  },
820
941
  annotations: READ_ONLY,
821
942
  async handler(args, context) {
822
- const prepared = await prepare(args, context);
943
+ const prepared = await prepare(args, context, outDirOverride(args));
823
944
  if (!prepared.ok) return prepared.outcome;
824
945
  const workspace = prepared.workspace;
825
946
  const read = readManifest(workspace);
@@ -878,10 +999,12 @@ var planTool = {
878
999
  type: "object",
879
1000
  properties: {
880
1001
  root: ROOT_PROPERTY,
1002
+ outDir: OUT_DIR_PROPERTY,
881
1003
  maxPartBytes: {
882
1004
  type: "string",
883
1005
  description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
884
- }
1006
+ },
1007
+ ...SCOPE_PROPERTIES
885
1008
  },
886
1009
  additionalProperties: false
887
1010
  },
@@ -898,7 +1021,7 @@ var planTool = {
898
1021
  },
899
1022
  annotations: READ_ONLY,
900
1023
  async handler(args, context) {
901
- const prepared = await prepare(args, context, { maxPartBytes: args.maxPartBytes });
1024
+ const prepared = await prepare(args, context, workspaceOverrides(args));
902
1025
  if (!prepared.ok) return prepared.outcome;
903
1026
  const workspace = prepared.workspace;
904
1027
  let manifest;
@@ -952,14 +1075,12 @@ var generateTool = {
952
1075
  type: "object",
953
1076
  properties: {
954
1077
  root: ROOT_PROPERTY,
1078
+ outDir: OUT_DIR_PROPERTY,
955
1079
  maxPartBytes: {
956
1080
  type: "string",
957
1081
  description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
958
1082
  },
959
- outDir: {
960
- type: "string",
961
- description: "Output directory relative to the root. Defaults to the project configuration."
962
- }
1083
+ ...SCOPE_PROPERTIES
963
1084
  },
964
1085
  additionalProperties: false
965
1086
  },
@@ -975,41 +1096,35 @@ var generateTool = {
975
1096
  },
976
1097
  annotations: WRITES,
977
1098
  async handler(args, context) {
978
- const prepared = await prepare(args, context, {
979
- maxPartBytes: args.maxPartBytes,
980
- outDir: args.outDir
981
- });
1099
+ const prepared = await prepare(args, context, workspaceOverrides(args));
982
1100
  if (!prepared.ok) return prepared.outcome;
983
1101
  const workspace = prepared.workspace;
984
- let manifest;
985
- try {
986
- manifest = generateSnapshot(workspace.config);
987
- } catch (error) {
988
- return failure(error instanceof Error ? error.message : String(error));
989
- }
1102
+ const result = await generateInWorker(workspace.root, workspace.cliOptions);
1103
+ if (!result.ok) return failure(result.message);
1104
+ const summary = result.summary;
990
1105
  forgetManifest(manifestPathFor(workspace));
991
- const written = relativeFor(workspace, path2.resolve(workspace.root, workspace.config.outDir));
1106
+ const written = relativeFor(workspace, path.resolve(workspace.root, workspace.config.outDir));
992
1107
  const lines = [
993
1108
  `Wrote ${written}/`,
994
- `Files: ${manifest.counts.totalFiles} included, ${manifest.counts.skippedCount} skipped, ${plural(manifest.counts.partsCount, "part")}`,
995
- `Parts: ${manifest.parts.map((part) => `${part.filename} (${formatBytes(part.byteSize)})`).join(", ")}`,
996
- `Fingerprint: ${manifest.fingerprint}`
1109
+ `Files: ${summary.totalFiles} included, ${summary.skippedCount} skipped, ${plural(summary.partsCount, "part")}`,
1110
+ `Parts: ${summary.parts.map((part) => `${part.filename} (${formatBytes(part.byteSize)})`).join(", ")}`,
1111
+ `Fingerprint: ${summary.fingerprint}`
997
1112
  ];
998
- if (manifest.warnings.length > 0) {
999
- lines.push(`Warnings (${manifest.warnings.length}):`);
1000
- for (const warning of manifest.warnings.slice(0, 5)) lines.push(` - ${warning}`);
1113
+ if (summary.warnings.length > 0) {
1114
+ lines.push(`Warnings (${summary.warnings.length}):`);
1115
+ for (const warning of summary.warnings.slice(0, 5)) lines.push(` - ${warning}`);
1001
1116
  }
1002
1117
  return {
1003
1118
  text: lines.join("\n"),
1004
1119
  structured: {
1005
- fingerprint: manifest.fingerprint,
1006
- files: manifest.counts.totalFiles,
1007
- skipped: manifest.counts.skippedCount,
1008
- parts: manifest.counts.partsCount,
1009
- emittedBytes: manifest.totals.totalContentBytes,
1120
+ fingerprint: summary.fingerprint,
1121
+ files: summary.totalFiles,
1122
+ skipped: summary.skippedCount,
1123
+ parts: summary.partsCount,
1124
+ emittedBytes: summary.totalContentBytes,
1010
1125
  outDir: written,
1011
- partFiles: manifest.parts.map((part) => part.filename),
1012
- warningCount: manifest.warnings.length
1126
+ partFiles: summary.parts.map((part) => part.filename),
1127
+ warningCount: summary.warnings.length
1013
1128
  }
1014
1129
  };
1015
1130
  }
@@ -1020,7 +1135,7 @@ var validateTool = {
1020
1135
  description: "Re-read the repository and report whether the snapshot on disk still matches it. Use it before trusting context that came from an earlier session, or after changing the config.",
1021
1136
  inputSchema: {
1022
1137
  type: "object",
1023
- properties: { root: ROOT_PROPERTY },
1138
+ properties: { root: ROOT_PROPERTY, outDir: OUT_DIR_PROPERTY, ...SCOPE_PROPERTIES },
1024
1139
  additionalProperties: false
1025
1140
  },
1026
1141
  outputSchema: {
@@ -1032,7 +1147,7 @@ var validateTool = {
1032
1147
  },
1033
1148
  annotations: READ_ONLY,
1034
1149
  async handler(args, context) {
1035
- const prepared = await prepare(args, context);
1150
+ const prepared = await prepare(args, context, workspaceOverrides(args));
1036
1151
  if (!prepared.ok) return prepared.outcome;
1037
1152
  const workspace = prepared.workspace;
1038
1153
  let result;
@@ -1099,7 +1214,8 @@ var locateTool = {
1099
1214
  default: 20,
1100
1215
  description: "Maximum matches to return."
1101
1216
  },
1102
- root: ROOT_PROPERTY
1217
+ root: ROOT_PROPERTY,
1218
+ outDir: OUT_DIR_PROPERTY
1103
1219
  },
1104
1220
  required: ["query"],
1105
1221
  additionalProperties: false
@@ -1110,7 +1226,7 @@ var locateTool = {
1110
1226
  },
1111
1227
  annotations: READ_ONLY,
1112
1228
  async handler(args, context) {
1113
- const prepared = await prepare(args, context);
1229
+ const prepared = await prepare(args, context, outDirOverride(args));
1114
1230
  if (!prepared.ok) return prepared.outcome;
1115
1231
  const workspace = prepared.workspace;
1116
1232
  const read = readManifest(workspace);
@@ -1145,12 +1261,13 @@ var locateTool = {
1145
1261
  continue;
1146
1262
  }
1147
1263
  }
1148
- if (wants("layer") && entry.layer.toLowerCase() === needle) add(entry, "layer");
1149
- else if (wants("kind") && entry.kind.toLowerCase() === needle) add(entry, "kind");
1264
+ if (wants("layer") && entry.layer.toLowerCase().includes(needle)) add(entry, "layer");
1265
+ else if (wants("kind") && entry.kind.toLowerCase().includes(needle)) add(entry, "kind");
1150
1266
  }
1151
1267
  if (wants("route")) {
1152
1268
  for (const route of manifest.indexes.routes) {
1153
- if (!route.path.toLowerCase().includes(needle)) continue;
1269
+ const method = route.methods.find((name) => name.toLowerCase().includes(needle));
1270
+ if (!route.path.toLowerCase().includes(needle) && method === void 0) continue;
1154
1271
  if (matches.some((match) => match.path === route.path)) continue;
1155
1272
  matches.push({
1156
1273
  path: route.path,
@@ -1185,6 +1302,127 @@ var locateTool = {
1185
1302
  };
1186
1303
  }
1187
1304
  };
1305
+ var dependentsTool = {
1306
+ name: "snapshot_dependents",
1307
+ title: "Follow the import graph",
1308
+ description: 'For one file in the snapshot: what it imports, and which files import it. Answers "what breaks if I change this?" without reading the parts.',
1309
+ inputSchema: {
1310
+ type: "object",
1311
+ properties: {
1312
+ path: {
1313
+ type: "string",
1314
+ description: "Snapshot-relative path, exactly as snapshot_locate or manifest.files[] reports it."
1315
+ },
1316
+ direction: {
1317
+ type: "string",
1318
+ enum: ["both", "imports", "imported-by"],
1319
+ default: "both",
1320
+ description: "Which edges to return."
1321
+ },
1322
+ limit: {
1323
+ type: "integer",
1324
+ minimum: 1,
1325
+ maximum: 200,
1326
+ default: 50,
1327
+ description: "Maximum entries per direction."
1328
+ },
1329
+ root: ROOT_PROPERTY,
1330
+ outDir: OUT_DIR_PROPERTY
1331
+ },
1332
+ required: ["path"],
1333
+ additionalProperties: false
1334
+ },
1335
+ outputSchema: {
1336
+ type: "object",
1337
+ properties: {
1338
+ path: { type: "string" },
1339
+ imports: { type: "array" },
1340
+ importedBy: { type: "array" },
1341
+ unresolved: { type: "array" }
1342
+ }
1343
+ },
1344
+ annotations: READ_ONLY,
1345
+ async handler(args, context) {
1346
+ const prepared = await prepare(args, context, outDirOverride(args));
1347
+ if (!prepared.ok) return prepared.outcome;
1348
+ const read = readManifest(prepared.workspace);
1349
+ if (!read.ok) return failure(read.message);
1350
+ const manifest = read.manifest;
1351
+ const requested = String(args.path ?? "").trim().replace(/\\/g, "/");
1352
+ if (requested.length === 0) return failure('"path" must not be empty.');
1353
+ const filesByPath = new Map(manifest.files.map((file) => [file.relativePath, file]));
1354
+ const entry = filesByPath.get(requested) ?? manifest.files.find((file) => file.relativePath.toLowerCase() === requested.toLowerCase());
1355
+ if (!entry) {
1356
+ return failure(
1357
+ `${requested} is not in the snapshot. Use snapshot_locate to find its path, or snapshot_explain to see whether it was excluded.`
1358
+ );
1359
+ }
1360
+ const direction = args.direction === "imports" || args.direction === "imported-by" ? String(args.direction) : "both";
1361
+ const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 50;
1362
+ const resolver = buildImportResolver(manifest.files);
1363
+ const imports = [];
1364
+ const unresolved = [];
1365
+ if (direction !== "imported-by") {
1366
+ const own = manifest.indexes.imports.byFile.find((file) => file.path === entry.relativePath);
1367
+ for (const specifier of own?.imports ?? []) {
1368
+ const resolved = resolveSpecifier(entry.relativePath, specifier, resolver);
1369
+ if (resolved === null) {
1370
+ unresolved.push(specifier);
1371
+ continue;
1372
+ }
1373
+ const target = filesByPath.get(resolved);
1374
+ if (target) imports.push({ path: resolved, specifier, part: target.part });
1375
+ }
1376
+ }
1377
+ const importedBy = [];
1378
+ if (direction !== "imports") {
1379
+ for (const candidate of manifest.indexes.imports.byFile) {
1380
+ if (candidate.path === entry.relativePath) continue;
1381
+ const edge = candidate.imports.some(
1382
+ (specifier) => resolveSpecifier(candidate.path, specifier, resolver) === entry.relativePath
1383
+ );
1384
+ if (!edge) continue;
1385
+ const source = filesByPath.get(candidate.path);
1386
+ importedBy.push({ path: candidate.path, part: source?.part ?? "" });
1387
+ }
1388
+ }
1389
+ const shownImports = imports.slice(0, limit);
1390
+ const shownImportedBy = importedBy.slice(0, limit);
1391
+ const lines = [
1392
+ `${entry.relativePath} \u2014 ${importedBy.length} importer(s), ${imports.length} import(s), ${unresolved.length} outside the snapshot.`
1393
+ ];
1394
+ if (direction !== "imports") {
1395
+ lines.push("", "Imported by:");
1396
+ if (shownImportedBy.length === 0) lines.push(" (nothing in the snapshot imports it)");
1397
+ for (const item of shownImportedBy) lines.push(` ${item.path} [${item.part}]`);
1398
+ if (importedBy.length > shownImportedBy.length) {
1399
+ lines.push(` \u2026 ${importedBy.length - shownImportedBy.length} more; raise \`limit\`.`);
1400
+ }
1401
+ }
1402
+ if (direction !== "imported-by") {
1403
+ lines.push("", "Imports:");
1404
+ if (shownImports.length === 0) lines.push(" (none recorded for this file)");
1405
+ for (const item of shownImports) {
1406
+ lines.push(` ${item.path} (from "${item.specifier}") [${item.part}]`);
1407
+ }
1408
+ if (imports.length > shownImports.length) {
1409
+ lines.push(` \u2026 ${imports.length - shownImports.length} more; raise \`limit\`.`);
1410
+ }
1411
+ if (unresolved.length > 0) {
1412
+ lines.push("", `Outside the snapshot (${unresolved.length}): ${unresolved.join(", ")}`);
1413
+ }
1414
+ }
1415
+ return {
1416
+ text: lines.join("\n"),
1417
+ structured: {
1418
+ path: entry.relativePath,
1419
+ imports: shownImports,
1420
+ importedBy: shownImportedBy,
1421
+ unresolved
1422
+ }
1423
+ };
1424
+ }
1425
+ };
1188
1426
  var readTool = {
1189
1427
  name: "snapshot_read",
1190
1428
  title: "Read a file from the snapshot",
@@ -1209,7 +1447,8 @@ var readTool = {
1209
1447
  default: 200,
1210
1448
  description: "How many lines to return."
1211
1449
  },
1212
- root: ROOT_PROPERTY
1450
+ root: ROOT_PROPERTY,
1451
+ outDir: OUT_DIR_PROPERTY
1213
1452
  },
1214
1453
  required: ["path"],
1215
1454
  additionalProperties: false
@@ -1227,7 +1466,7 @@ var readTool = {
1227
1466
  },
1228
1467
  annotations: READ_ONLY,
1229
1468
  async handler(args, context) {
1230
- const prepared = await prepare(args, context);
1469
+ const prepared = await prepare(args, context, outDirOverride(args));
1231
1470
  if (!prepared.ok) return prepared.outcome;
1232
1471
  const workspace = prepared.workspace;
1233
1472
  if (typeof args.path !== "string") return failure('"path" must be a string.');
@@ -1253,7 +1492,12 @@ var readTool = {
1253
1492
  `${relativePath} is not in the snapshot. Call snapshot_locate to find the path, or snapshot_explain to learn why it was excluded.`
1254
1493
  );
1255
1494
  }
1256
- const partPath = path2.resolve(workspace.root, workspace.config.outDir, "parts", entry.part);
1495
+ const partPath = partPathFor(workspace, entry.part);
1496
+ if (partPath === null) {
1497
+ return failure(
1498
+ `${entry.part} is not a part of this snapshot: its name leaves ${workspace.config.outDir}/parts. Run snapshot_generate to rebuild it.`
1499
+ );
1500
+ }
1257
1501
  let partText;
1258
1502
  try {
1259
1503
  partText = fs.readFileSync(partPath, "utf8");
@@ -1262,7 +1506,7 @@ var readTool = {
1262
1506
  `${entry.part} could not be read, so the snapshot is damaged. Run snapshot_generate to rebuild it.`
1263
1507
  );
1264
1508
  }
1265
- const section = findSection(parsePartSections(partText), entry.relativePath, entry.hashSha256);
1509
+ const section = findSection(parsePartSections(partText), entry.relativePath);
1266
1510
  if (!section) {
1267
1511
  return failure(
1268
1512
  `${relativePath} is recorded in ${entry.part} but its section could not be located there. Run snapshot_generate to rebuild the snapshot.`
@@ -1274,16 +1518,28 @@ var readTool = {
1274
1518
  let body = window.text;
1275
1519
  let truncatedByBytes = false;
1276
1520
  if (Buffer.byteLength(body, "utf8") > MAX_RESULT_BYTES) {
1277
- body = body.slice(0, MAX_RESULT_BYTES);
1521
+ const clipped = truncateToBytes(body, MAX_RESULT_BYTES);
1522
+ body = clipped.content;
1278
1523
  const lastBreak = body.lastIndexOf("\n");
1279
1524
  body = lastBreak > 0 ? body.slice(0, lastBreak) : body;
1280
1525
  truncatedByBytes = true;
1281
1526
  }
1282
1527
  const returnedLines = body.length === 0 ? 0 : body.split("\n").length;
1283
- const moreRemain = window.truncated || truncatedByBytes;
1528
+ const moreRemain = !window.pastEnd && (window.truncated || truncatedByBytes);
1284
1529
  const header = [
1285
1530
  `${entry.relativePath} \u2014 ${entry.part}, ${entry.layer}/${entry.kind}, ${entry.language}`,
1286
- section.inclusion === "preview" ? "Bounded preview: this source file is large, so the snapshot holds only its head and tail." : `Lines ${window.fromLine}\u2013${window.fromLine + returnedLines - 1} of ${window.totalLines}.`
1531
+ // The line range is the answer to "where am I in this file", and it is computed from the
1532
+ // body the window was taken from, so it is just as meaningful for a preview as for a full
1533
+ // file. It used to be replaced — not augmented — by the preview note, so a 40-line window
1534
+ // into a 300-line preview came back with no range at all and the caller could not tell
1535
+ // where the window ended. Both facts are stated now, and `signatures` is no longer
1536
+ // silently treated as a full file.
1537
+ ...section.inclusion === "preview" ? [
1538
+ "Bounded preview: the snapshot holds this file's head and tail only, so these line numbers count the preview, not the source."
1539
+ ] : section.inclusion === "signatures" ? [
1540
+ "Signatures: function and method bodies were replaced with `{ /* ... */ }` when the snapshot was generated."
1541
+ ] : [],
1542
+ window.totalLines === 0 ? "This file is empty." : window.pastEnd ? `This file has ${window.totalLines} line(s); fromLine=${window.fromLine} is past its end.` : `Lines ${window.fromLine}\u2013${window.fromLine + returnedLines - 1} of ${window.totalLines}.`
1287
1543
  ];
1288
1544
  const lines = [...header, "", body];
1289
1545
  if (moreRemain) {
@@ -1320,7 +1576,9 @@ var explainTool = {
1320
1576
  type: "string",
1321
1577
  description: "Path relative to the repository root. Directories are explained as a whole."
1322
1578
  },
1323
- root: ROOT_PROPERTY
1579
+ root: ROOT_PROPERTY,
1580
+ outDir: OUT_DIR_PROPERTY,
1581
+ ...SCOPE_PROPERTIES
1324
1582
  },
1325
1583
  required: ["path"],
1326
1584
  additionalProperties: false
@@ -1335,7 +1593,7 @@ var explainTool = {
1335
1593
  },
1336
1594
  annotations: READ_ONLY,
1337
1595
  async handler(args, context) {
1338
- const prepared = await prepare(args, context);
1596
+ const prepared = await prepare(args, context, workspaceOverrides(args));
1339
1597
  if (!prepared.ok) return prepared.outcome;
1340
1598
  const workspace = prepared.workspace;
1341
1599
  if (typeof args.path !== "string") return failure('"path" must be a string.');
@@ -1375,6 +1633,7 @@ function buildTools() {
1375
1633
  generateTool,
1376
1634
  validateTool,
1377
1635
  locateTool,
1636
+ dependentsTool,
1378
1637
  readTool,
1379
1638
  explainTool
1380
1639
  ];
@@ -1446,9 +1705,15 @@ var RESOURCE_TEMPLATE_URIS = ["snapshot://parts/{index}"];
1446
1705
 
1447
1706
  // src/agent/option-scope.ts
1448
1707
  var VALIDATE_OPTIONS = [
1708
+ "sarif",
1709
+ "skeleton",
1710
+ "changed-since",
1711
+ "staged",
1712
+ "with-dependents",
1449
1713
  "cwd",
1450
1714
  "out-dir",
1451
1715
  "max-part-bytes",
1716
+ "max-part-tokens",
1452
1717
  "large-file-bytes",
1453
1718
  "max-monolith-bytes",
1454
1719
  "include-monolith",
@@ -1465,9 +1730,15 @@ var VALIDATE_OPTIONS = [
1465
1730
  ];
1466
1731
  var COMMAND_OPTION_SCOPE = {
1467
1732
  generate: [
1733
+ "sarif",
1734
+ "skeleton",
1735
+ "changed-since",
1736
+ "staged",
1737
+ "with-dependents",
1468
1738
  "cwd",
1469
1739
  "out-dir",
1470
1740
  "max-part-bytes",
1741
+ "max-part-tokens",
1471
1742
  "large-file-bytes",
1472
1743
  "max-monolith-bytes",
1473
1744
  "include-monolith",
@@ -1505,6 +1776,12 @@ var COMMAND_OPTION_SCOPE = {
1505
1776
  "version"
1506
1777
  ],
1507
1778
  explain: [
1779
+ // The scope decides what `explain` says about a file, dependents included: without these,
1780
+ // a file that is in the snapshot only because it imports a changed one was explained as
1781
+ // `included: false, reason: "unchanged"`.
1782
+ "changed-since",
1783
+ "staged",
1784
+ "with-dependents",
1508
1785
  "cwd",
1509
1786
  "out-dir",
1510
1787
  "max-part-bytes",
@@ -1525,6 +1802,7 @@ var COMMAND_OPTION_SCOPE = {
1525
1802
  diff: ["format", "mode", "no-config", "no-update-check", "help", "version"],
1526
1803
  describe: ["format", "mode", "help", "version"],
1527
1804
  init: [
1805
+ "install-hook",
1528
1806
  "cwd",
1529
1807
  "out-dir",
1530
1808
  "max-part-bytes",
@@ -1547,13 +1825,29 @@ var COMMAND_OPTION_SCOPE = {
1547
1825
  // `mcp` reads the repository, so it takes --cwd. It has no --format: stdout carries JSON-RPC
1548
1826
  // messages, and there is no second shape to choose, so offering the flag would be a lie.
1549
1827
  mcp: ["cwd", "mode", "help", "version"],
1828
+ // `setup` generates through the `generate` command, so every generation setting it can
1829
+ // honour is in scope. Rejecting them made the one-command path unable to express a walk
1830
+ // scan, a monolith or a different large-file threshold; the alternative was to run
1831
+ // `setup` and then edit the config it had just written.
1550
1832
  setup: [
1833
+ "install-hook",
1551
1834
  "cwd",
1552
1835
  "out-dir",
1553
1836
  "max-part-bytes",
1837
+ // Persisted into the config `setup` writes, then forwarded to the generation it runs — so
1838
+ // the snapshot it produces validates against the config beside it. Accepting a setting here
1839
+ // without writing it down made `setup --large-file-bytes 100k` finish with a snapshot its
1840
+ // own `validate` called drifted, on the first run of a brand-new project.
1841
+ "max-part-tokens",
1842
+ "skeleton",
1843
+ "sarif",
1844
+ "large-file-bytes",
1845
+ "max-monolith-bytes",
1846
+ "include-monolith",
1554
1847
  "include-sensitive-files",
1555
1848
  "skip-artifacts",
1556
1849
  "only-artifacts",
1850
+ "discovery",
1557
1851
  "preset",
1558
1852
  "include",
1559
1853
  "exclude",
@@ -1839,6 +2133,13 @@ function buildAgentContract() {
1839
2133
  appliesTo: optionAppliesTo("max-part-bytes"),
1840
2134
  summary: 'Byte budget per markdown part. Accepts "120k" / "1.5mb". Minimum 4096.'
1841
2135
  },
2136
+ {
2137
+ flag: "--max-part-tokens",
2138
+ type: "string",
2139
+ default: "unset",
2140
+ appliesTo: optionAppliesTo("max-part-tokens"),
2141
+ summary: "Ceiling on a part expressed in tokens. Narrows --max-part-bytes instead of replacing it; the manifest records both units. Minimum 1024."
2142
+ },
1842
2143
  {
1843
2144
  flag: "--large-file-bytes",
1844
2145
  type: "string",
@@ -1888,6 +2189,41 @@ function buildAgentContract() {
1888
2189
  appliesTo: optionAppliesTo("only-artifacts"),
1889
2190
  summary: "Write only these artifacts. Cannot be combined with --skip-artifacts."
1890
2191
  },
2192
+ {
2193
+ flag: "--skeleton",
2194
+ type: "boolean",
2195
+ default: false,
2196
+ appliesTo: optionAppliesTo("skeleton"),
2197
+ summary: 'Replace function and method bodies with `{ /* ... */ }` in TypeScript/TSX files, keeping signatures and comments. The part records inclusionMode "signatures" for each file it touched.'
2198
+ },
2199
+ {
2200
+ flag: "--sarif",
2201
+ type: "boolean",
2202
+ default: false,
2203
+ appliesTo: optionAppliesTo("sarif"),
2204
+ summary: "Also write reports/audit.sarif (SARIF 2.1.0) so a CI security gate can read the redaction findings. The matched value is never included."
2205
+ },
2206
+ {
2207
+ flag: "--changed-since",
2208
+ type: "string",
2209
+ default: "unset",
2210
+ appliesTo: optionAppliesTo("changed-since"),
2211
+ summary: 'Snapshot only the files that differ from this git ref, plus untracked ones. Everything else is recorded in skipped[] with reason "unchanged". Requires a git checkout.'
2212
+ },
2213
+ {
2214
+ flag: "--staged",
2215
+ type: "boolean",
2216
+ default: false,
2217
+ appliesTo: optionAppliesTo("staged"),
2218
+ summary: "Snapshot only the files staged in the index. The pre-commit counterpart of --changed-since."
2219
+ },
2220
+ {
2221
+ flag: "--with-dependents",
2222
+ type: "boolean",
2223
+ default: false,
2224
+ appliesTo: optionAppliesTo("with-dependents"),
2225
+ summary: "Widen a changed-files scope by one hop: files that import a changed file are included too. Only meaningful with --changed-since or --staged."
2226
+ },
1891
2227
  {
1892
2228
  flag: "--format",
1893
2229
  type: "string",
@@ -1995,6 +2331,13 @@ function buildAgentContract() {
1995
2331
  appliesTo: optionAppliesTo("no-install"),
1996
2332
  summary: "Skip installing the dependency before generating: `setup`, or `generate --autopilot` (CI, offline, or a lockfile you do not want a package manager to touch)."
1997
2333
  },
2334
+ {
2335
+ flag: "--install-hook",
2336
+ type: "boolean",
2337
+ default: false,
2338
+ appliesTo: optionAppliesTo("install-hook"),
2339
+ summary: "After writing the config, install a pre-commit hook that runs `validate`. An existing hook this tool did not write is left alone."
2340
+ },
1998
2341
  {
1999
2342
  flag: "--no-update-check",
2000
2343
  type: "boolean",
@@ -2031,6 +2374,12 @@ function buildAgentContract() {
2031
2374
  { key: "branch", type: "string", default: "main", summary: "Branch recorded when git cannot report one." },
2032
2375
  { key: "preset", type: "string", default: "detected", summary: "Force a preset. Unknown names are an error." },
2033
2376
  { key: "maxPartBytes", type: "number | string", default: 307200, summary: "Per-part byte budget." },
2377
+ { key: "maxPartTokens", type: "number | string | null", default: null, summary: "Per-part token ceiling; narrows maxPartBytes. Minimum 1024." },
2378
+ { key: "skeleton", type: "boolean", default: false, summary: "Collapse function bodies to `{ /* ... */ }` in TS, JS, TSX and JSX files." },
2379
+ { key: "sarif", type: "boolean", default: false, summary: "Also write reports/audit.sarif." },
2380
+ { key: "changedSince", type: "string | null", default: null, summary: "Snapshot only files that differ from this git ref." },
2381
+ { key: "staged", type: "boolean", default: false, summary: "Snapshot only files staged in the index." },
2382
+ { key: "withDependents", type: "boolean", default: false, summary: "With a changed-files scope, also include one hop of importers." },
2034
2383
  { key: "largeFileBytes", type: "number | string", default: 204800, summary: "Preview threshold for large files." },
2035
2384
  { key: "previewHeadLines", type: "number", default: 80, summary: "Lines kept from the head of a large file." },
2036
2385
  { key: "previewTailLines", type: "number", default: 40, summary: "Lines kept from the tail of a large file." },
@@ -2213,14 +2562,19 @@ do not assume the repository is broken.
2213
2562
  matches the files on disk. Exit 1 means drift \u2014 re-run step 5.
2214
2563
 
2215
2564
  7. **Read the output in this order:**
2216
- - \`snapshot/parts/part-*.md\` \u2014 the code itself, each file tagged with its layer, kind,
2217
- language and, when it fits the budget, a structural summary (imports, exports,
2218
- classes, functions). A dropped summary appears as a \`Summary: (omitted \u2026)\` line.
2565
+ - \`snapshot/parts/part-*.md\` \u2014 the code itself. Each file carries one \`- Meta:\` line
2566
+ (layer/kind, language, inclusion, size) and, only when the body is **not** the whole
2567
+ file, a structural summary (imports, exports, classes, functions). A full file has no
2568
+ summary line, because the summary would repeat the code three lines below it \u2014 read
2569
+ \`manifest.json\` for it. A summary dropped to fit the budget appears as a
2570
+ \`Summary: (omitted \u2026)\` line.
2219
2571
  - \`snapshot/manifest.json\` \u2014 the index. \`files[]\` has \`relativePath\`, \`layer\`,
2220
- \`kind\`, \`language\`, \`part\`, \`inclusionMode\` and \`hashSha256\`, plus \`encoding\`
2221
- when the file was not plain UTF-8. \`summary\` is **optional**: it is absent for
2222
- languages with no structural summary, and \`summaryOmitted: true\` marks one dropped
2223
- to fit the part budget \u2014 never assume it exists.
2572
+ \`kind\`, \`language\`, \`part\`, \`inclusionMode\`, \`hashSha256\` and the structural
2573
+ \`summary\`, plus \`encoding\` when the file was not plain UTF-8. \`summary\` is
2574
+ **optional**: it is absent for languages with no structural summary, and
2575
+ \`summaryOmitted: true\` marks one dropped to fit the part budget \u2014 never assume it
2576
+ exists. The per-file \`hashSha256\` is only here and in the manifest; it is not printed
2577
+ into the parts.
2224
2578
  \`skipped[]\` records every excluded file **and why**. Prefer querying this over
2225
2579
  re-reading the parts.
2226
2580
  - \`snapshot/reports/audit.md\` \u2014 secrets found, large files, parse diagnostics, git
@@ -2274,7 +2628,7 @@ diff, and the commit, are then yours.
2274
2628
  ## When the client speaks MCP
2275
2629
 
2276
2630
  If the agent runs inside an MCP client, \`npx ${PACKAGE_NAME2}@latest mcp\` serves this repository
2277
- as seven tools over stdio instead of shelling out. Its reads come from the snapshot rather than
2631
+ as eight tools over stdio instead of shelling out. Its reads come from the snapshot rather than
2278
2632
  the working tree, so excluded files and redacted values never reach the context. Use
2279
2633
  \`snapshot_locate\` before \`snapshot_read\`, and \`snapshot_validate\` before trusting a snapshot
2280
2634
  from an earlier session.
@@ -2394,7 +2748,7 @@ function buildResourceProvider() {
2394
2748
  const report = REPORT_URIS.get(uri);
2395
2749
  if (report) {
2396
2750
  const text = readFileIfPresent(
2397
- path2.resolve(workspace.root, workspace.config.outDir, "reports", report.file)
2751
+ path.resolve(workspace.root, workspace.config.outDir, "reports", report.file)
2398
2752
  );
2399
2753
  if (text === null) return null;
2400
2754
  return { uri, mimeType: "text/markdown", text: capUtf8(text, MAX_RESOURCE_BYTES).text };
@@ -2412,9 +2766,9 @@ function buildResourceProvider() {
2412
2766
  const index = Number(partMatch[1]);
2413
2767
  const part = read.manifest.parts.find((candidate) => candidate.index === index);
2414
2768
  if (!part) return null;
2415
- const text = readFileIfPresent(
2416
- path2.resolve(workspace.root, workspace.config.outDir, "parts", part.filename)
2417
- );
2769
+ const partPath = partPathFor(workspace, part.filename);
2770
+ if (partPath === null) return null;
2771
+ const text = readFileIfPresent(partPath);
2418
2772
  if (text === null) return null;
2419
2773
  return { uri, mimeType: "text/markdown", text: capUtf8(text, MAX_RESOURCE_BYTES).text };
2420
2774
  }
@@ -2431,7 +2785,8 @@ var SERVER_INSTRUCTIONS = [
2431
2785
  "1. snapshot_status \u2014 is there a snapshot here, and has the tree moved since it was built?",
2432
2786
  "2. snapshot_locate \u2014 find which part holds the code you need.",
2433
2787
  "3. snapshot_read \u2014 read a window of one file. Never read a part end to end.",
2434
- "4. snapshot_validate \u2014 before trusting context that came from an earlier conversation.",
2788
+ "4. snapshot_dependents \u2014 before changing a file, see what imports it.",
2789
+ "5. snapshot_validate \u2014 before trusting context that came from an earlier conversation.",
2435
2790
  "",
2436
2791
  "Rules that matter:",
2437
2792
  "- Parts are budgeted, not small: a 12-part snapshot is hundreds of kilobytes. Use snapshot_locate first.",
@@ -2457,6 +2812,7 @@ function createWriter(output) {
2457
2812
  }
2458
2813
  function createLineReader(input, options) {
2459
2814
  let buffer = "";
2815
+ let bufferBytes = 0;
2460
2816
  let discarding = false;
2461
2817
  let discardedBytes = 0;
2462
2818
  const deliver = (line) => {
@@ -2469,43 +2825,71 @@ function createLineReader(input, options) {
2469
2825
  if (trimmed.trim().length === 0) return;
2470
2826
  options.onLine(trimmed);
2471
2827
  };
2828
+ const discardBuffered = () => {
2829
+ discarding = true;
2830
+ discardedBytes += bufferBytes;
2831
+ buffer = "";
2832
+ bufferBytes = 0;
2833
+ options.onOverflow?.(discardedBytes);
2834
+ };
2472
2835
  const drain = () => {
2473
2836
  let index = buffer.indexOf("\n");
2474
2837
  while (index >= 0) {
2475
2838
  const line = buffer.slice(0, index);
2476
2839
  buffer = buffer.slice(index + 1);
2477
- deliver(line);
2840
+ if (discarding) {
2841
+ deliver(line);
2842
+ } else {
2843
+ const lineBytes = Buffer.byteLength(line, "utf8");
2844
+ if (lineBytes > MAX_LINE_BYTES) options.onOverflow?.(lineBytes);
2845
+ else deliver(line);
2846
+ }
2478
2847
  index = buffer.indexOf("\n");
2479
2848
  }
2849
+ bufferBytes = Buffer.byteLength(buffer, "utf8");
2850
+ if (!discarding && bufferBytes > MAX_LINE_BYTES) discardBuffered();
2480
2851
  };
2481
2852
  const onData = (chunk) => {
2853
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
2482
2854
  if (discarding) {
2483
2855
  const index = chunk.indexOf("\n");
2484
2856
  if (index < 0) {
2485
- discardedBytes += chunk.length;
2857
+ discardedBytes += chunkBytes;
2486
2858
  return;
2487
2859
  }
2488
2860
  buffer = chunk.slice(index + 1);
2861
+ bufferBytes = Buffer.byteLength(buffer, "utf8");
2489
2862
  deliver("");
2490
2863
  drain();
2491
2864
  return;
2492
2865
  }
2493
- buffer += chunk;
2494
- if (buffer.length > MAX_LINE_BYTES && buffer.indexOf("\n") < 0) {
2866
+ if (chunk.indexOf("\n") < 0 && bufferBytes + chunkBytes > MAX_LINE_BYTES) {
2495
2867
  discarding = true;
2496
- discardedBytes = buffer.length;
2868
+ discardedBytes = bufferBytes + chunkBytes;
2497
2869
  buffer = "";
2870
+ bufferBytes = 0;
2498
2871
  options.onOverflow?.(discardedBytes);
2499
2872
  return;
2500
2873
  }
2874
+ buffer += chunk;
2501
2875
  drain();
2502
2876
  };
2503
2877
  const onEnd = () => {
2504
- if (buffer.length > 0) {
2505
- const rest = buffer;
2506
- buffer = "";
2507
- deliver(rest);
2878
+ const rest = buffer;
2879
+ const restBytes = bufferBytes || Buffer.byteLength(rest, "utf8");
2880
+ buffer = "";
2881
+ bufferBytes = 0;
2882
+ if (rest.length === 0) return;
2883
+ if (discarding) {
2884
+ discarding = false;
2885
+ discardedBytes = 0;
2886
+ return;
2887
+ }
2888
+ if (restBytes > MAX_LINE_BYTES) {
2889
+ options.onOverflow?.(restBytes);
2890
+ return;
2508
2891
  }
2892
+ deliver(rest);
2509
2893
  };
2510
2894
  input.setEncoding("utf8");
2511
2895
  input.on("data", onData);
@@ -2598,6 +2982,4 @@ async function runStdioServer(options) {
2598
2982
  return code;
2599
2983
  }
2600
2984
 
2601
- export { COMMAND_OPTION_SCOPE, ERROR_CODES, LEGACY_PROTOCOL_VERSIONS, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, SERVER_INSTRUCTIONS, SKILL_NAME, SUPPORTED_PROTOCOL_VERSIONS, buildAgentContract, buildPrompts, buildResourceProvider, buildTools, classifyRequest, coerceNumericStrings, commandsForOption, createLineReader, createMcpServer, createServer, createSession, createWriter, inspectModernMeta, negotiateLegacyVersion, renderContractText, renderSkill, runStdioServer, validateArguments };
2602
- //# sourceMappingURL=chunk-HQBEQ67K.js.map
2603
- //# sourceMappingURL=chunk-HQBEQ67K.js.map
2985
+ export { COMMAND_OPTION_SCOPE, ERROR_CODES, LEGACY_PROTOCOL_VERSIONS, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, SERVER_INSTRUCTIONS, SKILL_NAME, SUPPORTED_PROTOCOL_VERSIONS, buildAgentContract, buildPrompts, buildResourceProvider, buildTools, classifyRequest, coerceNumericStrings, commandsForOption, createLineReader, createMcpServer, createServer, createSession, createWriter, findSection, inspectModernMeta, negotiateLegacyVersion, parsePartSections, renderContractText, renderSkill, runStdioServer, validateArguments, windowText };