flatbread-cli 0.1.0 → 0.3.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 +49 -6
- package/THIRD-PARTY-NOTICES.md +31 -0
- package/dist/flatbread.mjs +1442 -282
- package/package.json +3 -2
package/dist/flatbread.mjs
CHANGED
|
@@ -6,11 +6,11 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
|
-
import { spawn as
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
10
|
+
import fs22 from "node:fs";
|
|
11
|
+
import os8 from "node:os";
|
|
12
|
+
import path21 from "node:path";
|
|
13
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
14
14
|
|
|
15
15
|
// ../../node_modules/zod/v3/external.js
|
|
16
16
|
var external_exports = {};
|
|
@@ -490,8 +490,8 @@ function getErrorMap() {
|
|
|
490
490
|
|
|
491
491
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
492
492
|
var makeIssue = (params) => {
|
|
493
|
-
const { data, path:
|
|
494
|
-
const fullPath = [...
|
|
493
|
+
const { data, path: path22, errorMaps, issueData } = params;
|
|
494
|
+
const fullPath = [...path22, ...issueData.path || []];
|
|
495
495
|
const fullIssue = {
|
|
496
496
|
...issueData,
|
|
497
497
|
path: fullPath
|
|
@@ -607,11 +607,11 @@ var errorUtil;
|
|
|
607
607
|
|
|
608
608
|
// ../../node_modules/zod/v3/types.js
|
|
609
609
|
var ParseInputLazyPath = class {
|
|
610
|
-
constructor(parent, value,
|
|
610
|
+
constructor(parent, value, path22, key) {
|
|
611
611
|
this._cachedPath = [];
|
|
612
612
|
this.parent = parent;
|
|
613
613
|
this.data = value;
|
|
614
|
-
this._path =
|
|
614
|
+
this._path = path22;
|
|
615
615
|
this._key = key;
|
|
616
616
|
}
|
|
617
617
|
get path() {
|
|
@@ -4340,6 +4340,15 @@ var VERDICT_WORDS = {
|
|
|
4340
4340
|
"not-rerun": { short: "not re-run", phrase: "no test after the last edit" },
|
|
4341
4341
|
"no-edits": { short: "no edits", phrase: null }
|
|
4342
4342
|
};
|
|
4343
|
+
function treeWords(lineage) {
|
|
4344
|
+
if (lineage?.dirtyFiles === void 0) return null;
|
|
4345
|
+
const total = lineage.dirtyFiles.length + (lineage.dirtyFilesOmitted ?? 0);
|
|
4346
|
+
if (lineage.commitNamed === true) {
|
|
4347
|
+
if (total === 0) return "clean at that commit";
|
|
4348
|
+
return total === 1 ? "1 file differs from that commit" : `${total} files differ from that commit`;
|
|
4349
|
+
}
|
|
4350
|
+
return total === 0 ? "clean tree" : `${total} uncommitted file${total === 1 ? "" : "s"}`;
|
|
4351
|
+
}
|
|
4343
4352
|
function escapeRegExp(value) {
|
|
4344
4353
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4345
4354
|
}
|
|
@@ -4375,8 +4384,8 @@ function firstLine(text2) {
|
|
|
4375
4384
|
function labelOf(step, cwd) {
|
|
4376
4385
|
const tool = step.tool;
|
|
4377
4386
|
if (tool === void 0) return step.type;
|
|
4378
|
-
const
|
|
4379
|
-
if (
|
|
4387
|
+
const path22 = firstString(tool.input, FILE_PATH_KEYS);
|
|
4388
|
+
if (path22 !== void 0) return `${tool.name} ${repoRelative(path22, cwd)}`;
|
|
4380
4389
|
const other = firstString(tool.input, TARGET_KEYS);
|
|
4381
4390
|
if (other !== void 0) return cut(`${tool.name}: ${repoRelative(firstLine(other), cwd)}`, BRIEF_LIMITS.quote);
|
|
4382
4391
|
return tool.name;
|
|
@@ -4624,9 +4633,9 @@ function runDiagnostics(trace) {
|
|
|
4624
4633
|
searches.set(key, found);
|
|
4625
4634
|
}
|
|
4626
4635
|
}
|
|
4627
|
-
for (const [
|
|
4636
|
+
for (const [path22, entries] of reads) {
|
|
4628
4637
|
const hit = densest(entries, WASTE_RULES.repeatedRead.times, WASTE_RULES.repeatedRead.windowMinutes * 6e4);
|
|
4629
|
-
if (hit) waste.push({ kind: "repeated-read", path:
|
|
4638
|
+
if (hit) waste.push({ kind: "repeated-read", path: path22, steps: hit, windowMinutes: WASTE_RULES.repeatedRead.windowMinutes });
|
|
4630
4639
|
}
|
|
4631
4640
|
for (const { label, entries } of searches.values()) {
|
|
4632
4641
|
const hit = densest(entries, WASTE_RULES.repeatedSearch.times, WASTE_RULES.repeatedSearch.windowMinutes * 6e4);
|
|
@@ -4704,19 +4713,19 @@ function filesOf(steps, cwd) {
|
|
|
4704
4713
|
const patches = [];
|
|
4705
4714
|
steps.forEach((step, index) => {
|
|
4706
4715
|
if (!isSuccessfulEdit(step) || step.tool === void 0) return;
|
|
4707
|
-
const
|
|
4708
|
-
const file = files.get(
|
|
4716
|
+
const path22 = repoRelative(firstString(step.tool.input, FILE_PATH_KEYS), cwd);
|
|
4717
|
+
const file = files.get(path22) ?? { path: path22 };
|
|
4709
4718
|
if (step.diff !== void 0) {
|
|
4710
4719
|
const counted = countPatch(step.diff.patch);
|
|
4711
4720
|
file.added = (file.added ?? 0) + counted.added;
|
|
4712
4721
|
file.removed = (file.removed ?? 0) + counted.removed;
|
|
4713
|
-
patches.push({ path:
|
|
4722
|
+
patches.push({ path: path22, patch: repoRelative(step.diff.patch, cwd), step: step.stepNumber });
|
|
4714
4723
|
}
|
|
4715
4724
|
const result = resultOf(steps, index);
|
|
4716
4725
|
if (step.tool.name.toLowerCase() === "write" && result !== void 0 && /^File created successfully\b/.test(result.trim())) {
|
|
4717
4726
|
file.created = true;
|
|
4718
4727
|
}
|
|
4719
|
-
files.set(
|
|
4728
|
+
files.set(path22, file);
|
|
4720
4729
|
});
|
|
4721
4730
|
return { files: [...files.values()], patches };
|
|
4722
4731
|
}
|
|
@@ -4748,7 +4757,8 @@ function buildExecutionBrief(trace) {
|
|
|
4748
4757
|
...source.dirtyFiles !== void 0 ? { dirtyFiles: source.dirtyFiles } : {},
|
|
4749
4758
|
...source.dirtyFilesOmitted !== void 0 ? { dirtyFilesOmitted: source.dirtyFilesOmitted } : {},
|
|
4750
4759
|
...source.gitCapturedAt !== void 0 ? { capturedAt: source.gitCapturedAt } : {},
|
|
4751
|
-
...source.prUrl !== void 0 ? { prUrl: source.prUrl } : {}
|
|
4760
|
+
...source.prUrl !== void 0 ? { prUrl: source.prUrl } : {},
|
|
4761
|
+
...source.commitNamed === true ? { commitNamed: true } : {}
|
|
4752
4762
|
};
|
|
4753
4763
|
return {
|
|
4754
4764
|
intent: {
|
|
@@ -4800,11 +4810,11 @@ var HARNESS_INFO = {
|
|
|
4800
4810
|
antigravity: {
|
|
4801
4811
|
id: "antigravity",
|
|
4802
4812
|
label: "Google Antigravity",
|
|
4803
|
-
formatLabel: "
|
|
4804
|
-
confidence: "
|
|
4805
|
-
implemented:
|
|
4813
|
+
formatLabel: "IDE conversations/<id>.pb (Windsurf's encrypted trajectory)",
|
|
4814
|
+
confidence: "documented",
|
|
4815
|
+
implemented: true,
|
|
4806
4816
|
humanTest: "A6",
|
|
4807
|
-
note: "
|
|
4817
|
+
note: "The IDE only, prompts and replies only. Read the way deja-vu reads Windsurf, which held on all nine IDE 1.23.2 conversations measured. Tool calls are undocumented and not read; the standalone Antigravity app is not read yet."
|
|
4808
4818
|
},
|
|
4809
4819
|
terminal: {
|
|
4810
4820
|
id: "terminal",
|
|
@@ -4879,14 +4889,14 @@ var WHEN = {
|
|
|
4879
4889
|
"no-edits": "in a run with no recorded edit"
|
|
4880
4890
|
};
|
|
4881
4891
|
function verificationLine(brief) {
|
|
4882
|
-
const
|
|
4892
|
+
const runs2 = brief.outcome.verification;
|
|
4883
4893
|
const verdict = brief.outcome.afterLastEdit;
|
|
4884
|
-
if (
|
|
4894
|
+
if (runs2.length === 0) {
|
|
4885
4895
|
return verdict === "no-edits" ? null : "Verification: no test run was recorded.";
|
|
4886
4896
|
}
|
|
4887
4897
|
const shown = [];
|
|
4888
4898
|
for (const when of ["before-first-edit", "between-edits", "after-last-edit", "no-edits"]) {
|
|
4889
|
-
const group =
|
|
4899
|
+
const group = runs2.filter((run) => run.when === when);
|
|
4890
4900
|
const last = group[group.length - 1];
|
|
4891
4901
|
if (last !== void 0) shown.push(last);
|
|
4892
4902
|
}
|
|
@@ -4894,7 +4904,7 @@ function verificationLine(brief) {
|
|
|
4894
4904
|
const result = run.result === "unknown" ? "ran (result not recorded)" : run.result;
|
|
4895
4905
|
return `${run.command} ${result} ${WHEN[run.when]}${run.counts ? ` (${run.counts})` : ""}.`;
|
|
4896
4906
|
});
|
|
4897
|
-
const omitted =
|
|
4907
|
+
const omitted = runs2.length - shown.length;
|
|
4898
4908
|
if (omitted > 0) parts.push(`${omitted} earlier test run${omitted === 1 ? "" : "s"} not listed.`);
|
|
4899
4909
|
if (verdict === "not-rerun") parts.push("Not re-run after the last edit.");
|
|
4900
4910
|
return `Verification: ${parts.join(" ")}`;
|
|
@@ -4922,11 +4932,11 @@ function lineageLine(brief) {
|
|
|
4922
4932
|
if (lineage === void 0) return null;
|
|
4923
4933
|
const parts = [];
|
|
4924
4934
|
if (lineage.branch !== void 0) parts.push(`branch ${lineage.branch}`);
|
|
4925
|
-
if (lineage.commit !== void 0)
|
|
4926
|
-
|
|
4927
|
-
const total = lineage.dirtyFiles.length + (lineage.dirtyFilesOmitted ?? 0);
|
|
4928
|
-
parts.push(total === 0 ? "clean tree" : `${total} uncommitted file${total === 1 ? "" : "s"}`);
|
|
4935
|
+
if (lineage.commit !== void 0) {
|
|
4936
|
+
parts.push(`commit ${lineage.commit.slice(0, 12)}${lineage.commitNamed === true ? " (named with --commit)" : ""}`);
|
|
4929
4937
|
}
|
|
4938
|
+
const tree = treeWords(lineage);
|
|
4939
|
+
if (tree !== null) parts.push(tree);
|
|
4930
4940
|
if (lineage.prUrl !== void 0) parts.push(`PR ${lineage.prUrl}`);
|
|
4931
4941
|
return parts.length === 0 ? null : `Git: ${parts.join(" \xB7 ")}`;
|
|
4932
4942
|
}
|
|
@@ -5086,13 +5096,13 @@ function categorizeTrace(trace) {
|
|
|
5086
5096
|
};
|
|
5087
5097
|
}
|
|
5088
5098
|
if (paths.length > 0) {
|
|
5089
|
-
if (paths.every((
|
|
5099
|
+
if (paths.every((path22) => matchesAny(path22, DOC_PATTERNS))) {
|
|
5090
5100
|
return { category: "docs", evidence: [`all ${paths.length} touched path(s) are documentation`, ...paths.slice(0, 3)] };
|
|
5091
5101
|
}
|
|
5092
|
-
if (paths.every((
|
|
5102
|
+
if (paths.every((path22) => matchesAny(path22, TEST_PATTERNS))) {
|
|
5093
5103
|
return { category: "tests", evidence: [`all ${paths.length} touched path(s) are tests`, ...paths.slice(0, 3)] };
|
|
5094
5104
|
}
|
|
5095
|
-
if (paths.every((
|
|
5105
|
+
if (paths.every((path22) => matchesAny(path22, INFRA_PATTERNS))) {
|
|
5096
5106
|
return { category: "infra", evidence: [`all ${paths.length} touched path(s) are build/CI/deploy config`, ...paths.slice(0, 3)] };
|
|
5097
5107
|
}
|
|
5098
5108
|
}
|
|
@@ -5259,9 +5269,39 @@ function searchTerms(q) {
|
|
|
5259
5269
|
function rowSearchText(row) {
|
|
5260
5270
|
return normalizeSearch(`${row.title} ${row.slug} ${row.harness} ${HARNESS_INFO[row.harness].label}`);
|
|
5261
5271
|
}
|
|
5272
|
+
var VOWEL = /[aeiouy]/;
|
|
5273
|
+
var UNDOUBLED = /([b-df-hj-km-np-rtv-xz])\1$/;
|
|
5274
|
+
function stem(word) {
|
|
5275
|
+
if (word.length < 4 || !/^[a-z]+$/.test(word)) return word;
|
|
5276
|
+
const keep = (base) => base.length >= 3 && VOWEL.test(base) ? base : word;
|
|
5277
|
+
const undouble = (base) => base.length >= 4 && UNDOUBLED.test(base) && !/(ll|ss|zz)$/.test(base) ? base.slice(0, -1) : base;
|
|
5278
|
+
if (/[^aeiou]ies$/.test(word) || /[^aeiou]ied$/.test(word)) return keep(word.slice(0, -3) + "y");
|
|
5279
|
+
if (word.endsWith("ing")) {
|
|
5280
|
+
const base = word.slice(0, -3);
|
|
5281
|
+
return base.length >= 3 && VOWEL.test(base) ? undouble(base) : word;
|
|
5282
|
+
}
|
|
5283
|
+
if (word.endsWith("ed") && !word.endsWith("eed")) {
|
|
5284
|
+
const base = word.slice(0, -2);
|
|
5285
|
+
return base.length >= 3 && VOWEL.test(base) ? undouble(base) : word;
|
|
5286
|
+
}
|
|
5287
|
+
if (/(?:ss|x|z|ch|sh)es$/.test(word)) return keep(word.slice(0, -2));
|
|
5288
|
+
if (word.endsWith("s") && !/(?:ss|us|is)$/.test(word)) return keep(word.slice(0, -1));
|
|
5289
|
+
return word;
|
|
5290
|
+
}
|
|
5291
|
+
function stemText(text2) {
|
|
5292
|
+
return text2.split(/[^\p{L}\p{N}]+/u).filter((word) => word.length > 0).map(stem).join(" ");
|
|
5293
|
+
}
|
|
5262
5294
|
function matchesSearch(row, q) {
|
|
5263
|
-
|
|
5264
|
-
|
|
5295
|
+
return matchesText(rowSearchText(row), q);
|
|
5296
|
+
}
|
|
5297
|
+
function matchesText(text2, q) {
|
|
5298
|
+
const haystack = normalizeSearch(text2);
|
|
5299
|
+
const stems = ` ${stemText(haystack)} `;
|
|
5300
|
+
return searchTerms(q).every((term) => {
|
|
5301
|
+
if (haystack.includes(term)) return true;
|
|
5302
|
+
const stemmed = stemText(term);
|
|
5303
|
+
return stemmed.length > 0 && stems.includes(stemmed);
|
|
5304
|
+
});
|
|
5265
5305
|
}
|
|
5266
5306
|
|
|
5267
5307
|
// ../schema/src/index.ts
|
|
@@ -5362,6 +5402,12 @@ var TraceSourceSchema = external_exports.object({
|
|
|
5362
5402
|
/** How many more dirty paths there were than `dirtyFiles` holds. Absent = none were left out. */
|
|
5363
5403
|
dirtyFilesOmitted: external_exports.number().int().positive().optional(),
|
|
5364
5404
|
gitCapturedAt: external_exports.string().optional(),
|
|
5405
|
+
/**
|
|
5406
|
+
* `flatbread share --commit [sha]` (P7.2): the person sharing NAMED `headCommitSha`, and
|
|
5407
|
+
* `dirtyFiles` was measured against it - so an empty list means a clean tree AT THAT COMMIT.
|
|
5408
|
+
* Absent = HEAD, read automatically.
|
|
5409
|
+
*/
|
|
5410
|
+
commitNamed: external_exports.literal(true).optional(),
|
|
5365
5411
|
/** `flatbread share --pr <url>`: the pull request this run belongs to, as the person sharing it said. */
|
|
5366
5412
|
prUrl: external_exports.string().url().max(GIT_LIMITS.prUrl).regex(/^https:\/\//, "a PR link must be https").optional(),
|
|
5367
5413
|
/** Absent on records written before Pass 6, which were all claude-code. */
|
|
@@ -5554,6 +5600,13 @@ var TraceRecordSchema = external_exports.object({
|
|
|
5554
5600
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["slug"], message: `slug "${record.slug}" is reserved` });
|
|
5555
5601
|
}
|
|
5556
5602
|
});
|
|
5603
|
+
function normalizeTitle(input) {
|
|
5604
|
+
if (typeof input !== "string") return null;
|
|
5605
|
+
const flat = input.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
5606
|
+
if (flat.length === 0) return null;
|
|
5607
|
+
if (flat.length > LIMITS.title) return null;
|
|
5608
|
+
return flat;
|
|
5609
|
+
}
|
|
5557
5610
|
var RecordSummarySchema = external_exports.object({
|
|
5558
5611
|
slug: SlugSchema,
|
|
5559
5612
|
title: external_exports.string(),
|
|
@@ -5656,6 +5709,9 @@ function claudeSettingsPath(env = process.env) {
|
|
|
5656
5709
|
if (configDir) return path.join(configDir, "settings.json");
|
|
5657
5710
|
return path.join(os.homedir(), ".claude", "settings.json");
|
|
5658
5711
|
}
|
|
5712
|
+
function encodeCwd(cwd) {
|
|
5713
|
+
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
5714
|
+
}
|
|
5659
5715
|
function isSubagentSessionFile(filePath) {
|
|
5660
5716
|
const segments = filePath.split(/[\\/]/).filter((segment2) => segment2.length > 0);
|
|
5661
5717
|
const base = segments[segments.length - 1] ?? "";
|
|
@@ -5991,50 +6047,282 @@ function buildTrace(input) {
|
|
|
5991
6047
|
}
|
|
5992
6048
|
|
|
5993
6049
|
// src/adapters/antigravity.ts
|
|
6050
|
+
import crypto from "node:crypto";
|
|
5994
6051
|
import fs3 from "node:fs";
|
|
5995
6052
|
import os2 from "node:os";
|
|
5996
6053
|
import path3 from "node:path";
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6054
|
+
import { fileURLToPath } from "node:url";
|
|
6055
|
+
var ANTIGRAVITY_ADAPTER_VERSION = 1;
|
|
6056
|
+
var KEY = Buffer.from("safeCodeiumworldKeYsecretBalloon");
|
|
6057
|
+
var NONCE = 12;
|
|
6058
|
+
var TAG = 16;
|
|
6059
|
+
var CONVERSATION = /[\\/]\.gemini[\\/]antigravity[^\\/]*[\\/]conversations[\\/][^\\/]+\.pb$/i;
|
|
6060
|
+
var PB_VARINT = 0;
|
|
6061
|
+
var PB_FIXED64 = 1;
|
|
6062
|
+
var PB_BYTES = 2;
|
|
6063
|
+
var PB_FIXED32 = 5;
|
|
6064
|
+
var Malformed = class extends Error {
|
|
6065
|
+
};
|
|
6066
|
+
function varint(b, at) {
|
|
6067
|
+
let value = 0n;
|
|
6068
|
+
let shift = 0n;
|
|
6069
|
+
for (let i = at; i < b.length && shift <= 63n; i++) {
|
|
6070
|
+
const byte = b[i];
|
|
6071
|
+
value |= BigInt(byte & 127) << shift;
|
|
6072
|
+
shift += 7n;
|
|
6073
|
+
if ((byte & 128) === 0) return [value, i + 1];
|
|
6074
|
+
}
|
|
6075
|
+
throw new Malformed("malformed protobuf");
|
|
6076
|
+
}
|
|
6077
|
+
function pbFields(b) {
|
|
6078
|
+
const out = [];
|
|
6079
|
+
let i = 0;
|
|
6080
|
+
while (i < b.length) {
|
|
6081
|
+
const [key, afterKey] = varint(b, i);
|
|
6082
|
+
i = afterKey;
|
|
6083
|
+
const num2 = Number(key >> 3n);
|
|
6084
|
+
const wire = Number(key & 7n);
|
|
6085
|
+
if (num2 === 0) throw new Malformed("malformed protobuf");
|
|
6086
|
+
let value = 0n;
|
|
6087
|
+
let data = Buffer.alloc(0);
|
|
6088
|
+
if (wire === PB_VARINT) {
|
|
6089
|
+
[value, i] = varint(b, i);
|
|
6090
|
+
} else if (wire === PB_FIXED64) {
|
|
6091
|
+
if (i + 8 > b.length) throw new Malformed("malformed protobuf");
|
|
6092
|
+
value = b.readBigUInt64LE(i);
|
|
6093
|
+
i += 8;
|
|
6094
|
+
} else if (wire === PB_FIXED32) {
|
|
6095
|
+
if (i + 4 > b.length) throw new Malformed("malformed protobuf");
|
|
6096
|
+
value = BigInt(b.readUInt32LE(i));
|
|
6097
|
+
i += 4;
|
|
6098
|
+
} else if (wire === PB_BYTES) {
|
|
6099
|
+
const [length, afterLength] = varint(b, i);
|
|
6100
|
+
if (length > BigInt(b.length - afterLength)) throw new Malformed("malformed protobuf");
|
|
6101
|
+
data = b.subarray(afterLength, afterLength + Number(length));
|
|
6102
|
+
i = afterLength + Number(length);
|
|
6103
|
+
} else {
|
|
6104
|
+
throw new Malformed("malformed protobuf");
|
|
6105
|
+
}
|
|
6106
|
+
out.push({ num: num2, wire, value, data });
|
|
6107
|
+
}
|
|
6108
|
+
return out;
|
|
6109
|
+
}
|
|
6110
|
+
function bytesField(fields, num2) {
|
|
6111
|
+
return fields.find((f) => f.num === num2 && f.wire === PB_BYTES)?.data;
|
|
6112
|
+
}
|
|
6113
|
+
function pbString(b) {
|
|
6114
|
+
return b === void 0 ? "" : b.toString("utf8").replaceAll("\0", "");
|
|
6115
|
+
}
|
|
6116
|
+
function pbTimestamp(b) {
|
|
6117
|
+
if (b === void 0) return void 0;
|
|
6118
|
+
let seconds = 0n;
|
|
6119
|
+
let nanos = 0n;
|
|
6120
|
+
for (const f of pbFields(b)) {
|
|
6121
|
+
if (f.wire !== PB_VARINT) continue;
|
|
6122
|
+
if (f.num === 1) seconds = BigInt.asIntN(64, f.value);
|
|
6123
|
+
if (f.num === 2) nanos = BigInt.asIntN(32, f.value);
|
|
6124
|
+
}
|
|
6125
|
+
if (seconds === 0n && nanos === 0n) return void 0;
|
|
6126
|
+
const ms = Number(seconds) * 1e3 + Math.floor(Number(nanos) / 1e6);
|
|
6127
|
+
const date = new Date(ms);
|
|
6128
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
6129
|
+
}
|
|
6130
|
+
function openConversation(b) {
|
|
6131
|
+
if (b.length < NONCE + TAG) return null;
|
|
6132
|
+
try {
|
|
6133
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", KEY, b.subarray(0, NONCE));
|
|
6134
|
+
decipher.setAuthTag(b.subarray(b.length - TAG));
|
|
6135
|
+
return Buffer.concat([decipher.update(b.subarray(NONCE, b.length - TAG)), decipher.final()]);
|
|
6136
|
+
} catch {
|
|
6137
|
+
return null;
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
6140
|
+
function folderPath(uri) {
|
|
6141
|
+
if (!uri.startsWith("file:")) return void 0;
|
|
6142
|
+
try {
|
|
6143
|
+
return fileURLToPath(uri);
|
|
6144
|
+
} catch {
|
|
6145
|
+
try {
|
|
6146
|
+
return decodeURIComponent(new URL(uri).pathname) || void 0;
|
|
6147
|
+
} catch {
|
|
6148
|
+
return void 0;
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6151
|
+
}
|
|
6152
|
+
function workspaceOf(metadata) {
|
|
6153
|
+
if (metadata === void 0) return void 0;
|
|
6154
|
+
for (const f of pbFields(metadata)) {
|
|
6155
|
+
if (f.num !== 1 || f.wire !== PB_BYTES) continue;
|
|
6156
|
+
const folder = folderPath(pbString(bytesField(pbFields(f.data), 1)));
|
|
6157
|
+
if (folder !== void 0) return folder;
|
|
6158
|
+
}
|
|
6159
|
+
return void 0;
|
|
6160
|
+
}
|
|
6161
|
+
function tally(counts, key) {
|
|
6162
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
6163
|
+
}
|
|
6164
|
+
function decodeTrajectory2(plain) {
|
|
6165
|
+
const events = [];
|
|
6166
|
+
const unread = /* @__PURE__ */ new Map();
|
|
6167
|
+
let sessionId;
|
|
6168
|
+
let cwd;
|
|
6169
|
+
let steps = 0;
|
|
6170
|
+
let clock;
|
|
6171
|
+
for (const f of pbFields(plain)) {
|
|
6172
|
+
if (f.wire === PB_BYTES && f.num === 6) {
|
|
6173
|
+
sessionId = pbString(f.data) || sessionId;
|
|
6174
|
+
continue;
|
|
6175
|
+
}
|
|
6176
|
+
if (f.wire === PB_BYTES && f.num === 7) {
|
|
6177
|
+
cwd ??= workspaceOf(f.data);
|
|
6178
|
+
clock ??= pbTimestamp(bytesField(pbFields(f.data), 2));
|
|
6179
|
+
continue;
|
|
6180
|
+
}
|
|
6181
|
+
if (f.wire !== PB_BYTES || f.num !== 2) {
|
|
6182
|
+
tally(unread, `trajectory field ${f.num}`);
|
|
6183
|
+
continue;
|
|
6184
|
+
}
|
|
6185
|
+
steps++;
|
|
6186
|
+
const step = pbFields(f.data);
|
|
6187
|
+
const metadata = bytesField(step, 5);
|
|
6188
|
+
const metaFields = metadata === void 0 ? [] : pbFields(metadata);
|
|
6189
|
+
clock = pbTimestamp(bytesField(metaFields, 1)) ?? clock;
|
|
6190
|
+
const completed = pbTimestamp(bytesField(metaFields, 8));
|
|
6191
|
+
for (const s of step) {
|
|
6192
|
+
if (s.num !== 5 && s.num !== 19 && s.num !== 20) tally(unread, `step field ${s.num}`);
|
|
6193
|
+
}
|
|
6194
|
+
const input = bytesField(step, 19);
|
|
6195
|
+
if (input !== void 0) {
|
|
6196
|
+
const fields = pbFields(input);
|
|
6197
|
+
const text2 = pbString(bytesField(fields, 2)).trim() || pbString(bytesField(fields, 1)).trim();
|
|
6198
|
+
if (text2) events.push({ kind: "prompt", ...clock ? { timestamp: clock } : {}, text: text2 });
|
|
6199
|
+
}
|
|
6200
|
+
const planner = bytesField(step, 20);
|
|
6201
|
+
if (planner !== void 0) {
|
|
6202
|
+
const fields = pbFields(planner);
|
|
6203
|
+
const text2 = pbString(bytesField(fields, 1)).trim() || pbString(bytesField(fields, 8)).trim();
|
|
6204
|
+
if (bytesField(fields, 3) !== void 0) tally(unread, "planner thinking");
|
|
6205
|
+
const at = completed ?? clock;
|
|
6206
|
+
if (text2) events.push({ kind: "text", ...at ? { timestamp: at } : {}, text: text2 });
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
6209
|
+
if (cwd !== void 0 || sessionId !== void 0) {
|
|
6210
|
+
events.unshift({ kind: "meta", ...sessionId ? { sessionId } : {}, ...cwd ? { cwd } : {} });
|
|
6211
|
+
}
|
|
6212
|
+
return { events, ...sessionId ? { sessionId } : {}, steps, unread };
|
|
6213
|
+
}
|
|
6214
|
+
function homeOf(env) {
|
|
6215
|
+
return (process.platform === "win32" ? env.USERPROFILE : env.HOME) || os2.homedir();
|
|
6216
|
+
}
|
|
6217
|
+
function listConversations(env = process.env) {
|
|
6218
|
+
const gemini = path3.join(homeOf(env), ".gemini");
|
|
6219
|
+
let roots;
|
|
6220
|
+
try {
|
|
6221
|
+
roots = fs3.readdirSync(gemini).filter((name) => /^antigravity/i.test(name));
|
|
6222
|
+
} catch {
|
|
6223
|
+
return [];
|
|
6224
|
+
}
|
|
6225
|
+
const found = [];
|
|
6226
|
+
for (const root of roots) {
|
|
6227
|
+
const dir = path3.join(gemini, root, "conversations");
|
|
6228
|
+
let names;
|
|
6229
|
+
try {
|
|
6230
|
+
names = fs3.readdirSync(dir);
|
|
6231
|
+
} catch {
|
|
6232
|
+
continue;
|
|
6233
|
+
}
|
|
6234
|
+
for (const name of names) {
|
|
6235
|
+
if (!name.toLowerCase().endsWith(".pb")) continue;
|
|
6236
|
+
const file = path3.join(dir, name);
|
|
6237
|
+
try {
|
|
6238
|
+
const stat = fs3.statSync(file);
|
|
6239
|
+
if (stat.isFile()) found.push({ file, mtime: stat.mtimeMs });
|
|
6240
|
+
} catch {
|
|
6241
|
+
continue;
|
|
6242
|
+
}
|
|
6243
|
+
}
|
|
6010
6244
|
}
|
|
6011
|
-
return
|
|
6245
|
+
return found.sort((a, b) => b.mtime - a.mtime).map((entry) => entry.file);
|
|
6012
6246
|
}
|
|
6247
|
+
var REFUSAL_TEST = "A6";
|
|
6013
6248
|
var antigravityAdapter = {
|
|
6014
6249
|
id: "antigravity",
|
|
6015
|
-
version:
|
|
6250
|
+
version: ANTIGRAVITY_ADAPTER_VERSION,
|
|
6016
6251
|
async detect(input) {
|
|
6017
|
-
|
|
6018
|
-
|
|
6252
|
+
if (CONVERSATION.test(input.path)) {
|
|
6253
|
+
return { score: 85, reason: "a conversations/<id>.pb under ~/.gemini/antigravity" };
|
|
6254
|
+
}
|
|
6255
|
+
return { score: 0, reason: "not an Antigravity conversations/<id>.pb" };
|
|
6019
6256
|
},
|
|
6020
6257
|
async read(input) {
|
|
6021
|
-
|
|
6258
|
+
const plain = openConversation(await fs3.promises.readFile(input.path));
|
|
6259
|
+
if (plain === null) {
|
|
6260
|
+
return {
|
|
6261
|
+
refused: true,
|
|
6262
|
+
reason: `${path3.basename(input.path)} did not authenticate with the key Antigravity IDE 1.23.2 uses. A newer build may have changed the envelope; report its version via HUMAN-TESTS.md A6.`,
|
|
6263
|
+
humanTest: REFUSAL_TEST
|
|
6264
|
+
};
|
|
6265
|
+
}
|
|
6266
|
+
let trajectory;
|
|
6267
|
+
try {
|
|
6268
|
+
trajectory = decodeTrajectory2(plain);
|
|
6269
|
+
} catch (error) {
|
|
6270
|
+
if (!(error instanceof Malformed)) throw error;
|
|
6271
|
+
return {
|
|
6272
|
+
refused: true,
|
|
6273
|
+
reason: `${path3.basename(input.path)} authenticated but is not the trajectory protobuf this reader knows.`,
|
|
6274
|
+
humanTest: REFUSAL_TEST
|
|
6275
|
+
};
|
|
6276
|
+
}
|
|
6277
|
+
const said = trajectory.events.filter((e) => e.kind === "prompt" || e.kind === "text").length;
|
|
6278
|
+
if (said === 0) {
|
|
6279
|
+
return {
|
|
6280
|
+
refused: true,
|
|
6281
|
+
reason: `${path3.basename(input.path)} has ${trajectory.steps} step(s) and no prompt or reply in the fields this reader knows.`,
|
|
6282
|
+
humanTest: REFUSAL_TEST
|
|
6283
|
+
};
|
|
6284
|
+
}
|
|
6285
|
+
const unreadFields = [...trajectory.unread].sort((a, b) => b[1] - a[1]).map(([field, count]) => `${field} x${count}`);
|
|
6022
6286
|
return {
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6287
|
+
events: trajectory.events,
|
|
6288
|
+
capabilities: { tokens: "unavailable", filesChanged: "unavailable" },
|
|
6289
|
+
sessionId: trajectory.sessionId ?? path3.basename(input.path, ".pb"),
|
|
6290
|
+
unreadFields,
|
|
6291
|
+
notes: [
|
|
6292
|
+
`${trajectory.steps} step(s), ${said} prompt/reply event(s)`,
|
|
6293
|
+
"tool calls, thinking and edits are not read: their step fields are undocumented",
|
|
6294
|
+
"CONFIDENCE: documented (Windsurf's schema, measured on IDE 1.23.2). See HUMAN-TESTS.md A6."
|
|
6295
|
+
]
|
|
6026
6296
|
};
|
|
6027
6297
|
},
|
|
6028
6298
|
discover(env) {
|
|
6029
|
-
return
|
|
6299
|
+
return listConversations(env);
|
|
6030
6300
|
},
|
|
6031
|
-
async describe() {
|
|
6032
|
-
const
|
|
6033
|
-
|
|
6034
|
-
|
|
6301
|
+
async describe(input) {
|
|
6302
|
+
const files = input ? [input.path] : listConversations();
|
|
6303
|
+
const lines2 = [`conversations found: ${files.length}`];
|
|
6304
|
+
for (const file of files.slice(0, 8)) lines2.push(` ${file}`);
|
|
6305
|
+
const target = files[0];
|
|
6306
|
+
if (target === void 0) return lines2;
|
|
6307
|
+
lines2.push("", `inspecting ${target}`);
|
|
6308
|
+
let plain;
|
|
6309
|
+
try {
|
|
6310
|
+
plain = openConversation(await fs3.promises.readFile(target));
|
|
6311
|
+
} catch (error) {
|
|
6312
|
+
lines2.push(` unreadable: ${error instanceof Error ? error.message : String(error)}`);
|
|
6313
|
+
return lines2;
|
|
6314
|
+
}
|
|
6315
|
+
lines2.push(` authenticated: ${plain === null ? "no" : "yes"}`);
|
|
6316
|
+
if (plain === null) return lines2;
|
|
6317
|
+
try {
|
|
6318
|
+
const trajectory = decodeTrajectory2(plain);
|
|
6319
|
+
lines2.push(` steps: ${trajectory.steps}, prompt/reply events: ${trajectory.events.filter((e) => e.kind !== "meta").length}`);
|
|
6320
|
+
for (const [field, count] of [...trajectory.unread].sort((a, b) => b[1] - a[1]).slice(0, 15)) {
|
|
6321
|
+
lines2.push(` ${String(count).padStart(6)} ${field}`);
|
|
6322
|
+
}
|
|
6323
|
+
} catch {
|
|
6324
|
+
lines2.push(" not the trajectory protobuf this reader knows");
|
|
6035
6325
|
}
|
|
6036
|
-
lines2.push("");
|
|
6037
|
-
lines2.push("If one is FOUND, HUMAN-TESTS.md A6 lists exactly what to report back.");
|
|
6038
6326
|
return lines2;
|
|
6039
6327
|
}
|
|
6040
6328
|
};
|
|
@@ -7347,8 +7635,8 @@ function isInsideDir(child, parent) {
|
|
|
7347
7635
|
}
|
|
7348
7636
|
function targetIndicatesDb(target, env) {
|
|
7349
7637
|
if (isDbTarget(target)) return true;
|
|
7350
|
-
const
|
|
7351
|
-
if (/^ses_[0-9a-zA-Z]+$/.test(
|
|
7638
|
+
const stem2 = path8.basename(target, ".json");
|
|
7639
|
+
if (/^ses_[0-9a-zA-Z]+$/.test(stem2)) return true;
|
|
7352
7640
|
const resolved = path8.resolve(target);
|
|
7353
7641
|
for (const dir of [dataDir(env), storageDir(env)]) {
|
|
7354
7642
|
if (resolved === path8.resolve(dir) || isInsideDir(resolved, dir)) return true;
|
|
@@ -7394,8 +7682,8 @@ async function resolveSource(target, env = process.env) {
|
|
|
7394
7682
|
const wantsDb = isDbTarget(target) || dbExists(data) && targetIndicatesDb(target, env);
|
|
7395
7683
|
if (wantsDb) {
|
|
7396
7684
|
const explicit = isDbTarget(target) ? splitDbTarget(target) : { dbPath: dbPathFor(data), sessionId: void 0 };
|
|
7397
|
-
const
|
|
7398
|
-
const sessionId = explicit.sessionId ?? (/^ses_[0-9a-zA-Z]+$/.test(
|
|
7685
|
+
const stem2 = path8.basename(target, ".json");
|
|
7686
|
+
const sessionId = explicit.sessionId ?? (/^ses_[0-9a-zA-Z]+$/.test(stem2) ? stem2 : void 0);
|
|
7399
7687
|
const result = await readDbSession(path8.dirname(explicit.dbPath), sessionId);
|
|
7400
7688
|
if (!("unavailable" in result)) {
|
|
7401
7689
|
if (result.messages.length === 0) {
|
|
@@ -8459,8 +8747,8 @@ function safeSlug(slug) {
|
|
|
8459
8747
|
if (slug.includes("..") || slug.length > 72) return null;
|
|
8460
8748
|
return slug;
|
|
8461
8749
|
}
|
|
8462
|
-
function isRecordStem(
|
|
8463
|
-
return safeSlug(
|
|
8750
|
+
function isRecordStem(stem2) {
|
|
8751
|
+
return safeSlug(stem2) !== null && SlugSchema.safeParse(stem2).success;
|
|
8464
8752
|
}
|
|
8465
8753
|
function readStoreRecords(dir) {
|
|
8466
8754
|
if (!fs12.existsSync(dir)) return { records: [], unreadable: 0 };
|
|
@@ -8520,9 +8808,9 @@ function createFileStore(options) {
|
|
|
8520
8808
|
async get(slug, getOptions = {}) {
|
|
8521
8809
|
const filePath = fileFor(slug);
|
|
8522
8810
|
if (filePath === null || !fs12.existsSync(filePath)) return { ok: false, reason: "not-found" };
|
|
8523
|
-
const
|
|
8524
|
-
if (!
|
|
8525
|
-
const record =
|
|
8811
|
+
const read2 = readRecord(filePath);
|
|
8812
|
+
if (!read2.ok) return { ok: false, reason: "unreadable", problem: read2.problem };
|
|
8813
|
+
const record = read2.record;
|
|
8526
8814
|
const hasManageToken = manageTokenMatches(getOptions.manageToken, record.manageTokenHash);
|
|
8527
8815
|
const viewer = { ...getOptions.viewer, hasManageToken };
|
|
8528
8816
|
const allowed = canRead(record, viewer);
|
|
@@ -8534,10 +8822,10 @@ function createFileStore(options) {
|
|
|
8534
8822
|
const summaries = [];
|
|
8535
8823
|
for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
|
|
8536
8824
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
8537
|
-
const
|
|
8538
|
-
if (!
|
|
8539
|
-
if (!canList(
|
|
8540
|
-
summaries.push(summarize(
|
|
8825
|
+
const read2 = readRecord(path12.join(dir, entry.name));
|
|
8826
|
+
if (!read2.ok) continue;
|
|
8827
|
+
if (!canList(read2.record)) continue;
|
|
8828
|
+
summaries.push(summarize(read2.record));
|
|
8541
8829
|
}
|
|
8542
8830
|
summaries.sort((a, b) => a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0);
|
|
8543
8831
|
return summaries;
|
|
@@ -8641,6 +8929,31 @@ function readGitLineage(cwd, options = {}) {
|
|
|
8641
8929
|
gitCapturedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
8642
8930
|
};
|
|
8643
8931
|
}
|
|
8932
|
+
function zList(output) {
|
|
8933
|
+
return output === null ? null : output.split("\0").filter((entry) => entry.length > 0);
|
|
8934
|
+
}
|
|
8935
|
+
function bindCommit(cwd, rev, options = {}) {
|
|
8936
|
+
const base = readGitLineage(cwd, options);
|
|
8937
|
+
if (base === null || cwd === void 0) {
|
|
8938
|
+
return { problem: `the session's directory (${cwd ?? "not recorded"}) is not a git work tree on this machine` };
|
|
8939
|
+
}
|
|
8940
|
+
const sha = git(cwd, ["rev-parse", "--verify", "-q", `${rev}^{commit}`], options)?.trim();
|
|
8941
|
+
if (sha === void 0 || !/^[0-9a-f]{7,64}$/.test(sha)) return { problem: `"${rev}" is not a commit in ${cwd}` };
|
|
8942
|
+
if (sha === base.headCommitSha) return { ...base, commitNamed: true };
|
|
8943
|
+
const tracked = zList(git(cwd, ["diff", "--name-only", "-z", sha, "--"], options));
|
|
8944
|
+
const untracked = zList(git(cwd, ["ls-files", "--others", "--exclude-standard", "-z"], options));
|
|
8945
|
+
if (tracked === null || untracked === null) return { problem: `git could not compare the working tree with ${sha}` };
|
|
8946
|
+
const dirty = [.../* @__PURE__ */ new Set([...tracked, ...untracked])];
|
|
8947
|
+
const kept = dirty.slice(0, GIT_LIMITS.dirtyFiles);
|
|
8948
|
+
const { dirtyFilesOmitted: _omitted, ...rest } = base;
|
|
8949
|
+
return {
|
|
8950
|
+
...rest,
|
|
8951
|
+
headCommitSha: sha,
|
|
8952
|
+
dirtyFiles: kept,
|
|
8953
|
+
...dirty.length > kept.length ? { dirtyFilesOmitted: dirty.length - kept.length } : {},
|
|
8954
|
+
commitNamed: true
|
|
8955
|
+
};
|
|
8956
|
+
}
|
|
8644
8957
|
function checkPrUrl(value) {
|
|
8645
8958
|
let parsed;
|
|
8646
8959
|
try {
|
|
@@ -8656,12 +8969,14 @@ function checkPrUrl(value) {
|
|
|
8656
8969
|
function withLineage(trace, lineage, prUrl) {
|
|
8657
8970
|
if (lineage === null && prUrl === void 0) return trace;
|
|
8658
8971
|
const source = { ...trace.source };
|
|
8659
|
-
const elsewhere = lineage !== null && source.gitBranch !== void 0 && lineage.gitBranch !== source.gitBranch;
|
|
8972
|
+
const elsewhere = lineage !== null && lineage.commitNamed !== true && source.gitBranch !== void 0 && lineage.gitBranch !== source.gitBranch;
|
|
8660
8973
|
if (lineage !== null && !elsewhere) {
|
|
8661
8974
|
if (source.gitBranch === void 0 && lineage.gitBranch !== void 0) source.gitBranch = lineage.gitBranch;
|
|
8662
8975
|
if (lineage.headCommitSha !== void 0) source.headCommitSha = lineage.headCommitSha;
|
|
8663
8976
|
source.dirtyFiles = lineage.dirtyFiles;
|
|
8664
8977
|
if (lineage.dirtyFilesOmitted !== void 0) source.dirtyFilesOmitted = lineage.dirtyFilesOmitted;
|
|
8978
|
+
else delete source.dirtyFilesOmitted;
|
|
8979
|
+
if (lineage.commitNamed === true) source.commitNamed = true;
|
|
8665
8980
|
source.gitCapturedAt = lineage.gitCapturedAt;
|
|
8666
8981
|
}
|
|
8667
8982
|
if (prUrl !== void 0) source.prUrl = prUrl;
|
|
@@ -8669,7 +8984,7 @@ function withLineage(trace, lineage, prUrl) {
|
|
|
8669
8984
|
}
|
|
8670
8985
|
|
|
8671
8986
|
// src/auth.ts
|
|
8672
|
-
import
|
|
8987
|
+
import crypto2 from "node:crypto";
|
|
8673
8988
|
import fs15 from "node:fs";
|
|
8674
8989
|
import os6 from "node:os";
|
|
8675
8990
|
import path14 from "node:path";
|
|
@@ -8699,7 +9014,7 @@ function isAuthFailure(value) {
|
|
|
8699
9014
|
return typeof value === "object" && value !== null && "ok" in value && value.ok === false;
|
|
8700
9015
|
}
|
|
8701
9016
|
function hashSecret(secret) {
|
|
8702
|
-
return
|
|
9017
|
+
return crypto2.createHash("sha256").update(secret).digest("hex");
|
|
8703
9018
|
}
|
|
8704
9019
|
function apiUrlFrom(env = process.env) {
|
|
8705
9020
|
const explicit = env["FLATBREAD_API_URL"]?.trim();
|
|
@@ -8712,7 +9027,7 @@ async function startCliLogin(options = {}) {
|
|
|
8712
9027
|
const apiUrl = options.apiUrl ?? apiUrlFrom(env);
|
|
8713
9028
|
const base = validateApiUrl(apiUrl);
|
|
8714
9029
|
if (isAuthFailure(base)) return base;
|
|
8715
|
-
const pollSecret =
|
|
9030
|
+
const pollSecret = crypto2.randomBytes(32).toString("hex");
|
|
8716
9031
|
const result = await postJson(
|
|
8717
9032
|
`${base}/api/cli-login/start`,
|
|
8718
9033
|
{ poll_secret_hash: hashSecret(pollSecret) },
|
|
@@ -9503,65 +9818,105 @@ function renderMarked(value, matches) {
|
|
|
9503
9818
|
return html;
|
|
9504
9819
|
}
|
|
9505
9820
|
var STYLE = `
|
|
9506
|
-
:root{
|
|
9821
|
+
:root{
|
|
9822
|
+
--canvas:#fafafa;--bg:#ffffff;--ivory:#fcfbf8;--surface:#f6f7f8;--inset:#f6f7f8;
|
|
9823
|
+
--line:rgba(226,232,240,.8);--line-soft:#eff0f2;--line-strong:#b9bec4;
|
|
9824
|
+
--fg:#23272c;--fg-2:rgba(35,39,44,.7);--fg-3:rgba(35,39,44,.56);--fg-lift:#2b3138;
|
|
9825
|
+
--fail-bg:#f6ece7;--fail-line:#c9755c;--fail-fg:#8a3b2a;
|
|
9826
|
+
--ok-bg:#eef1e8;--ok-line:#7f9a7a;--ok-fg:#3f5d43;
|
|
9827
|
+
--hold-bg:#f7f1e2;--hold-line:#c9a34a;--hold-fg:#7d601d;
|
|
9828
|
+
--intent-line:#d6d1c1;
|
|
9829
|
+
--shade:30,41,54;
|
|
9830
|
+
--shadow:0 1px 2px rgba(var(--shade),.04),0 10px 26px rgba(var(--shade),.08);
|
|
9831
|
+
--radius:10px;--radius-sm:6px;
|
|
9832
|
+
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI Variable Text","Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
|
9833
|
+
--mono:ui-monospace,"SF Mono",SFMono-Regular,"Cascadia Mono","Segoe UI Mono","Roboto Mono",Menlo,Consolas,"Liberation Mono",monospace
|
|
9834
|
+
}
|
|
9507
9835
|
*{box-sizing:border-box}
|
|
9508
|
-
body{margin:0
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9836
|
+
html,body{margin:0}
|
|
9837
|
+
body{background:var(--canvas);color:var(--fg);font:14px/1.55 var(--sans);-webkit-font-smoothing:antialiased}
|
|
9838
|
+
code,pre,dd,.mono{font-family:var(--mono)}
|
|
9839
|
+
header{background:var(--bg);border-bottom:1px solid var(--line-soft)}
|
|
9840
|
+
.hin{max-width:1440px;margin:0 auto;padding:14px 28px 16px}
|
|
9841
|
+
.bar{display:flex;align-items:center;gap:10px;margin:0 0 12px}
|
|
9842
|
+
.wordmark{display:inline-flex;align-items:center;height:28px;padding:0 12px;border-radius:999px;background:var(--ivory);border:1px solid rgba(var(--shade),.08);box-shadow:inset 0 1px 0 #fff,0 1px 2px rgba(var(--shade),.05);font:600 13.5px/1 var(--mono);letter-spacing:-.01em;font-variant-ligatures:none}
|
|
9843
|
+
.crumb{color:var(--fg-3);font-size:12.5px}
|
|
9844
|
+
h1{margin:0 0 6px;font-size:20px;line-height:1.3;font-weight:600;letter-spacing:-.01em}
|
|
9845
|
+
.meta{color:var(--fg-3);font-size:12.5px;font-variant-numeric:tabular-nums}
|
|
9512
9846
|
.meta b{color:var(--fg);font-weight:600}
|
|
9513
|
-
.
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
.step
|
|
9518
|
-
.
|
|
9519
|
-
.badge
|
|
9520
|
-
.
|
|
9521
|
-
.
|
|
9522
|
-
.badge.
|
|
9523
|
-
|
|
9524
|
-
.
|
|
9525
|
-
|
|
9526
|
-
.sub{
|
|
9527
|
-
dl{margin:0;padding:
|
|
9528
|
-
dt{color:var(--
|
|
9529
|
-
dd{margin:2px 0 0;white-space:pre-wrap;word-break:break-word}
|
|
9530
|
-
|
|
9847
|
+
.meta.path{font-family:var(--mono);font-size:11.5px;margin-top:2px;word-break:break-all}
|
|
9848
|
+
.wrap{max-width:1440px;margin:0 auto;padding:20px 28px 120px;display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:24px;align-items:start}
|
|
9849
|
+
main{min-width:0}
|
|
9850
|
+
aside{position:sticky;top:16px;max-height:calc(100vh - 32px);overflow:auto;background:var(--bg);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:18px}
|
|
9851
|
+
.step{background:var(--bg);border:1px solid var(--line);border-radius:var(--radius);box-shadow:0 1px 2px rgba(var(--shade),.04);margin:0 0 10px;overflow:hidden}
|
|
9852
|
+
.step>.head{display:flex;gap:10px;align-items:baseline;padding:9px 14px 0;font-size:12px;color:var(--fg-3);font-variant-numeric:tabular-nums}
|
|
9853
|
+
.badge{font-weight:600;color:var(--fg-2);font-size:12px}
|
|
9854
|
+
.step.user_prompt{border-left:3px solid var(--intent-line)}
|
|
9855
|
+
.step.error_recovery{border-color:color-mix(in srgb,var(--fail-line) 45%,transparent);border-left:3px solid var(--fail-line)}
|
|
9856
|
+
.badge.error_recovery,.sub.err{color:var(--fail-fg)}
|
|
9857
|
+
pre{margin:0;padding:8px 14px 12px;white-space:pre-wrap;word-break:break-word;font:12.5px/1.6 var(--mono);color:var(--fg)}
|
|
9858
|
+
.step.user_prompt>pre,.step.assistant_text>pre{font:14px/1.6 var(--sans)}
|
|
9859
|
+
.sub{padding:8px 14px 0;border-top:1px solid var(--line-soft);color:var(--fg-3);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
|
|
9860
|
+
.sub+pre,.sub+dl{background:var(--inset);margin:8px 14px 12px;padding:10px 12px;border-radius:var(--radius-sm)}
|
|
9861
|
+
dl{margin:0;padding:4px 14px 12px}
|
|
9862
|
+
dt{color:var(--fg-3);font-size:11.5px;margin-top:8px}
|
|
9863
|
+
dd{margin:2px 0 0;white-space:pre-wrap;word-break:break-word;font-size:12.5px}
|
|
9864
|
+
#trace-meta dd{font-family:var(--sans);font-size:13.5px}
|
|
9865
|
+
mark.hit{background:rgba(var(--shade),.08);box-shadow:inset 0 1px 1px rgba(var(--shade),.08);color:var(--fg-2);border-radius:4px;padding:0 4px;font-family:var(--mono);font-size:.9em}
|
|
9531
9866
|
mark.hit .was{display:none}
|
|
9532
|
-
mark.hit.keep{background:
|
|
9867
|
+
mark.hit.keep{background:var(--hold-bg);box-shadow:none;color:var(--fg);border-bottom:1px dashed var(--hold-line);border-radius:2px;font-family:inherit;font-size:inherit}
|
|
9533
9868
|
mark.hit.keep .tok{display:none}
|
|
9534
9869
|
mark.hit.keep .was{display:inline}
|
|
9535
|
-
aside h2{margin:0 0 4px;font-size:
|
|
9536
|
-
aside .note{color:var(--
|
|
9537
|
-
.
|
|
9538
|
-
.row:hover{border-color:var(--acc)}
|
|
9539
|
-
.row input{margin:3px 0 0}
|
|
9540
|
-
.state{font-size:11px;letter-spacing:.04em}
|
|
9541
|
-
.s-keep{display:none;color:var(--warn)}
|
|
9542
|
-
.s-red{color:var(--ok)}
|
|
9543
|
-
.row input:checked ~ .state .s-red{display:none}
|
|
9544
|
-
.row input:checked ~ .state .s-keep{display:inline}
|
|
9545
|
-
.row .rule{font-size:12px}
|
|
9546
|
-
.row .prev{color:var(--dim);font-size:11px;word-break:break-all}
|
|
9547
|
-
.row .times{color:var(--warn)}
|
|
9548
|
-
.keys{color:var(--dim);font-size:11px;margin:0 0 10px}
|
|
9549
|
-
.keys b{color:var(--fg);font-weight:600}
|
|
9550
|
-
.row .path{color:#5c6474;font-size:10px;word-break:break-all}
|
|
9551
|
-
fieldset{border:1px solid var(--line);border-radius:6px;margin:0 0 12px;padding:8px 10px}
|
|
9552
|
-
legend{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
|
|
9553
|
-
label.vis{display:block;padding:3px 0}
|
|
9554
|
-
.warn{color:var(--warn);font-size:11px;margin:4px 0 0}
|
|
9555
|
-
.actions{display:flex;gap:8px;margin-top:10px}
|
|
9556
|
-
button{font:inherit;padding:9px 14px;border-radius:5px;border:1px solid var(--line);background:#1d2430;color:var(--fg);cursor:pointer}
|
|
9557
|
-
button.primary{background:var(--ok);border-color:var(--ok);color:#04140b;font-weight:600}
|
|
9558
|
-
button:hover{border-color:var(--acc)}
|
|
9870
|
+
aside h2{margin:0 0 4px;font-size:15px;font-weight:600}
|
|
9871
|
+
aside .note{color:var(--fg-3);font-size:12.5px;margin:0 0 12px}
|
|
9872
|
+
aside .note code{font-size:11.5px;background:var(--inset);padding:1px 4px;border-radius:4px}
|
|
9559
9873
|
.tools{display:flex;gap:6px;margin:0 0 10px}
|
|
9560
|
-
.
|
|
9561
|
-
.
|
|
9562
|
-
|
|
9563
|
-
|
|
9874
|
+
.keys{color:var(--fg-3);font-size:12px;margin:0 0 14px}
|
|
9875
|
+
.keys b{display:inline-block;min-width:18px;padding:0 5px;border:1px solid rgba(var(--shade),.12);border-radius:999px;background:var(--bg);color:var(--fg);font:600 11px/16px var(--mono);text-align:center}
|
|
9876
|
+
fieldset{border:0;border-top:1px solid var(--line-soft);margin:0;padding:14px 0 12px}
|
|
9877
|
+
legend{float:left;width:100%;padding:0;margin:0 0 6px;color:var(--fg-3);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
|
|
9878
|
+
legend+*{clear:left}
|
|
9879
|
+
input.title{width:100%;font:inherit;padding:8px 10px;margin:0 0 6px;border:1px solid rgba(var(--shade),.12);border-radius:var(--radius-sm);background:var(--ivory);color:var(--fg);box-shadow:inset 0 1px 0 #fff}
|
|
9880
|
+
input.title:focus{outline:none;background:#fff;border-color:var(--fg-3);box-shadow:0 0 0 3px rgba(var(--shade),.05)}
|
|
9881
|
+
label.vis{display:flex;align-items:center;gap:8px;padding:3px 0;cursor:pointer}
|
|
9882
|
+
input[type=radio],input[type=checkbox]{accent-color:var(--fg);margin:0}
|
|
9883
|
+
.warn{color:var(--fail-fg);font-size:12.5px;margin:6px 0 0}
|
|
9884
|
+
.actions{display:flex;gap:8px;padding:14px 0 16px;border-top:1px solid var(--line-soft)}
|
|
9885
|
+
button{display:inline-flex;align-items:center;font:500 12.5px/1 var(--sans);padding:9px 14px;border-radius:var(--radius-sm);border:1px solid var(--line-strong);background:var(--bg);color:var(--fg);cursor:pointer}
|
|
9886
|
+
button:hover{background:var(--surface)}
|
|
9887
|
+
button:focus-visible{outline:2px solid var(--fg);outline-offset:1px}
|
|
9888
|
+
button.primary{background:var(--fg);border-color:var(--fg);color:#fff}
|
|
9889
|
+
button.primary:hover{background:var(--fg-lift);border-color:var(--fg-lift)}
|
|
9890
|
+
.tools button{padding:5px 10px;font-size:12px}
|
|
9891
|
+
.row{display:grid;grid-template-columns:16px 52px minmax(0,1fr);gap:8px;align-items:start;padding:9px 10px;border:1px solid var(--line);border-radius:var(--radius-sm);margin-bottom:6px;cursor:pointer;background:var(--bg)}
|
|
9892
|
+
.row:hover{background:var(--surface)}
|
|
9893
|
+
.row:focus-within{border-color:var(--fg-3)}
|
|
9894
|
+
.row input{margin:3px 0 0}
|
|
9895
|
+
.state{font-size:11.5px;font-weight:600}
|
|
9896
|
+
.s-red{color:var(--fg-3)}
|
|
9897
|
+
.s-keep{display:none;color:var(--hold-fg)}
|
|
9898
|
+
.row input:checked~.state .s-red{display:none}
|
|
9899
|
+
.row input:checked~.state .s-keep{display:inline}
|
|
9900
|
+
.row:has(input:checked){background:var(--hold-bg);border-color:color-mix(in srgb,var(--hold-line) 50%,transparent)}
|
|
9901
|
+
.row .rule{font-size:13px;color:var(--fg)}
|
|
9902
|
+
.row .times{font-weight:600}
|
|
9903
|
+
.row .prev{color:var(--fg-2);font:11.5px/1.5 var(--mono);word-break:break-all}
|
|
9904
|
+
.row .path{color:var(--fg-3);font-size:11px;word-break:break-all}
|
|
9905
|
+
.done{max-width:620px;margin:14vh auto;padding:28px 30px;background:var(--bg);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow)}
|
|
9906
|
+
.done h1{font-size:20px}
|
|
9907
|
+
.done .meta{font-size:13.5px;color:var(--fg-2);margin:10px 0 0}
|
|
9908
|
+
.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}
|
|
9909
|
+
a{color:var(--fg);text-underline-offset:2px}
|
|
9910
|
+
@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}}
|
|
9564
9911
|
`;
|
|
9912
|
+
var STEP_LABEL = {
|
|
9913
|
+
user_prompt: "prompt",
|
|
9914
|
+
assistant_text: "assistant",
|
|
9915
|
+
assistant_thinking: "thinking",
|
|
9916
|
+
tool_call: "tool call",
|
|
9917
|
+
tool_result: "tool result",
|
|
9918
|
+
error_recovery: "failure"
|
|
9919
|
+
};
|
|
9565
9920
|
var SCRIPT = `
|
|
9566
9921
|
(function(){
|
|
9567
9922
|
var boxes = Array.prototype.slice.call(document.querySelectorAll('input[name="keep"]'));
|
|
@@ -9594,6 +9949,9 @@ var SCRIPT = `
|
|
|
9594
9949
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
9595
9950
|
var tag = e.target && e.target.tagName;
|
|
9596
9951
|
var key = e.key;
|
|
9952
|
+
// TYPING IS NOT TRIAGE: j, k and space in the title field are letters. Enter submits the form
|
|
9953
|
+
// the way any text field does, through the first submit button - Confirm.
|
|
9954
|
+
if (tag === 'INPUT' && e.target.type === 'text') return;
|
|
9597
9955
|
if (key === 'j' || key === 'ArrowDown') { e.preventDefault(); focusRow(cursor + 1); return; }
|
|
9598
9956
|
if (key === 'k' || key === 'ArrowUp') { e.preventDefault(); focusRow(cursor - 1); return; }
|
|
9599
9957
|
if (key === ' ' && cursor >= 0 && tag !== 'BUTTON') {
|
|
@@ -9665,14 +10023,14 @@ function renderReviewPage(input) {
|
|
|
9665
10023
|
const base = `steps[${index}]`;
|
|
9666
10024
|
const parts = [];
|
|
9667
10025
|
parts.push(
|
|
9668
|
-
`<div class="head"><span class="badge ${escapeHtml(step.type)}">${escapeHtml(step.type)}</span><span>#${step.stepNumber}</span><span>${escapeHtml(step.timestamp)}</span></div>`
|
|
10026
|
+
`<div class="head"><span class="badge ${escapeHtml(step.type)}">${escapeHtml(STEP_LABEL[step.type] ?? step.type)}</span><span>#${step.stepNumber}</span><span>${escapeHtml(step.timestamp)}</span></div>`
|
|
9669
10027
|
);
|
|
9670
10028
|
parts.push(`<pre>${marked(`${base}.content`, step.content)}</pre>`);
|
|
9671
10029
|
const tool = step.tool;
|
|
9672
10030
|
if (tool) {
|
|
9673
10031
|
const inputSlots = slots.filter((slot) => slot.fieldPath.startsWith(`${base}.tool.input`));
|
|
9674
10032
|
parts.push(
|
|
9675
|
-
`<div class="sub">tool ${escapeHtml(tool.name)}${tool.isError ? " · errored" : ""}</div>`
|
|
10033
|
+
`<div class="sub${tool.isError ? " err" : ""}">tool ${escapeHtml(tool.name)}${tool.isError ? " · errored" : ""}</div>`
|
|
9676
10034
|
);
|
|
9677
10035
|
if (inputSlots.length > 0) {
|
|
9678
10036
|
parts.push(
|
|
@@ -9692,7 +10050,7 @@ function renderReviewPage(input) {
|
|
|
9692
10050
|
`<div class="sub">diff ${marked(`${base}.diff.filePath`, diff.filePath)}</div><pre>${marked(`${base}.diff.patch`, diff.patch)}</pre>`
|
|
9693
10051
|
);
|
|
9694
10052
|
}
|
|
9695
|
-
return `<section class="step" id="${escapeHtml(base)}">${parts.join("")}</section>`;
|
|
10053
|
+
return `<section class="step ${escapeHtml(step.type)}" id="${escapeHtml(base)}">${parts.join("")}</section>`;
|
|
9696
10054
|
}).join("");
|
|
9697
10055
|
const groups = groupMatches(matches);
|
|
9698
10056
|
const rowsHtml = groups.length === 0 ? `<p class="note">No matches. Nothing will be rewritten.</p>` : groups.map((group) => {
|
|
@@ -9706,15 +10064,16 @@ function renderReviewPage(input) {
|
|
|
9706
10064
|
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?`;
|
|
9707
10065
|
const destinationHtml = input.publishTo === void 0 ? `<p class="note">Local draft only. Confirm writes ${escapeHtml(input.draftPath ?? "a file in packages/cli/.tmp")} and nothing leaves this machine. The visibility above is stored in the draft. To upload a run, share it with <code>--publish</code> after <code>flatbread login</code>.</p>` : `<p class="warn">Confirm UPLOADS this run to ${escapeHtml(input.publishTo)} with the visibility selected above. Everything you KEEP below is uploaded as written.</p>`;
|
|
9708
10066
|
const body = `
|
|
9709
|
-
<header>
|
|
10067
|
+
<header><div class="hin">
|
|
10068
|
+
<div class="bar"><span class="wordmark">flatbread</span><span class="crumb">review before you share</span></div>
|
|
9710
10069
|
<h1>${marked("title", trace.title)}</h1>
|
|
9711
10070
|
<div class="meta">
|
|
9712
|
-
<b>${escapeHtml(trace.model)}</b> · ${trace.steps.length}
|
|
9713
|
-
${formatCount(trace.stats.filesChanged)}
|
|
9714
|
-
${Math.round(trace.stats.durationSeconds)}s · <b>${matches.length}</b> redaction matches
|
|
10071
|
+
<b>${escapeHtml(trace.model)}</b> · ${plural2(trace.steps.length, "step")} · ${plural2(toolCalls, "tool call")} ·
|
|
10072
|
+
${trace.stats.filesChanged === null ? `${formatCount(null)} files` : plural2(trace.stats.filesChanged, "file")} changed · ${plural2(trace.stats.errorCount, "error")} ·
|
|
10073
|
+
${Math.round(trace.stats.durationSeconds)}s · <b>${matches.length}</b> redaction match${matches.length === 1 ? "" : "es"}
|
|
9715
10074
|
</div>
|
|
9716
|
-
<div class="meta">${escapeHtml(sessionPath)}</div>
|
|
9717
|
-
</header>
|
|
10075
|
+
<div class="meta path">${escapeHtml(sessionPath)}</div>
|
|
10076
|
+
</div></header>
|
|
9718
10077
|
<form id="review" method="post" action="/confirm">
|
|
9719
10078
|
<div class="wrap">
|
|
9720
10079
|
<main>${metaHtml}${stepsHtml}</main>
|
|
@@ -9724,6 +10083,11 @@ function renderReviewPage(input) {
|
|
|
9724
10083
|
Every match is redacted by default. Tick a row to KEEP the original text instead.</p>
|
|
9725
10084
|
<div class="tools"><button id="keep-none" type="button">redact all</button><button id="keep-all" type="button">keep all</button></div>
|
|
9726
10085
|
<p class="keys"><b>j</b>/<b>k</b> move · <b>space</b> keep or redact · <b>enter</b> confirm</p>
|
|
10086
|
+
${input.titleDraft === void 0 ? "" : `<fieldset>
|
|
10087
|
+
<legend><label for="title">Title</label></legend>
|
|
10088
|
+
<input class="title" id="title" name="title" type="text" required maxlength="${LIMITS.title}" value="${escapeHtml(input.titleDraft)}">
|
|
10089
|
+
<p class="note">Change it here, or leave it. A secret typed into it is redacted.</p>
|
|
10090
|
+
</fieldset>`}
|
|
9727
10091
|
<fieldset>
|
|
9728
10092
|
<legend>Visibility</legend>
|
|
9729
10093
|
<label class="vis"><input type="radio" name="visibility" value="private" checked> private</label>
|
|
@@ -9742,13 +10106,14 @@ function renderReviewPage(input) {
|
|
|
9742
10106
|
<script>var PUBLIC_WARNING=${JSON.stringify(publicWarning)};${SCRIPT}</script>`;
|
|
9743
10107
|
return page(`Flatbread review - ${trace.title}`, body);
|
|
9744
10108
|
}
|
|
9745
|
-
function renderDonePage(message, outputPath, publishTo) {
|
|
10109
|
+
function renderDonePage(message, outputPath, publishTo, keepsRun = false) {
|
|
9746
10110
|
const network = publishTo === void 0 ? `<p class="meta">Nothing was uploaded. This file is on your machine only.</p>` : `<p class="meta">Uploading this run to ${escapeHtml(publishTo)} now. Your terminal says whether it arrived, and prints its link.</p>`;
|
|
9747
10111
|
return page(
|
|
9748
10112
|
"Flatbread - share draft written",
|
|
9749
10113
|
`<div class="done"><h1>${escapeHtml(message)}</h1>
|
|
9750
10114
|
<code>${escapeHtml(outputPath)}</code>
|
|
9751
10115
|
${network}
|
|
10116
|
+
${keepsRun ? `<p class="meta">That file holds only your latest share, and the next one replaces it. This run is also kept under its own ID, printed in your terminal: <code>flatbread pull <id></code> prints it again, and <code>flatbread search --local</code> lists every run you have shared.</p>` : ""}
|
|
9752
10117
|
<p class="meta"><strong>Go back to your terminal now.</strong> The manage token for this
|
|
9753
10118
|
run is printed there, once, at the very bottom. It is the only way to open the run
|
|
9754
10119
|
while it is private, and only its hash is stored, so nothing can print it again.</p>
|
|
@@ -9777,7 +10142,9 @@ async function readBody(req, limitBytes = 2 * 1024 * 1024) {
|
|
|
9777
10142
|
function parseDecisionBody(body) {
|
|
9778
10143
|
const form = new URLSearchParams(body);
|
|
9779
10144
|
const parsedVisibility = VisibilitySchema.safeParse(form.get("visibility"));
|
|
10145
|
+
const title = form.get("title");
|
|
9780
10146
|
return {
|
|
10147
|
+
...title !== null ? { title: normalizeTitle(title) } : {},
|
|
9781
10148
|
visibility: parsedVisibility.success ? parsedVisibility.data : "private",
|
|
9782
10149
|
// ONE CHECKBOX MAY DECIDE MANY MATCHES. A grouped row carries every id it governs,
|
|
9783
10150
|
// space-separated, so the browser sends one value and the caller still gets the flat
|
|
@@ -9787,12 +10154,11 @@ function parseDecisionBody(body) {
|
|
|
9787
10154
|
};
|
|
9788
10155
|
}
|
|
9789
10156
|
async function listenOnFirstFreePort(server, ports) {
|
|
9790
|
-
for (const port of ports) {
|
|
10157
|
+
for (const port of [...ports, 0]) {
|
|
9791
10158
|
const ok = await new Promise((resolve) => {
|
|
9792
|
-
const onError = (
|
|
10159
|
+
const onError = () => {
|
|
9793
10160
|
server.removeListener("listening", onListening);
|
|
9794
|
-
|
|
9795
|
-
else resolve(false);
|
|
10161
|
+
resolve(false);
|
|
9796
10162
|
};
|
|
9797
10163
|
const onListening = () => {
|
|
9798
10164
|
server.removeListener("error", onError);
|
|
@@ -9852,10 +10218,26 @@ async function startReviewServer(options) {
|
|
|
9852
10218
|
void (async () => {
|
|
9853
10219
|
try {
|
|
9854
10220
|
const body = await readBody(req);
|
|
9855
|
-
const { visibility, keepIds } = parseDecisionBody(body);
|
|
9856
|
-
|
|
10221
|
+
const { visibility, keepIds, title } = parseDecisionBody(body);
|
|
10222
|
+
if (title === null) {
|
|
10223
|
+
send(
|
|
10224
|
+
400,
|
|
10225
|
+
page(
|
|
10226
|
+
"Flatbread - title refused",
|
|
10227
|
+
`<div class="done"><h1>That title cannot be used</h1><p class="meta">A title is 1-${LIMITS.title} characters. Go back, change it, and confirm again. Nothing was written.</p></div>`
|
|
10228
|
+
)
|
|
10229
|
+
);
|
|
10230
|
+
return;
|
|
10231
|
+
}
|
|
10232
|
+
const changed = title !== void 0 && options.titleDraft !== void 0 && title !== normalizeTitle(options.titleDraft);
|
|
10233
|
+
const confirmed = {
|
|
10234
|
+
action: "confirm",
|
|
10235
|
+
visibility,
|
|
10236
|
+
keepIds,
|
|
10237
|
+
...changed ? { title } : {}
|
|
10238
|
+
};
|
|
9857
10239
|
const message = options.onConfirm ? await options.onConfirm(confirmed) : "";
|
|
9858
|
-
send(200, renderDonePage("Share draft written", message, options.publishTo));
|
|
10240
|
+
send(200, renderDonePage("Share draft written", message, options.publishTo, options.keepsRun === true));
|
|
9859
10241
|
finish(confirmed);
|
|
9860
10242
|
} catch (error) {
|
|
9861
10243
|
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>`));
|
|
@@ -9890,6 +10272,16 @@ async function startReviewServer(options) {
|
|
|
9890
10272
|
}
|
|
9891
10273
|
|
|
9892
10274
|
// src/share.ts
|
|
10275
|
+
var spanKey = (match) => `${match.fieldPath}\0${match.start}\0${match.end}\0${match.original}`;
|
|
10276
|
+
function applyReview(trace, matches, keepIds, title) {
|
|
10277
|
+
if (title === void 0 || title === trace.title) return applyRedactions(trace, matches, { keepIds });
|
|
10278
|
+
const retitled = { ...trace, title };
|
|
10279
|
+
const kept = new Set(keepIds);
|
|
10280
|
+
const keptSpans = new Set(matches.filter((m) => kept.has(m.id) && m.fieldPath !== "title").map(spanKey));
|
|
10281
|
+
const rescanned = scanTrace(retitled).matches;
|
|
10282
|
+
const keep = rescanned.filter((m) => m.fieldPath !== "title" && keptSpans.has(spanKey(m))).map((m) => m.id);
|
|
10283
|
+
return applyRedactions(retitled, rescanned, { keepIds: keep });
|
|
10284
|
+
}
|
|
9893
10285
|
function openInBrowser(url) {
|
|
9894
10286
|
try {
|
|
9895
10287
|
if (process.platform === "win32") {
|
|
@@ -9999,6 +10391,7 @@ function reportPersist(outcome, options, log) {
|
|
|
9999
10391
|
log(` slug ${outcome.slug}`);
|
|
10000
10392
|
log(` visibility ${outcome.record.visibility}`);
|
|
10001
10393
|
if (outcome.recordPath) log(` record ${outcome.recordPath}`);
|
|
10394
|
+
log(` again flatbread pull ${outcome.slug} (the next share replaces ${path15.basename(options.outputPath)}, not this record)`);
|
|
10002
10395
|
log("");
|
|
10003
10396
|
log(`open ${outcome.publish?.ok ? outcome.publish.url : `${base}/r/${outcome.slug}`}`);
|
|
10004
10397
|
log("");
|
|
@@ -10064,17 +10457,22 @@ async function runShare(options) {
|
|
|
10064
10457
|
if (parsed.humanTest) console.error(` next step HUMAN-TESTS.md section ${parsed.humanTest}`);
|
|
10065
10458
|
return { exitCode: 1, status: "not-found" };
|
|
10066
10459
|
}
|
|
10067
|
-
const
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
options.
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10460
|
+
const cwd = parsed.trace.source.cwd;
|
|
10461
|
+
let lineage;
|
|
10462
|
+
if (options.commit !== void 0) {
|
|
10463
|
+
const bound = (options.bindCommit ?? ((dir, rev) => bindCommit(dir, rev)))(cwd, options.commit);
|
|
10464
|
+
if ("problem" in bound) {
|
|
10465
|
+
console.error(`--commit ${options.commit}: ${bound.problem}`);
|
|
10466
|
+
console.error("Nothing was written. Run it from the session's repository, or leave --commit off.");
|
|
10467
|
+
return { exitCode: 1, status: "bad-commit" };
|
|
10468
|
+
}
|
|
10469
|
+
lineage = bound;
|
|
10470
|
+
} else {
|
|
10471
|
+
lineage = (options.readLineage ?? ((dir) => readGitLineage(dir)))(cwd);
|
|
10077
10472
|
}
|
|
10473
|
+
const lined = withLineage(parsed.trace, lineage, options.prUrl);
|
|
10474
|
+
const trace = options.title !== void 0 ? FlatbreadTraceSchema.parse({ ...lined, title: options.title }) : lined;
|
|
10475
|
+
for (const note of parsed.notes) log(`note: ${note}`);
|
|
10078
10476
|
const scan = scanTrace(trace);
|
|
10079
10477
|
const byRule = /* @__PURE__ */ new Map();
|
|
10080
10478
|
for (const match of scan.matches) byRule.set(match.rule, (byRule.get(match.rule) ?? 0) + 1);
|
|
@@ -10101,11 +10499,13 @@ async function runShare(options) {
|
|
|
10101
10499
|
slots: scan.slots,
|
|
10102
10500
|
sessionPath: target,
|
|
10103
10501
|
draftPath: options.outputPath,
|
|
10502
|
+
titleDraft: previewTitle,
|
|
10503
|
+
keepsRun: options.persist !== false && (options.storeDir ?? resolveStoreDir()) !== null,
|
|
10104
10504
|
...isPublisher(reviewPublisher) ? { publishTo: reviewPublisher.description } : {},
|
|
10105
10505
|
...options.ports ? { ports: options.ports } : {},
|
|
10106
10506
|
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
10107
10507
|
onConfirm: (decision2) => {
|
|
10108
|
-
const applied2 =
|
|
10508
|
+
const applied2 = applyReview(trace, scan.matches, decision2.keepIds, decision2.title);
|
|
10109
10509
|
const draft2 = buildDraft(applied2.trace, applied2.report, decision2.visibility);
|
|
10110
10510
|
writeDraft(options.outputPath, draft2);
|
|
10111
10511
|
written = draft2;
|
|
@@ -10114,9 +10514,9 @@ async function runShare(options) {
|
|
|
10114
10514
|
});
|
|
10115
10515
|
if (!session) {
|
|
10116
10516
|
console.error(
|
|
10117
|
-
`could not bind a review port (tried ${(options.ports ??
|
|
10517
|
+
`could not bind a review port on ${REVIEW_SERVER.host} (tried ${(options.ports ?? REVIEW_SERVER.ports).join(", ")}, then any port the OS would give)`
|
|
10118
10518
|
);
|
|
10119
|
-
console.error("
|
|
10519
|
+
console.error("Loopback itself refused, so this machine needs a look; --no-review redacts everything without the page.");
|
|
10120
10520
|
return { exitCode: 1, status: "port-unavailable" };
|
|
10121
10521
|
}
|
|
10122
10522
|
log(` review at ${session.url}`);
|
|
@@ -10385,11 +10785,160 @@ function describeReadFailure(failure, what) {
|
|
|
10385
10785
|
|
|
10386
10786
|
// src/mcp.ts
|
|
10387
10787
|
import readline3 from "node:readline";
|
|
10788
|
+
|
|
10789
|
+
// src/bucket.ts
|
|
10790
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
10791
|
+
import fs18 from "node:fs";
|
|
10792
|
+
import path17 from "node:path";
|
|
10793
|
+
var BUCKET_INDEX_VERSION = 1;
|
|
10794
|
+
var BUCKET_INDEX_FILE = "index.json";
|
|
10795
|
+
var CAPTURE_LOG_FILE = "capture.log";
|
|
10796
|
+
var COMMIT_SESSION_WINDOW_MS = 2 * 60 * 60 * 1e3;
|
|
10797
|
+
function bucketDir(env = process.env) {
|
|
10798
|
+
return path17.join(flatbreadHome(env), "bucket");
|
|
10799
|
+
}
|
|
10800
|
+
function bucketSlug(harness, sessionKey) {
|
|
10801
|
+
return `local-${createHash2("sha256").update(`${harness}\0${sessionKey}`, "utf8").digest("hex").slice(0, 12)}`;
|
|
10802
|
+
}
|
|
10803
|
+
function rowOf(record) {
|
|
10804
|
+
const sha = record.trace.source.headCommitSha;
|
|
10805
|
+
return {
|
|
10806
|
+
summary: summarize(record),
|
|
10807
|
+
sessionPath: record.trace.source.sourcePath,
|
|
10808
|
+
capturedAt: record.updatedAt,
|
|
10809
|
+
...record.trace.source.commitNamed === true && sha !== void 0 ? { commit: sha } : {}
|
|
10810
|
+
};
|
|
10811
|
+
}
|
|
10812
|
+
function recordStems(dir) {
|
|
10813
|
+
return fs18.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();
|
|
10814
|
+
}
|
|
10815
|
+
function writeAtomic(file, body) {
|
|
10816
|
+
fs18.mkdirSync(path17.dirname(file), { recursive: true });
|
|
10817
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
10818
|
+
fs18.writeFileSync(temp, body, "utf8");
|
|
10819
|
+
fs18.renameSync(temp, file);
|
|
10820
|
+
}
|
|
10821
|
+
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);
|
|
10822
|
+
function readIndex(dir) {
|
|
10823
|
+
try {
|
|
10824
|
+
const parsed = JSON.parse(fs18.readFileSync(path17.join(dir, BUCKET_INDEX_FILE), "utf8"));
|
|
10825
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
10826
|
+
const { version, rows } = parsed;
|
|
10827
|
+
if (version !== BUCKET_INDEX_VERSION || !Array.isArray(rows)) return null;
|
|
10828
|
+
const out = [];
|
|
10829
|
+
for (const row of rows) {
|
|
10830
|
+
if (typeof row !== "object" || row === null) return null;
|
|
10831
|
+
const { summary, sessionPath, capturedAt, commit } = row;
|
|
10832
|
+
const parsedSummary = RecordSummarySchema.safeParse(summary);
|
|
10833
|
+
if (!parsedSummary.success || typeof sessionPath !== "string" || typeof capturedAt !== "string") return null;
|
|
10834
|
+
if (commit !== void 0 && typeof commit !== "string") return null;
|
|
10835
|
+
out.push({ summary: parsedSummary.data, sessionPath, capturedAt, ...commit !== void 0 ? { commit } : {} });
|
|
10836
|
+
}
|
|
10837
|
+
return out;
|
|
10838
|
+
} catch {
|
|
10839
|
+
return null;
|
|
10840
|
+
}
|
|
10841
|
+
}
|
|
10842
|
+
function writeIndex(dir, rows) {
|
|
10843
|
+
writeAtomic(path17.join(dir, BUCKET_INDEX_FILE), JSON.stringify({ version: BUCKET_INDEX_VERSION, rows: newestFirst(rows) }, null, 2) + "\n");
|
|
10844
|
+
}
|
|
10845
|
+
function readBucket(dir) {
|
|
10846
|
+
if (!fs18.existsSync(dir)) return { rows: [], rebuilt: false, unreadable: 0 };
|
|
10847
|
+
const stems = recordStems(dir);
|
|
10848
|
+
const indexed = readIndex(dir);
|
|
10849
|
+
if (indexed !== null && JSON.stringify(indexed.map((row) => row.summary.slug).sort()) === JSON.stringify(stems)) {
|
|
10850
|
+
return { rows: newestFirst(indexed), rebuilt: false, unreadable: 0 };
|
|
10851
|
+
}
|
|
10852
|
+
const { records, unreadable } = readStoreRecords(dir);
|
|
10853
|
+
const rows = newestFirst(records.map(rowOf));
|
|
10854
|
+
try {
|
|
10855
|
+
writeIndex(dir, rows);
|
|
10856
|
+
} catch {
|
|
10857
|
+
}
|
|
10858
|
+
return { rows, rebuilt: true, unreadable };
|
|
10859
|
+
}
|
|
10860
|
+
function readBucketRecord(dir, slug) {
|
|
10861
|
+
if (!SlugSchema.safeParse(slug).success) return null;
|
|
10862
|
+
try {
|
|
10863
|
+
const parsed = TraceRecordSchema.safeParse(JSON.parse(fs18.readFileSync(path17.join(dir, `${slug}.json`), "utf8")));
|
|
10864
|
+
return parsed.success ? parsed.data : null;
|
|
10865
|
+
} catch {
|
|
10866
|
+
return null;
|
|
10867
|
+
}
|
|
10868
|
+
}
|
|
10869
|
+
function matchesQuery(summary, query) {
|
|
10870
|
+
if (query.harness !== void 0 && summary.harness !== query.harness) return false;
|
|
10871
|
+
if (query.category !== void 0 && summary.category !== query.category) return false;
|
|
10872
|
+
if (query.onlyWithRecoveries === true && summary.failuresRecovered === 0) return false;
|
|
10873
|
+
return matchesSearch(summary, query.query ?? "");
|
|
10874
|
+
}
|
|
10875
|
+
async function captureToBucket(options) {
|
|
10876
|
+
const dir = options.dir ?? bucketDir(options.env);
|
|
10877
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
10878
|
+
const parsed = await parseWithHarness({ filePath: options.file, ...options.harness !== void 0 ? { harness: options.harness } : {} });
|
|
10879
|
+
if (!parsed.ok) return { status: "refused", reason: `could not parse ${options.file}: ${parsed.reason}` };
|
|
10880
|
+
const cwd = parsed.trace.source.cwd;
|
|
10881
|
+
let lineage;
|
|
10882
|
+
if (options.commit !== void 0) {
|
|
10883
|
+
const bound = (options.bindCommit ?? ((at, rev) => bindCommit(at, rev)))(cwd, options.commit);
|
|
10884
|
+
if ("problem" in bound) return { status: "refused", reason: `--commit ${options.commit}: ${bound.problem}` };
|
|
10885
|
+
lineage = bound;
|
|
10886
|
+
} else {
|
|
10887
|
+
lineage = (options.readLineage ?? ((at) => readGitLineage(at)))(cwd);
|
|
10888
|
+
}
|
|
10889
|
+
const trace = withLineage(parsed.trace, lineage);
|
|
10890
|
+
const scan = scanTrace(trace);
|
|
10891
|
+
const applied = applyRedactions(trace, scan.matches, { keepIds: [] });
|
|
10892
|
+
const sessionKey = trace.source.sessionId.length > 0 ? trace.source.sessionId : path17.resolve(options.file);
|
|
10893
|
+
const slug = bucketSlug(parsed.harness, sessionKey);
|
|
10894
|
+
const recordPath = path17.join(dir, `${slug}.json`);
|
|
10895
|
+
const previous = readBucketRecord(dir, slug);
|
|
10896
|
+
const commit = applied.trace.source.commitNamed === true ? applied.trace.source.headCommitSha : void 0;
|
|
10897
|
+
const draft = ShareDraftSchema.parse({
|
|
10898
|
+
schemaVersion: 1,
|
|
10899
|
+
createdAt: previous?.createdAt ?? now,
|
|
10900
|
+
trace: { ...applied.trace, visibility: "private" },
|
|
10901
|
+
redaction: applied.report,
|
|
10902
|
+
uploaded: false
|
|
10903
|
+
});
|
|
10904
|
+
const done = { slug, path: recordPath, steps: draft.trace.steps.length, redacted: draft.redaction.redactedCount, ...commit !== void 0 ? { commit } : {} };
|
|
10905
|
+
if (previous !== null && previous.trace.steps.length === draft.trace.steps.length && previous.trace.source.headCommitSha === draft.trace.source.headCommitSha && previous.trace.source.commitNamed === draft.trace.source.commitNamed) {
|
|
10906
|
+
return { status: "unchanged", ...done };
|
|
10907
|
+
}
|
|
10908
|
+
const record = buildRecord(draft, { slug, manageTokenHash: hashManageToken(mintManageToken()), now, origin: "local", visibility: "private" });
|
|
10909
|
+
writeAtomic(recordPath, JSON.stringify(record, null, 2) + "\n");
|
|
10910
|
+
const rows = readBucket(dir).rows.filter((row) => row.summary.slug !== slug);
|
|
10911
|
+
writeIndex(dir, [...rows, rowOf(record)]);
|
|
10912
|
+
return { status: "captured", ...done };
|
|
10913
|
+
}
|
|
10914
|
+
function sessionForRepo(repo, options = {}) {
|
|
10915
|
+
const projects = getProjectsDir(options.env ?? process.env);
|
|
10916
|
+
const folder = path17.join(projects, encodeCwd(path17.resolve(repo)));
|
|
10917
|
+
const newest = listSessionFiles(folder)[0];
|
|
10918
|
+
if (newest === void 0) return { problem: `no Claude Code session recorded for ${repo}` };
|
|
10919
|
+
const age = (options.nowMs ?? Date.now()) - newest.mtimeMs;
|
|
10920
|
+
const window = options.windowMs ?? COMMIT_SESSION_WINDOW_MS;
|
|
10921
|
+
if (age > window) {
|
|
10922
|
+
return { problem: `the newest session for ${repo} last moved ${Math.round(age / 6e4)} min ago, outside the ${Math.round(window / 6e4)} min window` };
|
|
10923
|
+
}
|
|
10924
|
+
return { file: newest.filePath };
|
|
10925
|
+
}
|
|
10926
|
+
function logCapture(dir, line, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
10927
|
+
try {
|
|
10928
|
+
fs18.mkdirSync(dir, { recursive: true });
|
|
10929
|
+
fs18.appendFileSync(path17.join(dir, CAPTURE_LOG_FILE), `${now} ${line.replace(/[\r\n]+/g, " ")}
|
|
10930
|
+
`, "utf8");
|
|
10931
|
+
} catch {
|
|
10932
|
+
}
|
|
10933
|
+
}
|
|
10934
|
+
|
|
10935
|
+
// src/mcp.ts
|
|
10388
10936
|
var MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
10389
|
-
var MCP_SERVER_VERSION = "0.
|
|
10390
|
-
function defaultMcpDeps(storeDir2) {
|
|
10937
|
+
var MCP_SERVER_VERSION = "0.3.0";
|
|
10938
|
+
function defaultMcpDeps(storeDir2, bucketDir2) {
|
|
10391
10939
|
return {
|
|
10392
10940
|
storeDir: storeDir2,
|
|
10941
|
+
bucketDir: bucketDir2,
|
|
10393
10942
|
search: (query) => searchRemote(query, { client: "mcp" }),
|
|
10394
10943
|
capsule: (ref) => fetchCapsule(ref, { client: "mcp" })
|
|
10395
10944
|
};
|
|
@@ -10434,12 +10983,6 @@ function hitLine(hit) {
|
|
|
10434
10983
|
if (hit.visibility !== "public") parts.push(hit.visibility);
|
|
10435
10984
|
return `- ${parts.join(" \xB7 ")}`;
|
|
10436
10985
|
}
|
|
10437
|
-
function matchesLocal(summary, query) {
|
|
10438
|
-
if (query.harness !== void 0 && summary.harness !== query.harness) return false;
|
|
10439
|
-
if (query.category !== void 0 && summary.category !== query.category) return false;
|
|
10440
|
-
if (query.onlyWithRecoveries === true && summary.failuresRecovered === 0) return false;
|
|
10441
|
-
return matchesSearch(summary, query.query ?? "");
|
|
10442
|
-
}
|
|
10443
10986
|
function readQuery(args) {
|
|
10444
10987
|
const query = {};
|
|
10445
10988
|
if (args["query"] !== void 0) {
|
|
@@ -10467,42 +11010,75 @@ function readQuery(args) {
|
|
|
10467
11010
|
function localRecords(deps) {
|
|
10468
11011
|
return deps.storeDir === null ? { records: [], unreadable: 0 } : readStoreRecords(deps.storeDir);
|
|
10469
11012
|
}
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
11013
|
+
var runs = (count) => `${count} matching run${count === 1 ? "" : "s"}`;
|
|
11014
|
+
function capturedLine(row) {
|
|
11015
|
+
return `${hitLine(row.summary)} \xB7 captured ${row.capturedAt.slice(0, 10)}${row.commit !== void 0 ? ` at ${row.commit.slice(0, 12)}` : ""}`;
|
|
11016
|
+
}
|
|
11017
|
+
async function searchEverywhere(query, deps, options = {}) {
|
|
10473
11018
|
const out = [];
|
|
10474
11019
|
const local = localRecords(deps);
|
|
10475
|
-
const localHits = local.records.map((record) => summarize(record)).filter((summary) =>
|
|
11020
|
+
const localHits = local.records.map((record) => summarize(record)).filter((summary) => matchesQuery(summary, query));
|
|
10476
11021
|
if (deps.storeDir !== null) {
|
|
10477
|
-
out.push(`This machine (${localHits.length}
|
|
11022
|
+
out.push(`This machine (${runs(localHits.length)}):`);
|
|
10478
11023
|
for (const hit of localHits) out.push(hitLine(hit));
|
|
10479
11024
|
if (local.unreadable > 0) out.push(`(${local.unreadable} local record file${local.unreadable === 1 ? "" : "s"} could not be read)`);
|
|
10480
11025
|
}
|
|
11026
|
+
const seen = new Set(localHits.map((hit) => hit.slug));
|
|
11027
|
+
for (const record of local.records) {
|
|
11028
|
+
const { harness, sessionId } = record.trace.source;
|
|
11029
|
+
if (sessionId.length > 0 && localHits.some((hit) => hit.slug === record.slug)) seen.add(bucketSlug(harness, sessionId));
|
|
11030
|
+
}
|
|
11031
|
+
if (deps.bucketDir !== null) {
|
|
11032
|
+
const bucket = readBucket(deps.bucketDir);
|
|
11033
|
+
const captured = bucket.rows.filter((row) => !seen.has(row.summary.slug) && matchesQuery(row.summary, query));
|
|
11034
|
+
if (out.length > 0) out.push("");
|
|
11035
|
+
out.push(`Captured on this machine, not shared (${runs(captured.length)}):`);
|
|
11036
|
+
for (const row of captured) {
|
|
11037
|
+
out.push(capturedLine(row));
|
|
11038
|
+
seen.add(row.summary.slug);
|
|
11039
|
+
}
|
|
11040
|
+
if (bucket.unreadable > 0) out.push(`(${bucket.unreadable} captured run file${bucket.unreadable === 1 ? "" : "s"} could not be read)`);
|
|
11041
|
+
}
|
|
11042
|
+
if (options.remote === false) {
|
|
11043
|
+
if (out.length > 0) out.push("");
|
|
11044
|
+
out.push("The site was not searched: --local");
|
|
11045
|
+
return { lines: out, siteSearched: false };
|
|
11046
|
+
}
|
|
10481
11047
|
const remote = await deps.search(query);
|
|
10482
11048
|
if (out.length > 0) out.push("");
|
|
10483
11049
|
if (remote.ok) {
|
|
10484
|
-
const seen = new Set(localHits.map((hit) => hit.slug));
|
|
10485
11050
|
const fresh = remote.results.filter((hit) => !seen.has(hit.slug));
|
|
10486
|
-
out.push(`${remote.site} (${fresh.length}
|
|
11051
|
+
out.push(`${remote.site} (${runs(fresh.length)}):`);
|
|
10487
11052
|
for (const hit of fresh) out.push(hitLine(hit));
|
|
10488
11053
|
} else {
|
|
10489
11054
|
out.push(`The site was not searched: ${describeReadFailure(remote, "search")}`);
|
|
10490
11055
|
}
|
|
10491
|
-
|
|
10492
|
-
|
|
11056
|
+
return { lines: out, siteSearched: remote.ok };
|
|
11057
|
+
}
|
|
11058
|
+
async function searchTool(args, deps) {
|
|
11059
|
+
const query = readQuery(args);
|
|
11060
|
+
if (typeof query === "string") return text(query, true);
|
|
11061
|
+
const { lines: lines2 } = await searchEverywhere(query, deps);
|
|
11062
|
+
return text([...lines2, "", "Pass a run ID to flatbread_get_capsule for what that run learned."].join("\n"));
|
|
11063
|
+
}
|
|
11064
|
+
async function readCapsule(ref, deps) {
|
|
11065
|
+
if (ref.site === void 0) {
|
|
11066
|
+
const stored = localRecords(deps).records.find((record) => record.slug === ref.slug);
|
|
11067
|
+
if (stored !== void 0) return { ok: true, capsule: buildCapsule(stored.trace), from: "store" };
|
|
11068
|
+
const captured = deps.bucketDir !== null ? readBucketRecord(deps.bucketDir, ref.slug) : null;
|
|
11069
|
+
if (captured !== null) return { ok: true, capsule: buildCapsule(captured.trace), from: "bucket" };
|
|
11070
|
+
}
|
|
11071
|
+
const remote = await deps.capsule(ref);
|
|
11072
|
+
return remote.ok ? { ok: true, capsule: remote.capsule, from: "site" } : remote;
|
|
10493
11073
|
}
|
|
10494
11074
|
async function capsuleTool(args, deps) {
|
|
10495
11075
|
const raw = args["slug"];
|
|
10496
11076
|
if (typeof raw !== "string") return text("slug must be a run ID or a /r/<id> link", true);
|
|
10497
11077
|
const ref = parseRunRef(raw);
|
|
10498
11078
|
if (ref === null) return text(`"${raw}" is not a run ID or a /r/<id> link`, true);
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
}
|
|
10503
|
-
const remote = await deps.capsule(ref);
|
|
10504
|
-
if (remote.ok) return text(remote.capsule);
|
|
10505
|
-
return text(describeReadFailure(remote, `capsule ${ref.slug}`), true);
|
|
11079
|
+
const read2 = await readCapsule(ref, deps);
|
|
11080
|
+
if (read2.ok) return text(read2.capsule);
|
|
11081
|
+
return text(describeReadFailure(read2, `capsule ${ref.slug}`), true);
|
|
10506
11082
|
}
|
|
10507
11083
|
async function handleMcpMessage(message, deps) {
|
|
10508
11084
|
if (typeof message !== "object" || message === null || Array.isArray(message)) {
|
|
@@ -10551,7 +11127,7 @@ async function handleMcpMessage(message, deps) {
|
|
|
10551
11127
|
}
|
|
10552
11128
|
async function runMcpServer(deps, io) {
|
|
10553
11129
|
const input = io?.input ?? process.stdin;
|
|
10554
|
-
const
|
|
11130
|
+
const write2 = io?.write ?? ((line) => process.stdout.write(line));
|
|
10555
11131
|
const log = io?.log ?? ((line) => process.stderr.write(`${line}
|
|
10556
11132
|
`));
|
|
10557
11133
|
log(`flatbread mcp ${MCP_SERVER_VERSION} on stdio${deps.storeDir === null ? " (no local store)" : ` (local store ${deps.storeDir})`}`);
|
|
@@ -10564,12 +11140,12 @@ async function runMcpServer(deps, io) {
|
|
|
10564
11140
|
try {
|
|
10565
11141
|
message = JSON.parse(line);
|
|
10566
11142
|
} catch {
|
|
10567
|
-
|
|
11143
|
+
write2(`${JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } })}
|
|
10568
11144
|
`);
|
|
10569
11145
|
return;
|
|
10570
11146
|
}
|
|
10571
11147
|
const reply = await handleMcpMessage(message, deps);
|
|
10572
|
-
if (reply !== null)
|
|
11148
|
+
if (reply !== null) write2(`${JSON.stringify(reply)}
|
|
10573
11149
|
`);
|
|
10574
11150
|
});
|
|
10575
11151
|
}
|
|
@@ -10577,10 +11153,251 @@ async function runMcpServer(deps, io) {
|
|
|
10577
11153
|
return 0;
|
|
10578
11154
|
}
|
|
10579
11155
|
|
|
11156
|
+
// src/claude-mcp.ts
|
|
11157
|
+
import fs19 from "node:fs";
|
|
11158
|
+
import os7 from "node:os";
|
|
11159
|
+
import path18 from "node:path";
|
|
11160
|
+
var MCP_SERVER_NAME = "flatbread";
|
|
11161
|
+
function claudeUserConfigPath(env = process.env) {
|
|
11162
|
+
const dir = env["CLAUDE_CONFIG_DIR"]?.trim();
|
|
11163
|
+
return path18.join(dir ? dir : os7.homedir(), ".claude.json");
|
|
11164
|
+
}
|
|
11165
|
+
function mcpEntryFor(execPath, entryPath) {
|
|
11166
|
+
return { type: "stdio", command: execPath, args: [entryPath, "mcp"] };
|
|
11167
|
+
}
|
|
11168
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11169
|
+
function runsFlatbreadMcp(entry) {
|
|
11170
|
+
if (!isObject(entry) || !Array.isArray(entry["args"])) return false;
|
|
11171
|
+
const words = [entry["command"], ...entry["args"]].filter((word) => typeof word === "string");
|
|
11172
|
+
return words[words.length - 1] === "mcp" && words.some((word) => /flatbread/i.test(word));
|
|
11173
|
+
}
|
|
11174
|
+
var BOM = "\uFEFF";
|
|
11175
|
+
function readConfig(file) {
|
|
11176
|
+
let raw;
|
|
11177
|
+
try {
|
|
11178
|
+
raw = fs19.readFileSync(file, "utf8");
|
|
11179
|
+
} catch (error) {
|
|
11180
|
+
if (error.code === "ENOENT") return { root: {}, raw: null };
|
|
11181
|
+
return { problem: error.message };
|
|
11182
|
+
}
|
|
11183
|
+
const text2 = raw.startsWith(BOM) ? raw.slice(1) : raw;
|
|
11184
|
+
if (text2.trim() === "") return { root: {}, raw };
|
|
11185
|
+
let parsed;
|
|
11186
|
+
try {
|
|
11187
|
+
parsed = JSON.parse(text2);
|
|
11188
|
+
} catch (error) {
|
|
11189
|
+
return { problem: `not JSON (${error.message})` };
|
|
11190
|
+
}
|
|
11191
|
+
if (!isObject(parsed)) return { problem: "not a JSON object" };
|
|
11192
|
+
if (parsed["mcpServers"] !== void 0 && !isObject(parsed["mcpServers"])) return { problem: "its mcpServers is not an object" };
|
|
11193
|
+
return { root: parsed, raw };
|
|
11194
|
+
}
|
|
11195
|
+
function serializeLike(raw, root) {
|
|
11196
|
+
let body = JSON.stringify(root, null, 2);
|
|
11197
|
+
if (raw === null || raw.trimEnd() !== raw || raw.trim() === "") body += "\n";
|
|
11198
|
+
if (raw !== null && raw.includes("\r\n")) body = body.replace(/\n/g, "\r\n");
|
|
11199
|
+
if (raw !== null && raw.startsWith(BOM)) body = BOM + body;
|
|
11200
|
+
return body;
|
|
11201
|
+
}
|
|
11202
|
+
function write(file, read2, next) {
|
|
11203
|
+
if (read2.raw !== null) {
|
|
11204
|
+
try {
|
|
11205
|
+
fs19.accessSync(file, fs19.constants.W_OK);
|
|
11206
|
+
} catch {
|
|
11207
|
+
return `${file} is read-only, so it was left as it was`;
|
|
11208
|
+
}
|
|
11209
|
+
if (fs19.readFileSync(file, "utf8") !== read2.raw) return `${file} changed while setup ran (Claude Code writes it); run setup again`;
|
|
11210
|
+
const backup = `${file}.flatbread.bak`;
|
|
11211
|
+
if (!fs19.existsSync(backup)) fs19.copyFileSync(file, backup);
|
|
11212
|
+
}
|
|
11213
|
+
const mode = read2.raw !== null ? fs19.statSync(file).mode & 511 : 384;
|
|
11214
|
+
fs19.mkdirSync(path18.dirname(file), { recursive: true });
|
|
11215
|
+
const temp = `${file}.flatbread-${process.pid}.tmp`;
|
|
11216
|
+
fs19.writeFileSync(temp, next, { encoding: "utf8", mode });
|
|
11217
|
+
fs19.renameSync(temp, file);
|
|
11218
|
+
return null;
|
|
11219
|
+
}
|
|
11220
|
+
function registerClaudeMcp(options) {
|
|
11221
|
+
const { file, log } = options;
|
|
11222
|
+
const read2 = readConfig(file);
|
|
11223
|
+
if ("problem" in read2) {
|
|
11224
|
+
log(` mcp refused: ${file}: ${read2.problem}`);
|
|
11225
|
+
log(" Claude Code's own state lives there, so a file we cannot read is not one we rewrite.");
|
|
11226
|
+
if (options.uninstall !== true) log(` by hand: claude mcp add --scope user ${MCP_SERVER_NAME} -- "${options.execPath}" "${options.entryPath}" mcp`);
|
|
11227
|
+
return 1;
|
|
11228
|
+
}
|
|
11229
|
+
const servers = isObject(read2.root["mcpServers"]) ? read2.root["mcpServers"] : void 0;
|
|
11230
|
+
const current = servers?.[MCP_SERVER_NAME];
|
|
11231
|
+
if (options.uninstall === true) {
|
|
11232
|
+
if (current === void 0) {
|
|
11233
|
+
log(` mcp no ${MCP_SERVER_NAME} server in ${file}`);
|
|
11234
|
+
return 0;
|
|
11235
|
+
}
|
|
11236
|
+
if (!runsFlatbreadMcp(current)) {
|
|
11237
|
+
log(` mcp ${file}: the ${MCP_SERVER_NAME} server there does not run flatbread, left alone`);
|
|
11238
|
+
return 0;
|
|
11239
|
+
}
|
|
11240
|
+
if (options.dryRun === true) {
|
|
11241
|
+
log(` mcp ${file}: would remove the ${MCP_SERVER_NAME} server`);
|
|
11242
|
+
return 0;
|
|
11243
|
+
}
|
|
11244
|
+
const rest = { ...servers };
|
|
11245
|
+
delete rest[MCP_SERVER_NAME];
|
|
11246
|
+
const problem2 = write(file, read2, serializeLike(read2.raw, { ...read2.root, mcpServers: rest }));
|
|
11247
|
+
if (problem2 !== null) {
|
|
11248
|
+
log(` mcp refused: ${problem2}`);
|
|
11249
|
+
return 1;
|
|
11250
|
+
}
|
|
11251
|
+
log(` mcp ${file}: removed the ${MCP_SERVER_NAME} server`);
|
|
11252
|
+
const backup = `${file}.flatbread.bak`;
|
|
11253
|
+
if (fs19.existsSync(backup)) log(` ${backup} is the copy setup took before its first write; delete it when you like`);
|
|
11254
|
+
return 0;
|
|
11255
|
+
}
|
|
11256
|
+
const entry = mcpEntryFor(options.execPath, options.entryPath);
|
|
11257
|
+
if (current !== void 0 && !runsFlatbreadMcp(current)) {
|
|
11258
|
+
log(` mcp refused: ${file} already has a server named ${MCP_SERVER_NAME} that does not run flatbread, left alone`);
|
|
11259
|
+
return 1;
|
|
11260
|
+
}
|
|
11261
|
+
if (JSON.stringify(current) === JSON.stringify(entry)) {
|
|
11262
|
+
log(` mcp ${file}: registered for every project, unchanged`);
|
|
11263
|
+
return 0;
|
|
11264
|
+
}
|
|
11265
|
+
const verb = current === void 0 ? "register" : "update";
|
|
11266
|
+
if (options.dryRun === true) {
|
|
11267
|
+
log(` mcp ${file}: would ${verb} "${MCP_SERVER_NAME}": ${JSON.stringify(entry)}`);
|
|
11268
|
+
return 0;
|
|
11269
|
+
}
|
|
11270
|
+
const root = { ...read2.root, mcpServers: { ...servers ?? {}, [MCP_SERVER_NAME]: entry } };
|
|
11271
|
+
const problem = write(file, read2, serializeLike(read2.raw, root));
|
|
11272
|
+
if (problem !== null) {
|
|
11273
|
+
log(` mcp refused: ${problem}`);
|
|
11274
|
+
return 1;
|
|
11275
|
+
}
|
|
11276
|
+
log(` mcp ${file}: ${verb === "register" ? "registered" : "updated"} for every project (user scope)`);
|
|
11277
|
+
log(" restart Claude Code (or reload the VS Code window) so it starts the server");
|
|
11278
|
+
return 0;
|
|
11279
|
+
}
|
|
11280
|
+
|
|
11281
|
+
// src/git-hook.ts
|
|
11282
|
+
import fs20 from "node:fs";
|
|
11283
|
+
import path19 from "node:path";
|
|
11284
|
+
var POST_COMMIT_MARKER = "# flatbread post-commit hook - remove with: flatbread setup --uninstall";
|
|
11285
|
+
var CHAINED_HOOK = "post-commit.pre-flatbread";
|
|
11286
|
+
var sq = (value) => `'${value.replace(/\\/g, "/").replace(/'/g, "'\\''")}'`;
|
|
11287
|
+
function captureLine(execPath, entryPath) {
|
|
11288
|
+
return `${sq(execPath)} ${sq(entryPath)} capture --commit "$(git rev-parse HEAD)" --repo "$(git rev-parse --show-toplevel)" --quiet >/dev/null 2>&1 &`;
|
|
11289
|
+
}
|
|
11290
|
+
function postCommitScript(execPath, entryPath) {
|
|
11291
|
+
return [
|
|
11292
|
+
"#!/bin/sh",
|
|
11293
|
+
POST_COMMIT_MARKER,
|
|
11294
|
+
"# Runs the hook that was here before, then records this commit against the newest agent session",
|
|
11295
|
+
"# in this repository, on this machine only. It never changes the commit; it exits with the",
|
|
11296
|
+
"# earlier hook's status. Off switch: FLATBREAD_HOOK_DISABLED=1",
|
|
11297
|
+
"status=0",
|
|
11298
|
+
`prior="$(dirname "$0")/${CHAINED_HOOK}"`,
|
|
11299
|
+
'if [ -x "$prior" ]; then',
|
|
11300
|
+
' "$prior" "$@"',
|
|
11301
|
+
" status=$?",
|
|
11302
|
+
"fi",
|
|
11303
|
+
'if [ -z "$FLATBREAD_HOOK_DISABLED" ]; then',
|
|
11304
|
+
` ( ${captureLine(execPath, entryPath)} )`,
|
|
11305
|
+
"fi",
|
|
11306
|
+
'exit "$status"',
|
|
11307
|
+
""
|
|
11308
|
+
].join("\n");
|
|
11309
|
+
}
|
|
11310
|
+
var isOurs = (body) => body.includes(POST_COMMIT_MARKER);
|
|
11311
|
+
function hooksDirFor(cwd, options = {}) {
|
|
11312
|
+
const top = git(cwd, ["rev-parse", "--show-toplevel"], options)?.trim();
|
|
11313
|
+
if (top === void 0 || top.length === 0) return { ok: false, reason: "not-a-repo", detail: `${cwd} is not inside a git repository` };
|
|
11314
|
+
const managed = git(top, ["config", "--show-scope", "--get", "core.hooksPath"], options)?.trim();
|
|
11315
|
+
let machineHooksPath;
|
|
11316
|
+
if (managed !== void 0 && managed.length > 0) {
|
|
11317
|
+
const tab = managed.indexOf(" ");
|
|
11318
|
+
const scope = tab > 0 ? managed.slice(0, tab) : "local";
|
|
11319
|
+
const value = tab > 0 ? managed.slice(tab + 1) : managed;
|
|
11320
|
+
if (scope !== "system" && scope !== "global") {
|
|
11321
|
+
return { ok: false, reason: "hooks-path", detail: `core.hooksPath is set to ${value} in this repository, so another tool owns its hooks` };
|
|
11322
|
+
}
|
|
11323
|
+
machineHooksPath = { scope, path: value };
|
|
11324
|
+
}
|
|
11325
|
+
const common = git(top, ["rev-parse", "--git-common-dir"], options)?.trim();
|
|
11326
|
+
if (common === void 0 || common.length === 0) return { ok: false, reason: "unreadable", detail: `git could not name the git directory of ${top}` };
|
|
11327
|
+
return { ok: true, repo: path19.resolve(top), dir: path19.resolve(top, common, "hooks"), ...machineHooksPath !== void 0 ? { machineHooksPath } : {} };
|
|
11328
|
+
}
|
|
11329
|
+
var read = (file) => {
|
|
11330
|
+
try {
|
|
11331
|
+
return fs20.readFileSync(file, "utf8");
|
|
11332
|
+
} catch {
|
|
11333
|
+
return null;
|
|
11334
|
+
}
|
|
11335
|
+
};
|
|
11336
|
+
function installGitHook(dir, script, options = {}) {
|
|
11337
|
+
const hook = path19.join(dir, "post-commit");
|
|
11338
|
+
const chained = path19.join(dir, CHAINED_HOOK);
|
|
11339
|
+
const current = read(hook);
|
|
11340
|
+
const write2 = (lines2) => {
|
|
11341
|
+
if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
|
|
11342
|
+
fs20.mkdirSync(dir, { recursive: true });
|
|
11343
|
+
if (current !== null && !isOurs(current)) fs20.renameSync(hook, chained);
|
|
11344
|
+
fs20.writeFileSync(hook, script, { encoding: "utf8", mode: 493 });
|
|
11345
|
+
fs20.chmodSync(hook, 493);
|
|
11346
|
+
return { code: 0, lines: lines2 };
|
|
11347
|
+
};
|
|
11348
|
+
if (current === null) return write2([`post-commit ${hook} (new)`]);
|
|
11349
|
+
if (isOurs(current)) {
|
|
11350
|
+
if (current === script) return { code: 0, lines: [`post-commit ${hook}`, "already installed, unchanged"] };
|
|
11351
|
+
return write2([`post-commit ${hook}`, "updated the flatbread hook"]);
|
|
11352
|
+
}
|
|
11353
|
+
if (fs20.existsSync(chained)) {
|
|
11354
|
+
return {
|
|
11355
|
+
code: 1,
|
|
11356
|
+
lines: [
|
|
11357
|
+
`refused ${hook}`,
|
|
11358
|
+
`${chained} already exists, and post-commit is not ours. Neither file was touched.`,
|
|
11359
|
+
"Move one of them yourself, then run setup again."
|
|
11360
|
+
]
|
|
11361
|
+
};
|
|
11362
|
+
}
|
|
11363
|
+
return write2([
|
|
11364
|
+
`post-commit ${hook}`,
|
|
11365
|
+
`your post-commit was kept as ${CHAINED_HOOK} and runs first, unchanged, with its exit status`
|
|
11366
|
+
]);
|
|
11367
|
+
}
|
|
11368
|
+
function uninstallGitHook(dir, options = {}) {
|
|
11369
|
+
const hook = path19.join(dir, "post-commit");
|
|
11370
|
+
const chained = path19.join(dir, CHAINED_HOOK);
|
|
11371
|
+
const current = read(hook);
|
|
11372
|
+
const hasChained = fs20.existsSync(chained);
|
|
11373
|
+
const done = (lines2, act) => {
|
|
11374
|
+
if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
|
|
11375
|
+
act();
|
|
11376
|
+
return { code: 0, lines: lines2 };
|
|
11377
|
+
};
|
|
11378
|
+
if (current !== null && !isOurs(current)) {
|
|
11379
|
+
return {
|
|
11380
|
+
code: 0,
|
|
11381
|
+
lines: [
|
|
11382
|
+
`post-commit ${hook} is not ours, left alone`,
|
|
11383
|
+
...hasChained ? [`${chained} is still there; it is yours to keep or delete`] : []
|
|
11384
|
+
]
|
|
11385
|
+
};
|
|
11386
|
+
}
|
|
11387
|
+
if (current === null && !hasChained) return { code: 0, lines: [`no flatbread post-commit in ${dir}`] };
|
|
11388
|
+
if (hasChained) {
|
|
11389
|
+
return done([`post-commit ${hook}`, `restored your post-commit from ${CHAINED_HOOK}`], () => {
|
|
11390
|
+
if (current !== null) fs20.rmSync(hook);
|
|
11391
|
+
fs20.renameSync(chained, hook);
|
|
11392
|
+
});
|
|
11393
|
+
}
|
|
11394
|
+
return done([`post-commit ${hook}`, "removed the flatbread hook"], () => fs20.rmSync(hook));
|
|
11395
|
+
}
|
|
11396
|
+
|
|
10580
11397
|
// src/hook.ts
|
|
10581
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
10582
|
-
import
|
|
10583
|
-
import
|
|
11398
|
+
import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
|
|
11399
|
+
import fs21 from "node:fs";
|
|
11400
|
+
import path20 from "node:path";
|
|
10584
11401
|
var HOOK_EVENTS = ["Stop", "SessionEnd"];
|
|
10585
11402
|
var OFFER_AFTER_STOPS = 2;
|
|
10586
11403
|
var MAX_TRACKED_SESSIONS = 50;
|
|
@@ -10590,14 +11407,14 @@ function emptyHookState() {
|
|
|
10590
11407
|
return { version: HOOK_STATE_VERSION, sessions: {} };
|
|
10591
11408
|
}
|
|
10592
11409
|
function hookStatePath(env = process.env) {
|
|
10593
|
-
return
|
|
11410
|
+
return path20.join(flatbreadHome(env), "hook-state.json");
|
|
10594
11411
|
}
|
|
10595
11412
|
function readHookState(env = process.env) {
|
|
10596
11413
|
const file = hookStatePath(env);
|
|
10597
|
-
if (!
|
|
11414
|
+
if (!fs21.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
|
|
10598
11415
|
let raw;
|
|
10599
11416
|
try {
|
|
10600
|
-
raw =
|
|
11417
|
+
raw = fs21.readFileSync(file, "utf8");
|
|
10601
11418
|
} catch (error) {
|
|
10602
11419
|
return { ok: false, reason: "unreadable", detail: error.message };
|
|
10603
11420
|
}
|
|
@@ -10640,9 +11457,9 @@ function pruneHookState(state, keep = MAX_TRACKED_SESSIONS) {
|
|
|
10640
11457
|
}
|
|
10641
11458
|
function writeHookState(state, env = process.env) {
|
|
10642
11459
|
const dir = flatbreadHome(env);
|
|
10643
|
-
|
|
11460
|
+
fs21.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
10644
11461
|
const file = hookStatePath(env);
|
|
10645
|
-
|
|
11462
|
+
fs21.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
|
|
10646
11463
|
encoding: "utf8",
|
|
10647
11464
|
mode: 384
|
|
10648
11465
|
});
|
|
@@ -10777,12 +11594,18 @@ function offerAfterFrom(env) {
|
|
|
10777
11594
|
}
|
|
10778
11595
|
return { value: parsed };
|
|
10779
11596
|
}
|
|
11597
|
+
function spawnDetachedDefault(command, args) {
|
|
11598
|
+
const child = spawn2(command, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
11599
|
+
child.on("error", () => {
|
|
11600
|
+
});
|
|
11601
|
+
child.unref();
|
|
11602
|
+
}
|
|
10780
11603
|
async function runHookNotify(io) {
|
|
10781
11604
|
const env = io.env ?? process.env;
|
|
10782
11605
|
const stdout = io.stdout ?? ((line) => process.stdout.write(line + "\n"));
|
|
10783
11606
|
const stderr = io.stderr ?? ((line) => process.stderr.write(line + "\n"));
|
|
10784
11607
|
const now = io.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
10785
|
-
const exists = io.exists ?? ((filePath) =>
|
|
11608
|
+
const exists = io.exists ?? ((filePath) => fs21.existsSync(filePath));
|
|
10786
11609
|
const execPath = io.execPath ?? process.execPath;
|
|
10787
11610
|
const entryPath = io.entryPath ?? process.argv[1] ?? "";
|
|
10788
11611
|
try {
|
|
@@ -10796,14 +11619,14 @@ async function runHookNotify(io) {
|
|
|
10796
11619
|
stderr(`flatbread hook: NOT RETRIEVED (${parsed.detail})`);
|
|
10797
11620
|
return 0;
|
|
10798
11621
|
}
|
|
10799
|
-
const
|
|
10800
|
-
if (!
|
|
10801
|
-
stderr(`flatbread hook: NOT RETRIEVED (${
|
|
11622
|
+
const read2 = readHookState(env);
|
|
11623
|
+
if (!read2.ok) {
|
|
11624
|
+
stderr(`flatbread hook: NOT RETRIEVED (${read2.reason}: ${read2.detail}) at ${hookStatePath(env)}`);
|
|
10802
11625
|
return 0;
|
|
10803
11626
|
}
|
|
10804
11627
|
const offerAfter = offerAfterFrom(env);
|
|
10805
11628
|
if (offerAfter.ignored) stderr(`flatbread hook: ${offerAfter.ignored}, using ${OFFER_AFTER_STOPS}`);
|
|
10806
|
-
const decision = decideHook(parsed.event,
|
|
11629
|
+
const decision = decideHook(parsed.event, read2.state, {
|
|
10807
11630
|
now: now(),
|
|
10808
11631
|
exists,
|
|
10809
11632
|
offerAfter: offerAfter.value,
|
|
@@ -10811,7 +11634,7 @@ async function runHookNotify(io) {
|
|
|
10811
11634
|
entryPath
|
|
10812
11635
|
});
|
|
10813
11636
|
let recorded = true;
|
|
10814
|
-
if (decision.nextState !==
|
|
11637
|
+
if (decision.nextState !== read2.state) {
|
|
10815
11638
|
try {
|
|
10816
11639
|
writeHookState(decision.nextState, env);
|
|
10817
11640
|
} catch (error) {
|
|
@@ -10819,6 +11642,11 @@ async function runHookNotify(io) {
|
|
|
10819
11642
|
stderr(`flatbread hook: could not record state (${error.message})`);
|
|
10820
11643
|
}
|
|
10821
11644
|
}
|
|
11645
|
+
const transcript = parsed.event.transcript_path;
|
|
11646
|
+
if (io.capture === true && parsed.event.hook_event_name === "SessionEnd" && transcript !== void 0 && exists(transcript)) {
|
|
11647
|
+
(io.spawnDetached ?? spawnDetachedDefault)(execPath, [entryPath, "capture", "--file", transcript, "--quiet"]);
|
|
11648
|
+
stderr("flatbread hook: capturing this session into the local bucket, in the background");
|
|
11649
|
+
}
|
|
10822
11650
|
if (decision.speak && recorded) {
|
|
10823
11651
|
stdout(JSON.stringify({ systemMessage: decision.systemMessage }));
|
|
10824
11652
|
stderr(`flatbread hook: ${decision.why}`);
|
|
@@ -10839,7 +11667,7 @@ function hookHandlerFor(options) {
|
|
|
10839
11667
|
const handler = {
|
|
10840
11668
|
type: "command",
|
|
10841
11669
|
command: options.execPath,
|
|
10842
|
-
args: [options.entryPath, "hook", "notify"]
|
|
11670
|
+
args: [options.entryPath, "hook", "notify", ...options.capture === true && options.event === "SessionEnd" ? ["--capture"] : []]
|
|
10843
11671
|
};
|
|
10844
11672
|
if (options.event === "Stop") handler.timeout = STOP_TIMEOUT_SECONDS;
|
|
10845
11673
|
return handler;
|
|
@@ -10934,10 +11762,10 @@ function removeHookConfig(settings) {
|
|
|
10934
11762
|
return { settings: root, removed };
|
|
10935
11763
|
}
|
|
10936
11764
|
function readSettingsFile(file) {
|
|
10937
|
-
if (!
|
|
11765
|
+
if (!fs21.existsSync(file)) return { present: false, file };
|
|
10938
11766
|
let raw;
|
|
10939
11767
|
try {
|
|
10940
|
-
raw =
|
|
11768
|
+
raw = fs21.readFileSync(file, "utf8");
|
|
10941
11769
|
} catch (error) {
|
|
10942
11770
|
return { present: true, file, unreadable: error.message };
|
|
10943
11771
|
}
|
|
@@ -10949,19 +11777,19 @@ function readSettingsFile(file) {
|
|
|
10949
11777
|
}
|
|
10950
11778
|
}
|
|
10951
11779
|
function writeSettingsFile(file, settings) {
|
|
10952
|
-
|
|
11780
|
+
fs21.mkdirSync(path20.dirname(file), { recursive: true });
|
|
10953
11781
|
const temp = `${file}.flatbread-${process.pid}.tmp`;
|
|
10954
|
-
|
|
10955
|
-
|
|
11782
|
+
fs21.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
11783
|
+
fs21.renameSync(temp, file);
|
|
10956
11784
|
}
|
|
10957
|
-
function handlersFor(execPath, entryPath) {
|
|
11785
|
+
function handlersFor(execPath, entryPath, capture = false) {
|
|
10958
11786
|
return {
|
|
10959
11787
|
Stop: hookHandlerFor({ execPath, entryPath, event: "Stop" }),
|
|
10960
|
-
SessionEnd: hookHandlerFor({ execPath, entryPath, event: "SessionEnd" })
|
|
11788
|
+
SessionEnd: hookHandlerFor({ execPath, entryPath, event: "SessionEnd", capture })
|
|
10961
11789
|
};
|
|
10962
11790
|
}
|
|
10963
|
-
function handlerBlockFor(execPath, entryPath) {
|
|
10964
|
-
const handlers = handlersFor(execPath, entryPath);
|
|
11791
|
+
function handlerBlockFor(execPath, entryPath, capture = false) {
|
|
11792
|
+
const handlers = handlersFor(execPath, entryPath, capture);
|
|
10965
11793
|
const hooks = {};
|
|
10966
11794
|
for (const event of HOOK_EVENTS) hooks[event] = [{ hooks: [handlers[event]] }];
|
|
10967
11795
|
return JSON.stringify({ hooks }, null, 2);
|
|
@@ -10972,21 +11800,22 @@ function runHookInstall(options = {}) {
|
|
|
10972
11800
|
const execPath = options.execPath ?? process.execPath;
|
|
10973
11801
|
const entryPath = options.entryPath ?? process.argv[1] ?? "";
|
|
10974
11802
|
const file = options.settingsPath ?? claudeSettingsPath(env);
|
|
10975
|
-
const
|
|
10976
|
-
|
|
11803
|
+
const capture = options.capture === true;
|
|
11804
|
+
const read2 = readSettingsFile(file);
|
|
11805
|
+
if (read2.present && "unreadable" in read2) {
|
|
10977
11806
|
log("");
|
|
10978
11807
|
log(` refused ${file}`);
|
|
10979
|
-
log(` ${
|
|
11808
|
+
log(` ${read2.unreadable}`);
|
|
10980
11809
|
log(" a settings file we cannot read is not one we will rewrite. Add this by hand:");
|
|
10981
11810
|
log("");
|
|
10982
|
-
for (const line of handlerBlockFor(execPath, entryPath).split("\n")) log(` ${line}`);
|
|
11811
|
+
for (const line of handlerBlockFor(execPath, entryPath, capture).split("\n")) log(` ${line}`);
|
|
10983
11812
|
log("");
|
|
10984
11813
|
return 1;
|
|
10985
11814
|
}
|
|
10986
|
-
const current =
|
|
11815
|
+
const current = read2.present ? read2.parsed : {};
|
|
10987
11816
|
let merged;
|
|
10988
11817
|
try {
|
|
10989
|
-
merged = mergeHookConfig(current, handlersFor(execPath, entryPath));
|
|
11818
|
+
merged = mergeHookConfig(current, handlersFor(execPath, entryPath, capture));
|
|
10990
11819
|
} catch (error) {
|
|
10991
11820
|
log("");
|
|
10992
11821
|
log(` refused ${file}`);
|
|
@@ -10995,7 +11824,7 @@ function runHookInstall(options = {}) {
|
|
|
10995
11824
|
return 1;
|
|
10996
11825
|
}
|
|
10997
11826
|
log("");
|
|
10998
|
-
log(` settings ${file}${
|
|
11827
|
+
log(` settings ${file}${read2.present ? "" : " (new)"}`);
|
|
10999
11828
|
log(` command "${execPath}" "${entryPath}" hook notify`);
|
|
11000
11829
|
if (merged.changes.length === 0) {
|
|
11001
11830
|
log(" already installed, unchanged");
|
|
@@ -11019,7 +11848,7 @@ function runHookInstall(options = {}) {
|
|
|
11019
11848
|
log(" refused: nothing was written, because this command line does not work here.");
|
|
11020
11849
|
log(" fix the command (or pass --no-probe if you know better), then install again:");
|
|
11021
11850
|
log("");
|
|
11022
|
-
for (const line of handlerBlockFor(execPath, entryPath).split("\n")) log(` ${line}`);
|
|
11851
|
+
for (const line of handlerBlockFor(execPath, entryPath, capture).split("\n")) log(` ${line}`);
|
|
11023
11852
|
log("");
|
|
11024
11853
|
return 1;
|
|
11025
11854
|
}
|
|
@@ -11048,7 +11877,9 @@ function runHookInstall(options = {}) {
|
|
|
11048
11877
|
log("");
|
|
11049
11878
|
return 1;
|
|
11050
11879
|
}
|
|
11051
|
-
log(
|
|
11880
|
+
log(
|
|
11881
|
+
capture ? " wrote it. Stop offers the share once per session; SessionEnd captures the run into the local bucket." : " wrote it. Stop offers the share once per session; SessionEnd only records the end."
|
|
11882
|
+
);
|
|
11052
11883
|
log(" off switch: FLATBREAD_HOOK_DISABLED=1, or flatbread hook uninstall");
|
|
11053
11884
|
log("");
|
|
11054
11885
|
return 0;
|
|
@@ -11057,23 +11888,23 @@ function runHookUninstall(options = {}) {
|
|
|
11057
11888
|
const env = options.env ?? process.env;
|
|
11058
11889
|
const log = options.log ?? ((line) => console.log(line));
|
|
11059
11890
|
const file = options.settingsPath ?? claudeSettingsPath(env);
|
|
11060
|
-
const
|
|
11891
|
+
const read2 = readSettingsFile(file);
|
|
11061
11892
|
log("");
|
|
11062
|
-
if (!
|
|
11893
|
+
if (!read2.present) {
|
|
11063
11894
|
log(` nothing to remove: ${file} does not exist`);
|
|
11064
11895
|
log("");
|
|
11065
11896
|
return 0;
|
|
11066
11897
|
}
|
|
11067
|
-
if ("unreadable" in
|
|
11898
|
+
if ("unreadable" in read2) {
|
|
11068
11899
|
log(` refused ${file}`);
|
|
11069
|
-
log(` ${
|
|
11900
|
+
log(` ${read2.unreadable}`);
|
|
11070
11901
|
log(" remove the flatbread entries by hand rather than let us rewrite a file we cannot read");
|
|
11071
11902
|
log("");
|
|
11072
11903
|
return 1;
|
|
11073
11904
|
}
|
|
11074
11905
|
let result;
|
|
11075
11906
|
try {
|
|
11076
|
-
result = removeHookConfig(
|
|
11907
|
+
result = removeHookConfig(read2.parsed);
|
|
11077
11908
|
} catch (error) {
|
|
11078
11909
|
log(` refused ${error instanceof Error ? error.message : String(error)}`);
|
|
11079
11910
|
log("");
|
|
@@ -11084,6 +11915,13 @@ function runHookUninstall(options = {}) {
|
|
|
11084
11915
|
log("");
|
|
11085
11916
|
return 0;
|
|
11086
11917
|
}
|
|
11918
|
+
if (options.dryRun === true) {
|
|
11919
|
+
log(` settings ${file}`);
|
|
11920
|
+
for (const line of result.removed) log(` ${line}`);
|
|
11921
|
+
log(" --dry-run: nothing was written");
|
|
11922
|
+
log("");
|
|
11923
|
+
return 0;
|
|
11924
|
+
}
|
|
11087
11925
|
try {
|
|
11088
11926
|
writeSettingsFile(file, result.settings);
|
|
11089
11927
|
} catch (error) {
|
|
@@ -11100,8 +11938,8 @@ function runHookUninstall(options = {}) {
|
|
|
11100
11938
|
function settingsCandidates(env, cwd) {
|
|
11101
11939
|
return [
|
|
11102
11940
|
{ label: "user", file: claudeSettingsPath(env) },
|
|
11103
|
-
{ label: "project", file:
|
|
11104
|
-
{ label: "local", file:
|
|
11941
|
+
{ label: "project", file: path20.join(cwd, ".claude", "settings.json") },
|
|
11942
|
+
{ label: "local", file: path20.join(cwd, ".claude", "settings.local.json") }
|
|
11105
11943
|
];
|
|
11106
11944
|
}
|
|
11107
11945
|
function installedEventsIn(parsed, exists) {
|
|
@@ -11131,23 +11969,23 @@ function runHookStatus(options = {}) {
|
|
|
11131
11969
|
const env = options.env ?? process.env;
|
|
11132
11970
|
const log = options.log ?? ((line) => console.log(line));
|
|
11133
11971
|
const cwd = options.cwd ?? process.cwd();
|
|
11134
|
-
const exists = options.exists ?? ((filePath) =>
|
|
11972
|
+
const exists = options.exists ?? ((filePath) => fs21.existsSync(filePath));
|
|
11135
11973
|
const candidates = options.settingsPath === void 0 ? settingsCandidates(env, cwd) : [{ label: "settings", file: options.settingsPath }];
|
|
11136
11974
|
log("");
|
|
11137
11975
|
log("flatbread hook");
|
|
11138
11976
|
log("");
|
|
11139
11977
|
let installed = 0;
|
|
11140
11978
|
for (const candidate of candidates) {
|
|
11141
|
-
const
|
|
11142
|
-
if (!
|
|
11979
|
+
const read3 = readSettingsFile(candidate.file);
|
|
11980
|
+
if (!read3.present) {
|
|
11143
11981
|
log(` ${candidate.label.padEnd(8)} ${candidate.file} (absent)`);
|
|
11144
11982
|
continue;
|
|
11145
11983
|
}
|
|
11146
|
-
if ("unreadable" in
|
|
11147
|
-
log(` ${candidate.label.padEnd(8)} ${candidate.file} NOT RETRIEVED (${
|
|
11984
|
+
if ("unreadable" in read3) {
|
|
11985
|
+
log(` ${candidate.label.padEnd(8)} ${candidate.file} NOT RETRIEVED (${read3.unreadable})`);
|
|
11148
11986
|
continue;
|
|
11149
11987
|
}
|
|
11150
|
-
const found = installedEventsIn(
|
|
11988
|
+
const found = installedEventsIn(read3.parsed, exists);
|
|
11151
11989
|
installed += found.events.length;
|
|
11152
11990
|
const summary = found.events.length === 0 ? "(no flatbread handler)" : found.events.join(", ");
|
|
11153
11991
|
log(` ${candidate.label.padEnd(8)} ${candidate.file} ${summary}`);
|
|
@@ -11171,15 +12009,15 @@ function runHookStatus(options = {}) {
|
|
|
11171
12009
|
}
|
|
11172
12010
|
}
|
|
11173
12011
|
log("");
|
|
11174
|
-
const
|
|
11175
|
-
if (!
|
|
11176
|
-
log(` state NOT RETRIEVED (${
|
|
12012
|
+
const read2 = readHookState(env);
|
|
12013
|
+
if (!read2.ok) {
|
|
12014
|
+
log(` state NOT RETRIEVED (${read2.reason}: ${read2.detail})`);
|
|
11177
12015
|
log(` ${hookStatePath(env)}`);
|
|
11178
12016
|
log("");
|
|
11179
12017
|
return 1;
|
|
11180
12018
|
}
|
|
11181
|
-
const sessions = Object.entries(
|
|
11182
|
-
log(` state ${hookStatePath(env)}${
|
|
12019
|
+
const sessions = Object.entries(read2.state.sessions);
|
|
12020
|
+
log(` state ${hookStatePath(env)}${read2.present ? "" : " (none yet)"}`);
|
|
11183
12021
|
log(` sessions ${sessions.length}`);
|
|
11184
12022
|
log(` offered ${sessions.filter(([, session]) => session.offeredAt !== void 0).length}`);
|
|
11185
12023
|
const recent = [...sessions].sort((a, b) => b[1].lastSeenAt > a[1].lastSeenAt ? 1 : -1).slice(0, 5);
|
|
@@ -11251,8 +12089,92 @@ function probeHandler(handler, run = spawnSync2) {
|
|
|
11251
12089
|
return { ok: true, detail: "ran, exited 0, printed nothing" };
|
|
11252
12090
|
}
|
|
11253
12091
|
|
|
12092
|
+
// src/setup.ts
|
|
12093
|
+
function runSetup(options = {}) {
|
|
12094
|
+
const env = options.env ?? process.env;
|
|
12095
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
12096
|
+
const execPath = options.execPath ?? process.execPath;
|
|
12097
|
+
const entryPath = options.entryPath ?? process.argv[1] ?? "";
|
|
12098
|
+
const dryRun = options.dryRun === true;
|
|
12099
|
+
const settings = options.settingsPath !== void 0 ? { settingsPath: options.settingsPath } : {};
|
|
12100
|
+
if (options.uninstall !== true && /[\\/]_npx[\\/]/.test(entryPath)) {
|
|
12101
|
+
log("");
|
|
12102
|
+
log(` refused: this flatbread runs from npx's cache (${entryPath}),`);
|
|
12103
|
+
log(" which npm may clear - and a hook that names a missing file fails silently. Install it, then run setup:");
|
|
12104
|
+
log(" npm install -g flatbread-cli");
|
|
12105
|
+
log(" flatbread setup");
|
|
12106
|
+
log("");
|
|
12107
|
+
return 1;
|
|
12108
|
+
}
|
|
12109
|
+
log("");
|
|
12110
|
+
log("Claude Code");
|
|
12111
|
+
const claude = options.uninstall === true ? runHookUninstall({ env, log, dryRun, ...settings }) : runHookInstall({
|
|
12112
|
+
env,
|
|
12113
|
+
log,
|
|
12114
|
+
execPath,
|
|
12115
|
+
entryPath,
|
|
12116
|
+
dryRun,
|
|
12117
|
+
capture: true,
|
|
12118
|
+
...options.noProbe === true ? { noProbe: true } : {},
|
|
12119
|
+
...options.spawn !== void 0 ? { spawn: options.spawn } : {},
|
|
12120
|
+
...settings
|
|
12121
|
+
});
|
|
12122
|
+
let mcp = 0;
|
|
12123
|
+
if (options.noMcp === true) {
|
|
12124
|
+
log(" --no-mcp: Claude Code's MCP servers were not touched");
|
|
12125
|
+
} else if (claude === 0 || options.uninstall === true) {
|
|
12126
|
+
mcp = registerClaudeMcp({
|
|
12127
|
+
file: options.claudeConfigPath ?? claudeUserConfigPath(env),
|
|
12128
|
+
execPath,
|
|
12129
|
+
entryPath,
|
|
12130
|
+
uninstall: options.uninstall === true,
|
|
12131
|
+
dryRun,
|
|
12132
|
+
log
|
|
12133
|
+
});
|
|
12134
|
+
} else {
|
|
12135
|
+
log(" mcp not registered, because the hook step above was refused");
|
|
12136
|
+
}
|
|
12137
|
+
log("");
|
|
12138
|
+
let git2 = 0;
|
|
12139
|
+
log("git");
|
|
12140
|
+
if (options.noGit === true) {
|
|
12141
|
+
log(" --no-git: the post-commit hook was not touched");
|
|
12142
|
+
} else {
|
|
12143
|
+
const where = hooksDirFor(options.repo ?? process.cwd(), options.git !== void 0 ? { git: options.git } : {});
|
|
12144
|
+
if (!where.ok) {
|
|
12145
|
+
log(` skipped ${where.detail}`);
|
|
12146
|
+
if (where.reason === "hooks-path" && options.uninstall !== true) {
|
|
12147
|
+
log(" add this line to that post-commit yourself to capture commits:");
|
|
12148
|
+
log(` ${captureLine(execPath, entryPath)}`);
|
|
12149
|
+
} else if (where.reason === "not-a-repo" && options.uninstall !== true) {
|
|
12150
|
+
log(" run setup inside a repository to bind its commits to your sessions");
|
|
12151
|
+
}
|
|
12152
|
+
if (where.reason === "unreadable") git2 = 1;
|
|
12153
|
+
} else {
|
|
12154
|
+
const outcome = options.uninstall === true ? uninstallGitHook(where.dir, { dryRun }) : installGitHook(where.dir, postCommitScript(execPath, entryPath), { dryRun });
|
|
12155
|
+
for (const line of outcome.lines) log(` ${line}`);
|
|
12156
|
+
if (where.machineHooksPath !== void 0 && options.uninstall !== true && outcome.code === 0) {
|
|
12157
|
+
log(` note git runs ${where.machineHooksPath.path} first (core.hooksPath, ${where.machineHooksPath.scope} config).`);
|
|
12158
|
+
log(" This hook fires only if that one runs the repository's own post-commit.");
|
|
12159
|
+
log(` After your next commit, ${bucketDir(env)}/capture.log says whether it did.`);
|
|
12160
|
+
}
|
|
12161
|
+
git2 = outcome.code;
|
|
12162
|
+
}
|
|
12163
|
+
}
|
|
12164
|
+
log("");
|
|
12165
|
+
if (options.uninstall === true) {
|
|
12166
|
+
log(` the bucket is left alone: ${bucketDir(env)}`);
|
|
12167
|
+
} else {
|
|
12168
|
+
log(` captures go to ${bucketDir(env)}: redacted, private, never uploaded`);
|
|
12169
|
+
log(" find them with: flatbread search <words>");
|
|
12170
|
+
log(" share one with: flatbread share --file <its session>, which opens the review");
|
|
12171
|
+
}
|
|
12172
|
+
log("");
|
|
12173
|
+
return claude !== 0 || mcp !== 0 || git2 !== 0 ? 1 : 0;
|
|
12174
|
+
}
|
|
12175
|
+
|
|
11254
12176
|
// src/login-server.ts
|
|
11255
|
-
import
|
|
12177
|
+
import crypto3 from "node:crypto";
|
|
11256
12178
|
import http2 from "node:http";
|
|
11257
12179
|
function callbackOrigin(port) {
|
|
11258
12180
|
return `http://127.0.0.1:${port}`;
|
|
@@ -11327,7 +12249,7 @@ function parseDelivery(raw, expectedState, nowSeconds) {
|
|
|
11327
12249
|
}
|
|
11328
12250
|
const given = Buffer.from(state, "utf8");
|
|
11329
12251
|
const wanted = Buffer.from(expectedState, "utf8");
|
|
11330
|
-
if (given.length !== wanted.length || !
|
|
12252
|
+
if (given.length !== wanted.length || !crypto3.timingSafeEqual(given, wanted)) {
|
|
11331
12253
|
return { ok: false, problem: "the callback state did not match" };
|
|
11332
12254
|
}
|
|
11333
12255
|
const sessionRaw = body["session"];
|
|
@@ -11375,7 +12297,7 @@ async function readBody2(req, limitBytes = 64 * 1024) {
|
|
|
11375
12297
|
async function startLoginServer(options = {}) {
|
|
11376
12298
|
const ports = options.ports ?? DEFAULT_LOGIN_PORTS;
|
|
11377
12299
|
const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
11378
|
-
const state = options.state ??
|
|
12300
|
+
const state = options.state ?? crypto3.randomBytes(32).toString("hex");
|
|
11379
12301
|
let settle;
|
|
11380
12302
|
const delivery = new Promise((resolve) => {
|
|
11381
12303
|
settle = resolve;
|
|
@@ -11461,26 +12383,35 @@ async function bindFirst(server, ports) {
|
|
|
11461
12383
|
}
|
|
11462
12384
|
|
|
11463
12385
|
// src/index.ts
|
|
11464
|
-
var PACKAGE_ROOT =
|
|
11465
|
-
var SOURCE_CHECKOUT =
|
|
11466
|
-
var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ?
|
|
11467
|
-
var OUTPUT_PATH =
|
|
11468
|
-
var SHARE_OUTPUT_PATH =
|
|
12386
|
+
var PACKAGE_ROOT = path21.resolve(path21.dirname(fileURLToPath2(import.meta.url)), "..");
|
|
12387
|
+
var SOURCE_CHECKOUT = fs22.existsSync(path21.join(PACKAGE_ROOT, "src", "index.ts"));
|
|
12388
|
+
var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ? path21.join(PACKAGE_ROOT, ".tmp") : path21.join(os8.homedir(), ".flatbread"));
|
|
12389
|
+
var OUTPUT_PATH = path21.join(TMP_DIR, "last-parse.json");
|
|
12390
|
+
var SHARE_OUTPUT_PATH = path21.join(TMP_DIR, "last-share.json");
|
|
11469
12391
|
function storeDir() {
|
|
11470
|
-
return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null :
|
|
12392
|
+
return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path21.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
|
|
11471
12393
|
}
|
|
11472
12394
|
var USAGE = `flatbread - registry of AI agent runs
|
|
11473
12395
|
|
|
11474
12396
|
Usage:
|
|
11475
|
-
flatbread parse --file <path> Parse a session log
|
|
12397
|
+
flatbread parse --file <path> Parse a session log and print its brief. Writes nothing
|
|
11476
12398
|
flatbread parse --latest Parse the newest session for a harness
|
|
11477
12399
|
flatbread share Review + sanitize the newest session
|
|
11478
12400
|
flatbread share --file <path> Review + sanitize a specific session
|
|
11479
12401
|
flatbread view Write the brief and capsule, print the viewer URL
|
|
11480
|
-
flatbread pull <id|url> Print a run's Context Capsule
|
|
12402
|
+
flatbread pull <id|url> Print a run's Context Capsule: a run this machine shared or
|
|
12403
|
+
captured is read here, anything else from the site
|
|
11481
12404
|
flatbread pull <id|url> --out <file> Write it to a file instead
|
|
11482
12405
|
flatbread mcp MCP server on stdio: flatbread_search_traces and
|
|
11483
12406
|
flatbread_get_capsule, for your coding agent
|
|
12407
|
+
flatbread search <words> Find runs: this machine's shared and captured ones, then
|
|
12408
|
+
the site (the same answer the MCP server gives)
|
|
12409
|
+
flatbread setup Auto-capture: the Claude Code hook, plus a chained git
|
|
12410
|
+
post-commit in this repository, and the MCP server for
|
|
12411
|
+
every Claude Code project. Captures stay local
|
|
12412
|
+
flatbread setup --uninstall Remove exactly what setup added
|
|
12413
|
+
flatbread capture --file <path> Capture one session into the local bucket (setup's hooks
|
|
12414
|
+
run this; redacted, private, never uploaded)
|
|
11484
12415
|
flatbread adapters List harness adapters and their confidence
|
|
11485
12416
|
flatbread login Sign in through the website, the way npm does
|
|
11486
12417
|
flatbread login --email <addr> Sign in by email instead (see the note below)
|
|
@@ -11496,15 +12427,41 @@ Usage:
|
|
|
11496
12427
|
Options for parse:
|
|
11497
12428
|
--harness <id> Skip detection and name the harness.
|
|
11498
12429
|
${HARNESS_IDS.join(", ")}
|
|
12430
|
+
--save Also write the raw (unredacted) trace where view and the
|
|
12431
|
+
local viewer read it
|
|
12432
|
+
--out <file> Write the raw trace to <file> instead
|
|
11499
12433
|
|
|
11500
12434
|
Options for share:
|
|
11501
12435
|
--harness <id> Skip detection and name the harness
|
|
11502
12436
|
--publish Upload the run to your account (needs flatbread login)
|
|
11503
12437
|
--pr <url> Link the pull request this run belongs to (https)
|
|
12438
|
+
--title <text> The run's title (1-${LIMITS.title} characters). The review page
|
|
12439
|
+
opens with it and can still change it
|
|
12440
|
+
--commit [sha] Bind the run to a commit (bare --commit = HEAD) and record
|
|
12441
|
+
whether the working tree was clean against it
|
|
11504
12442
|
--no-review Redact every match, skip the browser (non-interactive)
|
|
11505
12443
|
--no-open Start the review server but do not launch a browser
|
|
11506
12444
|
-h, --help Show this message
|
|
11507
12445
|
|
|
12446
|
+
Options for search:
|
|
12447
|
+
--harness <id> --category <kind> Only runs from that harness, or of that kind of job
|
|
12448
|
+
--recovered Only runs where the agent recovered from a failure
|
|
12449
|
+
--local This machine only; the site is not asked
|
|
12450
|
+
|
|
12451
|
+
Options for setup:
|
|
12452
|
+
--dry-run Say what would be written, write nothing
|
|
12453
|
+
--uninstall Remove the handlers, the MCP server and the post-commit;
|
|
12454
|
+
restore a post-commit hook setup had chained
|
|
12455
|
+
--no-git Leave the repository's hooks alone
|
|
12456
|
+
--no-mcp Leave Claude Code's MCP servers alone
|
|
12457
|
+
--settings <path> --no-probe As for hook install
|
|
12458
|
+
|
|
12459
|
+
Options for capture:
|
|
12460
|
+
--commit [sha] Bind the capture to a commit (bare = HEAD)
|
|
12461
|
+
--repo <dir> Without --file: the newest session of that repository,
|
|
12462
|
+
if it moved in the last ${Math.round(COMMIT_SESSION_WINDOW_MS / 6e4)} minutes
|
|
12463
|
+
--quiet Log to the bucket's capture.log and always exit 0 (hooks)
|
|
12464
|
+
|
|
11508
12465
|
Options for hook install and hook uninstall:
|
|
11509
12466
|
--settings <path> Settings file to edit. Default: the user settings file
|
|
11510
12467
|
(~/.claude/settings.json, or CLAUDE_CONFIG_DIR)
|
|
@@ -11519,15 +12476,19 @@ Options for adapters:
|
|
|
11519
12476
|
Notes:
|
|
11520
12477
|
parse and share record the git branch, HEAD commit and uncommitted file names when the
|
|
11521
12478
|
session ran inside a git repository. Nothing else from git is read.
|
|
11522
|
-
|
|
11523
|
-
Share drafts are written to ${
|
|
12479
|
+
parse --save writes ${path21.relative(process.cwd(), OUTPUT_PATH) || OUTPUT_PATH}
|
|
12480
|
+
Share drafts are written to ${path21.relative(process.cwd(), SHARE_OUTPUT_PATH) || SHARE_OUTPUT_PATH}
|
|
12481
|
+
(the latest only; every shared run is also kept under its ID, for search and pull)
|
|
11524
12482
|
Every parsed run defaults to visibility "private".
|
|
11525
12483
|
Nothing is uploaded unless you pass --publish, or set FLATBREAD_API_URL and
|
|
11526
12484
|
FLATBREAD_API_TOKEN yourself. pull and mcp only read: public runs, your own when you
|
|
11527
12485
|
are signed in, and a private run through its share link. The review UI listens on
|
|
11528
|
-
127.0.0.1 only
|
|
11529
|
-
|
|
12486
|
+
127.0.0.1 only, on 4321-4325 or, when all five are busy, a port the OS picks.
|
|
12487
|
+
setup registers the MCP server in ~/.claude.json, for every project. By hand instead:
|
|
12488
|
+
claude mcp add --scope user flatbread -- flatbread mcp
|
|
11530
12489
|
The viewer is a separate dev server: npm run dev -w @flatbread/web
|
|
12490
|
+
Captured runs live in ~/.flatbread/bucket (FLATBREAD_HOOK_DISABLED=1 stops every capture).
|
|
12491
|
+
A capture is not a share: to publish one, run share --file on its session.
|
|
11531
12492
|
The hook offers ONE LINE at the end of a turn and never uploads anything itself. Turn it
|
|
11532
12493
|
off with FLATBREAD_HOOK_DISABLED=1, or change when it speaks with
|
|
11533
12494
|
FLATBREAD_HOOK_OFFER_AFTER=<turns>. It records what it has seen in ~/.flatbread.
|
|
@@ -11548,6 +12509,15 @@ function parseArgv(argv) {
|
|
|
11548
12509
|
const arg = argv[i];
|
|
11549
12510
|
if (arg === "--latest") {
|
|
11550
12511
|
args.latest = true;
|
|
12512
|
+
} else if (arg === "--save") {
|
|
12513
|
+
args.out = OUTPUT_PATH;
|
|
12514
|
+
} else if (arg === "--out") {
|
|
12515
|
+
const value = argv[i + 1];
|
|
12516
|
+
if (value === void 0 || value.startsWith("--")) throw new Error("--out needs a file path");
|
|
12517
|
+
args.out = path21.resolve(value);
|
|
12518
|
+
i += 1;
|
|
12519
|
+
} else if (arg !== void 0 && arg.startsWith("--out=")) {
|
|
12520
|
+
args.out = path21.resolve(arg.slice("--out=".length));
|
|
11551
12521
|
} else if (arg === "--harness") {
|
|
11552
12522
|
const value = argv[i + 1];
|
|
11553
12523
|
if (value === void 0 || value.startsWith("--")) {
|
|
@@ -11643,7 +12613,9 @@ function report(result, outputPath) {
|
|
|
11643
12613
|
lines2.push("");
|
|
11644
12614
|
lines2.push(...briefLines(trace));
|
|
11645
12615
|
lines2.push("");
|
|
11646
|
-
lines2.push(
|
|
12616
|
+
lines2.push(
|
|
12617
|
+
outputPath === void 0 ? ` wrote nothing. --save writes ${OUTPUT_PATH} for the viewer` : ` wrote ${outputPath}`
|
|
12618
|
+
);
|
|
11647
12619
|
lines2.push("");
|
|
11648
12620
|
console.log(lines2.join("\n"));
|
|
11649
12621
|
}
|
|
@@ -11666,12 +12638,12 @@ async function runParse(args) {
|
|
|
11666
12638
|
target = found[0];
|
|
11667
12639
|
console.log(`latest ${harness} session: ${target}`);
|
|
11668
12640
|
} else if (args.file) {
|
|
11669
|
-
target =
|
|
11670
|
-
if (!
|
|
12641
|
+
target = path21.resolve(args.file);
|
|
12642
|
+
if (!fs22.existsSync(target)) {
|
|
11671
12643
|
console.error(`file not found: ${target}`);
|
|
11672
12644
|
return 1;
|
|
11673
12645
|
}
|
|
11674
|
-
if (!
|
|
12646
|
+
if (!fs22.statSync(target).isFile()) {
|
|
11675
12647
|
console.error(`not a file: ${target}`);
|
|
11676
12648
|
return 1;
|
|
11677
12649
|
}
|
|
@@ -11701,9 +12673,11 @@ async function runParse(args) {
|
|
|
11701
12673
|
}
|
|
11702
12674
|
const trace = withLineage(result.trace, readGitLineage(result.trace.source.cwd));
|
|
11703
12675
|
FlatbreadTraceSchema.parse(trace);
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
12676
|
+
if (args.out !== void 0) {
|
|
12677
|
+
fs22.mkdirSync(path21.dirname(args.out), { recursive: true });
|
|
12678
|
+
fs22.writeFileSync(args.out, JSON.stringify(trace, null, 2) + "\n", "utf8");
|
|
12679
|
+
}
|
|
12680
|
+
report({ ...result, trace }, args.out);
|
|
11707
12681
|
return 0;
|
|
11708
12682
|
}
|
|
11709
12683
|
function parseAdaptersArgv(argv) {
|
|
@@ -11765,8 +12739,8 @@ async function runAdapters(args) {
|
|
|
11765
12739
|
}
|
|
11766
12740
|
let input = null;
|
|
11767
12741
|
if (args.file !== void 0) {
|
|
11768
|
-
const resolved =
|
|
11769
|
-
if (!
|
|
12742
|
+
const resolved = path21.resolve(args.file);
|
|
12743
|
+
if (!fs22.existsSync(resolved)) {
|
|
11770
12744
|
console.error(`file not found: ${resolved}`);
|
|
11771
12745
|
return 1;
|
|
11772
12746
|
}
|
|
@@ -11780,6 +12754,16 @@ async function runAdapters(args) {
|
|
|
11780
12754
|
console.log("");
|
|
11781
12755
|
return 0;
|
|
11782
12756
|
}
|
|
12757
|
+
var COMMIT_REV = /^[A-Za-z0-9][A-Za-z0-9._/~^@{}-]{0,199}$/;
|
|
12758
|
+
function checkCommitRev(value) {
|
|
12759
|
+
if (!COMMIT_REV.test(value)) throw new Error(`--commit needs a commit SHA or ref, e.g. --commit HEAD (got "${value}")`);
|
|
12760
|
+
return value;
|
|
12761
|
+
}
|
|
12762
|
+
function checkTitle(value) {
|
|
12763
|
+
const title = normalizeTitle(value);
|
|
12764
|
+
if (title === null) throw new Error(`--title must be 1-${LIMITS.title} characters`);
|
|
12765
|
+
return title;
|
|
12766
|
+
}
|
|
11783
12767
|
function parseShareArgv(argv) {
|
|
11784
12768
|
const args = { noReview: false, open: true, publish: false };
|
|
11785
12769
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -11807,6 +12791,23 @@ function parseShareArgv(argv) {
|
|
|
11807
12791
|
i += 1;
|
|
11808
12792
|
} else if (arg !== void 0 && arg.startsWith("--pr=")) {
|
|
11809
12793
|
args.prUrl = checkPrUrl(arg.slice("--pr=".length));
|
|
12794
|
+
} else if (arg === "--title") {
|
|
12795
|
+
const value = argv[i + 1];
|
|
12796
|
+
if (value === void 0 || value.startsWith("--")) throw new Error("--title needs the run's title, in quotes");
|
|
12797
|
+
args.title = checkTitle(value);
|
|
12798
|
+
i += 1;
|
|
12799
|
+
} else if (arg !== void 0 && arg.startsWith("--title=")) {
|
|
12800
|
+
args.title = checkTitle(arg.slice("--title=".length));
|
|
12801
|
+
} else if (arg === "--commit") {
|
|
12802
|
+
const value = argv[i + 1];
|
|
12803
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
12804
|
+
args.commit = "HEAD";
|
|
12805
|
+
} else {
|
|
12806
|
+
args.commit = checkCommitRev(value);
|
|
12807
|
+
i += 1;
|
|
12808
|
+
}
|
|
12809
|
+
} else if (arg !== void 0 && arg.startsWith("--commit=")) {
|
|
12810
|
+
args.commit = checkCommitRev(arg.slice("--commit=".length));
|
|
11810
12811
|
} else if (arg === "--file") {
|
|
11811
12812
|
const value = argv[i + 1];
|
|
11812
12813
|
if (value === void 0 || value.startsWith("--")) {
|
|
@@ -11960,7 +12961,7 @@ function tryOpenBrowser(url) {
|
|
|
11960
12961
|
const windows = process.platform === "win32";
|
|
11961
12962
|
const command = windows ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
11962
12963
|
const args = windows ? ["/c", "start", "", url] : [url];
|
|
11963
|
-
const child =
|
|
12964
|
+
const child = spawn3(command, args, { stdio: "ignore", detached: true, windowsHide: true });
|
|
11964
12965
|
child.on("error", () => {
|
|
11965
12966
|
});
|
|
11966
12967
|
child.unref();
|
|
@@ -12065,7 +13066,7 @@ async function prompt(label) {
|
|
|
12065
13066
|
});
|
|
12066
13067
|
}
|
|
12067
13068
|
var HOOK_SUBCOMMANDS = ["install", "status", "uninstall", "notify"];
|
|
12068
|
-
var CLI_ENTRY =
|
|
13069
|
+
var CLI_ENTRY = fileURLToPath2(import.meta.url);
|
|
12069
13070
|
function parseHookArgv(argv) {
|
|
12070
13071
|
const [sub, ...rest] = argv;
|
|
12071
13072
|
if (sub === void 0) {
|
|
@@ -12075,7 +13076,7 @@ function parseHookArgv(argv) {
|
|
|
12075
13076
|
if (known === void 0) {
|
|
12076
13077
|
throw new Error(`unknown hook subcommand "${sub}". Known: ${HOOK_SUBCOMMANDS.join(", ")}`);
|
|
12077
13078
|
}
|
|
12078
|
-
const args = { sub: known, dryRun: false, noProbe: false };
|
|
13079
|
+
const args = { sub: known, dryRun: false, noProbe: false, capture: false };
|
|
12079
13080
|
for (let i = 0; i < rest.length; i += 1) {
|
|
12080
13081
|
const flag = rest[i];
|
|
12081
13082
|
if (flag === "--settings") {
|
|
@@ -12093,13 +13094,17 @@ function parseHookArgv(argv) {
|
|
|
12093
13094
|
args.noProbe = true;
|
|
12094
13095
|
continue;
|
|
12095
13096
|
}
|
|
13097
|
+
if (flag === "--capture" && known === "notify") {
|
|
13098
|
+
args.capture = true;
|
|
13099
|
+
continue;
|
|
13100
|
+
}
|
|
12096
13101
|
throw new Error(`unknown option for hook ${known}: ${flag}`);
|
|
12097
13102
|
}
|
|
12098
13103
|
if (known !== "install" && args.noProbe) {
|
|
12099
13104
|
throw new Error("--no-probe only means anything to hook install");
|
|
12100
13105
|
}
|
|
12101
13106
|
if (known === "notify" && (args.settings !== void 0 || args.dryRun)) {
|
|
12102
|
-
throw new Error("hook notify takes
|
|
13107
|
+
throw new Error("hook notify takes only --capture: Claude Code runs it with the event on stdin");
|
|
12103
13108
|
}
|
|
12104
13109
|
if (known === "status" && args.dryRun) {
|
|
12105
13110
|
throw new Error("hook status writes nothing, so --dry-run means nothing here");
|
|
@@ -12111,7 +13116,7 @@ function parseHookArgv(argv) {
|
|
|
12111
13116
|
}
|
|
12112
13117
|
async function runHookCommand(args) {
|
|
12113
13118
|
if (args.sub === "notify") {
|
|
12114
|
-
return runHookNotify({ readStdin: () => readAllStdin(), entryPath: CLI_ENTRY });
|
|
13119
|
+
return runHookNotify({ readStdin: () => readAllStdin(), entryPath: CLI_ENTRY, capture: args.capture });
|
|
12115
13120
|
}
|
|
12116
13121
|
if (args.sub === "install") {
|
|
12117
13122
|
return runHookInstall({
|
|
@@ -12154,13 +13159,13 @@ function parsePullArgv(argv) {
|
|
|
12154
13159
|
if (ref === void 0) throw new Error("pull needs a run ID or a /r/<id> link, e.g. flatbread pull quiet-otter-4821");
|
|
12155
13160
|
return { ref, ...out !== void 0 ? { out } : {} };
|
|
12156
13161
|
}
|
|
12157
|
-
async function runPull(args) {
|
|
13162
|
+
async function runPull(args, deps = { ...defaultMcpDeps(storeDir(), bucketDir()), capsule: (ref) => fetchCapsule(ref) }) {
|
|
12158
13163
|
const ref = parseRunRef(args.ref);
|
|
12159
13164
|
if (ref === null) {
|
|
12160
13165
|
console.error(`"${args.ref}" is not a run ID or a /r/<id> link`);
|
|
12161
13166
|
return 1;
|
|
12162
13167
|
}
|
|
12163
|
-
const result = await
|
|
13168
|
+
const result = await readCapsule(ref, deps);
|
|
12164
13169
|
if (!result.ok) {
|
|
12165
13170
|
console.error(describeReadFailure(result, `could not pull ${ref.slug}`));
|
|
12166
13171
|
return 1;
|
|
@@ -12169,19 +13174,148 @@ async function runPull(args) {
|
|
|
12169
13174
|
process.stdout.write(result.capsule);
|
|
12170
13175
|
return 0;
|
|
12171
13176
|
}
|
|
12172
|
-
const target =
|
|
12173
|
-
|
|
12174
|
-
|
|
12175
|
-
console.error(`wrote ${target}`);
|
|
13177
|
+
const target = path21.resolve(args.out);
|
|
13178
|
+
fs22.mkdirSync(path21.dirname(target), { recursive: true });
|
|
13179
|
+
fs22.writeFileSync(target, result.capsule, "utf8");
|
|
13180
|
+
console.error(`wrote ${target}${result.from === "site" ? "" : ` (from this machine's ${result.from === "store" ? "shared runs" : "captured runs"})`}`);
|
|
12176
13181
|
return 0;
|
|
12177
13182
|
}
|
|
13183
|
+
function parseSearchArgv(argv) {
|
|
13184
|
+
const query = {};
|
|
13185
|
+
const words = [];
|
|
13186
|
+
let local = false;
|
|
13187
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
13188
|
+
const arg = argv[i];
|
|
13189
|
+
const valueOf = (flag) => {
|
|
13190
|
+
if (arg.startsWith(`${flag}=`)) return arg.slice(flag.length + 1);
|
|
13191
|
+
const value = argv[i + 1];
|
|
13192
|
+
if (value === void 0 || value.startsWith("--")) throw new Error(`${flag} needs a value`);
|
|
13193
|
+
i += 1;
|
|
13194
|
+
return value;
|
|
13195
|
+
};
|
|
13196
|
+
if (arg === "--harness" || arg.startsWith("--harness=")) {
|
|
13197
|
+
query.harness = toHarnessId(valueOf("--harness"));
|
|
13198
|
+
} else if (arg === "--category" || arg.startsWith("--category=")) {
|
|
13199
|
+
const value = valueOf("--category");
|
|
13200
|
+
if (!TASK_CATEGORIES.includes(value)) throw new Error(`unknown --category "${value}". Known: ${TASK_CATEGORIES.join(", ")}`);
|
|
13201
|
+
query.category = value;
|
|
13202
|
+
} else if (arg === "--recovered") {
|
|
13203
|
+
query.onlyWithRecoveries = true;
|
|
13204
|
+
} else if (arg === "--local") {
|
|
13205
|
+
local = true;
|
|
13206
|
+
} else if (arg.startsWith("--")) {
|
|
13207
|
+
throw new Error(`Unknown option for search: ${arg}`);
|
|
13208
|
+
} else {
|
|
13209
|
+
words.push(arg);
|
|
13210
|
+
}
|
|
13211
|
+
}
|
|
13212
|
+
if (words.length > 0) query.query = words.join(" ");
|
|
13213
|
+
return { query, local };
|
|
13214
|
+
}
|
|
13215
|
+
async function runSearch(argv, deps = defaultMcpDeps(storeDir(), bucketDir()), print = console.log) {
|
|
13216
|
+
const { query, local } = parseSearchArgv(argv);
|
|
13217
|
+
const { lines: lines2, siteSearched } = await searchEverywhere(query, deps, { remote: !local });
|
|
13218
|
+
for (const line of lines2) print(line);
|
|
13219
|
+
return local || siteSearched ? 0 : 1;
|
|
13220
|
+
}
|
|
13221
|
+
function parseCaptureArgv(argv) {
|
|
13222
|
+
const args = { quiet: false };
|
|
13223
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
13224
|
+
const arg = argv[i];
|
|
13225
|
+
const next = argv[i + 1];
|
|
13226
|
+
if (arg === "--file" || arg === "--repo" || arg === "--harness") {
|
|
13227
|
+
if (next === void 0 || next.startsWith("--")) throw new Error(`${arg} needs a value`);
|
|
13228
|
+
if (arg === "--file") args.file = next;
|
|
13229
|
+
else if (arg === "--repo") args.repo = next;
|
|
13230
|
+
else args.harness = toHarnessId(next);
|
|
13231
|
+
i += 1;
|
|
13232
|
+
} else if (arg === "--commit") {
|
|
13233
|
+
if (next === void 0 || next.startsWith("--")) {
|
|
13234
|
+
args.commit = "HEAD";
|
|
13235
|
+
} else {
|
|
13236
|
+
args.commit = checkCommitRev(next);
|
|
13237
|
+
i += 1;
|
|
13238
|
+
}
|
|
13239
|
+
} else if (arg.startsWith("--commit=")) {
|
|
13240
|
+
args.commit = checkCommitRev(arg.slice("--commit=".length));
|
|
13241
|
+
} else if (arg === "--quiet") {
|
|
13242
|
+
args.quiet = true;
|
|
13243
|
+
} else {
|
|
13244
|
+
throw new Error(`Unknown option for capture: ${arg}`);
|
|
13245
|
+
}
|
|
13246
|
+
}
|
|
13247
|
+
if (args.file !== void 0 && args.repo !== void 0) throw new Error("capture takes --file or --repo, not both");
|
|
13248
|
+
return args;
|
|
13249
|
+
}
|
|
13250
|
+
async function runCapture(args, env = process.env) {
|
|
13251
|
+
const dir = bucketDir(env);
|
|
13252
|
+
const say = (line) => args.quiet ? logCapture(dir, line) : console.log(line);
|
|
13253
|
+
try {
|
|
13254
|
+
if (args.quiet && (env["FLATBREAD_HOOK_DISABLED"] ?? "").trim() !== "") {
|
|
13255
|
+
say("skipped FLATBREAD_HOOK_DISABLED is set");
|
|
13256
|
+
return 0;
|
|
13257
|
+
}
|
|
13258
|
+
let file = args.file;
|
|
13259
|
+
if (file === void 0) {
|
|
13260
|
+
const found = sessionForRepo(args.repo ?? process.cwd(), { env });
|
|
13261
|
+
if ("problem" in found) {
|
|
13262
|
+
say(`skipped ${found.problem}`);
|
|
13263
|
+
return 0;
|
|
13264
|
+
}
|
|
13265
|
+
file = found.file;
|
|
13266
|
+
}
|
|
13267
|
+
const result = await captureToBucket({
|
|
13268
|
+
file,
|
|
13269
|
+
env,
|
|
13270
|
+
dir,
|
|
13271
|
+
...args.harness !== void 0 ? { harness: args.harness } : {},
|
|
13272
|
+
...args.commit !== void 0 ? { commit: args.commit } : {}
|
|
13273
|
+
});
|
|
13274
|
+
if (result.status === "refused") {
|
|
13275
|
+
say(`refused ${result.reason}`);
|
|
13276
|
+
return args.quiet ? 0 : 1;
|
|
13277
|
+
}
|
|
13278
|
+
say(
|
|
13279
|
+
`${result.status} ${result.slug} ${result.steps} steps, ${result.redacted} redacted` + (result.commit !== void 0 ? `, at ${result.commit.slice(0, 12)}` : "") + ` -> ${result.path}`
|
|
13280
|
+
);
|
|
13281
|
+
return 0;
|
|
13282
|
+
} catch (error) {
|
|
13283
|
+
say(`failed ${error instanceof Error ? error.message : String(error)}`);
|
|
13284
|
+
return args.quiet ? 0 : 1;
|
|
13285
|
+
}
|
|
13286
|
+
}
|
|
13287
|
+
function parseSetupArgv(argv) {
|
|
13288
|
+
const args = {
|
|
13289
|
+
dryRun: false,
|
|
13290
|
+
uninstall: false,
|
|
13291
|
+
noGit: false,
|
|
13292
|
+
noMcp: false,
|
|
13293
|
+
noProbe: false
|
|
13294
|
+
};
|
|
13295
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
13296
|
+
const arg = argv[i];
|
|
13297
|
+
if (arg === "--dry-run") args.dryRun = true;
|
|
13298
|
+
else if (arg === "--uninstall") args.uninstall = true;
|
|
13299
|
+
else if (arg === "--no-git") args.noGit = true;
|
|
13300
|
+
else if (arg === "--no-mcp") args.noMcp = true;
|
|
13301
|
+
else if (arg === "--no-probe") args.noProbe = true;
|
|
13302
|
+
else if (arg === "--settings") {
|
|
13303
|
+
const value = argv[i + 1];
|
|
13304
|
+
if (value === void 0 || value.startsWith("--")) throw new Error("--settings needs a path");
|
|
13305
|
+
args.settings = value;
|
|
13306
|
+
i += 1;
|
|
13307
|
+
} else throw new Error(`Unknown option for setup: ${arg}`);
|
|
13308
|
+
}
|
|
13309
|
+
if (args.uninstall && args.noProbe) throw new Error("--no-probe only means anything when installing");
|
|
13310
|
+
return args;
|
|
13311
|
+
}
|
|
12178
13312
|
async function main(argv) {
|
|
12179
13313
|
const [command, ...rest] = argv;
|
|
12180
13314
|
if (command === void 0 || command === "--help" || command === "-h" || command === "help") {
|
|
12181
13315
|
console.log(USAGE);
|
|
12182
13316
|
return command === void 0 ? 1 : 0;
|
|
12183
13317
|
}
|
|
12184
|
-
if (command !== "parse" && command !== "share" && command !== "view" && command !== "adapters" && command !== "login" && command !== "logout" && command !== "whoami" && command !== "hook" && command !== "pull" && command !== "mcp") {
|
|
13318
|
+
if (command !== "parse" && command !== "share" && command !== "view" && command !== "adapters" && command !== "login" && command !== "logout" && command !== "whoami" && command !== "hook" && command !== "pull" && command !== "mcp" && command !== "search" && command !== "setup" && command !== "capture") {
|
|
12185
13319
|
console.error(`Unknown command: ${command}`);
|
|
12186
13320
|
console.error(USAGE);
|
|
12187
13321
|
return 1;
|
|
@@ -12200,6 +13334,25 @@ async function main(argv) {
|
|
|
12200
13334
|
}
|
|
12201
13335
|
return runHookCommand(hookArgs);
|
|
12202
13336
|
}
|
|
13337
|
+
if (command === "search" || command === "setup" || command === "capture") {
|
|
13338
|
+
try {
|
|
13339
|
+
if (command === "search") return await runSearch(rest);
|
|
13340
|
+
if (command === "capture") return await runCapture(parseCaptureArgv(rest));
|
|
13341
|
+
const setup = parseSetupArgv(rest);
|
|
13342
|
+
return runSetup({
|
|
13343
|
+
entryPath: CLI_ENTRY,
|
|
13344
|
+
dryRun: setup.dryRun,
|
|
13345
|
+
uninstall: setup.uninstall,
|
|
13346
|
+
noGit: setup.noGit,
|
|
13347
|
+
noMcp: setup.noMcp,
|
|
13348
|
+
noProbe: setup.noProbe,
|
|
13349
|
+
...setup.settings !== void 0 ? { settingsPath: setup.settings } : {}
|
|
13350
|
+
});
|
|
13351
|
+
} catch (error) {
|
|
13352
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
13353
|
+
return 1;
|
|
13354
|
+
}
|
|
13355
|
+
}
|
|
12203
13356
|
if (command === "login") return runLogin(rest);
|
|
12204
13357
|
if (command === "logout") return runLogout();
|
|
12205
13358
|
if (command === "whoami") return runWhoami();
|
|
@@ -12218,7 +13371,7 @@ async function main(argv) {
|
|
|
12218
13371
|
console.error(`mcp takes no options, got: ${rest.join(" ")}`);
|
|
12219
13372
|
return 1;
|
|
12220
13373
|
}
|
|
12221
|
-
return runMcpServer(defaultMcpDeps(storeDir()));
|
|
13374
|
+
return runMcpServer(defaultMcpDeps(storeDir(), bucketDir()));
|
|
12222
13375
|
}
|
|
12223
13376
|
if (command === "pull") {
|
|
12224
13377
|
let pullArgs;
|
|
@@ -12251,10 +13404,11 @@ async function main(argv) {
|
|
|
12251
13404
|
noReview: shareArgs.noReview,
|
|
12252
13405
|
publish: shareArgs.publish,
|
|
12253
13406
|
...shareArgs.prUrl !== void 0 ? { prUrl: shareArgs.prUrl } : {},
|
|
13407
|
+
...shareArgs.title !== void 0 ? { title: shareArgs.title } : {},
|
|
13408
|
+
...shareArgs.commit !== void 0 ? { commit: shareArgs.commit } : {},
|
|
12254
13409
|
...storeDir() !== null ? { storeDir: storeDir() } : {},
|
|
12255
13410
|
openBrowser: shareArgs.open && !shareArgs.noReview,
|
|
12256
|
-
outputPath: SHARE_OUTPUT_PATH
|
|
12257
|
-
parseOutputPath: OUTPUT_PATH
|
|
13411
|
+
outputPath: SHARE_OUTPUT_PATH
|
|
12258
13412
|
});
|
|
12259
13413
|
return result.exitCode;
|
|
12260
13414
|
}
|
|
@@ -12272,12 +13426,12 @@ var invokedDirectly = (() => {
|
|
|
12272
13426
|
if (entry === void 0) return false;
|
|
12273
13427
|
const real = (file) => {
|
|
12274
13428
|
try {
|
|
12275
|
-
return
|
|
13429
|
+
return fs22.realpathSync(file);
|
|
12276
13430
|
} catch {
|
|
12277
|
-
return
|
|
13431
|
+
return path21.resolve(file);
|
|
12278
13432
|
}
|
|
12279
13433
|
};
|
|
12280
|
-
return real(entry) === real(
|
|
13434
|
+
return real(entry) === real(fileURLToPath2(import.meta.url));
|
|
12281
13435
|
})();
|
|
12282
13436
|
if (invokedDirectly) {
|
|
12283
13437
|
main(process.argv.slice(2)).then((code) => {
|
|
@@ -12293,8 +13447,14 @@ export {
|
|
|
12293
13447
|
TMP_DIR,
|
|
12294
13448
|
main,
|
|
12295
13449
|
openableUrl,
|
|
13450
|
+
parseCaptureArgv,
|
|
12296
13451
|
parsePullArgv,
|
|
13452
|
+
parseSearchArgv,
|
|
13453
|
+
parseSetupArgv,
|
|
13454
|
+
runCapture,
|
|
12297
13455
|
runLogin,
|
|
12298
13456
|
runLogout,
|
|
13457
|
+
runPull,
|
|
13458
|
+
runSearch,
|
|
12299
13459
|
runWhoami
|
|
12300
13460
|
};
|