beecork 2.8.1 → 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 +173 -110
- package/package.json +1 -1
- package/skeleton/bridge.mjs +5 -0
- package/skills/browser-signals.md +17 -0
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
1201
|
import { spawn as spawn4 } from "node:child_process";
|
|
1202
|
-
import { join as
|
|
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
|
}
|
|
@@ -1747,6 +1747,53 @@ async function doEnsure() {
|
|
|
1747
1747
|
return { up: false, started: true, pid, reason: "spawn-failed" };
|
|
1748
1748
|
}
|
|
1749
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
|
+
|
|
1750
1797
|
// src/tools.ts
|
|
1751
1798
|
function runShell(command, opts) {
|
|
1752
1799
|
return new Promise((resolve2, reject) => {
|
|
@@ -1818,7 +1865,7 @@ function runShell(command, opts) {
|
|
|
1818
1865
|
var fail = (verb, err) => `Error ${verb}: ${err.message}`;
|
|
1819
1866
|
async function atomicWrite(abs, content) {
|
|
1820
1867
|
const tmp = `${abs}.beecork-${process.pid}.tmp`;
|
|
1821
|
-
await
|
|
1868
|
+
await writeFile3(tmp, content, "utf8");
|
|
1822
1869
|
try {
|
|
1823
1870
|
await chmod(tmp, (await stat(abs)).mode);
|
|
1824
1871
|
} catch {
|
|
@@ -1847,8 +1894,8 @@ function lineOffsets(text) {
|
|
|
1847
1894
|
for (let i = 0; i < text.length; i++) if (text[i] === "\n") starts.push(i + 1);
|
|
1848
1895
|
return starts;
|
|
1849
1896
|
}
|
|
1850
|
-
function matchWhitespace(
|
|
1851
|
-
const fileLines =
|
|
1897
|
+
function matchWhitespace(file2, oldText, newText) {
|
|
1898
|
+
const fileLines = file2.split("\n");
|
|
1852
1899
|
const oldLines = oldText.split("\n");
|
|
1853
1900
|
const n = oldLines.length;
|
|
1854
1901
|
const trim = (l) => l.trim();
|
|
@@ -1894,15 +1941,15 @@ function matchWhitespace(file, oldText, newText) {
|
|
|
1894
1941
|
if (mode === "strip") return l.startsWith(shift) ? l.slice(shift.length) : l;
|
|
1895
1942
|
return l;
|
|
1896
1943
|
}).join("\n");
|
|
1897
|
-
const offs = lineOffsets(
|
|
1944
|
+
const offs = lineOffsets(file2);
|
|
1898
1945
|
const start = offs[s];
|
|
1899
|
-
const end = s + n < offs.length ? offs[s + n] - 1 :
|
|
1946
|
+
const end = s + n < offs.length ? offs[s + n] - 1 : file2.length;
|
|
1900
1947
|
return { ok: true, start, end, after: reindented, healedVia: "whitespace" };
|
|
1901
1948
|
}
|
|
1902
|
-
function closestRegion(
|
|
1949
|
+
function closestRegion(file2, oldText) {
|
|
1903
1950
|
const anchor = oldText.split("\n").map((l) => l.trim()).find((l) => l !== "");
|
|
1904
1951
|
if (!anchor) return void 0;
|
|
1905
|
-
const fileLines =
|
|
1952
|
+
const fileLines = file2.split("\n");
|
|
1906
1953
|
const fmt = (i) => `${String(i + 1).padStart(5)} ${fileLines[i]}`;
|
|
1907
1954
|
const exact = [];
|
|
1908
1955
|
for (let i = 0; i < fileLines.length && exact.length < 3; i++) {
|
|
@@ -1923,23 +1970,23 @@ function closestRegion(file, oldText) {
|
|
|
1923
1970
|
}
|
|
1924
1971
|
return best >= 0 && bestScore >= Math.max(2, Math.ceil(words.length / 2)) ? fmt(best) : void 0;
|
|
1925
1972
|
}
|
|
1926
|
-
function resolveEdit(
|
|
1973
|
+
function resolveEdit(file2, oldText, newText) {
|
|
1927
1974
|
if (oldText === "") return { ok: false, reason: "not_found" };
|
|
1928
|
-
const exact = allIndexOf(
|
|
1975
|
+
const exact = allIndexOf(file2, oldText);
|
|
1929
1976
|
if (exact.length === 1) return { ok: true, start: exact[0], end: exact[0] + oldText.length, after: newText, healedVia: "exact" };
|
|
1930
1977
|
if (exact.length > 1) return { ok: false, reason: "ambiguous", count: exact.length };
|
|
1931
1978
|
const strippedOld = stripReadPrefix(oldText);
|
|
1932
1979
|
if (strippedOld !== null && strippedOld !== oldText) {
|
|
1933
|
-
const hits = allIndexOf(
|
|
1980
|
+
const hits = allIndexOf(file2, strippedOld);
|
|
1934
1981
|
if (hits.length === 1) {
|
|
1935
1982
|
const strippedNew = newText.split("\n").map((l) => l.replace(READ_PREFIX, "")).join("\n");
|
|
1936
1983
|
return { ok: true, start: hits[0], end: hits[0] + strippedOld.length, after: strippedNew, healedVia: "prefix" };
|
|
1937
1984
|
}
|
|
1938
1985
|
if (hits.length > 1) return { ok: false, reason: "ambiguous", count: hits.length };
|
|
1939
1986
|
}
|
|
1940
|
-
const ws = matchWhitespace(
|
|
1987
|
+
const ws = matchWhitespace(file2, oldText, newText);
|
|
1941
1988
|
if (ws) return ws;
|
|
1942
|
-
return { ok: false, reason: "not_found", closest: closestRegion(
|
|
1989
|
+
return { ok: false, reason: "not_found", closest: closestRegion(file2, oldText) };
|
|
1943
1990
|
}
|
|
1944
1991
|
var todos = [];
|
|
1945
1992
|
function httpGet(rawUrl, maxBytes, signal) {
|
|
@@ -2033,7 +2080,7 @@ async function walkTree(abs, prefix, items, cap) {
|
|
|
2033
2080
|
const e = kept[i];
|
|
2034
2081
|
const last = i === kept.length - 1;
|
|
2035
2082
|
items.push({ prefix: prefix + (last ? "\u2514\u2500 " : "\u251C\u2500 "), name: e.name + (e.isDirectory() ? "/" : ""), isDir: e.isDirectory() });
|
|
2036
|
-
if (e.isDirectory()) await walkTree(
|
|
2083
|
+
if (e.isDirectory()) await walkTree(join5(abs, e.name), prefix + (last ? " " : "\u2502 "), items, cap);
|
|
2037
2084
|
}
|
|
2038
2085
|
}
|
|
2039
2086
|
function parseRange(args, defLimit) {
|
|
@@ -2166,7 +2213,7 @@ var toolDefs = [
|
|
|
2166
2213
|
const MAX = config.searchMaxResults;
|
|
2167
2214
|
const deadline = Date.now() + config.searchTimeoutMs;
|
|
2168
2215
|
let truncated = false;
|
|
2169
|
-
async function walk(
|
|
2216
|
+
async function walk(dir2) {
|
|
2170
2217
|
if (results.length >= MAX) return;
|
|
2171
2218
|
if (Date.now() > deadline) {
|
|
2172
2219
|
truncated = true;
|
|
@@ -2174,7 +2221,7 @@ var toolDefs = [
|
|
|
2174
2221
|
}
|
|
2175
2222
|
let entries;
|
|
2176
2223
|
try {
|
|
2177
|
-
entries = await readdir2(
|
|
2224
|
+
entries = await readdir2(dir2, { withFileTypes: true });
|
|
2178
2225
|
} catch {
|
|
2179
2226
|
return;
|
|
2180
2227
|
}
|
|
@@ -2185,7 +2232,7 @@ var toolDefs = [
|
|
|
2185
2232
|
return;
|
|
2186
2233
|
}
|
|
2187
2234
|
if (IGNORE.has(e.name)) continue;
|
|
2188
|
-
const full = `${
|
|
2235
|
+
const full = `${dir2}/${e.name}`;
|
|
2189
2236
|
if (e.isDirectory()) {
|
|
2190
2237
|
await walk(full);
|
|
2191
2238
|
} else if (e.isFile()) {
|
|
@@ -2194,7 +2241,7 @@ var toolDefs = [
|
|
|
2194
2241
|
if (info && info.size > config.searchMaxFileBytes) continue;
|
|
2195
2242
|
let lines;
|
|
2196
2243
|
try {
|
|
2197
|
-
lines = (await
|
|
2244
|
+
lines = (await readFile4(full, "utf8")).split("\n");
|
|
2198
2245
|
} catch {
|
|
2199
2246
|
continue;
|
|
2200
2247
|
}
|
|
@@ -2262,7 +2309,7 @@ var toolDefs = [
|
|
|
2262
2309
|
run: async (args) => {
|
|
2263
2310
|
try {
|
|
2264
2311
|
const { abs } = resolveInRoot(String(args.path ?? "."));
|
|
2265
|
-
const original = await
|
|
2312
|
+
const original = await readFile4(abs, "utf8");
|
|
2266
2313
|
const res = resolveEdit(original, String(args.old_text ?? ""), String(args.new_text ?? ""));
|
|
2267
2314
|
if (!res.ok) {
|
|
2268
2315
|
if (res.reason === "ambiguous") {
|
|
@@ -2465,21 +2512,21 @@ ${list}`;
|
|
|
2465
2512
|
},
|
|
2466
2513
|
run: async (args) => {
|
|
2467
2514
|
try {
|
|
2468
|
-
const
|
|
2469
|
-
await
|
|
2470
|
-
const
|
|
2515
|
+
const dir2 = join5(process.cwd(), ".beecork");
|
|
2516
|
+
await mkdir4(dir2, { recursive: true });
|
|
2517
|
+
const file2 = join5(dir2, "memory.md");
|
|
2471
2518
|
const fact = String(args.fact).trim();
|
|
2472
2519
|
if (!fact) return 'Error: remember needs a non-empty "fact".';
|
|
2473
2520
|
let current = "";
|
|
2474
2521
|
try {
|
|
2475
|
-
current = await
|
|
2522
|
+
current = await readFile4(file2, "utf8");
|
|
2476
2523
|
} catch {
|
|
2477
2524
|
}
|
|
2478
2525
|
if (current.length + fact.length + 3 > config.memoryMaxChars) {
|
|
2479
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.`;
|
|
2480
2527
|
}
|
|
2481
|
-
if (!current) await
|
|
2482
|
-
await appendFile(
|
|
2528
|
+
if (!current) await writeFile3(file2, "# beecork memory\n\n", "utf8");
|
|
2529
|
+
await appendFile(file2, `- ${fact}
|
|
2483
2530
|
`, "utf8");
|
|
2484
2531
|
return `Remembered: ${fact}`;
|
|
2485
2532
|
} catch (err) {
|
|
@@ -2580,13 +2627,14 @@ ${skill.content}`;
|
|
|
2580
2627
|
},
|
|
2581
2628
|
{
|
|
2582
2629
|
name: "read_dev_signals",
|
|
2583
|
-
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.",
|
|
2584
2631
|
parameters: {
|
|
2585
2632
|
type: "object",
|
|
2586
2633
|
properties: {
|
|
2587
2634
|
kind: { type: "string", description: 'Filter: "network", "console", "pageError", "log", or "all" (default all).' },
|
|
2588
2635
|
since_minutes: { type: "number", description: "Only signals from the last N minutes (optional)." },
|
|
2589
|
-
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." }
|
|
2590
2638
|
},
|
|
2591
2639
|
required: []
|
|
2592
2640
|
},
|
|
@@ -2596,9 +2644,12 @@ ${skill.content}`;
|
|
|
2596
2644
|
const kind = args.kind ? String(args.kind) : "";
|
|
2597
2645
|
const limit = Math.min(Math.max(Number(args.limit) || 30, 1), 200);
|
|
2598
2646
|
const sinceMin = Number(args.since_minutes) || 0;
|
|
2599
|
-
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) });
|
|
2600
2650
|
if (kind && kind !== "all") params.set("kind", kind);
|
|
2601
2651
|
if (sinceMin > 0) params.set("since", String(Date.now() - sinceMin * 6e4));
|
|
2652
|
+
if (scope.length) params.set("origin", scope.join(","));
|
|
2602
2653
|
const timeout = AbortSignal.timeout(Math.min(config.webTimeoutMs, 5e3));
|
|
2603
2654
|
let data;
|
|
2604
2655
|
try {
|
|
@@ -2610,9 +2661,12 @@ ${skill.content}`;
|
|
|
2610
2661
|
return DEV_SIGNALS_SETUP;
|
|
2611
2662
|
}
|
|
2612
2663
|
const now = Date.now();
|
|
2613
|
-
|
|
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(", ")}` : "";
|
|
2614
2668
|
if (signals.length === 0) {
|
|
2615
|
-
return `The inbox is running${ens.started ? " (I just started it)" : ""}, but no ${kind && kind !== "all" ? `"${kind}" ` : ""}signals were captured${sinceMin ? ` in the last ${sinceMin} min` : ""} yet.
|
|
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.
|
|
2616
2670
|
|
|
2617
2671
|
If the Beecork Skeleton extension isn't loaded yet, connect it:
|
|
2618
2672
|
${EXTENSION_STEPS}
|
|
@@ -2625,8 +2679,15 @@ Already connected? Reproduce the issue in the browser (or open the app), then ca
|
|
|
2625
2679
|
const text = String(s.text ?? "").replace(/\s+/g, " ").slice(0, 300);
|
|
2626
2680
|
return `[${s.kind}] ${text}${s.url ? ` @ ${s.url}` : ""} (${ago2(s.ts)})`;
|
|
2627
2681
|
});
|
|
2628
|
-
|
|
2629
|
-
|
|
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}`;
|
|
2630
2691
|
}
|
|
2631
2692
|
},
|
|
2632
2693
|
{
|
|
@@ -2663,7 +2724,9 @@ ${lines.join("\n")}`;
|
|
|
2663
2724
|
} catch {
|
|
2664
2725
|
return DEV_SIGNALS_SETUP;
|
|
2665
2726
|
}
|
|
2666
|
-
|
|
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.`;
|
|
2667
2730
|
}
|
|
2668
2731
|
}
|
|
2669
2732
|
];
|
|
@@ -3028,49 +3091,49 @@ ${summary}` }, ...recent];
|
|
|
3028
3091
|
}
|
|
3029
3092
|
|
|
3030
3093
|
// src/memory.ts
|
|
3031
|
-
import { readFile 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";
|
|
3032
3095
|
import { homedir as homedir6 } from "node:os";
|
|
3033
|
-
import { join as
|
|
3096
|
+
import { join as join6, dirname as dirname4 } from "node:path";
|
|
3034
3097
|
var BEECORK = ".beecork";
|
|
3035
3098
|
function ancestorDirs() {
|
|
3036
3099
|
const home = homedir6();
|
|
3037
3100
|
const dirs = [];
|
|
3038
|
-
let
|
|
3039
|
-
while (
|
|
3040
|
-
dirs.push(
|
|
3041
|
-
|
|
3101
|
+
let dir2 = process.cwd();
|
|
3102
|
+
while (dir2 !== home && dir2 !== dirname4(dir2)) {
|
|
3103
|
+
dirs.push(dir2);
|
|
3104
|
+
dir2 = dirname4(dir2);
|
|
3042
3105
|
}
|
|
3043
3106
|
return dirs.reverse();
|
|
3044
3107
|
}
|
|
3045
3108
|
function corkPaths() {
|
|
3046
|
-
return [
|
|
3109
|
+
return [join6(homedir6(), BEECORK, "cork.md"), ...ancestorDirs().map((d) => join6(d, "cork.md"))];
|
|
3047
3110
|
}
|
|
3048
3111
|
function beecorkPaths(name) {
|
|
3049
|
-
return [
|
|
3112
|
+
return [join6(homedir6(), BEECORK, name), ...ancestorDirs().map((d) => join6(d, BEECORK, name))];
|
|
3050
3113
|
}
|
|
3051
3114
|
function standardInstructionPaths() {
|
|
3052
|
-
return ancestorDirs().flatMap((d) => [
|
|
3115
|
+
return ancestorDirs().flatMap((d) => [join6(d, "AGENTS.md"), join6(d, "CLAUDE.md")]);
|
|
3053
3116
|
}
|
|
3054
3117
|
async function loadInstructions() {
|
|
3055
3118
|
const home = homedir6();
|
|
3056
|
-
const homeBeecork =
|
|
3119
|
+
const homeBeecork = join6(home, ".beecork");
|
|
3057
3120
|
const trusted = [];
|
|
3058
3121
|
const project = [];
|
|
3059
3122
|
const sources = [];
|
|
3060
3123
|
const MAX_FILE = 8e3;
|
|
3061
3124
|
const MAX_TOTAL = 24e3;
|
|
3062
3125
|
let total = 0;
|
|
3063
|
-
for (const
|
|
3126
|
+
for (const file2 of [...corkPaths(), ...standardInstructionPaths(), ...beecorkPaths("memory.md")]) {
|
|
3064
3127
|
try {
|
|
3065
|
-
let content = (await
|
|
3128
|
+
let content = (await readFile5(file2, "utf8")).trim();
|
|
3066
3129
|
if (!content) continue;
|
|
3067
3130
|
if (content.length > MAX_FILE) content = content.slice(0, MAX_FILE) + "\n\u2026(truncated)";
|
|
3068
3131
|
if (total + content.length > MAX_TOTAL) content = content.slice(0, Math.max(0, MAX_TOTAL - total)) + "\n\u2026(truncated)";
|
|
3069
3132
|
total += content.length;
|
|
3070
|
-
const block = `## From ${tildify(
|
|
3133
|
+
const block = `## From ${tildify(file2)}
|
|
3071
3134
|
${content}`;
|
|
3072
|
-
(
|
|
3073
|
-
sources.push(
|
|
3135
|
+
(file2.startsWith(homeBeecork) ? trusted : project).push(block);
|
|
3136
|
+
sources.push(file2);
|
|
3074
3137
|
if (total >= MAX_TOTAL) break;
|
|
3075
3138
|
} catch {
|
|
3076
3139
|
}
|
|
@@ -3079,7 +3142,7 @@ ${content}`;
|
|
|
3079
3142
|
}
|
|
3080
3143
|
async function readJsonFile(path) {
|
|
3081
3144
|
try {
|
|
3082
|
-
return JSON.parse(await
|
|
3145
|
+
return JSON.parse(await readFile5(path, "utf8"));
|
|
3083
3146
|
} catch (err) {
|
|
3084
3147
|
if (err.code !== "ENOENT") {
|
|
3085
3148
|
console.error(color.yellow(`\u26A0 ignoring malformed ${tildify(path)}: ${err.message}`));
|
|
@@ -3106,60 +3169,60 @@ async function loadSettings() {
|
|
|
3106
3169
|
return { model, reasoningEffort, alwaysAllow, projectAlwaysAllowIgnored };
|
|
3107
3170
|
}
|
|
3108
3171
|
function userConfigPath() {
|
|
3109
|
-
return
|
|
3172
|
+
return join6(homedir6(), BEECORK, "config.json");
|
|
3110
3173
|
}
|
|
3111
3174
|
async function loadUserConfig() {
|
|
3112
3175
|
return await readJsonFile(userConfigPath()) ?? {};
|
|
3113
3176
|
}
|
|
3114
3177
|
async function saveUserConfig(patch) {
|
|
3115
|
-
const
|
|
3116
|
-
await
|
|
3178
|
+
const file2 = userConfigPath();
|
|
3179
|
+
await mkdir5(dirname4(file2), { recursive: true });
|
|
3117
3180
|
const merged = { ...await loadUserConfig(), ...patch };
|
|
3118
|
-
const tmp = `${
|
|
3119
|
-
await
|
|
3181
|
+
const tmp = `${file2}.tmp`;
|
|
3182
|
+
await writeFile4(tmp, JSON.stringify(merged, null, 2), { encoding: "utf8", mode: 384 });
|
|
3120
3183
|
await chmod2(tmp, 384).catch(() => {
|
|
3121
3184
|
});
|
|
3122
|
-
await rename2(tmp,
|
|
3185
|
+
await rename2(tmp, file2);
|
|
3123
3186
|
}
|
|
3124
3187
|
async function saveModelPreference(model) {
|
|
3125
3188
|
try {
|
|
3126
|
-
const
|
|
3127
|
-
await
|
|
3128
|
-
const current = await readJsonFile(
|
|
3129
|
-
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");
|
|
3130
3193
|
} catch {
|
|
3131
3194
|
}
|
|
3132
3195
|
}
|
|
3133
3196
|
async function saveReasoningPreference(reasoningEffort) {
|
|
3134
3197
|
try {
|
|
3135
|
-
const
|
|
3136
|
-
await
|
|
3137
|
-
const current = await readJsonFile(
|
|
3138
|
-
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");
|
|
3139
3202
|
} catch {
|
|
3140
3203
|
}
|
|
3141
3204
|
}
|
|
3142
|
-
var sessionsDir = () =>
|
|
3205
|
+
var sessionsDir = () => join6(process.cwd(), BEECORK, "sessions");
|
|
3143
3206
|
async function saveSession(messages) {
|
|
3144
3207
|
try {
|
|
3145
|
-
const
|
|
3146
|
-
await
|
|
3147
|
-
const
|
|
3148
|
-
const tmp = `${
|
|
3149
|
-
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");
|
|
3150
3213
|
await chmod2(tmp, 384).catch(() => {
|
|
3151
3214
|
});
|
|
3152
|
-
await rename2(tmp,
|
|
3153
|
-
await pruneSessions(
|
|
3215
|
+
await rename2(tmp, file2);
|
|
3216
|
+
await pruneSessions(dir2).catch(() => {
|
|
3154
3217
|
});
|
|
3155
3218
|
} catch {
|
|
3156
3219
|
}
|
|
3157
3220
|
}
|
|
3158
3221
|
var MAX_SESSIONS = 50;
|
|
3159
|
-
async function pruneSessions(
|
|
3160
|
-
const files = (await readdir3(
|
|
3222
|
+
async function pruneSessions(dir2) {
|
|
3223
|
+
const files = (await readdir3(dir2)).filter((f) => f.endsWith(".json"));
|
|
3161
3224
|
if (files.length <= MAX_SESSIONS) return;
|
|
3162
|
-
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(() => {
|
|
3163
3226
|
});
|
|
3164
3227
|
}
|
|
3165
3228
|
function sanitizeSession(raw) {
|
|
@@ -3196,10 +3259,10 @@ function dropIncompleteToolTail(messages) {
|
|
|
3196
3259
|
}
|
|
3197
3260
|
return messages;
|
|
3198
3261
|
}
|
|
3199
|
-
async function readSession(
|
|
3262
|
+
async function readSession(file2) {
|
|
3200
3263
|
try {
|
|
3201
|
-
const path =
|
|
3202
|
-
const parsed = sanitizeSession(JSON.parse(await
|
|
3264
|
+
const path = join6(sessionsDir(), file2);
|
|
3265
|
+
const parsed = sanitizeSession(JSON.parse(await readFile5(path, "utf8")));
|
|
3203
3266
|
await chmod2(path, 384).catch(() => {
|
|
3204
3267
|
});
|
|
3205
3268
|
return parsed;
|
|
@@ -3235,11 +3298,11 @@ async function listSessions() {
|
|
|
3235
3298
|
return [];
|
|
3236
3299
|
}
|
|
3237
3300
|
}
|
|
3238
|
-
async function loadSession(
|
|
3239
|
-
return await readSession(
|
|
3301
|
+
async function loadSession(file2) {
|
|
3302
|
+
return await readSession(file2) ?? [];
|
|
3240
3303
|
}
|
|
3241
3304
|
function projectApprovalsPath() {
|
|
3242
|
-
return
|
|
3305
|
+
return join6(homedir6(), BEECORK, "project-approvals.json");
|
|
3243
3306
|
}
|
|
3244
3307
|
async function loadProjectApprovals() {
|
|
3245
3308
|
const all = await readJsonFile(projectApprovalsPath());
|
|
@@ -3248,14 +3311,14 @@ async function loadProjectApprovals() {
|
|
|
3248
3311
|
}
|
|
3249
3312
|
async function addProjectApproval(tool) {
|
|
3250
3313
|
try {
|
|
3251
|
-
const
|
|
3252
|
-
await
|
|
3253
|
-
const all = await readJsonFile(
|
|
3314
|
+
const file2 = projectApprovalsPath();
|
|
3315
|
+
await mkdir5(dirname4(file2), { recursive: true });
|
|
3316
|
+
const all = await readJsonFile(file2) ?? {};
|
|
3254
3317
|
const list = new Set(Array.isArray(all[projectRoot]) ? all[projectRoot] : []);
|
|
3255
3318
|
list.add(tool);
|
|
3256
3319
|
all[projectRoot] = [...list];
|
|
3257
|
-
await
|
|
3258
|
-
await chmod2(
|
|
3320
|
+
await writeFile4(file2, JSON.stringify(all, null, 2), "utf8");
|
|
3321
|
+
await chmod2(file2, 384).catch(() => {
|
|
3259
3322
|
});
|
|
3260
3323
|
} catch {
|
|
3261
3324
|
}
|
|
@@ -3305,7 +3368,7 @@ async function askApproval(ask, call, reason, offerAlways = true) {
|
|
|
3305
3368
|
console.log(color.yellow(` edit ${stripControl(String(args.path ?? ""))}:`));
|
|
3306
3369
|
console.log(diffPreview(lineDiff(stripControl(String(args.old_text ?? "")), stripControl(String(args.new_text ?? "")))));
|
|
3307
3370
|
} else if (call.function.name === "write_file") {
|
|
3308
|
-
const existing = (await
|
|
3371
|
+
const existing = (await readFile6(resolveInRoot(String(args.path ?? ".")).abs, "utf8").catch(() => "")).slice(0, 2e5);
|
|
3309
3372
|
console.log(color.yellow(` write ${stripControl(String(args.path ?? ""))} ${existing ? "(overwrite)" : "(new file)"}:`));
|
|
3310
3373
|
console.log(diffPreview(lineDiff(stripControl(existing), stripControl(String(args.content ?? "")))));
|
|
3311
3374
|
} else {
|
|
@@ -3967,7 +4030,7 @@ function stopChrome() {
|
|
|
3967
4030
|
var chromeEnabled = () => config.statuslineEnabled && !!process.stdout.isTTY;
|
|
3968
4031
|
|
|
3969
4032
|
// src/commands.ts
|
|
3970
|
-
import { writeFile as
|
|
4033
|
+
import { writeFile as writeFile5, mkdir as mkdir6, chmod as chmod3 } from "node:fs/promises";
|
|
3971
4034
|
async function pick(opts) {
|
|
3972
4035
|
if (chromeEnabled()) {
|
|
3973
4036
|
const v = await chromePick(opts.items.map((it) => ({ label: it.label, hint: it.hint, value: it.value })), opts.initial, opts.title);
|
|
@@ -4092,15 +4155,15 @@ async function handleCommand(input, messages) {
|
|
|
4092
4155
|
replayConversation(restored);
|
|
4093
4156
|
console.log(color.green(`\u2191 resumed ${restored.length} messages \u2014 the bee has this context now. Continue below.`) + "\n");
|
|
4094
4157
|
} else if (cmd === "/good" || cmd === "/bad") {
|
|
4095
|
-
const
|
|
4158
|
+
const dir2 = cmd === "/bad" ? "eval/failures" : "eval/good";
|
|
4096
4159
|
try {
|
|
4097
|
-
await
|
|
4098
|
-
const
|
|
4099
|
-
await
|
|
4100
|
-
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(() => {
|
|
4101
4164
|
});
|
|
4102
4165
|
console.log(
|
|
4103
|
-
color.cyan(`saved this conversation \u2192 ${
|
|
4166
|
+
color.cyan(`saved this conversation \u2192 ${file2}`) + (cmd === "/bad" ? " (turn it into an eval task later)" : "") + "\n"
|
|
4104
4167
|
);
|
|
4105
4168
|
} catch (err) {
|
|
4106
4169
|
console.log(color.red(`couldn't save: ${err.message}`) + "\n");
|
|
@@ -4284,7 +4347,7 @@ ${instr.trusted}`;
|
|
|
4284
4347
|
saved = true;
|
|
4285
4348
|
try {
|
|
4286
4349
|
if (config.traceFile) {
|
|
4287
|
-
await
|
|
4350
|
+
await writeFile6(config.traceFile, JSON.stringify(trace), "utf8");
|
|
4288
4351
|
await chmod4(config.traceFile, 384).catch(() => {
|
|
4289
4352
|
});
|
|
4290
4353
|
} else if (messages.length > 1) {
|
package/package.json
CHANGED
package/skeleton/bridge.mjs
CHANGED
|
@@ -96,9 +96,14 @@ const server = http.createServer((req, res) => {
|
|
|
96
96
|
const kind = q.get("kind");
|
|
97
97
|
const since = Number(q.get("since")) || 0;
|
|
98
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);
|
|
99
101
|
let items = buffer.map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
100
102
|
if (kind && kind !== "all") items = items.filter((s) => s.kind === kind);
|
|
101
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)));
|
|
102
107
|
items = items.slice(-limit);
|
|
103
108
|
return void res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ signals: items }));
|
|
104
109
|
}
|
|
@@ -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
|