flatbread-cli 0.4.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.
Files changed (3) hide show
  1. package/README.md +75 -11
  2. package/dist/flatbread.mjs +1254 -292
  3. package/package.json +1 -1
@@ -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 fs24 from "node:fs";
10
+ import fs27 from "node:fs";
11
11
  import os9 from "node:os";
12
- import path23 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: path24, errorMaps, issueData } = params;
494
- const fullPath = [...path24, ...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, path24, key) {
610
+ constructor(parent, value, path27, key) {
611
611
  this._cachedPath = [];
612
612
  this.parent = parent;
613
613
  this.data = value;
614
- this._path = path24;
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 path24 = firstString(tool.input, FILE_PATH_KEYS);
4465
- if (path24 !== void 0) return `${tool.name} ${repoRelative(path24, 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 [path24, 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: path24, 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 path24 = repoRelative(firstString(step.tool.input, FILE_PATH_KEYS), cwd);
4799
- const file = files.get(path24) ?? { path: path24 };
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: path24, 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(path24, 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((path24) => matchesAny(path24, 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((path24) => matchesAny(path24, 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((path24) => matchesAny(path24, 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,7 +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);
11239
- if (query.onlyWithRecoveries) params.set("recoveries", "1");
11520
+ params.set("partial", "1");
11240
11521
  const answer = await get(`${apiUrl}/api/search?${params.toString()}`, headersFor(apiUrl, options, "application/json"), options);
11241
11522
  if (!answer.ok) return answer;
11242
11523
  if (answer.status !== 200) return failureOf(answer.status, answer.body);
@@ -11249,7 +11530,16 @@ async function searchRemote(query, options = {}) {
11249
11530
  const results = parsed.results;
11250
11531
  if (!Array.isArray(results)) return { ok: false, reason: "http", status: 200, detail: "the search answer had no results list" };
11251
11532
  if (!results.every(isHit)) return { ok: false, reason: "http", status: 200, detail: "a search result was not readable" };
11252
- 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";
11253
11543
  }
11254
11544
  function isHit(value) {
11255
11545
  if (typeof value !== "object" || value === null) return false;
@@ -11274,16 +11564,141 @@ function describeReadFailure(failure, what) {
11274
11564
  // src/mcp.ts
11275
11565
  import readline3 from "node:readline";
11276
11566
 
11277
- // src/bucket.ts
11278
- import { createHash as createHash2 } from "node:crypto";
11567
+ // src/caller-session.ts
11279
11568
  import fs19 from "node:fs";
11280
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";
11281
11696
  var BUCKET_INDEX_VERSION = 1;
11282
11697
  var BUCKET_INDEX_FILE = "index.json";
11283
11698
  var CAPTURE_LOG_FILE = "capture.log";
11284
11699
  var COMMIT_SESSION_WINDOW_MS = 2 * 60 * 60 * 1e3;
11285
11700
  function bucketDir(env = process.env) {
11286
- return path18.join(flatbreadHome(env), "bucket");
11701
+ return path19.join(flatbreadHome(env), "bucket");
11287
11702
  }
11288
11703
  function bucketSlug(harness, sessionKey) {
11289
11704
  return `local-${createHash2("sha256").update(`${harness}\0${sessionKey}`, "utf8").digest("hex").slice(0, 12)}`;
@@ -11298,18 +11713,18 @@ function rowOf(record) {
11298
11713
  };
11299
11714
  }
11300
11715
  function recordStems(dir) {
11301
- 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();
11302
11717
  }
11303
11718
  function writeAtomic(file, body) {
11304
- fs19.mkdirSync(path18.dirname(file), { recursive: true });
11719
+ fs20.mkdirSync(path19.dirname(file), { recursive: true });
11305
11720
  const temp = `${file}.${process.pid}.tmp`;
11306
- fs19.writeFileSync(temp, body, "utf8");
11307
- fs19.renameSync(temp, file);
11721
+ fs20.writeFileSync(temp, body, "utf8");
11722
+ fs20.renameSync(temp, file);
11308
11723
  }
11309
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);
11310
11725
  function readIndex(dir) {
11311
11726
  try {
11312
- 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"));
11313
11728
  if (typeof parsed !== "object" || parsed === null) return null;
11314
11729
  const { version, rows } = parsed;
11315
11730
  if (version !== BUCKET_INDEX_VERSION || !Array.isArray(rows)) return null;
@@ -11328,10 +11743,10 @@ function readIndex(dir) {
11328
11743
  }
11329
11744
  }
11330
11745
  function writeIndex(dir, rows) {
11331
- 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");
11332
11747
  }
11333
11748
  function readBucket(dir) {
11334
- if (!fs19.existsSync(dir)) return { rows: [], rebuilt: false, unreadable: 0 };
11749
+ if (!fs20.existsSync(dir)) return { rows: [], rebuilt: false, unreadable: 0 };
11335
11750
  const stems = recordStems(dir);
11336
11751
  const indexed = readIndex(dir);
11337
11752
  if (indexed !== null && JSON.stringify(indexed.map((row) => row.summary.slug).sort()) === JSON.stringify(stems)) {
@@ -11348,17 +11763,18 @@ function readBucket(dir) {
11348
11763
  function readBucketRecord(dir, slug) {
11349
11764
  if (!SlugSchema.safeParse(slug).success) return null;
11350
11765
  try {
11351
- 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")));
11352
11767
  return parsed.success ? parsed.data : null;
11353
11768
  } catch {
11354
11769
  return null;
11355
11770
  }
11356
11771
  }
11357
11772
  function matchesQuery(summary, query) {
11773
+ return matchesFilters(summary, query) && matchesSearch(summary, query.query ?? "");
11774
+ }
11775
+ function matchesFilters(summary, query) {
11358
11776
  if (query.harness !== void 0 && summary.harness !== query.harness) return false;
11359
- if (query.category !== void 0 && summary.category !== query.category) return false;
11360
- if (query.onlyWithRecoveries === true && summary.failuresRecovered === 0) return false;
11361
- return matchesSearch(summary, query.query ?? "");
11777
+ return query.category === void 0 || summary.category === query.category;
11362
11778
  }
11363
11779
  async function captureToBucket(options) {
11364
11780
  const dir = options.dir ?? bucketDir(options.env);
@@ -11377,9 +11793,9 @@ async function captureToBucket(options) {
11377
11793
  const trace = withLineage(parsed.trace, lineage);
11378
11794
  const scan = scanTrace(trace);
11379
11795
  const applied = applyRedactions(trace, scan.matches, { keepIds: [] });
11380
- 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);
11381
11797
  const slug = bucketSlug(parsed.harness, sessionKey);
11382
- const recordPath = path18.join(dir, `${slug}.json`);
11798
+ const recordPath = path19.join(dir, `${slug}.json`);
11383
11799
  const previous = readBucketRecord(dir, slug);
11384
11800
  const commit = applied.trace.source.commitNamed === true ? applied.trace.source.headCommitSha : void 0;
11385
11801
  const draft = ShareDraftSchema.parse({
@@ -11401,7 +11817,7 @@ async function captureToBucket(options) {
11401
11817
  }
11402
11818
  function sessionForRepo(repo, options = {}) {
11403
11819
  const projects = getProjectsDir(options.env ?? process.env);
11404
- const folder = path18.join(projects, encodeCwd(path18.resolve(repo)));
11820
+ const folder = path19.join(projects, encodeCwd(path19.resolve(repo)));
11405
11821
  const newest = listSessionFiles(folder)[0];
11406
11822
  if (newest === void 0) return { problem: `no Claude Code session recorded for ${repo}` };
11407
11823
  const age = (options.nowMs ?? Date.now()) - newest.mtimeMs;
@@ -11413,8 +11829,8 @@ function sessionForRepo(repo, options = {}) {
11413
11829
  }
11414
11830
  function logCapture(dir, line, now = (/* @__PURE__ */ new Date()).toISOString()) {
11415
11831
  try {
11416
- fs19.mkdirSync(dir, { recursive: true });
11417
- 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, " ")}
11418
11834
  `, "utf8");
11419
11835
  } catch {
11420
11836
  }
