beecork 2.7.2 → 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 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 dirname2 } from "node:path";
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 !== dirname2(dir)) {
2962
+ while (dir !== home && dir !== dirname3(dir)) {
2871
2963
  dirs.push(dir);
2872
- dir = dirname2(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(dirname2(file), { recursive: true });
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(dirname2(file), { recursive: true });
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(dirname2(file), { recursive: true });
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(dirname2(file), { recursive: true });
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.7.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).