codebase-snapshot 0.9.12 → 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, 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-3H45YMBG.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"];
@@ -617,7 +617,7 @@ function foldForComparison(value) {
617
617
  return process.platform === "win32" || process.platform === "darwin" ? value.toLowerCase() : value;
618
618
  }
619
619
  function canonicalPath(value) {
620
- const absolute = path2.resolve(value);
620
+ const absolute = path.resolve(value);
621
621
  try {
622
622
  return fs.realpathSync(absolute);
623
623
  } catch {
@@ -627,14 +627,14 @@ function canonicalPath(value) {
627
627
  function isInside(parent, child) {
628
628
  const foldedParent = foldForComparison(parent);
629
629
  const foldedChild = foldForComparison(child);
630
- return foldedChild === foldedParent || foldedChild.startsWith(`${foldedParent}${path2.sep}`);
630
+ return foldedChild === foldedParent || foldedChild.startsWith(`${foldedParent}${path.sep}`);
631
631
  }
632
632
  function resolveRoot(requested, context) {
633
633
  if (requested !== void 0 && (typeof requested !== "string" || requested.trim().length === 0)) {
634
634
  return { ok: false, message: '"root" must be a non-empty string when provided.' };
635
635
  }
636
636
  const wanted = typeof requested === "string" ? requested.trim() : context.cwd;
637
- const absolute = path2.resolve(context.cwd, wanted);
637
+ const absolute = path.resolve(context.cwd, wanted);
638
638
  let stats;
639
639
  try {
640
640
  stats = fs.statSync(absolute);
@@ -673,12 +673,35 @@ async function openWorkspace(root, overrides = {}) {
673
673
  }
674
674
  cliOptions.maxPartBytes = parsed;
675
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
+ }
676
686
  if (overrides.outDir !== void 0) {
677
687
  if (typeof overrides.outDir !== "string" || overrides.outDir.trim().length === 0) {
678
688
  return { ok: false, message: '"outDir" must be a non-empty string.' };
679
689
  }
680
690
  cliOptions.outDir = overrides.outDir.trim();
681
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;
682
705
  try {
683
706
  const config = resolveConfig(loaded.config, cliOptions, root);
684
707
  return { ok: true, workspace: { root, config, configFile: loaded.file, cliOptions } };
@@ -688,11 +711,21 @@ async function openWorkspace(root, overrides = {}) {
688
711
  }
689
712
  var manifestCache = /* @__PURE__ */ new Map();
690
713
  function manifestPathFor(workspace) {
691
- 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;
692
725
  }
693
726
  function readManifest(workspace) {
694
727
  const file = manifestPathFor(workspace);
695
- 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;
696
729
  let stats;
697
730
  try {
698
731
  stats = fs.statSync(file);
@@ -760,6 +793,44 @@ var OUT_DIR_PROPERTY = {
760
793
  description: "Snapshot directory, relative to the root. Defaults to the project configuration's outDir."
761
794
  };
762
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
+ }
763
834
  function failure(message) {
764
835
  return { text: message, isError: true };
765
836
  }
@@ -772,7 +843,7 @@ function formatBytes(bytes) {
772
843
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
773
844
  }
774
845
  function relativeFor(workspace, absolute) {
775
- return path2.relative(workspace.root, absolute).split(path2.sep).join("/");
846
+ return path.relative(workspace.root, absolute).split(path.sep).join("/");
776
847
  }
777
848
  function capped(values, limit = 20) {
778
849
  return { count: values.length, sample: values.slice(0, limit) };
@@ -790,13 +861,14 @@ function summarizeDrift(drift) {
790
861
  }
791
862
  function toRelativePath(root, value) {
792
863
  const cleaned = value.trim();
793
- if (cleaned.length === 0) return null;
794
- if (!path2.isAbsolute(cleaned)) {
795
- 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;
796
870
  }
797
- const relative3 = path2.relative(root, cleaned);
798
- if (relative3.startsWith("..") || path2.isAbsolute(relative3)) return null;
799
- return relative3.split(path2.sep).join("/");
871
+ return relative3.split(path.sep).join("/");
800
872
  }
801
873
  function symbolsOf(entry) {
802
874
  const summary = entry.summary;
@@ -931,7 +1003,8 @@ var planTool = {
931
1003
  maxPartBytes: {
932
1004
  type: "string",
933
1005
  description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
934
- }
1006
+ },
1007
+ ...SCOPE_PROPERTIES
935
1008
  },
936
1009
  additionalProperties: false
937
1010
  },
@@ -948,7 +1021,7 @@ var planTool = {
948
1021
  },
949
1022
  annotations: READ_ONLY,
950
1023
  async handler(args, context) {
951
- const prepared = await prepare(args, context, { maxPartBytes: args.maxPartBytes, outDir: args.outDir });
1024
+ const prepared = await prepare(args, context, workspaceOverrides(args));
952
1025
  if (!prepared.ok) return prepared.outcome;
953
1026
  const workspace = prepared.workspace;
954
1027
  let manifest;
@@ -1006,7 +1079,8 @@ var generateTool = {
1006
1079
  maxPartBytes: {
1007
1080
  type: "string",
1008
1081
  description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
1009
- }
1082
+ },
1083
+ ...SCOPE_PROPERTIES
1010
1084
  },
1011
1085
  additionalProperties: false
1012
1086
  },
@@ -1022,17 +1096,14 @@ var generateTool = {
1022
1096
  },
1023
1097
  annotations: WRITES,
1024
1098
  async handler(args, context) {
1025
- const prepared = await prepare(args, context, {
1026
- maxPartBytes: args.maxPartBytes,
1027
- outDir: args.outDir
1028
- });
1099
+ const prepared = await prepare(args, context, workspaceOverrides(args));
1029
1100
  if (!prepared.ok) return prepared.outcome;
1030
1101
  const workspace = prepared.workspace;
1031
1102
  const result = await generateInWorker(workspace.root, workspace.cliOptions);
1032
1103
  if (!result.ok) return failure(result.message);
1033
1104
  const summary = result.summary;
1034
1105
  forgetManifest(manifestPathFor(workspace));
1035
- const written = relativeFor(workspace, path2.resolve(workspace.root, workspace.config.outDir));
1106
+ const written = relativeFor(workspace, path.resolve(workspace.root, workspace.config.outDir));
1036
1107
  const lines = [
1037
1108
  `Wrote ${written}/`,
1038
1109
  `Files: ${summary.totalFiles} included, ${summary.skippedCount} skipped, ${plural(summary.partsCount, "part")}`,
@@ -1064,7 +1135,7 @@ var validateTool = {
1064
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.",
1065
1136
  inputSchema: {
1066
1137
  type: "object",
1067
- properties: { root: ROOT_PROPERTY, outDir: OUT_DIR_PROPERTY },
1138
+ properties: { root: ROOT_PROPERTY, outDir: OUT_DIR_PROPERTY, ...SCOPE_PROPERTIES },
1068
1139
  additionalProperties: false
1069
1140
  },
1070
1141
  outputSchema: {
@@ -1076,7 +1147,7 @@ var validateTool = {
1076
1147
  },
1077
1148
  annotations: READ_ONLY,
1078
1149
  async handler(args, context) {
1079
- const prepared = await prepare(args, context, outDirOverride(args));
1150
+ const prepared = await prepare(args, context, workspaceOverrides(args));
1080
1151
  if (!prepared.ok) return prepared.outcome;
1081
1152
  const workspace = prepared.workspace;
1082
1153
  let result;
@@ -1421,7 +1492,12 @@ var readTool = {
1421
1492
  `${relativePath} is not in the snapshot. Call snapshot_locate to find the path, or snapshot_explain to learn why it was excluded.`
1422
1493
  );
1423
1494
  }
1424
- 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
+ }
1425
1501
  let partText;
1426
1502
  try {
1427
1503
  partText = fs.readFileSync(partPath, "utf8");
@@ -1501,7 +1577,8 @@ var explainTool = {
1501
1577
  description: "Path relative to the repository root. Directories are explained as a whole."
1502
1578
  },
1503
1579
  root: ROOT_PROPERTY,
1504
- outDir: OUT_DIR_PROPERTY
1580
+ outDir: OUT_DIR_PROPERTY,
1581
+ ...SCOPE_PROPERTIES
1505
1582
  },
1506
1583
  required: ["path"],
1507
1584
  additionalProperties: false
@@ -1516,7 +1593,7 @@ var explainTool = {
1516
1593
  },
1517
1594
  annotations: READ_ONLY,
1518
1595
  async handler(args, context) {
1519
- const prepared = await prepare(args, context, outDirOverride(args));
1596
+ const prepared = await prepare(args, context, workspaceOverrides(args));
1520
1597
  if (!prepared.ok) return prepared.outcome;
1521
1598
  const workspace = prepared.workspace;
1522
1599
  if (typeof args.path !== "string") return failure('"path" must be a string.');
@@ -2298,7 +2375,7 @@ function buildAgentContract() {
2298
2375
  { key: "preset", type: "string", default: "detected", summary: "Force a preset. Unknown names are an error." },
2299
2376
  { key: "maxPartBytes", type: "number | string", default: 307200, summary: "Per-part byte budget." },
2300
2377
  { key: "maxPartTokens", type: "number | string | null", default: null, summary: "Per-part token ceiling; narrows maxPartBytes. Minimum 1024." },
2301
- { key: "skeleton", type: "boolean", default: false, summary: "Collapse function bodies to `{ /* ... */ }` in TypeScript files." },
2378
+ { key: "skeleton", type: "boolean", default: false, summary: "Collapse function bodies to `{ /* ... */ }` in TS, JS, TSX and JSX files." },
2302
2379
  { key: "sarif", type: "boolean", default: false, summary: "Also write reports/audit.sarif." },
2303
2380
  { key: "changedSince", type: "string | null", default: null, summary: "Snapshot only files that differ from this git ref." },
2304
2381
  { key: "staged", type: "boolean", default: false, summary: "Snapshot only files staged in the index." },
@@ -2671,7 +2748,7 @@ function buildResourceProvider() {
2671
2748
  const report = REPORT_URIS.get(uri);
2672
2749
  if (report) {
2673
2750
  const text = readFileIfPresent(
2674
- path2.resolve(workspace.root, workspace.config.outDir, "reports", report.file)
2751
+ path.resolve(workspace.root, workspace.config.outDir, "reports", report.file)
2675
2752
  );
2676
2753
  if (text === null) return null;
2677
2754
  return { uri, mimeType: "text/markdown", text: capUtf8(text, MAX_RESOURCE_BYTES).text };
@@ -2689,9 +2766,9 @@ function buildResourceProvider() {
2689
2766
  const index = Number(partMatch[1]);
2690
2767
  const part = read.manifest.parts.find((candidate) => candidate.index === index);
2691
2768
  if (!part) return null;
2692
- const text = readFileIfPresent(
2693
- path2.resolve(workspace.root, workspace.config.outDir, "parts", part.filename)
2694
- );
2769
+ const partPath = partPathFor(workspace, part.filename);
2770
+ if (partPath === null) return null;
2771
+ const text = readFileIfPresent(partPath);
2695
2772
  if (text === null) return null;
2696
2773
  return { uri, mimeType: "text/markdown", text: capUtf8(text, MAX_RESOURCE_BYTES).text };
2697
2774
  }
@@ -2735,6 +2812,7 @@ function createWriter(output) {
2735
2812
  }
2736
2813
  function createLineReader(input, options) {
2737
2814
  let buffer = "";
2815
+ let bufferBytes = 0;
2738
2816
  let discarding = false;
2739
2817
  let discardedBytes = 0;
2740
2818
  const deliver = (line) => {
@@ -2747,6 +2825,13 @@ function createLineReader(input, options) {
2747
2825
  if (trimmed.trim().length === 0) return;
2748
2826
  options.onLine(trimmed);
2749
2827
  };
2828
+ const discardBuffered = () => {
2829
+ discarding = true;
2830
+ discardedBytes += bufferBytes;
2831
+ buffer = "";
2832
+ bufferBytes = 0;
2833
+ options.onOverflow?.(discardedBytes);
2834
+ };
2750
2835
  const drain = () => {
2751
2836
  let index = buffer.indexOf("\n");
2752
2837
  while (index >= 0) {
@@ -2754,42 +2839,57 @@ function createLineReader(input, options) {
2754
2839
  buffer = buffer.slice(index + 1);
2755
2840
  if (discarding) {
2756
2841
  deliver(line);
2757
- } else if (line.length > MAX_LINE_BYTES) {
2758
- options.onOverflow?.(line.length);
2759
2842
  } else {
2760
- deliver(line);
2843
+ const lineBytes = Buffer.byteLength(line, "utf8");
2844
+ if (lineBytes > MAX_LINE_BYTES) options.onOverflow?.(lineBytes);
2845
+ else deliver(line);
2761
2846
  }
2762
2847
  index = buffer.indexOf("\n");
2763
2848
  }
2849
+ bufferBytes = Buffer.byteLength(buffer, "utf8");
2850
+ if (!discarding && bufferBytes > MAX_LINE_BYTES) discardBuffered();
2764
2851
  };
2765
2852
  const onData = (chunk) => {
2853
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
2766
2854
  if (discarding) {
2767
2855
  const index = chunk.indexOf("\n");
2768
2856
  if (index < 0) {
2769
- discardedBytes += chunk.length;
2857
+ discardedBytes += chunkBytes;
2770
2858
  return;
2771
2859
  }
2772
2860
  buffer = chunk.slice(index + 1);
2861
+ bufferBytes = Buffer.byteLength(buffer, "utf8");
2773
2862
  deliver("");
2774
2863
  drain();
2775
2864
  return;
2776
2865
  }
2777
- buffer += chunk;
2778
- drain();
2779
- if (buffer.length > MAX_LINE_BYTES) {
2866
+ if (chunk.indexOf("\n") < 0 && bufferBytes + chunkBytes > MAX_LINE_BYTES) {
2780
2867
  discarding = true;
2781
- discardedBytes = buffer.length;
2868
+ discardedBytes = bufferBytes + chunkBytes;
2782
2869
  buffer = "";
2870
+ bufferBytes = 0;
2783
2871
  options.onOverflow?.(discardedBytes);
2784
2872
  return;
2785
2873
  }
2874
+ buffer += chunk;
2875
+ drain();
2786
2876
  };
2787
2877
  const onEnd = () => {
2788
- if (buffer.length > 0) {
2789
- const rest = buffer;
2790
- buffer = "";
2791
- 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;
2792
2891
  }
2892
+ deliver(rest);
2793
2893
  };
2794
2894
  input.setEncoding("utf8");
2795
2895
  input.on("data", onData);