codebase-snapshot 0.9.8 → 0.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.d.cts CHANGED
@@ -1,3 +1,4 @@
1
+ export { G as GenerateSummary, W as WorkerResults, g as generateInWorker } from './generate-worker-D5Xgh27c.cjs';
1
2
  import { Readable, Writable } from 'node:stream';
2
3
 
3
4
  /**
@@ -187,7 +188,7 @@ interface ToolContext {
187
188
  * Client-facing hints.
188
189
  *
189
190
  * `readOnlyHint` is the one that changes behaviour rather than appearance: clients use it to
190
- * auto-approve a call. Six of the seven tools never touch the filesystem, so they are marked
191
+ * auto-approve a call. Seven of the eight tools never touch the filesystem, so they are marked
191
192
  * read-only and only `snapshot_generate` prompts.
192
193
  */
193
194
  interface ToolAnnotations {
@@ -297,6 +298,73 @@ declare function createMcpServer(options: McpServerOptions): McpServer;
297
298
  /** Every tool this server exposes, in the order a first-time caller should read them. */
298
299
  declare function buildTools(): McpTool[];
299
300
 
301
+ /**
302
+ * Reads one file's section back out of a rendered part.
303
+ *
304
+ * `snapshot_read` must not open the working tree. The snapshot is the contract: sensitive files
305
+ * are excluded from it, secret-shaped strings inside ordinary files are redacted on the way in,
306
+ * and a large file is emitted as a bounded preview. Reading the source directly would hand the
307
+ * model the `.env` the snapshot deliberately skipped and the credential it masked — so the read
308
+ * goes through the artifact instead.
309
+ *
310
+ * The format it parses is written by `renderFileSection`, and the parser is deliberately tied to
311
+ * that shape rather than to a regex over the whole document: a file's *content* may legitimately
312
+ * contain a line that looks like `### FILE: something`, and only fence tracking tells the two
313
+ * apart. Content fences are four or more backticks (`fenceForContent` never emits three), which
314
+ * is what makes "the next line equal to this exact fence" an unambiguous terminator.
315
+ */
316
+ interface PartSection {
317
+ /** The path as rendered in the `### FILE:` heading. */
318
+ path: string;
319
+ /** The `<a id="…">` anchor from the line above the heading, when present. */
320
+ anchor: string | null;
321
+ /** The file body, without its fence. Redacted and possibly a bounded preview. */
322
+ body: string;
323
+ /** `full`, `preview` or `signatures`. */
324
+ inclusion: string | null;
325
+ includedLines: number | null;
326
+ }
327
+ /** Splits a part into one section per emitted file. */
328
+ declare function parsePartSections(partText: string): PartSection[];
329
+ /**
330
+ * Finds the section for one file.
331
+ *
332
+ * The **anchor** is the identifier, not the heading. `anchorForPath` hashes the real
333
+ * `relativePath`, so it is exact and injective; the heading is the display form, and for a path
334
+ * with a control character (escaped as `\uXXXX`) or one longer than 300 characters (capped from
335
+ * the middle) it is not the path at all. The old fallback for those cases was the content
336
+ * SHA-256, which the part no longer carries — and which could never disambiguate two files with
337
+ * the same content, the exact case that needs it.
338
+ *
339
+ * The heading comparison is kept as a second chance, for a snapshot written before the anchor
340
+ * was read, or a part whose anchor line was edited by hand.
341
+ */
342
+ declare function findSection(sections: readonly PartSection[], relativePath: string): PartSection | null;
343
+ interface Window {
344
+ text: string;
345
+ fromLine: number;
346
+ returnedLines: number;
347
+ totalLines: number;
348
+ truncated: boolean;
349
+ nextFromLine: number | null;
350
+ /**
351
+ * True when `fromLine` is beyond the last line of the body.
352
+ *
353
+ * The start used to be clamped to the last line, so asking for line 9,000 of a 40-line file
354
+ * returned line 40 with a header that read `Lines 40–40 of 40` — a wrong answer dressed as a
355
+ * right one. The caller is told the request was out of range instead.
356
+ */
357
+ pastEnd: boolean;
358
+ }
359
+ /**
360
+ * Cuts a line window out of a body.
361
+ *
362
+ * Windows are the whole point of this module: a part holds hundreds of kilobytes, and the only
363
+ * safe default for a tool that returns content is "as little as answers the question". `next` is
364
+ * an explicit pointer rather than an instruction to keep reading, so an agent decides.
365
+ */
366
+ declare function windowText(body: string, fromLine: number, lineCount: number): Window;
367
+
300
368
  declare function buildPrompts(): McpPrompt[];
301
369
 
302
370
  declare function buildResourceProvider(): ResourceProvider;
@@ -409,4 +477,4 @@ declare function validateArguments(schema: InputSchema, value: unknown): string[
409
477
  */
410
478
  declare function coerceNumericStrings(schema: InputSchema, value: JsonObject): JsonObject;
411
479
 
412
- export { ERROR_CODES, type JsonRpcResponse, type JsonSchema, LEGACY_PROTOCOL_VERSIONS, type LineReaderOptions, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, type McpPrompt, type McpResourceContents, type McpResourceDescriptor, type McpResourceTemplate, type McpServer, type McpServerInfo, type McpServerOptions, type McpTool, type MessageWriter, type ProtocolEra, type ResourceProvider, SERVER_INSTRUCTIONS, SUPPORTED_PROTOCOL_VERSIONS, type ServerOptions, type SessionState, type ToolAnnotations, type ToolContext, type ToolOutcome, buildPrompts, buildResourceProvider, buildTools, classifyRequest, coerceNumericStrings, createLineReader, createMcpServer, createServer, createSession, createWriter, inspectModernMeta, negotiateLegacyVersion, runStdioServer, validateArguments };
480
+ export { ERROR_CODES, type JsonRpcResponse, type JsonSchema, LEGACY_PROTOCOL_VERSIONS, type LineReaderOptions, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, type McpPrompt, type McpResourceContents, type McpResourceDescriptor, type McpResourceTemplate, type McpServer, type McpServerInfo, type McpServerOptions, type McpTool, type MessageWriter, type PartSection, type ProtocolEra, type ResourceProvider, SERVER_INSTRUCTIONS, SUPPORTED_PROTOCOL_VERSIONS, type ServerOptions, type SessionState, type ToolAnnotations, type ToolContext, type ToolOutcome, type Window, buildPrompts, buildResourceProvider, buildTools, classifyRequest, coerceNumericStrings, createLineReader, createMcpServer, createServer, createSession, createWriter, findSection, inspectModernMeta, negotiateLegacyVersion, parsePartSections, runStdioServer, validateArguments, windowText };
package/dist/mcp.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { G as GenerateSummary, W as WorkerResults, g as generateInWorker } from './generate-worker-D5Xgh27c.js';
1
2
  import { Readable, Writable } from 'node:stream';
2
3
 
3
4
  /**
@@ -187,7 +188,7 @@ interface ToolContext {
187
188
  * Client-facing hints.
188
189
  *
189
190
  * `readOnlyHint` is the one that changes behaviour rather than appearance: clients use it to
190
- * auto-approve a call. Six of the seven tools never touch the filesystem, so they are marked
191
+ * auto-approve a call. Seven of the eight tools never touch the filesystem, so they are marked
191
192
  * read-only and only `snapshot_generate` prompts.
192
193
  */
193
194
  interface ToolAnnotations {
@@ -297,6 +298,73 @@ declare function createMcpServer(options: McpServerOptions): McpServer;
297
298
  /** Every tool this server exposes, in the order a first-time caller should read them. */
298
299
  declare function buildTools(): McpTool[];
299
300
 
301
+ /**
302
+ * Reads one file's section back out of a rendered part.
303
+ *
304
+ * `snapshot_read` must not open the working tree. The snapshot is the contract: sensitive files
305
+ * are excluded from it, secret-shaped strings inside ordinary files are redacted on the way in,
306
+ * and a large file is emitted as a bounded preview. Reading the source directly would hand the
307
+ * model the `.env` the snapshot deliberately skipped and the credential it masked — so the read
308
+ * goes through the artifact instead.
309
+ *
310
+ * The format it parses is written by `renderFileSection`, and the parser is deliberately tied to
311
+ * that shape rather than to a regex over the whole document: a file's *content* may legitimately
312
+ * contain a line that looks like `### FILE: something`, and only fence tracking tells the two
313
+ * apart. Content fences are four or more backticks (`fenceForContent` never emits three), which
314
+ * is what makes "the next line equal to this exact fence" an unambiguous terminator.
315
+ */
316
+ interface PartSection {
317
+ /** The path as rendered in the `### FILE:` heading. */
318
+ path: string;
319
+ /** The `<a id="…">` anchor from the line above the heading, when present. */
320
+ anchor: string | null;
321
+ /** The file body, without its fence. Redacted and possibly a bounded preview. */
322
+ body: string;
323
+ /** `full`, `preview` or `signatures`. */
324
+ inclusion: string | null;
325
+ includedLines: number | null;
326
+ }
327
+ /** Splits a part into one section per emitted file. */
328
+ declare function parsePartSections(partText: string): PartSection[];
329
+ /**
330
+ * Finds the section for one file.
331
+ *
332
+ * The **anchor** is the identifier, not the heading. `anchorForPath` hashes the real
333
+ * `relativePath`, so it is exact and injective; the heading is the display form, and for a path
334
+ * with a control character (escaped as `\uXXXX`) or one longer than 300 characters (capped from
335
+ * the middle) it is not the path at all. The old fallback for those cases was the content
336
+ * SHA-256, which the part no longer carries — and which could never disambiguate two files with
337
+ * the same content, the exact case that needs it.
338
+ *
339
+ * The heading comparison is kept as a second chance, for a snapshot written before the anchor
340
+ * was read, or a part whose anchor line was edited by hand.
341
+ */
342
+ declare function findSection(sections: readonly PartSection[], relativePath: string): PartSection | null;
343
+ interface Window {
344
+ text: string;
345
+ fromLine: number;
346
+ returnedLines: number;
347
+ totalLines: number;
348
+ truncated: boolean;
349
+ nextFromLine: number | null;
350
+ /**
351
+ * True when `fromLine` is beyond the last line of the body.
352
+ *
353
+ * The start used to be clamped to the last line, so asking for line 9,000 of a 40-line file
354
+ * returned line 40 with a header that read `Lines 40–40 of 40` — a wrong answer dressed as a
355
+ * right one. The caller is told the request was out of range instead.
356
+ */
357
+ pastEnd: boolean;
358
+ }
359
+ /**
360
+ * Cuts a line window out of a body.
361
+ *
362
+ * Windows are the whole point of this module: a part holds hundreds of kilobytes, and the only
363
+ * safe default for a tool that returns content is "as little as answers the question". `next` is
364
+ * an explicit pointer rather than an instruction to keep reading, so an agent decides.
365
+ */
366
+ declare function windowText(body: string, fromLine: number, lineCount: number): Window;
367
+
300
368
  declare function buildPrompts(): McpPrompt[];
301
369
 
302
370
  declare function buildResourceProvider(): ResourceProvider;
@@ -409,4 +477,4 @@ declare function validateArguments(schema: InputSchema, value: unknown): string[
409
477
  */
410
478
  declare function coerceNumericStrings(schema: InputSchema, value: JsonObject): JsonObject;
411
479
 
412
- export { ERROR_CODES, type JsonRpcResponse, type JsonSchema, LEGACY_PROTOCOL_VERSIONS, type LineReaderOptions, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, type McpPrompt, type McpResourceContents, type McpResourceDescriptor, type McpResourceTemplate, type McpServer, type McpServerInfo, type McpServerOptions, type McpTool, type MessageWriter, type ProtocolEra, type ResourceProvider, SERVER_INSTRUCTIONS, SUPPORTED_PROTOCOL_VERSIONS, type ServerOptions, type SessionState, type ToolAnnotations, type ToolContext, type ToolOutcome, buildPrompts, buildResourceProvider, buildTools, classifyRequest, coerceNumericStrings, createLineReader, createMcpServer, createServer, createSession, createWriter, inspectModernMeta, negotiateLegacyVersion, runStdioServer, validateArguments };
480
+ export { ERROR_CODES, type JsonRpcResponse, type JsonSchema, LEGACY_PROTOCOL_VERSIONS, type LineReaderOptions, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, type McpPrompt, type McpResourceContents, type McpResourceDescriptor, type McpResourceTemplate, type McpServer, type McpServerInfo, type McpServerOptions, type McpTool, type MessageWriter, type PartSection, type ProtocolEra, type ResourceProvider, SERVER_INSTRUCTIONS, SUPPORTED_PROTOCOL_VERSIONS, type ServerOptions, type SessionState, type ToolAnnotations, type ToolContext, type ToolOutcome, type Window, buildPrompts, buildResourceProvider, buildTools, classifyRequest, coerceNumericStrings, createLineReader, createMcpServer, createServer, createSession, createWriter, findSection, inspectModernMeta, negotiateLegacyVersion, parsePartSections, runStdioServer, validateArguments, windowText };
package/dist/mcp.js CHANGED
@@ -1,4 +1,2 @@
1
- export { ERROR_CODES, LEGACY_PROTOCOL_VERSIONS, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, SERVER_INSTRUCTIONS, SUPPORTED_PROTOCOL_VERSIONS, buildPrompts, buildResourceProvider, buildTools, classifyRequest, coerceNumericStrings, createLineReader, createMcpServer, createServer, createSession, createWriter, inspectModernMeta, negotiateLegacyVersion, runStdioServer, validateArguments } from './chunk-RQKXZOD7.js';
2
- import './chunk-C24OAT6T.js';
3
- //# sourceMappingURL=mcp.js.map
4
- //# sourceMappingURL=mcp.js.map
1
+ export { ERROR_CODES, LEGACY_PROTOCOL_VERSIONS, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, SERVER_INSTRUCTIONS, SUPPORTED_PROTOCOL_VERSIONS, buildPrompts, buildResourceProvider, buildTools, classifyRequest, coerceNumericStrings, createLineReader, createMcpServer, createServer, createSession, createWriter, findSection, inspectModernMeta, negotiateLegacyVersion, parsePartSections, runStdioServer, validateArguments, windowText } from './chunk-5OR3WVPW.js';
2
+ export { generateInWorker } from './chunk-3H45YMBG.js';
@@ -21,7 +21,7 @@ type TextEncoding = "utf-8" | "utf-8-lossy" | "utf-16le" | "utf-16be";
21
21
  * `full` – the complete file body was emitted.
22
22
  * `preview` – the body was truncated (by lines and/or bytes) before emission.
23
23
  */
24
- type InclusionMode = "full" | "preview";
24
+ type InclusionMode = "full" | "preview" | "signatures";
25
25
  interface SourceSummary {
26
26
  directives: string[];
27
27
  imports: string[];
@@ -88,7 +88,15 @@ type SkipReason = "binary" | "read-error" | "not-file"
88
88
  * expected — which used to be recorded as the generic `not-file`, implying the path was a
89
89
  * socket or a FIFO rather than a repository whose contents are simply somebody else's.
90
90
  */
91
- | "nested-repository";
91
+ | "nested-repository"
92
+ /**
93
+ * Not in the set that `--changed-since` or `--staged` asked for.
94
+ *
95
+ * Deliberately distinct from `excluded`, which means the user's own glob rejected the path.
96
+ * Here the file is perfectly includable and simply did not change; reporting it as
97
+ * `excluded` would send a reader looking for an exclude pattern they never wrote.
98
+ */
99
+ | "unchanged";
92
100
  interface SkipRecord {
93
101
  /** Path as it may be shown in an artifact: secret spans are masked. */
94
102
  path: string;
@@ -144,6 +152,23 @@ interface PartMetadata {
144
152
  filename: string;
145
153
  /** Exact UTF-8 byte length of the emitted part file. */
146
154
  byteSize: number;
155
+ /**
156
+ * Estimated tokens in this part.
157
+ *
158
+ * `byteSize` answers "will this fit the byte budget"; this answers the question a caller
159
+ * actually has, which is "will this fit my context window". An estimate, and labelled as
160
+ * one: `estimateTokens` documents exactly what it counts.
161
+ */
162
+ estimatedTokens: number;
163
+ /**
164
+ * The byte budget this part was partitioned against.
165
+ *
166
+ * With per-layer budgets a snapshot has several budgets at once, so "within budget"
167
+ * cannot be answered without knowing which one applied. Verification compared every
168
+ * part against the *largest* budget in the run, which passed a part that was over its
169
+ * own.
170
+ */
171
+ budgetBytes: number;
147
172
  sha256: string;
148
173
  lines: number;
149
174
  fileCount: number;
@@ -198,6 +223,14 @@ interface ManifestTotals {
198
223
  skippedFiles: number;
199
224
  totalSourceBytes: number;
200
225
  totalContentBytes: number;
226
+ /**
227
+ * Estimated tokens in the emitted parts.
228
+ *
229
+ * Optional because a manifest written before this was recorded has no such key. The number
230
+ * is an estimate — see `estimateTokens` for the method — and it exists because the snapshot
231
+ * is pasted into a context window, which is measured in tokens rather than bytes.
232
+ */
233
+ estimatedTokens?: number;
201
234
  partCount: number;
202
235
  warningCount: number;
203
236
  /**
@@ -256,13 +289,34 @@ interface ManifestConfigSnapshot {
256
289
  /** Digest of the effective settings; see `configDigest`. */
257
290
  digest?: string;
258
291
  maxPartBytes: number;
292
+ /**
293
+ * The token ceiling the run was given, or `undefined` when only bytes were named.
294
+ *
295
+ * Recorded so the manifest states the budget the user chose rather than only the byte
296
+ * value it was converted to.
297
+ */
298
+ maxPartTokens?: number;
259
299
  largeFileBytes: number;
260
300
  previewHeadLines: number;
261
301
  previewTailLines: number;
262
302
  includeMonolith: boolean;
303
+ /**
304
+ * The ceiling above which the monolith is not written; `null` for no ceiling.
305
+ *
306
+ * Optional because a manifest written before this was recorded has no such key.
307
+ */
308
+ maxMonolithBytes?: number | null;
263
309
  includeSensitiveFiles: boolean;
264
310
  /** How files were discovered; `undefined` on manifests written before this was recorded. */
265
311
  discovery?: DiscoveryMode;
312
+ /** The changed-files scope the snapshot was taken over, when one was used. */
313
+ changedSince?: string;
314
+ staged?: boolean;
315
+ withDependents?: boolean;
316
+ /** True when bodies were replaced by signatures. */
317
+ skeleton?: boolean;
318
+ /** True when `reports/audit.sarif` was written. */
319
+ sarif?: boolean;
266
320
  artifacts: ResolvedArtifacts;
267
321
  include: string[];
268
322
  preset: string;
@@ -315,6 +369,14 @@ interface Manifest {
315
369
  artifacts: {
316
370
  fullJson: ManifestArtifact | null;
317
371
  fullMarkdown: ManifestArtifact | null;
372
+ /**
373
+ * `reports/audit.sarif`, when `--sarif` asked for it.
374
+ *
375
+ * Optional because most runs do not write one, and a manifest written before this
376
+ * existed has no such key at all — the two are indistinguishable to a reader, which is
377
+ * why the presence of the file is also recorded in `config`.
378
+ */
379
+ sarif?: ManifestArtifact;
318
380
  };
319
381
  layers: Record<string, number>;
320
382
  kinds: Record<string, number>;
@@ -382,6 +444,15 @@ interface SnapshotConfig {
382
444
  tmpDir?: string;
383
445
  /** Raw bytes, or a human-readable size (`"120k"`, `"1.5mb"`). */
384
446
  maxPartBytes?: ByteSize;
447
+ /**
448
+ * A per-part ceiling in *tokens* rather than bytes.
449
+ *
450
+ * The snapshot is pasted into a context window, and a context window is measured in tokens;
451
+ * bytes are what the partitioner writes. Naming a token budget sets `maxPartBytes` to the
452
+ * byte count that corresponds to it, so the byte bound — the thing the partitioner actually
453
+ * enforces — never stops being true. The smaller of the two ceilings wins.
454
+ */
455
+ maxPartTokens?: ByteSize;
385
456
  largeFileBytes?: ByteSize;
386
457
  previewHeadLines?: number;
387
458
  previewTailLines?: number;
@@ -393,6 +464,31 @@ interface SnapshotConfig {
393
464
  include?: string[];
394
465
  /** Force a preset by name instead of auto-detecting one. */
395
466
  preset?: string;
467
+ /**
468
+ * Only include files changed since this git ref.
469
+ *
470
+ * The pull-request case: the diff is small and the repository is not. Unchanged files are
471
+ * recorded in `skipped[]` with reason `unchanged`, so the manifest accounts for them.
472
+ */
473
+ changedSince?: string;
474
+ /** Only include files staged for the next commit. */
475
+ staged?: boolean;
476
+ /**
477
+ * Emit signatures instead of bodies for TypeScript and TSX files.
478
+ *
479
+ * Matches `inclusionMode: "signatures"` on the entries it applies to, so a reader can tell a
480
+ * compressed file from a whole one without diffing it against the source.
481
+ */
482
+ skeleton?: boolean;
483
+ /** Also write `reports/audit.sarif` so a CI security gate can read the findings. */
484
+ sarif?: boolean;
485
+ /**
486
+ * With `changedSince` or `staged`, also include the files that import a changed one.
487
+ *
488
+ * A one-hop closure: the file you changed and the files that reference it are the context
489
+ * needed to change it safely.
490
+ */
491
+ withDependents?: boolean;
396
492
  /** Switch individual snapshot artifacts off. Everything is on by default. */
397
493
  artifacts?: ArtifactSelection;
398
494
  /**
@@ -418,6 +514,8 @@ interface ResolvedSnapshotConfig {
418
514
  outDir: string;
419
515
  tmpDir: string;
420
516
  maxPartBytes: number;
517
+ /** The token ceiling the user asked for, before it was turned into `maxPartBytes`. */
518
+ maxPartTokens?: number;
421
519
  largeFileBytes: number;
422
520
  previewHeadLines: number;
423
521
  previewTailLines: number;
@@ -430,6 +528,21 @@ interface ResolvedSnapshotConfig {
430
528
  * reader treats `undefined` as `auto`, so the effective mode is never guessed twice.
431
529
  */
432
530
  discovery?: DiscoveryMode;
531
+ /** Only files changed since this git ref; `undefined` scans everything. */
532
+ changedSince?: string;
533
+ /** Only files staged for the next commit. */
534
+ staged?: boolean;
535
+ /** Emit signatures instead of bodies for TypeScript and TSX files. */
536
+ skeleton?: boolean;
537
+ /**
538
+ * Also write `reports/audit.sarif`, the findings in SARIF 2.1.0.
539
+ *
540
+ * For a CI security gate: GitHub Code Scanning reads SARIF, and a markdown report it cannot
541
+ * ingest is a finding nobody's build can act on.
542
+ */
543
+ sarif?: boolean;
544
+ /** Also include the files that import a changed one. */
545
+ withDependents?: boolean;
433
546
  /** Empty means "no allow-list"; every discovered file is a candidate. */
434
547
  include: string[];
435
548
  /** The preset that was resolved (detected or forced). */
@@ -21,7 +21,7 @@ type TextEncoding = "utf-8" | "utf-8-lossy" | "utf-16le" | "utf-16be";
21
21
  * `full` – the complete file body was emitted.
22
22
  * `preview` – the body was truncated (by lines and/or bytes) before emission.
23
23
  */
24
- type InclusionMode = "full" | "preview";
24
+ type InclusionMode = "full" | "preview" | "signatures";
25
25
  interface SourceSummary {
26
26
  directives: string[];
27
27
  imports: string[];
@@ -88,7 +88,15 @@ type SkipReason = "binary" | "read-error" | "not-file"
88
88
  * expected — which used to be recorded as the generic `not-file`, implying the path was a
89
89
  * socket or a FIFO rather than a repository whose contents are simply somebody else's.
90
90
  */
91
- | "nested-repository";
91
+ | "nested-repository"
92
+ /**
93
+ * Not in the set that `--changed-since` or `--staged` asked for.
94
+ *
95
+ * Deliberately distinct from `excluded`, which means the user's own glob rejected the path.
96
+ * Here the file is perfectly includable and simply did not change; reporting it as
97
+ * `excluded` would send a reader looking for an exclude pattern they never wrote.
98
+ */
99
+ | "unchanged";
92
100
  interface SkipRecord {
93
101
  /** Path as it may be shown in an artifact: secret spans are masked. */
94
102
  path: string;
@@ -144,6 +152,23 @@ interface PartMetadata {
144
152
  filename: string;
145
153
  /** Exact UTF-8 byte length of the emitted part file. */
146
154
  byteSize: number;
155
+ /**
156
+ * Estimated tokens in this part.
157
+ *
158
+ * `byteSize` answers "will this fit the byte budget"; this answers the question a caller
159
+ * actually has, which is "will this fit my context window". An estimate, and labelled as
160
+ * one: `estimateTokens` documents exactly what it counts.
161
+ */
162
+ estimatedTokens: number;
163
+ /**
164
+ * The byte budget this part was partitioned against.
165
+ *
166
+ * With per-layer budgets a snapshot has several budgets at once, so "within budget"
167
+ * cannot be answered without knowing which one applied. Verification compared every
168
+ * part against the *largest* budget in the run, which passed a part that was over its
169
+ * own.
170
+ */
171
+ budgetBytes: number;
147
172
  sha256: string;
148
173
  lines: number;
149
174
  fileCount: number;
@@ -198,6 +223,14 @@ interface ManifestTotals {
198
223
  skippedFiles: number;
199
224
  totalSourceBytes: number;
200
225
  totalContentBytes: number;
226
+ /**
227
+ * Estimated tokens in the emitted parts.
228
+ *
229
+ * Optional because a manifest written before this was recorded has no such key. The number
230
+ * is an estimate — see `estimateTokens` for the method — and it exists because the snapshot
231
+ * is pasted into a context window, which is measured in tokens rather than bytes.
232
+ */
233
+ estimatedTokens?: number;
201
234
  partCount: number;
202
235
  warningCount: number;
203
236
  /**
@@ -256,13 +289,34 @@ interface ManifestConfigSnapshot {
256
289
  /** Digest of the effective settings; see `configDigest`. */
257
290
  digest?: string;
258
291
  maxPartBytes: number;
292
+ /**
293
+ * The token ceiling the run was given, or `undefined` when only bytes were named.
294
+ *
295
+ * Recorded so the manifest states the budget the user chose rather than only the byte
296
+ * value it was converted to.
297
+ */
298
+ maxPartTokens?: number;
259
299
  largeFileBytes: number;
260
300
  previewHeadLines: number;
261
301
  previewTailLines: number;
262
302
  includeMonolith: boolean;
303
+ /**
304
+ * The ceiling above which the monolith is not written; `null` for no ceiling.
305
+ *
306
+ * Optional because a manifest written before this was recorded has no such key.
307
+ */
308
+ maxMonolithBytes?: number | null;
263
309
  includeSensitiveFiles: boolean;
264
310
  /** How files were discovered; `undefined` on manifests written before this was recorded. */
265
311
  discovery?: DiscoveryMode;
312
+ /** The changed-files scope the snapshot was taken over, when one was used. */
313
+ changedSince?: string;
314
+ staged?: boolean;
315
+ withDependents?: boolean;
316
+ /** True when bodies were replaced by signatures. */
317
+ skeleton?: boolean;
318
+ /** True when `reports/audit.sarif` was written. */
319
+ sarif?: boolean;
266
320
  artifacts: ResolvedArtifacts;
267
321
  include: string[];
268
322
  preset: string;
@@ -315,6 +369,14 @@ interface Manifest {
315
369
  artifacts: {
316
370
  fullJson: ManifestArtifact | null;
317
371
  fullMarkdown: ManifestArtifact | null;
372
+ /**
373
+ * `reports/audit.sarif`, when `--sarif` asked for it.
374
+ *
375
+ * Optional because most runs do not write one, and a manifest written before this
376
+ * existed has no such key at all — the two are indistinguishable to a reader, which is
377
+ * why the presence of the file is also recorded in `config`.
378
+ */
379
+ sarif?: ManifestArtifact;
318
380
  };
319
381
  layers: Record<string, number>;
320
382
  kinds: Record<string, number>;
@@ -382,6 +444,15 @@ interface SnapshotConfig {
382
444
  tmpDir?: string;
383
445
  /** Raw bytes, or a human-readable size (`"120k"`, `"1.5mb"`). */
384
446
  maxPartBytes?: ByteSize;
447
+ /**
448
+ * A per-part ceiling in *tokens* rather than bytes.
449
+ *
450
+ * The snapshot is pasted into a context window, and a context window is measured in tokens;
451
+ * bytes are what the partitioner writes. Naming a token budget sets `maxPartBytes` to the
452
+ * byte count that corresponds to it, so the byte bound — the thing the partitioner actually
453
+ * enforces — never stops being true. The smaller of the two ceilings wins.
454
+ */
455
+ maxPartTokens?: ByteSize;
385
456
  largeFileBytes?: ByteSize;
386
457
  previewHeadLines?: number;
387
458
  previewTailLines?: number;
@@ -393,6 +464,31 @@ interface SnapshotConfig {
393
464
  include?: string[];
394
465
  /** Force a preset by name instead of auto-detecting one. */
395
466
  preset?: string;
467
+ /**
468
+ * Only include files changed since this git ref.
469
+ *
470
+ * The pull-request case: the diff is small and the repository is not. Unchanged files are
471
+ * recorded in `skipped[]` with reason `unchanged`, so the manifest accounts for them.
472
+ */
473
+ changedSince?: string;
474
+ /** Only include files staged for the next commit. */
475
+ staged?: boolean;
476
+ /**
477
+ * Emit signatures instead of bodies for TypeScript and TSX files.
478
+ *
479
+ * Matches `inclusionMode: "signatures"` on the entries it applies to, so a reader can tell a
480
+ * compressed file from a whole one without diffing it against the source.
481
+ */
482
+ skeleton?: boolean;
483
+ /** Also write `reports/audit.sarif` so a CI security gate can read the findings. */
484
+ sarif?: boolean;
485
+ /**
486
+ * With `changedSince` or `staged`, also include the files that import a changed one.
487
+ *
488
+ * A one-hop closure: the file you changed and the files that reference it are the context
489
+ * needed to change it safely.
490
+ */
491
+ withDependents?: boolean;
396
492
  /** Switch individual snapshot artifacts off. Everything is on by default. */
397
493
  artifacts?: ArtifactSelection;
398
494
  /**
@@ -418,6 +514,8 @@ interface ResolvedSnapshotConfig {
418
514
  outDir: string;
419
515
  tmpDir: string;
420
516
  maxPartBytes: number;
517
+ /** The token ceiling the user asked for, before it was turned into `maxPartBytes`. */
518
+ maxPartTokens?: number;
421
519
  largeFileBytes: number;
422
520
  previewHeadLines: number;
423
521
  previewTailLines: number;
@@ -430,6 +528,21 @@ interface ResolvedSnapshotConfig {
430
528
  * reader treats `undefined` as `auto`, so the effective mode is never guessed twice.
431
529
  */
432
530
  discovery?: DiscoveryMode;
531
+ /** Only files changed since this git ref; `undefined` scans everything. */
532
+ changedSince?: string;
533
+ /** Only files staged for the next commit. */
534
+ staged?: boolean;
535
+ /** Emit signatures instead of bodies for TypeScript and TSX files. */
536
+ skeleton?: boolean;
537
+ /**
538
+ * Also write `reports/audit.sarif`, the findings in SARIF 2.1.0.
539
+ *
540
+ * For a CI security gate: GitHub Code Scanning reads SARIF, and a markdown report it cannot
541
+ * ingest is a finding nobody's build can act on.
542
+ */
543
+ sarif?: boolean;
544
+ /** Also include the files that import a changed one. */
545
+ withDependents?: boolean;
433
546
  /** Empty means "no allow-list"; every discovered file is a candidate. */
434
547
  include: string[];
435
548
  /** The preset that was resolved (detected or forced). */
package/docs/agent.md CHANGED
@@ -115,7 +115,7 @@ devDependency and the `.gitignore` entries for the snapshot directory, and nothi
115
115
 
116
116
  ## When the client speaks MCP
117
117
 
118
- `codebase-snapshot mcp` serves the repository to a Model Context Protocol client over stdio: seven
118
+ `codebase-snapshot mcp` serves the repository to a Model Context Protocol client over stdio: eight
119
119
  tools, `snapshot://` resources and two prompts. Prefer it over shelling out when the agent already
120
120
  has an MCP connection, for two reasons that are properties of the design rather than of the wiring:
121
121