beecork 2.1.0 → 2.2.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +158 -75
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -61,7 +61,7 @@ outside (or any shell command) goes through a permission gate.
61
61
 
62
62
  ### Slash commands
63
63
 
64
- `/model` · `/key` · `/context` · `/clear` · `/resume` · `/good` · `/bad` · `/help` · `Shift+Tab` (rotate mode) · `exit`
64
+ `/model` · `/key` · `/update` · `/context` · `/clear` · `/resume` · `/good` · `/bad` · `/help` · `Shift+Tab` (rotate mode) · `exit`
65
65
 
66
66
  ### Memory
67
67
 
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 writeFile4, chmod as chmod4 } from "node:fs/promises";
4
+ import { writeFile as writeFile5, chmod as chmod4 } from "node:fs/promises";
5
5
  import { createInterface } from "node:readline/promises";
6
6
 
7
7
  // src/paths.ts
@@ -31,21 +31,6 @@ function canonical(p) {
31
31
  }
32
32
 
33
33
  // src/config.ts
34
- import { readFileSync } from "node:fs";
35
- import { parseEnv } from "node:util";
36
- var SAFE_ENV_KEYS = ["OPENROUTER_API_KEY", "OPENROUTER_MODEL", "BRAVE_API_KEY"];
37
- var keyFromProjectEnv = false;
38
- try {
39
- const fromFile = parseEnv(readFileSync(".env", "utf8"));
40
- for (const key of SAFE_ENV_KEYS) {
41
- if (process.env[key] === void 0 && fromFile[key] !== void 0) {
42
- process.env[key] = fromFile[key];
43
- if (key === "OPENROUTER_API_KEY") keyFromProjectEnv = true;
44
- }
45
- }
46
- } catch {
47
- }
48
- var KEY_FROM_PROJECT_ENV = keyFromProjectEnv;
49
34
  var API_KEY = process.env.OPENROUTER_API_KEY ?? "";