@@ -11422,13 +11838,14 @@ function logCapture(dir, line, now = (/* @__PURE__ */ new Date()).toISOString())
11422
11838
 
11423
11839
  // src/mcp.ts
11424
11840
  var MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
11425
- var MCP_SERVER_VERSION = "0.4.0";
11426
- function defaultMcpDeps(storeDir2, bucketDir2) {
11841
+ var MCP_SERVER_VERSION = "0.7.0";
11842
+ function defaultMcpDeps(storeDir2, bucketDir2, shareOutputPath) {
11427
11843
  return {
11428
11844
  storeDir: storeDir2,
11429
11845
  bucketDir: bucketDir2,
11430
11846
  search: (query) => searchRemote(query, { client: "mcp" }),
11431
- 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 }) } : {}
11432
11849
  };
11433
11850
  }
11434
11851
  var MCP_TOOLS = [
@@ -11439,10 +11856,9 @@ var MCP_TOOLS = [
11439
11856
  inputSchema: {
11440
11857
  type: "object",
11441
11858
  properties: {
11442
- 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" },
11443
11860
  harness: { type: "string", enum: [...HARNESS_IDS], description: "Only runs from this agent harness" },
11444
- category: { type: "string", enum: [...TASK_CATEGORIES], description: "Only runs of this kind of job" },
11445
- onlyWithRecoveries: { type: "boolean", description: "Only runs where the agent recovered from a failure" }
11861
+ category: { type: "string", enum: [...TASK_CATEGORIES], description: "Only runs of this kind of job" }
11446
11862
  },
11447
11863
  additionalProperties: false
11448
11864
  }
@@ -11457,6 +11873,20 @@ var MCP_TOOLS = [
11457
11873
  required: ["slug"],
11458
11874
  additionalProperties: false
11459
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
+ }
11460
11890
  }
11461
11891
  ];
11462
11892
  function text(body, isError = false) {
@@ -11489,10 +11919,7 @@ function readQuery(args) {
11489
11919
  }
11490
11920
  query.category = args["category"];
11491
11921
  }
11492
- if (args["onlyWithRecoveries"] !== void 0) {
11493
- if (typeof args["onlyWithRecoveries"] !== "boolean") return "onlyWithRecoveries must be true or false";
11494
- query.onlyWithRecoveries = args["onlyWithRecoveries"];
11495
- }
11922
+ if (args["onlyWithRecoveries"] !== void 0) return "onlyWithRecoveries was removed: search by words, harness and category";
11496
11923
  return query;
11497
11924
  }
11498
11925
  function localRecords(deps) {
@@ -11506,6 +11933,8 @@ async function searchEverywhere(query, deps, options = {}) {
11506
11933
  const out = [];
11507
11934
  const local = localRecords(deps);
11508
11935
  const localHits = local.records.map((record) => summarize(record)).filter((summary) => matchesQuery(summary, query));
11936
+ let found = localHits.length;
11937
+ const nearby = [];
11509
11938
  if (deps.storeDir !== null) {
11510
11939
  out.push(`This machine (${runs(localHits.length)}):`);
11511
11940
  for (const hit of localHits) out.push(hitLine(hit));
@@ -11516,9 +11945,23 @@ async function searchEverywhere(query, deps, options = {}) {
11516
11945
  const { harness, sessionId } = record.trace.source;
11517
11946
  if (sessionId.length > 0 && localHits.some((hit) => hit.slug === record.slug)) seen.add(bucketSlug(harness, sessionId));
11518
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
+ }
11519
11956
  if (deps.bucketDir !== null) {
11520
11957
  const bucket = readBucket(deps.bucketDir);
11521
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
+ }
11522
11965
  if (out.length > 0) out.push("");
11523
11966
  out.push(`Captured on this machine, not shared (${runs(captured.length)}):`);
11524
11967
  for (const row of captured) {
@@ -11527,21 +11970,31 @@ async function searchEverywhere(query, deps, options = {}) {
11527
11970
  }
11528
11971
  if (bucket.unreadable > 0) out.push(`(${bucket.unreadable} captured run file${bucket.unreadable === 1 ? "" : "s"} could not be read)`);
11529
11972
  }
11973
+ let siteSearched = false;
11530
11974
  if (options.remote === false) {
11531
11975
  if (out.length > 0) out.push("");
11532
11976
  out.push("The site was not searched: --local");
11533
- return { lines: out, siteSearched: false };
11534
- }
11535
- const remote = await deps.search(query);
11536
- if (out.length > 0) out.push("");
11537
- if (remote.ok) {
11538
- const fresh = remote.results.filter((hit) => !seen.has(hit.slug));
11539
- out.push(`${remote.site} (${runs(fresh.length)}):`);
11540
- for (const hit of fresh) out.push(hitLine(hit));
11541
11977
  } else {
11542
- 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
+ }
11992
+ }
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);
11543
11996
  }
11544
- return { lines: out, siteSearched: remote.ok };
11997
+ return { lines: out, siteSearched };
11545
11998
  }
11546
11999
  async function searchTool(args, deps) {
11547
12000
  const query = readQuery(args);
@@ -11559,6 +12012,50 @@ async function readCapsule(ref, deps) {
11559
12012
  const remote = await deps.capsule(ref);
11560
12013
  return remote.ok ? { ok: true, capsule: remote.capsule, from: "site" } : remote;
11561
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
+ }
11562
12059
  async function capsuleTool(args, deps) {
11563
12060
  const raw = args["slug"];
11564
12061
  if (typeof raw !== "string") return text("slug must be a run ID or a /r/<id> link", true);
@@ -11590,7 +12087,7 @@ async function handleMcpMessage(message, deps) {
11590
12087
  protocolVersion,
11591
12088
  capabilities: { tools: { listChanged: false } },
11592
12089
  serverInfo: { name: "flatbread", version: MCP_SERVER_VERSION },
11593
- 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."
11594
12091
  }
11595
12092
  };
11596
12093
  }
@@ -11604,6 +12101,7 @@ async function handleMcpMessage(message, deps) {
11604
12101
  try {
11605
12102
  if (name === "flatbread_search_traces") return { jsonrpc: "2.0", id, result: await searchTool(toolArgs, deps) };
11606
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) };
11607
12105
  } catch (error) {
11608
12106
  return { jsonrpc: "2.0", id, result: text(`flatbread failed: ${error instanceof Error ? error.message : String(error)}`, true) };
11609
12107
  }
@@ -11641,14 +12139,18 @@ async function runMcpServer(deps, io) {
11641
12139
  return 0;
11642
12140
  }
11643
12141
 
12142
+ // src/setup.ts
12143
+ import fs26 from "node:fs";
12144
+ import path25 from "node:path";
12145
+
11644
12146
  // src/claude-mcp.ts
11645
- import fs20 from "node:fs";
12147
+ import fs21 from "node:fs";
11646
12148
  import os8 from "node:os";
11647
- import path19 from "node:path";
12149
+ import path20 from "node:path";
11648
12150
  var MCP_SERVER_NAME = "flatbread";
11649
12151
  function claudeUserConfigPath(env = process.env) {
11650
12152
  const dir = env["CLAUDE_CONFIG_DIR"]?.trim();
11651
- return path19.join(dir ? dir : os8.homedir(), ".claude.json");
12153
+ return path20.join(dir ? dir : os8.homedir(), ".claude.json");
11652
12154
  }
