@kingkyylian/handoffkit 0.2.0 → 0.4.0

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/index.js CHANGED
@@ -1,15 +1,154 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { Command as Command6 } from "commander";
4
+ import { Command as Command7 } from "commander";
5
5
 
6
- // src/cli/commands/pack.ts
6
+ // src/cli/commands/cache.ts
7
7
  import { Command } from "commander";
8
- import { z as z2 } from "zod";
8
+ import { z } from "zod";
9
+
10
+ // src/core/cache.ts
11
+ import { mkdir, readdir, readFile, writeFile } from "fs/promises";
12
+ import { basename, join } from "path";
13
+
14
+ // src/core/redact.ts
15
+ var REDACTION = "[REDACTED]";
16
+ var SECRET_KEY_PATTERN = /(\b(?:[A-Z0-9]+[_.-])*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE[_-]?KEY|CLIENT[_-]?SECRET|ACCESS[_-]?TOKEN|REFRESH[_-]?TOKEN|COOKIE|SESSION|JWT|AUTH_TOKEN)(?:[_.-][A-Z0-9]+)*\b\s*(?:=|:)\s*)(["']?)([^\s"',}]+)/gi;
17
+ var TOKEN_PATTERNS = [
18
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi,
19
+ /\bsk-[A-Za-z0-9_-]{16,}/g,
20
+ /\bgh[pousr]_[A-Za-z0-9_]{16,}/g,
21
+ /\bnpm_[A-Za-z0-9_-]{16,}/g,
22
+ /\bxox[baprs]-[A-Za-z0-9-]{16,}/g,
23
+ /\bAIza[0-9A-Za-z_-]{20,}/g,
24
+ /\bAKIA[0-9A-Z]{16}\b/g,
25
+ /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
26
+ /\/\/([^/\s:@]+):([^@\s/]+)@/g
27
+ ];
28
+ function redactText(input) {
29
+ let output = input.replace(
30
+ /-----BEGIN ([A-Z ]*PRIVATE KEY)-----[\s\S]*?-----END \1-----/g,
31
+ (_match, keyType) => `-----BEGIN ${keyType}-----
32
+ ${REDACTION}
33
+ -----END ${keyType}-----`
34
+ );
35
+ output = output.replace(SECRET_KEY_PATTERN, (_match, prefix, quote) => {
36
+ return `${prefix}${quote}${REDACTION}${quote}`;
37
+ });
38
+ output = output.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer [REDACTED]");
39
+ output = output.replace(/\/\/([^/\s:@]+):([^@\s/]+)@/g, "//[REDACTED]@");
40
+ for (const pattern of TOKEN_PATTERNS.slice(1, -1)) {
41
+ output = output.replace(pattern, REDACTION);
42
+ }
43
+ return output;
44
+ }
45
+
46
+ // src/core/cache.ts
47
+ var CACHE_KINDS = ["resume", "verification"];
48
+ async function writeCacheArtifact(root, kind, data, options = {}) {
49
+ const createdAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
50
+ const envelope = {
51
+ version: 1,
52
+ kind,
53
+ createdAt,
54
+ data
55
+ };
56
+ const cacheDir = join(root, ".handoffkit", kind);
57
+ const artifactPath = join(cacheDir, `${cacheTimestamp(createdAt)}.json`);
58
+ const latestPath = join(cacheDir, "latest.json");
59
+ const contents = `${redactText(JSON.stringify(envelope, null, 2))}
60
+ `;
61
+ await mkdir(cacheDir, { recursive: true });
62
+ await Promise.all([writeFile(artifactPath, contents, "utf8"), writeFile(latestPath, contents, "utf8")]);
63
+ return { artifactPath, latestPath };
64
+ }
65
+ async function listCacheArtifacts(root) {
66
+ const summaries = await Promise.all(CACHE_KINDS.map((kind) => listCacheKind(root, kind)));
67
+ return summaries.flat().sort((a, b) => b.createdAt.localeCompare(a.createdAt) || a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));
68
+ }
69
+ async function readCacheArtifact(root, kind, name = "latest") {
70
+ const normalizedName = normalizeArtifactName(name);
71
+ const artifactPath = join(root, ".handoffkit", kind, `${normalizedName}.json`);
72
+ const envelope = JSON.parse(await readFile(artifactPath, "utf8"));
73
+ if (envelope.version !== 1 || envelope.kind !== kind || typeof envelope.createdAt !== "string") {
74
+ throw new Error(`Invalid cache artifact: .handoffkit/${kind}/${normalizedName}.json`);
75
+ }
76
+ return envelope;
77
+ }
78
+ async function readResumeSourceFromCache(root, ref) {
79
+ const { kind, name } = parseCacheRef(ref, "resume");
80
+ if (kind !== "resume") {
81
+ throw new Error("resume --from-cache only supports resume cache artifacts.");
82
+ }
83
+ const artifact = await readCacheArtifact(root, kind, name);
84
+ const source = resumeSourceFromArtifact(artifact);
85
+ if (!source) {
86
+ throw new Error(`Cache artifact does not contain a resume source: .handoffkit/${kind}/${name}.json`);
87
+ }
88
+ return source;
89
+ }
90
+ function parseCacheRef(ref, defaultKind) {
91
+ const normalized = ref.trim();
92
+ const parts = normalized.split("/");
93
+ if (parts.length === 1 && defaultKind) {
94
+ return { kind: defaultKind, name: normalizeArtifactName(parts[0] || "latest") };
95
+ }
96
+ const [kind, name] = parts;
97
+ if (parts.length === 2 && kind && isCacheArtifactKind(kind)) {
98
+ return { kind, name: normalizeArtifactName(name || "latest") };
99
+ }
100
+ throw new Error(`Invalid cache ref: ${ref}`);
101
+ }
102
+ function resumeSourceFromArtifact(artifact) {
103
+ const data = artifact.data;
104
+ return data.source;
105
+ }
106
+ async function listCacheKind(root, kind) {
107
+ const cacheDir = join(root, ".handoffkit", kind);
108
+ let entries;
109
+ try {
110
+ entries = await readdir(cacheDir);
111
+ } catch (error) {
112
+ if (error.code === "ENOENT") {
113
+ return [];
114
+ }
115
+ throw error;
116
+ }
117
+ const artifacts = await Promise.all(
118
+ entries.filter((entry) => entry.endsWith(".json")).map(async (entry) => {
119
+ const name = basename(entry, ".json");
120
+ try {
121
+ const artifact = await readCacheArtifact(root, kind, name);
122
+ return {
123
+ kind,
124
+ name,
125
+ createdAt: artifact.createdAt,
126
+ path: `.handoffkit/${kind}/${entry}`
127
+ };
128
+ } catch {
129
+ return void 0;
130
+ }
131
+ })
132
+ );
133
+ return artifacts.filter((artifact) => Boolean(artifact));
134
+ }
135
+ function isCacheArtifactKind(value) {
136
+ return CACHE_KINDS.includes(value);
137
+ }
138
+ function normalizeArtifactName(name) {
139
+ const withoutExtension = name.replace(/\.json$/i, "");
140
+ if (!/^[A-Za-z0-9_.-]+$/.test(withoutExtension)) {
141
+ throw new Error(`Invalid cache artifact name: ${name}`);
142
+ }
143
+ return withoutExtension;
144
+ }
145
+ function cacheTimestamp(timestamp) {
146
+ return timestamp.replace(/[:.]/g, "-");
147
+ }
9
148
 
10
149
  // src/core/git.ts
11
- import { readFile } from "fs/promises";
12
- import { basename } from "path";
150
+ import { readFile as readFile2 } from "fs/promises";
151
+ import { basename as basename2 } from "path";
13
152
  import { execa } from "execa";
14
153
 
15
154
  // src/cli/errors.ts
@@ -26,7 +165,7 @@ function formatCliError(error) {
26
165
 
27
166
  // src/core/git.ts
28
167
  var UNTRACKED_PATCH_CHAR_LIMIT = 2e4;
29
- var IGNORED_CHANGED_PATH_PREFIXES = ["node_modules/", "dist/", "coverage/", ".git/"];
168
+ var IGNORED_CHANGED_PATH_PREFIXES = ["node_modules/", "dist/", "coverage/", ".git/", ".handoffkit/"];
30
169
  async function findGitRoot(cwd) {
31
170
  const result = await execa("git", ["rev-parse", "--show-toplevel"], {
32
171
  cwd,
@@ -55,7 +194,7 @@ async function collectGitInfo(root, options) {
55
194
  const unstagedDiffSummary = options.includeDiffSummary ? joinSections([trackedUnstagedDiffSummary, renderUntrackedSummary(untrackedFiles)]) : "";
56
195
  const diff = options.includeDiff ? await collectDiff(root, untrackedFiles) : void 0;
57
196
  return {
58
- name: basename(root),
197
+ name: basename2(root),
59
198
  branch,
60
199
  ...options.since ? { baseRef: options.since } : {},
61
200
  status,
@@ -139,7 +278,7 @@ function renderUntrackedSummary(files) {
139
278
  async function untrackedPatch(root, files) {
140
279
  const patches = await Promise.all(
141
280
  files.map(async (file) => {
142
- const content = await readFile(`${root}/${file}`, "utf8");
281
+ const content = await readFile2(`${root}/${file}`, "utf8");
143
282
  const trimmedContent = content.length > UNTRACKED_PATCH_CHAR_LIMIT ? `${content.slice(0, UNTRACKED_PATCH_CHAR_LIMIT).trimEnd()}
144
283
  [truncated]` : content.trimEnd();
145
284
  return [`Untracked file: ${file}`, "```text", trimmedContent, "```"].join("\n");
@@ -157,43 +296,74 @@ function isIgnoredChangedPath(file) {
157
296
  return IGNORED_CHANGED_PATH_PREFIXES.some((prefix) => file === prefix.slice(0, -1) || file.startsWith(prefix));
158
297
  }
159
298
 
160
- // src/core/instructions.ts
161
- import { readFile as readFile2, stat } from "fs/promises";
162
- import fg from "fast-glob";
163
-
164
- // src/core/redact.ts
165
- var REDACTION = "[REDACTED]";
166
- var SECRET_KEY_PATTERN = /(\b(?:[A-Z0-9]+[_.-])*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE[_-]?KEY|CLIENT[_-]?SECRET|ACCESS[_-]?TOKEN|REFRESH[_-]?TOKEN|COOKIE|SESSION|JWT|AUTH_TOKEN)(?:[_.-][A-Z0-9]+)*\b\s*(?:=|:)\s*)(["']?)([^\s"',}]+)/gi;
167
- var TOKEN_PATTERNS = [
168
- /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi,
169
- /\bsk-[A-Za-z0-9_-]{16,}/g,
170
- /\bgh[pousr]_[A-Za-z0-9_]{16,}/g,
171
- /\bnpm_[A-Za-z0-9_-]{16,}/g,
172
- /\bxox[baprs]-[A-Za-z0-9-]{16,}/g,
173
- /\bAIza[0-9A-Za-z_-]{20,}/g,
174
- /\bAKIA[0-9A-Z]{16}\b/g,
175
- /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
176
- /\/\/([^/\s:@]+):([^@\s/]+)@/g
177
- ];
178
- function redactText(input) {
179
- let output = input.replace(
180
- /-----BEGIN ([A-Z ]*PRIVATE KEY)-----[\s\S]*?-----END \1-----/g,
181
- (_match, keyType) => `-----BEGIN ${keyType}-----
182
- ${REDACTION}
183
- -----END ${keyType}-----`
184
- );
185
- output = output.replace(SECRET_KEY_PATTERN, (_match, prefix, quote) => {
186
- return `${prefix}${quote}${REDACTION}${quote}`;
299
+ // src/cli/commands/cache.ts
300
+ var CacheFormatOptionsSchema = z.object({
301
+ format: z.enum(["markdown", "json"]).default("markdown")
302
+ });
303
+ var CacheKindSchema = z.enum(["verification", "resume"]);
304
+ function createCacheCommand() {
305
+ return new Command("cache").description("Inspect local .handoffkit cache artifacts.").summary("List and show explicit local cache artifacts.").addCommand(createCacheListCommand()).addCommand(createCacheShowCommand());
306
+ }
307
+ function createCacheListCommand() {
308
+ return new Command("list").description("List local cache artifacts.").option("--format <format>", "output format: markdown or json", "markdown").action(async (rawOptions) => {
309
+ const options = CacheFormatOptionsSchema.parse(rawOptions);
310
+ const root = await findGitRoot(process.cwd());
311
+ const artifacts = await listCacheArtifacts(root);
312
+ if (options.format === "json") {
313
+ process.stdout.write(`${JSON.stringify({ artifacts }, null, 2)}
314
+ `);
315
+ return;
316
+ }
317
+ process.stdout.write(renderCacheListMarkdown(artifacts));
187
318
  });
188
- output = output.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer [REDACTED]");
189
- output = output.replace(/\/\/([^/\s:@]+):([^@\s/]+)@/g, "//[REDACTED]@");
190
- for (const pattern of TOKEN_PATTERNS.slice(1, -1)) {
191
- output = output.replace(pattern, REDACTION);
319
+ }
320
+ function createCacheShowCommand() {
321
+ return new Command("show").description("Show one local cache artifact.").argument("<kind>", "cache kind: verification or resume").argument("[name]", "artifact name, defaults to latest", "latest").option("--format <format>", "output format: markdown or json", "markdown").action(async (kindInput, name, rawOptions) => {
322
+ const options = CacheFormatOptionsSchema.parse(rawOptions);
323
+ const kind = CacheKindSchema.parse(kindInput);
324
+ const root = await findGitRoot(process.cwd());
325
+ const artifact = await readCacheArtifact(root, kind, name);
326
+ if (options.format === "json") {
327
+ process.stdout.write(`${JSON.stringify(artifact, null, 2)}
328
+ `);
329
+ return;
330
+ }
331
+ process.stdout.write(renderCacheArtifactMarkdown(artifact, kind, name));
332
+ });
333
+ }
334
+ function renderCacheListMarkdown(artifacts) {
335
+ const lines = ["# Cache Artifacts", ""];
336
+ if (artifacts.length === 0) {
337
+ lines.push("No cache artifacts found.");
338
+ } else {
339
+ for (const artifact of artifacts) {
340
+ lines.push(`- ${artifact.kind}/${artifact.name} (${artifact.createdAt}) - \`${artifact.path}\``);
341
+ }
192
342
  }
193
- return output;
343
+ return `${lines.join("\n")}
344
+ `;
194
345
  }
346
+ function renderCacheArtifactMarkdown(artifact, kind, name) {
347
+ return [
348
+ "# Cache Artifact",
349
+ "",
350
+ `- Artifact: ${kind}/${name}`,
351
+ `- Created: ${artifact.createdAt}`,
352
+ "",
353
+ "```json",
354
+ JSON.stringify(artifact, null, 2),
355
+ "```",
356
+ ""
357
+ ].join("\n");
358
+ }
359
+
360
+ // src/cli/commands/pack.ts
361
+ import { Command as Command2 } from "commander";
362
+ import { z as z3 } from "zod";
195
363
 
196
364
  // src/core/instructions.ts
365
+ import { readFile as readFile3, stat } from "fs/promises";
366
+ import fg from "fast-glob";
197
367
  var INSTRUCTION_PATTERNS = [
198
368
  "**/AGENTS.md",
199
369
  "**/CLAUDE.md",
@@ -226,7 +396,7 @@ async function readPreview(root, path) {
226
396
  if (!metadata.isFile()) {
227
397
  return "Directory rule set detected.";
228
398
  }
229
- const content = await readFile2(fullPath, "utf8");
399
+ const content = await readFile3(fullPath, "utf8");
230
400
  const normalized = content.replace(/\r\n/g, "\n").trim();
231
401
  const preview = normalized.length > PREVIEW_CHAR_LIMIT ? `${normalized.slice(0, PREVIEW_CHAR_LIMIT).trimEnd()}
232
402
  [truncated]` : normalized;
@@ -252,22 +422,22 @@ function instructionKind(path) {
252
422
  }
253
423
 
254
424
  // src/core/package-json.ts
255
- import { access, readFile as readFile3 } from "fs/promises";
256
- import { join } from "path";
257
- import { z } from "zod";
258
- var PackageJsonSchema = z.object({
259
- name: z.string().optional(),
260
- packageManager: z.string().optional(),
261
- scripts: z.record(z.string(), z.string()).optional()
425
+ import { access, readFile as readFile4 } from "fs/promises";
426
+ import { join as join2 } from "path";
427
+ import { z as z2 } from "zod";
428
+ var PackageJsonSchema = z2.object({
429
+ name: z2.string().optional(),
430
+ packageManager: z2.string().optional(),
431
+ scripts: z2.record(z2.string(), z2.string()).optional()
262
432
  });
263
433
  var VERIFY_SCRIPT_ORDER = ["build", "test", "typecheck", "lint", "check", "verify", "ci"];
264
434
  var VERIFY_SCRIPT_PREFIX = /^(build|test|typecheck|lint|check|verify|ci)(:|$)/;
265
435
  async function detectPackageInfo(root) {
266
- const packageJsonPath = join(root, "package.json");
436
+ const packageJsonPath = join2(root, "package.json");
267
437
  if (!await pathExists(packageJsonPath)) {
268
438
  return void 0;
269
439
  }
270
- const rawPackageJson = await readFile3(packageJsonPath, "utf8");
440
+ const rawPackageJson = await readFile4(packageJsonPath, "utf8");
271
441
  const packageJson = PackageJsonSchema.parse(JSON.parse(rawPackageJson));
272
442
  const packageManager = await detectPackageManager(root, packageJson.packageManager);
273
443
  const verificationScripts = detectVerificationScripts(packageJson.scripts ?? {});
@@ -289,7 +459,7 @@ async function detectPackageManager(root, packageManagerField) {
289
459
  ["bun.lockb", "bun"]
290
460
  ];
291
461
  for (const [lockfile, manager] of lockfiles) {
292
- if (await pathExists(join(root, lockfile))) {
462
+ if (await pathExists(join2(root, lockfile))) {
293
463
  return manager;
294
464
  }
295
465
  }
@@ -320,29 +490,73 @@ function orderIndex(name) {
320
490
  }
321
491
 
322
492
  // src/core/risk.ts
493
+ var RISK_RULES = [
494
+ {
495
+ severity: "high",
496
+ title: "Security-sensitive code changed",
497
+ detail: "Review redaction, auth, token, or secret-handling changes carefully before handoff.",
498
+ matches: (file) => /(^|\/)(redact|secret|auth|token|security)/i.test(file)
499
+ },
500
+ {
501
+ severity: "high",
502
+ title: "Release or package publishing path changed",
503
+ detail: "Release and package changes can break install, provenance, or publish flow; run pnpm pack:dry-run and pnpm smoke:release before tagging or publishing.",
504
+ matches: isReleaseOrPackageFile
505
+ },
506
+ {
507
+ severity: "medium",
508
+ title: "CI workflow changed",
509
+ detail: "Workflow changes can fail only after push; confirm GitHub Actions still passes on the target branch.",
510
+ matches: (file) => file.startsWith(".github/workflows/")
511
+ },
512
+ {
513
+ severity: "medium",
514
+ title: "Build tooling or TypeScript config changed",
515
+ detail: "Tooling changes can break typecheck, lint, build output, or package entrypoints; run the full local check command.",
516
+ matches: isBuildToolingFile
517
+ },
518
+ {
519
+ severity: "medium",
520
+ title: "CLI behavior changed",
521
+ detail: "CLI entrypoint or command changes can break user-facing flags and output contracts; cover the changed command with unit or integration tests.",
522
+ matches: (file) => file.startsWith("src/cli/")
523
+ },
524
+ {
525
+ severity: "medium",
526
+ title: "Resume parsing changed",
527
+ detail: "Resume parser changes can drop handoff context; verify completed work, next steps, failures, and open questions are still extracted.",
528
+ matches: (file) => file === "src/core/resume.ts" || file.includes("/resume")
529
+ },
530
+ {
531
+ severity: "medium",
532
+ title: "Handoff report rendering changed",
533
+ detail: "Report rendering changes can hide critical context; verify Markdown and JSON output still include repository, verification, risk, and next-step sections.",
534
+ matches: (file) => file.startsWith("src/report/")
535
+ },
536
+ {
537
+ severity: "medium",
538
+ title: "Generated artifact or ignore policy changed",
539
+ detail: "Ignore/cache policy changes can pollute changedFiles or published packages; verify generated directories remain ignored and excluded from reports.",
540
+ matches: isGeneratedOrIgnorePolicyFile
541
+ },
542
+ {
543
+ severity: "low",
544
+ title: "Documentation changed",
545
+ detail: "Documentation-only changes still need examples, command names, and release instructions checked against the current CLI behavior.",
546
+ matches: isDocumentationFile
547
+ }
548
+ ];
323
549
  function analyzeRisk(report) {
324
550
  const files = report.repository.changedFiles;
325
551
  const notes = [];
326
- if (files.some((file) => /(^|\/)(redact|secret|auth|token|security)/i.test(file))) {
327
- notes.push({
328
- severity: "high",
329
- title: "Security-sensitive code changed",
330
- detail: "Review redaction, auth, token, or secret-handling changes carefully before handoff."
331
- });
332
- }
333
- if (files.some((file) => file === "package.json" || file.endsWith("-lock.yaml") || file.endsWith("lock.json"))) {
334
- notes.push({
335
- severity: "medium",
336
- title: "Dependency or package metadata changed",
337
- detail: "Run install/build verification and check package publishing metadata."
338
- });
339
- }
340
- if (files.some((file) => file.startsWith(".github/workflows/"))) {
341
- notes.push({
342
- severity: "medium",
343
- title: "CI workflow changed",
344
- detail: "Confirm GitHub Actions still passes after push."
345
- });
552
+ for (const rule of RISK_RULES) {
553
+ if (files.some(rule.matches)) {
554
+ notes.push({
555
+ severity: rule.severity,
556
+ title: rule.title,
557
+ detail: rule.detail
558
+ });
559
+ }
346
560
  }
347
561
  const sourceFiles = files.filter((file) => file.startsWith("src/") && file.endsWith(".ts"));
348
562
  const testFiles = files.filter((file) => file.startsWith("tests/") && file.endsWith(".test.ts"));
@@ -362,11 +576,23 @@ function analyzeRisk(report) {
362
576
  }
363
577
  return { notes };
364
578
  }
579
+ function isReleaseOrPackageFile(file) {
580
+ return file === "package.json" || file === "CHANGELOG.md" || file === "docs/RELEASE.md" || file === "scripts/release-smoke.mjs" || /^\.github\/workflows\/.*release.*\.ya?ml$/i.test(file) || /(^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?)$/i.test(file);
581
+ }
582
+ function isBuildToolingFile(file) {
583
+ return /(^|\/)(tsconfig(?:\.[^/]*)?\.json|tsup\.config\.ts|vitest\.config\.ts|eslint\.config\.[cm]?[jt]s|pnpm-workspace\.yaml)$/i.test(file) || file.startsWith("scripts/");
584
+ }
585
+ function isGeneratedOrIgnorePolicyFile(file) {
586
+ return file === ".gitignore" || file === ".npmignore" || file.startsWith(".handoffkit/") || file.startsWith("docs/checkpoints/") || /(^|\/)(dist|coverage|node_modules|\.tmp-tests)\//.test(file);
587
+ }
588
+ function isDocumentationFile(file) {
589
+ return file === "README.md" || file === "ROADMAP.md" || file === "CONTRIBUTING.md" || file === "SECURITY.md" || file.startsWith("docs/");
590
+ }
365
591
 
366
592
  // src/core/scanners.ts
367
- import { mkdtemp, readFile as readFile4 } from "fs/promises";
593
+ import { mkdtemp, readFile as readFile5 } from "fs/promises";
368
594
  import { tmpdir } from "os";
369
- import { relative, join as join2 } from "path";
595
+ import { relative, join as join3 } from "path";
370
596
  import { performance } from "perf_hooks";
371
597
  import { execa as execa2 } from "execa";
372
598
  import fg2 from "fast-glob";
@@ -452,14 +678,14 @@ async function runScanner(root, scanner) {
452
678
  }
453
679
  async function runGitleaks(root) {
454
680
  const started = performance.now();
455
- const tempDir = await mkdtemp(join2(tmpdir(), "handoffkit-gitleaks-"));
456
- const reportPath = join2(tempDir, "report.json");
681
+ const tempDir = await mkdtemp(join3(tmpdir(), "handoffkit-gitleaks-"));
682
+ const reportPath = join3(tempDir, "report.json");
457
683
  const result = await execa2(
458
684
  "gitleaks",
459
685
  ["dir", root, "--no-banner", "--no-color", "--redact=100", "--report-format", "json", "--report-path", reportPath, "--max-target-megabytes", "2"],
460
686
  { reject: false, all: true }
461
687
  );
462
- const rawReport = await readFile4(reportPath, "utf8").catch(() => "[]");
688
+ const rawReport = await readFile5(reportPath, "utf8").catch(() => "[]");
463
689
  const findings = normalizeGitleaksFindings(rawReport);
464
690
  return {
465
691
  name: "gitleaks",
@@ -606,6 +832,7 @@ async function collectHandoffReport(options) {
606
832
  ...packageInfo ? { packageInfo } : {},
607
833
  ...options.resumeSource ? { resumeSource: options.resumeSource } : {},
608
834
  ...options.includeVerification ? { verification: await runVerification(root) } : {},
835
+ ...options.includeCache ? { cache: { artifacts: await listCacheArtifacts(root) } } : {},
609
836
  secretScanning,
610
837
  budget: {
611
838
  requestedTokens: options.budget,
@@ -618,7 +845,7 @@ async function collectHandoffReport(options) {
618
845
  }
619
846
 
620
847
  // src/cli/output.ts
621
- import { mkdir, writeFile } from "fs/promises";
848
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
622
849
  import { dirname, resolve } from "path";
623
850
 
624
851
  // src/core/budget.ts
@@ -664,6 +891,7 @@ var genericOrder = [
664
891
  "package",
665
892
  "resume",
666
893
  "verification",
894
+ "cache",
667
895
  "risk",
668
896
  "secretScanning"
669
897
  ];
@@ -684,6 +912,7 @@ var profiles = {
684
912
  "gitStatus",
685
913
  "changedFiles",
686
914
  "verification",
915
+ "cache",
687
916
  "risk",
688
917
  "branchDelta",
689
918
  "diffSummary",
@@ -708,6 +937,7 @@ var profiles = {
708
937
  "resume",
709
938
  "repository",
710
939
  "verification",
940
+ "cache",
711
941
  "risk",
712
942
  "changedFiles",
713
943
  "gitStatus",
@@ -740,6 +970,7 @@ var profiles = {
740
970
  "instructionFiles",
741
971
  "package",
742
972
  "verification",
973
+ "cache",
743
974
  "risk",
744
975
  "resume",
745
976
  "secretScanning",
@@ -814,6 +1045,8 @@ function renderSection(section, report) {
814
1045
  return renderResumeSource(report);
815
1046
  case "verification":
816
1047
  return renderVerification(report);
1048
+ case "cache":
1049
+ return renderCache(report);
817
1050
  case "risk":
818
1051
  return renderRisk(report);
819
1052
  case "secretScanning":
@@ -919,6 +1152,18 @@ function renderVerification(report) {
919
1152
  ""
920
1153
  ];
921
1154
  }
1155
+ function renderCache(report) {
1156
+ if (!report.cache) {
1157
+ return [];
1158
+ }
1159
+ return ["## Cache Artifacts", renderCacheArtifacts(report.cache.artifacts), ""];
1160
+ }
1161
+ function renderCacheArtifacts(artifacts) {
1162
+ if (artifacts.length === 0) {
1163
+ return "No cache artifacts found.";
1164
+ }
1165
+ return artifacts.map((artifact) => `- ${artifact.kind}/${artifact.name} (${artifact.createdAt}) - \`${artifact.path}\``).join("\n");
1166
+ }
922
1167
  function renderRisk(report) {
923
1168
  if (!report.risk) {
924
1169
  return [];
@@ -989,8 +1234,8 @@ async function writeRenderedReport(report, format, budget, output) {
989
1234
  const rendered = redactText(renderOutput(report, format, budget));
990
1235
  if (output) {
991
1236
  const outputPath = resolve(process.cwd(), output);
992
- await mkdir(dirname(outputPath), { recursive: true });
993
- await writeFile(outputPath, rendered, "utf8");
1237
+ await mkdir2(dirname(outputPath), { recursive: true });
1238
+ await writeFile2(outputPath, rendered, "utf8");
994
1239
  process.stderr.write(`Wrote handoff packet to ${outputPath}
995
1240
  `);
996
1241
  return;
@@ -1014,21 +1259,24 @@ function renderOutput(report, format, budget) {
1014
1259
  }
1015
1260
 
1016
1261
  // src/cli/commands/pack.ts
1017
- var PackCliOptionsSchema = z2.object({
1018
- goal: z2.string().trim().min(1).default("Make your own goal"),
1019
- output: z2.string().optional(),
1020
- format: z2.enum(["markdown", "json"]).default("markdown"),
1021
- for: z2.enum(["generic", "codex", "claude", "cursor"]).default("generic"),
1022
- budget: z2.number().int().positive().default(4e3),
1023
- includeDiff: z2.boolean().default(false),
1024
- diff: z2.boolean().default(true),
1025
- since: z2.string().trim().min(1).optional(),
1026
- verify: z2.boolean().default(false),
1027
- scanSecrets: z2.boolean().default(false)
1262
+ var PackCliOptionsSchema = z3.object({
1263
+ goal: z3.string().trim().min(1).default("Make your own goal"),
1264
+ output: z3.string().optional(),
1265
+ format: z3.enum(["markdown", "json"]).default("markdown"),
1266
+ for: z3.enum(["generic", "codex", "claude", "cursor"]).default("generic"),
1267
+ budget: z3.number().int().positive().default(4e3),
1268
+ includeDiff: z3.boolean().default(false),
1269
+ diff: z3.boolean().default(true),
1270
+ since: z3.string().trim().min(1).optional(),
1271
+ verify: z3.boolean().default(false),
1272
+ scanSecrets: z3.boolean().default(false),
1273
+ cache: z3.boolean().default(false),
1274
+ includeCache: z3.boolean().default(false)
1028
1275
  });
1029
1276
  function createPackCommand() {
1030
- return new Command("pack").description("Create a safe local handoff packet for another AI assistant.").summary("Create a Markdown or JSON packet from the current git state.").option("--goal <text>", "handoff goal", "Make your own goal").option("--output <path>", "write output to a file instead of stdout").option("--format <format>", "output format: markdown or json", "markdown").option("--for <agent>", "target output: generic, codex, claude, or cursor", "generic").option("--budget <tokens>", "rough output token budget", parseBudget, 4e3).option("--since <ref>", "focus committed branch delta on a base ref").option("--verify", "run safe verification scripts and include results").option("--scan-secrets", "run optional local secret scanners and include bounded results").option("--include-diff", "include full staged and unstaged patches", false).option("--no-diff", "omit diff summaries and full patches").action(async (rawOptions) => {
1277
+ return new Command2("pack").description("Create a safe local handoff packet for another AI assistant.").summary("Create a Markdown or JSON packet from the current git state.").option("--goal <text>", "handoff goal", "Make your own goal").option("--output <path>", "write output to a file instead of stdout").option("--format <format>", "output format: markdown or json", "markdown").option("--for <agent>", "target output: generic, codex, claude, or cursor", "generic").option("--budget <tokens>", "rough output token budget", parseBudget, 4e3).option("--since <ref>", "focus committed branch delta on a base ref").option("--verify", "run safe verification scripts and include results").option("--scan-secrets", "run optional local secret scanners and include bounded results").option("--cache", "write explicit local cache artifacts under .handoffkit when available").option("--include-cache", "include recent .handoffkit artifact summaries").option("--include-diff", "include full staged and unstaged patches", false).option("--no-diff", "omit diff summaries and full patches").action(async (rawOptions) => {
1031
1278
  const options = parseOptions(rawOptions);
1279
+ const root = options.cache ? await findGitRoot(process.cwd()) : void 0;
1032
1280
  const report = await collectHandoffReport({
1033
1281
  goal: options.goal,
1034
1282
  cwd: process.cwd(),
@@ -1040,8 +1288,18 @@ function createPackCommand() {
1040
1288
  includeDiffSummary: options.diff,
1041
1289
  ...options.since ? { since: options.since } : {},
1042
1290
  includeVerification: options.verify,
1043
- scanSecrets: options.scanSecrets
1291
+ scanSecrets: options.scanSecrets,
1292
+ includeCache: options.includeCache
1044
1293
  });
1294
+ if (options.cache && root && report.verification) {
1295
+ const cache = await writeCacheArtifact(root, "verification", {
1296
+ goal: report.goal,
1297
+ target: report.target,
1298
+ verification: report.verification
1299
+ });
1300
+ process.stderr.write(`Wrote verification cache to ${cache.latestPath}
1301
+ `);
1302
+ }
1045
1303
  await writeRenderedReport(report, options.format, options.budget, options.output);
1046
1304
  });
1047
1305
  }
@@ -1063,18 +1321,27 @@ function parseBudget(value) {
1063
1321
  }
1064
1322
 
1065
1323
  // src/cli/commands/resume.ts
1066
- import { readFile as readFile5 } from "fs/promises";
1067
- import { Command as Command2 } from "commander";
1068
- import { z as z3 } from "zod";
1324
+ import { readFile as readFile6 } from "fs/promises";
1325
+ import { Command as Command3 } from "commander";
1326
+ import { z as z4 } from "zod";
1069
1327
 
1070
1328
  // src/core/resume.ts
1071
1329
  var RESUME_PREVIEW_LIMIT = 3e3;
1072
1330
  var SECTION_ALIASES = {
1073
- completed: [/^completed$/i, /^done$/i, /^done this session$/i, /^what changed$/i, /^implemented$/i],
1074
- remaining: [/^remaining$/i, /^next steps$/i, /^todo$/i, /^to do$/i],
1075
- failedCommands: [/^failed commands$/i, /^failures$/i, /^errors$/i],
1076
- openQuestions: [/^open questions$/i, /^open questions \/ risks$/i, /^open questions and risks$/i, /^questions$/i, /^blockers$/i],
1077
- verification: [/^verification$/i, /^tests$/i, /^validation$/i]
1331
+ completed: [/^completed$/i, /^completed work$/i, /^done$/i, /^done this session$/i, /^what changed$/i, /^what i changed$/i, /^implemented$/i],
1332
+ remaining: [/^remaining$/i, /^remaining work$/i, /^next steps$/i, /^next action$/i, /^next safest action$/i, /^todo$/i, /^to do$/i],
1333
+ failedCommands: [/^failed command$/i, /^failed commands$/i, /^command failed$/i, /^commands failed$/i, /^failure$/i, /^failures$/i, /^error$/i, /^errors$/i],
1334
+ openQuestions: [
1335
+ /^open question$/i,
1336
+ /^open questions$/i,
1337
+ /^open questions \/ risks$/i,
1338
+ /^open questions and risks$/i,
1339
+ /^question$/i,
1340
+ /^questions$/i,
1341
+ /^blocker$/i,
1342
+ /^blockers$/i
1343
+ ],
1344
+ verification: [/^verification$/i, /^tests$/i, /^tests run$/i, /^validation$/i]
1078
1345
  };
1079
1346
  function createResumeSource(path, content) {
1080
1347
  const normalized = content.replace(/\r\n/g, "\n").trim();
@@ -1109,12 +1376,22 @@ function parseResumeState(content) {
1109
1376
  section = sectionForHeading(heading);
1110
1377
  continue;
1111
1378
  }
1379
+ const transcriptLine = stripTranscriptPrefix(line);
1380
+ const labeled = parseLabeledTranscriptLine(transcriptLine);
1381
+ if (labeled) {
1382
+ heading = labeled.heading;
1383
+ section = labeled.section;
1384
+ if (labeled.item) {
1385
+ appendResumeItem(state, section, labeled.item, heading);
1386
+ }
1387
+ continue;
1388
+ }
1112
1389
  if (!section) {
1113
1390
  continue;
1114
1391
  }
1115
- const item = normalizeListItem(line);
1392
+ const item = normalizeListItem(transcriptLine);
1116
1393
  if (item) {
1117
- state[section].push({ text: redactText(item), ...heading ? { sourceHeading: redactText(heading) } : {} });
1394
+ appendResumeItem(state, section, item, heading);
1118
1395
  }
1119
1396
  }
1120
1397
  const next = state.remaining[0] ?? state.openQuestions[0] ?? state.failedCommands[0];
@@ -1132,10 +1409,41 @@ function sectionForHeading(heading) {
1132
1409
  }
1133
1410
  return void 0;
1134
1411
  }
1412
+ function parseLabeledTranscriptLine(line) {
1413
+ const match = line.match(/^([^:]{1,80}):(?:\s*(.*))?$/);
1414
+ if (!match?.[1]) {
1415
+ return void 0;
1416
+ }
1417
+ const heading = match[1].trim();
1418
+ const section = sectionForHeading(heading);
1419
+ if (!section) {
1420
+ return void 0;
1421
+ }
1422
+ const item = match[2]?.trim();
1423
+ return {
1424
+ section,
1425
+ heading,
1426
+ ...item ? { item } : {}
1427
+ };
1428
+ }
1429
+ function appendResumeItem(state, section, item, heading) {
1430
+ state[section].push({ text: redactText(item), ...heading ? { sourceHeading: redactText(heading) } : {} });
1431
+ }
1135
1432
  function normalizeListItem(line) {
1136
1433
  const match = line.match(/^[-*]\s+(.+)$/) ?? line.match(/^\d+\.\s+(.+)$/);
1137
1434
  return match?.[1]?.trim();
1138
1435
  }
1436
+ function stripTranscriptPrefix(line) {
1437
+ let current = line.trim();
1438
+ for (let i = 0; i < 4; i += 1) {
1439
+ const next = current.replace(/^\[[^\]\n]{1,60}\]\s*/, "").replace(/^(?:user|assistant|system|developer|tool|terminal|command|cmd|result|codex|claude|cursor|gemini)(?:\s*\([^)]*\))?\s*[:>]\s*/i, "").trim();
1440
+ if (next === current) {
1441
+ return current;
1442
+ }
1443
+ current = next;
1444
+ }
1445
+ return current;
1446
+ }
1139
1447
  function normalizeHeading(heading) {
1140
1448
  return heading.trim().replace(/:$/, "").replace(/\s*\/\s*/g, " / ").replace(/\s+/g, " ");
1141
1449
  }
@@ -1144,17 +1452,20 @@ function hasResumeState(state) {
1144
1452
  }
1145
1453
 
1146
1454
  // src/cli/commands/resume.ts
1147
- var ResumeOptionsSchema = z3.object({
1148
- goal: z3.string().trim().min(1).default("Resume interrupted AI coding session"),
1149
- output: z3.string().optional(),
1150
- format: z3.enum(["markdown", "json"]).default("markdown"),
1151
- for: z3.enum(["generic", "codex", "claude", "cursor"]).default("generic"),
1152
- budget: z3.number().int().positive().default(4e3)
1455
+ var ResumeOptionsSchema = z4.object({
1456
+ goal: z4.string().trim().min(1).default("Resume interrupted AI coding session"),
1457
+ output: z4.string().optional(),
1458
+ format: z4.enum(["markdown", "json"]).default("markdown"),
1459
+ for: z4.enum(["generic", "codex", "claude", "cursor"]).default("generic"),
1460
+ budget: z4.number().int().positive().default(4e3),
1461
+ cache: z4.boolean().default(false),
1462
+ fromCache: z4.string().trim().min(1).optional()
1153
1463
  });
1154
1464
  function createResumeCommand() {
1155
- return new Command2("resume").description("Create a fresh handoff packet using a previous handoff as resume context.").summary("Merge a previous handoff or transcript with fresh repo state.").argument("<path>", "previous handoff or transcript file").option("--goal <text>", "new handoff goal", "Resume interrupted AI coding session").option("--output <path>", "write output to a file instead of stdout").option("--format <format>", "output format: markdown or json", "markdown").option("--for <agent>", "target output: generic, codex, claude, or cursor", "generic").option("--budget <tokens>", "rough output token budget", parseBudget2, 4e3).action(async (path, rawOptions) => {
1465
+ return new Command3("resume").description("Create a fresh handoff packet using a previous handoff as resume context.").summary("Merge a previous handoff or transcript with fresh repo state.").argument("[path]", "previous handoff or transcript file").option("--goal <text>", "new handoff goal", "Resume interrupted AI coding session").option("--output <path>", "write output to a file instead of stdout").option("--format <format>", "output format: markdown or json", "markdown").option("--for <agent>", "target output: generic, codex, claude, or cursor", "generic").option("--budget <tokens>", "rough output token budget", parseBudget2, 4e3).option("--cache", "write a local resume artifact under .handoffkit/resume").option("--from-cache <ref>", "load resume source from .handoffkit/resume, for example latest or resume/latest").action(async (path, rawOptions) => {
1156
1466
  const options = ResumeOptionsSchema.parse(rawOptions);
1157
- const source = createResumeSource(path, await readFile5(path, "utf8"));
1467
+ const root = options.cache || options.fromCache ? await findGitRoot(process.cwd()) : void 0;
1468
+ const source = await resolveResumeSource(path, options.fromCache, root);
1158
1469
  const report = await collectHandoffReport({
1159
1470
  goal: options.goal,
1160
1471
  cwd: process.cwd(),
@@ -1166,11 +1477,36 @@ function createResumeCommand() {
1166
1477
  includeDiffSummary: true,
1167
1478
  includeVerification: false,
1168
1479
  scanSecrets: false,
1480
+ includeCache: false,
1169
1481
  resumeSource: source
1170
1482
  });
1483
+ if (options.cache && root) {
1484
+ const cache = await writeCacheArtifact(root, "resume", {
1485
+ goal: report.goal,
1486
+ target: report.target,
1487
+ source
1488
+ });
1489
+ process.stderr.write(`Wrote resume cache to ${cache.latestPath}
1490
+ `);
1491
+ }
1171
1492
  await writeRenderedReport(report, options.format, options.budget, options.output);
1172
1493
  });
1173
1494
  }
1495
+ async function resolveResumeSource(path, fromCache, root) {
1496
+ if (path && fromCache) {
1497
+ throw new Error("Pass either <path> or --from-cache, not both.");
1498
+ }
1499
+ if (fromCache) {
1500
+ if (!root) {
1501
+ throw new Error("--from-cache requires a git repository.");
1502
+ }
1503
+ return readResumeSourceFromCache(root, fromCache);
1504
+ }
1505
+ if (!path) {
1506
+ throw new Error("resume requires <path> or --from-cache <ref>.");
1507
+ }
1508
+ return createResumeSource(path, await readFile6(path, "utf8"));
1509
+ }
1174
1510
  function parseBudget2(value) {
1175
1511
  const parsed = Number.parseInt(value, 10);
1176
1512
  if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -1180,13 +1516,13 @@ function parseBudget2(value) {
1180
1516
  }
1181
1517
 
1182
1518
  // src/cli/commands/risk.ts
1183
- import { Command as Command3 } from "commander";
1184
- import { z as z4 } from "zod";
1185
- var RiskOptionsSchema = z4.object({
1186
- format: z4.enum(["markdown", "json"]).default("markdown")
1519
+ import { Command as Command4 } from "commander";
1520
+ import { z as z5 } from "zod";
1521
+ var RiskOptionsSchema = z5.object({
1522
+ format: z5.enum(["markdown", "json"]).default("markdown")
1187
1523
  });
1188
1524
  function createRiskCommand() {
1189
- return new Command3("risk").description("Show deterministic risk notes for the current handoff.").summary("Show deterministic risk notes from changed files.").option("--format <format>", "output format: markdown or json", "markdown").action(async (rawOptions) => {
1525
+ return new Command4("risk").description("Show deterministic risk notes for the current handoff.").summary("Show deterministic risk notes from changed files.").option("--format <format>", "output format: markdown or json", "markdown").action(async (rawOptions) => {
1190
1526
  const options = RiskOptionsSchema.parse(rawOptions);
1191
1527
  const report = await collectHandoffReport({
1192
1528
  goal: "Review local risk",
@@ -1197,7 +1533,8 @@ function createRiskCommand() {
1197
1533
  includeDiff: false,
1198
1534
  includeDiffSummary: false,
1199
1535
  includeVerification: false,
1200
- scanSecrets: false
1536
+ scanSecrets: false,
1537
+ includeCache: false
1201
1538
  });
1202
1539
  if (options.format === "json") {
1203
1540
  process.stdout.write(`${JSON.stringify(report.risk, null, 2)}
@@ -1212,13 +1549,13 @@ ${report.risk?.notes.map((note) => `- **${note.severity}**: ${note.title} - ${no
1212
1549
  }
1213
1550
 
1214
1551
  // src/cli/commands/scan-secrets.ts
1215
- import { Command as Command4 } from "commander";
1216
- import { z as z5 } from "zod";
1217
- var ScanSecretsOptionsSchema = z5.object({
1218
- format: z5.enum(["markdown", "json"]).default("markdown")
1552
+ import { Command as Command5 } from "commander";
1553
+ import { z as z6 } from "zod";
1554
+ var ScanSecretsOptionsSchema = z6.object({
1555
+ format: z6.enum(["markdown", "json"]).default("markdown")
1219
1556
  });
1220
1557
  function createScanSecretsCommand() {
1221
- return new Command4("scan-secrets").description("Run optional local secret scanners and print bounded redacted results.").summary("Run optional local secret scanners.").option("--format <format>", "output format: markdown or json", "markdown").action(async (rawOptions) => {
1558
+ return new Command5("scan-secrets").description("Run optional local secret scanners and print bounded redacted results.").summary("Run optional local secret scanners.").option("--format <format>", "output format: markdown or json", "markdown").action(async (rawOptions) => {
1222
1559
  const options = ScanSecretsOptionsSchema.parse(rawOptions);
1223
1560
  const root = await findGitRoot(process.cwd());
1224
1561
  const report = await runSecretScanners(root);
@@ -1259,16 +1596,22 @@ function scannerGuidanceLines2(scanner) {
1259
1596
  }
1260
1597
 
1261
1598
  // src/cli/commands/verify.ts
1262
- import { Command as Command5 } from "commander";
1263
- import { z as z6 } from "zod";
1264
- var VerifyOptionsSchema = z6.object({
1265
- format: z6.enum(["markdown", "json"]).default("markdown")
1599
+ import { Command as Command6 } from "commander";
1600
+ import { z as z7 } from "zod";
1601
+ var VerifyOptionsSchema = z7.object({
1602
+ format: z7.enum(["markdown", "json"]).default("markdown"),
1603
+ cache: z7.boolean().default(false)
1266
1604
  });
1267
1605
  function createVerifyCommand() {
1268
- return new Command5("verify").description("Run safe local verification scripts.").summary("Run safe detected verification scripts.").option("--format <format>", "output format: markdown or json", "markdown").action(async (rawOptions) => {
1606
+ return new Command6("verify").description("Run safe local verification scripts.").summary("Run safe detected verification scripts.").option("--format <format>", "output format: markdown or json", "markdown").option("--cache", "write a local verification artifact under .handoffkit/verification").action(async (rawOptions) => {
1269
1607
  const options = VerifyOptionsSchema.parse(rawOptions);
1270
1608
  const root = await findGitRoot(process.cwd());
1271
1609
  const verification = await runVerification(root);
1610
+ if (options.cache) {
1611
+ const cache = await writeCacheArtifact(root, "verification", verification);
1612
+ process.stderr.write(`Wrote verification cache to ${cache.latestPath}
1613
+ `);
1614
+ }
1272
1615
  if (options.format === "json") {
1273
1616
  process.stdout.write(redactText(`${JSON.stringify(verification, null, 2)}
1274
1617
  `));
@@ -1291,12 +1634,13 @@ function renderVerificationMarkdown(commands) {
1291
1634
  }
1292
1635
 
1293
1636
  // src/cli/index.ts
1294
- var program = new Command6().name("handoffkit").description("Create safe local handoff packets for AI-assisted coding sessions.").summary("Create local-first AI coding session handoff packets.").showHelpAfterError("(run with --help for usage)").version("0.2.0");
1637
+ var program = new Command7().name("handoffkit").description("Create safe local handoff packets for AI-assisted coding sessions.").summary("Create local-first AI coding session handoff packets.").showHelpAfterError("(run with --help for usage)").version("0.4.0");
1295
1638
  program.addCommand(createPackCommand());
1296
1639
  program.addCommand(createVerifyCommand());
1297
1640
  program.addCommand(createRiskCommand());
1298
1641
  program.addCommand(createScanSecretsCommand());
1299
1642
  program.addCommand(createResumeCommand());
1643
+ program.addCommand(createCacheCommand());
1300
1644
  try {
1301
1645
  await program.parseAsync(process.argv);
1302
1646
  } catch (error) {