beecork 2.7.1 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +105 -13
- package/package.json +3 -2
- package/skills/browser-signals.md +59 -0
package/dist/index.js
CHANGED
|
@@ -1296,7 +1296,8 @@ function killAllTasks() {
|
|
|
1296
1296
|
|
|
1297
1297
|
// src/skills.ts
|
|
1298
1298
|
import { readdir, readFile as readFile2 } from "node:fs/promises";
|
|
1299
|
-
import { join as join2 } from "node:path";
|
|
1299
|
+
import { join as join2, dirname as dirname2 } from "node:path";
|
|
1300
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1300
1301
|
import { homedir as homedir3 } from "node:os";
|
|
1301
1302
|
function parseSkill(raw) {
|
|
1302
1303
|
let body = raw;
|
|
@@ -1331,9 +1332,11 @@ function getSkill(name) {
|
|
|
1331
1332
|
}
|
|
1332
1333
|
async function loadSkills() {
|
|
1333
1334
|
registry.clear();
|
|
1335
|
+
const bundledDir = join2(dirname2(fileURLToPath2(import.meta.url)), "..", "skills");
|
|
1334
1336
|
const dirs = [
|
|
1335
1337
|
[join2(homedir3(), ".beecork", "skills"), "global"],
|
|
1336
|
-
[join2(process.cwd(), ".beecork", "skills"), "project"]
|
|
1338
|
+
[join2(process.cwd(), ".beecork", "skills"), "project"],
|
|
1339
|
+
[bundledDir, "bundled"]
|
|
1337
1340
|
];
|
|
1338
1341
|
for (const [dir, source] of dirs) {
|
|
1339
1342
|
let entries;
|
|
@@ -1346,13 +1349,13 @@ async function loadSkills() {
|
|
|
1346
1349
|
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
1347
1350
|
const name = e.name.slice(0, -3);
|
|
1348
1351
|
if (!/^[a-z0-9][a-z0-9_-]*$/i.test(name)) continue;
|
|
1352
|
+
if (registry.has(name)) {
|
|
1353
|
+
if (source === "project") console.error(color.yellow(`\u26A0 project skill /${name} ignored \u2014 a global skill of that name takes precedence`));
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1349
1356
|
try {
|
|
1350
1357
|
const raw = (await readFile2(join2(dir, e.name), "utf8")).trim();
|
|
1351
1358
|
if (!raw) continue;
|
|
1352
|
-
if (source === "project" && registry.has(name)) {
|
|
1353
|
-
console.error(color.yellow(`\u26A0 project skill /${name} ignored \u2014 a global skill of that name takes precedence`));
|
|
1354
|
-
continue;
|
|
1355
|
-
}
|
|
1356
1359
|
const { description, modelInvocable, body } = parseSkill(raw);
|
|
1357
1360
|
registry.set(name, { name, content: body, description, modelInvocable, path: join2(dir, e.name), source });
|
|
1358
1361
|
} catch {
|
|
@@ -1991,6 +1994,16 @@ async function readLineWindow(abs, offset1, limit) {
|
|
|
1991
1994
|
}
|
|
1992
1995
|
return { lines, startLine: start + 1, hasMore, empty: i === 0 };
|
|
1993
1996
|
}
|
|
1997
|
+
var DEV_SIGNALS_SETUP = `The browser link isn't connected yet (nothing is responding on localhost:8317).
|
|
1998
|
+
|
|
1999
|
+
This is "Beecork Skeleton" \u2014 a Chrome extension that sends the app's console errors and failed network requests to me, so I can see what the browser sees instead of guessing. One-time, local-only setup (no account, no signup).
|
|
2000
|
+
|
|
2001
|
+
Walk the user through connecting it:
|
|
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".
|
|
2005
|
+
|
|
2006
|
+
Then call read_dev_signals again. Full step-by-step + troubleshooting is in the "browser-signals" skill.`;
|
|
1994
2007
|
var toolDefs = [
|
|
1995
2008
|
{
|
|
1996
2009
|
name: "read_file",
|
|
@@ -2496,6 +2509,85 @@ ${skill.content}`;
|
|
|
2496
2509
|
}
|
|
2497
2510
|
return `No interactive user is available (headless run). Proceed with the most reasonable option for "${question}" and state the assumption you made.`;
|
|
2498
2511
|
}
|
|
2512
|
+
},
|
|
2513
|
+
{
|
|
2514
|
+
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.",
|
|
2516
|
+
parameters: {
|
|
2517
|
+
type: "object",
|
|
2518
|
+
properties: {
|
|
2519
|
+
kind: { type: "string", description: 'Filter: "network", "console", "pageError", "log", or "all" (default all).' },
|
|
2520
|
+
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)." }
|
|
2522
|
+
},
|
|
2523
|
+
required: []
|
|
2524
|
+
},
|
|
2525
|
+
run: async (args, signal) => {
|
|
2526
|
+
const base = process.env.BEECORK_DEV_SIGNALS_URL || "http://localhost:8317";
|
|
2527
|
+
const kind = args.kind ? String(args.kind) : "";
|
|
2528
|
+
const limit = Math.min(Math.max(Number(args.limit) || 30, 1), 200);
|
|
2529
|
+
const sinceMin = Number(args.since_minutes) || 0;
|
|
2530
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
2531
|
+
if (kind && kind !== "all") params.set("kind", kind);
|
|
2532
|
+
if (sinceMin > 0) params.set("since", String(Date.now() - sinceMin * 6e4));
|
|
2533
|
+
const timeout = AbortSignal.timeout(Math.min(config.webTimeoutMs, 5e3));
|
|
2534
|
+
let data;
|
|
2535
|
+
try {
|
|
2536
|
+
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 bridge may be unhealthy \u2014 try restarting it (node bridge/server.mjs).`;
|
|
2538
|
+
data = await res.json();
|
|
2539
|
+
} catch {
|
|
2540
|
+
return DEV_SIGNALS_SETUP;
|
|
2541
|
+
}
|
|
2542
|
+
const now = Date.now();
|
|
2543
|
+
const signals = (data.signals ?? []).filter((s) => s && s.kind !== "watch");
|
|
2544
|
+
if (signals.length === 0) {
|
|
2545
|
+
return `The browser link is connected, but no ${kind && kind !== "all" ? `"${kind}" ` : ""}signals were captured${sinceMin ? ` in the last ${sinceMin} min` : ""}. The watched tab just hasn't hit the error yet \u2014 reproduce the issue in the browser (or open the app), then call this again.`;
|
|
2546
|
+
}
|
|
2547
|
+
const ago2 = (ts) => ts ? `${Math.max(0, Math.round((now - ts) / 1e3))}s ago` : "";
|
|
2548
|
+
const lines = signals.map((s) => {
|
|
2549
|
+
if (s.kind === "network") return `[network] ${s.method || "GET"} ${s.url ?? ""} \u2192 ${s.status || s.text || "failed"} (${ago2(s.ts)})`;
|
|
2550
|
+
const text = String(s.text ?? "").replace(/\s+/g, " ").slice(0, 300);
|
|
2551
|
+
return `[${s.kind}] ${text}${s.url ? ` @ ${s.url}` : ""} (${ago2(s.ts)})`;
|
|
2552
|
+
});
|
|
2553
|
+
return `${signals.length} browser signal(s), newest last:
|
|
2554
|
+
${lines.join("\n")}`;
|
|
2555
|
+
}
|
|
2556
|
+
},
|
|
2557
|
+
{
|
|
2558
|
+
name: "watch_site",
|
|
2559
|
+
description: "Ask the Beecork Skeleton extension to start watching an APPROVED site's tab right now \u2014 use for an on-demand or production site you need to investigate (localhost/dev sites are watched automatically, so you don't need this for them). Only sites the user already approved are honored. After calling this, reproduce the issue (or open the site), then read_dev_signals.",
|
|
2560
|
+
parameters: {
|
|
2561
|
+
type: "object",
|
|
2562
|
+
properties: {
|
|
2563
|
+
url: { type: "string", description: "The site to watch, e.g. https://app.example.com (its origin is used)." },
|
|
2564
|
+
minutes: { type: "number", description: "How long to keep watching (default 10, max 120)." }
|
|
2565
|
+
},
|
|
2566
|
+
required: ["url"]
|
|
2567
|
+
},
|
|
2568
|
+
run: async (args, signal) => {
|
|
2569
|
+
const base = process.env.BEECORK_DEV_SIGNALS_URL || "http://localhost:8317";
|
|
2570
|
+
let origin;
|
|
2571
|
+
try {
|
|
2572
|
+
origin = new URL(String(args.url ?? "")).origin;
|
|
2573
|
+
} catch {
|
|
2574
|
+
return `Error: "${args.url}" is not a valid URL.`;
|
|
2575
|
+
}
|
|
2576
|
+
const minutes = Math.min(Math.max(Number(args.minutes) || 10, 1), 120);
|
|
2577
|
+
const timeout = AbortSignal.timeout(Math.min(config.webTimeoutMs, 5e3));
|
|
2578
|
+
try {
|
|
2579
|
+
const res = await fetch(`${base}/request-watch`, {
|
|
2580
|
+
method: "POST",
|
|
2581
|
+
headers: { "Content-Type": "application/json" },
|
|
2582
|
+
body: JSON.stringify({ origin, ttlMs: minutes * 6e4 }),
|
|
2583
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout
|
|
2584
|
+
});
|
|
2585
|
+
if (!res.ok) return `The browser link responded with HTTP ${res.status}.`;
|
|
2586
|
+
} catch {
|
|
2587
|
+
return DEV_SIGNALS_SETUP;
|
|
2588
|
+
}
|
|
2589
|
+
return `Requested watching ${origin} for ${minutes} min. 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
|
+
}
|
|
2499
2591
|
}
|
|
2500
2592
|
];
|
|
2501
2593
|
var TOOLS = toolDefs.map((t) => ({
|
|
@@ -2861,15 +2953,15 @@ ${summary}` }, ...recent];
|
|
|
2861
2953
|
// src/memory.ts
|
|
2862
2954
|
import { readFile as readFile4, writeFile as writeFile3, readdir as readdir3, mkdir as mkdir3, chmod as chmod2, rename as rename2, unlink } from "node:fs/promises";
|
|
2863
2955
|
import { homedir as homedir5 } from "node:os";
|
|
2864
|
-
import { join as join4, dirname as
|
|
2956
|
+
import { join as join4, dirname as dirname3 } from "node:path";
|
|
2865
2957
|
var BEECORK = ".beecork";
|
|
2866
2958
|
function ancestorDirs() {
|
|
2867
2959
|
const home = homedir5();
|
|
2868
2960
|
const dirs = [];
|
|
2869
2961
|
let dir = process.cwd();
|
|
2870
|
-
while (dir !== home && dir !==
|
|
2962
|
+
while (dir !== home && dir !== dirname3(dir)) {
|
|
2871
2963
|
dirs.push(dir);
|
|
2872
|
-
dir =
|
|
2964
|
+
dir = dirname3(dir);
|
|
2873
2965
|
}
|
|
2874
2966
|
return dirs.reverse();
|
|
2875
2967
|
}
|
|
@@ -2944,7 +3036,7 @@ async function loadUserConfig() {
|
|
|
2944
3036
|
}
|
|
2945
3037
|
async function saveUserConfig(patch) {
|
|
2946
3038
|
const file = userConfigPath();
|
|
2947
|
-
await mkdir3(
|
|
3039
|
+
await mkdir3(dirname3(file), { recursive: true });
|
|
2948
3040
|
const merged = { ...await loadUserConfig(), ...patch };
|
|
2949
3041
|
const tmp = `${file}.tmp`;
|
|
2950
3042
|
await writeFile3(tmp, JSON.stringify(merged, null, 2), { encoding: "utf8", mode: 384 });
|
|
@@ -2955,7 +3047,7 @@ async function saveUserConfig(patch) {
|
|
|
2955
3047
|
async function saveModelPreference(model) {
|
|
2956
3048
|
try {
|
|
2957
3049
|
const file = join4(homedir5(), BEECORK, "settings.json");
|
|
2958
|
-
await mkdir3(
|
|
3050
|
+
await mkdir3(dirname3(file), { recursive: true });
|
|
2959
3051
|
const current = await readJsonFile(file) ?? {};
|
|
2960
3052
|
await writeFile3(file, JSON.stringify({ ...current, model }, null, 2), "utf8");
|
|
2961
3053
|
} catch {
|
|
@@ -2964,7 +3056,7 @@ async function saveModelPreference(model) {
|
|
|
2964
3056
|
async function saveReasoningPreference(reasoningEffort) {
|
|
2965
3057
|
try {
|
|
2966
3058
|
const file = join4(homedir5(), BEECORK, "settings.json");
|
|
2967
|
-
await mkdir3(
|
|
3059
|
+
await mkdir3(dirname3(file), { recursive: true });
|
|
2968
3060
|
const current = await readJsonFile(file) ?? {};
|
|
2969
3061
|
await writeFile3(file, JSON.stringify({ ...current, reasoningEffort }, null, 2), "utf8");
|
|
2970
3062
|
} catch {
|
|
@@ -3080,7 +3172,7 @@ async function loadProjectApprovals() {
|
|
|
3080
3172
|
async function addProjectApproval(tool) {
|
|
3081
3173
|
try {
|
|
3082
3174
|
const file = projectApprovalsPath();
|
|
3083
|
-
await mkdir3(
|
|
3175
|
+
await mkdir3(dirname3(file), { recursive: true });
|
|
3084
3176
|
const all = await readJsonFile(file) ?? {};
|
|
3085
3177
|
const list = new Set(Array.isArray(all[projectRoot]) ? all[projectRoot] : []);
|
|
3086
3178
|
list.add(tool);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beecork",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
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": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"files": [
|
|
11
|
-
"dist/"
|
|
11
|
+
"dist/",
|
|
12
|
+
"skills/"
|
|
12
13
|
],
|
|
13
14
|
"engines": {
|
|
14
15
|
"node": ">=20.12"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Set up / use the browser link so beecork sees the app's console + network errors (read_dev_signals)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Browser signals — let beecork see the running app
|
|
6
|
+
|
|
7
|
+
You can read the user's app **console errors** and **failed network requests** straight
|
|
8
|
+
from the browser — on localhost *or* production, in their real logged-in session — with the
|
|
9
|
+
`read_dev_signals` tool. No copy-pasting errors. This is the **Beecork Skeleton** Chrome
|
|
10
|
+
extension plus a tiny local inbox (the "bridge").
|
|
11
|
+
|
|
12
|
+
## When to use `read_dev_signals`
|
|
13
|
+
|
|
14
|
+
Call it whenever the user reports something a browser would surface — a blank page, a broken
|
|
15
|
+
button, a form that won't submit, a save that 500s, a visual glitch. Instead of guessing, pull
|
|
16
|
+
the real errors:
|
|
17
|
+
|
|
18
|
+
- `read_dev_signals({ kind: "network" })` — failed requests (status ≥ 400 / network failures)
|
|
19
|
+
- `read_dev_signals({ since_minutes: 5 })` — everything captured in the last 5 minutes
|
|
20
|
+
- `read_dev_signals({})` — the most recent signals of any kind
|
|
21
|
+
|
|
22
|
+
Pull it **on demand** while debugging — don't call it in a loop or on every turn.
|
|
23
|
+
|
|
24
|
+
## Watching an on-demand / production site
|
|
25
|
+
|
|
26
|
+
Localhost/dev sites the user approved are watched automatically. A **production** (or
|
|
27
|
+
any "on-demand") approved site is idle until asked. To investigate one, call
|
|
28
|
+
`watch_site({ url })` — it asks the extension to watch that site for a while. Only sites
|
|
29
|
+
the user already approved are honored (beecork can't start watching a brand-new site on
|
|
30
|
+
its own). Then have the user reproduce the issue (or open the site) and call
|
|
31
|
+
`read_dev_signals`.
|
|
32
|
+
|
|
33
|
+
## If it says "not connected" — one-time setup
|
|
34
|
+
|
|
35
|
+
The tool returns setup steps when the bridge isn't running. Relay them to the user:
|
|
36
|
+
|
|
37
|
+
1. **Start the local inbox (bridge):** run `node bridge/server.mjs` in the `beecork-extension`
|
|
38
|
+
folder and leave it running. It listens on `localhost:8317`.
|
|
39
|
+
2. **Load the extension:** Chrome → `chrome://extensions` → enable **Developer mode** →
|
|
40
|
+
**Load unpacked** → select the `beecork-extension/extension` folder → pin the icon.
|
|
41
|
+
3. **Connect + approve:** click the icon (it auto-connects — no token to paste), tick
|
|
42
|
+
**Capture enabled**, open the app in a tab, and click **Pair this site**.
|
|
43
|
+
|
|
44
|
+
Then call `read_dev_signals` again.
|
|
45
|
+
|
|
46
|
+
## Empty result
|
|
47
|
+
|
|
48
|
+
If it connects but returns nothing, the watched tab just hasn't hit the error yet. Either ask
|
|
49
|
+
the user to reproduce it, or open the app yourself to trigger it — e.g.
|
|
50
|
+
`open -a "Google Chrome" http://localhost:3000` (macOS) — then reproduce the action and call
|
|
51
|
+
`read_dev_signals` again.
|
|
52
|
+
|
|
53
|
+
## How it stays safe
|
|
54
|
+
|
|
55
|
+
- **Approved sites only** — nothing is watched except sites the user approved in the popup.
|
|
56
|
+
- **Secrets redacted in the browser** — tokens, API keys, passwords, and Authorization values
|
|
57
|
+
are stripped *before* any signal leaves the tab, so they never reach the inbox.
|
|
58
|
+
- **Local + authenticated** — the inbox is `127.0.0.1` only, and only the extension can write to
|
|
59
|
+
it (an automatic local token; a web page you visit cannot).
|