50
35
  var RECOMMENDED_MODELS = [
51
36
  { slug: "deepseek/deepseek-v4-flash", price: "$0.09", note: "cheap + fast daily driver (default)" },
@@ -111,6 +96,75 @@ var config = {
111
96
  // headless: skip permission prompts (explicit truthy only)
112
97
  };
113
98
 
99
+ // src/update.ts
100
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
101
+ import { homedir as homedir2 } from "node:os";
102
+ import { join, dirname } from "node:path";
103
+ import { spawn } from "node:child_process";
104
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
105
+ var cacheFile = () => join(homedir2(), ".beecork", "update-check.json");
106
+ async function currentVersion() {
107
+ try {
108
+ const pkg = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
109
+ return String(pkg.version ?? "0.0.0");
110
+ } catch {
111
+ return "0.0.0";
112
+ }
113
+ }
114
+ function isNewer(a, b) {
115
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
116
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
117
+ for (let i = 0; i < 3; i++) {
118
+ if ((pa[i] || 0) > (pb[i] || 0)) return true;
119
+ if ((pa[i] || 0) < (pb[i] || 0)) return false;
120
+ }
121
+ return false;
122
+ }
123
+ async function fetchLatest() {
124
+ try {
125
+ const res = await fetch("https://registry.npmjs.org/beecork/latest", {
126
+ headers: { Accept: "application/vnd.npm.install-v1+json" },
127
+ // the slim metadata doc
128
+ signal: AbortSignal.timeout(3e3)
129
+ });
130
+ if (!res.ok) return null;
131
+ const v = (await res.json())?.version;
132
+ return typeof v === "string" ? v : null;
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+ async function checkForUpdate(current) {
138
+ if (process.env.NO_UPDATE_NOTIFIER || process.env.CI) return null;
139
+ let cache = {};
140
+ try {
141
+ cache = JSON.parse(await readFile(cacheFile(), "utf8"));
142
+ } catch {
143
+ }
144
+ if (!cache.checkedAt || Date.now() - cache.checkedAt > CHECK_INTERVAL_MS) {
145
+ void fetchLatest().then(async (latest) => {
146
+ if (!latest) return;
147
+ try {
148
+ const file = cacheFile();
149
+ await mkdir(dirname(file), { recursive: true });
150
+ await writeFile(file, JSON.stringify({ checkedAt: Date.now(), latest }), "utf8");
151
+ } catch {
152
+ }
153
+ });
154
+ }
155
+ return cache.latest && isNewer(cache.latest, current) ? cache.latest : null;
156
+ }
157
+ function selfUpdate() {
158
+ return new Promise((resolve2) => {
159
+ const child = spawn("npm", ["install", "-g", "beecork@latest"], { stdio: ["ignore", "pipe", "pipe"] });
160
+ let out2 = "";
161
+ child.stdout?.on("data", (d) => out2 += d);
162
+ child.stderr?.on("data", (d) => out2 += d);
163
+ child.on("error", (e) => resolve2({ ok: false, output: e.message }));
164
+ child.on("close", (code) => resolve2({ ok: code === 0, output: out2.trim() }));
165
+ });
166
+ }
167
+
114
168
  // src/state.ts
115
169
  var MODES = ["normal", "auto", "readonly"];
116
170
  function nextMode(m) {
@@ -393,7 +447,7 @@ function printBanner(model, sources) {
393
447
  }
394
448
 
395
449
  // src/agent.ts
396
- import { readFile as readFile3 } from "node:fs/promises";
450
+ import { readFile as readFile4 } from "node:fs/promises";
397
451
 
398
452
  // src/show.ts
399
453
  var BOX_W = () => Math.min(process.stdout.columns || 80, 100);
@@ -593,18 +647,18 @@ function createMarkdownStream(write) {
593
647
  }
594
648
 
595
649
  // src/tools.ts
596
- import { readFile, writeFile, appendFile, readdir, mkdir, stat, rename, chmod } from "node:fs/promises";
650
+ import { readFile as readFile2, writeFile as writeFile2, appendFile, readdir, mkdir as mkdir2, stat, rename, chmod } from "node:fs/promises";
597
651
  import { createReadStream } from "node:fs";
598
652
  import { createInterface as createLineReader } from "node:readline";
599
- import { spawn } from "node:child_process";
600
- import { join } from "node:path";
653
+ import { spawn as spawn2 } from "node:child_process";
654
+ import { join as join2 } from "node:path";
601
655
  import { lookup as dnsLookup } from "node:dns";
602
656
  import { request as httpRequest } from "node:http";
603
657
  import { request as httpsRequest } from "node:https";
604
658
  import { isIP } from "node:net";
605
659
 
606
660
  // src/safety.ts
607
- import { homedir as homedir2 } from "node:os";
661
+ import { homedir as homedir3 } from "node:os";
608
662
  function pathGuard(args) {
609
663
  const { abs, inRoot } = resolveInRoot(String(args.path ?? "."));
610
664
  return inRoot ? {} : { needsApproval: true, reason: `path is outside the project root: ${abs}` };
@@ -655,7 +709,7 @@ var RISKY_BASH = [
655
709
  // eval/source of a download
656
710
  ];
657
711
  function refsOutsideRoot(cmd) {
658
- const norm = cmd.replace(/\$\{?IFS\}?/g, " ").replace(/\$\{?HOME\}?/g, homedir2()).replace(/\$\{?PWD\}?/g, process.cwd()).replace(/(^|[\s"'`=(])~(?=\/|$)/g, (_m, p) => p + homedir2());
712
+ const norm = cmd.replace(/\$\{?IFS\}?/g, " ").replace(/\$\{?HOME\}?/g, homedir3()).replace(/\$\{?PWD\}?/g, process.cwd()).replace(/(^|[\s"'`=(])~(?=\/|$)/g, (_m, p) => p + homedir3());
659
713
  if (/(^|[\s"'`=(])(\.\.\/|~(\/|$))/.test(norm)) return true;
660
714
  for (const m of norm.matchAll(/(?:^|[\s"'`=(])(\/[^\s"'`;|&()<>]*)/g)) {
661
715
  if (!resolveInRoot(m[1]).inRoot) return true;
@@ -730,7 +784,7 @@ function decodeEntities(s) {
730
784
  function runShell(command, opts) {
731
785
  return new Promise((resolve2, reject) => {
732
786
  const unix = process.platform !== "win32";
733
- const child = spawn(command, { shell: true, detached: unix });
787
+ const child = spawn2(command, { shell: true, detached: unix });
734
788
  let stdout = "", stderr = "", outLen = 0, errLen = 0, timedOut = false, aborted = false;
735
789
  const kill = () => {
736
790
  try {
@@ -787,7 +841,7 @@ function runShell(command, opts) {
787
841
  var fail = (verb, err) => `Error ${verb}: ${err.message}`;
788
842
  async function atomicWrite(abs, content) {
789
843
  const tmp = `${abs}.beecork-${process.pid}.tmp`;
790
- await writeFile(tmp, content, "utf8");
844
+ await writeFile2(tmp, content, "utf8");
791
845
  try {
792
846
  await chmod(tmp, (await stat(abs)).mode);
793
847
  } catch {
@@ -886,7 +940,7 @@ async function walkTree(abs, prefix, items, cap) {
886
940
  const e = kept[i];
887
941
  const last = i === kept.length - 1;
888
942
  items.push({ prefix: prefix + (last ? "\u2514\u2500 " : "\u251C\u2500 "), name: e.name + (e.isDirectory() ? "/" : ""), isDir: e.isDirectory() });
889
- if (e.isDirectory()) await walkTree(join(abs, e.name), prefix + (last ? " " : "\u2502 "), items, cap);
943
+ if (e.isDirectory()) await walkTree(join2(abs, e.name), prefix + (last ? " " : "\u2502 "), items, cap);
890
944
  }
891
945
  }
892
946
  function parseRange(args, defLimit) {
@@ -1038,7 +1092,7 @@ var toolDefs = [
1038
1092
  if (info && info.size > config.searchMaxFileBytes) continue;
1039
1093
  let lines;
1040
1094
  try {
1041
- lines = (await readFile(full, "utf8")).split("\n");
1095
+ lines = (await readFile2(full, "utf8")).split("\n");
1042
1096
  } catch {
1043
1097
  continue;
1044
1098
  }
@@ -1104,7 +1158,7 @@ var toolDefs = [
1104
1158
  run: async (args) => {
1105
1159
  try {
1106
1160
  const { abs } = resolveInRoot(String(args.path ?? "."));
1107
- const original = await readFile(abs, "utf8");
1161
+ const original = await readFile2(abs, "utf8");
1108
1162
  const count = original.split(args.old_text).length - 1;
1109
1163
  if (count === 0) {
1110
1164
  return `Error: old_text not found in ${args.path}. Re-read the file and copy the exact text (including whitespace/indentation).`;
@@ -1143,14 +1197,19 @@ var toolDefs = [
1143
1197
  },
1144
1198
  {
1145
1199
  name: "run_bash",
1146
- description: "Run a shell command and return its output. Use for things like running tests or git.",
1200
+ description: "Run a shell command and return its output (tests, git, builds, etc.). You MUST set `explanation` with what the command does and why you need it \u2014 the user sees it and approves every run.",
1147
1201
  needsApproval: true,
1202
+ alwaysAsk: true,
1203
+ // shell access is confirmed every time — never silently "always"-cached
1148
1204
  guard: bashGuard,
1149
1205
  // risky commands (rm/dd/sudo/pipe-to-interpreter…) get the per-call gate
1150
1206
  parameters: {
1151
1207
  type: "object",
1152
- properties: { command: { type: "string", description: "The shell command to run." } },
1153
- required: ["command"]
1208
+ properties: {
1209
+ command: { type: "string", description: "The shell command to run." },
1210
+ explanation: { type: "string", description: "One sentence: WHAT this command does and WHY you need it now. Shown to the user before they approve." }
1211
+ },
1212
+ required: ["command", "explanation"]
1154
1213
  },
1155
1214
  run: async (args, signal) => {
1156
1215
  const danger = DANGEROUS_BASH.find((re) => re.test(String(args.command)));
@@ -1285,13 +1344,13 @@ ${body || "(no text content)"}`;
1285
1344
  },
1286
1345
  run: async (args) => {
1287
1346
  try {
1288
- const dir = join(process.cwd(), ".beecork");
1289
- await mkdir(dir, { recursive: true });
1290
- const file = join(dir, "memory.md");
1347
+ const dir = join2(process.cwd(), ".beecork");
1348
+ await mkdir2(dir, { recursive: true });
1349
+ const file = join2(dir, "memory.md");
1291
1350
  try {
1292
1351
  await stat(file);
1293
1352
  } catch {
1294
- await writeFile(file, "# beecork memory\n\n", "utf8");
1353
+ await writeFile2(file, "# beecork memory\n\n", "utf8");
1295
1354
  }
1296
1355
  await appendFile(file, `- ${String(args.fact).trim()}
1297
1356
  `, "utf8");
@@ -1559,29 +1618,29 @@ ${summary}` }, ...recent];
1559
1618
  }
1560
1619
 
1561
1620
  // src/memory.ts
1562
- import { readFile as readFile2, writeFile as writeFile2, readdir as readdir2, mkdir as mkdir2, chmod as chmod2, rename as rename2 } from "node:fs/promises";
1563
- import { homedir as homedir3 } from "node:os";
1564
- import { join as join2, dirname } from "node:path";
1621
+ import { readFile as readFile3, writeFile as writeFile3, readdir as readdir2, mkdir as mkdir3, chmod as chmod2, rename as rename2 } from "node:fs/promises";
1622
+ import { homedir as homedir4 } from "node:os";
1623
+ import { join as join3, dirname as dirname2 } from "node:path";
1565
1624
  var BEECORK = ".beecork";
1566
1625
  function ancestorDirs() {
1567
- const home = homedir3();
1626
+ const home = homedir4();
1568
1627
  const dirs = [];
1569
1628
  let dir = process.cwd();
1570
- while (dir !== home && dir !== dirname(dir)) {
1629
+ while (dir !== home && dir !== dirname2(dir)) {
1571
1630
  dirs.push(dir);
1572
- dir = dirname(dir);
1631
+ dir = dirname2(dir);
1573
1632
  }
1574
1633
  return dirs.reverse();
1575
1634
  }
1576
1635
  function corkPaths() {
1577
- return [join2(homedir3(), BEECORK, "cork.md"), ...ancestorDirs().map((d) => join2(d, "cork.md"))];
1636
+ return [join3(homedir4(), BEECORK, "cork.md"), ...ancestorDirs().map((d) => join3(d, "cork.md"))];
1578
1637
  }
1579
1638
  function beecorkPaths(name) {
1580
- return [join2(homedir3(), BEECORK, name), ...ancestorDirs().map((d) => join2(d, BEECORK, name))];
1639
+ return [join3(homedir4(), BEECORK, name), ...ancestorDirs().map((d) => join3(d, BEECORK, name))];
1581
1640
  }
1582
1641
  async function loadInstructions() {
1583
- const home = homedir3();
1584
- const homeBeecork = join2(home, ".beecork");
1642
+ const home = homedir4();
1643
+ const homeBeecork = join3(home, ".beecork");
1585
1644
  const trusted = [];
1586
1645
  const project = [];
1587
1646
  const sources = [];
@@ -1590,7 +1649,7 @@ async function loadInstructions() {
1590
1649
  let total = 0;
1591
1650
  for (const file of [...corkPaths(), ...beecorkPaths("memory.md")]) {
1592
1651
  try {
1593
- let content = (await readFile2(file, "utf8")).trim();
1652
+ let content = (await readFile3(file, "utf8")).trim();
1594
1653
  if (!content) continue;
1595
1654
  if (content.length > MAX_FILE) content = content.slice(0, MAX_FILE) + "\n\u2026(truncated)";
1596
1655
  if (total + content.length > MAX_TOTAL) content = content.slice(0, Math.max(0, MAX_TOTAL - total)) + "\n\u2026(truncated)";
@@ -1607,7 +1666,7 @@ ${content}`;
1607
1666
  }
1608
1667
  async function readJsonFile(path) {
1609
1668
  try {
1610
- return JSON.parse(await readFile2(path, "utf8"));
1669
+ return JSON.parse(await readFile3(path, "utf8"));
1611
1670
  } catch (err) {
1612
1671
  if (err.code !== "ENOENT") {
1613
1672
  console.error(color.yellow(`\u26A0 ignoring malformed ${tildify(path)}: ${err.message}`));
@@ -1632,29 +1691,29 @@ async function loadSettings() {
1632
1691
  return { model, alwaysAllow, projectAlwaysAllowIgnored };
1633
1692
  }
1634
1693
  function userConfigPath() {
1635
- return join2(homedir3(), BEECORK, "config.json");
1694
+ return join3(homedir4(), BEECORK, "config.json");
1636
1695
  }
1637
1696
  async function loadUserConfig() {
1638
1697
  return await readJsonFile(userConfigPath()) ?? {};
1639
1698
  }
1640
1699
  async function saveUserConfig(patch) {
1641
1700
  const file = userConfigPath();
1642
- await mkdir2(dirname(file), { recursive: true });
1701
+ await mkdir3(dirname2(file), { recursive: true });
1643
1702
  const merged = { ...await loadUserConfig(), ...patch };
1644
- await writeFile2(file, JSON.stringify(merged, null, 2), "utf8");
1703
+ await writeFile3(file, JSON.stringify(merged, null, 2), "utf8");
1645
1704
  try {
1646
1705
  await chmod2(file, 384);
1647
1706
  } catch {
1648
1707
  }
1649
1708
  }
1650
- var sessionsDir = () => join2(process.cwd(), BEECORK, "sessions");
1709
+ var sessionsDir = () => join3(process.cwd(), BEECORK, "sessions");
1651
1710
  async function saveSession(messages) {
1652
1711
  try {
1653
1712
  const dir = sessionsDir();
1654
- await mkdir2(dir, { recursive: true });
1655
- const file = join2(dir, `${Date.now()}.json`);
1713
+ await mkdir3(dir, { recursive: true });
1714
+ const file = join3(dir, `${Date.now()}.json`);
1656
1715
  const tmp = `${file}.tmp`;
1657
- await writeFile2(tmp, JSON.stringify(messages), "utf8");
1716
+ await writeFile3(tmp, JSON.stringify(messages), "utf8");
1658
1717
  await chmod2(tmp, 384).catch(() => {
1659
1718
  });
1660
1719
  await rename2(tmp, file);
@@ -1682,7 +1741,7 @@ function sanitizeSession(raw) {
1682
1741
  }
1683
1742
  async function readSession(file) {
1684
1743
  try {
1685
- return sanitizeSession(JSON.parse(await readFile2(join2(sessionsDir(), file), "utf8")));
1744
+ return sanitizeSession(JSON.parse(await readFile3(join3(sessionsDir(), file), "utf8")));
1686
1745
  } catch {
1687
1746
  return null;
1688
1747
  }
@@ -1719,7 +1778,7 @@ async function loadSession(file) {
1719
1778
  return await readSession(file) ?? [];
1720
1779
  }
1721
1780
  function projectApprovalsPath() {
1722
- return join2(homedir3(), BEECORK, "project-approvals.json");
1781
+ return join3(homedir4(), BEECORK, "project-approvals.json");
1723
1782
  }
1724
1783
  async function loadProjectApprovals() {
1725
1784
  const all = await readJsonFile(projectApprovalsPath());
@@ -1729,12 +1788,12 @@ async function loadProjectApprovals() {
1729
1788
  async function addProjectApproval(tool) {
1730
1789
  try {
1731
1790
  const file = projectApprovalsPath();
1732
- await mkdir2(dirname(file), { recursive: true });
1791
+ await mkdir3(dirname2(file), { recursive: true });
1733
1792
  const all = await readJsonFile(file) ?? {};
1734
1793
  const list = new Set(Array.isArray(all[projectRoot]) ? all[projectRoot] : []);
1735
1794
  list.add(tool);
1736
1795
  all[projectRoot] = [...list];
1737
- await writeFile2(file, JSON.stringify(all, null, 2), "utf8");
1796
+ await writeFile3(file, JSON.stringify(all, null, 2), "utf8");
1738
1797
  await chmod2(file, 384).catch(() => {
1739
1798
  });
1740
1799
  } catch {
@@ -1778,12 +1837,13 @@ async function askApproval(ask, call, reason) {
1778
1837
  \u26A0\uFE0F The agent wants to use: ${call.function.name}`));
1779
1838
  if (reason) console.log(color.red(` \u26A0 ${reason}`));
1780
1839
  if (call.function.name === "run_bash") {
1840
+ if (args.explanation) console.log(" " + color.cyan(stripControl(String(args.explanation))));
1781
1841
  console.log(color.yellow(` $ ${stripControl(String(args.command ?? ""))}`));
1782
1842
  } else if (call.function.name === "edit_file") {
1783
1843
  console.log(color.yellow(` edit ${stripControl(String(args.path ?? ""))}:`));
1784
1844
  console.log(diffPreview(lineDiff(stripControl(String(args.old_text ?? "")), stripControl(String(args.new_text ?? "")))));
1785
1845
  } else if (call.function.name === "write_file") {
1786
- const existing = (await readFile3(resolveInRoot(String(args.path ?? ".")).abs, "utf8").catch(() => "")).slice(0, 2e5);
1846
+ const existing = (await readFile4(resolveInRoot(String(args.path ?? ".")).abs, "utf8").catch(() => "")).slice(0, 2e5);
1787
1847
  console.log(color.yellow(` write ${stripControl(String(args.path ?? ""))} ${existing ? "(overwrite)" : "(new file)"}:`));
1788
1848
  console.log(diffPreview(lineDiff(stripControl(existing), stripControl(String(args.content ?? "")))));
1789
1849
  } else {
@@ -1803,8 +1863,9 @@ function decideApproval(tool, args, ctx) {
1803
1863
  if (ctx.autoApprove) return { action: "deny", kind: "headless", reason: guard.reason ?? "blocked" };
1804
1864
  return { action: "ask", cacheable: false, reason: guard.reason };
1805
1865
  }
1806
- if (tool?.needsApproval && !ctx.approvedTools.has(ctx.toolName) && !ctx.autoApprove && ctx.mode !== "auto") {
1807
- return { action: "ask", cacheable: true };
1866
+ if (tool?.needsApproval && !ctx.autoApprove && ctx.mode !== "auto") {
1867
+ if (tool.alwaysAsk) return { action: "ask", cacheable: false };
1868
+ if (!ctx.approvedTools.has(ctx.toolName)) return { action: "ask", cacheable: true };
1808
1869
  }
1809
1870
  return { action: "run" };
1810
1871
  }
@@ -1951,12 +2012,12 @@ async function runTurn(messages, userInput, ask, approvedTools, signal) {
1951
2012
  }
1952
2013
 
1953
2014
  // src/commands.ts
1954
- import { writeFile as writeFile3, mkdir as mkdir3, chmod as chmod3 } from "node:fs/promises";
2015
+ import { writeFile as writeFile4, mkdir as mkdir4, chmod as chmod3 } from "node:fs/promises";
1955
2016
 
1956
2017
  // src/skills.ts
1957
- import { readdir as readdir3, readFile as readFile4 } from "node:fs/promises";
1958
- import { join as join3 } from "node:path";
1959
- import { homedir as homedir4 } from "node:os";
2018
+ import { readdir as readdir3, readFile as readFile5 } from "node:fs/promises";
2019
+ import { join as join4 } from "node:path";
2020
+ import { homedir as homedir5 } from "node:os";
1960
2021
  var registry = /* @__PURE__ */ new Map();
1961
2022
  function skillNames() {
1962
2023
  return [...registry.keys()];
@@ -1967,8 +2028,8 @@ function getSkill(name) {
1967
2028
  async function loadSkills() {
1968
2029
  registry.clear();
1969
2030
  const dirs = [
1970
- [join3(homedir4(), ".beecork", "skills"), "global"],
1971
- [join3(process.cwd(), ".beecork", "skills"), "project"]
2031
+ [join4(homedir5(), ".beecork", "skills"), "global"],
2032
+ [join4(process.cwd(), ".beecork", "skills"), "project"]
1972
2033
  ];
1973
2034
  for (const [dir, source] of dirs) {
1974
2035
  let entries;
@@ -1982,7 +2043,7 @@ async function loadSkills() {
1982
2043
  const name = e.name.slice(0, -3);
1983
2044
  if (!/^[a-z0-9][a-z0-9_-]*$/i.test(name)) continue;
1984
2045
  try {
1985
- const content = (await readFile4(join3(dir, e.name), "utf8")).trim();
2046
+ const content = (await readFile5(join4(dir, e.name), "utf8")).trim();
1986
2047
  if (!content) continue;
1987
2048
  if (source === "project" && registry.has(name)) {
1988
2049
  console.error(color.yellow(`\u26A0 project skill /${name} ignored \u2014 a global skill of that name takes precedence`));
@@ -2328,6 +2389,7 @@ var SLASH_COMMANDS = [
2328
2389
  { name: "/context", desc: "conversation size in tokens" },
2329
2390
  { name: "/clear", desc: "clear the conversation" },
2330
2391
  { name: "/key", desc: "set + save your OpenRouter API key" },
2392
+ { name: "/update", desc: "update beecork to the latest version" },
2331
2393
  { name: "/resume", desc: "resume a previous session (pick from a list)" },
2332
2394
  { name: "/good", desc: "rate this conversation good" },
2333
2395
  { name: "/bad", desc: "rate this conversation bad (\u2192 eval/failures)" },
@@ -2376,6 +2438,14 @@ async function handleCommand(input, messages) {
2376
2438
  `~${estimateTokens(messages)} tokens in ${messages.length} messages (auto-compacts above ${config.maxContextTokens})`
2377
2439
  ) + "\n"
2378
2440
  );
2441
+ } else if (cmd === "/update") {
2442
+ console.log(color.dim("updating beecork\u2026 (npm install -g beecork@latest)"));
2443
+ const { ok, output } = await selfUpdate();
2444
+ if (ok) console.log(color.green("\u2713 updated \u2014 restart beecork to use the new version.") + "\n");
2445
+ else {
2446
+ console.log(color.red("update failed: ") + output.split("\n").filter(Boolean).slice(-1)[0]);
2447
+ console.log(color.dim(" run manually: npm install -g beecork (may need sudo / your version manager)") + "\n");
2448
+ }
2379
2449
  } else if (cmd === "/clear") {
2380
2450
  messages.splice(1);
2381
2451
  if (process.stdout.isTTY) process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
@@ -2410,9 +2480,9 @@ async function handleCommand(input, messages) {
2410
2480
  } else if (cmd === "/good" || cmd === "/bad") {
2411
2481
  const dir = cmd === "/bad" ? "eval/failures" : "eval/good";
2412
2482
  try {
2413
- await mkdir3(dir, { recursive: true });
2483
+ await mkdir4(dir, { recursive: true });
2414
2484
  const file = `${dir}/${Date.now()}.json`;
2415
- await writeFile3(file, JSON.stringify({ rating: cmd.slice(1), model: state.model, messages }, null, 2), "utf8");
2485
+ await writeFile4(file, JSON.stringify({ rating: cmd.slice(1), model: state.model, messages }, null, 2), "utf8");
2416
2486
  await chmod3(file, 384).catch(() => {
2417
2487
  });
2418
2488
  console.log(
@@ -2517,6 +2587,17 @@ function completer(line) {
2517
2587
 
2518
2588
  // src/index.ts
2519
2589
  async function main() {
2590
+ if (process.argv[2] === "update") {
2591
+ console.log("Updating beecork\u2026");
2592
+ const { ok, output } = await selfUpdate();
2593
+ if (ok) console.log(color.green("\u2713 beecork updated. Run `beecork` to use the new version."));
2594
+ else {
2595
+ console.error(color.red("Update failed:") + "\n" + output);
2596
+ console.error(color.dim("\nTry manually: npm install -g beecork (you may need sudo, or your Node version manager)."));
2597
+ process.exitCode = 1;
2598
+ }
2599
+ return;
2600
+ }
2520
2601
  const [userCfg, instr, settings, skills, projectApprovals] = await Promise.all([
2521
2602
  loadUserConfig(),
2522
2603
  // ~/.beecork/config.json (API keys)
@@ -2530,7 +2611,7 @@ async function main() {
2530
2611
  // per-project "always" from past sessions
2531
2612
  ]);
2532
2613
  const savedKey = String(userCfg.OPENROUTER_API_KEY ?? "");
2533
- let apiKey = KEY_FROM_PROJECT_ENV ? savedKey || API_KEY : API_KEY || savedKey;
2614
+ let apiKey = API_KEY || savedKey;
2534
2615
  state.braveKey = process.env.BRAVE_API_KEY || String(userCfg.BRAVE_API_KEY ?? "");
2535
2616
  if (settings.model && !process.env.OPENROUTER_MODEL) state.model = settings.model;
2536
2617
  let systemContent = SYSTEM_PROMPT;
@@ -2568,6 +2649,11 @@ ${instr.trusted}`;
2568
2649
  }
2569
2650
  const rl = tty ? null : createInterface({ input: process.stdin, output: process.stdout, completer });
2570
2651
  printBanner(state.model, instr.sources.map(tildify));
2652
+ if (tty) {
2653
+ const version = await currentVersion();
2654
+ const newer = await checkForUpdate(version);
2655
+ if (newer) console.log(color.dim(`\u25B8 beecork ${newer} is available (you have ${version}) \u2014 update: npm install -g beecork`) + "\n");
2656
+ }
2571
2657
  if (approvedTools.size) {
2572
2658
  console.log(color.dim(`pre-approved tools (won't ask this session): ${[...approvedTools].join(", ")}`) + "\n");
2573
2659
  }
@@ -2589,15 +2675,12 @@ ${instr.trusted}`;
2589
2675
  }
2590
2676
  }
2591
2677
  if (!apiKey) {
2592
- console.error("No OpenRouter API key. Set OPENROUTER_API_KEY, add it to .env, or run interactively to paste one.");
2678
+ console.error("No OpenRouter API key. Set the OPENROUTER_API_KEY env var, or run beecork interactively to paste one (saved to ~/.beecork).");
2593
2679
  teardownInput();
2594
2680
  rl?.close();
2595
2681
  process.exit(1);
2596
2682
  }
2597
2683
  state.apiKey = apiKey;
2598
- if (KEY_FROM_PROJECT_ENV && apiKey === API_KEY && apiKey) {
2599
- console.log(color.yellow("\u26A0 Using the OpenRouter API key from this project's .env \u2014 not your saved key. If this repo isn't yours, that key (and your prompts) may not be safe.") + "\n");
2600
- }
2601
2684
  const ask = tty ? (q) => readChoice(q) : (q) => rl.question(q);
2602
2685
  let activeTurn = null;
2603
2686
  if (!tty) {
@@ -2672,7 +2755,7 @@ ${instr.trusted}`;
2672
2755
  rl?.close();
2673
2756
  if (messages.length > 1 && !config.traceFile) await saveSession(messages.slice(1));
2674
2757
  if (config.traceFile) {
2675
- await writeFile4(config.traceFile, JSON.stringify(trace), "utf8");
2758
+ await writeFile5(config.traceFile, JSON.stringify(trace), "utf8");
2676
2759
  await chmod4(config.traceFile, 384).catch(() => {
2677
2760
  });
2678
2761
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beecork",
3
- "version": "2.1.0",
3
+ "version": "2.2.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": {