flatbread-cli 0.1.0 → 0.2.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 +34 -4
- package/dist/flatbread.mjs +884 -157
- package/package.json +1 -1
package/dist/flatbread.mjs
CHANGED
|
@@ -6,10 +6,10 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
|
-
import { spawn as
|
|
10
|
-
import
|
|
9
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
10
|
+
import fs21 from "node:fs";
|
|
11
11
|
import os7 from "node:os";
|
|
12
|
-
import
|
|
12
|
+
import path20 from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
|
|
15
15
|
// ../../node_modules/zod/v3/external.js
|
|
@@ -490,8 +490,8 @@ function getErrorMap() {
|
|
|
490
490
|
|
|
491
491
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
492
492
|
var makeIssue = (params) => {
|
|
493
|
-
const { data, path:
|
|
494
|
-
const fullPath = [...
|
|
493
|
+
const { data, path: path21, errorMaps, issueData } = params;
|
|
494
|
+
const fullPath = [...path21, ...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, path21, key) {
|
|
611
611
|
this._cachedPath = [];
|
|
612
612
|
this.parent = parent;
|
|
613
613
|
this.data = value;
|
|
614
|
-
this._path =
|
|
614
|
+
this._path = path21;
|
|
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 path21 = firstString(tool.input, FILE_PATH_KEYS);
|
|
4388
|
+
if (path21 !== void 0) return `${tool.name} ${repoRelative(path21, 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 [path21, 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: path21, 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 path21 = repoRelative(firstString(step.tool.input, FILE_PATH_KEYS), cwd);
|
|
4717
|
+
const file = files.get(path21) ?? { path: path21 };
|
|
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: path21, 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(path21, 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: {
|
|
@@ -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((path21) => matchesAny(path21, 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((path21) => matchesAny(path21, 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((path21) => matchesAny(path21, 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] ?? "";
|
|
@@ -7347,8 +7403,8 @@ function isInsideDir(child, parent) {
|
|
|
7347
7403
|
}
|
|
7348
7404
|
function targetIndicatesDb(target, env) {
|
|
7349
7405
|
if (isDbTarget(target)) return true;
|
|
7350
|
-
const
|
|
7351
|
-
if (/^ses_[0-9a-zA-Z]+$/.test(
|
|
7406
|
+
const stem2 = path8.basename(target, ".json");
|
|
7407
|
+
if (/^ses_[0-9a-zA-Z]+$/.test(stem2)) return true;
|
|
7352
7408
|
const resolved = path8.resolve(target);
|
|
7353
7409
|
for (const dir of [dataDir(env), storageDir(env)]) {
|
|
7354
7410
|
if (resolved === path8.resolve(dir) || isInsideDir(resolved, dir)) return true;
|
|
@@ -7394,8 +7450,8 @@ async function resolveSource(target, env = process.env) {
|
|
|
7394
7450
|
const wantsDb = isDbTarget(target) || dbExists(data) && targetIndicatesDb(target, env);
|
|
7395
7451
|
if (wantsDb) {
|
|
7396
7452
|
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(
|
|
7453
|
+
const stem2 = path8.basename(target, ".json");
|
|
7454
|
+
const sessionId = explicit.sessionId ?? (/^ses_[0-9a-zA-Z]+$/.test(stem2) ? stem2 : void 0);
|
|
7399
7455
|
const result = await readDbSession(path8.dirname(explicit.dbPath), sessionId);
|
|
7400
7456
|
if (!("unavailable" in result)) {
|
|
7401
7457
|
if (result.messages.length === 0) {
|
|
@@ -8459,8 +8515,8 @@ function safeSlug(slug) {
|
|
|
8459
8515
|
if (slug.includes("..") || slug.length > 72) return null;
|
|
8460
8516
|
return slug;
|
|
8461
8517
|
}
|
|
8462
|
-
function isRecordStem(
|
|
8463
|
-
return safeSlug(
|
|
8518
|
+
function isRecordStem(stem2) {
|
|
8519
|
+
return safeSlug(stem2) !== null && SlugSchema.safeParse(stem2).success;
|
|
8464
8520
|
}
|
|
8465
8521
|
function readStoreRecords(dir) {
|
|
8466
8522
|
if (!fs12.existsSync(dir)) return { records: [], unreadable: 0 };
|
|
@@ -8520,9 +8576,9 @@ function createFileStore(options) {
|
|
|
8520
8576
|
async get(slug, getOptions = {}) {
|
|
8521
8577
|
const filePath = fileFor(slug);
|
|
8522
8578
|
if (filePath === null || !fs12.existsSync(filePath)) return { ok: false, reason: "not-found" };
|
|
8523
|
-
const
|
|
8524
|
-
if (!
|
|
8525
|
-
const record =
|
|
8579
|
+
const read2 = readRecord(filePath);
|
|
8580
|
+
if (!read2.ok) return { ok: false, reason: "unreadable", problem: read2.problem };
|
|
8581
|
+
const record = read2.record;
|
|
8526
8582
|
const hasManageToken = manageTokenMatches(getOptions.manageToken, record.manageTokenHash);
|
|
8527
8583
|
const viewer = { ...getOptions.viewer, hasManageToken };
|
|
8528
8584
|
const allowed = canRead(record, viewer);
|
|
@@ -8534,10 +8590,10 @@ function createFileStore(options) {
|
|
|
8534
8590
|
const summaries = [];
|
|
8535
8591
|
for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
|
|
8536
8592
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
8537
|
-
const
|
|
8538
|
-
if (!
|
|
8539
|
-
if (!canList(
|
|
8540
|
-
summaries.push(summarize(
|
|
8593
|
+
const read2 = readRecord(path12.join(dir, entry.name));
|
|
8594
|
+
if (!read2.ok) continue;
|
|
8595
|
+
if (!canList(read2.record)) continue;
|
|
8596
|
+
summaries.push(summarize(read2.record));
|
|
8541
8597
|
}
|
|
8542
8598
|
summaries.sort((a, b) => a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0);
|
|
8543
8599
|
return summaries;
|
|
@@ -8641,6 +8697,31 @@ function readGitLineage(cwd, options = {}) {
|
|
|
8641
8697
|
gitCapturedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
8642
8698
|
};
|
|
8643
8699
|
}
|
|
8700
|
+
function zList(output) {
|
|
8701
|
+
return output === null ? null : output.split("\0").filter((entry) => entry.length > 0);
|
|
8702
|
+
}
|
|
8703
|
+
function bindCommit(cwd, rev, options = {}) {
|
|
8704
|
+
const base = readGitLineage(cwd, options);
|
|
8705
|
+
if (base === null || cwd === void 0) {
|
|
8706
|
+
return { problem: `the session's directory (${cwd ?? "not recorded"}) is not a git work tree on this machine` };
|
|
8707
|
+
}
|
|
8708
|
+
const sha = git(cwd, ["rev-parse", "--verify", "-q", `${rev}^{commit}`], options)?.trim();
|
|
8709
|
+
if (sha === void 0 || !/^[0-9a-f]{7,64}$/.test(sha)) return { problem: `"${rev}" is not a commit in ${cwd}` };
|
|
8710
|
+
if (sha === base.headCommitSha) return { ...base, commitNamed: true };
|
|
8711
|
+
const tracked = zList(git(cwd, ["diff", "--name-only", "-z", sha, "--"], options));
|
|
8712
|
+
const untracked = zList(git(cwd, ["ls-files", "--others", "--exclude-standard", "-z"], options));
|
|
8713
|
+
if (tracked === null || untracked === null) return { problem: `git could not compare the working tree with ${sha}` };
|
|
8714
|
+
const dirty = [.../* @__PURE__ */ new Set([...tracked, ...untracked])];
|
|
8715
|
+
const kept = dirty.slice(0, GIT_LIMITS.dirtyFiles);
|
|
8716
|
+
const { dirtyFilesOmitted: _omitted, ...rest } = base;
|
|
8717
|
+
return {
|
|
8718
|
+
...rest,
|
|
8719
|
+
headCommitSha: sha,
|
|
8720
|
+
dirtyFiles: kept,
|
|
8721
|
+
...dirty.length > kept.length ? { dirtyFilesOmitted: dirty.length - kept.length } : {},
|
|
8722
|
+
commitNamed: true
|
|
8723
|
+
};
|
|
8724
|
+
}
|
|
8644
8725
|
function checkPrUrl(value) {
|
|
8645
8726
|
let parsed;
|
|
8646
8727
|
try {
|
|
@@ -8656,12 +8737,14 @@ function checkPrUrl(value) {
|
|
|
8656
8737
|
function withLineage(trace, lineage, prUrl) {
|
|
8657
8738
|
if (lineage === null && prUrl === void 0) return trace;
|
|
8658
8739
|
const source = { ...trace.source };
|
|
8659
|
-
const elsewhere = lineage !== null && source.gitBranch !== void 0 && lineage.gitBranch !== source.gitBranch;
|
|
8740
|
+
const elsewhere = lineage !== null && lineage.commitNamed !== true && source.gitBranch !== void 0 && lineage.gitBranch !== source.gitBranch;
|
|
8660
8741
|
if (lineage !== null && !elsewhere) {
|
|
8661
8742
|
if (source.gitBranch === void 0 && lineage.gitBranch !== void 0) source.gitBranch = lineage.gitBranch;
|
|
8662
8743
|
if (lineage.headCommitSha !== void 0) source.headCommitSha = lineage.headCommitSha;
|
|
8663
8744
|
source.dirtyFiles = lineage.dirtyFiles;
|
|
8664
8745
|
if (lineage.dirtyFilesOmitted !== void 0) source.dirtyFilesOmitted = lineage.dirtyFilesOmitted;
|
|
8746
|
+
else delete source.dirtyFilesOmitted;
|
|
8747
|
+
if (lineage.commitNamed === true) source.commitNamed = true;
|
|
8665
8748
|
source.gitCapturedAt = lineage.gitCapturedAt;
|
|
8666
8749
|
}
|
|
8667
8750
|
if (prUrl !== void 0) source.prUrl = prUrl;
|
|
@@ -9549,6 +9632,8 @@ aside .note{color:var(--dim);font-size:12px;margin:0 0 12px}
|
|
|
9549
9632
|
.keys b{color:var(--fg);font-weight:600}
|
|
9550
9633
|
.row .path{color:#5c6474;font-size:10px;word-break:break-all}
|
|
9551
9634
|
fieldset{border:1px solid var(--line);border-radius:6px;margin:0 0 12px;padding:8px 10px}
|
|
9635
|
+
input.title{box-sizing:border-box;width:100%;font:inherit;padding:6px 8px;margin:4px 0 6px;border:1px solid var(--line);border-radius:5px;background:var(--bg);color:var(--fg)}
|
|
9636
|
+
input.title:focus{outline:none;border-color:var(--acc)}
|
|
9552
9637
|
legend{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
|
|
9553
9638
|
label.vis{display:block;padding:3px 0}
|
|
9554
9639
|
.warn{color:var(--warn);font-size:11px;margin:4px 0 0}
|
|
@@ -9594,6 +9679,9 @@ var SCRIPT = `
|
|
|
9594
9679
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
9595
9680
|
var tag = e.target && e.target.tagName;
|
|
9596
9681
|
var key = e.key;
|
|
9682
|
+
// TYPING IS NOT TRIAGE: j, k and space in the title field are letters. Enter submits the form
|
|
9683
|
+
// the way any text field does, through the first submit button - Confirm.
|
|
9684
|
+
if (tag === 'INPUT' && e.target.type === 'text') return;
|
|
9597
9685
|
if (key === 'j' || key === 'ArrowDown') { e.preventDefault(); focusRow(cursor + 1); return; }
|
|
9598
9686
|
if (key === 'k' || key === 'ArrowUp') { e.preventDefault(); focusRow(cursor - 1); return; }
|
|
9599
9687
|
if (key === ' ' && cursor >= 0 && tag !== 'BUTTON') {
|
|
@@ -9724,6 +9812,11 @@ function renderReviewPage(input) {
|
|
|
9724
9812
|
Every match is redacted by default. Tick a row to KEEP the original text instead.</p>
|
|
9725
9813
|
<div class="tools"><button id="keep-none" type="button">redact all</button><button id="keep-all" type="button">keep all</button></div>
|
|
9726
9814
|
<p class="keys"><b>j</b>/<b>k</b> move · <b>space</b> keep or redact · <b>enter</b> confirm</p>
|
|
9815
|
+
${input.titleDraft === void 0 ? "" : `<fieldset>
|
|
9816
|
+
<legend><label for="title">Title</label></legend>
|
|
9817
|
+
<input class="title" id="title" name="title" type="text" required maxlength="${LIMITS.title}" value="${escapeHtml(input.titleDraft)}">
|
|
9818
|
+
<p class="note">Change it here, or leave it. A secret typed into it is redacted.</p>
|
|
9819
|
+
</fieldset>`}
|
|
9727
9820
|
<fieldset>
|
|
9728
9821
|
<legend>Visibility</legend>
|
|
9729
9822
|
<label class="vis"><input type="radio" name="visibility" value="private" checked> private</label>
|
|
@@ -9777,7 +9870,9 @@ async function readBody(req, limitBytes = 2 * 1024 * 1024) {
|
|
|
9777
9870
|
function parseDecisionBody(body) {
|
|
9778
9871
|
const form = new URLSearchParams(body);
|
|
9779
9872
|
const parsedVisibility = VisibilitySchema.safeParse(form.get("visibility"));
|
|
9873
|
+
const title = form.get("title");
|
|
9780
9874
|
return {
|
|
9875
|
+
...title !== null ? { title: normalizeTitle(title) } : {},
|
|
9781
9876
|
visibility: parsedVisibility.success ? parsedVisibility.data : "private",
|
|
9782
9877
|
// ONE CHECKBOX MAY DECIDE MANY MATCHES. A grouped row carries every id it governs,
|
|
9783
9878
|
// space-separated, so the browser sends one value and the caller still gets the flat
|
|
@@ -9852,8 +9947,24 @@ async function startReviewServer(options) {
|
|
|
9852
9947
|
void (async () => {
|
|
9853
9948
|
try {
|
|
9854
9949
|
const body = await readBody(req);
|
|
9855
|
-
const { visibility, keepIds } = parseDecisionBody(body);
|
|
9856
|
-
|
|
9950
|
+
const { visibility, keepIds, title } = parseDecisionBody(body);
|
|
9951
|
+
if (title === null) {
|
|
9952
|
+
send(
|
|
9953
|
+
400,
|
|
9954
|
+
page(
|
|
9955
|
+
"Flatbread - title refused",
|
|
9956
|
+
`<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>`
|
|
9957
|
+
)
|
|
9958
|
+
);
|
|
9959
|
+
return;
|
|
9960
|
+
}
|
|
9961
|
+
const changed = title !== void 0 && options.titleDraft !== void 0 && title !== normalizeTitle(options.titleDraft);
|
|
9962
|
+
const confirmed = {
|
|
9963
|
+
action: "confirm",
|
|
9964
|
+
visibility,
|
|
9965
|
+
keepIds,
|
|
9966
|
+
...changed ? { title } : {}
|
|
9967
|
+
};
|
|
9857
9968
|
const message = options.onConfirm ? await options.onConfirm(confirmed) : "";
|
|
9858
9969
|
send(200, renderDonePage("Share draft written", message, options.publishTo));
|
|
9859
9970
|
finish(confirmed);
|
|
@@ -9890,6 +10001,16 @@ async function startReviewServer(options) {
|
|
|
9890
10001
|
}
|
|
9891
10002
|
|
|
9892
10003
|
// src/share.ts
|
|
10004
|
+
var spanKey = (match) => `${match.fieldPath}\0${match.start}\0${match.end}\0${match.original}`;
|
|
10005
|
+
function applyReview(trace, matches, keepIds, title) {
|
|
10006
|
+
if (title === void 0 || title === trace.title) return applyRedactions(trace, matches, { keepIds });
|
|
10007
|
+
const retitled = { ...trace, title };
|
|
10008
|
+
const kept = new Set(keepIds);
|
|
10009
|
+
const keptSpans = new Set(matches.filter((m) => kept.has(m.id) && m.fieldPath !== "title").map(spanKey));
|
|
10010
|
+
const rescanned = scanTrace(retitled).matches;
|
|
10011
|
+
const keep = rescanned.filter((m) => m.fieldPath !== "title" && keptSpans.has(spanKey(m))).map((m) => m.id);
|
|
10012
|
+
return applyRedactions(retitled, rescanned, { keepIds: keep });
|
|
10013
|
+
}
|
|
9893
10014
|
function openInBrowser(url) {
|
|
9894
10015
|
try {
|
|
9895
10016
|
if (process.platform === "win32") {
|
|
@@ -10064,17 +10185,22 @@ async function runShare(options) {
|
|
|
10064
10185
|
if (parsed.humanTest) console.error(` next step HUMAN-TESTS.md section ${parsed.humanTest}`);
|
|
10065
10186
|
return { exitCode: 1, status: "not-found" };
|
|
10066
10187
|
}
|
|
10067
|
-
const
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
options.
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10188
|
+
const cwd = parsed.trace.source.cwd;
|
|
10189
|
+
let lineage;
|
|
10190
|
+
if (options.commit !== void 0) {
|
|
10191
|
+
const bound = (options.bindCommit ?? ((dir, rev) => bindCommit(dir, rev)))(cwd, options.commit);
|
|
10192
|
+
if ("problem" in bound) {
|
|
10193
|
+
console.error(`--commit ${options.commit}: ${bound.problem}`);
|
|
10194
|
+
console.error("Nothing was written. Run it from the session's repository, or leave --commit off.");
|
|
10195
|
+
return { exitCode: 1, status: "bad-commit" };
|
|
10196
|
+
}
|
|
10197
|
+
lineage = bound;
|
|
10198
|
+
} else {
|
|
10199
|
+
lineage = (options.readLineage ?? ((dir) => readGitLineage(dir)))(cwd);
|
|
10077
10200
|
}
|
|
10201
|
+
const lined = withLineage(parsed.trace, lineage, options.prUrl);
|
|
10202
|
+
const trace = options.title !== void 0 ? FlatbreadTraceSchema.parse({ ...lined, title: options.title }) : lined;
|
|
10203
|
+
for (const note of parsed.notes) log(`note: ${note}`);
|
|
10078
10204
|
const scan = scanTrace(trace);
|
|
10079
10205
|
const byRule = /* @__PURE__ */ new Map();
|
|
10080
10206
|
for (const match of scan.matches) byRule.set(match.rule, (byRule.get(match.rule) ?? 0) + 1);
|
|
@@ -10101,11 +10227,12 @@ async function runShare(options) {
|
|
|
10101
10227
|
slots: scan.slots,
|
|
10102
10228
|
sessionPath: target,
|
|
10103
10229
|
draftPath: options.outputPath,
|
|
10230
|
+
titleDraft: previewTitle,
|
|
10104
10231
|
...isPublisher(reviewPublisher) ? { publishTo: reviewPublisher.description } : {},
|
|
10105
10232
|
...options.ports ? { ports: options.ports } : {},
|
|
10106
10233
|
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
10107
10234
|
onConfirm: (decision2) => {
|
|
10108
|
-
const applied2 =
|
|
10235
|
+
const applied2 = applyReview(trace, scan.matches, decision2.keepIds, decision2.title);
|
|
10109
10236
|
const draft2 = buildDraft(applied2.trace, applied2.report, decision2.visibility);
|
|
10110
10237
|
writeDraft(options.outputPath, draft2);
|
|
10111
10238
|
written = draft2;
|
|
@@ -10385,11 +10512,160 @@ function describeReadFailure(failure, what) {
|
|
|
10385
10512
|
|
|
10386
10513
|
// src/mcp.ts
|
|
10387
10514
|
import readline3 from "node:readline";
|
|
10515
|
+
|
|
10516
|
+
// src/bucket.ts
|
|
10517
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
10518
|
+
import fs18 from "node:fs";
|
|
10519
|
+
import path17 from "node:path";
|
|
10520
|
+
var BUCKET_INDEX_VERSION = 1;
|
|
10521
|
+
var BUCKET_INDEX_FILE = "index.json";
|
|
10522
|
+
var CAPTURE_LOG_FILE = "capture.log";
|
|
10523
|
+
var COMMIT_SESSION_WINDOW_MS = 2 * 60 * 60 * 1e3;
|
|
10524
|
+
function bucketDir(env = process.env) {
|
|
10525
|
+
return path17.join(flatbreadHome(env), "bucket");
|
|
10526
|
+
}
|
|
10527
|
+
function bucketSlug(harness, sessionKey) {
|
|
10528
|
+
return `local-${createHash2("sha256").update(`${harness}\0${sessionKey}`, "utf8").digest("hex").slice(0, 12)}`;
|
|
10529
|
+
}
|
|
10530
|
+
function rowOf(record) {
|
|
10531
|
+
const sha = record.trace.source.headCommitSha;
|
|
10532
|
+
return {
|
|
10533
|
+
summary: summarize(record),
|
|
10534
|
+
sessionPath: record.trace.source.sourcePath,
|
|
10535
|
+
capturedAt: record.updatedAt,
|
|
10536
|
+
...record.trace.source.commitNamed === true && sha !== void 0 ? { commit: sha } : {}
|
|
10537
|
+
};
|
|
10538
|
+
}
|
|
10539
|
+
function recordStems(dir) {
|
|
10540
|
+
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();
|
|
10541
|
+
}
|
|
10542
|
+
function writeAtomic(file, body) {
|
|
10543
|
+
fs18.mkdirSync(path17.dirname(file), { recursive: true });
|
|
10544
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
10545
|
+
fs18.writeFileSync(temp, body, "utf8");
|
|
10546
|
+
fs18.renameSync(temp, file);
|
|
10547
|
+
}
|
|
10548
|
+
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);
|
|
10549
|
+
function readIndex(dir) {
|
|
10550
|
+
try {
|
|
10551
|
+
const parsed = JSON.parse(fs18.readFileSync(path17.join(dir, BUCKET_INDEX_FILE), "utf8"));
|
|
10552
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
10553
|
+
const { version, rows } = parsed;
|
|
10554
|
+
if (version !== BUCKET_INDEX_VERSION || !Array.isArray(rows)) return null;
|
|
10555
|
+
const out = [];
|
|
10556
|
+
for (const row of rows) {
|
|
10557
|
+
if (typeof row !== "object" || row === null) return null;
|
|
10558
|
+
const { summary, sessionPath, capturedAt, commit } = row;
|
|
10559
|
+
const parsedSummary = RecordSummarySchema.safeParse(summary);
|
|
10560
|
+
if (!parsedSummary.success || typeof sessionPath !== "string" || typeof capturedAt !== "string") return null;
|
|
10561
|
+
if (commit !== void 0 && typeof commit !== "string") return null;
|
|
10562
|
+
out.push({ summary: parsedSummary.data, sessionPath, capturedAt, ...commit !== void 0 ? { commit } : {} });
|
|
10563
|
+
}
|
|
10564
|
+
return out;
|
|
10565
|
+
} catch {
|
|
10566
|
+
return null;
|
|
10567
|
+
}
|
|
10568
|
+
}
|
|
10569
|
+
function writeIndex(dir, rows) {
|
|
10570
|
+
writeAtomic(path17.join(dir, BUCKET_INDEX_FILE), JSON.stringify({ version: BUCKET_INDEX_VERSION, rows: newestFirst(rows) }, null, 2) + "\n");
|
|
10571
|
+
}
|
|
10572
|
+
function readBucket(dir) {
|
|
10573
|
+
if (!fs18.existsSync(dir)) return { rows: [], rebuilt: false, unreadable: 0 };
|
|
10574
|
+
const stems = recordStems(dir);
|
|
10575
|
+
const indexed = readIndex(dir);
|
|
10576
|
+
if (indexed !== null && JSON.stringify(indexed.map((row) => row.summary.slug).sort()) === JSON.stringify(stems)) {
|
|
10577
|
+
return { rows: newestFirst(indexed), rebuilt: false, unreadable: 0 };
|
|
10578
|
+
}
|
|
10579
|
+
const { records, unreadable } = readStoreRecords(dir);
|
|
10580
|
+
const rows = newestFirst(records.map(rowOf));
|
|
10581
|
+
try {
|
|
10582
|
+
writeIndex(dir, rows);
|
|
10583
|
+
} catch {
|
|
10584
|
+
}
|
|
10585
|
+
return { rows, rebuilt: true, unreadable };
|
|
10586
|
+
}
|
|
10587
|
+
function readBucketRecord(dir, slug) {
|
|
10588
|
+
if (!SlugSchema.safeParse(slug).success) return null;
|
|
10589
|
+
try {
|
|
10590
|
+
const parsed = TraceRecordSchema.safeParse(JSON.parse(fs18.readFileSync(path17.join(dir, `${slug}.json`), "utf8")));
|
|
10591
|
+
return parsed.success ? parsed.data : null;
|
|
10592
|
+
} catch {
|
|
10593
|
+
return null;
|
|
10594
|
+
}
|
|
10595
|
+
}
|
|
10596
|
+
function matchesQuery(summary, query) {
|
|
10597
|
+
if (query.harness !== void 0 && summary.harness !== query.harness) return false;
|
|
10598
|
+
if (query.category !== void 0 && summary.category !== query.category) return false;
|
|
10599
|
+
if (query.onlyWithRecoveries === true && summary.failuresRecovered === 0) return false;
|
|
10600
|
+
return matchesSearch(summary, query.query ?? "");
|
|
10601
|
+
}
|
|
10602
|
+
async function captureToBucket(options) {
|
|
10603
|
+
const dir = options.dir ?? bucketDir(options.env);
|
|
10604
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
10605
|
+
const parsed = await parseWithHarness({ filePath: options.file, ...options.harness !== void 0 ? { harness: options.harness } : {} });
|
|
10606
|
+
if (!parsed.ok) return { status: "refused", reason: `could not parse ${options.file}: ${parsed.reason}` };
|
|
10607
|
+
const cwd = parsed.trace.source.cwd;
|
|
10608
|
+
let lineage;
|
|
10609
|
+
if (options.commit !== void 0) {
|
|
10610
|
+
const bound = (options.bindCommit ?? ((at, rev) => bindCommit(at, rev)))(cwd, options.commit);
|
|
10611
|
+
if ("problem" in bound) return { status: "refused", reason: `--commit ${options.commit}: ${bound.problem}` };
|
|
10612
|
+
lineage = bound;
|
|
10613
|
+
} else {
|
|
10614
|
+
lineage = (options.readLineage ?? ((at) => readGitLineage(at)))(cwd);
|
|
10615
|
+
}
|
|
10616
|
+
const trace = withLineage(parsed.trace, lineage);
|
|
10617
|
+
const scan = scanTrace(trace);
|
|
10618
|
+
const applied = applyRedactions(trace, scan.matches, { keepIds: [] });
|
|
10619
|
+
const sessionKey = trace.source.sessionId.length > 0 ? trace.source.sessionId : path17.resolve(options.file);
|
|
10620
|
+
const slug = bucketSlug(parsed.harness, sessionKey);
|
|
10621
|
+
const recordPath = path17.join(dir, `${slug}.json`);
|
|
10622
|
+
const previous = readBucketRecord(dir, slug);
|
|
10623
|
+
const commit = applied.trace.source.commitNamed === true ? applied.trace.source.headCommitSha : void 0;
|
|
10624
|
+
const draft = ShareDraftSchema.parse({
|
|
10625
|
+
schemaVersion: 1,
|
|
10626
|
+
createdAt: previous?.createdAt ?? now,
|
|
10627
|
+
trace: { ...applied.trace, visibility: "private" },
|
|
10628
|
+
redaction: applied.report,
|
|
10629
|
+
uploaded: false
|
|
10630
|
+
});
|
|
10631
|
+
const done = { slug, path: recordPath, steps: draft.trace.steps.length, redacted: draft.redaction.redactedCount, ...commit !== void 0 ? { commit } : {} };
|
|
10632
|
+
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) {
|
|
10633
|
+
return { status: "unchanged", ...done };
|
|
10634
|
+
}
|
|
10635
|
+
const record = buildRecord(draft, { slug, manageTokenHash: hashManageToken(mintManageToken()), now, origin: "local", visibility: "private" });
|
|
10636
|
+
writeAtomic(recordPath, JSON.stringify(record, null, 2) + "\n");
|
|
10637
|
+
const rows = readBucket(dir).rows.filter((row) => row.summary.slug !== slug);
|
|
10638
|
+
writeIndex(dir, [...rows, rowOf(record)]);
|
|
10639
|
+
return { status: "captured", ...done };
|
|
10640
|
+
}
|
|
10641
|
+
function sessionForRepo(repo, options = {}) {
|
|
10642
|
+
const projects = getProjectsDir(options.env ?? process.env);
|
|
10643
|
+
const folder = path17.join(projects, encodeCwd(path17.resolve(repo)));
|
|
10644
|
+
const newest = listSessionFiles(folder)[0];
|
|
10645
|
+
if (newest === void 0) return { problem: `no Claude Code session recorded for ${repo}` };
|
|
10646
|
+
const age = (options.nowMs ?? Date.now()) - newest.mtimeMs;
|
|
10647
|
+
const window = options.windowMs ?? COMMIT_SESSION_WINDOW_MS;
|
|
10648
|
+
if (age > window) {
|
|
10649
|
+
return { problem: `the newest session for ${repo} last moved ${Math.round(age / 6e4)} min ago, outside the ${Math.round(window / 6e4)} min window` };
|
|
10650
|
+
}
|
|
10651
|
+
return { file: newest.filePath };
|
|
10652
|
+
}
|
|
10653
|
+
function logCapture(dir, line, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
10654
|
+
try {
|
|
10655
|
+
fs18.mkdirSync(dir, { recursive: true });
|
|
10656
|
+
fs18.appendFileSync(path17.join(dir, CAPTURE_LOG_FILE), `${now} ${line.replace(/[\r\n]+/g, " ")}
|
|
10657
|
+
`, "utf8");
|
|
10658
|
+
} catch {
|
|
10659
|
+
}
|
|
10660
|
+
}
|
|
10661
|
+
|
|
10662
|
+
// src/mcp.ts
|
|
10388
10663
|
var MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
10389
|
-
var MCP_SERVER_VERSION = "0.
|
|
10390
|
-
function defaultMcpDeps(storeDir2) {
|
|
10664
|
+
var MCP_SERVER_VERSION = "0.2.0";
|
|
10665
|
+
function defaultMcpDeps(storeDir2, bucketDir2) {
|
|
10391
10666
|
return {
|
|
10392
10667
|
storeDir: storeDir2,
|
|
10668
|
+
bucketDir: bucketDir2,
|
|
10393
10669
|
search: (query) => searchRemote(query, { client: "mcp" }),
|
|
10394
10670
|
capsule: (ref) => fetchCapsule(ref, { client: "mcp" })
|
|
10395
10671
|
};
|
|
@@ -10434,12 +10710,6 @@ function hitLine(hit) {
|
|
|
10434
10710
|
if (hit.visibility !== "public") parts.push(hit.visibility);
|
|
10435
10711
|
return `- ${parts.join(" \xB7 ")}`;
|
|
10436
10712
|
}
|
|
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
10713
|
function readQuery(args) {
|
|
10444
10714
|
const query = {};
|
|
10445
10715
|
if (args["query"] !== void 0) {
|
|
@@ -10467,29 +10737,52 @@ function readQuery(args) {
|
|
|
10467
10737
|
function localRecords(deps) {
|
|
10468
10738
|
return deps.storeDir === null ? { records: [], unreadable: 0 } : readStoreRecords(deps.storeDir);
|
|
10469
10739
|
}
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
10740
|
+
var runs = (count) => `${count} matching run${count === 1 ? "" : "s"}`;
|
|
10741
|
+
function capturedLine(row) {
|
|
10742
|
+
return `${hitLine(row.summary)} \xB7 captured ${row.capturedAt.slice(0, 10)}${row.commit !== void 0 ? ` at ${row.commit.slice(0, 12)}` : ""}`;
|
|
10743
|
+
}
|
|
10744
|
+
async function searchEverywhere(query, deps, options = {}) {
|
|
10473
10745
|
const out = [];
|
|
10474
10746
|
const local = localRecords(deps);
|
|
10475
|
-
const localHits = local.records.map((record) => summarize(record)).filter((summary) =>
|
|
10747
|
+
const localHits = local.records.map((record) => summarize(record)).filter((summary) => matchesQuery(summary, query));
|
|
10476
10748
|
if (deps.storeDir !== null) {
|
|
10477
|
-
out.push(`This machine (${localHits.length}
|
|
10749
|
+
out.push(`This machine (${runs(localHits.length)}):`);
|
|
10478
10750
|
for (const hit of localHits) out.push(hitLine(hit));
|
|
10479
10751
|
if (local.unreadable > 0) out.push(`(${local.unreadable} local record file${local.unreadable === 1 ? "" : "s"} could not be read)`);
|
|
10480
10752
|
}
|
|
10753
|
+
const seen = new Set(localHits.map((hit) => hit.slug));
|
|
10754
|
+
if (deps.bucketDir !== null) {
|
|
10755
|
+
const bucket = readBucket(deps.bucketDir);
|
|
10756
|
+
const captured = bucket.rows.filter((row) => !seen.has(row.summary.slug) && matchesQuery(row.summary, query));
|
|
10757
|
+
if (out.length > 0) out.push("");
|
|
10758
|
+
out.push(`Captured on this machine, not shared (${runs(captured.length)}):`);
|
|
10759
|
+
for (const row of captured) {
|
|
10760
|
+
out.push(capturedLine(row));
|
|
10761
|
+
seen.add(row.summary.slug);
|
|
10762
|
+
}
|
|
10763
|
+
if (bucket.unreadable > 0) out.push(`(${bucket.unreadable} captured run file${bucket.unreadable === 1 ? "" : "s"} could not be read)`);
|
|
10764
|
+
}
|
|
10765
|
+
if (options.remote === false) {
|
|
10766
|
+
if (out.length > 0) out.push("");
|
|
10767
|
+
out.push("The site was not searched: --local");
|
|
10768
|
+
return { lines: out, siteSearched: false };
|
|
10769
|
+
}
|
|
10481
10770
|
const remote = await deps.search(query);
|
|
10482
10771
|
if (out.length > 0) out.push("");
|
|
10483
10772
|
if (remote.ok) {
|
|
10484
|
-
const seen = new Set(localHits.map((hit) => hit.slug));
|
|
10485
10773
|
const fresh = remote.results.filter((hit) => !seen.has(hit.slug));
|
|
10486
|
-
out.push(`${remote.site} (${fresh.length}
|
|
10774
|
+
out.push(`${remote.site} (${runs(fresh.length)}):`);
|
|
10487
10775
|
for (const hit of fresh) out.push(hitLine(hit));
|
|
10488
10776
|
} else {
|
|
10489
10777
|
out.push(`The site was not searched: ${describeReadFailure(remote, "search")}`);
|
|
10490
10778
|
}
|
|
10491
|
-
|
|
10492
|
-
|
|
10779
|
+
return { lines: out, siteSearched: remote.ok };
|
|
10780
|
+
}
|
|
10781
|
+
async function searchTool(args, deps) {
|
|
10782
|
+
const query = readQuery(args);
|
|
10783
|
+
if (typeof query === "string") return text(query, true);
|
|
10784
|
+
const { lines: lines2 } = await searchEverywhere(query, deps);
|
|
10785
|
+
return text([...lines2, "", "Pass a run ID to flatbread_get_capsule for what that run learned."].join("\n"));
|
|
10493
10786
|
}
|
|
10494
10787
|
async function capsuleTool(args, deps) {
|
|
10495
10788
|
const raw = args["slug"];
|
|
@@ -10497,8 +10790,8 @@ async function capsuleTool(args, deps) {
|
|
|
10497
10790
|
const ref = parseRunRef(raw);
|
|
10498
10791
|
if (ref === null) return text(`"${raw}" is not a run ID or a /r/<id> link`, true);
|
|
10499
10792
|
if (ref.site === void 0) {
|
|
10500
|
-
const local = localRecords(deps).records.find((record) => record.slug === ref.slug);
|
|
10501
|
-
if (local !== void 0) return text(buildCapsule(local.trace));
|
|
10793
|
+
const local = localRecords(deps).records.find((record) => record.slug === ref.slug) ?? (deps.bucketDir !== null ? readBucketRecord(deps.bucketDir, ref.slug) : null);
|
|
10794
|
+
if (local !== null && local !== void 0) return text(buildCapsule(local.trace));
|
|
10502
10795
|
}
|
|
10503
10796
|
const remote = await deps.capsule(ref);
|
|
10504
10797
|
if (remote.ok) return text(remote.capsule);
|
|
@@ -10577,10 +10870,126 @@ async function runMcpServer(deps, io) {
|
|
|
10577
10870
|
return 0;
|
|
10578
10871
|
}
|
|
10579
10872
|
|
|
10873
|
+
// src/git-hook.ts
|
|
10874
|
+
import fs19 from "node:fs";
|
|
10875
|
+
import path18 from "node:path";
|
|
10876
|
+
var POST_COMMIT_MARKER = "# flatbread post-commit hook - remove with: flatbread setup --uninstall";
|
|
10877
|
+
var CHAINED_HOOK = "post-commit.pre-flatbread";
|
|
10878
|
+
var sq = (value) => `'${value.replace(/\\/g, "/").replace(/'/g, "'\\''")}'`;
|
|
10879
|
+
function captureLine(execPath, entryPath) {
|
|
10880
|
+
return `${sq(execPath)} ${sq(entryPath)} capture --commit "$(git rev-parse HEAD)" --repo "$(git rev-parse --show-toplevel)" --quiet >/dev/null 2>&1 &`;
|
|
10881
|
+
}
|
|
10882
|
+
function postCommitScript(execPath, entryPath) {
|
|
10883
|
+
return [
|
|
10884
|
+
"#!/bin/sh",
|
|
10885
|
+
POST_COMMIT_MARKER,
|
|
10886
|
+
"# Runs the hook that was here before, then records this commit against the newest agent session",
|
|
10887
|
+
"# in this repository, on this machine only. It never changes the commit; it exits with the",
|
|
10888
|
+
"# earlier hook's status. Off switch: FLATBREAD_HOOK_DISABLED=1",
|
|
10889
|
+
"status=0",
|
|
10890
|
+
`prior="$(dirname "$0")/${CHAINED_HOOK}"`,
|
|
10891
|
+
'if [ -x "$prior" ]; then',
|
|
10892
|
+
' "$prior" "$@"',
|
|
10893
|
+
" status=$?",
|
|
10894
|
+
"fi",
|
|
10895
|
+
'if [ -z "$FLATBREAD_HOOK_DISABLED" ]; then',
|
|
10896
|
+
` ( ${captureLine(execPath, entryPath)} )`,
|
|
10897
|
+
"fi",
|
|
10898
|
+
'exit "$status"',
|
|
10899
|
+
""
|
|
10900
|
+
].join("\n");
|
|
10901
|
+
}
|
|
10902
|
+
var isOurs = (body) => body.includes(POST_COMMIT_MARKER);
|
|
10903
|
+
function hooksDirFor(cwd, options = {}) {
|
|
10904
|
+
const top = git(cwd, ["rev-parse", "--show-toplevel"], options)?.trim();
|
|
10905
|
+
if (top === void 0 || top.length === 0) return { ok: false, reason: "not-a-repo", detail: `${cwd} is not inside a git repository` };
|
|
10906
|
+
const managed = git(top, ["config", "--show-scope", "--get", "core.hooksPath"], options)?.trim();
|
|
10907
|
+
let machineHooksPath;
|
|
10908
|
+
if (managed !== void 0 && managed.length > 0) {
|
|
10909
|
+
const tab = managed.indexOf(" ");
|
|
10910
|
+
const scope = tab > 0 ? managed.slice(0, tab) : "local";
|
|
10911
|
+
const value = tab > 0 ? managed.slice(tab + 1) : managed;
|
|
10912
|
+
if (scope !== "system" && scope !== "global") {
|
|
10913
|
+
return { ok: false, reason: "hooks-path", detail: `core.hooksPath is set to ${value} in this repository, so another tool owns its hooks` };
|
|
10914
|
+
}
|
|
10915
|
+
machineHooksPath = { scope, path: value };
|
|
10916
|
+
}
|
|
10917
|
+
const common = git(top, ["rev-parse", "--git-common-dir"], options)?.trim();
|
|
10918
|
+
if (common === void 0 || common.length === 0) return { ok: false, reason: "unreadable", detail: `git could not name the git directory of ${top}` };
|
|
10919
|
+
return { ok: true, repo: path18.resolve(top), dir: path18.resolve(top, common, "hooks"), ...machineHooksPath !== void 0 ? { machineHooksPath } : {} };
|
|
10920
|
+
}
|
|
10921
|
+
var read = (file) => {
|
|
10922
|
+
try {
|
|
10923
|
+
return fs19.readFileSync(file, "utf8");
|
|
10924
|
+
} catch {
|
|
10925
|
+
return null;
|
|
10926
|
+
}
|
|
10927
|
+
};
|
|
10928
|
+
function installGitHook(dir, script, options = {}) {
|
|
10929
|
+
const hook = path18.join(dir, "post-commit");
|
|
10930
|
+
const chained = path18.join(dir, CHAINED_HOOK);
|
|
10931
|
+
const current = read(hook);
|
|
10932
|
+
const write = (lines2) => {
|
|
10933
|
+
if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
|
|
10934
|
+
fs19.mkdirSync(dir, { recursive: true });
|
|
10935
|
+
if (current !== null && !isOurs(current)) fs19.renameSync(hook, chained);
|
|
10936
|
+
fs19.writeFileSync(hook, script, { encoding: "utf8", mode: 493 });
|
|
10937
|
+
fs19.chmodSync(hook, 493);
|
|
10938
|
+
return { code: 0, lines: lines2 };
|
|
10939
|
+
};
|
|
10940
|
+
if (current === null) return write([`post-commit ${hook} (new)`]);
|
|
10941
|
+
if (isOurs(current)) {
|
|
10942
|
+
if (current === script) return { code: 0, lines: [`post-commit ${hook}`, "already installed, unchanged"] };
|
|
10943
|
+
return write([`post-commit ${hook}`, "updated the flatbread hook"]);
|
|
10944
|
+
}
|
|
10945
|
+
if (fs19.existsSync(chained)) {
|
|
10946
|
+
return {
|
|
10947
|
+
code: 1,
|
|
10948
|
+
lines: [
|
|
10949
|
+
`refused ${hook}`,
|
|
10950
|
+
`${chained} already exists, and post-commit is not ours. Neither file was touched.`,
|
|
10951
|
+
"Move one of them yourself, then run setup again."
|
|
10952
|
+
]
|
|
10953
|
+
};
|
|
10954
|
+
}
|
|
10955
|
+
return write([
|
|
10956
|
+
`post-commit ${hook}`,
|
|
10957
|
+
`your post-commit was kept as ${CHAINED_HOOK} and runs first, unchanged, with its exit status`
|
|
10958
|
+
]);
|
|
10959
|
+
}
|
|
10960
|
+
function uninstallGitHook(dir, options = {}) {
|
|
10961
|
+
const hook = path18.join(dir, "post-commit");
|
|
10962
|
+
const chained = path18.join(dir, CHAINED_HOOK);
|
|
10963
|
+
const current = read(hook);
|
|
10964
|
+
const hasChained = fs19.existsSync(chained);
|
|
10965
|
+
const done = (lines2, act) => {
|
|
10966
|
+
if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
|
|
10967
|
+
act();
|
|
10968
|
+
return { code: 0, lines: lines2 };
|
|
10969
|
+
};
|
|
10970
|
+
if (current !== null && !isOurs(current)) {
|
|
10971
|
+
return {
|
|
10972
|
+
code: 0,
|
|
10973
|
+
lines: [
|
|
10974
|
+
`post-commit ${hook} is not ours, left alone`,
|
|
10975
|
+
...hasChained ? [`${chained} is still there; it is yours to keep or delete`] : []
|
|
10976
|
+
]
|
|
10977
|
+
};
|
|
10978
|
+
}
|
|
10979
|
+
if (current === null && !hasChained) return { code: 0, lines: [`no flatbread post-commit in ${dir}`] };
|
|
10980
|
+
if (hasChained) {
|
|
10981
|
+
return done([`post-commit ${hook}`, `restored your post-commit from ${CHAINED_HOOK}`], () => {
|
|
10982
|
+
if (current !== null) fs19.rmSync(hook);
|
|
10983
|
+
fs19.renameSync(chained, hook);
|
|
10984
|
+
});
|
|
10985
|
+
}
|
|
10986
|
+
return done([`post-commit ${hook}`, "removed the flatbread hook"], () => fs19.rmSync(hook));
|
|
10987
|
+
}
|
|
10988
|
+
|
|
10580
10989
|
// src/hook.ts
|
|
10581
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
10582
|
-
import
|
|
10583
|
-
import
|
|
10990
|
+
import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
|
|
10991
|
+
import fs20 from "node:fs";
|
|
10992
|
+
import path19 from "node:path";
|
|
10584
10993
|
var HOOK_EVENTS = ["Stop", "SessionEnd"];
|
|
10585
10994
|
var OFFER_AFTER_STOPS = 2;
|
|
10586
10995
|
var MAX_TRACKED_SESSIONS = 50;
|
|
@@ -10590,14 +10999,14 @@ function emptyHookState() {
|
|
|
10590
10999
|
return { version: HOOK_STATE_VERSION, sessions: {} };
|
|
10591
11000
|
}
|
|
10592
11001
|
function hookStatePath(env = process.env) {
|
|
10593
|
-
return
|
|
11002
|
+
return path19.join(flatbreadHome(env), "hook-state.json");
|
|
10594
11003
|
}
|
|
10595
11004
|
function readHookState(env = process.env) {
|
|
10596
11005
|
const file = hookStatePath(env);
|
|
10597
|
-
if (!
|
|
11006
|
+
if (!fs20.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
|
|
10598
11007
|
let raw;
|
|
10599
11008
|
try {
|
|
10600
|
-
raw =
|
|
11009
|
+
raw = fs20.readFileSync(file, "utf8");
|
|
10601
11010
|
} catch (error) {
|
|
10602
11011
|
return { ok: false, reason: "unreadable", detail: error.message };
|
|
10603
11012
|
}
|
|
@@ -10640,9 +11049,9 @@ function pruneHookState(state, keep = MAX_TRACKED_SESSIONS) {
|
|
|
10640
11049
|
}
|
|
10641
11050
|
function writeHookState(state, env = process.env) {
|
|
10642
11051
|
const dir = flatbreadHome(env);
|
|
10643
|
-
|
|
11052
|
+
fs20.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
10644
11053
|
const file = hookStatePath(env);
|
|
10645
|
-
|
|
11054
|
+
fs20.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
|
|
10646
11055
|
encoding: "utf8",
|
|
10647
11056
|
mode: 384
|
|
10648
11057
|
});
|
|
@@ -10777,12 +11186,18 @@ function offerAfterFrom(env) {
|
|
|
10777
11186
|
}
|
|
10778
11187
|
return { value: parsed };
|
|
10779
11188
|
}
|
|
11189
|
+
function spawnDetachedDefault(command, args) {
|
|
11190
|
+
const child = spawn2(command, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
11191
|
+
child.on("error", () => {
|
|
11192
|
+
});
|
|
11193
|
+
child.unref();
|
|
11194
|
+
}
|
|
10780
11195
|
async function runHookNotify(io) {
|
|
10781
11196
|
const env = io.env ?? process.env;
|
|
10782
11197
|
const stdout = io.stdout ?? ((line) => process.stdout.write(line + "\n"));
|
|
10783
11198
|
const stderr = io.stderr ?? ((line) => process.stderr.write(line + "\n"));
|
|
10784
11199
|
const now = io.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
10785
|
-
const exists = io.exists ?? ((filePath) =>
|
|
11200
|
+
const exists = io.exists ?? ((filePath) => fs20.existsSync(filePath));
|
|
10786
11201
|
const execPath = io.execPath ?? process.execPath;
|
|
10787
11202
|
const entryPath = io.entryPath ?? process.argv[1] ?? "";
|
|
10788
11203
|
try {
|
|
@@ -10796,14 +11211,14 @@ async function runHookNotify(io) {
|
|
|
10796
11211
|
stderr(`flatbread hook: NOT RETRIEVED (${parsed.detail})`);
|
|
10797
11212
|
return 0;
|
|
10798
11213
|
}
|
|
10799
|
-
const
|
|
10800
|
-
if (!
|
|
10801
|
-
stderr(`flatbread hook: NOT RETRIEVED (${
|
|
11214
|
+
const read2 = readHookState(env);
|
|
11215
|
+
if (!read2.ok) {
|
|
11216
|
+
stderr(`flatbread hook: NOT RETRIEVED (${read2.reason}: ${read2.detail}) at ${hookStatePath(env)}`);
|
|
10802
11217
|
return 0;
|
|
10803
11218
|
}
|
|
10804
11219
|
const offerAfter = offerAfterFrom(env);
|
|
10805
11220
|
if (offerAfter.ignored) stderr(`flatbread hook: ${offerAfter.ignored}, using ${OFFER_AFTER_STOPS}`);
|
|
10806
|
-
const decision = decideHook(parsed.event,
|
|
11221
|
+
const decision = decideHook(parsed.event, read2.state, {
|
|
10807
11222
|
now: now(),
|
|
10808
11223
|
exists,
|
|
10809
11224
|
offerAfter: offerAfter.value,
|
|
@@ -10811,7 +11226,7 @@ async function runHookNotify(io) {
|
|
|
10811
11226
|
entryPath
|
|
10812
11227
|
});
|
|
10813
11228
|
let recorded = true;
|
|
10814
|
-
if (decision.nextState !==
|
|
11229
|
+
if (decision.nextState !== read2.state) {
|
|
10815
11230
|
try {
|
|
10816
11231
|
writeHookState(decision.nextState, env);
|
|
10817
11232
|
} catch (error) {
|
|
@@ -10819,6 +11234,11 @@ async function runHookNotify(io) {
|
|
|
10819
11234
|
stderr(`flatbread hook: could not record state (${error.message})`);
|
|
10820
11235
|
}
|
|
10821
11236
|
}
|
|
11237
|
+
const transcript = parsed.event.transcript_path;
|
|
11238
|
+
if (io.capture === true && parsed.event.hook_event_name === "SessionEnd" && transcript !== void 0 && exists(transcript)) {
|
|
11239
|
+
(io.spawnDetached ?? spawnDetachedDefault)(execPath, [entryPath, "capture", "--file", transcript, "--quiet"]);
|
|
11240
|
+
stderr("flatbread hook: capturing this session into the local bucket, in the background");
|
|
11241
|
+
}
|
|
10822
11242
|
if (decision.speak && recorded) {
|
|
10823
11243
|
stdout(JSON.stringify({ systemMessage: decision.systemMessage }));
|
|
10824
11244
|
stderr(`flatbread hook: ${decision.why}`);
|
|
@@ -10839,7 +11259,7 @@ function hookHandlerFor(options) {
|
|
|
10839
11259
|
const handler = {
|
|
10840
11260
|
type: "command",
|
|
10841
11261
|
command: options.execPath,
|
|
10842
|
-
args: [options.entryPath, "hook", "notify"]
|
|
11262
|
+
args: [options.entryPath, "hook", "notify", ...options.capture === true && options.event === "SessionEnd" ? ["--capture"] : []]
|
|
10843
11263
|
};
|
|
10844
11264
|
if (options.event === "Stop") handler.timeout = STOP_TIMEOUT_SECONDS;
|
|
10845
11265
|
return handler;
|
|
@@ -10934,10 +11354,10 @@ function removeHookConfig(settings) {
|
|
|
10934
11354
|
return { settings: root, removed };
|
|
10935
11355
|
}
|
|
10936
11356
|
function readSettingsFile(file) {
|
|
10937
|
-
if (!
|
|
11357
|
+
if (!fs20.existsSync(file)) return { present: false, file };
|
|
10938
11358
|
let raw;
|
|
10939
11359
|
try {
|
|
10940
|
-
raw =
|
|
11360
|
+
raw = fs20.readFileSync(file, "utf8");
|
|
10941
11361
|
} catch (error) {
|
|
10942
11362
|
return { present: true, file, unreadable: error.message };
|
|
10943
11363
|
}
|
|
@@ -10949,19 +11369,19 @@ function readSettingsFile(file) {
|
|
|
10949
11369
|
}
|
|
10950
11370
|
}
|
|
10951
11371
|
function writeSettingsFile(file, settings) {
|
|
10952
|
-
|
|
11372
|
+
fs20.mkdirSync(path19.dirname(file), { recursive: true });
|
|
10953
11373
|
const temp = `${file}.flatbread-${process.pid}.tmp`;
|
|
10954
|
-
|
|
10955
|
-
|
|
11374
|
+
fs20.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
11375
|
+
fs20.renameSync(temp, file);
|
|
10956
11376
|
}
|
|
10957
|
-
function handlersFor(execPath, entryPath) {
|
|
11377
|
+
function handlersFor(execPath, entryPath, capture = false) {
|
|
10958
11378
|
return {
|
|
10959
11379
|
Stop: hookHandlerFor({ execPath, entryPath, event: "Stop" }),
|
|
10960
|
-
SessionEnd: hookHandlerFor({ execPath, entryPath, event: "SessionEnd" })
|
|
11380
|
+
SessionEnd: hookHandlerFor({ execPath, entryPath, event: "SessionEnd", capture })
|
|
10961
11381
|
};
|
|
10962
11382
|
}
|
|
10963
|
-
function handlerBlockFor(execPath, entryPath) {
|
|
10964
|
-
const handlers = handlersFor(execPath, entryPath);
|
|
11383
|
+
function handlerBlockFor(execPath, entryPath, capture = false) {
|
|
11384
|
+
const handlers = handlersFor(execPath, entryPath, capture);
|
|
10965
11385
|
const hooks = {};
|
|
10966
11386
|
for (const event of HOOK_EVENTS) hooks[event] = [{ hooks: [handlers[event]] }];
|
|
10967
11387
|
return JSON.stringify({ hooks }, null, 2);
|
|
@@ -10972,21 +11392,22 @@ function runHookInstall(options = {}) {
|
|
|
10972
11392
|
const execPath = options.execPath ?? process.execPath;
|
|
10973
11393
|
const entryPath = options.entryPath ?? process.argv[1] ?? "";
|
|
10974
11394
|
const file = options.settingsPath ?? claudeSettingsPath(env);
|
|
10975
|
-
const
|
|
10976
|
-
|
|
11395
|
+
const capture = options.capture === true;
|
|
11396
|
+
const read2 = readSettingsFile(file);
|
|
11397
|
+
if (read2.present && "unreadable" in read2) {
|
|
10977
11398
|
log("");
|
|
10978
11399
|
log(` refused ${file}`);
|
|
10979
|
-
log(` ${
|
|
11400
|
+
log(` ${read2.unreadable}`);
|
|
10980
11401
|
log(" a settings file we cannot read is not one we will rewrite. Add this by hand:");
|
|
10981
11402
|
log("");
|
|
10982
|
-
for (const line of handlerBlockFor(execPath, entryPath).split("\n")) log(` ${line}`);
|
|
11403
|
+
for (const line of handlerBlockFor(execPath, entryPath, capture).split("\n")) log(` ${line}`);
|
|
10983
11404
|
log("");
|
|
10984
11405
|
return 1;
|
|
10985
11406
|
}
|
|
10986
|
-
const current =
|
|
11407
|
+
const current = read2.present ? read2.parsed : {};
|
|
10987
11408
|
let merged;
|
|
10988
11409
|
try {
|
|
10989
|
-
merged = mergeHookConfig(current, handlersFor(execPath, entryPath));
|
|
11410
|
+
merged = mergeHookConfig(current, handlersFor(execPath, entryPath, capture));
|
|
10990
11411
|
} catch (error) {
|
|
10991
11412
|
log("");
|
|
10992
11413
|
log(` refused ${file}`);
|
|
@@ -10995,7 +11416,7 @@ function runHookInstall(options = {}) {
|
|
|
10995
11416
|
return 1;
|
|
10996
11417
|
}
|
|
10997
11418
|
log("");
|
|
10998
|
-
log(` settings ${file}${
|
|
11419
|
+
log(` settings ${file}${read2.present ? "" : " (new)"}`);
|
|
10999
11420
|
log(` command "${execPath}" "${entryPath}" hook notify`);
|
|
11000
11421
|
if (merged.changes.length === 0) {
|
|
11001
11422
|
log(" already installed, unchanged");
|
|
@@ -11019,7 +11440,7 @@ function runHookInstall(options = {}) {
|
|
|
11019
11440
|
log(" refused: nothing was written, because this command line does not work here.");
|
|
11020
11441
|
log(" fix the command (or pass --no-probe if you know better), then install again:");
|
|
11021
11442
|
log("");
|
|
11022
|
-
for (const line of handlerBlockFor(execPath, entryPath).split("\n")) log(` ${line}`);
|
|
11443
|
+
for (const line of handlerBlockFor(execPath, entryPath, capture).split("\n")) log(` ${line}`);
|
|
11023
11444
|
log("");
|
|
11024
11445
|
return 1;
|
|
11025
11446
|
}
|
|
@@ -11048,7 +11469,9 @@ function runHookInstall(options = {}) {
|
|
|
11048
11469
|
log("");
|
|
11049
11470
|
return 1;
|
|
11050
11471
|
}
|
|
11051
|
-
log(
|
|
11472
|
+
log(
|
|
11473
|
+
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."
|
|
11474
|
+
);
|
|
11052
11475
|
log(" off switch: FLATBREAD_HOOK_DISABLED=1, or flatbread hook uninstall");
|
|
11053
11476
|
log("");
|
|
11054
11477
|
return 0;
|
|
@@ -11057,23 +11480,23 @@ function runHookUninstall(options = {}) {
|
|
|
11057
11480
|
const env = options.env ?? process.env;
|
|
11058
11481
|
const log = options.log ?? ((line) => console.log(line));
|
|
11059
11482
|
const file = options.settingsPath ?? claudeSettingsPath(env);
|
|
11060
|
-
const
|
|
11483
|
+
const read2 = readSettingsFile(file);
|
|
11061
11484
|
log("");
|
|
11062
|
-
if (!
|
|
11485
|
+
if (!read2.present) {
|
|
11063
11486
|
log(` nothing to remove: ${file} does not exist`);
|
|
11064
11487
|
log("");
|
|
11065
11488
|
return 0;
|
|
11066
11489
|
}
|
|
11067
|
-
if ("unreadable" in
|
|
11490
|
+
if ("unreadable" in read2) {
|
|
11068
11491
|
log(` refused ${file}`);
|
|
11069
|
-
log(` ${
|
|
11492
|
+
log(` ${read2.unreadable}`);
|
|
11070
11493
|
log(" remove the flatbread entries by hand rather than let us rewrite a file we cannot read");
|
|
11071
11494
|
log("");
|
|
11072
11495
|
return 1;
|
|
11073
11496
|
}
|
|
11074
11497
|
let result;
|
|
11075
11498
|
try {
|
|
11076
|
-
result = removeHookConfig(
|
|
11499
|
+
result = removeHookConfig(read2.parsed);
|
|
11077
11500
|
} catch (error) {
|
|
11078
11501
|
log(` refused ${error instanceof Error ? error.message : String(error)}`);
|
|
11079
11502
|
log("");
|
|
@@ -11084,6 +11507,13 @@ function runHookUninstall(options = {}) {
|
|
|
11084
11507
|
log("");
|
|
11085
11508
|
return 0;
|
|
11086
11509
|
}
|
|
11510
|
+
if (options.dryRun === true) {
|
|
11511
|
+
log(` settings ${file}`);
|
|
11512
|
+
for (const line of result.removed) log(` ${line}`);
|
|
11513
|
+
log(" --dry-run: nothing was written");
|
|
11514
|
+
log("");
|
|
11515
|
+
return 0;
|
|
11516
|
+
}
|
|
11087
11517
|
try {
|
|
11088
11518
|
writeSettingsFile(file, result.settings);
|
|
11089
11519
|
} catch (error) {
|
|
@@ -11100,8 +11530,8 @@ function runHookUninstall(options = {}) {
|
|
|
11100
11530
|
function settingsCandidates(env, cwd) {
|
|
11101
11531
|
return [
|
|
11102
11532
|
{ label: "user", file: claudeSettingsPath(env) },
|
|
11103
|
-
{ label: "project", file:
|
|
11104
|
-
{ label: "local", file:
|
|
11533
|
+
{ label: "project", file: path19.join(cwd, ".claude", "settings.json") },
|
|
11534
|
+
{ label: "local", file: path19.join(cwd, ".claude", "settings.local.json") }
|
|
11105
11535
|
];
|
|
11106
11536
|
}
|
|
11107
11537
|
function installedEventsIn(parsed, exists) {
|
|
@@ -11131,23 +11561,23 @@ function runHookStatus(options = {}) {
|
|
|
11131
11561
|
const env = options.env ?? process.env;
|
|
11132
11562
|
const log = options.log ?? ((line) => console.log(line));
|
|
11133
11563
|
const cwd = options.cwd ?? process.cwd();
|
|
11134
|
-
const exists = options.exists ?? ((filePath) =>
|
|
11564
|
+
const exists = options.exists ?? ((filePath) => fs20.existsSync(filePath));
|
|
11135
11565
|
const candidates = options.settingsPath === void 0 ? settingsCandidates(env, cwd) : [{ label: "settings", file: options.settingsPath }];
|
|
11136
11566
|
log("");
|
|
11137
11567
|
log("flatbread hook");
|
|
11138
11568
|
log("");
|
|
11139
11569
|
let installed = 0;
|
|
11140
11570
|
for (const candidate of candidates) {
|
|
11141
|
-
const
|
|
11142
|
-
if (!
|
|
11571
|
+
const read3 = readSettingsFile(candidate.file);
|
|
11572
|
+
if (!read3.present) {
|
|
11143
11573
|
log(` ${candidate.label.padEnd(8)} ${candidate.file} (absent)`);
|
|
11144
11574
|
continue;
|
|
11145
11575
|
}
|
|
11146
|
-
if ("unreadable" in
|
|
11147
|
-
log(` ${candidate.label.padEnd(8)} ${candidate.file} NOT RETRIEVED (${
|
|
11576
|
+
if ("unreadable" in read3) {
|
|
11577
|
+
log(` ${candidate.label.padEnd(8)} ${candidate.file} NOT RETRIEVED (${read3.unreadable})`);
|
|
11148
11578
|
continue;
|
|
11149
11579
|
}
|
|
11150
|
-
const found = installedEventsIn(
|
|
11580
|
+
const found = installedEventsIn(read3.parsed, exists);
|
|
11151
11581
|
installed += found.events.length;
|
|
11152
11582
|
const summary = found.events.length === 0 ? "(no flatbread handler)" : found.events.join(", ");
|
|
11153
11583
|
log(` ${candidate.label.padEnd(8)} ${candidate.file} ${summary}`);
|
|
@@ -11171,15 +11601,15 @@ function runHookStatus(options = {}) {
|
|
|
11171
11601
|
}
|
|
11172
11602
|
}
|
|
11173
11603
|
log("");
|
|
11174
|
-
const
|
|
11175
|
-
if (!
|
|
11176
|
-
log(` state NOT RETRIEVED (${
|
|
11604
|
+
const read2 = readHookState(env);
|
|
11605
|
+
if (!read2.ok) {
|
|
11606
|
+
log(` state NOT RETRIEVED (${read2.reason}: ${read2.detail})`);
|
|
11177
11607
|
log(` ${hookStatePath(env)}`);
|
|
11178
11608
|
log("");
|
|
11179
11609
|
return 1;
|
|
11180
11610
|
}
|
|
11181
|
-
const sessions = Object.entries(
|
|
11182
|
-
log(` state ${hookStatePath(env)}${
|
|
11611
|
+
const sessions = Object.entries(read2.state.sessions);
|
|
11612
|
+
log(` state ${hookStatePath(env)}${read2.present ? "" : " (none yet)"}`);
|
|
11183
11613
|
log(` sessions ${sessions.length}`);
|
|
11184
11614
|
log(` offered ${sessions.filter(([, session]) => session.offeredAt !== void 0).length}`);
|
|
11185
11615
|
const recent = [...sessions].sort((a, b) => b[1].lastSeenAt > a[1].lastSeenAt ? 1 : -1).slice(0, 5);
|
|
@@ -11251,6 +11681,74 @@ function probeHandler(handler, run = spawnSync2) {
|
|
|
11251
11681
|
return { ok: true, detail: "ran, exited 0, printed nothing" };
|
|
11252
11682
|
}
|
|
11253
11683
|
|
|
11684
|
+
// src/setup.ts
|
|
11685
|
+
function runSetup(options = {}) {
|
|
11686
|
+
const env = options.env ?? process.env;
|
|
11687
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
11688
|
+
const execPath = options.execPath ?? process.execPath;
|
|
11689
|
+
const entryPath = options.entryPath ?? process.argv[1] ?? "";
|
|
11690
|
+
const dryRun = options.dryRun === true;
|
|
11691
|
+
const settings = options.settingsPath !== void 0 ? { settingsPath: options.settingsPath } : {};
|
|
11692
|
+
if (options.uninstall !== true && /[\\/]_npx[\\/]/.test(entryPath)) {
|
|
11693
|
+
log("");
|
|
11694
|
+
log(` refused: this flatbread runs from npx's cache (${entryPath}),`);
|
|
11695
|
+
log(" which npm may clear - and a hook that names a missing file fails silently. Install it, then run setup:");
|
|
11696
|
+
log(" npm install -g flatbread-cli");
|
|
11697
|
+
log(" flatbread setup");
|
|
11698
|
+
log("");
|
|
11699
|
+
return 1;
|
|
11700
|
+
}
|
|
11701
|
+
log("");
|
|
11702
|
+
log("Claude Code");
|
|
11703
|
+
const claude = options.uninstall === true ? runHookUninstall({ env, log, dryRun, ...settings }) : runHookInstall({
|
|
11704
|
+
env,
|
|
11705
|
+
log,
|
|
11706
|
+
execPath,
|
|
11707
|
+
entryPath,
|
|
11708
|
+
dryRun,
|
|
11709
|
+
capture: true,
|
|
11710
|
+
...options.noProbe === true ? { noProbe: true } : {},
|
|
11711
|
+
...options.spawn !== void 0 ? { spawn: options.spawn } : {},
|
|
11712
|
+
...settings
|
|
11713
|
+
});
|
|
11714
|
+
let git2 = 0;
|
|
11715
|
+
log("git");
|
|
11716
|
+
if (options.noGit === true) {
|
|
11717
|
+
log(" --no-git: the post-commit hook was not touched");
|
|
11718
|
+
} else {
|
|
11719
|
+
const where = hooksDirFor(options.repo ?? process.cwd(), options.git !== void 0 ? { git: options.git } : {});
|
|
11720
|
+
if (!where.ok) {
|
|
11721
|
+
log(` skipped ${where.detail}`);
|
|
11722
|
+
if (where.reason === "hooks-path" && options.uninstall !== true) {
|
|
11723
|
+
log(" add this line to that post-commit yourself to capture commits:");
|
|
11724
|
+
log(` ${captureLine(execPath, entryPath)}`);
|
|
11725
|
+
} else if (where.reason === "not-a-repo" && options.uninstall !== true) {
|
|
11726
|
+
log(" run setup inside a repository to bind its commits to your sessions");
|
|
11727
|
+
}
|
|
11728
|
+
if (where.reason === "unreadable") git2 = 1;
|
|
11729
|
+
} else {
|
|
11730
|
+
const outcome = options.uninstall === true ? uninstallGitHook(where.dir, { dryRun }) : installGitHook(where.dir, postCommitScript(execPath, entryPath), { dryRun });
|
|
11731
|
+
for (const line of outcome.lines) log(` ${line}`);
|
|
11732
|
+
if (where.machineHooksPath !== void 0 && options.uninstall !== true && outcome.code === 0) {
|
|
11733
|
+
log(` note git runs ${where.machineHooksPath.path} first (core.hooksPath, ${where.machineHooksPath.scope} config).`);
|
|
11734
|
+
log(" This hook fires only if that one runs the repository's own post-commit.");
|
|
11735
|
+
log(` After your next commit, ${bucketDir(env)}/capture.log says whether it did.`);
|
|
11736
|
+
}
|
|
11737
|
+
git2 = outcome.code;
|
|
11738
|
+
}
|
|
11739
|
+
}
|
|
11740
|
+
log("");
|
|
11741
|
+
if (options.uninstall === true) {
|
|
11742
|
+
log(` the bucket is left alone: ${bucketDir(env)}`);
|
|
11743
|
+
} else {
|
|
11744
|
+
log(` captures go to ${bucketDir(env)}: redacted, private, never uploaded`);
|
|
11745
|
+
log(" find them with: flatbread search <words>");
|
|
11746
|
+
log(" share one with: flatbread share --file <its session>, which opens the review");
|
|
11747
|
+
}
|
|
11748
|
+
log("");
|
|
11749
|
+
return claude !== 0 || git2 !== 0 ? 1 : 0;
|
|
11750
|
+
}
|
|
11751
|
+
|
|
11254
11752
|
// src/login-server.ts
|
|
11255
11753
|
import crypto2 from "node:crypto";
|
|
11256
11754
|
import http2 from "node:http";
|
|
@@ -11461,18 +11959,18 @@ async function bindFirst(server, ports) {
|
|
|
11461
11959
|
}
|
|
11462
11960
|
|
|
11463
11961
|
// 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 =
|
|
11962
|
+
var PACKAGE_ROOT = path20.resolve(path20.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11963
|
+
var SOURCE_CHECKOUT = fs21.existsSync(path20.join(PACKAGE_ROOT, "src", "index.ts"));
|
|
11964
|
+
var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ? path20.join(PACKAGE_ROOT, ".tmp") : path20.join(os7.homedir(), ".flatbread"));
|
|
11965
|
+
var OUTPUT_PATH = path20.join(TMP_DIR, "last-parse.json");
|
|
11966
|
+
var SHARE_OUTPUT_PATH = path20.join(TMP_DIR, "last-share.json");
|
|
11469
11967
|
function storeDir() {
|
|
11470
|
-
return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null :
|
|
11968
|
+
return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path20.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
|
|
11471
11969
|
}
|
|
11472
11970
|
var USAGE = `flatbread - registry of AI agent runs
|
|
11473
11971
|
|
|
11474
11972
|
Usage:
|
|
11475
|
-
flatbread parse --file <path> Parse a session log
|
|
11973
|
+
flatbread parse --file <path> Parse a session log and print its brief. Writes nothing
|
|
11476
11974
|
flatbread parse --latest Parse the newest session for a harness
|
|
11477
11975
|
flatbread share Review + sanitize the newest session
|
|
11478
11976
|
flatbread share --file <path> Review + sanitize a specific session
|
|
@@ -11481,6 +11979,13 @@ Usage:
|
|
|
11481
11979
|
flatbread pull <id|url> --out <file> Write it to a file instead
|
|
11482
11980
|
flatbread mcp MCP server on stdio: flatbread_search_traces and
|
|
11483
11981
|
flatbread_get_capsule, for your coding agent
|
|
11982
|
+
flatbread search <words> Find runs: this machine's shared and captured ones, then
|
|
11983
|
+
the site (the same answer the MCP server gives)
|
|
11984
|
+
flatbread setup Auto-capture: the Claude Code hook, plus a chained git
|
|
11985
|
+
post-commit in this repository. Captures stay local
|
|
11986
|
+
flatbread setup --uninstall Remove exactly what setup added
|
|
11987
|
+
flatbread capture --file <path> Capture one session into the local bucket (setup's hooks
|
|
11988
|
+
run this; redacted, private, never uploaded)
|
|
11484
11989
|
flatbread adapters List harness adapters and their confidence
|
|
11485
11990
|
flatbread login Sign in through the website, the way npm does
|
|
11486
11991
|
flatbread login --email <addr> Sign in by email instead (see the note below)
|
|
@@ -11496,15 +12001,40 @@ Usage:
|
|
|
11496
12001
|
Options for parse:
|
|
11497
12002
|
--harness <id> Skip detection and name the harness.
|
|
11498
12003
|
${HARNESS_IDS.join(", ")}
|
|
12004
|
+
--save Also write the raw (unredacted) trace where view and the
|
|
12005
|
+
local viewer read it
|
|
12006
|
+
--out <file> Write the raw trace to <file> instead
|
|
11499
12007
|
|
|
11500
12008
|
Options for share:
|
|
11501
12009
|
--harness <id> Skip detection and name the harness
|
|
11502
12010
|
--publish Upload the run to your account (needs flatbread login)
|
|
11503
12011
|
--pr <url> Link the pull request this run belongs to (https)
|
|
12012
|
+
--title <text> The run's title (1-${LIMITS.title} characters). The review page
|
|
12013
|
+
opens with it and can still change it
|
|
12014
|
+
--commit [sha] Bind the run to a commit (bare --commit = HEAD) and record
|
|
12015
|
+
whether the working tree was clean against it
|
|
11504
12016
|
--no-review Redact every match, skip the browser (non-interactive)
|
|
11505
12017
|
--no-open Start the review server but do not launch a browser
|
|
11506
12018
|
-h, --help Show this message
|
|
11507
12019
|
|
|
12020
|
+
Options for search:
|
|
12021
|
+
--harness <id> --category <kind> Only runs from that harness, or of that kind of job
|
|
12022
|
+
--recovered Only runs where the agent recovered from a failure
|
|
12023
|
+
--local This machine only; the site is not asked
|
|
12024
|
+
|
|
12025
|
+
Options for setup:
|
|
12026
|
+
--dry-run Say what would be written, write nothing
|
|
12027
|
+
--uninstall Remove the handlers and the post-commit; restore a
|
|
12028
|
+
post-commit hook setup had chained
|
|
12029
|
+
--no-git Leave the repository's hooks alone
|
|
12030
|
+
--settings <path> --no-probe As for hook install
|
|
12031
|
+
|
|
12032
|
+
Options for capture:
|
|
12033
|
+
--commit [sha] Bind the capture to a commit (bare = HEAD)
|
|
12034
|
+
--repo <dir> Without --file: the newest session of that repository,
|
|
12035
|
+
if it moved in the last ${Math.round(COMMIT_SESSION_WINDOW_MS / 6e4)} minutes
|
|
12036
|
+
--quiet Log to the bucket's capture.log and always exit 0 (hooks)
|
|
12037
|
+
|
|
11508
12038
|
Options for hook install and hook uninstall:
|
|
11509
12039
|
--settings <path> Settings file to edit. Default: the user settings file
|
|
11510
12040
|
(~/.claude/settings.json, or CLAUDE_CONFIG_DIR)
|
|
@@ -11519,8 +12049,8 @@ Options for adapters:
|
|
|
11519
12049
|
Notes:
|
|
11520
12050
|
parse and share record the git branch, HEAD commit and uncommitted file names when the
|
|
11521
12051
|
session ran inside a git repository. Nothing else from git is read.
|
|
11522
|
-
|
|
11523
|
-
Share drafts are written to ${
|
|
12052
|
+
parse --save writes ${path20.relative(process.cwd(), OUTPUT_PATH) || OUTPUT_PATH}
|
|
12053
|
+
Share drafts are written to ${path20.relative(process.cwd(), SHARE_OUTPUT_PATH) || SHARE_OUTPUT_PATH}
|
|
11524
12054
|
Every parsed run defaults to visibility "private".
|
|
11525
12055
|
Nothing is uploaded unless you pass --publish, or set FLATBREAD_API_URL and
|
|
11526
12056
|
FLATBREAD_API_TOKEN yourself. pull and mcp only read: public runs, your own when you
|
|
@@ -11528,6 +12058,8 @@ Notes:
|
|
|
11528
12058
|
127.0.0.1 only. Add the MCP server to Claude Code with:
|
|
11529
12059
|
claude mcp add flatbread -- npx -y flatbread-cli mcp
|
|
11530
12060
|
The viewer is a separate dev server: npm run dev -w @flatbread/web
|
|
12061
|
+
Captured runs live in ~/.flatbread/bucket (FLATBREAD_HOOK_DISABLED=1 stops every capture).
|
|
12062
|
+
A capture is not a share: to publish one, run share --file on its session.
|
|
11531
12063
|
The hook offers ONE LINE at the end of a turn and never uploads anything itself. Turn it
|
|
11532
12064
|
off with FLATBREAD_HOOK_DISABLED=1, or change when it speaks with
|
|
11533
12065
|
FLATBREAD_HOOK_OFFER_AFTER=<turns>. It records what it has seen in ~/.flatbread.
|
|
@@ -11548,6 +12080,15 @@ function parseArgv(argv) {
|
|
|
11548
12080
|
const arg = argv[i];
|
|
11549
12081
|
if (arg === "--latest") {
|
|
11550
12082
|
args.latest = true;
|
|
12083
|
+
} else if (arg === "--save") {
|
|
12084
|
+
args.out = OUTPUT_PATH;
|
|
12085
|
+
} else if (arg === "--out") {
|
|
12086
|
+
const value = argv[i + 1];
|
|
12087
|
+
if (value === void 0 || value.startsWith("--")) throw new Error("--out needs a file path");
|
|
12088
|
+
args.out = path20.resolve(value);
|
|
12089
|
+
i += 1;
|
|
12090
|
+
} else if (arg !== void 0 && arg.startsWith("--out=")) {
|
|
12091
|
+
args.out = path20.resolve(arg.slice("--out=".length));
|
|
11551
12092
|
} else if (arg === "--harness") {
|
|
11552
12093
|
const value = argv[i + 1];
|
|
11553
12094
|
if (value === void 0 || value.startsWith("--")) {
|
|
@@ -11643,7 +12184,9 @@ function report(result, outputPath) {
|
|
|
11643
12184
|
lines2.push("");
|
|
11644
12185
|
lines2.push(...briefLines(trace));
|
|
11645
12186
|
lines2.push("");
|
|
11646
|
-
lines2.push(
|
|
12187
|
+
lines2.push(
|
|
12188
|
+
outputPath === void 0 ? ` wrote nothing. --save writes ${OUTPUT_PATH} for the viewer` : ` wrote ${outputPath}`
|
|
12189
|
+
);
|
|
11647
12190
|
lines2.push("");
|
|
11648
12191
|
console.log(lines2.join("\n"));
|
|
11649
12192
|
}
|
|
@@ -11666,12 +12209,12 @@ async function runParse(args) {
|
|
|
11666
12209
|
target = found[0];
|
|
11667
12210
|
console.log(`latest ${harness} session: ${target}`);
|
|
11668
12211
|
} else if (args.file) {
|
|
11669
|
-
target =
|
|
11670
|
-
if (!
|
|
12212
|
+
target = path20.resolve(args.file);
|
|
12213
|
+
if (!fs21.existsSync(target)) {
|
|
11671
12214
|
console.error(`file not found: ${target}`);
|
|
11672
12215
|
return 1;
|
|
11673
12216
|
}
|
|
11674
|
-
if (!
|
|
12217
|
+
if (!fs21.statSync(target).isFile()) {
|
|
11675
12218
|
console.error(`not a file: ${target}`);
|
|
11676
12219
|
return 1;
|
|
11677
12220
|
}
|
|
@@ -11701,9 +12244,11 @@ async function runParse(args) {
|
|
|
11701
12244
|
}
|
|
11702
12245
|
const trace = withLineage(result.trace, readGitLineage(result.trace.source.cwd));
|
|
11703
12246
|
FlatbreadTraceSchema.parse(trace);
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
12247
|
+
if (args.out !== void 0) {
|
|
12248
|
+
fs21.mkdirSync(path20.dirname(args.out), { recursive: true });
|
|
12249
|
+
fs21.writeFileSync(args.out, JSON.stringify(trace, null, 2) + "\n", "utf8");
|
|
12250
|
+
}
|
|
12251
|
+
report({ ...result, trace }, args.out);
|
|
11707
12252
|
return 0;
|
|
11708
12253
|
}
|
|
11709
12254
|
function parseAdaptersArgv(argv) {
|
|
@@ -11765,8 +12310,8 @@ async function runAdapters(args) {
|
|
|
11765
12310
|
}
|
|
11766
12311
|
let input = null;
|
|
11767
12312
|
if (args.file !== void 0) {
|
|
11768
|
-
const resolved =
|
|
11769
|
-
if (!
|
|
12313
|
+
const resolved = path20.resolve(args.file);
|
|
12314
|
+
if (!fs21.existsSync(resolved)) {
|
|
11770
12315
|
console.error(`file not found: ${resolved}`);
|
|
11771
12316
|
return 1;
|
|
11772
12317
|
}
|
|
@@ -11780,6 +12325,16 @@ async function runAdapters(args) {
|
|
|
11780
12325
|
console.log("");
|
|
11781
12326
|
return 0;
|
|
11782
12327
|
}
|
|
12328
|
+
var COMMIT_REV = /^[A-Za-z0-9][A-Za-z0-9._/~^@{}-]{0,199}$/;
|
|
12329
|
+
function checkCommitRev(value) {
|
|
12330
|
+
if (!COMMIT_REV.test(value)) throw new Error(`--commit needs a commit SHA or ref, e.g. --commit HEAD (got "${value}")`);
|
|
12331
|
+
return value;
|
|
12332
|
+
}
|
|
12333
|
+
function checkTitle(value) {
|
|
12334
|
+
const title = normalizeTitle(value);
|
|
12335
|
+
if (title === null) throw new Error(`--title must be 1-${LIMITS.title} characters`);
|
|
12336
|
+
return title;
|
|
12337
|
+
}
|
|
11783
12338
|
function parseShareArgv(argv) {
|
|
11784
12339
|
const args = { noReview: false, open: true, publish: false };
|
|
11785
12340
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -11807,6 +12362,23 @@ function parseShareArgv(argv) {
|
|
|
11807
12362
|
i += 1;
|
|
11808
12363
|
} else if (arg !== void 0 && arg.startsWith("--pr=")) {
|
|
11809
12364
|
args.prUrl = checkPrUrl(arg.slice("--pr=".length));
|
|
12365
|
+
} else if (arg === "--title") {
|
|
12366
|
+
const value = argv[i + 1];
|
|
12367
|
+
if (value === void 0 || value.startsWith("--")) throw new Error("--title needs the run's title, in quotes");
|
|
12368
|
+
args.title = checkTitle(value);
|
|
12369
|
+
i += 1;
|
|
12370
|
+
} else if (arg !== void 0 && arg.startsWith("--title=")) {
|
|
12371
|
+
args.title = checkTitle(arg.slice("--title=".length));
|
|
12372
|
+
} else if (arg === "--commit") {
|
|
12373
|
+
const value = argv[i + 1];
|
|
12374
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
12375
|
+
args.commit = "HEAD";
|
|
12376
|
+
} else {
|
|
12377
|
+
args.commit = checkCommitRev(value);
|
|
12378
|
+
i += 1;
|
|
12379
|
+
}
|
|
12380
|
+
} else if (arg !== void 0 && arg.startsWith("--commit=")) {
|
|
12381
|
+
args.commit = checkCommitRev(arg.slice("--commit=".length));
|
|
11810
12382
|
} else if (arg === "--file") {
|
|
11811
12383
|
const value = argv[i + 1];
|
|
11812
12384
|
if (value === void 0 || value.startsWith("--")) {
|
|
@@ -11960,7 +12532,7 @@ function tryOpenBrowser(url) {
|
|
|
11960
12532
|
const windows = process.platform === "win32";
|
|
11961
12533
|
const command = windows ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
11962
12534
|
const args = windows ? ["/c", "start", "", url] : [url];
|
|
11963
|
-
const child =
|
|
12535
|
+
const child = spawn3(command, args, { stdio: "ignore", detached: true, windowsHide: true });
|
|
11964
12536
|
child.on("error", () => {
|
|
11965
12537
|
});
|
|
11966
12538
|
child.unref();
|
|
@@ -12075,7 +12647,7 @@ function parseHookArgv(argv) {
|
|
|
12075
12647
|
if (known === void 0) {
|
|
12076
12648
|
throw new Error(`unknown hook subcommand "${sub}". Known: ${HOOK_SUBCOMMANDS.join(", ")}`);
|
|
12077
12649
|
}
|
|
12078
|
-
const args = { sub: known, dryRun: false, noProbe: false };
|
|
12650
|
+
const args = { sub: known, dryRun: false, noProbe: false, capture: false };
|
|
12079
12651
|
for (let i = 0; i < rest.length; i += 1) {
|
|
12080
12652
|
const flag = rest[i];
|
|
12081
12653
|
if (flag === "--settings") {
|
|
@@ -12093,13 +12665,17 @@ function parseHookArgv(argv) {
|
|
|
12093
12665
|
args.noProbe = true;
|
|
12094
12666
|
continue;
|
|
12095
12667
|
}
|
|
12668
|
+
if (flag === "--capture" && known === "notify") {
|
|
12669
|
+
args.capture = true;
|
|
12670
|
+
continue;
|
|
12671
|
+
}
|
|
12096
12672
|
throw new Error(`unknown option for hook ${known}: ${flag}`);
|
|
12097
12673
|
}
|
|
12098
12674
|
if (known !== "install" && args.noProbe) {
|
|
12099
12675
|
throw new Error("--no-probe only means anything to hook install");
|
|
12100
12676
|
}
|
|
12101
12677
|
if (known === "notify" && (args.settings !== void 0 || args.dryRun)) {
|
|
12102
|
-
throw new Error("hook notify takes
|
|
12678
|
+
throw new Error("hook notify takes only --capture: Claude Code runs it with the event on stdin");
|
|
12103
12679
|
}
|
|
12104
12680
|
if (known === "status" && args.dryRun) {
|
|
12105
12681
|
throw new Error("hook status writes nothing, so --dry-run means nothing here");
|
|
@@ -12111,7 +12687,7 @@ function parseHookArgv(argv) {
|
|
|
12111
12687
|
}
|
|
12112
12688
|
async function runHookCommand(args) {
|
|
12113
12689
|
if (args.sub === "notify") {
|
|
12114
|
-
return runHookNotify({ readStdin: () => readAllStdin(), entryPath: CLI_ENTRY });
|
|
12690
|
+
return runHookNotify({ readStdin: () => readAllStdin(), entryPath: CLI_ENTRY, capture: args.capture });
|
|
12115
12691
|
}
|
|
12116
12692
|
if (args.sub === "install") {
|
|
12117
12693
|
return runHookInstall({
|
|
@@ -12169,19 +12745,146 @@ async function runPull(args) {
|
|
|
12169
12745
|
process.stdout.write(result.capsule);
|
|
12170
12746
|
return 0;
|
|
12171
12747
|
}
|
|
12172
|
-
const target =
|
|
12173
|
-
|
|
12174
|
-
|
|
12748
|
+
const target = path20.resolve(args.out);
|
|
12749
|
+
fs21.mkdirSync(path20.dirname(target), { recursive: true });
|
|
12750
|
+
fs21.writeFileSync(target, result.capsule, "utf8");
|
|
12175
12751
|
console.error(`wrote ${target}`);
|
|
12176
12752
|
return 0;
|
|
12177
12753
|
}
|
|
12754
|
+
function parseSearchArgv(argv) {
|
|
12755
|
+
const query = {};
|
|
12756
|
+
const words = [];
|
|
12757
|
+
let local = false;
|
|
12758
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
12759
|
+
const arg = argv[i];
|
|
12760
|
+
const valueOf = (flag) => {
|
|
12761
|
+
if (arg.startsWith(`${flag}=`)) return arg.slice(flag.length + 1);
|
|
12762
|
+
const value = argv[i + 1];
|
|
12763
|
+
if (value === void 0 || value.startsWith("--")) throw new Error(`${flag} needs a value`);
|
|
12764
|
+
i += 1;
|
|
12765
|
+
return value;
|
|
12766
|
+
};
|
|
12767
|
+
if (arg === "--harness" || arg.startsWith("--harness=")) {
|
|
12768
|
+
query.harness = toHarnessId(valueOf("--harness"));
|
|
12769
|
+
} else if (arg === "--category" || arg.startsWith("--category=")) {
|
|
12770
|
+
const value = valueOf("--category");
|
|
12771
|
+
if (!TASK_CATEGORIES.includes(value)) throw new Error(`unknown --category "${value}". Known: ${TASK_CATEGORIES.join(", ")}`);
|
|
12772
|
+
query.category = value;
|
|
12773
|
+
} else if (arg === "--recovered") {
|
|
12774
|
+
query.onlyWithRecoveries = true;
|
|
12775
|
+
} else if (arg === "--local") {
|
|
12776
|
+
local = true;
|
|
12777
|
+
} else if (arg.startsWith("--")) {
|
|
12778
|
+
throw new Error(`Unknown option for search: ${arg}`);
|
|
12779
|
+
} else {
|
|
12780
|
+
words.push(arg);
|
|
12781
|
+
}
|
|
12782
|
+
}
|
|
12783
|
+
if (words.length > 0) query.query = words.join(" ");
|
|
12784
|
+
return { query, local };
|
|
12785
|
+
}
|
|
12786
|
+
async function runSearch(argv, deps = defaultMcpDeps(storeDir(), bucketDir()), print = console.log) {
|
|
12787
|
+
const { query, local } = parseSearchArgv(argv);
|
|
12788
|
+
const { lines: lines2, siteSearched } = await searchEverywhere(query, deps, { remote: !local });
|
|
12789
|
+
for (const line of lines2) print(line);
|
|
12790
|
+
return local || siteSearched ? 0 : 1;
|
|
12791
|
+
}
|
|
12792
|
+
function parseCaptureArgv(argv) {
|
|
12793
|
+
const args = { quiet: false };
|
|
12794
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
12795
|
+
const arg = argv[i];
|
|
12796
|
+
const next = argv[i + 1];
|
|
12797
|
+
if (arg === "--file" || arg === "--repo" || arg === "--harness") {
|
|
12798
|
+
if (next === void 0 || next.startsWith("--")) throw new Error(`${arg} needs a value`);
|
|
12799
|
+
if (arg === "--file") args.file = next;
|
|
12800
|
+
else if (arg === "--repo") args.repo = next;
|
|
12801
|
+
else args.harness = toHarnessId(next);
|
|
12802
|
+
i += 1;
|
|
12803
|
+
} else if (arg === "--commit") {
|
|
12804
|
+
if (next === void 0 || next.startsWith("--")) {
|
|
12805
|
+
args.commit = "HEAD";
|
|
12806
|
+
} else {
|
|
12807
|
+
args.commit = checkCommitRev(next);
|
|
12808
|
+
i += 1;
|
|
12809
|
+
}
|
|
12810
|
+
} else if (arg.startsWith("--commit=")) {
|
|
12811
|
+
args.commit = checkCommitRev(arg.slice("--commit=".length));
|
|
12812
|
+
} else if (arg === "--quiet") {
|
|
12813
|
+
args.quiet = true;
|
|
12814
|
+
} else {
|
|
12815
|
+
throw new Error(`Unknown option for capture: ${arg}`);
|
|
12816
|
+
}
|
|
12817
|
+
}
|
|
12818
|
+
if (args.file !== void 0 && args.repo !== void 0) throw new Error("capture takes --file or --repo, not both");
|
|
12819
|
+
return args;
|
|
12820
|
+
}
|
|
12821
|
+
async function runCapture(args, env = process.env) {
|
|
12822
|
+
const dir = bucketDir(env);
|
|
12823
|
+
const say = (line) => args.quiet ? logCapture(dir, line) : console.log(line);
|
|
12824
|
+
try {
|
|
12825
|
+
if (args.quiet && (env["FLATBREAD_HOOK_DISABLED"] ?? "").trim() !== "") {
|
|
12826
|
+
say("skipped FLATBREAD_HOOK_DISABLED is set");
|
|
12827
|
+
return 0;
|
|
12828
|
+
}
|
|
12829
|
+
let file = args.file;
|
|
12830
|
+
if (file === void 0) {
|
|
12831
|
+
const found = sessionForRepo(args.repo ?? process.cwd(), { env });
|
|
12832
|
+
if ("problem" in found) {
|
|
12833
|
+
say(`skipped ${found.problem}`);
|
|
12834
|
+
return 0;
|
|
12835
|
+
}
|
|
12836
|
+
file = found.file;
|
|
12837
|
+
}
|
|
12838
|
+
const result = await captureToBucket({
|
|
12839
|
+
file,
|
|
12840
|
+
env,
|
|
12841
|
+
dir,
|
|
12842
|
+
...args.harness !== void 0 ? { harness: args.harness } : {},
|
|
12843
|
+
...args.commit !== void 0 ? { commit: args.commit } : {}
|
|
12844
|
+
});
|
|
12845
|
+
if (result.status === "refused") {
|
|
12846
|
+
say(`refused ${result.reason}`);
|
|
12847
|
+
return args.quiet ? 0 : 1;
|
|
12848
|
+
}
|
|
12849
|
+
say(
|
|
12850
|
+
`${result.status} ${result.slug} ${result.steps} steps, ${result.redacted} redacted` + (result.commit !== void 0 ? `, at ${result.commit.slice(0, 12)}` : "") + ` -> ${result.path}`
|
|
12851
|
+
);
|
|
12852
|
+
return 0;
|
|
12853
|
+
} catch (error) {
|
|
12854
|
+
say(`failed ${error instanceof Error ? error.message : String(error)}`);
|
|
12855
|
+
return args.quiet ? 0 : 1;
|
|
12856
|
+
}
|
|
12857
|
+
}
|
|
12858
|
+
function parseSetupArgv(argv) {
|
|
12859
|
+
const args = {
|
|
12860
|
+
dryRun: false,
|
|
12861
|
+
uninstall: false,
|
|
12862
|
+
noGit: false,
|
|
12863
|
+
noProbe: false
|
|
12864
|
+
};
|
|
12865
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
12866
|
+
const arg = argv[i];
|
|
12867
|
+
if (arg === "--dry-run") args.dryRun = true;
|
|
12868
|
+
else if (arg === "--uninstall") args.uninstall = true;
|
|
12869
|
+
else if (arg === "--no-git") args.noGit = true;
|
|
12870
|
+
else if (arg === "--no-probe") args.noProbe = true;
|
|
12871
|
+
else if (arg === "--settings") {
|
|
12872
|
+
const value = argv[i + 1];
|
|
12873
|
+
if (value === void 0 || value.startsWith("--")) throw new Error("--settings needs a path");
|
|
12874
|
+
args.settings = value;
|
|
12875
|
+
i += 1;
|
|
12876
|
+
} else throw new Error(`Unknown option for setup: ${arg}`);
|
|
12877
|
+
}
|
|
12878
|
+
if (args.uninstall && args.noProbe) throw new Error("--no-probe only means anything when installing");
|
|
12879
|
+
return args;
|
|
12880
|
+
}
|
|
12178
12881
|
async function main(argv) {
|
|
12179
12882
|
const [command, ...rest] = argv;
|
|
12180
12883
|
if (command === void 0 || command === "--help" || command === "-h" || command === "help") {
|
|
12181
12884
|
console.log(USAGE);
|
|
12182
12885
|
return command === void 0 ? 1 : 0;
|
|
12183
12886
|
}
|
|
12184
|
-
if (command !== "parse" && command !== "share" && command !== "view" && command !== "adapters" && command !== "login" && command !== "logout" && command !== "whoami" && command !== "hook" && command !== "pull" && command !== "mcp") {
|
|
12887
|
+
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
12888
|
console.error(`Unknown command: ${command}`);
|
|
12186
12889
|
console.error(USAGE);
|
|
12187
12890
|
return 1;
|
|
@@ -12200,6 +12903,24 @@ async function main(argv) {
|
|
|
12200
12903
|
}
|
|
12201
12904
|
return runHookCommand(hookArgs);
|
|
12202
12905
|
}
|
|
12906
|
+
if (command === "search" || command === "setup" || command === "capture") {
|
|
12907
|
+
try {
|
|
12908
|
+
if (command === "search") return await runSearch(rest);
|
|
12909
|
+
if (command === "capture") return await runCapture(parseCaptureArgv(rest));
|
|
12910
|
+
const setup = parseSetupArgv(rest);
|
|
12911
|
+
return runSetup({
|
|
12912
|
+
entryPath: CLI_ENTRY,
|
|
12913
|
+
dryRun: setup.dryRun,
|
|
12914
|
+
uninstall: setup.uninstall,
|
|
12915
|
+
noGit: setup.noGit,
|
|
12916
|
+
noProbe: setup.noProbe,
|
|
12917
|
+
...setup.settings !== void 0 ? { settingsPath: setup.settings } : {}
|
|
12918
|
+
});
|
|
12919
|
+
} catch (error) {
|
|
12920
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
12921
|
+
return 1;
|
|
12922
|
+
}
|
|
12923
|
+
}
|
|
12203
12924
|
if (command === "login") return runLogin(rest);
|
|
12204
12925
|
if (command === "logout") return runLogout();
|
|
12205
12926
|
if (command === "whoami") return runWhoami();
|
|
@@ -12218,7 +12939,7 @@ async function main(argv) {
|
|
|
12218
12939
|
console.error(`mcp takes no options, got: ${rest.join(" ")}`);
|
|
12219
12940
|
return 1;
|
|
12220
12941
|
}
|
|
12221
|
-
return runMcpServer(defaultMcpDeps(storeDir()));
|
|
12942
|
+
return runMcpServer(defaultMcpDeps(storeDir(), bucketDir()));
|
|
12222
12943
|
}
|
|
12223
12944
|
if (command === "pull") {
|
|
12224
12945
|
let pullArgs;
|
|
@@ -12251,10 +12972,11 @@ async function main(argv) {
|
|
|
12251
12972
|
noReview: shareArgs.noReview,
|
|
12252
12973
|
publish: shareArgs.publish,
|
|
12253
12974
|
...shareArgs.prUrl !== void 0 ? { prUrl: shareArgs.prUrl } : {},
|
|
12975
|
+
...shareArgs.title !== void 0 ? { title: shareArgs.title } : {},
|
|
12976
|
+
...shareArgs.commit !== void 0 ? { commit: shareArgs.commit } : {},
|
|
12254
12977
|
...storeDir() !== null ? { storeDir: storeDir() } : {},
|
|
12255
12978
|
openBrowser: shareArgs.open && !shareArgs.noReview,
|
|
12256
|
-
outputPath: SHARE_OUTPUT_PATH
|
|
12257
|
-
parseOutputPath: OUTPUT_PATH
|
|
12979
|
+
outputPath: SHARE_OUTPUT_PATH
|
|
12258
12980
|
});
|
|
12259
12981
|
return result.exitCode;
|
|
12260
12982
|
}
|
|
@@ -12272,9 +12994,9 @@ var invokedDirectly = (() => {
|
|
|
12272
12994
|
if (entry === void 0) return false;
|
|
12273
12995
|
const real = (file) => {
|
|
12274
12996
|
try {
|
|
12275
|
-
return
|
|
12997
|
+
return fs21.realpathSync(file);
|
|
12276
12998
|
} catch {
|
|
12277
|
-
return
|
|
12999
|
+
return path20.resolve(file);
|
|
12278
13000
|
}
|
|
12279
13001
|
};
|
|
12280
13002
|
return real(entry) === real(fileURLToPath(import.meta.url));
|
|
@@ -12293,8 +13015,13 @@ export {
|
|
|
12293
13015
|
TMP_DIR,
|
|
12294
13016
|
main,
|
|
12295
13017
|
openableUrl,
|
|
13018
|
+
parseCaptureArgv,
|
|
12296
13019
|
parsePullArgv,
|
|
13020
|
+
parseSearchArgv,
|
|
13021
|
+
parseSetupArgv,
|
|
13022
|
+
runCapture,
|
|
12297
13023
|
runLogin,
|
|
12298
13024
|
runLogout,
|
|
13025
|
+
runSearch,
|
|
12299
13026
|
runWhoami
|
|
12300
13027
|
};
|