11653
12155
  function mcpEntryFor(execPath, entryPath) {
11654
12156
  return { type: "stdio", command: execPath, args: [entryPath, "mcp"] };
@@ -11663,7 +12165,7 @@ var BOM = "\uFEFF";
11663
12165
  function readConfig(file) {
11664
12166
  let raw;
11665
12167
  try {
11666
- raw = fs20.readFileSync(file, "utf8");
12168
+ raw = fs21.readFileSync(file, "utf8");
11667
12169
  } catch (error) {
11668
12170
  if (error.code === "ENOENT") return { root: {}, raw: null };
11669
12171
  return { problem: error.message };
@@ -11690,28 +12192,38 @@ function serializeLike(raw, root) {
11690
12192
  function write(file, read2, next) {
11691
12193
  if (read2.raw !== null) {
11692
12194
  try {
11693
- fs20.accessSync(file, fs20.constants.W_OK);
12195
+ fs21.accessSync(file, fs21.constants.W_OK);
11694
12196
  } catch {
11695
12197
  return `${file} is read-only, so it was left as it was`;
11696
12198
  }
11697
- if (fs20.readFileSync(file, "utf8") !== read2.raw) return `${file} changed while setup ran (Claude Code writes it); run setup again`;
12199
+ if (fs21.readFileSync(file, "utf8") !== read2.raw) return `${file} changed while setup ran; run setup again`;
11698
12200
  const backup = `${file}.flatbread.bak`;
11699
- if (!fs20.existsSync(backup)) fs20.copyFileSync(file, backup);
12201
+ if (!fs21.existsSync(backup)) fs21.copyFileSync(file, backup);
11700
12202
  }
11701
- const mode = read2.raw !== null ? fs20.statSync(file).mode & 511 : 384;
11702
- 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 });
11703
12205
  const temp = `${file}.flatbread-${process.pid}.tmp`;
11704
- fs20.writeFileSync(temp, next, { encoding: "utf8", mode });
11705
- fs20.renameSync(temp, file);
12206
+ fs21.writeFileSync(temp, next, { encoding: "utf8", mode });
12207
+ fs21.renameSync(temp, file);
11706
12208
  return null;
11707
12209
  }
11708
12210
  function registerClaudeMcp(options) {
12211
+ return registerMcpJson({
12212
+ ...options,
12213
+ entry: { ...mcpEntryFor(options.execPath, options.entryPath) },
12214
+ app: "Claude Code",
12215
+ scope: "for every project (user scope)",
12216
+ restart: "restart Claude Code (or reload the VS Code window) so it starts the server",
12217
+ byHand: `claude mcp add --scope user ${MCP_SERVER_NAME} -- "${options.execPath}" "${options.entryPath}" mcp`
12218
+ });
12219
+ }
12220
+ function registerMcpJson(options) {
11709
12221
  const { file, log } = options;
11710
12222
  const read2 = readConfig(file);
11711
12223
  if ("problem" in read2) {
11712
12224
  log(` mcp refused: ${file}: ${read2.problem}`);
11713
- log(" Claude Code's own state lives there, so a file we cannot read is not one we rewrite.");
11714
- if (options.uninstall !== true) log(` by hand: claude mcp add --scope user ${MCP_SERVER_NAME} -- "${options.execPath}" "${options.entryPath}" mcp`);
12225
+ log(` ${options.app}'s own config lives there, so a file we cannot read is not one we rewrite.`);
12226
+ if (options.uninstall !== true && options.byHand !== void 0) log(` by hand: ${options.byHand}`);
11715
12227
  return 1;
11716
12228
  }
11717
12229
  const servers = isObject(read2.root["mcpServers"]) ? read2.root["mcpServers"] : void 0;
@@ -11738,16 +12250,16 @@ function registerClaudeMcp(options) {
11738
12250
  }
11739
12251
  log(` mcp ${file}: removed the ${MCP_SERVER_NAME} server`);
11740
12252
  const backup = `${file}.flatbread.bak`;
11741
- 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`);
11742
12254
  return 0;
11743
12255
  }
11744
- const entry = mcpEntryFor(options.execPath, options.entryPath);
12256
+ const entry = options.entry;
11745
12257
  if (current !== void 0 && !runsFlatbreadMcp(current)) {
11746
12258
  log(` mcp refused: ${file} already has a server named ${MCP_SERVER_NAME} that does not run flatbread, left alone`);
11747
12259
  return 1;
11748
12260
  }
11749
12261
  if (JSON.stringify(current) === JSON.stringify(entry)) {
11750
- log(` mcp ${file}: registered for every project, unchanged`);
12262
+ log(` mcp ${file}: registered ${options.scope}, unchanged`);
11751
12263
  return 0;
11752
12264
  }
11753
12265
  const verb = current === void 0 ? "register" : "update";
@@ -11761,14 +12273,367 @@ function registerClaudeMcp(options) {
11761
12273
  log(` mcp refused: ${problem}`);
11762
12274
  return 1;
11763
12275
  }
11764
- log(` mcp ${file}: ${verb === "register" ? "registered" : "updated"} for every project (user scope)`);
11765
- log(" restart Claude Code (or reload the VS Code window) so it starts the server");
12276
+ log(` mcp ${file}: ${verb === "register" ? "registered" : "updated"} ${options.scope}`);
12277
+ log(` ${options.restart}`);
12278
+ return 0;
12279
+ }
12280
+
12281
+ // src/codex-mcp.ts
12282
+ import fs22 from "node:fs";
12283
+ import path21 from "node:path";
12284
+ var BOM2 = "\uFEFF";
12285
+ var OWNED_KEYS = /* @__PURE__ */ new Set(["type", "command", "args"]);
12286
+ function codexConfigPath(codexHome2) {
12287
+ return path21.join(codexHome2, "config.toml");
12288
+ }
12289
+ function quote(value) {
12290
+ return JSON.stringify(value);
12291
+ }
12292
+ function codexBlock(execPath, entryPath) {
12293
+ return `[mcp_servers.${MCP_SERVER_NAME}]
12294
+ type = "stdio"
12295
+ command = ${quote(execPath)}
12296
+ args = [${[entryPath, "mcp"].map(quote).join(", ")}]
12297
+ `;
12298
+ }
12299
+ function tomlCode(line) {
12300
+ let out = "";
12301
+ let inQuote = "";
12302
+ let escaped = false;
12303
+ for (const c of line) {
12304
+ if (inQuote !== "") {
12305
+ out += c;
12306
+ if (inQuote === '"' && escaped) escaped = false;
12307
+ else if (inQuote === '"' && c === "\\") escaped = true;
12308
+ else if (c === inQuote) inQuote = "";
12309
+ continue;
12310
+ }
12311
+ if (c === "#") break;
12312
+ if (c === '"' || c === "'") inQuote = c;
12313
+ out += c;
12314
+ }
12315
+ return out.trim();
12316
+ }
12317
+ function openAfter(line, open) {
12318
+ let i = 0;
12319
+ while (i < line.length) {
12320
+ if (open === '"""') {
12321
+ if (line[i] === "\\") {
12322
+ i += 2;
12323
+ continue;
12324
+ }
12325
+ if (line.startsWith('"""', i)) {
12326
+ i += 3;
12327
+ while (line[i] === '"') i++;
12328
+ open = "";
12329
+ continue;
12330
+ }
12331
+ i++;
12332
+ } else if (open === "'''") {
12333
+ const j = line.indexOf("'''", i);
12334
+ if (j < 0) return open;
12335
+ i = j + 3;
12336
+ while (line[i] === "'") i++;
12337
+ open = "";
12338
+ } else if (line.startsWith('"""', i)) {
12339
+ open = '"""';
12340
+ i += 3;
12341
+ } else if (line.startsWith("'''", i)) {
12342
+ open = "'''";
12343
+ i += 3;
12344
+ } else if (line[i] === "#") {
12345
+ return "";
12346
+ } else if (line[i] === '"') {
12347
+ i++;
12348
+ while (i < line.length && line[i] !== '"') i += line[i] === "\\" ? 2 : 1;
12349
+ i++;
12350
+ } else if (line[i] === "'") {
12351
+ const j = line.indexOf("'", i + 1);
12352
+ if (j < 0) return "";
12353
+ i = j + 1;
12354
+ } else {
12355
+ i++;
12356
+ }
12357
+ }
12358
+ return open;
12359
+ }
12360
+ function tomlCodeLines(text2) {
12361
+ let open = "";
12362
+ return text2.split("\n").map((line) => {
12363
+ const was = open;
12364
+ open = openAfter(line, open);
12365
+ return was !== "" ? "" : line;
12366
+ });
12367
+ }
12368
+ function tomlKeyPath(key) {
12369
+ const parts = [];
12370
+ let part = "";
12371
+ let inQuote = "";
12372
+ for (const c of key) {
12373
+ if (inQuote !== "") {
12374
+ part += c;
12375
+ if (c === inQuote) inQuote = "";
12376
+ continue;
12377
+ }
12378
+ if (c === '"' || c === "'") {
12379
+ inQuote = c;
12380
+ part += c;
12381
+ } else if (c === ".") {
12382
+ parts.push(part);
12383
+ part = "";
12384
+ } else {
12385
+ part += c;
12386
+ }
12387
+ }
12388
+ parts.push(part);
12389
+ return parts.map((p) => {
12390
+ const t = p.trim();
12391
+ if (t.length >= 2 && (t[0] === '"' || t[0] === "'") && t[t.length - 1] === t[0]) {
12392
+ if (t[0] === "'") return t.slice(1, -1);
12393
+ try {
12394
+ return JSON.parse(t);
12395
+ } catch {
12396
+ return t;
12397
+ }
12398
+ }
12399
+ return t;
12400
+ });
12401
+ }
12402
+ function startsWithPath(full, prefix) {
12403
+ return prefix.length <= full.length && prefix.every((part, i) => full[i] === part);
12404
+ }
12405
+ function headerPath(line) {
12406
+ const code = tomlCode(line);
12407
+ if (!code.startsWith("[") || code.startsWith("[[") || !code.endsWith("]")) return null;
12408
+ return tomlKeyPath(code.slice(1, -1));
12409
+ }
12410
+ function arrayDelta(text2) {
12411
+ let depth = 0;
12412
+ let inQuote = "";
12413
+ let escaped = false;
12414
+ for (const c of text2) {
12415
+ if (inQuote !== "") {
12416
+ if (inQuote === '"' && escaped) escaped = false;
12417
+ else if (inQuote === '"' && c === "\\") escaped = true;
12418
+ else if (c === inQuote) inQuote = "";
12419
+ continue;
12420
+ }
12421
+ if (c === '"' || c === "'") inQuote = c;
12422
+ else if (c === "[") depth++;
12423
+ else if (c === "]") depth--;
12424
+ }
12425
+ return depth;
12426
+ }
12427
+ function stringValues(text2) {
12428
+ const out = [];
12429
+ const re = /"((?:[^"\\]|\\.)*)"|'([^']*)'/g;
12430
+ for (const match of text2.matchAll(re)) {
12431
+ if (match[2] !== void 0) out.push(match[2]);
12432
+ else {
12433
+ try {
12434
+ out.push(JSON.parse(`"${match[1] ?? ""}"`));
12435
+ } catch {
12436
+ }
12437
+ }
12438
+ }
12439
+ return out;
12440
+ }
12441
+ var OURS = ["mcp_servers", MCP_SERVER_NAME];
12442
+ function ourBlocks(code) {
12443
+ const all = [];
12444
+ let main2 = null;
12445
+ code.forEach((line, i) => {
12446
+ const header = headerPath(line);
12447
+ if (header === null || !startsWithPath(header, OURS)) return;
12448
+ let end = code.length;
12449
+ for (let j = i + 1; j < code.length; j++) {
12450
+ if (tomlCode(code[j] ?? "").startsWith("[")) {
12451
+ end = j;
12452
+ break;
12453
+ }
12454
+ }
12455
+ const block = { start: i, end };
12456
+ all.push(block);
12457
+ if (header.length === OURS.length && main2 === null) main2 = block;
12458
+ });
12459
+ return { main: main2, all };
12460
+ }
12461
+ function blockRunsFlatbread(code, block) {
12462
+ const words = [];
12463
+ for (let i = block.start + 1; i < block.end; i++) {
12464
+ const line = tomlCode(code[i] ?? "");
12465
+ const at = line.indexOf("=");
12466
+ if (at < 0) continue;
12467
+ const key = line.slice(0, at).trim();
12468
+ let value = line.slice(at + 1).trim();
12469
+ if (key === "command") words.unshift(...stringValues(value));
12470
+ if (key === "args") {
12471
+ let depth = arrayDelta(value);
12472
+ while (depth > 0 && i + 1 < block.end) {
12473
+ i++;
12474
+ const next = tomlCode(code[i] ?? "");
12475
+ value += `
12476
+ ${next}`;
12477
+ depth += arrayDelta(next);
12478
+ }
12479
+ words.push(...stringValues(value));
12480
+ }
12481
+ }
12482
+ return words[words.length - 1] === "mcp" && words.some((word) => /flatbread/i.test(word));
12483
+ }
12484
+ function tomlProblem(code) {
12485
+ let table = [];
12486
+ for (const [i, line] of code.entries()) {
12487
+ const text2 = tomlCode(line);
12488
+ if (text2.startsWith("[")) {
12489
+ if (!text2.endsWith("]")) return `line ${i + 1} is a table header that never closes: ${line.trim()}`;
12490
+ table = text2.startsWith("[[") ? tomlKeyPath(text2.slice(2, -2)) : tomlKeyPath(text2.slice(1, -1));
12491
+ continue;
12492
+ }
12493
+ const at = text2.indexOf("=");
12494
+ if (at < 0 || startsWithPath(table, OURS)) continue;
12495
+ const full = [...table, ...tomlKeyPath(text2.slice(0, at))];
12496
+ if (startsWithPath(OURS, full) || startsWithPath(full, OURS)) {
12497
+ return `line ${i + 1} sets ${full.join(".")} inline, and the [mcp_servers.${MCP_SERVER_NAME}] table would define it twice`;
12498
+ }
12499
+ }
12500
+ return null;
12501
+ }
12502
+ function writeLike(file, raw, lf) {
12503
+ let next = lf;
12504
+ if (raw !== null && raw.includes("\r\n")) next = next.replace(/\n/g, "\r\n");
12505
+ if (raw !== null && raw.startsWith(BOM2)) next = BOM2 + next;
12506
+ if (raw !== null) {
12507
+ try {
12508
+ fs22.accessSync(file, fs22.constants.W_OK);
12509
+ } catch {
12510
+ return `${file} is read-only, so it was left as it was`;
12511
+ }
12512
+ if (fs22.readFileSync(file, "utf8") !== raw) return `${file} changed while setup ran; run setup again`;
12513
+ const backup = `${file}.flatbread.bak`;
12514
+ if (!fs22.existsSync(backup)) fs22.copyFileSync(file, backup);
12515
+ }
12516
+ const mode = raw !== null ? fs22.statSync(file).mode & 511 : 384;
12517
+ fs22.mkdirSync(path21.dirname(file), { recursive: true });
12518
+ const temp = `${file}.flatbread-${process.pid}.tmp`;
12519
+ fs22.writeFileSync(temp, next, { encoding: "utf8", mode });
12520
+ fs22.renameSync(temp, file);
12521
+ return null;
12522
+ }
12523
+ function removeBlocks(lines2, blocks) {
12524
+ const drop = /* @__PURE__ */ new Set();
12525
+ for (const block of blocks) for (let i = block.start; i < block.end; i++) drop.add(i);
12526
+ return lines2.filter((_, i) => !drop.has(i));
12527
+ }
12528
+ function tidy(lines2) {
12529
+ const text2 = lines2.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "");
12530
+ return text2 === "" ? "" : `${text2}
12531
+ `;
12532
+ }
12533
+ function registerCodexMcp(options) {
12534
+ const { file, log } = options;
12535
+ let raw;
12536
+ try {
12537
+ raw = fs22.readFileSync(file, "utf8");
12538
+ } catch (error) {
12539
+ if (error.code !== "ENOENT") {
12540
+ log(` mcp refused: ${file}: ${error.message}`);
12541
+ return 1;
12542
+ }
12543
+ raw = null;
12544
+ }
12545
+ const text2 = (raw ?? "").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
12546
+ const lines2 = text2.split("\n");
12547
+ const code = tomlCodeLines(text2);
12548
+ const { main: main2, all } = ourBlocks(code);
12549
+ if (options.uninstall === true) {
12550
+ if (main2 === null) {
12551
+ log(` mcp no ${MCP_SERVER_NAME} server in ${file}`);
12552
+ return 0;
12553
+ }
12554
+ if (!blockRunsFlatbread(code, main2)) {
12555
+ log(` mcp ${file}: the ${MCP_SERVER_NAME} server there does not run flatbread, left alone`);
12556
+ return 0;
12557
+ }
12558
+ if (options.dryRun === true) {
12559
+ log(` mcp ${file}: would remove the ${MCP_SERVER_NAME} server`);
12560
+ return 0;
12561
+ }
12562
+ const problem2 = writeLike(file, raw, tidy(removeBlocks(lines2, all)));
12563
+ if (problem2 !== null) {
12564
+ log(` mcp refused: ${problem2}`);
12565
+ return 1;
12566
+ }
12567
+ log(` mcp ${file}: removed the ${MCP_SERVER_NAME} server`);
12568
+ return 0;
12569
+ }
12570
+ const problem = tomlProblem(code);
12571
+ if (problem !== null) {
12572
+ log(` mcp refused: ${file}: ${problem}`);
12573
+ log(" fix that line, or add this table by hand:");
12574
+ for (const line of codexBlock(options.execPath, options.entryPath).trimEnd().split("\n")) log(` ${line}`);
12575
+ return 1;
12576
+ }
12577
+ if (main2 !== null && !blockRunsFlatbread(code, main2)) {
12578
+ log(` mcp refused: ${file} already has a server named ${MCP_SERVER_NAME} that does not run flatbread, left alone`);
12579
+ return 1;
12580
+ }
12581
+ const block = codexBlock(options.execPath, options.entryPath);
12582
+ const ownLines = block.trimEnd().split("\n").slice(1);
12583
+ let next;
12584
+ if (main2 === null) {
12585
+ const kept = text2.replace(/\n+$/, "");
12586
+ next = `${kept}${kept === "" ? "" : "\n\n"}${block}`;
12587
+ } else {
12588
+ const body = [];
12589
+ let inserted = false;
12590
+ for (let i = main2.start + 1; i < main2.end; i++) {
12591
+ const lineCode = tomlCode(code[i] ?? "");
12592
+ const at = lineCode.indexOf("=");
12593
+ const key = at >= 0 ? lineCode.slice(0, at).trim() : "";
12594
+ if (!OWNED_KEYS.has(key)) {
12595
+ body.push(lines2[i] ?? "");
12596
+ continue;
12597
+ }
12598
+ if (!inserted) {
12599
+ body.push(...ownLines);
12600
+ inserted = true;
12601
+ }
12602
+ if (key === "args") {
12603
+ let depth = arrayDelta(lineCode.slice(at + 1));
12604
+ while (depth > 0 && i + 1 < main2.end) {
12605
+ i++;
12606
+ depth += arrayDelta(tomlCode(code[i] ?? ""));
12607
+ }
12608
+ }
12609
+ }
12610
+ if (!inserted) body.unshift(...ownLines);
12611
+ const out = [...lines2.slice(0, main2.start + 1), ...body, ...lines2.slice(main2.end)];
12612
+ next = out.join("\n");
12613
+ if (!next.endsWith("\n")) next += "\n";
12614
+ }
12615
+ if (next === text2) {
12616
+ log(` mcp ${file}: registered for Codex, unchanged`);
12617
+ return 0;
12618
+ }
12619
+ const verb = main2 === null ? "register" : "update";
12620
+ if (options.dryRun === true) {
12621
+ log(` mcp ${file}: would ${verb} [mcp_servers.${MCP_SERVER_NAME}]`);
12622
+ return 0;
12623
+ }
12624
+ const failed = writeLike(file, raw, next);
12625
+ if (failed !== null) {
12626
+ log(` mcp refused: ${failed}`);
12627
+ return 1;
12628
+ }
12629
+ log(` mcp ${file}: ${verb === "register" ? "registered" : "updated"} for Codex`);
12630
+ log(" start a new Codex session so it starts the server");
11766
12631
  return 0;
11767
12632
  }
11768
12633
 
11769
12634
  // src/git-hook.ts
11770
- import fs21 from "node:fs";
11771
- import path20 from "node:path";
12635
+ import fs23 from "node:fs";
12636
+ import path22 from "node:path";
11772
12637
  var POST_COMMIT_MARKER = "# flatbread post-commit hook - remove with: flatbread setup --uninstall";
11773
12638
  var CHAINED_HOOK = "post-commit.pre-flatbread";
11774
12639
  var sq = (value) => `'${value.replace(/\\/g, "/").replace(/'/g, "'\\''")}'`;
@@ -11812,25 +12677,25 @@ function hooksDirFor(cwd, options = {}) {
11812
12677
  }
11813
12678
  const common = git(top, ["rev-parse", "--git-common-dir"], options)?.trim();
11814
12679
  if (common === void 0 || common.length === 0) return { ok: false, reason: "unreadable", detail: `git could not name the git directory of ${top}` };
11815
- return { ok: true, repo: path20.resolve(top), dir: path20.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 } : {} };
11816
12681
  }
