beecork 2.8.0 → 2.8.2
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/dist/index.js +264 -124
- package/package.json +3 -2
- package/skeleton/bridge.mjs +181 -0
- package/skills/browser-signals.md +25 -6
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { writeFile as
|
|
4
|
+
import { writeFile as writeFile6, chmod as chmod4 } from "node:fs/promises";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
|
|
7
7
|
// src/paths.ts
|
|
@@ -187,10 +187,10 @@ async function findShadowingInstalls() {
|
|
|
187
187
|
const names = process.platform === "win32" ? ["beecork.cmd", "beecork"] : ["beecork"];
|
|
188
188
|
const seenRoots = /* @__PURE__ */ new Set([running]);
|
|
189
189
|
const others = [];
|
|
190
|
-
for (const
|
|
190
|
+
for (const dir2 of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
191
191
|
for (const name of names) {
|
|
192
192
|
try {
|
|
193
|
-
const bin = join(
|
|
193
|
+
const bin = join(dir2, name);
|
|
194
194
|
const root = dirname(dirname(await realpath(bin)));
|
|
195
195
|
if (seenRoots.has(root)) continue;
|
|
196
196
|
seenRoots.add(root);
|
|
@@ -241,9 +241,9 @@ async function checkForUpdate(current) {
|
|
|
241
241
|
void fetchLatest().then(async (latest) => {
|
|
242
242
|
if (!latest) return;
|
|
243
243
|
try {
|
|
244
|
-
const
|
|
245
|
-
await mkdir(dirname(
|
|
246
|
-
await writeFile(
|
|
244
|
+
const file2 = cacheFile();
|
|
245
|
+
await mkdir(dirname(file2), { recursive: true });
|
|
246
|
+
await writeFile(file2, JSON.stringify({ checkedAt: Date.now(), latest }), "utf8");
|
|
247
247
|
} catch {
|
|
248
248
|
}
|
|
249
249
|
});
|
|
@@ -615,7 +615,7 @@ function printBanner(model, version, sources) {
|
|
|
615
615
|
}
|
|
616
616
|
|
|
617
617
|
// src/agent.ts
|
|
618
|
-
import { readFile as
|
|
618
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
619
619
|
|
|
620
620
|
// src/input.ts
|
|
621
621
|
import { emitKeypressEvents } from "node:readline";
|
|
@@ -635,10 +635,10 @@ function inputLayout(text, before, promptW, cols2) {
|
|
|
635
635
|
const curPhysCol = curCol % c;
|
|
636
636
|
return { totalPhys, curPhysRow, curPhysCol };
|
|
637
637
|
}
|
|
638
|
-
function moveVertIndex(buf2, cur2,
|
|
638
|
+
function moveVertIndex(buf2, cur2, dir2) {
|
|
639
639
|
const lines = buf2.split("\n");
|
|
640
640
|
const { row, col } = rowColOf(buf2.slice(0, cur2));
|
|
641
|
-
const target = row +
|
|
641
|
+
const target = row + dir2;
|
|
642
642
|
if (target < 0 || target >= lines.length) return null;
|
|
643
643
|
let idx = 0;
|
|
644
644
|
for (let i = 0; i < target; i++) idx += lines[i].length + 1;
|
|
@@ -896,8 +896,8 @@ function readPrompt(opts) {
|
|
|
896
896
|
if (isPrintableCodePoint(str.codePointAt(0))) insert2(str);
|
|
897
897
|
}
|
|
898
898
|
}
|
|
899
|
-
function moveVert(
|
|
900
|
-
const idx = moveVertIndex(buf2, cur2,
|
|
899
|
+
function moveVert(dir2) {
|
|
900
|
+
const idx = moveVertIndex(buf2, cur2, dir2);
|
|
901
901
|
if (idx === null) return;
|
|
902
902
|
cur2 = idx;
|
|
903
903
|
render2();
|
|
@@ -1195,11 +1195,11 @@ function createMarkdownStream(write) {
|
|
|
1195
1195
|
}
|
|
1196
1196
|
|
|
1197
1197
|
// src/tools.ts
|
|
1198
|
-
import { readFile as
|
|
1198
|
+
import { readFile as readFile4, writeFile as writeFile3, appendFile, readdir as readdir2, mkdir as mkdir4, stat, rename, chmod } from "node:fs/promises";
|
|
1199
1199
|
import { createReadStream } from "node:fs";
|
|
1200
1200
|
import { createInterface as createLineReader } from "node:readline";
|
|
1201
|
-
import { spawn as
|
|
1202
|
-
import { join as
|
|
1201
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
1202
|
+
import { join as join5 } from "node:path";
|
|
1203
1203
|
import { lookup as dnsLookup } from "node:dns";
|
|
1204
1204
|
import { request as httpRequest } from "node:http";
|
|
1205
1205
|
import { request as httpsRequest } from "node:https";
|
|
@@ -1338,10 +1338,10 @@ async function loadSkills() {
|
|
|
1338
1338
|
[join2(process.cwd(), ".beecork", "skills"), "project"],
|
|
1339
1339
|
[bundledDir, "bundled"]
|
|
1340
1340
|
];
|
|
1341
|
-
for (const [
|
|
1341
|
+
for (const [dir2, source] of dirs) {
|
|
1342
1342
|
let entries;
|
|
1343
1343
|
try {
|
|
1344
|
-
entries = await readdir(
|
|
1344
|
+
entries = await readdir(dir2, { withFileTypes: true });
|
|
1345
1345
|
} catch {
|
|
1346
1346
|
continue;
|
|
1347
1347
|
}
|
|
@@ -1354,10 +1354,10 @@ async function loadSkills() {
|
|
|
1354
1354
|
continue;
|
|
1355
1355
|
}
|
|
1356
1356
|
try {
|
|
1357
|
-
const raw = (await readFile2(join2(
|
|
1357
|
+
const raw = (await readFile2(join2(dir2, e.name), "utf8")).trim();
|
|
1358
1358
|
if (!raw) continue;
|
|
1359
1359
|
const { description, modelInvocable, body } = parseSkill(raw);
|
|
1360
|
-
registry.set(name, { name, content: body, description, modelInvocable, path: join2(
|
|
1360
|
+
registry.set(name, { name, content: body, description, modelInvocable, path: join2(dir2, e.name), source });
|
|
1361
1361
|
} catch {
|
|
1362
1362
|
}
|
|
1363
1363
|
}
|
|
@@ -1678,11 +1678,127 @@ function decodeEntities(s) {
|
|
|
1678
1678
|
});
|
|
1679
1679
|
}
|
|
1680
1680
|
|
|
1681
|
+
// src/skeleton.ts
|
|
1682
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
1683
|
+
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
1684
|
+
import { join as join3, dirname as dirname3 } from "node:path";
|
|
1685
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1686
|
+
import { homedir as homedir5 } from "node:os";
|
|
1687
|
+
var port = () => Number(process.env.BEECORK_SKELETON_PORT) || 8317;
|
|
1688
|
+
var skeletonUrl = () => process.env.BEECORK_DEV_SIGNALS_URL || `http://localhost:${port()}`;
|
|
1689
|
+
var managedExternally = () => !!process.env.BEECORK_DEV_SIGNALS_URL;
|
|
1690
|
+
var skeletonHome = () => process.env.BEECORK_SKELETON_HOME || join3(homedir5(), ".beecork", "skeleton");
|
|
1691
|
+
var bridgeScript = () => join3(dirname3(fileURLToPath3(import.meta.url)), "..", "skeleton", "bridge.mjs");
|
|
1692
|
+
async function probe(url = skeletonUrl(), timeoutMs = 600) {
|
|
1693
|
+
try {
|
|
1694
|
+
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
1695
|
+
if (res.ok) {
|
|
1696
|
+
const j = await res.json();
|
|
1697
|
+
if (j && j.skeleton === true) return "up";
|
|
1698
|
+
}
|
|
1699
|
+
} catch {
|
|
1700
|
+
return "down";
|
|
1701
|
+
}
|
|
1702
|
+
try {
|
|
1703
|
+
const res = await fetch(`${url}/signals?limit=1`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
1704
|
+
if (res.ok) {
|
|
1705
|
+
const j = await res.json();
|
|
1706
|
+
if (Array.isArray(j && j.signals)) return "up";
|
|
1707
|
+
}
|
|
1708
|
+
} catch {
|
|
1709
|
+
}
|
|
1710
|
+
return "foreign";
|
|
1711
|
+
}
|
|
1712
|
+
var inFlight = null;
|
|
1713
|
+
function ensureBridge() {
|
|
1714
|
+
if (inFlight) return inFlight;
|
|
1715
|
+
inFlight = doEnsure().finally(() => {
|
|
1716
|
+
inFlight = null;
|
|
1717
|
+
});
|
|
1718
|
+
return inFlight;
|
|
1719
|
+
}
|
|
1720
|
+
async function doEnsure() {
|
|
1721
|
+
if (managedExternally()) return { up: false, reason: "external" };
|
|
1722
|
+
const first = await probe();
|
|
1723
|
+
if (first === "up") return { up: true };
|
|
1724
|
+
if (first === "foreign") return { up: false, reason: "foreign-port" };
|
|
1725
|
+
let pid;
|
|
1726
|
+
try {
|
|
1727
|
+
const home = skeletonHome();
|
|
1728
|
+
await mkdir2(home, { recursive: true });
|
|
1729
|
+
const child = spawn3(process.execPath, [bridgeScript()], {
|
|
1730
|
+
cwd: home,
|
|
1731
|
+
env: { ...process.env, BEECORK_SKELETON_HOME: home, BEECORK_SKELETON_PORT: String(port()) },
|
|
1732
|
+
detached: true,
|
|
1733
|
+
stdio: "ignore"
|
|
1734
|
+
// fire-and-forget; it logs to no one, which is fine
|
|
1735
|
+
});
|
|
1736
|
+
child.on("error", () => {
|
|
1737
|
+
});
|
|
1738
|
+
pid = child.pid;
|
|
1739
|
+
child.unref();
|
|
1740
|
+
} catch {
|
|
1741
|
+
return { up: false, reason: "spawn-failed" };
|
|
1742
|
+
}
|
|
1743
|
+
for (let i = 0; i < 10; i++) {
|
|
1744
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1745
|
+
if (await probe() === "up") return { up: true, started: true, pid };
|
|
1746
|
+
}
|
|
1747
|
+
return { up: false, started: true, pid, reason: "spawn-failed" };
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/projectSites.ts
|
|
1751
|
+
import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir3 } from "node:fs/promises";
|
|
1752
|
+
import { join as join4 } from "node:path";
|
|
1753
|
+
var dir = () => join4(process.cwd(), ".beecork");
|
|
1754
|
+
var file = () => join4(dir(), "skeleton.json");
|
|
1755
|
+
function toOrigin(input) {
|
|
1756
|
+
const s = String(input || "").trim();
|
|
1757
|
+
if (!s) return "";
|
|
1758
|
+
const withScheme = /^https?:\/\//i.test(s) ? s : "http://" + s;
|
|
1759
|
+
try {
|
|
1760
|
+
const o = new URL(withScheme).origin;
|
|
1761
|
+
return o === "null" ? "" : o;
|
|
1762
|
+
} catch {
|
|
1763
|
+
return "";
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
function isLoopbackOrigin(origin) {
|
|
1767
|
+
const o = toOrigin(origin);
|
|
1768
|
+
if (!o) return false;
|
|
1769
|
+
let host;
|
|
1770
|
+
try {
|
|
1771
|
+
host = new URL(o).hostname;
|
|
1772
|
+
} catch {
|
|
1773
|
+
return false;
|
|
1774
|
+
}
|
|
1775
|
+
host = host.replace(/^\[|\]$/g, "").toLowerCase();
|
|
1776
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "0.0.0.0" || host === "::1" || /^127\./.test(host);
|
|
1777
|
+
}
|
|
1778
|
+
async function loadProjectOrigins() {
|
|
1779
|
+
try {
|
|
1780
|
+
const cfg = JSON.parse(await readFile3(file(), "utf8"));
|
|
1781
|
+
if (!Array.isArray(cfg.origins)) return [];
|
|
1782
|
+
return [...new Set(cfg.origins.map(toOrigin).filter(Boolean))];
|
|
1783
|
+
} catch {
|
|
1784
|
+
return [];
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
async function addProjectOrigin(origin) {
|
|
1788
|
+
const o = toOrigin(origin);
|
|
1789
|
+
if (!o || isLoopbackOrigin(o)) return false;
|
|
1790
|
+
const cur2 = await loadProjectOrigins();
|
|
1791
|
+
if (cur2.includes(o)) return false;
|
|
1792
|
+
await mkdir3(dir(), { recursive: true });
|
|
1793
|
+
await writeFile2(file(), JSON.stringify({ origins: [...cur2, o] }, null, 2) + "\n");
|
|
1794
|
+
return true;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1681
1797
|
// src/tools.ts
|
|
1682
1798
|
function runShell(command, opts) {
|
|
1683
1799
|
return new Promise((resolve2, reject) => {
|
|
1684
1800
|
const unix2 = process.platform !== "win32";
|
|
1685
|
-
const child =
|
|
1801
|
+
const child = spawn4(command, { shell: true, detached: unix2, stdio: ["ignore", "pipe", "pipe"] });
|
|
1686
1802
|
let stdout = "", stderr = "", outLen = 0, errLen = 0, timedOut = false, aborted = false;
|
|
1687
1803
|
let settled = false, exitCode = null;
|
|
1688
1804
|
const kill = () => {
|
|
@@ -1749,7 +1865,7 @@ function runShell(command, opts) {
|
|
|
1749
1865
|
var fail = (verb, err) => `Error ${verb}: ${err.message}`;
|
|
1750
1866
|
async function atomicWrite(abs, content) {
|
|
1751
1867
|
const tmp = `${abs}.beecork-${process.pid}.tmp`;
|
|
1752
|
-
await
|
|
1868
|
+
await writeFile3(tmp, content, "utf8");
|
|
1753
1869
|
try {
|
|
1754
1870
|
await chmod(tmp, (await stat(abs)).mode);
|
|
1755
1871
|
} catch {
|
|
@@ -1778,8 +1894,8 @@ function lineOffsets(text) {
|
|
|
1778
1894
|
for (let i = 0; i < text.length; i++) if (text[i] === "\n") starts.push(i + 1);
|
|
1779
1895
|
return starts;
|
|
1780
1896
|
}
|
|
1781
|
-
function matchWhitespace(
|
|
1782
|
-
const fileLines =
|
|
1897
|
+
function matchWhitespace(file2, oldText, newText) {
|
|
1898
|
+
const fileLines = file2.split("\n");
|
|
1783
1899
|
const oldLines = oldText.split("\n");
|
|
1784
1900
|
const n = oldLines.length;
|
|
1785
1901
|
const trim = (l) => l.trim();
|
|
@@ -1825,15 +1941,15 @@ function matchWhitespace(file, oldText, newText) {
|
|
|
1825
1941
|
if (mode === "strip") return l.startsWith(shift) ? l.slice(shift.length) : l;
|
|
1826
1942
|
return l;
|
|
1827
1943
|
}).join("\n");
|
|
1828
|
-
const offs = lineOffsets(
|
|
1944
|
+
const offs = lineOffsets(file2);
|
|
1829
1945
|
const start = offs[s];
|
|
1830
|
-
const end = s + n < offs.length ? offs[s + n] - 1 :
|
|
1946
|
+
const end = s + n < offs.length ? offs[s + n] - 1 : file2.length;
|
|
1831
1947
|
return { ok: true, start, end, after: reindented, healedVia: "whitespace" };
|
|
1832
1948
|
}
|
|
1833
|
-
function closestRegion(
|
|
1949
|
+
function closestRegion(file2, oldText) {
|
|
1834
1950
|
const anchor = oldText.split("\n").map((l) => l.trim()).find((l) => l !== "");
|
|
1835
1951
|
if (!anchor) return void 0;
|
|
1836
|
-
const fileLines =
|
|
1952
|
+
const fileLines = file2.split("\n");
|
|
1837
1953
|
const fmt = (i) => `${String(i + 1).padStart(5)} ${fileLines[i]}`;
|
|
1838
1954
|
const exact = [];
|
|
1839
1955
|
for (let i = 0; i < fileLines.length && exact.length < 3; i++) {
|
|
@@ -1854,23 +1970,23 @@ function closestRegion(file, oldText) {
|
|
|
1854
1970
|
}
|
|
1855
1971
|
return best >= 0 && bestScore >= Math.max(2, Math.ceil(words.length / 2)) ? fmt(best) : void 0;
|
|
1856
1972
|
}
|
|
1857
|
-
function resolveEdit(
|
|
1973
|
+
function resolveEdit(file2, oldText, newText) {
|
|
1858
1974
|
if (oldText === "") return { ok: false, reason: "not_found" };
|
|
1859
|
-
const exact = allIndexOf(
|
|
1975
|
+
const exact = allIndexOf(file2, oldText);
|
|
1860
1976
|
if (exact.length === 1) return { ok: true, start: exact[0], end: exact[0] + oldText.length, after: newText, healedVia: "exact" };
|
|
1861
1977
|
if (exact.length > 1) return { ok: false, reason: "ambiguous", count: exact.length };
|
|
1862
1978
|
const strippedOld = stripReadPrefix(oldText);
|
|
1863
1979
|
if (strippedOld !== null && strippedOld !== oldText) {
|
|
1864
|
-
const hits = allIndexOf(
|
|
1980
|
+
const hits = allIndexOf(file2, strippedOld);
|
|
1865
1981
|
if (hits.length === 1) {
|
|
1866
1982
|
const strippedNew = newText.split("\n").map((l) => l.replace(READ_PREFIX, "")).join("\n");
|
|
1867
1983
|
return { ok: true, start: hits[0], end: hits[0] + strippedOld.length, after: strippedNew, healedVia: "prefix" };
|
|
1868
1984
|
}
|
|
1869
1985
|
if (hits.length > 1) return { ok: false, reason: "ambiguous", count: hits.length };
|
|
1870
1986
|
}
|
|
1871
|
-
const ws = matchWhitespace(
|
|
1987
|
+
const ws = matchWhitespace(file2, oldText, newText);
|
|
1872
1988
|
if (ws) return ws;
|
|
1873
|
-
return { ok: false, reason: "not_found", closest: closestRegion(
|
|
1989
|
+
return { ok: false, reason: "not_found", closest: closestRegion(file2, oldText) };
|
|
1874
1990
|
}
|
|
1875
1991
|
var todos = [];
|
|
1876
1992
|
function httpGet(rawUrl, maxBytes, signal) {
|
|
@@ -1964,7 +2080,7 @@ async function walkTree(abs, prefix, items, cap) {
|
|
|
1964
2080
|
const e = kept[i];
|
|
1965
2081
|
const last = i === kept.length - 1;
|
|
1966
2082
|
items.push({ prefix: prefix + (last ? "\u2514\u2500 " : "\u251C\u2500 "), name: e.name + (e.isDirectory() ? "/" : ""), isDir: e.isDirectory() });
|
|
1967
|
-
if (e.isDirectory()) await walkTree(
|
|
2083
|
+
if (e.isDirectory()) await walkTree(join5(abs, e.name), prefix + (last ? " " : "\u2502 "), items, cap);
|
|
1968
2084
|
}
|
|
1969
2085
|
}
|
|
1970
2086
|
function parseRange(args, defLimit) {
|
|
@@ -1994,14 +2110,13 @@ async function readLineWindow(abs, offset1, limit) {
|
|
|
1994
2110
|
}
|
|
1995
2111
|
return { lines, startLine: start + 1, hasMore, empty: i === 0 };
|
|
1996
2112
|
}
|
|
1997
|
-
var
|
|
2113
|
+
var EXTENSION_STEPS = `1. Load the extension: Chrome \u2192 chrome://extensions \u2192 turn on "Developer mode" \u2192 "Load unpacked" \u2192 select the beecork-extension/extension folder, and pin the icon.
|
|
2114
|
+
2. Click the icon (it auto-connects \u2014 no token to paste), tick "Capture enabled", open the app in a tab, and click "Pair this site".`;
|
|
2115
|
+
var DEV_SIGNALS_SETUP = `The browser link isn't connected yet.
|
|
1998
2116
|
|
|
1999
|
-
This is "Beecork Skeleton" \u2014 a Chrome extension that
|
|
2117
|
+
This is "Beecork Skeleton" \u2014 a Chrome extension that streams the app's console errors and failed network requests to me, so I see what the browser sees instead of guessing. beecork runs the local inbox for you automatically; the only one-time step is loading the extension (local-only, no account):
|
|
2000
2118
|
|
|
2001
|
-
|
|
2002
|
-
1. Start the local inbox: run \`node bridge/server.mjs\` in the beecork-extension folder, and leave it running.
|
|
2003
|
-
2. Load the extension: Chrome \u2192 chrome://extensions \u2192 turn on "Developer mode" \u2192 "Load unpacked" \u2192 select the beecork-extension/extension folder. Pin the icon.
|
|
2004
|
-
3. Click the icon (it auto-connects), tick "Capture enabled", open the app in a tab, and click "Pair this site".
|
|
2119
|
+
` + EXTENSION_STEPS + `
|
|
2005
2120
|
|
|
2006
2121
|
Then call read_dev_signals again. Full step-by-step + troubleshooting is in the "browser-signals" skill.`;
|
|
2007
2122
|
var toolDefs = [
|
|
@@ -2098,7 +2213,7 @@ var toolDefs = [
|
|
|
2098
2213
|
const MAX = config.searchMaxResults;
|
|
2099
2214
|
const deadline = Date.now() + config.searchTimeoutMs;
|
|
2100
2215
|
let truncated = false;
|
|
2101
|
-
async function walk(
|
|
2216
|
+
async function walk(dir2) {
|
|
2102
2217
|
if (results.length >= MAX) return;
|
|
2103
2218
|
if (Date.now() > deadline) {
|
|
2104
2219
|
truncated = true;
|
|
@@ -2106,7 +2221,7 @@ var toolDefs = [
|
|
|
2106
2221
|
}
|
|
2107
2222
|
let entries;
|
|
2108
2223
|
try {
|
|
2109
|
-
entries = await readdir2(
|
|
2224
|
+
entries = await readdir2(dir2, { withFileTypes: true });
|
|
2110
2225
|
} catch {
|
|
2111
2226
|
return;
|
|
2112
2227
|
}
|
|
@@ -2117,7 +2232,7 @@ var toolDefs = [
|
|
|
2117
2232
|
return;
|
|
2118
2233
|
}
|
|
2119
2234
|
if (IGNORE.has(e.name)) continue;
|
|
2120
|
-
const full = `${
|
|
2235
|
+
const full = `${dir2}/${e.name}`;
|
|
2121
2236
|
if (e.isDirectory()) {
|
|
2122
2237
|
await walk(full);
|
|
2123
2238
|
} else if (e.isFile()) {
|
|
@@ -2126,7 +2241,7 @@ var toolDefs = [
|
|
|
2126
2241
|
if (info && info.size > config.searchMaxFileBytes) continue;
|
|
2127
2242
|
let lines;
|
|
2128
2243
|
try {
|
|
2129
|
-
lines = (await
|
|
2244
|
+
lines = (await readFile4(full, "utf8")).split("\n");
|
|
2130
2245
|
} catch {
|
|
2131
2246
|
continue;
|
|
2132
2247
|
}
|
|
@@ -2194,7 +2309,7 @@ var toolDefs = [
|
|
|
2194
2309
|
run: async (args) => {
|
|
2195
2310
|
try {
|
|
2196
2311
|
const { abs } = resolveInRoot(String(args.path ?? "."));
|
|
2197
|
-
const original = await
|
|
2312
|
+
const original = await readFile4(abs, "utf8");
|
|
2198
2313
|
const res = resolveEdit(original, String(args.old_text ?? ""), String(args.new_text ?? ""));
|
|
2199
2314
|
if (!res.ok) {
|
|
2200
2315
|
if (res.reason === "ambiguous") {
|
|
@@ -2397,21 +2512,21 @@ ${list}`;
|
|
|
2397
2512
|
},
|
|
2398
2513
|
run: async (args) => {
|
|
2399
2514
|
try {
|
|
2400
|
-
const
|
|
2401
|
-
await
|
|
2402
|
-
const
|
|
2515
|
+
const dir2 = join5(process.cwd(), ".beecork");
|
|
2516
|
+
await mkdir4(dir2, { recursive: true });
|
|
2517
|
+
const file2 = join5(dir2, "memory.md");
|
|
2403
2518
|
const fact = String(args.fact).trim();
|
|
2404
2519
|
if (!fact) return 'Error: remember needs a non-empty "fact".';
|
|
2405
2520
|
let current = "";
|
|
2406
2521
|
try {
|
|
2407
|
-
current = await
|
|
2522
|
+
current = await readFile4(file2, "utf8");
|
|
2408
2523
|
} catch {
|
|
2409
2524
|
}
|
|
2410
2525
|
if (current.length + fact.length + 3 > config.memoryMaxChars) {
|
|
2411
2526
|
return `Error: memory is at its ${config.memoryMaxChars}-char budget. Consolidate first: read .beecork/memory.md, merge duplicate/overlapping lines and drop anything stale or no-longer-true, write_file the shorter version back, then call remember again with this fact.`;
|
|
2412
2527
|
}
|
|
2413
|
-
if (!current) await
|
|
2414
|
-
await appendFile(
|
|
2528
|
+
if (!current) await writeFile3(file2, "# beecork memory\n\n", "utf8");
|
|
2529
|
+
await appendFile(file2, `- ${fact}
|
|
2415
2530
|
`, "utf8");
|
|
2416
2531
|
return `Remembered: ${fact}`;
|
|
2417
2532
|
} catch (err) {
|
|
@@ -2512,37 +2627,51 @@ ${skill.content}`;
|
|
|
2512
2627
|
},
|
|
2513
2628
|
{
|
|
2514
2629
|
name: "read_dev_signals",
|
|
2515
|
-
description: "Read the browser's recent console errors and failed network requests for the user's app (localhost or production), captured live by the Beecork Skeleton extension \u2014 so you can SEE what's actually happening instead of guessing. Call this whenever the user reports a bug a browser would surface (blank page, broken button, failed save, a 500, a visual glitch). If it isn't connected yet it returns setup steps to relay to the user. Pull on demand; don't spam it.",
|
|
2630
|
+
description: "Read the browser's recent console errors and failed network requests for the user's app (localhost or production), captured live by the Beecork Skeleton extension \u2014 so you can SEE what's actually happening instead of guessing. Call this whenever the user reports a bug a browser would surface (blank page, broken button, failed save, a 500, a visual glitch). If it isn't connected yet it returns setup steps to relay to the user. Pull on demand; don't spam it. The inbox is SHARED across projects, so pass `origin` (this project's site, e.g. its dev URL or production URL) to see only THIS app's signals \u2014 otherwise you may get other apps' noise too.",
|
|
2516
2631
|
parameters: {
|
|
2517
2632
|
type: "object",
|
|
2518
2633
|
properties: {
|
|
2519
2634
|
kind: { type: "string", description: 'Filter: "network", "console", "pageError", "log", or "all" (default all).' },
|
|
2520
2635
|
since_minutes: { type: "number", description: "Only signals from the last N minutes (optional)." },
|
|
2521
|
-
limit: { type: "number", description: "Max signals to return (default 30, max 200)." }
|
|
2636
|
+
limit: { type: "number", description: "Max signals to return (default 30, max 200)." },
|
|
2637
|
+
origin: { type: "string", description: "Scope to one or more sites (comma-separated for several frontends), e.g. http://localhost:8000 or https://app.example.com. Defaults to this project's remembered production site(s) if set; pass localhost explicitly since dev ports aren't remembered." }
|
|
2522
2638
|
},
|
|
2523
2639
|
required: []
|
|
2524
2640
|
},
|
|
2525
2641
|
run: async (args, signal) => {
|
|
2526
|
-
const
|
|
2642
|
+
const ens = await ensureBridge().catch(() => ({ up: false, reason: "spawn-failed" }));
|
|
2643
|
+
const base = skeletonUrl();
|
|
2527
2644
|
const kind = args.kind ? String(args.kind) : "";
|
|
2528
2645
|
const limit = Math.min(Math.max(Number(args.limit) || 30, 1), 200);
|
|
2529
2646
|
const sinceMin = Number(args.since_minutes) || 0;
|
|
2530
|
-
const
|
|
2647
|
+
const explicit = args.origin ? String(args.origin).split(",").map((s) => toOrigin(s)).filter(Boolean) : [];
|
|
2648
|
+
const scope = explicit.length ? explicit : await loadProjectOrigins();
|
|
2649
|
+
const params = new URLSearchParams({ limit: String(scope.length ? Math.min(200, limit * 4) : limit) });
|
|
2531
2650
|
if (kind && kind !== "all") params.set("kind", kind);
|
|
2532
2651
|
if (sinceMin > 0) params.set("since", String(Date.now() - sinceMin * 6e4));
|
|
2652
|
+
if (scope.length) params.set("origin", scope.join(","));
|
|
2533
2653
|
const timeout = AbortSignal.timeout(Math.min(config.webTimeoutMs, 5e3));
|
|
2534
2654
|
let data;
|
|
2535
2655
|
try {
|
|
2536
2656
|
const res = await fetch(`${base}/signals?${params}`, { signal: signal ? AbortSignal.any([signal, timeout]) : timeout });
|
|
2537
|
-
if (!res.ok) return `The browser link responded with HTTP ${res.status}. The
|
|
2657
|
+
if (!res.ok) return `The browser link responded with HTTP ${res.status}. The inbox may be unhealthy \u2014 I'll try to start a fresh one on the next call.`;
|
|
2538
2658
|
data = await res.json();
|
|
2539
2659
|
} catch {
|
|
2660
|
+
if (ens.reason === "foreign-port") return `Another program is using the browser-link inbox port, so I couldn't start it. Free that port (or set BEECORK_DEV_SIGNALS_URL to a different inbox) and try again.`;
|
|
2540
2661
|
return DEV_SIGNALS_SETUP;
|
|
2541
2662
|
}
|
|
2542
2663
|
const now = Date.now();
|
|
2543
|
-
|
|
2664
|
+
let signals = (data.signals ?? []).filter((s) => s && s.kind !== "watch");
|
|
2665
|
+
if (scope.length) signals = signals.filter((s) => scope.some((o) => String(s.page || s.url || "").startsWith(o)));
|
|
2666
|
+
signals = signals.slice(-limit);
|
|
2667
|
+
const scopeLabel = scope.length ? ` from ${scope.join(", ")}` : "";
|
|
2544
2668
|
if (signals.length === 0) {
|
|
2545
|
-
return `The
|
|
2669
|
+
return `The inbox is running${ens.started ? " (I just started it)" : ""}, but no ${kind && kind !== "all" ? `"${kind}" ` : ""}signals${scopeLabel} were captured${sinceMin ? ` in the last ${sinceMin} min` : ""} yet.
|
|
2670
|
+
|
|
2671
|
+
If the Beecork Skeleton extension isn't loaded yet, connect it:
|
|
2672
|
+
${EXTENSION_STEPS}
|
|
2673
|
+
|
|
2674
|
+
Already connected? Reproduce the issue in the browser (or open the app), then call read_dev_signals again.`;
|
|
2546
2675
|
}
|
|
2547
2676
|
const ago2 = (ts) => ts ? `${Math.max(0, Math.round((now - ts) / 1e3))}s ago` : "";
|
|
2548
2677
|
const lines = signals.map((s) => {
|
|
@@ -2550,8 +2679,15 @@ ${skill.content}`;
|
|
|
2550
2679
|
const text = String(s.text ?? "").replace(/\s+/g, " ").slice(0, 300);
|
|
2551
2680
|
return `[${s.kind}] ${text}${s.url ? ` @ ${s.url}` : ""} (${ago2(s.ts)})`;
|
|
2552
2681
|
});
|
|
2553
|
-
|
|
2554
|
-
|
|
2682
|
+
let hint = "";
|
|
2683
|
+
if (!scope.length) {
|
|
2684
|
+
const distinct = [...new Set(signals.map((s) => toOrigin(String(s.page || s.url || ""))).filter(Boolean))];
|
|
2685
|
+
if (distinct.length > 1) hint = `
|
|
2686
|
+
|
|
2687
|
+
(These span ${distinct.length} sites: ${distinct.join(", ")}. To see only THIS project's, pass origin:"<its site>" \u2014 or call watch_site once and I'll remember it for this folder.)`;
|
|
2688
|
+
}
|
|
2689
|
+
return `${signals.length} browser signal(s)${scopeLabel}, newest last:
|
|
2690
|
+
${lines.join("\n")}${hint}`;
|
|
2555
2691
|
}
|
|
2556
2692
|
},
|
|
2557
2693
|
{
|
|
@@ -2566,7 +2702,9 @@ ${lines.join("\n")}`;
|
|
|
2566
2702
|
required: ["url"]
|
|
2567
2703
|
},
|
|
2568
2704
|
run: async (args, signal) => {
|
|
2569
|
-
|
|
2705
|
+
await ensureBridge().catch(() => {
|
|
2706
|
+
});
|
|
2707
|
+
const base = skeletonUrl();
|
|
2570
2708
|
let origin;
|
|
2571
2709
|
try {
|
|
2572
2710
|
origin = new URL(String(args.url ?? "")).origin;
|
|
@@ -2586,7 +2724,9 @@ ${lines.join("\n")}`;
|
|
|
2586
2724
|
} catch {
|
|
2587
2725
|
return DEV_SIGNALS_SETUP;
|
|
2588
2726
|
}
|
|
2589
|
-
|
|
2727
|
+
const remembered = await addProjectOrigin(origin).catch(() => false);
|
|
2728
|
+
const memo = remembered ? ` I'll remember ${origin} as this project's site, so read_dev_signals here now scopes to it automatically.` : "";
|
|
2729
|
+
return `Requested watching ${origin} for ${minutes} min.${memo} If the user has approved that site in the extension, it will start capturing shortly \u2014 have them reproduce the issue in a tab on ${origin} (or open it), then call read_dev_signals. If nothing shows up, the site isn't approved yet: ask the user to open it and click "Pair this site" in the Beecork Skeleton popup.`;
|
|
2590
2730
|
}
|
|
2591
2731
|
}
|
|
2592
2732
|
];
|
|
@@ -2951,49 +3091,49 @@ ${summary}` }, ...recent];
|
|
|
2951
3091
|
}
|
|
2952
3092
|
|
|
2953
3093
|
// src/memory.ts
|
|
2954
|
-
import { readFile as
|
|
2955
|
-
import { homedir as
|
|
2956
|
-
import { join as
|
|
3094
|
+
import { readFile as readFile5, writeFile as writeFile4, readdir as readdir3, mkdir as mkdir5, chmod as chmod2, rename as rename2, unlink } from "node:fs/promises";
|
|
3095
|
+
import { homedir as homedir6 } from "node:os";
|
|
3096
|
+
import { join as join6, dirname as dirname4 } from "node:path";
|
|
2957
3097
|
var BEECORK = ".beecork";
|
|
2958
3098
|
function ancestorDirs() {
|
|
2959
|
-
const home =
|
|
3099
|
+
const home = homedir6();
|
|
2960
3100
|
const dirs = [];
|
|
2961
|
-
let
|
|
2962
|
-
while (
|
|
2963
|
-
dirs.push(
|
|
2964
|
-
|
|
3101
|
+
let dir2 = process.cwd();
|
|
3102
|
+
while (dir2 !== home && dir2 !== dirname4(dir2)) {
|
|
3103
|
+
dirs.push(dir2);
|
|
3104
|
+
dir2 = dirname4(dir2);
|
|
2965
3105
|
}
|
|
2966
3106
|
return dirs.reverse();
|
|
2967
3107
|
}
|
|
2968
3108
|
function corkPaths() {
|
|
2969
|
-
return [
|
|
3109
|
+
return [join6(homedir6(), BEECORK, "cork.md"), ...ancestorDirs().map((d) => join6(d, "cork.md"))];
|
|
2970
3110
|
}
|
|
2971
3111
|
function beecorkPaths(name) {
|
|
2972
|
-
return [
|
|
3112
|
+
return [join6(homedir6(), BEECORK, name), ...ancestorDirs().map((d) => join6(d, BEECORK, name))];
|
|
2973
3113
|
}
|
|
2974
3114
|
function standardInstructionPaths() {
|
|
2975
|
-
return ancestorDirs().flatMap((d) => [
|
|
3115
|
+
return ancestorDirs().flatMap((d) => [join6(d, "AGENTS.md"), join6(d, "CLAUDE.md")]);
|
|
2976
3116
|
}
|
|
2977
3117
|
async function loadInstructions() {
|
|
2978
|
-
const home =
|
|
2979
|
-
const homeBeecork =
|
|
3118
|
+
const home = homedir6();
|
|
3119
|
+
const homeBeecork = join6(home, ".beecork");
|
|
2980
3120
|
const trusted = [];
|
|
2981
3121
|
const project = [];
|
|
2982
3122
|
const sources = [];
|
|
2983
3123
|
const MAX_FILE = 8e3;
|
|
2984
3124
|
const MAX_TOTAL = 24e3;
|
|
2985
3125
|
let total = 0;
|
|
2986
|
-
for (const
|
|
3126
|
+
for (const file2 of [...corkPaths(), ...standardInstructionPaths(), ...beecorkPaths("memory.md")]) {
|
|
2987
3127
|
try {
|
|
2988
|
-
let content = (await
|
|
3128
|
+
let content = (await readFile5(file2, "utf8")).trim();
|
|
2989
3129
|
if (!content) continue;
|
|
2990
3130
|
if (content.length > MAX_FILE) content = content.slice(0, MAX_FILE) + "\n\u2026(truncated)";
|
|
2991
3131
|
if (total + content.length > MAX_TOTAL) content = content.slice(0, Math.max(0, MAX_TOTAL - total)) + "\n\u2026(truncated)";
|
|
2992
3132
|
total += content.length;
|
|
2993
|
-
const block = `## From ${tildify(
|
|
3133
|
+
const block = `## From ${tildify(file2)}
|
|
2994
3134
|
${content}`;
|
|
2995
|
-
(
|
|
2996
|
-
sources.push(
|
|
3135
|
+
(file2.startsWith(homeBeecork) ? trusted : project).push(block);
|
|
3136
|
+
sources.push(file2);
|
|
2997
3137
|
if (total >= MAX_TOTAL) break;
|
|
2998
3138
|
} catch {
|
|
2999
3139
|
}
|
|
@@ -3002,7 +3142,7 @@ ${content}`;
|
|
|
3002
3142
|
}
|
|
3003
3143
|
async function readJsonFile(path) {
|
|
3004
3144
|
try {
|
|
3005
|
-
return JSON.parse(await
|
|
3145
|
+
return JSON.parse(await readFile5(path, "utf8"));
|
|
3006
3146
|
} catch (err) {
|
|
3007
3147
|
if (err.code !== "ENOENT") {
|
|
3008
3148
|
console.error(color.yellow(`\u26A0 ignoring malformed ${tildify(path)}: ${err.message}`));
|
|
@@ -3029,60 +3169,60 @@ async function loadSettings() {
|
|
|
3029
3169
|
return { model, reasoningEffort, alwaysAllow, projectAlwaysAllowIgnored };
|
|
3030
3170
|
}
|
|
3031
3171
|
function userConfigPath() {
|
|
3032
|
-
return
|
|
3172
|
+
return join6(homedir6(), BEECORK, "config.json");
|
|
3033
3173
|
}
|
|
3034
3174
|
async function loadUserConfig() {
|
|
3035
3175
|
return await readJsonFile(userConfigPath()) ?? {};
|
|
3036
3176
|
}
|
|
3037
3177
|
async function saveUserConfig(patch) {
|
|
3038
|
-
const
|
|
3039
|
-
await
|
|
3178
|
+
const file2 = userConfigPath();
|
|
3179
|
+
await mkdir5(dirname4(file2), { recursive: true });
|
|
3040
3180
|
const merged = { ...await loadUserConfig(), ...patch };
|
|
3041
|
-
const tmp = `${
|
|
3042
|
-
await
|
|
3181
|
+
const tmp = `${file2}.tmp`;
|
|
3182
|
+
await writeFile4(tmp, JSON.stringify(merged, null, 2), { encoding: "utf8", mode: 384 });
|
|
3043
3183
|
await chmod2(tmp, 384).catch(() => {
|
|
3044
3184
|
});
|
|
3045
|
-
await rename2(tmp,
|
|
3185
|
+
await rename2(tmp, file2);
|
|
3046
3186
|
}
|
|
3047
3187
|
async function saveModelPreference(model) {
|
|
3048
3188
|
try {
|
|
3049
|
-
const
|
|
3050
|
-
await
|
|
3051
|
-
const current = await readJsonFile(
|
|
3052
|
-
await
|
|
3189
|
+
const file2 = join6(homedir6(), BEECORK, "settings.json");
|
|
3190
|
+
await mkdir5(dirname4(file2), { recursive: true });
|
|
3191
|
+
const current = await readJsonFile(file2) ?? {};
|
|
3192
|
+
await writeFile4(file2, JSON.stringify({ ...current, model }, null, 2), "utf8");
|
|
3053
3193
|
} catch {
|
|
3054
3194
|
}
|
|
3055
3195
|
}
|
|
3056
3196
|
async function saveReasoningPreference(reasoningEffort) {
|
|
3057
3197
|
try {
|
|
3058
|
-
const
|
|
3059
|
-
await
|
|
3060
|
-
const current = await readJsonFile(
|
|
3061
|
-
await
|
|
3198
|
+
const file2 = join6(homedir6(), BEECORK, "settings.json");
|
|
3199
|
+
await mkdir5(dirname4(file2), { recursive: true });
|
|
3200
|
+
const current = await readJsonFile(file2) ?? {};
|
|
3201
|
+
await writeFile4(file2, JSON.stringify({ ...current, reasoningEffort }, null, 2), "utf8");
|
|
3062
3202
|
} catch {
|
|
3063
3203
|
}
|
|
3064
3204
|
}
|
|
3065
|
-
var sessionsDir = () =>
|
|
3205
|
+
var sessionsDir = () => join6(process.cwd(), BEECORK, "sessions");
|
|
3066
3206
|
async function saveSession(messages) {
|
|
3067
3207
|
try {
|
|
3068
|
-
const
|
|
3069
|
-
await
|
|
3070
|
-
const
|
|
3071
|
-
const tmp = `${
|
|
3072
|
-
await
|
|
3208
|
+
const dir2 = sessionsDir();
|
|
3209
|
+
await mkdir5(dir2, { recursive: true });
|
|
3210
|
+
const file2 = join6(dir2, `${Date.now()}.json`);
|
|
3211
|
+
const tmp = `${file2}.tmp`;
|
|
3212
|
+
await writeFile4(tmp, JSON.stringify(messages), "utf8");
|
|
3073
3213
|
await chmod2(tmp, 384).catch(() => {
|
|
3074
3214
|
});
|
|
3075
|
-
await rename2(tmp,
|
|
3076
|
-
await pruneSessions(
|
|
3215
|
+
await rename2(tmp, file2);
|
|
3216
|
+
await pruneSessions(dir2).catch(() => {
|
|
3077
3217
|
});
|
|
3078
3218
|
} catch {
|
|
3079
3219
|
}
|
|
3080
3220
|
}
|
|
3081
3221
|
var MAX_SESSIONS = 50;
|
|
3082
|
-
async function pruneSessions(
|
|
3083
|
-
const files = (await readdir3(
|
|
3222
|
+
async function pruneSessions(dir2) {
|
|
3223
|
+
const files = (await readdir3(dir2)).filter((f) => f.endsWith(".json"));
|
|
3084
3224
|
if (files.length <= MAX_SESSIONS) return;
|
|
3085
|
-
for (const f of files.sort().slice(0, files.length - MAX_SESSIONS)) await unlink(
|
|
3225
|
+
for (const f of files.sort().slice(0, files.length - MAX_SESSIONS)) await unlink(join6(dir2, f)).catch(() => {
|
|
3086
3226
|
});
|
|
3087
3227
|
}
|
|
3088
3228
|
function sanitizeSession(raw) {
|
|
@@ -3119,10 +3259,10 @@ function dropIncompleteToolTail(messages) {
|
|
|
3119
3259
|
}
|
|
3120
3260
|
return messages;
|
|
3121
3261
|
}
|
|
3122
|
-
async function readSession(
|
|
3262
|
+
async function readSession(file2) {
|
|
3123
3263
|
try {
|
|
3124
|
-
const path =
|
|
3125
|
-
const parsed = sanitizeSession(JSON.parse(await
|
|
3264
|
+
const path = join6(sessionsDir(), file2);
|
|
3265
|
+
const parsed = sanitizeSession(JSON.parse(await readFile5(path, "utf8")));
|
|
3126
3266
|
await chmod2(path, 384).catch(() => {
|
|
3127
3267
|
});
|
|
3128
3268
|
return parsed;
|
|
@@ -3158,11 +3298,11 @@ async function listSessions() {
|
|
|
3158
3298
|
return [];
|
|
3159
3299
|
}
|
|
3160
3300
|
}
|
|
3161
|
-
async function loadSession(
|
|
3162
|
-
return await readSession(
|
|
3301
|
+
async function loadSession(file2) {
|
|
3302
|
+
return await readSession(file2) ?? [];
|
|
3163
3303
|
}
|
|
3164
3304
|
function projectApprovalsPath() {
|
|
3165
|
-
return
|
|
3305
|
+
return join6(homedir6(), BEECORK, "project-approvals.json");
|
|
3166
3306
|
}
|
|
3167
3307
|
async function loadProjectApprovals() {
|
|
3168
3308
|
const all = await readJsonFile(projectApprovalsPath());
|
|
@@ -3171,14 +3311,14 @@ async function loadProjectApprovals() {
|
|
|
3171
3311
|
}
|
|
3172
3312
|
async function addProjectApproval(tool) {
|
|
3173
3313
|
try {
|
|
3174
|
-
const
|
|
3175
|
-
await
|
|
3176
|
-
const all = await readJsonFile(
|
|
3314
|
+
const file2 = projectApprovalsPath();
|
|
3315
|
+
await mkdir5(dirname4(file2), { recursive: true });
|
|
3316
|
+
const all = await readJsonFile(file2) ?? {};
|
|
3177
3317
|
const list = new Set(Array.isArray(all[projectRoot]) ? all[projectRoot] : []);
|
|
3178
3318
|
list.add(tool);
|
|
3179
3319
|
all[projectRoot] = [...list];
|
|
3180
|
-
await
|
|
3181
|
-
await chmod2(
|
|
3320
|
+
await writeFile4(file2, JSON.stringify(all, null, 2), "utf8");
|
|
3321
|
+
await chmod2(file2, 384).catch(() => {
|
|
3182
3322
|
});
|
|
3183
3323
|
} catch {
|
|
3184
3324
|
}
|
|
@@ -3228,7 +3368,7 @@ async function askApproval(ask, call, reason, offerAlways = true) {
|
|
|
3228
3368
|
console.log(color.yellow(` edit ${stripControl(String(args.path ?? ""))}:`));
|
|
3229
3369
|
console.log(diffPreview(lineDiff(stripControl(String(args.old_text ?? "")), stripControl(String(args.new_text ?? "")))));
|
|
3230
3370
|
} else if (call.function.name === "write_file") {
|
|
3231
|
-
const existing = (await
|
|
3371
|
+
const existing = (await readFile6(resolveInRoot(String(args.path ?? ".")).abs, "utf8").catch(() => "")).slice(0, 2e5);
|
|
3232
3372
|
console.log(color.yellow(` write ${stripControl(String(args.path ?? ""))} ${existing ? "(overwrite)" : "(new file)"}:`));
|
|
3233
3373
|
console.log(diffPreview(lineDiff(stripControl(existing), stripControl(String(args.content ?? "")))));
|
|
3234
3374
|
} else {
|
|
@@ -3890,7 +4030,7 @@ function stopChrome() {
|
|
|
3890
4030
|
var chromeEnabled = () => config.statuslineEnabled && !!process.stdout.isTTY;
|
|
3891
4031
|
|
|
3892
4032
|
// src/commands.ts
|
|
3893
|
-
import { writeFile as
|
|
4033
|
+
import { writeFile as writeFile5, mkdir as mkdir6, chmod as chmod3 } from "node:fs/promises";
|
|
3894
4034
|
async function pick(opts) {
|
|
3895
4035
|
if (chromeEnabled()) {
|
|
3896
4036
|
const v = await chromePick(opts.items.map((it) => ({ label: it.label, hint: it.hint, value: it.value })), opts.initial, opts.title);
|
|
@@ -4015,15 +4155,15 @@ async function handleCommand(input, messages) {
|
|
|
4015
4155
|
replayConversation(restored);
|
|
4016
4156
|
console.log(color.green(`\u2191 resumed ${restored.length} messages \u2014 the bee has this context now. Continue below.`) + "\n");
|
|
4017
4157
|
} else if (cmd === "/good" || cmd === "/bad") {
|
|
4018
|
-
const
|
|
4158
|
+
const dir2 = cmd === "/bad" ? "eval/failures" : "eval/good";
|
|
4019
4159
|
try {
|
|
4020
|
-
await
|
|
4021
|
-
const
|
|
4022
|
-
await
|
|
4023
|
-
await chmod3(
|
|
4160
|
+
await mkdir6(dir2, { recursive: true });
|
|
4161
|
+
const file2 = `${dir2}/${Date.now()}.json`;
|
|
4162
|
+
await writeFile5(file2, JSON.stringify({ rating: cmd.slice(1), model: state.model, messages }, null, 2), "utf8");
|
|
4163
|
+
await chmod3(file2, 384).catch(() => {
|
|
4024
4164
|
});
|
|
4025
4165
|
console.log(
|
|
4026
|
-
color.cyan(`saved this conversation \u2192 ${
|
|
4166
|
+
color.cyan(`saved this conversation \u2192 ${file2}`) + (cmd === "/bad" ? " (turn it into an eval task later)" : "") + "\n"
|
|
4027
4167
|
);
|
|
4028
4168
|
} catch (err) {
|
|
4029
4169
|
console.log(color.red(`couldn't save: ${err.message}`) + "\n");
|
|
@@ -4207,7 +4347,7 @@ ${instr.trusted}`;
|
|
|
4207
4347
|
saved = true;
|
|
4208
4348
|
try {
|
|
4209
4349
|
if (config.traceFile) {
|
|
4210
|
-
await
|
|
4350
|
+
await writeFile6(config.traceFile, JSON.stringify(trace), "utf8");
|
|
4211
4351
|
await chmod4(config.traceFile, 384).catch(() => {
|
|
4212
4352
|
});
|
|
4213
4353
|
} else if (messages.length > 1) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beecork",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.2",
|
|
4
4
|
"description": "beecork — a from-scratch CLI coding agent: multi-model (OpenRouter), BYOK, path-confined tools, built part by part.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/",
|
|
12
|
-
"skills/"
|
|
12
|
+
"skills/",
|
|
13
|
+
"skeleton/"
|
|
13
14
|
],
|
|
14
15
|
"engines": {
|
|
15
16
|
"node": ">=20.12"
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Beecork Skeleton — local inbox (bridge). SHIPPED WITH beecork and auto-started
|
|
2
|
+
// by it (src/skeleton.ts), so the user never runs this by hand. It can still be run
|
|
3
|
+
// directly (`node skeleton/bridge.mjs`) for development.
|
|
4
|
+
//
|
|
5
|
+
// Receives signals POSTed by the extension, keeps a *bounded* rolling window of the
|
|
6
|
+
// most recent ones, and mirrors that window to dev-signals.jsonl (one JSON object per
|
|
7
|
+
// line). Writes are atomic (temp file + rename) so a reader never sees a half file.
|
|
8
|
+
// No dependencies — Node built-ins only.
|
|
9
|
+
//
|
|
10
|
+
// Lifecycle notes (what makes it safe for beecork to own):
|
|
11
|
+
// - Single instance: a second copy racing for the port exits(0) quietly on EADDRINUSE
|
|
12
|
+
// instead of crashing — so parallel beecork sessions share ONE inbox.
|
|
13
|
+
// - Self-tidying: exits itself after IDLE_MS with no traffic and no reads, so an
|
|
14
|
+
// auto-started bridge can't linger forever after the extension/browser is gone.
|
|
15
|
+
// - Fixed home: reads/writes under BEECORK_SKELETON_HOME (beecork points this at
|
|
16
|
+
// ~/.beecork/skeleton) instead of whatever cwd it was launched from.
|
|
17
|
+
|
|
18
|
+
import http from "node:http";
|
|
19
|
+
import { writeFile, readFile, rename } from "node:fs/promises";
|
|
20
|
+
import { resolve } from "node:path";
|
|
21
|
+
import { randomBytes } from "node:crypto";
|
|
22
|
+
|
|
23
|
+
const PORT = Number(process.env.BEECORK_SKELETON_PORT) || 8317;
|
|
24
|
+
const HOME = process.env.BEECORK_SKELETON_HOME || process.cwd();
|
|
25
|
+
const FILE = resolve(HOME, "dev-signals.jsonl");
|
|
26
|
+
const TMP = FILE + ".tmp";
|
|
27
|
+
const TOKEN_FILE = resolve(HOME, ".beecork-token");
|
|
28
|
+
const MAX = 1000; // keep at most this many recent signals
|
|
29
|
+
const IDLE_MS = 60 * 60 * 1000; // self-shutdown after an hour with zero activity
|
|
30
|
+
|
|
31
|
+
// Pairing token: the extension must present this to write. Blocks any other local
|
|
32
|
+
// process or malicious web page from POSTing fake signals to your agent. Generated
|
|
33
|
+
// once and persisted; the extension fetches it via /pair so the user never sees it.
|
|
34
|
+
let TOKEN;
|
|
35
|
+
try {
|
|
36
|
+
TOKEN = (await readFile(TOKEN_FILE, "utf8")).trim();
|
|
37
|
+
if (!TOKEN) throw new Error("empty token file");
|
|
38
|
+
} catch {
|
|
39
|
+
TOKEN = randomBytes(24).toString("hex");
|
|
40
|
+
await writeFile(TOKEN_FILE, TOKEN + "\n", { mode: 0o600 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let buffer = []; // rolling in-memory window of recent signal lines; the file mirrors it.
|
|
44
|
+
const watchRequests = new Map(); // origin beecork asked to watch → expiry (ms)
|
|
45
|
+
let lastActivity = Date.now(); // reset on every request; drives idle self-shutdown
|
|
46
|
+
|
|
47
|
+
// Load any existing signals so a bridge restart keeps recent context.
|
|
48
|
+
try {
|
|
49
|
+
const prior = await readFile(FILE, "utf8");
|
|
50
|
+
buffer = prior.split("\n").filter(Boolean).slice(-MAX);
|
|
51
|
+
} catch {
|
|
52
|
+
/* no file yet — start empty */
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Persist the bounded window atomically. Writes are serialized through one chain so
|
|
56
|
+
// two concurrent ingests can't clobber the temp file mid-write.
|
|
57
|
+
let chain = Promise.resolve();
|
|
58
|
+
function persist() {
|
|
59
|
+
chain = chain
|
|
60
|
+
.then(async () => {
|
|
61
|
+
await writeFile(TMP, buffer.length ? buffer.join("\n") + "\n" : "");
|
|
62
|
+
await rename(TMP, FILE);
|
|
63
|
+
})
|
|
64
|
+
.catch((e) => console.error("[skeleton] persist failed:", e));
|
|
65
|
+
return chain;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A web page's fetch carries an http(s) Origin; refuse those so a page you visit can't
|
|
69
|
+
// read your captured app data or drive your extension. beecork's Node fetch has no web
|
|
70
|
+
// origin and passes. No CORS header is set on these routes on purpose.
|
|
71
|
+
const fromWebPage = (req) => /^https?:\/\//i.test(req.headers.origin || "");
|
|
72
|
+
|
|
73
|
+
const server = http.createServer((req, res) => {
|
|
74
|
+
lastActivity = Date.now();
|
|
75
|
+
|
|
76
|
+
// Liveness + identity marker: lets beecork tell OUR bridge apart from some other
|
|
77
|
+
// program that happens to hold the port, so it never spawns a duplicate or talks to
|
|
78
|
+
// a stranger. Non-sensitive, but web-origin-gated like everything else.
|
|
79
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
80
|
+
if (fromWebPage(req)) return void res.writeHead(403).end("forbidden");
|
|
81
|
+
return void res
|
|
82
|
+
.writeHead(200, { "Content-Type": "application/json" })
|
|
83
|
+
.end(JSON.stringify({ skeleton: true, port: PORT, signals: buffer.length }));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Auto-pairing: hand the token to the extension so the user never touches it.
|
|
87
|
+
if (req.method === "GET" && req.url === "/pair") {
|
|
88
|
+
if (fromWebPage(req)) return void res.writeHead(403).end("forbidden");
|
|
89
|
+
return void res.writeHead(200, { "Content-Type": "text/plain" }).end(TOKEN);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A distilled read for the agent: recent signals, filtered. ?kind=&since=<epoch-ms>&limit=
|
|
93
|
+
if (req.method === "GET" && req.url.startsWith("/signals")) {
|
|
94
|
+
if (fromWebPage(req)) return void res.writeHead(403).end("forbidden");
|
|
95
|
+
const q = new URL(req.url, "http://localhost").searchParams;
|
|
96
|
+
const kind = q.get("kind");
|
|
97
|
+
const since = Number(q.get("since")) || 0;
|
|
98
|
+
const limit = Math.min(Math.max(Number(q.get("limit")) || 50, 1), 1000);
|
|
99
|
+
// Scope to one or more origins (comma-separated) so a project sees only its own site's signals.
|
|
100
|
+
const origins = (q.get("origin") || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
101
|
+
let items = buffer.map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
102
|
+
if (kind && kind !== "all") items = items.filter((s) => s.kind === kind);
|
|
103
|
+
if (since) items = items.filter((s) => (s.ts || 0) >= since);
|
|
104
|
+
// Match the TAB the signal came from (s.page) when present, else the request/resource url. Keying
|
|
105
|
+
// on the tab origin is what makes scoping correct for cross-origin API calls a page makes.
|
|
106
|
+
if (origins.length) items = items.filter((s) => origins.some((o) => String(s.page || s.url || "").startsWith(o)));
|
|
107
|
+
items = items.slice(-limit);
|
|
108
|
+
return void res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ signals: items }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Reverse channel: beecork asks for a site to be watched. The extension honors it
|
|
112
|
+
// only for sites the user already approved.
|
|
113
|
+
if (req.method === "POST" && req.url === "/request-watch") {
|
|
114
|
+
if (fromWebPage(req)) return void res.writeHead(403).end("forbidden");
|
|
115
|
+
let body = "";
|
|
116
|
+
req.on("data", (c) => (body += c));
|
|
117
|
+
req.on("end", () => {
|
|
118
|
+
try {
|
|
119
|
+
const { origin: site, ttlMs } = JSON.parse(body || "{}");
|
|
120
|
+
if (site) watchRequests.set(String(site), Date.now() + (Number(ttlMs) || 10 * 60 * 1000));
|
|
121
|
+
res.writeHead(200).end("ok");
|
|
122
|
+
} catch {
|
|
123
|
+
res.writeHead(400).end("bad json");
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (req.method === "GET" && req.url.startsWith("/watch-requests")) {
|
|
129
|
+
if (fromWebPage(req)) return void res.writeHead(403).end("forbidden");
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
for (const [site, exp] of watchRequests) if (exp < now) watchRequests.delete(site); // expire
|
|
132
|
+
return void res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ origins: [...watchRequests.keys()] }));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Permissive CORS so the extension's service worker can POST to /ingest.
|
|
136
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
137
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
138
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
139
|
+
if (req.method === "OPTIONS") return void res.writeHead(204).end();
|
|
140
|
+
|
|
141
|
+
if (req.method === "POST" && req.url === "/ingest") {
|
|
142
|
+
let body = "";
|
|
143
|
+
req.on("data", (c) => (body += c));
|
|
144
|
+
req.on("end", async () => {
|
|
145
|
+
try {
|
|
146
|
+
const signal = JSON.parse(body);
|
|
147
|
+
if (signal.token !== TOKEN) { res.writeHead(401).end("unauthorized"); return; } // token-gated
|
|
148
|
+
delete signal.token; // never persist the token itself
|
|
149
|
+
buffer.push(JSON.stringify(signal));
|
|
150
|
+
if (buffer.length > MAX) buffer.splice(0, buffer.length - MAX); // bound it
|
|
151
|
+
await persist();
|
|
152
|
+
console.log(`[skeleton] ${signal.kind}: ${String(signal.text || "").slice(0, 200)}`);
|
|
153
|
+
res.writeHead(200).end("ok");
|
|
154
|
+
} catch {
|
|
155
|
+
res.writeHead(400).end("bad json");
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
res.writeHead(404).end();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Single-instance: if another bridge already holds the port (a parallel beecork
|
|
164
|
+
// session started it), step aside quietly rather than crash.
|
|
165
|
+
server.on("error", (e) => {
|
|
166
|
+
if (e && e.code === "EADDRINUSE") process.exit(0);
|
|
167
|
+
console.error("[skeleton]", e);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Self-tidying: an auto-started bridge shouldn't outlive its usefulness. When the
|
|
172
|
+
// extension is loaded it polls every ~30s, so this only fires once the browser side
|
|
173
|
+
// is truly gone AND nothing has read for an hour.
|
|
174
|
+
const idle = setInterval(() => {
|
|
175
|
+
if (Date.now() - lastActivity > IDLE_MS) { server.close(); process.exit(0); }
|
|
176
|
+
}, 5 * 60 * 1000);
|
|
177
|
+
idle.unref(); // don't let the timer alone keep the process alive
|
|
178
|
+
|
|
179
|
+
server.listen(PORT, "127.0.0.1", () => {
|
|
180
|
+
console.log(`[skeleton] inbox listening on http://localhost:${PORT} (home: ${HOME})`);
|
|
181
|
+
});
|
|
@@ -21,6 +21,23 @@ the real errors:
|
|
|
21
21
|
|
|
22
22
|
Pull it **on demand** while debugging — don't call it in a loop or on every turn.
|
|
23
23
|
|
|
24
|
+
## Seeing only THIS project's signals (scoping)
|
|
25
|
+
|
|
26
|
+
The inbox is **shared** across every project that uses beecork, so signals from other apps you
|
|
27
|
+
have open can show up too. Scope to the project you're in:
|
|
28
|
+
|
|
29
|
+
- Pass **`origin`** — the site(s) this project runs on, e.g.
|
|
30
|
+
`read_dev_signals({ origin: "http://localhost:8000" })`, or comma-separated for several frontends:
|
|
31
|
+
`{ origin: "http://localhost:3000,http://localhost:3001" }`. Get it from the project itself (dev
|
|
32
|
+
script / framework config) or from the user.
|
|
33
|
+
- **Production sites are remembered per folder; localhost is not.** Once you `watch_site` a production
|
|
34
|
+
URL, beecork saves that origin to `.beecork/skeleton.json` and later `read_dev_signals` calls in that
|
|
35
|
+
folder auto-scope to it. **localhost is deliberately never remembered** — dev ports get reused across
|
|
36
|
+
projects (`:8000` might be a different app tomorrow), so a saved localhost binding would mis-attribute
|
|
37
|
+
another app's signals. Pass the localhost `origin` each session instead (you know it from the project).
|
|
38
|
+
- If you call it unscoped and the feed spans multiple sites, the result lists the origins so you (or the
|
|
39
|
+
user) can pick this project's.
|
|
40
|
+
|
|
24
41
|
## Watching an on-demand / production site
|
|
25
42
|
|
|
26
43
|
Localhost/dev sites the user approved are watched automatically. A **production** (or
|
|
@@ -32,16 +49,18 @@ its own). Then have the user reproduce the issue (or open the site) and call
|
|
|
32
49
|
|
|
33
50
|
## If it says "not connected" — one-time setup
|
|
34
51
|
|
|
35
|
-
|
|
52
|
+
**beecork starts the local inbox itself** — the first time you call `read_dev_signals` or
|
|
53
|
+
`watch_site`, beecork auto-starts the bundled bridge (a `127.0.0.1:8317` inbox) in the
|
|
54
|
+
background and shares it across sessions. So there is **no bridge to run by hand**; the only
|
|
55
|
+
one-time step is loading the Chrome extension:
|
|
36
56
|
|
|
37
|
-
1. **
|
|
38
|
-
folder and leave it running. It listens on `localhost:8317`.
|
|
39
|
-
2. **Load the extension:** Chrome → `chrome://extensions` → enable **Developer mode** →
|
|
57
|
+
1. **Load the extension:** Chrome → `chrome://extensions` → enable **Developer mode** →
|
|
40
58
|
**Load unpacked** → select the `beecork-extension/extension` folder → pin the icon.
|
|
41
|
-
|
|
59
|
+
2. **Connect + approve:** click the icon (it auto-connects — no token to paste), tick
|
|
42
60
|
**Capture enabled**, open the app in a tab, and click **Pair this site**.
|
|
43
61
|
|
|
44
|
-
Then call `read_dev_signals` again.
|
|
62
|
+
Then call `read_dev_signals` again. (If beecork reports the inbox port is held by another
|
|
63
|
+
program, free port 8317 or set `BEECORK_DEV_SIGNALS_URL` to a different inbox.)
|
|
45
64
|
|
|
46
65
|
## Empty result
|
|
47
66
|
|