flatbread-cli 0.5.0 → 0.7.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,9 +7,9 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/index.ts
9
9
  import { spawn as spawn3 } from "node:child_process";
10
- import fs26 from "node:fs";
10
+ import fs27 from "node:fs";
11
11
  import os9 from "node:os";
12
- import path25 from "node:path";
12
+ import path26 from "node:path";
13
13
  import { fileURLToPath as fileURLToPath2 } from "node:url";
14
14
 
15
15
  // ../../node_modules/zod/v3/external.js
@@ -490,8 +490,8 @@ function getErrorMap() {
490
490
 
491
491
  // ../../node_modules/zod/v3/helpers/parseUtil.js
492
492
  var makeIssue = (params) => {
493
- const { data, path: path26, errorMaps, issueData } = params;
494
- const fullPath = [...path26, ...issueData.path || []];
493
+ const { data, path: path27, errorMaps, issueData } = params;
494
+ const fullPath = [...path27, ...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, path26, key) {
610
+ constructor(parent, value, path27, key) {
611
611
  this._cachedPath = [];
612
612
  this.parent = parent;
613
613
  this.data = value;
614
- this._path = path26;
614
+ this._path = path27;
615
615
  this._key = key;
616
616
  }
617
617
  get path() {
@@ -4327,7 +4327,7 @@ var HARNESS_INFO = {
4327
4327
  implemented: true,
4328
4328
  humanTest: "A13",
4329
4329
  toolResults: "measured",
4330
- note: "Read the way three open readers read it (deja-vu, entire, opentraces), from their fixtures. Not yet measured on a real rollout here. A failure is the exit line Codex prints above a tool's output; files changed are not counted (apply_patch names them inside the patch)."
4330
+ note: "Codex 0.163's format measured on two real rollouts: its script tool's calls are read from the item_completed items (commands with their exit codes, MCP calls, file changes). Older rollouts are read the way deja-vu, entire and opentraces read them: a failure is the exit line above a tool's output, and files changed are not counted."
4331
4331
  },
4332
4332
  cursor: {
4333
4333
  id: "cursor",
@@ -4390,7 +4390,10 @@ var MUTATING_TOOL_NAMES = /* @__PURE__ */ new Set([
4390
4390
  "edit",
4391
4391
  "multiedit",
4392
4392
  "notebookedit",
4393
- "update"
4393
+ "update",
4394
+ // Codex 0.163's FileChange item, one call per file with its `path` (decision 143). An older
4395
+ // rollout's apply_patch carries only the patch text, so it names no path and records no file.
4396
+ "apply_patch"
4394
4397
  ]);
4395
4398
  var FILE_PATH_KEYS = ["file_path", "notebook_path", "filePath", "path"];
4396
4399
  var TARGET_KEYS = [...FILE_PATH_KEYS, "command", "cmd", "pattern", "url", "query", "description", "prompt"];
@@ -4461,8 +4464,8 @@ function firstLine(text2) {
4461
4464
  function labelOf(step, cwd) {
4462
4465
  const tool = step.tool;
4463
4466
  if (tool === void 0) return step.type;
4464
- const path26 = firstString(tool.input, FILE_PATH_KEYS);
4465
- if (path26 !== void 0) return `${tool.name} ${repoRelative(path26, cwd)}`;
4467
+ const path27 = firstString(tool.input, FILE_PATH_KEYS);
4468
+ if (path27 !== void 0) return `${tool.name} ${repoRelative(path27, cwd)}`;
4466
4469
  const other = firstString(tool.input, TARGET_KEYS);
4467
4470
  if (other !== void 0) return cut(`${tool.name}: ${repoRelative(firstLine(other), cwd)}`, BRIEF_LIMITS.quote);
4468
4471
  return tool.name;
@@ -4714,9 +4717,9 @@ function runDiagnostics(trace) {
4714
4717
  searches.set(key, found);
4715
4718
  }
4716
4719
  }
4717
- for (const [path26, entries] of reads) {
4720
+ for (const [path27, entries] of reads) {
4718
4721
  const hit = densest(entries, WASTE_RULES.repeatedRead.times, WASTE_RULES.repeatedRead.windowMinutes * 6e4);
4719
- if (hit) waste.push({ kind: "repeated-read", path: path26, steps: hit, windowMinutes: WASTE_RULES.repeatedRead.windowMinutes });
4722
+ if (hit) waste.push({ kind: "repeated-read", path: path27, steps: hit, windowMinutes: WASTE_RULES.repeatedRead.windowMinutes });
4720
4723
  }
4721
4724
  for (const { label, entries } of searches.values()) {
4722
4725
  const hit = densest(entries, WASTE_RULES.repeatedSearch.times, WASTE_RULES.repeatedSearch.windowMinutes * 6e4);
@@ -4795,19 +4798,19 @@ function filesOf(steps, cwd) {
4795
4798
  const patches = [];
4796
4799
  steps.forEach((step, index) => {
4797
4800
  if (!isSuccessfulEdit(step) || step.tool === void 0) return;
4798
- const path26 = repoRelative(firstString(step.tool.input, FILE_PATH_KEYS), cwd);
4799
- const file = files.get(path26) ?? { path: path26 };
4801
+ const path27 = repoRelative(firstString(step.tool.input, FILE_PATH_KEYS), cwd);
4802
+ const file = files.get(path27) ?? { path: path27 };
4800
4803
  if (step.diff !== void 0) {
4801
4804
  const counted = countPatch(step.diff.patch);
4802
4805
  file.added = (file.added ?? 0) + counted.added;
4803
4806
  file.removed = (file.removed ?? 0) + counted.removed;
4804
- patches.push({ path: path26, patch: repoRelative(step.diff.patch, cwd), step: step.stepNumber });
4807
+ patches.push({ path: path27, patch: repoRelative(step.diff.patch, cwd), step: step.stepNumber });
4805
4808
  }
4806
4809
  const result = resultOf(steps, index);
4807
4810
  if (step.tool.name.toLowerCase() === "write" && result !== void 0 && /^File created successfully\b/.test(result.trim())) {
4808
4811
  file.created = true;
4809
4812
  }
4810
- files.set(path26, file);
4813
+ files.set(path27, file);
4811
4814
  });
4812
4815
  return { files: [...files.values()], patches };
4813
4816
  }
@@ -4846,7 +4849,8 @@ function buildExecutionBrief(trace) {
4846
4849
  intent: {
4847
4850
  ...first !== void 0 ? { initialPrompt: { step: first.stepNumber, text: repoRelative(first.content.trim(), cwd) } } : {},
4848
4851
  resteers,
4849
- approvals
4852
+ approvals,
4853
+ ...source.part !== void 0 ? { part: { fromPrompt: source.part.fromPrompt, prompts: source.part.prompts } } : {}
4850
4854
  },
4851
4855
  scars: scarsOf(steps, cwd),
4852
4856
  outcome: {
@@ -4980,6 +4984,8 @@ function buildCapsule(trace, options = {}) {
4980
4984
  out.push(head.join(" \xB7 "));
4981
4985
  const git2 = lineageLine(brief);
4982
4986
  if (git2 !== null) out.push(git2);
4987
+ const part = brief.intent.part;
4988
+ if (part !== void 0) out.push(`Part of a longer session: from prompt ${part.fromPrompt} of ${part.prompts}`);
4983
4989
  const asked = brief.intent.initialPrompt;
4984
4990
  if (asked !== void 0) {
4985
4991
  out.push("", "Asked:");
@@ -5120,13 +5126,13 @@ function categorizeTrace(trace) {
5120
5126
  };
5121
5127
  }
5122
5128
  if (paths.length > 0) {
5123
- if (paths.every((path26) => matchesAny(path26, DOC_PATTERNS))) {
5129
+ if (paths.every((path27) => matchesAny(path27, DOC_PATTERNS))) {
5124
5130
  return { category: "docs", evidence: [`all ${paths.length} touched path(s) are documentation`, ...paths.slice(0, 3)] };
5125
5131
  }
5126
- if (paths.every((path26) => matchesAny(path26, TEST_PATTERNS))) {
5132
+ if (paths.every((path27) => matchesAny(path27, TEST_PATTERNS))) {
5127
5133
  return { category: "tests", evidence: [`all ${paths.length} touched path(s) are tests`, ...paths.slice(0, 3)] };
5128
5134
  }
5129
- if (paths.every((path26) => matchesAny(path26, INFRA_PATTERNS))) {
5135
+ if (paths.every((path27) => matchesAny(path27, INFRA_PATTERNS))) {
5130
5136
  return { category: "infra", evidence: [`all ${paths.length} touched path(s) are build/CI/deploy config`, ...paths.slice(0, 3)] };
5131
5137
  }
5132
5138
  }
@@ -5319,13 +5325,22 @@ function matchesSearch(row, q) {
5319
5325
  return matchesText(rowSearchText(row), q);
5320
5326
  }
5321
5327
  function matchesText(text2, q) {
5328
+ const terms = searchTerms(q);
5329
+ return termsMatched(text2, terms) === terms.length;
5330
+ }
5331
+ function termsMatched(text2, terms) {
5322
5332
  const haystack = normalizeSearch(text2);
5323
5333
  const stems = ` ${stemText(haystack)} `;
5324
- return searchTerms(q).every((term) => {
5334
+ return terms.filter((term) => {
5325
5335
  if (haystack.includes(term)) return true;
5326
5336
  const stemmed = stemText(term);
5327
5337
  return stemmed.length > 0 && stems.includes(stemmed);
5328
- });
5338
+ }).length;
5339
+ }
5340
+ var CLOSEST_LIMIT = 5;
5341
+ function closestByTerms(rows, textOf2, q) {
5342
+ const terms = searchTerms(q);
5343
+ return rows.map((row, index) => ({ row, index, matched: termsMatched(textOf2(row), terms), terms: terms.length })).filter((hit) => hit.matched > 0 && hit.matched < hit.terms).sort((a, b) => b.matched - a.matched || a.index - b.index).slice(0, CLOSEST_LIMIT).map(({ row, matched, terms: count }) => ({ row, matched, terms: count }));
5329
5344
  }
5330
5345
 
5331
5346
  // ../schema/src/index.ts
@@ -5436,7 +5451,13 @@ var TraceSourceSchema = external_exports.object({
5436
5451
  /** `flatbread share --pr <url>`: the pull request this run belongs to, as the person sharing it said. */
5437
5452
  prUrl: external_exports.string().url().max(GIT_LIMITS.prUrl).regex(/^https:\/\//, "a PR link must be https").optional(),
5438
5453
  /** Absent on records written before Pass 6, which were all claude-code. */
5439
- adapter: AdapterProvenanceSchema.optional()
5454
+ adapter: AdapterProvenanceSchema.optional(),
5455
+ /**
5456
+ * A PART OF A SESSION (decision 144): the run is the steps from prompt `fromPrompt` to the end of a
5457
+ * session of `prompts` prompts. Absent = the whole session. Its tokens are null: every harness
5458
+ * records them for the whole session, never per part.
5459
+ */
5460
+ part: external_exports.object({ fromPrompt: external_exports.number().int().min(2), prompts: external_exports.number().int().min(2) }).refine((part) => part.fromPrompt <= part.prompts, "a part cannot start after the session's last prompt").optional()
5440
5461
  }).superRefine((source, ctx) => {
5441
5462
  if (source.harnessVersion !== void 0 && source.claudeCodeVersion !== void 0 && source.harnessVersion !== source.claudeCodeVersion) {
5442
5463
  ctx.addIssue({
@@ -7093,7 +7114,7 @@ var claudeCodeAdapter = {
7093
7114
  import fs8 from "node:fs";
7094
7115
  import os3 from "node:os";
7095
7116
  import path6 from "node:path";
7096
- var CODEX_ADAPTER_VERSION = 1;
7117
+ var CODEX_ADAPTER_VERSION = 2;
7097
7118
  var REFUSAL_TEST2 = "A13";
7098
7119
  var ROLLOUT_NAME = /^rollout-.*\.jsonl(\.zst)?$/i;
7099
7120
  var UNDER_CODEX = /[\\/]\.codex[\\/](?:archived_)?sessions[\\/]/i;
@@ -7148,6 +7169,46 @@ function withoutFraming(text2) {
7148
7169
  const at = text2.indexOf("\nOutput:\n");
7149
7170
  return (at >= 0 ? text2.slice(at + "\nOutput:\n".length) : text2).trim();
7150
7171
  }
7172
+ var COPY_ITEMS = /* @__PURE__ */ new Set(["UserMessage", "AgentMessage", "Reasoning"]);
7173
+ function commandOf(value) {
7174
+ if (!Array.isArray(value) || !value.every((part) => typeof part === "string")) return str(value);
7175
+ const parts = value;
7176
+ const flag = parts[parts.length - 2];
7177
+ return parts.length >= 3 && (flag === "-lc" || flag === "-c") ? parts[parts.length - 1] ?? "" : parts.join(" ");
7178
+ }
7179
+ function itemEvents(item, at) {
7180
+ const id = str(item["id"]);
7181
+ const type = str(item["type"]);
7182
+ const status = str(item["status"]);
7183
+ if (type === "CommandExecution") {
7184
+ const exit = item["exit_code"];
7185
+ const workdir = str(item["cwd"]).replace(/^file:\/\//, "");
7186
+ return [
7187
+ { kind: "tool_call", ...at, id, name: "exec_command", input: { cmd: commandOf(item["command"]), ...workdir !== "" ? { workdir } : {} } },
7188
+ { kind: "tool_result", ...at, id, text: str(item["aggregated_output"]).trim(), isError: typeof exit === "number" ? exit !== 0 : status === "failed" }
7189
+ ];
7190
+ }
7191
+ if (type === "McpToolCall") {
7192
+ const result = isRecord2(item["result"]) ? item["result"] : {};
7193
+ const args = isRecord2(item["arguments"]) ? item["arguments"] : {};
7194
+ return [
7195
+ { kind: "tool_call", ...at, id, name: `mcp__${str(item["server"])}__${str(item["tool"])}`, input: args },
7196
+ { kind: "tool_result", ...at, id, text: outputText(result["content"]).trim(), isError: status !== "completed" || result["isError"] === true }
7197
+ ];
7198
+ }
7199
+ if (type === "FileChange") {
7200
+ const changes = isRecord2(item["changes"]) ? item["changes"] : {};
7201
+ return Object.keys(changes).flatMap((file, index) => {
7202
+ const change = changes[file];
7203
+ const callId = `${id}#${index}`;
7204
+ return [
7205
+ { kind: "tool_call", ...at, id: callId, name: "apply_patch", input: { path: file, ...isRecord2(change) && str(change["type"]) !== "" ? { change: str(change["type"]) } : {} } },
7206
+ { kind: "tool_result", ...at, id: callId, text: index === 0 ? `${str(item["stdout"])}${str(item["stderr"])}`.trim() : "", isError: status !== "completed" }
7207
+ ];
7208
+ });
7209
+ }
7210
+ return null;
7211
+ }
7151
7212
  function parseArguments(value) {
7152
7213
  if (typeof value !== "string") return value ?? {};
7153
7214
  try {
@@ -7167,6 +7228,9 @@ function decodeRollout(text2) {
7167
7228
  let inheritedAt = -1;
7168
7229
  let inheritedStream = 0;
7169
7230
  let dropped = 0;
7231
+ let itemized = false;
7232
+ const openCalls = [];
7233
+ const scripted = /* @__PURE__ */ new Set();
7170
7234
  for (const raw of text2.replace(/\r\n/g, "\n").split("\n")) {
7171
7235
  if (raw.trim() === "") continue;
7172
7236
  let record;
@@ -7218,12 +7282,28 @@ function decodeRollout(text2) {
7218
7282
  const usage = isRecord2(payload["info"]) ? payload["info"]["total_token_usage"] : void 0;
7219
7283
  if (isRecord2(usage) && typeof usage["input_tokens"] === "number" && typeof usage["output_tokens"] === "number") {
7220
7284
  const cached = typeof usage["cached_input_tokens"] === "number" ? usage["cached_input_tokens"] : 0;
7221
- tokens = { input: usage["input_tokens"], cached, output: usage["output_tokens"] };
7285
+ const cacheWrite = usage["cache_write_input_tokens"];
7286
+ tokens = { input: usage["input_tokens"], cached, output: usage["output_tokens"], ...typeof cacheWrite === "number" ? { cacheWrite } : {} };
7222
7287
  } else {
7223
7288
  tally2(unread, "token_count without info.total_token_usage");
7224
7289
  }
7225
7290
  continue;
7226
7291
  }
7292
+ if (ptype === "item_completed") {
7293
+ const item = isRecord2(payload["item"]) ? payload["item"] : {};
7294
+ const inner = itemEvents(item, at);
7295
+ if (inner !== null) {
7296
+ itemized = true;
7297
+ const script = openCalls[openCalls.length - 1];
7298
+ if (script !== void 0) scripted.add(script);
7299
+ roled.push(...inner);
7300
+ } else if (COPY_ITEMS.has(str(item["type"]))) {
7301
+ tally2(unread, "item_completed copies of response_items");
7302
+ } else {
7303
+ tally2(unread, `item_completed ${str(item["type"]) || "untyped"}`);
7304
+ }
7305
+ continue;
7306
+ }
7227
7307
  const message = str(payload["message"]).trim();
7228
7308
  if (ptype === "user_message" && message !== "" && !isInjectedPrompt(message)) streamed.push({ kind: "prompt", ...at, text: message });
7229
7309
  else if (ptype === "agent_message" && message !== "") streamed.push({ kind: "text", ...at, text: message });
@@ -7234,6 +7314,14 @@ function decodeRollout(text2) {
7234
7314
  tally2(unread, "compaction summary");
7235
7315
  continue;
7236
7316
  }
7317
+ if (type === "world_state") {
7318
+ tally2(unread, "world_state (Codex's environment snapshot)");
7319
+ continue;
7320
+ }
7321
+ if (type === "token_usage_record") {
7322
+ tally2(unread, "token_usage_record (per response; the last token_count is the total)");
7323
+ continue;
7324
+ }
7237
7325
  if (type !== "response_item") {
7238
7326
  tally2(unread, `record ${type || "untyped"}`);
7239
7327
  continue;
@@ -7272,11 +7360,15 @@ function decodeRollout(text2) {
7272
7360
  }
7273
7361
  const input = ptype === "function_call" ? parseArguments(payload["arguments"]) : payload["input"] ?? {};
7274
7362
  roled.push({ kind: "tool_call", ...at, id, name, input });
7363
+ openCalls.push(id);
7275
7364
  break;
7276
7365
  }
7277
7366
  case "function_call_output":
7278
7367
  case "custom_tool_call_output": {
7279
7368
  const id = str(payload["call_id"]);
7369
+ const open = openCalls.lastIndexOf(id);
7370
+ if (open >= 0) openCalls.splice(open, 1);
7371
+ if (scripted.has(id)) break;
7280
7372
  const output = outputText(payload["output"]);
7281
7373
  const exit = EXIT.exec(output);
7282
7374
  roled.push({
@@ -7292,6 +7384,11 @@ function decodeRollout(text2) {
7292
7384
  tally2(unread, `response_item ${ptype || "untyped"}`);
7293
7385
  }
7294
7386
  }
7387
+ if (scripted.size > 0) {
7388
+ const kept = roled.filter((event) => !(event.kind === "tool_call" && scripted.has(event.id)));
7389
+ roled.length = 0;
7390
+ roled.push(...kept);
7391
+ }
7295
7392
  const spoken = roled.some((event) => event.kind === "prompt" || event.kind === "text");
7296
7393
  if (!spoken && streamed.length > 0) {
7297
7394
  roled.push(...streamed);
@@ -7300,7 +7397,7 @@ function decodeRollout(text2) {
7300
7397
  unread.set("event_msg turns (copies of response_items)", streamed.length);
7301
7398
  }
7302
7399
  const events = Object.keys(meta).length > 1 ? [meta, ...roled] : roled;
7303
- return { events, ...sessionId !== void 0 ? { sessionId } : {}, records, unread, tokens, dropped };
7400
+ return { events, ...sessionId !== void 0 ? { sessionId } : {}, records, unread, tokens, dropped, itemized };
7304
7401
  }
7305
7402
  function homeOf2(env) {
7306
7403
  return (process.platform === "win32" ? env.USERPROFILE : env.HOME) || os3.homedir();
@@ -7374,21 +7471,24 @@ var codexAdapter = {
7374
7471
  }
7375
7472
  const events = [...rollout.events];
7376
7473
  if (rollout.tokens !== null) {
7377
- const { input: all, cached, output } = rollout.tokens;
7474
+ const { input: all, cached, output, cacheWrite } = rollout.tokens;
7475
+ const write2 = cacheWrite !== void 0 ? { cacheCreationTokens: cacheWrite } : {};
7378
7476
  events.push(
7379
- cached <= all ? { kind: "usage", inputTokens: all - cached, cacheReadTokens: cached, outputTokens: output } : { kind: "usage", inputTokens: all, outputTokens: output }
7477
+ cached <= all ? { kind: "usage", inputTokens: all - cached, cacheReadTokens: cached, outputTokens: output, ...write2 } : { kind: "usage", inputTokens: all, outputTokens: output, ...write2 }
7380
7478
  );
7381
7479
  }
7382
7480
  const notes = [`${rollout.records} record(s), ${said} prompt/reply event(s)`];
7383
7481
  if (rollout.tokens === null) notes.push("no token_count with total_token_usage: tokens not recorded in this rollout");
7384
7482
  else if (rollout.tokens.cached > rollout.tokens.input) notes.push("cached input exceeds input: cache read left out rather than guessed");
7385
7483
  if (rollout.dropped > 0) notes.push(`${rollout.dropped} event(s) from a parent thread's copied history left out`);
7386
- notes.push("CONFIDENCE: documented (three readers in .ref, not yet measured here). See HUMAN-TESTS.md A13.");
7484
+ notes.push(
7485
+ rollout.itemized ? "CONFIDENCE: the 0.163 item format, measured on two real rollouts (HUMAN-TESTS.md A13)." : "CONFIDENCE: documented (three readers in .ref; this older format is not measured here). See HUMAN-TESTS.md A13."
7486
+ );
7387
7487
  return {
7388
7488
  events,
7389
- // apply_patch names its files inside the patch text, which the builder does not read, so a
7390
- // count from it would be a 0 nobody measured.
7391
- capabilities: { tokens: rollout.tokens === null ? "unavailable" : "measured", filesChanged: "unavailable" },
7489
+ // An older rollout's apply_patch names its files inside the patch text, which the builder does
7490
+ // not read, so a count from it would be a 0 nobody measured. A 0.163 FileChange names them.
7491
+ capabilities: { tokens: rollout.tokens === null ? "unavailable" : "measured", filesChanged: rollout.itemized ? "measured" : "unavailable" },
7392
7492
  ...rollout.sessionId !== void 0 ? { sessionId: rollout.sessionId } : {},
7393
7493
  unreadFields: [...rollout.unread].sort((a, b) => b[1] - a[1]).map(([field, count]) => `${field} x${count}`),
7394
7494
  notes
@@ -10394,6 +10494,9 @@ button.primary:hover{background:var(--fg-lift);border-color:var(--fg-lift)}
10394
10494
  .done h1{font-size:20px}
10395
10495
  .done .meta{font-size:13.5px;color:var(--fg-2);margin:10px 0 0}
10396
10496
  .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}
10497
+ .done .meta code{display:inline;padding:1px 5px;margin:0;font-size:12px}
10498
+ .start{display:flex;gap:6px}
10499
+ .start select{flex:1;min-width:0;font:inherit;font-size:12.5px;padding:6px 8px;border:1px solid rgba(var(--shade),.12);border-radius:var(--radius-sm);background:var(--ivory);color:var(--fg)}
10397
10500
  a{color:var(--fg);text-underline-offset:2px}
10398
10501
  @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}}
10399
10502
  `;
@@ -10440,6 +10543,9 @@ var SCRIPT = `
10440
10543
  // TYPING IS NOT TRIAGE: j, k and space in the title field are letters. Enter submits the form
10441
10544
  // the way any text field does, through the first submit button - Confirm.
10442
10545
  if (tag === 'INPUT' && e.target.type === 'text') return;
10546
+ // A START IS CHOSEN WITH THE ARROWS AND ENTER, so on the select none of them is triage - and
10547
+ // Enter there must never press Confirm.
10548
+ if (tag === 'SELECT') return;
10443
10549
  if (key === 'j' || key === 'ArrowDown') { e.preventDefault(); focusRow(cursor + 1); return; }
10444
10550
  if (key === 'k' || key === 'ArrowUp') { e.preventDefault(); focusRow(cursor - 1); return; }
10445
10551
  if (key === ' ' && cursor >= 0 && tag !== 'BUTTON') {
@@ -10550,7 +10656,7 @@ function renderReviewPage(input) {
10550
10656
  const ruleSummary = [...ruleCounts.entries()].sort((a, b) => b[1] - a[1]).map(([rule, count]) => `${escapeHtml(rule)}&nbsp;${count}`).join(" &middot; ");
10551
10657
  const toolCalls = trace.stats.toolCallCount;
10552
10658
  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?`;
10553
- 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>`;
10659
+ 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>, or ask your agent to share it.</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>`;
10554
10660
  const body = `
10555
10661
  <header><div class="hin">
10556
10662
  <div class="bar"><span class="wordmark">flatbread</span><span class="crumb">review before you share</span></div>
@@ -10558,11 +10664,13 @@ function renderReviewPage(input) {
10558
10664
  <div class="meta">
10559
10665
  <b>${escapeHtml(trace.model)}</b> &middot; ${plural2(trace.steps.length, "step")} &middot; ${plural2(toolCalls, "tool call")} &middot;
10560
10666
  ${trace.stats.filesChanged === null ? `${formatCount(null)} files` : plural2(trace.stats.filesChanged, "file")} changed &middot; ${failuresRecorded(trace.source.harness) ? plural2(trace.stats.errorCount, "error") : `${formatCount(null)} errors`} &middot;
10561
- ${Math.round(trace.stats.durationSeconds)}s &middot; <b>${matches.length}</b> redaction match${matches.length === 1 ? "" : "es"}
10667
+ ${Math.round(trace.stats.durationSeconds)}s &middot; <b>${matches.length}</b> redaction match${matches.length === 1 ? "" : "es"}${trace.source.part === void 0 ? "" : ` &middot; from prompt ${trace.source.part.fromPrompt} of ${trace.source.part.prompts}`}
10562
10668
  </div>
10563
- <div class="meta path">${escapeHtml(sessionPath)}</div>
10564
- </div></header>
10565
- <form id="review" method="post" action="/confirm">
10669
+ <div class="meta path">${escapeHtml(sessionPath)}</div>${input.foundBy === void 0 ? "" : `
10670
+ <div class="meta">Found as ${escapeHtml(input.foundBy)}</div>`}
10671
+ </div></header>${input.startsAt === void 0 ? "" : `
10672
+ <form id="starts" method="get" action="/"></form>`}
10673
+ <form id="review" method="post" action="/confirm">${input.startsAt === void 0 ? "" : `<input type="hidden" name="from" value="${input.startsAt.current}">`}
10566
10674
  <div class="wrap">
10567
10675
  <main>${metaHtml}${stepsHtml}</main>
10568
10676
  <aside>
@@ -10571,6 +10679,13 @@ function renderReviewPage(input) {
10571
10679
  Every match is redacted by default. Tick a row to KEEP the original text instead.</p>
10572
10680
  <div class="tools"><button id="keep-none" type="button">redact all</button><button id="keep-all" type="button">keep all</button></div>
10573
10681
  <p class="keys"><b>j</b>/<b>k</b> move &middot; <b>space</b> keep or redact &middot; <b>enter</b> confirm</p>
10682
+ ${input.startsAt === void 0 ? "" : `<fieldset>
10683
+ <legend><label for="from">Starts at</label></legend>
10684
+ <div class="start"><select id="from" name="from" form="starts">${input.startsAt.prompts.map(
10685
+ (prompt2) => `<option value="${prompt2.n}"${prompt2.n === input.startsAt?.current ? " selected" : ""}>${prompt2.n}. ${escapeHtml(prompt2.preview.length > 72 ? `${prompt2.preview.slice(0, 71)}\u2026` : prompt2.preview)}</option>`
10686
+ ).join("")}</select><button type="submit" form="starts">Show</button></div>
10687
+ <p class="note">Share the whole session, or only the part from a later prompt. Showing another start resets the choices below.</p>
10688
+ </fieldset>`}
10574
10689
  ${input.titleDraft === void 0 ? "" : `<fieldset>
10575
10690
  <legend><label for="title">Title</label></legend>
10576
10691
  <input class="title" id="title" name="title" type="text" required maxlength="${LIMITS.title}" value="${escapeHtml(input.titleDraft)}">
@@ -10594,18 +10709,20 @@ function renderReviewPage(input) {
10594
10709
  <script>var PUBLIC_WARNING=${JSON.stringify(publicWarning)};${SCRIPT}</script>`;
10595
10710
  return page(`Flatbread review - ${trace.title}`, body);
10596
10711
  }
10597
- function renderDonePage(message, outputPath, publishTo, keepsRun = false) {
10598
- 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>`;
10712
+ function renderDonePage(report2) {
10713
+ const { upload } = report2;
10714
+ const heading = upload.kind === "published" ? "Shared" : "Saved on this machine";
10715
+ const network = upload.kind === "published" ? `<p class="meta">Uploaded to <a href="${escapeHtml(upload.url)}">${escapeHtml(upload.url)}</a></p>` : upload.kind === "failed" ? `<p class="meta">Upload FAILED: ${escapeHtml(upload.reason)}. Nothing was uploaded, and the run is kept here.</p>` : `<p class="meta">Nothing was uploaded: ${escapeHtml(upload.reason)}.</p>`;
10716
+ const kept = report2.slug === void 0 ? "" : `<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, <code>${escapeHtml(report2.slug)}</code>: <code>flatbread pull ${escapeHtml(report2.slug)}</code> prints it again, and <code>flatbread search --local</code> lists every run you have shared.</p>`;
10717
+ const token = report2.manageToken === void 0 ? "" : `<p class="meta"><strong>Copy the manage token now.</strong> It opens this run while it is private. It is shown once: only its hash is stored, so nothing here or on the server can show it again.</p><code>${escapeHtml(report2.manageToken)}</code>`;
10599
10718
  return page(
10600
- "Flatbread - share draft written",
10601
- `<div class="done"><h1>${escapeHtml(message)}</h1>
10602
- <code>${escapeHtml(outputPath)}</code>
10719
+ `Flatbread - ${heading.toLowerCase()}`,
10720
+ `<div class="done"><h1>${heading}</h1>
10603
10721
  ${network}
10604
- ${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>` : ""}
10605
- <p class="meta"><strong>Go back to your terminal now.</strong> The manage token for this
10606
- run is printed there, once, at the very bottom. It is the only way to open the run
10607
- while it is private, and only its hash is stored, so nothing can print it again.</p>
10608
- <p class="meta">Then you can close this tab; the review server is shutting down.</p></div>`
10722
+ <code>${escapeHtml(report2.draftPath)}</code>
10723
+ ${kept}
10724
+ ${token}
10725
+ <p class="meta">You can close this tab; the review server is shutting down.</p></div>`
10609
10726
  );
10610
10727
  }
10611
10728
  function renderCancelPage() {
@@ -10631,8 +10748,10 @@ function parseDecisionBody(body) {
10631
10748
  const form = new URLSearchParams(body);
10632
10749
  const parsedVisibility = VisibilitySchema.safeParse(form.get("visibility"));
10633
10750
  const title = form.get("title");
10751
+ const from = form.get("from");
10634
10752
  return {
10635
10753
  ...title !== null ? { title: normalizeTitle(title) } : {},
10754
+ ...from !== null ? { from: /^\d{1,6}$/.test(from) ? Number(from) : 0 } : {},
10636
10755
  visibility: parsedVisibility.success ? parsedVisibility.data : "private",
10637
10756
  // ONE CHECKBOX MAY DECIDE MANY MATCHES. A grouped row carries every id it governs,
10638
10757
  // space-separated, so the browser sends one value and the caller still gets the flat
@@ -10665,13 +10784,18 @@ async function listenOnFirstFreePort(server, ports) {
10665
10784
  }
10666
10785
  async function startReviewServer(options) {
10667
10786
  const ports = options.ports ?? REVIEW_SERVER.ports;
10668
- const html = renderReviewPage(options);
10787
+ const { starts } = options;
10788
+ const offered = (n) => starts?.prompts.some((prompt2) => prompt2.n === n) === true;
10789
+ const inputAt = (n) => starts === void 0 ? options : { ...starts.inputOf(n), startsAt: { current: n, prompts: starts.prompts } };
10790
+ const current = starts?.current;
10791
+ const html = renderReviewPage(current === void 0 ? options : inputAt(current));
10669
10792
  let settle = () => {
10670
10793
  };
10671
10794
  const decision = new Promise((resolve) => {
10672
10795
  settle = resolve;
10673
10796
  });
10674
10797
  let settled = false;
10798
+ let confirming = false;
10675
10799
  const finish = (value) => {
10676
10800
  if (settled) return;
10677
10801
  settled = true;
@@ -10690,7 +10814,8 @@ async function startReviewServer(options) {
10690
10814
  res.end(body);
10691
10815
  };
10692
10816
  if (method === "GET" && (url === "/" || url.startsWith("/?"))) {
10693
- send(200, html);
10817
+ const asked = Number(new URLSearchParams(url.slice(url.indexOf("?") + 1)).get("from"));
10818
+ send(200, starts !== void 0 && url.startsWith("/?") && offered(asked) && asked !== current ? renderReviewPage(inputAt(asked)) : html);
10694
10819
  return;
10695
10820
  }
10696
10821
  if (method === "GET" && url === "/favicon.ico") {
@@ -10706,7 +10831,29 @@ async function startReviewServer(options) {
10706
10831
  void (async () => {
10707
10832
  try {
10708
10833
  const body = await readBody(req);
10709
- const { visibility, keepIds, title } = parseDecisionBody(body);
10834
+ const { visibility, keepIds, title, from } = parseDecisionBody(body);
10835
+ if (confirming || settled) {
10836
+ send(
10837
+ 409,
10838
+ page(
10839
+ "Flatbread - already decided",
10840
+ `<div class="done"><h1>This review is already decided</h1><p class="meta">Confirm runs once. The first tab that pressed it shows what happened.</p></div>`
10841
+ )
10842
+ );
10843
+ return;
10844
+ }
10845
+ if (from !== void 0 && !offered(from)) {
10846
+ send(
10847
+ 400,
10848
+ page(
10849
+ "Flatbread - start refused",
10850
+ `<div class="done"><h1>That start is not one this review offered</h1><p class="meta"><a href="/">Back to the review</a>. Nothing was written.</p></div>`
10851
+ )
10852
+ );
10853
+ return;
10854
+ }
10855
+ const start = from ?? current;
10856
+ const shown = start === void 0 ? options : inputAt(start);
10710
10857
  if (title === null) {
10711
10858
  send(
10712
10859
  400,
@@ -10717,15 +10864,17 @@ async function startReviewServer(options) {
10717
10864
  );
10718
10865
  return;
10719
10866
  }
10720
- const changed = title !== void 0 && options.titleDraft !== void 0 && title !== normalizeTitle(options.titleDraft);
10867
+ const changed = title !== void 0 && shown.titleDraft !== void 0 && title !== normalizeTitle(shown.titleDraft);
10721
10868
  const confirmed = {
10722
10869
  action: "confirm",
10723
10870
  visibility,
10724
10871
  keepIds,
10725
- ...changed ? { title } : {}
10872
+ ...changed ? { title } : {},
10873
+ ...start !== void 0 ? { from: start } : {}
10726
10874
  };
10727
- const message = options.onConfirm ? await options.onConfirm(confirmed) : "";
10728
- send(200, renderDonePage("Share draft written", message, options.publishTo, options.keepsRun === true));
10875
+ confirming = true;
10876
+ const report2 = options.onConfirm ? await options.onConfirm(confirmed) : { draftPath: options.draftPath ?? "", upload: { kind: "none", reason: "nothing was asked to keep this run" } };
10877
+ send(200, renderDonePage(report2));
10729
10878
  finish(confirmed);
10730
10879
  } catch (error) {
10731
10880
  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>`));
@@ -10748,7 +10897,9 @@ async function startReviewServer(options) {
10748
10897
  });
10749
10898
  };
10750
10899
  if (options.timeoutMs !== void 0) {
10751
- const timer = setTimeout(() => finish({ action: "timeout" }), options.timeoutMs);
10900
+ const timer = setTimeout(() => {
10901
+ if (!confirming) finish({ action: "timeout" });
10902
+ }, options.timeoutMs);
10752
10903
  timer.unref();
10753
10904
  }
10754
10905
  return {
@@ -10759,6 +10910,80 @@ async function startReviewServer(options) {
10759
10910
  };
10760
10911
  }
10761
10912
 
10913
+ // src/part.ts
10914
+ function promptsOf(trace) {
10915
+ return trace.steps.flatMap((step, stepIndex) => step.type === "user_prompt" ? [{ stepIndex, text: step.content }] : []).map((prompt2, index) => ({ n: index + 1, ...prompt2 }));
10916
+ }
10917
+ function isShareCall(step) {
10918
+ if (step.type !== "tool_call" || step.tool === void 0) return false;
10919
+ if (/(^|__)flatbread_share$/.test(step.tool.name)) return true;
10920
+ const command = firstStringKey(step.tool.input, ["command", "cmd"]);
10921
+ return command !== null && /\bflatbread(?:-cli(?:@[\w.-]+)?)?(?:\.mjs)?\s+share\b/.test(command.value);
10922
+ }
10923
+ function lastIndex(items, test, before = items.length) {
10924
+ for (let i = before - 1; i >= 0; i -= 1) if (test(items[i])) return i;
10925
+ return -1;
10926
+ }
10927
+ function withoutShareTurn(trace, options = {}) {
10928
+ const prompt2 = lastIndex(trace.steps, (step) => step.type === "user_prompt");
10929
+ const call = lastIndex(trace.steps, isShareCall);
10930
+ const requested = call > prompt2;
10931
+ if (!requested && options.requestIsLast !== true) return trace;
10932
+ let end = requested ? call : trace.steps.length;
10933
+ if (prompt2 >= 0 && !trace.steps.slice(prompt2 + 1, end).some((step) => step.type === "tool_call")) end = prompt2;
10934
+ return end === trace.steps.length ? trace : recount(trace, trace.steps.slice(0, end));
10935
+ }
10936
+ function resolveFrom(prompts, from) {
10937
+ const asked = from.trim();
10938
+ if (/^\d+$/.test(asked)) {
10939
+ const n = Number(asked);
10940
+ if (n >= 1 && n <= prompts.length) return { n };
10941
+ return { problem: `there is no prompt ${n}: this session has ${plural2(prompts.length, "prompt")}`, candidates: [...prompts] };
10942
+ }
10943
+ const phrase = normalizeSearch(asked);
10944
+ if (phrase.length === 0) return { problem: "no prompt was named", candidates: [...prompts] };
10945
+ const exact = prompts.filter((prompt2) => normalizeSearch(prompt2.text).includes(phrase));
10946
+ const terms = searchTerms(asked);
10947
+ const hits = exact.length > 0 ? exact : prompts.filter((prompt2) => termsMatched(prompt2.text, terms) === terms.length);
10948
+ if (hits.length === 1) return { n: hits[0].n };
10949
+ return hits.length === 0 ? { problem: `no prompt holds "${asked}"`, candidates: [...prompts] } : { problem: `${hits.length} prompts hold "${asked}"`, candidates: hits };
10950
+ }
10951
+ function partFrom(trace, n) {
10952
+ const prompts = promptsOf(trace);
10953
+ const start = prompts[n - 1];
10954
+ if (start === void 0) throw new Error(`no prompt ${n} in a session of ${prompts.length}`);
10955
+ if (n === 1) return trace;
10956
+ return recount(trace, trace.steps.slice(start.stepIndex), { fromPrompt: n, prompts: prompts.length });
10957
+ }
10958
+ function recount(trace, kept, part) {
10959
+ const steps = kept.map((step, index) => ({ ...step, stepNumber: index }));
10960
+ const toolCallCount = steps.filter((step) => step.type === "tool_call").length;
10961
+ const errorCount = steps.filter((step) => step.type === "error_recovery").length;
10962
+ const changed = /* @__PURE__ */ new Set();
10963
+ for (const step of steps) {
10964
+ if (step.type !== "tool_call" || step.tool === void 0 || step.tool.isError === true) continue;
10965
+ if (!MUTATING_TOOL_NAMES.has(step.tool.name.toLowerCase())) continue;
10966
+ const hit = firstStringKey(step.tool.input, FILE_PATH_KEYS2);
10967
+ if (hit !== null) changed.add(hit.value);
10968
+ }
10969
+ const filesChanged = trace.stats.filesChanged === null ? null : changed.size;
10970
+ const times = steps.map((step) => Date.parse(step.timestamp)).filter((ms) => !Number.isNaN(ms)).sort((a, b) => a - b);
10971
+ const startedAt = times.length > 0 ? new Date(times[0]).toISOString() : trace.stats.startedAt;
10972
+ const endedAt = times.length > 0 ? new Date(times[times.length - 1]).toISOString() : trace.stats.endedAt;
10973
+ const durationSeconds = Math.max(0, (Date.parse(endedAt) - Date.parse(startedAt)) / 1e3);
10974
+ const filesPhrase = filesChanged === null ? "an unrecorded number of files changed" : `${plural2(filesChanged, "file")} changed`;
10975
+ const errorsPhrase = failuresRecorded(trace.source.harness) ? plural2(errorCount, "tool error") : "tool errors not recorded";
10976
+ const firstPrompt = steps.find((step) => step.type === "user_prompt");
10977
+ const tokens = part === void 0 ? {} : { inputTokens: null, outputTokens: null, cacheReadTokens: null, cacheCreationTokens: null };
10978
+ return FlatbreadTraceSchema.parse({
10979
+ ...trace,
10980
+ ...part !== void 0 ? { title: titleFromPrompt(firstPrompt?.content ?? ""), source: { ...trace.source, part } } : {},
10981
+ summary: `${part !== void 0 ? `From prompt ${part.fromPrompt} of ${part.prompts}: ` : ""}${plural2(steps.length, "step")}, ${plural2(toolCallCount, "tool call")}, ${filesPhrase}, ${errorsPhrase} over ${Math.round(durationSeconds)}s.`,
10982
+ stats: { ...trace.stats, ...tokens, startedAt, endedAt, durationSeconds, toolCallCount, errorCount, filesChanged },
10983
+ steps
10984
+ });
10985
+ }
10986
+
10762
10987
  // src/share.ts
10763
10988
  var spanKey = (match) => `${match.fieldPath}\0${match.start}\0${match.end}\0${match.original}`;
10764
10989
  function applyReview(trace, matches, keepIds, title) {
@@ -10920,7 +11145,7 @@ async function runShare(options) {
10920
11145
  target = path16.resolve(options.file);
10921
11146
  if (!fs17.existsSync(target) || !fs17.statSync(target).isFile()) {
10922
11147
  console.error(`file not found: ${target}`);
10923
- return { exitCode: 1, status: "not-found" };
11148
+ return { exitCode: 1, status: "not-found", problem: `file not found: ${target}` };
10924
11149
  }
10925
11150
  } else {
10926
11151
  const harness = options.harness ?? "claude-code";
@@ -10943,7 +11168,7 @@ async function runShare(options) {
10943
11168
  console.error(`could not parse ${target}`);
10944
11169
  console.error(` reason ${parsed.reason}`);
10945
11170
  if (parsed.humanTest) console.error(` next step HUMAN-TESTS.md section ${parsed.humanTest}`);
10946
- return { exitCode: 1, status: "not-found" };
11171
+ return { exitCode: 1, status: "not-found", problem: `could not read ${target}: ${parsed.reason}` };
10947
11172
  }
10948
11173
  const cwd = parsed.trace.source.cwd;
10949
11174
  let lineage;
@@ -10958,15 +11183,44 @@ async function runShare(options) {
10958
11183
  } else {
10959
11184
  lineage = (options.readLineage ?? ((dir) => readGitLineage(dir)))(cwd);
10960
11185
  }
10961
- const lined = withLineage(parsed.trace, lineage, options.prUrl);
10962
- const trace = options.title !== void 0 ? FlatbreadTraceSchema.parse({ ...lined, title: options.title }) : lined;
11186
+ const whole = withoutShareTurn(withLineage(parsed.trace, lineage, options.prUrl), { requestIsLast: options.requestIsLast === true });
11187
+ const prompts = promptsOf(whole);
11188
+ const redactedWhole = applyRedactions(whole, scanTrace(whole).matches, { keepIds: [] }).trace;
11189
+ const shownPrompts = promptsOf(redactedWhole);
11190
+ const shown = (refs) => refs.map((ref) => shownPrompts[ref.n - 1] ?? ref);
11191
+ if (prompts.length === 0) {
11192
+ const problem = "this session has no prompt before the share request, so there is nothing to share";
11193
+ console.error(problem);
11194
+ return { exitCode: 1, status: "empty", problem, candidates: [] };
11195
+ }
11196
+ let from = 1;
11197
+ if (options.from !== void 0) {
11198
+ const resolved = resolveFrom(prompts, options.from);
11199
+ if ("problem" in resolved) {
11200
+ console.error(`--from ${options.from}: ${resolved.problem}`);
11201
+ for (const prompt2 of shown(resolved.candidates)) console.error(` ${prompt2.n} ${promptPreview(prompt2)}`);
11202
+ return { exitCode: 1, status: "bad-from", problem: resolved.problem, candidates: shown(resolved.candidates) };
11203
+ }
11204
+ from = resolved.n;
11205
+ }
10963
11206
  for (const note of parsed.notes) log(`note: ${note}`);
10964
- const scan = scanTrace(trace);
11207
+ const views = /* @__PURE__ */ new Map();
11208
+ const viewOf = (n) => {
11209
+ const cached = views.get(n);
11210
+ if (cached !== void 0) return cached;
11211
+ const part = partFrom(whole, n);
11212
+ const trace2 = options.title !== void 0 ? FlatbreadTraceSchema.parse({ ...part, title: options.title }) : part;
11213
+ const scan2 = scanTrace(trace2);
11214
+ const view = { trace: trace2, scan: scan2, previewTitle: applyRedactions(trace2, scan2.matches, { keepIds: [] }).trace.title };
11215
+ views.set(n, view);
11216
+ return view;
11217
+ };
11218
+ const { trace, scan, previewTitle } = viewOf(from);
10965
11219
  const byRule = /* @__PURE__ */ new Map();
10966
11220
  for (const match of scan.matches) byRule.set(match.rule, (byRule.get(match.rule) ?? 0) + 1);
10967
11221
  log("");
10968
- const previewTitle = applyRedactions(trace, scan.matches, { keepIds: [] }).trace.title;
10969
11222
  log(` ${previewTitle}`);
11223
+ if (from > 1) log(` from prompt ${from} of ${prompts.length}`);
10970
11224
  log(` ${trace.steps.length} steps, ${trace.stats.toolCallCount} tool calls`);
10971
11225
  log(` scanned ${scan.fieldsScanned} string fields`);
10972
11226
  log(` ${scan.matches.length} redaction ${scan.matches.length === 1 ? "match" : "matches"}`);
@@ -10974,94 +11228,121 @@ async function runShare(options) {
10974
11228
  log(` ${String(count).padStart(4)} ${rule} (${labelForRule(rule)})`);
10975
11229
  }
10976
11230
  log("");
10977
- let keepIds = [];
10978
- let visibility = "private";
10979
- let written;
10980
11231
  if (options.noReview) {
10981
11232
  log("--no-review: redacting every match, visibility stays private");
10982
- } else {
10983
- const reviewPublisher = options.publisher ?? (options.publish === true ? createPublisherForMachine() : createPublisher());
10984
- const session = await startReviewServer({
10985
- trace,
10986
- matches: scan.matches,
10987
- slots: scan.slots,
11233
+ const applied = applyRedactions(trace, scan.matches, { keepIds: [] });
11234
+ const draft = buildDraft(applied.trace, applied.report, "private");
11235
+ writeDraft(options.outputPath, draft);
11236
+ logDraft(draft, options.outputPath, log);
11237
+ const persisted = await persistDraft(draft, options, log);
11238
+ reportPersist(persisted, options, log);
11239
+ return { exitCode: 0, status: "written", outputPath: options.outputPath, draft, ...persisted ? { persisted } : {} };
11240
+ }
11241
+ const reviewPublisher = options.publisher ?? (options.publish === true ? createPublisherForMachine() : createPublisher());
11242
+ const publishTo = isPublisher(reviewPublisher) ? reviewPublisher.description : void 0;
11243
+ const inputOf = (n) => {
11244
+ const view = viewOf(n);
11245
+ return {
11246
+ trace: view.trace,
11247
+ matches: view.scan.matches,
11248
+ slots: view.scan.slots,
10988
11249
  sessionPath: target,
10989
11250
  draftPath: options.outputPath,
10990
- titleDraft: previewTitle,
10991
- keepsRun: options.persist !== false && (options.storeDir ?? resolveStoreDir()) !== null,
10992
- ...isPublisher(reviewPublisher) ? { publishTo: reviewPublisher.description } : {},
10993
- ...options.ports ? { ports: options.ports } : {},
10994
- ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
10995
- onConfirm: (decision2) => {
10996
- const applied2 = applyReview(trace, scan.matches, decision2.keepIds, decision2.title);
10997
- const draft2 = buildDraft(applied2.trace, applied2.report, decision2.visibility);
10998
- writeDraft(options.outputPath, draft2);
10999
- written = draft2;
11000
- return options.outputPath;
11001
- }
11002
- });
11003
- if (!session) {
11004
- console.error(
11005
- `could not bind a review port on ${REVIEW_SERVER.host} (tried ${(options.ports ?? REVIEW_SERVER.ports).join(", ")}, then any port the OS would give)`
11006
- );
11007
- console.error("Loopback itself refused, so this machine needs a look; --no-review redacts everything without the page.");
11008
- return { exitCode: 1, status: "port-unavailable" };
11009
- }
11010
- log(` review at ${session.url}`);
11011
- if (options.openBrowser) {
11012
- const opened = openInBrowser(session.url);
11013
- log(opened ? " opening your browser\u2026" : ` could not open a browser - visit ${session.url}`);
11014
- }
11015
- log(" waiting for Confirm or Cancel\u2026");
11016
- const decision = await session.decision;
11017
- await session.close();
11018
- if (decision.action === "cancel") {
11019
- log("cancelled - nothing was written");
11020
- return { exitCode: 0, status: "cancelled", url: session.url };
11021
- }
11022
- if (decision.action === "timeout") {
11023
- log("timed out waiting for review - nothing was written");
11024
- return { exitCode: 1, status: "timeout", url: session.url };
11025
- }
11026
- if (written) {
11027
- log("");
11028
- log(` visibility ${written.trace.visibility}`);
11029
- log(` redacted ${written.redaction.redactedCount}`);
11030
- log(` kept ${written.redaction.keptCount}`);
11031
- log("");
11032
- log(`wrote ${options.outputPath}`);
11033
- const persisted2 = await persistDraft(written, options, log);
11034
- reportPersist(persisted2, options, log);
11035
- return {
11036
- exitCode: 0,
11037
- status: "written",
11038
- outputPath: options.outputPath,
11039
- draft: written,
11040
- url: session.url,
11041
- ...persisted2 ? { persisted: persisted2 } : {}
11042
- };
11251
+ titleDraft: view.previewTitle,
11252
+ ...publishTo !== void 0 ? { publishTo } : {},
11253
+ ...options.foundBy !== void 0 ? { foundBy: options.foundBy } : {}
11254
+ };
11255
+ };
11256
+ let written;
11257
+ const session = await startReviewServer({
11258
+ ...inputOf(from),
11259
+ ...prompts.length > 1 ? { starts: { current: from, prompts: shownPrompts.map((prompt2) => ({ n: prompt2.n, preview: promptPreview(prompt2) })), inputOf } } : {},
11260
+ ...options.ports ? { ports: options.ports } : {},
11261
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
11262
+ // EVERYTHING HAPPENS INSIDE THE CONFIRM, so the page it answers with is the receipt: the link,
11263
+ // or why there is none, and the manage token. A share started by an agent has no terminal.
11264
+ onConfirm: async (decision2) => {
11265
+ const view = viewOf(decision2.from ?? from);
11266
+ const applied = applyReview(view.trace, view.scan.matches, decision2.keepIds, decision2.title);
11267
+ const draft = buildDraft(applied.trace, applied.report, decision2.visibility);
11268
+ writeDraft(options.outputPath, draft);
11269
+ const persisted = await persistDraft(draft, options, log);
11270
+ written = { draft, ...persisted ? { persisted } : {} };
11271
+ return doneReportOf(options.outputPath, persisted);
11043
11272
  }
11044
- keepIds = decision.keepIds;
11045
- visibility = decision.visibility;
11046
- }
11047
- const applied = applyRedactions(trace, scan.matches, { keepIds });
11048
- const draft = buildDraft(applied.trace, applied.report, visibility);
11049
- writeDraft(options.outputPath, draft);
11050
- log(` visibility ${draft.trace.visibility}`);
11051
- log(` redacted ${draft.redaction.redactedCount}`);
11052
- log(` kept ${draft.redaction.keptCount}`);
11053
- log("");
11054
- log(`wrote ${options.outputPath}`);
11055
- const persisted = await persistDraft(draft, options, log);
11056
- reportPersist(persisted, options, log);
11273
+ });
11274
+ if (!session) {
11275
+ console.error(
11276
+ `could not bind a review port on ${REVIEW_SERVER.host} (tried ${(options.ports ?? REVIEW_SERVER.ports).join(", ")}, then any port the OS would give)`
11277
+ );
11278
+ console.error("Loopback itself refused, so this machine needs a look; --no-review redacts everything without the page.");
11279
+ return { exitCode: 1, status: "port-unavailable" };
11280
+ }
11281
+ log(` review at ${session.url}`);
11282
+ if (options.openBrowser) {
11283
+ const opened = openInBrowser(session.url);
11284
+ log(opened ? " opening your browser\u2026" : ` could not open a browser - visit ${session.url}`);
11285
+ }
11286
+ log(" waiting for Confirm or Cancel\u2026");
11287
+ options.onReviewReady?.({
11288
+ url: session.url,
11289
+ sessionPath: target,
11290
+ steps: trace.steps.length,
11291
+ matches: scan.matches.length,
11292
+ prompts: prompts.length,
11293
+ fromPrompt: from,
11294
+ ...publishTo !== void 0 ? { publishTo } : {}
11295
+ });
11296
+ const decision = await session.decision;
11297
+ await session.close();
11298
+ if (decision.action === "cancel") {
11299
+ log("cancelled - nothing was written");
11300
+ return { exitCode: 0, status: "cancelled", url: session.url };
11301
+ }
11302
+ if (decision.action === "timeout") {
11303
+ log("timed out waiting for review - nothing was written");
11304
+ return { exitCode: 1, status: "timeout", url: session.url };
11305
+ }
11306
+ const done = written;
11307
+ if (done === void 0) {
11308
+ return { exitCode: 1, status: "cancelled", url: session.url };
11309
+ }
11310
+ logDraft(done.draft, options.outputPath, log);
11311
+ reportPersist(done.persisted, options, log);
11057
11312
  return {
11058
11313
  exitCode: 0,
11059
11314
  status: "written",
11060
11315
  outputPath: options.outputPath,
11061
- draft,
11062
- ...persisted ? { persisted } : {}
11316
+ draft: done.draft,
11317
+ url: session.url,
11318
+ ...done.persisted ? { persisted: done.persisted } : {}
11063
11319
  };
11064
11320
  }
11321
+ function promptPreview(prompt2) {
11322
+ return titleFromPrompt(prompt2.text, "(empty prompt)");
11323
+ }
11324
+ function logDraft(draft, outputPath, log) {
11325
+ log("");
11326
+ log(` visibility ${draft.trace.visibility}`);
11327
+ log(` redacted ${draft.redaction.redactedCount}`);
11328
+ log(` kept ${draft.redaction.keptCount}`);
11329
+ log("");
11330
+ log(`wrote ${outputPath}`);
11331
+ }
11332
+ function doneReportOf(draftPath, persisted) {
11333
+ if (persisted === void 0) return { draftPath, upload: { kind: "none", reason: "no record was kept, so nothing was uploaded" } };
11334
+ const base = { draftPath, slug: persisted.slug, manageToken: persisted.manageToken };
11335
+ if (persisted.publish?.ok === true) return { ...base, upload: { kind: "published", url: persisted.publish.url } };
11336
+ if (persisted.publish !== void 0) return { ...base, upload: { kind: "failed", reason: publishFailureDetail(persisted.publish) } };
11337
+ return { ...base, upload: { kind: "none", reason: skippedReason(persisted.publishSkipped) } };
11338
+ }
11339
+ function skippedReason(skipped) {
11340
+ if (skipped === "not-signed-in") return "this machine is not signed in. Run `flatbread login`, then share again";
11341
+ if (skipped === "wrong-origin") return "the saved sign-in was issued by a different site. Run `flatbread login` against this one";
11342
+ if (skipped === "partial-env") return "FLATBREAD_API_URL is not set, so there is nowhere to send it";
11343
+ if (skipped === "bad-url") return "FLATBREAD_API_URL is not an address this can send to";
11344
+ return "this share was not asked to upload";
11345
+ }
11065
11346
 
11066
11347
  // src/view.ts
11067
11348
  import fs18 from "node:fs";
@@ -11236,6 +11517,7 @@ async function searchRemote(query, options = {}) {
11236
11517
  if (query.query) params.set("q", query.query);
11237
11518
  if (query.harness) params.set("harness", query.harness);
11238
11519
  if (query.category) params.set("category", query.category);
11520
+ params.set("partial", "1");
11239
11521
  const answer = await get(`${apiUrl}/api/search?${params.toString()}`, headersFor(apiUrl, options, "application/json"), options);
11240
11522
  if (!answer.ok) return answer;
11241
11523
  if (answer.status !== 200) return failureOf(answer.status, answer.body);
@@ -11248,7 +11530,16 @@ async function searchRemote(query, options = {}) {
11248
11530
  const results = parsed.results;
11249
11531
  if (!Array.isArray(results)) return { ok: false, reason: "http", status: 200, detail: "the search answer had no results list" };
11250
11532
  if (!results.every(isHit)) return { ok: false, reason: "http", status: 200, detail: "a search result was not readable" };
11251
- return { ok: true, results, site: apiUrl };
11533
+ const closest = parsed.closest ?? [];
11534
+ if (!Array.isArray(closest) || !closest.every(isClosest)) {
11535
+ return { ok: false, reason: "http", status: 200, detail: "a closest result was not readable" };
11536
+ }
11537
+ return { ok: true, results, closest, site: apiUrl };
11538
+ }
11539
+ function isClosest(value) {
11540
+ if (!isHit(value)) return false;
11541
+ const hit = value;
11542
+ return typeof hit["matched"] === "number" && typeof hit["terms"] === "number";
11252
11543
  }
11253
11544
  function isHit(value) {
11254
11545
  if (typeof value !== "object" || value === null) return false;
@@ -11273,16 +11564,141 @@ function describeReadFailure(failure, what) {
11273
11564
  // src/mcp.ts
11274
11565
  import readline3 from "node:readline";
11275
11566
 
11276
- // src/bucket.ts
11277
- import { createHash as createHash2 } from "node:crypto";
11567
+ // src/caller-session.ts
11278
11568
  import fs19 from "node:fs";
11279
11569
  import path18 from "node:path";
11570
+ var SESSION_ENV = [
11571
+ { harness: "claude-code", variable: "CLAUDE_CODE_SESSION_ID", fileOf: (base, id) => base === `${id}.jsonl` },
11572
+ { harness: "codex", variable: "CODEX_SESSION_ID", fileOf: (base, id) => base.endsWith(`-${id}.jsonl`) || base.endsWith(`-${id}.jsonl.zst`) }
11573
+ ];
11574
+ var CODEX_ROLLOUTS_CHECKED = 40;
11575
+ function samePath(a, b) {
11576
+ const [x, y] = [path18.resolve(a), path18.resolve(b)];
11577
+ return process.platform === "win32" ? x.toLowerCase() === y.toLowerCase() : x === y;
11578
+ }
11579
+ function mtimeOf(file) {
11580
+ try {
11581
+ return fs19.statSync(file).mtimeMs;
11582
+ } catch {
11583
+ return 0;
11584
+ }
11585
+ }
11586
+ function ago(ms) {
11587
+ const seconds = Math.max(0, Math.round(ms / 1e3));
11588
+ return seconds < 120 ? `${seconds} s ago` : `${Math.round(seconds / 60)} min ago`;
11589
+ }
11590
+ async function newestHere(harness, cwd, env) {
11591
+ if (harness === "claude-code") return listSessionFiles(path18.join(getProjectsDir(env), encodeCwd(path18.resolve(cwd))))[0]?.filePath ?? null;
11592
+ if (harness === "codex") {
11593
+ for (const file of (adapterFor("codex").discover?.(env) ?? []).filter((f) => f.endsWith(".jsonl")).slice(0, CODEX_ROLLOUTS_CHECKED)) {
11594
+ const meta = (await readFirstJsonLine(file))?.["payload"];
11595
+ const recorded = typeof meta === "object" && meta !== null ? meta["cwd"] : void 0;
11596
+ if (typeof recorded === "string" && samePath(recorded, cwd)) return file;
11597
+ }
11598
+ }
11599
+ return null;
11600
+ }
11601
+ async function findCallerSession(options) {
11602
+ const { env, cwd } = options;
11603
+ const now = options.nowMs ?? Date.now();
11604
+ const unmatched = [];
11605
+ const named = [];
11606
+ for (const { harness, variable, fileOf } of SESSION_ENV) {
11607
+ if (options.harness !== void 0 && options.harness !== harness) continue;
11608
+ const id = env[variable]?.trim() ?? "";
11609
+ if (id.length === 0) continue;
11610
+ const file = (adapterFor(harness).discover?.(env) ?? []).find((candidate) => fileOf(path18.basename(candidate), id));
11611
+ if (file !== void 0) named.push({ file, harness, how: `named by ${variable}`, mtime: mtimeOf(file) });
11612
+ else unmatched.push(`${variable}=${id} names no session file on this machine`);
11613
+ }
11614
+ const claimed = named.sort((a, b) => b.mtime - a.mtime)[0];
11615
+ if (claimed !== void 0) return { file: claimed.file, harness: claimed.harness, how: claimed.how };
11616
+ const harnesses = options.harness !== void 0 ? [options.harness] : ["claude-code", "codex"];
11617
+ const here = [];
11618
+ for (const harness of harnesses) {
11619
+ const file = await newestHere(harness, cwd, env);
11620
+ if (file !== null) here.push({ file, harness, mtime: mtimeOf(file) });
11621
+ }
11622
+ const newest = here.sort((a, b) => b.mtime - a.mtime)[0];
11623
+ if (newest !== void 0) {
11624
+ return { file: newest.file, harness: newest.harness, how: `the newest ${HARNESS_INFO[newest.harness].label} session in ${cwd}, written ${ago(now - newest.mtime)}` };
11625
+ }
11626
+ if (options.harness !== void 0 && options.harness !== "claude-code" && options.harness !== "codex") {
11627
+ const file = adapterFor(options.harness).discover?.(env)[0];
11628
+ if (file !== void 0) {
11629
+ return { file, harness: options.harness, how: `the newest ${HARNESS_INFO[options.harness].label} session on this machine, written ${ago(now - mtimeOf(file))} (this harness records no folder to match)` };
11630
+ }
11631
+ }
11632
+ const which = options.harness !== void 0 ? HARNESS_INFO[options.harness].label : "Claude Code or Codex";
11633
+ return { problem: [`no ${which} session is recorded for ${cwd}`, ...unmatched].join("; ") };
11634
+ }
11635
+
11636
+ // src/agent-share.ts
11637
+ var AGENT_REVIEW_TIMEOUT_MS = 30 * 6e4;
11638
+ function createAgentShare(context) {
11639
+ let open = null;
11640
+ const log = context.log ?? ((line) => process.stderr.write(`${line}
11641
+ `));
11642
+ return async (request) => {
11643
+ if (open !== null) return { ok: true, ...open, alreadyOpen: true };
11644
+ const found = await (context.find ?? findCallerSession)({
11645
+ env: context.env,
11646
+ cwd: context.cwd,
11647
+ ...request.harness !== void 0 ? { harness: request.harness } : {}
11648
+ });
11649
+ if ("problem" in found) return { ok: false, problem: found.problem };
11650
+ let markReady = () => {
11651
+ };
11652
+ const ready = new Promise((resolve) => {
11653
+ markReady = resolve;
11654
+ });
11655
+ const done = (context.share ?? runShare)({
11656
+ file: found.file,
11657
+ harness: found.harness,
11658
+ requestIsLast: true,
11659
+ foundBy: found.how,
11660
+ ...request.from !== void 0 ? { from: request.from } : {},
11661
+ publish: request.publish,
11662
+ openBrowser: context.openBrowser ?? true,
11663
+ outputPath: context.outputPath,
11664
+ ...context.storeDir !== null ? { storeDir: context.storeDir } : {},
11665
+ ...context.ports !== void 0 ? { ports: context.ports } : {},
11666
+ timeoutMs: context.timeoutMs ?? AGENT_REVIEW_TIMEOUT_MS,
11667
+ log,
11668
+ onReviewReady: markReady
11669
+ });
11670
+ const first = await Promise.race([ready.then((r) => ({ ready: r })), done.then((result) => ({ result }))]);
11671
+ if ("result" in first) {
11672
+ const { result } = first;
11673
+ return {
11674
+ ok: false,
11675
+ problem: result.problem ?? `the share stopped before its review opened (${result.status})`,
11676
+ ...result.candidates !== void 0 ? { candidates: result.candidates } : {}
11677
+ };
11678
+ }
11679
+ const entry = { ready: first.ready, foundBy: found.how, publish: request.publish };
11680
+ open = entry;
11681
+ const release = () => {
11682
+ if (open === entry) open = null;
11683
+ };
11684
+ done.then(release, (error) => {
11685
+ log(`flatbread share failed: ${error instanceof Error ? error.message : String(error)}`);
11686
+ release();
11687
+ });
11688
+ return { ok: true, ...entry, alreadyOpen: false };
11689
+ };
11690
+ }
11691
+
11692
+ // src/bucket.ts
11693
+ import { createHash as createHash2 } from "node:crypto";
11694
+ import fs20 from "node:fs";
11695
+ import path19 from "node:path";
11280
11696
  var BUCKET_INDEX_VERSION = 1;
11281
11697
  var BUCKET_INDEX_FILE = "index.json";
11282
11698
  var CAPTURE_LOG_FILE = "capture.log";
11283
11699
  var COMMIT_SESSION_WINDOW_MS = 2 * 60 * 60 * 1e3;
11284
11700
  function bucketDir(env = process.env) {
11285
- return path18.join(flatbreadHome(env), "bucket");
11701
+ return path19.join(flatbreadHome(env), "bucket");
11286
11702
  }
11287
11703
  function bucketSlug(harness, sessionKey) {
11288
11704
  return `local-${createHash2("sha256").update(`${harness}\0${sessionKey}`, "utf8").digest("hex").slice(0, 12)}`;
@@ -11297,18 +11713,18 @@ function rowOf(record) {
11297
11713
  };
11298
11714
  }
11299
11715
  function recordStems(dir) {
11300
- return fs19.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -".json".length)).filter((stem2) => SlugSchema.safeParse(stem2).success).sort();
11716
+ return fs20.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -".json".length)).filter((stem2) => SlugSchema.safeParse(stem2).success).sort();
11301
11717
  }
11302
11718
  function writeAtomic(file, body) {
11303
- fs19.mkdirSync(path18.dirname(file), { recursive: true });
11719
+ fs20.mkdirSync(path19.dirname(file), { recursive: true });
11304
11720
  const temp = `${file}.${process.pid}.tmp`;
11305
- fs19.writeFileSync(temp, body, "utf8");
11306
- fs19.renameSync(temp, file);
11721
+ fs20.writeFileSync(temp, body, "utf8");
11722
+ fs20.renameSync(temp, file);
11307
11723
  }
11308
11724
  var newestFirst = (rows) => [...rows].sort((a, b) => a.capturedAt < b.capturedAt ? 1 : a.capturedAt > b.capturedAt ? -1 : a.summary.slug < b.summary.slug ? -1 : 1);
11309
11725
  function readIndex(dir) {
11310
11726
  try {
11311
- const parsed = JSON.parse(fs19.readFileSync(path18.join(dir, BUCKET_INDEX_FILE), "utf8"));
11727
+ const parsed = JSON.parse(fs20.readFileSync(path19.join(dir, BUCKET_INDEX_FILE), "utf8"));
11312
11728
  if (typeof parsed !== "object" || parsed === null) return null;
11313
11729
  const { version, rows } = parsed;
11314
11730
  if (version !== BUCKET_INDEX_VERSION || !Array.isArray(rows)) return null;
@@ -11327,10 +11743,10 @@ function readIndex(dir) {
11327
11743
  }
11328
11744
  }
11329
11745
  function writeIndex(dir, rows) {
11330
- writeAtomic(path18.join(dir, BUCKET_INDEX_FILE), JSON.stringify({ version: BUCKET_INDEX_VERSION, rows: newestFirst(rows) }, null, 2) + "\n");
11746
+ writeAtomic(path19.join(dir, BUCKET_INDEX_FILE), JSON.stringify({ version: BUCKET_INDEX_VERSION, rows: newestFirst(rows) }, null, 2) + "\n");
11331
11747
  }
11332
11748
  function readBucket(dir) {
11333
- if (!fs19.existsSync(dir)) return { rows: [], rebuilt: false, unreadable: 0 };
11749
+ if (!fs20.existsSync(dir)) return { rows: [], rebuilt: false, unreadable: 0 };
11334
11750
  const stems = recordStems(dir);
11335
11751
  const indexed = readIndex(dir);
11336
11752
  if (indexed !== null && JSON.stringify(indexed.map((row) => row.summary.slug).sort()) === JSON.stringify(stems)) {
@@ -11347,16 +11763,18 @@ function readBucket(dir) {
11347
11763
  function readBucketRecord(dir, slug) {
11348
11764
  if (!SlugSchema.safeParse(slug).success) return null;
11349
11765
  try {
11350
- const parsed = TraceRecordSchema.safeParse(JSON.parse(fs19.readFileSync(path18.join(dir, `${slug}.json`), "utf8")));
11766
+ const parsed = TraceRecordSchema.safeParse(JSON.parse(fs20.readFileSync(path19.join(dir, `${slug}.json`), "utf8")));
11351
11767
  return parsed.success ? parsed.data : null;
11352
11768
  } catch {
11353
11769
  return null;
11354
11770
  }
11355
11771
  }
11356
11772
  function matchesQuery(summary, query) {
11773
+ return matchesFilters(summary, query) && matchesSearch(summary, query.query ?? "");
11774
+ }
11775
+ function matchesFilters(summary, query) {
11357
11776
  if (query.harness !== void 0 && summary.harness !== query.harness) return false;
11358
- if (query.category !== void 0 && summary.category !== query.category) return false;
11359
- return matchesSearch(summary, query.query ?? "");
11777
+ return query.category === void 0 || summary.category === query.category;
11360
11778
  }
11361
11779
  async function captureToBucket(options) {
11362
11780
  const dir = options.dir ?? bucketDir(options.env);
@@ -11375,9 +11793,9 @@ async function captureToBucket(options) {
11375
11793
  const trace = withLineage(parsed.trace, lineage);
11376
11794
  const scan = scanTrace(trace);
11377
11795
  const applied = applyRedactions(trace, scan.matches, { keepIds: [] });
11378
- const sessionKey = trace.source.sessionId.length > 0 ? trace.source.sessionId : path18.resolve(options.file);
11796
+ const sessionKey = trace.source.sessionId.length > 0 ? trace.source.sessionId : path19.resolve(options.file);
11379
11797
  const slug = bucketSlug(parsed.harness, sessionKey);
11380
- const recordPath = path18.join(dir, `${slug}.json`);
11798
+ const recordPath = path19.join(dir, `${slug}.json`);
11381
11799
  const previous = readBucketRecord(dir, slug);
11382
11800
  const commit = applied.trace.source.commitNamed === true ? applied.trace.source.headCommitSha : void 0;
11383
11801
  const draft = ShareDraftSchema.parse({
@@ -11399,7 +11817,7 @@ async function captureToBucket(options) {
11399
11817
  }
11400
11818
  function sessionForRepo(repo, options = {}) {
11401
11819
  const projects = getProjectsDir(options.env ?? process.env);
11402
- const folder = path18.join(projects, encodeCwd(path18.resolve(repo)));
11820
+ const folder = path19.join(projects, encodeCwd(path19.resolve(repo)));
11403
11821
  const newest = listSessionFiles(folder)[0];
11404
11822
  if (newest === void 0) return { problem: `no Claude Code session recorded for ${repo}` };
11405
11823
  const age = (options.nowMs ?? Date.now()) - newest.mtimeMs;
@@ -11411,8 +11829,8 @@ function sessionForRepo(repo, options = {}) {
11411
11829
  }
11412
11830
  function logCapture(dir, line, now = (/* @__PURE__ */ new Date()).toISOString()) {
11413
11831
  try {
11414
- fs19.mkdirSync(dir, { recursive: true });
11415
- fs19.appendFileSync(path18.join(dir, CAPTURE_LOG_FILE), `${now} ${line.replace(/[\r\n]+/g, " ")}
11832
+ fs20.mkdirSync(dir, { recursive: true });
11833
+ fs20.appendFileSync(path19.join(dir, CAPTURE_LOG_FILE), `${now} ${line.replace(/[\r\n]+/g, " ")}
11416
11834
  `, "utf8");
11417
11835
  } catch {
11418
11836
  }
@@ -11420,13 +11838,14 @@ function logCapture(dir, line, now = (/* @__PURE__ */ new Date()).toISOString())
11420
11838
 
11421
11839
  // src/mcp.ts
11422
11840
  var MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
11423
- var MCP_SERVER_VERSION = "0.5.0";
11424
- function defaultMcpDeps(storeDir2, bucketDir2) {
11841
+ var MCP_SERVER_VERSION = "0.7.0";
11842
+ function defaultMcpDeps(storeDir2, bucketDir2, shareOutputPath) {
11425
11843
  return {
11426
11844
  storeDir: storeDir2,
11427
11845
  bucketDir: bucketDir2,
11428
11846
  search: (query) => searchRemote(query, { client: "mcp" }),
11429
- capsule: (ref) => fetchCapsule(ref, { client: "mcp" })
11847
+ capsule: (ref) => fetchCapsule(ref, { client: "mcp" }),
11848
+ ...shareOutputPath !== void 0 ? { share: createAgentShare({ env: process.env, cwd: process.cwd(), outputPath: shareOutputPath, storeDir: storeDir2 }) } : {}
11430
11849
  };
11431
11850
  }
11432
11851
  var MCP_TOOLS = [
@@ -11437,7 +11856,7 @@ var MCP_TOOLS = [
11437
11856
  inputSchema: {
11438
11857
  type: "object",
11439
11858
  properties: {
11440
- query: { type: "string", description: "Words that must all appear in the run's title or ID" },
11859
+ query: { type: "string", description: "Two to four words from the task or the error. Runs with every word in their title or ID come first; when none has them all, the closest are listed, marked with how many words they share" },
11441
11860
  harness: { type: "string", enum: [...HARNESS_IDS], description: "Only runs from this agent harness" },
11442
11861
  category: { type: "string", enum: [...TASK_CATEGORIES], description: "Only runs of this kind of job" }
11443
11862
  },
@@ -11454,6 +11873,20 @@ var MCP_TOOLS = [
11454
11873
  required: ["slug"],
11455
11874
  additionalProperties: false
11456
11875
  }
11876
+ },
11877
+ {
11878
+ name: "flatbread_share",
11879
+ title: "Share this run",
11880
+ description: `Share the session you are in now as a Flatbread run. Call it when the user says "share this run", "flatbread share", "share to flatbread" or anything like it. It finds this session, leaves the share request out, opens a review page in the user's browser with every secret redacted by default, and returns at once: nothing is kept or uploaded until the user presses Confirm there. For only part of a long session, pass from: the number of the user's prompt to start at, or words from that prompt.`,
11881
+ inputSchema: {
11882
+ type: "object",
11883
+ properties: {
11884
+ from: { type: "string", description: 'Start at this prompt: its number ("3"), or words from it. Leave out for the whole session' },
11885
+ publish: { type: "boolean", description: "Upload on Confirm when this machine is signed in to flatbread.io. Default true; false keeps it on this machine" },
11886
+ harness: { type: "string", enum: [...HARNESS_IDS], description: "Which agent you are, only when you are not Claude Code or Codex" }
11887
+ },
11888
+ additionalProperties: false
11889
+ }
11457
11890
  }
11458
11891
  ];
11459
11892
  function text(body, isError = false) {
@@ -11500,6 +11933,8 @@ async function searchEverywhere(query, deps, options = {}) {
11500
11933
  const out = [];
11501
11934
  const local = localRecords(deps);
11502
11935
  const localHits = local.records.map((record) => summarize(record)).filter((summary) => matchesQuery(summary, query));
11936
+ let found = localHits.length;
11937
+ const nearby = [];
11503
11938
  if (deps.storeDir !== null) {
11504
11939
  out.push(`This machine (${runs(localHits.length)}):`);
11505
11940
  for (const hit of localHits) out.push(hitLine(hit));
@@ -11510,9 +11945,23 @@ async function searchEverywhere(query, deps, options = {}) {
11510
11945
  const { harness, sessionId } = record.trace.source;
11511
11946
  if (sessionId.length > 0 && localHits.some((hit) => hit.slug === record.slug)) seen.add(bucketSlug(harness, sessionId));
11512
11947
  }
11948
+ const nearSeen = /* @__PURE__ */ new Set();
11949
+ const localNear = local.records.filter((record) => matchesFilters(summarize(record), query));
11950
+ for (const { row, matched, terms } of closestByTerms(localNear, (record) => rowSearchText(summarize(record)), query.query ?? "")) {
11951
+ nearby.push({ line: `${hitLine(summarize(row))} \xB7 ${matched} of ${terms} words`, matched });
11952
+ nearSeen.add(row.slug);
11953
+ const { harness, sessionId } = row.trace.source;
11954
+ if (sessionId.length > 0) nearSeen.add(bucketSlug(harness, sessionId));
11955
+ }
11513
11956
  if (deps.bucketDir !== null) {
11514
11957
  const bucket = readBucket(deps.bucketDir);
11515
11958
  const captured = bucket.rows.filter((row) => !seen.has(row.summary.slug) && matchesQuery(row.summary, query));
11959
+ found += captured.length;
11960
+ const bucketNear = bucket.rows.filter((row) => !nearSeen.has(row.summary.slug) && matchesFilters(row.summary, query));
11961
+ for (const { row, matched, terms } of closestByTerms(bucketNear, (row2) => rowSearchText(row2.summary), query.query ?? "")) {
11962
+ nearby.push({ line: `${capturedLine(row)} \xB7 ${matched} of ${terms} words`, matched });
11963
+ nearSeen.add(row.summary.slug);
11964
+ }
11516
11965
  if (out.length > 0) out.push("");
11517
11966
  out.push(`Captured on this machine, not shared (${runs(captured.length)}):`);
11518
11967
  for (const row of captured) {
@@ -11521,21 +11970,31 @@ async function searchEverywhere(query, deps, options = {}) {
11521
11970
  }
11522
11971
  if (bucket.unreadable > 0) out.push(`(${bucket.unreadable} captured run file${bucket.unreadable === 1 ? "" : "s"} could not be read)`);
11523
11972
  }
11973
+ let siteSearched = false;
11524
11974
  if (options.remote === false) {
11525
11975
  if (out.length > 0) out.push("");
11526
11976
  out.push("The site was not searched: --local");
11527
- return { lines: out, siteSearched: false };
11528
- }
11529
- const remote = await deps.search(query);
11530
- if (out.length > 0) out.push("");
11531
- if (remote.ok) {
11532
- const fresh = remote.results.filter((hit) => !seen.has(hit.slug));
11533
- out.push(`${remote.site} (${runs(fresh.length)}):`);
11534
- for (const hit of fresh) out.push(hitLine(hit));
11535
11977
  } else {
11536
- out.push(`The site was not searched: ${describeReadFailure(remote, "search")}`);
11978
+ const remote = await deps.search(query);
11979
+ if (out.length > 0) out.push("");
11980
+ if (remote.ok) {
11981
+ siteSearched = true;
11982
+ const fresh = remote.results.filter((hit) => !seen.has(hit.slug));
11983
+ found += fresh.length;
11984
+ out.push(`${remote.site} (${runs(fresh.length)}):`);
11985
+ for (const hit of fresh) out.push(hitLine(hit));
11986
+ for (const hit of (remote.closest ?? []).filter((hit2) => !seen.has(hit2.slug) && !nearSeen.has(hit2.slug))) {
11987
+ nearby.push({ line: `${hitLine(hit)} \xB7 ${hit.matched} of ${hit.terms} words`, matched: hit.matched });
11988
+ }
11989
+ } else {
11990
+ out.push(`The site was not searched: ${describeReadFailure(remote, "search")}`);
11991
+ }
11537
11992
  }
11538
- return { lines: out, siteSearched: remote.ok };
11993
+ if (found === 0 && nearby.length > 0) {
11994
+ out.push("", `No run has every word of "${query.query ?? ""}". Closest, by words in common:`);
11995
+ for (const hit of [...nearby].sort((a, b) => b.matched - a.matched).slice(0, CLOSEST_LIMIT)) out.push(hit.line);
11996
+ }
11997
+ return { lines: out, siteSearched };
11539
11998
  }
11540
11999
  async function searchTool(args, deps) {
11541
12000
  const query = readQuery(args);
@@ -11553,6 +12012,50 @@ async function readCapsule(ref, deps) {
11553
12012
  const remote = await deps.capsule(ref);
11554
12013
  return remote.ok ? { ok: true, capsule: remote.capsule, from: "site" } : remote;
11555
12014
  }
12015
+ function readShareRequest(args) {
12016
+ const from = args["from"];
12017
+ const publish = args["publish"];
12018
+ const harness = args["harness"];
12019
+ if (from !== void 0 && typeof from !== "string" && !(typeof from === "number" && Number.isInteger(from))) {
12020
+ return "from must be a prompt number or words from that prompt";
12021
+ }
12022
+ if (publish !== void 0 && typeof publish !== "boolean") return "publish must be true or false";
12023
+ if (harness !== void 0 && (typeof harness !== "string" || !HARNESS_IDS.includes(harness))) {
12024
+ return `harness must be one of ${HARNESS_IDS.join(", ")}`;
12025
+ }
12026
+ return {
12027
+ ...from !== void 0 && String(from).trim().length > 0 ? { from: String(from) } : {},
12028
+ publish: publish !== false,
12029
+ ...harness !== void 0 ? { harness } : {}
12030
+ };
12031
+ }
12032
+ function shareLines(start) {
12033
+ const { ready } = start;
12034
+ const matches = `${ready.matches} redaction match${ready.matches === 1 ? "" : "es"}`;
12035
+ return [
12036
+ start.alreadyOpen ? `A review is already open in the user's browser, so no new one was started: ${ready.url}` : `Review open in the user's browser: ${ready.url}`,
12037
+ `Session: ${ready.sessionPath} (found as ${start.foundBy})`,
12038
+ ready.fromPrompt > 1 ? `Shares prompt ${ready.fromPrompt} of ${ready.prompts} to the end` : `Shares the whole session: ${ready.prompts} prompt${ready.prompts === 1 ? "" : "s"}`,
12039
+ `${ready.steps} steps, ${matches}, each redacted unless the user keeps it`,
12040
+ ready.publishTo !== void 0 ? `Confirm uploads it to ${ready.publishTo} at the visibility the user picks there` : start.publish ? "Confirm keeps it on this machine only: this machine is not signed in, so nothing is uploaded (`flatbread login` signs it in)" : "Confirm keeps it on this machine only (publish: false)",
12041
+ "",
12042
+ "Nothing is kept or uploaded until the user presses Confirm on that page, which then shows the link and the manage token. Tell the user to review it there; do not wait for it."
12043
+ ];
12044
+ }
12045
+ async function shareTool(args, deps) {
12046
+ if (deps.share === void 0) return text("flatbread_share is not available in this server: run `flatbread share` in a terminal", true);
12047
+ const request = readShareRequest(args);
12048
+ if (typeof request === "string") return text(request, true);
12049
+ const start = await deps.share(request);
12050
+ if (start.ok) return text(shareLines(start).join("\n"));
12051
+ const lines2 = [`Nothing was shared: ${start.problem}.`];
12052
+ if (start.candidates !== void 0 && start.candidates.length > 0) {
12053
+ lines2.push("", "This session's prompts, redacted:");
12054
+ for (const prompt2 of start.candidates) lines2.push(`${prompt2.n}. ${titleFromPrompt(prompt2.text, "(empty prompt)")}`);
12055
+ lines2.push("", "Ask the user which prompt to start at, then call flatbread_share again with from set to its number.");
12056
+ }
12057
+ return text(lines2.join("\n"), true);
12058
+ }
11556
12059
  async function capsuleTool(args, deps) {
11557
12060
  const raw = args["slug"];
11558
12061
  if (typeof raw !== "string") return text("slug must be a run ID or a /r/<id> link", true);
@@ -11584,7 +12087,7 @@ async function handleMcpMessage(message, deps) {
11584
12087
  protocolVersion,
11585
12088
  capabilities: { tools: { listChanged: false } },
11586
12089
  serverInfo: { name: "flatbread", version: MCP_SERVER_VERSION },
11587
- instructions: "Flatbread holds recorded coding-agent runs. Before a task someone may have done before, or after an error you have not seen, call flatbread_search_traces, then flatbread_get_capsule on a promising run and read its blockers before repeating them."
12090
+ instructions: "Flatbread holds recorded coding-agent runs. Before a task someone may have done before, or after an error you have not seen, call flatbread_search_traces, then flatbread_get_capsule on a promising run and read its blockers before repeating them. When the user asks to share this run, call flatbread_share."
11588
12091
  }
11589
12092
  };
11590
12093
  }
@@ -11598,6 +12101,7 @@ async function handleMcpMessage(message, deps) {
11598
12101
  try {
11599
12102
  if (name === "flatbread_search_traces") return { jsonrpc: "2.0", id, result: await searchTool(toolArgs, deps) };
11600
12103
  if (name === "flatbread_get_capsule") return { jsonrpc: "2.0", id, result: await capsuleTool(toolArgs, deps) };
12104
+ if (name === "flatbread_share") return { jsonrpc: "2.0", id, result: await shareTool(toolArgs, deps) };
11601
12105
  } catch (error) {
11602
12106
  return { jsonrpc: "2.0", id, result: text(`flatbread failed: ${error instanceof Error ? error.message : String(error)}`, true) };
11603
12107
  }
@@ -11636,17 +12140,17 @@ async function runMcpServer(deps, io) {
11636
12140
  }
11637
12141
 
11638
12142
  // src/setup.ts
11639
- import fs25 from "node:fs";
11640
- import path24 from "node:path";
12143
+ import fs26 from "node:fs";
12144
+ import path25 from "node:path";
11641
12145
 
11642
12146
  // src/claude-mcp.ts
11643
- import fs20 from "node:fs";
12147
+ import fs21 from "node:fs";
11644
12148
  import os8 from "node:os";
11645
- import path19 from "node:path";
12149
+ import path20 from "node:path";
11646
12150
  var MCP_SERVER_NAME = "flatbread";
11647
12151
  function claudeUserConfigPath(env = process.env) {
11648
12152
  const dir = env["CLAUDE_CONFIG_DIR"]?.trim();
11649
- return path19.join(dir ? dir : os8.homedir(), ".claude.json");
12153
+ return path20.join(dir ? dir : os8.homedir(), ".claude.json");
11650
12154
  }
11651
12155
  function mcpEntryFor(execPath, entryPath) {
11652
12156
  return { type: "stdio", command: execPath, args: [entryPath, "mcp"] };
@@ -11661,7 +12165,7 @@ var BOM = "\uFEFF";
11661
12165
  function readConfig(file) {
11662
12166
  let raw;
11663
12167
  try {
11664
- raw = fs20.readFileSync(file, "utf8");
12168
+ raw = fs21.readFileSync(file, "utf8");
11665
12169
  } catch (error) {
11666
12170
  if (error.code === "ENOENT") return { root: {}, raw: null };
11667
12171
  return { problem: error.message };
@@ -11688,19 +12192,19 @@ function serializeLike(raw, root) {
11688
12192
  function write(file, read2, next) {
11689
12193
  if (read2.raw !== null) {
11690
12194
  try {
11691
- fs20.accessSync(file, fs20.constants.W_OK);
12195
+ fs21.accessSync(file, fs21.constants.W_OK);
11692
12196
  } catch {
11693
12197
  return `${file} is read-only, so it was left as it was`;
11694
12198
  }
11695
- if (fs20.readFileSync(file, "utf8") !== read2.raw) return `${file} changed while setup ran; run setup again`;
12199
+ if (fs21.readFileSync(file, "utf8") !== read2.raw) return `${file} changed while setup ran; run setup again`;
11696
12200
  const backup = `${file}.flatbread.bak`;
11697
- if (!fs20.existsSync(backup)) fs20.copyFileSync(file, backup);
12201
+ if (!fs21.existsSync(backup)) fs21.copyFileSync(file, backup);
11698
12202
  }
11699
- const mode = read2.raw !== null ? fs20.statSync(file).mode & 511 : 384;
11700
- fs20.mkdirSync(path19.dirname(file), { recursive: true });
12203
+ const mode = read2.raw !== null ? fs21.statSync(file).mode & 511 : 384;
12204
+ fs21.mkdirSync(path20.dirname(file), { recursive: true });
11701
12205
  const temp = `${file}.flatbread-${process.pid}.tmp`;
11702
- fs20.writeFileSync(temp, next, { encoding: "utf8", mode });
11703
- fs20.renameSync(temp, file);
12206
+ fs21.writeFileSync(temp, next, { encoding: "utf8", mode });
12207
+ fs21.renameSync(temp, file);
11704
12208
  return null;
11705
12209
  }
11706
12210
  function registerClaudeMcp(options) {
@@ -11746,7 +12250,7 @@ function registerMcpJson(options) {
11746
12250
  }
11747
12251
  log(` mcp ${file}: removed the ${MCP_SERVER_NAME} server`);
11748
12252
  const backup = `${file}.flatbread.bak`;
11749
- if (fs20.existsSync(backup)) log(` ${backup} is the copy setup took before its first write; delete it when you like`);
12253
+ if (fs21.existsSync(backup)) log(` ${backup} is the copy setup took before its first write; delete it when you like`);
11750
12254
  return 0;
11751
12255
  }
11752
12256
  const entry = options.entry;
@@ -11775,12 +12279,12 @@ function registerMcpJson(options) {
11775
12279
  }
11776
12280
 
11777
12281
  // src/codex-mcp.ts
11778
- import fs21 from "node:fs";
11779
- import path20 from "node:path";
12282
+ import fs22 from "node:fs";
12283
+ import path21 from "node:path";
11780
12284
  var BOM2 = "\uFEFF";
11781
12285
  var OWNED_KEYS = /* @__PURE__ */ new Set(["type", "command", "args"]);
11782
12286
  function codexConfigPath(codexHome2) {
11783
- return path20.join(codexHome2, "config.toml");
12287
+ return path21.join(codexHome2, "config.toml");
11784
12288
  }
11785
12289
  function quote(value) {
11786
12290
  return JSON.stringify(value);
@@ -12001,19 +12505,19 @@ function writeLike(file, raw, lf) {
12001
12505
  if (raw !== null && raw.startsWith(BOM2)) next = BOM2 + next;
12002
12506
  if (raw !== null) {
12003
12507
  try {
12004
- fs21.accessSync(file, fs21.constants.W_OK);
12508
+ fs22.accessSync(file, fs22.constants.W_OK);
12005
12509
  } catch {
12006
12510
  return `${file} is read-only, so it was left as it was`;
12007
12511
  }
12008
- if (fs21.readFileSync(file, "utf8") !== raw) return `${file} changed while setup ran; run setup again`;
12512
+ if (fs22.readFileSync(file, "utf8") !== raw) return `${file} changed while setup ran; run setup again`;
12009
12513
  const backup = `${file}.flatbread.bak`;
12010
- if (!fs21.existsSync(backup)) fs21.copyFileSync(file, backup);
12514
+ if (!fs22.existsSync(backup)) fs22.copyFileSync(file, backup);
12011
12515
  }
12012
- const mode = raw !== null ? fs21.statSync(file).mode & 511 : 384;
12013
- fs21.mkdirSync(path20.dirname(file), { recursive: true });
12516
+ const mode = raw !== null ? fs22.statSync(file).mode & 511 : 384;
12517
+ fs22.mkdirSync(path21.dirname(file), { recursive: true });
12014
12518
  const temp = `${file}.flatbread-${process.pid}.tmp`;
12015
- fs21.writeFileSync(temp, next, { encoding: "utf8", mode });
12016
- fs21.renameSync(temp, file);
12519
+ fs22.writeFileSync(temp, next, { encoding: "utf8", mode });
12520
+ fs22.renameSync(temp, file);
12017
12521
  return null;
12018
12522
  }
12019
12523
  function removeBlocks(lines2, blocks) {
@@ -12030,7 +12534,7 @@ function registerCodexMcp(options) {
12030
12534
  const { file, log } = options;
12031
12535
  let raw;
12032
12536
  try {
12033
- raw = fs21.readFileSync(file, "utf8");
12537
+ raw = fs22.readFileSync(file, "utf8");
12034
12538
  } catch (error) {
12035
12539
  if (error.code !== "ENOENT") {
12036
12540
  log(` mcp refused: ${file}: ${error.message}`);
@@ -12128,8 +12632,8 @@ function registerCodexMcp(options) {
12128
12632
  }
12129
12633
 
12130
12634
  // src/git-hook.ts
12131
- import fs22 from "node:fs";
12132
- import path21 from "node:path";
12635
+ import fs23 from "node:fs";
12636
+ import path22 from "node:path";
12133
12637
  var POST_COMMIT_MARKER = "# flatbread post-commit hook - remove with: flatbread setup --uninstall";
12134
12638
  var CHAINED_HOOK = "post-commit.pre-flatbread";
12135
12639
  var sq = (value) => `'${value.replace(/\\/g, "/").replace(/'/g, "'\\''")}'`;
@@ -12173,25 +12677,25 @@ function hooksDirFor(cwd, options = {}) {
12173
12677
  }
12174
12678
  const common = git(top, ["rev-parse", "--git-common-dir"], options)?.trim();
12175
12679
  if (common === void 0 || common.length === 0) return { ok: false, reason: "unreadable", detail: `git could not name the git directory of ${top}` };
12176
- return { ok: true, repo: path21.resolve(top), dir: path21.resolve(top, common, "hooks"), ...machineHooksPath !== void 0 ? { machineHooksPath } : {} };
12680
+ return { ok: true, repo: path22.resolve(top), dir: path22.resolve(top, common, "hooks"), ...machineHooksPath !== void 0 ? { machineHooksPath } : {} };
12177
12681
  }
12178
12682
  var read = (file) => {
12179
12683
  try {
12180
- return fs22.readFileSync(file, "utf8");
12684
+ return fs23.readFileSync(file, "utf8");
12181
12685
  } catch {
12182
12686
  return null;
12183
12687
  }
12184
12688
  };
12185
12689
  function installGitHook(dir, script, options = {}) {
12186
- const hook = path21.join(dir, "post-commit");
12187
- const chained = path21.join(dir, CHAINED_HOOK);
12690
+ const hook = path22.join(dir, "post-commit");
12691
+ const chained = path22.join(dir, CHAINED_HOOK);
12188
12692
  const current = read(hook);
12189
12693
  const write2 = (lines2) => {
12190
12694
  if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
12191
- fs22.mkdirSync(dir, { recursive: true });
12192
- if (current !== null && !isOurs(current)) fs22.renameSync(hook, chained);
12193
- fs22.writeFileSync(hook, script, { encoding: "utf8", mode: 493 });
12194
- fs22.chmodSync(hook, 493);
12695
+ fs23.mkdirSync(dir, { recursive: true });
12696
+ if (current !== null && !isOurs(current)) fs23.renameSync(hook, chained);
12697
+ fs23.writeFileSync(hook, script, { encoding: "utf8", mode: 493 });
12698
+ fs23.chmodSync(hook, 493);
12195
12699
  return { code: 0, lines: lines2 };
12196
12700
  };
12197
12701
  if (current === null) return write2([`post-commit ${hook} (new)`]);
@@ -12199,7 +12703,7 @@ function installGitHook(dir, script, options = {}) {
12199
12703
  if (current === script) return { code: 0, lines: [`post-commit ${hook}`, "already installed, unchanged"] };
12200
12704
  return write2([`post-commit ${hook}`, "updated the flatbread hook"]);
12201
12705
  }
12202
- if (fs22.existsSync(chained)) {
12706
+ if (fs23.existsSync(chained)) {
12203
12707
  return {
12204
12708
  code: 1,
12205
12709
  lines: [
@@ -12215,10 +12719,10 @@ function installGitHook(dir, script, options = {}) {
12215
12719
  ]);
12216
12720
  }
12217
12721
  function uninstallGitHook(dir, options = {}) {
12218
- const hook = path21.join(dir, "post-commit");
12219
- const chained = path21.join(dir, CHAINED_HOOK);
12722
+ const hook = path22.join(dir, "post-commit");
12723
+ const chained = path22.join(dir, CHAINED_HOOK);
12220
12724
  const current = read(hook);
12221
- const hasChained = fs22.existsSync(chained);
12725
+ const hasChained = fs23.existsSync(chained);
12222
12726
  const done = (lines2, act) => {
12223
12727
  if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
12224
12728
  act();
@@ -12236,17 +12740,17 @@ function uninstallGitHook(dir, options = {}) {
12236
12740
  if (current === null && !hasChained) return { code: 0, lines: [`no flatbread post-commit in ${dir}`] };
12237
12741
  if (hasChained) {
12238
12742
  return done([`post-commit ${hook}`, `restored your post-commit from ${CHAINED_HOOK}`], () => {
12239
- if (current !== null) fs22.rmSync(hook);
12240
- fs22.renameSync(chained, hook);
12743
+ if (current !== null) fs23.rmSync(hook);
12744
+ fs23.renameSync(chained, hook);
12241
12745
  });
12242
12746
  }
12243
- return done([`post-commit ${hook}`, "removed the flatbread hook"], () => fs22.rmSync(hook));
12747
+ return done([`post-commit ${hook}`, "removed the flatbread hook"], () => fs23.rmSync(hook));
12244
12748
  }
12245
12749
 
12246
12750
  // src/hook.ts
12247
12751
  import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
12248
- import fs23 from "node:fs";
12249
- import path22 from "node:path";
12752
+ import fs24 from "node:fs";
12753
+ import path23 from "node:path";
12250
12754
  var HOOK_EVENTS = ["Stop", "SessionEnd"];
12251
12755
  var OFFER_AFTER_STOPS = 2;
12252
12756
  var MAX_TRACKED_SESSIONS = 50;
@@ -12256,14 +12760,14 @@ function emptyHookState() {
12256
12760
  return { version: HOOK_STATE_VERSION, sessions: {} };
12257
12761
  }
12258
12762
  function hookStatePath(env = process.env) {
12259
- return path22.join(flatbreadHome(env), "hook-state.json");
12763
+ return path23.join(flatbreadHome(env), "hook-state.json");
12260
12764
  }
12261
12765
  function readHookState(env = process.env) {
12262
12766
  const file = hookStatePath(env);
12263
- if (!fs23.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
12767
+ if (!fs24.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
12264
12768
  let raw;
12265
12769
  try {
12266
- raw = fs23.readFileSync(file, "utf8");
12770
+ raw = fs24.readFileSync(file, "utf8");
12267
12771
  } catch (error) {
12268
12772
  return { ok: false, reason: "unreadable", detail: error.message };
12269
12773
  }
@@ -12306,9 +12810,9 @@ function pruneHookState(state, keep = MAX_TRACKED_SESSIONS) {
12306
12810
  }
12307
12811
  function writeHookState(state, env = process.env) {
12308
12812
  const dir = flatbreadHome(env);
12309
- fs23.mkdirSync(dir, { recursive: true, mode: 448 });
12813
+ fs24.mkdirSync(dir, { recursive: true, mode: 448 });
12310
12814
  const file = hookStatePath(env);
12311
- fs23.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
12815
+ fs24.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
12312
12816
  encoding: "utf8",
12313
12817
  mode: 384
12314
12818
  });
@@ -12454,7 +12958,7 @@ async function runHookNotify(io) {
12454
12958
  const stdout = io.stdout ?? ((line) => process.stdout.write(line + "\n"));
12455
12959
  const stderr = io.stderr ?? ((line) => process.stderr.write(line + "\n"));
12456
12960
  const now = io.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
12457
- const exists = io.exists ?? ((filePath) => fs23.existsSync(filePath));
12961
+ const exists = io.exists ?? ((filePath) => fs24.existsSync(filePath));
12458
12962
  const execPath = io.execPath ?? process.execPath;
12459
12963
  const entryPath = io.entryPath ?? process.argv[1] ?? "";
12460
12964
  try {
@@ -12611,10 +13115,10 @@ function removeHookConfig(settings) {
12611
13115
  return { settings: root, removed };
12612
13116
  }
12613
13117
  function readSettingsFile(file) {
12614
- if (!fs23.existsSync(file)) return { present: false, file };
13118
+ if (!fs24.existsSync(file)) return { present: false, file };
12615
13119
  let raw;
12616
13120
  try {
12617
- raw = fs23.readFileSync(file, "utf8");
13121
+ raw = fs24.readFileSync(file, "utf8");
12618
13122
  } catch (error) {
12619
13123
  return { present: true, file, unreadable: error.message };
12620
13124
  }
@@ -12626,10 +13130,10 @@ function readSettingsFile(file) {
12626
13130
  }
12627
13131
  }
12628
13132
  function writeSettingsFile(file, settings) {
12629
- fs23.mkdirSync(path22.dirname(file), { recursive: true });
13133
+ fs24.mkdirSync(path23.dirname(file), { recursive: true });
12630
13134
  const temp = `${file}.flatbread-${process.pid}.tmp`;
12631
- fs23.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
12632
- fs23.renameSync(temp, file);
13135
+ fs24.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
13136
+ fs24.renameSync(temp, file);
12633
13137
  }
12634
13138
  function handlersFor(execPath, entryPath, capture = false) {
12635
13139
  return {
@@ -12787,8 +13291,8 @@ function runHookUninstall(options = {}) {
12787
13291
  function settingsCandidates(env, cwd) {
12788
13292
  return [
12789
13293
  { label: "user", file: claudeSettingsPath(env) },
12790
- { label: "project", file: path22.join(cwd, ".claude", "settings.json") },
12791
- { label: "local", file: path22.join(cwd, ".claude", "settings.local.json") }
13294
+ { label: "project", file: path23.join(cwd, ".claude", "settings.json") },
13295
+ { label: "local", file: path23.join(cwd, ".claude", "settings.local.json") }
12792
13296
  ];
12793
13297
  }
12794
13298
  function installedEventsIn(parsed, exists) {
@@ -12818,7 +13322,7 @@ function runHookStatus(options = {}) {
12818
13322
  const env = options.env ?? process.env;
12819
13323
  const log = options.log ?? ((line) => console.log(line));
12820
13324
  const cwd = options.cwd ?? process.cwd();
12821
- const exists = options.exists ?? ((filePath) => fs23.existsSync(filePath));
13325
+ const exists = options.exists ?? ((filePath) => fs24.existsSync(filePath));
12822
13326
  const candidates = options.settingsPath === void 0 ? settingsCandidates(env, cwd) : [{ label: "settings", file: options.settingsPath }];
12823
13327
  log("");
12824
13328
  log("flatbread hook");
@@ -12939,21 +13443,27 @@ function probeHandler(handler, run = spawnSync2) {
12939
13443
  }
12940
13444
 
12941
13445
  // src/skill.ts
12942
- import fs24 from "node:fs";
12943
- import path23 from "node:path";
13446
+ import fs25 from "node:fs";
13447
+ import path24 from "node:path";
12944
13448
  var SKILL_NAME = "flatbread";
12945
13449
  var MARKER = "<!-- written by flatbread setup; flatbread setup --uninstall removes it -->";
12946
- var DESCRIPTION = "Search recorded AI coding-agent runs - this machine's own and public ones on flatbread.io - for one that already did this task or hit this error. Use BEFORE your first edit in any coding task, however small it looks; when a command, test or build fails; when the user says it was done or fixed before; and before you call a change done.";
13450
+ var DESCRIPTION = `Search recorded AI coding-agent runs - this machine's own and public ones on flatbread.io - for one that already did this task or hit this error. Use BEFORE your first edit in any coding task, however small it looks; when a command, test or build fails; when the user says it was done or fixed before; and before you call a change done. Also when the user says "share this run", "flatbread share" or "share to flatbread".`;
12947
13451
  var BODY = `Search Flatbread once before your first edit in a coding task, even one that looks small and well-defined. A small task is exactly the one someone has already done, and the search costs one call. Then search again whenever a command, a test or a build fails, with the exact error line as the query.
12948
13452
 
12949
13453
  ## How
12950
13454
 
12951
- 1. Call \`flatbread_search_traces\` with two to four specific words from the task or the error (all of them must appear in a run's title). Try a second wording before deciding nothing is there.
13455
+ 1. Call \`flatbread_search_traces\` with two to four specific words from the task or the error. Runs with every word come first; when none has them all, the closest are listed with how many words they share - open one only if its title is the same job. Try a second wording before deciding nothing is there.
12952
13456
  2. If a hit looks like the same job, call \`flatbread_get_capsule\` with its run ID. The capsule is that run's own record: what was asked, how the person steered it, each blocker and the fix, what the tests said, the diff.
12953
13457
  3. Use what it says. A blocker listed under "do not repeat" is a dead end someone already paid for: do not walk into it again.
12954
13458
 
12955
13459
  If the tools are not connected in this session, the same search runs in the shell: \`flatbread search <words>\` and \`flatbread pull <run id>\`.
12956
13460
 
13461
+ ## When the user asks to share this run
13462
+
13463
+ "Share this run", "flatbread share", "share to flatbread", "save this run": call \`flatbread_share\`. Do not open a terminal or look for the session yourself - the tool finds this session, leaves the share request out, and opens a review page in the user's browser. If the user wants only part of the session ("share it from where I asked about the retry"), pass \`from\` with that prompt's number or words from it; if the tool lists the prompts instead, ask the user which one. Then tell the user the page is open, in one line: nothing is kept or uploaded until they press Confirm there, and the page shows the link. Do not wait for it.
13464
+
13465
+ Without the tool connected, the shell does the same with the person at the terminal: \`flatbread share\` (\`--from <n|words>\` for a part).
13466
+
12957
13467
  ## Saying what you used
12958
13468
 
12959
13469
  When a capsule changed what you did, say so in one line at the start of your reply, with the run ID: "Flatbread: <run id> hit this before - <what it said>." Say nothing about searches that found nothing useful.
@@ -12982,7 +13492,7 @@ var ANTIGRAVITY_PLUGIN_MANIFEST = `${JSON.stringify({ name: SKILL_NAME }, null,
12982
13492
  `;
12983
13493
  function isDir(dir) {
12984
13494
  try {
12985
- return fs24.statSync(dir).isDirectory();
13495
+ return fs25.statSync(dir).isDirectory();
12986
13496
  } catch {
12987
13497
  return false;
12988
13498
  }
@@ -12990,32 +13500,32 @@ function isDir(dir) {
12990
13500
  function skillTargets(env) {
12991
13501
  const home = homeOf3(env);
12992
13502
  if (home === null) return [];
12993
- const claudeDir = env["CLAUDE_CONFIG_DIR"]?.trim() || path23.join(home, ".claude");
13503
+ const claudeDir = env["CLAUDE_CONFIG_DIR"]?.trim() || path24.join(home, ".claude");
12994
13504
  const targets = [
12995
- { file: path23.join(claudeDir, "skills", SKILL_NAME, "SKILL.md") },
12996
- { file: path23.join(home, ".agents", "skills", SKILL_NAME, "SKILL.md") }
13505
+ { file: path24.join(claudeDir, "skills", SKILL_NAME, "SKILL.md") },
13506
+ { file: path24.join(home, ".agents", "skills", SKILL_NAME, "SKILL.md") }
12997
13507
  ];
12998
- const gemini = path23.join(home, ".gemini");
12999
- if (["antigravity", "antigravity-cli", "config"].some((dir) => isDir(path23.join(gemini, dir)))) {
13000
- const plugin = path23.join(gemini, "config", "plugins", SKILL_NAME);
13508
+ const gemini = path24.join(home, ".gemini");
13509
+ if (["antigravity", "antigravity-cli", "config"].some((dir) => isDir(path24.join(gemini, dir)))) {
13510
+ const plugin = path24.join(gemini, "config", "plugins", SKILL_NAME);
13001
13511
  targets.push({
13002
- file: path23.join(plugin, "skills", SKILL_NAME, "SKILL.md"),
13003
- companion: { file: path23.join(plugin, "plugin.json"), text: ANTIGRAVITY_PLUGIN_MANIFEST }
13512
+ file: path24.join(plugin, "skills", SKILL_NAME, "SKILL.md"),
13513
+ companion: { file: path24.join(plugin, "plugin.json"), text: ANTIGRAVITY_PLUGIN_MANIFEST }
13004
13514
  });
13005
13515
  }
13006
- const opencode = path23.join(env["XDG_CONFIG_HOME"]?.trim() || path23.join(home, ".config"), "opencode");
13007
- if (isDir(opencode)) targets.push({ file: path23.join(opencode, "skills", SKILL_NAME, "SKILL.md") });
13516
+ const opencode = path24.join(env["XDG_CONFIG_HOME"]?.trim() || path24.join(home, ".config"), "opencode");
13517
+ if (isDir(opencode)) targets.push({ file: path24.join(opencode, "skills", SKILL_NAME, "SKILL.md") });
13008
13518
  return targets;
13009
13519
  }
13010
13520
  function removeIfEmpty(dir) {
13011
13521
  try {
13012
- fs24.rmdirSync(dir);
13522
+ fs25.rmdirSync(dir);
13013
13523
  } catch {
13014
13524
  }
13015
13525
  }
13016
13526
  function readOrNull(file) {
13017
13527
  try {
13018
- return fs24.readFileSync(file, "utf8");
13528
+ return fs25.readFileSync(file, "utf8");
13019
13529
  } catch {
13020
13530
  return null;
13021
13531
  }
@@ -13045,12 +13555,12 @@ function installSkill(options) {
13045
13555
  options.log(` skill would remove ${file}`);
13046
13556
  continue;
13047
13557
  }
13048
- fs24.rmSync(file);
13049
- removeIfEmpty(path23.dirname(file));
13558
+ fs25.rmSync(file);
13559
+ removeIfEmpty(path24.dirname(file));
13050
13560
  if (companion !== void 0) {
13051
- removeIfEmpty(path23.dirname(path23.dirname(file)));
13052
- if (readOrNull(companion.file) === companion.text) fs24.rmSync(companion.file);
13053
- removeIfEmpty(path23.dirname(companion.file));
13561
+ removeIfEmpty(path24.dirname(path24.dirname(file)));
13562
+ if (readOrNull(companion.file) === companion.text) fs25.rmSync(companion.file);
13563
+ removeIfEmpty(path24.dirname(companion.file));
13054
13564
  }
13055
13565
  options.log(` skill removed ${file}`);
13056
13566
  continue;
@@ -13063,9 +13573,9 @@ function installSkill(options) {
13063
13573
  options.log(` skill would ${current === null ? "write" : "update"} ${file}`);
13064
13574
  continue;
13065
13575
  }
13066
- fs24.mkdirSync(path23.dirname(file), { recursive: true });
13067
- if (companion !== void 0 && readOrNull(companion.file) === null) fs24.writeFileSync(companion.file, companion.text);
13068
- fs24.writeFileSync(file, text2);
13576
+ fs25.mkdirSync(path24.dirname(file), { recursive: true });
13577
+ if (companion !== void 0 && readOrNull(companion.file) === null) fs25.writeFileSync(companion.file, companion.text);
13578
+ fs25.writeFileSync(file, text2);
13069
13579
  options.log(` skill ${current === null ? "wrote" : "updated"} ${file}`);
13070
13580
  }
13071
13581
  return code;
@@ -13074,7 +13584,7 @@ function installSkill(options) {
13074
13584
  // src/setup.ts
13075
13585
  function isDir2(dir) {
13076
13586
  try {
13077
- return fs25.statSync(dir).isDirectory();
13587
+ return fs26.statSync(dir).isDirectory();
13078
13588
  } catch {
13079
13589
  return false;
13080
13590
  }
@@ -13090,17 +13600,17 @@ function otherAgents(env, step) {
13090
13600
  const entry = { command: step.execPath, args: [step.entryPath, "mcp"] };
13091
13601
  let code = 0;
13092
13602
  log("Codex");
13093
- const codexHome2 = env["CODEX_HOME"]?.trim() || path24.join(home, ".codex");
13603
+ const codexHome2 = env["CODEX_HOME"]?.trim() || path25.join(home, ".codex");
13094
13604
  if (!isDir2(codexHome2)) log(` skipped not installed here (no ${codexHome2})`);
13095
13605
  else code = Math.max(code, registerCodexMcp({ file: codexConfigPath(codexHome2), ...step }));
13096
13606
  if (isDir2(codexHome2) && !step.uninstall) log(" skill Codex reads the shared ~/.agents/skills copy above");
13097
13607
  log("");
13098
13608
  log("Cursor");
13099
- const cursorHome = path24.join(home, ".cursor");
13609
+ const cursorHome = path25.join(home, ".cursor");
13100
13610
  if (!isDir2(cursorHome)) log(` skipped not installed here (no ${cursorHome})`);
13101
13611
  else {
13102
13612
  code = Math.max(code, registerMcpJson({
13103
- file: path24.join(cursorHome, "mcp.json"),
13613
+ file: path25.join(cursorHome, "mcp.json"),
13104
13614
  entry,
13105
13615
  app: "Cursor",
13106
13616
  scope: "for every project",
@@ -13110,12 +13620,12 @@ function otherAgents(env, step) {
13110
13620
  }
13111
13621
  log("");
13112
13622
  log("Antigravity");
13113
- const gemini = path24.join(home, ".gemini");
13114
- if (!["antigravity", "antigravity-cli", "config"].some((dir) => isDir2(path24.join(gemini, dir)))) {
13115
- log(` skipped not installed here (no ${path24.join(gemini, "antigravity")})`);
13623
+ const gemini = path25.join(home, ".gemini");
13624
+ if (!["antigravity", "antigravity-cli", "config"].some((dir) => isDir2(path25.join(gemini, dir)))) {
13625
+ log(` skipped not installed here (no ${path25.join(gemini, "antigravity")})`);
13116
13626
  } else {
13117
13627
  code = Math.max(code, registerMcpJson({
13118
- file: path24.join(gemini, "config", "mcp_config.json"),
13628
+ file: path25.join(gemini, "config", "mcp_config.json"),
13119
13629
  entry,
13120
13630
  app: "Antigravity",
13121
13631
  scope: "for Antigravity",
@@ -13418,13 +13928,13 @@ async function bindFirst(server, ports) {
13418
13928
  }
13419
13929
 
13420
13930
  // src/index.ts
13421
- var PACKAGE_ROOT = path25.resolve(path25.dirname(fileURLToPath2(import.meta.url)), "..");
13422
- var SOURCE_CHECKOUT = fs26.existsSync(path25.join(PACKAGE_ROOT, "src", "index.ts"));
13423
- var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ? path25.join(PACKAGE_ROOT, ".tmp") : path25.join(os9.homedir(), ".flatbread"));
13424
- var OUTPUT_PATH = path25.join(TMP_DIR, "last-parse.json");
13425
- var SHARE_OUTPUT_PATH = path25.join(TMP_DIR, "last-share.json");
13931
+ var PACKAGE_ROOT = path26.resolve(path26.dirname(fileURLToPath2(import.meta.url)), "..");
13932
+ var SOURCE_CHECKOUT = fs27.existsSync(path26.join(PACKAGE_ROOT, "src", "index.ts"));
13933
+ var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ? path26.join(PACKAGE_ROOT, ".tmp") : path26.join(os9.homedir(), ".flatbread"));
13934
+ var OUTPUT_PATH = path26.join(TMP_DIR, "last-parse.json");
13935
+ var SHARE_OUTPUT_PATH = path26.join(TMP_DIR, "last-share.json");
13426
13936
  function storeDir() {
13427
- return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path25.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
13937
+ return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path26.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
13428
13938
  }
13429
13939
  var USAGE = `flatbread - registry of AI agent runs
13430
13940
 
@@ -13437,8 +13947,9 @@ Usage:
13437
13947
  flatbread pull <id|url> Print a run's Context Capsule: a run this machine shared or
13438
13948
  captured is read here, anything else from the site
13439
13949
  flatbread pull <id|url> --out <file> Write it to a file instead
13440
- flatbread mcp MCP server on stdio: flatbread_search_traces and
13441
- flatbread_get_capsule, for your coding agent
13950
+ flatbread mcp MCP server on stdio for your coding agent:
13951
+ flatbread_search_traces, flatbread_get_capsule, and
13952
+ flatbread_share ("share this run" in the chat)
13442
13953
  flatbread search <words> Find runs: this machine's shared and captured ones, then
13443
13954
  the site (the same answer the MCP server gives)
13444
13955
  flatbread setup Auto-capture: the Claude Code hook, plus a chained git
@@ -13474,6 +13985,8 @@ Options for share:
13474
13985
  opens with it and can still change it
13475
13986
  --commit [sha] Bind the run to a commit (bare --commit = HEAD) and record
13476
13987
  whether the working tree was clean against it
13988
+ --from <n|words> Share only the part from that prompt to the end: its
13989
+ number, or words from it. The review page can change it
13477
13990
  --no-review Redact every match, skip the browser (non-interactive)
13478
13991
  --no-open Start the review server but do not launch a browser
13479
13992
  -h, --help Show this message
@@ -13510,13 +14023,14 @@ Options for adapters:
13510
14023
  Notes:
13511
14024
  parse and share record the git branch, HEAD commit and uncommitted file names when the
13512
14025
  session ran inside a git repository. Nothing else from git is read.
13513
- parse --save writes ${path25.relative(process.cwd(), OUTPUT_PATH) || OUTPUT_PATH}
13514
- Share drafts are written to ${path25.relative(process.cwd(), SHARE_OUTPUT_PATH) || SHARE_OUTPUT_PATH}
14026
+ parse --save writes ${path26.relative(process.cwd(), OUTPUT_PATH) || OUTPUT_PATH}
14027
+ Share drafts are written to ${path26.relative(process.cwd(), SHARE_OUTPUT_PATH) || SHARE_OUTPUT_PATH}
13515
14028
  (the latest only; every shared run is also kept under its ID, for search and pull)
13516
14029
  Every parsed run defaults to visibility "private".
13517
- Nothing is uploaded unless you pass --publish, or set FLATBREAD_API_URL and
13518
- FLATBREAD_API_TOKEN yourself. pull and mcp only read: public runs, your own when you
13519
- are signed in, and a private run through its share link. The review UI listens on
14030
+ Nothing is uploaded unless you pass --publish, set FLATBREAD_API_URL and
14031
+ FLATBREAD_API_TOKEN yourself, or press Confirm on a review your agent opened with
14032
+ flatbread_share. pull and search only read: public runs, your own when you are signed
14033
+ in, and a private run through its share link. The review UI listens on
13520
14034
  127.0.0.1 only, on 4321-4325 or, when all five are busy, a port the OS picks.
13521
14035
  setup registers the MCP server in ~/.claude.json, for every project. By hand instead:
13522
14036
  claude mcp add --scope user flatbread -- flatbread mcp
@@ -13548,10 +14062,10 @@ function parseArgv(argv) {
13548
14062
  } else if (arg === "--out") {
13549
14063
  const value = argv[i + 1];
13550
14064
  if (value === void 0 || value.startsWith("--")) throw new Error("--out needs a file path");
13551
- args.out = path25.resolve(value);
14065
+ args.out = path26.resolve(value);
13552
14066
  i += 1;
13553
14067
  } else if (arg !== void 0 && arg.startsWith("--out=")) {
13554
- args.out = path25.resolve(arg.slice("--out=".length));
14068
+ args.out = path26.resolve(arg.slice("--out=".length));
13555
14069
  } else if (arg === "--harness") {
13556
14070
  const value = argv[i + 1];
13557
14071
  if (value === void 0 || value.startsWith("--")) {
@@ -13672,12 +14186,12 @@ async function runParse(args) {
13672
14186
  target = found[0];
13673
14187
  console.log(`latest ${harness} session: ${target}`);
13674
14188
  } else if (args.file) {
13675
- target = path25.resolve(args.file);
13676
- if (!fs26.existsSync(target)) {
14189
+ target = path26.resolve(args.file);
14190
+ if (!fs27.existsSync(target)) {
13677
14191
  console.error(`file not found: ${target}`);
13678
14192
  return 1;
13679
14193
  }
13680
- if (!fs26.statSync(target).isFile()) {
14194
+ if (!fs27.statSync(target).isFile()) {
13681
14195
  console.error(`not a file: ${target}`);
13682
14196
  return 1;
13683
14197
  }
@@ -13708,8 +14222,8 @@ async function runParse(args) {
13708
14222
  const trace = withLineage(result.trace, readGitLineage(result.trace.source.cwd));
13709
14223
  FlatbreadTraceSchema.parse(trace);
13710
14224
  if (args.out !== void 0) {
13711
- fs26.mkdirSync(path25.dirname(args.out), { recursive: true });
13712
- fs26.writeFileSync(args.out, JSON.stringify(trace, null, 2) + "\n", "utf8");
14225
+ fs27.mkdirSync(path26.dirname(args.out), { recursive: true });
14226
+ fs27.writeFileSync(args.out, JSON.stringify(trace, null, 2) + "\n", "utf8");
13713
14227
  }
13714
14228
  report({ ...result, trace }, args.out);
13715
14229
  return 0;
@@ -13773,8 +14287,8 @@ async function runAdapters(args) {
13773
14287
  }
13774
14288
  let input = null;
13775
14289
  if (args.file !== void 0) {
13776
- const resolved = path25.resolve(args.file);
13777
- if (!fs26.existsSync(resolved)) {
14290
+ const resolved = path26.resolve(args.file);
14291
+ if (!fs27.existsSync(resolved)) {
13778
14292
  console.error(`file not found: ${resolved}`);
13779
14293
  return 1;
13780
14294
  }
@@ -13842,6 +14356,12 @@ function parseShareArgv(argv) {
13842
14356
  }
13843
14357
  } else if (arg !== void 0 && arg.startsWith("--commit=")) {
13844
14358
  args.commit = checkCommitRev(arg.slice("--commit=".length));
14359
+ } else if (arg === "--from" || arg !== void 0 && arg.startsWith("--from=")) {
14360
+ const value = arg === "--from" ? argv[i += 1] : arg.slice("--from=".length);
14361
+ if (value === void 0 || value.startsWith("--") || value.trim().length === 0) {
14362
+ throw new Error('--from needs a prompt number or words from that prompt, e.g. --from 3 or --from "retry helper"');
14363
+ }
14364
+ args.from = value;
13845
14365
  } else if (arg === "--file") {
13846
14366
  const value = argv[i + 1];
13847
14367
  if (value === void 0 || value.startsWith("--")) {
@@ -14208,9 +14728,9 @@ async function runPull(args, deps = { ...defaultMcpDeps(storeDir(), bucketDir())
14208
14728
  process.stdout.write(result.capsule);
14209
14729
  return 0;
14210
14730
  }
14211
- const target = path25.resolve(args.out);
14212
- fs26.mkdirSync(path25.dirname(target), { recursive: true });
14213
- fs26.writeFileSync(target, result.capsule, "utf8");
14731
+ const target = path26.resolve(args.out);
14732
+ fs27.mkdirSync(path26.dirname(target), { recursive: true });
14733
+ fs27.writeFileSync(target, result.capsule, "utf8");
14214
14734
  console.error(`wrote ${target}${result.from === "site" ? "" : ` (from this machine's ${result.from === "store" ? "shared runs" : "captured runs"})`}`);
14215
14735
  return 0;
14216
14736
  }
@@ -14403,7 +14923,7 @@ async function main(argv) {
14403
14923
  console.error(`mcp takes no options, got: ${rest.join(" ")}`);
14404
14924
  return 1;
14405
14925
  }
14406
- return runMcpServer(defaultMcpDeps(storeDir(), bucketDir()));
14926
+ return runMcpServer(defaultMcpDeps(storeDir(), bucketDir(), SHARE_OUTPUT_PATH));
14407
14927
  }
14408
14928
  if (command === "pull") {
14409
14929
  let pullArgs;
@@ -14438,6 +14958,7 @@ async function main(argv) {
14438
14958
  ...shareArgs.prUrl !== void 0 ? { prUrl: shareArgs.prUrl } : {},
14439
14959
  ...shareArgs.title !== void 0 ? { title: shareArgs.title } : {},
14440
14960
  ...shareArgs.commit !== void 0 ? { commit: shareArgs.commit } : {},
14961
+ ...shareArgs.from !== void 0 ? { from: shareArgs.from } : {},
14441
14962
  ...storeDir() !== null ? { storeDir: storeDir() } : {},
14442
14963
  openBrowser: shareArgs.open && !shareArgs.noReview,
14443
14964
  outputPath: SHARE_OUTPUT_PATH
@@ -14458,9 +14979,9 @@ var invokedDirectly = (() => {
14458
14979
  if (entry === void 0) return false;
14459
14980
  const real = (file) => {
14460
14981
  try {
14461
- return fs26.realpathSync(file);
14982
+ return fs27.realpathSync(file);
14462
14983
  } catch {
14463
- return path25.resolve(file);
14984
+ return path26.resolve(file);
14464
14985
  }
14465
14986
  };
14466
14987
  return real(entry) === real(fileURLToPath2(import.meta.url));