beecork 2.8.1 → 2.8.3
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 +276 -117
- 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
|
|
@@ -60,6 +60,13 @@ function normalizeEffort(raw) {
|
|
|
60
60
|
const v = (raw ?? "").trim().toLowerCase();
|
|
61
61
|
return EFFORTS.includes(v) ? v : void 0;
|
|
62
62
|
}
|
|
63
|
+
function normalizeProviderSort(raw) {
|
|
64
|
+
if (raw == null) return "latency";
|
|
65
|
+
const v = raw.trim().toLowerCase();
|
|
66
|
+
if (v === "latency" || v === "price" || v === "throughput") return v;
|
|
67
|
+
if (["", "off", "none", "default", "0", "false", "no"].includes(v)) return "";
|
|
68
|
+
return "latency";
|
|
69
|
+
}
|
|
63
70
|
function parseExtra(raw) {
|
|
64
71
|
if (!raw || !raw.trim()) return {};
|
|
65
72
|
try {
|
|
@@ -95,6 +102,10 @@ var config = {
|
|
|
95
102
|
// transient API failures
|
|
96
103
|
apiTimeoutMs: num("API_TIMEOUT_MS", 18e4),
|
|
97
104
|
// per-attempt model-call timeout (generous for reasoning models)
|
|
105
|
+
// Run a batch's independent, read-only tool calls (read_file/search/list_dir/web_fetch/…) CONCURRENTLY
|
|
106
|
+
// instead of one-at-a-time — the model is told to batch independent calls, so this cashes in the win
|
|
107
|
+
// (N web_fetches take ~1× instead of N×). Mutating/approval/interactive tools always stay serial.
|
|
108
|
+
parallelTools: !["0", "false", "off", "no"].includes((process.env.PARALLEL_TOOLS ?? "").trim().toLowerCase()),
|
|
98
109
|
// Tool operational limits
|
|
99
110
|
execTimeoutMs: num("EXEC_TIMEOUT_MS", 3e4),
|
|
100
111
|
// run_bash command timeout
|
|
@@ -115,6 +126,11 @@ var config = {
|
|
|
115
126
|
// startup default; changed live via /effort
|
|
116
127
|
openRouterExtra: parseExtra(process.env.OPENROUTER_EXTRA),
|
|
117
128
|
// advanced: extra request-body params (sampling, provider routing, …)
|
|
129
|
+
// Provider routing (overridable by a `provider` block in OPENROUTER_EXTRA). The same model is served
|
|
130
|
+
// by many OpenRouter backends with very different time-to-first-token; "latency" (the default) routes
|
|
131
|
+
// to the fastest-responding one so replies start streaming right away instead of stalling on a slow
|
|
132
|
+
// provider. Set OPENROUTER_PROVIDER_SORT=off to fall back to OpenRouter's default load-balanced routing.
|
|
133
|
+
providerSort: normalizeProviderSort(process.env.OPENROUTER_PROVIDER_SORT),
|
|
118
134
|
// Background tasks (run_bash background:true → check_task / stop_task)
|
|
119
135
|
maxBackgroundTasks: num("MAX_BG_TASKS", 5),
|
|
120
136
|
// per-session cap on concurrent background commands
|
|
@@ -187,10 +203,10 @@ async function findShadowingInstalls() {
|
|
|
187
203
|
const names = process.platform === "win32" ? ["beecork.cmd", "beecork"] : ["beecork"];
|
|
188
204
|
const seenRoots = /* @__PURE__ */ new Set([running]);
|
|
189
205
|
const others = [];
|
|
190
|
-
for (const
|
|
206
|
+
for (const dir2 of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
191
207
|
for (const name of names) {
|
|
192
208
|
try {
|
|
193
|
-
const bin = join(
|
|
209
|
+
const bin = join(dir2, name);
|
|
194
210
|
const root = dirname(dirname(await realpath(bin)));
|
|
195
211
|
if (seenRoots.has(root)) continue;
|
|
196
212
|
seenRoots.add(root);
|
|
@@ -241,9 +257,9 @@ async function checkForUpdate(current) {
|
|
|
241
257
|
void fetchLatest().then(async (latest) => {
|
|
242
258
|
if (!latest) return;
|
|
243
259
|
try {
|
|
244
|
-
const
|
|
245
|
-
await mkdir(dirname(
|
|
246
|
-
await writeFile(
|
|
260
|
+
const file2 = cacheFile();
|
|
261
|
+
await mkdir(dirname(file2), { recursive: true });
|
|
262
|
+
await writeFile(file2, JSON.stringify({ checkedAt: Date.now(), latest }), "utf8");
|
|
247
263
|
} catch {
|
|
248
264
|
}
|
|
249
265
|
});
|
|
@@ -615,7 +631,7 @@ function printBanner(model, version, sources) {
|
|
|
615
631
|
}
|
|
616
632
|
|
|
617
633
|
// src/agent.ts
|
|
618
|
-
import { readFile as
|
|
634
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
619
635
|
|
|
620
636
|
// src/input.ts
|
|
621
637
|
import { emitKeypressEvents } from "node:readline";
|
|
@@ -635,10 +651,10 @@ function inputLayout(text, before, promptW, cols2) {
|
|
|
635
651
|
const curPhysCol = curCol % c;
|
|
636
652
|
return { totalPhys, curPhysRow, curPhysCol };
|
|
637
653
|
}
|
|
638
|
-
function moveVertIndex(buf2, cur2,
|
|
654
|
+
function moveVertIndex(buf2, cur2, dir2) {
|
|
639
655
|
const lines = buf2.split("\n");
|
|
640
656
|
const { row, col } = rowColOf(buf2.slice(0, cur2));
|
|
641
|
-
const target = row +
|
|
657
|
+
const target = row + dir2;
|
|
642
658
|
if (target < 0 || target >= lines.length) return null;
|
|
643
659
|
let idx = 0;
|
|
644
660
|
for (let i = 0; i < target; i++) idx += lines[i].length + 1;
|
|
@@ -896,8 +912,8 @@ function readPrompt(opts) {
|
|
|
896
912
|
if (isPrintableCodePoint(str.codePointAt(0))) insert2(str);
|
|
897
913
|
}
|
|
898
914
|
}
|
|
899
|
-
function moveVert(
|
|
900
|
-
const idx = moveVertIndex(buf2, cur2,
|
|
915
|
+
function moveVert(dir2) {
|
|
916
|
+
const idx = moveVertIndex(buf2, cur2, dir2);
|
|
901
917
|
if (idx === null) return;
|
|
902
918
|
cur2 = idx;
|
|
903
919
|
render2();
|
|
@@ -1072,6 +1088,9 @@ function loadCatalog() {
|
|
|
1072
1088
|
}).catch(() => {
|
|
1073
1089
|
});
|
|
1074
1090
|
}
|
|
1091
|
+
function primeCatalog() {
|
|
1092
|
+
loadCatalog();
|
|
1093
|
+
}
|
|
1075
1094
|
var baseId = (slug) => slug.split(":")[0];
|
|
1076
1095
|
function shouldSendReasoning(model) {
|
|
1077
1096
|
loadCatalog();
|
|
@@ -1195,11 +1214,11 @@ function createMarkdownStream(write) {
|
|
|
1195
1214
|
}
|
|
1196
1215
|
|
|
1197
1216
|
// src/tools.ts
|
|
1198
|
-
import { readFile as
|
|
1217
|
+
import { readFile as readFile4, writeFile as writeFile3, appendFile, readdir as readdir2, mkdir as mkdir4, stat, rename, chmod } from "node:fs/promises";
|
|
1199
1218
|
import { createReadStream } from "node:fs";
|
|
1200
1219
|
import { createInterface as createLineReader } from "node:readline";
|
|
1201
1220
|
import { spawn as spawn4 } from "node:child_process";
|
|
1202
|
-
import { join as
|
|
1221
|
+
import { join as join5 } from "node:path";
|
|
1203
1222
|
import { lookup as dnsLookup } from "node:dns";
|
|
1204
1223
|
import { request as httpRequest } from "node:http";
|
|
1205
1224
|
import { request as httpsRequest } from "node:https";
|
|
@@ -1338,10 +1357,10 @@ async function loadSkills() {
|
|
|
1338
1357
|
[join2(process.cwd(), ".beecork", "skills"), "project"],
|
|
1339
1358
|
[bundledDir, "bundled"]
|
|
1340
1359
|
];
|
|
1341
|
-
for (const [
|
|
1360
|
+
for (const [dir2, source] of dirs) {
|
|
1342
1361
|
let entries;
|
|
1343
1362
|
try {
|
|
1344
|
-
entries = await readdir(
|
|
1363
|
+
entries = await readdir(dir2, { withFileTypes: true });
|
|
1345
1364
|
} catch {
|
|
1346
1365
|
continue;
|
|
1347
1366
|
}
|
|
@@ -1354,10 +1373,10 @@ async function loadSkills() {
|
|
|
1354
1373
|
continue;
|
|
1355
1374
|
}
|
|
1356
1375
|
try {
|
|
1357
|
-
const raw = (await readFile2(join2(
|
|
1376
|
+
const raw = (await readFile2(join2(dir2, e.name), "utf8")).trim();
|
|
1358
1377
|
if (!raw) continue;
|
|
1359
1378
|
const { description, modelInvocable, body } = parseSkill(raw);
|
|
1360
|
-
registry.set(name, { name, content: body, description, modelInvocable, path: join2(
|
|
1379
|
+
registry.set(name, { name, content: body, description, modelInvocable, path: join2(dir2, e.name), source });
|
|
1361
1380
|
} catch {
|
|
1362
1381
|
}
|
|
1363
1382
|
}
|
|
@@ -1747,6 +1766,53 @@ async function doEnsure() {
|
|
|
1747
1766
|
return { up: false, started: true, pid, reason: "spawn-failed" };
|
|
1748
1767
|
}
|
|
1749
1768
|
|
|
1769
|
+
// src/projectSites.ts
|
|
1770
|
+
import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir3 } from "node:fs/promises";
|
|
1771
|
+
import { join as join4 } from "node:path";
|
|
1772
|
+
var dir = () => join4(process.cwd(), ".beecork");
|
|
1773
|
+
var file = () => join4(dir(), "skeleton.json");
|
|
1774
|
+
function toOrigin(input) {
|
|
1775
|
+
const s = String(input || "").trim();
|
|
1776
|
+
if (!s) return "";
|
|
1777
|
+
const withScheme = /^https?:\/\//i.test(s) ? s : "http://" + s;
|
|
1778
|
+
try {
|
|
1779
|
+
const o = new URL(withScheme).origin;
|
|
1780
|
+
return o === "null" ? "" : o;
|
|
1781
|
+
} catch {
|
|
1782
|
+
return "";
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function isLoopbackOrigin(origin) {
|
|
1786
|
+
const o = toOrigin(origin);
|
|
1787
|
+
if (!o) return false;
|
|
1788
|
+
let host;
|
|
1789
|
+
try {
|
|
1790
|
+
host = new URL(o).hostname;
|
|
1791
|
+
} catch {
|
|
1792
|
+
return false;
|
|
1793
|
+
}
|
|
1794
|
+
host = host.replace(/^\[|\]$/g, "").toLowerCase();
|
|
1795
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "0.0.0.0" || host === "::1" || /^127\./.test(host);
|
|
1796
|
+
}
|
|
1797
|
+
async function loadProjectOrigins() {
|
|
1798
|
+
try {
|
|
1799
|
+
const cfg = JSON.parse(await readFile3(file(), "utf8"));
|
|
1800
|
+
if (!Array.isArray(cfg.origins)) return [];
|
|
1801
|
+
return [...new Set(cfg.origins.map(toOrigin).filter(Boolean))];
|
|
1802
|
+
} catch {
|
|
1803
|
+
return [];
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
async function addProjectOrigin(origin) {
|
|
1807
|
+
const o = toOrigin(origin);
|
|
1808
|
+
if (!o || isLoopbackOrigin(o)) return false;
|
|
1809
|
+
const cur2 = await loadProjectOrigins();
|
|
1810
|
+
if (cur2.includes(o)) return false;
|
|
1811
|
+
await mkdir3(dir(), { recursive: true });
|
|
1812
|
+
await writeFile2(file(), JSON.stringify({ origins: [...cur2, o] }, null, 2) + "\n");
|
|
1813
|
+
return true;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1750
1816
|
// src/tools.ts
|
|
1751
1817
|
function runShell(command, opts) {
|
|
1752
1818
|
return new Promise((resolve2, reject) => {
|
|
@@ -1818,7 +1884,7 @@ function runShell(command, opts) {
|
|
|
1818
1884
|
var fail = (verb, err) => `Error ${verb}: ${err.message}`;
|
|
1819
1885
|
async function atomicWrite(abs, content) {
|
|
1820
1886
|
const tmp = `${abs}.beecork-${process.pid}.tmp`;
|
|
1821
|
-
await
|
|
1887
|
+
await writeFile3(tmp, content, "utf8");
|
|
1822
1888
|
try {
|
|
1823
1889
|
await chmod(tmp, (await stat(abs)).mode);
|
|
1824
1890
|
} catch {
|
|
@@ -1847,8 +1913,8 @@ function lineOffsets(text) {
|
|
|
1847
1913
|
for (let i = 0; i < text.length; i++) if (text[i] === "\n") starts.push(i + 1);
|
|
1848
1914
|
return starts;
|
|
1849
1915
|
}
|
|
1850
|
-
function matchWhitespace(
|
|
1851
|
-
const fileLines =
|
|
1916
|
+
function matchWhitespace(file2, oldText, newText) {
|
|
1917
|
+
const fileLines = file2.split("\n");
|
|
1852
1918
|
const oldLines = oldText.split("\n");
|
|
1853
1919
|
const n = oldLines.length;
|
|
1854
1920
|
const trim = (l) => l.trim();
|
|
@@ -1894,15 +1960,15 @@ function matchWhitespace(file, oldText, newText) {
|
|
|
1894
1960
|
if (mode === "strip") return l.startsWith(shift) ? l.slice(shift.length) : l;
|
|
1895
1961
|
return l;
|
|
1896
1962
|
}).join("\n");
|
|
1897
|
-
const offs = lineOffsets(
|
|
1963
|
+
const offs = lineOffsets(file2);
|
|
1898
1964
|
const start = offs[s];
|
|
1899
|
-
const end = s + n < offs.length ? offs[s + n] - 1 :
|
|
1965
|
+
const end = s + n < offs.length ? offs[s + n] - 1 : file2.length;
|
|
1900
1966
|
return { ok: true, start, end, after: reindented, healedVia: "whitespace" };
|
|
1901
1967
|
}
|
|
1902
|
-
function closestRegion(
|
|
1968
|
+
function closestRegion(file2, oldText) {
|
|
1903
1969
|
const anchor = oldText.split("\n").map((l) => l.trim()).find((l) => l !== "");
|
|
1904
1970
|
if (!anchor) return void 0;
|
|
1905
|
-
const fileLines =
|
|
1971
|
+
const fileLines = file2.split("\n");
|
|
1906
1972
|
const fmt = (i) => `${String(i + 1).padStart(5)} ${fileLines[i]}`;
|
|
1907
1973
|
const exact = [];
|
|
1908
1974
|
for (let i = 0; i < fileLines.length && exact.length < 3; i++) {
|
|
@@ -1923,23 +1989,23 @@ function closestRegion(file, oldText) {
|
|
|
1923
1989
|
}
|
|
1924
1990
|
return best >= 0 && bestScore >= Math.max(2, Math.ceil(words.length / 2)) ? fmt(best) : void 0;
|
|
1925
1991
|
}
|
|
1926
|
-
function resolveEdit(
|
|
1992
|
+
function resolveEdit(file2, oldText, newText) {
|
|
1927
1993
|
if (oldText === "") return { ok: false, reason: "not_found" };
|
|
1928
|
-
const exact = allIndexOf(
|
|
1994
|
+
const exact = allIndexOf(file2, oldText);
|
|
1929
1995
|
if (exact.length === 1) return { ok: true, start: exact[0], end: exact[0] + oldText.length, after: newText, healedVia: "exact" };
|
|
1930
1996
|
if (exact.length > 1) return { ok: false, reason: "ambiguous", count: exact.length };
|
|
1931
1997
|
const strippedOld = stripReadPrefix(oldText);
|
|
1932
1998
|
if (strippedOld !== null && strippedOld !== oldText) {
|
|
1933
|
-
const hits = allIndexOf(
|
|
1999
|
+
const hits = allIndexOf(file2, strippedOld);
|
|
1934
2000
|
if (hits.length === 1) {
|
|
1935
2001
|
const strippedNew = newText.split("\n").map((l) => l.replace(READ_PREFIX, "")).join("\n");
|
|
1936
2002
|
return { ok: true, start: hits[0], end: hits[0] + strippedOld.length, after: strippedNew, healedVia: "prefix" };
|
|
1937
2003
|
}
|
|
1938
2004
|
if (hits.length > 1) return { ok: false, reason: "ambiguous", count: hits.length };
|
|
1939
2005
|
}
|
|
1940
|
-
const ws = matchWhitespace(
|
|
2006
|
+
const ws = matchWhitespace(file2, oldText, newText);
|
|
1941
2007
|
if (ws) return ws;
|
|
1942
|
-
return { ok: false, reason: "not_found", closest: closestRegion(
|
|
2008
|
+
return { ok: false, reason: "not_found", closest: closestRegion(file2, oldText) };
|
|
1943
2009
|
}
|
|
1944
2010
|
var todos = [];
|
|
1945
2011
|
function httpGet(rawUrl, maxBytes, signal) {
|
|
@@ -2033,7 +2099,7 @@ async function walkTree(abs, prefix, items, cap) {
|
|
|
2033
2099
|
const e = kept[i];
|
|
2034
2100
|
const last = i === kept.length - 1;
|
|
2035
2101
|
items.push({ prefix: prefix + (last ? "\u2514\u2500 " : "\u251C\u2500 "), name: e.name + (e.isDirectory() ? "/" : ""), isDir: e.isDirectory() });
|
|
2036
|
-
if (e.isDirectory()) await walkTree(
|
|
2102
|
+
if (e.isDirectory()) await walkTree(join5(abs, e.name), prefix + (last ? " " : "\u2502 "), items, cap);
|
|
2037
2103
|
}
|
|
2038
2104
|
}
|
|
2039
2105
|
function parseRange(args, defLimit) {
|
|
@@ -2166,7 +2232,7 @@ var toolDefs = [
|
|
|
2166
2232
|
const MAX = config.searchMaxResults;
|
|
2167
2233
|
const deadline = Date.now() + config.searchTimeoutMs;
|
|
2168
2234
|
let truncated = false;
|
|
2169
|
-
async function walk(
|
|
2235
|
+
async function walk(dir2) {
|
|
2170
2236
|
if (results.length >= MAX) return;
|
|
2171
2237
|
if (Date.now() > deadline) {
|
|
2172
2238
|
truncated = true;
|
|
@@ -2174,7 +2240,7 @@ var toolDefs = [
|
|
|
2174
2240
|
}
|
|
2175
2241
|
let entries;
|
|
2176
2242
|
try {
|
|
2177
|
-
entries = await readdir2(
|
|
2243
|
+
entries = await readdir2(dir2, { withFileTypes: true });
|
|
2178
2244
|
} catch {
|
|
2179
2245
|
return;
|
|
2180
2246
|
}
|
|
@@ -2185,7 +2251,7 @@ var toolDefs = [
|
|
|
2185
2251
|
return;
|
|
2186
2252
|
}
|
|
2187
2253
|
if (IGNORE.has(e.name)) continue;
|
|
2188
|
-
const full = `${
|
|
2254
|
+
const full = `${dir2}/${e.name}`;
|
|
2189
2255
|
if (e.isDirectory()) {
|
|
2190
2256
|
await walk(full);
|
|
2191
2257
|
} else if (e.isFile()) {
|
|
@@ -2194,7 +2260,7 @@ var toolDefs = [
|
|
|
2194
2260
|
if (info && info.size > config.searchMaxFileBytes) continue;
|
|
2195
2261
|
let lines;
|
|
2196
2262
|
try {
|
|
2197
|
-
lines = (await
|
|
2263
|
+
lines = (await readFile4(full, "utf8")).split("\n");
|
|
2198
2264
|
} catch {
|
|
2199
2265
|
continue;
|
|
2200
2266
|
}
|
|
@@ -2262,7 +2328,7 @@ var toolDefs = [
|
|
|
2262
2328
|
run: async (args) => {
|
|
2263
2329
|
try {
|
|
2264
2330
|
const { abs } = resolveInRoot(String(args.path ?? "."));
|
|
2265
|
-
const original = await
|
|
2331
|
+
const original = await readFile4(abs, "utf8");
|
|
2266
2332
|
const res = resolveEdit(original, String(args.old_text ?? ""), String(args.new_text ?? ""));
|
|
2267
2333
|
if (!res.ok) {
|
|
2268
2334
|
if (res.reason === "ambiguous") {
|
|
@@ -2465,21 +2531,21 @@ ${list}`;
|
|
|
2465
2531
|
},
|
|
2466
2532
|
run: async (args) => {
|
|
2467
2533
|
try {
|
|
2468
|
-
const
|
|
2469
|
-
await
|
|
2470
|
-
const
|
|
2534
|
+
const dir2 = join5(process.cwd(), ".beecork");
|
|
2535
|
+
await mkdir4(dir2, { recursive: true });
|
|
2536
|
+
const file2 = join5(dir2, "memory.md");
|
|
2471
2537
|
const fact = String(args.fact).trim();
|
|
2472
2538
|
if (!fact) return 'Error: remember needs a non-empty "fact".';
|
|
2473
2539
|
let current = "";
|
|
2474
2540
|
try {
|
|
2475
|
-
current = await
|
|
2541
|
+
current = await readFile4(file2, "utf8");
|
|
2476
2542
|
} catch {
|
|
2477
2543
|
}
|
|
2478
2544
|
if (current.length + fact.length + 3 > config.memoryMaxChars) {
|
|
2479
2545
|
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
2546
|
}
|
|
2481
|
-
if (!current) await
|
|
2482
|
-
await appendFile(
|
|
2547
|
+
if (!current) await writeFile3(file2, "# beecork memory\n\n", "utf8");
|
|
2548
|
+
await appendFile(file2, `- ${fact}
|
|
2483
2549
|
`, "utf8");
|
|
2484
2550
|
return `Remembered: ${fact}`;
|
|
2485
2551
|
} catch (err) {
|
|
@@ -2580,13 +2646,14 @@ ${skill.content}`;
|
|
|
2580
2646
|
},
|
|
2581
2647
|
{
|
|
2582
2648
|
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.",
|
|
2649
|
+
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
2650
|
parameters: {
|
|
2585
2651
|
type: "object",
|
|
2586
2652
|
properties: {
|
|
2587
2653
|
kind: { type: "string", description: 'Filter: "network", "console", "pageError", "log", or "all" (default all).' },
|
|
2588
2654
|
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)." }
|
|
2655
|
+
limit: { type: "number", description: "Max signals to return (default 30, max 200)." },
|
|
2656
|
+
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
2657
|
},
|
|
2591
2658
|
required: []
|
|
2592
2659
|
},
|
|
@@ -2596,9 +2663,12 @@ ${skill.content}`;
|
|
|
2596
2663
|
const kind = args.kind ? String(args.kind) : "";
|
|
2597
2664
|
const limit = Math.min(Math.max(Number(args.limit) || 30, 1), 200);
|
|
2598
2665
|
const sinceMin = Number(args.since_minutes) || 0;
|
|
2599
|
-
const
|
|
2666
|
+
const explicit = args.origin ? String(args.origin).split(",").map((s) => toOrigin(s)).filter(Boolean) : [];
|
|
2667
|
+
const scope = explicit.length ? explicit : await loadProjectOrigins();
|
|
2668
|
+
const params = new URLSearchParams({ limit: String(scope.length ? Math.min(200, limit * 4) : limit) });
|
|
2600
2669
|
if (kind && kind !== "all") params.set("kind", kind);
|
|
2601
2670
|
if (sinceMin > 0) params.set("since", String(Date.now() - sinceMin * 6e4));
|
|
2671
|
+
if (scope.length) params.set("origin", scope.join(","));
|
|
2602
2672
|
const timeout = AbortSignal.timeout(Math.min(config.webTimeoutMs, 5e3));
|
|
2603
2673
|
let data;
|
|
2604
2674
|
try {
|
|
@@ -2610,9 +2680,12 @@ ${skill.content}`;
|
|
|
2610
2680
|
return DEV_SIGNALS_SETUP;
|
|
2611
2681
|
}
|
|
2612
2682
|
const now = Date.now();
|
|
2613
|
-
|
|
2683
|
+
let signals = (data.signals ?? []).filter((s) => s && s.kind !== "watch");
|
|
2684
|
+
if (scope.length) signals = signals.filter((s) => scope.some((o) => String(s.page || s.url || "").startsWith(o)));
|
|
2685
|
+
signals = signals.slice(-limit);
|
|
2686
|
+
const scopeLabel = scope.length ? ` from ${scope.join(", ")}` : "";
|
|
2614
2687
|
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.
|
|
2688
|
+
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
2689
|
|
|
2617
2690
|
If the Beecork Skeleton extension isn't loaded yet, connect it:
|
|
2618
2691
|
${EXTENSION_STEPS}
|
|
@@ -2625,8 +2698,15 @@ Already connected? Reproduce the issue in the browser (or open the app), then ca
|
|
|
2625
2698
|
const text = String(s.text ?? "").replace(/\s+/g, " ").slice(0, 300);
|
|
2626
2699
|
return `[${s.kind}] ${text}${s.url ? ` @ ${s.url}` : ""} (${ago2(s.ts)})`;
|
|
2627
2700
|
});
|
|
2628
|
-
|
|
2629
|
-
|
|
2701
|
+
let hint = "";
|
|
2702
|
+
if (!scope.length) {
|
|
2703
|
+
const distinct = [...new Set(signals.map((s) => toOrigin(String(s.page || s.url || ""))).filter(Boolean))];
|
|
2704
|
+
if (distinct.length > 1) hint = `
|
|
2705
|
+
|
|
2706
|
+
(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.)`;
|
|
2707
|
+
}
|
|
2708
|
+
return `${signals.length} browser signal(s)${scopeLabel}, newest last:
|
|
2709
|
+
${lines.join("\n")}${hint}`;
|
|
2630
2710
|
}
|
|
2631
2711
|
},
|
|
2632
2712
|
{
|
|
@@ -2663,7 +2743,9 @@ ${lines.join("\n")}`;
|
|
|
2663
2743
|
} catch {
|
|
2664
2744
|
return DEV_SIGNALS_SETUP;
|
|
2665
2745
|
}
|
|
2666
|
-
|
|
2746
|
+
const remembered = await addProjectOrigin(origin).catch(() => false);
|
|
2747
|
+
const memo = remembered ? ` I'll remember ${origin} as this project's site, so read_dev_signals here now scopes to it automatically.` : "";
|
|
2748
|
+
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
2749
|
}
|
|
2668
2750
|
}
|
|
2669
2751
|
];
|
|
@@ -2752,7 +2834,7 @@ function parseSSELine(line) {
|
|
|
2752
2834
|
return out3;
|
|
2753
2835
|
}
|
|
2754
2836
|
function buildRequestBody(opts) {
|
|
2755
|
-
const { model, messages, includeTools, effort, reasoningSupported, extra, tools } = opts;
|
|
2837
|
+
const { model, messages, includeTools, effort, reasoningSupported, extra, tools, providerSort } = opts;
|
|
2756
2838
|
const body = { ...extra };
|
|
2757
2839
|
body.model = model;
|
|
2758
2840
|
body.messages = messages;
|
|
@@ -2761,6 +2843,7 @@ function buildRequestBody(opts) {
|
|
|
2761
2843
|
if (reasoningSupported && !("reasoning" in extra)) {
|
|
2762
2844
|
body.reasoning = effort === "off" ? { enabled: false } : { effort };
|
|
2763
2845
|
}
|
|
2846
|
+
if (providerSort && !("provider" in extra)) body.provider = { sort: providerSort };
|
|
2764
2847
|
return body;
|
|
2765
2848
|
}
|
|
2766
2849
|
function pruneReasoningForSend(messages) {
|
|
@@ -2786,7 +2869,8 @@ async function callModel(messages, includeTools = true, signal, opts) {
|
|
|
2786
2869
|
effort: state.reasoningEffort,
|
|
2787
2870
|
reasoningSupported: shouldSendReasoning(state.model),
|
|
2788
2871
|
extra: config.openRouterExtra,
|
|
2789
|
-
tools: opts?.tools
|
|
2872
|
+
tools: opts?.tools,
|
|
2873
|
+
providerSort: config.providerSort
|
|
2790
2874
|
});
|
|
2791
2875
|
const tries = config.retryAttempts;
|
|
2792
2876
|
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
@@ -3028,49 +3112,49 @@ ${summary}` }, ...recent];
|
|
|
3028
3112
|
}
|
|
3029
3113
|
|
|
3030
3114
|
// src/memory.ts
|
|
3031
|
-
import { readFile as
|
|
3115
|
+
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
3116
|
import { homedir as homedir6 } from "node:os";
|
|
3033
|
-
import { join as
|
|
3117
|
+
import { join as join6, dirname as dirname4 } from "node:path";
|
|
3034
3118
|
var BEECORK = ".beecork";
|
|
3035
3119
|
function ancestorDirs() {
|
|
3036
3120
|
const home = homedir6();
|
|
3037
3121
|
const dirs = [];
|
|
3038
|
-
let
|
|
3039
|
-
while (
|
|
3040
|
-
dirs.push(
|
|
3041
|
-
|
|
3122
|
+
let dir2 = process.cwd();
|
|
3123
|
+
while (dir2 !== home && dir2 !== dirname4(dir2)) {
|
|
3124
|
+
dirs.push(dir2);
|
|
3125
|
+
dir2 = dirname4(dir2);
|
|
3042
3126
|
}
|
|
3043
3127
|
return dirs.reverse();
|
|
3044
3128
|
}
|
|
3045
3129
|
function corkPaths() {
|
|
3046
|
-
return [
|
|
3130
|
+
return [join6(homedir6(), BEECORK, "cork.md"), ...ancestorDirs().map((d) => join6(d, "cork.md"))];
|
|
3047
3131
|
}
|
|
3048
3132
|
function beecorkPaths(name) {
|
|
3049
|
-
return [
|
|
3133
|
+
return [join6(homedir6(), BEECORK, name), ...ancestorDirs().map((d) => join6(d, BEECORK, name))];
|
|
3050
3134
|
}
|
|
3051
3135
|
function standardInstructionPaths() {
|
|
3052
|
-
return ancestorDirs().flatMap((d) => [
|
|
3136
|
+
return ancestorDirs().flatMap((d) => [join6(d, "AGENTS.md"), join6(d, "CLAUDE.md")]);
|
|
3053
3137
|
}
|
|
3054
3138
|
async function loadInstructions() {
|
|
3055
3139
|
const home = homedir6();
|
|
3056
|
-
const homeBeecork =
|
|
3140
|
+
const homeBeecork = join6(home, ".beecork");
|
|
3057
3141
|
const trusted = [];
|
|
3058
3142
|
const project = [];
|
|
3059
3143
|
const sources = [];
|
|
3060
3144
|
const MAX_FILE = 8e3;
|
|
3061
3145
|
const MAX_TOTAL = 24e3;
|
|
3062
3146
|
let total = 0;
|
|
3063
|
-
for (const
|
|
3147
|
+
for (const file2 of [...corkPaths(), ...standardInstructionPaths(), ...beecorkPaths("memory.md")]) {
|
|
3064
3148
|
try {
|
|
3065
|
-
let content = (await
|
|
3149
|
+
let content = (await readFile5(file2, "utf8")).trim();
|
|
3066
3150
|
if (!content) continue;
|
|
3067
3151
|
if (content.length > MAX_FILE) content = content.slice(0, MAX_FILE) + "\n\u2026(truncated)";
|
|
3068
3152
|
if (total + content.length > MAX_TOTAL) content = content.slice(0, Math.max(0, MAX_TOTAL - total)) + "\n\u2026(truncated)";
|
|
3069
3153
|
total += content.length;
|
|
3070
|
-
const block = `## From ${tildify(
|
|
3154
|
+
const block = `## From ${tildify(file2)}
|
|
3071
3155
|
${content}`;
|
|
3072
|
-
(
|
|
3073
|
-
sources.push(
|
|
3156
|
+
(file2.startsWith(homeBeecork) ? trusted : project).push(block);
|
|
3157
|
+
sources.push(file2);
|
|
3074
3158
|
if (total >= MAX_TOTAL) break;
|
|
3075
3159
|
} catch {
|
|
3076
3160
|
}
|
|
@@ -3079,7 +3163,7 @@ ${content}`;
|
|
|
3079
3163
|
}
|
|
3080
3164
|
async function readJsonFile(path) {
|
|
3081
3165
|
try {
|
|
3082
|
-
return JSON.parse(await
|
|
3166
|
+
return JSON.parse(await readFile5(path, "utf8"));
|
|
3083
3167
|
} catch (err) {
|
|
3084
3168
|
if (err.code !== "ENOENT") {
|
|
3085
3169
|
console.error(color.yellow(`\u26A0 ignoring malformed ${tildify(path)}: ${err.message}`));
|
|
@@ -3106,60 +3190,60 @@ async function loadSettings() {
|
|
|
3106
3190
|
return { model, reasoningEffort, alwaysAllow, projectAlwaysAllowIgnored };
|
|
3107
3191
|
}
|
|
3108
3192
|
function userConfigPath() {
|
|
3109
|
-
return
|
|
3193
|
+
return join6(homedir6(), BEECORK, "config.json");
|
|
3110
3194
|
}
|
|
3111
3195
|
async function loadUserConfig() {
|
|
3112
3196
|
return await readJsonFile(userConfigPath()) ?? {};
|
|
3113
3197
|
}
|
|
3114
3198
|
async function saveUserConfig(patch) {
|
|
3115
|
-
const
|
|
3116
|
-
await
|
|
3199
|
+
const file2 = userConfigPath();
|
|
3200
|
+
await mkdir5(dirname4(file2), { recursive: true });
|
|
3117
3201
|
const merged = { ...await loadUserConfig(), ...patch };
|
|
3118
|
-
const tmp = `${
|
|
3119
|
-
await
|
|
3202
|
+
const tmp = `${file2}.tmp`;
|
|
3203
|
+
await writeFile4(tmp, JSON.stringify(merged, null, 2), { encoding: "utf8", mode: 384 });
|
|
3120
3204
|
await chmod2(tmp, 384).catch(() => {
|
|
3121
3205
|
});
|
|
3122
|
-
await rename2(tmp,
|
|
3206
|
+
await rename2(tmp, file2);
|
|
3123
3207
|
}
|
|
3124
3208
|
async function saveModelPreference(model) {
|
|
3125
3209
|
try {
|
|
3126
|
-
const
|
|
3127
|
-
await
|
|
3128
|
-
const current = await readJsonFile(
|
|
3129
|
-
await
|
|
3210
|
+
const file2 = join6(homedir6(), BEECORK, "settings.json");
|
|
3211
|
+
await mkdir5(dirname4(file2), { recursive: true });
|
|
3212
|
+
const current = await readJsonFile(file2) ?? {};
|
|
3213
|
+
await writeFile4(file2, JSON.stringify({ ...current, model }, null, 2), "utf8");
|
|
3130
3214
|
} catch {
|
|
3131
3215
|
}
|
|
3132
3216
|
}
|
|
3133
3217
|
async function saveReasoningPreference(reasoningEffort) {
|
|
3134
3218
|
try {
|
|
3135
|
-
const
|
|
3136
|
-
await
|
|
3137
|
-
const current = await readJsonFile(
|
|
3138
|
-
await
|
|
3219
|
+
const file2 = join6(homedir6(), BEECORK, "settings.json");
|
|
3220
|
+
await mkdir5(dirname4(file2), { recursive: true });
|
|
3221
|
+
const current = await readJsonFile(file2) ?? {};
|
|
3222
|
+
await writeFile4(file2, JSON.stringify({ ...current, reasoningEffort }, null, 2), "utf8");
|
|
3139
3223
|
} catch {
|
|
3140
3224
|
}
|
|
3141
3225
|
}
|
|
3142
|
-
var sessionsDir = () =>
|
|
3226
|
+
var sessionsDir = () => join6(process.cwd(), BEECORK, "sessions");
|
|
3143
3227
|
async function saveSession(messages) {
|
|
3144
3228
|
try {
|
|
3145
|
-
const
|
|
3146
|
-
await
|
|
3147
|
-
const
|
|
3148
|
-
const tmp = `${
|
|
3149
|
-
await
|
|
3229
|
+
const dir2 = sessionsDir();
|
|
3230
|
+
await mkdir5(dir2, { recursive: true });
|
|
3231
|
+
const file2 = join6(dir2, `${Date.now()}.json`);
|
|
3232
|
+
const tmp = `${file2}.tmp`;
|
|
3233
|
+
await writeFile4(tmp, JSON.stringify(messages), "utf8");
|
|
3150
3234
|
await chmod2(tmp, 384).catch(() => {
|
|
3151
3235
|
});
|
|
3152
|
-
await rename2(tmp,
|
|
3153
|
-
await pruneSessions(
|
|
3236
|
+
await rename2(tmp, file2);
|
|
3237
|
+
await pruneSessions(dir2).catch(() => {
|
|
3154
3238
|
});
|
|
3155
3239
|
} catch {
|
|
3156
3240
|
}
|
|
3157
3241
|
}
|
|
3158
3242
|
var MAX_SESSIONS = 50;
|
|
3159
|
-
async function pruneSessions(
|
|
3160
|
-
const files = (await readdir3(
|
|
3243
|
+
async function pruneSessions(dir2) {
|
|
3244
|
+
const files = (await readdir3(dir2)).filter((f) => f.endsWith(".json"));
|
|
3161
3245
|
if (files.length <= MAX_SESSIONS) return;
|
|
3162
|
-
for (const f of files.sort().slice(0, files.length - MAX_SESSIONS)) await unlink(
|
|
3246
|
+
for (const f of files.sort().slice(0, files.length - MAX_SESSIONS)) await unlink(join6(dir2, f)).catch(() => {
|
|
3163
3247
|
});
|
|
3164
3248
|
}
|
|
3165
3249
|
function sanitizeSession(raw) {
|
|
@@ -3196,10 +3280,10 @@ function dropIncompleteToolTail(messages) {
|
|
|
3196
3280
|
}
|
|
3197
3281
|
return messages;
|
|
3198
3282
|
}
|
|
3199
|
-
async function readSession(
|
|
3283
|
+
async function readSession(file2) {
|
|
3200
3284
|
try {
|
|
3201
|
-
const path =
|
|
3202
|
-
const parsed = sanitizeSession(JSON.parse(await
|
|
3285
|
+
const path = join6(sessionsDir(), file2);
|
|
3286
|
+
const parsed = sanitizeSession(JSON.parse(await readFile5(path, "utf8")));
|
|
3203
3287
|
await chmod2(path, 384).catch(() => {
|
|
3204
3288
|
});
|
|
3205
3289
|
return parsed;
|
|
@@ -3235,11 +3319,11 @@ async function listSessions() {
|
|
|
3235
3319
|
return [];
|
|
3236
3320
|
}
|
|
3237
3321
|
}
|
|
3238
|
-
async function loadSession(
|
|
3239
|
-
return await readSession(
|
|
3322
|
+
async function loadSession(file2) {
|
|
3323
|
+
return await readSession(file2) ?? [];
|
|
3240
3324
|
}
|
|
3241
3325
|
function projectApprovalsPath() {
|
|
3242
|
-
return
|
|
3326
|
+
return join6(homedir6(), BEECORK, "project-approvals.json");
|
|
3243
3327
|
}
|
|
3244
3328
|
async function loadProjectApprovals() {
|
|
3245
3329
|
const all = await readJsonFile(projectApprovalsPath());
|
|
@@ -3248,14 +3332,14 @@ async function loadProjectApprovals() {
|
|
|
3248
3332
|
}
|
|
3249
3333
|
async function addProjectApproval(tool) {
|
|
3250
3334
|
try {
|
|
3251
|
-
const
|
|
3252
|
-
await
|
|
3253
|
-
const all = await readJsonFile(
|
|
3335
|
+
const file2 = projectApprovalsPath();
|
|
3336
|
+
await mkdir5(dirname4(file2), { recursive: true });
|
|
3337
|
+
const all = await readJsonFile(file2) ?? {};
|
|
3254
3338
|
const list = new Set(Array.isArray(all[projectRoot]) ? all[projectRoot] : []);
|
|
3255
3339
|
list.add(tool);
|
|
3256
3340
|
all[projectRoot] = [...list];
|
|
3257
|
-
await
|
|
3258
|
-
await chmod2(
|
|
3341
|
+
await writeFile4(file2, JSON.stringify(all, null, 2), "utf8");
|
|
3342
|
+
await chmod2(file2, 384).catch(() => {
|
|
3259
3343
|
});
|
|
3260
3344
|
} catch {
|
|
3261
3345
|
}
|
|
@@ -3305,7 +3389,7 @@ async function askApproval(ask, call, reason, offerAlways = true) {
|
|
|
3305
3389
|
console.log(color.yellow(` edit ${stripControl(String(args.path ?? ""))}:`));
|
|
3306
3390
|
console.log(diffPreview(lineDiff(stripControl(String(args.old_text ?? "")), stripControl(String(args.new_text ?? "")))));
|
|
3307
3391
|
} else if (call.function.name === "write_file") {
|
|
3308
|
-
const existing = (await
|
|
3392
|
+
const existing = (await readFile6(resolveInRoot(String(args.path ?? ".")).abs, "utf8").catch(() => "")).slice(0, 2e5);
|
|
3309
3393
|
console.log(color.yellow(` write ${stripControl(String(args.path ?? ""))} ${existing ? "(overwrite)" : "(new file)"}:`));
|
|
3310
3394
|
console.log(diffPreview(lineDiff(stripControl(existing), stripControl(String(args.content ?? "")))));
|
|
3311
3395
|
} else {
|
|
@@ -3365,7 +3449,69 @@ async function handleAskUser(args) {
|
|
|
3365
3449
|
if (choice) console.log(color.green(` \u2192 ${stripControl(choice.label)}`) + "\n");
|
|
3366
3450
|
return askUserMessage(question, choice, true);
|
|
3367
3451
|
}
|
|
3368
|
-
|
|
3452
|
+
var PARALLEL_SAFE = /* @__PURE__ */ new Set([
|
|
3453
|
+
"read_file",
|
|
3454
|
+
"search",
|
|
3455
|
+
"list_dir",
|
|
3456
|
+
"read_skill",
|
|
3457
|
+
"web_fetch",
|
|
3458
|
+
"web_search",
|
|
3459
|
+
"read_dev_signals",
|
|
3460
|
+
"check_task"
|
|
3461
|
+
]);
|
|
3462
|
+
function lastMutationIndex(calls) {
|
|
3463
|
+
return calls.reduce((acc, c, i) => c.function.name === "write_file" || c.function.name === "edit_file" ? i : acc, -1);
|
|
3464
|
+
}
|
|
3465
|
+
function isParallelSafe(call, deps) {
|
|
3466
|
+
if (!config.parallelTools) return false;
|
|
3467
|
+
if (!PARALLEL_SAFE.has(call.function.name)) return false;
|
|
3468
|
+
const tool = toolsByName.get(call.function.name);
|
|
3469
|
+
if (!tool) return false;
|
|
3470
|
+
let args;
|
|
3471
|
+
try {
|
|
3472
|
+
args = JSON.parse(call.function.arguments);
|
|
3473
|
+
} catch {
|
|
3474
|
+
return false;
|
|
3475
|
+
}
|
|
3476
|
+
const decision = decideApproval(tool, args, {
|
|
3477
|
+
mode: state.mode,
|
|
3478
|
+
autoApprove: config.autoApprove,
|
|
3479
|
+
approvedTools: deps.approvedTools,
|
|
3480
|
+
approvedGuardKeys: deps.approvedGuardKeys,
|
|
3481
|
+
toolName: call.function.name,
|
|
3482
|
+
dangerouslySkip: config.dangerouslySkipPermissions
|
|
3483
|
+
});
|
|
3484
|
+
return decision.action === "run";
|
|
3485
|
+
}
|
|
3486
|
+
async function runReadOnlyBatch(block, messages, step, deps) {
|
|
3487
|
+
const { callCounts, signal } = deps;
|
|
3488
|
+
const parts = await Promise.all(block.map(async (call) => {
|
|
3489
|
+
const sig = `${call.function.name}:${call.function.arguments}`;
|
|
3490
|
+
const seen = (callCounts.get(sig) ?? 0) + 1;
|
|
3491
|
+
callCounts.set(sig, seen);
|
|
3492
|
+
if (seen >= config.loopRepeatLimit) {
|
|
3493
|
+
return { call, out: color.yellow(` \u21B3 skipped \u2014 repeated identical call ${seen}\xD7`), content: "You have already called this exact tool with these exact arguments several times; it is not making progress. Stop repeating it \u2014 try a different approach or give your final answer." };
|
|
3494
|
+
}
|
|
3495
|
+
let callArgs = {};
|
|
3496
|
+
try {
|
|
3497
|
+
callArgs = JSON.parse(call.function.arguments);
|
|
3498
|
+
} catch {
|
|
3499
|
+
}
|
|
3500
|
+
if (config.traceFile) trace.push({ tool: call.function.name, args: call.function.arguments, step });
|
|
3501
|
+
let result = await runTool(call, signal);
|
|
3502
|
+
const summary = summarizeResult(call.function.name, callArgs, result);
|
|
3503
|
+
if (result.length > config.maxToolResultChars) {
|
|
3504
|
+
result = result.slice(0, config.maxToolResultChars) + `
|
|
3505
|
+
\u2026[truncated ${result.length - config.maxToolResultChars} chars]`;
|
|
3506
|
+
}
|
|
3507
|
+
return { call, out: " " + renderToolCall(call.function.name, callArgs) + summary, content: result };
|
|
3508
|
+
}));
|
|
3509
|
+
for (const p of parts) {
|
|
3510
|
+
process.stdout.write(p.out + "\n");
|
|
3511
|
+
messages.push({ role: "tool", tool_call_id: p.call.id, content: p.content });
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
async function handleToolCall(call, messages, step, deps, verifyThisCall = true) {
|
|
3369
3515
|
const { approvedTools, approvedGuardKeys, callCounts, ask, signal } = deps;
|
|
3370
3516
|
const pushTool = (content) => messages.push({ role: "tool", tool_call_id: call.id, content });
|
|
3371
3517
|
const sig = `${call.function.name}:${call.function.arguments}`;
|
|
@@ -3440,7 +3586,7 @@ async function handleToolCall(call, messages, step, deps) {
|
|
|
3440
3586
|
}
|
|
3441
3587
|
const summary = summarizeResult(call.function.name, callArgs, result);
|
|
3442
3588
|
let verifyOut = "";
|
|
3443
|
-
if (config.verifyCommand && (call.function.name === "write_file" || call.function.name === "edit_file")) {
|
|
3589
|
+
if (verifyThisCall && config.verifyCommand && (call.function.name === "write_file" || call.function.name === "edit_file")) {
|
|
3444
3590
|
verifyOut = await runVerify(signal);
|
|
3445
3591
|
result += `
|
|
3446
3592
|
|
|
@@ -3497,9 +3643,21 @@ async function runTurn(messages, userInput, ask, approvedTools, approvedGuardKey
|
|
|
3497
3643
|
break;
|
|
3498
3644
|
}
|
|
3499
3645
|
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3646
|
+
const calls = message.tool_calls;
|
|
3647
|
+
const lastMutationIdx = lastMutationIndex(calls);
|
|
3648
|
+
let i = 0;
|
|
3649
|
+
while (i < calls.length && !signal?.aborted) {
|
|
3650
|
+
if (isParallelSafe(calls[i], deps)) {
|
|
3651
|
+
let j = i + 1;
|
|
3652
|
+
while (j < calls.length && isParallelSafe(calls[j], deps)) j++;
|
|
3653
|
+
if (j - i > 1) {
|
|
3654
|
+
await runReadOnlyBatch(calls.slice(i, j), messages, step, deps);
|
|
3655
|
+
i = j;
|
|
3656
|
+
continue;
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
await handleToolCall(calls[i], messages, step, deps, i === lastMutationIdx);
|
|
3660
|
+
i++;
|
|
3503
3661
|
}
|
|
3504
3662
|
} else {
|
|
3505
3663
|
answered = !steering2?.length;
|
|
@@ -3967,7 +4125,7 @@ function stopChrome() {
|
|
|
3967
4125
|
var chromeEnabled = () => config.statuslineEnabled && !!process.stdout.isTTY;
|
|
3968
4126
|
|
|
3969
4127
|
// src/commands.ts
|
|
3970
|
-
import { writeFile as
|
|
4128
|
+
import { writeFile as writeFile5, mkdir as mkdir6, chmod as chmod3 } from "node:fs/promises";
|
|
3971
4129
|
async function pick(opts) {
|
|
3972
4130
|
if (chromeEnabled()) {
|
|
3973
4131
|
const v = await chromePick(opts.items.map((it) => ({ label: it.label, hint: it.hint, value: it.value })), opts.initial, opts.title);
|
|
@@ -4092,15 +4250,15 @@ async function handleCommand(input, messages) {
|
|
|
4092
4250
|
replayConversation(restored);
|
|
4093
4251
|
console.log(color.green(`\u2191 resumed ${restored.length} messages \u2014 the bee has this context now. Continue below.`) + "\n");
|
|
4094
4252
|
} else if (cmd === "/good" || cmd === "/bad") {
|
|
4095
|
-
const
|
|
4253
|
+
const dir2 = cmd === "/bad" ? "eval/failures" : "eval/good";
|
|
4096
4254
|
try {
|
|
4097
|
-
await
|
|
4098
|
-
const
|
|
4099
|
-
await
|
|
4100
|
-
await chmod3(
|
|
4255
|
+
await mkdir6(dir2, { recursive: true });
|
|
4256
|
+
const file2 = `${dir2}/${Date.now()}.json`;
|
|
4257
|
+
await writeFile5(file2, JSON.stringify({ rating: cmd.slice(1), model: state.model, messages }, null, 2), "utf8");
|
|
4258
|
+
await chmod3(file2, 384).catch(() => {
|
|
4101
4259
|
});
|
|
4102
4260
|
console.log(
|
|
4103
|
-
color.cyan(`saved this conversation \u2192 ${
|
|
4261
|
+
color.cyan(`saved this conversation \u2192 ${file2}`) + (cmd === "/bad" ? " (turn it into an eval task later)" : "") + "\n"
|
|
4104
4262
|
);
|
|
4105
4263
|
} catch (err) {
|
|
4106
4264
|
console.log(color.red(`couldn't save: ${err.message}`) + "\n");
|
|
@@ -4284,7 +4442,7 @@ ${instr.trusted}`;
|
|
|
4284
4442
|
saved = true;
|
|
4285
4443
|
try {
|
|
4286
4444
|
if (config.traceFile) {
|
|
4287
|
-
await
|
|
4445
|
+
await writeFile6(config.traceFile, JSON.stringify(trace), "utf8");
|
|
4288
4446
|
await chmod4(config.traceFile, 384).catch(() => {
|
|
4289
4447
|
});
|
|
4290
4448
|
} else if (messages.length > 1) {
|
|
@@ -4351,6 +4509,7 @@ ${instr.trusted}`;
|
|
|
4351
4509
|
process.exit(1);
|
|
4352
4510
|
}
|
|
4353
4511
|
state.apiKey = apiKey;
|
|
4512
|
+
primeCatalog();
|
|
4354
4513
|
const ask = tty ? (q) => readChoice(q) : (q) => rl.question(q);
|
|
4355
4514
|
let activeTurn = null;
|
|
4356
4515
|
let userTurns = 0;
|
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
|