flatbread-cli 0.2.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.
@@ -7,10 +7,10 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/index.ts
9
9
  import { spawn as spawn3 } from "node:child_process";
10
- import fs21 from "node:fs";
11
- import os7 from "node:os";
12
- import path20 from "node:path";
13
- import { fileURLToPath } from "node:url";
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: path21, errorMaps, issueData } = params;
494
- const fullPath = [...path21, ...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, path21, key) {
610
+ constructor(parent, value, path22, key) {
611
611
  this._cachedPath = [];
612
612
  this.parent = parent;
613
613
  this.data = value;
614
- this._path = path21;
614
+ this._path = path22;
615
615
  this._key = key;
616
616
  }
617
617
  get path() {
@@ -4384,8 +4384,8 @@ function firstLine(text2) {
4384
4384
  function labelOf(step, cwd) {
4385
4385
  const tool = step.tool;
4386
4386
  if (tool === void 0) return step.type;
4387
- const path21 = firstString(tool.input, FILE_PATH_KEYS);
4388
- if (path21 !== void 0) return `${tool.name} ${repoRelative(path21, cwd)}`;
4387
+ const path22 = firstString(tool.input, FILE_PATH_KEYS);
4388
+ if (path22 !== void 0) return `${tool.name} ${repoRelative(path22, cwd)}`;
4389
4389
  const other = firstString(tool.input, TARGET_KEYS);
4390
4390
  if (other !== void 0) return cut(`${tool.name}: ${repoRelative(firstLine(other), cwd)}`, BRIEF_LIMITS.quote);
4391
4391
  return tool.name;
@@ -4633,9 +4633,9 @@ function runDiagnostics(trace) {
4633
4633
  searches.set(key, found);
4634
4634
  }
4635
4635
  }
4636
- for (const [path21, entries] of reads) {
4636
+ for (const [path22, entries] of reads) {
4637
4637
  const hit = densest(entries, WASTE_RULES.repeatedRead.times, WASTE_RULES.repeatedRead.windowMinutes * 6e4);
4638
- if (hit) waste.push({ kind: "repeated-read", path: path21, steps: hit, windowMinutes: WASTE_RULES.repeatedRead.windowMinutes });
4638
+ if (hit) waste.push({ kind: "repeated-read", path: path22, steps: hit, windowMinutes: WASTE_RULES.repeatedRead.windowMinutes });
4639
4639
  }
4640
4640
  for (const { label, entries } of searches.values()) {
4641
4641
  const hit = densest(entries, WASTE_RULES.repeatedSearch.times, WASTE_RULES.repeatedSearch.windowMinutes * 6e4);
@@ -4713,19 +4713,19 @@ function filesOf(steps, cwd) {
4713
4713
  const patches = [];
4714
4714
  steps.forEach((step, index) => {
4715
4715
  if (!isSuccessfulEdit(step) || step.tool === void 0) return;
4716
- const path21 = repoRelative(firstString(step.tool.input, FILE_PATH_KEYS), cwd);
4717
- const file = files.get(path21) ?? { path: path21 };
4716
+ const path22 = repoRelative(firstString(step.tool.input, FILE_PATH_KEYS), cwd);
4717
+ const file = files.get(path22) ?? { path: path22 };
4718
4718
  if (step.diff !== void 0) {
4719
4719
  const counted = countPatch(step.diff.patch);
4720
4720
  file.added = (file.added ?? 0) + counted.added;
4721
4721
  file.removed = (file.removed ?? 0) + counted.removed;
4722
- patches.push({ path: path21, patch: repoRelative(step.diff.patch, cwd), step: step.stepNumber });
4722
+ patches.push({ path: path22, patch: repoRelative(step.diff.patch, cwd), step: step.stepNumber });
4723
4723
  }
4724
4724
  const result = resultOf(steps, index);
4725
4725
  if (step.tool.name.toLowerCase() === "write" && result !== void 0 && /^File created successfully\b/.test(result.trim())) {
4726
4726
  file.created = true;
4727
4727
  }
4728
- files.set(path21, file);
4728
+ files.set(path22, file);
4729
4729
  });
4730
4730
  return { files: [...files.values()], patches };
4731
4731
  }
@@ -4810,11 +4810,11 @@ var HARNESS_INFO = {
4810
4810
  antigravity: {
4811
4811
  id: "antigravity",
4812
4812
  label: "Google Antigravity",
4813
- formatLabel: "unknown",
4814
- confidence: "inferred",
4815
- implemented: false,
4813
+ formatLabel: "IDE conversations/<id>.pb (Windsurf's encrypted trajectory)",
4814
+ confidence: "documented",
4815
+ implemented: true,
4816
4816
  humanTest: "A6",
4817
- 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."
4818
4818
  },
4819
4819
  terminal: {
4820
4820
  id: "terminal",
@@ -5096,13 +5096,13 @@ function categorizeTrace(trace) {
5096
5096
  };
5097
5097
  }
5098
5098
  if (paths.length > 0) {
5099
- if (paths.every((path21) => matchesAny(path21, DOC_PATTERNS))) {
5099
+ if (paths.every((path22) => matchesAny(path22, DOC_PATTERNS))) {
5100
5100
  return { category: "docs", evidence: [`all ${paths.length} touched path(s) are documentation`, ...paths.slice(0, 3)] };
5101
5101
  }
5102
- if (paths.every((path21) => matchesAny(path21, TEST_PATTERNS))) {
5102
+ if (paths.every((path22) => matchesAny(path22, TEST_PATTERNS))) {
5103
5103
  return { category: "tests", evidence: [`all ${paths.length} touched path(s) are tests`, ...paths.slice(0, 3)] };
5104
5104
  }
5105
- if (paths.every((path21) => matchesAny(path21, INFRA_PATTERNS))) {
5105
+ if (paths.every((path22) => matchesAny(path22, INFRA_PATTERNS))) {
5106
5106
  return { category: "infra", evidence: [`all ${paths.length} touched path(s) are build/CI/deploy config`, ...paths.slice(0, 3)] };
5107
5107
  }
5108
5108
  }
@@ -6047,50 +6047,282 @@ function buildTrace(input) {
6047
6047
  }
6048
6048
 
6049
6049
  // src/adapters/antigravity.ts
6050
+ import crypto from "node:crypto";
6050
6051
  import fs3 from "node:fs";
6051
6052
  import os2 from "node:os";
6052
6053
  import path3 from "node:path";
6053
- function candidateStateDirs(env = process.env) {
6054
- const home = os2.homedir();
6055
- const dirs = [];
6056
- if (process.platform === "win32") {
6057
- const appData = env.APPDATA ?? path3.join(home, "AppData", "Roaming");
6058
- dirs.push(path3.join(appData, "Antigravity", "User"));
6059
- dirs.push(path3.join(appData, "Google", "Antigravity", "User"));
6060
- } else if (process.platform === "darwin") {
6061
- dirs.push(path3.join(home, "Library", "Application Support", "Antigravity", "User"));
6062
- dirs.push(path3.join(home, "Library", "Application Support", "Google", "Antigravity", "User"));
6063
- } else {
6064
- dirs.push(path3.join(home, ".config", "Antigravity", "User"));
6065
- 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 [];
6066
6224
  }
6067
- return dirs;
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
+ }
6244
+ }
6245
+ return found.sort((a, b) => b.mtime - a.mtime).map((entry) => entry.file);
6068
6246
  }
6247
+ var REFUSAL_TEST = "A6";
6069
6248
  var antigravityAdapter = {
6070
6249
  id: "antigravity",
6071
- version: 0,
6250
+ version: ANTIGRAVITY_ADAPTER_VERSION,
6072
6251
  async detect(input) {
6073
- void input;
6074
- 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" };
6075
6256
  },
6076
6257
  async read(input) {
6077
- 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}`);
6078
6286
  return {
6079
- refused: true,
6080
- 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).",
6081
- 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
+ ]
6082
6296
  };
6083
6297
  },
6084
6298
  discover(env) {
6085
- return candidateStateDirs(env).filter((dir) => fs3.existsSync(dir));
6299
+ return listConversations(env);
6086
6300
  },
6087
- async describe() {
6088
- const lines2 = ["Antigravity support is a stub. It detects an install and refuses to parse."];
6089
- for (const dir of candidateStateDirs()) {
6090
- 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");
6091
6325
  }
6092
- lines2.push("");
6093
- lines2.push("If one is FOUND, HUMAN-TESTS.md A6 lists exactly what to report back.");
6094
6326
  return lines2;
6095
6327
  }
6096
6328
  };
@@ -8752,7 +8984,7 @@ function withLineage(trace, lineage, prUrl) {
8752
8984
  }
8753
8985
 
8754
8986
  // src/auth.ts
8755
- import crypto from "node:crypto";
8987
+ import crypto2 from "node:crypto";
8756
8988
  import fs15 from "node:fs";
8757
8989
  import os6 from "node:os";
8758
8990
  import path14 from "node:path";
@@ -8782,7 +9014,7 @@ function isAuthFailure(value) {
8782
9014
  return typeof value === "object" && value !== null && "ok" in value && value.ok === false;
8783
9015
  }
8784
9016
  function hashSecret(secret) {
8785
- return crypto.createHash("sha256").update(secret).digest("hex");
9017
+ return crypto2.createHash("sha256").update(secret).digest("hex");
8786
9018
  }
8787
9019
  function apiUrlFrom(env = process.env) {
8788
9020
  const explicit = env["FLATBREAD_API_URL"]?.trim();
@@ -8795,7 +9027,7 @@ async function startCliLogin(options = {}) {
8795
9027
  const apiUrl = options.apiUrl ?? apiUrlFrom(env);
8796
9028
  const base = validateApiUrl(apiUrl);
8797
9029
  if (isAuthFailure(base)) return base;
8798
- const pollSecret = crypto.randomBytes(32).toString("hex");
9030
+ const pollSecret = crypto2.randomBytes(32).toString("hex");
8799
9031
  const result = await postJson(
8800
9032
  `${base}/api/cli-login/start`,
8801
9033
  { poll_secret_hash: hashSecret(pollSecret) },
@@ -9586,67 +9818,105 @@ function renderMarked(value, matches) {
9586
9818
  return html;
9587
9819
  }
9588
9820
  var STYLE = `
9589
- :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
+ }
9590
9835
  *{box-sizing:border-box}
9591
- body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace}
9592
- header{position:sticky;top:0;z-index:5;background:var(--panel);border-bottom:1px solid var(--line);padding:14px 20px}
9593
- h1{margin:0 0 6px;font-size:16px;font-weight:600;letter-spacing:.01em}
9594
- .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}
9595
9846
  .meta b{color:var(--fg);font-weight:600}
9596
- .wrap{display:grid;grid-template-columns:minmax(0,1fr) 380px;gap:0;align-items:start}
9597
- main{padding:18px 20px 120px;min-width:0}
9598
- aside{position:sticky;top:74px;max-height:calc(100vh - 74px);overflow:auto;border-left:1px solid var(--line);padding:16px;background:var(--panel)}
9599
- .step{border:1px solid var(--line);border-radius:6px;margin:0 0 12px;background:var(--panel)}
9600
- .step>.head{display:flex;gap:10px;align-items:center;padding:8px 12px;border-bottom:1px solid var(--line);font-size:12px;color:var(--dim)}
9601
- .badge{border:1px solid var(--line);border-radius:999px;padding:1px 9px;color:var(--fg);font-size:11px}
9602
- .badge.user_prompt{border-color:#3f6fd8;color:#9dbcff}
9603
- .badge.assistant_text{border-color:#3f8f6f;color:#8fe0bb}
9604
- .badge.assistant_thinking{border-color:#6a5acd;color:#c0b6ff}
9605
- .badge.tool_call{border-color:#b08a2e;color:#ffd98a}
9606
- .badge.tool_result{border-color:#4a5568;color:#c3cbdc}
9607
- .badge.error_recovery{border-color:#c85a54;color:#ff9e99}
9608
- pre{margin:0;padding:10px 12px;white-space:pre-wrap;word-break:break-word;font:inherit}
9609
- .sub{padding:6px 12px;border-top:1px solid var(--line);color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
9610
- dl{margin:0;padding:2px 12px 10px}
9611
- dt{color:var(--dim);font-size:12px;margin-top:8px}
9612
- dd{margin:2px 0 0;white-space:pre-wrap;word-break:break-word}
9613
- 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}
9614
9866
  mark.hit .was{display:none}
9615
- 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}
9616
9868
  mark.hit.keep .tok{display:none}
9617
9869
  mark.hit.keep .was{display:inline}
9618
- aside h2{margin:0 0 4px;font-size:13px}
9619
- aside .note{color:var(--dim);font-size:12px;margin:0 0 12px}
9620
- .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}
9621
- .row:hover{border-color:var(--acc)}
9622
- .row input{margin:3px 0 0}
9623
- .state{font-size:11px;letter-spacing:.04em}
9624
- .s-keep{display:none;color:var(--warn)}
9625
- .s-red{color:var(--ok)}
9626
- .row input:checked ~ .state .s-red{display:none}
9627
- .row input:checked ~ .state .s-keep{display:inline}
9628
- .row .rule{font-size:12px}
9629
- .row .prev{color:var(--dim);font-size:11px;word-break:break-all}
9630
- .row .times{color:var(--warn)}
9631
- .keys{color:var(--dim);font-size:11px;margin:0 0 10px}
9632
- .keys b{color:var(--fg);font-weight:600}
9633
- .row .path{color:#5c6474;font-size:10px;word-break:break-all}
9634
- fieldset{border:1px solid var(--line);border-radius:6px;margin:0 0 12px;padding:8px 10px}
9635
- input.title{box-sizing:border-box;width:100%;font:inherit;padding:6px 8px;margin:4px 0 6px;border:1px solid var(--line);border-radius:5px;background:var(--bg);color:var(--fg)}
9636
- input.title:focus{outline:none;border-color:var(--acc)}
9637
- legend{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
9638
- label.vis{display:block;padding:3px 0}
9639
- .warn{color:var(--warn);font-size:11px;margin:4px 0 0}
9640
- .actions{display:flex;gap:8px;margin-top:10px}
9641
- button{font:inherit;padding:9px 14px;border-radius:5px;border:1px solid var(--line);background:#1d2430;color:var(--fg);cursor:pointer}
9642
- button.primary{background:var(--ok);border-color:var(--ok);color:#04140b;font-weight:600}
9643
- 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}
9644
9873
  .tools{display:flex;gap:6px;margin:0 0 10px}
9645
- .tools button{padding:4px 8px;font-size:11px}
9646
- .done{max-width:760px;margin:12vh auto;padding:0 20px}
9647
- .done code{display:block;background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:12px;margin:12px 0;word-break:break-all}
9648
- 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}}
9649
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
+ };
9650
9920
  var SCRIPT = `
9651
9921
  (function(){
9652
9922
  var boxes = Array.prototype.slice.call(document.querySelectorAll('input[name="keep"]'));
@@ -9753,14 +10023,14 @@ function renderReviewPage(input) {
9753
10023
  const base = `steps[${index}]`;
9754
10024
  const parts = [];
9755
10025
  parts.push(
9756
- `<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>`
9757
10027
  );
9758
10028
  parts.push(`<pre>${marked(`${base}.content`, step.content)}</pre>`);
9759
10029
  const tool = step.tool;
9760
10030
  if (tool) {
9761
10031
  const inputSlots = slots.filter((slot) => slot.fieldPath.startsWith(`${base}.tool.input`));
9762
10032
  parts.push(
9763
- `<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>`
9764
10034
  );
9765
10035
  if (inputSlots.length > 0) {
9766
10036
  parts.push(
@@ -9780,7 +10050,7 @@ function renderReviewPage(input) {
9780
10050
  `<div class="sub">diff ${marked(`${base}.diff.filePath`, diff.filePath)}</div><pre>${marked(`${base}.diff.patch`, diff.patch)}</pre>`
9781
10051
  );
9782
10052
  }
9783
- return `<section class="step" id="${escapeHtml(base)}">${parts.join("")}</section>`;
10053
+ return `<section class="step ${escapeHtml(step.type)}" id="${escapeHtml(base)}">${parts.join("")}</section>`;
9784
10054
  }).join("");
9785
10055
  const groups = groupMatches(matches);
9786
10056
  const rowsHtml = groups.length === 0 ? `<p class="note">No matches. Nothing will be rewritten.</p>` : groups.map((group) => {
@@ -9794,15 +10064,16 @@ function renderReviewPage(input) {
9794
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?`;
9795
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>`;
9796
10066
  const body = `
9797
- <header>
10067
+ <header><div class="hin">
10068
+ <div class="bar"><span class="wordmark">flatbread</span><span class="crumb">review before you share</span></div>
9798
10069
  <h1>${marked("title", trace.title)}</h1>
9799
10070
  <div class="meta">
9800
- <b>${escapeHtml(trace.model)}</b> &middot; ${trace.steps.length} steps &middot; ${toolCalls} tool calls &middot;
9801
- ${formatCount(trace.stats.filesChanged)} files changed &middot; ${trace.stats.errorCount} errors &middot;
9802
- ${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"}
9803
10074
  </div>
9804
- <div class="meta">${escapeHtml(sessionPath)}</div>
9805
- </header>
10075
+ <div class="meta path">${escapeHtml(sessionPath)}</div>
10076
+ </div></header>
9806
10077
  <form id="review" method="post" action="/confirm">
9807
10078
  <div class="wrap">
9808
10079
  <main>${metaHtml}${stepsHtml}</main>
@@ -9835,13 +10106,14 @@ function renderReviewPage(input) {
9835
10106
  <script>var PUBLIC_WARNING=${JSON.stringify(publicWarning)};${SCRIPT}</script>`;
9836
10107
  return page(`Flatbread review - ${trace.title}`, body);
9837
10108
  }
9838
- function renderDonePage(message, outputPath, publishTo) {
10109
+ function renderDonePage(message, outputPath, publishTo, keepsRun = false) {
9839
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>`;
9840
10111
  return page(
9841
10112
  "Flatbread - share draft written",
9842
10113
  `<div class="done"><h1>${escapeHtml(message)}</h1>
9843
10114
  <code>${escapeHtml(outputPath)}</code>
9844
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>` : ""}
9845
10117
  <p class="meta"><strong>Go back to your terminal now.</strong> The manage token for this
9846
10118
  run is printed there, once, at the very bottom. It is the only way to open the run
9847
10119
  while it is private, and only its hash is stored, so nothing can print it again.</p>
@@ -9882,12 +10154,11 @@ function parseDecisionBody(body) {
9882
10154
  };
9883
10155
  }
9884
10156
  async function listenOnFirstFreePort(server, ports) {
9885
- for (const port of ports) {
10157
+ for (const port of [...ports, 0]) {
9886
10158
  const ok = await new Promise((resolve) => {
9887
- const onError = (error) => {
10159
+ const onError = () => {
9888
10160
  server.removeListener("listening", onListening);
9889
- if (error.code === "EADDRINUSE" || error.code === "EACCES") resolve(false);
9890
- else resolve(false);
10161
+ resolve(false);
9891
10162
  };
9892
10163
  const onListening = () => {
9893
10164
  server.removeListener("error", onError);
@@ -9966,7 +10237,7 @@ async function startReviewServer(options) {
9966
10237
  ...changed ? { title } : {}
9967
10238
  };
9968
10239
  const message = options.onConfirm ? await options.onConfirm(confirmed) : "";
9969
- send(200, renderDonePage("Share draft written", message, options.publishTo));
10240
+ send(200, renderDonePage("Share draft written", message, options.publishTo, options.keepsRun === true));
9970
10241
  finish(confirmed);
9971
10242
  } catch (error) {
9972
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>`));
@@ -10120,6 +10391,7 @@ function reportPersist(outcome, options, log) {
10120
10391
  log(` slug ${outcome.slug}`);
10121
10392
  log(` visibility ${outcome.record.visibility}`);
10122
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)`);
10123
10395
  log("");
10124
10396
  log(`open ${outcome.publish?.ok ? outcome.publish.url : `${base}/r/${outcome.slug}`}`);
10125
10397
  log("");
@@ -10228,6 +10500,7 @@ async function runShare(options) {
10228
10500
  sessionPath: target,
10229
10501
  draftPath: options.outputPath,
10230
10502
  titleDraft: previewTitle,
10503
+ keepsRun: options.persist !== false && (options.storeDir ?? resolveStoreDir()) !== null,
10231
10504
  ...isPublisher(reviewPublisher) ? { publishTo: reviewPublisher.description } : {},
10232
10505
  ...options.ports ? { ports: options.ports } : {},
10233
10506
  ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
@@ -10241,9 +10514,9 @@ async function runShare(options) {
10241
10514
  });
10242
10515
  if (!session) {
10243
10516
  console.error(
10244
- `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)`
10245
10518
  );
10246
- 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.");
10247
10520
  return { exitCode: 1, status: "port-unavailable" };
10248
10521
  }
10249
10522
  log(` review at ${session.url}`);
@@ -10661,7 +10934,7 @@ function logCapture(dir, line, now = (/* @__PURE__ */ new Date()).toISOString())
10661
10934
 
10662
10935
  // src/mcp.ts
10663
10936
  var MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
10664
- var MCP_SERVER_VERSION = "0.2.0";
10937
+ var MCP_SERVER_VERSION = "0.3.0";
10665
10938
  function defaultMcpDeps(storeDir2, bucketDir2) {
10666
10939
  return {
10667
10940
  storeDir: storeDir2,
@@ -10751,6 +11024,10 @@ async function searchEverywhere(query, deps, options = {}) {
10751
11024
  if (local.unreadable > 0) out.push(`(${local.unreadable} local record file${local.unreadable === 1 ? "" : "s"} could not be read)`);
10752
11025
  }
10753
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
+ }
10754
11031
  if (deps.bucketDir !== null) {
10755
11032
  const bucket = readBucket(deps.bucketDir);
10756
11033
  const captured = bucket.rows.filter((row) => !seen.has(row.summary.slug) && matchesQuery(row.summary, query));
@@ -10784,18 +11061,24 @@ async function searchTool(args, deps) {
10784
11061
  const { lines: lines2 } = await searchEverywhere(query, deps);
10785
11062
  return text([...lines2, "", "Pass a run ID to flatbread_get_capsule for what that run learned."].join("\n"));
10786
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;
11073
+ }
10787
11074
  async function capsuleTool(args, deps) {
10788
11075
  const raw = args["slug"];
10789
11076
  if (typeof raw !== "string") return text("slug must be a run ID or a /r/<id> link", true);
10790
11077
  const ref = parseRunRef(raw);
10791
11078
  if (ref === null) return text(`"${raw}" is not a run ID or a /r/<id> link`, true);
10792
- if (ref.site === void 0) {
10793
- const local = localRecords(deps).records.find((record) => record.slug === ref.slug) ?? (deps.bucketDir !== null ? readBucketRecord(deps.bucketDir, ref.slug) : null);
10794
- if (local !== null && local !== void 0) return text(buildCapsule(local.trace));
10795
- }
10796
- const remote = await deps.capsule(ref);
10797
- if (remote.ok) return text(remote.capsule);
10798
- 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);
10799
11082
  }
10800
11083
  async function handleMcpMessage(message, deps) {
10801
11084
  if (typeof message !== "object" || message === null || Array.isArray(message)) {
@@ -10844,7 +11127,7 @@ async function handleMcpMessage(message, deps) {
10844
11127
  }
10845
11128
  async function runMcpServer(deps, io) {
10846
11129
  const input = io?.input ?? process.stdin;
10847
- const write = io?.write ?? ((line) => process.stdout.write(line));
11130
+ const write2 = io?.write ?? ((line) => process.stdout.write(line));
10848
11131
  const log = io?.log ?? ((line) => process.stderr.write(`${line}
10849
11132
  `));
10850
11133
  log(`flatbread mcp ${MCP_SERVER_VERSION} on stdio${deps.storeDir === null ? " (no local store)" : ` (local store ${deps.storeDir})`}`);
@@ -10857,12 +11140,12 @@ async function runMcpServer(deps, io) {
10857
11140
  try {
10858
11141
  message = JSON.parse(line);
10859
11142
  } catch {
10860
- 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" } })}
10861
11144
  `);
10862
11145
  return;
10863
11146
  }
10864
11147
  const reply = await handleMcpMessage(message, deps);
10865
- if (reply !== null) write(`${JSON.stringify(reply)}
11148
+ if (reply !== null) write2(`${JSON.stringify(reply)}
10866
11149
  `);
10867
11150
  });
10868
11151
  }
@@ -10870,9 +11153,134 @@ async function runMcpServer(deps, io) {
10870
11153
  return 0;
10871
11154
  }
10872
11155
 
10873
- // src/git-hook.ts
11156
+ // src/claude-mcp.ts
10874
11157
  import fs19 from "node:fs";
11158
+ import os7 from "node:os";
10875
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";
10876
11284
  var POST_COMMIT_MARKER = "# flatbread post-commit hook - remove with: flatbread setup --uninstall";
10877
11285
  var CHAINED_HOOK = "post-commit.pre-flatbread";
10878
11286
  var sq = (value) => `'${value.replace(/\\/g, "/").replace(/'/g, "'\\''")}'`;
@@ -10916,33 +11324,33 @@ function hooksDirFor(cwd, options = {}) {
10916
11324
  }
10917
11325
  const common = git(top, ["rev-parse", "--git-common-dir"], options)?.trim();
10918
11326
  if (common === void 0 || common.length === 0) return { ok: false, reason: "unreadable", detail: `git could not name the git directory of ${top}` };
10919
- return { ok: true, repo: path18.resolve(top), dir: path18.resolve(top, common, "hooks"), ...machineHooksPath !== void 0 ? { machineHooksPath } : {} };
11327
+ return { ok: true, repo: path19.resolve(top), dir: path19.resolve(top, common, "hooks"), ...machineHooksPath !== void 0 ? { machineHooksPath } : {} };
10920
11328
  }
10921
11329
  var read = (file) => {
10922
11330
  try {
10923
- return fs19.readFileSync(file, "utf8");
11331
+ return fs20.readFileSync(file, "utf8");
10924
11332
  } catch {
10925
11333
  return null;
10926
11334
  }
10927
11335
  };
10928
11336
  function installGitHook(dir, script, options = {}) {
10929
- const hook = path18.join(dir, "post-commit");
10930
- const chained = path18.join(dir, CHAINED_HOOK);
11337
+ const hook = path19.join(dir, "post-commit");
11338
+ const chained = path19.join(dir, CHAINED_HOOK);
10931
11339
  const current = read(hook);
10932
- const write = (lines2) => {
11340
+ const write2 = (lines2) => {
10933
11341
  if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
10934
- fs19.mkdirSync(dir, { recursive: true });
10935
- if (current !== null && !isOurs(current)) fs19.renameSync(hook, chained);
10936
- fs19.writeFileSync(hook, script, { encoding: "utf8", mode: 493 });
10937
- fs19.chmodSync(hook, 493);
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);
10938
11346
  return { code: 0, lines: lines2 };
10939
11347
  };
10940
- if (current === null) return write([`post-commit ${hook} (new)`]);
11348
+ if (current === null) return write2([`post-commit ${hook} (new)`]);
10941
11349
  if (isOurs(current)) {
10942
11350
  if (current === script) return { code: 0, lines: [`post-commit ${hook}`, "already installed, unchanged"] };
10943
- return write([`post-commit ${hook}`, "updated the flatbread hook"]);
11351
+ return write2([`post-commit ${hook}`, "updated the flatbread hook"]);
10944
11352
  }
10945
- if (fs19.existsSync(chained)) {
11353
+ if (fs20.existsSync(chained)) {
10946
11354
  return {
10947
11355
  code: 1,
10948
11356
  lines: [
@@ -10952,16 +11360,16 @@ function installGitHook(dir, script, options = {}) {
10952
11360
  ]
10953
11361
  };
10954
11362
  }
10955
- return write([
11363
+ return write2([
10956
11364
  `post-commit ${hook}`,
10957
11365
  `your post-commit was kept as ${CHAINED_HOOK} and runs first, unchanged, with its exit status`
10958
11366
  ]);
10959
11367
  }
10960
11368
  function uninstallGitHook(dir, options = {}) {
10961
- const hook = path18.join(dir, "post-commit");
10962
- const chained = path18.join(dir, CHAINED_HOOK);
11369
+ const hook = path19.join(dir, "post-commit");
11370
+ const chained = path19.join(dir, CHAINED_HOOK);
10963
11371
  const current = read(hook);
10964
- const hasChained = fs19.existsSync(chained);
11372
+ const hasChained = fs20.existsSync(chained);
10965
11373
  const done = (lines2, act) => {
10966
11374
  if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
10967
11375
  act();
@@ -10979,17 +11387,17 @@ function uninstallGitHook(dir, options = {}) {
10979
11387
  if (current === null && !hasChained) return { code: 0, lines: [`no flatbread post-commit in ${dir}`] };
10980
11388
  if (hasChained) {
10981
11389
  return done([`post-commit ${hook}`, `restored your post-commit from ${CHAINED_HOOK}`], () => {
10982
- if (current !== null) fs19.rmSync(hook);
10983
- fs19.renameSync(chained, hook);
11390
+ if (current !== null) fs20.rmSync(hook);
11391
+ fs20.renameSync(chained, hook);
10984
11392
  });
10985
11393
  }
10986
- return done([`post-commit ${hook}`, "removed the flatbread hook"], () => fs19.rmSync(hook));
11394
+ return done([`post-commit ${hook}`, "removed the flatbread hook"], () => fs20.rmSync(hook));
10987
11395
  }
10988
11396
 
10989
11397
  // src/hook.ts
10990
11398
  import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
10991
- import fs20 from "node:fs";
10992
- import path19 from "node:path";
11399
+ import fs21 from "node:fs";
11400
+ import path20 from "node:path";
10993
11401
  var HOOK_EVENTS = ["Stop", "SessionEnd"];
10994
11402
  var OFFER_AFTER_STOPS = 2;
10995
11403
  var MAX_TRACKED_SESSIONS = 50;
@@ -10999,14 +11407,14 @@ function emptyHookState() {
10999
11407
  return { version: HOOK_STATE_VERSION, sessions: {} };
11000
11408
  }
11001
11409
  function hookStatePath(env = process.env) {
11002
- return path19.join(flatbreadHome(env), "hook-state.json");
11410
+ return path20.join(flatbreadHome(env), "hook-state.json");
11003
11411
  }
11004
11412
  function readHookState(env = process.env) {
11005
11413
  const file = hookStatePath(env);
11006
- if (!fs20.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
11414
+ if (!fs21.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
11007
11415
  let raw;
11008
11416
  try {
11009
- raw = fs20.readFileSync(file, "utf8");
11417
+ raw = fs21.readFileSync(file, "utf8");
11010
11418
  } catch (error) {
11011
11419
  return { ok: false, reason: "unreadable", detail: error.message };
11012
11420
  }
@@ -11049,9 +11457,9 @@ function pruneHookState(state, keep = MAX_TRACKED_SESSIONS) {
11049
11457
  }
11050
11458
  function writeHookState(state, env = process.env) {
11051
11459
  const dir = flatbreadHome(env);
11052
- fs20.mkdirSync(dir, { recursive: true, mode: 448 });
11460
+ fs21.mkdirSync(dir, { recursive: true, mode: 448 });
11053
11461
  const file = hookStatePath(env);
11054
- fs20.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
11462
+ fs21.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
11055
11463
  encoding: "utf8",
11056
11464
  mode: 384
11057
11465
  });
@@ -11197,7 +11605,7 @@ async function runHookNotify(io) {
11197
11605
  const stdout = io.stdout ?? ((line) => process.stdout.write(line + "\n"));
11198
11606
  const stderr = io.stderr ?? ((line) => process.stderr.write(line + "\n"));
11199
11607
  const now = io.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
11200
- const exists = io.exists ?? ((filePath) => fs20.existsSync(filePath));
11608
+ const exists = io.exists ?? ((filePath) => fs21.existsSync(filePath));
11201
11609
  const execPath = io.execPath ?? process.execPath;
11202
11610
  const entryPath = io.entryPath ?? process.argv[1] ?? "";
11203
11611
  try {
@@ -11354,10 +11762,10 @@ function removeHookConfig(settings) {
11354
11762
  return { settings: root, removed };
11355
11763
  }
11356
11764
  function readSettingsFile(file) {
11357
- if (!fs20.existsSync(file)) return { present: false, file };
11765
+ if (!fs21.existsSync(file)) return { present: false, file };
11358
11766
  let raw;
11359
11767
  try {
11360
- raw = fs20.readFileSync(file, "utf8");
11768
+ raw = fs21.readFileSync(file, "utf8");
11361
11769
  } catch (error) {
11362
11770
  return { present: true, file, unreadable: error.message };
11363
11771
  }
@@ -11369,10 +11777,10 @@ function readSettingsFile(file) {
11369
11777
  }
11370
11778
  }
11371
11779
  function writeSettingsFile(file, settings) {
11372
- fs20.mkdirSync(path19.dirname(file), { recursive: true });
11780
+ fs21.mkdirSync(path20.dirname(file), { recursive: true });
11373
11781
  const temp = `${file}.flatbread-${process.pid}.tmp`;
11374
- fs20.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
11375
- fs20.renameSync(temp, file);
11782
+ fs21.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
11783
+ fs21.renameSync(temp, file);
11376
11784
  }
11377
11785
  function handlersFor(execPath, entryPath, capture = false) {
11378
11786
  return {
@@ -11530,8 +11938,8 @@ function runHookUninstall(options = {}) {
11530
11938
  function settingsCandidates(env, cwd) {
11531
11939
  return [
11532
11940
  { label: "user", file: claudeSettingsPath(env) },
11533
- { label: "project", file: path19.join(cwd, ".claude", "settings.json") },
11534
- { label: "local", file: path19.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") }
11535
11943
  ];
11536
11944
  }
11537
11945
  function installedEventsIn(parsed, exists) {
@@ -11561,7 +11969,7 @@ function runHookStatus(options = {}) {
11561
11969
  const env = options.env ?? process.env;
11562
11970
  const log = options.log ?? ((line) => console.log(line));
11563
11971
  const cwd = options.cwd ?? process.cwd();
11564
- const exists = options.exists ?? ((filePath) => fs20.existsSync(filePath));
11972
+ const exists = options.exists ?? ((filePath) => fs21.existsSync(filePath));
11565
11973
  const candidates = options.settingsPath === void 0 ? settingsCandidates(env, cwd) : [{ label: "settings", file: options.settingsPath }];
11566
11974
  log("");
11567
11975
  log("flatbread hook");
@@ -11711,6 +12119,22 @@ function runSetup(options = {}) {
11711
12119
  ...options.spawn !== void 0 ? { spawn: options.spawn } : {},
11712
12120
  ...settings
11713
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("");
11714
12138
  let git2 = 0;
11715
12139
  log("git");
11716
12140
  if (options.noGit === true) {
@@ -11746,11 +12170,11 @@ function runSetup(options = {}) {
11746
12170
  log(" share one with: flatbread share --file <its session>, which opens the review");
11747
12171
  }
11748
12172
  log("");
11749
- return claude !== 0 || git2 !== 0 ? 1 : 0;
12173
+ return claude !== 0 || mcp !== 0 || git2 !== 0 ? 1 : 0;
11750
12174
  }
11751
12175
 
11752
12176
  // src/login-server.ts
11753
- import crypto2 from "node:crypto";
12177
+ import crypto3 from "node:crypto";
11754
12178
  import http2 from "node:http";
11755
12179
  function callbackOrigin(port) {
11756
12180
  return `http://127.0.0.1:${port}`;
@@ -11825,7 +12249,7 @@ function parseDelivery(raw, expectedState, nowSeconds) {
11825
12249
  }
11826
12250
  const given = Buffer.from(state, "utf8");
11827
12251
  const wanted = Buffer.from(expectedState, "utf8");
11828
- if (given.length !== wanted.length || !crypto2.timingSafeEqual(given, wanted)) {
12252
+ if (given.length !== wanted.length || !crypto3.timingSafeEqual(given, wanted)) {
11829
12253
  return { ok: false, problem: "the callback state did not match" };
11830
12254
  }
11831
12255
  const sessionRaw = body["session"];
@@ -11873,7 +12297,7 @@ async function readBody2(req, limitBytes = 64 * 1024) {
11873
12297
  async function startLoginServer(options = {}) {
11874
12298
  const ports = options.ports ?? DEFAULT_LOGIN_PORTS;
11875
12299
  const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
11876
- const state = options.state ?? crypto2.randomBytes(32).toString("hex");
12300
+ const state = options.state ?? crypto3.randomBytes(32).toString("hex");
11877
12301
  let settle;
11878
12302
  const delivery = new Promise((resolve) => {
11879
12303
  settle = resolve;
@@ -11959,13 +12383,13 @@ async function bindFirst(server, ports) {
11959
12383
  }
11960
12384
 
11961
12385
  // src/index.ts
11962
- var PACKAGE_ROOT = path20.resolve(path20.dirname(fileURLToPath(import.meta.url)), "..");
11963
- var SOURCE_CHECKOUT = fs21.existsSync(path20.join(PACKAGE_ROOT, "src", "index.ts"));
11964
- var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ? path20.join(PACKAGE_ROOT, ".tmp") : path20.join(os7.homedir(), ".flatbread"));
11965
- var OUTPUT_PATH = path20.join(TMP_DIR, "last-parse.json");
11966
- var SHARE_OUTPUT_PATH = path20.join(TMP_DIR, "last-share.json");
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");
11967
12391
  function storeDir() {
11968
- return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path20.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
12392
+ return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path21.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
11969
12393
  }
11970
12394
  var USAGE = `flatbread - registry of AI agent runs
11971
12395
 
@@ -11975,14 +12399,16 @@ Usage:
11975
12399
  flatbread share Review + sanitize the newest session
11976
12400
  flatbread share --file <path> Review + sanitize a specific session
11977
12401
  flatbread view Write the brief and capsule, print the viewer URL
11978
- 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
11979
12404
  flatbread pull <id|url> --out <file> Write it to a file instead
11980
12405
  flatbread mcp MCP server on stdio: flatbread_search_traces and
11981
12406
  flatbread_get_capsule, for your coding agent
11982
12407
  flatbread search <words> Find runs: this machine's shared and captured ones, then
11983
12408
  the site (the same answer the MCP server gives)
11984
12409
  flatbread setup Auto-capture: the Claude Code hook, plus a chained git
11985
- post-commit in this repository. Captures stay local
12410
+ post-commit in this repository, and the MCP server for
12411
+ every Claude Code project. Captures stay local
11986
12412
  flatbread setup --uninstall Remove exactly what setup added
11987
12413
  flatbread capture --file <path> Capture one session into the local bucket (setup's hooks
11988
12414
  run this; redacted, private, never uploaded)
@@ -12024,9 +12450,10 @@ Options for search:
12024
12450
 
12025
12451
  Options for setup:
12026
12452
  --dry-run Say what would be written, write nothing
12027
- --uninstall Remove the handlers and the post-commit; restore a
12028
- post-commit hook setup had chained
12453
+ --uninstall Remove the handlers, the MCP server and the post-commit;
12454
+ restore a post-commit hook setup had chained
12029
12455
  --no-git Leave the repository's hooks alone
12456
+ --no-mcp Leave Claude Code's MCP servers alone
12030
12457
  --settings <path> --no-probe As for hook install
12031
12458
 
12032
12459
  Options for capture:
@@ -12049,14 +12476,16 @@ Options for adapters:
12049
12476
  Notes:
12050
12477
  parse and share record the git branch, HEAD commit and uncommitted file names when the
12051
12478
  session ran inside a git repository. Nothing else from git is read.
12052
- parse --save writes ${path20.relative(process.cwd(), OUTPUT_PATH) || OUTPUT_PATH}
12053
- Share drafts are written to ${path20.relative(process.cwd(), SHARE_OUTPUT_PATH) || SHARE_OUTPUT_PATH}
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)
12054
12482
  Every parsed run defaults to visibility "private".
12055
12483
  Nothing is uploaded unless you pass --publish, or set FLATBREAD_API_URL and
12056
12484
  FLATBREAD_API_TOKEN yourself. pull and mcp only read: public runs, your own when you
12057
12485
  are signed in, and a private run through its share link. The review UI listens on
12058
- 127.0.0.1 only. Add the MCP server to Claude Code with:
12059
- 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
12060
12489
  The viewer is a separate dev server: npm run dev -w @flatbread/web
12061
12490
  Captured runs live in ~/.flatbread/bucket (FLATBREAD_HOOK_DISABLED=1 stops every capture).
12062
12491
  A capture is not a share: to publish one, run share --file on its session.
@@ -12085,10 +12514,10 @@ function parseArgv(argv) {
12085
12514
  } else if (arg === "--out") {
12086
12515
  const value = argv[i + 1];
12087
12516
  if (value === void 0 || value.startsWith("--")) throw new Error("--out needs a file path");
12088
- args.out = path20.resolve(value);
12517
+ args.out = path21.resolve(value);
12089
12518
  i += 1;
12090
12519
  } else if (arg !== void 0 && arg.startsWith("--out=")) {
12091
- args.out = path20.resolve(arg.slice("--out=".length));
12520
+ args.out = path21.resolve(arg.slice("--out=".length));
12092
12521
  } else if (arg === "--harness") {
12093
12522
  const value = argv[i + 1];
12094
12523
  if (value === void 0 || value.startsWith("--")) {
@@ -12209,12 +12638,12 @@ async function runParse(args) {
12209
12638
  target = found[0];
12210
12639
  console.log(`latest ${harness} session: ${target}`);
12211
12640
  } else if (args.file) {
12212
- target = path20.resolve(args.file);
12213
- if (!fs21.existsSync(target)) {
12641
+ target = path21.resolve(args.file);
12642
+ if (!fs22.existsSync(target)) {
12214
12643
  console.error(`file not found: ${target}`);
12215
12644
  return 1;
12216
12645
  }
12217
- if (!fs21.statSync(target).isFile()) {
12646
+ if (!fs22.statSync(target).isFile()) {
12218
12647
  console.error(`not a file: ${target}`);
12219
12648
  return 1;
12220
12649
  }
@@ -12245,8 +12674,8 @@ async function runParse(args) {
12245
12674
  const trace = withLineage(result.trace, readGitLineage(result.trace.source.cwd));
12246
12675
  FlatbreadTraceSchema.parse(trace);
12247
12676
  if (args.out !== void 0) {
12248
- fs21.mkdirSync(path20.dirname(args.out), { recursive: true });
12249
- fs21.writeFileSync(args.out, JSON.stringify(trace, null, 2) + "\n", "utf8");
12677
+ fs22.mkdirSync(path21.dirname(args.out), { recursive: true });
12678
+ fs22.writeFileSync(args.out, JSON.stringify(trace, null, 2) + "\n", "utf8");
12250
12679
  }
12251
12680
  report({ ...result, trace }, args.out);
12252
12681
  return 0;
@@ -12310,8 +12739,8 @@ async function runAdapters(args) {
12310
12739
  }
12311
12740
  let input = null;
12312
12741
  if (args.file !== void 0) {
12313
- const resolved = path20.resolve(args.file);
12314
- if (!fs21.existsSync(resolved)) {
12742
+ const resolved = path21.resolve(args.file);
12743
+ if (!fs22.existsSync(resolved)) {
12315
12744
  console.error(`file not found: ${resolved}`);
12316
12745
  return 1;
12317
12746
  }
@@ -12637,7 +13066,7 @@ async function prompt(label) {
12637
13066
  });
12638
13067
  }
12639
13068
  var HOOK_SUBCOMMANDS = ["install", "status", "uninstall", "notify"];
12640
- var CLI_ENTRY = fileURLToPath(import.meta.url);
13069
+ var CLI_ENTRY = fileURLToPath2(import.meta.url);
12641
13070
  function parseHookArgv(argv) {
12642
13071
  const [sub, ...rest] = argv;
12643
13072
  if (sub === void 0) {
@@ -12730,13 +13159,13 @@ function parsePullArgv(argv) {
12730
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");
12731
13160
  return { ref, ...out !== void 0 ? { out } : {} };
12732
13161
  }
12733
- async function runPull(args) {
13162
+ async function runPull(args, deps = { ...defaultMcpDeps(storeDir(), bucketDir()), capsule: (ref) => fetchCapsule(ref) }) {
12734
13163
  const ref = parseRunRef(args.ref);
12735
13164
  if (ref === null) {
12736
13165
  console.error(`"${args.ref}" is not a run ID or a /r/<id> link`);
12737
13166
  return 1;
12738
13167
  }
12739
- const result = await fetchCapsule(ref);
13168
+ const result = await readCapsule(ref, deps);
12740
13169
  if (!result.ok) {
12741
13170
  console.error(describeReadFailure(result, `could not pull ${ref.slug}`));
12742
13171
  return 1;
@@ -12745,10 +13174,10 @@ async function runPull(args) {
12745
13174
  process.stdout.write(result.capsule);
12746
13175
  return 0;
12747
13176
  }
12748
- const target = path20.resolve(args.out);
12749
- fs21.mkdirSync(path20.dirname(target), { recursive: true });
12750
- fs21.writeFileSync(target, result.capsule, "utf8");
12751
- 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"})`}`);
12752
13181
  return 0;
12753
13182
  }
12754
13183
  function parseSearchArgv(argv) {
@@ -12860,6 +13289,7 @@ function parseSetupArgv(argv) {
12860
13289
  dryRun: false,
12861
13290
  uninstall: false,
12862
13291
  noGit: false,
13292
+ noMcp: false,
12863
13293
  noProbe: false
12864
13294
  };
12865
13295
  for (let i = 0; i < argv.length; i += 1) {
@@ -12867,6 +13297,7 @@ function parseSetupArgv(argv) {
12867
13297
  if (arg === "--dry-run") args.dryRun = true;
12868
13298
  else if (arg === "--uninstall") args.uninstall = true;
12869
13299
  else if (arg === "--no-git") args.noGit = true;
13300
+ else if (arg === "--no-mcp") args.noMcp = true;
12870
13301
  else if (arg === "--no-probe") args.noProbe = true;
12871
13302
  else if (arg === "--settings") {
12872
13303
  const value = argv[i + 1];
@@ -12913,6 +13344,7 @@ async function main(argv) {
12913
13344
  dryRun: setup.dryRun,
12914
13345
  uninstall: setup.uninstall,
12915
13346
  noGit: setup.noGit,
13347
+ noMcp: setup.noMcp,
12916
13348
  noProbe: setup.noProbe,
12917
13349
  ...setup.settings !== void 0 ? { settingsPath: setup.settings } : {}
12918
13350
  });
@@ -12994,12 +13426,12 @@ var invokedDirectly = (() => {
12994
13426
  if (entry === void 0) return false;
12995
13427
  const real = (file) => {
12996
13428
  try {
12997
- return fs21.realpathSync(file);
13429
+ return fs22.realpathSync(file);
12998
13430
  } catch {
12999
- return path20.resolve(file);
13431
+ return path21.resolve(file);
13000
13432
  }
13001
13433
  };
13002
- return real(entry) === real(fileURLToPath(import.meta.url));
13434
+ return real(entry) === real(fileURLToPath2(import.meta.url));
13003
13435
  })();
13004
13436
  if (invokedDirectly) {
13005
13437
  main(process.argv.slice(2)).then((code) => {
@@ -13022,6 +13454,7 @@ export {
13022
13454
  runCapture,
13023
13455
  runLogin,
13024
13456
  runLogout,
13457
+ runPull,
13025
13458
  runSearch,
13026
13459
  runWhoami
13027
13460
  };