flatbread-cli 0.2.0 → 0.3.1

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: "app brain/<id>/.system_generated/logs/transcript.jsonl, or IDE conversations/<id>.pb",
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: "Both stores measured on one machine. The app's transcript is read the way deja-vu reads it: prompts, replies and the planner's tool calls, not tool output. The IDE's .pb is read the way deja-vu reads Windsurf: prompts and replies only."
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,417 @@ 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 = 2;
6056
+ var KEY = Buffer.from("safeCodeiumworldKeYsecretBalloon");
6057
+ var NONCE = 12;
6058
+ var TAG = 16;
6059
+ var CONVERSATION = /[\\/]\.gemini[\\/]antigravity[^\\/]*[\\/]conversations[\\/][^\\/]+\.pb$/i;
6060
+ var TRANSCRIPT = /[\\/]\.gemini[\\/]antigravity[^\\/]*[\\/]brain[\\/][^\\/]+[\\/]\.system_generated[\\/]logs[\\/]transcript\.jsonl$/i;
6061
+ var PB_VARINT = 0;
6062
+ var PB_FIXED64 = 1;
6063
+ var PB_BYTES = 2;
6064
+ var PB_FIXED32 = 5;
6065
+ var Malformed = class extends Error {
6066
+ };
6067
+ function varint(b, at) {
6068
+ let value = 0n;
6069
+ let shift = 0n;
6070
+ for (let i = at; i < b.length && shift <= 63n; i++) {
6071
+ const byte = b[i];
6072
+ value |= BigInt(byte & 127) << shift;
6073
+ shift += 7n;
6074
+ if ((byte & 128) === 0) return [value, i + 1];
6075
+ }
6076
+ throw new Malformed("malformed protobuf");
6077
+ }
6078
+ function pbFields(b) {
6079
+ const out = [];
6080
+ let i = 0;
6081
+ while (i < b.length) {
6082
+ const [key, afterKey] = varint(b, i);
6083
+ i = afterKey;
6084
+ const num2 = Number(key >> 3n);
6085
+ const wire = Number(key & 7n);
6086
+ if (num2 === 0) throw new Malformed("malformed protobuf");
6087
+ let value = 0n;
6088
+ let data = Buffer.alloc(0);
6089
+ if (wire === PB_VARINT) {
6090
+ [value, i] = varint(b, i);
6091
+ } else if (wire === PB_FIXED64) {
6092
+ if (i + 8 > b.length) throw new Malformed("malformed protobuf");
6093
+ value = b.readBigUInt64LE(i);
6094
+ i += 8;
6095
+ } else if (wire === PB_FIXED32) {
6096
+ if (i + 4 > b.length) throw new Malformed("malformed protobuf");
6097
+ value = BigInt(b.readUInt32LE(i));
6098
+ i += 4;
6099
+ } else if (wire === PB_BYTES) {
6100
+ const [length, afterLength] = varint(b, i);
6101
+ if (length > BigInt(b.length - afterLength)) throw new Malformed("malformed protobuf");
6102
+ data = b.subarray(afterLength, afterLength + Number(length));
6103
+ i = afterLength + Number(length);
6104
+ } else {
6105
+ throw new Malformed("malformed protobuf");
6106
+ }
6107
+ out.push({ num: num2, wire, value, data });
6108
+ }
6109
+ return out;
6110
+ }
6111
+ function bytesField(fields, num2) {
6112
+ return fields.find((f) => f.num === num2 && f.wire === PB_BYTES)?.data;
6113
+ }
6114
+ function pbString(b) {
6115
+ return b === void 0 ? "" : b.toString("utf8").replaceAll("\0", "");
6116
+ }
6117
+ function pbTimestamp(b) {
6118
+ if (b === void 0) return void 0;
6119
+ let seconds = 0n;
6120
+ let nanos = 0n;
6121
+ for (const f of pbFields(b)) {
6122
+ if (f.wire !== PB_VARINT) continue;
6123
+ if (f.num === 1) seconds = BigInt.asIntN(64, f.value);
6124
+ if (f.num === 2) nanos = BigInt.asIntN(32, f.value);
6125
+ }
6126
+ if (seconds === 0n && nanos === 0n) return void 0;
6127
+ const ms = Number(seconds) * 1e3 + Math.floor(Number(nanos) / 1e6);
6128
+ const date = new Date(ms);
6129
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
6130
+ }
6131
+ function openConversation(b) {
6132
+ if (b.length < NONCE + TAG) return null;
6133
+ try {
6134
+ const decipher = crypto.createDecipheriv("aes-256-gcm", KEY, b.subarray(0, NONCE));
6135
+ decipher.setAuthTag(b.subarray(b.length - TAG));
6136
+ return Buffer.concat([decipher.update(b.subarray(NONCE, b.length - TAG)), decipher.final()]);
6137
+ } catch {
6138
+ return null;
6139
+ }
6140
+ }
6141
+ function folderPath(uri) {
6142
+ if (!uri.startsWith("file:")) return void 0;
6143
+ try {
6144
+ return fileURLToPath(uri);
6145
+ } catch {
6146
+ try {
6147
+ return decodeURIComponent(new URL(uri).pathname) || void 0;
6148
+ } catch {
6149
+ return void 0;
6150
+ }
6151
+ }
6152
+ }
6153
+ function workspaceOf(metadata) {
6154
+ if (metadata === void 0) return void 0;
6155
+ for (const f of pbFields(metadata)) {
6156
+ if (f.num !== 1 || f.wire !== PB_BYTES) continue;
6157
+ const folder = folderPath(pbString(bytesField(pbFields(f.data), 1)));
6158
+ if (folder !== void 0) return folder;
6159
+ }
6160
+ return void 0;
6161
+ }
6162
+ function tally(counts, key) {
6163
+ counts.set(key, (counts.get(key) ?? 0) + 1);
6164
+ }
6165
+ function decodeTrajectory2(plain) {
6166
+ const events = [];
6167
+ const unread = /* @__PURE__ */ new Map();
6168
+ let sessionId;
6169
+ let cwd;
6170
+ let steps = 0;
6171
+ let clock;
6172
+ for (const f of pbFields(plain)) {
6173
+ if (f.wire === PB_BYTES && f.num === 6) {
6174
+ sessionId = pbString(f.data) || sessionId;
6175
+ continue;
6176
+ }
6177
+ if (f.wire === PB_BYTES && f.num === 7) {
6178
+ cwd ??= workspaceOf(f.data);
6179
+ clock ??= pbTimestamp(bytesField(pbFields(f.data), 2));
6180
+ continue;
6181
+ }
6182
+ if (f.wire !== PB_BYTES || f.num !== 2) {
6183
+ tally(unread, `trajectory field ${f.num}`);
6184
+ continue;
6185
+ }
6186
+ steps++;
6187
+ const step = pbFields(f.data);
6188
+ const metadata = bytesField(step, 5);
6189
+ const metaFields = metadata === void 0 ? [] : pbFields(metadata);
6190
+ clock = pbTimestamp(bytesField(metaFields, 1)) ?? clock;
6191
+ const completed = pbTimestamp(bytesField(metaFields, 8));
6192
+ for (const s of step) {
6193
+ if (s.num !== 5 && s.num !== 19 && s.num !== 20) tally(unread, `step field ${s.num}`);
6194
+ }
6195
+ const input = bytesField(step, 19);
6196
+ if (input !== void 0) {
6197
+ const fields = pbFields(input);
6198
+ const text2 = pbString(bytesField(fields, 2)).trim() || pbString(bytesField(fields, 1)).trim();
6199
+ if (text2) events.push({ kind: "prompt", ...clock ? { timestamp: clock } : {}, text: text2 });
6200
+ }
6201
+ const planner = bytesField(step, 20);
6202
+ if (planner !== void 0) {
6203
+ const fields = pbFields(planner);
6204
+ const text2 = pbString(bytesField(fields, 1)).trim() || pbString(bytesField(fields, 8)).trim();
6205
+ if (bytesField(fields, 3) !== void 0) tally(unread, "planner thinking");
6206
+ const at = completed ?? clock;
6207
+ if (text2) events.push({ kind: "text", ...at ? { timestamp: at } : {}, text: text2 });
6208
+ }
6209
+ }
6210
+ if (cwd !== void 0 || sessionId !== void 0) {
6211
+ events.unshift({ kind: "meta", ...sessionId ? { sessionId } : {}, ...cwd ? { cwd } : {} });
6212
+ }
6213
+ return { events, ...sessionId ? { sessionId } : {}, steps, unread };
6214
+ }
6215
+ function homeOf(env) {
6216
+ return (process.platform === "win32" ? env.USERPROFILE : env.HOME) || os2.homedir();
6217
+ }
6218
+ var USER_BLOCKS = [/<ADDITIONAL_METADATA>[\s\S]*?<\/ADDITIONAL_METADATA>/g, /<USER_SETTINGS_CHANGE>[\s\S]*?<\/USER_SETTINGS_CHANGE>/g];
6219
+ var ARTIFACT_COMMENT = /^Comments on artifact URI:.*$|^The user has (?:approved|rejected) this document\.$/gm;
6220
+ var REQUEST_TAG = /[ \t]*\n?\s*<\/?USER_REQUEST>\s*/g;
6221
+ function cleanUserContent(text2) {
6222
+ let out = text2;
6223
+ for (const block of USER_BLOCKS) out = out.replace(block, "");
6224
+ out = out.replace(ARTIFACT_COMMENT, "");
6225
+ return out.replace(REQUEST_TAG, "\n").trim();
6226
+ }
6227
+ function decodeArg(value) {
6228
+ if (typeof value !== "string" || !value.startsWith('"')) return value;
6229
+ try {
6230
+ return JSON.parse(value);
6231
+ } catch {
6232
+ return value;
6233
+ }
6234
+ }
6235
+ function isRecord(value) {
6236
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6237
+ }
6238
+ function decodeTranscript(text2) {
6239
+ const events = [];
6240
+ const unread = /* @__PURE__ */ new Map();
6241
+ let cwd;
6242
+ let steps = 0;
6243
+ let line = 0;
6244
+ for (const raw of text2.replace(/\r\n/g, "\n").split("\n")) {
6245
+ line++;
6246
+ if (raw.trim() === "") continue;
6247
+ let record;
6248
+ try {
6249
+ record = JSON.parse(raw);
6250
+ } catch {
6251
+ tally(unread, "malformed line");
6252
+ continue;
6253
+ }
6254
+ if (!isRecord(record)) {
6255
+ tally(unread, "malformed line");
6256
+ continue;
6257
+ }
6258
+ steps++;
6259
+ const source = typeof record["source"] === "string" ? record["source"] : "";
6260
+ const type = typeof record["type"] === "string" ? record["type"] : "";
6261
+ const content = typeof record["content"] === "string" ? record["content"] : "";
6262
+ const created = typeof record["created_at"] === "string" && !Number.isNaN(Date.parse(record["created_at"])) ? record["created_at"] : void 0;
6263
+ const at = created ? { timestamp: created } : {};
6264
+ if (source === "USER_EXPLICIT") {
6265
+ const prompt2 = cleanUserContent(content);
6266
+ if (prompt2) events.push({ kind: "prompt", ...at, text: prompt2 });
6267
+ continue;
6268
+ }
6269
+ if (source !== "MODEL") {
6270
+ tally(unread, type ? `${source || "no"} source, type ${type}` : `${source || "no"} source`);
6271
+ continue;
6272
+ }
6273
+ if (type !== "" && type !== "PLANNER_RESPONSE") {
6274
+ tally(unread, `tool step ${type}`);
6275
+ continue;
6276
+ }
6277
+ if (content.trim()) events.push({ kind: "text", ...at, text: content.trim() });
6278
+ const calls = Array.isArray(record["tool_calls"]) ? record["tool_calls"] : [];
6279
+ calls.forEach((call, k) => {
6280
+ if (!isRecord(call) || typeof call["name"] !== "string") return;
6281
+ const args = {};
6282
+ if (isRecord(call["args"])) for (const [key, value] of Object.entries(call["args"])) args[key] = decodeArg(value);
6283
+ if (cwd === void 0 && call["name"] === "run_command" && typeof args["Cwd"] === "string" && args["Cwd"]) cwd = args["Cwd"];
6284
+ const step = typeof record["step_index"] === "number" ? record["step_index"] : `line${line}`;
6285
+ events.push({ kind: "tool_call", ...at, id: `${step}.${k}`, name: call["name"], input: args });
6286
+ });
6066
6287
  }
6067
- return dirs;
6288
+ if (cwd !== void 0) events.unshift({ kind: "meta", cwd });
6289
+ return { events, steps, unread };
6290
+ }
6291
+ function transcriptId(file) {
6292
+ return path3.basename(path3.dirname(path3.dirname(path3.dirname(file))));
6293
+ }
6294
+ function listConversations(env = process.env) {
6295
+ const gemini = path3.join(homeOf(env), ".gemini");
6296
+ let roots;
6297
+ try {
6298
+ roots = fs3.readdirSync(gemini).filter((name) => /^antigravity/i.test(name));
6299
+ } catch {
6300
+ return [];
6301
+ }
6302
+ const statFile = (file) => {
6303
+ try {
6304
+ const stat = fs3.statSync(file);
6305
+ return stat.isFile() ? stat : null;
6306
+ } catch {
6307
+ return null;
6308
+ }
6309
+ };
6310
+ const readNames = (dir) => {
6311
+ try {
6312
+ return fs3.readdirSync(dir);
6313
+ } catch {
6314
+ return [];
6315
+ }
6316
+ };
6317
+ const pb = /* @__PURE__ */ new Map();
6318
+ const transcripts = /* @__PURE__ */ new Map();
6319
+ for (const root of roots) {
6320
+ const conversations = path3.join(gemini, root, "conversations");
6321
+ for (const name of readNames(conversations)) {
6322
+ if (!name.toLowerCase().endsWith(".pb")) continue;
6323
+ const file = path3.join(conversations, name);
6324
+ const stat = statFile(file);
6325
+ if (stat === null) continue;
6326
+ const id = name.slice(0, -".pb".length).toLowerCase();
6327
+ const kept = pb.get(id);
6328
+ if (kept === void 0 || stat.size > kept.size) pb.set(id, { file, mtime: stat.mtimeMs, size: stat.size });
6329
+ }
6330
+ const brain = path3.join(gemini, root, "brain");
6331
+ for (const name of readNames(brain)) {
6332
+ const file = path3.join(brain, name, ".system_generated", "logs", "transcript.jsonl");
6333
+ const stat = statFile(file);
6334
+ if (stat === null) continue;
6335
+ const id = name.toLowerCase();
6336
+ const kept = transcripts.get(id);
6337
+ if (kept === void 0 || stat.size > kept.size) transcripts.set(id, { file, mtime: stat.mtimeMs, size: stat.size });
6338
+ }
6339
+ }
6340
+ for (const id of transcripts.keys()) pb.delete(id);
6341
+ return [...pb.values(), ...transcripts.values()].sort((a, b) => b.mtime - a.mtime).map((entry) => entry.file);
6342
+ }
6343
+ var REFUSAL_TEST = "A6";
6344
+ async function readTranscript(file) {
6345
+ const trajectory = decodeTranscript(await fs3.promises.readFile(file, "utf8"));
6346
+ const id = transcriptId(file);
6347
+ const said = trajectory.events.filter((e) => e.kind === "prompt" || e.kind === "text").length;
6348
+ if (said === 0) {
6349
+ return {
6350
+ refused: true,
6351
+ reason: `${id}'s transcript has ${trajectory.steps} record(s) and no prompt or reply this reader knows.`,
6352
+ humanTest: REFUSAL_TEST
6353
+ };
6354
+ }
6355
+ return {
6356
+ events: trajectory.events,
6357
+ capabilities: { tokens: "unavailable", filesChanged: "unavailable" },
6358
+ sessionId: id,
6359
+ unreadFields: [...trajectory.unread].sort((a, b) => b[1] - a[1]).map(([field, count]) => `${field} x${count}`),
6360
+ notes: [
6361
+ `${trajectory.steps} record(s), ${said} prompt/reply event(s)`,
6362
+ "tool steps' bodies and checkpoints are not read; tool calls come from the planner's tool_calls",
6363
+ "CONFIDENCE: documented (deja-vu's transcript reader, measured on app 2.24.0). See HUMAN-TESTS.md A6."
6364
+ ]
6365
+ };
6068
6366
  }
