codebase-snapshot 0.9.0 → 0.9.4

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.
package/dist/bin.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { spawnSync } from "child_process";
5
- import * as fs12 from "fs";
6
- import * as path13 from "path";
5
+ import * as fs16 from "fs";
6
+ import * as path17 from "path";
7
7
  import { parseArgs } from "util";
8
8
 
9
9
  // src/diff.ts
@@ -167,8 +167,8 @@ function workspacePackageDirs(cwd, patterns) {
167
167
  function scanWorkspacePackages(cwd, patterns) {
168
168
  const files = /* @__PURE__ */ new Set();
169
169
  const dependencies = /* @__PURE__ */ new Set();
170
- for (const relative8 of workspacePackageDirs(cwd, patterns)) {
171
- const full = path.join(cwd, relative8);
170
+ for (const relative11 of workspacePackageDirs(cwd, patterns)) {
171
+ const full = path.join(cwd, relative11);
172
172
  try {
173
173
  for (const entry of fs.readdirSync(full, { withFileTypes: true })) files.add(entry.name);
174
174
  } catch {
@@ -245,25 +245,41 @@ function buildPresetContext(cwd) {
245
245
  markdownCount: files.has("package.json") ? 0 : countMarkdownFiles(cwd)
246
246
  };
247
247
  }
248
- function hasFile(context, name) {
249
- return context.files.has(name);
248
+
249
+ // src/presets/evidence.ts
250
+ function fileEvidence(context, name) {
251
+ return context.files.has(name) ? name : null;
250
252
  }
251
- function hasFileStartingWith(context, prefix) {
253
+ function fileStartingWithEvidence(context, prefix) {
252
254
  for (const file of context.files) {
253
- if (file.startsWith(prefix)) return true;
255
+ if (file.startsWith(prefix)) return file;
254
256
  }
255
257
  for (const file of context.workspaceFiles) {
256
- if (file.startsWith(prefix)) return true;
258
+ if (file.startsWith(prefix)) return file;
257
259
  }
258
- return false;
260
+ return null;
261
+ }
262
+ function dependencyEvidence(context, ...names) {
263
+ for (const name of names) {
264
+ if (context.dependencies.has(name)) return `depends on ${name}`;
265
+ }
266
+ return null;
259
267
  }
260
- function manifestContains(context, manifest, needle) {
268
+ function manifestEvidence(context, manifest, needle) {
261
269
  const content = context.manifests[manifest];
262
- if (!content) return false;
263
- return content.toLowerCase().includes(needle.toLowerCase());
270
+ if (!content) return null;
271
+ if (!content.toLowerCase().includes(needle.toLowerCase())) return null;
272
+ return `${needle} in ${manifest}`;
273
+ }
274
+ function suffixEvidence(context, suffix) {
275
+ for (const file of context.files) {
276
+ if (file.endsWith(suffix)) return file;
277
+ }
278
+ return null;
264
279
  }
265
- function hasDependency(context, ...names) {
266
- return names.some((name) => context.dependencies.has(name));
280
+ function markdownEvidence(context) {
281
+ if (context.markdownCount < 3 || context.files.has("package.json")) return null;
282
+ return `${context.markdownCount} markdown files, no package.json`;
267
283
  }
268
284
 
269
285
  // src/presets/helpers.ts
@@ -471,7 +487,7 @@ var JS_PRESETS = [
471
487
  {
472
488
  name: "nextjs",
473
489
  priority: 100,
474
- detect: (context) => hasFileStartingWith(context, "next.config") || hasDependency(context, "next"),
490
+ detect: (context) => fileStartingWithEvidence(context, "next.config") ?? dependencyEvidence(context, "next"),
475
491
  layers: NEXTJS_LAYERS,
476
492
  kinds: NEXTJS_KINDS,
477
493
  exclude: NEXT_LIKE_EXCLUDES,
@@ -480,7 +496,7 @@ var JS_PRESETS = [
480
496
  {
481
497
  name: "nuxt",
482
498
  priority: 98,
483
- detect: (context) => hasFileStartingWith(context, "nuxt.config") || hasDependency(context, "nuxt", "nuxt3"),
499
+ detect: (context) => fileStartingWithEvidence(context, "nuxt.config") ?? dependencyEvidence(context, "nuxt", "nuxt3"),
484
500
  layers: [
485
501
  layer("pages", "pages/", "src/pages/"),
486
502
  layer("components", "components/", "src/components/"),
@@ -504,7 +520,7 @@ var JS_PRESETS = [
504
520
  {
505
521
  name: "sveltekit",
506
522
  priority: 96,
507
- detect: (context) => hasFileStartingWith(context, "svelte.config") || hasDependency(context, "@sveltejs/kit", "svelte"),
523
+ detect: (context) => fileStartingWithEvidence(context, "svelte.config") ?? dependencyEvidence(context, "@sveltejs/kit", "svelte"),
508
524
  layers: [
509
525
  layer("routes", "src/routes/", "routes/"),
510
526
  layer("lib", "src/lib/", "lib/"),
@@ -525,7 +541,7 @@ var JS_PRESETS = [
525
541
  {
526
542
  name: "remix",
527
543
  priority: 94,
528
- detect: (context) => hasDependency(context, "@remix-run/react", "@remix-run/node", "@remix-run/dev"),
544
+ detect: (context) => dependencyEvidence(context, "@remix-run/react", "@remix-run/node", "@remix-run/dev"),
529
545
  layers: [
530
546
  layer("routes", "app/routes/", "routes/"),
531
547
  layer("components", "app/components/"),
@@ -539,7 +555,7 @@ var JS_PRESETS = [
539
555
  {
540
556
  name: "astro",
541
557
  priority: 92,
542
- detect: (context) => hasFileStartingWith(context, "astro.config") || hasDependency(context, "astro"),
558
+ detect: (context) => fileStartingWithEvidence(context, "astro.config") ?? dependencyEvidence(context, "astro"),
543
559
  layers: [
544
560
  layer("pages", "src/pages/"),
545
561
  layer("layouts", "src/layouts/"),
@@ -554,7 +570,7 @@ var JS_PRESETS = [
554
570
  {
555
571
  name: "angular",
556
572
  priority: 90,
557
- detect: (context) => hasFile(context, "angular.json") || hasDependency(context, "@angular/core"),
573
+ detect: (context) => fileEvidence(context, "angular.json") ?? dependencyEvidence(context, "@angular/core"),
558
574
  layers: [
559
575
  layer("app", "src/app/"),
560
576
  layer("features", "src/app/features/"),
@@ -578,7 +594,7 @@ var JS_PRESETS = [
578
594
  {
579
595
  name: "solid",
580
596
  priority: 88,
581
- detect: (context) => hasDependency(context, "solid-js", "solid-start"),
597
+ detect: (context) => dependencyEvidence(context, "solid-js", "solid-start"),
582
598
  layers: [
583
599
  layer("pages", "src/routes/", "src/pages/"),
584
600
  layer("components", "src/components/"),
@@ -591,7 +607,15 @@ var JS_PRESETS = [
591
607
  {
592
608
  name: "vue-vite",
593
609
  priority: 86,
594
- detect: (context) => hasDependency(context, "vue") && (hasDependency(context, "vite") || hasFileStartingWith(context, "vite.config")),
610
+ // `vue` is the decisive signal; `vite` (or its config file) only says the app is
611
+ // built with Vite rather than shipped pre-compiled, so it is required but not what
612
+ // gets reported.
613
+ detect: (context) => {
614
+ const framework = dependencyEvidence(context, "vue");
615
+ if (framework === null) return null;
616
+ const build = dependencyEvidence(context, "vite") ?? fileStartingWithEvidence(context, "vite.config");
617
+ return build === null ? null : framework;
618
+ },
595
619
  layers: [
596
620
  layer("pages", "src/pages/", "src/views/"),
597
621
  layer("components", "src/components/"),
@@ -616,7 +640,12 @@ var JS_PRESETS = [
616
640
  {
617
641
  name: "react-vite",
618
642
  priority: 84,
619
- detect: (context) => hasDependency(context, "react") && (hasDependency(context, "vite") || hasFileStartingWith(context, "vite.config")),
643
+ detect: (context) => {
644
+ const framework = dependencyEvidence(context, "react");
645
+ if (framework === null) return null;
646
+ const build = dependencyEvidence(context, "vite") ?? fileStartingWithEvidence(context, "vite.config");
647
+ return build === null ? null : framework;
648
+ },
620
649
  layers: VITE_LAYERS,
621
650
  kinds: VITE_KINDS,
622
651
  exclude: [".vite", "dist"],
@@ -625,7 +654,7 @@ var JS_PRESETS = [
625
654
  {
626
655
  name: "nest",
627
656
  priority: 80,
628
- detect: (context) => hasDependency(context, "@nestjs/core"),
657
+ detect: (context) => dependencyEvidence(context, "@nestjs/core"),
629
658
  layers: [
630
659
  layer("modules", "src/modules/", "modules/"),
631
660
  layer("controllers", "src/controllers/", "controllers/"),
@@ -651,7 +680,7 @@ var JS_PRESETS = [
651
680
  {
652
681
  name: "node-express",
653
682
  priority: 78,
654
- detect: (context) => hasDependency(context, "express", "fastify", "koa", "@hapi/hapi"),
683
+ detect: (context) => dependencyEvidence(context, "express", "fastify", "koa", "@hapi/hapi"),
655
684
  layers: NODE_LAYERS,
656
685
  kinds: NODE_KINDS,
657
686
  exclude: ["dist"],
@@ -662,7 +691,7 @@ var JS_PRESETS = [
662
691
  // A bare package.json is often just tooling around a non-JS project, so it
663
692
  // ranks below every definitive language manifest.
664
693
  priority: 45,
665
- detect: (context) => hasFile(context, "package.json"),
694
+ detect: (context) => fileEvidence(context, "package.json"),
666
695
  layers: [
667
696
  layer("src", "src/"),
668
697
  layer("lib", "lib/"),
@@ -676,13 +705,12 @@ var JS_PRESETS = [
676
705
  ];
677
706
 
678
707
  // src/presets/lang-presets.ts
679
- var anyFileEndingWith = (context, suffix) => Array.from(context.files).some((file) => file.endsWith(suffix));
680
708
  var DIR = (name) => `${name}/`;
681
709
  var LANGUAGE_PRESETS = [
682
710
  {
683
711
  name: "python-django",
684
712
  priority: 56,
685
- detect: (context) => hasFile(context, "manage.py") || manifestContains(context, "pyproject.toml", "django") || manifestContains(context, "requirements.txt", "django"),
713
+ detect: (context) => fileEvidence(context, "manage.py") ?? manifestEvidence(context, "pyproject.toml", "django") ?? manifestEvidence(context, "requirements.txt", "django"),
686
714
  layers: [
687
715
  layer("views", DIR("views"), DIR("app/views"), DIR("apps/views")),
688
716
  layer("models", DIR("models"), DIR("app/models"), DIR("apps/models")),
@@ -708,7 +736,7 @@ var LANGUAGE_PRESETS = [
708
736
  {
709
737
  name: "python-fastapi",
710
738
  priority: 54,
711
- detect: (context) => manifestContains(context, "requirements.txt", "fastapi") || manifestContains(context, "requirements.txt", "flask") || manifestContains(context, "pyproject.toml", "fastapi") || manifestContains(context, "pyproject.toml", "flask"),
739
+ detect: (context) => manifestEvidence(context, "requirements.txt", "fastapi") ?? manifestEvidence(context, "requirements.txt", "flask") ?? manifestEvidence(context, "pyproject.toml", "fastapi") ?? manifestEvidence(context, "pyproject.toml", "flask"),
712
740
  layers: [
713
741
  layer("routers", DIR("routers"), DIR("routes"), DIR("api")),
714
742
  layer("models", DIR("models"), DIR("db")),
@@ -732,7 +760,7 @@ var LANGUAGE_PRESETS = [
732
760
  name: "python-lib",
733
761
  // Below node-lib: a Node repo with a stray .py file is still a Node repo.
734
762
  priority: 30,
735
- detect: (context) => hasFile(context, "pyproject.toml") || hasFile(context, "setup.py") || hasFile(context, "requirements.txt") || anyFileEndingWith(context, ".py"),
763
+ detect: (context) => fileEvidence(context, "pyproject.toml") ?? fileEvidence(context, "setup.py") ?? fileEvidence(context, "requirements.txt") ?? suffixEvidence(context, ".py"),
736
764
  layers: [
737
765
  layer("package", DIR("src"), DIR("lib")),
738
766
  layer("tests", DIR("tests"), DIR("test")),
@@ -745,7 +773,7 @@ var LANGUAGE_PRESETS = [
745
773
  {
746
774
  name: "go",
747
775
  priority: 50,
748
- detect: (context) => hasFile(context, "go.mod"),
776
+ detect: (context) => fileEvidence(context, "go.mod"),
749
777
  layers: [
750
778
  layer("cmd", "cmd/"),
751
779
  layer("internal", "internal/"),
@@ -769,7 +797,7 @@ var LANGUAGE_PRESETS = [
769
797
  {
770
798
  name: "rust",
771
799
  priority: 48,
772
- detect: (context) => hasFile(context, "Cargo.toml"),
800
+ detect: (context) => fileEvidence(context, "Cargo.toml"),
773
801
  layers: [
774
802
  layer("src", "src/"),
775
803
  layer("crates", "crates/"),
@@ -785,7 +813,7 @@ var LANGUAGE_PRESETS = [
785
813
  {
786
814
  name: "java-spring",
787
815
  priority: 46,
788
- detect: (context) => hasFile(context, "pom.xml") || hasFile(context, "build.gradle") || hasFile(context, "build.gradle.kts"),
816
+ detect: (context) => fileEvidence(context, "pom.xml") ?? fileEvidence(context, "build.gradle") ?? fileEvidence(context, "build.gradle.kts"),
789
817
  layers: [
790
818
  layer("api", "src/main/java/"),
791
819
  layer("test", "src/test/java/"),
@@ -808,7 +836,7 @@ var LANGUAGE_PRESETS = [
808
836
  {
809
837
  name: "dotnet",
810
838
  priority: 44,
811
- detect: (context) => anyFileEndingWith(context, ".csproj") || anyFileEndingWith(context, ".sln") || hasFile(context, "global.json"),
839
+ detect: (context) => suffixEvidence(context, ".csproj") ?? suffixEvidence(context, ".sln") ?? fileEvidence(context, "global.json"),
812
840
  layers: [
813
841
  layer("web", "src/", "Web/"),
814
842
  layer("domain", "Domain/", "Core/"),
@@ -831,7 +859,7 @@ var LANGUAGE_PRESETS = [
831
859
  {
832
860
  name: "php-laravel",
833
861
  priority: 42,
834
- detect: (context) => hasFile(context, "artisan") || manifestContains(context, "composer.json", "laravel") || hasFile(context, "composer.json"),
862
+ detect: (context) => fileEvidence(context, "artisan") ?? manifestEvidence(context, "composer.json", "laravel") ?? fileEvidence(context, "composer.json"),
835
863
  layers: [
836
864
  layer("app", "app/"),
837
865
  layer("routes", "routes/"),
@@ -857,7 +885,7 @@ var LANGUAGE_PRESETS = [
857
885
  {
858
886
  name: "ruby-rails",
859
887
  priority: 40,
860
- detect: (context) => hasFile(context, "Gemfile") || hasFile(context, "config.ru") || hasFile(context, "Rakefile"),
888
+ detect: (context) => fileEvidence(context, "Gemfile") ?? fileEvidence(context, "config.ru") ?? fileEvidence(context, "Rakefile"),
861
889
  layers: [
862
890
  layer("app", "app/"),
863
891
  layer("controllers", "app/controllers/"),
@@ -930,7 +958,7 @@ var SHAPE_PRESETS = [
930
958
  {
931
959
  name: "monorepo",
932
960
  priority: 70,
933
- detect: (context) => hasFile(context, "pnpm-workspace.yaml") || hasFile(context, "nx.json") || hasFile(context, "turbo.json") || hasFile(context, "lerna.json") || manifestContains(context, "package.json", '"workspaces"'),
961
+ detect: (context) => fileEvidence(context, "pnpm-workspace.yaml") ?? fileEvidence(context, "nx.json") ?? fileEvidence(context, "turbo.json") ?? fileEvidence(context, "lerna.json") ?? manifestEvidence(context, "package.json", '"workspaces"'),
934
962
  layers: [
935
963
  layer("apps", "apps/"),
936
964
  layer("packages", "packages/"),
@@ -944,7 +972,7 @@ var SHAPE_PRESETS = [
944
972
  {
945
973
  name: "infra",
946
974
  priority: 24,
947
- detect: (context) => Array.from(context.files).some((file) => file.endsWith(".tf")) || hasFile(context, "Chart.yaml") || hasFile(context, "kustomization.yaml") || hasFile(context, "docker-compose.yml") || hasFile(context, "docker-compose.yaml"),
975
+ detect: (context) => suffixEvidence(context, ".tf") ?? fileEvidence(context, "Chart.yaml") ?? fileEvidence(context, "kustomization.yaml") ?? fileEvidence(context, "docker-compose.yml") ?? fileEvidence(context, "docker-compose.yaml"),
948
976
  layers: [
949
977
  layer("modules", "modules/"),
950
978
  layer("environments", "env/", "environments/"),
@@ -966,7 +994,7 @@ var SHAPE_PRESETS = [
966
994
  // up to two directories down (bounded in the context builder). Counting
967
995
  // only the root turned every real documentation repository — README.md plus
968
996
  // the pages under `docs/` — into `generic`.
969
- detect: (context) => context.markdownCount >= 3 && !hasFile(context, "package.json"),
997
+ detect: (context) => markdownEvidence(context),
970
998
  layers: [
971
999
  layer("content", "content/", "docs/"),
972
1000
  layer("assets", "assets/", "static/", "public/"),
@@ -979,7 +1007,10 @@ var SHAPE_PRESETS = [
979
1007
  {
980
1008
  name: "generic",
981
1009
  priority: 0,
982
- detect: () => true,
1010
+ // No detector: this is the fallback, and `detectPresetFromContext` reaches it only
1011
+ // when every other preset reported no evidence. `fallback: true` is what lets the
1012
+ // wizard tell "I recognised this" apart from "I gave up".
1013
+ fallback: true,
983
1014
  layers: DEFAULT_LAYERS,
984
1015
  kinds: DEFAULT_KINDS,
985
1016
  exclude: [],
@@ -1011,20 +1042,27 @@ function detectPresetFromContext(context, forced) {
1011
1042
  if (forced !== void 0) {
1012
1043
  const preset = BY_NAME.get(forced);
1013
1044
  if (!preset) throw new Error(unknownPresetMessage(forced));
1014
- return toDetected(preset);
1045
+ return toDetected(preset, null);
1015
1046
  }
1016
1047
  for (const preset of PRESETS) {
1017
- if (preset.detect(context)) return toDetected(preset);
1048
+ const reason = preset.detect?.(context) ?? null;
1049
+ if (reason !== null) return toDetected(preset, reason);
1050
+ }
1051
+ const fallback = PRESETS.find((preset) => preset.fallback === true);
1052
+ if (fallback === void 0) {
1053
+ throw new Error("No fallback preset is defined. Exactly one preset must set `fallback: true`.");
1018
1054
  }
1019
- return toDetected(PRESETS[PRESETS.length - 1]);
1055
+ return toDetected(fallback, null, true);
1020
1056
  }
1021
- function toDetected(preset) {
1057
+ function toDetected(preset, reason, fallback = false) {
1022
1058
  return {
1023
1059
  presetName: preset.name,
1024
1060
  layers: preset.layers,
1025
1061
  kinds: preset.kinds,
1026
1062
  exclude: preset.exclude ? [...preset.exclude] : [],
1027
- recommendedBudget: preset.recommendedBudget ?? null
1063
+ recommendedBudget: preset.recommendedBudget ?? null,
1064
+ reason,
1065
+ fallback
1028
1066
  };
1029
1067
  }
1030
1068
  function detectPreset(cwd, forced) {
@@ -1470,8 +1508,8 @@ function readLinkTarget(linkPath, fallback) {
1470
1508
  }
1471
1509
  }
1472
1510
  function isInside(root, candidate) {
1473
- const relative8 = path2.relative(root, candidate);
1474
- return relative8 === "" || !relative8.startsWith(`..${path2.sep}`) && relative8 !== ".." && !path2.isAbsolute(relative8);
1511
+ const relative11 = path2.relative(root, candidate);
1512
+ return relative11 === "" || !relative11.startsWith(`..${path2.sep}`) && relative11 !== ".." && !path2.isAbsolute(relative11);
1475
1513
  }
1476
1514
  function getGitInfo(cwd, fallbackBranch = "main") {
1477
1515
  try {
@@ -2153,6 +2191,20 @@ var CONFIG_FILENAMES = [
2153
2191
  ".snapshotrc.json",
2154
2192
  ".snapshotrc"
2155
2193
  ];
2194
+ function findConfigFile(cwd) {
2195
+ for (const filename of CONFIG_FILENAMES) {
2196
+ const fullPath = path3.join(cwd, filename);
2197
+ if (fs3.existsSync(fullPath)) return fullPath;
2198
+ }
2199
+ const pkgJsonPath = path3.join(cwd, "package.json");
2200
+ if (!fs3.existsSync(pkgJsonPath)) return null;
2201
+ try {
2202
+ const parsed = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf8"));
2203
+ return parsed.snapshot === void 0 ? null : pkgJsonPath;
2204
+ } catch {
2205
+ return null;
2206
+ }
2207
+ }
2156
2208
  var RESOLVED_CONFIG_BRAND = /* @__PURE__ */ Symbol.for(
2157
2209
  "codebase-snapshot/resolved-config"
2158
2210
  );
@@ -2608,11 +2660,11 @@ function outputExcludePatterns(cwd, outDir, tmpDir) {
2608
2660
  const patterns = [];
2609
2661
  for (const dir of [outDir, tmpDir]) {
2610
2662
  const absolute = path3.resolve(cwd, dir);
2611
- const relative8 = path3.relative(cwd, absolute);
2612
- if (!relative8 || relative8 === ".." || relative8.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative8)) {
2663
+ const relative11 = path3.relative(cwd, absolute);
2664
+ if (!relative11 || relative11 === ".." || relative11.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative11)) {
2613
2665
  continue;
2614
2666
  }
2615
- patterns.push(`${relative8.replace(/\\/g, "/")}/**`);
2667
+ patterns.push(`${relative11.replace(/\\/g, "/")}/**`);
2616
2668
  }
2617
2669
  return patterns;
2618
2670
  }
@@ -4108,21 +4160,21 @@ function describeMatcher(matcher) {
4108
4160
  }
4109
4161
  function normalizeTarget(cwd, target) {
4110
4162
  const absolute = path8.resolve(cwd, target);
4111
- const relative8 = path8.relative(cwd, absolute);
4112
- if (relative8.startsWith("..") || path8.isAbsolute(relative8)) return target;
4113
- return relative8.split(path8.sep).join("/");
4163
+ const relative11 = path8.relative(cwd, absolute);
4164
+ if (relative11.startsWith("..") || path8.isAbsolute(relative11)) return target;
4165
+ return relative11.split(path8.sep).join("/");
4114
4166
  }
4115
4167
  function findRuleMatch(rules, relativePath) {
4116
4168
  return rules.find(
4117
4169
  (rule) => matchRule(relativePath, rule.match)
4118
4170
  );
4119
4171
  }
4120
- function findRecordedAncestor(skipped, relative8, maskedPath2) {
4172
+ function findRecordedAncestor(skipped, relative11, maskedPath2) {
4121
4173
  let nearest;
4122
4174
  for (const record of skipped) {
4123
4175
  const ancestorPath = record.path;
4124
4176
  if (ancestorPath.length === 0) continue;
4125
- const isAncestor = relative8.startsWith(`${ancestorPath}/`) || maskedPath2.startsWith(`${ancestorPath}/`);
4177
+ const isAncestor = relative11.startsWith(`${ancestorPath}/`) || maskedPath2.startsWith(`${ancestorPath}/`);
4126
4178
  if (!isAncestor) continue;
4127
4179
  if (!nearest || ancestorPath.length > nearest.path.length) nearest = record;
4128
4180
  }
@@ -4137,9 +4189,9 @@ function readManifest2(config) {
4137
4189
  }
4138
4190
  }
4139
4191
  function explainPath(config, target) {
4140
- const relative8 = normalizeTarget(config.cwd, target);
4141
- const absolute = path8.join(config.cwd, relative8);
4142
- const maskedPath2 = findSecretInPath(relative8, config.secretRules)?.masked ?? relative8;
4192
+ const relative11 = normalizeTarget(config.cwd, target);
4193
+ const absolute = path8.join(config.cwd, relative11);
4194
+ const maskedPath2 = findSecretInPath(relative11, config.secretRules)?.masked ?? relative11;
4143
4195
  const result = {
4144
4196
  path: maskedPath2,
4145
4197
  exists: fs7.existsSync(absolute),
@@ -4154,11 +4206,11 @@ function explainPath(config, target) {
4154
4206
  // that is only missing because the walker dropped the tool's own output directory.
4155
4207
  toolOwnedPaths: [config.outDir, config.tmpDir].map(
4156
4208
  (directory) => path8.relative(config.cwd, path8.resolve(config.cwd, directory)).replace(/\\/g, "/")
4157
- ).filter((relative9) => relative9.length > 0 && !relative9.startsWith(".."))
4209
+ ).filter((relative12) => relative12.length > 0 && !relative12.startsWith(".."))
4158
4210
  });
4159
- if (!collection.files.includes(relative8)) {
4211
+ if (!collection.files.includes(relative11)) {
4160
4212
  const record = collection.skipped.find(
4161
- (entry2) => entry2.path === relative8 || entry2.path === maskedPath2
4213
+ (entry2) => entry2.path === relative11 || entry2.path === maskedPath2
4162
4214
  );
4163
4215
  if (record) {
4164
4216
  result.reason = record.reason;
@@ -4167,7 +4219,7 @@ function explainPath(config, target) {
4167
4219
  result.reason = "missing";
4168
4220
  result.detail = "no such file or directory, relative to the configured cwd";
4169
4221
  } else {
4170
- const ancestor = findRecordedAncestor(collection.skipped, relative8, maskedPath2);
4222
+ const ancestor = findRecordedAncestor(collection.skipped, relative11, maskedPath2);
4171
4223
  if (ancestor) {
4172
4224
  result.reason = ancestor.reason;
4173
4225
  result.detail = ancestor.detail ? `${ancestor.detail} \u2014 inherited from the skipped directory \`${ancestor.path}\`` : `inside the skipped directory \`${ancestor.path}\``;
@@ -4185,12 +4237,12 @@ function explainPath(config, target) {
4185
4237
  result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
4186
4238
  return result;
4187
4239
  }
4188
- result.layer = classifyLayer(relative8, config.layers);
4189
- result.kind = classifyKind(relative8, config.kinds);
4190
- result.language = detectLanguage(relative8);
4191
- const layerRule = findRuleMatch(config.layers, relative8);
4240
+ result.layer = classifyLayer(relative11, config.layers);
4241
+ result.kind = classifyKind(relative11, config.kinds);
4242
+ result.language = detectLanguage(relative11);
4243
+ const layerRule = findRuleMatch(config.layers, relative11);
4192
4244
  if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
4193
- const kindRule = findRuleMatch(config.kinds, relative8);
4245
+ const kindRule = findRuleMatch(config.kinds, relative11);
4194
4246
  if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
4195
4247
  if (fs7.existsSync(absolute) && fs7.statSync(absolute).isFile()) {
4196
4248
  const buffer = fs7.readFileSync(absolute);
@@ -4205,12 +4257,12 @@ function explainPath(config, target) {
4205
4257
  result.bytes = buffer.length;
4206
4258
  result.lines = countLines(text2);
4207
4259
  if (decoded.encoding !== "utf-8") result.encoding = decoded.encoding;
4208
- const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative8);
4260
+ const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative11);
4209
4261
  result.secretCount = findings.length;
4210
4262
  result.secrets = findings.slice(0, 20).map((finding) => ({ rules: finding.rules, line: finding.line }));
4211
4263
  }
4212
4264
  const manifest = readManifest2(config);
4213
- const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative8) : void 0;
4265
+ const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative11) : void 0;
4214
4266
  result.part = entry?.part ?? null;
4215
4267
  result.inclusionMode = entry?.inclusionMode;
4216
4268
  return result;
@@ -4294,7 +4346,7 @@ function gitignorePath(cwd, filePath) {
4294
4346
 
4295
4347
  // src/version.ts
4296
4348
  var GENERATOR_NAME = "codebase-snapshot";
4297
- var GENERATOR_VERSION = "0.9.0";
4349
+ var GENERATOR_VERSION = "0.9.4";
4298
4350
  var LEGACY_GENERATOR_NAMES = ["@ankurhalder/codebase-snapshot"];
4299
4351
  function isOurGeneratorName(name) {
4300
4352
  if (typeof name !== "string") return false;
@@ -4506,7 +4558,7 @@ function keyReader(input) {
4506
4558
  next() {
4507
4559
  const queued = queue.shift();
4508
4560
  if (queued) return Promise.resolve(queued);
4509
- return new Promise((resolve6) => waiters.push(resolve6));
4561
+ return new Promise((resolve9) => waiters.push(resolve9));
4510
4562
  },
4511
4563
  close() {
4512
4564
  input.off("keypress", onKey);
@@ -4861,16 +4913,18 @@ function updatePackageJson(cwd, notes) {
4861
4913
  return { scripts: added, dependency };
4862
4914
  }
4863
4915
  var GITIGNORE_ENTRIES = ["snapshot/", "snapshot.tmp/"];
4864
- function resolvePresetChoice(answers, presetName) {
4916
+ function resolvePresetChoice(detectedName, presetName) {
4865
4917
  return {
4866
4918
  value: presetName,
4867
4919
  label: presetName,
4868
- hint: presetName === answers.preset ? "detected" : void 0
4920
+ hint: presetName === detectedName ? "detected" : void 0
4869
4921
  };
4870
4922
  }
4871
4923
  async function runInit(options) {
4872
4924
  const io = options.io ?? defaultIO();
4873
4925
  const detected = detectPreset(options.cwd, options.preset);
4926
+ let reason = detected.reason;
4927
+ let fallback = detected.fallback;
4874
4928
  const notes = [];
4875
4929
  const answers = {
4876
4930
  preset: detected.presetName,
@@ -4892,112 +4946,143 @@ async function runInit(options) {
4892
4946
  if (interactive) {
4893
4947
  io.output.write("\n codebase-snapshot\n");
4894
4948
  io.output.write(" Turn this repository into an LLM-ready, redacted, byte-bounded snapshot.\n\n");
4895
- io.output.write(` Detected ${detected.presetName}
4949
+ if (fallback) {
4950
+ io.output.write(" \u26A0 Could not tell what kind of project this is.\n");
4951
+ io.output.write(" No package.json, go.mod, Cargo.toml, pyproject.toml, Gemfile,\n");
4952
+ io.output.write(" composer.json, pom.xml, *.csproj, next.config.*, vite.config.*, *.tf\n");
4953
+ io.output.write(" or workspace marker was found here.\n\n");
4954
+ } else {
4955
+ io.output.write(` Detected ${detected.presetName}
4956
+ `);
4957
+ io.output.write(` Because ${reason}
4958
+
4896
4959
  `);
4960
+ }
4897
4961
  io.output.write(` Directory ${options.cwd}
4898
4962
 
4899
4963
  `);
4900
- const useDetected = await confirm(io, `Set up for ${detected.presetName}?`, {
4901
- defaultValue: true
4964
+ const paceChoices = fallback ? [
4965
+ { value: "choose", label: "Pick the closest preset", hint: "then generate with the defaults" },
4966
+ { value: "recommended", label: "Use the defaults", hint: "generic \xB7 everything the scanner finds" },
4967
+ { value: "walk", label: "Go through each setting", hint: "every question starts on its default" }
4968
+ ] : [
4969
+ {
4970
+ value: "recommended",
4971
+ label: "Use the recommended setup",
4972
+ hint: `${detected.presetName} \xB7 config \xB7 npm scripts \xB7 install \xB7 generate`
4973
+ },
4974
+ { value: "walk", label: "Go through each setting", hint: "every question starts on its default" },
4975
+ { value: "choose", label: "Pick a different preset", hint: "everything else stays at the defaults" }
4976
+ ];
4977
+ const pace = await select(io, "How should I set this up?", paceChoices, {
4978
+ defaultValue: fallback ? "choose" : "recommended"
4902
4979
  });
4903
- if (!useDetected) {
4980
+ if (pace === "choose") {
4904
4981
  answers.preset = await select(
4905
4982
  io,
4906
4983
  "What kind of project is this?",
4907
- listPresetNames().map((name) => resolvePresetChoice(answers, name))
4984
+ listPresetNames().map(
4985
+ (name) => resolvePresetChoice(fallback ? null : detected.presetName, name)
4986
+ ),
4987
+ { defaultValue: answers.preset }
4908
4988
  );
4909
4989
  const chosen = detectPreset(options.cwd, answers.preset);
4910
4990
  answers.preset = chosen.presetName;
4991
+ reason = null;
4992
+ fallback = false;
4911
4993
  }
4912
- const scope = await select(
4913
- io,
4914
- "What should go into the snapshot?",
4915
- [
4916
- { value: "all", label: "Everything the scanner finds", hint: "recommended \xB7 respects .gitignore" },
4917
- { value: "folders", label: "Only specific folders\u2026" },
4918
- { value: "no-tests", label: "Everything, but skip tests" },
4919
- { value: "custom", label: "Let me write glob patterns" }
4920
- ],
4921
- { defaultValue: "all" }
4922
- );
4923
- if (scope === "folders") {
4924
- const dirs = topLevelDirs(options.cwd).slice(0, 40);
4925
- if (dirs.length === 0) {
4926
- notes.push("No top-level directories found; the snapshot will include everything.");
4994
+ if (pace === "walk") {
4995
+ const scope = await select(
4996
+ io,
4997
+ "What should go into the snapshot?",
4998
+ [
4999
+ { value: "all", label: "Everything the scanner finds", hint: "recommended \xB7 respects .gitignore" },
5000
+ { value: "folders", label: "Only specific folders\u2026" },
5001
+ { value: "no-tests", label: "Everything, but skip tests" },
5002
+ { value: "custom", label: "Let me write glob patterns" }
5003
+ ],
5004
+ { defaultValue: "all" }
5005
+ );
5006
+ if (scope === "folders") {
5007
+ const dirs = topLevelDirs(options.cwd).slice(0, 40);
5008
+ if (dirs.length === 0) {
5009
+ notes.push("No top-level directories found; the snapshot will include everything.");
5010
+ } else {
5011
+ const chosen = await multiSelect(
5012
+ io,
5013
+ "Select folders",
5014
+ dirs.map((name) => ({ value: name, label: `${name}/` })),
5015
+ { defaults: dirs }
5016
+ );
5017
+ answers.include = chosen.map((name) => `${name}/**`);
5018
+ }
5019
+ } else if (scope === "no-tests") {
5020
+ answers.excludePatterns = [
5021
+ ...answers.excludePatterns,
5022
+ "**/*.test.*",
5023
+ "**/*.spec.*",
5024
+ "tests",
5025
+ "test"
5026
+ ];
5027
+ } else if (scope === "custom") {
5028
+ const raw = await text(io, "Glob patterns to include (comma separated)", {
5029
+ defaultValue: ""
5030
+ });
5031
+ answers.include = raw.split(",").map((value) => value.trim()).filter(Boolean);
5032
+ }
5033
+ const currentBudget = String(answers.maxPartBytes);
5034
+ const sizeChoice = await select(io, "How large should each part be?", SIZE_CHOICES, {
5035
+ defaultValue: SIZE_CHOICES.some((choice) => choice.value === currentBudget) ? currentBudget : "120k"
5036
+ });
5037
+ if (sizeChoice === "custom") {
5038
+ const raw = await text(io, "Part size (e.g. 150k, 1.5mb, 900000)", {
5039
+ defaultValue: String(answers.maxPartBytes),
5040
+ validate: (value) => parseByteSize(value) === void 0 ? "Not a valid size." : null
5041
+ });
5042
+ answers.maxPartBytes = parseByteSize(raw) ?? answers.maxPartBytes;
4927
5043
  } else {
4928
- const chosen = await multiSelect(
4929
- io,
4930
- "Select folders",
4931
- dirs.map((name) => ({ value: name, label: `${name}/` })),
4932
- { defaults: dirs }
4933
- );
4934
- answers.include = chosen.map((name) => `${name}/**`);
4935
- }
4936
- } else if (scope === "no-tests") {
4937
- answers.excludePatterns = [
4938
- ...answers.excludePatterns,
4939
- "**/*.test.*",
4940
- "**/*.spec.*",
4941
- "tests",
4942
- "test"
5044
+ answers.maxPartBytes = sizeChoice;
5045
+ }
5046
+ const artifactChoices = [
5047
+ { value: "readme", label: "README.md", hint: "orientation page" },
5048
+ { value: "manifest", label: "manifest.json", hint: "required by validate and report" },
5049
+ { value: "parts", label: "parts/", hint: "the code payload" },
5050
+ { value: "navigation", label: "reports/navigation.md", hint: "file \u2192 part index" },
5051
+ { value: "projectTree", label: "reports/project-tree.md", hint: "ASCII tree" },
5052
+ { value: "audit", label: "reports/audit.md", hint: "security and health findings" },
5053
+ { value: "monolith", label: "full-codebase.json + .md", hint: "roughly doubles output" }
4943
5054
  ];
4944
- } else if (scope === "custom") {
4945
- const raw = await text(io, "Glob patterns to include (comma separated)", {
4946
- defaultValue: ""
4947
- });
4948
- answers.include = raw.split(",").map((value) => value.trim()).filter(Boolean);
4949
- }
4950
- const sizeChoice = await select(io, "How large should each part be?", SIZE_CHOICES, {
4951
- defaultValue: "120k"
4952
- });
4953
- if (sizeChoice === "custom") {
4954
- const raw = await text(io, "Part size (e.g. 150k, 1.5mb, 900000)", {
4955
- defaultValue: String(answers.maxPartBytes),
4956
- validate: (value) => parseByteSize(value) === void 0 ? "Not a valid size." : null
4957
- });
4958
- answers.maxPartBytes = parseByteSize(raw) ?? answers.maxPartBytes;
4959
- } else {
4960
- answers.maxPartBytes = sizeChoice;
4961
- }
4962
- const artifactChoices = [
4963
- { value: "readme", label: "README.md", hint: "orientation page" },
4964
- { value: "manifest", label: "manifest.json", hint: "required by validate and report" },
4965
- { value: "parts", label: "parts/", hint: "the code payload" },
4966
- { value: "navigation", label: "reports/navigation.md", hint: "file \u2192 part index" },
4967
- { value: "projectTree", label: "reports/project-tree.md", hint: "ASCII tree" },
4968
- { value: "audit", label: "reports/audit.md", hint: "security and health findings" },
4969
- { value: "monolith", label: "full-codebase.json + .md", hint: "roughly doubles output" }
4970
- ];
4971
- const presetEnabled = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]);
4972
- const selectedArtifacts = await multiSelect(
4973
- io,
4974
- "Which artifacts should be generated?",
4975
- artifactChoices,
4976
- { defaults: presetEnabled }
4977
- );
4978
- for (const name of ARTIFACT_NAMES) answers.artifacts[name] = selectedArtifacts.includes(name);
4979
- const enabledCount = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]).length;
4980
- answers.includeSensitiveFiles = await confirm(
4981
- io,
4982
- "Include credential files (.env, *.pem, .npmrc, .ssh/)?",
4983
- { defaultValue: false }
4984
- );
4985
- io.output.write("\n Review\n\n");
4986
- io.output.write(` project ${answers.preset}
5055
+ const presetEnabled = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]);
5056
+ const selectedArtifacts = await multiSelect(
5057
+ io,
5058
+ "Which artifacts should be generated?",
5059
+ artifactChoices,
5060
+ { defaults: presetEnabled }
5061
+ );
5062
+ for (const name of ARTIFACT_NAMES) answers.artifacts[name] = selectedArtifacts.includes(name);
5063
+ const enabledCount = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]).length;
5064
+ answers.includeSensitiveFiles = await confirm(
5065
+ io,
5066
+ "Include credential files (.env, *.pem, .npmrc, .ssh/)?",
5067
+ { defaultValue: false }
5068
+ );
5069
+ io.output.write("\n Review\n\n");
5070
+ io.output.write(` project ${answers.preset}
4987
5071
  `);
4988
- io.output.write(` include ${answers.include.length ? answers.include.join(", ") : "everything"}
5072
+ io.output.write(` include ${answers.include.length ? answers.include.join(", ") : "everything"}
4989
5073
  `);
4990
- io.output.write(` part size ${String(answers.maxPartBytes)}
5074
+ io.output.write(` part size ${String(answers.maxPartBytes)}
4991
5075
  `);
4992
- io.output.write(` artifacts ${enabledCount} of ${ARTIFACT_NAMES.length}
5076
+ io.output.write(` artifacts ${enabledCount} of ${ARTIFACT_NAMES.length}
4993
5077
  `);
4994
- io.output.write(` credentials ${answers.includeSensitiveFiles ? "included" : "skipped"}
5078
+ io.output.write(` credentials ${answers.includeSensitiveFiles ? "included" : "skipped"}
4995
5079
 
4996
5080
  `);
4997
- io.output.write(" Will write snapshot.config with your answers, plus every other setting\n");
4998
- io.output.write(" at its default \u2014 so nothing is hidden behind the docs.\n\n");
4999
- const proceed = await confirm(io, "Write this and continue?", { defaultValue: true });
5000
- if (!proceed) throw new PromptCancelled();
5081
+ io.output.write(" Will write snapshot.config with your answers, plus every other setting\n");
5082
+ io.output.write(" at its default \u2014 so nothing is hidden behind the docs.\n\n");
5083
+ const proceed = await confirm(io, "Write this and continue?", { defaultValue: true });
5084
+ if (!proceed) throw new PromptCancelled();
5085
+ }
5001
5086
  }
5002
5087
  const configText = renderConfig(answers);
5003
5088
  const preferredName = chooseConfigFileName(options.cwd);
@@ -5021,6 +5106,8 @@ async function runInit(options) {
5021
5106
  written: false,
5022
5107
  reused,
5023
5108
  preset: answers.preset,
5109
+ reason,
5110
+ fallback,
5024
5111
  scripts: [],
5025
5112
  gitignoreEntries: [],
5026
5113
  dependency: { name: GENERATOR_NAME, range: `^${GENERATOR_VERSION}`, added: false },
@@ -5055,6 +5142,8 @@ async function runInit(options) {
5055
5142
  written: true,
5056
5143
  reused,
5057
5144
  preset: answers.preset,
5145
+ reason,
5146
+ fallback,
5058
5147
  scripts,
5059
5148
  gitignoreEntries: ignore.added,
5060
5149
  dependency,
@@ -5062,12 +5151,58 @@ async function runInit(options) {
5062
5151
  };
5063
5152
  }
5064
5153
 
5154
+ // src/wizard/autopilot.ts
5155
+ import * as fs10 from "fs";
5156
+ import * as path11 from "path";
5157
+ function describeTarget(cwd) {
5158
+ const relative11 = path11.relative(process.cwd(), cwd);
5159
+ if (relative11 === "") return "this directory";
5160
+ if (relative11.startsWith("..") || path11.isAbsolute(relative11)) return cwd;
5161
+ return relative11;
5162
+ }
5163
+ function firstRunMessage(reason, cwd) {
5164
+ const where = describeTarget(cwd);
5165
+ if (reason === "empty") {
5166
+ return `Nothing to snapshot: ${where} has no files. Run this inside the repository you want to snapshot, or pass --cwd.`;
5167
+ }
5168
+ return `No configuration found in ${where} and this session is not interactive, so nothing was generated.
5169
+ Choose one:
5170
+ ${GENERATOR_NAME} --autopilot detect the project, set it up and generate, no questions
5171
+ ${GENERATOR_NAME} init --yes write a config from the detected defaults, then generate
5172
+ ${GENERATOR_NAME} init write a config interactively`;
5173
+ }
5174
+ function planFirstRun(facts) {
5175
+ if (facts.emptyTarget) return { action: "refuse", exitCode: 1, reason: "empty" };
5176
+ if (facts.autopilot) return { action: "setup", interactive: false };
5177
+ if (facts.hasConfig || facts.hasPriorSnapshot) return { action: "generate" };
5178
+ if (facts.interactive) return { action: "setup", interactive: true };
5179
+ return { action: "refuse", exitCode: 2, reason: "undecided" };
5180
+ }
5181
+ var NOT_CONTENT = /* @__PURE__ */ new Set([".git", ".hg", ".svn", ".bzr", ".DS_Store", "Thumbs.db"]);
5182
+ function isEffectivelyEmpty(cwd) {
5183
+ try {
5184
+ return fs10.readdirSync(cwd).every((entry) => NOT_CONTENT.has(entry));
5185
+ } catch {
5186
+ return false;
5187
+ }
5188
+ }
5189
+ function hasPriorSnapshot(cwd, outDir) {
5190
+ let parsed;
5191
+ try {
5192
+ parsed = JSON.parse(fs10.readFileSync(path11.join(cwd, outDir, ".codebase-snapshot"), "utf8"));
5193
+ } catch {
5194
+ return false;
5195
+ }
5196
+ if (parsed === null || typeof parsed !== "object") return false;
5197
+ return isOurGeneratorName(parsed.generator);
5198
+ }
5199
+
5065
5200
  // src/index.ts
5066
5201
  import * as crypto3 from "crypto";
5067
5202
  import { AsyncLocalStorage } from "async_hooks";
5068
- import * as fs10 from "fs";
5203
+ import * as fs11 from "fs";
5069
5204
  import * as os from "os";
5070
- import * as path11 from "path";
5205
+ import * as path12 from "path";
5071
5206
 
5072
5207
  // src/ast/profiles/shared.ts
5073
5208
  var MAX_SUMMARY_CHARS = 1e6;
@@ -5291,9 +5426,9 @@ var JAVA_METHOD = /^\s+(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)
5291
5426
  var jvmProfile = {
5292
5427
  id: "jvm",
5293
5428
  extensions: [".java", ".kt", ".kts", ".scala"],
5294
- summarize(path14, content) {
5429
+ summarize(path18, content) {
5295
5430
  const summary = emptySummary();
5296
- const lower = path14.toLowerCase();
5431
+ const lower = path18.toLowerCase();
5297
5432
  const isJava = lower.endsWith(".java");
5298
5433
  const isKotlin = lower.endsWith(".kt") || lower.endsWith(".kts");
5299
5434
  eachLine(content, (line) => {
@@ -5749,16 +5884,16 @@ function summarizeTypeScriptSource(relativePath, content) {
5749
5884
  // src/ast/profiles/sfc.ts
5750
5885
  var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
5751
5886
  var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
5752
- function componentName(path14) {
5753
- const base = path14.split("/").pop() ?? path14;
5887
+ function componentName(path18) {
5888
+ const base = path18.split("/").pop() ?? path18;
5754
5889
  return base.replace(/\.[^.]+$/, "");
5755
5890
  }
5756
5891
  var sfcProfile = {
5757
5892
  id: "sfc",
5758
5893
  extensions: [".vue", ".svelte", ".astro"],
5759
- summarize(path14, content) {
5894
+ summarize(path18, content) {
5760
5895
  let script = "";
5761
- if (path14.toLowerCase().endsWith(".astro")) {
5896
+ if (path18.toLowerCase().endsWith(".astro")) {
5762
5897
  const frontmatter = ASTRO_FRONTMATTER.exec(content);
5763
5898
  if (frontmatter) script += `${frontmatter[1]}
5764
5899
  `;
@@ -5767,7 +5902,7 @@ var sfcProfile = {
5767
5902
  script += `${match[1]}
5768
5903
  `;
5769
5904
  }
5770
- const name = componentName(path14);
5905
+ const name = componentName(path18);
5771
5906
  if (!script.trim()) {
5772
5907
  const summary2 = emptySummary();
5773
5908
  if (name) {
@@ -5801,15 +5936,15 @@ for (const profile of LANGUAGE_PROFILES) {
5801
5936
  BY_EXTENSION.set(extension.toLowerCase(), profile);
5802
5937
  }
5803
5938
  }
5804
- function profileForPath(path14) {
5805
- const lower = path14.toLowerCase();
5939
+ function profileForPath(path18) {
5940
+ const lower = path18.toLowerCase();
5806
5941
  const dot = lower.lastIndexOf(".");
5807
5942
  if (dot === -1) return null;
5808
5943
  return BY_EXTENSION.get(lower.slice(dot)) ?? null;
5809
5944
  }
5810
- function summarizeWithProfile(path14, content) {
5811
- const profile = profileForPath(path14);
5812
- return profile ? profile.summarize(path14, content) : null;
5945
+ function summarizeWithProfile(path18, content) {
5946
+ const profile = profileForPath(path18);
5947
+ return profile ? profile.summarize(path18, content) : null;
5813
5948
  }
5814
5949
 
5815
5950
  // src/ast/structured.ts
@@ -6739,13 +6874,13 @@ function buildImportResolver(files) {
6739
6874
  const relativePath = file.relativePath;
6740
6875
  remember(byPath, relativePath, relativePath);
6741
6876
  remember(byPathFolded, relativePath.toLowerCase(), relativePath);
6742
- const extension = path11.posix.extname(relativePath);
6877
+ const extension = path12.posix.extname(relativePath);
6743
6878
  if (!SOURCE_EXTENSIONS.has(extension)) continue;
6744
6879
  const stem = relativePath.slice(0, -extension.length);
6745
6880
  remember(byStem, stem, relativePath);
6746
6881
  remember(byStemFolded, stem.toLowerCase(), relativePath);
6747
- if (path11.posix.basename(stem) === "index") {
6748
- const directory = path11.posix.dirname(stem);
6882
+ if (path12.posix.basename(stem) === "index") {
6883
+ const directory = path12.posix.dirname(stem);
6749
6884
  remember(byStem, directory, relativePath);
6750
6885
  remember(byStemFolded, directory.toLowerCase(), relativePath);
6751
6886
  }
@@ -6754,8 +6889,8 @@ function buildImportResolver(files) {
6754
6889
  }
6755
6890
  function resolveSpecifier(fromFile, specifier, resolver) {
6756
6891
  if (!RELATIVE_SPECIFIER.test(specifier)) return null;
6757
- const joined = path11.posix.normalize(
6758
- path11.posix.join(path11.posix.dirname(fromFile), specifier)
6892
+ const joined = path12.posix.normalize(
6893
+ path12.posix.join(path12.posix.dirname(fromFile), specifier)
6759
6894
  );
6760
6895
  const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
6761
6896
  const direct = exact(joined);
@@ -6848,11 +6983,11 @@ function verifyInMemory(files, parts, maxPartBytes) {
6848
6983
  return errors;
6849
6984
  }
6850
6985
  function resolveSafeOutputDirs(config) {
6851
- const cwd = path11.resolve(config.cwd);
6852
- const outDir = path11.resolve(cwd, config.outDir);
6853
- const tmpDir = path11.resolve(cwd, config.tmpDir);
6854
- const root = path11.parse(cwd).root;
6855
- const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path11.sep}`);
6986
+ const cwd = path12.resolve(config.cwd);
6987
+ const outDir = path12.resolve(cwd, config.outDir);
6988
+ const tmpDir = path12.resolve(cwd, config.tmpDir);
6989
+ const root = path12.parse(cwd).root;
6990
+ const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path12.sep}`);
6856
6991
  const reject = (reason) => {
6857
6992
  throw new Error(
6858
6993
  `Refusing to generate: ${reason}. outDir=${outDir} tmpDir=${tmpDir} cwd=${cwd}`
@@ -6908,14 +7043,14 @@ function listEntries(dir) {
6908
7043
  const walk = (absolute, prefix) => {
6909
7044
  let children;
6910
7045
  try {
6911
- children = fs10.readdirSync(absolute, { withFileTypes: true });
7046
+ children = fs11.readdirSync(absolute, { withFileTypes: true });
6912
7047
  } catch {
6913
7048
  return;
6914
7049
  }
6915
7050
  for (const child of children) {
6916
- const relative8 = prefix === "" ? child.name : `${prefix}/${child.name}`;
6917
- entries.push(relative8);
6918
- if (child.isDirectory()) walk(path11.join(absolute, child.name), relative8);
7051
+ const relative11 = prefix === "" ? child.name : `${prefix}/${child.name}`;
7052
+ entries.push(relative11);
7053
+ if (child.isDirectory()) walk(path12.join(absolute, child.name), relative11);
6919
7054
  }
6920
7055
  };
6921
7056
  walk(dir, "");
@@ -6932,28 +7067,28 @@ function markerPayload(dir, state, id, createdAt) {
6932
7067
  pid: process.pid,
6933
7068
  entries: listEntries(dir)
6934
7069
  };
6935
- const manifestPath = path11.join(dir, "manifest.json");
6936
- if (fs10.existsSync(manifestPath)) {
6937
- marker2.manifestSha256 = sha256Hex2(fs10.readFileSync(manifestPath));
7070
+ const manifestPath = path12.join(dir, "manifest.json");
7071
+ if (fs11.existsSync(manifestPath)) {
7072
+ marker2.manifestSha256 = sha256Hex2(fs11.readFileSync(manifestPath));
6938
7073
  }
6939
7074
  return marker2;
6940
7075
  }
6941
7076
  function writeOwnershipMarker(dir, marker2) {
6942
- fs10.writeFileSync(
6943
- path11.join(dir, OWNERSHIP_MARKER),
7077
+ fs11.writeFileSync(
7078
+ path12.join(dir, OWNERSHIP_MARKER),
6944
7079
  `${JSON.stringify(marker2, null, 2)}
6945
7080
  `,
6946
7081
  "utf8"
6947
7082
  );
6948
7083
  }
6949
7084
  function readOwnershipMarker(dir) {
6950
- const markerPath = path11.join(dir, OWNERSHIP_MARKER);
6951
- if (!fs10.existsSync(markerPath)) {
7085
+ const markerPath = path12.join(dir, OWNERSHIP_MARKER);
7086
+ if (!fs11.existsSync(markerPath)) {
6952
7087
  return { marker: null, problem: "it holds no .codebase-snapshot marker" };
6953
7088
  }
6954
7089
  let raw;
6955
7090
  try {
6956
- raw = fs10.readFileSync(markerPath, "utf8");
7091
+ raw = fs11.readFileSync(markerPath, "utf8");
6957
7092
  } catch {
6958
7093
  return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
6959
7094
  }
@@ -7021,11 +7156,11 @@ function assessSnapshotDirectory(dir) {
7021
7156
  marker: marker2
7022
7157
  };
7023
7158
  }
7024
- const manifestPath = path11.join(dir, "manifest.json");
7025
- if (fs10.existsSync(manifestPath)) {
7159
+ const manifestPath = path12.join(dir, "manifest.json");
7160
+ if (fs11.existsSync(manifestPath)) {
7026
7161
  let raw;
7027
7162
  try {
7028
- raw = fs10.readFileSync(manifestPath, "utf8");
7163
+ raw = fs11.readFileSync(manifestPath, "utf8");
7029
7164
  } catch {
7030
7165
  return {
7031
7166
  owned: false,
@@ -7107,13 +7242,13 @@ function containsNoFiles(dir) {
7107
7242
  const current = pending.pop();
7108
7243
  let entries;
7109
7244
  try {
7110
- entries = fs10.readdirSync(current, { withFileTypes: true });
7245
+ entries = fs11.readdirSync(current, { withFileTypes: true });
7111
7246
  } catch {
7112
7247
  return false;
7113
7248
  }
7114
7249
  for (const entry of entries) {
7115
7250
  if (entry.isDirectory()) {
7116
- pending.push(path11.join(current, entry.name));
7251
+ pending.push(path12.join(current, entry.name));
7117
7252
  } else if (!IGNORABLE_ENTRIES.has(entry.name)) {
7118
7253
  return false;
7119
7254
  }
@@ -7122,13 +7257,13 @@ function containsNoFiles(dir) {
7122
7257
  return true;
7123
7258
  }
7124
7259
  function assertReplaceable(target, label, secretRules) {
7125
- if (!fs10.existsSync(target)) return;
7126
- if (!fs10.statSync(target).isDirectory()) {
7260
+ if (!fs11.existsSync(target)) return;
7261
+ if (!fs11.statSync(target).isDirectory()) {
7127
7262
  throw new Error(
7128
7263
  `Refusing to write into ${label} ${target}: it exists but is not a directory.`
7129
7264
  );
7130
7265
  }
7131
- const entries = fs10.readdirSync(target);
7266
+ const entries = fs11.readdirSync(target);
7132
7267
  if (entries.length === 0) return;
7133
7268
  if (entries.every((name) => IGNORABLE_ENTRIES.has(name))) return;
7134
7269
  if (containsNoFiles(target)) return;
@@ -7148,18 +7283,18 @@ function assertReplaceable(target, label, secretRules) {
7148
7283
  );
7149
7284
  }
7150
7285
  function assertStagingRootUsable(tmpDir, secretRules) {
7151
- if (!fs10.existsSync(tmpDir)) return;
7152
- if (!fs10.statSync(tmpDir).isDirectory()) {
7286
+ if (!fs11.existsSync(tmpDir)) return;
7287
+ if (!fs11.statSync(tmpDir).isDirectory()) {
7153
7288
  throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
7154
7289
  }
7155
- const entries = fs10.readdirSync(tmpDir);
7290
+ const entries = fs11.readdirSync(tmpDir);
7156
7291
  if (entries.length === 0) return;
7157
7292
  const foreign = entries.filter((name) => {
7158
- const full = path11.join(tmpDir, name);
7293
+ const full = path12.join(tmpDir, name);
7159
7294
  if (!name.startsWith(RUN_DIR_PREFIX)) return true;
7160
7295
  let stats;
7161
7296
  try {
7162
- stats = fs10.statSync(full);
7297
+ stats = fs11.statSync(full);
7163
7298
  } catch {
7164
7299
  return true;
7165
7300
  }
@@ -7167,7 +7302,7 @@ function assertStagingRootUsable(tmpDir, secretRules) {
7167
7302
  const { marker: marker2 } = readOwnershipMarker(full);
7168
7303
  if (marker2) return false;
7169
7304
  try {
7170
- return fs10.readdirSync(full).length > 0;
7305
+ return fs11.readdirSync(full).length > 0;
7171
7306
  } catch {
7172
7307
  return true;
7173
7308
  }
@@ -7182,13 +7317,13 @@ function assertStagingRootUsable(tmpDir, secretRules) {
7182
7317
  function cleanStaleRunDirs(tmpDir) {
7183
7318
  let entries;
7184
7319
  try {
7185
- entries = fs10.readdirSync(tmpDir);
7320
+ entries = fs11.readdirSync(tmpDir);
7186
7321
  } catch {
7187
7322
  return;
7188
7323
  }
7189
7324
  for (const name of entries) {
7190
7325
  if (!name.startsWith(RUN_DIR_PREFIX)) continue;
7191
- const full = path11.join(tmpDir, name);
7326
+ const full = path12.join(tmpDir, name);
7192
7327
  const { marker: marker2 } = readOwnershipMarker(full);
7193
7328
  if (marker2) {
7194
7329
  const created = Date.parse(marker2.createdAt);
@@ -7196,33 +7331,33 @@ function cleanStaleRunDirs(tmpDir) {
7196
7331
  const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
7197
7332
  if (aged || abandoned) {
7198
7333
  try {
7199
- fs10.rmSync(full, { recursive: true, force: true });
7334
+ fs11.rmSync(full, { recursive: true, force: true });
7200
7335
  } catch {
7201
7336
  }
7202
7337
  }
7203
7338
  continue;
7204
7339
  }
7205
7340
  try {
7206
- if (fs10.readdirSync(full).length === 0) fs10.rmSync(full, { recursive: true, force: true });
7341
+ if (fs11.readdirSync(full).length === 0) fs11.rmSync(full, { recursive: true, force: true });
7207
7342
  } catch {
7208
7343
  }
7209
7344
  }
7210
7345
  }
7211
7346
  function removeIfEmpty(dir) {
7212
7347
  try {
7213
- if (fs10.readdirSync(dir).length === 0) fs10.rmSync(dir, { recursive: true, force: true });
7348
+ if (fs11.readdirSync(dir).length === 0) fs11.rmSync(dir, { recursive: true, force: true });
7214
7349
  } catch {
7215
7350
  }
7216
7351
  }
7217
7352
  function ensureParentDirectory(target, label) {
7218
- const parent = path11.dirname(target);
7219
- if (fs10.existsSync(parent) && !fs10.statSync(parent).isDirectory()) {
7353
+ const parent = path12.dirname(target);
7354
+ if (fs11.existsSync(parent) && !fs11.statSync(parent).isDirectory()) {
7220
7355
  throw new Error(
7221
7356
  `Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
7222
7357
  );
7223
7358
  }
7224
7359
  try {
7225
- fs10.mkdirSync(parent, { recursive: true });
7360
+ fs11.mkdirSync(parent, { recursive: true });
7226
7361
  } catch (err) {
7227
7362
  throw new Error(
7228
7363
  `Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
@@ -7231,7 +7366,7 @@ function ensureParentDirectory(target, label) {
7231
7366
  }
7232
7367
  function acquireRunLock(stagingRoot) {
7233
7368
  const key = lockKeyFor(stagingRoot);
7234
- const lockPath = path11.join(os.tmpdir(), `codebase-snapshot-${key}.lock`);
7369
+ const lockPath = path12.join(os.tmpdir(), `codebase-snapshot-${key}.lock`);
7235
7370
  const payload = `${JSON.stringify({
7236
7371
  pid: process.pid,
7237
7372
  stagingRoot,
@@ -7240,22 +7375,22 @@ function acquireRunLock(stagingRoot) {
7240
7375
  `;
7241
7376
  for (let attempt = 0; attempt < 2; attempt += 1) {
7242
7377
  try {
7243
- const handle = fs10.openSync(lockPath, "wx");
7378
+ const handle = fs11.openSync(lockPath, "wx");
7244
7379
  try {
7245
- fs10.writeSync(handle, payload);
7380
+ fs11.writeSync(handle, payload);
7246
7381
  } finally {
7247
- fs10.closeSync(handle);
7382
+ fs11.closeSync(handle);
7248
7383
  }
7249
7384
  return () => {
7250
7385
  try {
7251
- fs10.rmSync(lockPath, { force: true });
7386
+ fs11.rmSync(lockPath, { force: true });
7252
7387
  } catch {
7253
7388
  }
7254
7389
  };
7255
7390
  } catch (err) {
7256
7391
  if (err.code !== "EEXIST") throw err;
7257
7392
  if (attempt === 0 && lockOwnerIsGone(lockPath)) {
7258
- fs10.rmSync(lockPath, { force: true });
7393
+ fs11.rmSync(lockPath, { force: true });
7259
7394
  continue;
7260
7395
  }
7261
7396
  throw new Error(
@@ -7268,7 +7403,7 @@ function acquireRunLock(stagingRoot) {
7268
7403
  function lockKeyFor(stagingRoot) {
7269
7404
  let key = stagingRoot;
7270
7405
  try {
7271
- key = fs10.realpathSync(stagingRoot);
7406
+ key = fs11.realpathSync(stagingRoot);
7272
7407
  } catch {
7273
7408
  }
7274
7409
  if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
@@ -7277,7 +7412,7 @@ function lockKeyFor(stagingRoot) {
7277
7412
  function lockOwnerIsGone(lockPath) {
7278
7413
  let raw;
7279
7414
  try {
7280
- raw = fs10.readFileSync(lockPath, "utf8");
7415
+ raw = fs11.readFileSync(lockPath, "utf8");
7281
7416
  } catch {
7282
7417
  return false;
7283
7418
  }
@@ -7297,12 +7432,12 @@ function lockOwnerIsGone(lockPath) {
7297
7432
  }
7298
7433
  function writeManifestAtomically(manifestPath, manifest) {
7299
7434
  const tempPath = `${manifestPath}.${process.pid}.tmp`;
7300
- fs10.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
7435
+ fs11.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
7301
7436
  try {
7302
- fs10.renameSync(tempPath, manifestPath);
7437
+ fs11.renameSync(tempPath, manifestPath);
7303
7438
  } catch (err) {
7304
7439
  try {
7305
- fs10.rmSync(tempPath, { force: true });
7440
+ fs11.rmSync(tempPath, { force: true });
7306
7441
  } catch {
7307
7442
  }
7308
7443
  throw err;
@@ -7325,31 +7460,31 @@ function swapIntoPlace(stagingDir, outDir) {
7325
7460
  }
7326
7461
  function swapOnce(stagingDir, outDir) {
7327
7462
  const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
7328
- const hadPrevious = fs10.existsSync(outDir);
7329
- if (hadPrevious) fs10.renameSync(outDir, backupDir);
7463
+ const hadPrevious = fs11.existsSync(outDir);
7464
+ if (hadPrevious) fs11.renameSync(outDir, backupDir);
7330
7465
  try {
7331
7466
  try {
7332
- fs10.renameSync(stagingDir, outDir);
7467
+ fs11.renameSync(stagingDir, outDir);
7333
7468
  } catch (err) {
7334
7469
  if (err.code !== "EXDEV") throw err;
7335
7470
  const partialDir = `${outDir}.partial-${process.pid}-${Date.now()}`;
7336
7471
  try {
7337
- fs10.cpSync(stagingDir, partialDir, { recursive: true });
7338
- fs10.renameSync(partialDir, outDir);
7339
- fs10.rmSync(stagingDir, { recursive: true, force: true });
7472
+ fs11.cpSync(stagingDir, partialDir, { recursive: true });
7473
+ fs11.renameSync(partialDir, outDir);
7474
+ fs11.rmSync(stagingDir, { recursive: true, force: true });
7340
7475
  } catch (copyErr) {
7341
7476
  try {
7342
- fs10.rmSync(partialDir, { recursive: true, force: true });
7477
+ fs11.rmSync(partialDir, { recursive: true, force: true });
7343
7478
  } catch {
7344
7479
  }
7345
7480
  throw copyErr;
7346
7481
  }
7347
7482
  }
7348
- if (hadPrevious) fs10.rmSync(backupDir, { recursive: true, force: true });
7483
+ if (hadPrevious) fs11.rmSync(backupDir, { recursive: true, force: true });
7349
7484
  } catch (err) {
7350
- if (hadPrevious && fs10.existsSync(backupDir)) {
7351
- if (fs10.existsSync(outDir)) fs10.rmSync(outDir, { recursive: true, force: true });
7352
- fs10.renameSync(backupDir, outDir);
7485
+ if (hadPrevious && fs11.existsSync(backupDir)) {
7486
+ if (fs11.existsSync(outDir)) fs11.rmSync(outDir, { recursive: true, force: true });
7487
+ fs11.renameSync(backupDir, outDir);
7353
7488
  }
7354
7489
  throw err;
7355
7490
  }
@@ -7444,8 +7579,8 @@ function generateSnapshot(configInput, options) {
7444
7579
  // first run cannot have marked them yet, so the name alone keeps a run from recording its own
7445
7580
  // output — but only at these exact paths, not at every directory that shares the name.
7446
7581
  toolOwnedPaths: [config.outDir, config.tmpDir].map(
7447
- (directory) => path11.relative(config.cwd, path11.resolve(config.cwd, directory)).replace(/\\/g, "/")
7448
- ).filter((relative8) => relative8.length > 0 && !relative8.startsWith(".."))
7582
+ (directory) => path12.relative(config.cwd, path12.resolve(config.cwd, directory)).replace(/\\/g, "/")
7583
+ ).filter((relative11) => relative11.length > 0 && !relative11.startsWith(".."))
7449
7584
  });
7450
7585
  const files = [];
7451
7586
  const monolithBodies = /* @__PURE__ */ new Map();
@@ -7465,12 +7600,12 @@ function generateSnapshot(configInput, options) {
7465
7600
  let totalSizeBytes = 0;
7466
7601
  let largeFileCount = 0;
7467
7602
  for (const relativePath of collection.files) {
7468
- const absolutePath = path11.join(config.cwd, relativePath);
7603
+ const absolutePath = path12.join(config.cwd, relativePath);
7469
7604
  let stats;
7470
7605
  try {
7471
- stats = fs10.statSync(absolutePath);
7606
+ stats = fs11.statSync(absolutePath);
7472
7607
  if (!stats.isFile()) {
7473
- const nestedRepository = stats.isDirectory() && fs10.existsSync(path11.join(absolutePath, ".git"));
7608
+ const nestedRepository = stats.isDirectory() && fs11.existsSync(path12.join(absolutePath, ".git"));
7474
7609
  skipped.push(
7475
7610
  nestedRepository ? {
7476
7611
  path: relativePath,
@@ -7490,7 +7625,7 @@ function generateSnapshot(configInput, options) {
7490
7625
  }
7491
7626
  let buffer;
7492
7627
  try {
7493
- buffer = fs10.readFileSync(absolutePath);
7628
+ buffer = fs11.readFileSync(absolutePath);
7494
7629
  } catch {
7495
7630
  skipped.push({ path: relativePath, reason: "read-error" });
7496
7631
  continue;
@@ -7820,12 +7955,12 @@ function generateSnapshot(configInput, options) {
7820
7955
  let stagingDir = null;
7821
7956
  try {
7822
7957
  ensureParentDirectory(outDir, "outDir");
7823
- fs10.mkdirSync(tmpDir, { recursive: true });
7958
+ fs11.mkdirSync(tmpDir, { recursive: true });
7824
7959
  cleanStaleRunDirs(tmpDir);
7825
7960
  const snapshotId = crypto3.randomUUID();
7826
7961
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
7827
- stagingDir = path11.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
7828
- fs10.mkdirSync(stagingDir);
7962
+ stagingDir = path12.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
7963
+ fs11.mkdirSync(stagingDir);
7829
7964
  writeOwnershipMarker(stagingDir, {
7830
7965
  generator: GENERATOR_NAME,
7831
7966
  version: GENERATOR_VERSION,
@@ -7835,41 +7970,41 @@ function generateSnapshot(configInput, options) {
7835
7970
  id: snapshotId,
7836
7971
  pid: process.pid
7837
7972
  });
7838
- const partsDir = path11.join(stagingDir, "parts");
7839
- const reportsDir = path11.join(stagingDir, "reports");
7840
- if (artifacts.parts) fs10.mkdirSync(partsDir, { recursive: true });
7973
+ const partsDir = path12.join(stagingDir, "parts");
7974
+ const reportsDir = path12.join(stagingDir, "reports");
7975
+ if (artifacts.parts) fs11.mkdirSync(partsDir, { recursive: true });
7841
7976
  if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
7842
- fs10.mkdirSync(reportsDir, { recursive: true });
7977
+ fs11.mkdirSync(reportsDir, { recursive: true });
7843
7978
  }
7844
7979
  if (artifacts.parts) {
7845
7980
  for (const part of parts) {
7846
- fs10.writeFileSync(path11.join(partsDir, part.filename), part.markdown, "utf8");
7981
+ fs11.writeFileSync(path12.join(partsDir, part.filename), part.markdown, "utf8");
7847
7982
  }
7848
7983
  }
7849
7984
  if (projectTree !== null) {
7850
- fs10.writeFileSync(path11.join(reportsDir, "project-tree.md"), projectTree, "utf8");
7985
+ fs11.writeFileSync(path12.join(reportsDir, "project-tree.md"), projectTree, "utf8");
7851
7986
  }
7852
7987
  if (artifacts.navigation) {
7853
- fs10.writeFileSync(path11.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
7988
+ fs11.writeFileSync(path12.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
7854
7989
  }
7855
7990
  if (artifacts.audit) {
7856
- fs10.writeFileSync(
7857
- path11.join(reportsDir, "audit.md"),
7991
+ fs11.writeFileSync(
7992
+ path12.join(reportsDir, "audit.md"),
7858
7993
  generateAuditReport(manifest, secretFindings, config.secretRules),
7859
7994
  "utf8"
7860
7995
  );
7861
7996
  }
7862
7997
  if (artifacts.readme) {
7863
- fs10.writeFileSync(path11.join(stagingDir, "README.md"), generateReadme(manifest, artifacts), "utf8");
7998
+ fs11.writeFileSync(path12.join(stagingDir, "README.md"), generateReadme(manifest, artifacts), "utf8");
7864
7999
  }
7865
8000
  if (artifacts.manifest) {
7866
- fs10.writeFileSync(path11.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
8001
+ fs11.writeFileSync(path12.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
7867
8002
  }
7868
8003
  if (monolithJson !== null) {
7869
- fs10.writeFileSync(path11.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
8004
+ fs11.writeFileSync(path12.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
7870
8005
  }
7871
8006
  if (monolithMarkdown !== null) {
7872
- fs10.writeFileSync(path11.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
8007
+ fs11.writeFileSync(path12.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
7873
8008
  }
7874
8009
  writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
7875
8010
  swapIntoPlace(stagingDir, outDir);
@@ -7878,7 +8013,7 @@ function generateSnapshot(configInput, options) {
7878
8013
  const pendingHook = runAfterGenerate(config, manifest);
7879
8014
  const shipHookState = () => {
7880
8015
  if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
7881
- writeManifestAtomically(path11.join(outDir, "manifest.json"), manifest);
8016
+ writeManifestAtomically(path12.join(outDir, "manifest.json"), manifest);
7882
8017
  }
7883
8018
  writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
7884
8019
  };
@@ -7901,7 +8036,7 @@ function generateSnapshot(configInput, options) {
7901
8036
  } catch (err) {
7902
8037
  if (stagingDir !== null) {
7903
8038
  try {
7904
- fs10.rmSync(stagingDir, { recursive: true, force: true });
8039
+ fs11.rmSync(stagingDir, { recursive: true, force: true });
7905
8040
  } catch {
7906
8041
  }
7907
8042
  }
@@ -7912,177 +8047,1178 @@ function generateSnapshot(configInput, options) {
7912
8047
  }
7913
8048
  }
7914
8049
 
7915
- // src/agent/option-scope.ts
7916
- var VALIDATE_OPTIONS = [
7917
- "cwd",
7918
- "out-dir",
7919
- "max-part-bytes",
7920
- "large-file-bytes",
7921
- "max-monolith-bytes",
7922
- "include-monolith",
7923
- "include-sensitive-files",
7924
- "skip-artifacts",
7925
- "only-artifacts",
7926
- "no-config",
7927
- "discovery",
7928
- "format",
7929
- "mode",
7930
- "help",
7931
- "version"
7932
- ];
7933
- var COMMAND_OPTION_SCOPE = {
7934
- generate: [
7935
- "cwd",
7936
- "out-dir",
7937
- "max-part-bytes",
7938
- "large-file-bytes",
7939
- "max-monolith-bytes",
7940
- "include-monolith",
7941
- "include-sensitive-files",
7942
- "skip-artifacts",
7943
- "only-artifacts",
7944
- "no-config",
7945
- "discovery",
7946
- "format",
7947
- "mode",
7948
- "dry-run",
7949
- "watch",
7950
- "help",
7951
- "version"
7952
- ],
7953
- validate: VALIDATE_OPTIONS,
7954
- report: [
7955
- "cwd",
7956
- "out-dir",
7957
- "max-part-bytes",
7958
- "large-file-bytes",
7959
- "max-monolith-bytes",
7960
- "include-monolith",
7961
- "include-sensitive-files",
7962
- "skip-artifacts",
7963
- "only-artifacts",
7964
- "no-config",
7965
- "format",
7966
- "mode",
7967
- "help",
7968
- "version"
7969
- ],
7970
- explain: [
7971
- "cwd",
7972
- "out-dir",
7973
- "max-part-bytes",
7974
- "large-file-bytes",
7975
- "max-monolith-bytes",
7976
- "include-monolith",
7977
- "include-sensitive-files",
7978
- "skip-artifacts",
7979
- "only-artifacts",
7980
- "no-config",
7981
- "discovery",
7982
- "format",
7983
- "mode",
7984
- "help",
7985
- "version"
7986
- ],
7987
- diff: ["format", "mode", "no-config", "help", "version"],
7988
- describe: ["format", "mode", "help", "version"],
7989
- init: [
7990
- "cwd",
7991
- "out-dir",
7992
- "max-part-bytes",
7993
- "include-sensitive-files",
7994
- "skip-artifacts",
7995
- "only-artifacts",
7996
- "preset",
7997
- "include",
7998
- "exclude",
7999
- "yes",
8000
- "force",
8001
- "dry-run",
8002
- "format",
8003
- "mode",
8004
- "help",
8005
- "version"
8006
- ],
8007
- skill: ["cwd", "format", "mode", "install", "target", "help", "version"],
8008
- setup: [
8009
- "cwd",
8010
- "out-dir",
8011
- "max-part-bytes",
8012
- "include-sensitive-files",
8013
- "skip-artifacts",
8014
- "only-artifacts",
8015
- "preset",
8016
- "include",
8017
- "exclude",
8018
- "yes",
8019
- "force",
8020
- "no-install",
8021
- "dry-run",
8022
- "format",
8023
- "mode",
8024
- "help",
8025
- "version"
8026
- ]
8027
- };
8028
- var COMMAND_ORDER = [
8029
- "generate",
8030
- "validate",
8031
- "report",
8032
- "explain",
8033
- "init",
8034
- "setup",
8035
- "diff",
8036
- "describe",
8037
- "skill"
8038
- ];
8039
- function commandsForOption(option) {
8040
- return COMMAND_ORDER.filter((command) => COMMAND_OPTION_SCOPE[command].includes(option));
8050
+ // src/mcp/tools.ts
8051
+ import * as fs13 from "fs";
8052
+ import * as path14 from "path";
8053
+
8054
+ // src/mcp/part.ts
8055
+ var FILE_HEADING = "### FILE: ";
8056
+ var CONTENT_MARKERS = /* @__PURE__ */ new Set(["#### Content", "#### Bounded Preview"]);
8057
+ function parsePartSections(partText) {
8058
+ const lines = partText.split("\n");
8059
+ const sections = [];
8060
+ let index = 0;
8061
+ while (index < lines.length) {
8062
+ if (!lines[index].startsWith(FILE_HEADING)) {
8063
+ index += 1;
8064
+ continue;
8065
+ }
8066
+ const heading = lines[index].slice(FILE_HEADING.length);
8067
+ let cursor = index + 1;
8068
+ let inclusion = null;
8069
+ let sha256 = null;
8070
+ let includedLines = null;
8071
+ while (cursor < lines.length && !CONTENT_MARKERS.has(lines[cursor])) {
8072
+ const line = lines[cursor];
8073
+ if (line.startsWith(FILE_HEADING)) break;
8074
+ if (line.startsWith("- Inclusion: ")) inclusion = line.slice("- Inclusion: ".length).trim();
8075
+ else if (line.startsWith("- SHA-256: ")) sha256 = line.slice("- SHA-256: ".length).trim();
8076
+ else if (line.startsWith("- Included Lines: ")) {
8077
+ const value = Number(line.slice("- Included Lines: ".length).trim());
8078
+ includedLines = Number.isFinite(value) ? value : null;
8079
+ }
8080
+ cursor += 1;
8081
+ }
8082
+ if (cursor >= lines.length || !CONTENT_MARKERS.has(lines[cursor])) {
8083
+ index = cursor + 1;
8084
+ continue;
8085
+ }
8086
+ let fenceLine = cursor + 1;
8087
+ while (fenceLine < lines.length && lines[fenceLine].trim().length === 0) fenceLine += 1;
8088
+ const fenceMatch = /^(`{4,})/.exec(lines[fenceLine] ?? "");
8089
+ if (!fenceMatch) {
8090
+ index = cursor + 1;
8091
+ continue;
8092
+ }
8093
+ const fence = fenceMatch[1];
8094
+ const body = [];
8095
+ let bodyLine = fenceLine + 1;
8096
+ while (bodyLine < lines.length && lines[bodyLine] !== fence) {
8097
+ body.push(lines[bodyLine]);
8098
+ bodyLine += 1;
8099
+ }
8100
+ sections.push({ path: heading, body: body.join("\n"), inclusion, sha256, includedLines });
8101
+ index = bodyLine + 1;
8102
+ }
8103
+ return sections;
8104
+ }
8105
+ function findSection(sections, relativePath, sha256) {
8106
+ const exact = sections.find((section) => section.path === relativePath);
8107
+ if (exact) return exact;
8108
+ if (sha256) {
8109
+ const byHash = sections.filter((section) => section.sha256 === sha256);
8110
+ if (byHash.length === 1) return byHash[0];
8111
+ if (byHash.length > 1) {
8112
+ const byPath = byHash.find(
8113
+ (section) => section.path.toLowerCase() === relativePath.toLowerCase()
8114
+ );
8115
+ if (byPath) return byPath;
8116
+ }
8117
+ }
8118
+ const folded = sections.filter((section) => section.path.toLowerCase() === relativePath.toLowerCase());
8119
+ return folded.length === 1 ? folded[0] : null;
8041
8120
  }
8042
- function optionAppliesTo(option) {
8043
- return commandsForOption(option).join("/");
8121
+ function windowText(body, fromLine, lineCount) {
8122
+ const lines = body.split("\n");
8123
+ const total = lines.length;
8124
+ const start = Math.min(Math.max(1, Math.trunc(fromLine)), Math.max(1, total));
8125
+ const end = Math.min(total, start + Math.max(1, Math.trunc(lineCount)) - 1);
8126
+ const slice = lines.slice(start - 1, end);
8127
+ return {
8128
+ text: slice.join("\n"),
8129
+ fromLine: start,
8130
+ returnedLines: slice.length,
8131
+ totalLines: total,
8132
+ truncated: end < total,
8133
+ nextFromLine: end < total ? end + 1 : null
8134
+ };
8044
8135
  }
8045
8136
 
8046
- // src/agent/contract.ts
8047
- var CONTRACT_VERSION = "1.0.0";
8048
- var PACKAGE_NAME = "codebase-snapshot";
8049
- var MAJOR_VERSION = GENERATOR_VERSION.split(".")[0];
8050
- var ARTIFACT_SUMMARY = {
8051
- readme: "README.md describing the snapshot at the output root.",
8052
- manifest: "manifest.json: machine-readable index of every file, part, hash and config.",
8053
- parts: "parts/*.md: the packed, budget-limited markdown parts that carry the code.",
8054
- navigation: "reports/navigation.md: links between parts and layers.",
8055
- projectTree: "reports/project-tree.md: the directory tree of included files.",
8056
- audit: "reports/audit.md: secret findings, large files, parse diagnostics, git state.",
8057
- monolith: "full-codebase.json and full-codebase.md: the whole repository in one file (off by default)."
8058
- };
8059
- var ARTIFACT_DEFAULTS = {
8060
- readme: true,
8061
- manifest: true,
8062
- parts: true,
8063
- navigation: true,
8064
- projectTree: true,
8065
- audit: true,
8066
- monolith: false
8067
- };
8068
- function renderContractText(contract) {
8069
- const lines = [];
8070
- lines.push(`${contract.package} v${contract.version}`);
8071
- lines.push(contract.summary);
8072
- lines.push("");
8073
- lines.push(`Node : ${contract.requires.node}`);
8074
- lines.push(`Schema : ${contract.schemaVersion} (contract ${contract.contractVersion})`);
8075
- lines.push(`One-shot : ${contract.invocation.oneShot} <command>`);
8076
- lines.push(`Installed : ${contract.invocation.installed} <command>`);
8077
- lines.push("");
8078
- lines.push("Workflow:");
8079
- for (const step of contract.workflow) {
8080
- lines.push(` ${step.step}. ${step.id}`);
8081
- lines.push(` run : ${step.run}`);
8082
- lines.push(` expect : ${step.expect}`);
8083
- if (step.notes) lines.push(` note : ${step.notes}`);
8084
- }
8085
- lines.push("");
8137
+ // src/mcp/workspace.ts
8138
+ import * as fs12 from "fs";
8139
+ import * as path13 from "path";
8140
+ function foldForComparison(value) {
8141
+ return process.platform === "win32" || process.platform === "darwin" ? value.toLowerCase() : value;
8142
+ }
8143
+ function canonicalPath(value) {
8144
+ const absolute = path13.resolve(value);
8145
+ try {
8146
+ return fs12.realpathSync(absolute);
8147
+ } catch {
8148
+ return absolute;
8149
+ }
8150
+ }
8151
+ function isInside2(parent, child) {
8152
+ const foldedParent = foldForComparison(parent);
8153
+ const foldedChild = foldForComparison(child);
8154
+ return foldedChild === foldedParent || foldedChild.startsWith(`${foldedParent}${path13.sep}`);
8155
+ }
8156
+ function resolveRoot(requested, context) {
8157
+ if (requested !== void 0 && (typeof requested !== "string" || requested.trim().length === 0)) {
8158
+ return { ok: false, message: '"root" must be a non-empty string when provided.' };
8159
+ }
8160
+ const wanted = typeof requested === "string" ? requested.trim() : context.cwd;
8161
+ const absolute = path13.resolve(context.cwd, wanted);
8162
+ let stats;
8163
+ try {
8164
+ stats = fs12.statSync(absolute);
8165
+ } catch {
8166
+ return { ok: false, message: `"${wanted}" does not exist, so it cannot be a repository root.` };
8167
+ }
8168
+ if (!stats.isDirectory()) {
8169
+ return { ok: false, message: `"${wanted}" is not a directory.` };
8170
+ }
8171
+ const resolved = canonicalPath(absolute);
8172
+ for (const allowed of context.roots) {
8173
+ if (isInside2(canonicalPath(allowed), resolved)) return { ok: true, root: resolved };
8174
+ }
8175
+ const shown = context.roots.join(", ");
8176
+ return {
8177
+ ok: false,
8178
+ message: `"${wanted}" resolves outside the directories this server may read (${shown}). Restart it with --root <path> to add a repository.`
8179
+ };
8180
+ }
8181
+ async function openWorkspace(root, overrides = {}) {
8182
+ const loaded = await loadConfigFileDetailed(root);
8183
+ if (loaded.errors.length > 0) {
8184
+ return {
8185
+ ok: false,
8186
+ message: `The config file could not be read: ${loaded.errors.join("; ")} Fix it, or delete it to use the defaults \u2014 this server will not silently ignore it.`
8187
+ };
8188
+ }
8189
+ const cliOptions = { cwd: root };
8190
+ if (overrides.maxPartBytes !== void 0) {
8191
+ const parsed = parseByteSize(overrides.maxPartBytes);
8192
+ if (parsed === void 0) {
8193
+ return {
8194
+ ok: false,
8195
+ message: '"maxPartBytes" must be a byte count, for example 307200 or "300k".'
8196
+ };
8197
+ }
8198
+ cliOptions.maxPartBytes = parsed;
8199
+ }
8200
+ if (overrides.outDir !== void 0) {
8201
+ if (typeof overrides.outDir !== "string" || overrides.outDir.trim().length === 0) {
8202
+ return { ok: false, message: '"outDir" must be a non-empty string.' };
8203
+ }
8204
+ cliOptions.outDir = overrides.outDir.trim();
8205
+ }
8206
+ try {
8207
+ const config = resolveConfig(loaded.config, cliOptions, root);
8208
+ return { ok: true, workspace: { root, config, configFile: loaded.file } };
8209
+ } catch (error) {
8210
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
8211
+ }
8212
+ }
8213
+ var manifestCache = /* @__PURE__ */ new Map();
8214
+ function manifestPathFor(workspace) {
8215
+ return path13.resolve(workspace.root, workspace.config.outDir, "manifest.json");
8216
+ }
8217
+ function readManifest3(workspace) {
8218
+ const file = manifestPathFor(workspace);
8219
+ const shown = path13.relative(workspace.root, file).split(path13.sep).join("/") || file;
8220
+ let stats;
8221
+ try {
8222
+ stats = fs12.statSync(file);
8223
+ } catch {
8224
+ return {
8225
+ ok: false,
8226
+ missing: true,
8227
+ message: `There is no snapshot manifest at ${shown}. Call snapshot_plan to preview what would be included, or snapshot_generate to write it.`
8228
+ };
8229
+ }
8230
+ const cached = manifestCache.get(file);
8231
+ if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
8232
+ return { ok: true, manifest: cached.manifest, file };
8233
+ }
8234
+ let raw;
8235
+ try {
8236
+ raw = fs12.readFileSync(file, "utf8");
8237
+ } catch (error) {
8238
+ return {
8239
+ ok: false,
8240
+ missing: false,
8241
+ message: `${shown} exists but could not be read: ${error instanceof Error ? error.message : String(error)}`
8242
+ };
8243
+ }
8244
+ let parsed;
8245
+ try {
8246
+ parsed = JSON.parse(raw);
8247
+ } catch {
8248
+ return { ok: false, missing: false, message: `${shown} is not valid JSON.` };
8249
+ }
8250
+ const problem = describeManifestShapeProblem(parsed);
8251
+ if (problem) {
8252
+ return { ok: false, missing: false, message: `${shown} is not a snapshot manifest: ${problem}` };
8253
+ }
8254
+ const manifest = parsed;
8255
+ manifestCache.set(file, { mtimeMs: stats.mtimeMs, size: stats.size, manifest });
8256
+ return { ok: true, manifest, file };
8257
+ }
8258
+ function forgetManifest(file) {
8259
+ manifestCache.delete(file);
8260
+ }
8261
+
8262
+ // src/mcp/tools.ts
8263
+ var READ_ONLY = {
8264
+ readOnlyHint: true,
8265
+ destructiveHint: false,
8266
+ idempotentHint: true,
8267
+ openWorldHint: false
8268
+ };
8269
+ var WRITES = {
8270
+ readOnlyHint: false,
8271
+ // It replaces the snapshot directory but never anything it did not write, so it is additive with
8272
+ // respect to the user's files.
8273
+ destructiveHint: false,
8274
+ idempotentHint: true,
8275
+ openWorldHint: false
8276
+ };
8277
+ var MAX_RESULT_BYTES = 64 * 1024;
8278
+ var ROOT_PROPERTY = {
8279
+ type: "string",
8280
+ description: "Repository root. Defaults to the directory this server was started in."
8281
+ };
8282
+ function failure(message) {
8283
+ return { text: message, isError: true };
8284
+ }
8285
+ function plural(count, word) {
8286
+ return `${count} ${word}${count === 1 ? "" : "s"}`;
8287
+ }
8288
+ function formatBytes(bytes) {
8289
+ if (bytes < 1024) return `${bytes} B`;
8290
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
8291
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
8292
+ }
8293
+ function relativeFor(workspace, absolute) {
8294
+ return path14.relative(workspace.root, absolute).split(path14.sep).join("/");
8295
+ }
8296
+ function capped(values, limit = 20) {
8297
+ return { count: values.length, sample: values.slice(0, limit) };
8298
+ }
8299
+ function summarizeDrift(drift) {
8300
+ return {
8301
+ missingInSnapshot: capped(drift.missingInSnapshot),
8302
+ missingOnDisk: capped(drift.missingOnDisk),
8303
+ unreadable: capped(drift.unreadable),
8304
+ hashMismatches: capped(drift.hashMismatches),
8305
+ partIssues: capped(drift.partIssues),
8306
+ totalsMismatches: capped(drift.totalsMismatches),
8307
+ configMismatches: capped(drift.configMismatches)
8308
+ };
8309
+ }
8310
+ function toRelativePath(root, value) {
8311
+ const cleaned = value.trim();
8312
+ if (cleaned.length === 0) return null;
8313
+ if (!path14.isAbsolute(cleaned)) {
8314
+ return cleaned.replace(/\\/g, "/").replace(/^\.\//, "");
8315
+ }
8316
+ const relative11 = path14.relative(root, cleaned);
8317
+ if (relative11.startsWith("..") || path14.isAbsolute(relative11)) return null;
8318
+ return relative11.split(path14.sep).join("/");
8319
+ }
8320
+ function symbolsOf(entry) {
8321
+ const summary = entry.summary;
8322
+ if (!summary || typeof summary !== "object" || !("exports" in summary)) return [];
8323
+ const source = summary;
8324
+ return [...source.exports, ...source.reactComponents, ...source.routeHandlers];
8325
+ }
8326
+ async function prepare(args, context, overrides = {}) {
8327
+ const rootOutcome = resolveRoot(args.root, context);
8328
+ if (!rootOutcome.ok) return { ok: false, outcome: failure(rootOutcome.message) };
8329
+ const opened = await openWorkspace(rootOutcome.root, overrides);
8330
+ if (!opened.ok) return { ok: false, outcome: failure(opened.message) };
8331
+ return { ok: true, workspace: opened.workspace };
8332
+ }
8333
+ var statusTool = {
8334
+ name: "snapshot_status",
8335
+ title: "Snapshot status",
8336
+ 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.",
8337
+ inputSchema: {
8338
+ type: "object",
8339
+ properties: { root: ROOT_PROPERTY },
8340
+ additionalProperties: false
8341
+ },
8342
+ outputSchema: {
8343
+ type: "object",
8344
+ properties: {
8345
+ present: { type: "boolean" },
8346
+ root: { type: "string" },
8347
+ outDir: { type: "string" },
8348
+ fingerprint: { type: "string" },
8349
+ generatedAt: { type: "string" },
8350
+ generatorVersion: { type: "string" },
8351
+ generatorVersionMatches: { type: "boolean" },
8352
+ includedFiles: { type: "integer" },
8353
+ skippedFiles: { type: "integer" },
8354
+ partCount: { type: "integer" },
8355
+ warningCount: { type: "integer" }
8356
+ }
8357
+ },
8358
+ annotations: READ_ONLY,
8359
+ async handler(args, context) {
8360
+ const prepared = await prepare(args, context);
8361
+ if (!prepared.ok) return prepared.outcome;
8362
+ const workspace = prepared.workspace;
8363
+ const read = readManifest3(workspace);
8364
+ if (!read.ok) {
8365
+ return {
8366
+ text: read.message,
8367
+ structured: {
8368
+ present: false,
8369
+ root: workspace.root,
8370
+ outDir: workspace.config.outDir,
8371
+ message: read.message
8372
+ }
8373
+ };
8374
+ }
8375
+ const manifest = read.manifest;
8376
+ const versionMatches = manifest.generator.version === GENERATOR_VERSION;
8377
+ const lines = [
8378
+ `Snapshot: ${workspace.config.outDir}/`,
8379
+ `Generated: ${manifest.generatedAt} by ${manifest.generator.name} ${manifest.generator.version}`,
8380
+ `Files: ${manifest.counts.totalFiles} included, ${manifest.counts.skippedCount} skipped, ${plural(manifest.counts.partsCount, "part")}`,
8381
+ `Content: ${formatBytes(manifest.counts.totalSizeBytes)} of source in ${formatBytes(manifest.totals.totalContentBytes)} emitted`,
8382
+ `Fingerprint: ${manifest.fingerprint}`,
8383
+ `Git: ${manifest.git.branch} @ ${manifest.git.commit}${manifest.git.isDirty ? " (dirty)" : " (clean)"}`
8384
+ ];
8385
+ if (manifest.totals.warningCount > 0) lines.push(`Warnings: ${manifest.totals.warningCount}`);
8386
+ if (!versionMatches) {
8387
+ lines.push(
8388
+ `Note: written by ${manifest.generator.version}, this server is ${GENERATOR_VERSION}. Re-run snapshot_generate if the difference matters.`
8389
+ );
8390
+ }
8391
+ return {
8392
+ text: lines.join("\n"),
8393
+ structured: {
8394
+ present: true,
8395
+ root: workspace.root,
8396
+ outDir: workspace.config.outDir,
8397
+ fingerprint: manifest.fingerprint,
8398
+ generatedAt: manifest.generatedAt,
8399
+ generatorVersion: manifest.generator.version,
8400
+ generatorVersionMatches: versionMatches,
8401
+ includedFiles: manifest.counts.totalFiles,
8402
+ skippedFiles: manifest.counts.skippedCount,
8403
+ partCount: manifest.counts.partsCount,
8404
+ warningCount: manifest.totals.warningCount,
8405
+ preset: manifest.config.preset,
8406
+ maxPartBytes: manifest.budgets.maxPartBytes
8407
+ }
8408
+ };
8409
+ }
8410
+ };
8411
+ var planTool = {
8412
+ name: "snapshot_plan",
8413
+ title: "Preview a snapshot",
8414
+ description: "Preview a snapshot without writing anything: how many files and parts it would contain, and why every excluded file was excluded. Use it to choose a part budget before generating.",
8415
+ inputSchema: {
8416
+ type: "object",
8417
+ properties: {
8418
+ root: ROOT_PROPERTY,
8419
+ maxPartBytes: {
8420
+ type: "string",
8421
+ description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
8422
+ }
8423
+ },
8424
+ additionalProperties: false
8425
+ },
8426
+ outputSchema: {
8427
+ type: "object",
8428
+ properties: {
8429
+ files: { type: "integer" },
8430
+ skipped: { type: "integer" },
8431
+ parts: { type: "integer" },
8432
+ sourceBytes: { type: "integer" },
8433
+ emittedBytes: { type: "integer" },
8434
+ preset: { type: "string" }
8435
+ }
8436
+ },
8437
+ annotations: READ_ONLY,
8438
+ async handler(args, context) {
8439
+ const prepared = await prepare(args, context, { maxPartBytes: args.maxPartBytes });
8440
+ if (!prepared.ok) return prepared.outcome;
8441
+ const workspace = prepared.workspace;
8442
+ let manifest;
8443
+ try {
8444
+ manifest = generateSnapshot(workspace.config, { dryRun: true });
8445
+ } catch (error) {
8446
+ return failure(error instanceof Error ? error.message : String(error));
8447
+ }
8448
+ const reasons = /* @__PURE__ */ new Map();
8449
+ for (const record of manifest.skipped) {
8450
+ reasons.set(record.reason, (reasons.get(record.reason) ?? 0) + 1);
8451
+ }
8452
+ const topReasons = [...reasons.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, 8).map(([reason, count]) => ({ reason, count }));
8453
+ const lines = [
8454
+ "Dry run \u2014 nothing was written.",
8455
+ `Preset: ${manifest.config.preset}`,
8456
+ `Would include ${plural(manifest.counts.totalFiles, "file")} in ${plural(manifest.counts.partsCount, "part")} (${formatBytes(manifest.counts.totalSizeBytes)} of source, ${formatBytes(manifest.totals.totalContentBytes)} emitted).`,
8457
+ `Would skip ${plural(manifest.counts.skippedCount, "file")}.`
8458
+ ];
8459
+ if (topReasons.length > 0) {
8460
+ lines.push("Top skip reasons:");
8461
+ for (const entry of topReasons) lines.push(` - ${entry.reason} (${entry.count})`);
8462
+ }
8463
+ if (manifest.warnings.length > 0) {
8464
+ lines.push(`Warnings (${manifest.warnings.length}):`);
8465
+ for (const warning of manifest.warnings.slice(0, 5)) lines.push(` - ${warning}`);
8466
+ }
8467
+ return {
8468
+ text: lines.join("\n"),
8469
+ structured: {
8470
+ files: manifest.counts.totalFiles,
8471
+ skipped: manifest.counts.skippedCount,
8472
+ parts: manifest.counts.partsCount,
8473
+ sourceBytes: manifest.counts.totalSizeBytes,
8474
+ emittedBytes: manifest.totals.totalContentBytes,
8475
+ preset: manifest.config.preset,
8476
+ maxPartBytes: manifest.budgets.maxPartBytes,
8477
+ layers: manifest.layers,
8478
+ skipReasons: topReasons,
8479
+ warnings: manifest.warnings.slice(0, 5),
8480
+ warningCount: manifest.warnings.length
8481
+ }
8482
+ };
8483
+ }
8484
+ };
8485
+ var generateTool = {
8486
+ name: "snapshot_generate",
8487
+ title: "Generate the snapshot",
8488
+ description: "Write the snapshot to its output directory and return its fingerprint, parts and counts. Replaces the previous snapshot in that directory. A large repository can take several seconds.",
8489
+ inputSchema: {
8490
+ type: "object",
8491
+ properties: {
8492
+ root: ROOT_PROPERTY,
8493
+ maxPartBytes: {
8494
+ type: "string",
8495
+ description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
8496
+ },
8497
+ outDir: {
8498
+ type: "string",
8499
+ description: "Output directory relative to the root. Defaults to the project configuration."
8500
+ }
8501
+ },
8502
+ additionalProperties: false
8503
+ },
8504
+ outputSchema: {
8505
+ type: "object",
8506
+ properties: {
8507
+ fingerprint: { type: "string" },
8508
+ files: { type: "integer" },
8509
+ skipped: { type: "integer" },
8510
+ parts: { type: "integer" },
8511
+ emittedBytes: { type: "integer" }
8512
+ }
8513
+ },
8514
+ annotations: WRITES,
8515
+ async handler(args, context) {
8516
+ const prepared = await prepare(args, context, {
8517
+ maxPartBytes: args.maxPartBytes,
8518
+ outDir: args.outDir
8519
+ });
8520
+ if (!prepared.ok) return prepared.outcome;
8521
+ const workspace = prepared.workspace;
8522
+ let manifest;
8523
+ try {
8524
+ manifest = generateSnapshot(workspace.config);
8525
+ } catch (error) {
8526
+ return failure(error instanceof Error ? error.message : String(error));
8527
+ }
8528
+ forgetManifest(manifestPathFor(workspace));
8529
+ const written = relativeFor(workspace, path14.resolve(workspace.root, workspace.config.outDir));
8530
+ const lines = [
8531
+ `Wrote ${written}/`,
8532
+ `Files: ${manifest.counts.totalFiles} included, ${manifest.counts.skippedCount} skipped, ${plural(manifest.counts.partsCount, "part")}`,
8533
+ `Parts: ${manifest.parts.map((part) => `${part.filename} (${formatBytes(part.byteSize)})`).join(", ")}`,
8534
+ `Fingerprint: ${manifest.fingerprint}`
8535
+ ];
8536
+ if (manifest.warnings.length > 0) {
8537
+ lines.push(`Warnings (${manifest.warnings.length}):`);
8538
+ for (const warning of manifest.warnings.slice(0, 5)) lines.push(` - ${warning}`);
8539
+ }
8540
+ return {
8541
+ text: lines.join("\n"),
8542
+ structured: {
8543
+ fingerprint: manifest.fingerprint,
8544
+ files: manifest.counts.totalFiles,
8545
+ skipped: manifest.counts.skippedCount,
8546
+ parts: manifest.counts.partsCount,
8547
+ emittedBytes: manifest.totals.totalContentBytes,
8548
+ outDir: written,
8549
+ partFiles: manifest.parts.map((part) => part.filename),
8550
+ warningCount: manifest.warnings.length
8551
+ }
8552
+ };
8553
+ }
8554
+ };
8555
+ var validateTool = {
8556
+ name: "snapshot_validate",
8557
+ title: "Validate the snapshot",
8558
+ 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.",
8559
+ inputSchema: {
8560
+ type: "object",
8561
+ properties: { root: ROOT_PROPERTY },
8562
+ additionalProperties: false
8563
+ },
8564
+ outputSchema: {
8565
+ type: "object",
8566
+ properties: {
8567
+ current: { type: "boolean" },
8568
+ manifestPresent: { type: "boolean" }
8569
+ }
8570
+ },
8571
+ annotations: READ_ONLY,
8572
+ async handler(args, context) {
8573
+ const prepared = await prepare(args, context);
8574
+ if (!prepared.ok) return prepared.outcome;
8575
+ const workspace = prepared.workspace;
8576
+ let result;
8577
+ try {
8578
+ result = validateSnapshot(workspace.config);
8579
+ } catch (error) {
8580
+ return failure(error instanceof Error ? error.message : String(error));
8581
+ }
8582
+ const lines = [];
8583
+ if (result.valid) {
8584
+ lines.push("The snapshot matches the repository.");
8585
+ } else {
8586
+ lines.push("The snapshot is out of date.");
8587
+ const drift = result.drift;
8588
+ const observations = [
8589
+ ["file(s) changed on disk", drift.hashMismatches.length],
8590
+ ["file(s) added since the snapshot", drift.missingInSnapshot.length],
8591
+ ["file(s) gone since the snapshot", drift.missingOnDisk.length],
8592
+ ["part file(s) damaged or missing", drift.partIssues.length],
8593
+ ["setting(s) changed since the snapshot", drift.configMismatches.length],
8594
+ ["internal count(s) disagreeing", drift.totalsMismatches.length]
8595
+ ];
8596
+ for (const [label, count] of observations) {
8597
+ if (count > 0) lines.push(` - ${count} ${label}`);
8598
+ }
8599
+ for (const error of result.errors.slice(0, 5)) lines.push(` - ${error}`);
8600
+ lines.push("Run snapshot_generate to refresh it.");
8601
+ }
8602
+ for (const warning of result.warnings.slice(0, 5)) lines.push(`Note: ${warning}`);
8603
+ return {
8604
+ text: lines.join("\n"),
8605
+ structured: {
8606
+ current: result.valid,
8607
+ manifestPresent: result.manifestPresent,
8608
+ drift: summarizeDrift(result.drift),
8609
+ errors: result.errors.slice(0, 10),
8610
+ warnings: result.warnings.slice(0, 10)
8611
+ }
8612
+ // A stale snapshot is an answer, not a tool failure: the caller asked whether it is current.
8613
+ };
8614
+ }
8615
+ };
8616
+ var locateTool = {
8617
+ name: "snapshot_locate",
8618
+ title: "Locate code in the snapshot",
8619
+ description: "Find which part holds the code you need. Matches file paths, exported symbols, component names and routes from the snapshot's index, so one part can be read instead of all of them.",
8620
+ inputSchema: {
8621
+ type: "object",
8622
+ properties: {
8623
+ query: {
8624
+ type: "string",
8625
+ description: "Text to match, case-insensitively, against paths and exported symbols."
8626
+ },
8627
+ by: {
8628
+ type: "string",
8629
+ enum: ["auto", "path", "symbol", "route", "layer", "kind"],
8630
+ default: "auto",
8631
+ description: "Restrict what is matched. Defaults to every category."
8632
+ },
8633
+ limit: {
8634
+ type: "integer",
8635
+ minimum: 1,
8636
+ maximum: 100,
8637
+ default: 20,
8638
+ description: "Maximum matches to return."
8639
+ },
8640
+ root: ROOT_PROPERTY
8641
+ },
8642
+ required: ["query"],
8643
+ additionalProperties: false
8644
+ },
8645
+ outputSchema: {
8646
+ type: "object",
8647
+ properties: { total: { type: "integer" } }
8648
+ },
8649
+ annotations: READ_ONLY,
8650
+ async handler(args, context) {
8651
+ const prepared = await prepare(args, context);
8652
+ if (!prepared.ok) return prepared.outcome;
8653
+ const workspace = prepared.workspace;
8654
+ const read = readManifest3(workspace);
8655
+ if (!read.ok) return failure(read.message);
8656
+ const query = String(args.query ?? "").trim();
8657
+ if (query.length === 0) return failure('"query" must not be empty.');
8658
+ const needle = query.toLowerCase();
8659
+ const by = typeof args.by === "string" ? args.by : "auto";
8660
+ const limit = typeof args.limit === "number" ? args.limit : 20;
8661
+ const manifest = read.manifest;
8662
+ const matches = [];
8663
+ const add = (entry, matchedBy, detail) => {
8664
+ matches.push({
8665
+ path: entry.relativePath,
8666
+ part: entry.part,
8667
+ layer: entry.layer,
8668
+ kind: entry.kind,
8669
+ matchedBy,
8670
+ ...detail ? { detail } : {}
8671
+ });
8672
+ };
8673
+ const wants = (category) => by === "auto" || by === category;
8674
+ for (const entry of manifest.files) {
8675
+ if (wants("path") && entry.relativePath.toLowerCase().includes(needle)) {
8676
+ add(entry, "path");
8677
+ continue;
8678
+ }
8679
+ if (wants("symbol")) {
8680
+ const symbol = symbolsOf(entry).find((name) => name.toLowerCase().includes(needle));
8681
+ if (symbol) {
8682
+ add(entry, "symbol", symbol);
8683
+ continue;
8684
+ }
8685
+ }
8686
+ if (wants("layer") && entry.layer.toLowerCase() === needle) add(entry, "layer");
8687
+ else if (wants("kind") && entry.kind.toLowerCase() === needle) add(entry, "kind");
8688
+ }
8689
+ if (wants("route")) {
8690
+ for (const route of manifest.indexes.routes) {
8691
+ if (!route.path.toLowerCase().includes(needle)) continue;
8692
+ if (matches.some((match) => match.path === route.path)) continue;
8693
+ matches.push({
8694
+ path: route.path,
8695
+ part: route.part,
8696
+ layer: "route",
8697
+ kind: "route",
8698
+ matchedBy: "route",
8699
+ detail: route.methods.join(", ")
8700
+ });
8701
+ }
8702
+ }
8703
+ if (matches.length === 0) {
8704
+ return {
8705
+ text: `Nothing in the snapshot matches "${query}". Try a shorter fragment, or snapshot_explain a path to see whether it is included at all.`,
8706
+ structured: { total: 0, matches: [] }
8707
+ };
8708
+ }
8709
+ const shown = matches.slice(0, limit);
8710
+ const lines = [
8711
+ `${matches.length} match${matches.length === 1 ? "" : "es"} for "${query}"` + (matches.length > shown.length ? ` (showing ${shown.length})` : "") + ":"
8712
+ ];
8713
+ for (const match of shown) {
8714
+ const detail = match.detail ? ` \u2014 ${match.detail}` : "";
8715
+ lines.push(` ${match.path}${detail} [${match.part}, ${match.layer}/${match.kind}]`);
8716
+ }
8717
+ if (matches.length > shown.length) {
8718
+ lines.push("Raise `limit` to see more.");
8719
+ }
8720
+ return {
8721
+ text: lines.join("\n"),
8722
+ structured: { total: matches.length, returned: shown.length, matches: shown }
8723
+ };
8724
+ }
8725
+ };
8726
+ var readTool = {
8727
+ name: "snapshot_read",
8728
+ title: "Read a file from the snapshot",
8729
+ description: "Return a line window of one file as the snapshot contains it: redacted, and bounded to a preview when the source is large. Reports the next `fromLine` when more remains.",
8730
+ inputSchema: {
8731
+ type: "object",
8732
+ properties: {
8733
+ path: {
8734
+ type: "string",
8735
+ description: "File path relative to the repository root, as snapshot_locate reports it."
8736
+ },
8737
+ fromLine: {
8738
+ type: "integer",
8739
+ minimum: 1,
8740
+ default: 1,
8741
+ description: "First line to return, 1-based."
8742
+ },
8743
+ lines: {
8744
+ type: "integer",
8745
+ minimum: 1,
8746
+ maximum: 2e3,
8747
+ default: 200,
8748
+ description: "How many lines to return."
8749
+ },
8750
+ root: ROOT_PROPERTY
8751
+ },
8752
+ required: ["path"],
8753
+ additionalProperties: false
8754
+ },
8755
+ outputSchema: {
8756
+ type: "object",
8757
+ properties: {
8758
+ path: { type: "string" },
8759
+ part: { type: "string" },
8760
+ fromLine: { type: "integer" },
8761
+ returnedLines: { type: "integer" },
8762
+ totalLines: { type: "integer" },
8763
+ nextFromLine: { type: "integer" }
8764
+ }
8765
+ },
8766
+ annotations: READ_ONLY,
8767
+ async handler(args, context) {
8768
+ const prepared = await prepare(args, context);
8769
+ if (!prepared.ok) return prepared.outcome;
8770
+ const workspace = prepared.workspace;
8771
+ if (typeof args.path !== "string") return failure('"path" must be a string.');
8772
+ const relativePath = toRelativePath(workspace.root, args.path);
8773
+ if (!relativePath) {
8774
+ return failure(
8775
+ `"${args.path}" is outside ${workspace.root}. Paths are read relative to the repository root.`
8776
+ );
8777
+ }
8778
+ const read = readManifest3(workspace);
8779
+ if (!read.ok) return failure(read.message);
8780
+ const manifest = read.manifest;
8781
+ const entry = manifest.files.find((file) => file.relativePath === relativePath) ?? manifest.files.find((file) => file.relativePath.toLowerCase() === relativePath.toLowerCase());
8782
+ if (!entry) {
8783
+ const skipped = manifest.skipped.find((record) => record.path === relativePath);
8784
+ if (skipped) {
8785
+ const detail = skipped.detail ? ` (${skipped.detail})` : "";
8786
+ return failure(
8787
+ `${relativePath} is not in the snapshot: ${skipped.reason}${detail}. Sensitive and ignored files stay out unless they are included explicitly.`
8788
+ );
8789
+ }
8790
+ return failure(
8791
+ `${relativePath} is not in the snapshot. Call snapshot_locate to find the path, or snapshot_explain to learn why it was excluded.`
8792
+ );
8793
+ }
8794
+ const partPath = path14.resolve(workspace.root, workspace.config.outDir, "parts", entry.part);
8795
+ let partText;
8796
+ try {
8797
+ partText = fs13.readFileSync(partPath, "utf8");
8798
+ } catch {
8799
+ return failure(
8800
+ `${entry.part} could not be read, so the snapshot is damaged. Run snapshot_generate to rebuild it.`
8801
+ );
8802
+ }
8803
+ const section = findSection(parsePartSections(partText), entry.relativePath, entry.hashSha256);
8804
+ if (!section) {
8805
+ return failure(
8806
+ `${relativePath} is recorded in ${entry.part} but its section could not be located there. Run snapshot_generate to rebuild the snapshot.`
8807
+ );
8808
+ }
8809
+ const fromLine = typeof args.fromLine === "number" ? args.fromLine : 1;
8810
+ const lineCount = typeof args.lines === "number" ? args.lines : 200;
8811
+ const window = windowText(section.body, fromLine, lineCount);
8812
+ let body = window.text;
8813
+ let truncatedByBytes = false;
8814
+ if (Buffer.byteLength(body, "utf8") > MAX_RESULT_BYTES) {
8815
+ body = body.slice(0, MAX_RESULT_BYTES);
8816
+ const lastBreak = body.lastIndexOf("\n");
8817
+ body = lastBreak > 0 ? body.slice(0, lastBreak) : body;
8818
+ truncatedByBytes = true;
8819
+ }
8820
+ const returnedLines = body.length === 0 ? 0 : body.split("\n").length;
8821
+ const moreRemain = window.truncated || truncatedByBytes;
8822
+ const header = [
8823
+ `${entry.relativePath} \u2014 ${entry.part}, ${entry.layer}/${entry.kind}, ${entry.language}`,
8824
+ 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}.`
8825
+ ];
8826
+ const lines = [...header, "", body];
8827
+ if (moreRemain) {
8828
+ const next = window.fromLine + returnedLines;
8829
+ lines.push("", `Truncated. Call snapshot_read with fromLine=${next} for the rest.`);
8830
+ }
8831
+ return {
8832
+ text: lines.join("\n"),
8833
+ structured: {
8834
+ path: entry.relativePath,
8835
+ part: entry.part,
8836
+ layer: entry.layer,
8837
+ kind: entry.kind,
8838
+ language: entry.language,
8839
+ inclusion: section.inclusion ?? entry.inclusionMode,
8840
+ fromLine: window.fromLine,
8841
+ returnedLines,
8842
+ totalLines: window.totalLines,
8843
+ truncated: moreRemain,
8844
+ nextFromLine: moreRemain ? window.fromLine + returnedLines : null,
8845
+ sha256: entry.hashSha256
8846
+ }
8847
+ };
8848
+ }
8849
+ };
8850
+ var explainTool = {
8851
+ name: "snapshot_explain",
8852
+ title: "Explain one path",
8853
+ description: 'Explain how the next snapshot would treat one path: its layer, kind, language and part, or the exact reason it is excluded. Answers "why is this file missing".',
8854
+ inputSchema: {
8855
+ type: "object",
8856
+ properties: {
8857
+ path: {
8858
+ type: "string",
8859
+ description: "Path relative to the repository root. Directories are explained as a whole."
8860
+ },
8861
+ root: ROOT_PROPERTY
8862
+ },
8863
+ required: ["path"],
8864
+ additionalProperties: false
8865
+ },
8866
+ outputSchema: {
8867
+ type: "object",
8868
+ properties: {
8869
+ path: { type: "string" },
8870
+ included: { type: "boolean" },
8871
+ reason: { type: "string" }
8872
+ }
8873
+ },
8874
+ annotations: READ_ONLY,
8875
+ async handler(args, context) {
8876
+ const prepared = await prepare(args, context);
8877
+ if (!prepared.ok) return prepared.outcome;
8878
+ const workspace = prepared.workspace;
8879
+ if (typeof args.path !== "string") return failure('"path" must be a string.');
8880
+ const relativePath = toRelativePath(workspace.root, args.path);
8881
+ if (!relativePath) {
8882
+ return failure(`"${args.path}" is outside ${workspace.root}.`);
8883
+ }
8884
+ let result;
8885
+ try {
8886
+ result = explainPath(workspace.config, relativePath);
8887
+ } catch (error) {
8888
+ return failure(error instanceof Error ? error.message : String(error));
8889
+ }
8890
+ return {
8891
+ text: renderExplainText(result),
8892
+ structured: {
8893
+ path: result.path,
8894
+ included: result.included,
8895
+ ...result.reason ? { reason: result.reason } : {},
8896
+ ...result.detail ? { detail: result.detail } : {},
8897
+ ...result.layer ? { layer: result.layer } : {},
8898
+ ...result.kind ? { kind: result.kind } : {},
8899
+ ...result.language ? { language: result.language } : {},
8900
+ ...result.part ? { part: result.part } : {},
8901
+ ...result.inclusionMode ? { inclusion: result.inclusionMode } : {},
8902
+ ...typeof result.lines === "number" ? { lines: result.lines } : {},
8903
+ ...typeof result.bytes === "number" ? { bytes: result.bytes } : {},
8904
+ ...typeof result.secretCount === "number" ? { secretCount: result.secretCount } : {}
8905
+ }
8906
+ };
8907
+ }
8908
+ };
8909
+ function buildTools() {
8910
+ return [
8911
+ statusTool,
8912
+ planTool,
8913
+ generateTool,
8914
+ validateTool,
8915
+ locateTool,
8916
+ readTool,
8917
+ explainTool
8918
+ ];
8919
+ }
8920
+
8921
+ // src/mcp/prompts.ts
8922
+ var ORIENT = `You have a codebase-snapshot server for this repository. Build a map of it before reading any code.
8923
+
8924
+ 1. Call snapshot_status. If no snapshot exists, call snapshot_plan to preview it, then snapshot_generate.
8925
+ 2. Call snapshot_locate for the areas that matter. Search by exported symbol, route or path fragment rather than guessing filenames.
8926
+ 3. Report back: the layers present, the entry points, and the files that matter here \u2014 cited as paths with their part.
8927
+ 4. Read one file at a time with snapshot_read, one window at a time. Do not read a part end to end.`;
8928
+ var KEEP_CURRENT = `Keep the context you were given provably current while you work.
8929
+
8930
+ 1. Before relying on a snapshot from an earlier session, call snapshot_validate.
8931
+ 2. If it reports drift, call snapshot_generate once, then snapshot_validate again.
8932
+ 3. After changes you make, call snapshot_validate and report what it said.
8933
+ 4. Never call a snapshot current unless snapshot_validate said so in this session.`;
8934
+ function withFocus(base, focus) {
8935
+ return typeof focus === "string" && focus.trim().length > 0 ? `${base}
8936
+
8937
+ Focus for this pass: ${focus.trim()}` : base;
8938
+ }
8939
+ function buildPrompts() {
8940
+ return [
8941
+ {
8942
+ name: "orient",
8943
+ title: "Orient in this repository",
8944
+ description: "Map the repository from the snapshot before reading any of it.",
8945
+ arguments: [
8946
+ {
8947
+ name: "focus",
8948
+ description: 'Optional area to concentrate on, for example "authentication".',
8949
+ required: false
8950
+ }
8951
+ ],
8952
+ build(args) {
8953
+ return {
8954
+ description: "Map the repository before reading it.",
8955
+ messages: [{ role: "user", content: { type: "text", text: withFocus(ORIENT, args.focus) } }]
8956
+ };
8957
+ }
8958
+ },
8959
+ {
8960
+ name: "keep-current",
8961
+ title: "Keep the snapshot current",
8962
+ description: "Validate before trusting earlier context, and again after making changes.",
8963
+ arguments: [],
8964
+ build() {
8965
+ return {
8966
+ description: "Keep the context current.",
8967
+ messages: [{ role: "user", content: { type: "text", text: KEEP_CURRENT } }]
8968
+ };
8969
+ }
8970
+ }
8971
+ ];
8972
+ }
8973
+
8974
+ // src/mcp/protocol.ts
8975
+ var LEGACY_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18"];
8976
+ var MODERN_PROTOCOL_VERSIONS = ["2026-07-28"];
8977
+ var SUPPORTED_PROTOCOL_VERSIONS = [
8978
+ ...MODERN_PROTOCOL_VERSIONS,
8979
+ ...LEGACY_PROTOCOL_VERSIONS
8980
+ ];
8981
+ var DEFAULT_LEGACY_PROTOCOL_VERSION = LEGACY_PROTOCOL_VERSIONS[0];
8982
+ var ERROR_CODES = {
8983
+ parseError: -32700,
8984
+ invalidRequest: -32600,
8985
+ methodNotFound: -32601,
8986
+ invalidParams: -32602,
8987
+ internalError: -32603,
8988
+ unsupportedProtocolVersion: -32022
8989
+ };
8990
+ var METHOD3 = {
8991
+ initialize: "initialize",
8992
+ initialized: "notifications/initialized",
8993
+ discover: "server/discover",
8994
+ ping: "ping",
8995
+ toolsList: "tools/list",
8996
+ toolsCall: "tools/call",
8997
+ resourcesList: "resources/list",
8998
+ resourcesRead: "resources/read",
8999
+ resourceTemplatesList: "resources/templates/list",
9000
+ promptsList: "prompts/list",
9001
+ promptsGet: "prompts/get",
9002
+ cancelled: "notifications/cancelled"
9003
+ };
9004
+ var META_KEYS = {
9005
+ protocolVersion: "io.modelcontextprotocol/protocolVersion",
9006
+ clientCapabilities: "io.modelcontextprotocol/clientCapabilities",
9007
+ clientInfo: "io.modelcontextprotocol/clientInfo",
9008
+ serverInfo: "io.modelcontextprotocol/serverInfo",
9009
+ progressToken: "progressToken"
9010
+ };
9011
+ function isJsonObject(value) {
9012
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9013
+ }
9014
+ function serverInfoMeta(info2) {
9015
+ const value = { name: info2.name, version: info2.version };
9016
+ if (info2.title) value.title = info2.title;
9017
+ return { [META_KEYS.serverInfo]: value };
9018
+ }
9019
+ function successResponse(id, result, modern, info2) {
9020
+ if (!modern) return { jsonrpc: "2.0", id, result };
9021
+ const withMeta = {
9022
+ ...result,
9023
+ resultType: "complete",
9024
+ _meta: { ...isJsonObject(result._meta) ? result._meta : {}, ...serverInfoMeta(info2) }
9025
+ };
9026
+ return { jsonrpc: "2.0", id, result: withMeta };
9027
+ }
9028
+ function errorResponse(id, code, message, data) {
9029
+ const error = { code, message };
9030
+ if (data !== void 0) error.data = data;
9031
+ return { jsonrpc: "2.0", id, error };
9032
+ }
9033
+
9034
+ // src/mcp/uris.ts
9035
+ var STATIC_RESOURCE_URIS = [
9036
+ "snapshot://manifest",
9037
+ "snapshot://contract",
9038
+ "snapshot://skill",
9039
+ "snapshot://reports/audit",
9040
+ "snapshot://reports/tree",
9041
+ "snapshot://reports/navigation"
9042
+ ];
9043
+ var RESOURCE_TEMPLATE_URIS = ["snapshot://parts/{index}"];
9044
+
9045
+ // src/agent/option-scope.ts
9046
+ var VALIDATE_OPTIONS = [
9047
+ "cwd",
9048
+ "out-dir",
9049
+ "max-part-bytes",
9050
+ "large-file-bytes",
9051
+ "max-monolith-bytes",
9052
+ "include-monolith",
9053
+ "include-sensitive-files",
9054
+ "skip-artifacts",
9055
+ "only-artifacts",
9056
+ "no-config",
9057
+ "discovery",
9058
+ "format",
9059
+ "mode",
9060
+ "help",
9061
+ "version"
9062
+ ];
9063
+ var COMMAND_OPTION_SCOPE = {
9064
+ generate: [
9065
+ "cwd",
9066
+ "out-dir",
9067
+ "max-part-bytes",
9068
+ "large-file-bytes",
9069
+ "max-monolith-bytes",
9070
+ "include-monolith",
9071
+ "include-sensitive-files",
9072
+ "skip-artifacts",
9073
+ "only-artifacts",
9074
+ "no-config",
9075
+ "discovery",
9076
+ "format",
9077
+ "mode",
9078
+ "dry-run",
9079
+ "watch",
9080
+ "autopilot",
9081
+ "no-install",
9082
+ "help",
9083
+ "version"
9084
+ ],
9085
+ validate: VALIDATE_OPTIONS,
9086
+ report: [
9087
+ "cwd",
9088
+ "out-dir",
9089
+ "max-part-bytes",
9090
+ "large-file-bytes",
9091
+ "max-monolith-bytes",
9092
+ "include-monolith",
9093
+ "include-sensitive-files",
9094
+ "skip-artifacts",
9095
+ "only-artifacts",
9096
+ "no-config",
9097
+ "format",
9098
+ "mode",
9099
+ "help",
9100
+ "version"
9101
+ ],
9102
+ explain: [
9103
+ "cwd",
9104
+ "out-dir",
9105
+ "max-part-bytes",
9106
+ "large-file-bytes",
9107
+ "max-monolith-bytes",
9108
+ "include-monolith",
9109
+ "include-sensitive-files",
9110
+ "skip-artifacts",
9111
+ "only-artifacts",
9112
+ "no-config",
9113
+ "discovery",
9114
+ "format",
9115
+ "mode",
9116
+ "help",
9117
+ "version"
9118
+ ],
9119
+ diff: ["format", "mode", "no-config", "help", "version"],
9120
+ describe: ["format", "mode", "help", "version"],
9121
+ init: [
9122
+ "cwd",
9123
+ "out-dir",
9124
+ "max-part-bytes",
9125
+ "include-sensitive-files",
9126
+ "skip-artifacts",
9127
+ "only-artifacts",
9128
+ "preset",
9129
+ "include",
9130
+ "exclude",
9131
+ "yes",
9132
+ "force",
9133
+ "dry-run",
9134
+ "format",
9135
+ "mode",
9136
+ "help",
9137
+ "version"
9138
+ ],
9139
+ skill: ["cwd", "format", "mode", "install", "target", "help", "version"],
9140
+ // `mcp` reads the repository, so it takes --cwd. It has no --format: stdout carries JSON-RPC
9141
+ // messages, and there is no second shape to choose, so offering the flag would be a lie.
9142
+ mcp: ["cwd", "mode", "help", "version"],
9143
+ setup: [
9144
+ "cwd",
9145
+ "out-dir",
9146
+ "max-part-bytes",
9147
+ "include-sensitive-files",
9148
+ "skip-artifacts",
9149
+ "only-artifacts",
9150
+ "preset",
9151
+ "include",
9152
+ "exclude",
9153
+ "yes",
9154
+ "force",
9155
+ "no-install",
9156
+ "dry-run",
9157
+ "format",
9158
+ "mode",
9159
+ "help",
9160
+ "version"
9161
+ ]
9162
+ };
9163
+ var COMMAND_ORDER = [
9164
+ "generate",
9165
+ "validate",
9166
+ "report",
9167
+ "explain",
9168
+ "init",
9169
+ "setup",
9170
+ "diff",
9171
+ "describe",
9172
+ "skill",
9173
+ "mcp"
9174
+ ];
9175
+ function commandsForOption(option) {
9176
+ return COMMAND_ORDER.filter((command) => COMMAND_OPTION_SCOPE[command].includes(option));
9177
+ }
9178
+ function optionAppliesTo(option) {
9179
+ return commandsForOption(option).join("/");
9180
+ }
9181
+
9182
+ // src/agent/contract.ts
9183
+ var CONTRACT_VERSION = "1.0.0";
9184
+ var PACKAGE_NAME = "codebase-snapshot";
9185
+ var MAJOR_VERSION = GENERATOR_VERSION.split(".")[0];
9186
+ var ARTIFACT_SUMMARY = {
9187
+ readme: "README.md describing the snapshot at the output root.",
9188
+ manifest: "manifest.json: machine-readable index of every file, part, hash and config.",
9189
+ parts: "parts/*.md: the packed, budget-limited markdown parts that carry the code.",
9190
+ navigation: "reports/navigation.md: links between parts and layers.",
9191
+ projectTree: "reports/project-tree.md: the directory tree of included files.",
9192
+ audit: "reports/audit.md: secret findings, large files, parse diagnostics, git state.",
9193
+ monolith: "full-codebase.json and full-codebase.md: the whole repository in one file (off by default)."
9194
+ };
9195
+ var ARTIFACT_DEFAULTS = {
9196
+ readme: true,
9197
+ manifest: true,
9198
+ parts: true,
9199
+ navigation: true,
9200
+ projectTree: true,
9201
+ audit: true,
9202
+ monolith: false
9203
+ };
9204
+ function renderContractText(contract) {
9205
+ const lines = [];
9206
+ lines.push(`${contract.package} v${contract.version}`);
9207
+ lines.push(contract.summary);
9208
+ lines.push("");
9209
+ lines.push(`Node : ${contract.requires.node}`);
9210
+ lines.push(`Schema : ${contract.schemaVersion} (contract ${contract.contractVersion})`);
9211
+ lines.push(`One-shot : ${contract.invocation.oneShot} <command>`);
9212
+ lines.push(`Installed : ${contract.invocation.installed} <command>`);
9213
+ lines.push("");
9214
+ lines.push("Workflow:");
9215
+ for (const step of contract.workflow) {
9216
+ lines.push(` ${step.step}. ${step.id}`);
9217
+ lines.push(` run : ${step.run}`);
9218
+ lines.push(` expect : ${step.expect}`);
9219
+ if (step.notes) lines.push(` note : ${step.notes}`);
9220
+ }
9221
+ lines.push("");
8086
9222
  lines.push("Commands:");
8087
9223
  for (const command of contract.commands) {
8088
9224
  const args = command.positionals.length > 0 ? ` ${command.positionals.join(" ")}` : "";
@@ -8108,6 +9244,19 @@ function renderContractText(contract) {
8108
9244
  lines.push(` ${"".padEnd(24)} ${option.summary}`);
8109
9245
  }
8110
9246
  lines.push("");
9247
+ lines.push("MCP:");
9248
+ lines.push(` command ${contract.mcp.command}`);
9249
+ lines.push(` transport ${contract.mcp.transport}`);
9250
+ lines.push(
9251
+ ` versions modern ${contract.mcp.protocolVersions.modern.join(", ")} / legacy ${contract.mcp.protocolVersions.legacy.join(", ")}`
9252
+ );
9253
+ lines.push(
9254
+ ` tools ${contract.mcp.tools.map((tool) => tool.readOnly ? tool.name : `${tool.name} (writes)`).join(", ")}`
9255
+ );
9256
+ lines.push(` prompts ${contract.mcp.prompts.map((prompt) => prompt.name).join(", ")}`);
9257
+ lines.push(` resources ${contract.mcp.resources.join(", ")}`);
9258
+ lines.push(` templates ${contract.mcp.resourceTemplates.join(", ")}`);
9259
+ lines.push("");
8111
9260
  lines.push("Safety:");
8112
9261
  for (const note of contract.safety) lines.push(` - ${note}`);
8113
9262
  lines.push("");
@@ -8133,11 +9282,13 @@ function buildAgentContract() {
8133
9282
  invocation: {
8134
9283
  oneShot: `npx ${PACKAGE_NAME}@latest`,
8135
9284
  installed: "npx codebase-snapshot",
8136
- bins: ["codebase-snapshot", "snap"],
9285
+ bins: ["codebase-snapshot", "snapshot"],
8137
9286
  notes: [
8138
9287
  `The bare package name also works: npx ${PACKAGE_NAME} <command>.`,
8139
- "Pin an exact version \u2014 for example `codebase-snapshot@0.9.0` \u2014 when you need the same build on every machine. A major range is not enough before 1.0: npm's caret on a 0.x version pins the minor, and 0.x minors are breaking by convention.",
9288
+ "`codebase-snapshot` is the canonical name and the one to script against. `snapshot` is the short alias a global install provides; in a project that has the package installed, `npx snapshot` resolves the local bin first.",
9289
+ `Pin an exact version \u2014 for example \`npx ${PACKAGE_NAME}@${GENERATOR_VERSION} --version\` \u2014 when you need the same build on every machine. A major range is not enough before 1.0: npm's caret on a 0.x version pins the minor, and 0.x minors are breaking by convention.`,
8140
9290
  "Both bin names point at the same file. npm resolves a package by its bin entries, so they must stay aliases of one executable.",
9291
+ "A bare run in a project with no configuration asks once, sets the project up and generates \u2014 but only at a terminal. With no TTY it exits 2 and names the alternatives, so an unattended caller must say what it wants: `generate` after `init --yes`, or `--autopilot`.",
8141
9292
  "Human progress goes to stderr. With --format json the payload on stdout is pure JSON.",
8142
9293
  "A failing run also writes pure JSON to stdout: { error: { message, exitCode } }. The exit code and the stderr line are unchanged."
8143
9294
  ]
@@ -8148,7 +9299,7 @@ function buildAgentContract() {
8148
9299
  id: "verify",
8149
9300
  run: `npx ${PACKAGE_NAME}@latest --version`,
8150
9301
  // The requirement is the major version; the exact build is context. A
8151
- // floating tag may print 4.0.7 today and 4.0.9 next week, and an agent
9302
+ // floating tag may print a different patch today than next week, and an agent
8152
9303
  // must not conclude that a working tool is broken because of a patch.
8153
9304
  expect: `prints v${MAJOR_VERSION} or newer on stdout and exits 0 (this build is v${GENERATOR_VERSION}: compare the major version, not the patch)`,
8154
9305
  notes: `If it prints an older major than this contract's, the @latest tag is behind the release: pin the exact version with \`npx ${PACKAGE_NAME}@${GENERATOR_VERSION} --version\` instead of retrying @latest. If it prints nothing, clear the npm cache and retry, or use \`npx -p <pkg> codebase-snapshot\`.`
@@ -8192,9 +9343,9 @@ function buildAgentContract() {
8192
9343
  {
8193
9344
  name: "generate",
8194
9345
  aliases: ["default"],
8195
- summary: "Write the snapshot. This is the command you normally want.",
9346
+ summary: "Write the snapshot. This is the command you normally want. In a project with no configuration, a terminal asks once and sets the project up first; without a terminal it exits 2 and names `--autopilot` and `init --yes`.",
8196
9347
  positionals: [],
8197
- exits: [0, 1]
9348
+ exits: [0, 1, 2]
8198
9349
  },
8199
9350
  {
8200
9351
  name: "validate",
@@ -8251,6 +9402,13 @@ function buildAgentContract() {
8251
9402
  summary: "Print the agent skill, or --install it into the tool registries this repository's agent reads (.claude/skills, .cursor/rules). Files it creates are added to .gitignore. It never writes AGENTS.md: that file belongs to the repository.",
8252
9403
  positionals: [],
8253
9404
  exits: [0, 1, 2]
9405
+ },
9406
+ {
9407
+ name: "mcp",
9408
+ aliases: [],
9409
+ summary: "Serve this repository to an MCP client over stdio. The tools, resources and protocol versions it offers are listed in this contract's `mcp` block.",
9410
+ positionals: [],
9411
+ exits: [0, 2]
8254
9412
  }
8255
9413
  ],
8256
9414
  exitCodes: {
@@ -8401,6 +9559,13 @@ function buildAgentContract() {
8401
9559
  appliesTo: optionAppliesTo("force"),
8402
9560
  summary: "Overwrite an existing config file."
8403
9561
  },
9562
+ {
9563
+ flag: "--autopilot",
9564
+ type: "boolean",
9565
+ default: false,
9566
+ appliesTo: optionAppliesTo("autopilot"),
9567
+ summary: "Set the project up without asking, then generate: detect the preset, write the config and .gitignore, add the npm scripts and devDependency, install with the project's own package manager, generate. Never overwrites an existing config, and never prompts. Refuses an empty directory (exit 1); contradicts --no-config (exit 2)."
9568
+ },
8404
9569
  {
8405
9570
  flag: "--install",
8406
9571
  type: "boolean",
@@ -8420,7 +9585,7 @@ function buildAgentContract() {
8420
9585
  type: "boolean",
8421
9586
  default: false,
8422
9587
  appliesTo: optionAppliesTo("no-install"),
8423
- summary: "setup only: skip installing the dependency before generating (CI, offline, or a lockfile you do not want a package manager to touch)."
9588
+ 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)."
8424
9589
  }
8425
9590
  ],
8426
9591
  presets: PRESETS.map((preset) => ({
@@ -8492,6 +9657,36 @@ function buildAgentContract() {
8492
9657
  "The tool refuses to replace an outDir that it did not create, so a mistyped outDir cannot delete source code.",
8493
9658
  "Nothing is ever written above the repository root; --out-dir may not be the root or an ancestor."
8494
9659
  ],
9660
+ mcp: {
9661
+ command: `npx ${PACKAGE_NAME} mcp`,
9662
+ transport: "stdio",
9663
+ protocolVersions: {
9664
+ modern: [...MODERN_PROTOCOL_VERSIONS],
9665
+ legacy: [...LEGACY_PROTOCOL_VERSIONS],
9666
+ default: DEFAULT_LEGACY_PROTOCOL_VERSION
9667
+ },
9668
+ tools: buildTools().map((tool) => ({
9669
+ name: tool.name,
9670
+ title: tool.title,
9671
+ summary: tool.description,
9672
+ readOnly: tool.annotations.readOnlyHint,
9673
+ parameters: Object.keys(tool.inputSchema.properties),
9674
+ required: tool.inputSchema.required ?? []
9675
+ })),
9676
+ prompts: buildPrompts().map((prompt) => ({
9677
+ name: prompt.name,
9678
+ summary: prompt.description,
9679
+ arguments: prompt.arguments.map((argument) => argument.name)
9680
+ })),
9681
+ resources: [...STATIC_RESOURCE_URIS],
9682
+ resourceTemplates: [...RESOURCE_TEMPLATE_URIS],
9683
+ notes: [
9684
+ "stdout carries JSON-RPC messages only; every note and log line goes to stderr.",
9685
+ "The server speaks both protocol eras: the legacy `initialize` handshake and the stateless revision, which is detected from `server/discover` or from per-request `_meta`.",
9686
+ "Everything it can do is a subset of the CLI. It never installs packages and never writes a config file.",
9687
+ "A `root` argument may name a sub-directory of the server's --cwd, and nothing outside it is readable."
9688
+ ]
9689
+ },
8495
9690
  troubleshooting: [
8496
9691
  {
8497
9692
  symptom: "Command prints nothing and exits 0",
@@ -8523,8 +9718,8 @@ function buildAgentContract() {
8523
9718
  }
8524
9719
 
8525
9720
  // src/agent/install-skill.ts
8526
- import * as fs11 from "fs";
8527
- import * as path12 from "path";
9721
+ import * as fs14 from "fs";
9722
+ import * as path15 from "path";
8528
9723
 
8529
9724
  // src/agent/skill.ts
8530
9725
  var PACKAGE_NAME2 = "codebase-snapshot";
@@ -8553,10 +9748,11 @@ globally, and you do not need to clone anything.
8553
9748
 
8554
9749
  **Pin a version** \u2014 \`npx ${PACKAGE_NAME2}@${GENERATOR_VERSION} <command>\` \u2014 when you
8555
9750
  need the same version on every machine. The bare package name works too: this package ships
8556
- one executable file, published under two names (\`codebase-snapshot\` and \`snap\`), which is what
8557
- lets npm pick it. If a command exits 0 printing nothing,
8558
- the executable was not resolved \u2014 clear the npm cache and retry; do not assume the repository
8559
- is broken.
9751
+ one executable file, published under two names (\`codebase-snapshot\` and \`snapshot\`), which is
9752
+ what lets npm pick it. A global install gives the short command \`snapshot\`; inside a project
9753
+ that has this package installed, \`npx snapshot\` resolves the local copy first. If a command
9754
+ exits 0 printing nothing, the executable was not resolved \u2014 clear the npm cache and retry;
9755
+ do not assume the repository is broken.
8560
9756
 
8561
9757
  ## Step by step
8562
9758
 
@@ -8581,8 +9777,11 @@ is broken.
8581
9777
  does steps 4\u20136 in one go with defaults: it writes the config (an existing one is kept),
8582
9778
  adds the npm scripts and the devDependency, installs with the project's package manager,
8583
9779
  and generates. Use \`--no-install\` when the machine is offline or the user does not want
8584
- their lockfile touched. Prefer the explicit steps below when you need to control any of
8585
- those, or when something fails and you want to see which part.
9780
+ their lockfile touched. \`--autopilot\` does exactly the same thing from the default
9781
+ command (\`npx ${PACKAGE_NAME2}@latest --autopilot\`) and is the form to reach for when a
9782
+ single invocation must both set the project up and produce the snapshot. Prefer the
9783
+ explicit steps below when you need to control any of those, or when something fails and
9784
+ you want to see which part.
8586
9785
 
8587
9786
  4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
8588
9787
  the project shape, then writes a commented config file listing **every** setting at its
@@ -8590,7 +9789,10 @@ is broken.
8590
9789
  the running Node can strip types (Node >= 22.6) **and** the project is an ES module
8591
9790
  (\`"type": "module"\` in package.json); otherwise \`snapshot.config.mjs\`. Pass
8592
9791
  \`--preset <name>\` only if detection is wrong; the preset names are in the contract.
8593
- This step is optional: without a config the tool runs on the same defaults.
9792
+ Skipping this step is safe **at a terminal**: the first \`generate\` asks once, can write
9793
+ the config for you and then generates. Unattended, a \`generate\` with no config exits 2 \u2014
9794
+ run this step, or \`--autopilot\`, or pass \`--no-config\` to run deliberately on the
9795
+ built-in defaults.
8594
9796
 
8595
9797
  5. **Generate.** Run \`npx ${PACKAGE_NAME2}@latest generate\`. Expect exit 0 and a
8596
9798
  \`./snapshot/\` directory. If you need a different location use \`--out-dir <path>\`; if
@@ -8638,6 +9840,10 @@ any of this to get a good first snapshot.
8638
9840
  deleting anything.
8639
9841
  - **Exit 2 is a usage error**, not a failure of the repository. Re-read the contract
8640
9842
  before retrying.
9843
+ - **A first run refuses rather than guessing.** In a project with no config and no previous
9844
+ snapshot, \`generate\` without a terminal exits 2 instead of silently using defaults. That
9845
+ is deliberate: say which set-up you want (\`--autopilot\`, \`init --yes\`, or \`--no-config\`)
9846
+ rather than retrying the same command.
8641
9847
  - **\`--format json\` keeps stdout pure.** Progress goes to stderr, so \`stdout\` is safe to
8642
9848
  pipe into a JSON parser.
8643
9849
 
@@ -8653,6 +9859,14 @@ It never writes \`AGENTS.md\`. That file is the repository's own agent contract
8653
9859
  commands, conventions, house rules \u2014 and this tool has no business editing it. If you want
8654
9860
  this skill in there, run \`npx ${PACKAGE_NAME2}@latest skill\` and paste the text yourself: the
8655
9861
  diff, and the commit, are then yours.
9862
+
9863
+ ## When the client speaks MCP
9864
+
9865
+ If the agent runs inside an MCP client, \`npx ${PACKAGE_NAME2}@latest mcp\` serves this repository
9866
+ as seven tools over stdio instead of shelling out. Its reads come from the snapshot rather than
9867
+ the working tree, so excluded files and redacted values never reach the context. Use
9868
+ \`snapshot_locate\` before \`snapshot_read\`, and \`snapshot_validate\` before trusting a snapshot
9869
+ from an earlier session.
8656
9870
  `;
8657
9871
  }
8658
9872
 
@@ -8663,17 +9877,17 @@ var LEGACY_AGENTS_MARKER = "<!-- codebase-snapshot:skill:start -->";
8663
9877
  function targetPath(cwd, target) {
8664
9878
  switch (target) {
8665
9879
  case "claude":
8666
- return path12.join(cwd, ".claude", "skills", SKILL_NAME, "SKILL.md");
9880
+ return path15.join(cwd, ".claude", "skills", SKILL_NAME, "SKILL.md");
8667
9881
  case "cursor":
8668
- return path12.join(cwd, ".cursor", "rules", `${SKILL_NAME}.mdc`);
9882
+ return path15.join(cwd, ".cursor", "rules", `${SKILL_NAME}.mdc`);
8669
9883
  }
8670
9884
  }
8671
9885
  function assertWritableInsideRepository(cwd, filePath) {
8672
- const root = fs11.realpathSync(cwd);
8673
- const shown = path12.relative(root, filePath) || filePath;
9886
+ const root = fs14.realpathSync(cwd);
9887
+ const shown = path15.relative(root, filePath) || filePath;
8674
9888
  let stat = null;
8675
9889
  try {
8676
- stat = fs11.lstatSync(filePath);
9890
+ stat = fs14.lstatSync(filePath);
8677
9891
  } catch {
8678
9892
  stat = null;
8679
9893
  }
@@ -8689,32 +9903,32 @@ function assertWritableInsideRepository(cwd, filePath) {
8689
9903
  }
8690
9904
  let existing = filePath;
8691
9905
  const missing = [];
8692
- while (!fs11.existsSync(existing)) {
8693
- missing.unshift(path12.basename(existing));
8694
- const parent = path12.dirname(existing);
9906
+ while (!fs14.existsSync(existing)) {
9907
+ missing.unshift(path15.basename(existing));
9908
+ const parent = path15.dirname(existing);
8695
9909
  if (parent === existing) break;
8696
9910
  existing = parent;
8697
9911
  }
8698
- const realAncestor = fs11.realpathSync(existing);
8699
- const resolved = missing.length === 0 ? realAncestor : path12.join(realAncestor, ...missing);
8700
- if (resolved !== root && !resolved.startsWith(`${root}${path12.sep}`)) {
9912
+ const realAncestor = fs14.realpathSync(existing);
9913
+ const resolved = missing.length === 0 ? realAncestor : path15.join(realAncestor, ...missing);
9914
+ if (resolved !== root && !resolved.startsWith(`${root}${path15.sep}`)) {
8701
9915
  throw new Error(
8702
9916
  `${REFUSED}: ${shown} resolves outside the repository (${resolved}). Move it back inside ${root}, or remove the link, and run again.`
8703
9917
  );
8704
9918
  }
8705
9919
  }
8706
9920
  function writeIfChanged(target, filePath, content) {
8707
- const existing = fs11.existsSync(filePath) ? fs11.readFileSync(filePath, "utf8") : null;
9921
+ const existing = fs14.existsSync(filePath) ? fs14.readFileSync(filePath, "utf8") : null;
8708
9922
  if (existing === content) return { target, path: filePath, action: "unchanged" };
8709
- fs11.mkdirSync(path12.dirname(filePath), { recursive: true });
8710
- fs11.writeFileSync(filePath, content, "utf8");
9923
+ fs14.mkdirSync(path15.dirname(filePath), { recursive: true });
9924
+ fs14.writeFileSync(filePath, content, "utf8");
8711
9925
  return { target, path: filePath, action: existing === null ? "created" : "updated" };
8712
9926
  }
8713
9927
  function legacyAgentsNote(cwd) {
8714
- const filePath = path12.join(cwd, "AGENTS.md");
9928
+ const filePath = path15.join(cwd, "AGENTS.md");
8715
9929
  try {
8716
- if (!fs11.existsSync(filePath)) return null;
8717
- if (!fs11.readFileSync(filePath, "utf8").includes(LEGACY_AGENTS_MARKER)) return null;
9930
+ if (!fs14.existsSync(filePath)) return null;
9931
+ if (!fs14.readFileSync(filePath, "utf8").includes(LEGACY_AGENTS_MARKER)) return null;
8718
9932
  } catch {
8719
9933
  return null;
8720
9934
  }
@@ -8726,9 +9940,9 @@ function installSkill(cwd, target = "all") {
8726
9940
  const notes = [];
8727
9941
  const wanted = target === "all" ? ["claude", "cursor"] : [target];
8728
9942
  for (const item of wanted) assertWritableInsideRepository(cwd, targetPath(cwd, item));
8729
- const createsSomething = wanted.some((item) => !fs11.existsSync(targetPath(cwd, item)));
9943
+ const createsSomething = wanted.some((item) => !fs14.existsSync(targetPath(cwd, item)));
8730
9944
  if (createsSomething) {
8731
- assertWritableInsideRepository(cwd, path12.join(cwd, ".gitignore"));
9945
+ assertWritableInsideRepository(cwd, path15.join(cwd, ".gitignore"));
8732
9946
  }
8733
9947
  for (const item of wanted) {
8734
9948
  written.push(writeIfChanged(item, targetPath(cwd, item), skill));
@@ -8744,6 +9958,771 @@ function isSkillTarget(value) {
8744
9958
  return SKILL_TARGETS.includes(value);
8745
9959
  }
8746
9960
 
9961
+ // src/mcp/era.ts
9962
+ function createSession() {
9963
+ return { era: null, protocolVersion: null, ready: false };
9964
+ }
9965
+ function isLegacyVersion(version) {
9966
+ return LEGACY_PROTOCOL_VERSIONS.includes(version);
9967
+ }
9968
+ function isModernVersion(version) {
9969
+ return MODERN_PROTOCOL_VERSIONS.includes(version);
9970
+ }
9971
+ function inspectModernMeta(params) {
9972
+ const raw = params?._meta;
9973
+ if (!isJsonObject(raw)) return { kind: "absent" };
9974
+ const version = raw[META_KEYS.protocolVersion];
9975
+ if (typeof version !== "string" || version.length === 0) {
9976
+ return { kind: "incomplete", missing: META_KEYS.protocolVersion };
9977
+ }
9978
+ const capabilities2 = raw[META_KEYS.clientCapabilities];
9979
+ if (!isJsonObject(capabilities2)) {
9980
+ return { kind: "incomplete", missing: META_KEYS.clientCapabilities };
9981
+ }
9982
+ const info2 = raw[META_KEYS.clientInfo];
9983
+ return {
9984
+ kind: "present",
9985
+ meta: {
9986
+ version,
9987
+ clientCapabilities: capabilities2,
9988
+ clientInfo: isJsonObject(info2) ? info2 : null
9989
+ }
9990
+ };
9991
+ }
9992
+ function unsupportedVersionData(requested) {
9993
+ return { requested, supported: [...SUPPORTED_PROTOCOL_VERSIONS] };
9994
+ }
9995
+ function classifyRequest(method, params, session) {
9996
+ const inspection = inspectModernMeta(params);
9997
+ if (session.era === "modern") {
9998
+ if (inspection.kind === "absent") {
9999
+ return {
10000
+ kind: "error",
10001
+ code: ERROR_CODES.invalidRequest,
10002
+ message: `This request has no _meta. The modern era requires "${META_KEYS.protocolVersion}" and "${META_KEYS.clientCapabilities}" on every request.`
10003
+ };
10004
+ }
10005
+ if (inspection.kind === "incomplete") {
10006
+ return {
10007
+ kind: "error",
10008
+ code: ERROR_CODES.invalidParams,
10009
+ message: `The request _meta is missing the required key "${inspection.missing}".`
10010
+ };
10011
+ }
10012
+ if (!isModernVersion(inspection.meta.version)) {
10013
+ return {
10014
+ kind: "error",
10015
+ code: ERROR_CODES.unsupportedProtocolVersion,
10016
+ message: `Unsupported protocol version "${inspection.meta.version}".`,
10017
+ data: unsupportedVersionData(inspection.meta.version)
10018
+ };
10019
+ }
10020
+ return { kind: "era", era: "modern", version: inspection.meta.version };
10021
+ }
10022
+ if (session.era === "legacy") {
10023
+ return { kind: "era", era: "legacy", version: session.protocolVersion };
10024
+ }
10025
+ if (method === METHOD3.initialize) {
10026
+ return { kind: "era", era: "legacy", version: null };
10027
+ }
10028
+ if (inspection.kind === "present") {
10029
+ if (!isModernVersion(inspection.meta.version)) {
10030
+ return {
10031
+ kind: "error",
10032
+ code: ERROR_CODES.unsupportedProtocolVersion,
10033
+ message: `Unsupported protocol version "${inspection.meta.version}".`,
10034
+ data: unsupportedVersionData(inspection.meta.version)
10035
+ };
10036
+ }
10037
+ return { kind: "era", era: "modern", version: inspection.meta.version };
10038
+ }
10039
+ if (inspection.kind === "incomplete") {
10040
+ return {
10041
+ kind: "error",
10042
+ code: ERROR_CODES.invalidParams,
10043
+ message: `The request _meta is missing the required key "${inspection.missing}".`
10044
+ };
10045
+ }
10046
+ if (method === METHOD3.discover) {
10047
+ return { kind: "era", era: "modern", version: MODERN_PROTOCOL_VERSIONS[0] };
10048
+ }
10049
+ return { kind: "era", era: "legacy", version: null };
10050
+ }
10051
+ function negotiateLegacyVersion(requested) {
10052
+ if (typeof requested === "string" && isLegacyVersion(requested)) {
10053
+ return { version: requested, matched: true };
10054
+ }
10055
+ return { version: DEFAULT_LEGACY_PROTOCOL_VERSION, matched: false };
10056
+ }
10057
+
10058
+ // src/mcp/schema.ts
10059
+ function validateArguments(schema, value) {
10060
+ if (value === void 0) value = {};
10061
+ if (!isJsonObject(value)) return ["arguments must be a JSON object"];
10062
+ const problems = [];
10063
+ const known = new Set(Object.keys(schema.properties));
10064
+ for (const key of Object.keys(value)) {
10065
+ if (!known.has(key) && schema.additionalProperties !== true) {
10066
+ const allowed = [...known].sort().join(", ");
10067
+ problems.push(`unknown argument "${key}" (accepted: ${allowed || "none"})`);
10068
+ }
10069
+ }
10070
+ for (const required of schema.required ?? []) {
10071
+ if (value[required] === void 0) problems.push(`missing required argument "${required}"`);
10072
+ }
10073
+ for (const [key, propertySchema] of Object.entries(schema.properties)) {
10074
+ const argument = value[key];
10075
+ if (argument === void 0) continue;
10076
+ problems.push(...checkValue(key, propertySchema, argument));
10077
+ }
10078
+ return problems;
10079
+ }
10080
+ function checkValue(path18, schema, value) {
10081
+ if (schema.enum && !schema.enum.includes(value)) {
10082
+ return [`"${path18}" must be one of: ${schema.enum.join(", ")}`];
10083
+ }
10084
+ switch (schema.type) {
10085
+ case "string": {
10086
+ if (typeof value !== "string") return [`"${path18}" must be a string`];
10087
+ const problems = [];
10088
+ if (schema.minLength !== void 0 && value.length < schema.minLength) {
10089
+ problems.push(`"${path18}" must be at least ${schema.minLength} character(s)`);
10090
+ }
10091
+ if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
10092
+ problems.push(`"${path18}" must be at most ${schema.maxLength} character(s)`);
10093
+ }
10094
+ return problems;
10095
+ }
10096
+ case "number":
10097
+ case "integer": {
10098
+ if (typeof value !== "number" || Number.isNaN(value)) {
10099
+ return [`"${path18}" must be a number`];
10100
+ }
10101
+ if (schema.type === "integer" && !Number.isInteger(value)) {
10102
+ return [`"${path18}" must be an integer`];
10103
+ }
10104
+ const problems = [];
10105
+ if (schema.minimum !== void 0 && value < schema.minimum) {
10106
+ problems.push(`"${path18}" must be >= ${schema.minimum}`);
10107
+ }
10108
+ if (schema.maximum !== void 0 && value > schema.maximum) {
10109
+ problems.push(`"${path18}" must be <= ${schema.maximum}`);
10110
+ }
10111
+ return problems;
10112
+ }
10113
+ case "boolean":
10114
+ return typeof value === "boolean" ? [] : [`"${path18}" must be a boolean`];
10115
+ case "array": {
10116
+ if (!Array.isArray(value)) return [`"${path18}" must be an array`];
10117
+ if (!schema.items) return [];
10118
+ const problems = [];
10119
+ value.forEach((item, index) => {
10120
+ problems.push(...checkValue(`${path18}[${index}]`, schema.items, item));
10121
+ });
10122
+ return problems;
10123
+ }
10124
+ case "object":
10125
+ return isJsonObject(value) ? [] : [`"${path18}" must be an object`];
10126
+ default:
10127
+ return [];
10128
+ }
10129
+ }
10130
+ function applyDefaults(schema, value) {
10131
+ const out = { ...value };
10132
+ for (const [key, propertySchema] of Object.entries(schema.properties)) {
10133
+ if (out[key] === void 0 && propertySchema.default !== void 0) {
10134
+ out[key] = propertySchema.default;
10135
+ }
10136
+ }
10137
+ return out;
10138
+ }
10139
+
10140
+ // src/mcp/server.ts
10141
+ function capabilities() {
10142
+ return {
10143
+ tools: { listChanged: false },
10144
+ resources: { subscribe: false, listChanged: false },
10145
+ prompts: { listChanged: false }
10146
+ };
10147
+ }
10148
+ function requestIdOf(message) {
10149
+ const raw = message.id;
10150
+ return typeof raw === "string" || typeof raw === "number" ? raw : null;
10151
+ }
10152
+ function createMcpServer(options) {
10153
+ const session = createSession();
10154
+ const toolsByName = new Map(options.tools.map((tool) => [tool.name, tool]));
10155
+ const promptsByName = new Map(options.prompts.map((prompt) => [prompt.name, prompt]));
10156
+ const identity = () => ({
10157
+ name: options.info.name,
10158
+ title: options.info.title,
10159
+ version: options.info.version,
10160
+ websiteUrl: options.info.websiteUrl
10161
+ });
10162
+ const ok = (id, result, modern) => successResponse(id, result, modern, options.info);
10163
+ function handleNotification(method) {
10164
+ if (method === METHOD3.initialized && session.era === "legacy") session.ready = true;
10165
+ }
10166
+ function initialize(id, params) {
10167
+ const negotiation = negotiateLegacyVersion(params?.protocolVersion);
10168
+ session.era = "legacy";
10169
+ session.protocolVersion = negotiation.version;
10170
+ session.ready = true;
10171
+ options.context.log(
10172
+ negotiation.matched ? `mcp: client initialized with protocol ${negotiation.version}` : `mcp: client asked for an unsupported version and was offered ${negotiation.version}`
10173
+ );
10174
+ return {
10175
+ jsonrpc: "2.0",
10176
+ id,
10177
+ result: {
10178
+ protocolVersion: negotiation.version,
10179
+ capabilities: capabilities(),
10180
+ serverInfo: identity(),
10181
+ instructions: options.instructions
10182
+ }
10183
+ };
10184
+ }
10185
+ function discover(id) {
10186
+ session.era = "modern";
10187
+ session.ready = true;
10188
+ return ok(
10189
+ id,
10190
+ {
10191
+ supportedVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
10192
+ capabilities: capabilities(),
10193
+ instructions: options.instructions,
10194
+ // The capability set depends on which repository the server was started for, so the answer
10195
+ // may be cached only by the client that asked, and not at all after this response.
10196
+ cacheScope: "private",
10197
+ ttlMs: 0
10198
+ },
10199
+ true
10200
+ );
10201
+ }
10202
+ function listTools(id, modern) {
10203
+ const tools = options.tools.map((tool) => ({
10204
+ name: tool.name,
10205
+ title: tool.title,
10206
+ description: tool.description,
10207
+ inputSchema: tool.inputSchema,
10208
+ ...tool.outputSchema ? { outputSchema: tool.outputSchema } : {},
10209
+ annotations: tool.annotations
10210
+ }));
10211
+ return ok(id, { tools }, modern);
10212
+ }
10213
+ async function callTool(id, modern, params) {
10214
+ const name = params?.name;
10215
+ if (typeof name !== "string") {
10216
+ return errorResponse(id, ERROR_CODES.invalidParams, 'tools/call requires a "name" string.');
10217
+ }
10218
+ const tool = toolsByName.get(name);
10219
+ if (!tool) {
10220
+ const known = [...toolsByName.keys()].sort().join(", ");
10221
+ return errorResponse(id, ERROR_CODES.invalidParams, `Unknown tool "${name}". Available: ${known}.`);
10222
+ }
10223
+ const rawArguments = params?.arguments ?? {};
10224
+ if (!isJsonObject(rawArguments)) {
10225
+ return errorResponse(id, ERROR_CODES.invalidParams, '"arguments" must be a JSON object.');
10226
+ }
10227
+ const problems = validateArguments(tool.inputSchema, rawArguments);
10228
+ if (problems.length > 0) {
10229
+ return ok(
10230
+ id,
10231
+ {
10232
+ content: [{ type: "text", text: `Invalid arguments for ${tool.name}:
10233
+ - ${problems.join("\n- ")}` }],
10234
+ isError: true
10235
+ },
10236
+ modern
10237
+ );
10238
+ }
10239
+ const args = applyDefaults(tool.inputSchema, rawArguments);
10240
+ let outcome;
10241
+ try {
10242
+ outcome = await tool.handler(args, options.context);
10243
+ } catch (error) {
10244
+ const detail = error instanceof Error ? error.message : String(error);
10245
+ options.context.log(`mcp: tool ${tool.name} failed: ${detail}`);
10246
+ return ok(
10247
+ id,
10248
+ { content: [{ type: "text", text: `${tool.name} failed: ${detail}` }], isError: true },
10249
+ modern
10250
+ );
10251
+ }
10252
+ return ok(
10253
+ id,
10254
+ {
10255
+ content: [{ type: "text", text: outcome.text }],
10256
+ ...outcome.structured ? { structuredContent: outcome.structured } : {},
10257
+ ...outcome.isError ? { isError: true } : {}
10258
+ },
10259
+ modern
10260
+ );
10261
+ }
10262
+ async function listResources(id, modern) {
10263
+ const resources = await options.resources.list(options.context);
10264
+ return ok(id, { resources }, modern);
10265
+ }
10266
+ async function readResource(id, modern, params) {
10267
+ const uri = params?.uri;
10268
+ if (typeof uri !== "string") {
10269
+ return errorResponse(id, ERROR_CODES.invalidParams, 'resources/read requires a "uri" string.');
10270
+ }
10271
+ const contents = await options.resources.read(uri, options.context);
10272
+ if (!contents) {
10273
+ return errorResponse(id, ERROR_CODES.invalidParams, `Unknown resource "${uri}".`);
10274
+ }
10275
+ return ok(id, { contents: [contents] }, modern);
10276
+ }
10277
+ function listPrompts(id, modern) {
10278
+ const prompts = options.prompts.map((prompt) => ({
10279
+ name: prompt.name,
10280
+ ...prompt.title ? { title: prompt.title } : {},
10281
+ description: prompt.description,
10282
+ arguments: prompt.arguments
10283
+ }));
10284
+ return ok(id, { prompts }, modern);
10285
+ }
10286
+ function getPrompt(id, modern, params) {
10287
+ const name = params?.name;
10288
+ if (typeof name !== "string") {
10289
+ return errorResponse(id, ERROR_CODES.invalidParams, 'prompts/get requires a "name" string.');
10290
+ }
10291
+ const prompt = promptsByName.get(name);
10292
+ if (!prompt) {
10293
+ const known = [...promptsByName.keys()].sort().join(", ");
10294
+ return errorResponse(id, ERROR_CODES.invalidParams, `Unknown prompt "${name}". Available: ${known}.`);
10295
+ }
10296
+ const rawArguments = params?.arguments ?? {};
10297
+ if (!isJsonObject(rawArguments)) {
10298
+ return errorResponse(id, ERROR_CODES.invalidParams, '"arguments" must be a JSON object.');
10299
+ }
10300
+ const built = prompt.build(rawArguments);
10301
+ const messages = built.messages.map((message) => ({
10302
+ role: message.role,
10303
+ content: { type: "text", text: message.content.text }
10304
+ }));
10305
+ return ok(id, { description: built.description, messages }, modern);
10306
+ }
10307
+ async function route(name, id, modern, params) {
10308
+ switch (name) {
10309
+ case METHOD3.initialize:
10310
+ return modern ? errorResponse(
10311
+ id,
10312
+ ERROR_CODES.methodNotFound,
10313
+ '"initialize" does not exist in this protocol revision; call "server/discover".'
10314
+ ) : initialize(id, params);
10315
+ case METHOD3.discover:
10316
+ return discover(id);
10317
+ case METHOD3.ping:
10318
+ return modern ? errorResponse(
10319
+ id,
10320
+ ERROR_CODES.methodNotFound,
10321
+ '"ping" was removed in this protocol revision; call "server/discover".'
10322
+ ) : ok(id, {}, modern);
10323
+ default:
10324
+ break;
10325
+ }
10326
+ if (!session.ready) {
10327
+ return errorResponse(
10328
+ id,
10329
+ ERROR_CODES.invalidRequest,
10330
+ 'This connection has not been initialized. Send "initialize" first.'
10331
+ );
10332
+ }
10333
+ switch (name) {
10334
+ case METHOD3.toolsList:
10335
+ return listTools(id, modern);
10336
+ case METHOD3.toolsCall:
10337
+ return callTool(id, modern, params);
10338
+ case METHOD3.resourcesList:
10339
+ return listResources(id, modern);
10340
+ case METHOD3.resourcesRead:
10341
+ return readResource(id, modern, params);
10342
+ case METHOD3.resourceTemplatesList:
10343
+ return ok(id, { resourceTemplates: options.resources.templates() }, modern);
10344
+ case METHOD3.promptsList:
10345
+ return listPrompts(id, modern);
10346
+ case METHOD3.promptsGet:
10347
+ return getPrompt(id, modern, params);
10348
+ default:
10349
+ return errorResponse(id, ERROR_CODES.methodNotFound, `Unknown method "${name}".`);
10350
+ }
10351
+ }
10352
+ async function dispatch(message) {
10353
+ if (!isJsonObject(message)) {
10354
+ return errorResponse(null, ERROR_CODES.parseError, "Message is not a JSON object.");
10355
+ }
10356
+ const isRequest = Object.prototype.hasOwnProperty.call(message, "id");
10357
+ const method = message.method;
10358
+ const params = message.params;
10359
+ if (message.jsonrpc !== "2.0" && isRequest) {
10360
+ return errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, 'Message must carry "jsonrpc": "2.0".');
10361
+ }
10362
+ if (typeof method !== "string" || method.length === 0) {
10363
+ return isRequest ? errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, 'Message must carry a "method" string.') : null;
10364
+ }
10365
+ if (params !== void 0 && !isJsonObject(params)) {
10366
+ return isRequest ? errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, '"params" must be a JSON object when present.') : null;
10367
+ }
10368
+ const named = params;
10369
+ const decision = classifyRequest(method, named, session);
10370
+ const modern = decision.kind === "era" ? decision.era === "modern" : session.era === "modern";
10371
+ if (!isRequest) {
10372
+ if (decision.kind === "era" && session.era === null) {
10373
+ session.era = decision.era;
10374
+ session.protocolVersion = decision.version;
10375
+ }
10376
+ handleNotification(method);
10377
+ return null;
10378
+ }
10379
+ const id = requestIdOf(message);
10380
+ if (id === null) {
10381
+ return errorResponse(null, ERROR_CODES.invalidRequest, 'A request "id" must be a string or a number.');
10382
+ }
10383
+ if (decision.kind === "error") {
10384
+ return errorResponse(id, decision.code, decision.message, decision.data);
10385
+ }
10386
+ if (session.era === null) {
10387
+ session.era = decision.era;
10388
+ session.protocolVersion = decision.version;
10389
+ }
10390
+ return route(method, id, modern, named);
10391
+ }
10392
+ return {
10393
+ session,
10394
+ async handle(message) {
10395
+ try {
10396
+ return await dispatch(message);
10397
+ } catch (error) {
10398
+ const detail = error instanceof Error ? error.message : String(error);
10399
+ options.context.log(`mcp: internal error: ${detail}`);
10400
+ const id = isJsonObject(message) ? requestIdOf(message) : null;
10401
+ return errorResponse(id, ERROR_CODES.internalError, "Internal server error.");
10402
+ }
10403
+ }
10404
+ };
10405
+ }
10406
+
10407
+ // src/mcp/resources.ts
10408
+ import * as fs15 from "fs";
10409
+ import * as path16 from "path";
10410
+ var MAX_RESOURCE_BYTES = 512 * 1024;
10411
+ var PART_URI = /^snapshot:\/\/parts\/(\d+)$/;
10412
+ var REPORT_URIS = /* @__PURE__ */ new Map([
10413
+ ["snapshot://reports/audit", { file: "audit.md", title: "Secret audit and risk findings" }],
10414
+ ["snapshot://reports/tree", { file: "project-tree.md", title: "Project tree" }],
10415
+ ["snapshot://reports/navigation", { file: "navigation.md", title: "Navigation index" }]
10416
+ ]);
10417
+ function formatBytes2(bytes) {
10418
+ if (bytes < 1024) return `${bytes} B`;
10419
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
10420
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
10421
+ }
10422
+ function truncationMarker(originalBytes) {
10423
+ return `
10424
+
10425
+ [truncated: ${formatBytes2(originalBytes)} total. Use the snapshot_read tool for a line window.]
10426
+ `;
10427
+ }
10428
+ function capUtf8(text2, limit) {
10429
+ const size2 = Buffer.byteLength(text2, "utf8");
10430
+ if (size2 <= limit) return { text: text2, truncated: false };
10431
+ const cut = Buffer.from(text2, "utf8").subarray(0, limit).toString("utf8").replace(/\uFFFD+$/, "");
10432
+ return { text: `${cut}${truncationMarker(size2)}`, truncated: true };
10433
+ }
10434
+ async function workspaceFor(context) {
10435
+ const root = resolveRoot(void 0, context);
10436
+ if (!root.ok) return { error: root.message };
10437
+ const opened = await openWorkspace(root.root);
10438
+ if (!opened.ok) return { error: opened.message };
10439
+ return opened.workspace;
10440
+ }
10441
+ function readFileIfPresent(file) {
10442
+ try {
10443
+ return fs15.readFileSync(file, "utf8");
10444
+ } catch {
10445
+ return null;
10446
+ }
10447
+ }
10448
+ function buildResourceProvider() {
10449
+ return {
10450
+ async list(context) {
10451
+ const resources = [
10452
+ {
10453
+ uri: "snapshot://manifest",
10454
+ name: "manifest",
10455
+ title: "Snapshot manifest",
10456
+ description: "Every file in the snapshot with its layer, kind, language, part and hash, plus the reason each excluded file was excluded.",
10457
+ mimeType: "application/json"
10458
+ },
10459
+ {
10460
+ uri: "snapshot://contract",
10461
+ name: "contract",
10462
+ title: "Tool contract",
10463
+ description: "The machine-readable description of the codebase-snapshot command line.",
10464
+ mimeType: "application/json"
10465
+ },
10466
+ {
10467
+ uri: "snapshot://skill",
10468
+ name: "skill",
10469
+ title: "Agent skill",
10470
+ description: "Usage guidance for an agent working with this tool.",
10471
+ mimeType: "text/markdown"
10472
+ }
10473
+ ];
10474
+ for (const [uri, report] of REPORT_URIS) {
10475
+ resources.push({
10476
+ uri,
10477
+ name: report.file.replace(/\.md$/, ""),
10478
+ title: report.title,
10479
+ description: `${report.title} from the most recent snapshot.`,
10480
+ mimeType: "text/markdown"
10481
+ });
10482
+ }
10483
+ const workspace = await workspaceFor(context);
10484
+ if ("error" in workspace) return resources;
10485
+ const read = readManifest3(workspace);
10486
+ if (!read.ok) return resources;
10487
+ for (const part of read.manifest.parts) {
10488
+ resources.push({
10489
+ uri: `snapshot://parts/${part.index}`,
10490
+ name: `part-${String(part.index).padStart(3, "0")}`,
10491
+ title: `Part ${part.index} (${part.fileCount} files, ${formatBytes2(part.byteSize)})`,
10492
+ description: `Part ${part.index} of ${read.manifest.parts.length}: ${part.fileCount} file(s), ${part.lines} line(s), ${formatBytes2(part.byteSize)}.`,
10493
+ mimeType: "text/markdown"
10494
+ });
10495
+ }
10496
+ return resources;
10497
+ },
10498
+ templates() {
10499
+ return [
10500
+ {
10501
+ uriTemplate: "snapshot://parts/{index}",
10502
+ name: "part",
10503
+ title: "Snapshot part",
10504
+ description: "One budgeted part of the snapshot, by its 1-based index.",
10505
+ mimeType: "text/markdown"
10506
+ }
10507
+ ];
10508
+ },
10509
+ async read(uri, context) {
10510
+ if (uri === "snapshot://contract") {
10511
+ const contract = buildAgentContract();
10512
+ return { uri, mimeType: "application/json", text: JSON.stringify(contract, null, 2) };
10513
+ }
10514
+ if (uri === "snapshot://skill") {
10515
+ return { uri, mimeType: "text/markdown", text: renderSkill() };
10516
+ }
10517
+ const workspace = await workspaceFor(context);
10518
+ if ("error" in workspace) return null;
10519
+ const report = REPORT_URIS.get(uri);
10520
+ if (report) {
10521
+ const text2 = readFileIfPresent(
10522
+ path16.resolve(workspace.root, workspace.config.outDir, "reports", report.file)
10523
+ );
10524
+ if (text2 === null) return null;
10525
+ return { uri, mimeType: "text/markdown", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
10526
+ }
10527
+ if (uri === "snapshot://manifest") {
10528
+ const read = readManifest3(workspace);
10529
+ if (!read.ok) return null;
10530
+ const text2 = readFileIfPresent(read.file) ?? JSON.stringify(read.manifest, null, 2);
10531
+ return { uri, mimeType: "application/json", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
10532
+ }
10533
+ const partMatch = PART_URI.exec(uri);
10534
+ if (partMatch) {
10535
+ const read = readManifest3(workspace);
10536
+ if (!read.ok) return null;
10537
+ const index = Number(partMatch[1]);
10538
+ const part = read.manifest.parts.find((candidate) => candidate.index === index);
10539
+ if (!part) return null;
10540
+ const text2 = readFileIfPresent(
10541
+ path16.resolve(workspace.root, workspace.config.outDir, "parts", part.filename)
10542
+ );
10543
+ if (text2 === null) return null;
10544
+ return { uri, mimeType: "text/markdown", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
10545
+ }
10546
+ return null;
10547
+ }
10548
+ };
10549
+ }
10550
+
10551
+ // src/mcp/instructions.ts
10552
+ var SERVER_INSTRUCTIONS = [
10553
+ "codebase-snapshot serves this repository as a size-budgeted snapshot, not as a directory walk.",
10554
+ "",
10555
+ "Work in this order:",
10556
+ "1. snapshot_status \u2014 is there a snapshot here, and has the tree moved since it was built?",
10557
+ "2. snapshot_locate \u2014 find which part holds the code you need.",
10558
+ "3. snapshot_read \u2014 read a window of one file. Never read a part end to end.",
10559
+ "4. snapshot_validate \u2014 before trusting context that came from an earlier conversation.",
10560
+ "",
10561
+ "Rules that matter:",
10562
+ "- Parts are budgeted, not small: a 12-part snapshot is hundreds of kilobytes. Use snapshot_locate first.",
10563
+ "- A file absent from the snapshot has a recorded reason. Ask snapshot_explain, do not assume.",
10564
+ "- snapshot_generate is the only tool that writes. It never installs packages and never writes config.",
10565
+ "- Paths are relative to the repository root the server was started for, and nothing outside it is readable."
10566
+ ].join("\n");
10567
+
10568
+ // src/mcp/stdio.ts
10569
+ var MAX_LINE_BYTES = 32 * 1024 * 1024;
10570
+ function createWriter(output) {
10571
+ return {
10572
+ write(value) {
10573
+ return new Promise((resolve9, reject) => {
10574
+ output.write(`${JSON.stringify(value)}
10575
+ `, (error) => {
10576
+ if (error) reject(error);
10577
+ else resolve9();
10578
+ });
10579
+ });
10580
+ }
10581
+ };
10582
+ }
10583
+ function createLineReader(input, options) {
10584
+ let buffer = "";
10585
+ let discarding = false;
10586
+ let discardedBytes = 0;
10587
+ const deliver = (line) => {
10588
+ if (discarding) {
10589
+ discarding = false;
10590
+ discardedBytes = 0;
10591
+ return;
10592
+ }
10593
+ const trimmed = line.endsWith("\r") ? line.slice(0, -1) : line;
10594
+ if (trimmed.trim().length === 0) return;
10595
+ options.onLine(trimmed);
10596
+ };
10597
+ const drain = () => {
10598
+ let index = buffer.indexOf("\n");
10599
+ while (index >= 0) {
10600
+ const line = buffer.slice(0, index);
10601
+ buffer = buffer.slice(index + 1);
10602
+ deliver(line);
10603
+ index = buffer.indexOf("\n");
10604
+ }
10605
+ };
10606
+ const onData = (chunk) => {
10607
+ if (discarding) {
10608
+ const index = chunk.indexOf("\n");
10609
+ if (index < 0) {
10610
+ discardedBytes += chunk.length;
10611
+ return;
10612
+ }
10613
+ buffer = chunk.slice(index + 1);
10614
+ deliver("");
10615
+ drain();
10616
+ return;
10617
+ }
10618
+ buffer += chunk;
10619
+ if (buffer.length > MAX_LINE_BYTES && buffer.indexOf("\n") < 0) {
10620
+ discarding = true;
10621
+ discardedBytes = buffer.length;
10622
+ buffer = "";
10623
+ options.onOverflow?.(discardedBytes);
10624
+ return;
10625
+ }
10626
+ drain();
10627
+ };
10628
+ const onEnd = () => {
10629
+ if (buffer.length > 0) {
10630
+ const rest = buffer;
10631
+ buffer = "";
10632
+ deliver(rest);
10633
+ }
10634
+ };
10635
+ input.setEncoding("utf8");
10636
+ input.on("data", onData);
10637
+ input.on("end", onEnd);
10638
+ return () => {
10639
+ input.off("data", onData);
10640
+ input.off("end", onEnd);
10641
+ };
10642
+ }
10643
+
10644
+ // src/mcp/run.ts
10645
+ var PROJECT_URL = "https://ankurhalder.github.io/codebase-snapshot/";
10646
+ function createServer(options) {
10647
+ const cwd = options.cwd;
10648
+ const roots = options.roots && options.roots.length > 0 ? options.roots : [cwd];
10649
+ const context = {
10650
+ cwd,
10651
+ roots,
10652
+ log: options.log ?? ((line) => process.stderr.write(`${line}
10653
+ `))
10654
+ };
10655
+ return createMcpServer({
10656
+ info: {
10657
+ name: GENERATOR_NAME,
10658
+ title: "codebase-snapshot",
10659
+ version: GENERATOR_VERSION,
10660
+ websiteUrl: PROJECT_URL
10661
+ },
10662
+ instructions: SERVER_INSTRUCTIONS,
10663
+ tools: buildTools(),
10664
+ resources: buildResourceProvider(),
10665
+ prompts: buildPrompts(),
10666
+ context
10667
+ });
10668
+ }
10669
+ async function runStdioServer(options) {
10670
+ const log = options.log ?? ((line) => process.stderr.write(`${line}
10671
+ `));
10672
+ const server = createServer(options);
10673
+ const writer = createWriter(process.stdout);
10674
+ let writeChain = Promise.resolve();
10675
+ const send = (message) => {
10676
+ writeChain = writeChain.then(() => writer.write(message)).catch((error) => {
10677
+ log(`mcp: could not write a response: ${error instanceof Error ? error.message : String(error)}`);
10678
+ });
10679
+ return writeChain;
10680
+ };
10681
+ let inFlight = 0;
10682
+ let inputEnded = false;
10683
+ let resolveDone = () => void 0;
10684
+ const finished = new Promise((resolve9) => {
10685
+ resolveDone = resolve9;
10686
+ });
10687
+ const finishIfIdle = () => {
10688
+ if (inputEnded && inFlight === 0) resolveDone(0);
10689
+ };
10690
+ const handleLine = async (line) => {
10691
+ let message;
10692
+ try {
10693
+ message = JSON.parse(line);
10694
+ } catch {
10695
+ await send(errorResponse(null, ERROR_CODES.parseError, "Message is not valid JSON."));
10696
+ return;
10697
+ }
10698
+ const response = await server.handle(message);
10699
+ if (response) await send(response);
10700
+ };
10701
+ const stopReading = createLineReader(process.stdin, {
10702
+ onOverflow: (bytes) => log(`mcp: discarded an oversized message (${bytes} bytes)`),
10703
+ onLine: (line) => {
10704
+ inFlight += 1;
10705
+ void handleLine(line).finally(() => {
10706
+ inFlight -= 1;
10707
+ finishIfIdle();
10708
+ });
10709
+ }
10710
+ });
10711
+ const onEnd = () => {
10712
+ inputEnded = true;
10713
+ finishIfIdle();
10714
+ };
10715
+ process.stdin.on("end", onEnd);
10716
+ process.stdin.on("close", onEnd);
10717
+ log(`mcp: serving ${options.cwd} over stdio`);
10718
+ const code = await finished;
10719
+ stopReading();
10720
+ process.stdin.off("end", onEnd);
10721
+ process.stdin.off("close", onEnd);
10722
+ await writeChain;
10723
+ return code;
10724
+ }
10725
+
8747
10726
  // src/cli.ts
8748
10727
  var VERSION = GENERATOR_VERSION;
8749
10728
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -8757,7 +10736,8 @@ var COMMAND_ALIASES = /* @__PURE__ */ new Map([
8757
10736
  ["explain", "explain"],
8758
10737
  ["diff", "diff"],
8759
10738
  ["describe", "describe"],
8760
- ["skill", "skill"]
10739
+ ["skill", "skill"],
10740
+ ["mcp", "mcp"]
8761
10741
  ]);
8762
10742
  var COMMAND_ARITY = {
8763
10743
  generate: 0,
@@ -8768,7 +10748,8 @@ var COMMAND_ARITY = {
8768
10748
  explain: 1,
8769
10749
  diff: 2,
8770
10750
  describe: 0,
8771
- skill: 0
10751
+ skill: 0,
10752
+ mcp: 0
8772
10753
  };
8773
10754
  var CLI_OPTIONS = {
8774
10755
  cwd: { type: "string" },
@@ -8791,6 +10772,7 @@ var CLI_OPTIONS = {
8791
10772
  exclude: { type: "string" },
8792
10773
  yes: { type: "boolean" },
8793
10774
  force: { type: "boolean" },
10775
+ autopilot: { type: "boolean" },
8794
10776
  install: { type: "boolean" },
8795
10777
  "no-install": { type: "boolean" },
8796
10778
  target: { type: "string" },
@@ -8806,6 +10788,14 @@ Production-grade LLM codebase snapshot generator, auditor, and AST partitioner
8806
10788
  Usage:
8807
10789
  codebase-snapshot [command] [options]
8808
10790
 
10791
+ First run:
10792
+ codebase-snapshot No config yet: asks once, sets the project up, generates.
10793
+ A configured project just generates. It never falls back
10794
+ to defaults it was not given.
10795
+ codebase-snapshot --autopilot The same without questions: detect the preset, write the
10796
+ config, add the npm scripts and devDependency, install,
10797
+ then generate.
10798
+
8809
10799
  Commands:
8810
10800
  setup Everything at once: config, scripts, dependency, install, generate
8811
10801
  generate (default) Generate a complete partitioned codebase snapshot
@@ -8816,6 +10806,7 @@ Commands:
8816
10806
  diff <a> <b> Compare two snapshots (directories or manifest.json files)
8817
10807
  describe Print the machine-readable contract (--format json for agents)
8818
10808
  skill Print the bundled agent skill; --install writes it into the repo
10809
+ mcp Serve this repository to an MCP client over stdio
8819
10810
 
8820
10811
  Options:
8821
10812
  --cwd <path> Target repository directory (default: current directory)
@@ -8838,6 +10829,12 @@ Options:
8838
10829
  auto: git when usable. git: require git. walk: read
8839
10830
  .gitignore directly (escape hatch for a broken .git)
8840
10831
 
10832
+ generate only:
10833
+ --autopilot Set the project up without asking, then generate: write the
10834
+ config and .gitignore, add the npm scripts and devDependency,
10835
+ and install with the project's own package manager.
10836
+ --no-install With --autopilot: skip the dependency install.
10837
+
8841
10838
  init only:
8842
10839
  --preset <name> Force a project preset instead of detecting one
8843
10840
  --include <globs> Comma-separated allow-list
@@ -8917,10 +10914,10 @@ function parseCliArgs(argv) {
8917
10914
  }
8918
10915
  if (values.cwd !== void 0) {
8919
10916
  const target = String(values.cwd);
8920
- if (!fs12.existsSync(target)) {
10917
+ if (!fs16.existsSync(target)) {
8921
10918
  throw new UsageError(`--cwd directory does not exist: ${target}`);
8922
10919
  }
8923
- if (!fs12.statSync(target).isDirectory()) {
10920
+ if (!fs16.statSync(target).isDirectory()) {
8924
10921
  throw new UsageError(`--cwd is not a directory: ${target}`);
8925
10922
  }
8926
10923
  }
@@ -9040,8 +11037,17 @@ function parseCliArgs(argv) {
9040
11037
  `--target must be one of: ${SKILL_TARGETS.join(", ")} (received "${String(values.target)}").`
9041
11038
  );
9042
11039
  }
11040
+ if (values["no-install"] === true && command === "generate" && values.autopilot !== true) {
11041
+ throw new UsageError("--no-install only applies to `setup`, or to `generate --autopilot`.");
11042
+ }
11043
+ if (values.autopilot === true && values["no-config"] === true) {
11044
+ throw new UsageError(
11045
+ "--autopilot sets a project up and writes a config; --no-config ignores config files. Pass one or the other."
11046
+ );
11047
+ }
9043
11048
  return {
9044
11049
  command,
11050
+ explicitCommand: firstPositional !== void 0 || explicitMode !== void 0,
9045
11051
  args,
9046
11052
  format: rawFormat,
9047
11053
  dryRun: values["dry-run"] === true,
@@ -9053,10 +11059,11 @@ function parseCliArgs(argv) {
9053
11059
  exclude: values.exclude === void 0 ? void 0 : String(values.exclude),
9054
11060
  yes: values.yes === true,
9055
11061
  force: values.force === true,
11062
+ autopilot: values.autopilot === true,
9056
11063
  install: values.install === true,
9057
11064
  noInstall: values["no-install"] === true,
9058
11065
  target: values.target === void 0 ? void 0 : String(values.target),
9059
- cwd: values.cwd === void 0 ? process.cwd() : path13.resolve(String(values.cwd)),
11066
+ cwd: values.cwd === void 0 ? process.cwd() : path17.resolve(String(values.cwd)),
9060
11067
  outDir: values["out-dir"] === void 0 ? void 0 : String(values["out-dir"]),
9061
11068
  includeMonolith: values["include-monolith"] === true || onlyNamesMonolith === true ? true : void 0,
9062
11069
  includeSensitiveFiles: values["include-sensitive-files"] === true ? true : void 0,
@@ -9113,26 +11120,29 @@ function splitList(value) {
9113
11120
  return value.split(",").map((entry) => entry.trim()).filter(Boolean);
9114
11121
  }
9115
11122
  function displayOutPath(cwd, outDir, ...segments) {
9116
- const absolute = path13.resolve(cwd, outDir, ...segments);
9117
- const relative8 = path13.relative(cwd, absolute);
9118
- if (relative8 && !relative8.startsWith("..") && !path13.isAbsolute(relative8)) return relative8;
11123
+ const absolute = path17.resolve(cwd, outDir, ...segments);
11124
+ const relative11 = path17.relative(cwd, absolute);
11125
+ if (relative11 && !relative11.startsWith("..") && !path17.isAbsolute(relative11)) return relative11;
9119
11126
  return absolute;
9120
11127
  }
9121
11128
  function nextStepHint(cwd, scriptsAddedNow) {
9122
11129
  let hasScript = scriptsAddedNow;
9123
11130
  if (!hasScript) {
9124
11131
  try {
9125
- const parsed = JSON.parse(fs12.readFileSync(path13.join(cwd, "package.json"), "utf8"));
11132
+ const parsed = JSON.parse(fs16.readFileSync(path17.join(cwd, "package.json"), "utf8"));
9126
11133
  hasScript = typeof parsed.scripts?.snapshot === "string";
9127
11134
  } catch {
9128
11135
  hasScript = false;
9129
11136
  }
9130
11137
  }
9131
11138
  if (!hasScript) return `npx ${GENERATOR_NAME} generate`;
9132
- const bin = path13.join(cwd, "node_modules", ".bin", "codebase-snapshot");
9133
- const installed = fs12.existsSync(bin) || fs12.existsSync(`${bin}.cmd`);
11139
+ const bin = path17.join(cwd, "node_modules", ".bin", "codebase-snapshot");
11140
+ const installed = fs16.existsSync(bin) || fs16.existsSync(`${bin}.cmd`);
9134
11141
  return installed ? "npm run snapshot" : "npm install && npm run snapshot";
9135
11142
  }
11143
+ function presetSummary(init) {
11144
+ return init.reason === null ? `preset: ${init.preset}` : `preset: ${init.preset}, because ${init.reason}`;
11145
+ }
9136
11146
  async function runInitCommand(invocation) {
9137
11147
  try {
9138
11148
  const result = await runInit({
@@ -9157,7 +11167,7 @@ async function runInitCommand(invocation) {
9157
11167
  info(result.configText);
9158
11168
  return 0;
9159
11169
  }
9160
- info(`\u2705 Created ${path13.basename(result.configPath)} (preset: ${result.preset})`);
11170
+ info(`\u2705 Created ${path17.basename(result.configPath)} (${presetSummary(result)})`);
9161
11171
  if (result.scripts.length > 0) info(` Added npm scripts: ${result.scripts.join(", ")}`);
9162
11172
  if (result.dependency.added && result.dependency.range !== null) {
9163
11173
  info(` Added devDependency: ${result.dependency.name} ${result.dependency.range}`);
@@ -9177,7 +11187,7 @@ async function runInitCommand(invocation) {
9177
11187
  }
9178
11188
  }
9179
11189
  function detectPackageManager(cwd) {
9180
- const has = (name) => fs12.existsSync(path13.join(cwd, name));
11190
+ const has = (name) => fs16.existsSync(path17.join(cwd, name));
9181
11191
  if (has("bun.lockb") || has("bun.lock")) return { name: "bun", command: "bun", args: ["install"] };
9182
11192
  if (has("pnpm-lock.yaml")) return { name: "pnpm", command: "pnpm", args: ["install"] };
9183
11193
  if (has("yarn.lock")) return { name: "yarn", command: "yarn", args: ["install"] };
@@ -9228,8 +11238,9 @@ async function captureStdout(fn) {
9228
11238
  process.stdout.write = original;
9229
11239
  }
9230
11240
  }
9231
- async function runSetupCommand(invocation) {
11241
+ async function runSetupCommand(invocation, options = {}) {
9232
11242
  try {
11243
+ const interactive = options.interactive === true;
9233
11244
  const init = await runInit({
9234
11245
  cwd: invocation.cwd,
9235
11246
  preset: invocation.preset,
@@ -9239,7 +11250,7 @@ async function runSetupCommand(invocation) {
9239
11250
  exclude: splitList(invocation.exclude),
9240
11251
  skipArtifacts: invocation.skipArtifacts,
9241
11252
  includeSensitiveFiles: invocation.includeSensitiveFiles,
9242
- yes: true,
11253
+ yes: !interactive,
9243
11254
  force: invocation.force,
9244
11255
  reuseConfig: true,
9245
11256
  dryRun: invocation.dryRun
@@ -9266,7 +11277,7 @@ async function runSetupCommand(invocation) {
9266
11277
  }
9267
11278
  info("\n\u{1F9EA} Dry run \u2014 setup would:");
9268
11279
  info(
9269
- ` 1. ${init.reused ? `keep ${path13.basename(init.configPath)}` : `write ${path13.basename(init.configPath)} (preset: ${init.preset})`}`
11280
+ ` 1. ${init.reused ? `keep ${path17.basename(init.configPath)}` : `write ${path17.basename(init.configPath)} (preset: ${init.preset})`}`
9270
11281
  );
9271
11282
  info(" 2. add the npm scripts, the devDependency and the .gitignore entries");
9272
11283
  if (manager !== null) info(` 3. install with ${manager}`);
@@ -9276,11 +11287,11 @@ async function runSetupCommand(invocation) {
9276
11287
  return 0;
9277
11288
  }
9278
11289
  if (invocation.format !== "json") {
9279
- info(`\u{1F680} Setup for: ${invocation.cwd}`);
11290
+ if (!interactive) info(`\u{1F680} Setup for: ${invocation.cwd}`);
9280
11291
  if (init.reused) {
9281
- info(`\u267B Keeping ${path13.basename(init.configPath)} (preset: ${init.preset})`);
11292
+ info(`\u267B Keeping ${path17.basename(init.configPath)} (${presetSummary(init)})`);
9282
11293
  } else {
9283
- info(`\u2705 Created ${path13.basename(init.configPath)} (preset: ${init.preset})`);
11294
+ info(`\u2705 Created ${path17.basename(init.configPath)} (${presetSummary(init)})`);
9284
11295
  }
9285
11296
  if (init.scripts.length > 0) info(` Added npm scripts: ${init.scripts.join(", ")}`);
9286
11297
  if (init.dependency.added && init.dependency.range !== null) {
@@ -9309,6 +11320,7 @@ async function runSetupCommand(invocation) {
9309
11320
  if ((invocation.skipArtifacts ?? []).length > 0) {
9310
11321
  generateArgs.push("--skip-artifacts", (invocation.skipArtifacts ?? []).join(","));
9311
11322
  }
11323
+ if (invocation.watch) generateArgs.push("--watch");
9312
11324
  if (invocation.format === "json") {
9313
11325
  const generated = await captureStdout(() => main(generateArgs));
9314
11326
  let manifest = null;
@@ -9348,6 +11360,24 @@ async function runSetupCommand(invocation) {
9348
11360
  return fail(invocation.format, err.message);
9349
11361
  }
9350
11362
  }
11363
+ async function firstRun(invocation) {
11364
+ const plan = planFirstRun({
11365
+ hasConfig: findConfigFile(invocation.cwd) !== null,
11366
+ hasPriorSnapshot: hasPriorSnapshot(invocation.cwd, invocation.outDir ?? DEFAULT_ANSWERS.outDir),
11367
+ // A machine must never be asked a question, and `--format json` is how a machine says so.
11368
+ interactive: invocation.format === "text" && isInteractive(defaultIO()),
11369
+ autopilot: invocation.autopilot,
11370
+ emptyTarget: isEffectivelyEmpty(invocation.cwd)
11371
+ });
11372
+ if (plan.action === "generate") return null;
11373
+ if (plan.action === "refuse") {
11374
+ return fail(invocation.format, firstRunMessage(plan.reason, invocation.cwd), plan.exitCode);
11375
+ }
11376
+ info(
11377
+ plan.interactive ? "\u2699 This project has no snapshot configuration yet." : "\u2699 --autopilot: setting this project up before generating."
11378
+ );
11379
+ return await runSetupCommand(invocation, { interactive: plan.interactive });
11380
+ }
9351
11381
  function runSkillCommand(invocation) {
9352
11382
  if (!invocation.install) {
9353
11383
  if (invocation.format === "json") {
@@ -9369,7 +11399,7 @@ function runSkillCommand(invocation) {
9369
11399
  }
9370
11400
  info(`\u2705 Agent skill installed (${SKILL_NAME}):`);
9371
11401
  for (const item of result.written) {
9372
- const shown = path13.relative(invocation.cwd, item.path) || item.path;
11402
+ const shown = path17.relative(invocation.cwd, item.path) || item.path;
9373
11403
  info(` - ${item.action.padEnd(9)} ${shown}`);
9374
11404
  }
9375
11405
  if (result.gitignored.length > 0) {
@@ -9464,9 +11494,16 @@ ${USAGE}
9464
11494
  if (invocation.command === "skill") {
9465
11495
  return runSkillCommand(invocation);
9466
11496
  }
11497
+ if (invocation.command === "mcp") {
11498
+ return await runStdioServer({ cwd: path17.resolve(invocation.cwd) });
11499
+ }
9467
11500
  if (invocation.command === "setup") {
9468
11501
  return runSetupCommand(invocation);
9469
11502
  }
11503
+ if (invocation.command === "generate" && !invocation.noConfig && (!invocation.explicitCommand || invocation.autopilot)) {
11504
+ const decided = await firstRun(invocation);
11505
+ if (decided !== null) return decided;
11506
+ }
9470
11507
  const skipConfig = invocation.noConfig || invocation.command === "diff";
9471
11508
  const loaded = skipConfig ? { config: null, warnings: [], errors: [], file: null } : await loadConfigFileDetailed(invocation.cwd);
9472
11509
  if (invocation.noConfig) info("\u2699 --no-config: using built-in defaults, config file ignored");
@@ -9475,7 +11512,7 @@ ${USAGE}
9475
11512
  warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
9476
11513
  return 1;
9477
11514
  }
9478
- if (loaded.file) info(`\u2699 Using config ${path13.relative(process.cwd(), loaded.file) || loaded.file}`);
11515
+ if (loaded.file) info(`\u2699 Using config ${path17.relative(process.cwd(), loaded.file) || loaded.file}`);
9479
11516
  const resolved = resolveConfig(
9480
11517
  loaded.config,
9481
11518
  {
@@ -9497,10 +11534,10 @@ ${USAGE}
9497
11534
  if (invocation.command === "validate") {
9498
11535
  const result = validateSnapshot(resolved);
9499
11536
  if (result.valid) {
9500
- const manifestPath = path13.resolve(resolved.cwd, resolved.outDir, "manifest.json");
11537
+ const manifestPath = path17.resolve(resolved.cwd, resolved.outDir, "manifest.json");
9501
11538
  let manifest;
9502
11539
  try {
9503
- manifest = JSON.parse(fs12.readFileSync(manifestPath, "utf8"));
11540
+ manifest = JSON.parse(fs16.readFileSync(manifestPath, "utf8"));
9504
11541
  } catch {
9505
11542
  manifest = void 0;
9506
11543
  }
@@ -9513,13 +11550,13 @@ ${USAGE}
9513
11550
  return result.valid ? 0 : 1;
9514
11551
  }
9515
11552
  if (invocation.command === "report") {
9516
- const manifestPath = path13.resolve(resolved.cwd, resolved.outDir, "manifest.json");
9517
- if (!fs12.existsSync(manifestPath)) {
11553
+ const manifestPath = path17.resolve(resolved.cwd, resolved.outDir, "manifest.json");
11554
+ if (!fs16.existsSync(manifestPath)) {
9518
11555
  return fail(invocation.format, `Manifest not found at ${manifestPath}. Run generate first.`);
9519
11556
  }
9520
11557
  let manifest;
9521
11558
  try {
9522
- manifest = JSON.parse(fs12.readFileSync(manifestPath, "utf8"));
11559
+ manifest = JSON.parse(fs16.readFileSync(manifestPath, "utf8"));
9523
11560
  } catch (err) {
9524
11561
  return fail(invocation.format, `Could not read ${manifestPath}: ${err.message}`);
9525
11562
  }
@@ -9683,20 +11720,20 @@ function finishWatchRun(state, now) {
9683
11720
  };
9684
11721
  }
9685
11722
  function comparablePath(value) {
9686
- const absolute = path13.resolve(value);
11723
+ const absolute = path17.resolve(value);
9687
11724
  return process.platform === "win32" ? absolute.toLowerCase() : absolute;
9688
11725
  }
9689
11726
  function isSameOrInside(parent, candidate) {
9690
- const relative8 = path13.relative(parent, candidate);
9691
- return relative8 === "" || relative8 !== ".." && !relative8.startsWith(`..${path13.sep}`) && !path13.isAbsolute(relative8);
11727
+ const relative11 = path17.relative(parent, candidate);
11728
+ return relative11 === "" || relative11 !== ".." && !relative11.startsWith(`..${path17.sep}`) && !path17.isAbsolute(relative11);
9692
11729
  }
9693
11730
  function isGeneratedWatchPath(candidate, paths) {
9694
- const absolute = comparablePath(path13.resolve(paths.cwd, candidate));
9695
- const outDir = comparablePath(path13.resolve(paths.cwd, paths.outDir));
9696
- const tmpDir = comparablePath(path13.resolve(paths.cwd, paths.tmpDir));
11731
+ const absolute = comparablePath(path17.resolve(paths.cwd, candidate));
11732
+ const outDir = comparablePath(path17.resolve(paths.cwd, paths.outDir));
11733
+ const tmpDir = comparablePath(path17.resolve(paths.cwd, paths.tmpDir));
9697
11734
  if (isSameOrInside(outDir, absolute) || isSameOrInside(tmpDir, absolute)) return true;
9698
- const parent = path13.dirname(absolute);
9699
- if (parent === path13.dirname(outDir) && path13.basename(absolute).startsWith(`${path13.basename(outDir)}.backup-`)) {
11735
+ const parent = path17.dirname(absolute);
11736
+ if (parent === path17.dirname(outDir) && path17.basename(absolute).startsWith(`${path17.basename(outDir)}.backup-`)) {
9700
11737
  return true;
9701
11738
  }
9702
11739
  return absolute.split(/[\\/]+/).some((segment) => VCS_SEGMENTS.has(segment));
@@ -9713,8 +11750,8 @@ function decideWatchEvent(state, event, paths) {
9713
11750
  return { state, action: "schedule" };
9714
11751
  }
9715
11752
  async function runWatchLoop(invocation, resolved) {
9716
- const outDir = path13.resolve(resolved.cwd, resolved.outDir);
9717
- const tmpDir = path13.resolve(resolved.cwd, resolved.tmpDir);
11753
+ const outDir = path17.resolve(resolved.cwd, resolved.outDir);
11754
+ const tmpDir = path17.resolve(resolved.cwd, resolved.tmpDir);
9718
11755
  const paths = { cwd: resolved.cwd, outDir, tmpDir };
9719
11756
  let state = initialWatchState();
9720
11757
  let timer = null;
@@ -9779,11 +11816,11 @@ async function runWatchLoop(invocation, resolved) {
9779
11816
  };
9780
11817
  info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
9781
11818
  void regenerate();
9782
- const watcher = fs12.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
11819
+ const watcher = fs16.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
9783
11820
  const decision = decideWatchEvent(
9784
11821
  state,
9785
11822
  {
9786
- path: filename ? path13.resolve(resolved.cwd, filename.toString()) : null,
11823
+ path: filename ? path17.resolve(resolved.cwd, filename.toString()) : null,
9787
11824
  now: performance.now()
9788
11825
  },
9789
11826
  paths
@@ -9798,12 +11835,12 @@ async function runWatchLoop(invocation, resolved) {
9798
11835
  }
9799
11836
  if (decision.action === "schedule") schedule();
9800
11837
  });
9801
- await new Promise((resolve6) => {
11838
+ await new Promise((resolve9) => {
9802
11839
  const stop = () => {
9803
11840
  stopped = true;
9804
11841
  if (timer) clearTimeout(timer);
9805
11842
  watcher.close();
9806
- resolve6();
11843
+ resolve9();
9807
11844
  };
9808
11845
  process.once("SIGINT", stop);
9809
11846
  process.once("SIGTERM", stop);
@@ -9811,7 +11848,7 @@ async function runWatchLoop(invocation, resolved) {
9811
11848
  return 0;
9812
11849
  }
9813
11850
  var entryFile = process.argv[1] ?? "";
9814
- var entryName = path13.basename(entryFile).toLowerCase();
11851
+ var entryName = path17.basename(entryFile).toLowerCase();
9815
11852
  var invokedDirectly = entryName === "cli.js" || entryName === "cli.cjs";
9816
11853
  if (invokedDirectly) {
9817
11854
  void main().then(