flatbread-cli 0.4.0 → 0.5.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 +10 -2
- package/dist/flatbread.mjs +559 -118
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -168,8 +168,16 @@ reload the VS Code window) afterwards.
|
|
|
168
168
|
A connected tool is not a used one: in a real session the agent read the tool list and implemented
|
|
169
169
|
the fix without searching. So setup also writes a skill that says when to search - before the first
|
|
170
170
|
edit, after a failure - to `~/.claude/skills/flatbread/SKILL.md` (or under `$CLAUDE_CONFIG_DIR`) and
|
|
171
|
-
`~/.agents/skills/flatbread/SKILL.md`, the shared skills folder Codex
|
|
172
|
-
|
|
171
|
+
`~/.agents/skills/flatbread/SKILL.md`, the shared skills folder Codex, Cursor and Gemini CLI read.
|
|
172
|
+
Where Antigravity is installed it also goes inside a plugin
|
|
173
|
+
(`~/.gemini/config/plugins/flatbread/`, with its `plugin.json`), and where OpenCode is installed to
|
|
174
|
+
`~/.config/opencode/skills/flatbread/SKILL.md`. A file there that flatbread did not write is left alone.
|
|
175
|
+
|
|
176
|
+
The other agents get the MCP server too, each only if it is installed (its folder exists): Codex as a
|
|
177
|
+
`[mcp_servers.flatbread]` table in `~/.codex/config.toml` (or `$CODEX_HOME`), Cursor in
|
|
178
|
+
`~/.cursor/mcp.json`, Antigravity in `~/.gemini/config/mcp_config.json`. Same rules as Claude Code's
|
|
179
|
+
file, and in the TOML every other line, comment and table stays where it was. `--no-mcp` skips every
|
|
180
|
+
server and every skill.
|
|
173
181
|
|
|
174
182
|
`setup --dry-run` writes nothing. `setup --uninstall` removes the handlers, the MCP entry, the skill and
|
|
175
183
|
the hook, and puts a chained hook back byte for byte. `FLATBREAD_HOOK_DISABLED=1` stops every capture.
|
package/dist/flatbread.mjs
CHANGED
|
@@ -7,9 +7,9 @@ var __export = (target, all) => {
|
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
9
|
import { spawn as spawn3 } from "node:child_process";
|
|
10
|
-
import
|
|
10
|
+
import fs26 from "node:fs";
|
|
11
11
|
import os9 from "node:os";
|
|
12
|
-
import
|
|
12
|
+
import path25 from "node:path";
|
|
13
13
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
14
14
|
|
|
15
15
|
// ../../node_modules/zod/v3/external.js
|
|
@@ -490,8 +490,8 @@ function getErrorMap() {
|
|
|
490
490
|
|
|
491
491
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
492
492
|
var makeIssue = (params) => {
|
|
493
|
-
const { data, path:
|
|
494
|
-
const fullPath = [...
|
|
493
|
+
const { data, path: path26, errorMaps, issueData } = params;
|
|
494
|
+
const fullPath = [...path26, ...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, path26, key) {
|
|
611
611
|
this._cachedPath = [];
|
|
612
612
|
this.parent = parent;
|
|
613
613
|
this.data = value;
|
|
614
|
-
this._path =
|
|
614
|
+
this._path = path26;
|
|
615
615
|
this._key = key;
|
|
616
616
|
}
|
|
617
617
|
get path() {
|
|
@@ -4461,8 +4461,8 @@ function firstLine(text2) {
|
|
|
4461
4461
|
function labelOf(step, cwd) {
|
|
4462
4462
|
const tool = step.tool;
|
|
4463
4463
|
if (tool === void 0) return step.type;
|
|
4464
|
-
const
|
|
4465
|
-
if (
|
|
4464
|
+
const path26 = firstString(tool.input, FILE_PATH_KEYS);
|
|
4465
|
+
if (path26 !== void 0) return `${tool.name} ${repoRelative(path26, cwd)}`;
|
|
4466
4466
|
const other = firstString(tool.input, TARGET_KEYS);
|
|
4467
4467
|
if (other !== void 0) return cut(`${tool.name}: ${repoRelative(firstLine(other), cwd)}`, BRIEF_LIMITS.quote);
|
|
4468
4468
|
return tool.name;
|
|
@@ -4714,9 +4714,9 @@ function runDiagnostics(trace) {
|
|
|
4714
4714
|
searches.set(key, found);
|
|
4715
4715
|
}
|
|
4716
4716
|
}
|
|
4717
|
-
for (const [
|
|
4717
|
+
for (const [path26, entries] of reads) {
|
|
4718
4718
|
const hit = densest(entries, WASTE_RULES.repeatedRead.times, WASTE_RULES.repeatedRead.windowMinutes * 6e4);
|
|
4719
|
-
if (hit) waste.push({ kind: "repeated-read", path:
|
|
4719
|
+
if (hit) waste.push({ kind: "repeated-read", path: path26, steps: hit, windowMinutes: WASTE_RULES.repeatedRead.windowMinutes });
|
|
4720
4720
|
}
|
|
4721
4721
|
for (const { label, entries } of searches.values()) {
|
|
4722
4722
|
const hit = densest(entries, WASTE_RULES.repeatedSearch.times, WASTE_RULES.repeatedSearch.windowMinutes * 6e4);
|
|
@@ -4795,19 +4795,19 @@ function filesOf(steps, cwd) {
|
|
|
4795
4795
|
const patches = [];
|
|
4796
4796
|
steps.forEach((step, index) => {
|
|
4797
4797
|
if (!isSuccessfulEdit(step) || step.tool === void 0) return;
|
|
4798
|
-
const
|
|
4799
|
-
const file = files.get(
|
|
4798
|
+
const path26 = repoRelative(firstString(step.tool.input, FILE_PATH_KEYS), cwd);
|
|
4799
|
+
const file = files.get(path26) ?? { path: path26 };
|
|
4800
4800
|
if (step.diff !== void 0) {
|
|
4801
4801
|
const counted = countPatch(step.diff.patch);
|
|
4802
4802
|
file.added = (file.added ?? 0) + counted.added;
|
|
4803
4803
|
file.removed = (file.removed ?? 0) + counted.removed;
|
|
4804
|
-
patches.push({ path:
|
|
4804
|
+
patches.push({ path: path26, patch: repoRelative(step.diff.patch, cwd), step: step.stepNumber });
|
|
4805
4805
|
}
|
|
4806
4806
|
const result = resultOf(steps, index);
|
|
4807
4807
|
if (step.tool.name.toLowerCase() === "write" && result !== void 0 && /^File created successfully\b/.test(result.trim())) {
|
|
4808
4808
|
file.created = true;
|
|
4809
4809
|
}
|
|
4810
|
-
files.set(
|
|
4810
|
+
files.set(path26, file);
|
|
4811
4811
|
});
|
|
4812
4812
|
return { files: [...files.values()], patches };
|
|
4813
4813
|
}
|
|
@@ -5120,13 +5120,13 @@ function categorizeTrace(trace) {
|
|
|
5120
5120
|
};
|
|
5121
5121
|
}
|
|
5122
5122
|
if (paths.length > 0) {
|
|
5123
|
-
if (paths.every((
|
|
5123
|
+
if (paths.every((path26) => matchesAny(path26, DOC_PATTERNS))) {
|
|
5124
5124
|
return { category: "docs", evidence: [`all ${paths.length} touched path(s) are documentation`, ...paths.slice(0, 3)] };
|
|
5125
5125
|
}
|
|
5126
|
-
if (paths.every((
|
|
5126
|
+
if (paths.every((path26) => matchesAny(path26, TEST_PATTERNS))) {
|
|
5127
5127
|
return { category: "tests", evidence: [`all ${paths.length} touched path(s) are tests`, ...paths.slice(0, 3)] };
|
|
5128
5128
|
}
|
|
5129
|
-
if (paths.every((
|
|
5129
|
+
if (paths.every((path26) => matchesAny(path26, INFRA_PATTERNS))) {
|
|
5130
5130
|
return { category: "infra", evidence: [`all ${paths.length} touched path(s) are build/CI/deploy config`, ...paths.slice(0, 3)] };
|
|
5131
5131
|
}
|
|
5132
5132
|
}
|
|
@@ -11236,7 +11236,6 @@ async function searchRemote(query, options = {}) {
|
|
|
11236
11236
|
if (query.query) params.set("q", query.query);
|
|
11237
11237
|
if (query.harness) params.set("harness", query.harness);
|
|
11238
11238
|
if (query.category) params.set("category", query.category);
|
|
11239
|
-
if (query.onlyWithRecoveries) params.set("recoveries", "1");
|
|
11240
11239
|
const answer = await get(`${apiUrl}/api/search?${params.toString()}`, headersFor(apiUrl, options, "application/json"), options);
|
|
11241
11240
|
if (!answer.ok) return answer;
|
|
11242
11241
|
if (answer.status !== 200) return failureOf(answer.status, answer.body);
|
|
@@ -11357,7 +11356,6 @@ function readBucketRecord(dir, slug) {
|
|
|
11357
11356
|
function matchesQuery(summary, query) {
|
|
11358
11357
|
if (query.harness !== void 0 && summary.harness !== query.harness) return false;
|
|
11359
11358
|
if (query.category !== void 0 && summary.category !== query.category) return false;
|
|
11360
|
-
if (query.onlyWithRecoveries === true && summary.failuresRecovered === 0) return false;
|
|
11361
11359
|
return matchesSearch(summary, query.query ?? "");
|
|
11362
11360
|
}
|
|
11363
11361
|
async function captureToBucket(options) {
|
|
@@ -11422,7 +11420,7 @@ function logCapture(dir, line, now = (/* @__PURE__ */ new Date()).toISOString())
|
|
|
11422
11420
|
|
|
11423
11421
|
// src/mcp.ts
|
|
11424
11422
|
var MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
11425
|
-
var MCP_SERVER_VERSION = "0.
|
|
11423
|
+
var MCP_SERVER_VERSION = "0.5.0";
|
|
11426
11424
|
function defaultMcpDeps(storeDir2, bucketDir2) {
|
|
11427
11425
|
return {
|
|
11428
11426
|
storeDir: storeDir2,
|
|
@@ -11441,8 +11439,7 @@ var MCP_TOOLS = [
|
|
|
11441
11439
|
properties: {
|
|
11442
11440
|
query: { type: "string", description: "Words that must all appear in the run's title or ID" },
|
|
11443
11441
|
harness: { type: "string", enum: [...HARNESS_IDS], description: "Only runs from this agent harness" },
|
|
11444
|
-
category: { type: "string", enum: [...TASK_CATEGORIES], description: "Only runs of this kind of job" }
|
|
11445
|
-
onlyWithRecoveries: { type: "boolean", description: "Only runs where the agent recovered from a failure" }
|
|
11442
|
+
category: { type: "string", enum: [...TASK_CATEGORIES], description: "Only runs of this kind of job" }
|
|
11446
11443
|
},
|
|
11447
11444
|
additionalProperties: false
|
|
11448
11445
|
}
|
|
@@ -11489,10 +11486,7 @@ function readQuery(args) {
|
|
|
11489
11486
|
}
|
|
11490
11487
|
query.category = args["category"];
|
|
11491
11488
|
}
|
|
11492
|
-
if (args["onlyWithRecoveries"] !== void 0)
|
|
11493
|
-
if (typeof args["onlyWithRecoveries"] !== "boolean") return "onlyWithRecoveries must be true or false";
|
|
11494
|
-
query.onlyWithRecoveries = args["onlyWithRecoveries"];
|
|
11495
|
-
}
|
|
11489
|
+
if (args["onlyWithRecoveries"] !== void 0) return "onlyWithRecoveries was removed: search by words, harness and category";
|
|
11496
11490
|
return query;
|
|
11497
11491
|
}
|
|
11498
11492
|
function localRecords(deps) {
|
|
@@ -11641,6 +11635,10 @@ async function runMcpServer(deps, io) {
|
|
|
11641
11635
|
return 0;
|
|
11642
11636
|
}
|
|
11643
11637
|
|
|
11638
|
+
// src/setup.ts
|
|
11639
|
+
import fs25 from "node:fs";
|
|
11640
|
+
import path24 from "node:path";
|
|
11641
|
+
|
|
11644
11642
|
// src/claude-mcp.ts
|
|
11645
11643
|
import fs20 from "node:fs";
|
|
11646
11644
|
import os8 from "node:os";
|
|
@@ -11694,7 +11692,7 @@ function write(file, read2, next) {
|
|
|
11694
11692
|
} catch {
|
|
11695
11693
|
return `${file} is read-only, so it was left as it was`;
|
|
11696
11694
|
}
|
|
11697
|
-
if (fs20.readFileSync(file, "utf8") !== read2.raw) return `${file} changed while setup ran
|
|
11695
|
+
if (fs20.readFileSync(file, "utf8") !== read2.raw) return `${file} changed while setup ran; run setup again`;
|
|
11698
11696
|
const backup = `${file}.flatbread.bak`;
|
|
11699
11697
|
if (!fs20.existsSync(backup)) fs20.copyFileSync(file, backup);
|
|
11700
11698
|
}
|
|
@@ -11706,12 +11704,22 @@ function write(file, read2, next) {
|
|
|
11706
11704
|
return null;
|
|
11707
11705
|
}
|
|
11708
11706
|
function registerClaudeMcp(options) {
|
|
11707
|
+
return registerMcpJson({
|
|
11708
|
+
...options,
|
|
11709
|
+
entry: { ...mcpEntryFor(options.execPath, options.entryPath) },
|
|
11710
|
+
app: "Claude Code",
|
|
11711
|
+
scope: "for every project (user scope)",
|
|
11712
|
+
restart: "restart Claude Code (or reload the VS Code window) so it starts the server",
|
|
11713
|
+
byHand: `claude mcp add --scope user ${MCP_SERVER_NAME} -- "${options.execPath}" "${options.entryPath}" mcp`
|
|
11714
|
+
});
|
|
11715
|
+
}
|
|
11716
|
+
function registerMcpJson(options) {
|
|
11709
11717
|
const { file, log } = options;
|
|
11710
11718
|
const read2 = readConfig(file);
|
|
11711
11719
|
if ("problem" in read2) {
|
|
11712
11720
|
log(` mcp refused: ${file}: ${read2.problem}`);
|
|
11713
|
-
log(
|
|
11714
|
-
if (options.uninstall !== true) log(` by hand:
|
|
11721
|
+
log(` ${options.app}'s own config lives there, so a file we cannot read is not one we rewrite.`);
|
|
11722
|
+
if (options.uninstall !== true && options.byHand !== void 0) log(` by hand: ${options.byHand}`);
|
|
11715
11723
|
return 1;
|
|
11716
11724
|
}
|
|
11717
11725
|
const servers = isObject(read2.root["mcpServers"]) ? read2.root["mcpServers"] : void 0;
|
|
@@ -11741,13 +11749,13 @@ function registerClaudeMcp(options) {
|
|
|
11741
11749
|
if (fs20.existsSync(backup)) log(` ${backup} is the copy setup took before its first write; delete it when you like`);
|
|
11742
11750
|
return 0;
|
|
11743
11751
|
}
|
|
11744
|
-
const entry =
|
|
11752
|
+
const entry = options.entry;
|
|
11745
11753
|
if (current !== void 0 && !runsFlatbreadMcp(current)) {
|
|
11746
11754
|
log(` mcp refused: ${file} already has a server named ${MCP_SERVER_NAME} that does not run flatbread, left alone`);
|
|
11747
11755
|
return 1;
|
|
11748
11756
|
}
|
|
11749
11757
|
if (JSON.stringify(current) === JSON.stringify(entry)) {
|
|
11750
|
-
log(` mcp ${file}: registered
|
|
11758
|
+
log(` mcp ${file}: registered ${options.scope}, unchanged`);
|
|
11751
11759
|
return 0;
|
|
11752
11760
|
}
|
|
11753
11761
|
const verb = current === void 0 ? "register" : "update";
|
|
@@ -11761,14 +11769,367 @@ function registerClaudeMcp(options) {
|
|
|
11761
11769
|
log(` mcp refused: ${problem}`);
|
|
11762
11770
|
return 1;
|
|
11763
11771
|
}
|
|
11764
|
-
log(` mcp ${file}: ${verb === "register" ? "registered" : "updated"}
|
|
11765
|
-
log(
|
|
11772
|
+
log(` mcp ${file}: ${verb === "register" ? "registered" : "updated"} ${options.scope}`);
|
|
11773
|
+
log(` ${options.restart}`);
|
|
11766
11774
|
return 0;
|
|
11767
11775
|
}
|
|
11768
11776
|
|
|
11769
|
-
// src/
|
|
11777
|
+
// src/codex-mcp.ts
|
|
11770
11778
|
import fs21 from "node:fs";
|
|
11771
11779
|
import path20 from "node:path";
|
|
11780
|
+
var BOM2 = "\uFEFF";
|
|
11781
|
+
var OWNED_KEYS = /* @__PURE__ */ new Set(["type", "command", "args"]);
|
|
11782
|
+
function codexConfigPath(codexHome2) {
|
|
11783
|
+
return path20.join(codexHome2, "config.toml");
|
|
11784
|
+
}
|
|
11785
|
+
function quote(value) {
|
|
11786
|
+
return JSON.stringify(value);
|
|
11787
|
+
}
|
|
11788
|
+
function codexBlock(execPath, entryPath) {
|
|
11789
|
+
return `[mcp_servers.${MCP_SERVER_NAME}]
|
|
11790
|
+
type = "stdio"
|
|
11791
|
+
command = ${quote(execPath)}
|
|
11792
|
+
args = [${[entryPath, "mcp"].map(quote).join(", ")}]
|
|
11793
|
+
`;
|
|
11794
|
+
}
|
|
11795
|
+
function tomlCode(line) {
|
|
11796
|
+
let out = "";
|
|
11797
|
+
let inQuote = "";
|
|
11798
|
+
let escaped = false;
|
|
11799
|
+
for (const c of line) {
|
|
11800
|
+
if (inQuote !== "") {
|
|
11801
|
+
out += c;
|
|
11802
|
+
if (inQuote === '"' && escaped) escaped = false;
|
|
11803
|
+
else if (inQuote === '"' && c === "\\") escaped = true;
|
|
11804
|
+
else if (c === inQuote) inQuote = "";
|
|
11805
|
+
continue;
|
|
11806
|
+
}
|
|
11807
|
+
if (c === "#") break;
|
|
11808
|
+
if (c === '"' || c === "'") inQuote = c;
|
|
11809
|
+
out += c;
|
|
11810
|
+
}
|
|
11811
|
+
return out.trim();
|
|
11812
|
+
}
|
|
11813
|
+
function openAfter(line, open) {
|
|
11814
|
+
let i = 0;
|
|
11815
|
+
while (i < line.length) {
|
|
11816
|
+
if (open === '"""') {
|
|
11817
|
+
if (line[i] === "\\") {
|
|
11818
|
+
i += 2;
|
|
11819
|
+
continue;
|
|
11820
|
+
}
|
|
11821
|
+
if (line.startsWith('"""', i)) {
|
|
11822
|
+
i += 3;
|
|
11823
|
+
while (line[i] === '"') i++;
|
|
11824
|
+
open = "";
|
|
11825
|
+
continue;
|
|
11826
|
+
}
|
|
11827
|
+
i++;
|
|
11828
|
+
} else if (open === "'''") {
|
|
11829
|
+
const j = line.indexOf("'''", i);
|
|
11830
|
+
if (j < 0) return open;
|
|
11831
|
+
i = j + 3;
|
|
11832
|
+
while (line[i] === "'") i++;
|
|
11833
|
+
open = "";
|
|
11834
|
+
} else if (line.startsWith('"""', i)) {
|
|
11835
|
+
open = '"""';
|
|
11836
|
+
i += 3;
|
|
11837
|
+
} else if (line.startsWith("'''", i)) {
|
|
11838
|
+
open = "'''";
|
|
11839
|
+
i += 3;
|
|
11840
|
+
} else if (line[i] === "#") {
|
|
11841
|
+
return "";
|
|
11842
|
+
} else if (line[i] === '"') {
|
|
11843
|
+
i++;
|
|
11844
|
+
while (i < line.length && line[i] !== '"') i += line[i] === "\\" ? 2 : 1;
|
|
11845
|
+
i++;
|
|
11846
|
+
} else if (line[i] === "'") {
|
|
11847
|
+
const j = line.indexOf("'", i + 1);
|
|
11848
|
+
if (j < 0) return "";
|
|
11849
|
+
i = j + 1;
|
|
11850
|
+
} else {
|
|
11851
|
+
i++;
|
|
11852
|
+
}
|
|
11853
|
+
}
|
|
11854
|
+
return open;
|
|
11855
|
+
}
|
|
11856
|
+
function tomlCodeLines(text2) {
|
|
11857
|
+
let open = "";
|
|
11858
|
+
return text2.split("\n").map((line) => {
|
|
11859
|
+
const was = open;
|
|
11860
|
+
open = openAfter(line, open);
|
|
11861
|
+
return was !== "" ? "" : line;
|
|
11862
|
+
});
|
|
11863
|
+
}
|
|
11864
|
+
function tomlKeyPath(key) {
|
|
11865
|
+
const parts = [];
|
|
11866
|
+
let part = "";
|
|
11867
|
+
let inQuote = "";
|
|
11868
|
+
for (const c of key) {
|
|
11869
|
+
if (inQuote !== "") {
|
|
11870
|
+
part += c;
|
|
11871
|
+
if (c === inQuote) inQuote = "";
|
|
11872
|
+
continue;
|
|
11873
|
+
}
|
|
11874
|
+
if (c === '"' || c === "'") {
|
|
11875
|
+
inQuote = c;
|
|
11876
|
+
part += c;
|
|
11877
|
+
} else if (c === ".") {
|
|
11878
|
+
parts.push(part);
|
|
11879
|
+
part = "";
|
|
11880
|
+
} else {
|
|
11881
|
+
part += c;
|
|
11882
|
+
}
|
|
11883
|
+
}
|
|
11884
|
+
parts.push(part);
|
|
11885
|
+
return parts.map((p) => {
|
|
11886
|
+
const t = p.trim();
|
|
11887
|
+
if (t.length >= 2 && (t[0] === '"' || t[0] === "'") && t[t.length - 1] === t[0]) {
|
|
11888
|
+
if (t[0] === "'") return t.slice(1, -1);
|
|
11889
|
+
try {
|
|
11890
|
+
return JSON.parse(t);
|
|
11891
|
+
} catch {
|
|
11892
|
+
return t;
|
|
11893
|
+
}
|
|
11894
|
+
}
|
|
11895
|
+
return t;
|
|
11896
|
+
});
|
|
11897
|
+
}
|
|
11898
|
+
function startsWithPath(full, prefix) {
|
|
11899
|
+
return prefix.length <= full.length && prefix.every((part, i) => full[i] === part);
|
|
11900
|
+
}
|
|
11901
|
+
function headerPath(line) {
|
|
11902
|
+
const code = tomlCode(line);
|
|
11903
|
+
if (!code.startsWith("[") || code.startsWith("[[") || !code.endsWith("]")) return null;
|
|
11904
|
+
return tomlKeyPath(code.slice(1, -1));
|
|
11905
|
+
}
|
|
11906
|
+
function arrayDelta(text2) {
|
|
11907
|
+
let depth = 0;
|
|
11908
|
+
let inQuote = "";
|
|
11909
|
+
let escaped = false;
|
|
11910
|
+
for (const c of text2) {
|
|
11911
|
+
if (inQuote !== "") {
|
|
11912
|
+
if (inQuote === '"' && escaped) escaped = false;
|
|
11913
|
+
else if (inQuote === '"' && c === "\\") escaped = true;
|
|
11914
|
+
else if (c === inQuote) inQuote = "";
|
|
11915
|
+
continue;
|
|
11916
|
+
}
|
|
11917
|
+
if (c === '"' || c === "'") inQuote = c;
|
|
11918
|
+
else if (c === "[") depth++;
|
|
11919
|
+
else if (c === "]") depth--;
|
|
11920
|
+
}
|
|
11921
|
+
return depth;
|
|
11922
|
+
}
|
|
11923
|
+
function stringValues(text2) {
|
|
11924
|
+
const out = [];
|
|
11925
|
+
const re = /"((?:[^"\\]|\\.)*)"|'([^']*)'/g;
|
|
11926
|
+
for (const match of text2.matchAll(re)) {
|
|
11927
|
+
if (match[2] !== void 0) out.push(match[2]);
|
|
11928
|
+
else {
|
|
11929
|
+
try {
|
|
11930
|
+
out.push(JSON.parse(`"${match[1] ?? ""}"`));
|
|
11931
|
+
} catch {
|
|
11932
|
+
}
|
|
11933
|
+
}
|
|
11934
|
+
}
|
|
11935
|
+
return out;
|
|
11936
|
+
}
|
|
11937
|
+
var OURS = ["mcp_servers", MCP_SERVER_NAME];
|
|
11938
|
+
function ourBlocks(code) {
|
|
11939
|
+
const all = [];
|
|
11940
|
+
let main2 = null;
|
|
11941
|
+
code.forEach((line, i) => {
|
|
11942
|
+
const header = headerPath(line);
|
|
11943
|
+
if (header === null || !startsWithPath(header, OURS)) return;
|
|
11944
|
+
let end = code.length;
|
|
11945
|
+
for (let j = i + 1; j < code.length; j++) {
|
|
11946
|
+
if (tomlCode(code[j] ?? "").startsWith("[")) {
|
|
11947
|
+
end = j;
|
|
11948
|
+
break;
|
|
11949
|
+
}
|
|
11950
|
+
}
|
|
11951
|
+
const block = { start: i, end };
|
|
11952
|
+
all.push(block);
|
|
11953
|
+
if (header.length === OURS.length && main2 === null) main2 = block;
|
|
11954
|
+
});
|
|
11955
|
+
return { main: main2, all };
|
|
11956
|
+
}
|
|
11957
|
+
function blockRunsFlatbread(code, block) {
|
|
11958
|
+
const words = [];
|
|
11959
|
+
for (let i = block.start + 1; i < block.end; i++) {
|
|
11960
|
+
const line = tomlCode(code[i] ?? "");
|
|
11961
|
+
const at = line.indexOf("=");
|
|
11962
|
+
if (at < 0) continue;
|
|
11963
|
+
const key = line.slice(0, at).trim();
|
|
11964
|
+
let value = line.slice(at + 1).trim();
|
|
11965
|
+
if (key === "command") words.unshift(...stringValues(value));
|
|
11966
|
+
if (key === "args") {
|
|
11967
|
+
let depth = arrayDelta(value);
|
|
11968
|
+
while (depth > 0 && i + 1 < block.end) {
|
|
11969
|
+
i++;
|
|
11970
|
+
const next = tomlCode(code[i] ?? "");
|
|
11971
|
+
value += `
|
|
11972
|
+
${next}`;
|
|
11973
|
+
depth += arrayDelta(next);
|
|
11974
|
+
}
|
|
11975
|
+
words.push(...stringValues(value));
|
|
11976
|
+
}
|
|
11977
|
+
}
|
|
11978
|
+
return words[words.length - 1] === "mcp" && words.some((word) => /flatbread/i.test(word));
|
|
11979
|
+
}
|
|
11980
|
+
function tomlProblem(code) {
|
|
11981
|
+
let table = [];
|
|
11982
|
+
for (const [i, line] of code.entries()) {
|
|
11983
|
+
const text2 = tomlCode(line);
|
|
11984
|
+
if (text2.startsWith("[")) {
|
|
11985
|
+
if (!text2.endsWith("]")) return `line ${i + 1} is a table header that never closes: ${line.trim()}`;
|
|
11986
|
+
table = text2.startsWith("[[") ? tomlKeyPath(text2.slice(2, -2)) : tomlKeyPath(text2.slice(1, -1));
|
|
11987
|
+
continue;
|
|
11988
|
+
}
|
|
11989
|
+
const at = text2.indexOf("=");
|
|
11990
|
+
if (at < 0 || startsWithPath(table, OURS)) continue;
|
|
11991
|
+
const full = [...table, ...tomlKeyPath(text2.slice(0, at))];
|
|
11992
|
+
if (startsWithPath(OURS, full) || startsWithPath(full, OURS)) {
|
|
11993
|
+
return `line ${i + 1} sets ${full.join(".")} inline, and the [mcp_servers.${MCP_SERVER_NAME}] table would define it twice`;
|
|
11994
|
+
}
|
|
11995
|
+
}
|
|
11996
|
+
return null;
|
|
11997
|
+
}
|
|
11998
|
+
function writeLike(file, raw, lf) {
|
|
11999
|
+
let next = lf;
|
|
12000
|
+
if (raw !== null && raw.includes("\r\n")) next = next.replace(/\n/g, "\r\n");
|
|
12001
|
+
if (raw !== null && raw.startsWith(BOM2)) next = BOM2 + next;
|
|
12002
|
+
if (raw !== null) {
|
|
12003
|
+
try {
|
|
12004
|
+
fs21.accessSync(file, fs21.constants.W_OK);
|
|
12005
|
+
} catch {
|
|
12006
|
+
return `${file} is read-only, so it was left as it was`;
|
|
12007
|
+
}
|
|
12008
|
+
if (fs21.readFileSync(file, "utf8") !== raw) return `${file} changed while setup ran; run setup again`;
|
|
12009
|
+
const backup = `${file}.flatbread.bak`;
|
|
12010
|
+
if (!fs21.existsSync(backup)) fs21.copyFileSync(file, backup);
|
|
12011
|
+
}
|
|
12012
|
+
const mode = raw !== null ? fs21.statSync(file).mode & 511 : 384;
|
|
12013
|
+
fs21.mkdirSync(path20.dirname(file), { recursive: true });
|
|
12014
|
+
const temp = `${file}.flatbread-${process.pid}.tmp`;
|
|
12015
|
+
fs21.writeFileSync(temp, next, { encoding: "utf8", mode });
|
|
12016
|
+
fs21.renameSync(temp, file);
|
|
12017
|
+
return null;
|
|
12018
|
+
}
|
|
12019
|
+
function removeBlocks(lines2, blocks) {
|
|
12020
|
+
const drop = /* @__PURE__ */ new Set();
|
|
12021
|
+
for (const block of blocks) for (let i = block.start; i < block.end; i++) drop.add(i);
|
|
12022
|
+
return lines2.filter((_, i) => !drop.has(i));
|
|
12023
|
+
}
|
|
12024
|
+
function tidy(lines2) {
|
|
12025
|
+
const text2 = lines2.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "");
|
|
12026
|
+
return text2 === "" ? "" : `${text2}
|
|
12027
|
+
`;
|
|
12028
|
+
}
|
|
12029
|
+
function registerCodexMcp(options) {
|
|
12030
|
+
const { file, log } = options;
|
|
12031
|
+
let raw;
|
|
12032
|
+
try {
|
|
12033
|
+
raw = fs21.readFileSync(file, "utf8");
|
|
12034
|
+
} catch (error) {
|
|
12035
|
+
if (error.code !== "ENOENT") {
|
|
12036
|
+
log(` mcp refused: ${file}: ${error.message}`);
|
|
12037
|
+
return 1;
|
|
12038
|
+
}
|
|
12039
|
+
raw = null;
|
|
12040
|
+
}
|
|
12041
|
+
const text2 = (raw ?? "").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
12042
|
+
const lines2 = text2.split("\n");
|
|
12043
|
+
const code = tomlCodeLines(text2);
|
|
12044
|
+
const { main: main2, all } = ourBlocks(code);
|
|
12045
|
+
if (options.uninstall === true) {
|
|
12046
|
+
if (main2 === null) {
|
|
12047
|
+
log(` mcp no ${MCP_SERVER_NAME} server in ${file}`);
|
|
12048
|
+
return 0;
|
|
12049
|
+
}
|
|
12050
|
+
if (!blockRunsFlatbread(code, main2)) {
|
|
12051
|
+
log(` mcp ${file}: the ${MCP_SERVER_NAME} server there does not run flatbread, left alone`);
|
|
12052
|
+
return 0;
|
|
12053
|
+
}
|
|
12054
|
+
if (options.dryRun === true) {
|
|
12055
|
+
log(` mcp ${file}: would remove the ${MCP_SERVER_NAME} server`);
|
|
12056
|
+
return 0;
|
|
12057
|
+
}
|
|
12058
|
+
const problem2 = writeLike(file, raw, tidy(removeBlocks(lines2, all)));
|
|
12059
|
+
if (problem2 !== null) {
|
|
12060
|
+
log(` mcp refused: ${problem2}`);
|
|
12061
|
+
return 1;
|
|
12062
|
+
}
|
|
12063
|
+
log(` mcp ${file}: removed the ${MCP_SERVER_NAME} server`);
|
|
12064
|
+
return 0;
|
|
12065
|
+
}
|
|
12066
|
+
const problem = tomlProblem(code);
|
|
12067
|
+
if (problem !== null) {
|
|
12068
|
+
log(` mcp refused: ${file}: ${problem}`);
|
|
12069
|
+
log(" fix that line, or add this table by hand:");
|
|
12070
|
+
for (const line of codexBlock(options.execPath, options.entryPath).trimEnd().split("\n")) log(` ${line}`);
|
|
12071
|
+
return 1;
|
|
12072
|
+
}
|
|
12073
|
+
if (main2 !== null && !blockRunsFlatbread(code, main2)) {
|
|
12074
|
+
log(` mcp refused: ${file} already has a server named ${MCP_SERVER_NAME} that does not run flatbread, left alone`);
|
|
12075
|
+
return 1;
|
|
12076
|
+
}
|
|
12077
|
+
const block = codexBlock(options.execPath, options.entryPath);
|
|
12078
|
+
const ownLines = block.trimEnd().split("\n").slice(1);
|
|
12079
|
+
let next;
|
|
12080
|
+
if (main2 === null) {
|
|
12081
|
+
const kept = text2.replace(/\n+$/, "");
|
|
12082
|
+
next = `${kept}${kept === "" ? "" : "\n\n"}${block}`;
|
|
12083
|
+
} else {
|
|
12084
|
+
const body = [];
|
|
12085
|
+
let inserted = false;
|
|
12086
|
+
for (let i = main2.start + 1; i < main2.end; i++) {
|
|
12087
|
+
const lineCode = tomlCode(code[i] ?? "");
|
|
12088
|
+
const at = lineCode.indexOf("=");
|
|
12089
|
+
const key = at >= 0 ? lineCode.slice(0, at).trim() : "";
|
|
12090
|
+
if (!OWNED_KEYS.has(key)) {
|
|
12091
|
+
body.push(lines2[i] ?? "");
|
|
12092
|
+
continue;
|
|
12093
|
+
}
|
|
12094
|
+
if (!inserted) {
|
|
12095
|
+
body.push(...ownLines);
|
|
12096
|
+
inserted = true;
|
|
12097
|
+
}
|
|
12098
|
+
if (key === "args") {
|
|
12099
|
+
let depth = arrayDelta(lineCode.slice(at + 1));
|
|
12100
|
+
while (depth > 0 && i + 1 < main2.end) {
|
|
12101
|
+
i++;
|
|
12102
|
+
depth += arrayDelta(tomlCode(code[i] ?? ""));
|
|
12103
|
+
}
|
|
12104
|
+
}
|
|
12105
|
+
}
|
|
12106
|
+
if (!inserted) body.unshift(...ownLines);
|
|
12107
|
+
const out = [...lines2.slice(0, main2.start + 1), ...body, ...lines2.slice(main2.end)];
|
|
12108
|
+
next = out.join("\n");
|
|
12109
|
+
if (!next.endsWith("\n")) next += "\n";
|
|
12110
|
+
}
|
|
12111
|
+
if (next === text2) {
|
|
12112
|
+
log(` mcp ${file}: registered for Codex, unchanged`);
|
|
12113
|
+
return 0;
|
|
12114
|
+
}
|
|
12115
|
+
const verb = main2 === null ? "register" : "update";
|
|
12116
|
+
if (options.dryRun === true) {
|
|
12117
|
+
log(` mcp ${file}: would ${verb} [mcp_servers.${MCP_SERVER_NAME}]`);
|
|
12118
|
+
return 0;
|
|
12119
|
+
}
|
|
12120
|
+
const failed = writeLike(file, raw, next);
|
|
12121
|
+
if (failed !== null) {
|
|
12122
|
+
log(` mcp refused: ${failed}`);
|
|
12123
|
+
return 1;
|
|
12124
|
+
}
|
|
12125
|
+
log(` mcp ${file}: ${verb === "register" ? "registered" : "updated"} for Codex`);
|
|
12126
|
+
log(" start a new Codex session so it starts the server");
|
|
12127
|
+
return 0;
|
|
12128
|
+
}
|
|
12129
|
+
|
|
12130
|
+
// src/git-hook.ts
|
|
12131
|
+
import fs22 from "node:fs";
|
|
12132
|
+
import path21 from "node:path";
|
|
11772
12133
|
var POST_COMMIT_MARKER = "# flatbread post-commit hook - remove with: flatbread setup --uninstall";
|
|
11773
12134
|
var CHAINED_HOOK = "post-commit.pre-flatbread";
|
|
11774
12135
|
var sq = (value) => `'${value.replace(/\\/g, "/").replace(/'/g, "'\\''")}'`;
|
|
@@ -11812,25 +12173,25 @@ function hooksDirFor(cwd, options = {}) {
|
|
|
11812
12173
|
}
|
|
11813
12174
|
const common = git(top, ["rev-parse", "--git-common-dir"], options)?.trim();
|
|
11814
12175
|
if (common === void 0 || common.length === 0) return { ok: false, reason: "unreadable", detail: `git could not name the git directory of ${top}` };
|
|
11815
|
-
return { ok: true, repo:
|
|
12176
|
+
return { ok: true, repo: path21.resolve(top), dir: path21.resolve(top, common, "hooks"), ...machineHooksPath !== void 0 ? { machineHooksPath } : {} };
|
|
11816
12177
|
}
|
|
11817
12178
|
var read = (file) => {
|
|
11818
12179
|
try {
|
|
11819
|
-
return
|
|
12180
|
+
return fs22.readFileSync(file, "utf8");
|
|
11820
12181
|
} catch {
|
|
11821
12182
|
return null;
|
|
11822
12183
|
}
|
|
11823
12184
|
};
|
|
11824
12185
|
function installGitHook(dir, script, options = {}) {
|
|
11825
|
-
const hook =
|
|
11826
|
-
const chained =
|
|
12186
|
+
const hook = path21.join(dir, "post-commit");
|
|
12187
|
+
const chained = path21.join(dir, CHAINED_HOOK);
|
|
11827
12188
|
const current = read(hook);
|
|
11828
12189
|
const write2 = (lines2) => {
|
|
11829
12190
|
if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
|
|
11830
|
-
|
|
11831
|
-
if (current !== null && !isOurs(current))
|
|
11832
|
-
|
|
11833
|
-
|
|
12191
|
+
fs22.mkdirSync(dir, { recursive: true });
|
|
12192
|
+
if (current !== null && !isOurs(current)) fs22.renameSync(hook, chained);
|
|
12193
|
+
fs22.writeFileSync(hook, script, { encoding: "utf8", mode: 493 });
|
|
12194
|
+
fs22.chmodSync(hook, 493);
|
|
11834
12195
|
return { code: 0, lines: lines2 };
|
|
11835
12196
|
};
|
|
11836
12197
|
if (current === null) return write2([`post-commit ${hook} (new)`]);
|
|
@@ -11838,7 +12199,7 @@ function installGitHook(dir, script, options = {}) {
|
|
|
11838
12199
|
if (current === script) return { code: 0, lines: [`post-commit ${hook}`, "already installed, unchanged"] };
|
|
11839
12200
|
return write2([`post-commit ${hook}`, "updated the flatbread hook"]);
|
|
11840
12201
|
}
|
|
11841
|
-
if (
|
|
12202
|
+
if (fs22.existsSync(chained)) {
|
|
11842
12203
|
return {
|
|
11843
12204
|
code: 1,
|
|
11844
12205
|
lines: [
|
|
@@ -11854,10 +12215,10 @@ function installGitHook(dir, script, options = {}) {
|
|
|
11854
12215
|
]);
|
|
11855
12216
|
}
|
|
11856
12217
|
function uninstallGitHook(dir, options = {}) {
|
|
11857
|
-
const hook =
|
|
11858
|
-
const chained =
|
|
12218
|
+
const hook = path21.join(dir, "post-commit");
|
|
12219
|
+
const chained = path21.join(dir, CHAINED_HOOK);
|
|
11859
12220
|
const current = read(hook);
|
|
11860
|
-
const hasChained =
|
|
12221
|
+
const hasChained = fs22.existsSync(chained);
|
|
11861
12222
|
const done = (lines2, act) => {
|
|
11862
12223
|
if (options.dryRun === true) return { code: 0, lines: [...lines2, "--dry-run: nothing was written"] };
|
|
11863
12224
|
act();
|
|
@@ -11875,17 +12236,17 @@ function uninstallGitHook(dir, options = {}) {
|
|
|
11875
12236
|
if (current === null && !hasChained) return { code: 0, lines: [`no flatbread post-commit in ${dir}`] };
|
|
11876
12237
|
if (hasChained) {
|
|
11877
12238
|
return done([`post-commit ${hook}`, `restored your post-commit from ${CHAINED_HOOK}`], () => {
|
|
11878
|
-
if (current !== null)
|
|
11879
|
-
|
|
12239
|
+
if (current !== null) fs22.rmSync(hook);
|
|
12240
|
+
fs22.renameSync(chained, hook);
|
|
11880
12241
|
});
|
|
11881
12242
|
}
|
|
11882
|
-
return done([`post-commit ${hook}`, "removed the flatbread hook"], () =>
|
|
12243
|
+
return done([`post-commit ${hook}`, "removed the flatbread hook"], () => fs22.rmSync(hook));
|
|
11883
12244
|
}
|
|
11884
12245
|
|
|
11885
12246
|
// src/hook.ts
|
|
11886
12247
|
import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
|
|
11887
|
-
import
|
|
11888
|
-
import
|
|
12248
|
+
import fs23 from "node:fs";
|
|
12249
|
+
import path22 from "node:path";
|
|
11889
12250
|
var HOOK_EVENTS = ["Stop", "SessionEnd"];
|
|
11890
12251
|
var OFFER_AFTER_STOPS = 2;
|
|
11891
12252
|
var MAX_TRACKED_SESSIONS = 50;
|
|
@@ -11895,14 +12256,14 @@ function emptyHookState() {
|
|
|
11895
12256
|
return { version: HOOK_STATE_VERSION, sessions: {} };
|
|
11896
12257
|
}
|
|
11897
12258
|
function hookStatePath(env = process.env) {
|
|
11898
|
-
return
|
|
12259
|
+
return path22.join(flatbreadHome(env), "hook-state.json");
|
|
11899
12260
|
}
|
|
11900
12261
|
function readHookState(env = process.env) {
|
|
11901
12262
|
const file = hookStatePath(env);
|
|
11902
|
-
if (!
|
|
12263
|
+
if (!fs23.existsSync(file)) return { ok: true, state: emptyHookState(), present: false };
|
|
11903
12264
|
let raw;
|
|
11904
12265
|
try {
|
|
11905
|
-
raw =
|
|
12266
|
+
raw = fs23.readFileSync(file, "utf8");
|
|
11906
12267
|
} catch (error) {
|
|
11907
12268
|
return { ok: false, reason: "unreadable", detail: error.message };
|
|
11908
12269
|
}
|
|
@@ -11945,9 +12306,9 @@ function pruneHookState(state, keep = MAX_TRACKED_SESSIONS) {
|
|
|
11945
12306
|
}
|
|
11946
12307
|
function writeHookState(state, env = process.env) {
|
|
11947
12308
|
const dir = flatbreadHome(env);
|
|
11948
|
-
|
|
12309
|
+
fs23.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
11949
12310
|
const file = hookStatePath(env);
|
|
11950
|
-
|
|
12311
|
+
fs23.writeFileSync(file, JSON.stringify(pruneHookState(state), null, 2) + "\n", {
|
|
11951
12312
|
encoding: "utf8",
|
|
11952
12313
|
mode: 384
|
|
11953
12314
|
});
|
|
@@ -12093,7 +12454,7 @@ async function runHookNotify(io) {
|
|
|
12093
12454
|
const stdout = io.stdout ?? ((line) => process.stdout.write(line + "\n"));
|
|
12094
12455
|
const stderr = io.stderr ?? ((line) => process.stderr.write(line + "\n"));
|
|
12095
12456
|
const now = io.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
12096
|
-
const exists = io.exists ?? ((filePath) =>
|
|
12457
|
+
const exists = io.exists ?? ((filePath) => fs23.existsSync(filePath));
|
|
12097
12458
|
const execPath = io.execPath ?? process.execPath;
|
|
12098
12459
|
const entryPath = io.entryPath ?? process.argv[1] ?? "";
|
|
12099
12460
|
try {
|
|
@@ -12250,10 +12611,10 @@ function removeHookConfig(settings) {
|
|
|
12250
12611
|
return { settings: root, removed };
|
|
12251
12612
|
}
|
|
12252
12613
|
function readSettingsFile(file) {
|
|
12253
|
-
if (!
|
|
12614
|
+
if (!fs23.existsSync(file)) return { present: false, file };
|
|
12254
12615
|
let raw;
|
|
12255
12616
|
try {
|
|
12256
|
-
raw =
|
|
12617
|
+
raw = fs23.readFileSync(file, "utf8");
|
|
12257
12618
|
} catch (error) {
|
|
12258
12619
|
return { present: true, file, unreadable: error.message };
|
|
12259
12620
|
}
|
|
@@ -12265,10 +12626,10 @@ function readSettingsFile(file) {
|
|
|
12265
12626
|
}
|
|
12266
12627
|
}
|
|
12267
12628
|
function writeSettingsFile(file, settings) {
|
|
12268
|
-
|
|
12629
|
+
fs23.mkdirSync(path22.dirname(file), { recursive: true });
|
|
12269
12630
|
const temp = `${file}.flatbread-${process.pid}.tmp`;
|
|
12270
|
-
|
|
12271
|
-
|
|
12631
|
+
fs23.writeFileSync(temp, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
12632
|
+
fs23.renameSync(temp, file);
|
|
12272
12633
|
}
|
|
12273
12634
|
function handlersFor(execPath, entryPath, capture = false) {
|
|
12274
12635
|
return {
|
|
@@ -12426,8 +12787,8 @@ function runHookUninstall(options = {}) {
|
|
|
12426
12787
|
function settingsCandidates(env, cwd) {
|
|
12427
12788
|
return [
|
|
12428
12789
|
{ label: "user", file: claudeSettingsPath(env) },
|
|
12429
|
-
{ label: "project", file:
|
|
12430
|
-
{ label: "local", file:
|
|
12790
|
+
{ label: "project", file: path22.join(cwd, ".claude", "settings.json") },
|
|
12791
|
+
{ label: "local", file: path22.join(cwd, ".claude", "settings.local.json") }
|
|
12431
12792
|
];
|
|
12432
12793
|
}
|
|
12433
12794
|
function installedEventsIn(parsed, exists) {
|
|
@@ -12457,7 +12818,7 @@ function runHookStatus(options = {}) {
|
|
|
12457
12818
|
const env = options.env ?? process.env;
|
|
12458
12819
|
const log = options.log ?? ((line) => console.log(line));
|
|
12459
12820
|
const cwd = options.cwd ?? process.cwd();
|
|
12460
|
-
const exists = options.exists ?? ((filePath) =>
|
|
12821
|
+
const exists = options.exists ?? ((filePath) => fs23.existsSync(filePath));
|
|
12461
12822
|
const candidates = options.settingsPath === void 0 ? settingsCandidates(env, cwd) : [{ label: "settings", file: options.settingsPath }];
|
|
12462
12823
|
log("");
|
|
12463
12824
|
log("flatbread hook");
|
|
@@ -12578,8 +12939,8 @@ function probeHandler(handler, run = spawnSync2) {
|
|
|
12578
12939
|
}
|
|
12579
12940
|
|
|
12580
12941
|
// src/skill.ts
|
|
12581
|
-
import
|
|
12582
|
-
import
|
|
12942
|
+
import fs24 from "node:fs";
|
|
12943
|
+
import path23 from "node:path";
|
|
12583
12944
|
var SKILL_NAME = "flatbread";
|
|
12584
12945
|
var MARKER = "<!-- written by flatbread setup; flatbread setup --uninstall removes it -->";
|
|
12585
12946
|
var DESCRIPTION = "Search recorded AI coding-agent runs - this machine's own and public ones on flatbread.io - for one that already did this task or hit this error. Use BEFORE your first edit in any coding task, however small it looks; when a command, test or build fails; when the user says it was done or fixed before; and before you call a change done.";
|
|
@@ -12617,28 +12978,57 @@ function homeOf3(env) {
|
|
|
12617
12978
|
const home = (process.platform === "win32" ? env["USERPROFILE"] : env["HOME"])?.trim();
|
|
12618
12979
|
return home ? home : null;
|
|
12619
12980
|
}
|
|
12620
|
-
|
|
12981
|
+
var ANTIGRAVITY_PLUGIN_MANIFEST = `${JSON.stringify({ name: SKILL_NAME }, null, 2)}
|
|
12982
|
+
`;
|
|
12983
|
+
function isDir(dir) {
|
|
12984
|
+
try {
|
|
12985
|
+
return fs24.statSync(dir).isDirectory();
|
|
12986
|
+
} catch {
|
|
12987
|
+
return false;
|
|
12988
|
+
}
|
|
12989
|
+
}
|
|
12990
|
+
function skillTargets(env) {
|
|
12621
12991
|
const home = homeOf3(env);
|
|
12622
12992
|
if (home === null) return [];
|
|
12623
|
-
const claudeDir = env["CLAUDE_CONFIG_DIR"]?.trim() ||
|
|
12624
|
-
|
|
12993
|
+
const claudeDir = env["CLAUDE_CONFIG_DIR"]?.trim() || path23.join(home, ".claude");
|
|
12994
|
+
const targets = [
|
|
12995
|
+
{ file: path23.join(claudeDir, "skills", SKILL_NAME, "SKILL.md") },
|
|
12996
|
+
{ file: path23.join(home, ".agents", "skills", SKILL_NAME, "SKILL.md") }
|
|
12997
|
+
];
|
|
12998
|
+
const gemini = path23.join(home, ".gemini");
|
|
12999
|
+
if (["antigravity", "antigravity-cli", "config"].some((dir) => isDir(path23.join(gemini, dir)))) {
|
|
13000
|
+
const plugin = path23.join(gemini, "config", "plugins", SKILL_NAME);
|
|
13001
|
+
targets.push({
|
|
13002
|
+
file: path23.join(plugin, "skills", SKILL_NAME, "SKILL.md"),
|
|
13003
|
+
companion: { file: path23.join(plugin, "plugin.json"), text: ANTIGRAVITY_PLUGIN_MANIFEST }
|
|
13004
|
+
});
|
|
13005
|
+
}
|
|
13006
|
+
const opencode = path23.join(env["XDG_CONFIG_HOME"]?.trim() || path23.join(home, ".config"), "opencode");
|
|
13007
|
+
if (isDir(opencode)) targets.push({ file: path23.join(opencode, "skills", SKILL_NAME, "SKILL.md") });
|
|
13008
|
+
return targets;
|
|
13009
|
+
}
|
|
13010
|
+
function removeIfEmpty(dir) {
|
|
13011
|
+
try {
|
|
13012
|
+
fs24.rmdirSync(dir);
|
|
13013
|
+
} catch {
|
|
13014
|
+
}
|
|
12625
13015
|
}
|
|
12626
13016
|
function readOrNull(file) {
|
|
12627
13017
|
try {
|
|
12628
|
-
return
|
|
13018
|
+
return fs24.readFileSync(file, "utf8");
|
|
12629
13019
|
} catch {
|
|
12630
13020
|
return null;
|
|
12631
13021
|
}
|
|
12632
13022
|
}
|
|
12633
13023
|
function installSkill(options) {
|
|
12634
13024
|
const text2 = skillText();
|
|
12635
|
-
const
|
|
12636
|
-
if (
|
|
13025
|
+
const targets = skillTargets(options.env ?? process.env);
|
|
13026
|
+
if (targets.length === 0) {
|
|
12637
13027
|
options.log(` skill skipped: no ${process.platform === "win32" ? "USERPROFILE" : "HOME"} in the environment`);
|
|
12638
13028
|
return 1;
|
|
12639
13029
|
}
|
|
12640
13030
|
let code = 0;
|
|
12641
|
-
for (const file of
|
|
13031
|
+
for (const { file, companion } of targets) {
|
|
12642
13032
|
const current = readOrNull(file);
|
|
12643
13033
|
const ours = current !== null && current.includes(MARKER);
|
|
12644
13034
|
if (current !== null && !ours) {
|
|
@@ -12655,10 +13045,12 @@ function installSkill(options) {
|
|
|
12655
13045
|
options.log(` skill would remove ${file}`);
|
|
12656
13046
|
continue;
|
|
12657
13047
|
}
|
|
12658
|
-
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
|
|
13048
|
+
fs24.rmSync(file);
|
|
13049
|
+
removeIfEmpty(path23.dirname(file));
|
|
13050
|
+
if (companion !== void 0) {
|
|
13051
|
+
removeIfEmpty(path23.dirname(path23.dirname(file)));
|
|
13052
|
+
if (readOrNull(companion.file) === companion.text) fs24.rmSync(companion.file);
|
|
13053
|
+
removeIfEmpty(path23.dirname(companion.file));
|
|
12662
13054
|
}
|
|
12663
13055
|
options.log(` skill removed ${file}`);
|
|
12664
13056
|
continue;
|
|
@@ -12671,14 +13063,68 @@ function installSkill(options) {
|
|
|
12671
13063
|
options.log(` skill would ${current === null ? "write" : "update"} ${file}`);
|
|
12672
13064
|
continue;
|
|
12673
13065
|
}
|
|
12674
|
-
|
|
12675
|
-
|
|
13066
|
+
fs24.mkdirSync(path23.dirname(file), { recursive: true });
|
|
13067
|
+
if (companion !== void 0 && readOrNull(companion.file) === null) fs24.writeFileSync(companion.file, companion.text);
|
|
13068
|
+
fs24.writeFileSync(file, text2);
|
|
12676
13069
|
options.log(` skill ${current === null ? "wrote" : "updated"} ${file}`);
|
|
12677
13070
|
}
|
|
12678
13071
|
return code;
|
|
12679
13072
|
}
|
|
12680
13073
|
|
|
12681
13074
|
// src/setup.ts
|
|
13075
|
+
function isDir2(dir) {
|
|
13076
|
+
try {
|
|
13077
|
+
return fs25.statSync(dir).isDirectory();
|
|
13078
|
+
} catch {
|
|
13079
|
+
return false;
|
|
13080
|
+
}
|
|
13081
|
+
}
|
|
13082
|
+
function otherAgents(env, step) {
|
|
13083
|
+
const { log } = step;
|
|
13084
|
+
const home = homeOf3(env);
|
|
13085
|
+
if (home === null) {
|
|
13086
|
+
log("Other agents");
|
|
13087
|
+
log(" skipped no home directory in the environment");
|
|
13088
|
+
return 1;
|
|
13089
|
+
}
|
|
13090
|
+
const entry = { command: step.execPath, args: [step.entryPath, "mcp"] };
|
|
13091
|
+
let code = 0;
|
|
13092
|
+
log("Codex");
|
|
13093
|
+
const codexHome2 = env["CODEX_HOME"]?.trim() || path24.join(home, ".codex");
|
|
13094
|
+
if (!isDir2(codexHome2)) log(` skipped not installed here (no ${codexHome2})`);
|
|
13095
|
+
else code = Math.max(code, registerCodexMcp({ file: codexConfigPath(codexHome2), ...step }));
|
|
13096
|
+
if (isDir2(codexHome2) && !step.uninstall) log(" skill Codex reads the shared ~/.agents/skills copy above");
|
|
13097
|
+
log("");
|
|
13098
|
+
log("Cursor");
|
|
13099
|
+
const cursorHome = path24.join(home, ".cursor");
|
|
13100
|
+
if (!isDir2(cursorHome)) log(` skipped not installed here (no ${cursorHome})`);
|
|
13101
|
+
else {
|
|
13102
|
+
code = Math.max(code, registerMcpJson({
|
|
13103
|
+
file: path24.join(cursorHome, "mcp.json"),
|
|
13104
|
+
entry,
|
|
13105
|
+
app: "Cursor",
|
|
13106
|
+
scope: "for every project",
|
|
13107
|
+
restart: "restart Cursor so it starts the server",
|
|
13108
|
+
...step
|
|
13109
|
+
}));
|
|
13110
|
+
}
|
|
13111
|
+
log("");
|
|
13112
|
+
log("Antigravity");
|
|
13113
|
+
const gemini = path24.join(home, ".gemini");
|
|
13114
|
+
if (!["antigravity", "antigravity-cli", "config"].some((dir) => isDir2(path24.join(gemini, dir)))) {
|
|
13115
|
+
log(` skipped not installed here (no ${path24.join(gemini, "antigravity")})`);
|
|
13116
|
+
} else {
|
|
13117
|
+
code = Math.max(code, registerMcpJson({
|
|
13118
|
+
file: path24.join(gemini, "config", "mcp_config.json"),
|
|
13119
|
+
entry,
|
|
13120
|
+
app: "Antigravity",
|
|
13121
|
+
scope: "for Antigravity",
|
|
13122
|
+
restart: "restart Antigravity so it starts the server",
|
|
13123
|
+
...step
|
|
13124
|
+
}));
|
|
13125
|
+
}
|
|
13126
|
+
return code;
|
|
13127
|
+
}
|
|
12682
13128
|
function runSetup(options = {}) {
|
|
12683
13129
|
const env = options.env ?? process.env;
|
|
12684
13130
|
const log = options.log ?? ((line) => console.log(line));
|
|
@@ -12710,20 +13156,18 @@ function runSetup(options = {}) {
|
|
|
12710
13156
|
});
|
|
12711
13157
|
let mcp = 0;
|
|
12712
13158
|
let skill = 0;
|
|
13159
|
+
const uninstall = options.uninstall === true;
|
|
12713
13160
|
if (options.noMcp === true) {
|
|
12714
|
-
log(" --no-mcp:
|
|
12715
|
-
} else if (claude === 0 || options.uninstall === true) {
|
|
12716
|
-
mcp = registerClaudeMcp({
|
|
12717
|
-
file: options.claudeConfigPath ?? claudeUserConfigPath(env),
|
|
12718
|
-
execPath,
|
|
12719
|
-
entryPath,
|
|
12720
|
-
uninstall: options.uninstall === true,
|
|
12721
|
-
dryRun,
|
|
12722
|
-
log
|
|
12723
|
-
});
|
|
12724
|
-
skill = installSkill({ env, dryRun, uninstall: options.uninstall === true, log });
|
|
13161
|
+
log(" --no-mcp: no agent's MCP servers and no flatbread skill were touched");
|
|
12725
13162
|
} else {
|
|
12726
|
-
|
|
13163
|
+
if (claude === 0 || uninstall) {
|
|
13164
|
+
mcp = registerClaudeMcp({ file: options.claudeConfigPath ?? claudeUserConfigPath(env), execPath, entryPath, uninstall, dryRun, log });
|
|
13165
|
+
} else {
|
|
13166
|
+
log(" mcp not registered, because the hook step above was refused");
|
|
13167
|
+
}
|
|
13168
|
+
skill = installSkill({ env, dryRun, uninstall, log });
|
|
13169
|
+
log("");
|
|
13170
|
+
mcp = Math.max(mcp, otherAgents(env, { execPath, entryPath, uninstall, dryRun, log }));
|
|
12727
13171
|
}
|
|
12728
13172
|
log("");
|
|
12729
13173
|
let git2 = 0;
|
|
@@ -12974,13 +13418,13 @@ async function bindFirst(server, ports) {
|
|
|
12974
13418
|
}
|
|
12975
13419
|
|
|
12976
13420
|
// src/index.ts
|
|
12977
|
-
var PACKAGE_ROOT =
|
|
12978
|
-
var SOURCE_CHECKOUT =
|
|
12979
|
-
var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ?
|
|
12980
|
-
var OUTPUT_PATH =
|
|
12981
|
-
var SHARE_OUTPUT_PATH =
|
|
13421
|
+
var PACKAGE_ROOT = path25.resolve(path25.dirname(fileURLToPath2(import.meta.url)), "..");
|
|
13422
|
+
var SOURCE_CHECKOUT = fs26.existsSync(path25.join(PACKAGE_ROOT, "src", "index.ts"));
|
|
13423
|
+
var TMP_DIR = process.env["FLATBREAD_TMP_DIR"]?.trim() || (SOURCE_CHECKOUT ? path25.join(PACKAGE_ROOT, ".tmp") : path25.join(os9.homedir(), ".flatbread"));
|
|
13424
|
+
var OUTPUT_PATH = path25.join(TMP_DIR, "last-parse.json");
|
|
13425
|
+
var SHARE_OUTPUT_PATH = path25.join(TMP_DIR, "last-share.json");
|
|
12982
13426
|
function storeDir() {
|
|
12983
|
-
return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null :
|
|
13427
|
+
return resolveStoreDir() ?? (SOURCE_CHECKOUT ? null : path25.join(TMP_DIR, DEFAULT_STORE_DIRNAME));
|
|
12984
13428
|
}
|
|
12985
13429
|
var USAGE = `flatbread - registry of AI agent runs
|
|
12986
13430
|
|
|
@@ -13036,7 +13480,6 @@ Options for share:
|
|
|
13036
13480
|
|
|
13037
13481
|
Options for search:
|
|
13038
13482
|
--harness <id> --category <kind> Only runs from that harness, or of that kind of job
|
|
13039
|
-
--recovered Only runs where the agent recovered from a failure
|
|
13040
13483
|
--local This machine only; the site is not asked
|
|
13041
13484
|
|
|
13042
13485
|
Options for setup:
|
|
@@ -13044,7 +13487,7 @@ Options for setup:
|
|
|
13044
13487
|
--uninstall Remove the handlers, the MCP server and the post-commit;
|
|
13045
13488
|
restore a post-commit hook setup had chained
|
|
13046
13489
|
--no-git Leave the repository's hooks alone
|
|
13047
|
-
--no-mcp Leave
|
|
13490
|
+
--no-mcp Leave every agent's MCP servers and the flatbread skill alone
|
|
13048
13491
|
--settings <path> --no-probe As for hook install
|
|
13049
13492
|
|
|
13050
13493
|
Options for capture:
|
|
@@ -13067,8 +13510,8 @@ Options for adapters:
|
|
|
13067
13510
|
Notes:
|
|
13068
13511
|
parse and share record the git branch, HEAD commit and uncommitted file names when the
|
|
13069
13512
|
session ran inside a git repository. Nothing else from git is read.
|
|
13070
|
-
parse --save writes ${
|
|
13071
|
-
Share drafts are written to ${
|
|
13513
|
+
parse --save writes ${path25.relative(process.cwd(), OUTPUT_PATH) || OUTPUT_PATH}
|
|
13514
|
+
Share drafts are written to ${path25.relative(process.cwd(), SHARE_OUTPUT_PATH) || SHARE_OUTPUT_PATH}
|
|
13072
13515
|
(the latest only; every shared run is also kept under its ID, for search and pull)
|
|
13073
13516
|
Every parsed run defaults to visibility "private".
|
|
13074
13517
|
Nothing is uploaded unless you pass --publish, or set FLATBREAD_API_URL and
|
|
@@ -13105,10 +13548,10 @@ function parseArgv(argv) {
|
|
|
13105
13548
|
} else if (arg === "--out") {
|
|
13106
13549
|
const value = argv[i + 1];
|
|
13107
13550
|
if (value === void 0 || value.startsWith("--")) throw new Error("--out needs a file path");
|
|
13108
|
-
args.out =
|
|
13551
|
+
args.out = path25.resolve(value);
|
|
13109
13552
|
i += 1;
|
|
13110
13553
|
} else if (arg !== void 0 && arg.startsWith("--out=")) {
|
|
13111
|
-
args.out =
|
|
13554
|
+
args.out = path25.resolve(arg.slice("--out=".length));
|
|
13112
13555
|
} else if (arg === "--harness") {
|
|
13113
13556
|
const value = argv[i + 1];
|
|
13114
13557
|
if (value === void 0 || value.startsWith("--")) {
|
|
@@ -13229,12 +13672,12 @@ async function runParse(args) {
|
|
|
13229
13672
|
target = found[0];
|
|
13230
13673
|
console.log(`latest ${harness} session: ${target}`);
|
|
13231
13674
|
} else if (args.file) {
|
|
13232
|
-
target =
|
|
13233
|
-
if (!
|
|
13675
|
+
target = path25.resolve(args.file);
|
|
13676
|
+
if (!fs26.existsSync(target)) {
|
|
13234
13677
|
console.error(`file not found: ${target}`);
|
|
13235
13678
|
return 1;
|
|
13236
13679
|
}
|
|
13237
|
-
if (!
|
|
13680
|
+
if (!fs26.statSync(target).isFile()) {
|
|
13238
13681
|
console.error(`not a file: ${target}`);
|
|
13239
13682
|
return 1;
|
|
13240
13683
|
}
|
|
@@ -13265,8 +13708,8 @@ async function runParse(args) {
|
|
|
13265
13708
|
const trace = withLineage(result.trace, readGitLineage(result.trace.source.cwd));
|
|
13266
13709
|
FlatbreadTraceSchema.parse(trace);
|
|
13267
13710
|
if (args.out !== void 0) {
|
|
13268
|
-
|
|
13269
|
-
|
|
13711
|
+
fs26.mkdirSync(path25.dirname(args.out), { recursive: true });
|
|
13712
|
+
fs26.writeFileSync(args.out, JSON.stringify(trace, null, 2) + "\n", "utf8");
|
|
13270
13713
|
}
|
|
13271
13714
|
report({ ...result, trace }, args.out);
|
|
13272
13715
|
return 0;
|
|
@@ -13330,8 +13773,8 @@ async function runAdapters(args) {
|
|
|
13330
13773
|
}
|
|
13331
13774
|
let input = null;
|
|
13332
13775
|
if (args.file !== void 0) {
|
|
13333
|
-
const resolved =
|
|
13334
|
-
if (!
|
|
13776
|
+
const resolved = path25.resolve(args.file);
|
|
13777
|
+
if (!fs26.existsSync(resolved)) {
|
|
13335
13778
|
console.error(`file not found: ${resolved}`);
|
|
13336
13779
|
return 1;
|
|
13337
13780
|
}
|
|
@@ -13765,9 +14208,9 @@ async function runPull(args, deps = { ...defaultMcpDeps(storeDir(), bucketDir())
|
|
|
13765
14208
|
process.stdout.write(result.capsule);
|
|
13766
14209
|
return 0;
|
|
13767
14210
|
}
|
|
13768
|
-
const target =
|
|
13769
|
-
|
|
13770
|
-
|
|
14211
|
+
const target = path25.resolve(args.out);
|
|
14212
|
+
fs26.mkdirSync(path25.dirname(target), { recursive: true });
|
|
14213
|
+
fs26.writeFileSync(target, result.capsule, "utf8");
|
|
13771
14214
|
console.error(`wrote ${target}${result.from === "site" ? "" : ` (from this machine's ${result.from === "store" ? "shared runs" : "captured runs"})`}`);
|
|
13772
14215
|
return 0;
|
|
13773
14216
|
}
|
|
@@ -13790,8 +14233,6 @@ function parseSearchArgv(argv) {
|
|
|
13790
14233
|
const value = valueOf("--category");
|
|
13791
14234
|
if (!TASK_CATEGORIES.includes(value)) throw new Error(`unknown --category "${value}". Known: ${TASK_CATEGORIES.join(", ")}`);
|
|
13792
14235
|
query.category = value;
|
|
13793
|
-
} else if (arg === "--recovered") {
|
|
13794
|
-
query.onlyWithRecoveries = true;
|
|
13795
14236
|
} else if (arg === "--local") {
|
|
13796
14237
|
local = true;
|
|
13797
14238
|
} else if (arg.startsWith("--")) {
|
|
@@ -14017,9 +14458,9 @@ var invokedDirectly = (() => {
|
|
|
14017
14458
|
if (entry === void 0) return false;
|
|
14018
14459
|
const real = (file) => {
|
|
14019
14460
|
try {
|
|
14020
|
-
return
|
|
14461
|
+
return fs26.realpathSync(file);
|
|
14021
14462
|
} catch {
|
|
14022
|
-
return
|
|
14463
|
+
return path25.resolve(file);
|
|
14023
14464
|
}
|
|
14024
14465
|
};
|
|
14025
14466
|
return real(entry) === real(fileURLToPath2(import.meta.url));
|
package/package.json
CHANGED