flatbread-cli 0.1.0 → 0.2.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.
@@ -6,10 +6,10 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // src/index.ts
9
- import { spawn as spawn2 } from "node:child_process";
10
- import fs19 from "node:fs";
9
+ import { spawn as spawn3 } from "node:child_process";
10
+ import fs21 from "node:fs";
11
11
  import os7 from "node:os";
12
- import path18 from "node:path";
12
+ import path20 from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
14
 
15
15
  // ../../node_modules/zod/v3/external.js
@@ -490,8 +490,8 @@ function getErrorMap() {
490
490
 
491
491
  // ../../node_modules/zod/v3/helpers/parseUtil.js
492
492
  var makeIssue = (params) => {
493
- const { data, path: path19, errorMaps, issueData } = params;
494
- const fullPath = [...path19, ...issueData.path || []];
493
+ const { data, path: path21, errorMaps, issueData } = params;
494
+ const fullPath = [...path21, ...issueData.path || []];
495
495
  const fullIssue = {
496
496
  ...issueData,
497
497
  path: fullPath
@@ -607,11 +607,11 @@ var errorUtil;
607
607
 
608
608
  // ../../node_modules/zod/v3/types.js
609
609
  var ParseInputLazyPath = class {
610
- constructor(parent, value, path19, key) {
610
+ constructor(parent, value, path21, key) {
611
611
  this._cachedPath = [];
612
612
  this.parent = parent;
613
613
  this.data = value;
614
- this._path = path19;
614
+ this._path = path21;
615
615
  this._key = key;
616
616
  }
617
617
  get path() {
@@ -4340,6 +4340,15 @@ var VERDICT_WORDS = {
4340
4340
  "not-rerun": { short: "not re-run", phrase: "no test after the last edit" },
4341
4341
  "no-edits": { short: "no edits", phrase: null }
4342
4342
  };
4343
+ function treeWords(lineage) {
4344
+ if (lineage?.dirtyFiles === void 0) return null;
4345
+ const total = lineage.dirtyFiles.length + (lineage.dirtyFilesOmitted ?? 0);
4346
+ if (lineage.commitNamed === true) {
4347
+ if (total === 0) return "clean at that commit";
4348
+ return total === 1 ? "1 file differs from that commit" : `${total} files differ from that commit`;
4349
+ }
4350
+ return total === 0 ? "clean tree" : `${total} uncommitted file${total === 1 ? "" : "s"}`;
4351
+ }
4343
4352
  function escapeRegExp(value) {
4344
4353
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4345
4354
  }
@@ -4375,8 +4384,8 @@ function firstLine(text2) {
4375
4384
  function labelOf(step, cwd) {
4376
4385
  const tool = step.tool;
4377
4386
  if (tool === void 0) return step.type;
4378
- const path19 = firstString(tool.input, FILE_PATH_KEYS);
4379
- if (path19 !== void 0) return `${tool.name} ${repoRelative(path19, cwd)}`;
4387
+ const path21 = firstString(tool.input, FILE_PATH_KEYS);
4388
+ if (path21 !== void 0) return `${tool.name} ${repoRelative(path21, cwd)}`;
4380
4389
  const other = firstString(tool.input, TARGET_KEYS);
4381
4390
  if (other !== void 0) return cut(`${tool.name}: ${repoRelative(firstLine(other), cwd)}`, BRIEF_LIMITS.quote);
4382
4391
  return tool.name;
@@ -4624,9 +4633,9 @@ function runDiagnostics(trace) {
4624
4633
  searches.set(key, found);
4625
4634
  }
4626
4635
  }
4627
- for (const [path19, entries] of reads) {
4636
+ for (const [path21, entries] of reads) {
4628
4637
  const hit = densest(entries, WASTE_RULES.repeatedRead.times, WASTE_RULES.repeatedRead.windowMinutes * 6e4);
4629
- if (hit) waste.push({ kind: "repeated-read", path: path19, steps: hit, windowMinutes: WASTE_RULES.repeatedRead.windowMinutes });
4638
+ if (hit) waste.push({ kind: "repeated-read", path: path21, steps: hit, windowMinutes: WASTE_RULES.repeatedRead.windowMinutes });
4630
4639
  }
4631
4640
  for (const { label, entries } of searches.values()) {
4632
4641
  const hit = densest(entries, WASTE_RULES.repeatedSearch.times, WASTE_RULES.repeatedSearch.windowMinutes * 6e4);
@@ -4704,19 +4713,19 @@ function filesOf(steps, cwd) {
4704
4713
  const patches = [];
4705
4714
  steps.forEach((step, index) => {
4706
4715
  if (!isSuccessfulEdit(step) || step.tool === void 0) return;
4707
- const path19 = repoRelative(firstString(step.tool.input, FILE_PATH_KEYS), cwd);
4708
- const file = files.get(path19) ?? { path: path19 };
4716
+ const path21 = repoRelative(firstString(step.tool.input, FILE_PATH_KEYS), cwd);
4717
+ const file = files.get(path21) ?? { path: path21 };
4709
4718
  if (step.diff !== void 0) {
4710
4719
  const counted = countPatch(step.diff.patch);
4711
4720
  file.added = (file.added ?? 0) + counted.added;
4712
4721
  file.removed = (file.removed ?? 0) + counted.removed;
4713
- patches.push({ path: path19, patch: repoRelative(step.diff.patch, cwd), step: step.stepNumber });
4722
+ patches.push({ path: path21, patch: repoRelative(step.diff.patch, cwd), step: step.stepNumber });
4714
4723
  }
4715
4724
  const result = resultOf(steps, index);
4716
4725
  if (step.tool.name.toLowerCase() === "write" && result !== void 0 && /^File created successfully\b/.test(result.trim())) {
4717
4726
  file.created = true;
4718
4727
  }
4719
- files.set(path19, file);
4728
+ files.set(path21, file);
4720
4729
  });
4721
4730
  return { files: [...files.values()], patches };
4722
4731
  }
@@ -4748,7 +4757,8 @@ function buildExecutionBrief(trace) {
4748
4757
  ...source.dirtyFiles !== void 0 ? { dirtyFiles: source.dirtyFiles } : {},
4749
4758
  ...source.dirtyFilesOmitted !== void 0 ? { dirtyFilesOmitted: source.dirtyFilesOmitted } : {},
4750
4759
  ...source.gitCapturedAt !== void 0 ? { capturedAt: source.gitCapturedAt } : {},
4751
- ...source.prUrl !== void 0 ? { prUrl: source.prUrl } : {}
4760
+ ...source.prUrl !== void 0 ? { prUrl: source.prUrl } : {},
4761
+ ...source.commitNamed === true ? { commitNamed: true } : {}
4752
4762
  };
4753
4763
  return {
4754
4764
  intent: {
@@ -4879,14 +4889,14 @@ var WHEN = {
4879
4889
  "no-edits": "in a run with no recorded edit"
4880
4890
  };
4881
4891
  function verificationLine(brief) {
4882
- const runs = brief.outcome.verification;
4892
+ const runs2 = brief.outcome.verification;
4883
4893
  const verdict = brief.outcome.afterLastEdit;
4884
- if (runs.length === 0) {
4894
+ if (runs2.length === 0) {
4885
4895
  return verdict === "no-edits" ? null : "Verification: no test run was recorded.";
4886
4896
  }
4887
4897
  const shown = [];
4888
4898
  for (const when of ["before-first-edit", "between-edits", "after-last-edit", "no-edits"]) {
4889
- const group = runs.filter((run) => run.when === when);
4899
+ const group = runs2.filter((run) => run.when === when);
4890
4900
  const last = group[group.length - 1];
4891
4901
  if (last !== void 0) shown.push(last);
4892
4902
  }
@@ -4894,7 +4904,7 @@ function verificationLine(brief) {
4894
4904
  const result = run.result === "unknown" ? "ran (result not recorded)" : run.result;
4895
4905
  return `${run.command} ${result} ${WHEN[run.when]}${run.counts ? ` (${run.counts})` : ""}.`;
4896
4906
  });
4897
- const omitted = runs.length - shown.length;
4907
+ const omitted = runs2.length - shown.length;
4898
4908
  if (omitted > 0) parts.push(`${omitted} earlier test run${omitted === 1 ? "" : "s"} not listed.`);
4899
4909
  if (verdict === "not-rerun") parts.push("Not re-run after the last edit.");
4900
4910
  return `Verification: ${parts.join(" ")}`;
@@ -4922,11 +4932,11 @@ function lineageLine(brief) {
4922
4932
  if (lineage === void 0) return null;
4923
4933
  const parts = [];
4924
4934
  if (lineage.branch !== void 0) parts.push(`branch ${lineage.branch}`);
4925
- if (lineage.commit !== void 0) parts.push(`commit ${lineage.commit.slice(0, 12)}`);
4926
- if (lineage.dirtyFiles !== void 0) {
4927
- const total = lineage.dirtyFiles.length + (lineage.dirtyFilesOmitted ?? 0);
4928
- parts.push(total === 0 ? "clean tree" : `${total} uncommitted file${total === 1 ? "" : "s"}`);
4935
+ if (lineage.commit !== void 0) {
4936
+ parts.push(`commit ${lineage.commit.slice(0, 12)}${lineage.commitNamed === true ? " (named with --commit)" : ""}`);
4929
4937
  }
4938
+ const tree = treeWords(lineage);
4939
+ if (tree !== null) parts.push(tree);
4930
4940
  if (lineage.prUrl !== void 0) parts.push(`PR ${lineage.prUrl}`);
4931
4941
  return parts.length === 0 ? null : `Git: ${parts.join(" \xB7 ")}`;
4932
4942
  }
@@ -5086,13 +5096,13 @@ function categorizeTrace(trace) {
5086
5096
  };
5087
5097
  }
5088
5098
  if (paths.length > 0) {
5089
- if (paths.every((path19) => matchesAny(path19, DOC_PATTERNS))) {
5099
+ if (paths.every((path21) => matchesAny(path21, DOC_PATTERNS))) {
5090
5100
  return { category: "docs", evidence: [`all ${paths.length} touched path(s) are documentation`, ...paths.slice(0, 3)] };
5091
5101
  }
5092
- if (paths.every((path19) => matchesAny(path19, TEST_PATTERNS))) {
5102
+ if (paths.every((path21) => matchesAny(path21, TEST_PATTERNS))) {
5093
5103
  return { category: "tests", evidence: [`all ${paths.length} touched path(s) are tests`, ...paths.slice(0, 3)] };
5094
5104
  }
5095
- if (paths.every((path19) => matchesAny(path19, INFRA_PATTERNS))) {
5105
+ if (paths.every((path21) => matchesAny(path21, INFRA_PATTERNS))) {
5096
5106
  return { category: "infra", evidence: [`all ${paths.length} touched path(s) are build/CI/deploy config`, ...paths.slice(0, 3)] };
5097
5107
  }
5098
5108
  }
@@ -5259,9 +5269,39 @@ function searchTerms(q) {
5259
5269
  function rowSearchText(row) {
5260
5270
  return normalizeSearch(`${row.title} ${row.slug} ${row.harness} ${HARNESS_INFO[row.harness].label}`);
5261
5271
  }
5272
+ var VOWEL = /[aeiouy]/;
5273
+ var UNDOUBLED = /([b-df-hj-km-np-rtv-xz])\1$/;
5274
+ function stem(word) {
5275
+ if (word.length < 4 || !/^[a-z]+$/.test(word)) return word;
5276
+ const keep = (base) => base.length >= 3 && VOWEL.test(base) ? base : word;
5277
+ const undouble = (base) => base.length >= 4 && UNDOUBLED.test(base) && !/(ll|ss|zz)$/.test(base) ? base.slice(0, -1) : base;
5278
+ if (/[^aeiou]ies$/.test(word) || /[^aeiou]ied$/.test(word)) return keep(word.slice(0, -3) + "y");
5279
+ if (word.endsWith("ing")) {
5280
+ const base = word.slice(0, -3);
5281
+ return base.length >= 3 && VOWEL.test(base) ? undouble(base) : word;
5282
+ }
5283
+ if (word.endsWith("ed") && !word.endsWith("eed")) {
5284
+ const base = word.slice(0, -2);
5285
+ return base.length >= 3 && VOWEL.test(base) ? undouble(base) : word;
5286
+ }
5287
+ if (/(?:ss|x|z|ch|sh)es$/.test(word)) return keep(word.slice(0, -2));
5288
+ if (word.endsWith("s") && !/(?:ss|us|is)$/.test(word)) return keep(word.slice(0, -1));
5289
+ return word;
5290
+ }
5291
+ function stemText(text2) {
5292
+ return text2.split(/[^\p{L}\p{N}]+/u).filter((word) => word.length > 0).map(stem).join(" ");
5293
+ }
5262
5294
  function matchesSearch(row, q) {
5263
- const haystack = rowSearchText(row);
5264
- return searchTerms(q).every((term) => haystack.includes(term));
5295
+ return matchesText(rowSearchText(row), q);
5296
+ }
5297
+ function matchesText(text2, q) {
5298
+ const haystack = normalizeSearch(text2);
5299
+ const stems = ` ${stemText(haystack)} `;
5300
+ return searchTerms(q).every((term) => {
5301
+ if (haystack.includes(term)) return true;
5302
+ const stemmed = stemText(term);
5303
+ return stemmed.length > 0 && stems.includes(stemmed);
5304
+ });
5265
5305
  }
5266
5306
 
5267
5307
  // ../schema/src/index.ts
@@ -5362,6 +5402,12 @@ var TraceSourceSchema = external_exports.object({
5362
5402
  /** How many more dirty paths there were than `dirtyFiles` holds. Absent = none were left out. */
5363
5403
  dirtyFilesOmitted: external_exports.number().int().positive().optional(),
5364
5404
  gitCapturedAt: external_exports.string().optional(),
5405
+ /**
5406
+ * `flatbread share --commit [sha]` (P7.2): the person sharing NAMED `headCommitSha`, and
5407
+ * `dirtyFiles` was measured against it - so an empty list means a clean tree AT THAT COMMIT.
5408
+ * Absent = HEAD, read automatically.
5409
+ */
5410
+ commitNamed: external_exports.literal(true).optional(),
5365
5411
  /** `flatbread share --pr <url>`: the pull request this run belongs to, as the person sharing it said. */
5366
5412
  prUrl: external_exports.string().url().max(GIT_LIMITS.prUrl).regex(/^https:\/\//, "a PR link must be https").optional(),
5367
5413
  /** Absent on records written before Pass 6, which were all claude-code. */
@@ -5554,6 +5600,13 @@ var TraceRecordSchema = external_exports.object({
5554
5600
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["slug"], message: `slug "${record.slug}" is reserved` });
5555
5601
  }
5556
5602
  });
5603
+ function normalizeTitle(input) {
5604
+ if (typeof input !== "string") return null;
5605
+ const flat = input.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
5606
+ if (flat.length === 0) return null;
5607
+ if (flat.length > LIMITS.title) return null;
5608
+ return flat;
5609
+ }
5557
5610
  var RecordSummarySchema = external_exports.object({
5558
5611
  slug: SlugSchema,
5559
5612
  title: external_exports.string(),
@@ -5656,6 +5709,9 @@ function claudeSettingsPath(env = process.env) {
5656
5709
  if (configDir) return path.join(configDir, "settings.json");
5657
5710
  return path.join(os.homedir(), ".claude", "settings.json");
5658
5711
  }
5712
+ function encodeCwd(cwd) {
5713
+ return cwd.replace(/[^a-zA-Z0-9]/g, "-");
5714
+ }
5659
5715
  function isSubagentSessionFile(filePath) {
5660
5716
  const segments = filePath.split(/[\\/]/).filter((segment2) => segment2.length > 0);
5661
5717
  const base = segments[segments.length - 1] ?? "";
@@ -7347,8 +7403,8 @@ function isInsideDir(child, parent) {
7347
7403
  }
7348
7404
  function targetIndicatesDb(target, env) {
7349
7405
  if (isDbTarget(target)) return true;
7350
- const stem = path8.basename(target, ".json");
7351
- if (/^ses_[0-9a-zA-Z]+$/.test(stem)) return true;
7406
+ const stem2 = path8.basename(target, ".json");
7407
+ if (/^ses_[0-9a-zA-Z]+$/.test(stem2)) return true;
7352
7408
  const resolved = path8.resolve(target);
7353
7409
  for (const dir of [dataDir(env), storageDir(env)]) {
7354
7410
  if (resolved === path8.resolve(dir) || isInsideDir(resolved, dir)) return true;
@@ -7394,8 +7450,8 @@ async function resolveSource(target, env = process.env) {
7394
7450
  const wantsDb = isDbTarget(target) || dbExists(data) && targetIndicatesDb(target, env);
7395
7451
  if (wantsDb) {
7396
7452
  const explicit = isDbTarget(target) ? splitDbTarget(target) : { dbPath: dbPathFor(data), sessionId: void 0 };
7397
- const stem = path8.basename(target, ".json");
7398
- const sessionId = explicit.sessionId ?? (/^ses_[0-9a-zA-Z]+$/.test(stem) ? stem : void 0);
7453
+ const stem2 = path8.basename(target, ".json");
7454
+ const sessionId = explicit.sessionId ?? (/^ses_[0-9a-zA-Z]+$/.test(stem2) ? stem2 : void 0);
7399
7455
  const result = await readDbSession(path8.dirname(explicit.dbPath), sessionId);
7400
7456
  if (!("unavailable" in result)) {
7401
7457
  if (result.messages.length === 0) {
@@ -8459,8 +8515,8 @@ function safeSlug(slug) {
8459
8515
  if (slug.includes("..") || slug.length > 72) return null;
8460
8516
  return slug;
8461
8517
  }
8462
- function isRecordStem(stem) {
8463
- return safeSlug(stem) !== null && SlugSchema.safeParse(stem).success;
8518
+ function isRecordStem(stem2) {
8519
+ return safeSlug(stem2) !== null && SlugSchema.safeParse(stem2).success;
8464
8520
  }
8465
8521
  function readStoreRecords(dir) {
8466
8522
  if (!fs12.existsSync(dir)) return { records: [], unreadable: 0 };
@@ -8520,9 +8576,9 @@ function createFileStore(options) {
8520
8576
  async get(slug, getOptions = {}) {
8521
8577
  const filePath = fileFor(slug);
8522
8578
  if (filePath === null || !fs12.existsSync(filePath)) return { ok: false, reason: "not-found" };
8523
- const read = readRecord(filePath);
8524
- if (!read.ok) return { ok: false, reason: "unreadable", problem: read.problem };
8525
- const record = read.record;
8579
+ const read2 = readRecord(filePath);
8580
+ if (!read2.ok) return { ok: false, reason: "unreadable", problem: read2.problem };
8581
+ const record = read2.record;
8526
8582
  const hasManageToken = manageTokenMatches(getOptions.manageToken, record.manageTokenHash);
8527
8583
  const viewer = { ...getOptions.viewer, hasManageToken };
8528
8584
  const allowed = canRead(record, viewer);
@@ -8534,10 +8590,10 @@ function createFileStore(options) {
8534
8590
  const summaries = [];
8535
8591
  for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
8536
8592
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
8537
- const read = readRecord(path12.join(dir, entry.name));
8538
- if (!read.ok) continue;
8539
- if (!canList(read.record)) continue;
8540
- summaries.push(summarize(read.record));
8593
+ const read2 = readRecord(path12.join(dir, entry.name));
8594
+ if (!read2.ok) continue;
8595
+ if (!canList(read2.record)) continue;
8596
+ summaries.push(summarize(read2.record));
8541
8597
  }
8542
8598
  summaries.sort((a, b) => a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0);
8543
8599
  return summaries;
@@ -8641,6 +8697,31 @@ function readGitLineage(cwd, options = {}) {
8641
8697
  gitCapturedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
8642
8698
  };
8643
8699
  }
8700
+ function zList(output) {
8701
+ return output === null ? null : output.split("\0").filter((entry) => entry.length > 0);
8702
+ }
8703
+ function bindCommit(cwd, rev, options = {}) {
8704
+ const base = readGitLineage(cwd, options);
8705
+ if (base === null || cwd === void 0) {
8706
+ return { problem: `the session's directory (${cwd ?? "not recorded"}) is not a git work tree on this machine` };
8707
+ }
8708
+ const sha = git(cwd, ["rev-parse", "--verify", "-q", `${rev}^{commit}`], options)?.trim();
8709
+ if (sha === void 0 || !/^[0-9a-f]{7,64}$/.test(sha)) return { problem: `"${rev}" is not a commit in ${cwd}` };
8710
+ if (sha === base.headCommitSha) return { ...base, commitNamed: true };
8711
+ const tracked = zList(git(cwd, ["diff", "--name-only", "-z", sha, "--"], options));
8712
+ const untracked = zList(git(cwd, ["ls-files", "--others", "--exclude-standard", "-z"], options));
8713
+ if (tracked === null || untracked === null) return { problem: `git could not compare the working tree with ${sha}` };
8714
+ const dirty = [.../* @__PURE__ */ new Set([...tracked, ...untracked])];
8715
+ const kept = dirty.slice(0, GIT_LIMITS.dirtyFiles);
8716
+ const { dirtyFilesOmitted: _omitted, ...rest } = base;
8717
+ return {
8718
+ ...rest,
8719
+ headCommitSha: sha,
8720
+ dirtyFiles: kept,
8721
+ ...dirty.length > kept.length ? { dirtyFilesOmitted: dirty.length - kept.length } : {},
8722
+ commitNamed: true
8723
+ };
8724
+ }
8644
8725
  function checkPrUrl(value) {
8645
8726
  let parsed;
8646
8727
  try {
@@ -8656,12 +8737,14 @@ function checkPrUrl(value) {
8656
8737
  function withLineage(trace, lineage, prUrl) {
8657
8738
  if (lineage === null && prUrl === void 0) return trace;
8658
8739
  const source = { ...trace.source };
8659
- const elsewhere = lineage !== null && source.gitBranch !== void 0 && lineage.gitBranch !== source.gitBranch;
8740
+ const elsewhere = lineage !== null && lineage.commitNamed !== true && source.gitBranch !== void 0 && lineage.gitBranch !== source.gitBranch;
8660
8741
  if (lineage !== null && !elsewhere) {
8661
8742
  if (source.gitBranch === void 0 && lineage.gitBranch !== void 0) source.gitBranch = lineage.gitBranch;
8662
8743
  if (lineage.headCommitSha !== void 0) source.headCommitSha = lineage.headCommitSha;
8663
8744
  source.dirtyFiles = lineage.dirtyFiles;
8664
8745
  if (lineage.dirtyFilesOmitted !== void 0) source.dirtyFilesOmitted = lineage.dirtyFilesOmitted;
8746
+ else delete source.dirtyFilesOmitted;
8747
+ if (lineage.commitNamed === true) source.commitNamed = true;
8665
8748
  source.gitCapturedAt = lineage.gitCapturedAt;
8666
8749
  }
8667
8750
  if (prUrl !== void 0) source.prUrl = prUrl;
@@ -9549,6 +9632,8 @@ aside .note{color:var(--dim);font-size:12px;margin:0 0 12px}
9549
9632
  .keys b{color:var(--fg);font-weight:600}
9550
9633
  .row .path{color:#5c6474;font-size:10px;word-break:break-all}
9551
9634
  fieldset{border:1px solid var(--line);border-radius:6px;margin:0 0 12px;padding:8px 10px}
9635
+ input.title{box-sizing:border-box;width:100%;font:inherit;padding:6px 8px;margin:4px 0 6px;border:1px solid var(--line);border-radius:5px;background:var(--bg);color:var(--fg)}
9636
+ input.title:focus{outline:none;border-color:var(--acc)}
9552
9637
  legend{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
9553
9638
  label.vis{display:block;padding:3px 0}
9554
9639
  .warn{color:var(--warn);font-size:11px;margin:4px 0 0}
@@ -9594,6 +9679,9 @@ var SCRIPT = `
9594
9679
  if (e.metaKey || e.ctrlKey || e.altKey) return;
9595
9680
  var tag = e.target && e.target.tagName;
9596
9681
  var key = e.key;
9682
+ // TYPING IS NOT TRIAGE: j, k and space in the title field are letters. Enter submits the form
9683
+ // the way any text field does, through the first submit button - Confirm.
9684
+ if (tag === 'INPUT' && e.target.type === 'text') return;
9597
9685
  if (key === 'j' || key === 'ArrowDown') { e.preventDefault(); focusRow(cursor + 1); return; }
9598
9686
  if (key === 'k' || key === 'ArrowUp') { e.preventDefault(); focusRow(cursor - 1); return; }
9599
9687
  if (key === ' ' && cursor >= 0 && tag !== 'BUTTON') {
@@ -9724,6 +9812,11 @@ function renderReviewPage(input) {
9724
9812
  Every match is redacted by default. Tick a row to KEEP the original text instead.</p>
9725
9813
  <div class="tools"><button id="keep-none" type="button">redact all</button><button id="keep-all" type="button">keep all</button></div>
9726
9814
  <p class="keys"><b>j</b>/<b>k</b> move &middot; <b>space</b> keep or redact &middot; <b>enter</b> confirm</p>
9815
+ ${input.titleDraft === void 0 ? "" : `<fieldset>
9816
+ <legend><label for="title">Title</label></legend>
9817
+ <input class="title" id="title" name="title" type="text" required maxlength="${LIMITS.title}" value="${escapeHtml(input.titleDraft)}">
9818
+ <p class="note">Change it here, or leave it. A secret typed into it is redacted.</p>
9819
+ </fieldset>`}
9727
9820
  <fieldset>
9728
9821
  <legend>Visibility</legend>
9729
9822
  <label class="vis"><input type="radio" name="visibility" value="private" checked> private</label>
@@ -9777,7 +9870,9 @@ async function readBody(req, limitBytes = 2 * 1024 * 1024) {
9777
9870
  function parseDecisionBody(body) {
9778
9871
  const form = new URLSearchParams(body);
9779
9872
  const parsedVisibility = VisibilitySchema.safeParse(form.get("visibility"));
9873
+ const title = form.get("title");
9780
9874
  return {
9875
+ ...title !== null ? { title: normalizeTitle(title) } : {},
9781
9876
  visibility: parsedVisibility.success ? parsedVisibility.data : "private",
9782
9877
  // ONE CHECKBOX MAY DECIDE MANY MATCHES. A grouped row carries every id it governs,
9783
9878
  // space-separated, so the browser sends one value and the caller still gets the flat
@@ -9852,8 +9947,24 @@ async function startReviewServer(options) {
9852
9947
  void (async () => {
9853
9948
  try {
9854
9949
  const body = await readBody(req);
9855
- const { visibility, keepIds } = parseDecisionBody(body);
9856
- const confirmed = { action: "confirm", visibility, keepIds };
9950
+ const { visibility, keepIds, title } = parseDecisionBody(body);
9951
+ if (title === null) {
9952
+ send(
9953
+ 400,
9954
+ page(
9955
+ "Flatbread - title refused",
9956
+ `<div class="done"><h1>That title cannot be used</h1><p class="meta">A title is 1-${LIMITS.title} characters. Go back, change it, and confirm again. Nothing was written.</p></div>`
9957
+ )
9958
+ );
9959
+ return;
9960
+ }
9961
+ const changed = title !== void 0 && options.titleDraft !== void 0 && title !== normalizeTitle(options.titleDraft);
9962
+ const confirmed = {
9963
+ action: "confirm",
9964
+ visibility,
9965
+ keepIds,
9966
+ ...changed ? { title } : {}
9967
+ };
9857
9968
  const message = options.onConfirm ? await options.onConfirm(confirmed) : "";
9858
9969
  send(200, renderDonePage("Share draft written", message, options.publishTo));
9859
9970
  finish(confirmed);
@@ -9890,6 +10001,16 @@ async function startReviewServer(options) {
9890
10001
  }
9891
10002
 
9892
10003
  // src/share.ts
10004
+ var spanKey = (match) => `${match.fieldPath}\0${match.start}\0${match.end}\0${match.original}`;
10005
+ function applyReview(trace, matches, keepIds, title) {
10006
+ if (title === void 0 || title === trace.title) return applyRedactions(trace, matches, { keepIds });
10007
+ const retitled = { ...trace, title };
10008
+ const kept = new Set(keepIds);
10009
+ const keptSpans = new Set(matches.filter((m) => kept.has(m.id) && m.fieldPath !== "title").map(spanKey));
10010
+ const rescanned = scanTrace(retitled).matches;
10011
+ const keep = rescanned.filter((m) => m.fieldPath !== "title" && keptSpans.has(spanKey(m))).map((m) => m.id);
10012
+ return applyRedactions(retitled, rescanned, { keepIds: keep });
10013
+ }
9893
10014
  function openInBrowser(url) {
9894
10015
  try {
9895
10016
  if (process.platform === "win32") {
@@ -10064,17 +10185,22 @@ async function runShare(options) {
10064
10185
  if (parsed.humanTest) console.error(` next step HUMAN-TESTS.md section ${parsed.humanTest}`);
10065
10186
  return { exitCode: 1, status: "not-found" };
10066
10187
  }
10067
- const trace = withLineage(
10068
- parsed.trace,
10069
- (options.readLineage ?? ((cwd) => readGitLineage(cwd)))(parsed.trace.source.cwd),
10070
- options.prUrl
10071
- );
10072
- for (const note of parsed.notes) log(`note: ${note}`);
10073
- if (options.parseOutputPath) {
10074
- FlatbreadTraceSchema.parse(trace);
10075
- fs16.mkdirSync(path15.dirname(options.parseOutputPath), { recursive: true });
10076
- fs16.writeFileSync(options.parseOutputPath, JSON.stringify(trace, null, 2) + "\n", "utf8");
10188
+ const cwd = parsed.trace.source.cwd;
10189
+ let lineage;
10190
+ if (options.commit !== void 0) {
10191
+ const bound = (options.bindCommit ?? ((dir, rev) => bindCommit(dir, rev)))(cwd, options.commit);
10192
+ if ("problem" in bound) {
10193
+ console.error(`--commit ${options.commit}: ${bound.problem}`);
10194
+ console.error("Nothing was written. Run it from the session's repository, or leave --commit off.");
10195
+ return { exitCode: 1, status: "bad-commit" };
10196
+ }
10197
+ lineage = bound;
10198
+ } else {
10199
+ lineage = (options.readLineage ?? ((dir) => readGitLineage(dir)))(cwd);
10077
10200
  }
10201
+ const lined = withLineage(parsed.trace, lineage, options.prUrl);
10202
+ const trace = options.title !== void 0 ? FlatbreadTraceSchema.parse({ ...lined, title: options.title }) : lined;
10203
+ for (const note of parsed.notes) log(`note: ${note}`);
10078
10204
  const scan = scanTrace(trace);
10079
10205
  const byRule = /* @__PURE__ */ new Map();
10080
10206
  for (const match of scan.matches) byRule.set(match.rule, (byRule.get(match.rule) ?? 0) + 1);
@@ -10101,11 +10227,12 @@ async function runShare(options) {
10101
10227
  slots: scan.slots,
10102
10228
  sessionPath: target,
10103
10229
  draftPath: options.outputPath,
10230
+ titleDraft: previewTitle,
10104
10231
  ...isPublisher(reviewPublisher) ? { publishTo: reviewPublisher.description } : {},
10105
10232
  ...options.ports ? { ports: options.ports } : {},
10106
10233
  ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
10107
10234
  onConfirm: (decision2) => {
10108
- const applied2 = applyRedactions(trace, scan.matches, { keepIds: decision2.keepIds });
10235
+ const applied2 = applyReview(trace, scan.matches, decision2.keepIds, decision2.title);
10109
10236
  const draft2 = buildDraft(applied2.trace, applied2.report, decision2.visibility);
10110
10237
  writeDraft(options.outputPath, draft2);
10111
10238
  written = draft2;
@@ -10385,11 +10512,160 @@ function describeReadFailure(failure, what) {
10385
10512
 
10386
10513
  // src/mcp.ts
10387
10514
  import readline3 from "node:readline";
10515
+
10516
+ // src/bucket.ts
10517
+ import { createHash as createHash2 } from "node:crypto";
10518
+ import fs18 from "node:fs";
10519
+ import path17 from "node:path";
10520
+ var BUCKET_INDEX_VERSION = 1;
10521
+ var BUCKET_INDEX_FILE = "index.json";
10522
+ var CAPTURE_LOG_FILE = "capture.log";
10523
+ var COMMIT_SESSION_WINDOW_MS = 2 * 60 * 60 * 1e3;
10524
+ function bucketDir(env = process.env) {
10525
+ return path17.join(flatbreadHome(env), "bucket");
10526
+ }
10527
+ function bucketSlug(harness, sessionKey) {
10528
+ return `local-${createHash2("sha256").update(`${harness}\0${sessionKey}`, "utf8").digest("hex").slice(0, 12)}`;
10529
+ }
10530
+ function rowOf(record) {
10531
+ const sha = record.trace.source.headCommitSha;
10532
+ return {
10533
+ summary: summarize(record),
10534
+ sessionPath: record.trace.source.sourcePath,
10535
+ capturedAt: record.updatedAt,
10536
+ ...record.trace.source.commitNamed === true && sha !== void 0 ? { commit: sha } : {}
10537
+ };
10538
+ }
10539
+ function recordStems(dir) {
10540
+ return fs18.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -".json".length)).filter((stem2) => SlugSchema.safeParse(stem2).success).sort();
10541
+ }
10542
+ function writeAtomic(file, body) {
10543
+ fs18.mkdirSync(path17.dirname(file), { recursive: true });
10544
+ const temp = `${file}.${process.pid}.tmp`;
10545
+ fs18.writeFileSync(temp, body, "utf8");
10546
+ fs18.renameSync(temp, file);
10547
+ }
10548
+ var newestFirst = (rows) => [...rows].sort((a, b) => a.capturedAt < b.capturedAt ? 1 : a.capturedAt > b.capturedAt ? -1 : a.summary.slug < b.summary.slug ? -1 : 1);
10549
+ function readIndex(dir) {
10550
+ try {
10551
+ const parsed = JSON.parse(fs18.readFileSync(path17.join(dir, BUCKET_INDEX_FILE), "utf8"));
10552
+ if (typeof parsed !== "object" || parsed === null) return null;
10553
+ const { version, rows } = parsed;
10554
+ if (version !== BUCKET_INDEX_VERSION || !Array.isArray(rows)) return null;
10555
+ const out = [];
10556
+ for (const row of rows) {
10557
+ if (typeof row !== "object" || row === null) return null;
10558
+ const { summary, sessionPath, capturedAt, commit } = row;
10559
+ const parsedSummary = RecordSummarySchema.safeParse(summary);
10560
+ if (!parsedSummary.success || typeof sessionPath !== "string" || typeof capturedAt !== "string") return null;
10561
+ if (commit !== void 0 && typeof commit !== "string") return null;
10562
+ out.push({ summary: parsedSummary.data, sessionPath, capturedAt, ...commit !== void 0 ? { commit } : {} });
10563
+ }
10564
+ return out;
10565
+ } catch {
10566
+ return null;
10567
+ }
10568
+ }
10569
+ function writeIndex(dir, rows) {
10570
+ writeAtomic(path17.join(dir, BUCKET_INDEX_FILE), JSON.stringify({ version: BUCKET_INDEX_VERSION, rows: newestFirst(rows) }, null, 2) + "\n");
10571
+ }
10572
+ function readBucket(dir) {
10573
+ if (!fs18.existsSync(dir)) return { rows: [], rebuilt: false, unreadable: 0 };
10574
+ const stems = recordStems(dir);
10575
+ const indexed = readIndex(dir);
10576
+ if (indexed !== null && JSON.stringify(indexed.map((row) => row.summary.slug).sort()) === JSON.stringify(stems)) {
10577
+ return { rows: newestFirst(indexed), rebuilt: false, unreadable: 0 };
10578
+ }
10579
+ const { records, unreadable } = readStoreRecords(dir);
10580
+ const rows = newestFirst(records.map(rowOf));
10581
+ try {
10582
+ writeIndex(dir, rows);
10583
+ } catch {
10584
+ }
10585
+ return { rows, rebuilt: true, unreadable };
10586
+ }
10587
+ function readBucketRecord(dir, slug) {
10588
+ if (!SlugSchema.safeParse(slug).success) return null;
10589
+ try {
10590
+ const parsed = TraceRecordSchema.safeParse(JSON.parse(fs18.readFileSync(path17.join(dir, `${slug}.json`), "utf8")));
10591
+ return parsed.success ? parsed.data : null;
10592
+ } catch {
10593
+ return null;
10594
+ }
10595
+ }
10596
+ function matchesQuery(summary, query) {
10597
+ if (query.harness !== void 0 && summary.harness !== query.harness) return false;
10598
+ if (query.category !== void 0 && summary.category !== query.category) return false;
10599
+ if (query.onlyWithRecoveries === true && summary.failuresRecovered === 0) return false;
10600
+ return matchesSearch(summary, query.query ?? "");
10601
+ }
10602
+ async function captureToBucket(options) {
10603
+ const dir = options.dir ?? bucketDir(options.env);
10604
+ const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
10605
+ const parsed = await parseWithHarness({ filePath: options.file, ...options.harness !== void 0 ? { harness: options.harness } : {} });
10606
+ if (!parsed.ok) return { status: "refused", reason: `could not parse ${options.file}: ${parsed.reason}` };
10607
+ const cwd = parsed.trace.source.cwd;
10608
+ let lineage;
10609
+ if (options.commit !== void 0) {
10610
+ const bound = (options.bindCommit ?? ((at, rev) => bindCommit(at, rev)))(cwd, options.commit);
10611
+ if ("problem" in bound) return { status: "refused", reason: `--commit ${options.commit}: ${bound.problem}` };
10612
+ lineage = bound;
10613
+ } else {
10614
+ lineage = (options.readLineage ?? ((at) => readGitLineage(at)))(cwd);
10615
+ }
10616
+ const trace = withLineage(parsed.trace, lineage);
10617
+ const scan = scanTrace(trace);
10618
+ const applied = applyRedactions(trace, scan.matches, { keepIds: [] });
10619
+ const sessionKey = trace.source.sessionId.length > 0 ? trace.source.sessionId : path17.resolve(options.file);
10620
+ const slug = bucketSlug(parsed.harness, sessionKey);
10621
+ const recordPath = path17.join(dir, `${slug}.json`);
10622
+ const previous = readBucketRecord(dir, slug);
10623
+ const commit = applied.trace.source.commitNamed === true ? applied.trace.source.headCommitSha : void 0;
10624
+ const draft = ShareDraftSchema.parse({
10625
+ schemaVersion: 1,
10626
+ createdAt: previous?.createdAt ?? now,
10627
+ trace: { ...applied.trace, visibility: "private" },
10628
+ redaction: applied.report,
10629
+ uploaded: false
10630
+ });
10631
+ const done = { slug, path: recordPath, steps: draft.trace.steps.length, redacted: draft.redaction.redactedCount, ...commit !== void 0 ? { commit } : {} };
10632
+ if (previous !== null && previous.trace.steps.length === draft.trace.steps.length && previous.trace.source.headCommitSha === draft.trace.source.headCommitSha && previous.trace.source.commitNamed === draft.trace.source.commitNamed) {
10633
+ return { status: "unchanged", ...done };
10634
+ }
10635
+ const record = buildRecord(draft, { slug, manageTokenHash: hashManageToken(mintManageToken()), now, origin: "local", visibility: "private" });
10636
+ writeAtomic(recordPath, JSON.stringify(record, null, 2) + "\n");
10637
+ const rows = readBucket(dir).rows.filter((row) => row.summary.slug !== slug);
10638
+ writeIndex(dir, [...rows, rowOf(record)]);
10639
+ return { status: "captured", ...done };
10640
+ }
10641
+ function sessionForRepo(repo, options = {}) {
10642
+ const projects = getProjectsDir(options.env ?? process.env);
10643
+ const folder = path17.join(projects, encodeCwd(path17.resolve(repo)));
10644
+ const newest = listSessionFiles(folder)[0];
10645
+ if (newest === void 0) return { problem: `no Claude Code session recorded for ${repo}` };
10646
+ const age = (options.nowMs ?? Date.now()) - newest.mtimeMs;
10647
+ const window = options.windowMs ?? COMMIT_SESSION_WINDOW_MS;
10648
+ if (age > window) {
10649
+ return { problem: `the newest session for ${repo} last moved ${Math.round(age / 6e4)} min ago, outside the ${Math.round(window / 6e4)} min window` };
10650
+ }
10651
+ return { file: newest.filePath };
10652
+ }
10653
+ function logCapture(dir, line, now = (/* @__PURE__ */ new Date()).toISOString()) {
10654
+ try {
10655
+ fs18.mkdirSync(dir, { recursive: true });
10656
+ fs18.appendFileSync(path17.join(dir, CAPTURE_LOG_FILE), `${now} ${line.replace(/[\r\n]+/g, " ")}
10657
+ `, "utf8");
10658
+ } catch {
10659
+ }
10660
+ }
10661
+
10662
+ // src/mcp.ts
10388
10663
  var MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
10389
- var MCP_SERVER_VERSION = "0.1.0";
10390
- function defaultMcpDeps(storeDir2) {
10664
+ var MCP_SERVER_VERSION = "0.2.0";
10665
+ function defaultMcpDeps(storeDir2, bucketDir2) {
10391
10666
  return {
10392
10667
  storeDir: storeDir2,
10668
+ bucketDir: bucketDir2,
10393
10669
  search: (query) => searchRemote(query, { client: "mcp" }),
10394
10670
  capsule: (ref) => fetchCapsule(ref, { client: "mcp" })
10395
10671
  };
@@ -10434,12 +10710,6 @@ function hitLine(hit) {
10434
10710
  if (hit.visibility !== "public") parts.push(hit.visibility);
10435
10711
  return `- ${parts.join(" \xB7 ")}`;
10436
10712
  }
10437
- function matchesLocal(summary, query) {
10438
- if (query.harness !== void 0 && summary.harness !== query.harness) return false;
10439
- if (query.category !== void 0 && summary.category !== query.category) return false;
10440
- if (query.onlyWithRecoveries === true && summary.failuresRecovered === 0) return false;
10441
- return matchesSearch(summary, query.query ?? "");
10442
- }
10443
10713
  function readQuery(args) {
10444
10714
  const query = {};
10445
10715
  if (args["query"] !== void 0) {
@@ -10467,29 +10737,52 @@ function readQuery(args) {
10467
10737
  function localRecords(deps) {
10468
10738
  return deps.storeDir === null ? { records: [], unreadable: 0 } : readStoreRecords(deps.storeDir);
10469
10739
  }
10470
- async function searchTool(args, deps) {
10471
- const query = readQuery(args);
10472
- if (typeof query === "string") return text(query, true);
10740
+ var runs = (count) => `${count} matching run${count === 1 ? "" : "s"}`;
10741
+ function capturedLine(row) {
10742
+ return `${hitLine(row.summary)} \xB7 captured ${row.capturedAt.slice(0, 10)}${row.commit !== void 0 ? ` at ${row.commit.slice(0, 12)}` : ""}`;
10743
+ }
10744
+ async function searchEverywhere(query, deps, options = {}) {
10473
10745
  const out = [];
10474
10746
  const local = localRecords(deps);
10475
- const localHits = local.records.map((record) => summarize(record)).filter((summary) => matchesLocal(summary, query));
10747
+ const localHits = local.records.map((record) => summarize(record)).filter((summary) => matchesQuery(summary, query));
10476
10748
  if (deps.storeDir !== null) {
10477
- out.push(`This machine (${localHits.length} matching run${localHits.length === 1 ? "" : "s"}):`);
10749
+ out.push(`This machine (${runs(localHits.length)}):`);
10478
10750
  for (const hit of localHits) out.push(hitLine(hit));
10479
10751
  if (local.unreadable > 0) out.push(`(${local.unreadable} local record file${local.unreadable === 1 ? "" : "s"} could not be read)`);
10480
10752
  }
10753
+ const seen = new Set(localHits.map((hit) => hit.slug));
10754
+ if (deps.bucketDir !== null) {
10755
+ const bucket = readBucket(deps.bucketDir);
10756
+ const captured = bucket.rows.filter((row) => !seen.has(row.summary.slug) && matchesQuery(row.summary, query));
10757
+ if (out.length > 0) out.push("");
10758
+ out.push(`Captured on this machine, not shared (${runs(captured.length)}):`);
10759
+ for (const row of captured) {
10760
+ out.push(capturedLine(row));
10761
+ seen.add(row.summary.slug);
10762
+ }
10763
+ if (bucket.unreadable > 0) out.push(`(${bucket.unreadable} captured run file${bucket.unreadable === 1 ? "" : "s"} could not be read)`);
10764
+ }
10765
+ if (options.remote === false) {
10766
+ if (out.length > 0) out.push("");
10767
+ out.push("The site was not searched: --local");
10768
+ return { lines: out, siteSearched: false };
10769
+ }
10481
10770
  const remote = await deps.search(query);
10482
10771
  if (out.length > 0) out.push("");
10483
10772
  if (remote.ok) {
10484
- const seen = new Set(localHits.map((hit) => hit.slug));
10485
10773
  const fresh = remote.results.filter((hit) => !seen.has(hit.slug));
10486
- out.push(`${remote.site} (${fresh.length} matching run${fresh.length === 1 ? "" : "s"}):`);
10774
+ out.push(`${remote.site} (${runs(fresh.length)}):`);
10487
10775
  for (const hit of fresh) out.push(hitLine(hit));
10488
10776
  } else {
10489
10777
  out.push(`The site was not searched: ${describeReadFailure(remote, "search")}`);
10490
10778
  }
10491
- out.push("", "Pass a run ID to flatbread_get_capsule for what that run learned.");
10492
- return text(out.join("\n"));
10779
+ return { lines: out, siteSearched: remote.ok };
10780
+ }
10781
+ async function searchTool(args, deps) {
10782
+ const query = readQuery(args);
10783
+ if (typeof query === "string") return text(query, true);
10784
+ const { lines: lines2 } = await searchEverywhere(query, deps);
10785
+ return text([...lines2, "", "Pass a run ID to flatbread_get_capsule for what that run learned."].join("\n"));
10493
10786
  }
10494
10787
  async function capsuleTool(args, deps) {
10495
10788
  const raw = args["slug"];
@@ -10497,8 +10790,8 @@ async function capsuleTool(args, deps) {
10497
10790
  const ref = parseRunRef(raw);
10498
10791
  if (ref === null) return text(`"${raw}" is not a run ID or a /r/<id> link`, true);
10499
10792
  if (ref.site === void 0) {
10500
- const local = localRecords(deps).records.find((record) => record.slug === ref.slug);
10501
- if (local !== void 0) return text(buildCapsule(local.trace));
10793
+ const local = localRecords(deps).records.find((record) => record.slug === ref.slug) ?? (deps.bucketDir !== null ? readBucketRecord(deps.bucketDir, ref.slug) : null);
10794
+ if (local !== null && local !== void 0) return text(buildCapsule(local.trace));
10502
10795
  }
10503
10796
  const remote = await deps.capsule(ref);
10504
10797
  if (remote.ok) return text(remote.capsule);
@@ -10577,10 +10870,126 @@ async function runMcpServer(deps, io) {
10577
10870
  return 0;
10578
10871
  }
10579
10872
 
10873
+ // src/git-hook.ts
10874
+ import fs19 from "node:fs";
10875
+ import path18 from "node:path";
10876
+ var POST_COMMIT_MARKER = "# flatbread post-commit hook - remove with: flatbread setup --uninstall";
10877
+ var CHAINED_HOOK = "post-commit.pre-flatbread";
10878
+ var sq = (value) => `'${value.replace(/\\/g, "/").replace(/'/g, "'\\''")}'`;
10879
+ function captureLine(execPath, entryPath) {
10880
+ return `${sq(execPath)} ${sq(entryPath)} capture --commit "$(git rev-parse HEAD)" --repo "$(git rev-parse --show-toplevel)" --quiet >/dev/null 2>&1 &`;
10881
+ }
10882
+ function postCommitScript(execPath, entryPath) {
10883
+ return [
10884
+ "#!/bin/sh",
10885
+ POST_COMMIT_MARKER,
10886
+ "# Runs the hook that was here before, then records this commit against the newest agent session",
10887
+ "# in this repository, on this machine only. It never changes the commit; it exits with the",
10888
+ "# earlier hook's status. Off switch: FLATBREAD_HOOK_DISABLED=1",
10889
+ "status=0",
10890
+ `prior="$(dirname "$0")/${CHAINED_HOOK}"`,
10891
+ 'if [ -x "$prior" ]; then',
10892
+ ' "$prior" "$@"',
10893
+ " status=$?",
10894
+ "fi",
10895
+ 'if [ -z "$FLATBREAD_HOOK_DISABLED" ]; then',
10896
+ ` ( ${captureLine(execPath, entryPath)} )`,
10897
+ "fi",
10898
+ 'exit "$status"',
10899
+ ""
10900
+ ].join("\n");
10901
+ }
10902
+ var isOurs = (body) => body.includes(POST_COMMIT_MARKER);
10903
+ function hooksDirFor(cwd, options = {}) {
10904
+ const top = git(cwd, ["rev-parse", "--show-toplevel"], options)?.trim();
10905
+ if (top === void 0 || top.length === 0) return { ok: false, reason: "not-a-repo", detail: `${cwd} is not inside a git repository` };
10906
+ const managed = git(top, ["config", "--show-scope", "--get", "core.hooksPath"], options)?.trim();
10907
+ let machineHooksPath;
10908
+ if (managed !== void 0 && managed.length > 0) {
10909
+ const tab = managed.indexOf(" ");
10910
+ const scope = tab > 0 ? managed.slice(0, tab) : "local";
10911
+ const value = tab > 0 ? managed.slice(tab + 1) : managed;
10912
+ if (scope !== "system" && scope !== "global") {
10913
+ return { ok: false, reason: "hooks-path", detail: `core.hooksPath is set to ${value} in this repository, so another tool owns its hooks` };
10914
+ }
10915
+ machineHooksPath = { scope, path: value };
10916
+ }
10917
+ const common = git(top, ["rev-parse", "--git-common-dir"], options)?.trim();
10918
+ if (common === void 0 || common.length === 0) return { ok: false, reason: "unreadable", detail: `git could not name the git directory of ${top}` };
10919
+ return { ok: true, repo: path18.resolve(top), dir: path18.resolve(top, common, "hooks"), ...machineHooksPath !== void 0 ? { machineHooksPath } : {} };
10920
+ }
10921
+ var read = (file) => {
10922
+ try {
10923
+ return fs19.readFileSync(file, "utf8");
10924
+ } catch {
10925
+ return null;
10926
+ }
10927
+ };
10928
+ function installGitHook(dir, script, options = {}) {
10929
+ const hook = path18.join(dir, "post-commit");
10930
+ const chained = path18.join(dir, CHAINED_HOOK);
10931
+ const current = read(hook);
10932
+ const write = (lines2) => {
10933
+ if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
10934
+ fs19.mkdirSync(dir, { recursive: true });
10935
+ if (current !== null && !isOurs(current)) fs19.renameSync(hook, chained);
10936
+ fs19.writeFileSync(hook, script, { encoding: "utf8", mode: 493 });
10937
+ fs19.chmodSync(hook, 493);
10938
+ return { code: 0, lines: lines2 };
10939
+ };
10940
+ if (current === null) return write([`post-commit ${hook} (new)`]);
10941
+ if (isOurs(current)) {
10942
+ if (current === script) return { code: 0, lines: [`post-commit ${hook}`, "already installed, unchanged"] };
10943
+ return write([`post-commit ${hook}`, "updated the flatbread hook"]);
10944
+ }
10945
+ if (fs19.existsSync(chained)) {
10946
+ return {
10947
+ code: 1,
10948
+ lines: [
10949
+ `refused ${hook}`,
10950
+ `${chained} already exists, and post-commit is not ours. Neither file was touched.`,
10951
+ "Move one of them yourself, then run setup again."
10952
+ ]
10953
+ };
10954
+ }
10955
+ return write([
10956
+ `post-commit ${hook}`,
10957
+ `your post-commit was kept as ${CHAINED_HOOK} and runs first, unchanged, with its exit status`
10958
+ ]);
10959
+ }
10960
+ function uninstallGitHook(dir, options = {}) {
10961
+ const hook = path18.join(dir, "post-commit");
10962
+ const chained = path18.join(dir, CHAINED_HOOK);
10963
+ const current = read(hook);
10964
+ const hasChained = fs19.existsSync(chained);
10965
+ const done = (lines2, act) => {
10966
+ if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
10967
+ act();
10968
+ return { code: 0, lines: lines2 };
10969
+ };
10970
+ if (current !== null && !isOurs(current)) {
10971
+ return {
10972
+ code: 0,
10973
+ lines: [
10974
+ `post-commit ${hook} is not ours, left alone`,
10975
+ ...hasChained ? [`${chained} is still there; it is yours to keep or delete`] : []
10976
+ ]
10977
+ };
10978
+ }
10979
+ if (current === null && !hasChained) return { code: 0, lines: [`no flatbread post-commit in ${dir}`] };
10980
+ if (hasChained) {
10981
+ return done([`post-commit ${hook}`, `restored your post-commit from ${CHAINED_HOOK}`], () => {
10982
+ if (current !== null) fs19.rmSync(hook);
10983
+ fs19.renameSync(chained, hook);
10984
+ });
10985
+ }
10986
+ return done([`post-commit ${hook}`, "removed the flatbread hook"], () => fs19.rmSync(hook));
10987
+ }
10988
+
10580
10989
  // src/hook.ts
10581
- import { spawnSync as spawnSync2 } from "node:child_process";
10582
- import fs18 from "node:fs";
10583
- import path17 from "node:path";
10990
+ import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
10991
+ import fs20 from "node:fs";
10992
+ import path19 from "node:path";
10584
10993
  var HOOK_EVENTS = ["Stop", "SessionEnd"];
10585
10994
  var OFFER_AFTER_STOPS = 2;
10586
10995
  var MAX_TRACKED_SESSIONS = 50;
@@ -10590,14 +10999,14 @@ function emptyHookState() {
10590
10999
  return { version: HOOK_STATE_VERSION, sessions: {} };
10591
11000
  }
10592
11001
  function hookStatePath(env = process.env) {
10593
- return path17.join(flatbreadHome(env), "hook-state.json");
11002
+ return path19.join(flatbreadHome(env), "hook-state.json");
10594
11003
  }
10595
11004
  function readHookState(env = process.env) {
10596
11005
  const file = hookStatePath(env);
10597
- if (!fs18.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
11006
+ if (!fs20.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
10598
11007
  let raw;
10599
11008
  try {
10600
- raw = fs18.readFileSync(file, "utf8");
11009
+ raw = fs20.readFileSync(file, "utf8");
10601
11010
  } catch (error) {
10602
11011
  return { ok: false, reason: "unreadable", detail: error.message };
10603
11012
  }
@@ -10640,9 +11049,9 @@ function pruneHookState(state, keep = MAX_TRACKED_SESSIONS) {
10640
11049
  }
10641
11050
  function writeHookState(state, env = process.env) {
10642
11051
  const dir = flatbreadHome(env);
10643
- fs18.mkdirSync(dir, { recursive: true, mode: 448 });
11052
+ fs20.mkdirSync(dir, { recursive: true, mode: 448 });
10644
11053
  const file = hookStatePath(env);
10645
- fs18.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
11054
+ fs20.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
10646
11055
  encoding: "utf8",
10647
11056
  mode: 384
10648
11057
  });
@@ -10777,12 +11186,18 @@ function offerAfterFrom(env) {
10777
11186
  }
10778
11187
  return { value: parsed };
10779
11188
  }
11189
+ function spawnDetachedDefault(command, args) {
11190
+ const child = spawn2(command, args, { detached: true, stdio: "ignore", windowsHide: true });
11191
+ child.on("error", () => {
11192
+ });
11193
+ child.unref();
11194
+ }
10780
11195
  async function runHookNotify(io) {
10781
11196
  const env = io.env ?? process.env;
10782
11197
  const stdout = io.stdout ?? ((line) => process.stdout.write(line + "\n"));
10783
11198
  const stderr = io.stderr ?? ((line) => process.stderr.write(line + "\n"));
10784
11199
  const now = io.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
10785
- const exists = io.exists ?? ((filePath) => fs18.existsSync(filePath));
11200
+ const exists = io.exists ?? ((filePath) => fs20.existsSync(filePath));
10786
11201
  const execPath = io.execPath ?? process.execPath;
10787
11202
  const entryPath = io.entryPath ?? process.argv[1] ?? "";
10788
11203
  try {
@@ -10796,14 +11211,14 @@ async function runHookNotify(io) {
10796
11211
  stderr(`flatbread hook: NOT RETRIEVED (${parsed.detail})`);
10797
11212
  return 0;
10798
11213
  }
10799
- const read = readHookState(env);
10800
- if (!read.ok) {
10801
- stderr(`flatbread hook: NOT RETRIEVED (${read.reason}: ${read.detail}) at ${hookStatePath(env)}`);
11214
+ const read2 = readHookState(env);
11215
+ if (!read2.ok) {
11216
+ stderr(`flatbread hook: NOT RETRIEVED (${read2.reason}: ${read2.detail}) at ${hookStatePath(env)}`);
10802
11217
  return 0;
10803
11218
  }
10804
11219
  const offerAfter = offerAfterFrom(env);
10805
11220
  if (offerAfter.ignored) stderr(`flatbread hook: ${offerAfter.ignored}, using ${OFFER_AFTER_STOPS}`);
10806
- const decision = decideHook(parsed.event, read.state, {
11221
+ const decision = decideHook(parsed.event, read2.state, {
10807
11222
  now: now(),
10808
11223
  exists,
10809
11224
  offerAfter: offerAfter.value,
@@ -10811,7 +11226,7 @@ async function runHookNotify(io) {
10811
11226
  entryPath
10812
11227
  });
10813
11228
  let recorded = true;
10814
- if (decision.nextState !== read.state) {
11229
+ if (decision.nextState !== read2.state) {
10815
11230
  try {
10816
11231
  writeHookState(decision.nextState, env);
10817
11232
  } catch (error) {
@@ -10819,6 +11234,11 @@ async function runHookNotify(io) {
10819
11234
  stderr(`flatbread hook: could not record state (${error.message})`);
10820
11235
  }
10821
11236
  }
11237
+ const transcript = parsed.event.transcript_path;
11238
+ if (io.capture === true && parsed.event.hook_event_name === "SessionEnd" && transcript !== void 0 && exists(transcript)) {
11239
+ (io.spawnDetached ?? spawnDetachedDefault)(execPath, [entryPath, "capture", "--file", transcript, "--quiet"]);
11240
+ stderr("flatbread hook: capturing this session into the local bucket, in the background");
11241
+ }
10822
11242
  if (decision.speak && recorded) {
10823
11243
  stdout(JSON.stringify({ systemMessage: decision.systemMessage }));
10824
11244
  stderr(`flatbread hook: ${decision.why}`);
@@ -10839,7 +11259,7 @@ function hookHandlerFor(options) {
10839
11259
  const handler = {
10840
11260
  type: "command",
10841
11261
  command: options.execPath,
10842
- args: [options.entryPath, "hook", "notify"]
11262
+ args: [options.entryPath, "hook", "notify", ...options.capture === true && options.event === "SessionEnd" ? ["--capture"] : []]
10843
11263
  };
10844
11264
  if (options.event === "Stop") handler.timeout = STOP_TIMEOUT_SECONDS;
10845
11265
  return handler;
@@ -10934,10 +11354,10 @@ function removeHookConfig(settings) {
10934
11354
  return { settings: root, removed };
10935
11355
  }
10936
11356
  function readSettingsFile(file) {
10937
- if (!fs18.existsSync(file)) return { present: false, file };
11357
+ if (!fs20.existsSync(file)) return { present: false, file };
10938
11358
  let raw;
10939
11359
  try {
10940
- raw = fs18.readFileSync(file, "utf8");
11360
+ raw = fs20.readFileSync(file, "utf8");
10941
11361
  } catch (error) {
10942
11362
  return { present: true, file, unreadable: error.message };
10943
11363
  }
@@ -10949,19 +11369,19 @@ function readSettingsFile(file) {
10949
11369
  }
10950
11370
  }
10951
11371
  function writeSettingsFile(file, settings) {
10952
- fs18.mkdirSync(path17.dirname(file), { recursive: true });
11372
+ fs20.mkdirSync(path19.dirname(file), { recursive: true });
10953
11373
  const temp = `${file}.flatbread-${process.pid}.tmp`;
10954
- fs18.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
10955
- fs18.renameSync(temp, file);
11374
+ fs20.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
11375
+ fs20.renameSync(temp, file);
10956
11376
  }
10957
- function handlersFor(execPath, entryPath) {
11377
+ function handlersFor(execPath, entryPath, capture = false) {
10958
11378
  return {
10959
11379
  Stop: hookHandlerFor({ execPath, entryPath, event: "Stop" }),
10960
- SessionEnd: hookHandlerFor({ execPath, entryPath, event: "SessionEnd" })
11380
+ SessionEnd: hookHandlerFor({ execPath, entryPath, event: "SessionEnd", capture })
10961
11381
  };
10962
11382
  }
10963
- function handlerBlockFor(execPath, entryPath) {
10964
- const handlers = handlersFor(execPath, entryPath);
11383
+ function handlerBlockFor(execPath, entryPath, capture = false) {
11384
+ const handlers = handlersFor(execPath, entryPath, capture);
10965
11385
  const hooks = {};
10966
11386
  for (const event of HOOK_EVENTS) hooks[event] = [{ hooks: [handlers[event]] }];
10967
11387
  return JSON.stringify({ hooks }, null, 2);
@@ -10972,21 +11392,22 @@ function runHookInstall(options = {}) {
10972
11392
  const execPath = options.execPath ?? process.execPath;
10973
11393
  const entryPath = options.entryPath ?? process.argv[1] ?? "";
10974
11394
  const file = options.settingsPath ?? claudeSettingsPath(env);
10975
- const read = readSettingsFile(file);
10976
- if (read.present && "unreadable" in read) {
11395
+ const capture = options.capture === true;
11396
+ const read2 = readSettingsFile(file);
11397
+ if (read2.present && "unreadable" in read2) {
10977
11398
  log("");
10978
11399
  log(` refused ${file}`);
10979
- log(` ${read.unreadable}`);
11400
+ log(` ${read2.unreadable}`);
10980
11401
  log(" a settings file we cannot read is not one we will rewrite. Add this by hand:");
10981
11402
  log("");
10982
- for (const line of handlerBlockFor(execPath, entryPath).split("\n")) log(` ${line}`);
11403
+ for (const line of handlerBlockFor(execPath, entryPath, capture).split("\n")) log(` ${line}`);
10983
11404
  log("");
10984
11405
  return 1;
10985
11406
  }
10986
- const current = read.present ? read.parsed : {};
11407
+ const current = read2.present ? read2.parsed : {};
10987
11408
  let merged;
10988
11409
  try {
10989
- merged = mergeHookConfig(current, handlersFor(execPath, entryPath));
11410
+ merged = mergeHookConfig(current, handlersFor(execPath, entryPath, capture));
10990
11411
  } catch (error) {
10991
11412
  log("");
10992
11413
  log(` refused ${file}`);
@@ -10995,7 +11416,7 @@ function runHookInstall(options = {}) {
10995
11416
  return 1;
10996
11417
  }
10997
11418
  log("");
10998
- log(` settings ${file}${read.present ? "" : " (new)"}`);
11419
+ log(` settings ${file}${read2.present ? "" : " (new)"}`);
10999
11420
  log(` command "${execPath}" "${entryPath}" hook notify`);
11000
11421
  if (merged.changes.length === 0) {
11001
11422
  log(" already installed, unchanged");
@@ -11019,7 +11440,7 @@ function runHookInstall(options = {}) {
11019
11440
  log(" refused: nothing was written, because this command line does not work here.");
11020
11441
  log(" fix the command (or pass --no-probe if you know better), then install again:");
11021
11442
  log("");
11022
- for (const line of handlerBlockFor(execPath, entryPath).split("\n")) log(` ${line}`);
11443
+ for (const line of handlerBlockFor(execPath, entryPath, capture).split("\n")) log(` ${line}`);
11023
11444
  log("");
11024
11445
  return 1;
11025
11446
  }
@@ -11048,7 +11469,9 @@ function runHookInstall(options = {}) {
11048
11469
  log("");
11049
11470
  return 1;
11050
11471
  }
11051
- log(" wrote it. Stop offers the share once per session; SessionEnd only records the end.");
11472
+ log(
11473
+ capture ? " wrote it. Stop offers the share once per session; SessionEnd captures the run into the local bucket." : " wrote it. Stop offers the share once per session; SessionEnd only records the end."
11474
+ );
11052
11475
  log(" off switch: FLATBREAD_HOOK_DISABLED=1, or flatbread hook uninstall");
11053
11476
  log("");
11054
11477
  return 0;
@@ -11057,23 +11480,23 @@ function runHookUninstall(options = {}) {
11057
11480
  const env = options.env ?? process.env;
11058
11481
  const log = options.log ?? ((line) => console.log(line));
11059
11482
  const file = options.settingsPath ?? claudeSettingsPath(env);
11060
- const read = readSettingsFile(file);
11483
+ const read2 = readSettingsFile(file);
11061
11484
  log("");
11062
- if (!read.present) {
11485
+ if (!read2.present) {
11063
11486
  log(` nothing to remove: ${file} does not exist`);
11064
11487
  log("");
11065
11488
  return 0;
11066
11489
  }
11067
- if ("unreadable" in read) {
11490
+ if ("unreadable" in read2) {
11068
11491
  log(` refused ${file}`);
11069
- log(` ${read.unreadable}`);
11492
+ log(` ${read2.unreadable}`);
11070
11493
  log(" remove the flatbread entries by hand rather than let us rewrite a file we cannot read");
11071
11494
  log("");
11072
11495
  return 1;
11073
11496
  }
11074
11497
  let result;
11075
11498
  try {
11076
- result = removeHookConfig(read.parsed);
11499
+ result = removeHookConfig(read2.parsed);
11077
11500
  } catch (error) {
11078
11501
  log(` refused ${error instanceof Error ? error.message : String(error)}`);
11079
11502
  log("");
@@ -11084,6 +11507,13 @@ function runHookUninstall(options = {}) {
11084
11507
  log("");
11085
11508
  return 0;
11086
11509
  }
11510
+ if (options.dryRun === true) {
11511
+ log(` settings ${file}`);
11512
+ for (const line of result.removed) log(` ${line}`);
11513
+ log(" --dry-run: nothing was written");
11514
+ log("");
11515
+ return 0;
11516
+ }
11087
11517
  try {
11088
11518
  writeSettingsFile(file, result.settings);
11089
11519
  } catch (error) {
@@ -11100,8 +11530,8 @@ function runHookUninstall(options = {}) {
11100
11530
  function settingsCandidates(env, cwd) {
11101
11531
  return [
11102
11532
  { label: "user", file: claudeSettingsPath(env) },
11103
- { label: "project", file: path17.join(cwd, ".claude", "settings.json") },
11104
- { label: "local", file: path17.join(cwd, ".claude", "settings.local.json") }
11533
+ { label: "project", file: path19.join(cwd, ".claude", "settings.json") },
11534
+ { label: "local", file: path19.join(cwd, ".claude", "settings.local.json") }
11105
11535
  ];
11106
11536
  }
11107
11537
  function installedEventsIn(parsed, exists) {
@@ -11131,23 +11561,23 @@ function runHookStatus(options = {}) {
11131
11561
  const env = options.env ?? process.env;
11132
11562
  const log = options.log ?? ((line) => console.log(line));
11133
11563
  const cwd = options.cwd ?? process.cwd();
11134
- const exists = options.exists ?? ((filePath) => fs18.existsSync(filePath));
11564
+ const exists = options.exists ?? ((filePath) => fs20.existsSync(filePath));
11135
11565
  const candidates = options.settingsPath === void 0 ? settingsCandidates(env, cwd) : [{ label: "settings", file: options.settingsPath }];
11136
11566
  log("");
11137
11567
  log("flatbread hook");
11138
11568
  log("");
11139
11569
  let installed = 0;
11140
11570
  for (const candidate of candidates) {
11141
- const read2 = readSettingsFile(candidate.file);
11142
- if (!read2.present) {
11571
+ const read3 = readSettingsFile(candidate.file);
11572
+ if (!read3.present) {
11143
11573
  log(` ${candidate.label.padEnd(8)} ${candidate.file} (absent)`);
11144
11574
  continue;
11145
11575
  }
11146
- if ("unreadable" in read2) {
11147
- log(` ${candidate.label.padEnd(8)} ${candidate.file} NOT RETRIEVED (${read2.unreadable})`);
11576
+ if ("unreadable" in read3) {
11577
+ log(` ${candidate.label.padEnd(8)} ${candidate.file} NOT RETRIEVED (${read3.unreadable})`);
11148
11578
  continue;
11149
11579
  }
11150
- const found = installedEventsIn(read2.parsed, exists);
11580
+ const found = installedEventsIn(read3.parsed, exists);
11151
11581
  installed += found.events.length;
11152
11582
  const summary = found.events.length === 0 ? "(no flatbread handler)" : found.events.join(", ");
11153
11583
  log(` ${candidate.label.padEnd(8)} ${candidate.file} ${summary}`);
@@ -11171,15 +11601,15 @@ function runHookStatus(options = {}) {
11171
11601
  }
11172
11602
  }
11173
11603
  log("");
11174
- const read = readHookState(env);
11175
- if (!read.ok) {
11176
- log(` state NOT RETRIEVED (${read.reason}: ${read.detail})`);
11604
+ const read2 = readHookState(env);
11605
+ if (!read2.ok) {
11606
+ log(` state NOT RETRIEVED (${read2.reason}: ${read2.detail})`);
11177
11607
  log(` ${hookStatePath(env)}`);
11178
11608
  log("");
11179
11609
  return 1;
11180
11610
  }
11181
- const sessions = Object.entries(read.state.sessions);
11182
- log(` state ${hookStatePath(env)}${read.present ? "" : " (none yet)"}`);
11611
+ const sessions = Object.entries(read2.state.sessions);
11612
+ log(` state ${hookStatePath(env)}${read2.present ? "" : " (none yet)"}`);
11183
11613
  log(` sessions ${sessions.length}`);
11184
11614
  log(` offered ${sessions.filter(([, session]) => session.offeredAt !== void 0).length}`);
11185
11615
  const recent = [...sessions].sort((a, b) => b[1].lastSeenAt > a[1].lastSeenAt ? 1 : -1).slice(0, 5);
@@ -11251,6 +11681,74 @@ function probeHandler(handler, run = spawnSync2) {
11251
11681
  return { ok: true, detail: "ran, exited 0, printed nothing" };
11252
11682
  }
11253
11683
 
11684
+ // src/setup.ts
11685
+ function runSetup(options = {}) {
11686
+ const env = options.env ?? process.env;
11687
+ const log = options.log ?? ((line) => console.log(line));
11688
+ const execPath = options.execPath ?? process.execPath;
11689
+ const entryPath = options.entryPath ?? process.argv[1] ?? "";
11690
+ const dryRun = options.dryRun === true;
11691
+ const settings = options.settingsPath !== void 0 ? { settingsPath: options.settingsPath } : {};
11692
+ if (options.uninstall !== true && /[\\/]_npx[\\/]/.test(entryPath)) {
11693
+ log("");
11694
+ log(` refused: this flatbread runs from npx's cache (${entryPath}),`);
11695
+ log(" which npm may clear - and a hook that names a missing file fails silently. Install it, then run setup:");
11696
+ log(" npm install -g flatbread-cli");
11697
+ log(" flatbread setup");
11698
+ log("");
11699
+ return 1;
11700
+ }
11701
+ log("");
11702
+ log("Claude Code");
11703
+ const claude = options.uninstall === true ? runHookUninstall({ env, log, dryRun, ...settings }) : runHookInstall({
11704
+ env,
11705
+ log,
11706
+ execPath,
11707
+ entryPath,
11708
+ dryRun,
11709
+ capture: true,
11710
+ ...options.noProbe === true ? { noProbe: true } : {},
11711
+ ...options.spawn !== void 0 ? { spawn: options.spawn } : {},
11712
+ ...settings
11713
+ });
11714
+ let git2 = 0;
11715
+ log("git");
11716
+ if (options.noGit === true) {
11717
+ log(" --no-git: the post-commit hook was not touched");
11718
+ } else {
11719
+ const where = hooksDirFor(options.repo ?? process.cwd(), options.git !== void 0 ? { git: options.git } : {});
11720
+ if (!where.ok) {
11721
+ log(` skipped ${where.detail}`);
11722
+ if (where.reason === "hooks-path" && options.uninstall !== true) {
11723
+ log(" add this line to that post-commit yourself to capture commits:");
11724
+ log(` ${captureLine(execPath, entryPath)}`);
11725
+ } else if (where.reason === "not-a-repo" && options.uninstall !== true) {
11726
+ log(" run setup inside a repository to bind its commits to your sessions");
11727
+ }
11728
+ if (where.reason === "unreadable") git2 = 1;
11729
+ } else {
11730
+ const outcome = options.uninstall === true ? uninstallGitHook(where.dir, { dryRun }) : installGitHook(where.dir, postCommitScript(execPath, entryPath), { dryRun });
11731
+ for (const line of outcome.lines) log(` ${line}`);
11732
+ if (where.machineHooksPath !== void 0 && options.uninstall !== true && outcome.code === 0) {
11733
+ log(` note git runs ${where.machineHooksPath.path} first (core.hooksPath, ${where.machineHooksPath.scope} config).`);
11734
+ log(" This hook fires only if that one runs the repository's own post-commit.");
11735
+ log(` After your next commit, ${bucketDir(env)}/capture.log says whether it did.`);
11736
+ }
11737
+ git2 = outcome.code;
11738
+ }
11739
+ }
11740
+ log("");
11741
+ if (options.uninstall === true) {
11742
+ log(` the bucket is left alone: ${bucketDir(env)}`);
11743
+ } else {
11744
+ log(` captures go to ${bucketDir(env)}: redacted, private, never uploaded`);
11745
+ log(" find them with: flatbread search <words>");
11746
+ log(" share one with: flatbread share --file <its session>, which opens the review");
11747
+ }
11748
+ log("");
11749
+ return claude !== 0 || git2 !== 0 ? 1 : 0;
11750
+ }
11751
+
11254
11752
  // src/login-server.ts
11255
11753
  import crypto2 from "node:crypto";
11256
11754
  import http2 from "node:http";
@@ -11461,18 +11959,18 @@ async function bindFirst(server, ports) {
11461
11959
  }
11462
11960
 
11463
11961
  // src/index.ts
11464
- var PACKAGE_ROOT = path18.resolve(path18.dirname(fileURLToPath(import.meta.url)), "..");
11465
- var SOURCE_CHECKOUT = fs19.existsSync(path18.join(PACKAGE_ROOT, "src", "index.ts"));
11466
- var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ? path18.join(PACKAGE_ROOT, ".tmp") : path18.join(os7.homedir(), ".flatbread"));
11467
- var OUTPUT_PATH = path18.join(TMP_DIR, "last-parse.json");
11468
- var SHARE_OUTPUT_PATH = path18.join(TMP_DIR, "last-share.json");
11962
+ var PACKAGE_ROOT = path20.resolve(path20.dirname(fileURLToPath(import.meta.url)), "..");
11963
+ var SOURCE_CHECKOUT = fs21.existsSync(path20.join(PACKAGE_ROOT, "src", "index.ts"));
11964
+ var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ? path20.join(PACKAGE_ROOT, ".tmp") : path20.join(os7.homedir(), ".flatbread"));
11965
+ var OUTPUT_PATH = path20.join(TMP_DIR, "last-parse.json");
11966
+ var SHARE_OUTPUT_PATH = path20.join(TMP_DIR, "last-share.json");
11469
11967
  function storeDir() {
11470
- return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path18.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
11968
+ return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path20.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
11471
11969
  }
11472
11970
  var USAGE = `flatbread - registry of AI agent runs
11473
11971
 
11474
11972
  Usage:
11475
- flatbread parse --file <path> Parse a session log (harness auto-detected)
11973
+ flatbread parse --file <path> Parse a session log and print its brief. Writes nothing
11476
11974
  flatbread parse --latest Parse the newest session for a harness
11477
11975
  flatbread share Review + sanitize the newest session
11478
11976
  flatbread share --file <path> Review + sanitize a specific session
@@ -11481,6 +11979,13 @@ Usage:
11481
11979
  flatbread pull <id|url> --out <file> Write it to a file instead
11482
11980
  flatbread mcp MCP server on stdio: flatbread_search_traces and
11483
11981
  flatbread_get_capsule, for your coding agent
11982
+ flatbread search <words> Find runs: this machine's shared and captured ones, then
11983
+ the site (the same answer the MCP server gives)
11984
+ flatbread setup Auto-capture: the Claude Code hook, plus a chained git
11985
+ post-commit in this repository. Captures stay local
11986
+ flatbread setup --uninstall Remove exactly what setup added
11987
+ flatbread capture --file <path> Capture one session into the local bucket (setup's hooks
11988
+ run this; redacted, private, never uploaded)
11484
11989
  flatbread adapters List harness adapters and their confidence
11485
11990
  flatbread login Sign in through the website, the way npm does
11486
11991
  flatbread login --email <addr> Sign in by email instead (see the note below)
@@ -11496,15 +12001,40 @@ Usage:
11496
12001
  Options for parse:
11497
12002
  --harness <id> Skip detection and name the harness.
11498
12003
  ${HARNESS_IDS.join(", ")}
12004
+ --save Also write the raw (unredacted) trace where view and the
12005
+ local viewer read it
12006
+ --out <file> Write the raw trace to <file> instead
11499
12007
 
11500
12008
  Options for share:
11501
12009
  --harness <id> Skip detection and name the harness
11502
12010
  --publish Upload the run to your account (needs flatbread login)
11503
12011
  --pr <url> Link the pull request this run belongs to (https)
12012
+ --title <text> The run's title (1-${LIMITS.title} characters). The review page
12013
+ opens with it and can still change it
12014
+ --commit [sha] Bind the run to a commit (bare --commit = HEAD) and record
12015
+ whether the working tree was clean against it
11504
12016
  --no-review Redact every match, skip the browser (non-interactive)
11505
12017
  --no-open Start the review server but do not launch a browser
11506
12018
  -h, --help Show this message
11507
12019
 
12020
+ Options for search:
12021
+ --harness <id> --category <kind> Only runs from that harness, or of that kind of job
12022
+ --recovered Only runs where the agent recovered from a failure
12023
+ --local This machine only; the site is not asked
12024
+
12025
+ Options for setup:
12026
+ --dry-run Say what would be written, write nothing
12027
+ --uninstall Remove the handlers and the post-commit; restore a
12028
+ post-commit hook setup had chained
12029
+ --no-git Leave the repository's hooks alone
12030
+ --settings <path> --no-probe As for hook install
12031
+
12032
+ Options for capture:
12033
+ --commit [sha] Bind the capture to a commit (bare = HEAD)
12034
+ --repo <dir> Without --file: the newest session of that repository,
12035
+ if it moved in the last ${Math.round(COMMIT_SESSION_WINDOW_MS / 6e4)} minutes
12036
+ --quiet Log to the bucket's capture.log and always exit 0 (hooks)
12037
+
11508
12038
  Options for hook install and hook uninstall:
11509
12039
  --settings <path> Settings file to edit. Default: the user settings file
11510
12040
  (~/.claude/settings.json, or CLAUDE_CONFIG_DIR)
@@ -11519,8 +12049,8 @@ Options for adapters:
11519
12049
  Notes:
11520
12050
  parse and share record the git branch, HEAD commit and uncommitted file names when the
11521
12051
  session ran inside a git repository. Nothing else from git is read.
11522
- Parsed traces are written to ${path18.relative(process.cwd(), OUTPUT_PATH) || OUTPUT_PATH}
11523
- Share drafts are written to ${path18.relative(process.cwd(), SHARE_OUTPUT_PATH) || SHARE_OUTPUT_PATH}
12052
+ parse --save writes ${path20.relative(process.cwd(), OUTPUT_PATH) || OUTPUT_PATH}
12053
+ Share drafts are written to ${path20.relative(process.cwd(), SHARE_OUTPUT_PATH) || SHARE_OUTPUT_PATH}
11524
12054
  Every parsed run defaults to visibility "private".
11525
12055
  Nothing is uploaded unless you pass --publish, or set FLATBREAD_API_URL and
11526
12056
  FLATBREAD_API_TOKEN yourself. pull and mcp only read: public runs, your own when you
@@ -11528,6 +12058,8 @@ Notes:
11528
12058
  127.0.0.1 only. Add the MCP server to Claude Code with:
11529
12059
  claude mcp add flatbread -- npx -y flatbread-cli mcp
11530
12060
  The viewer is a separate dev server: npm run dev -w @flatbread/web
12061
+ Captured runs live in ~/.flatbread/bucket (FLATBREAD_HOOK_DISABLED=1 stops every capture).
12062
+ A capture is not a share: to publish one, run share --file on its session.
11531
12063
  The hook offers ONE LINE at the end of a turn and never uploads anything itself. Turn it
11532
12064
  off with FLATBREAD_HOOK_DISABLED=1, or change when it speaks with
11533
12065
  FLATBREAD_HOOK_OFFER_AFTER=<turns>. It records what it has seen in ~/.flatbread.
@@ -11548,6 +12080,15 @@ function parseArgv(argv) {
11548
12080
  const arg = argv[i];
11549
12081
  if (arg === "--latest") {
11550
12082
  args.latest = true;
12083
+ } else if (arg === "--save") {
12084
+ args.out = OUTPUT_PATH;
12085
+ } else if (arg === "--out") {
12086
+ const value = argv[i + 1];
12087
+ if (value === void 0 || value.startsWith("--")) throw new Error("--out needs a file path");
12088
+ args.out = path20.resolve(value);
12089
+ i += 1;
12090
+ } else if (arg !== void 0 && arg.startsWith("--out=")) {
12091
+ args.out = path20.resolve(arg.slice("--out=".length));
11551
12092
  } else if (arg === "--harness") {
11552
12093
  const value = argv[i + 1];
11553
12094
  if (value === void 0 || value.startsWith("--")) {
@@ -11643,7 +12184,9 @@ function report(result, outputPath) {
11643
12184
  lines2.push("");
11644
12185
  lines2.push(...briefLines(trace));
11645
12186
  lines2.push("");
11646
- lines2.push(` wrote ${outputPath}`);
12187
+ lines2.push(
12188
+ outputPath === void 0 ? ` wrote nothing. --save writes ${OUTPUT_PATH} for the viewer` : ` wrote ${outputPath}`
12189
+ );
11647
12190
  lines2.push("");
11648
12191
  console.log(lines2.join("\n"));
11649
12192
  }
@@ -11666,12 +12209,12 @@ async function runParse(args) {
11666
12209
  target = found[0];
11667
12210
  console.log(`latest ${harness} session: ${target}`);
11668
12211
  } else if (args.file) {
11669
- target = path18.resolve(args.file);
11670
- if (!fs19.existsSync(target)) {
12212
+ target = path20.resolve(args.file);
12213
+ if (!fs21.existsSync(target)) {
11671
12214
  console.error(`file not found: ${target}`);
11672
12215
  return 1;
11673
12216
  }
11674
- if (!fs19.statSync(target).isFile()) {
12217
+ if (!fs21.statSync(target).isFile()) {
11675
12218
  console.error(`not a file: ${target}`);
11676
12219
  return 1;
11677
12220
  }
@@ -11701,9 +12244,11 @@ async function runParse(args) {
11701
12244
  }
11702
12245
  const trace = withLineage(result.trace, readGitLineage(result.trace.source.cwd));
11703
12246
  FlatbreadTraceSchema.parse(trace);
11704
- fs19.mkdirSync(path18.dirname(OUTPUT_PATH), { recursive: true });
11705
- fs19.writeFileSync(OUTPUT_PATH, JSON.stringify(trace, null, 2) + "\n", "utf8");
11706
- report({ ...result, trace }, OUTPUT_PATH);
12247
+ if (args.out !== void 0) {
12248
+ fs21.mkdirSync(path20.dirname(args.out), { recursive: true });
12249
+ fs21.writeFileSync(args.out, JSON.stringify(trace, null, 2) + "\n", "utf8");
12250
+ }
12251
+ report({ ...result, trace }, args.out);
11707
12252
  return 0;
11708
12253
  }
11709
12254
  function parseAdaptersArgv(argv) {
@@ -11765,8 +12310,8 @@ async function runAdapters(args) {
11765
12310
  }
11766
12311
  let input = null;
11767
12312
  if (args.file !== void 0) {
11768
- const resolved = path18.resolve(args.file);
11769
- if (!fs19.existsSync(resolved)) {
12313
+ const resolved = path20.resolve(args.file);
12314
+ if (!fs21.existsSync(resolved)) {
11770
12315
  console.error(`file not found: ${resolved}`);
11771
12316
  return 1;
11772
12317
  }
@@ -11780,6 +12325,16 @@ async function runAdapters(args) {
11780
12325
  console.log("");
11781
12326
  return 0;
11782
12327
  }
12328
+ var COMMIT_REV = /^[A-Za-z0-9][A-Za-z0-9._/~^@{}-]{0,199}$/;
12329
+ function checkCommitRev(value) {
12330
+ if (!COMMIT_REV.test(value)) throw new Error(`--commit needs a commit SHA or ref, e.g. --commit HEAD (got "${value}")`);
12331
+ return value;
12332
+ }
12333
+ function checkTitle(value) {
12334
+ const title = normalizeTitle(value);
12335
+ if (title === null) throw new Error(`--title must be 1-${LIMITS.title} characters`);
12336
+ return title;
12337
+ }
11783
12338
  function parseShareArgv(argv) {
11784
12339
  const args = { noReview: false, open: true, publish: false };
11785
12340
  for (let i = 0; i < argv.length; i += 1) {
@@ -11807,6 +12362,23 @@ function parseShareArgv(argv) {
11807
12362
  i += 1;
11808
12363
  } else if (arg !== void 0 && arg.startsWith("--pr=")) {
11809
12364
  args.prUrl = checkPrUrl(arg.slice("--pr=".length));
12365
+ } else if (arg === "--title") {
12366
+ const value = argv[i + 1];
12367
+ if (value === void 0 || value.startsWith("--")) throw new Error("--title needs the run's title, in quotes");
12368
+ args.title = checkTitle(value);
12369
+ i += 1;
12370
+ } else if (arg !== void 0 && arg.startsWith("--title=")) {
12371
+ args.title = checkTitle(arg.slice("--title=".length));
12372
+ } else if (arg === "--commit") {
12373
+ const value = argv[i + 1];
12374
+ if (value === void 0 || value.startsWith("--")) {
12375
+ args.commit = "HEAD";
12376
+ } else {
12377
+ args.commit = checkCommitRev(value);
12378
+ i += 1;
12379
+ }
12380
+ } else if (arg !== void 0 && arg.startsWith("--commit=")) {
12381
+ args.commit = checkCommitRev(arg.slice("--commit=".length));
11810
12382
  } else if (arg === "--file") {
11811
12383
  const value = argv[i + 1];
11812
12384
  if (value === void 0 || value.startsWith("--")) {
@@ -11960,7 +12532,7 @@ function tryOpenBrowser(url) {
11960
12532
  const windows = process.platform === "win32";
11961
12533
  const command = windows ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
11962
12534
  const args = windows ? ["/c", "start", "", url] : [url];
11963
- const child = spawn2(command, args, { stdio: "ignore", detached: true, windowsHide: true });
12535
+ const child = spawn3(command, args, { stdio: "ignore", detached: true, windowsHide: true });
11964
12536
  child.on("error", () => {
11965
12537
  });
11966
12538
  child.unref();
@@ -12075,7 +12647,7 @@ function parseHookArgv(argv) {
12075
12647
  if (known === void 0) {
12076
12648
  throw new Error(`unknown hook subcommand "${sub}". Known: ${HOOK_SUBCOMMANDS.join(", ")}`);
12077
12649
  }
12078
- const args = { sub: known, dryRun: false, noProbe: false };
12650
+ const args = { sub: known, dryRun: false, noProbe: false, capture: false };
12079
12651
  for (let i = 0; i < rest.length; i += 1) {
12080
12652
  const flag = rest[i];
12081
12653
  if (flag === "--settings") {
@@ -12093,13 +12665,17 @@ function parseHookArgv(argv) {
12093
12665
  args.noProbe = true;
12094
12666
  continue;
12095
12667
  }
12668
+ if (flag === "--capture" && known === "notify") {
12669
+ args.capture = true;
12670
+ continue;
12671
+ }
12096
12672
  throw new Error(`unknown option for hook ${known}: ${flag}`);
12097
12673
  }
12098
12674
  if (known !== "install" && args.noProbe) {
12099
12675
  throw new Error("--no-probe only means anything to hook install");
12100
12676
  }
12101
12677
  if (known === "notify" && (args.settings !== void 0 || args.dryRun)) {
12102
- throw new Error("hook notify takes no options: Claude Code runs it with the event on stdin");
12678
+ throw new Error("hook notify takes only --capture: Claude Code runs it with the event on stdin");
12103
12679
  }
12104
12680
  if (known === "status" && args.dryRun) {
12105
12681
  throw new Error("hook status writes nothing, so --dry-run means nothing here");
@@ -12111,7 +12687,7 @@ function parseHookArgv(argv) {
12111
12687
  }
12112
12688
  async function runHookCommand(args) {
12113
12689
  if (args.sub === "notify") {
12114
- return runHookNotify({ readStdin: () => readAllStdin(), entryPath: CLI_ENTRY });
12690
+ return runHookNotify({ readStdin: () => readAllStdin(), entryPath: CLI_ENTRY, capture: args.capture });
12115
12691
  }
12116
12692
  if (args.sub === "install") {
12117
12693
  return runHookInstall({
@@ -12169,19 +12745,146 @@ async function runPull(args) {
12169
12745
  process.stdout.write(result.capsule);
12170
12746
  return 0;
12171
12747
  }
12172
- const target = path18.resolve(args.out);
12173
- fs19.mkdirSync(path18.dirname(target), { recursive: true });
12174
- fs19.writeFileSync(target, result.capsule, "utf8");
12748
+ const target = path20.resolve(args.out);
12749
+ fs21.mkdirSync(path20.dirname(target), { recursive: true });
12750
+ fs21.writeFileSync(target, result.capsule, "utf8");
12175
12751
  console.error(`wrote ${target}`);
12176
12752
  return 0;
12177
12753
  }
12754
+ function parseSearchArgv(argv) {
12755
+ const query = {};
12756
+ const words = [];
12757
+ let local = false;
12758
+ for (let i = 0; i < argv.length; i += 1) {
12759
+ const arg = argv[i];
12760
+ const valueOf = (flag) => {
12761
+ if (arg.startsWith(`${flag}=`)) return arg.slice(flag.length + 1);
12762
+ const value = argv[i + 1];
12763
+ if (value === void 0 || value.startsWith("--")) throw new Error(`${flag} needs a value`);
12764
+ i += 1;
12765
+ return value;
12766
+ };
12767
+ if (arg === "--harness" || arg.startsWith("--harness=")) {
12768
+ query.harness = toHarnessId(valueOf("--harness"));
12769
+ } else if (arg === "--category" || arg.startsWith("--category=")) {
12770
+ const value = valueOf("--category");
12771
+ if (!TASK_CATEGORIES.includes(value)) throw new Error(`unknown --category "${value}". Known: ${TASK_CATEGORIES.join(", ")}`);
12772
+ query.category = value;
12773
+ } else if (arg === "--recovered") {
12774
+ query.onlyWithRecoveries = true;
12775
+ } else if (arg === "--local") {
12776
+ local = true;
12777
+ } else if (arg.startsWith("--")) {
12778
+ throw new Error(`Unknown option for search: ${arg}`);
12779
+ } else {
12780
+ words.push(arg);
12781
+ }
12782
+ }
12783
+ if (words.length > 0) query.query = words.join(" ");
12784
+ return { query, local };
12785
+ }
12786
+ async function runSearch(argv, deps = defaultMcpDeps(storeDir(), bucketDir()), print = console.log) {
12787
+ const { query, local } = parseSearchArgv(argv);
12788
+ const { lines: lines2, siteSearched } = await searchEverywhere(query, deps, { remote: !local });
12789
+ for (const line of lines2) print(line);
12790
+ return local || siteSearched ? 0 : 1;
12791
+ }
12792
+ function parseCaptureArgv(argv) {
12793
+ const args = { quiet: false };
12794
+ for (let i = 0; i < argv.length; i += 1) {
12795
+ const arg = argv[i];
12796
+ const next = argv[i + 1];
12797
+ if (arg === "--file" || arg === "--repo" || arg === "--harness") {
12798
+ if (next === void 0 || next.startsWith("--")) throw new Error(`${arg} needs a value`);
12799
+ if (arg === "--file") args.file = next;
12800
+ else if (arg === "--repo") args.repo = next;
12801
+ else args.harness = toHarnessId(next);
12802
+ i += 1;
12803
+ } else if (arg === "--commit") {
12804
+ if (next === void 0 || next.startsWith("--")) {
12805
+ args.commit = "HEAD";
12806
+ } else {
12807
+ args.commit = checkCommitRev(next);
12808
+ i += 1;
12809
+ }
12810
+ } else if (arg.startsWith("--commit=")) {
12811
+ args.commit = checkCommitRev(arg.slice("--commit=".length));
12812
+ } else if (arg === "--quiet") {
12813
+ args.quiet = true;
12814
+ } else {
12815
+ throw new Error(`Unknown option for capture: ${arg}`);
12816
+ }
12817
+ }
12818
+ if (args.file !== void 0 && args.repo !== void 0) throw new Error("capture takes --file or --repo, not both");
12819
+ return args;
12820
+ }
12821
+ async function runCapture(args, env = process.env) {
12822
+ const dir = bucketDir(env);
12823
+ const say = (line) => args.quiet ? logCapture(dir, line) : console.log(line);
12824
+ try {
12825
+ if (args.quiet && (env["FLATBREAD_HOOK_DISABLED"] ?? "").trim() !== "") {
12826
+ say("skipped FLATBREAD_HOOK_DISABLED is set");
12827
+ return 0;
12828
+ }
12829
+ let file = args.file;
12830
+ if (file === void 0) {
12831
+ const found = sessionForRepo(args.repo ?? process.cwd(), { env });
12832
+ if ("problem" in found) {
12833
+ say(`skipped ${found.problem}`);
12834
+ return 0;
12835
+ }
12836
+ file = found.file;
12837
+ }
12838
+ const result = await captureToBucket({
12839
+ file,
12840
+ env,
12841
+ dir,
12842
+ ...args.harness !== void 0 ? { harness: args.harness } : {},
12843
+ ...args.commit !== void 0 ? { commit: args.commit } : {}
12844
+ });
12845
+ if (result.status === "refused") {
12846
+ say(`refused ${result.reason}`);
12847
+ return args.quiet ? 0 : 1;
12848
+ }
12849
+ say(
12850
+ `${result.status} ${result.slug} ${result.steps} steps, ${result.redacted} redacted` + (result.commit !== void 0 ? `, at ${result.commit.slice(0, 12)}` : "") + ` -> ${result.path}`
12851
+ );
12852
+ return 0;
12853
+ } catch (error) {
12854
+ say(`failed ${error instanceof Error ? error.message : String(error)}`);
12855
+ return args.quiet ? 0 : 1;
12856
+ }
12857
+ }
12858
+ function parseSetupArgv(argv) {
12859
+ const args = {
12860
+ dryRun: false,
12861
+ uninstall: false,
12862
+ noGit: false,
12863
+ noProbe: false
12864
+ };
12865
+ for (let i = 0; i < argv.length; i += 1) {
12866
+ const arg = argv[i];
12867
+ if (arg === "--dry-run") args.dryRun = true;
12868
+ else if (arg === "--uninstall") args.uninstall = true;
12869
+ else if (arg === "--no-git") args.noGit = true;
12870
+ else if (arg === "--no-probe") args.noProbe = true;
12871
+ else if (arg === "--settings") {
12872
+ const value = argv[i + 1];
12873
+ if (value === void 0 || value.startsWith("--")) throw new Error("--settings needs a path");
12874
+ args.settings = value;
12875
+ i += 1;
12876
+ } else throw new Error(`Unknown option for setup: ${arg}`);
12877
+ }
12878
+ if (args.uninstall && args.noProbe) throw new Error("--no-probe only means anything when installing");
12879
+ return args;
12880
+ }
12178
12881
  async function main(argv) {
12179
12882
  const [command, ...rest] = argv;
12180
12883
  if (command === void 0 || command === "--help" || command === "-h" || command === "help") {
12181
12884
  console.log(USAGE);
12182
12885
  return command === void 0 ? 1 : 0;
12183
12886
  }
12184
- if (command !== "parse" && command !== "share" && command !== "view" && command !== "adapters" && command !== "login" && command !== "logout" && command !== "whoami" && command !== "hook" && command !== "pull" && command !== "mcp") {
12887
+ if (command !== "parse" && command !== "share" && command !== "view" && command !== "adapters" && command !== "login" && command !== "logout" && command !== "whoami" && command !== "hook" && command !== "pull" && command !== "mcp" && command !== "search" && command !== "setup" && command !== "capture") {
12185
12888
  console.error(`Unknown command: ${command}`);
12186
12889
  console.error(USAGE);
12187
12890
  return 1;
@@ -12200,6 +12903,24 @@ async function main(argv) {
12200
12903
  }
12201
12904
  return runHookCommand(hookArgs);
12202
12905
  }
12906
+ if (command === "search" || command === "setup" || command === "capture") {
12907
+ try {
12908
+ if (command === "search") return await runSearch(rest);
12909
+ if (command === "capture") return await runCapture(parseCaptureArgv(rest));
12910
+ const setup = parseSetupArgv(rest);
12911
+ return runSetup({
12912
+ entryPath: CLI_ENTRY,
12913
+ dryRun: setup.dryRun,
12914
+ uninstall: setup.uninstall,
12915
+ noGit: setup.noGit,
12916
+ noProbe: setup.noProbe,
12917
+ ...setup.settings !== void 0 ? { settingsPath: setup.settings } : {}
12918
+ });
12919
+ } catch (error) {
12920
+ console.error(error instanceof Error ? error.message : String(error));
12921
+ return 1;
12922
+ }
12923
+ }
12203
12924
  if (command === "login") return runLogin(rest);
12204
12925
  if (command === "logout") return runLogout();
12205
12926
  if (command === "whoami") return runWhoami();
@@ -12218,7 +12939,7 @@ async function main(argv) {
12218
12939
  console.error(`mcp takes no options, got: ${rest.join(" ")}`);
12219
12940
  return 1;
12220
12941
  }
12221
- return runMcpServer(defaultMcpDeps(storeDir()));
12942
+ return runMcpServer(defaultMcpDeps(storeDir(), bucketDir()));
12222
12943
  }
12223
12944
  if (command === "pull") {
12224
12945
  let pullArgs;
@@ -12251,10 +12972,11 @@ async function main(argv) {
12251
12972
  noReview: shareArgs.noReview,
12252
12973
  publish: shareArgs.publish,
12253
12974
  ...shareArgs.prUrl !== void 0 ? { prUrl: shareArgs.prUrl } : {},
12975
+ ...shareArgs.title !== void 0 ? { title: shareArgs.title } : {},
12976
+ ...shareArgs.commit !== void 0 ? { commit: shareArgs.commit } : {},
12254
12977
  ...storeDir() !== null ? { storeDir: storeDir() } : {},
12255
12978
  openBrowser: shareArgs.open && !shareArgs.noReview,
12256
- outputPath: SHARE_OUTPUT_PATH,
12257
- parseOutputPath: OUTPUT_PATH
12979
+ outputPath: SHARE_OUTPUT_PATH
12258
12980
  });
12259
12981
  return result.exitCode;
12260
12982
  }
@@ -12272,9 +12994,9 @@ var invokedDirectly = (() => {
12272
12994
  if (entry === void 0) return false;
12273
12995
  const real = (file) => {
12274
12996
  try {
12275
- return fs19.realpathSync(file);
12997
+ return fs21.realpathSync(file);
12276
12998
  } catch {
12277
- return path18.resolve(file);
12999
+ return path20.resolve(file);
12278
13000
  }
12279
13001
  };
12280
13002
  return real(entry) === real(fileURLToPath(import.meta.url));
@@ -12293,8 +13015,13 @@ export {
12293
13015
  TMP_DIR,
12294
13016
  main,
12295
13017
  openableUrl,
13018
+ parseCaptureArgv,
12296
13019
  parsePullArgv,
13020
+ parseSearchArgv,
13021
+ parseSetupArgv,
13022
+ runCapture,
12297
13023
  runLogin,
12298
13024
  runLogout,
13025
+ runSearch,
12299
13026
  runWhoami
12300
13027
  };