codebase-snapshot 0.9.1 → 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.
@@ -0,0 +1,2577 @@
1
+ import { GENERATOR_VERSION, MIN_MAX_PART_BYTES, SCHEMA_VERSION, GENERATOR_NAME, ARTIFACT_NAMES, PRESETS, generateSnapshot, validateSnapshot, explainPath, renderExplainText, describeManifestShapeProblem, loadConfigFileDetailed, parseByteSize, resolveConfig } from './chunk-FCWD4LY7.js';
2
+ import * as fs from 'fs';
3
+ import * as path2 from 'path';
4
+
5
+ // src/mcp/protocol.ts
6
+ var LEGACY_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18"];
7
+ var MODERN_PROTOCOL_VERSIONS = ["2026-07-28"];
8
+ var SUPPORTED_PROTOCOL_VERSIONS = [
9
+ ...MODERN_PROTOCOL_VERSIONS,
10
+ ...LEGACY_PROTOCOL_VERSIONS
11
+ ];
12
+ var DEFAULT_LEGACY_PROTOCOL_VERSION = LEGACY_PROTOCOL_VERSIONS[0];
13
+ var ERROR_CODES = {
14
+ parseError: -32700,
15
+ invalidRequest: -32600,
16
+ methodNotFound: -32601,
17
+ invalidParams: -32602,
18
+ internalError: -32603,
19
+ unsupportedProtocolVersion: -32022
20
+ };
21
+ var METHOD = {
22
+ initialize: "initialize",
23
+ initialized: "notifications/initialized",
24
+ discover: "server/discover",
25
+ ping: "ping",
26
+ toolsList: "tools/list",
27
+ toolsCall: "tools/call",
28
+ resourcesList: "resources/list",
29
+ resourcesRead: "resources/read",
30
+ resourceTemplatesList: "resources/templates/list",
31
+ promptsList: "prompts/list",
32
+ promptsGet: "prompts/get",
33
+ cancelled: "notifications/cancelled"
34
+ };
35
+ var META_KEYS = {
36
+ protocolVersion: "io.modelcontextprotocol/protocolVersion",
37
+ clientCapabilities: "io.modelcontextprotocol/clientCapabilities",
38
+ clientInfo: "io.modelcontextprotocol/clientInfo",
39
+ serverInfo: "io.modelcontextprotocol/serverInfo"};
40
+ function isJsonObject(value) {
41
+ return typeof value === "object" && value !== null && !Array.isArray(value);
42
+ }
43
+ function serverInfoMeta(info) {
44
+ const value = { name: info.name, version: info.version };
45
+ if (info.title) value.title = info.title;
46
+ return { [META_KEYS.serverInfo]: value };
47
+ }
48
+ function successResponse(id, result, modern, info) {
49
+ if (!modern) return { jsonrpc: "2.0", id, result };
50
+ const withMeta = {
51
+ ...result,
52
+ resultType: "complete",
53
+ _meta: { ...isJsonObject(result._meta) ? result._meta : {}, ...serverInfoMeta(info) }
54
+ };
55
+ return { jsonrpc: "2.0", id, result: withMeta };
56
+ }
57
+ function errorResponse(id, code, message, data) {
58
+ const error = { code, message };
59
+ if (data !== void 0) error.data = data;
60
+ return { jsonrpc: "2.0", id, error };
61
+ }
62
+
63
+ // src/mcp/era.ts
64
+ function createSession() {
65
+ return { era: null, protocolVersion: null, ready: false };
66
+ }
67
+ function isLegacyVersion(version) {
68
+ return LEGACY_PROTOCOL_VERSIONS.includes(version);
69
+ }
70
+ function isModernVersion(version) {
71
+ return MODERN_PROTOCOL_VERSIONS.includes(version);
72
+ }
73
+ function inspectModernMeta(params) {
74
+ const raw = params?._meta;
75
+ if (!isJsonObject(raw)) return { kind: "absent" };
76
+ const version = raw[META_KEYS.protocolVersion];
77
+ if (typeof version !== "string" || version.length === 0) {
78
+ return { kind: "incomplete", missing: META_KEYS.protocolVersion };
79
+ }
80
+ const capabilities2 = raw[META_KEYS.clientCapabilities];
81
+ if (!isJsonObject(capabilities2)) {
82
+ return { kind: "incomplete", missing: META_KEYS.clientCapabilities };
83
+ }
84
+ const info = raw[META_KEYS.clientInfo];
85
+ return {
86
+ kind: "present",
87
+ meta: {
88
+ version,
89
+ clientCapabilities: capabilities2,
90
+ clientInfo: isJsonObject(info) ? info : null
91
+ }
92
+ };
93
+ }
94
+ function unsupportedVersionData(requested) {
95
+ return { requested, supported: [...SUPPORTED_PROTOCOL_VERSIONS] };
96
+ }
97
+ function classifyRequest(method, params, session) {
98
+ const inspection = inspectModernMeta(params);
99
+ if (session.era === "modern") {
100
+ if (inspection.kind === "absent") {
101
+ return {
102
+ kind: "error",
103
+ code: ERROR_CODES.invalidRequest,
104
+ message: `This request has no _meta. The modern era requires "${META_KEYS.protocolVersion}" and "${META_KEYS.clientCapabilities}" on every request.`
105
+ };
106
+ }
107
+ if (inspection.kind === "incomplete") {
108
+ return {
109
+ kind: "error",
110
+ code: ERROR_CODES.invalidParams,
111
+ message: `The request _meta is missing the required key "${inspection.missing}".`
112
+ };
113
+ }
114
+ if (!isModernVersion(inspection.meta.version)) {
115
+ return {
116
+ kind: "error",
117
+ code: ERROR_CODES.unsupportedProtocolVersion,
118
+ message: `Unsupported protocol version "${inspection.meta.version}".`,
119
+ data: unsupportedVersionData(inspection.meta.version)
120
+ };
121
+ }
122
+ return { kind: "era", era: "modern", version: inspection.meta.version };
123
+ }
124
+ if (session.era === "legacy") {
125
+ return { kind: "era", era: "legacy", version: session.protocolVersion };
126
+ }
127
+ if (method === METHOD.initialize) {
128
+ return { kind: "era", era: "legacy", version: null };
129
+ }
130
+ if (inspection.kind === "present") {
131
+ if (!isModernVersion(inspection.meta.version)) {
132
+ return {
133
+ kind: "error",
134
+ code: ERROR_CODES.unsupportedProtocolVersion,
135
+ message: `Unsupported protocol version "${inspection.meta.version}".`,
136
+ data: unsupportedVersionData(inspection.meta.version)
137
+ };
138
+ }
139
+ return { kind: "era", era: "modern", version: inspection.meta.version };
140
+ }
141
+ if (inspection.kind === "incomplete") {
142
+ return {
143
+ kind: "error",
144
+ code: ERROR_CODES.invalidParams,
145
+ message: `The request _meta is missing the required key "${inspection.missing}".`
146
+ };
147
+ }
148
+ if (method === METHOD.discover) {
149
+ return { kind: "era", era: "modern", version: MODERN_PROTOCOL_VERSIONS[0] };
150
+ }
151
+ return { kind: "era", era: "legacy", version: null };
152
+ }
153
+ function negotiateLegacyVersion(requested) {
154
+ if (typeof requested === "string" && isLegacyVersion(requested)) {
155
+ return { version: requested, matched: true };
156
+ }
157
+ return { version: DEFAULT_LEGACY_PROTOCOL_VERSION, matched: false };
158
+ }
159
+
160
+ // src/mcp/schema.ts
161
+ function validateArguments(schema, value) {
162
+ if (value === void 0) value = {};
163
+ if (!isJsonObject(value)) return ["arguments must be a JSON object"];
164
+ const problems = [];
165
+ const known = new Set(Object.keys(schema.properties));
166
+ for (const key of Object.keys(value)) {
167
+ if (!known.has(key) && schema.additionalProperties !== true) {
168
+ const allowed = [...known].sort().join(", ");
169
+ problems.push(`unknown argument "${key}" (accepted: ${allowed || "none"})`);
170
+ }
171
+ }
172
+ for (const required of schema.required ?? []) {
173
+ if (value[required] === void 0) problems.push(`missing required argument "${required}"`);
174
+ }
175
+ for (const [key, propertySchema] of Object.entries(schema.properties)) {
176
+ const argument = value[key];
177
+ if (argument === void 0) continue;
178
+ problems.push(...checkValue(key, propertySchema, argument));
179
+ }
180
+ return problems;
181
+ }
182
+ function checkValue(path4, schema, value) {
183
+ if (schema.enum && !schema.enum.includes(value)) {
184
+ return [`"${path4}" must be one of: ${schema.enum.join(", ")}`];
185
+ }
186
+ switch (schema.type) {
187
+ case "string": {
188
+ if (typeof value !== "string") return [`"${path4}" must be a string`];
189
+ const problems = [];
190
+ if (schema.minLength !== void 0 && value.length < schema.minLength) {
191
+ problems.push(`"${path4}" must be at least ${schema.minLength} character(s)`);
192
+ }
193
+ if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
194
+ problems.push(`"${path4}" must be at most ${schema.maxLength} character(s)`);
195
+ }
196
+ return problems;
197
+ }
198
+ case "number":
199
+ case "integer": {
200
+ if (typeof value !== "number" || Number.isNaN(value)) {
201
+ return [`"${path4}" must be a number`];
202
+ }
203
+ if (schema.type === "integer" && !Number.isInteger(value)) {
204
+ return [`"${path4}" must be an integer`];
205
+ }
206
+ const problems = [];
207
+ if (schema.minimum !== void 0 && value < schema.minimum) {
208
+ problems.push(`"${path4}" must be >= ${schema.minimum}`);
209
+ }
210
+ if (schema.maximum !== void 0 && value > schema.maximum) {
211
+ problems.push(`"${path4}" must be <= ${schema.maximum}`);
212
+ }
213
+ return problems;
214
+ }
215
+ case "boolean":
216
+ return typeof value === "boolean" ? [] : [`"${path4}" must be a boolean`];
217
+ case "array": {
218
+ if (!Array.isArray(value)) return [`"${path4}" must be an array`];
219
+ if (!schema.items) return [];
220
+ const problems = [];
221
+ value.forEach((item, index) => {
222
+ problems.push(...checkValue(`${path4}[${index}]`, schema.items, item));
223
+ });
224
+ return problems;
225
+ }
226
+ case "object":
227
+ return isJsonObject(value) ? [] : [`"${path4}" must be an object`];
228
+ default:
229
+ return [];
230
+ }
231
+ }
232
+ function applyDefaults(schema, value) {
233
+ const out = { ...value };
234
+ for (const [key, propertySchema] of Object.entries(schema.properties)) {
235
+ if (out[key] === void 0 && propertySchema.default !== void 0) {
236
+ out[key] = propertySchema.default;
237
+ }
238
+ }
239
+ return out;
240
+ }
241
+
242
+ // src/mcp/server.ts
243
+ function capabilities() {
244
+ return {
245
+ tools: { listChanged: false },
246
+ resources: { subscribe: false, listChanged: false },
247
+ prompts: { listChanged: false }
248
+ };
249
+ }
250
+ function requestIdOf(message) {
251
+ const raw = message.id;
252
+ return typeof raw === "string" || typeof raw === "number" ? raw : null;
253
+ }
254
+ function createMcpServer(options) {
255
+ const session = createSession();
256
+ const toolsByName = new Map(options.tools.map((tool) => [tool.name, tool]));
257
+ const promptsByName = new Map(options.prompts.map((prompt) => [prompt.name, prompt]));
258
+ const identity = () => ({
259
+ name: options.info.name,
260
+ title: options.info.title,
261
+ version: options.info.version,
262
+ websiteUrl: options.info.websiteUrl
263
+ });
264
+ const ok = (id, result, modern) => successResponse(id, result, modern, options.info);
265
+ function handleNotification(method) {
266
+ if (method === METHOD.initialized && session.era === "legacy") session.ready = true;
267
+ }
268
+ function initialize(id, params) {
269
+ const negotiation = negotiateLegacyVersion(params?.protocolVersion);
270
+ session.era = "legacy";
271
+ session.protocolVersion = negotiation.version;
272
+ session.ready = true;
273
+ options.context.log(
274
+ negotiation.matched ? `mcp: client initialized with protocol ${negotiation.version}` : `mcp: client asked for an unsupported version and was offered ${negotiation.version}`
275
+ );
276
+ return {
277
+ jsonrpc: "2.0",
278
+ id,
279
+ result: {
280
+ protocolVersion: negotiation.version,
281
+ capabilities: capabilities(),
282
+ serverInfo: identity(),
283
+ instructions: options.instructions
284
+ }
285
+ };
286
+ }
287
+ function discover(id) {
288
+ session.era = "modern";
289
+ session.ready = true;
290
+ return ok(
291
+ id,
292
+ {
293
+ supportedVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
294
+ capabilities: capabilities(),
295
+ instructions: options.instructions,
296
+ // The capability set depends on which repository the server was started for, so the answer
297
+ // may be cached only by the client that asked, and not at all after this response.
298
+ cacheScope: "private",
299
+ ttlMs: 0
300
+ },
301
+ true
302
+ );
303
+ }
304
+ function listTools(id, modern) {
305
+ const tools = options.tools.map((tool) => ({
306
+ name: tool.name,
307
+ title: tool.title,
308
+ description: tool.description,
309
+ inputSchema: tool.inputSchema,
310
+ ...tool.outputSchema ? { outputSchema: tool.outputSchema } : {},
311
+ annotations: tool.annotations
312
+ }));
313
+ return ok(id, { tools }, modern);
314
+ }
315
+ async function callTool(id, modern, params) {
316
+ const name = params?.name;
317
+ if (typeof name !== "string") {
318
+ return errorResponse(id, ERROR_CODES.invalidParams, 'tools/call requires a "name" string.');
319
+ }
320
+ const tool = toolsByName.get(name);
321
+ if (!tool) {
322
+ const known = [...toolsByName.keys()].sort().join(", ");
323
+ return errorResponse(id, ERROR_CODES.invalidParams, `Unknown tool "${name}". Available: ${known}.`);
324
+ }
325
+ const rawArguments = params?.arguments ?? {};
326
+ if (!isJsonObject(rawArguments)) {
327
+ return errorResponse(id, ERROR_CODES.invalidParams, '"arguments" must be a JSON object.');
328
+ }
329
+ const problems = validateArguments(tool.inputSchema, rawArguments);
330
+ if (problems.length > 0) {
331
+ return ok(
332
+ id,
333
+ {
334
+ content: [{ type: "text", text: `Invalid arguments for ${tool.name}:
335
+ - ${problems.join("\n- ")}` }],
336
+ isError: true
337
+ },
338
+ modern
339
+ );
340
+ }
341
+ const args = applyDefaults(tool.inputSchema, rawArguments);
342
+ let outcome;
343
+ try {
344
+ outcome = await tool.handler(args, options.context);
345
+ } catch (error) {
346
+ const detail = error instanceof Error ? error.message : String(error);
347
+ options.context.log(`mcp: tool ${tool.name} failed: ${detail}`);
348
+ return ok(
349
+ id,
350
+ { content: [{ type: "text", text: `${tool.name} failed: ${detail}` }], isError: true },
351
+ modern
352
+ );
353
+ }
354
+ return ok(
355
+ id,
356
+ {
357
+ content: [{ type: "text", text: outcome.text }],
358
+ ...outcome.structured ? { structuredContent: outcome.structured } : {},
359
+ ...outcome.isError ? { isError: true } : {}
360
+ },
361
+ modern
362
+ );
363
+ }
364
+ async function listResources(id, modern) {
365
+ const resources = await options.resources.list(options.context);
366
+ return ok(id, { resources }, modern);
367
+ }
368
+ async function readResource(id, modern, params) {
369
+ const uri = params?.uri;
370
+ if (typeof uri !== "string") {
371
+ return errorResponse(id, ERROR_CODES.invalidParams, 'resources/read requires a "uri" string.');
372
+ }
373
+ const contents = await options.resources.read(uri, options.context);
374
+ if (!contents) {
375
+ return errorResponse(id, ERROR_CODES.invalidParams, `Unknown resource "${uri}".`);
376
+ }
377
+ return ok(id, { contents: [contents] }, modern);
378
+ }
379
+ function listPrompts(id, modern) {
380
+ const prompts = options.prompts.map((prompt) => ({
381
+ name: prompt.name,
382
+ ...prompt.title ? { title: prompt.title } : {},
383
+ description: prompt.description,
384
+ arguments: prompt.arguments
385
+ }));
386
+ return ok(id, { prompts }, modern);
387
+ }
388
+ function getPrompt(id, modern, params) {
389
+ const name = params?.name;
390
+ if (typeof name !== "string") {
391
+ return errorResponse(id, ERROR_CODES.invalidParams, 'prompts/get requires a "name" string.');
392
+ }
393
+ const prompt = promptsByName.get(name);
394
+ if (!prompt) {
395
+ const known = [...promptsByName.keys()].sort().join(", ");
396
+ return errorResponse(id, ERROR_CODES.invalidParams, `Unknown prompt "${name}". Available: ${known}.`);
397
+ }
398
+ const rawArguments = params?.arguments ?? {};
399
+ if (!isJsonObject(rawArguments)) {
400
+ return errorResponse(id, ERROR_CODES.invalidParams, '"arguments" must be a JSON object.');
401
+ }
402
+ const built = prompt.build(rawArguments);
403
+ const messages = built.messages.map((message) => ({
404
+ role: message.role,
405
+ content: { type: "text", text: message.content.text }
406
+ }));
407
+ return ok(id, { description: built.description, messages }, modern);
408
+ }
409
+ async function route(name, id, modern, params) {
410
+ switch (name) {
411
+ case METHOD.initialize:
412
+ return modern ? errorResponse(
413
+ id,
414
+ ERROR_CODES.methodNotFound,
415
+ '"initialize" does not exist in this protocol revision; call "server/discover".'
416
+ ) : initialize(id, params);
417
+ case METHOD.discover:
418
+ return discover(id);
419
+ case METHOD.ping:
420
+ return modern ? errorResponse(
421
+ id,
422
+ ERROR_CODES.methodNotFound,
423
+ '"ping" was removed in this protocol revision; call "server/discover".'
424
+ ) : ok(id, {}, modern);
425
+ }
426
+ if (!session.ready) {
427
+ return errorResponse(
428
+ id,
429
+ ERROR_CODES.invalidRequest,
430
+ 'This connection has not been initialized. Send "initialize" first.'
431
+ );
432
+ }
433
+ switch (name) {
434
+ case METHOD.toolsList:
435
+ return listTools(id, modern);
436
+ case METHOD.toolsCall:
437
+ return callTool(id, modern, params);
438
+ case METHOD.resourcesList:
439
+ return listResources(id, modern);
440
+ case METHOD.resourcesRead:
441
+ return readResource(id, modern, params);
442
+ case METHOD.resourceTemplatesList:
443
+ return ok(id, { resourceTemplates: options.resources.templates() }, modern);
444
+ case METHOD.promptsList:
445
+ return listPrompts(id, modern);
446
+ case METHOD.promptsGet:
447
+ return getPrompt(id, modern, params);
448
+ default:
449
+ return errorResponse(id, ERROR_CODES.methodNotFound, `Unknown method "${name}".`);
450
+ }
451
+ }
452
+ async function dispatch(message) {
453
+ if (!isJsonObject(message)) {
454
+ return errorResponse(null, ERROR_CODES.parseError, "Message is not a JSON object.");
455
+ }
456
+ const isRequest = Object.prototype.hasOwnProperty.call(message, "id");
457
+ const method = message.method;
458
+ const params = message.params;
459
+ if (message.jsonrpc !== "2.0" && isRequest) {
460
+ return errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, 'Message must carry "jsonrpc": "2.0".');
461
+ }
462
+ if (typeof method !== "string" || method.length === 0) {
463
+ return isRequest ? errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, 'Message must carry a "method" string.') : null;
464
+ }
465
+ if (params !== void 0 && !isJsonObject(params)) {
466
+ return isRequest ? errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, '"params" must be a JSON object when present.') : null;
467
+ }
468
+ const named = params;
469
+ const decision = classifyRequest(method, named, session);
470
+ const modern = decision.kind === "era" ? decision.era === "modern" : session.era === "modern";
471
+ if (!isRequest) {
472
+ if (decision.kind === "era" && session.era === null) {
473
+ session.era = decision.era;
474
+ session.protocolVersion = decision.version;
475
+ }
476
+ handleNotification(method);
477
+ return null;
478
+ }
479
+ const id = requestIdOf(message);
480
+ if (id === null) {
481
+ return errorResponse(null, ERROR_CODES.invalidRequest, 'A request "id" must be a string or a number.');
482
+ }
483
+ if (decision.kind === "error") {
484
+ return errorResponse(id, decision.code, decision.message, decision.data);
485
+ }
486
+ if (session.era === null) {
487
+ session.era = decision.era;
488
+ session.protocolVersion = decision.version;
489
+ }
490
+ return route(method, id, modern, named);
491
+ }
492
+ return {
493
+ session,
494
+ async handle(message) {
495
+ try {
496
+ return await dispatch(message);
497
+ } catch (error) {
498
+ const detail = error instanceof Error ? error.message : String(error);
499
+ options.context.log(`mcp: internal error: ${detail}`);
500
+ const id = isJsonObject(message) ? requestIdOf(message) : null;
501
+ return errorResponse(id, ERROR_CODES.internalError, "Internal server error.");
502
+ }
503
+ }
504
+ };
505
+ }
506
+
507
+ // src/mcp/part.ts
508
+ var FILE_HEADING = "### FILE: ";
509
+ var CONTENT_MARKERS = /* @__PURE__ */ new Set(["#### Content", "#### Bounded Preview"]);
510
+ function parsePartSections(partText) {
511
+ const lines = partText.split("\n");
512
+ const sections = [];
513
+ let index = 0;
514
+ while (index < lines.length) {
515
+ if (!lines[index].startsWith(FILE_HEADING)) {
516
+ index += 1;
517
+ continue;
518
+ }
519
+ const heading = lines[index].slice(FILE_HEADING.length);
520
+ let cursor = index + 1;
521
+ let inclusion = null;
522
+ let sha256 = null;
523
+ let includedLines = null;
524
+ while (cursor < lines.length && !CONTENT_MARKERS.has(lines[cursor])) {
525
+ const line = lines[cursor];
526
+ if (line.startsWith(FILE_HEADING)) break;
527
+ if (line.startsWith("- Inclusion: ")) inclusion = line.slice("- Inclusion: ".length).trim();
528
+ else if (line.startsWith("- SHA-256: ")) sha256 = line.slice("- SHA-256: ".length).trim();
529
+ else if (line.startsWith("- Included Lines: ")) {
530
+ const value = Number(line.slice("- Included Lines: ".length).trim());
531
+ includedLines = Number.isFinite(value) ? value : null;
532
+ }
533
+ cursor += 1;
534
+ }
535
+ if (cursor >= lines.length || !CONTENT_MARKERS.has(lines[cursor])) {
536
+ index = cursor + 1;
537
+ continue;
538
+ }
539
+ let fenceLine = cursor + 1;
540
+ while (fenceLine < lines.length && lines[fenceLine].trim().length === 0) fenceLine += 1;
541
+ const fenceMatch = /^(`{4,})/.exec(lines[fenceLine] ?? "");
542
+ if (!fenceMatch) {
543
+ index = cursor + 1;
544
+ continue;
545
+ }
546
+ const fence = fenceMatch[1];
547
+ const body = [];
548
+ let bodyLine = fenceLine + 1;
549
+ while (bodyLine < lines.length && lines[bodyLine] !== fence) {
550
+ body.push(lines[bodyLine]);
551
+ bodyLine += 1;
552
+ }
553
+ sections.push({ path: heading, body: body.join("\n"), inclusion, sha256, includedLines });
554
+ index = bodyLine + 1;
555
+ }
556
+ return sections;
557
+ }
558
+ function findSection(sections, relativePath, sha256) {
559
+ const exact = sections.find((section) => section.path === relativePath);
560
+ if (exact) return exact;
561
+ if (sha256) {
562
+ const byHash = sections.filter((section) => section.sha256 === sha256);
563
+ if (byHash.length === 1) return byHash[0];
564
+ if (byHash.length > 1) {
565
+ const byPath = byHash.find(
566
+ (section) => section.path.toLowerCase() === relativePath.toLowerCase()
567
+ );
568
+ if (byPath) return byPath;
569
+ }
570
+ }
571
+ const folded = sections.filter((section) => section.path.toLowerCase() === relativePath.toLowerCase());
572
+ return folded.length === 1 ? folded[0] : null;
573
+ }
574
+ function windowText(body, fromLine, lineCount) {
575
+ const lines = body.split("\n");
576
+ const total = lines.length;
577
+ const start = Math.min(Math.max(1, Math.trunc(fromLine)), Math.max(1, total));
578
+ const end = Math.min(total, start + Math.max(1, Math.trunc(lineCount)) - 1);
579
+ const slice = lines.slice(start - 1, end);
580
+ return {
581
+ text: slice.join("\n"),
582
+ fromLine: start,
583
+ returnedLines: slice.length,
584
+ totalLines: total,
585
+ truncated: end < total,
586
+ nextFromLine: end < total ? end + 1 : null
587
+ };
588
+ }
589
+ function foldForComparison(value) {
590
+ return process.platform === "win32" || process.platform === "darwin" ? value.toLowerCase() : value;
591
+ }
592
+ function canonicalPath(value) {
593
+ const absolute = path2.resolve(value);
594
+ try {
595
+ return fs.realpathSync(absolute);
596
+ } catch {
597
+ return absolute;
598
+ }
599
+ }
600
+ function isInside(parent, child) {
601
+ const foldedParent = foldForComparison(parent);
602
+ const foldedChild = foldForComparison(child);
603
+ return foldedChild === foldedParent || foldedChild.startsWith(`${foldedParent}${path2.sep}`);
604
+ }
605
+ function resolveRoot(requested, context) {
606
+ if (requested !== void 0 && (typeof requested !== "string" || requested.trim().length === 0)) {
607
+ return { ok: false, message: '"root" must be a non-empty string when provided.' };
608
+ }
609
+ const wanted = typeof requested === "string" ? requested.trim() : context.cwd;
610
+ const absolute = path2.resolve(context.cwd, wanted);
611
+ let stats;
612
+ try {
613
+ stats = fs.statSync(absolute);
614
+ } catch {
615
+ return { ok: false, message: `"${wanted}" does not exist, so it cannot be a repository root.` };
616
+ }
617
+ if (!stats.isDirectory()) {
618
+ return { ok: false, message: `"${wanted}" is not a directory.` };
619
+ }
620
+ const resolved = canonicalPath(absolute);
621
+ for (const allowed of context.roots) {
622
+ if (isInside(canonicalPath(allowed), resolved)) return { ok: true, root: resolved };
623
+ }
624
+ const shown = context.roots.join(", ");
625
+ return {
626
+ ok: false,
627
+ message: `"${wanted}" resolves outside the directories this server may read (${shown}). Restart it with --root <path> to add a repository.`
628
+ };
629
+ }
630
+ async function openWorkspace(root, overrides = {}) {
631
+ const loaded = await loadConfigFileDetailed(root);
632
+ if (loaded.errors.length > 0) {
633
+ return {
634
+ ok: false,
635
+ 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.`
636
+ };
637
+ }
638
+ const cliOptions = { cwd: root };
639
+ if (overrides.maxPartBytes !== void 0) {
640
+ const parsed = parseByteSize(overrides.maxPartBytes);
641
+ if (parsed === void 0) {
642
+ return {
643
+ ok: false,
644
+ message: '"maxPartBytes" must be a byte count, for example 307200 or "300k".'
645
+ };
646
+ }
647
+ cliOptions.maxPartBytes = parsed;
648
+ }
649
+ if (overrides.outDir !== void 0) {
650
+ if (typeof overrides.outDir !== "string" || overrides.outDir.trim().length === 0) {
651
+ return { ok: false, message: '"outDir" must be a non-empty string.' };
652
+ }
653
+ cliOptions.outDir = overrides.outDir.trim();
654
+ }
655
+ try {
656
+ const config = resolveConfig(loaded.config, cliOptions, root);
657
+ return { ok: true, workspace: { root, config, configFile: loaded.file } };
658
+ } catch (error) {
659
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
660
+ }
661
+ }
662
+ var manifestCache = /* @__PURE__ */ new Map();
663
+ function manifestPathFor(workspace) {
664
+ return path2.resolve(workspace.root, workspace.config.outDir, "manifest.json");
665
+ }
666
+ function readManifest(workspace) {
667
+ const file = manifestPathFor(workspace);
668
+ const shown = path2.relative(workspace.root, file).split(path2.sep).join("/") || file;
669
+ let stats;
670
+ try {
671
+ stats = fs.statSync(file);
672
+ } catch {
673
+ return {
674
+ ok: false,
675
+ missing: true,
676
+ message: `There is no snapshot manifest at ${shown}. Call snapshot_plan to preview what would be included, or snapshot_generate to write it.`
677
+ };
678
+ }
679
+ const cached = manifestCache.get(file);
680
+ if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
681
+ return { ok: true, manifest: cached.manifest, file };
682
+ }
683
+ let raw;
684
+ try {
685
+ raw = fs.readFileSync(file, "utf8");
686
+ } catch (error) {
687
+ return {
688
+ ok: false,
689
+ missing: false,
690
+ message: `${shown} exists but could not be read: ${error instanceof Error ? error.message : String(error)}`
691
+ };
692
+ }
693
+ let parsed;
694
+ try {
695
+ parsed = JSON.parse(raw);
696
+ } catch {
697
+ return { ok: false, missing: false, message: `${shown} is not valid JSON.` };
698
+ }
699
+ const problem = describeManifestShapeProblem(parsed);
700
+ if (problem) {
701
+ return { ok: false, missing: false, message: `${shown} is not a snapshot manifest: ${problem}` };
702
+ }
703
+ const manifest = parsed;
704
+ manifestCache.set(file, { mtimeMs: stats.mtimeMs, size: stats.size, manifest });
705
+ return { ok: true, manifest, file };
706
+ }
707
+ function forgetManifest(file) {
708
+ manifestCache.delete(file);
709
+ }
710
+
711
+ // src/mcp/tools.ts
712
+ var READ_ONLY = {
713
+ readOnlyHint: true,
714
+ destructiveHint: false,
715
+ idempotentHint: true,
716
+ openWorldHint: false
717
+ };
718
+ var WRITES = {
719
+ readOnlyHint: false,
720
+ // It replaces the snapshot directory but never anything it did not write, so it is additive with
721
+ // respect to the user's files.
722
+ destructiveHint: false,
723
+ idempotentHint: true,
724
+ openWorldHint: false
725
+ };
726
+ var MAX_RESULT_BYTES = 64 * 1024;
727
+ var ROOT_PROPERTY = {
728
+ type: "string",
729
+ description: "Repository root. Defaults to the directory this server was started in."
730
+ };
731
+ function failure(message) {
732
+ return { text: message, isError: true };
733
+ }
734
+ function plural(count, word) {
735
+ return `${count} ${word}${count === 1 ? "" : "s"}`;
736
+ }
737
+ function formatBytes(bytes) {
738
+ if (bytes < 1024) return `${bytes} B`;
739
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
740
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
741
+ }
742
+ function relativeFor(workspace, absolute) {
743
+ return path2.relative(workspace.root, absolute).split(path2.sep).join("/");
744
+ }
745
+ function capped(values, limit = 20) {
746
+ return { count: values.length, sample: values.slice(0, limit) };
747
+ }
748
+ function summarizeDrift(drift) {
749
+ return {
750
+ missingInSnapshot: capped(drift.missingInSnapshot),
751
+ missingOnDisk: capped(drift.missingOnDisk),
752
+ unreadable: capped(drift.unreadable),
753
+ hashMismatches: capped(drift.hashMismatches),
754
+ partIssues: capped(drift.partIssues),
755
+ totalsMismatches: capped(drift.totalsMismatches),
756
+ configMismatches: capped(drift.configMismatches)
757
+ };
758
+ }
759
+ function toRelativePath(root, value) {
760
+ const cleaned = value.trim();
761
+ if (cleaned.length === 0) return null;
762
+ if (!path2.isAbsolute(cleaned)) {
763
+ return cleaned.replace(/\\/g, "/").replace(/^\.\//, "");
764
+ }
765
+ const relative3 = path2.relative(root, cleaned);
766
+ if (relative3.startsWith("..") || path2.isAbsolute(relative3)) return null;
767
+ return relative3.split(path2.sep).join("/");
768
+ }
769
+ function symbolsOf(entry) {
770
+ const summary = entry.summary;
771
+ if (!summary || typeof summary !== "object" || !("exports" in summary)) return [];
772
+ const source = summary;
773
+ return [...source.exports, ...source.reactComponents, ...source.routeHandlers];
774
+ }
775
+ async function prepare(args, context, overrides = {}) {
776
+ const rootOutcome = resolveRoot(args.root, context);
777
+ if (!rootOutcome.ok) return { ok: false, outcome: failure(rootOutcome.message) };
778
+ const opened = await openWorkspace(rootOutcome.root, overrides);
779
+ if (!opened.ok) return { ok: false, outcome: failure(opened.message) };
780
+ return { ok: true, workspace: opened.workspace };
781
+ }
782
+ var statusTool = {
783
+ name: "snapshot_status",
784
+ title: "Snapshot status",
785
+ 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.",
786
+ inputSchema: {
787
+ type: "object",
788
+ properties: { root: ROOT_PROPERTY },
789
+ additionalProperties: false
790
+ },
791
+ outputSchema: {
792
+ type: "object",
793
+ properties: {
794
+ present: { type: "boolean" },
795
+ root: { type: "string" },
796
+ outDir: { type: "string" },
797
+ fingerprint: { type: "string" },
798
+ generatedAt: { type: "string" },
799
+ generatorVersion: { type: "string" },
800
+ generatorVersionMatches: { type: "boolean" },
801
+ includedFiles: { type: "integer" },
802
+ skippedFiles: { type: "integer" },
803
+ partCount: { type: "integer" },
804
+ warningCount: { type: "integer" }
805
+ }
806
+ },
807
+ annotations: READ_ONLY,
808
+ async handler(args, context) {
809
+ const prepared = await prepare(args, context);
810
+ if (!prepared.ok) return prepared.outcome;
811
+ const workspace = prepared.workspace;
812
+ const read = readManifest(workspace);
813
+ if (!read.ok) {
814
+ return {
815
+ text: read.message,
816
+ structured: {
817
+ present: false,
818
+ root: workspace.root,
819
+ outDir: workspace.config.outDir,
820
+ message: read.message
821
+ }
822
+ };
823
+ }
824
+ const manifest = read.manifest;
825
+ const versionMatches = manifest.generator.version === GENERATOR_VERSION;
826
+ const lines = [
827
+ `Snapshot: ${workspace.config.outDir}/`,
828
+ `Generated: ${manifest.generatedAt} by ${manifest.generator.name} ${manifest.generator.version}`,
829
+ `Files: ${manifest.counts.totalFiles} included, ${manifest.counts.skippedCount} skipped, ${plural(manifest.counts.partsCount, "part")}`,
830
+ `Content: ${formatBytes(manifest.counts.totalSizeBytes)} of source in ${formatBytes(manifest.totals.totalContentBytes)} emitted`,
831
+ `Fingerprint: ${manifest.fingerprint}`,
832
+ `Git: ${manifest.git.branch} @ ${manifest.git.commit}${manifest.git.isDirty ? " (dirty)" : " (clean)"}`
833
+ ];
834
+ if (manifest.totals.warningCount > 0) lines.push(`Warnings: ${manifest.totals.warningCount}`);
835
+ if (!versionMatches) {
836
+ lines.push(
837
+ `Note: written by ${manifest.generator.version}, this server is ${GENERATOR_VERSION}. Re-run snapshot_generate if the difference matters.`
838
+ );
839
+ }
840
+ return {
841
+ text: lines.join("\n"),
842
+ structured: {
843
+ present: true,
844
+ root: workspace.root,
845
+ outDir: workspace.config.outDir,
846
+ fingerprint: manifest.fingerprint,
847
+ generatedAt: manifest.generatedAt,
848
+ generatorVersion: manifest.generator.version,
849
+ generatorVersionMatches: versionMatches,
850
+ includedFiles: manifest.counts.totalFiles,
851
+ skippedFiles: manifest.counts.skippedCount,
852
+ partCount: manifest.counts.partsCount,
853
+ warningCount: manifest.totals.warningCount,
854
+ preset: manifest.config.preset,
855
+ maxPartBytes: manifest.budgets.maxPartBytes
856
+ }
857
+ };
858
+ }
859
+ };
860
+ var planTool = {
861
+ name: "snapshot_plan",
862
+ title: "Preview a snapshot",
863
+ 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.",
864
+ inputSchema: {
865
+ type: "object",
866
+ properties: {
867
+ root: ROOT_PROPERTY,
868
+ maxPartBytes: {
869
+ type: "string",
870
+ description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
871
+ }
872
+ },
873
+ additionalProperties: false
874
+ },
875
+ outputSchema: {
876
+ type: "object",
877
+ properties: {
878
+ files: { type: "integer" },
879
+ skipped: { type: "integer" },
880
+ parts: { type: "integer" },
881
+ sourceBytes: { type: "integer" },
882
+ emittedBytes: { type: "integer" },
883
+ preset: { type: "string" }
884
+ }
885
+ },
886
+ annotations: READ_ONLY,
887
+ async handler(args, context) {
888
+ const prepared = await prepare(args, context, { maxPartBytes: args.maxPartBytes });
889
+ if (!prepared.ok) return prepared.outcome;
890
+ const workspace = prepared.workspace;
891
+ let manifest;
892
+ try {
893
+ manifest = generateSnapshot(workspace.config, { dryRun: true });
894
+ } catch (error) {
895
+ return failure(error instanceof Error ? error.message : String(error));
896
+ }
897
+ const reasons = /* @__PURE__ */ new Map();
898
+ for (const record of manifest.skipped) {
899
+ reasons.set(record.reason, (reasons.get(record.reason) ?? 0) + 1);
900
+ }
901
+ 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 }));
902
+ const lines = [
903
+ "Dry run \u2014 nothing was written.",
904
+ `Preset: ${manifest.config.preset}`,
905
+ `Would include ${plural(manifest.counts.totalFiles, "file")} in ${plural(manifest.counts.partsCount, "part")} (${formatBytes(manifest.counts.totalSizeBytes)} of source, ${formatBytes(manifest.totals.totalContentBytes)} emitted).`,
906
+ `Would skip ${plural(manifest.counts.skippedCount, "file")}.`
907
+ ];
908
+ if (topReasons.length > 0) {
909
+ lines.push("Top skip reasons:");
910
+ for (const entry of topReasons) lines.push(` - ${entry.reason} (${entry.count})`);
911
+ }
912
+ if (manifest.warnings.length > 0) {
913
+ lines.push(`Warnings (${manifest.warnings.length}):`);
914
+ for (const warning of manifest.warnings.slice(0, 5)) lines.push(` - ${warning}`);
915
+ }
916
+ return {
917
+ text: lines.join("\n"),
918
+ structured: {
919
+ files: manifest.counts.totalFiles,
920
+ skipped: manifest.counts.skippedCount,
921
+ parts: manifest.counts.partsCount,
922
+ sourceBytes: manifest.counts.totalSizeBytes,
923
+ emittedBytes: manifest.totals.totalContentBytes,
924
+ preset: manifest.config.preset,
925
+ maxPartBytes: manifest.budgets.maxPartBytes,
926
+ layers: manifest.layers,
927
+ skipReasons: topReasons,
928
+ warnings: manifest.warnings.slice(0, 5),
929
+ warningCount: manifest.warnings.length
930
+ }
931
+ };
932
+ }
933
+ };
934
+ var generateTool = {
935
+ name: "snapshot_generate",
936
+ title: "Generate the snapshot",
937
+ 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.",
938
+ inputSchema: {
939
+ type: "object",
940
+ properties: {
941
+ root: ROOT_PROPERTY,
942
+ maxPartBytes: {
943
+ type: "string",
944
+ description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
945
+ },
946
+ outDir: {
947
+ type: "string",
948
+ description: "Output directory relative to the root. Defaults to the project configuration."
949
+ }
950
+ },
951
+ additionalProperties: false
952
+ },
953
+ outputSchema: {
954
+ type: "object",
955
+ properties: {
956
+ fingerprint: { type: "string" },
957
+ files: { type: "integer" },
958
+ skipped: { type: "integer" },
959
+ parts: { type: "integer" },
960
+ emittedBytes: { type: "integer" }
961
+ }
962
+ },
963
+ annotations: WRITES,
964
+ async handler(args, context) {
965
+ const prepared = await prepare(args, context, {
966
+ maxPartBytes: args.maxPartBytes,
967
+ outDir: args.outDir
968
+ });
969
+ if (!prepared.ok) return prepared.outcome;
970
+ const workspace = prepared.workspace;
971
+ let manifest;
972
+ try {
973
+ manifest = generateSnapshot(workspace.config);
974
+ } catch (error) {
975
+ return failure(error instanceof Error ? error.message : String(error));
976
+ }
977
+ forgetManifest(manifestPathFor(workspace));
978
+ const written = relativeFor(workspace, path2.resolve(workspace.root, workspace.config.outDir));
979
+ const lines = [
980
+ `Wrote ${written}/`,
981
+ `Files: ${manifest.counts.totalFiles} included, ${manifest.counts.skippedCount} skipped, ${plural(manifest.counts.partsCount, "part")}`,
982
+ `Parts: ${manifest.parts.map((part) => `${part.filename} (${formatBytes(part.byteSize)})`).join(", ")}`,
983
+ `Fingerprint: ${manifest.fingerprint}`
984
+ ];
985
+ if (manifest.warnings.length > 0) {
986
+ lines.push(`Warnings (${manifest.warnings.length}):`);
987
+ for (const warning of manifest.warnings.slice(0, 5)) lines.push(` - ${warning}`);
988
+ }
989
+ return {
990
+ text: lines.join("\n"),
991
+ structured: {
992
+ fingerprint: manifest.fingerprint,
993
+ files: manifest.counts.totalFiles,
994
+ skipped: manifest.counts.skippedCount,
995
+ parts: manifest.counts.partsCount,
996
+ emittedBytes: manifest.totals.totalContentBytes,
997
+ outDir: written,
998
+ partFiles: manifest.parts.map((part) => part.filename),
999
+ warningCount: manifest.warnings.length
1000
+ }
1001
+ };
1002
+ }
1003
+ };
1004
+ var validateTool = {
1005
+ name: "snapshot_validate",
1006
+ title: "Validate the snapshot",
1007
+ 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.",
1008
+ inputSchema: {
1009
+ type: "object",
1010
+ properties: { root: ROOT_PROPERTY },
1011
+ additionalProperties: false
1012
+ },
1013
+ outputSchema: {
1014
+ type: "object",
1015
+ properties: {
1016
+ current: { type: "boolean" },
1017
+ manifestPresent: { type: "boolean" }
1018
+ }
1019
+ },
1020
+ annotations: READ_ONLY,
1021
+ async handler(args, context) {
1022
+ const prepared = await prepare(args, context);
1023
+ if (!prepared.ok) return prepared.outcome;
1024
+ const workspace = prepared.workspace;
1025
+ let result;
1026
+ try {
1027
+ result = validateSnapshot(workspace.config);
1028
+ } catch (error) {
1029
+ return failure(error instanceof Error ? error.message : String(error));
1030
+ }
1031
+ const lines = [];
1032
+ if (result.valid) {
1033
+ lines.push("The snapshot matches the repository.");
1034
+ } else {
1035
+ lines.push("The snapshot is out of date.");
1036
+ const drift = result.drift;
1037
+ const observations = [
1038
+ ["file(s) changed on disk", drift.hashMismatches.length],
1039
+ ["file(s) added since the snapshot", drift.missingInSnapshot.length],
1040
+ ["file(s) gone since the snapshot", drift.missingOnDisk.length],
1041
+ ["part file(s) damaged or missing", drift.partIssues.length],
1042
+ ["setting(s) changed since the snapshot", drift.configMismatches.length],
1043
+ ["internal count(s) disagreeing", drift.totalsMismatches.length]
1044
+ ];
1045
+ for (const [label, count] of observations) {
1046
+ if (count > 0) lines.push(` - ${count} ${label}`);
1047
+ }
1048
+ for (const error of result.errors.slice(0, 5)) lines.push(` - ${error}`);
1049
+ lines.push("Run snapshot_generate to refresh it.");
1050
+ }
1051
+ for (const warning of result.warnings.slice(0, 5)) lines.push(`Note: ${warning}`);
1052
+ return {
1053
+ text: lines.join("\n"),
1054
+ structured: {
1055
+ current: result.valid,
1056
+ manifestPresent: result.manifestPresent,
1057
+ drift: summarizeDrift(result.drift),
1058
+ errors: result.errors.slice(0, 10),
1059
+ warnings: result.warnings.slice(0, 10)
1060
+ }
1061
+ // A stale snapshot is an answer, not a tool failure: the caller asked whether it is current.
1062
+ };
1063
+ }
1064
+ };
1065
+ var locateTool = {
1066
+ name: "snapshot_locate",
1067
+ title: "Locate code in the snapshot",
1068
+ 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.",
1069
+ inputSchema: {
1070
+ type: "object",
1071
+ properties: {
1072
+ query: {
1073
+ type: "string",
1074
+ description: "Text to match, case-insensitively, against paths and exported symbols."
1075
+ },
1076
+ by: {
1077
+ type: "string",
1078
+ enum: ["auto", "path", "symbol", "route", "layer", "kind"],
1079
+ default: "auto",
1080
+ description: "Restrict what is matched. Defaults to every category."
1081
+ },
1082
+ limit: {
1083
+ type: "integer",
1084
+ minimum: 1,
1085
+ maximum: 100,
1086
+ default: 20,
1087
+ description: "Maximum matches to return."
1088
+ },
1089
+ root: ROOT_PROPERTY
1090
+ },
1091
+ required: ["query"],
1092
+ additionalProperties: false
1093
+ },
1094
+ outputSchema: {
1095
+ type: "object",
1096
+ properties: { total: { type: "integer" } }
1097
+ },
1098
+ annotations: READ_ONLY,
1099
+ async handler(args, context) {
1100
+ const prepared = await prepare(args, context);
1101
+ if (!prepared.ok) return prepared.outcome;
1102
+ const workspace = prepared.workspace;
1103
+ const read = readManifest(workspace);
1104
+ if (!read.ok) return failure(read.message);
1105
+ const query = String(args.query ?? "").trim();
1106
+ if (query.length === 0) return failure('"query" must not be empty.');
1107
+ const needle = query.toLowerCase();
1108
+ const by = typeof args.by === "string" ? args.by : "auto";
1109
+ const limit = typeof args.limit === "number" ? args.limit : 20;
1110
+ const manifest = read.manifest;
1111
+ const matches = [];
1112
+ const add = (entry, matchedBy, detail) => {
1113
+ matches.push({
1114
+ path: entry.relativePath,
1115
+ part: entry.part,
1116
+ layer: entry.layer,
1117
+ kind: entry.kind,
1118
+ matchedBy,
1119
+ ...detail ? { detail } : {}
1120
+ });
1121
+ };
1122
+ const wants = (category) => by === "auto" || by === category;
1123
+ for (const entry of manifest.files) {
1124
+ if (wants("path") && entry.relativePath.toLowerCase().includes(needle)) {
1125
+ add(entry, "path");
1126
+ continue;
1127
+ }
1128
+ if (wants("symbol")) {
1129
+ const symbol = symbolsOf(entry).find((name) => name.toLowerCase().includes(needle));
1130
+ if (symbol) {
1131
+ add(entry, "symbol", symbol);
1132
+ continue;
1133
+ }
1134
+ }
1135
+ if (wants("layer") && entry.layer.toLowerCase() === needle) add(entry, "layer");
1136
+ else if (wants("kind") && entry.kind.toLowerCase() === needle) add(entry, "kind");
1137
+ }
1138
+ if (wants("route")) {
1139
+ for (const route of manifest.indexes.routes) {
1140
+ if (!route.path.toLowerCase().includes(needle)) continue;
1141
+ if (matches.some((match) => match.path === route.path)) continue;
1142
+ matches.push({
1143
+ path: route.path,
1144
+ part: route.part,
1145
+ layer: "route",
1146
+ kind: "route",
1147
+ matchedBy: "route",
1148
+ detail: route.methods.join(", ")
1149
+ });
1150
+ }
1151
+ }
1152
+ if (matches.length === 0) {
1153
+ return {
1154
+ text: `Nothing in the snapshot matches "${query}". Try a shorter fragment, or snapshot_explain a path to see whether it is included at all.`,
1155
+ structured: { total: 0, matches: [] }
1156
+ };
1157
+ }
1158
+ const shown = matches.slice(0, limit);
1159
+ const lines = [
1160
+ `${matches.length} match${matches.length === 1 ? "" : "es"} for "${query}"` + (matches.length > shown.length ? ` (showing ${shown.length})` : "") + ":"
1161
+ ];
1162
+ for (const match of shown) {
1163
+ const detail = match.detail ? ` \u2014 ${match.detail}` : "";
1164
+ lines.push(` ${match.path}${detail} [${match.part}, ${match.layer}/${match.kind}]`);
1165
+ }
1166
+ if (matches.length > shown.length) {
1167
+ lines.push("Raise `limit` to see more.");
1168
+ }
1169
+ return {
1170
+ text: lines.join("\n"),
1171
+ structured: { total: matches.length, returned: shown.length, matches: shown }
1172
+ };
1173
+ }
1174
+ };
1175
+ var readTool = {
1176
+ name: "snapshot_read",
1177
+ title: "Read a file from the snapshot",
1178
+ 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.",
1179
+ inputSchema: {
1180
+ type: "object",
1181
+ properties: {
1182
+ path: {
1183
+ type: "string",
1184
+ description: "File path relative to the repository root, as snapshot_locate reports it."
1185
+ },
1186
+ fromLine: {
1187
+ type: "integer",
1188
+ minimum: 1,
1189
+ default: 1,
1190
+ description: "First line to return, 1-based."
1191
+ },
1192
+ lines: {
1193
+ type: "integer",
1194
+ minimum: 1,
1195
+ maximum: 2e3,
1196
+ default: 200,
1197
+ description: "How many lines to return."
1198
+ },
1199
+ root: ROOT_PROPERTY
1200
+ },
1201
+ required: ["path"],
1202
+ additionalProperties: false
1203
+ },
1204
+ outputSchema: {
1205
+ type: "object",
1206
+ properties: {
1207
+ path: { type: "string" },
1208
+ part: { type: "string" },
1209
+ fromLine: { type: "integer" },
1210
+ returnedLines: { type: "integer" },
1211
+ totalLines: { type: "integer" },
1212
+ nextFromLine: { type: "integer" }
1213
+ }
1214
+ },
1215
+ annotations: READ_ONLY,
1216
+ async handler(args, context) {
1217
+ const prepared = await prepare(args, context);
1218
+ if (!prepared.ok) return prepared.outcome;
1219
+ const workspace = prepared.workspace;
1220
+ if (typeof args.path !== "string") return failure('"path" must be a string.');
1221
+ const relativePath = toRelativePath(workspace.root, args.path);
1222
+ if (!relativePath) {
1223
+ return failure(
1224
+ `"${args.path}" is outside ${workspace.root}. Paths are read relative to the repository root.`
1225
+ );
1226
+ }
1227
+ const read = readManifest(workspace);
1228
+ if (!read.ok) return failure(read.message);
1229
+ const manifest = read.manifest;
1230
+ const entry = manifest.files.find((file) => file.relativePath === relativePath) ?? manifest.files.find((file) => file.relativePath.toLowerCase() === relativePath.toLowerCase());
1231
+ if (!entry) {
1232
+ const skipped = manifest.skipped.find((record) => record.path === relativePath);
1233
+ if (skipped) {
1234
+ const detail = skipped.detail ? ` (${skipped.detail})` : "";
1235
+ return failure(
1236
+ `${relativePath} is not in the snapshot: ${skipped.reason}${detail}. Sensitive and ignored files stay out unless they are included explicitly.`
1237
+ );
1238
+ }
1239
+ return failure(
1240
+ `${relativePath} is not in the snapshot. Call snapshot_locate to find the path, or snapshot_explain to learn why it was excluded.`
1241
+ );
1242
+ }
1243
+ const partPath = path2.resolve(workspace.root, workspace.config.outDir, "parts", entry.part);
1244
+ let partText;
1245
+ try {
1246
+ partText = fs.readFileSync(partPath, "utf8");
1247
+ } catch {
1248
+ return failure(
1249
+ `${entry.part} could not be read, so the snapshot is damaged. Run snapshot_generate to rebuild it.`
1250
+ );
1251
+ }
1252
+ const section = findSection(parsePartSections(partText), entry.relativePath, entry.hashSha256);
1253
+ if (!section) {
1254
+ return failure(
1255
+ `${relativePath} is recorded in ${entry.part} but its section could not be located there. Run snapshot_generate to rebuild the snapshot.`
1256
+ );
1257
+ }
1258
+ const fromLine = typeof args.fromLine === "number" ? args.fromLine : 1;
1259
+ const lineCount = typeof args.lines === "number" ? args.lines : 200;
1260
+ const window = windowText(section.body, fromLine, lineCount);
1261
+ let body = window.text;
1262
+ let truncatedByBytes = false;
1263
+ if (Buffer.byteLength(body, "utf8") > MAX_RESULT_BYTES) {
1264
+ body = body.slice(0, MAX_RESULT_BYTES);
1265
+ const lastBreak = body.lastIndexOf("\n");
1266
+ body = lastBreak > 0 ? body.slice(0, lastBreak) : body;
1267
+ truncatedByBytes = true;
1268
+ }
1269
+ const returnedLines = body.length === 0 ? 0 : body.split("\n").length;
1270
+ const moreRemain = window.truncated || truncatedByBytes;
1271
+ const header = [
1272
+ `${entry.relativePath} \u2014 ${entry.part}, ${entry.layer}/${entry.kind}, ${entry.language}`,
1273
+ 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}.`
1274
+ ];
1275
+ const lines = [...header, "", body];
1276
+ if (moreRemain) {
1277
+ const next = window.fromLine + returnedLines;
1278
+ lines.push("", `Truncated. Call snapshot_read with fromLine=${next} for the rest.`);
1279
+ }
1280
+ return {
1281
+ text: lines.join("\n"),
1282
+ structured: {
1283
+ path: entry.relativePath,
1284
+ part: entry.part,
1285
+ layer: entry.layer,
1286
+ kind: entry.kind,
1287
+ language: entry.language,
1288
+ inclusion: section.inclusion ?? entry.inclusionMode,
1289
+ fromLine: window.fromLine,
1290
+ returnedLines,
1291
+ totalLines: window.totalLines,
1292
+ truncated: moreRemain,
1293
+ nextFromLine: moreRemain ? window.fromLine + returnedLines : null,
1294
+ sha256: entry.hashSha256
1295
+ }
1296
+ };
1297
+ }
1298
+ };
1299
+ var explainTool = {
1300
+ name: "snapshot_explain",
1301
+ title: "Explain one path",
1302
+ 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".',
1303
+ inputSchema: {
1304
+ type: "object",
1305
+ properties: {
1306
+ path: {
1307
+ type: "string",
1308
+ description: "Path relative to the repository root. Directories are explained as a whole."
1309
+ },
1310
+ root: ROOT_PROPERTY
1311
+ },
1312
+ required: ["path"],
1313
+ additionalProperties: false
1314
+ },
1315
+ outputSchema: {
1316
+ type: "object",
1317
+ properties: {
1318
+ path: { type: "string" },
1319
+ included: { type: "boolean" },
1320
+ reason: { type: "string" }
1321
+ }
1322
+ },
1323
+ annotations: READ_ONLY,
1324
+ async handler(args, context) {
1325
+ const prepared = await prepare(args, context);
1326
+ if (!prepared.ok) return prepared.outcome;
1327
+ const workspace = prepared.workspace;
1328
+ if (typeof args.path !== "string") return failure('"path" must be a string.');
1329
+ const relativePath = toRelativePath(workspace.root, args.path);
1330
+ if (!relativePath) {
1331
+ return failure(`"${args.path}" is outside ${workspace.root}.`);
1332
+ }
1333
+ let result;
1334
+ try {
1335
+ result = explainPath(workspace.config, relativePath);
1336
+ } catch (error) {
1337
+ return failure(error instanceof Error ? error.message : String(error));
1338
+ }
1339
+ return {
1340
+ text: renderExplainText(result),
1341
+ structured: {
1342
+ path: result.path,
1343
+ included: result.included,
1344
+ ...result.reason ? { reason: result.reason } : {},
1345
+ ...result.detail ? { detail: result.detail } : {},
1346
+ ...result.layer ? { layer: result.layer } : {},
1347
+ ...result.kind ? { kind: result.kind } : {},
1348
+ ...result.language ? { language: result.language } : {},
1349
+ ...result.part ? { part: result.part } : {},
1350
+ ...result.inclusionMode ? { inclusion: result.inclusionMode } : {},
1351
+ ...typeof result.lines === "number" ? { lines: result.lines } : {},
1352
+ ...typeof result.bytes === "number" ? { bytes: result.bytes } : {},
1353
+ ...typeof result.secretCount === "number" ? { secretCount: result.secretCount } : {}
1354
+ }
1355
+ };
1356
+ }
1357
+ };
1358
+ function buildTools() {
1359
+ return [
1360
+ statusTool,
1361
+ planTool,
1362
+ generateTool,
1363
+ validateTool,
1364
+ locateTool,
1365
+ readTool,
1366
+ explainTool
1367
+ ];
1368
+ }
1369
+
1370
+ // src/mcp/prompts.ts
1371
+ var ORIENT = `You have a codebase-snapshot server for this repository. Build a map of it before reading any code.
1372
+
1373
+ 1. Call snapshot_status. If no snapshot exists, call snapshot_plan to preview it, then snapshot_generate.
1374
+ 2. Call snapshot_locate for the areas that matter. Search by exported symbol, route or path fragment rather than guessing filenames.
1375
+ 3. Report back: the layers present, the entry points, and the files that matter here \u2014 cited as paths with their part.
1376
+ 4. Read one file at a time with snapshot_read, one window at a time. Do not read a part end to end.`;
1377
+ var KEEP_CURRENT = `Keep the context you were given provably current while you work.
1378
+
1379
+ 1. Before relying on a snapshot from an earlier session, call snapshot_validate.
1380
+ 2. If it reports drift, call snapshot_generate once, then snapshot_validate again.
1381
+ 3. After changes you make, call snapshot_validate and report what it said.
1382
+ 4. Never call a snapshot current unless snapshot_validate said so in this session.`;
1383
+ function withFocus(base, focus) {
1384
+ return typeof focus === "string" && focus.trim().length > 0 ? `${base}
1385
+
1386
+ Focus for this pass: ${focus.trim()}` : base;
1387
+ }
1388
+ function buildPrompts() {
1389
+ return [
1390
+ {
1391
+ name: "orient",
1392
+ title: "Orient in this repository",
1393
+ description: "Map the repository from the snapshot before reading any of it.",
1394
+ arguments: [
1395
+ {
1396
+ name: "focus",
1397
+ description: 'Optional area to concentrate on, for example "authentication".',
1398
+ required: false
1399
+ }
1400
+ ],
1401
+ build(args) {
1402
+ return {
1403
+ description: "Map the repository before reading it.",
1404
+ messages: [{ role: "user", content: { type: "text", text: withFocus(ORIENT, args.focus) } }]
1405
+ };
1406
+ }
1407
+ },
1408
+ {
1409
+ name: "keep-current",
1410
+ title: "Keep the snapshot current",
1411
+ description: "Validate before trusting earlier context, and again after making changes.",
1412
+ arguments: [],
1413
+ build() {
1414
+ return {
1415
+ description: "Keep the context current.",
1416
+ messages: [{ role: "user", content: { type: "text", text: KEEP_CURRENT } }]
1417
+ };
1418
+ }
1419
+ }
1420
+ ];
1421
+ }
1422
+
1423
+ // src/mcp/uris.ts
1424
+ var STATIC_RESOURCE_URIS = [
1425
+ "snapshot://manifest",
1426
+ "snapshot://contract",
1427
+ "snapshot://skill",
1428
+ "snapshot://reports/audit",
1429
+ "snapshot://reports/tree",
1430
+ "snapshot://reports/navigation"
1431
+ ];
1432
+ var RESOURCE_TEMPLATE_URIS = ["snapshot://parts/{index}"];
1433
+
1434
+ // src/agent/option-scope.ts
1435
+ var VALIDATE_OPTIONS = [
1436
+ "cwd",
1437
+ "out-dir",
1438
+ "max-part-bytes",
1439
+ "large-file-bytes",
1440
+ "max-monolith-bytes",
1441
+ "include-monolith",
1442
+ "include-sensitive-files",
1443
+ "skip-artifacts",
1444
+ "only-artifacts",
1445
+ "no-config",
1446
+ "discovery",
1447
+ "format",
1448
+ "mode",
1449
+ "help",
1450
+ "version"
1451
+ ];
1452
+ var COMMAND_OPTION_SCOPE = {
1453
+ generate: [
1454
+ "cwd",
1455
+ "out-dir",
1456
+ "max-part-bytes",
1457
+ "large-file-bytes",
1458
+ "max-monolith-bytes",
1459
+ "include-monolith",
1460
+ "include-sensitive-files",
1461
+ "skip-artifacts",
1462
+ "only-artifacts",
1463
+ "no-config",
1464
+ "discovery",
1465
+ "format",
1466
+ "mode",
1467
+ "dry-run",
1468
+ "watch",
1469
+ "autopilot",
1470
+ "no-install",
1471
+ "help",
1472
+ "version"
1473
+ ],
1474
+ validate: VALIDATE_OPTIONS,
1475
+ report: [
1476
+ "cwd",
1477
+ "out-dir",
1478
+ "max-part-bytes",
1479
+ "large-file-bytes",
1480
+ "max-monolith-bytes",
1481
+ "include-monolith",
1482
+ "include-sensitive-files",
1483
+ "skip-artifacts",
1484
+ "only-artifacts",
1485
+ "no-config",
1486
+ "format",
1487
+ "mode",
1488
+ "help",
1489
+ "version"
1490
+ ],
1491
+ explain: [
1492
+ "cwd",
1493
+ "out-dir",
1494
+ "max-part-bytes",
1495
+ "large-file-bytes",
1496
+ "max-monolith-bytes",
1497
+ "include-monolith",
1498
+ "include-sensitive-files",
1499
+ "skip-artifacts",
1500
+ "only-artifacts",
1501
+ "no-config",
1502
+ "discovery",
1503
+ "format",
1504
+ "mode",
1505
+ "help",
1506
+ "version"
1507
+ ],
1508
+ diff: ["format", "mode", "no-config", "help", "version"],
1509
+ describe: ["format", "mode", "help", "version"],
1510
+ init: [
1511
+ "cwd",
1512
+ "out-dir",
1513
+ "max-part-bytes",
1514
+ "include-sensitive-files",
1515
+ "skip-artifacts",
1516
+ "only-artifacts",
1517
+ "preset",
1518
+ "include",
1519
+ "exclude",
1520
+ "yes",
1521
+ "force",
1522
+ "dry-run",
1523
+ "format",
1524
+ "mode",
1525
+ "help",
1526
+ "version"
1527
+ ],
1528
+ skill: ["cwd", "format", "mode", "install", "target", "help", "version"],
1529
+ // `mcp` reads the repository, so it takes --cwd. It has no --format: stdout carries JSON-RPC
1530
+ // messages, and there is no second shape to choose, so offering the flag would be a lie.
1531
+ mcp: ["cwd", "mode", "help", "version"],
1532
+ setup: [
1533
+ "cwd",
1534
+ "out-dir",
1535
+ "max-part-bytes",
1536
+ "include-sensitive-files",
1537
+ "skip-artifacts",
1538
+ "only-artifacts",
1539
+ "preset",
1540
+ "include",
1541
+ "exclude",
1542
+ "yes",
1543
+ "force",
1544
+ "no-install",
1545
+ "dry-run",
1546
+ "format",
1547
+ "mode",
1548
+ "help",
1549
+ "version"
1550
+ ]
1551
+ };
1552
+ var COMMAND_ORDER = [
1553
+ "generate",
1554
+ "validate",
1555
+ "report",
1556
+ "explain",
1557
+ "init",
1558
+ "setup",
1559
+ "diff",
1560
+ "describe",
1561
+ "skill",
1562
+ "mcp"
1563
+ ];
1564
+ function commandsForOption(option) {
1565
+ return COMMAND_ORDER.filter((command) => COMMAND_OPTION_SCOPE[command].includes(option));
1566
+ }
1567
+ function optionAppliesTo(option) {
1568
+ return commandsForOption(option).join("/");
1569
+ }
1570
+
1571
+ // src/agent/contract.ts
1572
+ var CONTRACT_VERSION = "1.0.0";
1573
+ var PACKAGE_NAME = "codebase-snapshot";
1574
+ var MAJOR_VERSION = GENERATOR_VERSION.split(".")[0];
1575
+ var ARTIFACT_SUMMARY = {
1576
+ readme: "README.md describing the snapshot at the output root.",
1577
+ manifest: "manifest.json: machine-readable index of every file, part, hash and config.",
1578
+ parts: "parts/*.md: the packed, budget-limited markdown parts that carry the code.",
1579
+ navigation: "reports/navigation.md: links between parts and layers.",
1580
+ projectTree: "reports/project-tree.md: the directory tree of included files.",
1581
+ audit: "reports/audit.md: secret findings, large files, parse diagnostics, git state.",
1582
+ monolith: "full-codebase.json and full-codebase.md: the whole repository in one file (off by default)."
1583
+ };
1584
+ var ARTIFACT_DEFAULTS = {
1585
+ readme: true,
1586
+ manifest: true,
1587
+ parts: true,
1588
+ navigation: true,
1589
+ projectTree: true,
1590
+ audit: true,
1591
+ monolith: false
1592
+ };
1593
+ function renderContractText(contract) {
1594
+ const lines = [];
1595
+ lines.push(`${contract.package} v${contract.version}`);
1596
+ lines.push(contract.summary);
1597
+ lines.push("");
1598
+ lines.push(`Node : ${contract.requires.node}`);
1599
+ lines.push(`Schema : ${contract.schemaVersion} (contract ${contract.contractVersion})`);
1600
+ lines.push(`One-shot : ${contract.invocation.oneShot} <command>`);
1601
+ lines.push(`Installed : ${contract.invocation.installed} <command>`);
1602
+ lines.push("");
1603
+ lines.push("Workflow:");
1604
+ for (const step of contract.workflow) {
1605
+ lines.push(` ${step.step}. ${step.id}`);
1606
+ lines.push(` run : ${step.run}`);
1607
+ lines.push(` expect : ${step.expect}`);
1608
+ if (step.notes) lines.push(` note : ${step.notes}`);
1609
+ }
1610
+ lines.push("");
1611
+ lines.push("Commands:");
1612
+ for (const command of contract.commands) {
1613
+ const args = command.positionals.length > 0 ? ` ${command.positionals.join(" ")}` : "";
1614
+ lines.push(` ${(command.name + args).padEnd(26)} ${command.summary}`);
1615
+ }
1616
+ lines.push("");
1617
+ lines.push("Exit codes:");
1618
+ for (const [code, meaning] of Object.entries(contract.exitCodes)) {
1619
+ lines.push(` ${code} ${meaning}`);
1620
+ }
1621
+ lines.push("");
1622
+ lines.push(`Presets (${contract.presets.length}): ${contract.presets.map((p) => p.name).join(", ")}`);
1623
+ lines.push("");
1624
+ lines.push("Artifacts:");
1625
+ for (const artifact of contract.artifacts) {
1626
+ lines.push(` ${artifact.name.padEnd(12)} ${artifact.default ? "on " : "off"} ${artifact.summary}`);
1627
+ }
1628
+ lines.push("");
1629
+ lines.push("Options:");
1630
+ for (const option of contract.options) {
1631
+ const fallback = option.default === void 0 ? "" : ` [${JSON.stringify(option.default)}]`;
1632
+ lines.push(` ${option.flag.padEnd(24)} ${option.appliesTo.padEnd(32)}${fallback}`);
1633
+ lines.push(` ${"".padEnd(24)} ${option.summary}`);
1634
+ }
1635
+ lines.push("");
1636
+ lines.push("MCP:");
1637
+ lines.push(` command ${contract.mcp.command}`);
1638
+ lines.push(` transport ${contract.mcp.transport}`);
1639
+ lines.push(
1640
+ ` versions modern ${contract.mcp.protocolVersions.modern.join(", ")} / legacy ${contract.mcp.protocolVersions.legacy.join(", ")}`
1641
+ );
1642
+ lines.push(
1643
+ ` tools ${contract.mcp.tools.map((tool) => tool.readOnly ? tool.name : `${tool.name} (writes)`).join(", ")}`
1644
+ );
1645
+ lines.push(` prompts ${contract.mcp.prompts.map((prompt) => prompt.name).join(", ")}`);
1646
+ lines.push(` resources ${contract.mcp.resources.join(", ")}`);
1647
+ lines.push(` templates ${contract.mcp.resourceTemplates.join(", ")}`);
1648
+ lines.push("");
1649
+ lines.push("Safety:");
1650
+ for (const note of contract.safety) lines.push(` - ${note}`);
1651
+ lines.push("");
1652
+ lines.push("Troubleshooting:");
1653
+ for (const item of contract.troubleshooting) {
1654
+ lines.push(` ${item.symptom}`);
1655
+ lines.push(` cause: ${item.cause}`);
1656
+ lines.push(` fix : ${item.fix}`);
1657
+ }
1658
+ lines.push("");
1659
+ lines.push("This is the human rendering. Agents should use: describe --format json");
1660
+ return lines.join("\n");
1661
+ }
1662
+ function buildAgentContract() {
1663
+ return {
1664
+ generator: GENERATOR_NAME,
1665
+ package: PACKAGE_NAME,
1666
+ version: GENERATOR_VERSION,
1667
+ schemaVersion: SCHEMA_VERSION,
1668
+ contractVersion: CONTRACT_VERSION,
1669
+ summary: "Turns a repository into an LLM-ready snapshot: code packed into size-budgeted markdown parts, plus a manifest, a project tree, navigation and a secret audit. Auto-detects the project shape (framework, language, monorepo, docs, infra).",
1670
+ requires: { node: ">=18.0.0", dependencies: ["typescript"] },
1671
+ invocation: {
1672
+ oneShot: `npx ${PACKAGE_NAME}@latest`,
1673
+ installed: "npx codebase-snapshot",
1674
+ bins: ["codebase-snapshot", "snapshot"],
1675
+ notes: [
1676
+ `The bare package name also works: npx ${PACKAGE_NAME} <command>.`,
1677
+ "`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.",
1678
+ `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.`,
1679
+ "Both bin names point at the same file. npm resolves a package by its bin entries, so they must stay aliases of one executable.",
1680
+ "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`.",
1681
+ "Human progress goes to stderr. With --format json the payload on stdout is pure JSON.",
1682
+ "A failing run also writes pure JSON to stdout: { error: { message, exitCode } }. The exit code and the stderr line are unchanged."
1683
+ ]
1684
+ },
1685
+ workflow: [
1686
+ {
1687
+ step: 1,
1688
+ id: "verify",
1689
+ run: `npx ${PACKAGE_NAME}@latest --version`,
1690
+ // The requirement is the major version; the exact build is context. A
1691
+ // floating tag may print a different patch today than next week, and an agent
1692
+ // must not conclude that a working tool is broken because of a patch.
1693
+ 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)`,
1694
+ 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\`.`
1695
+ },
1696
+ {
1697
+ step: 2,
1698
+ id: "understand",
1699
+ run: `npx ${PACKAGE_NAME}@latest describe --format json`,
1700
+ expect: "the full contract as JSON: commands, options, presets, exit codes",
1701
+ notes: "Read this before acting if you have no prior knowledge of the tool."
1702
+ },
1703
+ {
1704
+ step: 3,
1705
+ id: "configure",
1706
+ run: `npx ${PACKAGE_NAME}@latest init --yes`,
1707
+ expect: 'writes a commented config file listing every setting at its current value: snapshot.config.ts only when the running Node can strip types (>= 22.6) and the project is an ES module (`"type": "module"` in package.json); snapshot.config.mjs otherwise',
1708
+ notes: "Non-interactive. The detected preset is chosen automatically; override with --preset <name>. Skip this step to run on built-in defaults."
1709
+ },
1710
+ {
1711
+ step: 4,
1712
+ id: "generate",
1713
+ run: `npx ${PACKAGE_NAME}@latest generate`,
1714
+ expect: "exit 0, and ./snapshot/ written with parts, reports and manifest.json"
1715
+ },
1716
+ {
1717
+ step: 5,
1718
+ id: "verify-output",
1719
+ run: `npx ${PACKAGE_NAME}@latest validate`,
1720
+ expect: "exit 0 means the snapshot matches the files on disk",
1721
+ notes: "Exit 1 means drift or a damaged snapshot; re-run generate."
1722
+ },
1723
+ {
1724
+ step: 6,
1725
+ id: "consume",
1726
+ run: "read ./snapshot/parts/*.md, then ./snapshot/manifest.json, then ./snapshot/reports/audit.md",
1727
+ expect: "the code, the index of what was included, and the risk findings",
1728
+ notes: "manifest.json.files[] lists every included file with layer, kind, language, part and summary. manifest.skipped[] explains every file that was left out and why."
1729
+ }
1730
+ ],
1731
+ commands: [
1732
+ {
1733
+ name: "generate",
1734
+ aliases: ["default"],
1735
+ 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`.",
1736
+ positionals: [],
1737
+ exits: [0, 1, 2]
1738
+ },
1739
+ {
1740
+ name: "validate",
1741
+ aliases: ["check"],
1742
+ summary: "Compare the snapshot on disk with the repository; exit 1 on drift. `check` is an alias of `validate`.",
1743
+ positionals: [],
1744
+ exits: [0, 1]
1745
+ },
1746
+ {
1747
+ name: "report",
1748
+ aliases: [],
1749
+ summary: "Print snapshot health, layer metrics and the secret audit.",
1750
+ positionals: [],
1751
+ exits: [0, 1]
1752
+ },
1753
+ {
1754
+ name: "setup",
1755
+ aliases: [],
1756
+ summary: "Everything a repository needs in one command, with defaults: write the config when there is none (an existing one is kept), add the npm scripts, devDependency and .gitignore entries, install dependencies with the project's package manager, then generate the snapshot. --no-install skips the install; --dry-run writes nothing.",
1757
+ positionals: [],
1758
+ exits: [0, 1, 2]
1759
+ },
1760
+ {
1761
+ name: "init",
1762
+ aliases: [],
1763
+ summary: "Write a commented config file (snapshot.config.ts only when Node can strip types and the project is an ES module; snapshot.config.mjs otherwise), add the snapshot scripts and the devDependency, and ignore the snapshot directories. Nothing else is written. Interactive, or --yes for CI.",
1764
+ positionals: [],
1765
+ exits: [0, 1, 2]
1766
+ },
1767
+ {
1768
+ name: "explain",
1769
+ aliases: [],
1770
+ summary: "Explain how one path would be treated: layer, kind, part, skip reason. Always exits 0 when it can report; read `included` and `reason` instead of the exit code.",
1771
+ positionals: ["<path>"],
1772
+ exits: [0]
1773
+ },
1774
+ {
1775
+ name: "diff",
1776
+ aliases: [],
1777
+ summary: "Compare two snapshots (directories or manifest.json files).",
1778
+ positionals: ["<before>", "<after>"],
1779
+ exits: [0, 1]
1780
+ },
1781
+ {
1782
+ name: "describe",
1783
+ aliases: [],
1784
+ summary: "Print this contract. --format json for machines, text for humans.",
1785
+ positionals: [],
1786
+ exits: [0]
1787
+ },
1788
+ {
1789
+ name: "skill",
1790
+ aliases: [],
1791
+ 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.",
1792
+ positionals: [],
1793
+ exits: [0, 1, 2]
1794
+ },
1795
+ {
1796
+ name: "mcp",
1797
+ aliases: [],
1798
+ 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.",
1799
+ positionals: [],
1800
+ exits: [0, 2]
1801
+ }
1802
+ ],
1803
+ exitCodes: {
1804
+ "0": "success (and validate: no drift)",
1805
+ "1": "drift detected, or generation refused/failed (nothing was written)",
1806
+ "2": "usage error: unknown flag/command, bad value, wrong argument count"
1807
+ },
1808
+ options: [
1809
+ {
1810
+ flag: "--out-dir",
1811
+ type: "string",
1812
+ default: "snapshot",
1813
+ appliesTo: optionAppliesTo("out-dir"),
1814
+ summary: "Where the snapshot is written. Must not be the repo root or a directory the tool did not create."
1815
+ },
1816
+ {
1817
+ flag: "--max-part-bytes",
1818
+ type: "string",
1819
+ default: 307200,
1820
+ appliesTo: optionAppliesTo("max-part-bytes"),
1821
+ summary: 'Byte budget per markdown part. Accepts "120k" / "1.5mb". Minimum 4096.'
1822
+ },
1823
+ {
1824
+ flag: "--large-file-bytes",
1825
+ type: "string",
1826
+ default: 204800,
1827
+ appliesTo: optionAppliesTo("large-file-bytes"),
1828
+ summary: "Above this size a file gets a head/tail preview instead of the full body."
1829
+ },
1830
+ {
1831
+ flag: "--max-monolith-bytes",
1832
+ type: "string",
1833
+ default: "unset",
1834
+ appliesTo: optionAppliesTo("max-monolith-bytes"),
1835
+ summary: "Skip the monolith artifacts when they would exceed this size."
1836
+ },
1837
+ {
1838
+ flag: "--include-monolith",
1839
+ type: "boolean",
1840
+ default: false,
1841
+ appliesTo: optionAppliesTo("include-monolith"),
1842
+ summary: "Also write full-codebase.json and full-codebase.md."
1843
+ },
1844
+ {
1845
+ flag: "--include-sensitive-files",
1846
+ type: "boolean",
1847
+ default: false,
1848
+ appliesTo: optionAppliesTo("include-sensitive-files"),
1849
+ summary: "Also emit .env / *.pem / .npmrc style files. Off by default; they are recorded as skipped."
1850
+ },
1851
+ {
1852
+ flag: "--discovery",
1853
+ type: "string",
1854
+ default: "auto",
1855
+ appliesTo: optionAppliesTo("discovery"),
1856
+ summary: "How files are discovered: auto (git when usable), git (require git), walk (read the root .gitignore directly; the escape hatch for a .git that cannot be listed)."
1857
+ },
1858
+ {
1859
+ flag: "--skip-artifacts",
1860
+ type: "string",
1861
+ default: "unset",
1862
+ appliesTo: optionAppliesTo("skip-artifacts"),
1863
+ summary: "Comma-separated artifacts to omit (readme, manifest, parts, navigation, projectTree, audit, monolith)."
1864
+ },
1865
+ {
1866
+ flag: "--only-artifacts",
1867
+ type: "string",
1868
+ default: "unset",
1869
+ appliesTo: optionAppliesTo("only-artifacts"),
1870
+ summary: "Write only these artifacts. Cannot be combined with --skip-artifacts."
1871
+ },
1872
+ {
1873
+ flag: "--format",
1874
+ type: "string",
1875
+ default: "text",
1876
+ // `optionAppliesTo` always returns the explicit slash list, so this is the same scoped form
1877
+ // as every other option — no prose sentinel a machine consumer could misread.
1878
+ appliesTo: optionAppliesTo("format"),
1879
+ summary: 'Output format: "text" or "json". JSON goes to stdout, progress to stderr.'
1880
+ },
1881
+ {
1882
+ flag: "--mode",
1883
+ type: "string",
1884
+ default: "generate",
1885
+ appliesTo: optionAppliesTo("mode"),
1886
+ summary: "Select the command when no command word is given. Values: generate, validate, check, report, init, setup, explain, diff, describe, skill. A command word and --mode must name the same command; a contradiction is a usage error (exit 2)."
1887
+ },
1888
+ {
1889
+ flag: "--cwd",
1890
+ type: "string",
1891
+ default: "process.cwd()",
1892
+ appliesTo: optionAppliesTo("cwd"),
1893
+ summary: "Repository to operate on. Must be an existing directory."
1894
+ },
1895
+ {
1896
+ flag: "--dry-run",
1897
+ type: "boolean",
1898
+ default: false,
1899
+ appliesTo: optionAppliesTo("dry-run"),
1900
+ summary: "Compute everything and print the result without writing."
1901
+ },
1902
+ {
1903
+ flag: "--watch",
1904
+ type: "boolean",
1905
+ default: false,
1906
+ appliesTo: optionAppliesTo("watch"),
1907
+ summary: "Regenerate on every change until interrupted."
1908
+ },
1909
+ {
1910
+ flag: "--no-config",
1911
+ type: "boolean",
1912
+ default: false,
1913
+ appliesTo: optionAppliesTo("no-config"),
1914
+ summary: "Ignore the config file and run on the built-in defaults."
1915
+ },
1916
+ {
1917
+ flag: "--preset",
1918
+ type: "string",
1919
+ default: "detected",
1920
+ appliesTo: optionAppliesTo("preset"),
1921
+ summary: "Force a project preset instead of detecting one. Unknown names are a usage error."
1922
+ },
1923
+ {
1924
+ flag: "--include",
1925
+ type: "string",
1926
+ default: "unset",
1927
+ appliesTo: optionAppliesTo("include"),
1928
+ summary: "Comma-separated allow-list of globs to record in the generated config."
1929
+ },
1930
+ {
1931
+ flag: "--exclude",
1932
+ type: "string",
1933
+ default: "unset",
1934
+ appliesTo: optionAppliesTo("exclude"),
1935
+ summary: "Comma-separated globs to record in the generated config."
1936
+ },
1937
+ {
1938
+ flag: "--yes",
1939
+ type: "boolean",
1940
+ default: false,
1941
+ appliesTo: optionAppliesTo("yes"),
1942
+ summary: "Accept every default without prompting. Required in a non-TTY."
1943
+ },
1944
+ {
1945
+ flag: "--force",
1946
+ type: "boolean",
1947
+ default: false,
1948
+ appliesTo: optionAppliesTo("force"),
1949
+ summary: "Overwrite an existing config file."
1950
+ },
1951
+ {
1952
+ flag: "--autopilot",
1953
+ type: "boolean",
1954
+ default: false,
1955
+ appliesTo: optionAppliesTo("autopilot"),
1956
+ 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)."
1957
+ },
1958
+ {
1959
+ flag: "--install",
1960
+ type: "boolean",
1961
+ default: false,
1962
+ appliesTo: optionAppliesTo("install"),
1963
+ summary: "Write the skill into the repository instead of printing it."
1964
+ },
1965
+ {
1966
+ flag: "--target",
1967
+ type: "string",
1968
+ default: "all",
1969
+ appliesTo: optionAppliesTo("target"),
1970
+ summary: 'Where to install: "claude", "cursor" or "all".'
1971
+ },
1972
+ {
1973
+ flag: "--no-install",
1974
+ type: "boolean",
1975
+ default: false,
1976
+ appliesTo: optionAppliesTo("no-install"),
1977
+ 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)."
1978
+ }
1979
+ ],
1980
+ presets: PRESETS.map((preset) => ({
1981
+ name: preset.name,
1982
+ priority: preset.priority,
1983
+ recommendedBudget: preset.recommendedBudget ?? "300k",
1984
+ excludes: preset.exclude ? [...preset.exclude] : []
1985
+ })),
1986
+ artifacts: ARTIFACT_NAMES.map((name) => ({
1987
+ name,
1988
+ default: ARTIFACT_DEFAULTS[name] ?? true,
1989
+ summary: ARTIFACT_SUMMARY[name] ?? name
1990
+ })),
1991
+ config: {
1992
+ fileNames: [
1993
+ "snapshot.config.ts",
1994
+ "snapshot.config.js",
1995
+ "snapshot.config.mjs",
1996
+ "snapshot.config.cjs",
1997
+ "snapshot.config.json",
1998
+ ".snapshotrc.json",
1999
+ ".snapshotrc",
2000
+ 'package.json \u2192 "snapshot" key'
2001
+ ],
2002
+ settings: [
2003
+ { key: "outDir", type: "string", default: "snapshot", summary: "Snapshot output directory." },
2004
+ { key: "tmpDir", type: "string", default: "snapshot.tmp", summary: "Scratch directory used for the atomic swap." },
2005
+ { key: "branch", type: "string", default: "main", summary: "Branch recorded when git cannot report one." },
2006
+ { key: "preset", type: "string", default: "detected", summary: "Force a preset. Unknown names are an error." },
2007
+ { key: "maxPartBytes", type: "number | string", default: 307200, summary: "Per-part byte budget." },
2008
+ { key: "largeFileBytes", type: "number | string", default: 204800, summary: "Preview threshold for large files." },
2009
+ { key: "previewHeadLines", type: "number", default: 80, summary: "Lines kept from the head of a large file." },
2010
+ { key: "previewTailLines", type: "number", default: 40, summary: "Lines kept from the tail of a large file." },
2011
+ { key: "includeMonolith", type: "boolean", default: false, summary: "Also write the single-file monolith." },
2012
+ { key: "maxMonolithBytes", type: "number | string | null", default: null, summary: "Skip the monolith above this size." },
2013
+ { key: "includeSensitiveFiles", type: "boolean", default: false, summary: "Emit .env / *.pem / credential files." },
2014
+ { key: "include", type: "string[]", default: "[]", summary: "Allow-list of globs; empty means everything." },
2015
+ { key: "excludePatterns", type: "string[]", default: "[]", summary: "Extra exclude globs." },
2016
+ { key: "discovery", type: '"auto" | "git" | "walk"', default: "auto", summary: "How files are discovered; `walk` scans the tree and reads .gitignore directly when a .git directory cannot be listed." },
2017
+ { key: "artifacts", type: "object", default: "all on except monolith", summary: "Per-artifact opt-out." },
2018
+ { key: "budgets", type: "object", default: "{ default, byLayer }", summary: "Per-layer byte budgets." },
2019
+ { key: "layersMode", type: '"merge" | "replace"', default: "merge", summary: "Whether custom layers extend or replace the preset's." },
2020
+ { key: "layers", type: "array", default: "preset layers", summary: "Custom layer rules (glob or RegExp)." },
2021
+ { key: "kinds", type: "array", default: "preset kinds", summary: "Custom kind rules." },
2022
+ { key: "secrets", type: "object", default: "{}", summary: "Custom secret patterns, or disable the defaults." },
2023
+ { key: "hooks", type: "object", default: "{}", summary: "afterGenerate(manifest) runs after a successful generate." }
2024
+ ]
2025
+ },
2026
+ output: {
2027
+ layout: [
2028
+ "<outDir>/README.md",
2029
+ "<outDir>/manifest.json",
2030
+ "<outDir>/parts/part-NNN.md",
2031
+ "<outDir>/reports/project-tree.md",
2032
+ "<outDir>/reports/navigation.md",
2033
+ "<outDir>/reports/audit.md",
2034
+ "<outDir>/.codebase-snapshot (ownership marker, always written)"
2035
+ ],
2036
+ notes: [
2037
+ "Parts are ordered and budget-limited; a single file larger than the budget becomes a head/tail preview.",
2038
+ "manifest.json.files[] carries relativePath, layer, kind, language, part, inclusionMode and hash; `summary` is conditional \u2014 it is absent for languages with no structural summary, and `summaryOmitted: true` marks a summary dropped to fit the part budget; encoding appears when the file was not plain UTF-8.",
2039
+ "manifest.skipped[] carries the path and the exact reason it was excluded.",
2040
+ `The output directory is replaced atomically; ${MIN_MAX_PART_BYTES} bytes is the smallest legal budget.`
2041
+ ]
2042
+ },
2043
+ safety: [
2044
+ "Sensitive files (.env, *.pem, .npmrc, .ssh/, .tfstate) are excluded by default and listed in manifest.skipped[].",
2045
+ "Secret-shaped strings in ordinary source files are redacted in the emitted parts.",
2046
+ "The tool refuses to replace an outDir that it did not create, so a mistyped outDir cannot delete source code.",
2047
+ "Nothing is ever written above the repository root; --out-dir may not be the root or an ancestor."
2048
+ ],
2049
+ mcp: {
2050
+ command: `npx ${PACKAGE_NAME} mcp`,
2051
+ transport: "stdio",
2052
+ protocolVersions: {
2053
+ modern: [...MODERN_PROTOCOL_VERSIONS],
2054
+ legacy: [...LEGACY_PROTOCOL_VERSIONS],
2055
+ default: DEFAULT_LEGACY_PROTOCOL_VERSION
2056
+ },
2057
+ tools: buildTools().map((tool) => ({
2058
+ name: tool.name,
2059
+ title: tool.title,
2060
+ summary: tool.description,
2061
+ readOnly: tool.annotations.readOnlyHint,
2062
+ parameters: Object.keys(tool.inputSchema.properties),
2063
+ required: tool.inputSchema.required ?? []
2064
+ })),
2065
+ prompts: buildPrompts().map((prompt) => ({
2066
+ name: prompt.name,
2067
+ summary: prompt.description,
2068
+ arguments: prompt.arguments.map((argument) => argument.name)
2069
+ })),
2070
+ resources: [...STATIC_RESOURCE_URIS],
2071
+ resourceTemplates: [...RESOURCE_TEMPLATE_URIS],
2072
+ notes: [
2073
+ "stdout carries JSON-RPC messages only; every note and log line goes to stderr.",
2074
+ "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`.",
2075
+ "Everything it can do is a subset of the CLI. It never installs packages and never writes a config file.",
2076
+ "A `root` argument may name a sub-directory of the server's --cwd, and nothing outside it is readable."
2077
+ ]
2078
+ },
2079
+ troubleshooting: [
2080
+ {
2081
+ symptom: "Command prints nothing and exits 0",
2082
+ cause: "A stale global install of this package (often a leftover `npm link`) is shadowing npx: npm runs the global binary instead of installing the published one.",
2083
+ fix: "Check `npm ls -g --depth=0` for codebase-snapshot and remove it with `npm rm -g codebase-snapshot`. If it is not installed globally, clear the npx cache with `npm cache clean --force` and retry."
2084
+ },
2085
+ {
2086
+ symptom: "Exit 1 with 'Refusing to replace outDir'",
2087
+ cause: "The outDir exists and contains files this tool did not write.",
2088
+ fix: "Choose a new/empty --out-dir, or delete the existing directory yourself."
2089
+ },
2090
+ {
2091
+ symptom: "Exit 1 with 'The config file could not be read'",
2092
+ cause: "snapshot.config.* exists but does not parse.",
2093
+ fix: "Fix the syntax, or delete the config to use the defaults."
2094
+ },
2095
+ {
2096
+ symptom: "Exit 1 from validate",
2097
+ cause: "The snapshot no longer matches the files on disk.",
2098
+ fix: "Re-run generate."
2099
+ },
2100
+ {
2101
+ symptom: "A directory you expected is missing from the snapshot",
2102
+ cause: "It is excluded by the preset, by default ignores, or by your own globs.",
2103
+ fix: "Run `explain <path>`; manifest.skipped[] also records the reason for every file."
2104
+ }
2105
+ ]
2106
+ };
2107
+ }
2108
+
2109
+ // src/agent/skill.ts
2110
+ var PACKAGE_NAME2 = "codebase-snapshot";
2111
+ var MAJOR_VERSION2 = GENERATOR_VERSION.split(".")[0];
2112
+ var SKILL_NAME = "codebase-snapshot";
2113
+ function renderSkill() {
2114
+ return `---
2115
+ name: ${SKILL_NAME}
2116
+ description: Create an LLM-ready snapshot of a repository (size-budgeted markdown parts, a JSON manifest, project tree, navigation and a secret audit) when a whole codebase has to be read, summarized or handed to a model that cannot clone it. Use when asked to snapshot a codebase, prepare repository context, or analyze a project's structure.
2117
+ version: ${GENERATOR_VERSION}
2118
+ ---
2119
+
2120
+ # codebase-snapshot
2121
+
2122
+ Turns a repository into an LLM-ready snapshot under \`./snapshot/\`: code packed into
2123
+ size-budgeted markdown parts, plus a manifest that indexes every file, a project tree, a
2124
+ navigation index and a secret audit.
2125
+
2126
+ Use it when you need to understand a whole repository, or produce repository context for
2127
+ another model, and reading files one by one would not fit in context.
2128
+
2129
+ ## Before you start
2130
+
2131
+ This tool is a Node CLI. It needs **Node.js 18 or newer**. You do not need to install it
2132
+ globally, and you do not need to clone anything.
2133
+
2134
+ **Pin a version** \u2014 \`npx ${PACKAGE_NAME2}@${GENERATOR_VERSION} <command>\` \u2014 when you
2135
+ need the same version on every machine. The bare package name works too: this package ships
2136
+ one executable file, published under two names (\`codebase-snapshot\` and \`snapshot\`), which is
2137
+ what lets npm pick it. A global install gives the short command \`snapshot\`; inside a project
2138
+ that has this package installed, \`npx snapshot\` resolves the local copy first. If a command
2139
+ exits 0 printing nothing, the executable was not resolved \u2014 clear the npm cache and retry;
2140
+ do not assume the repository is broken.
2141
+
2142
+ ## Step by step
2143
+
2144
+ 1. **Verify the tool.** Run \`npx ${PACKAGE_NAME2}@latest --version\`. Expect \`v${MAJOR_VERSION2}\` or
2145
+ newer on stdout \u2014 compare the major version, not the patch, because the
2146
+ \`@latest\` tag can lag this release. If what it prints is older, pin the exact
2147
+ release (\`@${GENERATOR_VERSION}\`) instead of retrying. Empty output means you hit the
2148
+ bare-spec problem above.
2149
+
2150
+ 2. **Read the contract, not the prose.** Run
2151
+ \`npx ${PACKAGE_NAME2}@latest describe --format json\`. It returns the full machine
2152
+ contract: every command, every option with its default, the exit-code meanings, the
2153
+ 25 project presets, the artifact list, the config settings and a troubleshooting
2154
+ table. Read it before you make decisions; do not guess option names.
2155
+
2156
+ 3. **Look before you leap (optional but cheap).** To see how a specific file or directory
2157
+ would be treated, run \`npx ${PACKAGE_NAME2}@latest explain <path>\`. It reports the
2158
+ layer, kind, language, target part, and the exact reason a path would be skipped.
2159
+ \`--format json\` if you want to parse it.
2160
+
2161
+ 3b. **One command, if the user just wants it working.** \`npx ${PACKAGE_NAME2}@latest setup\`
2162
+ does steps 4\u20136 in one go with defaults: it writes the config (an existing one is kept),
2163
+ adds the npm scripts and the devDependency, installs with the project's package manager,
2164
+ and generates. Use \`--no-install\` when the machine is offline or the user does not want
2165
+ their lockfile touched. \`--autopilot\` does exactly the same thing from the default
2166
+ command (\`npx ${PACKAGE_NAME2}@latest --autopilot\`) and is the form to reach for when a
2167
+ single invocation must both set the project up and produce the snapshot. Prefer the
2168
+ explicit steps below when you need to control any of those, or when something fails and
2169
+ you want to see which part.
2170
+
2171
+ 4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
2172
+ the project shape, then writes a commented config file listing **every** setting at its
2173
+ current value. The filename depends on the project: \`snapshot.config.ts\` only when
2174
+ the running Node can strip types (Node >= 22.6) **and** the project is an ES module
2175
+ (\`"type": "module"\` in package.json); otherwise \`snapshot.config.mjs\`. Pass
2176
+ \`--preset <name>\` only if detection is wrong; the preset names are in the contract.
2177
+ Skipping this step is safe **at a terminal**: the first \`generate\` asks once, can write
2178
+ the config for you and then generates. Unattended, a \`generate\` with no config exits 2 \u2014
2179
+ run this step, or \`--autopilot\`, or pass \`--no-config\` to run deliberately on the
2180
+ built-in defaults.
2181
+
2182
+ 5. **Generate.** Run \`npx ${PACKAGE_NAME2}@latest generate\`. Expect exit 0 and a
2183
+ \`./snapshot/\` directory. If you need a different location use \`--out-dir <path>\`; if
2184
+ you need smaller or larger parts use \`--max-part-bytes 120k\`.
2185
+
2186
+ 6. **Verify.** Run \`npx ${PACKAGE_NAME2}@latest validate\`. Exit 0 means the snapshot
2187
+ matches the files on disk. Exit 1 means drift \u2014 re-run step 5.
2188
+
2189
+ 7. **Read the output in this order:**
2190
+ - \`snapshot/parts/part-*.md\` \u2014 the code itself, each file tagged with its layer, kind,
2191
+ language and, when it fits the budget, a structural summary (imports, exports,
2192
+ classes, functions). A dropped summary appears as a \`Summary: (omitted \u2026)\` line.
2193
+ - \`snapshot/manifest.json\` \u2014 the index. \`files[]\` has \`relativePath\`, \`layer\`,
2194
+ \`kind\`, \`language\`, \`part\`, \`inclusionMode\` and \`hashSha256\`, plus \`encoding\`
2195
+ when the file was not plain UTF-8. \`summary\` is **optional**: it is absent for
2196
+ languages with no structural summary, and \`summaryOmitted: true\` marks one dropped
2197
+ to fit the part budget \u2014 never assume it exists.
2198
+ \`skipped[]\` records every excluded file **and why**. Prefer querying this over
2199
+ re-reading the parts.
2200
+ - \`snapshot/reports/audit.md\` \u2014 secrets found, large files, parse diagnostics, git
2201
+ state. Read this before you quote code back to a user.
2202
+ - \`snapshot/reports/project-tree.md\` and \`navigation.md\` \u2014 structure and cross-links.
2203
+
2204
+ ## Project types
2205
+
2206
+ Detection is automatic and covers Next.js, Nuxt, SvelteKit, Remix, Astro, Angular, Solid,
2207
+ Vue+React with Vite, Nest, Express/Fastify/Koa, any Node library, monorepos, Django,
2208
+ FastAPI/Flask, plain Python, Go, Rust, Java/Spring, .NET, PHP/Laravel, Ruby/Rails,
2209
+ Terraform/Docker infra, docs-only repositories, and a generic fallback. It sets sensible
2210
+ layers, kinds, ecosystem ignore patterns and a recommended part budget. For non-TypeScript
2211
+ languages it extracts imports, exports, classes and functions structurally; Vue, Svelte and
2212
+ Astro script blocks go through the real TypeScript compiler. You do not need to configure
2213
+ any of this to get a good first snapshot.
2214
+
2215
+ ## Things that will surprise you
2216
+
2217
+ - **Sensitive files are excluded by default** (\`.env\`, \`*.pem\`, \`.npmrc\`, \`.ssh/\`,
2218
+ \`.tfstate\`) and are listed in \`manifest.skipped[]\`. Add \`--include-sensitive-files\` only
2219
+ if the user asks for them explicitly.
2220
+ - **Secret-shaped strings inside normal source files are redacted** in the emitted parts.
2221
+ If the user needs the raw value, say so rather than working around it.
2222
+ - **The output directory is owned by the tool.** It is replaced atomically, and the tool
2223
+ refuses to replace a directory it did not create. Never point \`--out-dir\` at source
2224
+ code; if you see \`Refusing to replace outDir\`, choose a new directory rather than
2225
+ deleting anything.
2226
+ - **Exit 2 is a usage error**, not a failure of the repository. Re-read the contract
2227
+ before retrying.
2228
+ - **A first run refuses rather than guessing.** In a project with no config and no previous
2229
+ snapshot, \`generate\` without a terminal exits 2 instead of silently using defaults. That
2230
+ is deliberate: say which set-up you want (\`--autopilot\`, \`init --yes\`, or \`--no-config\`)
2231
+ rather than retrying the same command.
2232
+ - **\`--format json\` keeps stdout pure.** Progress goes to stderr, so \`stdout\` is safe to
2233
+ pipe into a JSON parser.
2234
+
2235
+ ## Making this available to other agents
2236
+
2237
+ Run \`npx ${PACKAGE_NAME2}@latest skill --install\`. It writes this skill into the two
2238
+ registries agent tools read by name \u2014 \`.claude/skills/${SKILL_NAME}/SKILL.md\` and
2239
+ \`.cursor/rules/${SKILL_NAME}.mdc\` \u2014 and appends exactly those files to \`.gitignore\`, so the
2240
+ install leaves nothing untracked. It is idempotent, and it refuses a target that resolves
2241
+ outside the repository, so a linked \`.claude/\` cannot be used to write elsewhere.
2242
+
2243
+ It never writes \`AGENTS.md\`. That file is the repository's own agent contract \u2014 build
2244
+ commands, conventions, house rules \u2014 and this tool has no business editing it. If you want
2245
+ this skill in there, run \`npx ${PACKAGE_NAME2}@latest skill\` and paste the text yourself: the
2246
+ diff, and the commit, are then yours.
2247
+
2248
+ ## When the client speaks MCP
2249
+
2250
+ If the agent runs inside an MCP client, \`npx ${PACKAGE_NAME2}@latest mcp\` serves this repository
2251
+ as seven tools over stdio instead of shelling out. Its reads come from the snapshot rather than
2252
+ the working tree, so excluded files and redacted values never reach the context. Use
2253
+ \`snapshot_locate\` before \`snapshot_read\`, and \`snapshot_validate\` before trusting a snapshot
2254
+ from an earlier session.
2255
+ `;
2256
+ }
2257
+
2258
+ // src/mcp/resources.ts
2259
+ var MAX_RESOURCE_BYTES = 512 * 1024;
2260
+ var PART_URI = /^snapshot:\/\/parts\/(\d+)$/;
2261
+ var REPORT_URIS = /* @__PURE__ */ new Map([
2262
+ ["snapshot://reports/audit", { file: "audit.md", title: "Secret audit and risk findings" }],
2263
+ ["snapshot://reports/tree", { file: "project-tree.md", title: "Project tree" }],
2264
+ ["snapshot://reports/navigation", { file: "navigation.md", title: "Navigation index" }]
2265
+ ]);
2266
+ function formatBytes2(bytes) {
2267
+ if (bytes < 1024) return `${bytes} B`;
2268
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
2269
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2270
+ }
2271
+ function truncationMarker(originalBytes) {
2272
+ return `
2273
+
2274
+ [truncated: ${formatBytes2(originalBytes)} total. Use the snapshot_read tool for a line window.]
2275
+ `;
2276
+ }
2277
+ function capUtf8(text, limit) {
2278
+ const size = Buffer.byteLength(text, "utf8");
2279
+ if (size <= limit) return { text, truncated: false };
2280
+ const cut = Buffer.from(text, "utf8").subarray(0, limit).toString("utf8").replace(/\uFFFD+$/, "");
2281
+ return { text: `${cut}${truncationMarker(size)}`, truncated: true };
2282
+ }
2283
+ async function workspaceFor(context) {
2284
+ const root = resolveRoot(void 0, context);
2285
+ if (!root.ok) return { error: root.message };
2286
+ const opened = await openWorkspace(root.root);
2287
+ if (!opened.ok) return { error: opened.message };
2288
+ return opened.workspace;
2289
+ }
2290
+ function readFileIfPresent(file) {
2291
+ try {
2292
+ return fs.readFileSync(file, "utf8");
2293
+ } catch {
2294
+ return null;
2295
+ }
2296
+ }
2297
+ function buildResourceProvider() {
2298
+ return {
2299
+ async list(context) {
2300
+ const resources = [
2301
+ {
2302
+ uri: "snapshot://manifest",
2303
+ name: "manifest",
2304
+ title: "Snapshot manifest",
2305
+ description: "Every file in the snapshot with its layer, kind, language, part and hash, plus the reason each excluded file was excluded.",
2306
+ mimeType: "application/json"
2307
+ },
2308
+ {
2309
+ uri: "snapshot://contract",
2310
+ name: "contract",
2311
+ title: "Tool contract",
2312
+ description: "The machine-readable description of the codebase-snapshot command line.",
2313
+ mimeType: "application/json"
2314
+ },
2315
+ {
2316
+ uri: "snapshot://skill",
2317
+ name: "skill",
2318
+ title: "Agent skill",
2319
+ description: "Usage guidance for an agent working with this tool.",
2320
+ mimeType: "text/markdown"
2321
+ }
2322
+ ];
2323
+ for (const [uri, report] of REPORT_URIS) {
2324
+ resources.push({
2325
+ uri,
2326
+ name: report.file.replace(/\.md$/, ""),
2327
+ title: report.title,
2328
+ description: `${report.title} from the most recent snapshot.`,
2329
+ mimeType: "text/markdown"
2330
+ });
2331
+ }
2332
+ const workspace = await workspaceFor(context);
2333
+ if ("error" in workspace) return resources;
2334
+ const read = readManifest(workspace);
2335
+ if (!read.ok) return resources;
2336
+ for (const part of read.manifest.parts) {
2337
+ resources.push({
2338
+ uri: `snapshot://parts/${part.index}`,
2339
+ name: `part-${String(part.index).padStart(3, "0")}`,
2340
+ title: `Part ${part.index} (${part.fileCount} files, ${formatBytes2(part.byteSize)})`,
2341
+ description: `Part ${part.index} of ${read.manifest.parts.length}: ${part.fileCount} file(s), ${part.lines} line(s), ${formatBytes2(part.byteSize)}.`,
2342
+ mimeType: "text/markdown"
2343
+ });
2344
+ }
2345
+ return resources;
2346
+ },
2347
+ templates() {
2348
+ return [
2349
+ {
2350
+ uriTemplate: "snapshot://parts/{index}",
2351
+ name: "part",
2352
+ title: "Snapshot part",
2353
+ description: "One budgeted part of the snapshot, by its 1-based index.",
2354
+ mimeType: "text/markdown"
2355
+ }
2356
+ ];
2357
+ },
2358
+ async read(uri, context) {
2359
+ if (uri === "snapshot://contract") {
2360
+ const contract = buildAgentContract();
2361
+ return { uri, mimeType: "application/json", text: JSON.stringify(contract, null, 2) };
2362
+ }
2363
+ if (uri === "snapshot://skill") {
2364
+ return { uri, mimeType: "text/markdown", text: renderSkill() };
2365
+ }
2366
+ const workspace = await workspaceFor(context);
2367
+ if ("error" in workspace) return null;
2368
+ const report = REPORT_URIS.get(uri);
2369
+ if (report) {
2370
+ const text = readFileIfPresent(
2371
+ path2.resolve(workspace.root, workspace.config.outDir, "reports", report.file)
2372
+ );
2373
+ if (text === null) return null;
2374
+ return { uri, mimeType: "text/markdown", text: capUtf8(text, MAX_RESOURCE_BYTES).text };
2375
+ }
2376
+ if (uri === "snapshot://manifest") {
2377
+ const read = readManifest(workspace);
2378
+ if (!read.ok) return null;
2379
+ const text = readFileIfPresent(read.file) ?? JSON.stringify(read.manifest, null, 2);
2380
+ return { uri, mimeType: "application/json", text: capUtf8(text, MAX_RESOURCE_BYTES).text };
2381
+ }
2382
+ const partMatch = PART_URI.exec(uri);
2383
+ if (partMatch) {
2384
+ const read = readManifest(workspace);
2385
+ if (!read.ok) return null;
2386
+ const index = Number(partMatch[1]);
2387
+ const part = read.manifest.parts.find((candidate) => candidate.index === index);
2388
+ if (!part) return null;
2389
+ const text = readFileIfPresent(
2390
+ path2.resolve(workspace.root, workspace.config.outDir, "parts", part.filename)
2391
+ );
2392
+ if (text === null) return null;
2393
+ return { uri, mimeType: "text/markdown", text: capUtf8(text, MAX_RESOURCE_BYTES).text };
2394
+ }
2395
+ return null;
2396
+ }
2397
+ };
2398
+ }
2399
+
2400
+ // src/mcp/instructions.ts
2401
+ var SERVER_INSTRUCTIONS = [
2402
+ "codebase-snapshot serves this repository as a size-budgeted snapshot, not as a directory walk.",
2403
+ "",
2404
+ "Work in this order:",
2405
+ "1. snapshot_status \u2014 is there a snapshot here, and has the tree moved since it was built?",
2406
+ "2. snapshot_locate \u2014 find which part holds the code you need.",
2407
+ "3. snapshot_read \u2014 read a window of one file. Never read a part end to end.",
2408
+ "4. snapshot_validate \u2014 before trusting context that came from an earlier conversation.",
2409
+ "",
2410
+ "Rules that matter:",
2411
+ "- Parts are budgeted, not small: a 12-part snapshot is hundreds of kilobytes. Use snapshot_locate first.",
2412
+ "- A file absent from the snapshot has a recorded reason. Ask snapshot_explain, do not assume.",
2413
+ "- snapshot_generate is the only tool that writes. It never installs packages and never writes config.",
2414
+ "- Paths are relative to the repository root the server was started for, and nothing outside it is readable."
2415
+ ].join("\n");
2416
+
2417
+ // src/mcp/stdio.ts
2418
+ var MAX_LINE_BYTES = 32 * 1024 * 1024;
2419
+ function createWriter(output) {
2420
+ return {
2421
+ write(value) {
2422
+ return new Promise((resolve4, reject) => {
2423
+ output.write(`${JSON.stringify(value)}
2424
+ `, (error) => {
2425
+ if (error) reject(error);
2426
+ else resolve4();
2427
+ });
2428
+ });
2429
+ }
2430
+ };
2431
+ }
2432
+ function createLineReader(input, options) {
2433
+ let buffer = "";
2434
+ let discarding = false;
2435
+ let discardedBytes = 0;
2436
+ const deliver = (line) => {
2437
+ if (discarding) {
2438
+ discarding = false;
2439
+ discardedBytes = 0;
2440
+ return;
2441
+ }
2442
+ const trimmed = line.endsWith("\r") ? line.slice(0, -1) : line;
2443
+ if (trimmed.trim().length === 0) return;
2444
+ options.onLine(trimmed);
2445
+ };
2446
+ const drain = () => {
2447
+ let index = buffer.indexOf("\n");
2448
+ while (index >= 0) {
2449
+ const line = buffer.slice(0, index);
2450
+ buffer = buffer.slice(index + 1);
2451
+ deliver(line);
2452
+ index = buffer.indexOf("\n");
2453
+ }
2454
+ };
2455
+ const onData = (chunk) => {
2456
+ if (discarding) {
2457
+ const index = chunk.indexOf("\n");
2458
+ if (index < 0) {
2459
+ discardedBytes += chunk.length;
2460
+ return;
2461
+ }
2462
+ buffer = chunk.slice(index + 1);
2463
+ deliver("");
2464
+ drain();
2465
+ return;
2466
+ }
2467
+ buffer += chunk;
2468
+ if (buffer.length > MAX_LINE_BYTES && buffer.indexOf("\n") < 0) {
2469
+ discarding = true;
2470
+ discardedBytes = buffer.length;
2471
+ buffer = "";
2472
+ options.onOverflow?.(discardedBytes);
2473
+ return;
2474
+ }
2475
+ drain();
2476
+ };
2477
+ const onEnd = () => {
2478
+ if (buffer.length > 0) {
2479
+ const rest = buffer;
2480
+ buffer = "";
2481
+ deliver(rest);
2482
+ }
2483
+ };
2484
+ input.setEncoding("utf8");
2485
+ input.on("data", onData);
2486
+ input.on("end", onEnd);
2487
+ return () => {
2488
+ input.off("data", onData);
2489
+ input.off("end", onEnd);
2490
+ };
2491
+ }
2492
+
2493
+ // src/mcp/run.ts
2494
+ var PROJECT_URL = "https://ankurhalder.github.io/codebase-snapshot/";
2495
+ function createServer(options) {
2496
+ const cwd = options.cwd;
2497
+ const roots = options.roots && options.roots.length > 0 ? options.roots : [cwd];
2498
+ const context = {
2499
+ cwd,
2500
+ roots,
2501
+ log: options.log ?? ((line) => process.stderr.write(`${line}
2502
+ `))
2503
+ };
2504
+ return createMcpServer({
2505
+ info: {
2506
+ name: GENERATOR_NAME,
2507
+ title: "codebase-snapshot",
2508
+ version: GENERATOR_VERSION,
2509
+ websiteUrl: PROJECT_URL
2510
+ },
2511
+ instructions: SERVER_INSTRUCTIONS,
2512
+ tools: buildTools(),
2513
+ resources: buildResourceProvider(),
2514
+ prompts: buildPrompts(),
2515
+ context
2516
+ });
2517
+ }
2518
+ async function runStdioServer(options) {
2519
+ const log = options.log ?? ((line) => process.stderr.write(`${line}
2520
+ `));
2521
+ const server = createServer(options);
2522
+ const writer = createWriter(process.stdout);
2523
+ let writeChain = Promise.resolve();
2524
+ const send = (message) => {
2525
+ writeChain = writeChain.then(() => writer.write(message)).catch((error) => {
2526
+ log(`mcp: could not write a response: ${error instanceof Error ? error.message : String(error)}`);
2527
+ });
2528
+ return writeChain;
2529
+ };
2530
+ let inFlight = 0;
2531
+ let inputEnded = false;
2532
+ let resolveDone = () => void 0;
2533
+ const finished = new Promise((resolve4) => {
2534
+ resolveDone = resolve4;
2535
+ });
2536
+ const finishIfIdle = () => {
2537
+ if (inputEnded && inFlight === 0) resolveDone(0);
2538
+ };
2539
+ const handleLine = async (line) => {
2540
+ let message;
2541
+ try {
2542
+ message = JSON.parse(line);
2543
+ } catch {
2544
+ await send(errorResponse(null, ERROR_CODES.parseError, "Message is not valid JSON."));
2545
+ return;
2546
+ }
2547
+ const response = await server.handle(message);
2548
+ if (response) await send(response);
2549
+ };
2550
+ const stopReading = createLineReader(process.stdin, {
2551
+ onOverflow: (bytes) => log(`mcp: discarded an oversized message (${bytes} bytes)`),
2552
+ onLine: (line) => {
2553
+ inFlight += 1;
2554
+ void handleLine(line).finally(() => {
2555
+ inFlight -= 1;
2556
+ finishIfIdle();
2557
+ });
2558
+ }
2559
+ });
2560
+ const onEnd = () => {
2561
+ inputEnded = true;
2562
+ finishIfIdle();
2563
+ };
2564
+ process.stdin.on("end", onEnd);
2565
+ process.stdin.on("close", onEnd);
2566
+ log(`mcp: serving ${options.cwd} over stdio`);
2567
+ const code = await finished;
2568
+ stopReading();
2569
+ process.stdin.off("end", onEnd);
2570
+ process.stdin.off("close", onEnd);
2571
+ await writeChain;
2572
+ return code;
2573
+ }
2574
+
2575
+ export { COMMAND_OPTION_SCOPE, ERROR_CODES, LEGACY_PROTOCOL_VERSIONS, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, SERVER_INSTRUCTIONS, SKILL_NAME, SUPPORTED_PROTOCOL_VERSIONS, buildAgentContract, buildPrompts, buildResourceProvider, buildTools, classifyRequest, commandsForOption, createLineReader, createMcpServer, createServer, createSession, createWriter, inspectModernMeta, negotiateLegacyVersion, renderContractText, renderSkill, runStdioServer, validateArguments };
2576
+ //# sourceMappingURL=chunk-EWZYK3VX.js.map
2577
+ //# sourceMappingURL=chunk-EWZYK3VX.js.map