11817
12682
  var read = (file) => {
11818
12683
  try {
11819
- return fs21.readFileSync(file, "utf8");
12684
+ return fs23.readFileSync(file, "utf8");
11820
12685
  } catch {
11821
12686
  return null;
11822
12687
  }
11823
12688
  };
11824
12689
  function installGitHook(dir, script, options = {}) {
11825
- const hook = path20.join(dir, "post-commit");
11826
- const chained = path20.join(dir, CHAINED_HOOK);
12690
+ const hook = path22.join(dir, "post-commit");
12691
+ const chained = path22.join(dir, CHAINED_HOOK);
11827
12692
  const current = read(hook);
11828
12693
  const write2 = (lines2) => {
11829
12694
  if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
11830
- fs21.mkdirSync(dir, { recursive: true });
11831
- if (current !== null && !isOurs(current)) fs21.renameSync(hook, chained);
11832
- fs21.writeFileSync(hook, script, { encoding: "utf8", mode: 493 });
11833
- fs21.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);
11834
12699
  return { code: 0, lines: lines2 };
11835
12700
  };
11836
12701
  if (current === null) return write2([`post-commit ${hook} (new)`]);
@@ -11838,7 +12703,7 @@ function installGitHook(dir, script, options = {}) {
11838
12703
  if (current === script) return { code: 0, lines: [`post-commit ${hook}`, "already installed, unchanged"] };
11839
12704
  return write2([`post-commit ${hook}`, "updated the flatbread hook"]);
11840
12705
  }
