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