flatbread-cli 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -9
- package/dist/flatbread.mjs +812 -291
- package/package.json +1 -1
package/dist/flatbread.mjs
CHANGED
|
@@ -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
|
|
10
|
+
import fs27 from "node:fs";
|
|
11
11
|
import os9 from "node:os";
|
|
12
|
-
import
|
|
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:
|
|
494
|
-
const fullPath = [...
|
|
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,
|
|
610
|
+
constructor(parent, value, path27, key) {
|
|
611
611
|
this._cachedPath = [];
|
|
612
612
|
this.parent = parent;
|
|
613
613
|
this.data = value;
|
|
614
|
-
this._path =
|
|
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: "
|
|
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
|
|
4465
|
-
if (
|
|
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 [
|
|
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:
|
|
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
|
|
4799
|
-
const file = files.get(
|
|
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:
|
|
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(
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
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)} ${count}`).join(" · ");
|
|
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
|
|
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> · ${plural2(trace.steps.length, "step")} · ${plural2(toolCalls, "tool call")} ·
|
|
10560
10666
|
${trace.stats.filesChanged === null ? `${formatCount(null)} files` : plural2(trace.stats.filesChanged, "file")} changed · ${failuresRecorded(trace.source.harness) ? plural2(trace.stats.errorCount, "error") : `${formatCount(null)} errors`} ·
|
|
10561
|
-
${Math.round(trace.stats.durationSeconds)}s · <b>${matches.length}</b> redaction match${matches.length === 1 ? "" : "es"}
|
|
10667
|
+
${Math.round(trace.stats.durationSeconds)}s · <b>${matches.length}</b> redaction match${matches.length === 1 ? "" : "es"}${trace.source.part === void 0 ? "" : ` · from prompt ${trace.source.part.fromPrompt} of ${trace.source.part.prompts}`}
|
|
10562
10668
|
</div>
|
|
10563
|
-
<div class="meta path">${escapeHtml(sessionPath)}</div
|
|
10564
|
-
</div
|
|
10565
|
-
|
|
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 · <b>space</b> keep or redact · <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(
|
|
10598
|
-
const
|
|
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
|
-
|
|
10601
|
-
`<div class="done"><h1>${
|
|
10602
|
-
<code>${escapeHtml(outputPath)}</code>
|
|
10719
|
+
`Flatbread - ${heading.toLowerCase()}`,
|
|
10720
|
+
`<div class="done"><h1>${heading}</h1>
|
|
10603
10721
|
${network}
|
|
10604
|
-
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
10728
|
-
|
|
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(() =>
|
|
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
|
|
10962
|
-
const
|
|
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
|
|
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
|
-
|
|
10983
|
-
const
|
|
10984
|
-
|
|
10985
|
-
|
|
10986
|
-
|
|
10987
|
-
|
|
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
|
-
|
|
10992
|
-
...
|
|
10993
|
-
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
|
|
10997
|
-
|
|
10998
|
-
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
|
|
11002
|
-
|
|
11003
|
-
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
);
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
|
|
11010
|
-
|
|
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
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
11047
|
-
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
log(`
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
|
|
11056
|
-
|
|
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
|
-
|
|
11316
|
+
draft: done.draft,
|
|
11317
|
+
url: session.url,
|
|
11318
|
+
...done.persisted ? { persisted: done.persisted } : {}
|
|
11063
11319
|
};
|
|
11064
11320
|
}
|
|
11321
|
+
function promptPreview(prompt2) {
|
|
11322
|
+
return titleFromPrompt(prompt2.text, "(empty prompt)");
|
|
11323
|
+
}
|
|
11324
|
+
function logDraft(draft, outputPath, log) {
|
|
11325
|
+
log("");
|
|
11326
|
+
log(` visibility ${draft.trace.visibility}`);
|
|
11327
|
+
log(` redacted ${draft.redaction.redactedCount}`);
|
|
11328
|
+
log(` kept ${draft.redaction.keptCount}`);
|
|
11329
|
+
log("");
|
|
11330
|
+
log(`wrote ${outputPath}`);
|
|
11331
|
+
}
|
|
11332
|
+
function doneReportOf(draftPath, persisted) {
|
|
11333
|
+
if (persisted === void 0) return { draftPath, upload: { kind: "none", reason: "no record was kept, so nothing was uploaded" } };
|
|
11334
|
+
const base = { draftPath, slug: persisted.slug, manageToken: persisted.manageToken };
|
|
11335
|
+
if (persisted.publish?.ok === true) return { ...base, upload: { kind: "published", url: persisted.publish.url } };
|
|
11336
|
+
if (persisted.publish !== void 0) return { ...base, upload: { kind: "failed", reason: publishFailureDetail(persisted.publish) } };
|
|
11337
|
+
return { ...base, upload: { kind: "none", reason: skippedReason(persisted.publishSkipped) } };
|
|
11338
|
+
}
|
|
11339
|
+
function skippedReason(skipped) {
|
|
11340
|
+
if (skipped === "not-signed-in") return "this machine is not signed in. Run `flatbread login`, then share again";
|
|
11341
|
+
if (skipped === "wrong-origin") return "the saved sign-in was issued by a different site. Run `flatbread login` against this one";
|
|
11342
|
+
if (skipped === "partial-env") return "FLATBREAD_API_URL is not set, so there is nowhere to send it";
|
|
11343
|
+
if (skipped === "bad-url") return "FLATBREAD_API_URL is not an address this can send to";
|
|
11344
|
+
return "this share was not asked to upload";
|
|
11345
|
+
}
|
|
11065
11346
|
|
|
11066
11347
|
// src/view.ts
|
|
11067
11348
|
import fs18 from "node:fs";
|
|
@@ -11236,6 +11517,7 @@ async function searchRemote(query, options = {}) {
|
|
|
11236
11517
|
if (query.query) params.set("q", query.query);
|
|
11237
11518
|
if (query.harness) params.set("harness", query.harness);
|
|
11238
11519
|
if (query.category) params.set("category", query.category);
|
|
11520
|
+
params.set("partial", "1");
|
|
11239
11521
|
const answer = await get(`${apiUrl}/api/search?${params.toString()}`, headersFor(apiUrl, options, "application/json"), options);
|
|
11240
11522
|
if (!answer.ok) return answer;
|
|
11241
11523
|
if (answer.status !== 200) return failureOf(answer.status, answer.body);
|
|
@@ -11248,7 +11530,16 @@ async function searchRemote(query, options = {}) {
|
|
|
11248
11530
|
const results = parsed.results;
|
|
11249
11531
|
if (!Array.isArray(results)) return { ok: false, reason: "http", status: 200, detail: "the search answer had no results list" };
|
|
11250
11532
|
if (!results.every(isHit)) return { ok: false, reason: "http", status: 200, detail: "a search result was not readable" };
|
|
11251
|
-
|
|
11533
|
+
const closest = parsed.closest ?? [];
|
|
11534
|
+
if (!Array.isArray(closest) || !closest.every(isClosest)) {
|
|
11535
|
+
return { ok: false, reason: "http", status: 200, detail: "a closest result was not readable" };
|
|
11536
|
+
}
|
|
11537
|
+
return { ok: true, results, closest, site: apiUrl };
|
|
11538
|
+
}
|
|
11539
|
+
function isClosest(value) {
|
|
11540
|
+
if (!isHit(value)) return false;
|
|
11541
|
+
const hit = value;
|
|
11542
|
+
return typeof hit["matched"] === "number" && typeof hit["terms"] === "number";
|
|
11252
11543
|
}
|
|
11253
11544
|
function isHit(value) {
|
|
11254
11545
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -11273,16 +11564,141 @@ function describeReadFailure(failure, what) {
|
|
|
11273
11564
|
// src/mcp.ts
|
|
11274
11565
|
import readline3 from "node:readline";
|
|
11275
11566
|
|
|
11276
|
-
// src/
|
|
11277
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
11567
|
+
// src/caller-session.ts
|
|
11278
11568
|
import fs19 from "node:fs";
|
|
11279
11569
|
import path18 from "node:path";
|
|
11570
|
+
var SESSION_ENV = [
|
|
11571
|
+
{ harness: "claude-code", variable: "CLAUDE_CODE_SESSION_ID", fileOf: (base, id) => base === `${id}.jsonl` },
|
|
11572
|
+
{ harness: "codex", variable: "CODEX_SESSION_ID", fileOf: (base, id) => base.endsWith(`-${id}.jsonl`) || base.endsWith(`-${id}.jsonl.zst`) }
|
|
11573
|
+
];
|
|
11574
|
+
var CODEX_ROLLOUTS_CHECKED = 40;
|
|
11575
|
+
function samePath(a, b) {
|
|
11576
|
+
const [x, y] = [path18.resolve(a), path18.resolve(b)];
|
|
11577
|
+
return process.platform === "win32" ? x.toLowerCase() === y.toLowerCase() : x === y;
|
|
11578
|
+
}
|
|
11579
|
+
function mtimeOf(file) {
|
|
11580
|
+
try {
|
|
11581
|
+
return fs19.statSync(file).mtimeMs;
|
|
11582
|
+
} catch {
|
|
11583
|
+
return 0;
|
|
11584
|
+
}
|
|
11585
|
+
}
|
|
11586
|
+
function ago(ms) {
|
|
11587
|
+
const seconds = Math.max(0, Math.round(ms / 1e3));
|
|
11588
|
+
return seconds < 120 ? `${seconds} s ago` : `${Math.round(seconds / 60)} min ago`;
|
|
11589
|
+
}
|
|
11590
|
+
async function newestHere(harness, cwd, env) {
|
|
11591
|
+
if (harness === "claude-code") return listSessionFiles(path18.join(getProjectsDir(env), encodeCwd(path18.resolve(cwd))))[0]?.filePath ?? null;
|
|
11592
|
+
if (harness === "codex") {
|
|
11593
|
+
for (const file of (adapterFor("codex").discover?.(env) ?? []).filter((f) => f.endsWith(".jsonl")).slice(0, CODEX_ROLLOUTS_CHECKED)) {
|
|
11594
|
+
const meta = (await readFirstJsonLine(file))?.["payload"];
|
|
11595
|
+
const recorded = typeof meta === "object" && meta !== null ? meta["cwd"] : void 0;
|
|
11596
|
+
if (typeof recorded === "string" && samePath(recorded, cwd)) return file;
|
|
11597
|
+
}
|
|
11598
|
+
}
|
|
11599
|
+
return null;
|
|
11600
|
+
}
|
|
11601
|
+
async function findCallerSession(options) {
|
|
11602
|
+
const { env, cwd } = options;
|
|
11603
|
+
const now = options.nowMs ?? Date.now();
|
|
11604
|
+
const unmatched = [];
|
|
11605
|
+
const named = [];
|
|
11606
|
+
for (const { harness, variable, fileOf } of SESSION_ENV) {
|
|
11607
|
+
if (options.harness !== void 0 && options.harness !== harness) continue;
|
|
11608
|
+
const id = env[variable]?.trim() ?? "";
|
|
11609
|
+
if (id.length === 0) continue;
|
|
11610
|
+
const file = (adapterFor(harness).discover?.(env) ?? []).find((candidate) => fileOf(path18.basename(candidate), id));
|
|
11611
|
+
if (file !== void 0) named.push({ file, harness, how: `named by ${variable}`, mtime: mtimeOf(file) });
|
|
11612
|
+
else unmatched.push(`${variable}=${id} names no session file on this machine`);
|
|
11613
|
+
}
|
|
11614
|
+
const claimed = named.sort((a, b) => b.mtime - a.mtime)[0];
|
|
11615
|
+
if (claimed !== void 0) return { file: claimed.file, harness: claimed.harness, how: claimed.how };
|
|
11616
|
+
const harnesses = options.harness !== void 0 ? [options.harness] : ["claude-code", "codex"];
|
|
11617
|
+
const here = [];
|
|
11618
|
+
for (const harness of harnesses) {
|
|
11619
|
+
const file = await newestHere(harness, cwd, env);
|
|
11620
|
+
if (file !== null) here.push({ file, harness, mtime: mtimeOf(file) });
|
|
11621
|
+
}
|
|
11622
|
+
const newest = here.sort((a, b) => b.mtime - a.mtime)[0];
|
|
11623
|
+
if (newest !== void 0) {
|
|
11624
|
+
return { file: newest.file, harness: newest.harness, how: `the newest ${HARNESS_INFO[newest.harness].label} session in ${cwd}, written ${ago(now - newest.mtime)}` };
|
|
11625
|
+
}
|
|
11626
|
+
if (options.harness !== void 0 && options.harness !== "claude-code" && options.harness !== "codex") {
|
|
11627
|
+
const file = adapterFor(options.harness).discover?.(env)[0];
|
|
11628
|
+
if (file !== void 0) {
|
|
11629
|
+
return { file, harness: options.harness, how: `the newest ${HARNESS_INFO[options.harness].label} session on this machine, written ${ago(now - mtimeOf(file))} (this harness records no folder to match)` };
|
|
11630
|
+
}
|
|
11631
|
+
}
|
|
11632
|
+
const which = options.harness !== void 0 ? HARNESS_INFO[options.harness].label : "Claude Code or Codex";
|
|
11633
|
+
return { problem: [`no ${which} session is recorded for ${cwd}`, ...unmatched].join("; ") };
|
|
11634
|
+
}
|
|
11635
|
+
|
|
11636
|
+
// src/agent-share.ts
|
|
11637
|
+
var AGENT_REVIEW_TIMEOUT_MS = 30 * 6e4;
|
|
11638
|
+
function createAgentShare(context) {
|
|
11639
|
+
let open = null;
|
|
11640
|
+
const log = context.log ?? ((line) => process.stderr.write(`${line}
|
|
11641
|
+
`));
|
|
11642
|
+
return async (request) => {
|
|
11643
|
+
if (open !== null) return { ok: true, ...open, alreadyOpen: true };
|
|
11644
|
+
const found = await (context.find ?? findCallerSession)({
|
|
11645
|
+
env: context.env,
|
|
11646
|
+
cwd: context.cwd,
|
|
11647
|
+
...request.harness !== void 0 ? { harness: request.harness } : {}
|
|
11648
|
+
});
|
|
11649
|
+
if ("problem" in found) return { ok: false, problem: found.problem };
|
|
11650
|
+
let markReady = () => {
|
|
11651
|
+
};
|
|
11652
|
+
const ready = new Promise((resolve) => {
|
|
11653
|
+
markReady = resolve;
|
|
11654
|
+
});
|
|
11655
|
+
const done = (context.share ?? runShare)({
|
|
11656
|
+
file: found.file,
|
|
11657
|
+
harness: found.harness,
|
|
11658
|
+
requestIsLast: true,
|
|
11659
|
+
foundBy: found.how,
|
|
11660
|
+
...request.from !== void 0 ? { from: request.from } : {},
|
|
11661
|
+
publish: request.publish,
|
|
11662
|
+
openBrowser: context.openBrowser ?? true,
|
|
11663
|
+
outputPath: context.outputPath,
|
|
11664
|
+
...context.storeDir !== null ? { storeDir: context.storeDir } : {},
|
|
11665
|
+
...context.ports !== void 0 ? { ports: context.ports } : {},
|
|
11666
|
+
timeoutMs: context.timeoutMs ?? AGENT_REVIEW_TIMEOUT_MS,
|
|
11667
|
+
log,
|
|
11668
|
+
onReviewReady: markReady
|
|
11669
|
+
});
|
|
11670
|
+
const first = await Promise.race([ready.then((r) => ({ ready: r })), done.then((result) => ({ result }))]);
|
|
11671
|
+
if ("result" in first) {
|
|
11672
|
+
const { result } = first;
|
|
11673
|
+
return {
|
|
11674
|
+
ok: false,
|
|
11675
|
+
problem: result.problem ?? `the share stopped before its review opened (${result.status})`,
|
|
11676
|
+
...result.candidates !== void 0 ? { candidates: result.candidates } : {}
|
|
11677
|
+
};
|
|
11678
|
+
}
|
|
11679
|
+
const entry = { ready: first.ready, foundBy: found.how, publish: request.publish };
|
|
11680
|
+
open = entry;
|
|
11681
|
+
const release = () => {
|
|
11682
|
+
if (open === entry) open = null;
|
|
11683
|
+
};
|
|
11684
|
+
done.then(release, (error) => {
|
|
11685
|
+
log(`flatbread share failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
11686
|
+
release();
|
|
11687
|
+
});
|
|
11688
|
+
return { ok: true, ...entry, alreadyOpen: false };
|
|
11689
|
+
};
|
|
11690
|
+
}
|
|
11691
|
+
|
|
11692
|
+
// src/bucket.ts
|
|
11693
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
11694
|
+
import fs20 from "node:fs";
|
|
11695
|
+
import path19 from "node:path";
|
|
11280
11696
|
var BUCKET_INDEX_VERSION = 1;
|
|
11281
11697
|
var BUCKET_INDEX_FILE = "index.json";
|
|
11282
11698
|
var CAPTURE_LOG_FILE = "capture.log";
|
|
11283
11699
|
var COMMIT_SESSION_WINDOW_MS = 2 * 60 * 60 * 1e3;
|
|
11284
11700
|
function bucketDir(env = process.env) {
|
|
11285
|
-
return
|
|
11701
|
+
return path19.join(flatbreadHome(env), "bucket");
|
|
11286
11702
|
}
|
|
11287
11703
|
function bucketSlug(harness, sessionKey) {
|
|
11288
11704
|
return `local-${createHash2("sha256").update(`${harness}\0${sessionKey}`, "utf8").digest("hex").slice(0, 12)}`;
|
|
@@ -11297,18 +11713,18 @@ function rowOf(record) {
|
|
|
11297
11713
|
};
|
|
11298
11714
|
}
|
|
11299
11715
|
function recordStems(dir) {
|
|
11300
|
-
return
|
|
11716
|
+
return fs20.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -".json".length)).filter((stem2) => SlugSchema.safeParse(stem2).success).sort();
|
|
11301
11717
|
}
|
|
11302
11718
|
function writeAtomic(file, body) {
|
|
11303
|
-
|
|
11719
|
+
fs20.mkdirSync(path19.dirname(file), { recursive: true });
|
|
11304
11720
|
const temp = `${file}.${process.pid}.tmp`;
|
|
11305
|
-
|
|
11306
|
-
|
|
11721
|
+
fs20.writeFileSync(temp, body, "utf8");
|
|
11722
|
+
fs20.renameSync(temp, file);
|
|
11307
11723
|
}
|
|
11308
11724
|
var newestFirst = (rows) => [...rows].sort((a, b) => a.capturedAt < b.capturedAt ? 1 : a.capturedAt > b.capturedAt ? -1 : a.summary.slug < b.summary.slug ? -1 : 1);
|
|
11309
11725
|
function readIndex(dir) {
|
|
11310
11726
|
try {
|
|
11311
|
-
const parsed = JSON.parse(
|
|
11727
|
+
const parsed = JSON.parse(fs20.readFileSync(path19.join(dir, BUCKET_INDEX_FILE), "utf8"));
|
|
11312
11728
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
11313
11729
|
const { version, rows } = parsed;
|
|
11314
11730
|
if (version !== BUCKET_INDEX_VERSION || !Array.isArray(rows)) return null;
|
|
@@ -11327,10 +11743,10 @@ function readIndex(dir) {
|
|
|
11327
11743
|
}
|
|
11328
11744
|
}
|
|
11329
11745
|
function writeIndex(dir, rows) {
|
|
11330
|
-
writeAtomic(
|
|
11746
|
+
writeAtomic(path19.join(dir, BUCKET_INDEX_FILE), JSON.stringify({ version: BUCKET_INDEX_VERSION, rows: newestFirst(rows) }, null, 2) + "\n");
|
|
11331
11747
|
}
|
|
11332
11748
|
function readBucket(dir) {
|
|
11333
|
-
if (!
|
|
11749
|
+
if (!fs20.existsSync(dir)) return { rows: [], rebuilt: false, unreadable: 0 };
|
|
11334
11750
|
const stems = recordStems(dir);
|
|
11335
11751
|
const indexed = readIndex(dir);
|
|
11336
11752
|
if (indexed !== null && JSON.stringify(indexed.map((row) => row.summary.slug).sort()) === JSON.stringify(stems)) {
|
|
@@ -11347,16 +11763,18 @@ function readBucket(dir) {
|
|
|
11347
11763
|
function readBucketRecord(dir, slug) {
|
|
11348
11764
|
if (!SlugSchema.safeParse(slug).success) return null;
|
|
11349
11765
|
try {
|
|
11350
|
-
const parsed = TraceRecordSchema.safeParse(JSON.parse(
|
|
11766
|
+
const parsed = TraceRecordSchema.safeParse(JSON.parse(fs20.readFileSync(path19.join(dir, `${slug}.json`), "utf8")));
|
|
11351
11767
|
return parsed.success ? parsed.data : null;
|
|
11352
11768
|
} catch {
|
|
11353
11769
|
return null;
|
|
11354
11770
|
}
|
|
11355
11771
|
}
|
|
11356
11772
|
function matchesQuery(summary, query) {
|
|
11773
|
+
return matchesFilters(summary, query) && matchesSearch(summary, query.query ?? "");
|
|
11774
|
+
}
|
|
11775
|
+
function matchesFilters(summary, query) {
|
|
11357
11776
|
if (query.harness !== void 0 && summary.harness !== query.harness) return false;
|
|
11358
|
-
|
|
11359
|
-
return matchesSearch(summary, query.query ?? "");
|
|
11777
|
+
return query.category === void 0 || summary.category === query.category;
|
|
11360
11778
|
}
|
|
11361
11779
|
async function captureToBucket(options) {
|
|
11362
11780
|
const dir = options.dir ?? bucketDir(options.env);
|
|
@@ -11375,9 +11793,9 @@ async function captureToBucket(options) {
|
|
|
11375
11793
|
const trace = withLineage(parsed.trace, lineage);
|
|
11376
11794
|
const scan = scanTrace(trace);
|
|
11377
11795
|
const applied = applyRedactions(trace, scan.matches, { keepIds: [] });
|
|
11378
|
-
const sessionKey = trace.source.sessionId.length > 0 ? trace.source.sessionId :
|
|
11796
|
+
const sessionKey = trace.source.sessionId.length > 0 ? trace.source.sessionId : path19.resolve(options.file);
|
|
11379
11797
|
const slug = bucketSlug(parsed.harness, sessionKey);
|
|
11380
|
-
const recordPath =
|
|
11798
|
+
const recordPath = path19.join(dir, `${slug}.json`);
|
|
11381
11799
|
const previous = readBucketRecord(dir, slug);
|
|
11382
11800
|
const commit = applied.trace.source.commitNamed === true ? applied.trace.source.headCommitSha : void 0;
|
|
11383
11801
|
const draft = ShareDraftSchema.parse({
|
|
@@ -11399,7 +11817,7 @@ async function captureToBucket(options) {
|
|
|
11399
11817
|
}
|
|
11400
11818
|
function sessionForRepo(repo, options = {}) {
|
|
11401
11819
|
const projects = getProjectsDir(options.env ?? process.env);
|
|
11402
|
-
const folder =
|
|
11820
|
+
const folder = path19.join(projects, encodeCwd(path19.resolve(repo)));
|
|
11403
11821
|
const newest = listSessionFiles(folder)[0];
|
|
11404
11822
|
if (newest === void 0) return { problem: `no Claude Code session recorded for ${repo}` };
|
|
11405
11823
|
const age = (options.nowMs ?? Date.now()) - newest.mtimeMs;
|
|
@@ -11411,8 +11829,8 @@ function sessionForRepo(repo, options = {}) {
|
|
|
11411
11829
|
}
|
|
11412
11830
|
function logCapture(dir, line, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
11413
11831
|
try {
|
|
11414
|
-
|
|
11415
|
-
|
|
11832
|
+
fs20.mkdirSync(dir, { recursive: true });
|
|
11833
|
+
fs20.appendFileSync(path19.join(dir, CAPTURE_LOG_FILE), `${now} ${line.replace(/[\r\n]+/g, " ")}
|
|
11416
11834
|
`, "utf8");
|
|
11417
11835
|
} catch {
|
|
11418
11836
|
}
|
|
@@ -11420,13 +11838,14 @@ function logCapture(dir, line, now = (/* @__PURE__ */ new Date()).toISOString())
|
|
|
11420
11838
|
|
|
11421
11839
|
// src/mcp.ts
|
|
11422
11840
|
var MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
11423
|
-
var MCP_SERVER_VERSION = "0.
|
|
11424
|
-
function defaultMcpDeps(storeDir2, bucketDir2) {
|
|
11841
|
+
var MCP_SERVER_VERSION = "0.7.0";
|
|
11842
|
+
function defaultMcpDeps(storeDir2, bucketDir2, shareOutputPath) {
|
|
11425
11843
|
return {
|
|
11426
11844
|
storeDir: storeDir2,
|
|
11427
11845
|
bucketDir: bucketDir2,
|
|
11428
11846
|
search: (query) => searchRemote(query, { client: "mcp" }),
|
|
11429
|
-
capsule: (ref) => fetchCapsule(ref, { client: "mcp" })
|
|
11847
|
+
capsule: (ref) => fetchCapsule(ref, { client: "mcp" }),
|
|
11848
|
+
...shareOutputPath !== void 0 ? { share: createAgentShare({ env: process.env, cwd: process.cwd(), outputPath: shareOutputPath, storeDir: storeDir2 }) } : {}
|
|
11430
11849
|
};
|
|
11431
11850
|
}
|
|
11432
11851
|
var MCP_TOOLS = [
|
|
@@ -11437,7 +11856,7 @@ var MCP_TOOLS = [
|
|
|
11437
11856
|
inputSchema: {
|
|
11438
11857
|
type: "object",
|
|
11439
11858
|
properties: {
|
|
11440
|
-
query: { type: "string", description: "
|
|
11859
|
+
query: { type: "string", description: "Two to four words from the task or the error. Runs with every word in their title or ID come first; when none has them all, the closest are listed, marked with how many words they share" },
|
|
11441
11860
|
harness: { type: "string", enum: [...HARNESS_IDS], description: "Only runs from this agent harness" },
|
|
11442
11861
|
category: { type: "string", enum: [...TASK_CATEGORIES], description: "Only runs of this kind of job" }
|
|
11443
11862
|
},
|
|
@@ -11454,6 +11873,20 @@ var MCP_TOOLS = [
|
|
|
11454
11873
|
required: ["slug"],
|
|
11455
11874
|
additionalProperties: false
|
|
11456
11875
|
}
|
|
11876
|
+
},
|
|
11877
|
+
{
|
|
11878
|
+
name: "flatbread_share",
|
|
11879
|
+
title: "Share this run",
|
|
11880
|
+
description: `Share the session you are in now as a Flatbread run. Call it when the user says "share this run", "flatbread share", "share to flatbread" or anything like it. It finds this session, leaves the share request out, opens a review page in the user's browser with every secret redacted by default, and returns at once: nothing is kept or uploaded until the user presses Confirm there. For only part of a long session, pass from: the number of the user's prompt to start at, or words from that prompt.`,
|
|
11881
|
+
inputSchema: {
|
|
11882
|
+
type: "object",
|
|
11883
|
+
properties: {
|
|
11884
|
+
from: { type: "string", description: 'Start at this prompt: its number ("3"), or words from it. Leave out for the whole session' },
|
|
11885
|
+
publish: { type: "boolean", description: "Upload on Confirm when this machine is signed in to flatbread.io. Default true; false keeps it on this machine" },
|
|
11886
|
+
harness: { type: "string", enum: [...HARNESS_IDS], description: "Which agent you are, only when you are not Claude Code or Codex" }
|
|
11887
|
+
},
|
|
11888
|
+
additionalProperties: false
|
|
11889
|
+
}
|
|
11457
11890
|
}
|
|
11458
11891
|
];
|
|
11459
11892
|
function text(body, isError = false) {
|
|
@@ -11500,6 +11933,8 @@ async function searchEverywhere(query, deps, options = {}) {
|
|
|
11500
11933
|
const out = [];
|
|
11501
11934
|
const local = localRecords(deps);
|
|
11502
11935
|
const localHits = local.records.map((record) => summarize(record)).filter((summary) => matchesQuery(summary, query));
|
|
11936
|
+
let found = localHits.length;
|
|
11937
|
+
const nearby = [];
|
|
11503
11938
|
if (deps.storeDir !== null) {
|
|
11504
11939
|
out.push(`This machine (${runs(localHits.length)}):`);
|
|
11505
11940
|
for (const hit of localHits) out.push(hitLine(hit));
|
|
@@ -11510,9 +11945,23 @@ async function searchEverywhere(query, deps, options = {}) {
|
|
|
11510
11945
|
const { harness, sessionId } = record.trace.source;
|
|
11511
11946
|
if (sessionId.length > 0 && localHits.some((hit) => hit.slug === record.slug)) seen.add(bucketSlug(harness, sessionId));
|
|
11512
11947
|
}
|
|
11948
|
+
const nearSeen = /* @__PURE__ */ new Set();
|
|
11949
|
+
const localNear = local.records.filter((record) => matchesFilters(summarize(record), query));
|
|
11950
|
+
for (const { row, matched, terms } of closestByTerms(localNear, (record) => rowSearchText(summarize(record)), query.query ?? "")) {
|
|
11951
|
+
nearby.push({ line: `${hitLine(summarize(row))} \xB7 ${matched} of ${terms} words`, matched });
|
|
11952
|
+
nearSeen.add(row.slug);
|
|
11953
|
+
const { harness, sessionId } = row.trace.source;
|
|
11954
|
+
if (sessionId.length > 0) nearSeen.add(bucketSlug(harness, sessionId));
|
|
11955
|
+
}
|
|
11513
11956
|
if (deps.bucketDir !== null) {
|
|
11514
11957
|
const bucket = readBucket(deps.bucketDir);
|
|
11515
11958
|
const captured = bucket.rows.filter((row) => !seen.has(row.summary.slug) && matchesQuery(row.summary, query));
|
|
11959
|
+
found += captured.length;
|
|
11960
|
+
const bucketNear = bucket.rows.filter((row) => !nearSeen.has(row.summary.slug) && matchesFilters(row.summary, query));
|
|
11961
|
+
for (const { row, matched, terms } of closestByTerms(bucketNear, (row2) => rowSearchText(row2.summary), query.query ?? "")) {
|
|
11962
|
+
nearby.push({ line: `${capturedLine(row)} \xB7 ${matched} of ${terms} words`, matched });
|
|
11963
|
+
nearSeen.add(row.summary.slug);
|
|
11964
|
+
}
|
|
11516
11965
|
if (out.length > 0) out.push("");
|
|
11517
11966
|
out.push(`Captured on this machine, not shared (${runs(captured.length)}):`);
|
|
11518
11967
|
for (const row of captured) {
|
|
@@ -11521,21 +11970,31 @@ async function searchEverywhere(query, deps, options = {}) {
|
|
|
11521
11970
|
}
|
|
11522
11971
|
if (bucket.unreadable > 0) out.push(`(${bucket.unreadable} captured run file${bucket.unreadable === 1 ? "" : "s"} could not be read)`);
|
|
11523
11972
|
}
|
|
11973
|
+
let siteSearched = false;
|
|
11524
11974
|
if (options.remote === false) {
|
|
11525
11975
|
if (out.length > 0) out.push("");
|
|
11526
11976
|
out.push("The site was not searched: --local");
|
|
11527
|
-
return { lines: out, siteSearched: false };
|
|
11528
|
-
}
|
|
11529
|
-
const remote = await deps.search(query);
|
|
11530
|
-
if (out.length > 0) out.push("");
|
|
11531
|
-
if (remote.ok) {
|
|
11532
|
-
const fresh = remote.results.filter((hit) => !seen.has(hit.slug));
|
|
11533
|
-
out.push(`${remote.site} (${runs(fresh.length)}):`);
|
|
11534
|
-
for (const hit of fresh) out.push(hitLine(hit));
|
|
11535
11977
|
} else {
|
|
11536
|
-
|
|
11978
|
+
const remote = await deps.search(query);
|
|
11979
|
+
if (out.length > 0) out.push("");
|
|
11980
|
+
if (remote.ok) {
|
|
11981
|
+
siteSearched = true;
|
|
11982
|
+
const fresh = remote.results.filter((hit) => !seen.has(hit.slug));
|
|
11983
|
+
found += fresh.length;
|
|
11984
|
+
out.push(`${remote.site} (${runs(fresh.length)}):`);
|
|
11985
|
+
for (const hit of fresh) out.push(hitLine(hit));
|
|
11986
|
+
for (const hit of (remote.closest ?? []).filter((hit2) => !seen.has(hit2.slug) && !nearSeen.has(hit2.slug))) {
|
|
11987
|
+
nearby.push({ line: `${hitLine(hit)} \xB7 ${hit.matched} of ${hit.terms} words`, matched: hit.matched });
|
|
11988
|
+
}
|
|
11989
|
+
} else {
|
|
11990
|
+
out.push(`The site was not searched: ${describeReadFailure(remote, "search")}`);
|
|
11991
|
+
}
|
|
11537
11992
|
}
|
|
11538
|
-
|
|
11993
|
+
if (found === 0 && nearby.length > 0) {
|
|
11994
|
+
out.push("", `No run has every word of "${query.query ?? ""}". Closest, by words in common:`);
|
|
11995
|
+
for (const hit of [...nearby].sort((a, b) => b.matched - a.matched).slice(0, CLOSEST_LIMIT)) out.push(hit.line);
|
|
11996
|
+
}
|
|
11997
|
+
return { lines: out, siteSearched };
|
|
11539
11998
|
}
|
|
11540
11999
|
async function searchTool(args, deps) {
|
|
11541
12000
|
const query = readQuery(args);
|
|
@@ -11553,6 +12012,50 @@ async function readCapsule(ref, deps) {
|
|
|
11553
12012
|
const remote = await deps.capsule(ref);
|
|
11554
12013
|
return remote.ok ? { ok: true, capsule: remote.capsule, from: "site" } : remote;
|
|
11555
12014
|
}
|
|
12015
|
+
function readShareRequest(args) {
|
|
12016
|
+
const from = args["from"];
|
|
12017
|
+
const publish = args["publish"];
|
|
12018
|
+
const harness = args["harness"];
|
|
12019
|
+
if (from !== void 0 && typeof from !== "string" && !(typeof from === "number" && Number.isInteger(from))) {
|
|
12020
|
+
return "from must be a prompt number or words from that prompt";
|
|
12021
|
+
}
|
|
12022
|
+
if (publish !== void 0 && typeof publish !== "boolean") return "publish must be true or false";
|
|
12023
|
+
if (harness !== void 0 && (typeof harness !== "string" || !HARNESS_IDS.includes(harness))) {
|
|
12024
|
+
return `harness must be one of ${HARNESS_IDS.join(", ")}`;
|
|
12025
|
+
}
|
|
12026
|
+
return {
|
|
12027
|
+
...from !== void 0 && String(from).trim().length > 0 ? { from: String(from) } : {},
|
|
12028
|
+
publish: publish !== false,
|
|
12029
|
+
...harness !== void 0 ? { harness } : {}
|
|
12030
|
+
};
|
|
12031
|
+
}
|
|
12032
|
+
function shareLines(start) {
|
|
12033
|
+
const { ready } = start;
|
|
12034
|
+
const matches = `${ready.matches} redaction match${ready.matches === 1 ? "" : "es"}`;
|
|
12035
|
+
return [
|
|
12036
|
+
start.alreadyOpen ? `A review is already open in the user's browser, so no new one was started: ${ready.url}` : `Review open in the user's browser: ${ready.url}`,
|
|
12037
|
+
`Session: ${ready.sessionPath} (found as ${start.foundBy})`,
|
|
12038
|
+
ready.fromPrompt > 1 ? `Shares prompt ${ready.fromPrompt} of ${ready.prompts} to the end` : `Shares the whole session: ${ready.prompts} prompt${ready.prompts === 1 ? "" : "s"}`,
|
|
12039
|
+
`${ready.steps} steps, ${matches}, each redacted unless the user keeps it`,
|
|
12040
|
+
ready.publishTo !== void 0 ? `Confirm uploads it to ${ready.publishTo} at the visibility the user picks there` : start.publish ? "Confirm keeps it on this machine only: this machine is not signed in, so nothing is uploaded (`flatbread login` signs it in)" : "Confirm keeps it on this machine only (publish: false)",
|
|
12041
|
+
"",
|
|
12042
|
+
"Nothing is kept or uploaded until the user presses Confirm on that page, which then shows the link and the manage token. Tell the user to review it there; do not wait for it."
|
|
12043
|
+
];
|
|
12044
|
+
}
|
|
12045
|
+
async function shareTool(args, deps) {
|
|
12046
|
+
if (deps.share === void 0) return text("flatbread_share is not available in this server: run `flatbread share` in a terminal", true);
|
|
12047
|
+
const request = readShareRequest(args);
|
|
12048
|
+
if (typeof request === "string") return text(request, true);
|
|
12049
|
+
const start = await deps.share(request);
|
|
12050
|
+
if (start.ok) return text(shareLines(start).join("\n"));
|
|
12051
|
+
const lines2 = [`Nothing was shared: ${start.problem}.`];
|
|
12052
|
+
if (start.candidates !== void 0 && start.candidates.length > 0) {
|
|
12053
|
+
lines2.push("", "This session's prompts, redacted:");
|
|
12054
|
+
for (const prompt2 of start.candidates) lines2.push(`${prompt2.n}. ${titleFromPrompt(prompt2.text, "(empty prompt)")}`);
|
|
12055
|
+
lines2.push("", "Ask the user which prompt to start at, then call flatbread_share again with from set to its number.");
|
|
12056
|
+
}
|
|
12057
|
+
return text(lines2.join("\n"), true);
|
|
12058
|
+
}
|
|
11556
12059
|
async function capsuleTool(args, deps) {
|
|
11557
12060
|
const raw = args["slug"];
|
|
11558
12061
|
if (typeof raw !== "string") return text("slug must be a run ID or a /r/<id> link", true);
|
|
@@ -11584,7 +12087,7 @@ async function handleMcpMessage(message, deps) {
|
|
|
11584
12087
|
protocolVersion,
|
|
11585
12088
|
capabilities: { tools: { listChanged: false } },
|
|
11586
12089
|
serverInfo: { name: "flatbread", version: MCP_SERVER_VERSION },
|
|
11587
|
-
instructions: "Flatbread holds recorded coding-agent runs. Before a task someone may have done before, or after an error you have not seen, call flatbread_search_traces, then flatbread_get_capsule on a promising run and read its blockers before repeating them."
|
|
12090
|
+
instructions: "Flatbread holds recorded coding-agent runs. Before a task someone may have done before, or after an error you have not seen, call flatbread_search_traces, then flatbread_get_capsule on a promising run and read its blockers before repeating them. When the user asks to share this run, call flatbread_share."
|
|
11588
12091
|
}
|
|
11589
12092
|
};
|
|
11590
12093
|
}
|
|
@@ -11598,6 +12101,7 @@ async function handleMcpMessage(message, deps) {
|
|
|
11598
12101
|
try {
|
|
11599
12102
|
if (name === "flatbread_search_traces") return { jsonrpc: "2.0", id, result: await searchTool(toolArgs, deps) };
|
|
11600
12103
|
if (name === "flatbread_get_capsule") return { jsonrpc: "2.0", id, result: await capsuleTool(toolArgs, deps) };
|
|
12104
|
+
if (name === "flatbread_share") return { jsonrpc: "2.0", id, result: await shareTool(toolArgs, deps) };
|
|
11601
12105
|
} catch (error) {
|
|
11602
12106
|
return { jsonrpc: "2.0", id, result: text(`flatbread failed: ${error instanceof Error ? error.message : String(error)}`, true) };
|
|
11603
12107
|
}
|
|
@@ -11636,17 +12140,17 @@ async function runMcpServer(deps, io) {
|
|
|
11636
12140
|
}
|
|
11637
12141
|
|
|
11638
12142
|
// src/setup.ts
|
|
11639
|
-
import
|
|
11640
|
-
import
|
|
12143
|
+
import fs26 from "node:fs";
|
|
12144
|
+
import path25 from "node:path";
|
|
11641
12145
|
|
|
11642
12146
|
// src/claude-mcp.ts
|
|
11643
|
-
import
|
|
12147
|
+
import fs21 from "node:fs";
|
|
11644
12148
|
import os8 from "node:os";
|
|
11645
|
-
import
|
|
12149
|
+
import path20 from "node:path";
|
|
11646
12150
|
var MCP_SERVER_NAME = "flatbread";
|
|
11647
12151
|
function claudeUserConfigPath(env = process.env) {
|
|
11648
12152
|
const dir = env["CLAUDE_CONFIG_DIR"]?.trim();
|
|
11649
|
-
return
|
|
12153
|
+
return path20.join(dir ? dir : os8.homedir(), ".claude.json");
|
|
11650
12154
|
}
|
|
11651
12155
|
function mcpEntryFor(execPath, entryPath) {
|
|
11652
12156
|
return { type: "stdio", command: execPath, args: [entryPath, "mcp"] };
|
|
@@ -11661,7 +12165,7 @@ var BOM = "\uFEFF";
|
|
|
11661
12165
|
function readConfig(file) {
|
|
11662
12166
|
let raw;
|
|
11663
12167
|
try {
|
|
11664
|
-
raw =
|
|
12168
|
+
raw = fs21.readFileSync(file, "utf8");
|
|
11665
12169
|
} catch (error) {
|
|
11666
12170
|
if (error.code === "ENOENT") return { root: {}, raw: null };
|
|
11667
12171
|
return { problem: error.message };
|
|
@@ -11688,19 +12192,19 @@ function serializeLike(raw, root) {
|
|
|
11688
12192
|
function write(file, read2, next) {
|
|
11689
12193
|
if (read2.raw !== null) {
|
|
11690
12194
|
try {
|
|
11691
|
-
|
|
12195
|
+
fs21.accessSync(file, fs21.constants.W_OK);
|
|
11692
12196
|
} catch {
|
|
11693
12197
|
return `${file} is read-only, so it was left as it was`;
|
|
11694
12198
|
}
|
|
11695
|
-
if (
|
|
12199
|
+
if (fs21.readFileSync(file, "utf8") !== read2.raw) return `${file} changed while setup ran; run setup again`;
|
|
11696
12200
|
const backup = `${file}.flatbread.bak`;
|
|
11697
|
-
if (!
|
|
12201
|
+
if (!fs21.existsSync(backup)) fs21.copyFileSync(file, backup);
|
|
11698
12202
|
}
|
|
11699
|
-
const mode = read2.raw !== null ?
|
|
11700
|
-
|
|
12203
|
+
const mode = read2.raw !== null ? fs21.statSync(file).mode & 511 : 384;
|
|
12204
|
+
fs21.mkdirSync(path20.dirname(file), { recursive: true });
|
|
11701
12205
|
const temp = `${file}.flatbread-${process.pid}.tmp`;
|
|
11702
|
-
|
|
11703
|
-
|
|
12206
|
+
fs21.writeFileSync(temp, next, { encoding: "utf8", mode });
|
|
12207
|
+
fs21.renameSync(temp, file);
|
|
11704
12208
|
return null;
|
|
11705
12209
|
}
|
|
11706
12210
|
function registerClaudeMcp(options) {
|
|
@@ -11746,7 +12250,7 @@ function registerMcpJson(options) {
|
|
|
11746
12250
|
}
|
|
11747
12251
|
log(` mcp ${file}: removed the ${MCP_SERVER_NAME} server`);
|
|
11748
12252
|
const backup = `${file}.flatbread.bak`;
|
|
11749
|
-
if (
|
|
12253
|
+
if (fs21.existsSync(backup)) log(` ${backup} is the copy setup took before its first write; delete it when you like`);
|
|
11750
12254
|
return 0;
|
|
11751
12255
|
}
|
|
11752
12256
|
const entry = options.entry;
|
|
@@ -11775,12 +12279,12 @@ function registerMcpJson(options) {
|
|
|
11775
12279
|
}
|
|
11776
12280
|
|
|
11777
12281
|
// src/codex-mcp.ts
|
|
11778
|
-
import
|
|
11779
|
-
import
|
|
12282
|
+
import fs22 from "node:fs";
|
|
12283
|
+
import path21 from "node:path";
|
|
11780
12284
|
var BOM2 = "\uFEFF";
|
|
11781
12285
|
var OWNED_KEYS = /* @__PURE__ */ new Set(["type", "command", "args"]);
|
|
11782
12286
|
function codexConfigPath(codexHome2) {
|
|
11783
|
-
return
|
|
12287
|
+
return path21.join(codexHome2, "config.toml");
|
|
11784
12288
|
}
|
|
11785
12289
|
function quote(value) {
|
|
11786
12290
|
return JSON.stringify(value);
|
|
@@ -12001,19 +12505,19 @@ function writeLike(file, raw, lf) {
|
|
|
12001
12505
|
if (raw !== null && raw.startsWith(BOM2)) next = BOM2 + next;
|
|
12002
12506
|
if (raw !== null) {
|
|
12003
12507
|
try {
|
|
12004
|
-
|
|
12508
|
+
fs22.accessSync(file, fs22.constants.W_OK);
|
|
12005
12509
|
} catch {
|
|
12006
12510
|
return `${file} is read-only, so it was left as it was`;
|
|
12007
12511
|
}
|
|
12008
|
-
if (
|
|
12512
|
+
if (fs22.readFileSync(file, "utf8") !== raw) return `${file} changed while setup ran; run setup again`;
|
|
12009
12513
|
const backup = `${file}.flatbread.bak`;
|
|
12010
|
-
if (!
|
|
12514
|
+
if (!fs22.existsSync(backup)) fs22.copyFileSync(file, backup);
|
|
12011
12515
|
}
|
|
12012
|
-
const mode = raw !== null ?
|
|
12013
|
-
|
|
12516
|
+
const mode = raw !== null ? fs22.statSync(file).mode & 511 : 384;
|
|
12517
|
+
fs22.mkdirSync(path21.dirname(file), { recursive: true });
|
|
12014
12518
|
const temp = `${file}.flatbread-${process.pid}.tmp`;
|
|
12015
|
-
|
|
12016
|
-
|
|
12519
|
+
fs22.writeFileSync(temp, next, { encoding: "utf8", mode });
|
|
12520
|
+
fs22.renameSync(temp, file);
|
|
12017
12521
|
return null;
|
|
12018
12522
|
}
|
|
12019
12523
|
function removeBlocks(lines2, blocks) {
|
|
@@ -12030,7 +12534,7 @@ function registerCodexMcp(options) {
|
|
|
12030
12534
|
const { file, log } = options;
|
|
12031
12535
|
let raw;
|
|
12032
12536
|
try {
|
|
12033
|
-
raw =
|
|
12537
|
+
raw = fs22.readFileSync(file, "utf8");
|
|
12034
12538
|
} catch (error) {
|
|
12035
12539
|
if (error.code !== "ENOENT") {
|
|
12036
12540
|
log(` mcp refused: ${file}: ${error.message}`);
|
|
@@ -12128,8 +12632,8 @@ function registerCodexMcp(options) {
|
|
|
12128
12632
|
}
|
|
12129
12633
|
|
|
12130
12634
|
// src/git-hook.ts
|
|
12131
|
-
import
|
|
12132
|
-
import
|
|
12635
|
+
import fs23 from "node:fs";
|
|
12636
|
+
import path22 from "node:path";
|
|
12133
12637
|
var POST_COMMIT_MARKER = "# flatbread post-commit hook - remove with: flatbread setup --uninstall";
|
|
12134
12638
|
var CHAINED_HOOK = "post-commit.pre-flatbread";
|
|
12135
12639
|
var sq = (value) => `'${value.replace(/\\/g, "/").replace(/'/g, "'\\''")}'`;
|
|
@@ -12173,25 +12677,25 @@ function hooksDirFor(cwd, options = {}) {
|
|
|
12173
12677
|
}
|
|
12174
12678
|
const common = git(top, ["rev-parse", "--git-common-dir"], options)?.trim();
|
|
12175
12679
|
if (common === void 0 || common.length === 0) return { ok: false, reason: "unreadable", detail: `git could not name the git directory of ${top}` };
|
|
12176
|
-
return { ok: true, repo:
|
|
12680
|
+
return { ok: true, repo: path22.resolve(top), dir: path22.resolve(top, common, "hooks"), ...machineHooksPath !== void 0 ? { machineHooksPath } : {} };
|
|
12177
12681
|
}
|
|
12178
12682
|
var read = (file) => {
|
|
12179
12683
|
try {
|
|
12180
|
-
return
|
|
12684
|
+
return fs23.readFileSync(file, "utf8");
|
|
12181
12685
|
} catch {
|
|
12182
12686
|
return null;
|
|
12183
12687
|
}
|
|
12184
12688
|
};
|
|
12185
12689
|
function installGitHook(dir, script, options = {}) {
|
|
12186
|
-
const hook =
|
|
12187
|
-
const chained =
|
|
12690
|
+
const hook = path22.join(dir, "post-commit");
|
|
12691
|
+
const chained = path22.join(dir, CHAINED_HOOK);
|
|
12188
12692
|
const current = read(hook);
|
|
12189
12693
|
const write2 = (lines2) => {
|
|
12190
12694
|
if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
|
|
12191
|
-
|
|
12192
|
-
if (current !== null && !isOurs(current))
|
|
12193
|
-
|
|
12194
|
-
|
|
12695
|
+
fs23.mkdirSync(dir, { recursive: true });
|
|
12696
|
+
if (current !== null && !isOurs(current)) fs23.renameSync(hook, chained);
|
|
12697
|
+
fs23.writeFileSync(hook, script, { encoding: "utf8", mode: 493 });
|
|
12698
|
+
fs23.chmodSync(hook, 493);
|
|
12195
12699
|
return { code: 0, lines: lines2 };
|
|
12196
12700
|
};
|
|
12197
12701
|
if (current === null) return write2([`post-commit ${hook} (new)`]);
|
|
@@ -12199,7 +12703,7 @@ function installGitHook(dir, script, options = {}) {
|
|
|
12199
12703
|
if (current === script) return { code: 0, lines: [`post-commit ${hook}`, "already installed, unchanged"] };
|
|
12200
12704
|
return write2([`post-commit ${hook}`, "updated the flatbread hook"]);
|
|
12201
12705
|
}
|
|
12202
|
-
if (
|
|
12706
|
+
if (fs23.existsSync(chained)) {
|
|
12203
12707
|
return {
|
|
12204
12708
|
code: 1,
|
|
12205
12709
|
lines: [
|
|
@@ -12215,10 +12719,10 @@ function installGitHook(dir, script, options = {}) {
|
|
|
12215
12719
|
]);
|
|
12216
12720
|
}
|
|
12217
12721
|
function uninstallGitHook(dir, options = {}) {
|
|
12218
|
-
const hook =
|
|
12219
|
-
const chained =
|
|
12722
|
+
const hook = path22.join(dir, "post-commit");
|
|
12723
|
+
const chained = path22.join(dir, CHAINED_HOOK);
|
|
12220
12724
|
const current = read(hook);
|
|
12221
|
-
const hasChained =
|
|
12725
|
+
const hasChained = fs23.existsSync(chained);
|
|
12222
12726
|
const done = (lines2, act) => {
|
|
12223
12727
|
if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
|
|
12224
12728
|
act();
|
|
@@ -12236,17 +12740,17 @@ function uninstallGitHook(dir, options = {}) {
|
|
|
12236
12740
|
if (current === null && !hasChained) return { code: 0, lines: [`no flatbread post-commit in ${dir}`] };
|
|
12237
12741
|
if (hasChained) {
|
|
12238
12742
|
return done([`post-commit ${hook}`, `restored your post-commit from ${CHAINED_HOOK}`], () => {
|
|
12239
|
-
if (current !== null)
|
|
12240
|
-
|
|
12743
|
+
if (current !== null) fs23.rmSync(hook);
|
|
12744
|
+
fs23.renameSync(chained, hook);
|
|
12241
12745
|
});
|
|
12242
12746
|
}
|
|
12243
|
-
return done([`post-commit ${hook}`, "removed the flatbread hook"], () =>
|
|
12747
|
+
return done([`post-commit ${hook}`, "removed the flatbread hook"], () => fs23.rmSync(hook));
|
|
12244
12748
|
}
|
|
12245
12749
|
|
|
12246
12750
|
// src/hook.ts
|
|
12247
12751
|
import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
|
|
12248
|
-
import
|
|
12249
|
-
import
|
|
12752
|
+
import fs24 from "node:fs";
|
|
12753
|
+
import path23 from "node:path";
|
|
12250
12754
|
var HOOK_EVENTS = ["Stop", "SessionEnd"];
|
|
12251
12755
|
var OFFER_AFTER_STOPS = 2;
|
|
12252
12756
|
var MAX_TRACKED_SESSIONS = 50;
|
|
@@ -12256,14 +12760,14 @@ function emptyHookState() {
|
|
|
12256
12760
|
return { version: HOOK_STATE_VERSION, sessions: {} };
|
|
12257
12761
|
}
|
|
12258
12762
|
function hookStatePath(env = process.env) {
|
|
12259
|
-
return
|
|
12763
|
+
return path23.join(flatbreadHome(env), "hook-state.json");
|
|
12260
12764
|
}
|
|
12261
12765
|
function readHookState(env = process.env) {
|
|
12262
12766
|
const file = hookStatePath(env);
|
|
12263
|
-
if (!
|
|
12767
|
+
if (!fs24.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
|
|
12264
12768
|
let raw;
|
|
12265
12769
|
try {
|
|
12266
|
-
raw =
|
|
12770
|
+
raw = fs24.readFileSync(file, "utf8");
|
|
12267
12771
|
} catch (error) {
|
|
12268
12772
|
return { ok: false, reason: "unreadable", detail: error.message };
|
|
12269
12773
|
}
|
|
@@ -12306,9 +12810,9 @@ function pruneHookState(state, keep = MAX_TRACKED_SESSIONS) {
|
|
|
12306
12810
|
}
|
|
12307
12811
|
function writeHookState(state, env = process.env) {
|
|
12308
12812
|
const dir = flatbreadHome(env);
|
|
12309
|
-
|
|
12813
|
+
fs24.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
12310
12814
|
const file = hookStatePath(env);
|
|
12311
|
-
|
|
12815
|
+
fs24.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
|
|
12312
12816
|
encoding: "utf8",
|
|
12313
12817
|
mode: 384
|
|
12314
12818
|
});
|
|
@@ -12454,7 +12958,7 @@ async function runHookNotify(io) {
|
|
|
12454
12958
|
const stdout = io.stdout ?? ((line) => process.stdout.write(line + "\n"));
|
|
12455
12959
|
const stderr = io.stderr ?? ((line) => process.stderr.write(line + "\n"));
|
|
12456
12960
|
const now = io.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
12457
|
-
const exists = io.exists ?? ((filePath) =>
|
|
12961
|
+
const exists = io.exists ?? ((filePath) => fs24.existsSync(filePath));
|
|
12458
12962
|
const execPath = io.execPath ?? process.execPath;
|
|
12459
12963
|
const entryPath = io.entryPath ?? process.argv[1] ?? "";
|
|
12460
12964
|
try {
|
|
@@ -12611,10 +13115,10 @@ function removeHookConfig(settings) {
|
|
|
12611
13115
|
return { settings: root, removed };
|
|
12612
13116
|
}
|
|
12613
13117
|
function readSettingsFile(file) {
|
|
12614
|
-
if (!
|
|
13118
|
+
if (!fs24.existsSync(file)) return { present: false, file };
|
|
12615
13119
|
let raw;
|
|
12616
13120
|
try {
|
|
12617
|
-
raw =
|
|
13121
|
+
raw = fs24.readFileSync(file, "utf8");
|
|
12618
13122
|
} catch (error) {
|
|
12619
13123
|
return { present: true, file, unreadable: error.message };
|
|
12620
13124
|
}
|
|
@@ -12626,10 +13130,10 @@ function readSettingsFile(file) {
|
|
|
12626
13130
|
}
|
|
12627
13131
|
}
|
|
12628
13132
|
function writeSettingsFile(file, settings) {
|
|
12629
|
-
|
|
13133
|
+
fs24.mkdirSync(path23.dirname(file), { recursive: true });
|
|
12630
13134
|
const temp = `${file}.flatbread-${process.pid}.tmp`;
|
|
12631
|
-
|
|
12632
|
-
|
|
13135
|
+
fs24.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
13136
|
+
fs24.renameSync(temp, file);
|
|
12633
13137
|
}
|
|
12634
13138
|
function handlersFor(execPath, entryPath, capture = false) {
|
|
12635
13139
|
return {
|
|
@@ -12787,8 +13291,8 @@ function runHookUninstall(options = {}) {
|
|
|
12787
13291
|
function settingsCandidates(env, cwd) {
|
|
12788
13292
|
return [
|
|
12789
13293
|
{ label: "user", file: claudeSettingsPath(env) },
|
|
12790
|
-
{ label: "project", file:
|
|
12791
|
-
{ label: "local", file:
|
|
13294
|
+
{ label: "project", file: path23.join(cwd, ".claude", "settings.json") },
|
|
13295
|
+
{ label: "local", file: path23.join(cwd, ".claude", "settings.local.json") }
|
|
12792
13296
|
];
|
|
12793
13297
|
}
|
|
12794
13298
|
function installedEventsIn(parsed, exists) {
|
|
@@ -12818,7 +13322,7 @@ function runHookStatus(options = {}) {
|
|
|
12818
13322
|
const env = options.env ?? process.env;
|
|
12819
13323
|
const log = options.log ?? ((line) => console.log(line));
|
|
12820
13324
|
const cwd = options.cwd ?? process.cwd();
|
|
12821
|
-
const exists = options.exists ?? ((filePath) =>
|
|
13325
|
+
const exists = options.exists ?? ((filePath) => fs24.existsSync(filePath));
|
|
12822
13326
|
const candidates = options.settingsPath === void 0 ? settingsCandidates(env, cwd) : [{ label: "settings", file: options.settingsPath }];
|
|
12823
13327
|
log("");
|
|
12824
13328
|
log("flatbread hook");
|
|
@@ -12939,21 +13443,27 @@ function probeHandler(handler, run = spawnSync2) {
|
|
|
12939
13443
|
}
|
|
12940
13444
|
|
|
12941
13445
|
// src/skill.ts
|
|
12942
|
-
import
|
|
12943
|
-
import
|
|
13446
|
+
import fs25 from "node:fs";
|
|
13447
|
+
import path24 from "node:path";
|
|
12944
13448
|
var SKILL_NAME = "flatbread";
|
|
12945
13449
|
var MARKER = "<!-- written by flatbread setup; flatbread setup --uninstall removes it -->";
|
|
12946
|
-
var DESCRIPTION =
|
|
13450
|
+
var DESCRIPTION = `Search recorded AI coding-agent runs - this machine's own and public ones on flatbread.io - for one that already did this task or hit this error. Use BEFORE your first edit in any coding task, however small it looks; when a command, test or build fails; when the user says it was done or fixed before; and before you call a change done. Also when the user says "share this run", "flatbread share" or "share to flatbread".`;
|
|
12947
13451
|
var BODY = `Search Flatbread once before your first edit in a coding task, even one that looks small and well-defined. A small task is exactly the one someone has already done, and the search costs one call. Then search again whenever a command, a test or a build fails, with the exact error line as the query.
|
|
12948
13452
|
|
|
12949
13453
|
## How
|
|
12950
13454
|
|
|
12951
|
-
1. Call \`flatbread_search_traces\` with two to four specific words from the task or the error
|
|
13455
|
+
1. Call \`flatbread_search_traces\` with two to four specific words from the task or the error. Runs with every word come first; when none has them all, the closest are listed with how many words they share - open one only if its title is the same job. Try a second wording before deciding nothing is there.
|
|
12952
13456
|
2. If a hit looks like the same job, call \`flatbread_get_capsule\` with its run ID. The capsule is that run's own record: what was asked, how the person steered it, each blocker and the fix, what the tests said, the diff.
|
|
12953
13457
|
3. Use what it says. A blocker listed under "do not repeat" is a dead end someone already paid for: do not walk into it again.
|
|
12954
13458
|
|
|
12955
13459
|
If the tools are not connected in this session, the same search runs in the shell: \`flatbread search <words>\` and \`flatbread pull <run id>\`.
|
|
12956
13460
|
|
|
13461
|
+
## When the user asks to share this run
|
|
13462
|
+
|
|
13463
|
+
"Share this run", "flatbread share", "share to flatbread", "save this run": call \`flatbread_share\`. Do not open a terminal or look for the session yourself - the tool finds this session, leaves the share request out, and opens a review page in the user's browser. If the user wants only part of the session ("share it from where I asked about the retry"), pass \`from\` with that prompt's number or words from it; if the tool lists the prompts instead, ask the user which one. Then tell the user the page is open, in one line: nothing is kept or uploaded until they press Confirm there, and the page shows the link. Do not wait for it.
|
|
13464
|
+
|
|
13465
|
+
Without the tool connected, the shell does the same with the person at the terminal: \`flatbread share\` (\`--from <n|words>\` for a part).
|
|
13466
|
+
|
|
12957
13467
|
## Saying what you used
|
|
12958
13468
|
|
|
12959
13469
|
When a capsule changed what you did, say so in one line at the start of your reply, with the run ID: "Flatbread: <run id> hit this before - <what it said>." Say nothing about searches that found nothing useful.
|
|
@@ -12982,7 +13492,7 @@ var ANTIGRAVITY_PLUGIN_MANIFEST = `${JSON.stringify({ name: SKILL_NAME }, null,
|
|
|
12982
13492
|
`;
|
|
12983
13493
|
function isDir(dir) {
|
|
12984
13494
|
try {
|
|
12985
|
-
return
|
|
13495
|
+
return fs25.statSync(dir).isDirectory();
|
|
12986
13496
|
} catch {
|
|
12987
13497
|
return false;
|
|
12988
13498
|
}
|
|
@@ -12990,32 +13500,32 @@ function isDir(dir) {
|
|
|
12990
13500
|
function skillTargets(env) {
|
|
12991
13501
|
const home = homeOf3(env);
|
|
12992
13502
|
if (home === null) return [];
|
|
12993
|
-
const claudeDir = env["CLAUDE_CONFIG_DIR"]?.trim() ||
|
|
13503
|
+
const claudeDir = env["CLAUDE_CONFIG_DIR"]?.trim() || path24.join(home, ".claude");
|
|
12994
13504
|
const targets = [
|
|
12995
|
-
{ file:
|
|
12996
|
-
{ file:
|
|
13505
|
+
{ file: path24.join(claudeDir, "skills", SKILL_NAME, "SKILL.md") },
|
|
13506
|
+
{ file: path24.join(home, ".agents", "skills", SKILL_NAME, "SKILL.md") }
|
|
12997
13507
|
];
|
|
12998
|
-
const gemini =
|
|
12999
|
-
if (["antigravity", "antigravity-cli", "config"].some((dir) => isDir(
|
|
13000
|
-
const plugin =
|
|
13508
|
+
const gemini = path24.join(home, ".gemini");
|
|
13509
|
+
if (["antigravity", "antigravity-cli", "config"].some((dir) => isDir(path24.join(gemini, dir)))) {
|
|
13510
|
+
const plugin = path24.join(gemini, "config", "plugins", SKILL_NAME);
|
|
13001
13511
|
targets.push({
|
|
13002
|
-
file:
|
|
13003
|
-
companion: { file:
|
|
13512
|
+
file: path24.join(plugin, "skills", SKILL_NAME, "SKILL.md"),
|
|
13513
|
+
companion: { file: path24.join(plugin, "plugin.json"), text: ANTIGRAVITY_PLUGIN_MANIFEST }
|
|
13004
13514
|
});
|
|
13005
13515
|
}
|
|
13006
|
-
const opencode =
|
|
13007
|
-
if (isDir(opencode)) targets.push({ file:
|
|
13516
|
+
const opencode = path24.join(env["XDG_CONFIG_HOME"]?.trim() || path24.join(home, ".config"), "opencode");
|
|
13517
|
+
if (isDir(opencode)) targets.push({ file: path24.join(opencode, "skills", SKILL_NAME, "SKILL.md") });
|
|
13008
13518
|
return targets;
|
|
13009
13519
|
}
|
|
13010
13520
|
function removeIfEmpty(dir) {
|
|
13011
13521
|
try {
|
|
13012
|
-
|
|
13522
|
+
fs25.rmdirSync(dir);
|
|
13013
13523
|
} catch {
|
|
13014
13524
|
}
|
|
13015
13525
|
}
|
|
13016
13526
|
function readOrNull(file) {
|
|
13017
13527
|
try {
|
|
13018
|
-
return
|
|
13528
|
+
return fs25.readFileSync(file, "utf8");
|
|
13019
13529
|
} catch {
|
|
13020
13530
|
return null;
|
|
13021
13531
|
}
|
|
@@ -13045,12 +13555,12 @@ function installSkill(options) {
|
|
|
13045
13555
|
options.log(` skill would remove ${file}`);
|
|
13046
13556
|
continue;
|
|
13047
13557
|
}
|
|
13048
|
-
|
|
13049
|
-
removeIfEmpty(
|
|
13558
|
+
fs25.rmSync(file);
|
|
13559
|
+
removeIfEmpty(path24.dirname(file));
|
|
13050
13560
|
if (companion !== void 0) {
|
|
13051
|
-
removeIfEmpty(
|
|
13052
|
-
if (readOrNull(companion.file) === companion.text)
|
|
13053
|
-
removeIfEmpty(
|
|
13561
|
+
removeIfEmpty(path24.dirname(path24.dirname(file)));
|
|
13562
|
+
if (readOrNull(companion.file) === companion.text) fs25.rmSync(companion.file);
|
|
13563
|
+
removeIfEmpty(path24.dirname(companion.file));
|
|
13054
13564
|
}
|
|
13055
13565
|
options.log(` skill removed ${file}`);
|
|
13056
13566
|
continue;
|
|
@@ -13063,9 +13573,9 @@ function installSkill(options) {
|
|
|
13063
13573
|
options.log(` skill would ${current === null ? "write" : "update"} ${file}`);
|
|
13064
13574
|
continue;
|
|
13065
13575
|
}
|
|
13066
|
-
|
|
13067
|
-
if (companion !== void 0 && readOrNull(companion.file) === null)
|
|
13068
|
-
|
|
13576
|
+
fs25.mkdirSync(path24.dirname(file), { recursive: true });
|
|
13577
|
+
if (companion !== void 0 && readOrNull(companion.file) === null) fs25.writeFileSync(companion.file, companion.text);
|
|
13578
|
+
fs25.writeFileSync(file, text2);
|
|
13069
13579
|
options.log(` skill ${current === null ? "wrote" : "updated"} ${file}`);
|
|
13070
13580
|
}
|
|
13071
13581
|
return code;
|
|
@@ -13074,7 +13584,7 @@ function installSkill(options) {
|
|
|
13074
13584
|
// src/setup.ts
|
|
13075
13585
|
function isDir2(dir) {
|
|
13076
13586
|
try {
|
|
13077
|
-
return
|
|
13587
|
+
return fs26.statSync(dir).isDirectory();
|
|
13078
13588
|
} catch {
|
|
13079
13589
|
return false;
|
|
13080
13590
|
}
|
|
@@ -13090,17 +13600,17 @@ function otherAgents(env, step) {
|
|
|
13090
13600
|
const entry = { command: step.execPath, args: [step.entryPath, "mcp"] };
|
|
13091
13601
|
let code = 0;
|
|
13092
13602
|
log("Codex");
|
|
13093
|
-
const codexHome2 = env["CODEX_HOME"]?.trim() ||
|
|
13603
|
+
const codexHome2 = env["CODEX_HOME"]?.trim() || path25.join(home, ".codex");
|
|
13094
13604
|
if (!isDir2(codexHome2)) log(` skipped not installed here (no ${codexHome2})`);
|
|
13095
13605
|
else code = Math.max(code, registerCodexMcp({ file: codexConfigPath(codexHome2), ...step }));
|
|
13096
13606
|
if (isDir2(codexHome2) && !step.uninstall) log(" skill Codex reads the shared ~/.agents/skills copy above");
|
|
13097
13607
|
log("");
|
|
13098
13608
|
log("Cursor");
|
|
13099
|
-
const cursorHome =
|
|
13609
|
+
const cursorHome = path25.join(home, ".cursor");
|
|
13100
13610
|
if (!isDir2(cursorHome)) log(` skipped not installed here (no ${cursorHome})`);
|
|
13101
13611
|
else {
|
|
13102
13612
|
code = Math.max(code, registerMcpJson({
|
|
13103
|
-
file:
|
|
13613
|
+
file: path25.join(cursorHome, "mcp.json"),
|
|
13104
13614
|
entry,
|
|
13105
13615
|
app: "Cursor",
|
|
13106
13616
|
scope: "for every project",
|
|
@@ -13110,12 +13620,12 @@ function otherAgents(env, step) {
|
|
|
13110
13620
|
}
|
|
13111
13621
|
log("");
|
|
13112
13622
|
log("Antigravity");
|
|
13113
|
-
const gemini =
|
|
13114
|
-
if (!["antigravity", "antigravity-cli", "config"].some((dir) => isDir2(
|
|
13115
|
-
log(` skipped not installed here (no ${
|
|
13623
|
+
const gemini = path25.join(home, ".gemini");
|
|
13624
|
+
if (!["antigravity", "antigravity-cli", "config"].some((dir) => isDir2(path25.join(gemini, dir)))) {
|
|
13625
|
+
log(` skipped not installed here (no ${path25.join(gemini, "antigravity")})`);
|
|
13116
13626
|
} else {
|
|
13117
13627
|
code = Math.max(code, registerMcpJson({
|
|
13118
|
-
file:
|
|
13628
|
+
file: path25.join(gemini, "config", "mcp_config.json"),
|
|
13119
13629
|
entry,
|
|
13120
13630
|
app: "Antigravity",
|
|
13121
13631
|
scope: "for Antigravity",
|
|
@@ -13418,13 +13928,13 @@ async function bindFirst(server, ports) {
|
|
|
13418
13928
|
}
|
|
13419
13929
|
|
|
13420
13930
|
// src/index.ts
|
|
13421
|
-
var PACKAGE_ROOT =
|
|
13422
|
-
var SOURCE_CHECKOUT =
|
|
13423
|
-
var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ?
|
|
13424
|
-
var OUTPUT_PATH =
|
|
13425
|
-
var SHARE_OUTPUT_PATH =
|
|
13931
|
+
var PACKAGE_ROOT = path26.resolve(path26.dirname(fileURLToPath2(import.meta.url)), "..");
|
|
13932
|
+
var SOURCE_CHECKOUT = fs27.existsSync(path26.join(PACKAGE_ROOT, "src", "index.ts"));
|
|
13933
|
+
var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ? path26.join(PACKAGE_ROOT, ".tmp") : path26.join(os9.homedir(), ".flatbread"));
|
|
13934
|
+
var OUTPUT_PATH = path26.join(TMP_DIR, "last-parse.json");
|
|
13935
|
+
var SHARE_OUTPUT_PATH = path26.join(TMP_DIR, "last-share.json");
|
|
13426
13936
|
function storeDir() {
|
|
13427
|
-
return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null :
|
|
13937
|
+
return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path26.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
|
|
13428
13938
|
}
|
|
13429
13939
|
var USAGE = `flatbread - registry of AI agent runs
|
|
13430
13940
|
|
|
@@ -13437,8 +13947,9 @@ Usage:
|
|
|
13437
13947
|
flatbread pull <id|url> Print a run's Context Capsule: a run this machine shared or
|
|
13438
13948
|
captured is read here, anything else from the site
|
|
13439
13949
|
flatbread pull <id|url> --out <file> Write it to a file instead
|
|
13440
|
-
flatbread mcp MCP server on stdio
|
|
13441
|
-
flatbread_get_capsule,
|
|
13950
|
+
flatbread mcp MCP server on stdio for your coding agent:
|
|
13951
|
+
flatbread_search_traces, flatbread_get_capsule, and
|
|
13952
|
+
flatbread_share ("share this run" in the chat)
|
|
13442
13953
|
flatbread search <words> Find runs: this machine's shared and captured ones, then
|
|
13443
13954
|
the site (the same answer the MCP server gives)
|
|
13444
13955
|
flatbread setup Auto-capture: the Claude Code hook, plus a chained git
|
|
@@ -13474,6 +13985,8 @@ Options for share:
|
|
|
13474
13985
|
opens with it and can still change it
|
|
13475
13986
|
--commit [sha] Bind the run to a commit (bare --commit = HEAD) and record
|
|
13476
13987
|
whether the working tree was clean against it
|
|
13988
|
+
--from <n|words> Share only the part from that prompt to the end: its
|
|
13989
|
+
number, or words from it. The review page can change it
|
|
13477
13990
|
--no-review Redact every match, skip the browser (non-interactive)
|
|
13478
13991
|
--no-open Start the review server but do not launch a browser
|
|
13479
13992
|
-h, --help Show this message
|
|
@@ -13510,13 +14023,14 @@ Options for adapters:
|
|
|
13510
14023
|
Notes:
|
|
13511
14024
|
parse and share record the git branch, HEAD commit and uncommitted file names when the
|
|
13512
14025
|
session ran inside a git repository. Nothing else from git is read.
|
|
13513
|
-
parse --save writes ${
|
|
13514
|
-
Share drafts are written to ${
|
|
14026
|
+
parse --save writes ${path26.relative(process.cwd(), OUTPUT_PATH) || OUTPUT_PATH}
|
|
14027
|
+
Share drafts are written to ${path26.relative(process.cwd(), SHARE_OUTPUT_PATH) || SHARE_OUTPUT_PATH}
|
|
13515
14028
|
(the latest only; every shared run is also kept under its ID, for search and pull)
|
|
13516
14029
|
Every parsed run defaults to visibility "private".
|
|
13517
|
-
Nothing is uploaded unless you pass --publish,
|
|
13518
|
-
FLATBREAD_API_TOKEN yourself
|
|
13519
|
-
|
|
14030
|
+
Nothing is uploaded unless you pass --publish, set FLATBREAD_API_URL and
|
|
14031
|
+
FLATBREAD_API_TOKEN yourself, or press Confirm on a review your agent opened with
|
|
14032
|
+
flatbread_share. pull and search only read: public runs, your own when you are signed
|
|
14033
|
+
in, and a private run through its share link. The review UI listens on
|
|
13520
14034
|
127.0.0.1 only, on 4321-4325 or, when all five are busy, a port the OS picks.
|
|
13521
14035
|
setup registers the MCP server in ~/.claude.json, for every project. By hand instead:
|
|
13522
14036
|
claude mcp add --scope user flatbread -- flatbread mcp
|
|
@@ -13548,10 +14062,10 @@ function parseArgv(argv) {
|
|
|
13548
14062
|
} else if (arg === "--out") {
|
|
13549
14063
|
const value = argv[i + 1];
|
|
13550
14064
|
if (value === void 0 || value.startsWith("--")) throw new Error("--out needs a file path");
|
|
13551
|
-
args.out =
|
|
14065
|
+
args.out = path26.resolve(value);
|
|
13552
14066
|
i += 1;
|
|
13553
14067
|
} else if (arg !== void 0 && arg.startsWith("--out=")) {
|
|
13554
|
-
args.out =
|
|
14068
|
+
args.out = path26.resolve(arg.slice("--out=".length));
|
|
13555
14069
|
} else if (arg === "--harness") {
|
|
13556
14070
|
const value = argv[i + 1];
|
|
13557
14071
|
if (value === void 0 || value.startsWith("--")) {
|
|
@@ -13672,12 +14186,12 @@ async function runParse(args) {
|
|
|
13672
14186
|
target = found[0];
|
|
13673
14187
|
console.log(`latest ${harness} session: ${target}`);
|
|
13674
14188
|
} else if (args.file) {
|
|
13675
|
-
target =
|
|
13676
|
-
if (!
|
|
14189
|
+
target = path26.resolve(args.file);
|
|
14190
|
+
if (!fs27.existsSync(target)) {
|
|
13677
14191
|
console.error(`file not found: ${target}`);
|
|
13678
14192
|
return 1;
|
|
13679
14193
|
}
|
|
13680
|
-
if (!
|
|
14194
|
+
if (!fs27.statSync(target).isFile()) {
|
|
13681
14195
|
console.error(`not a file: ${target}`);
|
|
13682
14196
|
return 1;
|
|
13683
14197
|
}
|
|
@@ -13708,8 +14222,8 @@ async function runParse(args) {
|
|
|
13708
14222
|
const trace = withLineage(result.trace, readGitLineage(result.trace.source.cwd));
|
|
13709
14223
|
FlatbreadTraceSchema.parse(trace);
|
|
13710
14224
|
if (args.out !== void 0) {
|
|
13711
|
-
|
|
13712
|
-
|
|
14225
|
+
fs27.mkdirSync(path26.dirname(args.out), { recursive: true });
|
|
14226
|
+
fs27.writeFileSync(args.out, JSON.stringify(trace, null, 2) + "\n", "utf8");
|
|
13713
14227
|
}
|
|
13714
14228
|
report({ ...result, trace }, args.out);
|
|
13715
14229
|
return 0;
|
|
@@ -13773,8 +14287,8 @@ async function runAdapters(args) {
|
|
|
13773
14287
|
}
|
|
13774
14288
|
let input = null;
|
|
13775
14289
|
if (args.file !== void 0) {
|
|
13776
|
-
const resolved =
|
|
13777
|
-
if (!
|
|
14290
|
+
const resolved = path26.resolve(args.file);
|
|
14291
|
+
if (!fs27.existsSync(resolved)) {
|
|
13778
14292
|
console.error(`file not found: ${resolved}`);
|
|
13779
14293
|
return 1;
|
|
13780
14294
|
}
|
|
@@ -13842,6 +14356,12 @@ function parseShareArgv(argv) {
|
|
|
13842
14356
|
}
|
|
13843
14357
|
} else if (arg !== void 0 && arg.startsWith("--commit=")) {
|
|
13844
14358
|
args.commit = checkCommitRev(arg.slice("--commit=".length));
|
|
14359
|
+
} else if (arg === "--from" || arg !== void 0 && arg.startsWith("--from=")) {
|
|
14360
|
+
const value = arg === "--from" ? argv[i += 1] : arg.slice("--from=".length);
|
|
14361
|
+
if (value === void 0 || value.startsWith("--") || value.trim().length === 0) {
|
|
14362
|
+
throw new Error('--from needs a prompt number or words from that prompt, e.g. --from 3 or --from "retry helper"');
|
|
14363
|
+
}
|
|
14364
|
+
args.from = value;
|
|
13845
14365
|
} else if (arg === "--file") {
|
|
13846
14366
|
const value = argv[i + 1];
|
|
13847
14367
|
if (value === void 0 || value.startsWith("--")) {
|
|
@@ -14208,9 +14728,9 @@ async function runPull(args, deps = { ...defaultMcpDeps(storeDir(), bucketDir())
|
|
|
14208
14728
|
process.stdout.write(result.capsule);
|
|
14209
14729
|
return 0;
|
|
14210
14730
|
}
|
|
14211
|
-
const target =
|
|
14212
|
-
|
|
14213
|
-
|
|
14731
|
+
const target = path26.resolve(args.out);
|
|
14732
|
+
fs27.mkdirSync(path26.dirname(target), { recursive: true });
|
|
14733
|
+
fs27.writeFileSync(target, result.capsule, "utf8");
|
|
14214
14734
|
console.error(`wrote ${target}${result.from === "site" ? "" : ` (from this machine's ${result.from === "store" ? "shared runs" : "captured runs"})`}`);
|
|
14215
14735
|
return 0;
|
|
14216
14736
|
}
|
|
@@ -14403,7 +14923,7 @@ async function main(argv) {
|
|
|
14403
14923
|
console.error(`mcp takes no options, got: ${rest.join(" ")}`);
|
|
14404
14924
|
return 1;
|
|
14405
14925
|
}
|
|
14406
|
-
return runMcpServer(defaultMcpDeps(storeDir(), bucketDir()));
|
|
14926
|
+
return runMcpServer(defaultMcpDeps(storeDir(), bucketDir(), SHARE_OUTPUT_PATH));
|
|
14407
14927
|
}
|
|
14408
14928
|
if (command === "pull") {
|
|
14409
14929
|
let pullArgs;
|
|
@@ -14438,6 +14958,7 @@ async function main(argv) {
|
|
|
14438
14958
|
...shareArgs.prUrl !== void 0 ? { prUrl: shareArgs.prUrl } : {},
|
|
14439
14959
|
...shareArgs.title !== void 0 ? { title: shareArgs.title } : {},
|
|
14440
14960
|
...shareArgs.commit !== void 0 ? { commit: shareArgs.commit } : {},
|
|
14961
|
+
...shareArgs.from !== void 0 ? { from: shareArgs.from } : {},
|
|
14441
14962
|
...storeDir() !== null ? { storeDir: storeDir() } : {},
|
|
14442
14963
|
openBrowser: shareArgs.open && !shareArgs.noReview,
|
|
14443
14964
|
outputPath: SHARE_OUTPUT_PATH
|
|
@@ -14458,9 +14979,9 @@ var invokedDirectly = (() => {
|
|
|
14458
14979
|
if (entry === void 0) return false;
|
|
14459
14980
|
const real = (file) => {
|
|
14460
14981
|
try {
|
|
14461
|
-
return
|
|
14982
|
+
return fs27.realpathSync(file);
|
|
14462
14983
|
} catch {
|
|
14463
|
-
return
|
|
14984
|
+
return path26.resolve(file);
|
|
14464
14985
|
}
|
|
14465
14986
|
};
|
|
14466
14987
|
return real(entry) === real(fileURLToPath2(import.meta.url));
|