11841
- if (fs21.existsSync(chained)) {
12706
+ if (fs23.existsSync(chained)) {
11842
12707
  return {
11843
12708
  code: 1,
11844
12709
  lines: [
@@ -11854,10 +12719,10 @@ function installGitHook(dir, script, options = {}) {
11854
12719
  ]);
11855
12720
  }
11856
12721
  function uninstallGitHook(dir, options = {}) {
11857
- const hook = path20.join(dir, "post-commit");
11858
- const chained = path20.join(dir, CHAINED_HOOK);
12722
+ const hook = path22.join(dir, "post-commit");
12723
+ const chained = path22.join(dir, CHAINED_HOOK);
11859
12724
  const current = read(hook);
11860
- const hasChained = fs21.existsSync(chained);
12725
+ const hasChained = fs23.existsSync(chained);
11861
12726
  const done = (lines2, act) => {
11862
12727
  if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
11863
12728
  act();
@@ -11875,17 +12740,17 @@ function uninstallGitHook(dir, options = {}) {
11875
12740
  if (current === null && !hasChained) return { code: 0, lines: [`no flatbread post-commit in ${dir}`] };
11876
12741
  if (hasChained) {
11877
12742
  return done([`post-commit ${hook}`, `restored your post-commit from ${CHAINED_HOOK}`], () => {
11878
- if (current !== null) fs21.rmSync(hook);
11879
- fs21.renameSync(chained, hook);
12743
+ if (current !== null) fs23.rmSync(hook);
12744
+ fs23.renameSync(chained, hook);
11880
12745
  });
11881
12746
  }
11882
- return done([`post-commit ${hook}`, "removed the flatbread hook"], () => fs21.rmSync(hook));
12747
+ return done([`post-commit ${hook}`, "removed the flatbread hook"], () => fs23.rmSync(hook));
11883
12748
  }
11884
12749
 
11885
12750
  // src/hook.ts
11886
12751
  import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
11887
- import fs22 from "node:fs";
11888
- import path21 from "node:path";
12752
+ import fs24 from "node:fs";
12753
+ import path23 from "node:path";
11889
12754
  var HOOK_EVENTS = ["Stop", "SessionEnd"];
11890
12755
  var OFFER_AFTER_STOPS = 2;
11891
12756
  var MAX_TRACKED_SESSIONS = 50;
@@ -11895,14 +12760,14 @@ function emptyHookState() {
11895
12760
  return { version: HOOK_STATE_VERSION, sessions: {} };
11896
12761
  }
11897
12762
  function hookStatePath(env = process.env) {
11898
- return path21.join(flatbreadHome(env), "hook-state.json");
12763
+ return path23.join(flatbreadHome(env), "hook-state.json");
11899
12764
  }
11900
12765
  function readHookState(env = process.env) {
11901
12766
  const file = hookStatePath(env);
11902
- if (!fs22.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
12767
+ if (!fs24.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
11903
12768
  let raw;
11904
12769
  try {
11905
- raw = fs22.readFileSync(file, "utf8");
12770
+ raw = fs24.readFileSync(file, "utf8");
11906
12771
  } catch (error) {
11907
12772
  return { ok: false, reason: "unreadable", detail: error.message };
11908
12773
  }
@@ -11945,9 +12810,9 @@ function pruneHookState(state, keep = MAX_TRACKED_SESSIONS) {
11945
12810
  }
11946
12811
  function writeHookState(state, env = process.env) {
11947
12812
  const dir = flatbreadHome(env);
11948
- fs22.mkdirSync(dir, { recursive: true, mode: 448 });
12813
+ fs24.mkdirSync(dir, { recursive: true, mode: 448 });
11949
12814
  const file = hookStatePath(env);
11950
- fs22.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
12815
+ fs24.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
11951
12816
  encoding: "utf8",
11952
12817
  mode: 384
11953
12818
  });
@@ -12093,7 +12958,7 @@ async function runHookNotify(io) {
12093
12958
  const stdout = io.stdout ?? ((line) => process.stdout.write(line + "\n"));
12094
12959
  const stderr = io.stderr ?? ((line) => process.stderr.write(line + "\n"));
12095
12960
  const now = io.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
12096
- const exists = io.exists ?? ((filePath) => fs22.existsSync(filePath));
12961
+ const exists = io.exists ?? ((filePath) => fs24.existsSync(filePath));
12097
12962
  const execPath = io.execPath ?? process.execPath;
12098
12963
  const entryPath = io.entryPath ?? process.argv[1] ?? "";
12099
12964
  try {
@@ -12250,10 +13115,10 @@ function removeHookConfig(settings) {
12250
13115
  return { settings: root, removed };
12251
13116
  }
12252
13117
  function readSettingsFile(file) {
12253
- if (!fs22.existsSync(file)) return { present: false, file };
13118
+ if (!fs24.existsSync(file)) return { present: false, file };
12254
13119
  let raw;
12255
13120
  try {
12256
- raw = fs22.readFileSync(file, "utf8");
13121
+ raw = fs24.readFileSync(file, "utf8");
12257
13122
  } catch (error) {
12258
13123
  return { present: true, file, unreadable: error.message };
12259
13124
  }
@@ -12265,10 +13130,10 @@ function readSettingsFile(file) {
12265
13130
  }
12266
13131
  }
12267
13132
  function writeSettingsFile(file, settings) {
12268
- fs22.mkdirSync(path21.dirname(file), { recursive: true });
13133
+ fs24.mkdirSync(path23.dirname(file), { recursive: true });
12269
13134
  const temp = `${file}.flatbread-${process.pid}.tmp`;
12270
- fs22.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
12271
- fs22.renameSync(temp, file);
13135
+ fs24.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
13136
+ fs24.renameSync(temp, file);
12272
13137
  }
12273
13138
  function handlersFor(execPath, entryPath, capture = false) {
12274
13139
  return {
@@ -12426,8 +13291,8 @@ function runHookUninstall(options = {}) {
12426
13291
  function settingsCandidates(env, cwd) {
12427
13292
  return [
12428
13293
  { label: "user", file: claudeSettingsPath(env) },
12429
- { label: "project", file: path21.join(cwd, ".claude", "settings.json") },
12430
- { label: "local", file: path21.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") }
12431
13296
  ];
12432
13297
  }
12433
13298
  function installedEventsIn(parsed, exists) {
@@ -12457,7 +13322,7 @@ function runHookStatus(options = {}) {
12457
13322
  const env = options.env ?? process.env;
12458
13323
  const log = options.log ?? ((line) => console.log(line));
12459
13324
  const cwd = options.cwd ?? process.cwd();
12460
- const exists = options.exists ?? ((filePath) => fs22.existsSync(filePath));
13325
+ const exists = options.exists ?? ((filePath) => fs24.existsSync(filePath));
12461
13326
  const candidates = options.settingsPath === void 0 ? settingsCandidates(env, cwd) : [{ label: "settings", file: options.settingsPath }];
12462
13327
  log("");
12463
13328
  log("flatbread hook");
@@ -12578,21 +13443,27 @@ function probeHandler(handler, run = spawnSync2) {
12578
13443
  }
12579
13444
 
12580
13445
  // src/skill.ts
12581
- import fs23 from "node:fs";
12582
- import path22 from "node:path";
13446
+ import fs25 from "node:fs";
13447
+ import path24 from "node:path";
12583
13448
  var SKILL_NAME = "flatbread";
12584
13449
  var MARKER = "<!-- written by flatbread setup; flatbread setup --uninstall removes it -->";
12585
- 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".`;
12586
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.
12587
13452
 
12588
13453
  ## How
12589
13454
 
12590
- 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.
12591
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.
12592
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.
12593
13458
 
12594
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>\`.
12595
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
+
12596
13467
  ## Saying what you used
12597
13468
 
12598
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.
@@ -12617,28 +13488,57 @@ function homeOf3(env) {
12617
13488
  const home = (process.platform === "win32" ? env["USERPROFILE"] : env["HOME"])?.trim();
12618
13489
  return home ? home : null;
12619
13490
  }
12620
- function skillPaths(env = process.env) {
13491
+ var ANTIGRAVITY_PLUGIN_MANIFEST = `${JSON.stringify({ name: SKILL_NAME }, null, 2)}
13492
+ `;
13493
+ function isDir(dir) {
13494
+ try {
13495
+ return fs25.statSync(dir).isDirectory();
13496
+ } catch {
13497
+ return false;
13498
+ }
13499
+ }
13500
+ function skillTargets(env) {
12621
13501
  const home = homeOf3(env);
12622
13502
  if (home === null) return [];
12623
- const claudeDir = env["CLAUDE_CONFIG_DIR"]?.trim() || path22.join(home, ".claude");
12624
- return [path22.join(claudeDir, "skills", SKILL_NAME, "SKILL.md"), path22.join(home, ".agents", "skills", SKILL_NAME, "SKILL.md")];
13503
+ const claudeDir = env["CLAUDE_CONFIG_DIR"]?.trim() || path24.join(home, ".claude");
13504
+ const targets = [
13505
+ { file: path24.join(claudeDir, "skills", SKILL_NAME, "SKILL.md") },
13506
+ { file: path24.join(home, ".agents", "skills", SKILL_NAME, "SKILL.md") }
13507
+ ];
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);
13511
+ targets.push({
13512
+ file: path24.join(plugin, "skills", SKILL_NAME, "SKILL.md"),
13513
+ companion: { file: path24.join(plugin, "plugin.json"), text: ANTIGRAVITY_PLUGIN_MANIFEST }
13514
+ });
13515
+ }
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") });
13518
+ return targets;
13519
+ }
13520
+ function removeIfEmpty(dir) {
13521
+ try {
13522
+ fs25.rmdirSync(dir);
13523
+ } catch {
13524
+ }
12625
13525
  }
12626
13526
  function readOrNull(file) {
12627
13527
  try {
12628
- return fs23.readFileSync(file, "utf8");
13528
+ return fs25.readFileSync(file, "utf8");
12629
13529
  } catch {
12630
13530
  return null;
12631
13531
  }
12632
13532
  }
12633
13533
  function installSkill(options) {
12634
13534
  const text2 = skillText();
12635
- const files = skillPaths(options.env);
12636
- if (files.length === 0) {
13535
+ const targets = skillTargets(options.env ?? process.env);
13536
+ if (targets.length === 0) {
12637
13537
  options.log(` skill skipped: no ${process.platform === "win32" ? "USERPROFILE" : "HOME"} in the environment`);
12638
13538
  return 1;
12639
13539
  }
12640
13540
  let code = 0;
12641
- for (const file of files) {
13541
+ for (const { file, companion } of targets) {
12642
13542
  const current = readOrNull(file);
12643
13543
  const ours = current !== null && current.includes(MARKER);
12644
13544
  if (current !== null && !ours) {
@@ -12655,10 +13555,12 @@ function installSkill(options) {
12655
13555
  options.log(` skill would remove ${file}`);
12656
13556
  continue;
12657
13557
  }
12658
- fs23.rmSync(file);
12659
- try {
12660
- fs23.rmdirSync(path22.dirname(file));
12661
- } catch {
13558
+ fs25.rmSync(file);
13559
+ removeIfEmpty(path24.dirname(file));
13560
+ if (companion !== void 0) {
13561
+ removeIfEmpty(path24.dirname(path24.dirname(file)));
13562
+ if (readOrNull(companion.file) === companion.text) fs25.rmSync(companion.file);
13563
+ removeIfEmpty(path24.dirname(companion.file));
12662
13564
  }
12663
13565
  options.log(` skill removed ${file}`);
12664
13566
  continue;
@@ -12671,14 +13573,68 @@ function installSkill(options) {
12671
13573
  options.log(` skill would ${current === null ? "write" : "update"} ${file}`);
12672
13574
  continue;
12673
13575
  }
12674
- fs23.mkdirSync(path22.dirname(file), { recursive: true });
12675
- fs23.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);
12676
13579
  options.log(` skill ${current === null ? "wrote" : "updated"} ${file}`);
12677
13580
  }
12678
13581
  return code;
12679
13582
  }
12680
13583
 
12681
13584
  // src/setup.ts
13585
+ function isDir2(dir) {
13586
+ try {
13587
+ return fs26.statSync(dir).isDirectory();
13588
+ } catch {
13589
+ return false;
13590
+ }
13591
+ }
13592
+ function otherAgents(env, step) {
13593
+ const { log } = step;
13594
+ const home = homeOf3(env);
13595
+ if (home === null) {
13596
+ log("Other agents");
13597
+ log(" skipped no home directory in the environment");
13598
+ return 1;
13599
+ }
13600
+ const entry = { command: step.execPath, args: [step.entryPath, "mcp"] };
13601
+ let code = 0;
13602
+ log("Codex");
13603
+ const codexHome2 = env["CODEX_HOME"]?.trim() || path25.join(home, ".codex");
13604
+ if (!isDir2(codexHome2)) log(` skipped not installed here (no ${codexHome2})`);
13605
+ else code = Math.max(code, registerCodexMcp({ file: codexConfigPath(codexHome2), ...step }));
13606
+ if (isDir2(codexHome2) && !step.uninstall) log(" skill Codex reads the shared ~/.agents/skills copy above");
13607
+ log("");
13608
+ log("Cursor");
13609
+ const cursorHome = path25.join(home, ".cursor");
13610
+ if (!isDir2(cursorHome)) log(` skipped not installed here (no ${cursorHome})`);
13611
+ else {
13612
+ code = Math.max(code, registerMcpJson({
13613
+ file: path25.join(cursorHome, "mcp.json"),
13614
+ entry,
13615
+ app: "Cursor",
13616
+ scope: "for every project",
13617
+ restart: "restart Cursor so it starts the server",
13618
+ ...step
13619
+ }));
13620
+ }
13621
+ log("");
13622
+ log("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")})`);
13626
+ } else {
13627
+ code = Math.max(code, registerMcpJson({
13628
+ file: path25.join(gemini, "config", "mcp_config.json"),
13629
+ entry,
13630
+ app: "Antigravity",
13631
+ scope: "for Antigravity",
13632
+ restart: "restart Antigravity so it starts the server",
13633
+ ...step
13634
+ }));
13635
+ }
13636
+ return code;
13637
+ }
12682
13638
  function runSetup(options = {}) {
12683
13639
  const env = options.env ?? process.env;
12684
13640
  const log = options.log ?? ((line) => console.log(line));
@@ -12710,20 +13666,18 @@ function runSetup(options = {}) {
12710
13666
  });
12711
13667
  let mcp = 0;
12712
13668
  let skill = 0;
13669
+ const uninstall = options.uninstall === true;
12713
13670
  if (options.noMcp === true) {
12714
- log(" --no-mcp: Claude Code's MCP servers and the flatbread skill were not touched");
12715
- } else if (claude === 0 || options.uninstall === true) {
12716
- mcp = registerClaudeMcp({
12717
- file: options.claudeConfigPath ?? claudeUserConfigPath(env),
12718
- execPath,
12719
- entryPath,
12720
- uninstall: options.uninstall === true,
12721
- dryRun,
12722
- log
12723
- });
12724
- skill = installSkill({ env, dryRun, uninstall: options.uninstall === true, log });
13671
+ log(" --no-mcp: no agent's MCP servers and no flatbread skill were touched");
12725
13672
  } else {
12726
- log(" mcp not registered, because the hook step above was refused");
13673
+ if (claude === 0 || uninstall) {
13674
+ mcp = registerClaudeMcp({ file: options.claudeConfigPath ?? claudeUserConfigPath(env), execPath, entryPath, uninstall, dryRun, log });
13675
+ } else {
13676
+ log(" mcp not registered, because the hook step above was refused");
13677
+ }
13678
+ skill = installSkill({ env, dryRun, uninstall, log });
13679
+ log("");
13680
+ mcp = Math.max(mcp, otherAgents(env, { execPath, entryPath, uninstall, dryRun, log }));
12727
13681
  }
12728
13682
  log("");
12729
13683
  let git2 = 0;
@@ -12974,13 +13928,13 @@ async function bindFirst(server, ports) {
12974
13928
  }
12975
13929
 
12976
13930
  // src/index.ts
12977
- var PACKAGE_ROOT = path23.resolve(path23.dirname(fileURLToPath2(import.meta.url)), "..");
12978
- var SOURCE_CHECKOUT = fs24.existsSync(path23.join(PACKAGE_ROOT, "src", "index.ts"));
12979
- var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ? path23.join(PACKAGE_ROOT, ".tmp") : path23.join(os9.homedir(), ".flatbread"));
12980
- var OUTPUT_PATH = path23.join(TMP_DIR, "last-parse.json");
12981
- var SHARE_OUTPUT_PATH = path23.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");
12982
13936
  function storeDir() {
12983
- return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path23.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
13937
+ return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path26.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
12984
13938
  }
12985
13939
  var USAGE = `flatbread - registry of AI agent runs
12986
13940
 
@@ -12993,8 +13947,9 @@ Usage:
12993
13947
  flatbread pull <id|url> Print a run's Context Capsule: a run this machine shared or
12994
13948
  captured is read here, anything else from the site
12995
13949
  flatbread pull <id|url> --out <file> Write it to a file instead
12996
- flatbread mcp MCP server on stdio: flatbread_search_traces and
12997
- 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)
12998
13953
  flatbread search <words> Find runs: this machine's shared and captured ones, then
12999
13954
  the site (the same answer the MCP server gives)
13000
13955
  flatbread setup Auto-capture: the Claude Code hook, plus a chained git
@@ -13030,13 +13985,14 @@ Options for share:
13030
13985
  opens with it and can still change it
13031
13986
  --commit [sha] Bind the run to a commit (bare --commit = HEAD) and record
13032
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
13033
13990
  --no-review Redact every match, skip the browser (non-interactive)
13034
13991
  --no-open Start the review server but do not launch a browser
13035
13992
  -h, --help Show this message
13036
13993
 
13037
13994
  Options for search:
13038
13995
  --harness <id> --category <kind> Only runs from that harness, or of that kind of job
13039
- --recovered Only runs where the agent recovered from a failure
13040
13996
  --local This machine only; the site is not asked
13041
13997
 
13042
13998
  Options for setup:
@@ -13044,7 +14000,7 @@ Options for setup:
13044
14000
  --uninstall Remove the handlers, the MCP server and the post-commit;
13045
14001
  restore a post-commit hook setup had chained
13046
14002
  --no-git Leave the repository's hooks alone
13047
- --no-mcp Leave Claude Code's MCP servers and the flatbread skill alone
14003
+ --no-mcp Leave every agent's MCP servers and the flatbread skill alone
13048
14004
  --settings <path> --no-probe As for hook install
13049
14005
 
13050
14006
  Options for capture:
@@ -13067,13 +14023,14 @@ Options for adapters:
13067
14023
  Notes:
13068
14024
  parse and share record the git branch, HEAD commit and uncommitted file names when the
13069
14025
  session ran inside a git repository. Nothing else from git is read.
13070
- parse --save writes ${path23.relative(process.cwd(), OUTPUT_PATH) || OUTPUT_PATH}
13071
- Share drafts are written to ${path23.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}
13072
14028
  (the latest only; every shared run is also kept under its ID, for search and pull)
13073
14029
  Every parsed run defaults to visibility "private".
13074
- Nothing is uploaded unless you pass --publish, or set FLATBREAD_API_URL and
13075
- FLATBREAD_API_TOKEN yourself. pull and mcp only read: public runs, your own when you
13076
- 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
13077
14034
  127.0.0.1 only, on 4321-4325 or, when all five are busy, a port the OS picks.
13078
14035
  setup registers the MCP server in ~/.claude.json, for every project. By hand instead:
13079
14036
  claude mcp add --scope user flatbread -- flatbread mcp
@@ -13105,10 +14062,10 @@ function parseArgv(argv) {
13105
14062
  } else if (arg === "--out") {
13106
14063
  const value = argv[i + 1];
13107
14064
  if (value === void 0 || value.startsWith("--")) throw new Error("--out needs a file path");
13108
- args.out = path23.resolve(value);
14065
+ args.out = path26.resolve(value);
13109
14066
  i += 1;
13110
14067
  } else if (arg !== void 0 && arg.startsWith("--out=")) {
13111
- args.out = path23.resolve(arg.slice("--out=".length));
14068
+ args.out = path26.resolve(arg.slice("--out=".length));
13112
14069
  } else if (arg === "--harness") {
13113
14070
  const value = argv[i + 1];
13114
14071
  if (value === void 0 || value.startsWith("--")) {
@@ -13229,12 +14186,12 @@ async function runParse(args) {
13229
14186
  target = found[0];
13230
14187
  console.log(`latest ${harness} session: ${target}`);
13231
14188
  } else if (args.file) {
13232
- target = path23.resolve(args.file);
13233
- if (!fs24.existsSync(target)) {
14189
+ target = path26.resolve(args.file);
14190
+ if (!fs27.existsSync(target)) {
13234
14191
  console.error(`file not found: ${target}`);
13235
14192
  return 1;
13236
14193
  }
13237
- if (!fs24.statSync(target).isFile()) {
14194
+ if (!fs27.statSync(target).isFile()) {
13238
14195
  console.error(`not a file: ${target}`);
13239
14196
  return 1;
13240
14197
  }
@@ -13265,8 +14222,8 @@ async function runParse(args) {
13265
14222
  const trace = withLineage(result.trace, readGitLineage(result.trace.source.cwd));
13266
14223
  FlatbreadTraceSchema.parse(trace);
13267
14224
  if (args.out !== void 0) {
13268
- fs24.mkdirSync(path23.dirname(args.out), { recursive: true });
13269
- fs24.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");
13270
14227
  }
13271
14228
  report({ ...result, trace }, args.out);
13272
14229
  return 0;
@@ -13330,8 +14287,8 @@ async function runAdapters(args) {
13330
14287
  }
13331
14288
  let input = null;
13332
14289
  if (args.file !== void 0) {
13333
- const resolved = path23.resolve(args.file);
13334
- if (!fs24.existsSync(resolved)) {
14290
+ const resolved = path26.resolve(args.file);
14291
+ if (!fs27.existsSync(resolved)) {
13335
14292
  console.error(`file not found: ${resolved}`);
13336
14293
  return 1;
13337
14294
  }
@@ -13399,6 +14356,12 @@ function parseShareArgv(argv) {
13399
14356
  }
13400
14357
  } else if (arg !== void 0 && arg.startsWith("--commit=")) {
13401
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;
13402
14365
  } else if (arg === "--file") {
13403
14366
  const value = argv[i + 1];
13404
14367
  if (value === void 0 || value.startsWith("--")) {
@@ -13765,9 +14728,9 @@ async function runPull(args, deps = { ...defaultMcpDeps(storeDir(), bucketDir())
13765
14728
  process.stdout.write(result.capsule);
13766
14729
  return 0;
13767
14730
  }
13768
- const target = path23.resolve(args.out);
13769
- fs24.mkdirSync(path23.dirname(target), { recursive: true });
13770
- fs24.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");
13771
14734
  console.error(`wrote ${target}${result.from === "site" ? "" : ` (from this machine's ${result.from === "store" ? "shared runs" : "captured runs"})`}`);
13772
14735
  return 0;
13773
14736
  }
@@ -13790,8 +14753,6 @@ function parseSearchArgv(argv) {
13790
14753
  const value = valueOf("--category");
13791
14754
  if (!TASK_CATEGORIES.includes(value)) throw new Error(`unknown --category "${value}". Known: ${TASK_CATEGORIES.join(", ")}`);
13792
14755
  query.category = value;
13793
- } else if (arg === "--recovered") {
13794
- query.onlyWithRecoveries = true;
13795
14756
  } else if (arg === "--local") {
13796
14757
  local = true;
13797
14758
  } else if (arg.startsWith("--")) {
@@ -13962,7 +14923,7 @@ async function main(argv) {
13962
14923
  console.error(`mcp takes no options, got: ${rest.join(" ")}`);
13963
14924
  return 1;
13964
14925
  }
13965
- return runMcpServer(defaultMcpDeps(storeDir(), bucketDir()));
14926
+ return runMcpServer(defaultMcpDeps(storeDir(), bucketDir(), SHARE_OUTPUT_PATH));
13966
14927
  }
13967
14928
  if (command === "pull") {
13968
14929
  let pullArgs;
@@ -13997,6 +14958,7 @@ async function main(argv) {
13997
14958
  ...shareArgs.prUrl !== void 0 ? { prUrl: shareArgs.prUrl } : {},
13998
14959
  ...shareArgs.title !== void 0 ? { title: shareArgs.title } : {},
13999
14960
  ...shareArgs.commit !== void 0 ? { commit: shareArgs.commit } : {},
14961
+ ...shareArgs.from !== void 0 ? { from: shareArgs.from } : {},
14000
14962
  ...storeDir() !== null ? { storeDir: storeDir() } : {},
14001
14963
  openBrowser: shareArgs.open && !shareArgs.noReview,
14002
14964
  outputPath: SHARE_OUTPUT_PATH
@@ -14017,9 +14979,9 @@ var invokedDirectly = (() => {
14017
14979
  if (entry === void 0) return false;
14018
14980
  const real = (file) => {
14019
14981
  try {
14020
- return fs24.realpathSync(file);
14982
+ return fs27.realpathSync(file);
14021
14983
  } catch {
14022
- return path23.resolve(file);
14984
+ return path26.resolve(file);
14023
14985
  }
14024
14986
  };
14025
14987
  return real(entry) === real(fileURLToPath2(import.meta.url));