6069
6367
  var antigravityAdapter = {
6070
6368
  id: "antigravity",
6071
- version: 0,
6369
+ version: ANTIGRAVITY_ADAPTER_VERSION,
6072
6370
  async detect(input) {
6073
- void input;
6074
- return { score: 0, reason: "Antigravity's log format is unknown; this adapter cannot claim a file" };
6371
+ if (CONVERSATION.test(input.path)) {
6372
+ return { score: 85, reason: "a conversations/<id>.pb under ~/.gemini/antigravity" };
6373
+ }
6374
+ if (TRANSCRIPT.test(input.path)) {
6375
+ return { score: 85, reason: "a brain/<id>/.system_generated/logs/transcript.jsonl under ~/.gemini/antigravity" };
6376
+ }
6377
+ return { score: 0, reason: "not an Antigravity conversations/<id>.pb or brain transcript" };
6075
6378
  },
6076
6379
  async read(input) {
6077
- void input;
6380
+ if (input.path.toLowerCase().endsWith(".jsonl")) return readTranscript(input.path);
6381
+ const plain = openConversation(await fs3.promises.readFile(input.path));
6382
+ if (plain === null) {
6383
+ return {
6384
+ refused: true,
6385
+ 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.`,
6386
+ humanTest: REFUSAL_TEST
6387
+ };
6388
+ }
6389
+ let trajectory;
6390
+ try {
6391
+ trajectory = decodeTrajectory2(plain);
6392
+ } catch (error) {
6393
+ if (!(error instanceof Malformed)) throw error;
6394
+ return {
6395
+ refused: true,
6396
+ reason: `${path3.basename(input.path)} authenticated but is not the trajectory protobuf this reader knows.`,
6397
+ humanTest: REFUSAL_TEST
6398
+ };
6399
+ }
6400
+ const said = trajectory.events.filter((e) => e.kind === "prompt" || e.kind === "text").length;
6401
+ if (said === 0) {
6402
+ return {
6403
+ refused: true,
6404
+ reason: `${path3.basename(input.path)} has ${trajectory.steps} step(s) and no prompt or reply in the fields this reader knows.`,
6405
+ humanTest: REFUSAL_TEST
6406
+ };
6407
+ }
6408
+ const unreadFields = [...trajectory.unread].sort((a, b) => b[1] - a[1]).map(([field, count]) => `${field} x${count}`);
6078
6409
  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"
6410
+ events: trajectory.events,
6411
+ capabilities: { tokens: "unavailable", filesChanged: "unavailable" },
6412
+ sessionId: trajectory.sessionId ?? path3.basename(input.path, ".pb"),
6413
+ unreadFields,
6414
+ notes: [
6415
+ `${trajectory.steps} step(s), ${said} prompt/reply event(s)`,
6416
+ "tool calls, thinking and edits are not read: their step fields are undocumented",
6417
+ "CONFIDENCE: documented (Windsurf's schema, measured on IDE 1.23.2). See HUMAN-TESTS.md A6."
6418
+ ]
6082
6419
  };
6083
6420
  },
6084
6421
  discover(env) {
6085
- return candidateStateDirs(env).filter((dir) => fs3.existsSync(dir));
6422
+ return listConversations(env);
6086
6423
  },
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}`);
6424
+ async describe(input) {
6425
+ const files = input ? [input.path] : listConversations();
6426
+ const lines2 = [`conversations found: ${files.length}`];
6427
+ for (const file of files.slice(0, 8)) lines2.push(` ${file}`);
6428
+ const target = files[0];
6429
+ if (target === void 0) return lines2;
6430
+ lines2.push("", `inspecting ${target}`);
6431
+ if (target.toLowerCase().endsWith(".jsonl")) {
6432
+ try {
6433
+ const trajectory = decodeTranscript(await fs3.promises.readFile(target, "utf8"));
6434
+ lines2.push(` records: ${trajectory.steps}, events: ${trajectory.events.filter((e) => e.kind !== "meta").length}`);
6435
+ for (const [field, count] of [...trajectory.unread].sort((a, b) => b[1] - a[1]).slice(0, 15)) {
6436
+ lines2.push(` ${String(count).padStart(6)} ${field}`);
6437
+ }
6438
+ } catch (error) {
6439
+ lines2.push(` unreadable: ${error instanceof Error ? error.message : String(error)}`);
6440
+ }
6441
+ return lines2;
6442
+ }
6443
+ let plain;
6444
+ try {
6445
+ plain = openConversation(await fs3.promises.readFile(target));
6446
+ } catch (error) {
6447
+ lines2.push(` unreadable: ${error instanceof Error ? error.message : String(error)}`);
6448
+ return lines2;
6449
+ }
6450
+ lines2.push(` authenticated: ${plain === null ? "no" : "yes"}`);
6451
+ if (plain === null) return lines2;
6452
+ try {
6453
+ const trajectory = decodeTrajectory2(plain);
6454
+ lines2.push(` steps: ${trajectory.steps}, prompt/reply events: ${trajectory.events.filter((e) => e.kind !== "meta").length}`);
6455
+ for (const [field, count] of [...trajectory.unread].sort((a, b) => b[1] - a[1]).slice(0, 15)) {
6456
+ lines2.push(` ${String(count).padStart(6)} ${field}`);
6457
+ }
6458
+ } catch {
6459
+ lines2.push(" not the trajectory protobuf this reader knows");
6091
6460
  }
6092
- lines2.push("");
6093
- lines2.push("If one is FOUND, HUMAN-TESTS.md A6 lists exactly what to report back.");
6094
6461
  return lines2;
6095
6462
  }
6096
6463
  };
@@ -8752,7 +9119,7 @@ function withLineage(trace, lineage, prUrl) {
8752
9119
  }
8753
9120
 
8754
9121
  // src/auth.ts
8755
- import crypto from "node:crypto";
9122
+ import crypto2 from "node:crypto";
8756
9123
  import fs15 from "node:fs";
8757
9124
  import os6 from "node:os";
8758
9125
  import path14 from "node:path";
@@ -8782,7 +9149,7 @@ function isAuthFailure(value) {
8782
9149
  return typeof value === "object" && value !== null && "ok" in value && value.ok === false;
8783
9150
  }
8784
9151
  function hashSecret(secret) {
8785
- return crypto.createHash("sha256").update(secret).digest("hex");
9152
+ return crypto2.createHash("sha256").update(secret).digest("hex");
8786
9153
  }
8787
9154
  function apiUrlFrom(env = process.env) {
8788
9155
  const explicit = env["FLATBREAD_API_URL"]?.trim();
@@ -8795,7 +9162,7 @@ async function startCliLogin(options = {}) {
8795
9162
  const apiUrl = options.apiUrl ?? apiUrlFrom(env);
8796
9163
  const base = validateApiUrl(apiUrl);
8797
9164
  if (isAuthFailure(base)) return base;
8798
- const pollSecret = crypto.randomBytes(32).toString("hex");
9165
+ const pollSecret = crypto2.randomBytes(32).toString("hex");
8799
9166
  const result = await postJson(
8800
9167
  `${base}/api/cli-login/start`,
8801
9168
  { poll_secret_hash: hashSecret(pollSecret) },
@@ -9586,67 +9953,105 @@ function renderMarked(value, matches) {
9586
9953
  return html;
9587
9954
  }
9588
9955
  var STYLE = `
9589
- :root{--bg:#0f1115;--panel:#161a22;--line:#232936;--fg:#e6e9ef;--dim:#8b93a7;--hit:#3a2a12;--hitline:#c8892b;--ok:#2f9e63;--warn:#c85a54;--acc:#5b8def}
9956
+ :root{
9957
+ --canvas:#fafafa;--bg:#ffffff;--ivory:#fcfbf8;--surface:#f6f7f8;--inset:#f6f7f8;
9958
+ --line:rgba(226,232,240,.8);--line-soft:#eff0f2;--line-strong:#b9bec4;
9959
+ --fg:#23272c;--fg-2:rgba(35,39,44,.7);--fg-3:rgba(35,39,44,.56);--fg-lift:#2b3138;
9960
+ --fail-bg:#f6ece7;--fail-line:#c9755c;--fail-fg:#8a3b2a;
9961
+ --ok-bg:#eef1e8;--ok-line:#7f9a7a;--ok-fg:#3f5d43;
9962
+ --hold-bg:#f7f1e2;--hold-line:#c9a34a;--hold-fg:#7d601d;
9963
+ --intent-line:#d6d1c1;
9964
+ --shade:30,41,54;
9965
+ --shadow:0 1px 2px rgba(var(--shade),.04),0 10px 26px rgba(var(--shade),.08);
9966
+ --radius:10px;--radius-sm:6px;
9967
+ --sans:-apple-system,BlinkMacSystemFont,"Segoe UI Variable Text","Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
9968
+ --mono:ui-monospace,"SF Mono",SFMono-Regular,"Cascadia Mono","Segoe UI Mono","Roboto Mono",Menlo,Consolas,"Liberation Mono",monospace
9969
+ }
9590
9970
  *{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}
9971
+ html,body{margin:0}
9972
+ body{background:var(--canvas);color:var(--fg);font:14px/1.55 var(--sans);-webkit-font-smoothing:antialiased}
9973
+ code,pre,dd,.mono{font-family:var(--mono)}
9974
+ header{background:var(--bg);border-bottom:1px solid var(--line-soft)}
9975
+ .hin{max-width:1440px;margin:0 auto;padding:14px 28px 16px}
9976
+ .bar{display:flex;align-items:center;gap:10px;margin:0 0 12px}
9977
+ .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}
9978
+ .crumb{color:var(--fg-3);font-size:12.5px}
9979
+ h1{margin:0 0 6px;font-size:20px;line-height:1.3;font-weight:600;letter-spacing:-.01em}
9980
+ .meta{color:var(--fg-3);font-size:12.5px;font-variant-numeric:tabular-nums}
9595
9981
  .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}
9982
+ .meta.path{font-family:var(--mono);font-size:11.5px;margin-top:2px;word-break:break-all}
9983
+ .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}
9984
+ main{min-width:0}
9985
+ 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}
9986
+ .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}
9987
+ .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}
9988
+ .badge{font-weight:600;color:var(--fg-2);font-size:12px}
9989
+ .step.user_prompt{border-left:3px solid var(--intent-line)}
9990
+ .step.error_recovery{border-color:color-mix(in srgb,var(--fail-line) 45%,transparent);border-left:3px solid var(--fail-line)}
9991
+ .badge.error_recovery,.sub.err{color:var(--fail-fg)}
9992
+ 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)}
9993
+ .step.user_prompt>pre,.step.assistant_text>pre{font:14px/1.6 var(--sans)}
9994
+ .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}
9995
+ .sub+pre,.sub+dl{background:var(--inset);margin:8px 14px 12px;padding:10px 12px;border-radius:var(--radius-sm)}
9996
+ dl{margin:0;padding:4px 14px 12px}
9997
+ dt{color:var(--fg-3);font-size:11.5px;margin-top:8px}
9998
+ dd{margin:2px 0 0;white-space:pre-wrap;word-break:break-word;font-size:12.5px}
9999
+ #trace-meta dd{font-family:var(--sans);font-size:13.5px}
10000
+ 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
10001
  mark.hit .was{display:none}
9615
- mark.hit.keep{background:transparent;color:inherit;border-bottom:1px dashed var(--warn)}
10002
+ 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
10003
  mark.hit.keep .tok{display:none}
9617
10004
  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)}
10005
+ aside h2{margin:0 0 4px;font-size:15px;font-weight:600}
10006
+ aside .note{color:var(--fg-3);font-size:12.5px;margin:0 0 12px}
10007
+ aside .note code{font-size:11.5px;background:var(--inset);padding:1px 4px;border-radius:4px}
9644
10008
  .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)}
10009
+ .keys{color:var(--fg-3);font-size:12px;margin:0 0 14px}
10010
+ .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}
10011
+ fieldset{border:0;border-top:1px solid var(--line-soft);margin:0;padding:14px 0 12px}
10012
+ legend{float:left;width:100%;padding:0;margin:0 0 6px;color:var(--fg-3);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
10013
+ legend+*{clear:left}
10014
+ 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}
10015
+ input.title:focus{outline:none;background:#fff;border-color:var(--fg-3);box-shadow:0 0 0 3px rgba(var(--shade),.05)}
10016
+ label.vis{display:flex;align-items:center;gap:8px;padding:3px 0;cursor:pointer}
10017
+ input[type=radio],input[type=checkbox]{accent-color:var(--fg);margin:0}
10018
+ .warn{color:var(--fail-fg);font-size:12.5px;margin:6px 0 0}
10019
+ .actions{display:flex;gap:8px;padding:14px 0 16px;border-top:1px solid var(--line-soft)}
10020
+ 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}
10021
+ button:hover{background:var(--surface)}
10022
+ button:focus-visible{outline:2px solid var(--fg);outline-offset:1px}
10023
+ button.primary{background:var(--fg);border-color:var(--fg);color:#fff}
10024
+ button.primary:hover{background:var(--fg-lift);border-color:var(--fg-lift)}
10025
+ .tools button{padding:5px 10px;font-size:12px}
10026
+ .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)}
10027
+ .row:hover{background:var(--surface)}
10028
+ .row:focus-within{border-color:var(--fg-3)}
10029
+ .row input{margin:3px 0 0}
10030
+ .state{font-size:11.5px;font-weight:600}
10031
+ .s-red{color:var(--fg-3)}
10032
+ .s-keep{display:none;color:var(--hold-fg)}
10033
+ .row input:checked~.state .s-red{display:none}
10034
+ .row input:checked~.state .s-keep{display:inline}
10035
+ .row:has(input:checked){background:var(--hold-bg);border-color:color-mix(in srgb,var(--hold-line) 50%,transparent)}
10036
+ .row .rule{font-size:13px;color:var(--fg)}
10037
+ .row .times{font-weight:600}
10038
+ .row .prev{color:var(--fg-2);font:11.5px/1.5 var(--mono);word-break:break-all}
10039
+ .row .path{color:var(--fg-3);font-size:11px;word-break:break-all}
10040
+ .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)}
10041
+ .done h1{font-size:20px}
10042
+ .done .meta{font-size:13.5px;color:var(--fg-2);margin:10px 0 0}
10043
+ .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}
10044
+ a{color:var(--fg);text-underline-offset:2px}
10045
+ @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
10046
  `;
10047
+ var STEP_LABEL = {
10048
+ user_prompt: "prompt",
10049
+ assistant_text: "assistant",
10050
+ assistant_thinking: "thinking",
10051
+ tool_call: "tool call",
10052
+ tool_result: "tool result",
10053
+ error_recovery: "failure"
10054
+ };
9650
10055
  var SCRIPT = `
9651
10056
  (function(){
9652
10057
  var boxes = Array.prototype.slice.call(document.querySelectorAll('input[name="keep"]'));
@@ -9753,14 +10158,14 @@ function renderReviewPage(input) {
9753
10158
  const base = `steps[${index}]`;
9754
10159
  const parts = [];
9755
10160
  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>`
10161
+ `<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
10162
  );
9758
10163
  parts.push(`<pre>${marked(`${base}.content`, step.content)}</pre>`);
9759
10164
  const tool = step.tool;
9760
10165
  if (tool) {
9761
10166
  const inputSlots = slots.filter((slot) => slot.fieldPath.startsWith(`${base}.tool.input`));
9762
10167
  parts.push(
9763
- `<div class="sub">tool ${escapeHtml(tool.name)}${tool.isError ? " &middot; errored" : ""}</div>`
10168
+ `<div class="sub${tool.isError ? " err" : ""}">tool ${escapeHtml(tool.name)}${tool.isError ? " &middot; errored" : ""}</div>`
9764
10169
  );
9765
10170
  if (inputSlots.length > 0) {
9766
10171
  parts.push(
@@ -9780,7 +10185,7 @@ function renderReviewPage(input) {
9780
10185
  `<div class="sub">diff ${marked(`${base}.diff.filePath`, diff.filePath)}</div><pre>${marked(`${base}.diff.patch`, diff.patch)}</pre>`
9781
10186
  );
9782
10187
  }
9783
- return `<section class="step" id="${escapeHtml(base)}">${parts.join("")}</section>`;
10188
+ return `<section class="step ${escapeHtml(step.type)}" id="${escapeHtml(base)}">${parts.join("")}</section>`;
9784
10189
  }).join("");
9785
10190
  const groups = groupMatches(matches);
9786
10191
  const rowsHtml = groups.length === 0 ? `<p class="note">No matches. Nothing will be rewritten.</p>` : groups.map((group) => {
@@ -9794,15 +10199,16 @@ function renderReviewPage(input) {
9794
10199
  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
10200
  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
10201
  const body = `
9797
- <header>
10202
+ <header><div class="hin">
10203
+ <div class="bar"><span class="wordmark">flatbread</span><span class="crumb">review before you share</span></div>
9798
10204
  <h1>${marked("title", trace.title)}</h1>
9799
10205
  <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
10206
+ <b>${escapeHtml(trace.model)}</b> &middot; ${plural2(trace.steps.length, "step")} &middot; ${plural2(toolCalls, "tool call")} &middot;
10207
+ ${trace.stats.filesChanged === null ? `${formatCount(null)} files` : plural2(trace.stats.filesChanged, "file")} changed &middot; ${plural2(trace.stats.errorCount, "error")} &middot;
10208
+ ${Math.round(trace.stats.durationSeconds)}s &middot; <b>${matches.length}</b> redaction match${matches.length === 1 ? "" : "es"}
9803
10209
  </div>
9804
- <div class="meta">${escapeHtml(sessionPath)}</div>
9805
- </header>
10210
+ <div class="meta path">${escapeHtml(sessionPath)}</div>
10211
+ </div></header>
9806
10212
  <form id="review" method="post" action="/confirm">
9807
10213
  <div class="wrap">
9808
10214
  <main>${metaHtml}${stepsHtml}</main>
@@ -9835,13 +10241,14 @@ function renderReviewPage(input) {
9835
10241
  <script>var PUBLIC_WARNING=${JSON.stringify(publicWarning)};${SCRIPT}</script>`;
9836
10242
  return page(`Flatbread review - ${trace.title}`, body);
9837
10243
  }
9838
- function renderDonePage(message, outputPath, publishTo) {
10244
+ function renderDonePage(message, outputPath, publishTo, keepsRun = false) {
9839
10245
  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
10246
  return page(
9841
10247
  "Flatbread - share draft written",
9842
10248
  `<div class="done"><h1>${escapeHtml(message)}</h1>
9843
10249
  <code>${escapeHtml(outputPath)}</code>
9844
10250
  ${network}
10251
+ ${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
10252
  <p class="meta"><strong>Go back to your terminal now.</strong> The manage token for this
9846
10253
  run is printed there, once, at the very bottom. It is the only way to open the run
9847
10254
  while it is private, and only its hash is stored, so nothing can print it again.</p>
@@ -9882,12 +10289,11 @@ function parseDecisionBody(body) {
9882
10289
  };
9883
10290
  }
9884
10291
  async function listenOnFirstFreePort(server, ports) {
9885
- for (const port of ports) {
10292
+ for (const port of [...ports, 0]) {
9886
10293
  const ok = await new Promise((resolve) => {
9887
- const onError = (error) => {
10294
+ const onError = () => {
9888
10295
  server.removeListener("listening", onListening);
9889
- if (error.code === "EADDRINUSE" || error.code === "EACCES") resolve(false);
9890
- else resolve(false);
10296
+ resolve(false);
9891
10297
  };
9892
10298
  const onListening = () => {
9893
10299
  server.removeListener("error", onError);
@@ -9966,7 +10372,7 @@ async function startReviewServer(options) {
9966
10372
  ...changed ? { title } : {}
9967
10373
  };
9968
10374
  const message = options.onConfirm ? await options.onConfirm(confirmed) : "";
9969
- send(200, renderDonePage("Share draft written", message, options.publishTo));
10375
+ send(200, renderDonePage("Share draft written", message, options.publishTo, options.keepsRun === true));
9970
10376
  finish(confirmed);
9971
10377
  } catch (error) {
9972
10378
  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 +10526,7 @@ function reportPersist(outcome, options, log) {
10120
10526
  log(` slug ${outcome.slug}`);
10121
10527
  log(` visibility ${outcome.record.visibility}`);
10122
10528
  if (outcome.recordPath) log(` record ${outcome.recordPath}`);
10529
+ log(` again flatbread pull ${outcome.slug} (the next share replaces ${path15.basename(options.outputPath)}, not this record)`);
10123
10530
  log("");
10124
10531
  log(`open ${outcome.publish?.ok ? outcome.publish.url : `${base}/r/${outcome.slug}`}`);
10125
10532
  log("");
@@ -10228,6 +10635,7 @@ async function runShare(options) {
10228
10635
  sessionPath: target,
10229
10636
  draftPath: options.outputPath,
10230
10637
  titleDraft: previewTitle,
10638
+ keepsRun: options.persist !== false && (options.storeDir ?? resolveStoreDir()) !== null,
10231
10639
  ...isPublisher(reviewPublisher) ? { publishTo: reviewPublisher.description } : {},
10232
10640
  ...options.ports ? { ports: options.ports } : {},
10233
10641
  ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
@@ -10241,9 +10649,9 @@ async function runShare(options) {
10241
10649
  });
10242
10650
  if (!session) {
10243
10651
  console.error(
10244
- `could not bind a review port (tried ${(options.ports ?? [4321, 4322, 4323, 4324, 4325]).join(", ")})`
10652
+ `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
10653
  );
10246
- console.error("Close whatever is using them, or run with --no-review.");
10654
+ console.error("Loopback itself refused, so this machine needs a look; --no-review redacts everything without the page.");
10247
10655
  return { exitCode: 1, status: "port-unavailable" };
10248
10656
  }
10249
10657
  log(` review at ${session.url}`);
@@ -10661,7 +11069,7 @@ function logCapture(dir, line, now = (/* @__PURE__ */ new Date()).toISOString())
10661
11069
 
10662
11070
  // src/mcp.ts
10663
11071
  var MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
10664
- var MCP_SERVER_VERSION = "0.2.0";
11072
+ var MCP_SERVER_VERSION = "0.3.1";
10665
11073
  function defaultMcpDeps(storeDir2, bucketDir2) {
10666
11074
  return {
10667
11075
  storeDir: storeDir2,
@@ -10751,6 +11159,10 @@ async function searchEverywhere(query, deps, options = {}) {
10751
11159
  if (local.unreadable > 0) out.push(`(${local.unreadable} local record file${local.unreadable === 1 ? "" : "s"} could not be read)`);
10752
11160
  }
10753
11161
  const seen = new Set(localHits.map((hit) => hit.slug));
11162
+ for (const record of local.records) {
11163
+ const { harness, sessionId } = record.trace.source;
11164
+ if (sessionId.length > 0 && localHits.some((hit) => hit.slug === record.slug)) seen.add(bucketSlug(harness, sessionId));
11165
+ }
10754
11166
  if (deps.bucketDir !== null) {
10755
11167
  const bucket = readBucket(deps.bucketDir);
10756
11168
  const captured = bucket.rows.filter((row) => !seen.has(row.summary.slug) && matchesQuery(row.summary, query));
@@ -10784,18 +11196,24 @@ async function searchTool(args, deps) {
10784
11196
  const { lines: lines2 } = await searchEverywhere(query, deps);
10785
11197
  return text([...lines2, "", "Pass a run ID to flatbread_get_capsule for what that run learned."].join("\n"));
10786
11198
  }
11199
+ async function readCapsule(ref, deps) {
11200
+ if (ref.site === void 0) {
11201
+ const stored = localRecords(deps).records.find((record) => record.slug === ref.slug);
11202
+ if (stored !== void 0) return { ok: true, capsule: buildCapsule(stored.trace), from: "store" };
11203
+ const captured = deps.bucketDir !== null ? readBucketRecord(deps.bucketDir, ref.slug) : null;
11204
+ if (captured !== null) return { ok: true, capsule: buildCapsule(captured.trace), from: "bucket" };
11205
+ }
11206
+ const remote = await deps.capsule(ref);
11207
+ return remote.ok ? { ok: true, capsule: remote.capsule, from: "site" } : remote;
11208
+ }
10787
11209
  async function capsuleTool(args, deps) {
10788
11210
  const raw = args["slug"];
10789
11211
  if (typeof raw !== "string") return text("slug must be a run ID or a /r/<id> link", true);
10790
11212
  const ref = parseRunRef(raw);
10791
11213
  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);
11214
+ const read2 = await readCapsule(ref, deps);
11215
+ if (read2.ok) return text(read2.capsule);
11216
+ return text(describeReadFailure(read2, `capsule ${ref.slug}`), true);
10799
11217
  }
10800
11218
  async function handleMcpMessage(message, deps) {
10801
11219
  if (typeof message !== "object" || message === null || Array.isArray(message)) {
@@ -10844,7 +11262,7 @@ async function handleMcpMessage(message, deps) {
10844
11262
  }
10845
11263
  async function runMcpServer(deps, io) {
10846
11264
  const input = io?.input ?? process.stdin;
10847
- const write = io?.write ?? ((line) => process.stdout.write(line));
11265
+ const write2 = io?.write ?? ((line) => process.stdout.write(line));
10848
11266
  const log = io?.log ?? ((line) => process.stderr.write(`${line}
10849
11267
  `));
10850
11268
  log(`flatbread mcp ${MCP_SERVER_VERSION} on stdio${deps.storeDir === null ? " (no local store)" : ` (local store ${deps.storeDir})`}`);
@@ -10857,12 +11275,12 @@ async function runMcpServer(deps, io) {
10857
11275
  try {
10858
11276
  message = JSON.parse(line);
10859
11277
  } catch {
10860
- write(`${JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } })}
11278
+ write2(`${JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } })}
10861
11279
  `);
10862
11280
  return;
10863
11281
  }
10864
11282
  const reply = await handleMcpMessage(message, deps);
10865
- if (reply !== null) write(`${JSON.stringify(reply)}
11283
+ if (reply !== null) write2(`${JSON.stringify(reply)}
10866
11284
  `);
10867
11285
  });
10868
11286
  }
@@ -10870,9 +11288,134 @@ async function runMcpServer(deps, io) {
10870
11288
  return 0;
10871
11289
  }
10872
11290
 
10873
- // src/git-hook.ts
11291
+ // src/claude-mcp.ts
10874
11292
  import fs19 from "node:fs";
11293
+ import os7 from "node:os";
10875
11294
  import path18 from "node:path";
11295
+ var MCP_SERVER_NAME = "flatbread";
11296
+ function claudeUserConfigPath(env = process.env) {
11297
+ const dir = env["CLAUDE_CONFIG_DIR"]?.trim();
11298
+ return path18.join(dir ? dir : os7.homedir(), ".claude.json");
11299
+ }
11300
+ function mcpEntryFor(execPath, entryPath) {
11301
+ return { type: "stdio", command: execPath, args: [entryPath, "mcp"] };
11302
+ }
11303
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
11304
+ function runsFlatbreadMcp(entry) {
11305
+ if (!isObject(entry) || !Array.isArray(entry["args"])) return false;
11306
+ const words = [entry["command"], ...entry["args"]].filter((word) => typeof word === "string");
11307
+ return words[words.length - 1] === "mcp" && words.some((word) => /flatbread/i.test(word));
11308
+ }
11309
+ var BOM = "\uFEFF";
11310
+ function readConfig(file) {
11311
+ let raw;
11312
+ try {
11313
+ raw = fs19.readFileSync(file, "utf8");
11314
+ } catch (error) {
11315
+ if (error.code === "ENOENT") return { root: {}, raw: null };
11316
+ return { problem: error.message };
11317
+ }
11318
+ const text2 = raw.startsWith(BOM) ? raw.slice(1) : raw;
11319
+ if (text2.trim() === "") return { root: {}, raw };
11320
+ let parsed;
11321
+ try {
11322
+ parsed = JSON.parse(text2);
11323
+ } catch (error) {
11324
+ return { problem: `not JSON (${error.message})` };
11325
+ }
11326
+ if (!isObject(parsed)) return { problem: "not a JSON object" };
11327
+ if (parsed["mcpServers"] !== void 0 && !isObject(parsed["mcpServers"])) return { problem: "its mcpServers is not an object" };
11328
+ return { root: parsed, raw };
11329
+ }
11330
+ function serializeLike(raw, root) {
11331
+ let body = JSON.stringify(root, null, 2);
11332
+ if (raw === null || raw.trimEnd() !== raw || raw.trim() === "") body += "\n";
11333
+ if (raw !== null && raw.includes("\r\n")) body = body.replace(/\n/g, "\r\n");
11334
+ if (raw !== null && raw.startsWith(BOM)) body = BOM + body;
11335
+ return body;
11336
+ }
11337
+ function write(file, read2, next) {
11338
+ if (read2.raw !== null) {
11339
+ try {
11340
+ fs19.accessSync(file, fs19.constants.W_OK);
11341
+ } catch {
11342
+ return `${file} is read-only, so it was left as it was`;
11343
+ }
11344
+ if (fs19.readFileSync(file, "utf8") !== read2.raw) return `${file} changed while setup ran (Claude Code writes it); run setup again`;
11345
+ const backup = `${file}.flatbread.bak`;
11346
+ if (!fs19.existsSync(backup)) fs19.copyFileSync(file, backup);
11347
+ }
11348
+ const mode = read2.raw !== null ? fs19.statSync(file).mode & 511 : 384;
11349
+ fs19.mkdirSync(path18.dirname(file), { recursive: true });
11350
+ const temp = `${file}.flatbread-${process.pid}.tmp`;
11351
+ fs19.writeFileSync(temp, next, { encoding: "utf8", mode });
11352
+ fs19.renameSync(temp, file);
11353
+ return null;
11354
+ }
11355
+ function registerClaudeMcp(options) {
11356
+ const { file, log } = options;
11357
+ const read2 = readConfig(file);
11358
+ if ("problem" in read2) {
11359
+ log(` mcp refused: ${file}: ${read2.problem}`);
11360
+ log(" Claude Code's own state lives there, so a file we cannot read is not one we rewrite.");
11361
+ if (options.uninstall !== true) log(` by hand: claude mcp add --scope user ${MCP_SERVER_NAME} -- "${options.execPath}" "${options.entryPath}" mcp`);
11362
+ return 1;
11363
+ }
11364
+ const servers = isObject(read2.root["mcpServers"]) ? read2.root["mcpServers"] : void 0;
11365
+ const current = servers?.[MCP_SERVER_NAME];
11366
+ if (options.uninstall === true) {
11367
+ if (current === void 0) {
11368
+ log(` mcp no ${MCP_SERVER_NAME} server in ${file}`);
11369
+ return 0;
11370
+ }
11371
+ if (!runsFlatbreadMcp(current)) {
11372
+ log(` mcp ${file}: the ${MCP_SERVER_NAME} server there does not run flatbread, left alone`);
11373
+ return 0;
11374
+ }
11375
+ if (options.dryRun === true) {
11376
+ log(` mcp ${file}: would remove the ${MCP_SERVER_NAME} server`);
11377
+ return 0;
11378
+ }
11379
+ const rest = { ...servers };
11380
+ delete rest[MCP_SERVER_NAME];
11381
+ const problem2 = write(file, read2, serializeLike(read2.raw, { ...read2.root, mcpServers: rest }));
11382
+ if (problem2 !== null) {
11383
+ log(` mcp refused: ${problem2}`);
11384
+ return 1;
11385
+ }
11386
+ log(` mcp ${file}: removed the ${MCP_SERVER_NAME} server`);
11387
+ const backup = `${file}.flatbread.bak`;
11388
+ if (fs19.existsSync(backup)) log(` ${backup} is the copy setup took before its first write; delete it when you like`);
11389
+ return 0;
11390
+ }
11391
+ const entry = mcpEntryFor(options.execPath, options.entryPath);
11392
+ if (current !== void 0 && !runsFlatbreadMcp(current)) {
11393
+ log(` mcp refused: ${file} already has a server named ${MCP_SERVER_NAME} that does not run flatbread, left alone`);
11394
+ return 1;
11395
+ }
11396
+ if (JSON.stringify(current) === JSON.stringify(entry)) {
11397
+ log(` mcp ${file}: registered for every project, unchanged`);
11398
+ return 0;
11399
+ }
11400
+ const verb = current === void 0 ? "register" : "update";
11401
+ if (options.dryRun === true) {
11402
+ log(` mcp ${file}: would ${verb} "${MCP_SERVER_NAME}": ${JSON.stringify(entry)}`);
11403
+ return 0;
11404
+ }
11405
+ const root = { ...read2.root, mcpServers: { ...servers ?? {}, [MCP_SERVER_NAME]: entry } };
11406
+ const problem = write(file, read2, serializeLike(read2.raw, root));
11407
+ if (problem !== null) {
11408
+ log(` mcp refused: ${problem}`);
11409
+ return 1;
11410
+ }
11411
+ log(` mcp ${file}: ${verb === "register" ? "registered" : "updated"} for every project (user scope)`);
11412
+ log(" restart Claude Code (or reload the VS Code window) so it starts the server");
11413
+ return 0;
11414
+ }
11415
+
11416
+ // src/git-hook.ts
11417
+ import fs20 from "node:fs";
11418
+ import path19 from "node:path";
10876
11419
  var POST_COMMIT_MARKER = "# flatbread post-commit hook - remove with: flatbread setup --uninstall";
10877
11420
  var CHAINED_HOOK = "post-commit.pre-flatbread";
10878
11421
  var sq = (value) => `'${value.replace(/\\/g, "/").replace(/'/g, "'\\''")}'`;
@@ -10916,33 +11459,33 @@ function hooksDirFor(cwd, options = {}) {
10916
11459
  }
10917
11460
  const common = git(top, ["rev-parse", "--git-common-dir"], options)?.trim();
10918
11461
  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 } : {} };
11462
+ return { ok: true, repo: path19.resolve(top), dir: path19.resolve(top, common, "hooks"), ...machineHooksPath !== void 0 ? { machineHooksPath } : {} };
10920
11463
  }
10921
11464
  var read = (file) => {
10922
11465
  try {
10923
- return fs19.readFileSync(file, "utf8");
11466
+ return fs20.readFileSync(file, "utf8");
10924
11467
  } catch {
10925
11468
  return null;
10926
11469
  }
10927
11470
  };
10928
11471
  function installGitHook(dir, script, options = {}) {
10929
- const hook = path18.join(dir, "post-commit");
10930
- const chained = path18.join(dir, CHAINED_HOOK);
11472
+ const hook = path19.join(dir, "post-commit");
11473
+ const chained = path19.join(dir, CHAINED_HOOK);
10931
11474
  const current = read(hook);
10932
- const write = (lines2) => {
11475
+ const write2 = (lines2) => {
10933
11476
  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);
11477
+ fs20.mkdirSync(dir, { recursive: true });
11478
+ if (current !== null && !isOurs(current)) fs20.renameSync(hook, chained);
11479
+ fs20.writeFileSync(hook, script, { encoding: "utf8", mode: 493 });
11480
+ fs20.chmodSync(hook, 493);
10938
11481
  return { code: 0, lines: lines2 };
10939
11482
  };
10940
- if (current === null) return write([`post-commit ${hook} (new)`]);
11483
+ if (current === null) return write2([`post-commit ${hook} (new)`]);
10941
11484
  if (isOurs(current)) {
10942
11485
  if (current === script) return { code: 0, lines: [`post-commit ${hook}`, "already installed, unchanged"] };
10943
- return write([`post-commit ${hook}`, "updated the flatbread hook"]);
11486
+ return write2([`post-commit ${hook}`, "updated the flatbread hook"]);
10944
11487
  }
10945
- if (fs19.existsSync(chained)) {
11488
+ if (fs20.existsSync(chained)) {
10946
11489
  return {
10947
11490
  code: 1,
10948
11491
  lines: [
@@ -10952,16 +11495,16 @@ function installGitHook(dir, script, options = {}) {
10952
11495
  ]
10953
11496
  };
10954
11497
  }
10955
- return write([
11498
+ return write2([
10956
11499
  `post-commit ${hook}`,
10957
11500
  `your post-commit was kept as ${CHAINED_HOOK} and runs first, unchanged, with its exit status`
10958
11501
  ]);
10959
11502
  }
10960
11503
  function uninstallGitHook(dir, options = {}) {
10961
- const hook = path18.join(dir, "post-commit");
10962
- const chained = path18.join(dir, CHAINED_HOOK);
11504
+ const hook = path19.join(dir, "post-commit");
11505
+ const chained = path19.join(dir, CHAINED_HOOK);
10963
11506
  const current = read(hook);
10964
- const hasChained = fs19.existsSync(chained);
11507
+ const hasChained = fs20.existsSync(chained);
10965
11508
  const done = (lines2, act) => {
10966
11509
  if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
10967
11510
  act();
@@ -10979,17 +11522,17 @@ function uninstallGitHook(dir, options = {}) {
10979
11522
  if (current === null && !hasChained) return { code: 0, lines: [`no flatbread post-commit in ${dir}`] };
10980
11523
  if (hasChained) {
10981
11524
  return done([`post-commit ${hook}`, `restored your post-commit from ${CHAINED_HOOK}`], () => {
10982
- if (current !== null) fs19.rmSync(hook);
10983
- fs19.renameSync(chained, hook);
11525
+ if (current !== null) fs20.rmSync(hook);
11526
+ fs20.renameSync(chained, hook);
10984
11527
  });
10985
11528
  }
10986
- return done([`post-commit ${hook}`, "removed the flatbread hook"], () => fs19.rmSync(hook));
11529
+ return done([`post-commit ${hook}`, "removed the flatbread hook"], () => fs20.rmSync(hook));
10987
11530
  }
10988
11531
 
10989
11532
  // src/hook.ts
10990
11533
  import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
10991
- import fs20 from "node:fs";
10992
- import path19 from "node:path";
11534
+ import fs21 from "node:fs";
11535
+ import path20 from "node:path";
10993
11536
  var HOOK_EVENTS = ["Stop", "SessionEnd"];
10994
11537
  var OFFER_AFTER_STOPS = 2;
10995
11538
  var MAX_TRACKED_SESSIONS = 50;
@@ -10999,14 +11542,14 @@ function emptyHookState() {
10999
11542
  return { version: HOOK_STATE_VERSION, sessions: {} };
11000
11543
  }
11001
11544
  function hookStatePath(env = process.env) {
11002
- return path19.join(flatbreadHome(env), "hook-state.json");
11545
+ return path20.join(flatbreadHome(env), "hook-state.json");
11003
11546
  }
11004
11547
  function readHookState(env = process.env) {
11005
11548
  const file = hookStatePath(env);
11006
- if (!fs20.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
11549
+ if (!fs21.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
11007
11550
  let raw;
11008
11551
  try {
11009
- raw = fs20.readFileSync(file, "utf8");
11552
+ raw = fs21.readFileSync(file, "utf8");
11010
11553
  } catch (error) {
11011
11554
  return { ok: false, reason: "unreadable", detail: error.message };
11012
11555
  }
@@ -11049,9 +11592,9 @@ function pruneHookState(state, keep = MAX_TRACKED_SESSIONS) {
11049
11592
  }
11050
11593
  function writeHookState(state, env = process.env) {
11051
11594
  const dir = flatbreadHome(env);
11052
- fs20.mkdirSync(dir, { recursive: true, mode: 448 });
11595
+ fs21.mkdirSync(dir, { recursive: true, mode: 448 });
11053
11596
  const file = hookStatePath(env);
11054
- fs20.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
11597
+ fs21.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
11055
11598
  encoding: "utf8",
11056
11599
  mode: 384
11057
11600
  });
@@ -11197,7 +11740,7 @@ async function runHookNotify(io) {
11197
11740
  const stdout = io.stdout ?? ((line) => process.stdout.write(line + "\n"));
11198
11741
  const stderr = io.stderr ?? ((line) => process.stderr.write(line + "\n"));
11199
11742
  const now = io.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
11200
- const exists = io.exists ?? ((filePath) => fs20.existsSync(filePath));
11743
+ const exists = io.exists ?? ((filePath) => fs21.existsSync(filePath));
11201
11744
  const execPath = io.execPath ?? process.execPath;
11202
11745
  const entryPath = io.entryPath ?? process.argv[1] ?? "";
11203
11746
  try {
@@ -11354,10 +11897,10 @@ function removeHookConfig(settings) {
11354
11897
  return { settings: root, removed };
11355
11898
  }
11356
11899
  function readSettingsFile(file) {
11357
- if (!fs20.existsSync(file)) return { present: false, file };
11900
+ if (!fs21.existsSync(file)) return { present: false, file };
11358
11901
  let raw;
11359
11902
  try {
11360
- raw = fs20.readFileSync(file, "utf8");
11903
+ raw = fs21.readFileSync(file, "utf8");
11361
11904
  } catch (error) {
11362
11905
  return { present: true, file, unreadable: error.message };
11363
11906
  }
@@ -11369,10 +11912,10 @@ function readSettingsFile(file) {
11369
11912
  }
11370
11913
  }
11371
11914
  function writeSettingsFile(file, settings) {
11372
- fs20.mkdirSync(path19.dirname(file), { recursive: true });
11915
+ fs21.mkdirSync(path20.dirname(file), { recursive: true });
11373
11916
  const temp = `${file}.flatbread-${process.pid}.tmp`;
11374
- fs20.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
11375
- fs20.renameSync(temp, file);
11917
+ fs21.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
11918
+ fs21.renameSync(temp, file);
11376
11919
  }
11377
11920
  function handlersFor(execPath, entryPath, capture = false) {
11378
11921
  return {
@@ -11530,8 +12073,8 @@ function runHookUninstall(options = {}) {
11530
12073
  function settingsCandidates(env, cwd) {
11531
12074
  return [
11532
12075
  { 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") }
12076
+ { label: "project", file: path20.join(cwd, ".claude", "settings.json") },
12077
+ { label: "local", file: path20.join(cwd, ".claude", "settings.local.json") }
11535
12078
  ];
11536
12079
  }
11537
12080
  function installedEventsIn(parsed, exists) {
@@ -11561,7 +12104,7 @@ function runHookStatus(options = {}) {
11561
12104
  const env = options.env ?? process.env;
11562
12105
  const log = options.log ?? ((line) => console.log(line));
11563
12106
  const cwd = options.cwd ?? process.cwd();
11564
- const exists = options.exists ?? ((filePath) => fs20.existsSync(filePath));
12107
+ const exists = options.exists ?? ((filePath) => fs21.existsSync(filePath));
11565
12108
  const candidates = options.settingsPath === void 0 ? settingsCandidates(env, cwd) : [{ label: "settings", file: options.settingsPath }];
11566
12109
  log("");
11567
12110
  log("flatbread hook");
@@ -11711,6 +12254,22 @@ function runSetup(options = {}) {
11711
12254
  ...options.spawn !== void 0 ? { spawn: options.spawn } : {},
11712
12255
  ...settings
11713
12256
  });
12257
+ let mcp = 0;
12258
+ if (options.noMcp === true) {
12259
+ log(" --no-mcp: Claude Code's MCP servers were not touched");
12260
+ } else if (claude === 0 || options.uninstall === true) {
12261
+ mcp = registerClaudeMcp({
12262
+ file: options.claudeConfigPath ?? claudeUserConfigPath(env),
12263
+ execPath,
12264
+ entryPath,
12265
+ uninstall: options.uninstall === true,
12266
+ dryRun,
12267
+ log
12268
+ });
12269
+ } else {
12270
+ log(" mcp not registered, because the hook step above was refused");
12271
+ }
12272
+ log("");
11714
12273
  let git2 = 0;
11715
12274
  log("git");
11716
12275
  if (options.noGit === true) {
@@ -11746,11 +12305,11 @@ function runSetup(options = {}) {
11746
12305
  log(" share one with: flatbread share --file <its session>, which opens the review");
11747
12306
  }
11748
12307
  log("");
11749
- return claude !== 0 || git2 !== 0 ? 1 : 0;
12308
+ return claude !== 0 || mcp !== 0 || git2 !== 0 ? 1 : 0;
11750
12309
  }
11751
12310
 
11752
12311
  // src/login-server.ts
11753
- import crypto2 from "node:crypto";
12312
+ import crypto3 from "node:crypto";
11754
12313
  import http2 from "node:http";
11755
12314
  function callbackOrigin(port) {
11756
12315
  return `http://127.0.0.1:${port}`;
@@ -11825,7 +12384,7 @@ function parseDelivery(raw, expectedState, nowSeconds) {
11825
12384
  }
11826
12385
  const given = Buffer.from(state, "utf8");
11827
12386
  const wanted = Buffer.from(expectedState, "utf8");
11828
- if (given.length !== wanted.length || !crypto2.timingSafeEqual(given, wanted)) {
12387
+ if (given.length !== wanted.length || !crypto3.timingSafeEqual(given, wanted)) {
11829
12388
  return { ok: false, problem: "the callback state did not match" };
11830
12389
  }
11831
12390
  const sessionRaw = body["session"];
@@ -11873,7 +12432,7 @@ async function readBody2(req, limitBytes = 64 * 1024) {
11873
12432
  async function startLoginServer(options = {}) {
11874
12433
  const ports = options.ports ?? DEFAULT_LOGIN_PORTS;
11875
12434
  const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
11876
- const state = options.state ?? crypto2.randomBytes(32).toString("hex");
12435
+ const state = options.state ?? crypto3.randomBytes(32).toString("hex");
11877
12436
  let settle;
11878
12437
  const delivery = new Promise((resolve) => {
11879
12438
  settle = resolve;
@@ -11959,13 +12518,13 @@ async function bindFirst(server, ports) {
11959
12518
  }
11960
12519
 
11961
12520
  // 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");
12521
+ var PACKAGE_ROOT = path21.resolve(path21.dirname(fileURLToPath2(import.meta.url)), "..");
12522
+ var SOURCE_CHECKOUT = fs22.existsSync(path21.join(PACKAGE_ROOT, "src", "index.ts"));
12523
+ var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ? path21.join(PACKAGE_ROOT, ".tmp") : path21.join(os8.homedir(), ".flatbread"));
12524
+ var OUTPUT_PATH = path21.join(TMP_DIR, "last-parse.json");
12525
+ var SHARE_OUTPUT_PATH = path21.join(TMP_DIR, "last-share.json");
11967
12526
  function storeDir() {
11968
- return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path20.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
12527
+ return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path21.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
11969
12528
  }
11970
12529
  var USAGE = `flatbread - registry of AI agent runs
11971
12530
 
@@ -11975,14 +12534,16 @@ Usage:
11975
12534
  flatbread share Review + sanitize the newest session
11976
12535
  flatbread share --file <path> Review + sanitize a specific session
11977
12536
  flatbread view Write the brief and capsule, print the viewer URL
11978
- flatbread pull <id|url> Print a run's Context Capsule
12537
+ flatbread pull <id|url> Print a run's Context Capsule: a run this machine shared or
12538
+ captured is read here, anything else from the site
11979
12539
  flatbread pull <id|url> --out <file> Write it to a file instead
11980
12540
  flatbread mcp MCP server on stdio: flatbread_search_traces and
11981
12541
  flatbread_get_capsule, for your coding agent
11982
12542
  flatbread search <words> Find runs: this machine's shared and captured ones, then
11983
12543
  the site (the same answer the MCP server gives)
11984
12544
  flatbread setup Auto-capture: the Claude Code hook, plus a chained git
11985
- post-commit in this repository. Captures stay local
12545
+ post-commit in this repository, and the MCP server for
12546
+ every Claude Code project. Captures stay local
11986
12547
  flatbread setup --uninstall Remove exactly what setup added
11987
12548
  flatbread capture --file <path> Capture one session into the local bucket (setup's hooks
11988
12549
  run this; redacted, private, never uploaded)
@@ -12024,9 +12585,10 @@ Options for search:
12024
12585
 
12025
12586
  Options for setup:
12026
12587
  --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
12588
+ --uninstall Remove the handlers, the MCP server and the post-commit;
12589
+ restore a post-commit hook setup had chained
12029
12590
  --no-git Leave the repository's hooks alone
12591
+ --no-mcp Leave Claude Code's MCP servers alone
12030
12592
  --settings <path> --no-probe As for hook install
12031
12593
 
12032
12594
  Options for capture:
@@ -12049,14 +12611,16 @@ Options for adapters:
12049
12611
  Notes:
12050
12612
  parse and share record the git branch, HEAD commit and uncommitted file names when the
12051
12613
  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}
12614
+ parse --save writes ${path21.relative(process.cwd(), OUTPUT_PATH) || OUTPUT_PATH}
12615
+ Share drafts are written to ${path21.relative(process.cwd(), SHARE_OUTPUT_PATH) || SHARE_OUTPUT_PATH}
12616
+ (the latest only; every shared run is also kept under its ID, for search and pull)
12054
12617
  Every parsed run defaults to visibility "private".
12055
12618
  Nothing is uploaded unless you pass --publish, or set FLATBREAD_API_URL and
12056
12619
  FLATBREAD_API_TOKEN yourself. pull and mcp only read: public runs, your own when you
12057
12620
  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
12621
+ 127.0.0.1 only, on 4321-4325 or, when all five are busy, a port the OS picks.
12622
+ setup registers the MCP server in ~/.claude.json, for every project. By hand instead:
12623
+ claude mcp add --scope user flatbread -- flatbread mcp
12060
12624
  The viewer is a separate dev server: npm run dev -w @flatbread/web
12061
12625
  Captured runs live in ~/.flatbread/bucket (FLATBREAD_HOOK_DISABLED=1 stops every capture).
12062
12626
  A capture is not a share: to publish one, run share --file on its session.
@@ -12085,10 +12649,10 @@ function parseArgv(argv) {
12085
12649
  } else if (arg === "--out") {
12086
12650
  const value = argv[i + 1];
12087
12651
  if (value === void 0 || value.startsWith("--")) throw new Error("--out needs a file path");
12088
- args.out = path20.resolve(value);
12652
+ args.out = path21.resolve(value);
12089
12653
  i += 1;
12090
12654
  } else if (arg !== void 0 && arg.startsWith("--out=")) {
12091
- args.out = path20.resolve(arg.slice("--out=".length));
12655
+ args.out = path21.resolve(arg.slice("--out=".length));
12092
12656
  } else if (arg === "--harness") {
12093
12657
  const value = argv[i + 1];
12094
12658
  if (value === void 0 || value.startsWith("--")) {
@@ -12209,12 +12773,12 @@ async function runParse(args) {
12209
12773
  target = found[0];
12210
12774
  console.log(`latest ${harness} session: ${target}`);
12211
12775
  } else if (args.file) {
12212
- target = path20.resolve(args.file);
12213
- if (!fs21.existsSync(target)) {
12776
+ target = path21.resolve(args.file);
12777
+ if (!fs22.existsSync(target)) {
12214
12778
  console.error(`file not found: ${target}`);
12215
12779
  return 1;
12216
12780
  }
12217
- if (!fs21.statSync(target).isFile()) {
12781
+ if (!fs22.statSync(target).isFile()) {
12218
12782
  console.error(`not a file: ${target}`);
12219
12783
  return 1;
12220
12784
  }
@@ -12245,8 +12809,8 @@ async function runParse(args) {
12245
12809
  const trace = withLineage(result.trace, readGitLineage(result.trace.source.cwd));
12246
12810
  FlatbreadTraceSchema.parse(trace);
12247
12811
  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");
12812
+ fs22.mkdirSync(path21.dirname(args.out), { recursive: true });
12813
+ fs22.writeFileSync(args.out, JSON.stringify(trace, null, 2) + "\n", "utf8");
12250
12814
  }
12251
12815
  report({ ...result, trace }, args.out);
12252
12816
  return 0;
@@ -12310,8 +12874,8 @@ async function runAdapters(args) {
12310
12874
  }
12311
12875
  let input = null;
12312
12876
  if (args.file !== void 0) {
12313
- const resolved = path20.resolve(args.file);
12314
- if (!fs21.existsSync(resolved)) {
12877
+ const resolved = path21.resolve(args.file);
12878
+ if (!fs22.existsSync(resolved)) {
12315
12879
  console.error(`file not found: ${resolved}`);
12316
12880
  return 1;
12317
12881
  }
@@ -12637,7 +13201,7 @@ async function prompt(label) {
12637
13201
  });
12638
13202
  }
12639
13203
  var HOOK_SUBCOMMANDS = ["install", "status", "uninstall", "notify"];
12640
- var CLI_ENTRY = fileURLToPath(import.meta.url);
13204
+ var CLI_ENTRY = fileURLToPath2(import.meta.url);
12641
13205
  function parseHookArgv(argv) {
12642
13206
  const [sub, ...rest] = argv;
12643
13207
  if (sub === void 0) {
@@ -12730,13 +13294,13 @@ function parsePullArgv(argv) {
12730
13294
  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
13295
  return { ref, ...out !== void 0 ? { out } : {} };
12732
13296
  }
12733
- async function runPull(args) {
13297
+ async function runPull(args, deps = { ...defaultMcpDeps(storeDir(), bucketDir()), capsule: (ref) => fetchCapsule(ref) }) {
12734
13298
  const ref = parseRunRef(args.ref);
12735
13299
  if (ref === null) {
12736
13300
  console.error(`"${args.ref}" is not a run ID or a /r/<id> link`);
12737
13301
  return 1;
12738
13302
  }
12739
- const result = await fetchCapsule(ref);
13303
+ const result = await readCapsule(ref, deps);
12740
13304
  if (!result.ok) {
12741
13305
  console.error(describeReadFailure(result, `could not pull ${ref.slug}`));
12742
13306
  return 1;
@@ -12745,10 +13309,10 @@ async function runPull(args) {
12745
13309
  process.stdout.write(result.capsule);
12746
13310
  return 0;
12747
13311
  }
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}`);
13312
+ const target = path21.resolve(args.out);
13313
+ fs22.mkdirSync(path21.dirname(target), { recursive: true });
13314
+ fs22.writeFileSync(target, result.capsule, "utf8");
13315
+ console.error(`wrote ${target}${result.from === "site" ? "" : ` (from this machine's ${result.from === "store" ? "shared runs" : "captured runs"})`}`);
12752
13316
  return 0;
12753
13317
  }
12754
13318
  function parseSearchArgv(argv) {
@@ -12860,6 +13424,7 @@ function parseSetupArgv(argv) {
12860
13424
  dryRun: false,
12861
13425
  uninstall: false,
12862
13426
  noGit: false,
13427
+ noMcp: false,
12863
13428
  noProbe: false
12864
13429
  };
12865
13430
  for (let i = 0; i < argv.length; i += 1) {
@@ -12867,6 +13432,7 @@ function parseSetupArgv(argv) {
12867
13432
  if (arg === "--dry-run") args.dryRun = true;
12868
13433
  else if (arg === "--uninstall") args.uninstall = true;
12869
13434
  else if (arg === "--no-git") args.noGit = true;
13435
+ else if (arg === "--no-mcp") args.noMcp = true;
12870
13436
  else if (arg === "--no-probe") args.noProbe = true;
12871
13437
  else if (arg === "--settings") {
12872
13438
  const value = argv[i + 1];
@@ -12913,6 +13479,7 @@ async function main(argv) {
12913
13479
  dryRun: setup.dryRun,
12914
13480
  uninstall: setup.uninstall,
12915
13481
  noGit: setup.noGit,
13482
+ noMcp: setup.noMcp,
12916
13483
  noProbe: setup.noProbe,
12917
13484
  ...setup.settings !== void 0 ? { settingsPath: setup.settings } : {}
12918
13485
  });
@@ -12994,12 +13561,12 @@ var invokedDirectly = (() => {
12994
13561
  if (entry === void 0) return false;
12995
13562
  const real = (file) => {
12996
13563
  try {
12997
- return fs21.realpathSync(file);
13564
+ return fs22.realpathSync(file);
12998
13565
  } catch {
12999
- return path20.resolve(file);
13566
+ return path21.resolve(file);
13000
13567
  }
13001
13568
  };
13002
- return real(entry) === real(fileURLToPath(import.meta.url));
13569
+ return real(entry) === real(fileURLToPath2(import.meta.url));
13003
13570
  })();
13004
13571
  if (invokedDirectly) {
13005
13572
  main(process.argv.slice(2)).then((code) => {
@@ -13022,6 +13589,7 @@ export {
13022
13589
  runCapture,
13023
13590
  runLogin,
13024
13591
  runLogout,
13592
+ runPull,
13025
13593
  runSearch,
13026
13594
  runWhoami
13027
13595
  };