flatbread-cli 0.1.0 → 0.3.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,11 +6,11 @@ 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";
11
- import os7 from "node:os";
12
- import path18 from "node:path";
13
- import { fileURLToPath } from "node:url";
9
+ import { spawn as spawn3 } from "node:child_process";
10
+ import fs22 from "node:fs";
11
+ import os8 from "node:os";
12
+ import path21 from "node:path";
13
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
14
14
 
15
15
  // ../../node_modules/zod/v3/external.js
16
16
  var external_exports = {};
@@ -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: path22, errorMaps, issueData } = params;
494
+ const fullPath = [...path22, ...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, path22, key) {
611
611
  this._cachedPath = [];
612
612
  this.parent = parent;
613
613
  this.data = value;
614
- this._path = path19;
614
+ this._path = path22;
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 path22 = firstString(tool.input, FILE_PATH_KEYS);
4388
+ if (path22 !== void 0) return `${tool.name} ${repoRelative(path22, 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 [path22, 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: path22, 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 path22 = repoRelative(firstString(step.tool.input, FILE_PATH_KEYS), cwd);
4717
+ const file = files.get(path22) ?? { path: path22 };
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: path22, 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(path22, 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: {
@@ -4800,11 +4810,11 @@ var HARNESS_INFO = {
4800
4810
  antigravity: {
4801
4811
  id: "antigravity",
4802
4812
  label: "Google Antigravity",
4803
- formatLabel: "unknown",
4804
- confidence: "inferred",
4805
- implemented: false,
4813
+ formatLabel: "IDE conversations/<id>.pb (Windsurf's encrypted trajectory)",
4814
+ confidence: "documented",
4815
+ implemented: true,
4806
4816
  humanTest: "A6",
4807
- note: "Detector only. The format is unpublished and unseen, so this adapter refuses rather than inventing a timeline."
4817
+ note: "The IDE only, prompts and replies only. Read the way deja-vu reads Windsurf, which held on all nine IDE 1.23.2 conversations measured. Tool calls are undocumented and not read; the standalone Antigravity app is not read yet."
4808
4818
  },
4809
4819
  terminal: {
4810
4820
  id: "terminal",
@@ -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((path22) => matchesAny(path22, 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((path22) => matchesAny(path22, 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((path22) => matchesAny(path22, 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] ?? "";
@@ -5991,50 +6047,282 @@ function buildTrace(input) {
5991
6047
  }
5992
6048
 
5993
6049
  // src/adapters/antigravity.ts
6050
+ import crypto from "node:crypto";
5994
6051
  import fs3 from "node:fs";
5995
6052
  import os2 from "node:os";
5996
6053
  import path3 from "node:path";
5997
- function candidateStateDirs(env = process.env) {
5998
- const home = os2.homedir();
5999
- const dirs = [];
6000
- if (process.platform === "win32") {
6001
- const appData = env.APPDATA ?? path3.join(home, "AppData", "Roaming");
6002
- dirs.push(path3.join(appData, "Antigravity", "User"));
6003
- dirs.push(path3.join(appData, "Google", "Antigravity", "User"));
6004
- } else if (process.platform === "darwin") {
6005
- dirs.push(path3.join(home, "Library", "Application Support", "Antigravity", "User"));
6006
- dirs.push(path3.join(home, "Library", "Application Support", "Google", "Antigravity", "User"));
6007
- } else {
6008
- dirs.push(path3.join(home, ".config", "Antigravity", "User"));
6009
- dirs.push(path3.join(home, ".config", "Google", "Antigravity", "User"));
6054
+ import { fileURLToPath } from "node:url";
6055
+ var ANTIGRAVITY_ADAPTER_VERSION = 1;
6056
+ var KEY = Buffer.from("safeCodeiumworldKeYsecretBalloon");
6057
+ var NONCE = 12;
6058
+ var TAG = 16;
6059
+ var CONVERSATION = /[\\/]\.gemini[\\/]antigravity[^\\/]*[\\/]conversations[\\/][^\\/]+\.pb$/i;
6060
+ var PB_VARINT = 0;
6061
+ var PB_FIXED64 = 1;
6062
+ var PB_BYTES = 2;
6063
+ var PB_FIXED32 = 5;
6064
+ var Malformed = class extends Error {
6065
+ };
6066
+ function varint(b, at) {
6067
+ let value = 0n;
6068
+ let shift = 0n;
6069
+ for (let i = at; i < b.length && shift <= 63n; i++) {
6070
+ const byte = b[i];
6071
+ value |= BigInt(byte & 127) << shift;
6072
+ shift += 7n;
6073
+ if ((byte & 128) === 0) return [value, i + 1];
6074
+ }
6075
+ throw new Malformed("malformed protobuf");
6076
+ }
6077
+ function pbFields(b) {
6078
+ const out = [];
6079
+ let i = 0;
6080
+ while (i < b.length) {
6081
+ const [key, afterKey] = varint(b, i);
6082
+ i = afterKey;
6083
+ const num2 = Number(key >> 3n);
6084
+ const wire = Number(key & 7n);
6085
+ if (num2 === 0) throw new Malformed("malformed protobuf");
6086
+ let value = 0n;
6087
+ let data = Buffer.alloc(0);
6088
+ if (wire === PB_VARINT) {
6089
+ [value, i] = varint(b, i);
6090
+ } else if (wire === PB_FIXED64) {
6091
+ if (i + 8 > b.length) throw new Malformed("malformed protobuf");
6092
+ value = b.readBigUInt64LE(i);
6093
+ i += 8;
6094
+ } else if (wire === PB_FIXED32) {
6095
+ if (i + 4 > b.length) throw new Malformed("malformed protobuf");
6096
+ value = BigInt(b.readUInt32LE(i));
6097
+ i += 4;
6098
+ } else if (wire === PB_BYTES) {
6099
+ const [length, afterLength] = varint(b, i);
6100
+ if (length > BigInt(b.length - afterLength)) throw new Malformed("malformed protobuf");
6101
+ data = b.subarray(afterLength, afterLength + Number(length));
6102
+ i = afterLength + Number(length);
6103
+ } else {
6104
+ throw new Malformed("malformed protobuf");
6105
+ }
6106
+ out.push({ num: num2, wire, value, data });
6107
+ }
6108
+ return out;
6109
+ }
6110
+ function bytesField(fields, num2) {
6111
+ return fields.find((f) => f.num === num2 && f.wire === PB_BYTES)?.data;
6112
+ }
6113
+ function pbString(b) {
6114
+ return b === void 0 ? "" : b.toString("utf8").replaceAll("\0", "");
6115
+ }
6116
+ function pbTimestamp(b) {
6117
+ if (b === void 0) return void 0;
6118
+ let seconds = 0n;
6119
+ let nanos = 0n;
6120
+ for (const f of pbFields(b)) {
6121
+ if (f.wire !== PB_VARINT) continue;
6122
+ if (f.num === 1) seconds = BigInt.asIntN(64, f.value);
6123
+ if (f.num === 2) nanos = BigInt.asIntN(32, f.value);
6124
+ }
6125
+ if (seconds === 0n && nanos === 0n) return void 0;
6126
+ const ms = Number(seconds) * 1e3 + Math.floor(Number(nanos) / 1e6);
6127
+ const date = new Date(ms);
6128
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
6129
+ }
6130
+ function openConversation(b) {
6131
+ if (b.length < NONCE + TAG) return null;
6132
+ try {
6133
+ const decipher = crypto.createDecipheriv("aes-256-gcm", KEY, b.subarray(0, NONCE));
6134
+ decipher.setAuthTag(b.subarray(b.length - TAG));
6135
+ return Buffer.concat([decipher.update(b.subarray(NONCE, b.length - TAG)), decipher.final()]);
6136
+ } catch {
6137
+ return null;
6138
+ }
6139
+ }
6140
+ function folderPath(uri) {
6141
+ if (!uri.startsWith("file:")) return void 0;
6142
+ try {
6143
+ return fileURLToPath(uri);
6144
+ } catch {
6145
+ try {
6146
+ return decodeURIComponent(new URL(uri).pathname) || void 0;
6147
+ } catch {
6148
+ return void 0;
6149
+ }
6150
+ }
6151
+ }
6152
+ function workspaceOf(metadata) {
6153
+ if (metadata === void 0) return void 0;
6154
+ for (const f of pbFields(metadata)) {
6155
+ if (f.num !== 1 || f.wire !== PB_BYTES) continue;
6156
+ const folder = folderPath(pbString(bytesField(pbFields(f.data), 1)));
6157
+ if (folder !== void 0) return folder;
6158
+ }
6159
+ return void 0;
6160
+ }
6161
+ function tally(counts, key) {
6162
+ counts.set(key, (counts.get(key) ?? 0) + 1);
6163
+ }
6164
+ function decodeTrajectory2(plain) {
6165
+ const events = [];
6166
+ const unread = /* @__PURE__ */ new Map();
6167
+ let sessionId;
6168
+ let cwd;
6169
+ let steps = 0;
6170
+ let clock;
6171
+ for (const f of pbFields(plain)) {
6172
+ if (f.wire === PB_BYTES && f.num === 6) {
6173
+ sessionId = pbString(f.data) || sessionId;
6174
+ continue;
6175
+ }
6176
+ if (f.wire === PB_BYTES && f.num === 7) {
6177
+ cwd ??= workspaceOf(f.data);
6178
+ clock ??= pbTimestamp(bytesField(pbFields(f.data), 2));
6179
+ continue;
6180
+ }
6181
+ if (f.wire !== PB_BYTES || f.num !== 2) {
6182
+ tally(unread, `trajectory field ${f.num}`);
6183
+ continue;
6184
+ }
6185
+ steps++;
6186
+ const step = pbFields(f.data);
6187
+ const metadata = bytesField(step, 5);
6188
+ const metaFields = metadata === void 0 ? [] : pbFields(metadata);
6189
+ clock = pbTimestamp(bytesField(metaFields, 1)) ?? clock;
6190
+ const completed = pbTimestamp(bytesField(metaFields, 8));
6191
+ for (const s of step) {
6192
+ if (s.num !== 5 && s.num !== 19 && s.num !== 20) tally(unread, `step field ${s.num}`);
6193
+ }
6194
+ const input = bytesField(step, 19);
6195
+ if (input !== void 0) {
6196
+ const fields = pbFields(input);
6197
+ const text2 = pbString(bytesField(fields, 2)).trim() || pbString(bytesField(fields, 1)).trim();
6198
+ if (text2) events.push({ kind: "prompt", ...clock ? { timestamp: clock } : {}, text: text2 });
6199
+ }
6200
+ const planner = bytesField(step, 20);
6201
+ if (planner !== void 0) {
6202
+ const fields = pbFields(planner);
6203
+ const text2 = pbString(bytesField(fields, 1)).trim() || pbString(bytesField(fields, 8)).trim();
6204
+ if (bytesField(fields, 3) !== void 0) tally(unread, "planner thinking");
6205
+ const at = completed ?? clock;
6206
+ if (text2) events.push({ kind: "text", ...at ? { timestamp: at } : {}, text: text2 });
6207
+ }
6208
+ }
6209
+ if (cwd !== void 0 || sessionId !== void 0) {
6210
+ events.unshift({ kind: "meta", ...sessionId ? { sessionId } : {}, ...cwd ? { cwd } : {} });
6211
+ }
6212
+ return { events, ...sessionId ? { sessionId } : {}, steps, unread };
6213
+ }
6214
+ function homeOf(env) {
6215
+ return (process.platform === "win32" ? env.USERPROFILE : env.HOME) || os2.homedir();
6216
+ }
6217
+ function listConversations(env = process.env) {
6218
+ const gemini = path3.join(homeOf(env), ".gemini");
6219
+ let roots;
6220
+ try {
6221
+ roots = fs3.readdirSync(gemini).filter((name) => /^antigravity/i.test(name));
6222
+ } catch {
6223
+ return [];
6224
+ }
6225
+ const found = [];
6226
+ for (const root of roots) {
6227
+ const dir = path3.join(gemini, root, "conversations");
6228
+ let names;
6229
+ try {
6230
+ names = fs3.readdirSync(dir);
6231
+ } catch {
6232
+ continue;
6233
+ }
6234
+ for (const name of names) {
6235
+ if (!name.toLowerCase().endsWith(".pb")) continue;
6236
+ const file = path3.join(dir, name);
6237
+ try {
6238
+ const stat = fs3.statSync(file);
6239
+ if (stat.isFile()) found.push({ file, mtime: stat.mtimeMs });
6240
+ } catch {
6241
+ continue;
6242
+ }
6243
+ }
6010
6244
  }
6011
- return dirs;
6245
+ return found.sort((a, b) => b.mtime - a.mtime).map((entry) => entry.file);
6012
6246
  }
6247
+ var REFUSAL_TEST = "A6";
6013
6248
  var antigravityAdapter = {
6014
6249
  id: "antigravity",
6015
- version: 0,
6250
+ version: ANTIGRAVITY_ADAPTER_VERSION,
6016
6251
  async detect(input) {
6017
- void input;
6018
- return { score: 0, reason: "Antigravity's log format is unknown; this adapter cannot claim a file" };
6252
+ if (CONVERSATION.test(input.path)) {
6253
+ return { score: 85, reason: "a conversations/<id>.pb under ~/.gemini/antigravity" };
6254
+ }
6255
+ return { score: 0, reason: "not an Antigravity conversations/<id>.pb" };
6019
6256
  },
6020
6257
  async read(input) {
6021
- void input;
6258
+ const plain = openConversation(await fs3.promises.readFile(input.path));
6259
+ if (plain === null) {
6260
+ return {
6261
+ refused: true,
6262
+ reason: `${path3.basename(input.path)} did not authenticate with the key Antigravity IDE 1.23.2 uses. A newer build may have changed the envelope; report its version via HUMAN-TESTS.md A6.`,
6263
+ humanTest: REFUSAL_TEST
6264
+ };
6265
+ }
6266
+ let trajectory;
6267
+ try {
6268
+ trajectory = decodeTrajectory2(plain);
6269
+ } catch (error) {
6270
+ if (!(error instanceof Malformed)) throw error;
6271
+ return {
6272
+ refused: true,
6273
+ reason: `${path3.basename(input.path)} authenticated but is not the trajectory protobuf this reader knows.`,
6274
+ humanTest: REFUSAL_TEST
6275
+ };
6276
+ }
6277
+ const said = trajectory.events.filter((e) => e.kind === "prompt" || e.kind === "text").length;
6278
+ if (said === 0) {
6279
+ return {
6280
+ refused: true,
6281
+ reason: `${path3.basename(input.path)} has ${trajectory.steps} step(s) and no prompt or reply in the fields this reader knows.`,
6282
+ humanTest: REFUSAL_TEST
6283
+ };
6284
+ }
6285
+ const unreadFields = [...trajectory.unread].sort((a, b) => b[1] - a[1]).map(([field, count]) => `${field} x${count}`);
6022
6286
  return {
6023
- refused: true,
6024
- reason: "Antigravity's session format has never been observed. Writing a parser from no information would produce a plausible but fictional timeline. Send a sample session (see HUMAN-TESTS.md A6).",
6025
- humanTest: "A6"
6287
+ events: trajectory.events,
6288
+ capabilities: { tokens: "unavailable", filesChanged: "unavailable" },
6289
+ sessionId: trajectory.sessionId ?? path3.basename(input.path, ".pb"),
6290
+ unreadFields,
6291
+ notes: [
6292
+ `${trajectory.steps} step(s), ${said} prompt/reply event(s)`,
6293
+ "tool calls, thinking and edits are not read: their step fields are undocumented",
6294
+ "CONFIDENCE: documented (Windsurf's schema, measured on IDE 1.23.2). See HUMAN-TESTS.md A6."
6295
+ ]
6026
6296
  };
6027
6297
  },
6028
6298
  discover(env) {
6029
- return candidateStateDirs(env).filter((dir) => fs3.existsSync(dir));
6299
+ return listConversations(env);
6030
6300
  },
6031
- async describe() {
6032
- const lines2 = ["Antigravity support is a stub. It detects an install and refuses to parse."];
6033
- for (const dir of candidateStateDirs()) {
6034
- lines2.push(` ${fs3.existsSync(dir) ? "FOUND " : "absent "} ${dir}`);
6301
+ async describe(input) {
6302
+ const files = input ? [input.path] : listConversations();
6303
+ const lines2 = [`conversations found: ${files.length}`];
6304
+ for (const file of files.slice(0, 8)) lines2.push(` ${file}`);
6305
+ const target = files[0];
6306
+ if (target === void 0) return lines2;
6307
+ lines2.push("", `inspecting ${target}`);
6308
+ let plain;
6309
+ try {
6310
+ plain = openConversation(await fs3.promises.readFile(target));
6311
+ } catch (error) {
6312
+ lines2.push(` unreadable: ${error instanceof Error ? error.message : String(error)}`);
6313
+ return lines2;
6314
+ }
6315
+ lines2.push(` authenticated: ${plain === null ? "no" : "yes"}`);
6316
+ if (plain === null) return lines2;
6317
+ try {
6318
+ const trajectory = decodeTrajectory2(plain);
6319
+ lines2.push(` steps: ${trajectory.steps}, prompt/reply events: ${trajectory.events.filter((e) => e.kind !== "meta").length}`);
6320
+ for (const [field, count] of [...trajectory.unread].sort((a, b) => b[1] - a[1]).slice(0, 15)) {
6321
+ lines2.push(` ${String(count).padStart(6)} ${field}`);
6322
+ }
6323
+ } catch {
6324
+ lines2.push(" not the trajectory protobuf this reader knows");
6035
6325
  }
6036
- lines2.push("");
6037
- lines2.push("If one is FOUND, HUMAN-TESTS.md A6 lists exactly what to report back.");
6038
6326
  return lines2;
6039
6327
  }
6040
6328
  };
@@ -7347,8 +7635,8 @@ function isInsideDir(child, parent) {
7347
7635
  }
7348
7636
  function targetIndicatesDb(target, env) {
7349
7637
  if (isDbTarget(target)) return true;
7350
- const stem = path8.basename(target, ".json");
7351
- if (/^ses_[0-9a-zA-Z]+$/.test(stem)) return true;
7638
+ const stem2 = path8.basename(target, ".json");
7639
+ if (/^ses_[0-9a-zA-Z]+$/.test(stem2)) return true;
7352
7640
  const resolved = path8.resolve(target);
7353
7641
  for (const dir of [dataDir(env), storageDir(env)]) {
7354
7642
  if (resolved === path8.resolve(dir) || isInsideDir(resolved, dir)) return true;
@@ -7394,8 +7682,8 @@ async function resolveSource(target, env = process.env) {
7394
7682
  const wantsDb = isDbTarget(target) || dbExists(data) && targetIndicatesDb(target, env);
7395
7683
  if (wantsDb) {
7396
7684
  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);
7685
+ const stem2 = path8.basename(target, ".json");
7686
+ const sessionId = explicit.sessionId ?? (/^ses_[0-9a-zA-Z]+$/.test(stem2) ? stem2 : void 0);
7399
7687
  const result = await readDbSession(path8.dirname(explicit.dbPath), sessionId);
7400
7688
  if (!("unavailable" in result)) {
7401
7689
  if (result.messages.length === 0) {
@@ -8459,8 +8747,8 @@ function safeSlug(slug) {
8459
8747
  if (slug.includes("..") || slug.length > 72) return null;
8460
8748
  return slug;
8461
8749
  }
8462
- function isRecordStem(stem) {
8463
- return safeSlug(stem) !== null && SlugSchema.safeParse(stem).success;
8750
+ function isRecordStem(stem2) {
8751
+ return safeSlug(stem2) !== null && SlugSchema.safeParse(stem2).success;
8464
8752
  }
8465
8753
  function readStoreRecords(dir) {
8466
8754
  if (!fs12.existsSync(dir)) return { records: [], unreadable: 0 };
@@ -8520,9 +8808,9 @@ function createFileStore(options) {
8520
8808
  async get(slug, getOptions = {}) {
8521
8809
  const filePath = fileFor(slug);
8522
8810
  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;
8811
+ const read2 = readRecord(filePath);
8812
+ if (!read2.ok) return { ok: false, reason: "unreadable", problem: read2.problem };
8813
+ const record = read2.record;
8526
8814
  const hasManageToken = manageTokenMatches(getOptions.manageToken, record.manageTokenHash);
8527
8815
  const viewer = { ...getOptions.viewer, hasManageToken };
8528
8816
  const allowed = canRead(record, viewer);
@@ -8534,10 +8822,10 @@ function createFileStore(options) {
8534
8822
  const summaries = [];
8535
8823
  for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
8536
8824
  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));
8825
+ const read2 = readRecord(path12.join(dir, entry.name));
8826
+ if (!read2.ok) continue;
8827
+ if (!canList(read2.record)) continue;
8828
+ summaries.push(summarize(read2.record));
8541
8829
  }
8542
8830
  summaries.sort((a, b) => a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0);
8543
8831
  return summaries;
@@ -8641,6 +8929,31 @@ function readGitLineage(cwd, options = {}) {
8641
8929
  gitCapturedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
8642
8930
  };
8643
8931
  }
8932
+ function zList(output) {
8933
+ return output === null ? null : output.split("\0").filter((entry) => entry.length > 0);
8934
+ }
8935
+ function bindCommit(cwd, rev, options = {}) {
8936
+ const base = readGitLineage(cwd, options);
8937
+ if (base === null || cwd === void 0) {
8938
+ return { problem: `the session's directory (${cwd ?? "not recorded"}) is not a git work tree on this machine` };
8939
+ }
8940
+ const sha = git(cwd, ["rev-parse", "--verify", "-q", `${rev}^{commit}`], options)?.trim();
8941
+ if (sha === void 0 || !/^[0-9a-f]{7,64}$/.test(sha)) return { problem: `"${rev}" is not a commit in ${cwd}` };
8942
+ if (sha === base.headCommitSha) return { ...base, commitNamed: true };
8943
+ const tracked = zList(git(cwd, ["diff", "--name-only", "-z", sha, "--"], options));
8944
+ const untracked = zList(git(cwd, ["ls-files", "--others", "--exclude-standard", "-z"], options));
8945
+ if (tracked === null || untracked === null) return { problem: `git could not compare the working tree with ${sha}` };
8946
+ const dirty = [.../* @__PURE__ */ new Set([...tracked, ...untracked])];
8947
+ const kept = dirty.slice(0, GIT_LIMITS.dirtyFiles);
8948
+ const { dirtyFilesOmitted: _omitted, ...rest } = base;
8949
+ return {
8950
+ ...rest,
8951
+ headCommitSha: sha,
8952
+ dirtyFiles: kept,
8953
+ ...dirty.length > kept.length ? { dirtyFilesOmitted: dirty.length - kept.length } : {},
8954
+ commitNamed: true
8955
+ };
8956
+ }
8644
8957
  function checkPrUrl(value) {
8645
8958
  let parsed;
8646
8959
  try {
@@ -8656,12 +8969,14 @@ function checkPrUrl(value) {
8656
8969
  function withLineage(trace, lineage, prUrl) {
8657
8970
  if (lineage === null && prUrl === void 0) return trace;
8658
8971
  const source = { ...trace.source };
8659
- const elsewhere = lineage !== null && source.gitBranch !== void 0 && lineage.gitBranch !== source.gitBranch;
8972
+ const elsewhere = lineage !== null && lineage.commitNamed !== true && source.gitBranch !== void 0 && lineage.gitBranch !== source.gitBranch;
8660
8973
  if (lineage !== null && !elsewhere) {
8661
8974
  if (source.gitBranch === void 0 && lineage.gitBranch !== void 0) source.gitBranch = lineage.gitBranch;
8662
8975
  if (lineage.headCommitSha !== void 0) source.headCommitSha = lineage.headCommitSha;
8663
8976
  source.dirtyFiles = lineage.dirtyFiles;
8664
8977
  if (lineage.dirtyFilesOmitted !== void 0) source.dirtyFilesOmitted = lineage.dirtyFilesOmitted;
8978
+ else delete source.dirtyFilesOmitted;
8979
+ if (lineage.commitNamed === true) source.commitNamed = true;
8665
8980
  source.gitCapturedAt = lineage.gitCapturedAt;
8666
8981
  }
8667
8982
  if (prUrl !== void 0) source.prUrl = prUrl;
@@ -8669,7 +8984,7 @@ function withLineage(trace, lineage, prUrl) {
8669
8984
  }
8670
8985
 
8671
8986
  // src/auth.ts
8672
- import crypto from "node:crypto";
8987
+ import crypto2 from "node:crypto";
8673
8988
  import fs15 from "node:fs";
8674
8989
  import os6 from "node:os";
8675
8990
  import path14 from "node:path";
@@ -8699,7 +9014,7 @@ function isAuthFailure(value) {
8699
9014
  return typeof value === "object" && value !== null && "ok" in value && value.ok === false;
8700
9015
  }
8701
9016
  function hashSecret(secret) {
8702
- return crypto.createHash("sha256").update(secret).digest("hex");
9017
+ return crypto2.createHash("sha256").update(secret).digest("hex");
8703
9018
  }
8704
9019
  function apiUrlFrom(env = process.env) {
8705
9020
  const explicit = env["FLATBREAD_API_URL"]?.trim();
@@ -8712,7 +9027,7 @@ async function startCliLogin(options = {}) {
8712
9027
  const apiUrl = options.apiUrl ?? apiUrlFrom(env);
8713
9028
  const base = validateApiUrl(apiUrl);
8714
9029
  if (isAuthFailure(base)) return base;
8715
- const pollSecret = crypto.randomBytes(32).toString("hex");
9030
+ const pollSecret = crypto2.randomBytes(32).toString("hex");
8716
9031
  const result = await postJson(
8717
9032
  `${base}/api/cli-login/start`,
8718
9033
  { poll_secret_hash: hashSecret(pollSecret) },
@@ -9503,65 +9818,105 @@ function renderMarked(value, matches) {
9503
9818
  return html;
9504
9819
  }
9505
9820
  var STYLE = `
9506
- :root{--bg:#0f1115;--panel:#161a22;--line:#232936;--fg:#e6e9ef;--dim:#8b93a7;--hit:#3a2a12;--hitline:#c8892b;--ok:#2f9e63;--warn:#c85a54;--acc:#5b8def}
9821
+ :root{
9822
+ --canvas:#fafafa;--bg:#ffffff;--ivory:#fcfbf8;--surface:#f6f7f8;--inset:#f6f7f8;
9823
+ --line:rgba(226,232,240,.8);--line-soft:#eff0f2;--line-strong:#b9bec4;
9824
+ --fg:#23272c;--fg-2:rgba(35,39,44,.7);--fg-3:rgba(35,39,44,.56);--fg-lift:#2b3138;
9825
+ --fail-bg:#f6ece7;--fail-line:#c9755c;--fail-fg:#8a3b2a;
9826
+ --ok-bg:#eef1e8;--ok-line:#7f9a7a;--ok-fg:#3f5d43;
9827
+ --hold-bg:#f7f1e2;--hold-line:#c9a34a;--hold-fg:#7d601d;
9828
+ --intent-line:#d6d1c1;
9829
+ --shade:30,41,54;
9830
+ --shadow:0 1px 2px rgba(var(--shade),.04),0 10px 26px rgba(var(--shade),.08);
9831
+ --radius:10px;--radius-sm:6px;
9832
+ --sans:-apple-system,BlinkMacSystemFont,"Segoe UI Variable Text","Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
9833
+ --mono:ui-monospace,"SF Mono",SFMono-Regular,"Cascadia Mono","Segoe UI Mono","Roboto Mono",Menlo,Consolas,"Liberation Mono",monospace
9834
+ }
9507
9835
  *{box-sizing:border-box}
9508
- body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace}
9509
- header{position:sticky;top:0;z-index:5;background:var(--panel);border-bottom:1px solid var(--line);padding:14px 20px}
9510
- h1{margin:0 0 6px;font-size:16px;font-weight:600;letter-spacing:.01em}
9511
- .meta{color:var(--dim);font-size:12px}
9836
+ html,body{margin:0}
9837
+ body{background:var(--canvas);color:var(--fg);font:14px/1.55 var(--sans);-webkit-font-smoothing:antialiased}
9838
+ code,pre,dd,.mono{font-family:var(--mono)}
9839
+ header{background:var(--bg);border-bottom:1px solid var(--line-soft)}
9840
+ .hin{max-width:1440px;margin:0 auto;padding:14px 28px 16px}
9841
+ .bar{display:flex;align-items:center;gap:10px;margin:0 0 12px}
9842
+ .wordmark{display:inline-flex;align-items:center;height:28px;padding:0 12px;border-radius:999px;background:var(--ivory);border:1px solid rgba(var(--shade),.08);box-shadow:inset 0 1px 0 #fff,0 1px 2px rgba(var(--shade),.05);font:600 13.5px/1 var(--mono);letter-spacing:-.01em;font-variant-ligatures:none}
9843
+ .crumb{color:var(--fg-3);font-size:12.5px}
9844
+ h1{margin:0 0 6px;font-size:20px;line-height:1.3;font-weight:600;letter-spacing:-.01em}
9845
+ .meta{color:var(--fg-3);font-size:12.5px;font-variant-numeric:tabular-nums}
9512
9846
  .meta b{color:var(--fg);font-weight:600}
9513
- .wrap{display:grid;grid-template-columns:minmax(0,1fr) 380px;gap:0;align-items:start}
9514
- main{padding:18px 20px 120px;min-width:0}
9515
- aside{position:sticky;top:74px;max-height:calc(100vh - 74px);overflow:auto;border-left:1px solid var(--line);padding:16px;background:var(--panel)}
9516
- .step{border:1px solid var(--line);border-radius:6px;margin:0 0 12px;background:var(--panel)}
9517
- .step>.head{display:flex;gap:10px;align-items:center;padding:8px 12px;border-bottom:1px solid var(--line);font-size:12px;color:var(--dim)}
9518
- .badge{border:1px solid var(--line);border-radius:999px;padding:1px 9px;color:var(--fg);font-size:11px}
9519
- .badge.user_prompt{border-color:#3f6fd8;color:#9dbcff}
9520
- .badge.assistant_text{border-color:#3f8f6f;color:#8fe0bb}
9521
- .badge.assistant_thinking{border-color:#6a5acd;color:#c0b6ff}
9522
- .badge.tool_call{border-color:#b08a2e;color:#ffd98a}
9523
- .badge.tool_result{border-color:#4a5568;color:#c3cbdc}
9524
- .badge.error_recovery{border-color:#c85a54;color:#ff9e99}
9525
- pre{margin:0;padding:10px 12px;white-space:pre-wrap;word-break:break-word;font:inherit}
9526
- .sub{padding:6px 12px;border-top:1px solid var(--line);color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
9527
- dl{margin:0;padding:2px 12px 10px}
9528
- dt{color:var(--dim);font-size:12px;margin-top:8px}
9529
- dd{margin:2px 0 0;white-space:pre-wrap;word-break:break-word}
9530
- mark.hit{background:var(--hit);color:#ffd98a;border-bottom:1px solid var(--hitline);border-radius:2px;padding:0 2px}
9847
+ .meta.path{font-family:var(--mono);font-size:11.5px;margin-top:2px;word-break:break-all}
9848
+ .wrap{max-width:1440px;margin:0 auto;padding:20px 28px 120px;display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:24px;align-items:start}
9849
+ main{min-width:0}
9850
+ aside{position:sticky;top:16px;max-height:calc(100vh - 32px);overflow:auto;background:var(--bg);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:18px}
9851
+ .step{background:var(--bg);border:1px solid var(--line);border-radius:var(--radius);box-shadow:0 1px 2px rgba(var(--shade),.04);margin:0 0 10px;overflow:hidden}
9852
+ .step>.head{display:flex;gap:10px;align-items:baseline;padding:9px 14px 0;font-size:12px;color:var(--fg-3);font-variant-numeric:tabular-nums}
9853
+ .badge{font-weight:600;color:var(--fg-2);font-size:12px}
9854
+ .step.user_prompt{border-left:3px solid var(--intent-line)}
9855
+ .step.error_recovery{border-color:color-mix(in srgb,var(--fail-line) 45%,transparent);border-left:3px solid var(--fail-line)}
9856
+ .badge.error_recovery,.sub.err{color:var(--fail-fg)}
9857
+ pre{margin:0;padding:8px 14px 12px;white-space:pre-wrap;word-break:break-word;font:12.5px/1.6 var(--mono);color:var(--fg)}
9858
+ .step.user_prompt>pre,.step.assistant_text>pre{font:14px/1.6 var(--sans)}
9859
+ .sub{padding:8px 14px 0;border-top:1px solid var(--line-soft);color:var(--fg-3);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
9860
+ .sub+pre,.sub+dl{background:var(--inset);margin:8px 14px 12px;padding:10px 12px;border-radius:var(--radius-sm)}
9861
+ dl{margin:0;padding:4px 14px 12px}
9862
+ dt{color:var(--fg-3);font-size:11.5px;margin-top:8px}
9863
+ dd{margin:2px 0 0;white-space:pre-wrap;word-break:break-word;font-size:12.5px}
9864
+ #trace-meta dd{font-family:var(--sans);font-size:13.5px}
9865
+ mark.hit{background:rgba(var(--shade),.08);box-shadow:inset 0 1px 1px rgba(var(--shade),.08);color:var(--fg-2);border-radius:4px;padding:0 4px;font-family:var(--mono);font-size:.9em}
9531
9866
  mark.hit .was{display:none}
9532
- mark.hit.keep{background:transparent;color:inherit;border-bottom:1px dashed var(--warn)}
9867
+ mark.hit.keep{background:var(--hold-bg);box-shadow:none;color:var(--fg);border-bottom:1px dashed var(--hold-line);border-radius:2px;font-family:inherit;font-size:inherit}
9533
9868
  mark.hit.keep .tok{display:none}
9534
9869
  mark.hit.keep .was{display:inline}
9535
- aside h2{margin:0 0 4px;font-size:13px}
9536
- aside .note{color:var(--dim);font-size:12px;margin:0 0 12px}
9537
- .row{display:grid;grid-template-columns:16px 58px minmax(0,1fr);gap:8px;align-items:start;padding:7px 8px;border:1px solid var(--line);border-radius:5px;margin-bottom:6px;cursor:pointer}
9538
- .row:hover{border-color:var(--acc)}
9539
- .row input{margin:3px 0 0}
9540
- .state{font-size:11px;letter-spacing:.04em}
9541
- .s-keep{display:none;color:var(--warn)}
9542
- .s-red{color:var(--ok)}
9543
- .row input:checked ~ .state .s-red{display:none}
9544
- .row input:checked ~ .state .s-keep{display:inline}
9545
- .row .rule{font-size:12px}
9546
- .row .prev{color:var(--dim);font-size:11px;word-break:break-all}
9547
- .row .times{color:var(--warn)}
9548
- .keys{color:var(--dim);font-size:11px;margin:0 0 10px}
9549
- .keys b{color:var(--fg);font-weight:600}
9550
- .row .path{color:#5c6474;font-size:10px;word-break:break-all}
9551
- fieldset{border:1px solid var(--line);border-radius:6px;margin:0 0 12px;padding:8px 10px}
9552
- legend{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
9553
- label.vis{display:block;padding:3px 0}
9554
- .warn{color:var(--warn);font-size:11px;margin:4px 0 0}
9555
- .actions{display:flex;gap:8px;margin-top:10px}
9556
- button{font:inherit;padding:9px 14px;border-radius:5px;border:1px solid var(--line);background:#1d2430;color:var(--fg);cursor:pointer}
9557
- button.primary{background:var(--ok);border-color:var(--ok);color:#04140b;font-weight:600}
9558
- button:hover{border-color:var(--acc)}
9870
+ aside h2{margin:0 0 4px;font-size:15px;font-weight:600}
9871
+ aside .note{color:var(--fg-3);font-size:12.5px;margin:0 0 12px}
9872
+ aside .note code{font-size:11.5px;background:var(--inset);padding:1px 4px;border-radius:4px}
9559
9873
  .tools{display:flex;gap:6px;margin:0 0 10px}
9560
- .tools button{padding:4px 8px;font-size:11px}
9561
- .done{max-width:760px;margin:12vh auto;padding:0 20px}
9562
- .done code{display:block;background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:12px;margin:12px 0;word-break:break-all}
9563
- a{color:var(--acc)}
9874
+ .keys{color:var(--fg-3);font-size:12px;margin:0 0 14px}
9875
+ .keys b{display:inline-block;min-width:18px;padding:0 5px;border:1px solid rgba(var(--shade),.12);border-radius:999px;background:var(--bg);color:var(--fg);font:600 11px/16px var(--mono);text-align:center}
9876
+ fieldset{border:0;border-top:1px solid var(--line-soft);margin:0;padding:14px 0 12px}
9877
+ legend{float:left;width:100%;padding:0;margin:0 0 6px;color:var(--fg-3);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
9878
+ legend+*{clear:left}
9879
+ input.title{width:100%;font:inherit;padding:8px 10px;margin:0 0 6px;border:1px solid rgba(var(--shade),.12);border-radius:var(--radius-sm);background:var(--ivory);color:var(--fg);box-shadow:inset 0 1px 0 #fff}
9880
+ input.title:focus{outline:none;background:#fff;border-color:var(--fg-3);box-shadow:0 0 0 3px rgba(var(--shade),.05)}
9881
+ label.vis{display:flex;align-items:center;gap:8px;padding:3px 0;cursor:pointer}
9882
+ input[type=radio],input[type=checkbox]{accent-color:var(--fg);margin:0}
9883
+ .warn{color:var(--fail-fg);font-size:12.5px;margin:6px 0 0}
9884
+ .actions{display:flex;gap:8px;padding:14px 0 16px;border-top:1px solid var(--line-soft)}
9885
+ button{display:inline-flex;align-items:center;font:500 12.5px/1 var(--sans);padding:9px 14px;border-radius:var(--radius-sm);border:1px solid var(--line-strong);background:var(--bg);color:var(--fg);cursor:pointer}
9886
+ button:hover{background:var(--surface)}
9887
+ button:focus-visible{outline:2px solid var(--fg);outline-offset:1px}
9888
+ button.primary{background:var(--fg);border-color:var(--fg);color:#fff}
9889
+ button.primary:hover{background:var(--fg-lift);border-color:var(--fg-lift)}
9890
+ .tools button{padding:5px 10px;font-size:12px}
9891
+ .row{display:grid;grid-template-columns:16px 52px minmax(0,1fr);gap:8px;align-items:start;padding:9px 10px;border:1px solid var(--line);border-radius:var(--radius-sm);margin-bottom:6px;cursor:pointer;background:var(--bg)}
9892
+ .row:hover{background:var(--surface)}
9893
+ .row:focus-within{border-color:var(--fg-3)}
9894
+ .row input{margin:3px 0 0}
9895
+ .state{font-size:11.5px;font-weight:600}
9896
+ .s-red{color:var(--fg-3)}
9897
+ .s-keep{display:none;color:var(--hold-fg)}
9898
+ .row input:checked~.state .s-red{display:none}
9899
+ .row input:checked~.state .s-keep{display:inline}
9900
+ .row:has(input:checked){background:var(--hold-bg);border-color:color-mix(in srgb,var(--hold-line) 50%,transparent)}
9901
+ .row .rule{font-size:13px;color:var(--fg)}
9902
+ .row .times{font-weight:600}
9903
+ .row .prev{color:var(--fg-2);font:11.5px/1.5 var(--mono);word-break:break-all}
9904
+ .row .path{color:var(--fg-3);font-size:11px;word-break:break-all}
9905
+ .done{max-width:620px;margin:14vh auto;padding:28px 30px;background:var(--bg);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow)}
9906
+ .done h1{font-size:20px}
9907
+ .done .meta{font-size:13.5px;color:var(--fg-2);margin:10px 0 0}
9908
+ .done code{display:block;background:var(--inset);border-radius:var(--radius-sm);padding:12px 14px;margin:14px 0;font-size:12.5px;word-break:break-all}
9909
+ a{color:var(--fg);text-underline-offset:2px}
9910
+ @media (max-width:960px){.wrap{grid-template-columns:minmax(0,1fr);padding:16px}aside{position:static;max-height:none;order:-1}.hin{padding:12px 16px}}
9564
9911
  `;
9912
+ var STEP_LABEL = {
9913
+ user_prompt: "prompt",
9914
+ assistant_text: "assistant",
9915
+ assistant_thinking: "thinking",
9916
+ tool_call: "tool call",
9917
+ tool_result: "tool result",
9918
+ error_recovery: "failure"
9919
+ };
9565
9920
  var SCRIPT = `
9566
9921
  (function(){
9567
9922
  var boxes = Array.prototype.slice.call(document.querySelectorAll('input[name="keep"]'));
@@ -9594,6 +9949,9 @@ var SCRIPT = `
9594
9949
  if (e.metaKey || e.ctrlKey || e.altKey) return;
9595
9950
  var tag = e.target && e.target.tagName;
9596
9951
  var key = e.key;
9952
+ // TYPING IS NOT TRIAGE: j, k and space in the title field are letters. Enter submits the form
9953
+ // the way any text field does, through the first submit button - Confirm.
9954
+ if (tag === 'INPUT' && e.target.type === 'text') return;
9597
9955
  if (key === 'j' || key === 'ArrowDown') { e.preventDefault(); focusRow(cursor + 1); return; }
9598
9956
  if (key === 'k' || key === 'ArrowUp') { e.preventDefault(); focusRow(cursor - 1); return; }
9599
9957
  if (key === ' ' && cursor >= 0 && tag !== 'BUTTON') {
@@ -9665,14 +10023,14 @@ function renderReviewPage(input) {
9665
10023
  const base = `steps[${index}]`;
9666
10024
  const parts = [];
9667
10025
  parts.push(
9668
- `<div class="head"><span class="badge ${escapeHtml(step.type)}">${escapeHtml(step.type)}</span><span>#${step.stepNumber}</span><span>${escapeHtml(step.timestamp)}</span></div>`
10026
+ `<div class="head"><span class="badge ${escapeHtml(step.type)}">${escapeHtml(STEP_LABEL[step.type] ?? step.type)}</span><span>#${step.stepNumber}</span><span>${escapeHtml(step.timestamp)}</span></div>`
9669
10027
  );
9670
10028
  parts.push(`<pre>${marked(`${base}.content`, step.content)}</pre>`);
9671
10029
  const tool = step.tool;
9672
10030
  if (tool) {
9673
10031
  const inputSlots = slots.filter((slot) => slot.fieldPath.startsWith(`${base}.tool.input`));
9674
10032
  parts.push(
9675
- `<div class="sub">tool ${escapeHtml(tool.name)}${tool.isError ? " &middot; errored" : ""}</div>`
10033
+ `<div class="sub${tool.isError ? " err" : ""}">tool ${escapeHtml(tool.name)}${tool.isError ? " &middot; errored" : ""}</div>`
9676
10034
  );
9677
10035
  if (inputSlots.length > 0) {
9678
10036
  parts.push(
@@ -9692,7 +10050,7 @@ function renderReviewPage(input) {
9692
10050
  `<div class="sub">diff ${marked(`${base}.diff.filePath`, diff.filePath)}</div><pre>${marked(`${base}.diff.patch`, diff.patch)}</pre>`
9693
10051
  );
9694
10052
  }
9695
- return `<section class="step" id="${escapeHtml(base)}">${parts.join("")}</section>`;
10053
+ return `<section class="step ${escapeHtml(step.type)}" id="${escapeHtml(base)}">${parts.join("")}</section>`;
9696
10054
  }).join("");
9697
10055
  const groups = groupMatches(matches);
9698
10056
  const rowsHtml = groups.length === 0 ? `<p class="note">No matches. Nothing will be rewritten.</p>` : groups.map((group) => {
@@ -9706,15 +10064,16 @@ function renderReviewPage(input) {
9706
10064
  const publicWarning = input.publishTo === void 0 ? "Visibility public: this draft records that choice, and the run becomes readable by anyone the moment it is uploaded. Continue?" : `Visibility public: confirming uploads this run to ${input.publishTo} where anyone with the link can read it. Continue?`;
9707
10065
  const destinationHtml = input.publishTo === void 0 ? `<p class="note">Local draft only. Confirm writes ${escapeHtml(input.draftPath ?? "a file in packages/cli/.tmp")} and nothing leaves this machine. The visibility above is stored in the draft. To upload a run, share it with <code>--publish</code> after <code>flatbread login</code>.</p>` : `<p class="warn">Confirm UPLOADS this run to ${escapeHtml(input.publishTo)} with the visibility selected above. Everything you KEEP below is uploaded as written.</p>`;
9708
10066
  const body = `
9709
- <header>
10067
+ <header><div class="hin">
10068
+ <div class="bar"><span class="wordmark">flatbread</span><span class="crumb">review before you share</span></div>
9710
10069
  <h1>${marked("title", trace.title)}</h1>
9711
10070
  <div class="meta">
9712
- <b>${escapeHtml(trace.model)}</b> &middot; ${trace.steps.length} steps &middot; ${toolCalls} tool calls &middot;
9713
- ${formatCount(trace.stats.filesChanged)} files changed &middot; ${trace.stats.errorCount} errors &middot;
9714
- ${Math.round(trace.stats.durationSeconds)}s &middot; <b>${matches.length}</b> redaction matches
10071
+ <b>${escapeHtml(trace.model)}</b> &middot; ${plural2(trace.steps.length, "step")} &middot; ${plural2(toolCalls, "tool call")} &middot;
10072
+ ${trace.stats.filesChanged === null ? `${formatCount(null)} files` : plural2(trace.stats.filesChanged, "file")} changed &middot; ${plural2(trace.stats.errorCount, "error")} &middot;
10073
+ ${Math.round(trace.stats.durationSeconds)}s &middot; <b>${matches.length}</b> redaction match${matches.length === 1 ? "" : "es"}
9715
10074
  </div>
9716
- <div class="meta">${escapeHtml(sessionPath)}</div>
9717
- </header>
10075
+ <div class="meta path">${escapeHtml(sessionPath)}</div>
10076
+ </div></header>
9718
10077
  <form id="review" method="post" action="/confirm">
9719
10078
  <div class="wrap">
9720
10079
  <main>${metaHtml}${stepsHtml}</main>
@@ -9724,6 +10083,11 @@ function renderReviewPage(input) {
9724
10083
  Every match is redacted by default. Tick a row to KEEP the original text instead.</p>
9725
10084
  <div class="tools"><button id="keep-none" type="button">redact all</button><button id="keep-all" type="button">keep all</button></div>
9726
10085
  <p class="keys"><b>j</b>/<b>k</b> move &middot; <b>space</b> keep or redact &middot; <b>enter</b> confirm</p>
10086
+ ${input.titleDraft === void 0 ? "" : `<fieldset>
10087
+ <legend><label for="title">Title</label></legend>
10088
+ <input class="title" id="title" name="title" type="text" required maxlength="${LIMITS.title}" value="${escapeHtml(input.titleDraft)}">
10089
+ <p class="note">Change it here, or leave it. A secret typed into it is redacted.</p>
10090
+ </fieldset>`}
9727
10091
  <fieldset>
9728
10092
  <legend>Visibility</legend>
9729
10093
  <label class="vis"><input type="radio" name="visibility" value="private" checked> private</label>
@@ -9742,13 +10106,14 @@ function renderReviewPage(input) {
9742
10106
  <script>var PUBLIC_WARNING=${JSON.stringify(publicWarning)};${SCRIPT}</script>`;
9743
10107
  return page(`Flatbread review - ${trace.title}`, body);
9744
10108
  }
9745
- function renderDonePage(message, outputPath, publishTo) {
10109
+ function renderDonePage(message, outputPath, publishTo, keepsRun = false) {
9746
10110
  const network = publishTo === void 0 ? `<p class="meta">Nothing was uploaded. This file is on your machine only.</p>` : `<p class="meta">Uploading this run to ${escapeHtml(publishTo)} now. Your terminal says whether it arrived, and prints its link.</p>`;
9747
10111
  return page(
9748
10112
  "Flatbread - share draft written",
9749
10113
  `<div class="done"><h1>${escapeHtml(message)}</h1>
9750
10114
  <code>${escapeHtml(outputPath)}</code>
9751
10115
  ${network}
10116
+ ${keepsRun ? `<p class="meta">That file holds only your latest share, and the next one replaces it. This run is also kept under its own ID, printed in your terminal: <code>flatbread pull &lt;id&gt;</code> prints it again, and <code>flatbread search --local</code> lists every run you have shared.</p>` : ""}
9752
10117
  <p class="meta"><strong>Go back to your terminal now.</strong> The manage token for this
9753
10118
  run is printed there, once, at the very bottom. It is the only way to open the run
9754
10119
  while it is private, and only its hash is stored, so nothing can print it again.</p>
@@ -9777,7 +10142,9 @@ async function readBody(req, limitBytes = 2 * 1024 * 1024) {
9777
10142
  function parseDecisionBody(body) {
9778
10143
  const form = new URLSearchParams(body);
9779
10144
  const parsedVisibility = VisibilitySchema.safeParse(form.get("visibility"));
10145
+ const title = form.get("title");
9780
10146
  return {
10147
+ ...title !== null ? { title: normalizeTitle(title) } : {},
9781
10148
  visibility: parsedVisibility.success ? parsedVisibility.data : "private",
9782
10149
  // ONE CHECKBOX MAY DECIDE MANY MATCHES. A grouped row carries every id it governs,
9783
10150
  // space-separated, so the browser sends one value and the caller still gets the flat
@@ -9787,12 +10154,11 @@ function parseDecisionBody(body) {
9787
10154
  };
9788
10155
  }
9789
10156
  async function listenOnFirstFreePort(server, ports) {
9790
- for (const port of ports) {
10157
+ for (const port of [...ports, 0]) {
9791
10158
  const ok = await new Promise((resolve) => {
9792
- const onError = (error) => {
10159
+ const onError = () => {
9793
10160
  server.removeListener("listening", onListening);
9794
- if (error.code === "EADDRINUSE" || error.code === "EACCES") resolve(false);
9795
- else resolve(false);
10161
+ resolve(false);
9796
10162
  };
9797
10163
  const onListening = () => {
9798
10164
  server.removeListener("error", onError);
@@ -9852,10 +10218,26 @@ async function startReviewServer(options) {
9852
10218
  void (async () => {
9853
10219
  try {
9854
10220
  const body = await readBody(req);
9855
- const { visibility, keepIds } = parseDecisionBody(body);
9856
- const confirmed = { action: "confirm", visibility, keepIds };
10221
+ const { visibility, keepIds, title } = parseDecisionBody(body);
10222
+ if (title === null) {
10223
+ send(
10224
+ 400,
10225
+ page(
10226
+ "Flatbread - title refused",
10227
+ `<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>`
10228
+ )
10229
+ );
10230
+ return;
10231
+ }
10232
+ const changed = title !== void 0 && options.titleDraft !== void 0 && title !== normalizeTitle(options.titleDraft);
10233
+ const confirmed = {
10234
+ action: "confirm",
10235
+ visibility,
10236
+ keepIds,
10237
+ ...changed ? { title } : {}
10238
+ };
9857
10239
  const message = options.onConfirm ? await options.onConfirm(confirmed) : "";
9858
- send(200, renderDonePage("Share draft written", message, options.publishTo));
10240
+ send(200, renderDonePage("Share draft written", message, options.publishTo, options.keepsRun === true));
9859
10241
  finish(confirmed);
9860
10242
  } catch (error) {
9861
10243
  send(500, page("Flatbread - error", `<div class="done"><h1>Could not write the share draft</h1><code>${escapeHtml(error instanceof Error ? error.message : String(error))}</code></div>`));
@@ -9890,6 +10272,16 @@ async function startReviewServer(options) {
9890
10272
  }
9891
10273
 
9892
10274
  // src/share.ts
10275
+ var spanKey = (match) => `${match.fieldPath}\0${match.start}\0${match.end}\0${match.original}`;
10276
+ function applyReview(trace, matches, keepIds, title) {
10277
+ if (title === void 0 || title === trace.title) return applyRedactions(trace, matches, { keepIds });
10278
+ const retitled = { ...trace, title };
10279
+ const kept = new Set(keepIds);
10280
+ const keptSpans = new Set(matches.filter((m) => kept.has(m.id) && m.fieldPath !== "title").map(spanKey));
10281
+ const rescanned = scanTrace(retitled).matches;
10282
+ const keep = rescanned.filter((m) => m.fieldPath !== "title" && keptSpans.has(spanKey(m))).map((m) => m.id);
10283
+ return applyRedactions(retitled, rescanned, { keepIds: keep });
10284
+ }
9893
10285
  function openInBrowser(url) {
9894
10286
  try {
9895
10287
  if (process.platform === "win32") {
@@ -9999,6 +10391,7 @@ function reportPersist(outcome, options, log) {
9999
10391
  log(` slug ${outcome.slug}`);
10000
10392
  log(` visibility ${outcome.record.visibility}`);
10001
10393
  if (outcome.recordPath) log(` record ${outcome.recordPath}`);
10394
+ log(` again flatbread pull ${outcome.slug} (the next share replaces ${path15.basename(options.outputPath)}, not this record)`);
10002
10395
  log("");
10003
10396
  log(`open ${outcome.publish?.ok ? outcome.publish.url : `${base}/r/${outcome.slug}`}`);
10004
10397
  log("");
@@ -10064,17 +10457,22 @@ async function runShare(options) {
10064
10457
  if (parsed.humanTest) console.error(` next step HUMAN-TESTS.md section ${parsed.humanTest}`);
10065
10458
  return { exitCode: 1, status: "not-found" };
10066
10459
  }
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");
10460
+ const cwd = parsed.trace.source.cwd;
10461
+ let lineage;
10462
+ if (options.commit !== void 0) {
10463
+ const bound = (options.bindCommit ?? ((dir, rev) => bindCommit(dir, rev)))(cwd, options.commit);
10464
+ if ("problem" in bound) {
10465
+ console.error(`--commit ${options.commit}: ${bound.problem}`);
10466
+ console.error("Nothing was written. Run it from the session's repository, or leave --commit off.");
10467
+ return { exitCode: 1, status: "bad-commit" };
10468
+ }
10469
+ lineage = bound;
10470
+ } else {
10471
+ lineage = (options.readLineage ?? ((dir) => readGitLineage(dir)))(cwd);
10077
10472
  }
10473
+ const lined = withLineage(parsed.trace, lineage, options.prUrl);
10474
+ const trace = options.title !== void 0 ? FlatbreadTraceSchema.parse({ ...lined, title: options.title }) : lined;
10475
+ for (const note of parsed.notes) log(`note: ${note}`);
10078
10476
  const scan = scanTrace(trace);
10079
10477
  const byRule = /* @__PURE__ */ new Map();
10080
10478
  for (const match of scan.matches) byRule.set(match.rule, (byRule.get(match.rule) ?? 0) + 1);
@@ -10101,11 +10499,13 @@ async function runShare(options) {
10101
10499
  slots: scan.slots,
10102
10500
  sessionPath: target,
10103
10501
  draftPath: options.outputPath,
10502
+ titleDraft: previewTitle,
10503
+ keepsRun: options.persist !== false && (options.storeDir ?? resolveStoreDir()) !== null,
10104
10504
  ...isPublisher(reviewPublisher) ? { publishTo: reviewPublisher.description } : {},
10105
10505
  ...options.ports ? { ports: options.ports } : {},
10106
10506
  ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
10107
10507
  onConfirm: (decision2) => {
10108
- const applied2 = applyRedactions(trace, scan.matches, { keepIds: decision2.keepIds });
10508
+ const applied2 = applyReview(trace, scan.matches, decision2.keepIds, decision2.title);
10109
10509
  const draft2 = buildDraft(applied2.trace, applied2.report, decision2.visibility);
10110
10510
  writeDraft(options.outputPath, draft2);
10111
10511
  written = draft2;
@@ -10114,9 +10514,9 @@ async function runShare(options) {
10114
10514
  });
10115
10515
  if (!session) {
10116
10516
  console.error(
10117
- `could not bind a review port (tried ${(options.ports ?? [4321, 4322, 4323, 4324, 4325]).join(", ")})`
10517
+ `could not bind a review port on ${REVIEW_SERVER.host} (tried ${(options.ports ?? REVIEW_SERVER.ports).join(", ")}, then any port the OS would give)`
10118
10518
  );
10119
- console.error("Close whatever is using them, or run with --no-review.");
10519
+ console.error("Loopback itself refused, so this machine needs a look; --no-review redacts everything without the page.");
10120
10520
  return { exitCode: 1, status: "port-unavailable" };
10121
10521
  }
10122
10522
  log(` review at ${session.url}`);
@@ -10385,11 +10785,160 @@ function describeReadFailure(failure, what) {
10385
10785
 
10386
10786
  // src/mcp.ts
10387
10787
  import readline3 from "node:readline";
10788
+
10789
+ // src/bucket.ts
10790
+ import { createHash as createHash2 } from "node:crypto";
10791
+ import fs18 from "node:fs";
10792
+ import path17 from "node:path";
10793
+ var BUCKET_INDEX_VERSION = 1;
10794
+ var BUCKET_INDEX_FILE = "index.json";
10795
+ var CAPTURE_LOG_FILE = "capture.log";
10796
+ var COMMIT_SESSION_WINDOW_MS = 2 * 60 * 60 * 1e3;
10797
+ function bucketDir(env = process.env) {
10798
+ return path17.join(flatbreadHome(env), "bucket");
10799
+ }
10800
+ function bucketSlug(harness, sessionKey) {
10801
+ return `local-${createHash2("sha256").update(`${harness}\0${sessionKey}`, "utf8").digest("hex").slice(0, 12)}`;
10802
+ }
10803
+ function rowOf(record) {
10804
+ const sha = record.trace.source.headCommitSha;
10805
+ return {
10806
+ summary: summarize(record),
10807
+ sessionPath: record.trace.source.sourcePath,
10808
+ capturedAt: record.updatedAt,
10809
+ ...record.trace.source.commitNamed === true && sha !== void 0 ? { commit: sha } : {}
10810
+ };
10811
+ }
10812
+ function recordStems(dir) {
10813
+ 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();
10814
+ }
10815
+ function writeAtomic(file, body) {
10816
+ fs18.mkdirSync(path17.dirname(file), { recursive: true });
10817
+ const temp = `${file}.${process.pid}.tmp`;
10818
+ fs18.writeFileSync(temp, body, "utf8");
10819
+ fs18.renameSync(temp, file);
10820
+ }
10821
+ 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);
10822
+ function readIndex(dir) {
10823
+ try {
10824
+ const parsed = JSON.parse(fs18.readFileSync(path17.join(dir, BUCKET_INDEX_FILE), "utf8"));
10825
+ if (typeof parsed !== "object" || parsed === null) return null;
10826
+ const { version, rows } = parsed;
10827
+ if (version !== BUCKET_INDEX_VERSION || !Array.isArray(rows)) return null;
10828
+ const out = [];
10829
+ for (const row of rows) {
10830
+ if (typeof row !== "object" || row === null) return null;
10831
+ const { summary, sessionPath, capturedAt, commit } = row;
10832
+ const parsedSummary = RecordSummarySchema.safeParse(summary);
10833
+ if (!parsedSummary.success || typeof sessionPath !== "string" || typeof capturedAt !== "string") return null;
10834
+ if (commit !== void 0 && typeof commit !== "string") return null;
10835
+ out.push({ summary: parsedSummary.data, sessionPath, capturedAt, ...commit !== void 0 ? { commit } : {} });
10836
+ }
10837
+ return out;
10838
+ } catch {
10839
+ return null;
10840
+ }
10841
+ }
10842
+ function writeIndex(dir, rows) {
10843
+ writeAtomic(path17.join(dir, BUCKET_INDEX_FILE), JSON.stringify({ version: BUCKET_INDEX_VERSION, rows: newestFirst(rows) }, null, 2) + "\n");
10844
+ }
10845
+ function readBucket(dir) {
10846
+ if (!fs18.existsSync(dir)) return { rows: [], rebuilt: false, unreadable: 0 };
10847
+ const stems = recordStems(dir);
10848
+ const indexed = readIndex(dir);
10849
+ if (indexed !== null && JSON.stringify(indexed.map((row) => row.summary.slug).sort()) === JSON.stringify(stems)) {
10850
+ return { rows: newestFirst(indexed), rebuilt: false, unreadable: 0 };
10851
+ }
10852
+ const { records, unreadable } = readStoreRecords(dir);
10853
+ const rows = newestFirst(records.map(rowOf));
10854
+ try {
10855
+ writeIndex(dir, rows);
10856
+ } catch {
10857
+ }
10858
+ return { rows, rebuilt: true, unreadable };
10859
+ }
10860
+ function readBucketRecord(dir, slug) {
10861
+ if (!SlugSchema.safeParse(slug).success) return null;
10862
+ try {
10863
+ const parsed = TraceRecordSchema.safeParse(JSON.parse(fs18.readFileSync(path17.join(dir, `${slug}.json`), "utf8")));
10864
+ return parsed.success ? parsed.data : null;
10865
+ } catch {
10866
+ return null;
10867
+ }
10868
+ }
10869
+ function matchesQuery(summary, query) {
10870
+ if (query.harness !== void 0 && summary.harness !== query.harness) return false;
10871
+ if (query.category !== void 0 && summary.category !== query.category) return false;
10872
+ if (query.onlyWithRecoveries === true && summary.failuresRecovered === 0) return false;
10873
+ return matchesSearch(summary, query.query ?? "");
10874
+ }
10875
+ async function captureToBucket(options) {
10876
+ const dir = options.dir ?? bucketDir(options.env);
10877
+ const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
10878
+ const parsed = await parseWithHarness({ filePath: options.file, ...options.harness !== void 0 ? { harness: options.harness } : {} });
10879
+ if (!parsed.ok) return { status: "refused", reason: `could not parse ${options.file}: ${parsed.reason}` };
10880
+ const cwd = parsed.trace.source.cwd;
10881
+ let lineage;
10882
+ if (options.commit !== void 0) {
10883
+ const bound = (options.bindCommit ?? ((at, rev) => bindCommit(at, rev)))(cwd, options.commit);
10884
+ if ("problem" in bound) return { status: "refused", reason: `--commit ${options.commit}: ${bound.problem}` };
10885
+ lineage = bound;
10886
+ } else {
10887
+ lineage = (options.readLineage ?? ((at) => readGitLineage(at)))(cwd);
10888
+ }
10889
+ const trace = withLineage(parsed.trace, lineage);
10890
+ const scan = scanTrace(trace);
10891
+ const applied = applyRedactions(trace, scan.matches, { keepIds: [] });
10892
+ const sessionKey = trace.source.sessionId.length > 0 ? trace.source.sessionId : path17.resolve(options.file);
10893
+ const slug = bucketSlug(parsed.harness, sessionKey);
10894
+ const recordPath = path17.join(dir, `${slug}.json`);
10895
+ const previous = readBucketRecord(dir, slug);
10896
+ const commit = applied.trace.source.commitNamed === true ? applied.trace.source.headCommitSha : void 0;
10897
+ const draft = ShareDraftSchema.parse({
10898
+ schemaVersion: 1,
10899
+ createdAt: previous?.createdAt ?? now,
10900
+ trace: { ...applied.trace, visibility: "private" },
10901
+ redaction: applied.report,
10902
+ uploaded: false
10903
+ });
10904
+ const done = { slug, path: recordPath, steps: draft.trace.steps.length, redacted: draft.redaction.redactedCount, ...commit !== void 0 ? { commit } : {} };
10905
+ 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) {
10906
+ return { status: "unchanged", ...done };
10907
+ }
10908
+ const record = buildRecord(draft, { slug, manageTokenHash: hashManageToken(mintManageToken()), now, origin: "local", visibility: "private" });
10909
+ writeAtomic(recordPath, JSON.stringify(record, null, 2) + "\n");
10910
+ const rows = readBucket(dir).rows.filter((row) => row.summary.slug !== slug);
10911
+ writeIndex(dir, [...rows, rowOf(record)]);
10912
+ return { status: "captured", ...done };
10913
+ }
10914
+ function sessionForRepo(repo, options = {}) {
10915
+ const projects = getProjectsDir(options.env ?? process.env);
10916
+ const folder = path17.join(projects, encodeCwd(path17.resolve(repo)));
10917
+ const newest = listSessionFiles(folder)[0];
10918
+ if (newest === void 0) return { problem: `no Claude Code session recorded for ${repo}` };
10919
+ const age = (options.nowMs ?? Date.now()) - newest.mtimeMs;
10920
+ const window = options.windowMs ?? COMMIT_SESSION_WINDOW_MS;
10921
+ if (age > window) {
10922
+ return { problem: `the newest session for ${repo} last moved ${Math.round(age / 6e4)} min ago, outside the ${Math.round(window / 6e4)} min window` };
10923
+ }
10924
+ return { file: newest.filePath };
10925
+ }
10926
+ function logCapture(dir, line, now = (/* @__PURE__ */ new Date()).toISOString()) {
10927
+ try {
10928
+ fs18.mkdirSync(dir, { recursive: true });
10929
+ fs18.appendFileSync(path17.join(dir, CAPTURE_LOG_FILE), `${now} ${line.replace(/[\r\n]+/g, " ")}
10930
+ `, "utf8");
10931
+ } catch {
10932
+ }
10933
+ }
10934
+
10935
+ // src/mcp.ts
10388
10936
  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) {
10937
+ var MCP_SERVER_VERSION = "0.3.0";
10938
+ function defaultMcpDeps(storeDir2, bucketDir2) {
10391
10939
  return {
10392
10940
  storeDir: storeDir2,
10941
+ bucketDir: bucketDir2,
10393
10942
  search: (query) => searchRemote(query, { client: "mcp" }),
10394
10943
  capsule: (ref) => fetchCapsule(ref, { client: "mcp" })
10395
10944
  };
@@ -10434,12 +10983,6 @@ function hitLine(hit) {
10434
10983
  if (hit.visibility !== "public") parts.push(hit.visibility);
10435
10984
  return `- ${parts.join(" \xB7 ")}`;
10436
10985
  }
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
10986
  function readQuery(args) {
10444
10987
  const query = {};
10445
10988
  if (args["query"] !== void 0) {
@@ -10467,42 +11010,75 @@ function readQuery(args) {
10467
11010
  function localRecords(deps) {
10468
11011
  return deps.storeDir === null ? { records: [], unreadable: 0 } : readStoreRecords(deps.storeDir);
10469
11012
  }
10470
- async function searchTool(args, deps) {
10471
- const query = readQuery(args);
10472
- if (typeof query === "string") return text(query, true);
11013
+ var runs = (count) => `${count} matching run${count === 1 ? "" : "s"}`;
11014
+ function capturedLine(row) {
11015
+ return `${hitLine(row.summary)} \xB7 captured ${row.capturedAt.slice(0, 10)}${row.commit !== void 0 ? ` at ${row.commit.slice(0, 12)}` : ""}`;
11016
+ }
11017
+ async function searchEverywhere(query, deps, options = {}) {
10473
11018
  const out = [];
10474
11019
  const local = localRecords(deps);
10475
- const localHits = local.records.map((record) => summarize(record)).filter((summary) => matchesLocal(summary, query));
11020
+ const localHits = local.records.map((record) => summarize(record)).filter((summary) => matchesQuery(summary, query));
10476
11021
  if (deps.storeDir !== null) {
10477
- out.push(`This machine (${localHits.length} matching run${localHits.length === 1 ? "" : "s"}):`);
11022
+ out.push(`This machine (${runs(localHits.length)}):`);
10478
11023
  for (const hit of localHits) out.push(hitLine(hit));
10479
11024
  if (local.unreadable > 0) out.push(`(${local.unreadable} local record file${local.unreadable === 1 ? "" : "s"} could not be read)`);
10480
11025
  }
11026
+ const seen = new Set(localHits.map((hit) => hit.slug));
11027
+ for (const record of local.records) {
11028
+ const { harness, sessionId } = record.trace.source;
11029
+ if (sessionId.length > 0 && localHits.some((hit) => hit.slug === record.slug)) seen.add(bucketSlug(harness, sessionId));
11030
+ }
11031
+ if (deps.bucketDir !== null) {
11032
+ const bucket = readBucket(deps.bucketDir);
11033
+ const captured = bucket.rows.filter((row) => !seen.has(row.summary.slug) && matchesQuery(row.summary, query));
11034
+ if (out.length > 0) out.push("");
11035
+ out.push(`Captured on this machine, not shared (${runs(captured.length)}):`);
11036
+ for (const row of captured) {
11037
+ out.push(capturedLine(row));
11038
+ seen.add(row.summary.slug);
11039
+ }
11040
+ if (bucket.unreadable > 0) out.push(`(${bucket.unreadable} captured run file${bucket.unreadable === 1 ? "" : "s"} could not be read)`);
11041
+ }
11042
+ if (options.remote === false) {
11043
+ if (out.length > 0) out.push("");
11044
+ out.push("The site was not searched: --local");
11045
+ return { lines: out, siteSearched: false };
11046
+ }
10481
11047
  const remote = await deps.search(query);
10482
11048
  if (out.length > 0) out.push("");
10483
11049
  if (remote.ok) {
10484
- const seen = new Set(localHits.map((hit) => hit.slug));
10485
11050
  const fresh = remote.results.filter((hit) => !seen.has(hit.slug));
10486
- out.push(`${remote.site} (${fresh.length} matching run${fresh.length === 1 ? "" : "s"}):`);
11051
+ out.push(`${remote.site} (${runs(fresh.length)}):`);
10487
11052
  for (const hit of fresh) out.push(hitLine(hit));
10488
11053
  } else {
10489
11054
  out.push(`The site was not searched: ${describeReadFailure(remote, "search")}`);
10490
11055
  }
10491
- out.push("", "Pass a run ID to flatbread_get_capsule for what that run learned.");
10492
- return text(out.join("\n"));
11056
+ return { lines: out, siteSearched: remote.ok };
11057
+ }
11058
+ async function searchTool(args, deps) {
11059
+ const query = readQuery(args);
11060
+ if (typeof query === "string") return text(query, true);
11061
+ const { lines: lines2 } = await searchEverywhere(query, deps);
11062
+ return text([...lines2, "", "Pass a run ID to flatbread_get_capsule for what that run learned."].join("\n"));
11063
+ }
11064
+ async function readCapsule(ref, deps) {
11065
+ if (ref.site === void 0) {
11066
+ const stored = localRecords(deps).records.find((record) => record.slug === ref.slug);
11067
+ if (stored !== void 0) return { ok: true, capsule: buildCapsule(stored.trace), from: "store" };
11068
+ const captured = deps.bucketDir !== null ? readBucketRecord(deps.bucketDir, ref.slug) : null;
11069
+ if (captured !== null) return { ok: true, capsule: buildCapsule(captured.trace), from: "bucket" };
11070
+ }
11071
+ const remote = await deps.capsule(ref);
11072
+ return remote.ok ? { ok: true, capsule: remote.capsule, from: "site" } : remote;
10493
11073
  }
10494
11074
  async function capsuleTool(args, deps) {
10495
11075
  const raw = args["slug"];
10496
11076
  if (typeof raw !== "string") return text("slug must be a run ID or a /r/<id> link", true);
10497
11077
  const ref = parseRunRef(raw);
10498
11078
  if (ref === null) return text(`"${raw}" is not a run ID or a /r/<id> link`, true);
10499
- 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));
10502
- }
10503
- const remote = await deps.capsule(ref);
10504
- if (remote.ok) return text(remote.capsule);
10505
- return text(describeReadFailure(remote, `capsule ${ref.slug}`), true);
11079
+ const read2 = await readCapsule(ref, deps);
11080
+ if (read2.ok) return text(read2.capsule);
11081
+ return text(describeReadFailure(read2, `capsule ${ref.slug}`), true);
10506
11082
  }
10507
11083
  async function handleMcpMessage(message, deps) {
10508
11084
  if (typeof message !== "object" || message === null || Array.isArray(message)) {
@@ -10551,7 +11127,7 @@ async function handleMcpMessage(message, deps) {
10551
11127
  }
10552
11128
  async function runMcpServer(deps, io) {
10553
11129
  const input = io?.input ?? process.stdin;
10554
- const write = io?.write ?? ((line) => process.stdout.write(line));
11130
+ const write2 = io?.write ?? ((line) => process.stdout.write(line));
10555
11131
  const log = io?.log ?? ((line) => process.stderr.write(`${line}
10556
11132
  `));
10557
11133
  log(`flatbread mcp ${MCP_SERVER_VERSION} on stdio${deps.storeDir === null ? " (no local store)" : ` (local store ${deps.storeDir})`}`);
@@ -10564,12 +11140,12 @@ async function runMcpServer(deps, io) {
10564
11140
  try {
10565
11141
  message = JSON.parse(line);
10566
11142
  } catch {
10567
- write(`${JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } })}
11143
+ write2(`${JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } })}
10568
11144
  `);
10569
11145
  return;
10570
11146
  }
10571
11147
  const reply = await handleMcpMessage(message, deps);
10572
- if (reply !== null) write(`${JSON.stringify(reply)}
11148
+ if (reply !== null) write2(`${JSON.stringify(reply)}
10573
11149
  `);
10574
11150
  });
10575
11151
  }
@@ -10577,10 +11153,251 @@ async function runMcpServer(deps, io) {
10577
11153
  return 0;
10578
11154
  }
10579
11155
 
11156
+ // src/claude-mcp.ts
11157
+ import fs19 from "node:fs";
11158
+ import os7 from "node:os";
11159
+ import path18 from "node:path";
11160
+ var MCP_SERVER_NAME = "flatbread";
11161
+ function claudeUserConfigPath(env = process.env) {
11162
+ const dir = env["CLAUDE_CONFIG_DIR"]?.trim();
11163
+ return path18.join(dir ? dir : os7.homedir(), ".claude.json");
11164
+ }
11165
+ function mcpEntryFor(execPath, entryPath) {
11166
+ return { type: "stdio", command: execPath, args: [entryPath, "mcp"] };
11167
+ }
11168
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
11169
+ function runsFlatbreadMcp(entry) {
11170
+ if (!isObject(entry) || !Array.isArray(entry["args"])) return false;
11171
+ const words = [entry["command"], ...entry["args"]].filter((word) => typeof word === "string");
11172
+ return words[words.length - 1] === "mcp" && words.some((word) => /flatbread/i.test(word));
11173
+ }
11174
+ var BOM = "\uFEFF";
11175
+ function readConfig(file) {
11176
+ let raw;
11177
+ try {
11178
+ raw = fs19.readFileSync(file, "utf8");
11179
+ } catch (error) {
11180
+ if (error.code === "ENOENT") return { root: {}, raw: null };
11181
+ return { problem: error.message };
11182
+ }
11183
+ const text2 = raw.startsWith(BOM) ? raw.slice(1) : raw;
11184
+ if (text2.trim() === "") return { root: {}, raw };
11185
+ let parsed;
11186
+ try {
11187
+ parsed = JSON.parse(text2);
11188
+ } catch (error) {
11189
+ return { problem: `not JSON (${error.message})` };
11190
+ }
11191
+ if (!isObject(parsed)) return { problem: "not a JSON object" };
11192
+ if (parsed["mcpServers"] !== void 0 && !isObject(parsed["mcpServers"])) return { problem: "its mcpServers is not an object" };
11193
+ return { root: parsed, raw };
11194
+ }
11195
+ function serializeLike(raw, root) {
11196
+ let body = JSON.stringify(root, null, 2);
11197
+ if (raw === null || raw.trimEnd() !== raw || raw.trim() === "") body += "\n";
11198
+ if (raw !== null && raw.includes("\r\n")) body = body.replace(/\n/g, "\r\n");
11199
+ if (raw !== null && raw.startsWith(BOM)) body = BOM + body;
11200
+ return body;
11201
+ }
11202
+ function write(file, read2, next) {
11203
+ if (read2.raw !== null) {
11204
+ try {
11205
+ fs19.accessSync(file, fs19.constants.W_OK);
11206
+ } catch {
11207
+ return `${file} is read-only, so it was left as it was`;
11208
+ }
11209
+ if (fs19.readFileSync(file, "utf8") !== read2.raw) return `${file} changed while setup ran (Claude Code writes it); run setup again`;
11210
+ const backup = `${file}.flatbread.bak`;
11211
+ if (!fs19.existsSync(backup)) fs19.copyFileSync(file, backup);
11212
+ }
11213
+ const mode = read2.raw !== null ? fs19.statSync(file).mode & 511 : 384;
11214
+ fs19.mkdirSync(path18.dirname(file), { recursive: true });
11215
+ const temp = `${file}.flatbread-${process.pid}.tmp`;
11216
+ fs19.writeFileSync(temp, next, { encoding: "utf8", mode });
11217
+ fs19.renameSync(temp, file);
11218
+ return null;
11219
+ }
11220
+ function registerClaudeMcp(options) {
11221
+ const { file, log } = options;
11222
+ const read2 = readConfig(file);
11223
+ if ("problem" in read2) {
11224
+ log(` mcp refused: ${file}: ${read2.problem}`);
11225
+ log(" Claude Code's own state lives there, so a file we cannot read is not one we rewrite.");
11226
+ if (options.uninstall !== true) log(` by hand: claude mcp add --scope user ${MCP_SERVER_NAME} -- "${options.execPath}" "${options.entryPath}" mcp`);
11227
+ return 1;
11228
+ }
11229
+ const servers = isObject(read2.root["mcpServers"]) ? read2.root["mcpServers"] : void 0;
11230
+ const current = servers?.[MCP_SERVER_NAME];
11231
+ if (options.uninstall === true) {
11232
+ if (current === void 0) {
11233
+ log(` mcp no ${MCP_SERVER_NAME} server in ${file}`);
11234
+ return 0;
11235
+ }
11236
+ if (!runsFlatbreadMcp(current)) {
11237
+ log(` mcp ${file}: the ${MCP_SERVER_NAME} server there does not run flatbread, left alone`);
11238
+ return 0;
11239
+ }
11240
+ if (options.dryRun === true) {
11241
+ log(` mcp ${file}: would remove the ${MCP_SERVER_NAME} server`);
11242
+ return 0;
11243
+ }
11244
+ const rest = { ...servers };
11245
+ delete rest[MCP_SERVER_NAME];
11246
+ const problem2 = write(file, read2, serializeLike(read2.raw, { ...read2.root, mcpServers: rest }));
11247
+ if (problem2 !== null) {
11248
+ log(` mcp refused: ${problem2}`);
11249
+ return 1;
11250
+ }
11251
+ log(` mcp ${file}: removed the ${MCP_SERVER_NAME} server`);
11252
+ const backup = `${file}.flatbread.bak`;
11253
+ if (fs19.existsSync(backup)) log(` ${backup} is the copy setup took before its first write; delete it when you like`);
11254
+ return 0;
11255
+ }
11256
+ const entry = mcpEntryFor(options.execPath, options.entryPath);
11257
+ if (current !== void 0 && !runsFlatbreadMcp(current)) {
11258
+ log(` mcp refused: ${file} already has a server named ${MCP_SERVER_NAME} that does not run flatbread, left alone`);
11259
+ return 1;
11260
+ }
11261
+ if (JSON.stringify(current) === JSON.stringify(entry)) {
11262
+ log(` mcp ${file}: registered for every project, unchanged`);
11263
+ return 0;
11264
+ }
11265
+ const verb = current === void 0 ? "register" : "update";
11266
+ if (options.dryRun === true) {
11267
+ log(` mcp ${file}: would ${verb} "${MCP_SERVER_NAME}": ${JSON.stringify(entry)}`);
11268
+ return 0;
11269
+ }
11270
+ const root = { ...read2.root, mcpServers: { ...servers ?? {}, [MCP_SERVER_NAME]: entry } };
11271
+ const problem = write(file, read2, serializeLike(read2.raw, root));
11272
+ if (problem !== null) {
11273
+ log(` mcp refused: ${problem}`);
11274
+ return 1;
11275
+ }
11276
+ log(` mcp ${file}: ${verb === "register" ? "registered" : "updated"} for every project (user scope)`);
11277
+ log(" restart Claude Code (or reload the VS Code window) so it starts the server");
11278
+ return 0;
11279
+ }
11280
+
11281
+ // src/git-hook.ts
11282
+ import fs20 from "node:fs";
11283
+ import path19 from "node:path";
11284
+ var POST_COMMIT_MARKER = "# flatbread post-commit hook - remove with: flatbread setup --uninstall";
11285
+ var CHAINED_HOOK = "post-commit.pre-flatbread";
11286
+ var sq = (value) => `'${value.replace(/\\/g, "/").replace(/'/g, "'\\''")}'`;
11287
+ function captureLine(execPath, entryPath) {
11288
+ return `${sq(execPath)} ${sq(entryPath)} capture --commit "$(git rev-parse HEAD)" --repo "$(git rev-parse --show-toplevel)" --quiet >/dev/null 2>&1 &`;
11289
+ }
11290
+ function postCommitScript(execPath, entryPath) {
11291
+ return [
11292
+ "#!/bin/sh",
11293
+ POST_COMMIT_MARKER,
11294
+ "# Runs the hook that was here before, then records this commit against the newest agent session",
11295
+ "# in this repository, on this machine only. It never changes the commit; it exits with the",
11296
+ "# earlier hook's status. Off switch: FLATBREAD_HOOK_DISABLED=1",
11297
+ "status=0",
11298
+ `prior="$(dirname "$0")/${CHAINED_HOOK}"`,
11299
+ 'if [ -x "$prior" ]; then',
11300
+ ' "$prior" "$@"',
11301
+ " status=$?",
11302
+ "fi",
11303
+ 'if [ -z "$FLATBREAD_HOOK_DISABLED" ]; then',
11304
+ ` ( ${captureLine(execPath, entryPath)} )`,
11305
+ "fi",
11306
+ 'exit "$status"',
11307
+ ""
11308
+ ].join("\n");
11309
+ }
11310
+ var isOurs = (body) => body.includes(POST_COMMIT_MARKER);
11311
+ function hooksDirFor(cwd, options = {}) {
11312
+ const top = git(cwd, ["rev-parse", "--show-toplevel"], options)?.trim();
11313
+ if (top === void 0 || top.length === 0) return { ok: false, reason: "not-a-repo", detail: `${cwd} is not inside a git repository` };
11314
+ const managed = git(top, ["config", "--show-scope", "--get", "core.hooksPath"], options)?.trim();
11315
+ let machineHooksPath;
11316
+ if (managed !== void 0 && managed.length > 0) {
11317
+ const tab = managed.indexOf(" ");
11318
+ const scope = tab > 0 ? managed.slice(0, tab) : "local";
11319
+ const value = tab > 0 ? managed.slice(tab + 1) : managed;
11320
+ if (scope !== "system" && scope !== "global") {
11321
+ return { ok: false, reason: "hooks-path", detail: `core.hooksPath is set to ${value} in this repository, so another tool owns its hooks` };
11322
+ }
11323
+ machineHooksPath = { scope, path: value };
11324
+ }
11325
+ const common = git(top, ["rev-parse", "--git-common-dir"], options)?.trim();
11326
+ if (common === void 0 || common.length === 0) return { ok: false, reason: "unreadable", detail: `git could not name the git directory of ${top}` };
11327
+ return { ok: true, repo: path19.resolve(top), dir: path19.resolve(top, common, "hooks"), ...machineHooksPath !== void 0 ? { machineHooksPath } : {} };
11328
+ }
11329
+ var read = (file) => {
11330
+ try {
11331
+ return fs20.readFileSync(file, "utf8");
11332
+ } catch {
11333
+ return null;
11334
+ }
11335
+ };
11336
+ function installGitHook(dir, script, options = {}) {
11337
+ const hook = path19.join(dir, "post-commit");
11338
+ const chained = path19.join(dir, CHAINED_HOOK);
11339
+ const current = read(hook);
11340
+ const write2 = (lines2) => {
11341
+ if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
11342
+ fs20.mkdirSync(dir, { recursive: true });
11343
+ if (current !== null && !isOurs(current)) fs20.renameSync(hook, chained);
11344
+ fs20.writeFileSync(hook, script, { encoding: "utf8", mode: 493 });
11345
+ fs20.chmodSync(hook, 493);
11346
+ return { code: 0, lines: lines2 };
11347
+ };
11348
+ if (current === null) return write2([`post-commit ${hook} (new)`]);
11349
+ if (isOurs(current)) {
11350
+ if (current === script) return { code: 0, lines: [`post-commit ${hook}`, "already installed, unchanged"] };
11351
+ return write2([`post-commit ${hook}`, "updated the flatbread hook"]);
11352
+ }
11353
+ if (fs20.existsSync(chained)) {
11354
+ return {
11355
+ code: 1,
11356
+ lines: [
11357
+ `refused ${hook}`,
11358
+ `${chained} already exists, and post-commit is not ours. Neither file was touched.`,
11359
+ "Move one of them yourself, then run setup again."
11360
+ ]
11361
+ };
11362
+ }
11363
+ return write2([
11364
+ `post-commit ${hook}`,
11365
+ `your post-commit was kept as ${CHAINED_HOOK} and runs first, unchanged, with its exit status`
11366
+ ]);
11367
+ }
11368
+ function uninstallGitHook(dir, options = {}) {
11369
+ const hook = path19.join(dir, "post-commit");
11370
+ const chained = path19.join(dir, CHAINED_HOOK);
11371
+ const current = read(hook);
11372
+ const hasChained = fs20.existsSync(chained);
11373
+ const done = (lines2, act) => {
11374
+ if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
11375
+ act();
11376
+ return { code: 0, lines: lines2 };
11377
+ };
11378
+ if (current !== null && !isOurs(current)) {
11379
+ return {
11380
+ code: 0,
11381
+ lines: [
11382
+ `post-commit ${hook} is not ours, left alone`,
11383
+ ...hasChained ? [`${chained} is still there; it is yours to keep or delete`] : []
11384
+ ]
11385
+ };
11386
+ }
11387
+ if (current === null && !hasChained) return { code: 0, lines: [`no flatbread post-commit in ${dir}`] };
11388
+ if (hasChained) {
11389
+ return done([`post-commit ${hook}`, `restored your post-commit from ${CHAINED_HOOK}`], () => {
11390
+ if (current !== null) fs20.rmSync(hook);
11391
+ fs20.renameSync(chained, hook);
11392
+ });
11393
+ }
11394
+ return done([`post-commit ${hook}`, "removed the flatbread hook"], () => fs20.rmSync(hook));
11395
+ }
11396
+
10580
11397
  // src/hook.ts
10581
- import { spawnSync as spawnSync2 } from "node:child_process";
10582
- import fs18 from "node:fs";
10583
- import path17 from "node:path";
11398
+ import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
11399
+ import fs21 from "node:fs";
11400
+ import path20 from "node:path";
10584
11401
  var HOOK_EVENTS = ["Stop", "SessionEnd"];
10585
11402
  var OFFER_AFTER_STOPS = 2;
10586
11403
  var MAX_TRACKED_SESSIONS = 50;
@@ -10590,14 +11407,14 @@ function emptyHookState() {
10590
11407
  return { version: HOOK_STATE_VERSION, sessions: {} };
10591
11408
  }
10592
11409
  function hookStatePath(env = process.env) {
10593
- return path17.join(flatbreadHome(env), "hook-state.json");
11410
+ return path20.join(flatbreadHome(env), "hook-state.json");
10594
11411
  }
10595
11412
  function readHookState(env = process.env) {
10596
11413
  const file = hookStatePath(env);
10597
- if (!fs18.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
11414
+ if (!fs21.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
10598
11415
  let raw;
10599
11416
  try {
10600
- raw = fs18.readFileSync(file, "utf8");
11417
+ raw = fs21.readFileSync(file, "utf8");
10601
11418
  } catch (error) {
10602
11419
  return { ok: false, reason: "unreadable", detail: error.message };
10603
11420
  }
@@ -10640,9 +11457,9 @@ function pruneHookState(state, keep = MAX_TRACKED_SESSIONS) {
10640
11457
  }
10641
11458
  function writeHookState(state, env = process.env) {
10642
11459
  const dir = flatbreadHome(env);
10643
- fs18.mkdirSync(dir, { recursive: true, mode: 448 });
11460
+ fs21.mkdirSync(dir, { recursive: true, mode: 448 });
10644
11461
  const file = hookStatePath(env);
10645
- fs18.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
11462
+ fs21.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
10646
11463
  encoding: "utf8",
10647
11464
  mode: 384
10648
11465
  });
@@ -10777,12 +11594,18 @@ function offerAfterFrom(env) {
10777
11594
  }
10778
11595
  return { value: parsed };
10779
11596
  }
11597
+ function spawnDetachedDefault(command, args) {
11598
+ const child = spawn2(command, args, { detached: true, stdio: "ignore", windowsHide: true });
11599
+ child.on("error", () => {
11600
+ });
11601
+ child.unref();
11602
+ }
10780
11603
  async function runHookNotify(io) {
10781
11604
  const env = io.env ?? process.env;
10782
11605
  const stdout = io.stdout ?? ((line) => process.stdout.write(line + "\n"));
10783
11606
  const stderr = io.stderr ?? ((line) => process.stderr.write(line + "\n"));
10784
11607
  const now = io.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
10785
- const exists = io.exists ?? ((filePath) => fs18.existsSync(filePath));
11608
+ const exists = io.exists ?? ((filePath) => fs21.existsSync(filePath));
10786
11609
  const execPath = io.execPath ?? process.execPath;
10787
11610
  const entryPath = io.entryPath ?? process.argv[1] ?? "";
10788
11611
  try {
@@ -10796,14 +11619,14 @@ async function runHookNotify(io) {
10796
11619
  stderr(`flatbread hook: NOT RETRIEVED (${parsed.detail})`);
10797
11620
  return 0;
10798
11621
  }
10799
- const read = readHookState(env);
10800
- if (!read.ok) {
10801
- stderr(`flatbread hook: NOT RETRIEVED (${read.reason}: ${read.detail}) at ${hookStatePath(env)}`);
11622
+ const read2 = readHookState(env);
11623
+ if (!read2.ok) {
11624
+ stderr(`flatbread hook: NOT RETRIEVED (${read2.reason}: ${read2.detail}) at ${hookStatePath(env)}`);
10802
11625
  return 0;
10803
11626
  }
10804
11627
  const offerAfter = offerAfterFrom(env);
10805
11628
  if (offerAfter.ignored) stderr(`flatbread hook: ${offerAfter.ignored}, using ${OFFER_AFTER_STOPS}`);
10806
- const decision = decideHook(parsed.event, read.state, {
11629
+ const decision = decideHook(parsed.event, read2.state, {
10807
11630
  now: now(),
10808
11631
  exists,
10809
11632
  offerAfter: offerAfter.value,
@@ -10811,7 +11634,7 @@ async function runHookNotify(io) {
10811
11634
  entryPath
10812
11635
  });
10813
11636
  let recorded = true;
10814
- if (decision.nextState !== read.state) {
11637
+ if (decision.nextState !== read2.state) {
10815
11638
  try {
10816
11639
  writeHookState(decision.nextState, env);
10817
11640
  } catch (error) {
@@ -10819,6 +11642,11 @@ async function runHookNotify(io) {
10819
11642
  stderr(`flatbread hook: could not record state (${error.message})`);
10820
11643
  }
10821
11644
  }
11645
+ const transcript = parsed.event.transcript_path;
11646
+ if (io.capture === true && parsed.event.hook_event_name === "SessionEnd" && transcript !== void 0 && exists(transcript)) {
11647
+ (io.spawnDetached ?? spawnDetachedDefault)(execPath, [entryPath, "capture", "--file", transcript, "--quiet"]);
11648
+ stderr("flatbread hook: capturing this session into the local bucket, in the background");
11649
+ }
10822
11650
  if (decision.speak && recorded) {
10823
11651
  stdout(JSON.stringify({ systemMessage: decision.systemMessage }));
10824
11652
  stderr(`flatbread hook: ${decision.why}`);
@@ -10839,7 +11667,7 @@ function hookHandlerFor(options) {
10839
11667
  const handler = {
10840
11668
  type: "command",
10841
11669
  command: options.execPath,
10842
- args: [options.entryPath, "hook", "notify"]
11670
+ args: [options.entryPath, "hook", "notify", ...options.capture === true && options.event === "SessionEnd" ? ["--capture"] : []]
10843
11671
  };
10844
11672
  if (options.event === "Stop") handler.timeout = STOP_TIMEOUT_SECONDS;
10845
11673
  return handler;
@@ -10934,10 +11762,10 @@ function removeHookConfig(settings) {
10934
11762
  return { settings: root, removed };
10935
11763
  }
10936
11764
  function readSettingsFile(file) {
10937
- if (!fs18.existsSync(file)) return { present: false, file };
11765
+ if (!fs21.existsSync(file)) return { present: false, file };
10938
11766
  let raw;
10939
11767
  try {
10940
- raw = fs18.readFileSync(file, "utf8");
11768
+ raw = fs21.readFileSync(file, "utf8");
10941
11769
  } catch (error) {
10942
11770
  return { present: true, file, unreadable: error.message };
10943
11771
  }
@@ -10949,19 +11777,19 @@ function readSettingsFile(file) {
10949
11777
  }
10950
11778
  }
10951
11779
  function writeSettingsFile(file, settings) {
10952
- fs18.mkdirSync(path17.dirname(file), { recursive: true });
11780
+ fs21.mkdirSync(path20.dirname(file), { recursive: true });
10953
11781
  const temp = `${file}.flatbread-${process.pid}.tmp`;
10954
- fs18.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
10955
- fs18.renameSync(temp, file);
11782
+ fs21.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
11783
+ fs21.renameSync(temp, file);
10956
11784
  }
10957
- function handlersFor(execPath, entryPath) {
11785
+ function handlersFor(execPath, entryPath, capture = false) {
10958
11786
  return {
10959
11787
  Stop: hookHandlerFor({ execPath, entryPath, event: "Stop" }),
10960
- SessionEnd: hookHandlerFor({ execPath, entryPath, event: "SessionEnd" })
11788
+ SessionEnd: hookHandlerFor({ execPath, entryPath, event: "SessionEnd", capture })
10961
11789
  };
10962
11790
  }
10963
- function handlerBlockFor(execPath, entryPath) {
10964
- const handlers = handlersFor(execPath, entryPath);
11791
+ function handlerBlockFor(execPath, entryPath, capture = false) {
11792
+ const handlers = handlersFor(execPath, entryPath, capture);
10965
11793
  const hooks = {};
10966
11794
  for (const event of HOOK_EVENTS) hooks[event] = [{ hooks: [handlers[event]] }];
10967
11795
  return JSON.stringify({ hooks }, null, 2);
@@ -10972,21 +11800,22 @@ function runHookInstall(options = {}) {
10972
11800
  const execPath = options.execPath ?? process.execPath;
10973
11801
  const entryPath = options.entryPath ?? process.argv[1] ?? "";
10974
11802
  const file = options.settingsPath ?? claudeSettingsPath(env);
10975
- const read = readSettingsFile(file);
10976
- if (read.present && "unreadable" in read) {
11803
+ const capture = options.capture === true;
11804
+ const read2 = readSettingsFile(file);
11805
+ if (read2.present && "unreadable" in read2) {
10977
11806
  log("");
10978
11807
  log(` refused ${file}`);
10979
- log(` ${read.unreadable}`);
11808
+ log(` ${read2.unreadable}`);
10980
11809
  log(" a settings file we cannot read is not one we will rewrite. Add this by hand:");
10981
11810
  log("");
10982
- for (const line of handlerBlockFor(execPath, entryPath).split("\n")) log(` ${line}`);
11811
+ for (const line of handlerBlockFor(execPath, entryPath, capture).split("\n")) log(` ${line}`);
10983
11812
  log("");
10984
11813
  return 1;
10985
11814
  }
10986
- const current = read.present ? read.parsed : {};
11815
+ const current = read2.present ? read2.parsed : {};
10987
11816
  let merged;
10988
11817
  try {
10989
- merged = mergeHookConfig(current, handlersFor(execPath, entryPath));
11818
+ merged = mergeHookConfig(current, handlersFor(execPath, entryPath, capture));
10990
11819
  } catch (error) {
10991
11820
  log("");
10992
11821
  log(` refused ${file}`);
@@ -10995,7 +11824,7 @@ function runHookInstall(options = {}) {
10995
11824
  return 1;
10996
11825
  }
10997
11826
  log("");
10998
- log(` settings ${file}${read.present ? "" : " (new)"}`);
11827
+ log(` settings ${file}${read2.present ? "" : " (new)"}`);
10999
11828
  log(` command "${execPath}" "${entryPath}" hook notify`);
11000
11829
  if (merged.changes.length === 0) {
11001
11830
  log(" already installed, unchanged");
@@ -11019,7 +11848,7 @@ function runHookInstall(options = {}) {
11019
11848
  log(" refused: nothing was written, because this command line does not work here.");
11020
11849
  log(" fix the command (or pass --no-probe if you know better), then install again:");
11021
11850
  log("");
11022
- for (const line of handlerBlockFor(execPath, entryPath).split("\n")) log(` ${line}`);
11851
+ for (const line of handlerBlockFor(execPath, entryPath, capture).split("\n")) log(` ${line}`);
11023
11852
  log("");
11024
11853
  return 1;
11025
11854
  }
@@ -11048,7 +11877,9 @@ function runHookInstall(options = {}) {
11048
11877
  log("");
11049
11878
  return 1;
11050
11879
  }
11051
- log(" wrote it. Stop offers the share once per session; SessionEnd only records the end.");
11880
+ log(
11881
+ 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."
11882
+ );
11052
11883
  log(" off switch: FLATBREAD_HOOK_DISABLED=1, or flatbread hook uninstall");
11053
11884
  log("");
11054
11885
  return 0;
@@ -11057,23 +11888,23 @@ function runHookUninstall(options = {}) {
11057
11888
  const env = options.env ?? process.env;
11058
11889
  const log = options.log ?? ((line) => console.log(line));
11059
11890
  const file = options.settingsPath ?? claudeSettingsPath(env);
11060
- const read = readSettingsFile(file);
11891
+ const read2 = readSettingsFile(file);
11061
11892
  log("");
11062
- if (!read.present) {
11893
+ if (!read2.present) {
11063
11894
  log(` nothing to remove: ${file} does not exist`);
11064
11895
  log("");
11065
11896
  return 0;
11066
11897
  }
11067
- if ("unreadable" in read) {
11898
+ if ("unreadable" in read2) {
11068
11899
  log(` refused ${file}`);
11069
- log(` ${read.unreadable}`);
11900
+ log(` ${read2.unreadable}`);
11070
11901
  log(" remove the flatbread entries by hand rather than let us rewrite a file we cannot read");
11071
11902
  log("");
11072
11903
  return 1;
11073
11904
  }
11074
11905
  let result;
11075
11906
  try {
11076
- result = removeHookConfig(read.parsed);
11907
+ result = removeHookConfig(read2.parsed);
11077
11908
  } catch (error) {
11078
11909
  log(` refused ${error instanceof Error ? error.message : String(error)}`);
11079
11910
  log("");
@@ -11084,6 +11915,13 @@ function runHookUninstall(options = {}) {
11084
11915
  log("");
11085
11916
  return 0;
11086
11917
  }
11918
+ if (options.dryRun === true) {
11919
+ log(` settings ${file}`);
11920
+ for (const line of result.removed) log(` ${line}`);
11921
+ log(" --dry-run: nothing was written");
11922
+ log("");
11923
+ return 0;
11924
+ }
11087
11925
  try {
11088
11926
  writeSettingsFile(file, result.settings);
11089
11927
  } catch (error) {
@@ -11100,8 +11938,8 @@ function runHookUninstall(options = {}) {
11100
11938
  function settingsCandidates(env, cwd) {
11101
11939
  return [
11102
11940
  { 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") }
11941
+ { label: "project", file: path20.join(cwd, ".claude", "settings.json") },
11942
+ { label: "local", file: path20.join(cwd, ".claude", "settings.local.json") }
11105
11943
  ];
11106
11944
  }
11107
11945
  function installedEventsIn(parsed, exists) {
@@ -11131,23 +11969,23 @@ function runHookStatus(options = {}) {
11131
11969
  const env = options.env ?? process.env;
11132
11970
  const log = options.log ?? ((line) => console.log(line));
11133
11971
  const cwd = options.cwd ?? process.cwd();
11134
- const exists = options.exists ?? ((filePath) => fs18.existsSync(filePath));
11972
+ const exists = options.exists ?? ((filePath) => fs21.existsSync(filePath));
11135
11973
  const candidates = options.settingsPath === void 0 ? settingsCandidates(env, cwd) : [{ label: "settings", file: options.settingsPath }];
11136
11974
  log("");
11137
11975
  log("flatbread hook");
11138
11976
  log("");
11139
11977
  let installed = 0;
11140
11978
  for (const candidate of candidates) {
11141
- const read2 = readSettingsFile(candidate.file);
11142
- if (!read2.present) {
11979
+ const read3 = readSettingsFile(candidate.file);
11980
+ if (!read3.present) {
11143
11981
  log(` ${candidate.label.padEnd(8)} ${candidate.file} (absent)`);
11144
11982
  continue;
11145
11983
  }
11146
- if ("unreadable" in read2) {
11147
- log(` ${candidate.label.padEnd(8)} ${candidate.file} NOT RETRIEVED (${read2.unreadable})`);
11984
+ if ("unreadable" in read3) {
11985
+ log(` ${candidate.label.padEnd(8)} ${candidate.file} NOT RETRIEVED (${read3.unreadable})`);
11148
11986
  continue;
11149
11987
  }
11150
- const found = installedEventsIn(read2.parsed, exists);
11988
+ const found = installedEventsIn(read3.parsed, exists);
11151
11989
  installed += found.events.length;
11152
11990
  const summary = found.events.length === 0 ? "(no flatbread handler)" : found.events.join(", ");
11153
11991
  log(` ${candidate.label.padEnd(8)} ${candidate.file} ${summary}`);
@@ -11171,15 +12009,15 @@ function runHookStatus(options = {}) {
11171
12009
  }
11172
12010
  }
11173
12011
  log("");
11174
- const read = readHookState(env);
11175
- if (!read.ok) {
11176
- log(` state NOT RETRIEVED (${read.reason}: ${read.detail})`);
12012
+ const read2 = readHookState(env);
12013
+ if (!read2.ok) {
12014
+ log(` state NOT RETRIEVED (${read2.reason}: ${read2.detail})`);
11177
12015
  log(` ${hookStatePath(env)}`);
11178
12016
  log("");
11179
12017
  return 1;
11180
12018
  }
11181
- const sessions = Object.entries(read.state.sessions);
11182
- log(` state ${hookStatePath(env)}${read.present ? "" : " (none yet)"}`);
12019
+ const sessions = Object.entries(read2.state.sessions);
12020
+ log(` state ${hookStatePath(env)}${read2.present ? "" : " (none yet)"}`);
11183
12021
  log(` sessions ${sessions.length}`);
11184
12022
  log(` offered ${sessions.filter(([, session]) => session.offeredAt !== void 0).length}`);
11185
12023
  const recent = [...sessions].sort((a, b) => b[1].lastSeenAt > a[1].lastSeenAt ? 1 : -1).slice(0, 5);
@@ -11251,8 +12089,92 @@ function probeHandler(handler, run = spawnSync2) {
11251
12089
  return { ok: true, detail: "ran, exited 0, printed nothing" };
11252
12090
  }
11253
12091
 
12092
+ // src/setup.ts
12093
+ function runSetup(options = {}) {
12094
+ const env = options.env ?? process.env;
12095
+ const log = options.log ?? ((line) => console.log(line));
12096
+ const execPath = options.execPath ?? process.execPath;
12097
+ const entryPath = options.entryPath ?? process.argv[1] ?? "";
12098
+ const dryRun = options.dryRun === true;
12099
+ const settings = options.settingsPath !== void 0 ? { settingsPath: options.settingsPath } : {};
12100
+ if (options.uninstall !== true && /[\\/]_npx[\\/]/.test(entryPath)) {
12101
+ log("");
12102
+ log(` refused: this flatbread runs from npx's cache (${entryPath}),`);
12103
+ log(" which npm may clear - and a hook that names a missing file fails silently. Install it, then run setup:");
12104
+ log(" npm install -g flatbread-cli");
12105
+ log(" flatbread setup");
12106
+ log("");
12107
+ return 1;
12108
+ }
12109
+ log("");
12110
+ log("Claude Code");
12111
+ const claude = options.uninstall === true ? runHookUninstall({ env, log, dryRun, ...settings }) : runHookInstall({
12112
+ env,
12113
+ log,
12114
+ execPath,
12115
+ entryPath,
12116
+ dryRun,
12117
+ capture: true,
12118
+ ...options.noProbe === true ? { noProbe: true } : {},
12119
+ ...options.spawn !== void 0 ? { spawn: options.spawn } : {},
12120
+ ...settings
12121
+ });
12122
+ let mcp = 0;
12123
+ if (options.noMcp === true) {
12124
+ log(" --no-mcp: Claude Code's MCP servers were not touched");
12125
+ } else if (claude === 0 || options.uninstall === true) {
12126
+ mcp = registerClaudeMcp({
12127
+ file: options.claudeConfigPath ?? claudeUserConfigPath(env),
12128
+ execPath,
12129
+ entryPath,
12130
+ uninstall: options.uninstall === true,
12131
+ dryRun,
12132
+ log
12133
+ });
12134
+ } else {
12135
+ log(" mcp not registered, because the hook step above was refused");
12136
+ }
12137
+ log("");
12138
+ let git2 = 0;
12139
+ log("git");
12140
+ if (options.noGit === true) {
12141
+ log(" --no-git: the post-commit hook was not touched");
12142
+ } else {
12143
+ const where = hooksDirFor(options.repo ?? process.cwd(), options.git !== void 0 ? { git: options.git } : {});
12144
+ if (!where.ok) {
12145
+ log(` skipped ${where.detail}`);
12146
+ if (where.reason === "hooks-path" && options.uninstall !== true) {
12147
+ log(" add this line to that post-commit yourself to capture commits:");
12148
+ log(` ${captureLine(execPath, entryPath)}`);
12149
+ } else if (where.reason === "not-a-repo" && options.uninstall !== true) {
12150
+ log(" run setup inside a repository to bind its commits to your sessions");
12151
+ }
12152
+ if (where.reason === "unreadable") git2 = 1;
12153
+ } else {
12154
+ const outcome = options.uninstall === true ? uninstallGitHook(where.dir, { dryRun }) : installGitHook(where.dir, postCommitScript(execPath, entryPath), { dryRun });
12155
+ for (const line of outcome.lines) log(` ${line}`);
12156
+ if (where.machineHooksPath !== void 0 && options.uninstall !== true && outcome.code === 0) {
12157
+ log(` note git runs ${where.machineHooksPath.path} first (core.hooksPath, ${where.machineHooksPath.scope} config).`);
12158
+ log(" This hook fires only if that one runs the repository's own post-commit.");
12159
+ log(` After your next commit, ${bucketDir(env)}/capture.log says whether it did.`);
12160
+ }
12161
+ git2 = outcome.code;
12162
+ }
12163
+ }
12164
+ log("");
12165
+ if (options.uninstall === true) {
12166
+ log(` the bucket is left alone: ${bucketDir(env)}`);
12167
+ } else {
12168
+ log(` captures go to ${bucketDir(env)}: redacted, private, never uploaded`);
12169
+ log(" find them with: flatbread search <words>");
12170
+ log(" share one with: flatbread share --file <its session>, which opens the review");
12171
+ }
12172
+ log("");
12173
+ return claude !== 0 || mcp !== 0 || git2 !== 0 ? 1 : 0;
12174
+ }
12175
+
11254
12176
  // src/login-server.ts
11255
- import crypto2 from "node:crypto";
12177
+ import crypto3 from "node:crypto";
11256
12178
  import http2 from "node:http";
11257
12179
  function callbackOrigin(port) {
11258
12180
  return `http://127.0.0.1:${port}`;
@@ -11327,7 +12249,7 @@ function parseDelivery(raw, expectedState, nowSeconds) {
11327
12249
  }
11328
12250
  const given = Buffer.from(state, "utf8");
11329
12251
  const wanted = Buffer.from(expectedState, "utf8");
11330
- if (given.length !== wanted.length || !crypto2.timingSafeEqual(given, wanted)) {
12252
+ if (given.length !== wanted.length || !crypto3.timingSafeEqual(given, wanted)) {
11331
12253
  return { ok: false, problem: "the callback state did not match" };
11332
12254
  }
11333
12255
  const sessionRaw = body["session"];
@@ -11375,7 +12297,7 @@ async function readBody2(req, limitBytes = 64 * 1024) {
11375
12297
  async function startLoginServer(options = {}) {
11376
12298
  const ports = options.ports ?? DEFAULT_LOGIN_PORTS;
11377
12299
  const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
11378
- const state = options.state ?? crypto2.randomBytes(32).toString("hex");
12300
+ const state = options.state ?? crypto3.randomBytes(32).toString("hex");
11379
12301
  let settle;
11380
12302
  const delivery = new Promise((resolve) => {
11381
12303
  settle = resolve;
@@ -11461,26 +12383,35 @@ async function bindFirst(server, ports) {
11461
12383
  }
11462
12384
 
11463
12385
  // 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");
12386
+ var PACKAGE_ROOT = path21.resolve(path21.dirname(fileURLToPath2(import.meta.url)), "..");
12387
+ var SOURCE_CHECKOUT = fs22.existsSync(path21.join(PACKAGE_ROOT, "src", "index.ts"));
12388
+ var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ? path21.join(PACKAGE_ROOT, ".tmp") : path21.join(os8.homedir(), ".flatbread"));
12389
+ var OUTPUT_PATH = path21.join(TMP_DIR, "last-parse.json");
12390
+ var SHARE_OUTPUT_PATH = path21.join(TMP_DIR, "last-share.json");
11469
12391
  function storeDir() {
11470
- return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path18.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
12392
+ return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path21.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
11471
12393
  }
11472
12394
  var USAGE = `flatbread - registry of AI agent runs
11473
12395
 
11474
12396
  Usage:
11475
- flatbread parse --file <path> Parse a session log (harness auto-detected)
12397
+ flatbread parse --file <path> Parse a session log and print its brief. Writes nothing
11476
12398
  flatbread parse --latest Parse the newest session for a harness
11477
12399
  flatbread share Review + sanitize the newest session
11478
12400
  flatbread share --file <path> Review + sanitize a specific session
11479
12401
  flatbread view Write the brief and capsule, print the viewer URL
11480
- flatbread pull <id|url> Print a run's Context Capsule
12402
+ flatbread pull <id|url> Print a run's Context Capsule: a run this machine shared or
12403
+ captured is read here, anything else from the site
11481
12404
  flatbread pull <id|url> --out <file> Write it to a file instead
11482
12405
  flatbread mcp MCP server on stdio: flatbread_search_traces and
11483
12406
  flatbread_get_capsule, for your coding agent
12407
+ flatbread search <words> Find runs: this machine's shared and captured ones, then
12408
+ the site (the same answer the MCP server gives)
12409
+ flatbread setup Auto-capture: the Claude Code hook, plus a chained git
12410
+ post-commit in this repository, and the MCP server for
12411
+ every Claude Code project. Captures stay local
12412
+ flatbread setup --uninstall Remove exactly what setup added
12413
+ flatbread capture --file <path> Capture one session into the local bucket (setup's hooks
12414
+ run this; redacted, private, never uploaded)
11484
12415
  flatbread adapters List harness adapters and their confidence
11485
12416
  flatbread login Sign in through the website, the way npm does
11486
12417
  flatbread login --email <addr> Sign in by email instead (see the note below)
@@ -11496,15 +12427,41 @@ Usage:
11496
12427
  Options for parse:
11497
12428
  --harness <id> Skip detection and name the harness.
11498
12429
  ${HARNESS_IDS.join(", ")}
12430
+ --save Also write the raw (unredacted) trace where view and the
12431
+ local viewer read it
12432
+ --out <file> Write the raw trace to <file> instead
11499
12433
 
11500
12434
  Options for share:
11501
12435
  --harness <id> Skip detection and name the harness
11502
12436
  --publish Upload the run to your account (needs flatbread login)
11503
12437
  --pr <url> Link the pull request this run belongs to (https)
12438
+ --title <text> The run's title (1-${LIMITS.title} characters). The review page
12439
+ opens with it and can still change it
12440
+ --commit [sha] Bind the run to a commit (bare --commit = HEAD) and record
12441
+ whether the working tree was clean against it
11504
12442
  --no-review Redact every match, skip the browser (non-interactive)
11505
12443
  --no-open Start the review server but do not launch a browser
11506
12444
  -h, --help Show this message
11507
12445
 
12446
+ Options for search:
12447
+ --harness <id> --category <kind> Only runs from that harness, or of that kind of job
12448
+ --recovered Only runs where the agent recovered from a failure
12449
+ --local This machine only; the site is not asked
12450
+
12451
+ Options for setup:
12452
+ --dry-run Say what would be written, write nothing
12453
+ --uninstall Remove the handlers, the MCP server and the post-commit;
12454
+ restore a post-commit hook setup had chained
12455
+ --no-git Leave the repository's hooks alone
12456
+ --no-mcp Leave Claude Code's MCP servers alone
12457
+ --settings <path> --no-probe As for hook install
12458
+
12459
+ Options for capture:
12460
+ --commit [sha] Bind the capture to a commit (bare = HEAD)
12461
+ --repo <dir> Without --file: the newest session of that repository,
12462
+ if it moved in the last ${Math.round(COMMIT_SESSION_WINDOW_MS / 6e4)} minutes
12463
+ --quiet Log to the bucket's capture.log and always exit 0 (hooks)
12464
+
11508
12465
  Options for hook install and hook uninstall:
11509
12466
  --settings <path> Settings file to edit. Default: the user settings file
11510
12467
  (~/.claude/settings.json, or CLAUDE_CONFIG_DIR)
@@ -11519,15 +12476,19 @@ Options for adapters:
11519
12476
  Notes:
11520
12477
  parse and share record the git branch, HEAD commit and uncommitted file names when the
11521
12478
  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}
12479
+ parse --save writes ${path21.relative(process.cwd(), OUTPUT_PATH) || OUTPUT_PATH}
12480
+ Share drafts are written to ${path21.relative(process.cwd(), SHARE_OUTPUT_PATH) || SHARE_OUTPUT_PATH}
12481
+ (the latest only; every shared run is also kept under its ID, for search and pull)
11524
12482
  Every parsed run defaults to visibility "private".
11525
12483
  Nothing is uploaded unless you pass --publish, or set FLATBREAD_API_URL and
11526
12484
  FLATBREAD_API_TOKEN yourself. pull and mcp only read: public runs, your own when you
11527
12485
  are signed in, and a private run through its share link. The review UI listens on
11528
- 127.0.0.1 only. Add the MCP server to Claude Code with:
11529
- claude mcp add flatbread -- npx -y flatbread-cli mcp
12486
+ 127.0.0.1 only, on 4321-4325 or, when all five are busy, a port the OS picks.
12487
+ setup registers the MCP server in ~/.claude.json, for every project. By hand instead:
12488
+ claude mcp add --scope user flatbread -- flatbread mcp
11530
12489
  The viewer is a separate dev server: npm run dev -w @flatbread/web
12490
+ Captured runs live in ~/.flatbread/bucket (FLATBREAD_HOOK_DISABLED=1 stops every capture).
12491
+ A capture is not a share: to publish one, run share --file on its session.
11531
12492
  The hook offers ONE LINE at the end of a turn and never uploads anything itself. Turn it
11532
12493
  off with FLATBREAD_HOOK_DISABLED=1, or change when it speaks with
11533
12494
  FLATBREAD_HOOK_OFFER_AFTER=<turns>. It records what it has seen in ~/.flatbread.
@@ -11548,6 +12509,15 @@ function parseArgv(argv) {
11548
12509
  const arg = argv[i];
11549
12510
  if (arg === "--latest") {
11550
12511
  args.latest = true;
12512
+ } else if (arg === "--save") {
12513
+ args.out = OUTPUT_PATH;
12514
+ } else if (arg === "--out") {
12515
+ const value = argv[i + 1];
12516
+ if (value === void 0 || value.startsWith("--")) throw new Error("--out needs a file path");
12517
+ args.out = path21.resolve(value);
12518
+ i += 1;
12519
+ } else if (arg !== void 0 && arg.startsWith("--out=")) {
12520
+ args.out = path21.resolve(arg.slice("--out=".length));
11551
12521
  } else if (arg === "--harness") {
11552
12522
  const value = argv[i + 1];
11553
12523
  if (value === void 0 || value.startsWith("--")) {
@@ -11643,7 +12613,9 @@ function report(result, outputPath) {
11643
12613
  lines2.push("");
11644
12614
  lines2.push(...briefLines(trace));
11645
12615
  lines2.push("");
11646
- lines2.push(` wrote ${outputPath}`);
12616
+ lines2.push(
12617
+ outputPath === void 0 ? ` wrote nothing. --save writes ${OUTPUT_PATH} for the viewer` : ` wrote ${outputPath}`
12618
+ );
11647
12619
  lines2.push("");
11648
12620
  console.log(lines2.join("\n"));
11649
12621
  }
@@ -11666,12 +12638,12 @@ async function runParse(args) {
11666
12638
  target = found[0];
11667
12639
  console.log(`latest ${harness} session: ${target}`);
11668
12640
  } else if (args.file) {
11669
- target = path18.resolve(args.file);
11670
- if (!fs19.existsSync(target)) {
12641
+ target = path21.resolve(args.file);
12642
+ if (!fs22.existsSync(target)) {
11671
12643
  console.error(`file not found: ${target}`);
11672
12644
  return 1;
11673
12645
  }
11674
- if (!fs19.statSync(target).isFile()) {
12646
+ if (!fs22.statSync(target).isFile()) {
11675
12647
  console.error(`not a file: ${target}`);
11676
12648
  return 1;
11677
12649
  }
@@ -11701,9 +12673,11 @@ async function runParse(args) {
11701
12673
  }
11702
12674
  const trace = withLineage(result.trace, readGitLineage(result.trace.source.cwd));
11703
12675
  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);
12676
+ if (args.out !== void 0) {
12677
+ fs22.mkdirSync(path21.dirname(args.out), { recursive: true });
12678
+ fs22.writeFileSync(args.out, JSON.stringify(trace, null, 2) + "\n", "utf8");
12679
+ }
12680
+ report({ ...result, trace }, args.out);
11707
12681
  return 0;
11708
12682
  }
11709
12683
  function parseAdaptersArgv(argv) {
@@ -11765,8 +12739,8 @@ async function runAdapters(args) {
11765
12739
  }
11766
12740
  let input = null;
11767
12741
  if (args.file !== void 0) {
11768
- const resolved = path18.resolve(args.file);
11769
- if (!fs19.existsSync(resolved)) {
12742
+ const resolved = path21.resolve(args.file);
12743
+ if (!fs22.existsSync(resolved)) {
11770
12744
  console.error(`file not found: ${resolved}`);
11771
12745
  return 1;
11772
12746
  }
@@ -11780,6 +12754,16 @@ async function runAdapters(args) {
11780
12754
  console.log("");
11781
12755
  return 0;
11782
12756
  }
12757
+ var COMMIT_REV = /^[A-Za-z0-9][A-Za-z0-9._/~^@{}-]{0,199}$/;
12758
+ function checkCommitRev(value) {
12759
+ if (!COMMIT_REV.test(value)) throw new Error(`--commit needs a commit SHA or ref, e.g. --commit HEAD (got "${value}")`);
12760
+ return value;
12761
+ }
12762
+ function checkTitle(value) {
12763
+ const title = normalizeTitle(value);
12764
+ if (title === null) throw new Error(`--title must be 1-${LIMITS.title} characters`);
12765
+ return title;
12766
+ }
11783
12767
  function parseShareArgv(argv) {
11784
12768
  const args = { noReview: false, open: true, publish: false };
11785
12769
  for (let i = 0; i < argv.length; i += 1) {
@@ -11807,6 +12791,23 @@ function parseShareArgv(argv) {
11807
12791
  i += 1;
11808
12792
  } else if (arg !== void 0 && arg.startsWith("--pr=")) {
11809
12793
  args.prUrl = checkPrUrl(arg.slice("--pr=".length));
12794
+ } else if (arg === "--title") {
12795
+ const value = argv[i + 1];
12796
+ if (value === void 0 || value.startsWith("--")) throw new Error("--title needs the run's title, in quotes");
12797
+ args.title = checkTitle(value);
12798
+ i += 1;
12799
+ } else if (arg !== void 0 && arg.startsWith("--title=")) {
12800
+ args.title = checkTitle(arg.slice("--title=".length));
12801
+ } else if (arg === "--commit") {
12802
+ const value = argv[i + 1];
12803
+ if (value === void 0 || value.startsWith("--")) {
12804
+ args.commit = "HEAD";
12805
+ } else {
12806
+ args.commit = checkCommitRev(value);
12807
+ i += 1;
12808
+ }
12809
+ } else if (arg !== void 0 && arg.startsWith("--commit=")) {
12810
+ args.commit = checkCommitRev(arg.slice("--commit=".length));
11810
12811
  } else if (arg === "--file") {
11811
12812
  const value = argv[i + 1];
11812
12813
  if (value === void 0 || value.startsWith("--")) {
@@ -11960,7 +12961,7 @@ function tryOpenBrowser(url) {
11960
12961
  const windows = process.platform === "win32";
11961
12962
  const command = windows ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
11962
12963
  const args = windows ? ["/c", "start", "", url] : [url];
11963
- const child = spawn2(command, args, { stdio: "ignore", detached: true, windowsHide: true });
12964
+ const child = spawn3(command, args, { stdio: "ignore", detached: true, windowsHide: true });
11964
12965
  child.on("error", () => {
11965
12966
  });
11966
12967
  child.unref();
@@ -12065,7 +13066,7 @@ async function prompt(label) {
12065
13066
  });
12066
13067
  }
12067
13068
  var HOOK_SUBCOMMANDS = ["install", "status", "uninstall", "notify"];
12068
- var CLI_ENTRY = fileURLToPath(import.meta.url);
13069
+ var CLI_ENTRY = fileURLToPath2(import.meta.url);
12069
13070
  function parseHookArgv(argv) {
12070
13071
  const [sub, ...rest] = argv;
12071
13072
  if (sub === void 0) {
@@ -12075,7 +13076,7 @@ function parseHookArgv(argv) {
12075
13076
  if (known === void 0) {
12076
13077
  throw new Error(`unknown hook subcommand "${sub}". Known: ${HOOK_SUBCOMMANDS.join(", ")}`);
12077
13078
  }
12078
- const args = { sub: known, dryRun: false, noProbe: false };
13079
+ const args = { sub: known, dryRun: false, noProbe: false, capture: false };
12079
13080
  for (let i = 0; i < rest.length; i += 1) {
12080
13081
  const flag = rest[i];
12081
13082
  if (flag === "--settings") {
@@ -12093,13 +13094,17 @@ function parseHookArgv(argv) {
12093
13094
  args.noProbe = true;
12094
13095
  continue;
12095
13096
  }
13097
+ if (flag === "--capture" && known === "notify") {
13098
+ args.capture = true;
13099
+ continue;
13100
+ }
12096
13101
  throw new Error(`unknown option for hook ${known}: ${flag}`);
12097
13102
  }
12098
13103
  if (known !== "install" && args.noProbe) {
12099
13104
  throw new Error("--no-probe only means anything to hook install");
12100
13105
  }
12101
13106
  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");
13107
+ throw new Error("hook notify takes only --capture: Claude Code runs it with the event on stdin");
12103
13108
  }
12104
13109
  if (known === "status" && args.dryRun) {
12105
13110
  throw new Error("hook status writes nothing, so --dry-run means nothing here");
@@ -12111,7 +13116,7 @@ function parseHookArgv(argv) {
12111
13116
  }
12112
13117
  async function runHookCommand(args) {
12113
13118
  if (args.sub === "notify") {
12114
- return runHookNotify({ readStdin: () => readAllStdin(), entryPath: CLI_ENTRY });
13119
+ return runHookNotify({ readStdin: () => readAllStdin(), entryPath: CLI_ENTRY, capture: args.capture });
12115
13120
  }
12116
13121
  if (args.sub === "install") {
12117
13122
  return runHookInstall({
@@ -12154,13 +13159,13 @@ function parsePullArgv(argv) {
12154
13159
  if (ref === void 0) throw new Error("pull needs a run ID or a /r/<id> link, e.g. flatbread pull quiet-otter-4821");
12155
13160
  return { ref, ...out !== void 0 ? { out } : {} };
12156
13161
  }
12157
- async function runPull(args) {
13162
+ async function runPull(args, deps = { ...defaultMcpDeps(storeDir(), bucketDir()), capsule: (ref) => fetchCapsule(ref) }) {
12158
13163
  const ref = parseRunRef(args.ref);
12159
13164
  if (ref === null) {
12160
13165
  console.error(`"${args.ref}" is not a run ID or a /r/<id> link`);
12161
13166
  return 1;
12162
13167
  }
12163
- const result = await fetchCapsule(ref);
13168
+ const result = await readCapsule(ref, deps);
12164
13169
  if (!result.ok) {
12165
13170
  console.error(describeReadFailure(result, `could not pull ${ref.slug}`));
12166
13171
  return 1;
@@ -12169,19 +13174,148 @@ async function runPull(args) {
12169
13174
  process.stdout.write(result.capsule);
12170
13175
  return 0;
12171
13176
  }
12172
- const target = path18.resolve(args.out);
12173
- fs19.mkdirSync(path18.dirname(target), { recursive: true });
12174
- fs19.writeFileSync(target, result.capsule, "utf8");
12175
- console.error(`wrote ${target}`);
13177
+ const target = path21.resolve(args.out);
13178
+ fs22.mkdirSync(path21.dirname(target), { recursive: true });
13179
+ fs22.writeFileSync(target, result.capsule, "utf8");
13180
+ console.error(`wrote ${target}${result.from === "site" ? "" : ` (from this machine's ${result.from === "store" ? "shared runs" : "captured runs"})`}`);
12176
13181
  return 0;
12177
13182
  }
13183
+ function parseSearchArgv(argv) {
13184
+ const query = {};
13185
+ const words = [];
13186
+ let local = false;
13187
+ for (let i = 0; i < argv.length; i += 1) {
13188
+ const arg = argv[i];
13189
+ const valueOf = (flag) => {
13190
+ if (arg.startsWith(`${flag}=`)) return arg.slice(flag.length + 1);
13191
+ const value = argv[i + 1];
13192
+ if (value === void 0 || value.startsWith("--")) throw new Error(`${flag} needs a value`);
13193
+ i += 1;
13194
+ return value;
13195
+ };
13196
+ if (arg === "--harness" || arg.startsWith("--harness=")) {
13197
+ query.harness = toHarnessId(valueOf("--harness"));
13198
+ } else if (arg === "--category" || arg.startsWith("--category=")) {
13199
+ const value = valueOf("--category");
13200
+ if (!TASK_CATEGORIES.includes(value)) throw new Error(`unknown --category "${value}". Known: ${TASK_CATEGORIES.join(", ")}`);
13201
+ query.category = value;
13202
+ } else if (arg === "--recovered") {
13203
+ query.onlyWithRecoveries = true;
13204
+ } else if (arg === "--local") {
13205
+ local = true;
13206
+ } else if (arg.startsWith("--")) {
13207
+ throw new Error(`Unknown option for search: ${arg}`);
13208
+ } else {
13209
+ words.push(arg);
13210
+ }
13211
+ }
13212
+ if (words.length > 0) query.query = words.join(" ");
13213
+ return { query, local };
13214
+ }
13215
+ async function runSearch(argv, deps = defaultMcpDeps(storeDir(), bucketDir()), print = console.log) {
13216
+ const { query, local } = parseSearchArgv(argv);
13217
+ const { lines: lines2, siteSearched } = await searchEverywhere(query, deps, { remote: !local });
13218
+ for (const line of lines2) print(line);
13219
+ return local || siteSearched ? 0 : 1;
13220
+ }
13221
+ function parseCaptureArgv(argv) {
13222
+ const args = { quiet: false };
13223
+ for (let i = 0; i < argv.length; i += 1) {
13224
+ const arg = argv[i];
13225
+ const next = argv[i + 1];
13226
+ if (arg === "--file" || arg === "--repo" || arg === "--harness") {
13227
+ if (next === void 0 || next.startsWith("--")) throw new Error(`${arg} needs a value`);
13228
+ if (arg === "--file") args.file = next;
13229
+ else if (arg === "--repo") args.repo = next;
13230
+ else args.harness = toHarnessId(next);
13231
+ i += 1;
13232
+ } else if (arg === "--commit") {
13233
+ if (next === void 0 || next.startsWith("--")) {
13234
+ args.commit = "HEAD";
13235
+ } else {
13236
+ args.commit = checkCommitRev(next);
13237
+ i += 1;
13238
+ }
13239
+ } else if (arg.startsWith("--commit=")) {
13240
+ args.commit = checkCommitRev(arg.slice("--commit=".length));
13241
+ } else if (arg === "--quiet") {
13242
+ args.quiet = true;
13243
+ } else {
13244
+ throw new Error(`Unknown option for capture: ${arg}`);
13245
+ }
13246
+ }
13247
+ if (args.file !== void 0 && args.repo !== void 0) throw new Error("capture takes --file or --repo, not both");
13248
+ return args;
13249
+ }
13250
+ async function runCapture(args, env = process.env) {
13251
+ const dir = bucketDir(env);
13252
+ const say = (line) => args.quiet ? logCapture(dir, line) : console.log(line);
13253
+ try {
13254
+ if (args.quiet && (env["FLATBREAD_HOOK_DISABLED"] ?? "").trim() !== "") {
13255
+ say("skipped FLATBREAD_HOOK_DISABLED is set");
13256
+ return 0;
13257
+ }
13258
+ let file = args.file;
13259
+ if (file === void 0) {
13260
+ const found = sessionForRepo(args.repo ?? process.cwd(), { env });
13261
+ if ("problem" in found) {
13262
+ say(`skipped ${found.problem}`);
13263
+ return 0;
13264
+ }
13265
+ file = found.file;
13266
+ }
13267
+ const result = await captureToBucket({
13268
+ file,
13269
+ env,
13270
+ dir,
13271
+ ...args.harness !== void 0 ? { harness: args.harness } : {},
13272
+ ...args.commit !== void 0 ? { commit: args.commit } : {}
13273
+ });
13274
+ if (result.status === "refused") {
13275
+ say(`refused ${result.reason}`);
13276
+ return args.quiet ? 0 : 1;
13277
+ }
13278
+ say(
13279
+ `${result.status} ${result.slug} ${result.steps} steps, ${result.redacted} redacted` + (result.commit !== void 0 ? `, at ${result.commit.slice(0, 12)}` : "") + ` -> ${result.path}`
13280
+ );
13281
+ return 0;
13282
+ } catch (error) {
13283
+ say(`failed ${error instanceof Error ? error.message : String(error)}`);
13284
+ return args.quiet ? 0 : 1;
13285
+ }
13286
+ }
13287
+ function parseSetupArgv(argv) {
13288
+ const args = {
13289
+ dryRun: false,
13290
+ uninstall: false,
13291
+ noGit: false,
13292
+ noMcp: false,
13293
+ noProbe: false
13294
+ };
13295
+ for (let i = 0; i < argv.length; i += 1) {
13296
+ const arg = argv[i];
13297
+ if (arg === "--dry-run") args.dryRun = true;
13298
+ else if (arg === "--uninstall") args.uninstall = true;
13299
+ else if (arg === "--no-git") args.noGit = true;
13300
+ else if (arg === "--no-mcp") args.noMcp = true;
13301
+ else if (arg === "--no-probe") args.noProbe = true;
13302
+ else if (arg === "--settings") {
13303
+ const value = argv[i + 1];
13304
+ if (value === void 0 || value.startsWith("--")) throw new Error("--settings needs a path");
13305
+ args.settings = value;
13306
+ i += 1;
13307
+ } else throw new Error(`Unknown option for setup: ${arg}`);
13308
+ }
13309
+ if (args.uninstall && args.noProbe) throw new Error("--no-probe only means anything when installing");
13310
+ return args;
13311
+ }
12178
13312
  async function main(argv) {
12179
13313
  const [command, ...rest] = argv;
12180
13314
  if (command === void 0 || command === "--help" || command === "-h" || command === "help") {
12181
13315
  console.log(USAGE);
12182
13316
  return command === void 0 ? 1 : 0;
12183
13317
  }
12184
- if (command !== "parse" && command !== "share" && command !== "view" && command !== "adapters" && command !== "login" && command !== "logout" && command !== "whoami" && command !== "hook" && command !== "pull" && command !== "mcp") {
13318
+ 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
13319
  console.error(`Unknown command: ${command}`);
12186
13320
  console.error(USAGE);
12187
13321
  return 1;
@@ -12200,6 +13334,25 @@ async function main(argv) {
12200
13334
  }
12201
13335
  return runHookCommand(hookArgs);
12202
13336
  }
13337
+ if (command === "search" || command === "setup" || command === "capture") {
13338
+ try {
13339
+ if (command === "search") return await runSearch(rest);
13340
+ if (command === "capture") return await runCapture(parseCaptureArgv(rest));
13341
+ const setup = parseSetupArgv(rest);
13342
+ return runSetup({
13343
+ entryPath: CLI_ENTRY,
13344
+ dryRun: setup.dryRun,
13345
+ uninstall: setup.uninstall,
13346
+ noGit: setup.noGit,
13347
+ noMcp: setup.noMcp,
13348
+ noProbe: setup.noProbe,
13349
+ ...setup.settings !== void 0 ? { settingsPath: setup.settings } : {}
13350
+ });
13351
+ } catch (error) {
13352
+ console.error(error instanceof Error ? error.message : String(error));
13353
+ return 1;
13354
+ }
13355
+ }
12203
13356
  if (command === "login") return runLogin(rest);
12204
13357
  if (command === "logout") return runLogout();
12205
13358
  if (command === "whoami") return runWhoami();
@@ -12218,7 +13371,7 @@ async function main(argv) {
12218
13371
  console.error(`mcp takes no options, got: ${rest.join(" ")}`);
12219
13372
  return 1;
12220
13373
  }
12221
- return runMcpServer(defaultMcpDeps(storeDir()));
13374
+ return runMcpServer(defaultMcpDeps(storeDir(), bucketDir()));
12222
13375
  }
12223
13376
  if (command === "pull") {
12224
13377
  let pullArgs;
@@ -12251,10 +13404,11 @@ async function main(argv) {
12251
13404
  noReview: shareArgs.noReview,
12252
13405
  publish: shareArgs.publish,
12253
13406
  ...shareArgs.prUrl !== void 0 ? { prUrl: shareArgs.prUrl } : {},
13407
+ ...shareArgs.title !== void 0 ? { title: shareArgs.title } : {},
13408
+ ...shareArgs.commit !== void 0 ? { commit: shareArgs.commit } : {},
12254
13409
  ...storeDir() !== null ? { storeDir: storeDir() } : {},
12255
13410
  openBrowser: shareArgs.open && !shareArgs.noReview,
12256
- outputPath: SHARE_OUTPUT_PATH,
12257
- parseOutputPath: OUTPUT_PATH
13411
+ outputPath: SHARE_OUTPUT_PATH
12258
13412
  });
12259
13413
  return result.exitCode;
12260
13414
  }
@@ -12272,12 +13426,12 @@ var invokedDirectly = (() => {
12272
13426
  if (entry === void 0) return false;
12273
13427
  const real = (file) => {
12274
13428
  try {
12275
- return fs19.realpathSync(file);
13429
+ return fs22.realpathSync(file);
12276
13430
  } catch {
12277
- return path18.resolve(file);
13431
+ return path21.resolve(file);
12278
13432
  }
12279
13433
  };
12280
- return real(entry) === real(fileURLToPath(import.meta.url));
13434
+ return real(entry) === real(fileURLToPath2(import.meta.url));
12281
13435
  })();
12282
13436
  if (invokedDirectly) {
12283
13437
  main(process.argv.slice(2)).then((code) => {
@@ -12293,8 +13447,14 @@ export {
12293
13447
  TMP_DIR,
12294
13448
  main,
12295
13449
  openableUrl,
13450
+ parseCaptureArgv,
12296
13451
  parsePullArgv,
13452
+ parseSearchArgv,
13453
+ parseSetupArgv,
13454
+ runCapture,
12297
13455
  runLogin,
12298
13456
  runLogout,
13457
+ runPull,
13458
+ runSearch,
12299
13459
  runWhoami
12300
13460
  };