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.
- package/README.md +1 -1
- package/dist/index.js +158 -75
- 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
|
|
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
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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: {
|
|
1153
|
-
|
|
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 =
|
|
1289
|
-
await
|
|
1290
|
-
const file =
|
|
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
|
|
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
|
|
1563
|
-
import { homedir as
|
|
1564
|
-
import { join as
|
|
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 =
|
|
1626
|
+
const home = homedir4();
|
|
1568
1627
|
const dirs = [];
|
|
1569
1628
|
let dir = process.cwd();
|
|
1570
|
-
while (dir !== home && dir !==
|
|
1629
|
+
while (dir !== home && dir !== dirname2(dir)) {
|
|
1571
1630
|
dirs.push(dir);
|
|
1572
|
-
dir =
|
|
1631
|
+
dir = dirname2(dir);
|
|
1573
1632
|
}
|
|
1574
1633
|
return dirs.reverse();
|
|
1575
1634
|
}
|
|
1576
1635
|
function corkPaths() {
|
|
1577
|
-
return [
|
|
1636
|
+
return [join3(homedir4(), BEECORK, "cork.md"), ...ancestorDirs().map((d) => join3(d, "cork.md"))];
|
|
1578
1637
|
}
|
|
1579
1638
|
function beecorkPaths(name) {
|
|
1580
|
-
return [
|
|
1639
|
+
return [join3(homedir4(), BEECORK, name), ...ancestorDirs().map((d) => join3(d, BEECORK, name))];
|
|
1581
1640
|
}
|
|
1582
1641
|
async function loadInstructions() {
|
|
1583
|
-
const home =
|
|
1584
|
-
const homeBeecork =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1701
|
+
await mkdir3(dirname2(file), { recursive: true });
|
|
1643
1702
|
const merged = { ...await loadUserConfig(), ...patch };
|
|
1644
|
-
await
|
|
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 = () =>
|
|
1709
|
+
var sessionsDir = () => join3(process.cwd(), BEECORK, "sessions");
|
|
1651
1710
|
async function saveSession(messages) {
|
|
1652
1711
|
try {
|
|
1653
1712
|
const dir = sessionsDir();
|
|
1654
|
-
await
|
|
1655
|
-
const file =
|
|
1713
|
+
await mkdir3(dir, { recursive: true });
|
|
1714
|
+
const file = join3(dir, `${Date.now()}.json`);
|
|
1656
1715
|
const tmp = `${file}.tmp`;
|
|
1657
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
1807
|
-
return { action: "ask", cacheable:
|
|
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
|
|
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
|
|
1958
|
-
import { join as
|
|
1959
|
-
import { homedir as
|
|
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
|
-
[
|
|
1971
|
-
[
|
|
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
|
|
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
|
|
2483
|
+
await mkdir4(dir, { recursive: true });
|
|
2414
2484
|
const file = `${dir}/${Date.now()}.json`;
|
|
2415
|
-
await
|
|
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 =
|
|
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
|
|
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
|
|
2758
|
+
await writeFile5(config.traceFile, JSON.stringify(trace), "utf8");
|
|
2676
2759
|
await chmod4(config.traceFile, 384).catch(() => {
|
|
2677
2760
|
});
|
|
2678
2761
|
}
|