beecork 2.3.0 → 2.4.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 +31 -3
- package/dist/index.js +1845 -1155
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -52,6 +52,20 @@ var num = (name, fallback) => {
|
|
|
52
52
|
return Number.isFinite(v) ? v : fallback;
|
|
53
53
|
};
|
|
54
54
|
var bool = (name) => ["1", "true", "yes", "on"].includes((process.env[name] ?? "").trim().toLowerCase());
|
|
55
|
+
var EFFORTS = ["off", "low", "medium", "high", "max"];
|
|
56
|
+
function normalizeEffort(raw) {
|
|
57
|
+
const v = (raw ?? "").trim().toLowerCase();
|
|
58
|
+
return EFFORTS.includes(v) ? v : void 0;
|
|
59
|
+
}
|
|
60
|
+
function parseExtra(raw) {
|
|
61
|
+
if (!raw || !raw.trim()) return {};
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
65
|
+
} catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
55
69
|
var config = {
|
|
56
70
|
apiUrl: "https://openrouter.ai/api/v1/chat/completions",
|
|
57
71
|
modelsUrl: "https://openrouter.ai/api/v1/models",
|
|
@@ -87,6 +101,19 @@ var config = {
|
|
|
87
101
|
// overall search traversal budget
|
|
88
102
|
searchMaxFileBytes: num("SEARCH_MAX_FILE_BYTES", 2e6),
|
|
89
103
|
// skip files larger than this
|
|
104
|
+
// Reasoning / thinking
|
|
105
|
+
reasoningEffort: normalizeEffort(process.env.REASONING_EFFORT) ?? "medium",
|
|
106
|
+
// startup default; changed live via /effort
|
|
107
|
+
openRouterExtra: parseExtra(process.env.OPENROUTER_EXTRA),
|
|
108
|
+
// advanced: extra request-body params (sampling, provider routing, …)
|
|
109
|
+
// Background tasks (run_bash background:true → check_task / stop_task)
|
|
110
|
+
maxBackgroundTasks: num("MAX_BG_TASKS", 5),
|
|
111
|
+
// per-session cap on concurrent background commands
|
|
112
|
+
backgroundTailChars: num("BG_TAIL_CHARS", 1e5),
|
|
113
|
+
// rolling tail buffer per task (drops oldest)
|
|
114
|
+
// Sub-agent (explore tool)
|
|
115
|
+
subAgentMaxSteps: num("SUBAGENT_MAX_STEPS", 15),
|
|
116
|
+
// child explorer's step budget (bounds cost/latency)
|
|
90
117
|
// Integrations / modes
|
|
91
118
|
verifyCommand: process.env.VERIFY_COMMAND ?? "",
|
|
92
119
|
// auto-run after edits (e.g. "npm run typecheck")
|
|
@@ -191,6 +218,8 @@ function modeLabel(m) {
|
|
|
191
218
|
var state = {
|
|
192
219
|
model: config.defaultModel,
|
|
193
220
|
// changed at runtime via the /model command
|
|
221
|
+
reasoningEffort: config.reasoningEffort,
|
|
222
|
+
// "thinking" depth; changed live via /effort, persisted like /model
|
|
194
223
|
apiKey: "",
|
|
195
224
|
// resolved at startup in index.ts: shell env → ~/.beecork/config.json → prompt
|
|
196
225
|
braveKey: "",
|
|
@@ -370,11 +399,18 @@ function summarizeResult(name, a, result) {
|
|
|
370
399
|
}
|
|
371
400
|
}
|
|
372
401
|
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
402
|
+
var steeringOnScreen = false;
|
|
403
|
+
function setSteeringActive(on) {
|
|
404
|
+
steeringOnScreen = on;
|
|
405
|
+
}
|
|
373
406
|
function startSpinner(label) {
|
|
374
407
|
if (!process.stdout.isTTY || !useColor) return () => {
|
|
375
408
|
};
|
|
376
409
|
let i = 0;
|
|
377
|
-
const draw = () =>
|
|
410
|
+
const draw = () => {
|
|
411
|
+
if (steeringOnScreen) return;
|
|
412
|
+
process.stdout.write("\r " + color.brand(SPINNER[i = (i + 1) % SPINNER.length]) + " " + color.dim(label));
|
|
413
|
+
};
|
|
378
414
|
process.stdout.write("\x1B[?25l");
|
|
379
415
|
draw();
|
|
380
416
|
const timer = setInterval(draw, 80);
|
|
@@ -469,752 +505,1394 @@ function printBanner(model, sources) {
|
|
|
469
505
|
// src/agent.ts
|
|
470
506
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
471
507
|
|
|
472
|
-
// src/
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
function renderListing(path, names) {
|
|
485
|
-
const safe = names.map(stripControl);
|
|
486
|
-
const out2 = [color.dim("\u256D\u2500 ") + color.bold(stripControl(path)) + color.dim(` ${safe.length} entr${safe.length === 1 ? "y" : "ies"}`)];
|
|
487
|
-
if (safe.length === 0) {
|
|
488
|
-
out2.push(color.dim("\u2502 ") + color.dim("(empty)"));
|
|
489
|
-
} else {
|
|
490
|
-
const avail = BOX_W() - 2;
|
|
491
|
-
const colW = Math.min(Math.max(...safe.map((n) => n.length)) + 2, 40);
|
|
492
|
-
const cols = Math.max(1, Math.floor(avail / colW));
|
|
493
|
-
const rows = Math.ceil(safe.length / cols);
|
|
494
|
-
for (let r = 0; r < rows; r++) {
|
|
495
|
-
let line = "";
|
|
496
|
-
for (let c = 0; c < cols; c++) {
|
|
497
|
-
const idx = c * rows + r;
|
|
498
|
-
if (idx >= safe.length) continue;
|
|
499
|
-
const n = safe[idx];
|
|
500
|
-
line += (n.endsWith("/") ? color.cyan(n) : n) + " ".repeat(Math.max(1, colW - n.length));
|
|
501
|
-
}
|
|
502
|
-
out2.push(color.dim("\u2502 ") + line.replace(/\s+$/, ""));
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
out2.push(color.dim("\u2570\u2500"));
|
|
506
|
-
return out2.join("\n") + "\n";
|
|
508
|
+
// src/input.ts
|
|
509
|
+
import { emitKeypressEvents } from "node:readline";
|
|
510
|
+
var out = (s) => process.stdout.write(s);
|
|
511
|
+
var active = null;
|
|
512
|
+
var started = false;
|
|
513
|
+
function initInput() {
|
|
514
|
+
if (started || !process.stdin.isTTY) return;
|
|
515
|
+
started = true;
|
|
516
|
+
process.stdin.setRawMode(true);
|
|
517
|
+
out("\x1B[?2004l");
|
|
518
|
+
emitKeypressEvents(process.stdin);
|
|
519
|
+
process.stdin.on("keypress", (str, key) => active?.(str, key));
|
|
507
520
|
}
|
|
508
|
-
function
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
out2.push(color.dim("\u2502 ") + color.dim(it.prefix) + (it.isDir ? color.cyan(nm) : nm));
|
|
521
|
+
function teardownInput() {
|
|
522
|
+
if (started && process.stdin.isTTY) {
|
|
523
|
+
out("\x1B[?2004h\x1B[?25h");
|
|
524
|
+
process.stdin.setRawMode(false);
|
|
525
|
+
process.stdin.pause();
|
|
514
526
|
}
|
|
515
|
-
if (truncated) out2.push(color.dim("\u2502 ") + color.dim("\u2026 (truncated)"));
|
|
516
|
-
out2.push(color.dim("\u2570\u2500"));
|
|
517
|
-
return out2.join("\n") + "\n";
|
|
518
527
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
528
|
+
function pushKeyHandler(h) {
|
|
529
|
+
const prev = active;
|
|
530
|
+
active = h;
|
|
531
|
+
return () => {
|
|
532
|
+
active = prev;
|
|
533
|
+
};
|
|
524
534
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
535
|
+
var isEnter = (k) => k?.name === "return" || k?.name === "enter";
|
|
536
|
+
var MENU_MAX = 8;
|
|
537
|
+
function readPrompt(opts) {
|
|
538
|
+
if (!process.stdin.isTTY) return Promise.resolve({ type: "eof" });
|
|
539
|
+
const history = opts.history ?? [];
|
|
540
|
+
const all = [
|
|
541
|
+
...opts.commands ?? [],
|
|
542
|
+
...(opts.skills ?? []).map((n) => ({ name: "/" + n, desc: "skill" }))
|
|
543
|
+
];
|
|
544
|
+
return new Promise((resolve2) => {
|
|
545
|
+
let buf = "";
|
|
546
|
+
let cur = 0;
|
|
547
|
+
let hist = history.length;
|
|
548
|
+
let sel = 0;
|
|
549
|
+
let menuHidden = false;
|
|
550
|
+
const BURST_IDLE_MS = 8;
|
|
551
|
+
let burstLen = 0;
|
|
552
|
+
let burstTimer = null;
|
|
553
|
+
let pendingRender = false;
|
|
554
|
+
const matches = () => {
|
|
555
|
+
if (opts.mask) return [];
|
|
556
|
+
const m = buf.match(/^\/(\S*)$/);
|
|
557
|
+
if (!m) return [];
|
|
558
|
+
const pre = "/" + m[1];
|
|
559
|
+
return all.filter((c) => c.name.startsWith(pre)).slice(0, MENU_MAX);
|
|
539
560
|
};
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
561
|
+
const menu = () => menuHidden ? [] : matches();
|
|
562
|
+
const highlight = () => {
|
|
563
|
+
if (opts.mask) return "*".repeat(buf.length);
|
|
564
|
+
const m = buf.match(/^(\/\S*)([\s\S]*)$/);
|
|
565
|
+
if (!m) return buf;
|
|
566
|
+
const [, token, rest] = m;
|
|
567
|
+
const known = all.some((c) => c.name === token);
|
|
568
|
+
const partial = all.some((c) => c.name.startsWith(token));
|
|
569
|
+
const styled = known ? color.green(token) : partial ? color.cyan(token) : color.red(token);
|
|
570
|
+
return styled + rest;
|
|
545
571
|
};
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
var visLen = (s) => stripAnsi(s).length;
|
|
557
|
-
var HR_RE = /^\s*([-*_])(\s*\1){2,}\s*$/;
|
|
558
|
-
var CODE_SPAN_RE = new RegExp(`${NUL}(\\d+)${NUL}`, "g");
|
|
559
|
-
function inline(s) {
|
|
560
|
-
const code = [];
|
|
561
|
-
s = s.replace(/`([^`]+)`/g, (_m, c) => {
|
|
562
|
-
code.push(c);
|
|
563
|
-
return `${NUL}${code.length - 1}${NUL}`;
|
|
564
|
-
});
|
|
565
|
-
s = s.replace(/\*\*([^*]+)\*\*/g, (_m, t) => color.bold(t));
|
|
566
|
-
s = s.replace(/__([^_]+)__/g, (_m, t) => color.bold(t));
|
|
567
|
-
s = s.replace(/(^|[^\\*])\*([^*\s][^*]*?)\*/g, (_m, p, t) => p + color.italic(t));
|
|
568
|
-
s = s.replace(/~~([^~]+)~~/g, (_m, t) => color.strike(t));
|
|
569
|
-
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, txt, url) => color.cyan(txt) + color.dim(` (${url})`));
|
|
570
|
-
s = s.replace(CODE_SPAN_RE, (_m, i) => color.cyan(code[+i]));
|
|
571
|
-
return s;
|
|
572
|
-
}
|
|
573
|
-
function blockLine(line) {
|
|
574
|
-
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
575
|
-
if (h) return color.bold(color.cyan(inline(h[2])));
|
|
576
|
-
if (HR_RE.test(line)) return color.dim("\u2500".repeat(40));
|
|
577
|
-
const q = line.match(/^\s*>\s?(.*)$/);
|
|
578
|
-
if (q) return color.dim("\u2502 " + inline(q[1]));
|
|
579
|
-
const b = line.match(/^(\s*)[-*+]\s+(.*)$/);
|
|
580
|
-
if (b) return b[1] + color.cyan("\u2022") + " " + inline(b[2]);
|
|
581
|
-
const n = line.match(/^(\s*)(\d+)\.\s+(.*)$/);
|
|
582
|
-
if (n) return n[1] + color.cyan(n[2] + ".") + " " + inline(n[3]);
|
|
583
|
-
return inline(line);
|
|
584
|
-
}
|
|
585
|
-
function renderTable(rows) {
|
|
586
|
-
const parse = (r) => r.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
587
|
-
const grid = rows.map(parse);
|
|
588
|
-
const isSep = (cells) => cells.length > 0 && cells.every((c) => /^:?-+:?$/.test(c));
|
|
589
|
-
const sepIdx = grid.findIndex(isSep);
|
|
590
|
-
const body = grid.filter((_, i) => i !== sepIdx);
|
|
591
|
-
if (body.length === 0) return "";
|
|
592
|
-
const ncol = Math.max(...body.map((r) => r.length));
|
|
593
|
-
const styled = body.map(
|
|
594
|
-
(r, ri) => Array.from({ length: ncol }, (_, c) => {
|
|
595
|
-
const raw = r[c] ?? "";
|
|
596
|
-
return sepIdx >= 0 && ri === 0 ? color.bold(inline(raw)) : inline(raw);
|
|
597
|
-
})
|
|
598
|
-
);
|
|
599
|
-
const width = Array.from({ length: ncol }, (_, c) => Math.max(...styled.map((r) => visLen(r[c]))));
|
|
600
|
-
const out2 = styled.map((r) => {
|
|
601
|
-
const cells = r.map((cell, c) => cell + " ".repeat(Math.max(0, width[c] - visLen(cell))));
|
|
602
|
-
return " " + cells.join(color.dim(" \u2502 "));
|
|
603
|
-
});
|
|
604
|
-
return out2.join("\n") + "\n";
|
|
605
|
-
}
|
|
606
|
-
function createMarkdownStream(write) {
|
|
607
|
-
let buf = "";
|
|
608
|
-
let scanFrom = 0;
|
|
609
|
-
let inCode = false;
|
|
610
|
-
let first = true;
|
|
611
|
-
let table = [];
|
|
612
|
-
const isRow = (l) => /^\s*\|.*\|\s*$/.test(l);
|
|
613
|
-
const flushTable = () => {
|
|
614
|
-
if (table.length) {
|
|
615
|
-
write(renderTable(table));
|
|
616
|
-
table = [];
|
|
617
|
-
}
|
|
618
|
-
};
|
|
619
|
-
function emit(line) {
|
|
620
|
-
const fence = line.match(/^\s*```(\w*)\s*$/);
|
|
621
|
-
if (first && line.trim()) {
|
|
622
|
-
first = false;
|
|
623
|
-
const block = !!fence || isRow(line) || HR_RE.test(line) || /^#{1,6}\s/.test(line);
|
|
624
|
-
if (block) write("\n");
|
|
572
|
+
let lastCurRow = 0;
|
|
573
|
+
const rowColOf = (s) => ({ row: (s.match(/\n/g) || []).length, col: s.length - (s.lastIndexOf("\n") + 1) });
|
|
574
|
+
function drawBlock() {
|
|
575
|
+
const prompt = opts.promptString();
|
|
576
|
+
const promptW = stripAnsi(prompt).length;
|
|
577
|
+
const indent = " ".repeat(promptW);
|
|
578
|
+
const lines = highlight().split("\n");
|
|
579
|
+
out(prompt + lines[0]);
|
|
580
|
+
for (let i = 1; i < lines.length; i++) out("\n" + indent + lines[i]);
|
|
581
|
+
return { promptW };
|
|
625
582
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
583
|
+
function render() {
|
|
584
|
+
const mm = menu();
|
|
585
|
+
if (sel >= mm.length) sel = Math.max(0, mm.length - 1);
|
|
586
|
+
out("\x1B[?25l");
|
|
587
|
+
if (lastCurRow > 0) out(`\x1B[${lastCurRow}A`);
|
|
588
|
+
out("\r\x1B[J");
|
|
589
|
+
const { promptW } = drawBlock();
|
|
590
|
+
const cols = Math.max(1, process.stdout.columns || 80);
|
|
591
|
+
for (let i = 0; i < mm.length; i++) {
|
|
592
|
+
const m = mm[i];
|
|
593
|
+
const name = m.name.padEnd(10);
|
|
594
|
+
const maxDesc = Math.max(0, cols - name.length - 4);
|
|
595
|
+
const desc = m.desc.length > maxDesc ? m.desc.slice(0, Math.max(0, maxDesc - 1)) + "\u2026" : m.desc;
|
|
596
|
+
out("\n" + (i === sel ? color.green("\u203A " + name) + " " + color.dim(desc) : color.dim(" " + name + " " + desc)));
|
|
597
|
+
}
|
|
598
|
+
const text = opts.mask ? "*".repeat(buf.length) : buf;
|
|
599
|
+
const physRows = text.split("\n").map((l) => Math.max(1, Math.ceil((promptW + displayWidth(l)) / cols)));
|
|
600
|
+
const totalInputPhys = physRows.reduce((a, b) => a + b, 0);
|
|
601
|
+
const before = opts.mask ? text : buf.slice(0, cur);
|
|
602
|
+
const curLogicalRow = (before.match(/\n/g) || []).length;
|
|
603
|
+
const curCol = promptW + displayWidth(before.slice(before.lastIndexOf("\n") + 1));
|
|
604
|
+
const physBefore = physRows.slice(0, curLogicalRow).reduce((a, b) => a + b, 0);
|
|
605
|
+
const curPhysRow = physBefore + Math.floor(curCol / cols);
|
|
606
|
+
const curPhysCol = curCol % cols;
|
|
607
|
+
const lastDrawnRow = totalInputPhys - 1 + mm.length;
|
|
608
|
+
if (lastDrawnRow > curPhysRow) out(`\x1B[${lastDrawnRow - curPhysRow}A`);
|
|
609
|
+
out("\r" + (curPhysCol > 0 ? `\x1B[${curPhysCol}C` : "") + "\x1B[?25h");
|
|
610
|
+
lastCurRow = curPhysRow;
|
|
631
611
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
612
|
+
function finish(result) {
|
|
613
|
+
restore();
|
|
614
|
+
if (burstTimer) {
|
|
615
|
+
clearTimeout(burstTimer);
|
|
616
|
+
burstTimer = null;
|
|
617
|
+
}
|
|
618
|
+
pendingRender = false;
|
|
619
|
+
out("\x1B[?25l");
|
|
620
|
+
if (lastCurRow > 0) out(`\x1B[${lastCurRow}A`);
|
|
621
|
+
out("\r\x1B[J");
|
|
622
|
+
drawBlock();
|
|
623
|
+
out("\n");
|
|
624
|
+
if (result.type === "line" && result.value.trim() && !opts.mask) history.push(result.value);
|
|
625
|
+
resolve2(result);
|
|
636
626
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
627
|
+
function insert(s) {
|
|
628
|
+
buf = buf.slice(0, cur) + s + buf.slice(cur);
|
|
629
|
+
cur += s.length;
|
|
630
|
+
menuHidden = false;
|
|
631
|
+
sel = 0;
|
|
632
|
+
pendingRender = true;
|
|
640
633
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
634
|
+
function onKey(str, key) {
|
|
635
|
+
const mm = menu();
|
|
636
|
+
burstLen++;
|
|
637
|
+
if (burstTimer) clearTimeout(burstTimer);
|
|
638
|
+
burstTimer = setTimeout(() => {
|
|
639
|
+
burstTimer = null;
|
|
640
|
+
burstLen = 0;
|
|
641
|
+
if (pendingRender) {
|
|
642
|
+
pendingRender = false;
|
|
643
|
+
render();
|
|
644
|
+
}
|
|
645
|
+
}, BURST_IDLE_MS);
|
|
646
|
+
if (isEnter(key)) {
|
|
647
|
+
if (key?.shift || key?.meta || burstLen > 1) {
|
|
648
|
+
insert("\n");
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
return finish({ type: "line", value: buf });
|
|
652
652
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
653
|
+
if (key?.ctrl && key.name === "c") {
|
|
654
|
+
if (buf) {
|
|
655
|
+
buf = "";
|
|
656
|
+
cur = 0;
|
|
657
|
+
render();
|
|
658
|
+
} else finish({ type: "quit" });
|
|
659
|
+
return;
|
|
659
660
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
661
|
+
if (key?.ctrl && key.name === "d") {
|
|
662
|
+
if (!buf) finish({ type: "eof" });
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (key?.name === "tab" && key.shift) {
|
|
666
|
+
opts.onShiftTab?.();
|
|
667
|
+
render();
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (key?.name === "tab") {
|
|
671
|
+
if (mm.length) {
|
|
672
|
+
buf = mm[sel].name + " ";
|
|
673
|
+
cur = buf.length;
|
|
674
|
+
menuHidden = false;
|
|
675
|
+
render();
|
|
676
|
+
}
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (key?.name === "up") {
|
|
680
|
+
if (mm.length) {
|
|
681
|
+
sel = (sel - 1 + mm.length) % mm.length;
|
|
682
|
+
render();
|
|
683
|
+
} else if (buf.includes("\n")) moveVert(-1);
|
|
684
|
+
else histPrev();
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (key?.name === "down") {
|
|
688
|
+
if (mm.length) {
|
|
689
|
+
sel = (sel + 1) % mm.length;
|
|
690
|
+
render();
|
|
691
|
+
} else if (buf.includes("\n")) moveVert(1);
|
|
692
|
+
else histNext();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (key?.name === "left") {
|
|
696
|
+
if (cur > 0) cur--;
|
|
697
|
+
render();
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (key?.name === "right") {
|
|
701
|
+
if (cur < buf.length) cur++;
|
|
702
|
+
render();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (key?.name === "home" || key?.ctrl && key.name === "a") {
|
|
706
|
+
cur = 0;
|
|
707
|
+
render();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (key?.name === "end" || key?.ctrl && key.name === "e") {
|
|
711
|
+
cur = buf.length;
|
|
712
|
+
render();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (key?.ctrl && key.name === "u") {
|
|
716
|
+
buf = buf.slice(cur);
|
|
717
|
+
cur = 0;
|
|
718
|
+
menuHidden = false;
|
|
719
|
+
render();
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (key?.name === "backspace") {
|
|
723
|
+
if (cur > 0) {
|
|
724
|
+
buf = buf.slice(0, cur - 1) + buf.slice(cur);
|
|
725
|
+
cur--;
|
|
726
|
+
menuHidden = false;
|
|
727
|
+
render();
|
|
728
|
+
}
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (key?.name === "delete") {
|
|
732
|
+
if (cur < buf.length) {
|
|
733
|
+
buf = buf.slice(0, cur) + buf.slice(cur + 1);
|
|
734
|
+
menuHidden = false;
|
|
735
|
+
render();
|
|
736
|
+
}
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (key?.name === "escape") {
|
|
740
|
+
if (mm.length) {
|
|
741
|
+
menuHidden = true;
|
|
742
|
+
render();
|
|
743
|
+
}
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (str && !key?.ctrl && !key?.meta && [...str].length === 1) {
|
|
747
|
+
if (isPrintableCodePoint(str.codePointAt(0))) insert(str);
|
|
664
748
|
}
|
|
665
749
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
function
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
750
|
+
function moveVert(dir) {
|
|
751
|
+
const lines = buf.split("\n");
|
|
752
|
+
const { row, col } = rowColOf(buf.slice(0, cur));
|
|
753
|
+
const target = row + dir;
|
|
754
|
+
if (target < 0 || target >= lines.length) return;
|
|
755
|
+
let idx = 0;
|
|
756
|
+
for (let i = 0; i < target; i++) idx += lines[i].length + 1;
|
|
757
|
+
cur = idx + Math.min(col, lines[target].length);
|
|
758
|
+
render();
|
|
759
|
+
}
|
|
760
|
+
function histPrev() {
|
|
761
|
+
if (history.length === 0) return;
|
|
762
|
+
hist = Math.max(0, hist - 1);
|
|
763
|
+
buf = history[hist] ?? "";
|
|
764
|
+
cur = buf.length;
|
|
765
|
+
render();
|
|
766
|
+
}
|
|
767
|
+
function histNext() {
|
|
768
|
+
if (hist >= history.length) return;
|
|
769
|
+
hist += 1;
|
|
770
|
+
buf = hist === history.length ? "" : history[hist];
|
|
771
|
+
cur = buf.length;
|
|
772
|
+
render();
|
|
773
|
+
}
|
|
774
|
+
const restore = pushKeyHandler(onKey);
|
|
775
|
+
render();
|
|
776
|
+
});
|
|
691
777
|
}
|
|
692
|
-
function
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
778
|
+
function readChoice(prompt) {
|
|
779
|
+
if (!process.stdin.isTTY) return Promise.resolve("n");
|
|
780
|
+
return new Promise((resolve2) => {
|
|
781
|
+
out(prompt);
|
|
782
|
+
const restore = pushKeyHandler((str, key) => {
|
|
783
|
+
let ch;
|
|
784
|
+
if (key?.ctrl && key.name === "c" || key?.name === "escape" || isEnter(key)) ch = "n";
|
|
785
|
+
else if (str && /^[yna]$/i.test(str)) ch = str.toLowerCase();
|
|
786
|
+
else return;
|
|
787
|
+
restore();
|
|
788
|
+
out(ch + "\n");
|
|
789
|
+
resolve2(ch);
|
|
790
|
+
});
|
|
791
|
+
});
|
|
697
792
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
793
|
+
function selectMenu(opts) {
|
|
794
|
+
if (!process.stdin.isTTY || opts.items.length === 0) return Promise.resolve(null);
|
|
795
|
+
return new Promise((resolve2) => {
|
|
796
|
+
let sel = Math.max(0, Math.min(opts.initial ?? 0, opts.items.length - 1));
|
|
797
|
+
let drawn = 0;
|
|
798
|
+
function render() {
|
|
799
|
+
out("\x1B[?25l");
|
|
800
|
+
if (drawn > 0) out(`\x1B[${drawn}A`);
|
|
801
|
+
out("\r\x1B[J");
|
|
802
|
+
out(color.dim(opts.title) + "\n");
|
|
803
|
+
opts.items.forEach((it, i) => {
|
|
804
|
+
const row = i === sel ? color.green("\u203A " + it.label) : " " + it.label;
|
|
805
|
+
out(row + (it.hint ? color.dim(" " + it.hint) : "") + "\n");
|
|
806
|
+
});
|
|
807
|
+
drawn = opts.items.length + 1;
|
|
808
|
+
}
|
|
809
|
+
function finish(v) {
|
|
810
|
+
restore();
|
|
811
|
+
if (drawn > 0) out(`\x1B[${drawn}A\r\x1B[J`);
|
|
812
|
+
out("\x1B[?25h");
|
|
813
|
+
resolve2(v);
|
|
814
|
+
}
|
|
815
|
+
const restore = pushKeyHandler((_str, key) => {
|
|
816
|
+
if (key?.name === "up") {
|
|
817
|
+
sel = (sel - 1 + opts.items.length) % opts.items.length;
|
|
818
|
+
render();
|
|
819
|
+
} else if (key?.name === "down") {
|
|
820
|
+
sel = (sel + 1) % opts.items.length;
|
|
821
|
+
render();
|
|
822
|
+
} else if (isEnter(key)) finish(opts.items[sel].value);
|
|
823
|
+
else if (key?.name === "escape" || key?.name === "q" || key?.ctrl && key.name === "c") finish(null);
|
|
824
|
+
});
|
|
825
|
+
render();
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// src/show.ts
|
|
830
|
+
var BOX_W = () => Math.min(process.stdout.columns || 80, 100);
|
|
831
|
+
function renderFileBox(path, startLine, lines, hasMore) {
|
|
832
|
+
const p = stripControl(path);
|
|
833
|
+
const numW = String(startLine + Math.max(0, lines.length - 1)).length;
|
|
834
|
+
const header = startLine === 1 && !hasMore ? `${lines.length} line${lines.length === 1 ? "" : "s"}` : `lines ${startLine}\u2013${startLine + lines.length - 1}${hasMore ? " (+ more)" : ""}`;
|
|
835
|
+
const out2 = [color.dim("\u256D\u2500 ") + color.bold(p) + color.dim(` ${header}`)];
|
|
836
|
+
lines.forEach((l, i) => out2.push(color.dim("\u2502 ") + color.dim(String(startLine + i).padStart(numW)) + " " + stripControl(l)));
|
|
837
|
+
if (hasMore) out2.push(color.dim("\u2502 ") + color.dim(`\u2026 more (show ${p} offset ${startLine + lines.length})`));
|
|
838
|
+
out2.push(color.dim("\u2570\u2500"));
|
|
839
|
+
return out2.join("\n") + "\n";
|
|
840
|
+
}
|
|
841
|
+
function renderListing(path, names) {
|
|
842
|
+
const safe = names.map(stripControl);
|
|
843
|
+
const out2 = [color.dim("\u256D\u2500 ") + color.bold(stripControl(path)) + color.dim(` ${safe.length} entr${safe.length === 1 ? "y" : "ies"}`)];
|
|
844
|
+
if (safe.length === 0) {
|
|
845
|
+
out2.push(color.dim("\u2502 ") + color.dim("(empty)"));
|
|
846
|
+
} else {
|
|
847
|
+
const avail = BOX_W() - 2;
|
|
848
|
+
const colW = Math.min(Math.max(...safe.map((n) => n.length)) + 2, 40);
|
|
849
|
+
const cols = Math.max(1, Math.floor(avail / colW));
|
|
850
|
+
const rows = Math.ceil(safe.length / cols);
|
|
851
|
+
for (let r = 0; r < rows; r++) {
|
|
852
|
+
let line = "";
|
|
853
|
+
for (let c = 0; c < cols; c++) {
|
|
854
|
+
const idx = c * rows + r;
|
|
855
|
+
if (idx >= safe.length) continue;
|
|
856
|
+
const n = safe[idx];
|
|
857
|
+
line += (n.endsWith("/") ? color.cyan(n) : n) + " ".repeat(Math.max(1, colW - n.length));
|
|
858
|
+
}
|
|
859
|
+
out2.push(color.dim("\u2502 ") + line.replace(/\s+$/, ""));
|
|
860
|
+
}
|
|
743
861
|
}
|
|
744
|
-
|
|
862
|
+
out2.push(color.dim("\u2570\u2500"));
|
|
863
|
+
return out2.join("\n") + "\n";
|
|
745
864
|
}
|
|
746
|
-
function
|
|
747
|
-
const
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
865
|
+
function renderTree(path, items, truncated) {
|
|
866
|
+
const dirs = items.filter((it) => it.isDir).length;
|
|
867
|
+
const out2 = [color.dim("\u256D\u2500 ") + color.bold(stripControl(path)) + color.dim(` ${items.length} entries \xB7 ${dirs} folders`)];
|
|
868
|
+
for (const it of items) {
|
|
869
|
+
const nm = stripControl(it.name);
|
|
870
|
+
out2.push(color.dim("\u2502 ") + color.dim(it.prefix) + (it.isDir ? color.cyan(nm) : nm));
|
|
871
|
+
}
|
|
872
|
+
if (truncated) out2.push(color.dim("\u2502 ") + color.dim("\u2026 (truncated)"));
|
|
873
|
+
out2.push(color.dim("\u2570\u2500"));
|
|
874
|
+
return out2.join("\n") + "\n";
|
|
752
875
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
876
|
+
var SHOW_MARK = "";
|
|
877
|
+
var SHOW_KINDS = ["file", "dir", "tree"];
|
|
878
|
+
var SHOW_RE = new RegExp(`^${SHOW_MARK}(${SHOW_KINDS.join("|")})${SHOW_MARK}`);
|
|
879
|
+
function showPayload(kind, obj) {
|
|
880
|
+
return SHOW_MARK + kind + SHOW_MARK + JSON.stringify(obj);
|
|
881
|
+
}
|
|
882
|
+
function renderShow(raw) {
|
|
883
|
+
const m = raw.match(SHOW_RE);
|
|
884
|
+
if (!m) return null;
|
|
885
|
+
let p;
|
|
886
|
+
try {
|
|
887
|
+
p = JSON.parse(raw.slice(m[0].length));
|
|
888
|
+
} catch {
|
|
889
|
+
return null;
|
|
758
890
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
if (g.slice(0, 7).every((h) => h === 0) && g[7] === 1) return true;
|
|
766
|
-
const embeddedV4 = () => `${g[6] >> 8 & 255}.${g[6] & 255}.${g[7] >> 8 & 255}.${g[7] & 255}`;
|
|
767
|
-
if (g[0] === 0 && g[1] === 0 && g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 65535) {
|
|
768
|
-
return isPrivateAddr(embeddedV4());
|
|
891
|
+
if (m[1] === "file") {
|
|
892
|
+
const range = `lines ${p.startLine}\u2013${p.startLine + p.lines.length - 1}${p.hasMore ? ", more follow" : ""}`;
|
|
893
|
+
return {
|
|
894
|
+
display: renderFileBox(p.path, p.startLine, p.lines, p.hasMore),
|
|
895
|
+
note: `Shown ${p.path} (${range}) on the user's screen \u2014 they can see it now. Do NOT paste, re-list, or re-describe its contents; add at most a one-line comment, or ask what's next. (If YOU need the contents to answer something, use read_file.)`
|
|
896
|
+
};
|
|
769
897
|
}
|
|
770
|
-
if (
|
|
771
|
-
return
|
|
898
|
+
if (m[1] === "dir") {
|
|
899
|
+
return {
|
|
900
|
+
display: renderListing(p.path, p.names),
|
|
901
|
+
note: `Shown the contents of ${p.path} (${p.names.length} entries) on the user's screen. Do NOT re-list or re-format them \u2014 a one-line comment at most, or ask what's next.`
|
|
902
|
+
};
|
|
772
903
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
904
|
+
const dirs = p.items.filter((it) => it.isDir).length;
|
|
905
|
+
return {
|
|
906
|
+
display: renderTree(p.path, p.items, p.truncated),
|
|
907
|
+
note: `Shown the file tree of ${p.path} (${p.items.length} entries, ${dirs} folders${p.truncated ? ", truncated" : ""}) on the user's screen. Do NOT re-type or re-format the tree \u2014 a one-line comment at most.`
|
|
908
|
+
};
|
|
776
909
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
if (
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
910
|
+
|
|
911
|
+
// src/capabilities.ts
|
|
912
|
+
var capable = null;
|
|
913
|
+
var started2 = false;
|
|
914
|
+
function loadCatalog() {
|
|
915
|
+
if (started2) return;
|
|
916
|
+
started2 = true;
|
|
917
|
+
fetch(config.modelsUrl, { signal: AbortSignal.timeout(config.webTimeoutMs) }).then((res) => res.json()).then((json) => {
|
|
918
|
+
const data = json.data;
|
|
919
|
+
if (!Array.isArray(data)) return;
|
|
920
|
+
const ids = /* @__PURE__ */ new Set();
|
|
921
|
+
for (const m of data) {
|
|
922
|
+
const id = m.id;
|
|
923
|
+
const params = m.supported_parameters;
|
|
924
|
+
if (typeof id === "string" && Array.isArray(params) && params.includes("reasoning")) ids.add(id);
|
|
925
|
+
}
|
|
926
|
+
if (ids.size) capable = ids;
|
|
927
|
+
}).catch(() => {
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
var baseId = (slug) => slug.split(":")[0];
|
|
931
|
+
function shouldSendReasoning(model) {
|
|
932
|
+
loadCatalog();
|
|
933
|
+
if (!capable) return true;
|
|
934
|
+
return capable.has(model) || capable.has(baseId(model));
|
|
788
935
|
}
|
|
789
936
|
|
|
790
|
-
// src/
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
s = s.replace(
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
937
|
+
// src/markdown.ts
|
|
938
|
+
var NUL = "\0";
|
|
939
|
+
var visLen = (s) => stripAnsi(s).length;
|
|
940
|
+
var HR_RE = /^\s*([-*_])(\s*\1){2,}\s*$/;
|
|
941
|
+
var CODE_SPAN_RE = new RegExp(`${NUL}(\\d+)${NUL}`, "g");
|
|
942
|
+
function inline(s) {
|
|
943
|
+
const code = [];
|
|
944
|
+
s = s.replace(/`([^`]+)`/g, (_m, c) => {
|
|
945
|
+
code.push(c);
|
|
946
|
+
return `${NUL}${code.length - 1}${NUL}`;
|
|
947
|
+
});
|
|
948
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, (_m, t) => color.bold(t));
|
|
949
|
+
s = s.replace(/__([^_]+)__/g, (_m, t) => color.bold(t));
|
|
950
|
+
s = s.replace(/(^|[^\\*])\*([^*\s][^*]*?)\*/g, (_m, p, t) => p + color.italic(t));
|
|
951
|
+
s = s.replace(/~~([^~]+)~~/g, (_m, t) => color.strike(t));
|
|
952
|
+
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, txt, url) => color.cyan(txt) + color.dim(` (${url})`));
|
|
953
|
+
s = s.replace(CODE_SPAN_RE, (_m, i) => color.cyan(code[+i]));
|
|
954
|
+
return s;
|
|
801
955
|
}
|
|
802
|
-
function
|
|
803
|
-
const
|
|
804
|
-
return
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
956
|
+
function blockLine(line) {
|
|
957
|
+
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
958
|
+
if (h) return color.bold(color.cyan(inline(h[2])));
|
|
959
|
+
if (HR_RE.test(line)) return color.dim("\u2500".repeat(40));
|
|
960
|
+
const q = line.match(/^\s*>\s?(.*)$/);
|
|
961
|
+
if (q) return color.dim("\u2502 " + inline(q[1]));
|
|
962
|
+
const b = line.match(/^(\s*)[-*+]\s+(.*)$/);
|
|
963
|
+
if (b) return b[1] + color.cyan("\u2022") + " " + inline(b[2]);
|
|
964
|
+
const n = line.match(/^(\s*)(\d+)\.\s+(.*)$/);
|
|
965
|
+
if (n) return n[1] + color.cyan(n[2] + ".") + " " + inline(n[3]);
|
|
966
|
+
return inline(line);
|
|
967
|
+
}
|
|
968
|
+
function renderTable(rows) {
|
|
969
|
+
const parse = (r) => r.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
970
|
+
const grid = rows.map(parse);
|
|
971
|
+
const isSep = (cells) => cells.length > 0 && cells.every((c) => /^:?-+:?$/.test(c));
|
|
972
|
+
const sepIdx = grid.findIndex(isSep);
|
|
973
|
+
const body = grid.filter((_, i) => i !== sepIdx);
|
|
974
|
+
if (body.length === 0) return "";
|
|
975
|
+
const ncol = Math.max(...body.map((r) => r.length));
|
|
976
|
+
const styled = body.map(
|
|
977
|
+
(r, ri) => Array.from({ length: ncol }, (_, c) => {
|
|
978
|
+
const raw = r[c] ?? "";
|
|
979
|
+
return sepIdx >= 0 && ri === 0 ? color.bold(inline(raw)) : inline(raw);
|
|
980
|
+
})
|
|
981
|
+
);
|
|
982
|
+
const width = Array.from({ length: ncol }, (_, c) => Math.max(...styled.map((r) => visLen(r[c]))));
|
|
983
|
+
const out2 = styled.map((r) => {
|
|
984
|
+
const cells = r.map((cell, c) => cell + " ".repeat(Math.max(0, width[c] - visLen(cell))));
|
|
985
|
+
return " " + cells.join(color.dim(" \u2502 "));
|
|
810
986
|
});
|
|
987
|
+
return out2.join("\n") + "\n";
|
|
811
988
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
else child.kill("SIGKILL");
|
|
824
|
-
} catch {
|
|
825
|
-
try {
|
|
826
|
-
child.kill("SIGKILL");
|
|
827
|
-
} catch {
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
};
|
|
831
|
-
const timer = setTimeout(() => {
|
|
832
|
-
timedOut = true;
|
|
833
|
-
kill();
|
|
834
|
-
}, opts.timeout);
|
|
835
|
-
const onAbort = () => {
|
|
836
|
-
aborted = true;
|
|
837
|
-
kill();
|
|
838
|
-
};
|
|
839
|
-
if (opts.signal) {
|
|
840
|
-
if (opts.signal.aborted) onAbort();
|
|
841
|
-
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
989
|
+
function createMarkdownStream(write) {
|
|
990
|
+
let buf = "";
|
|
991
|
+
let scanFrom = 0;
|
|
992
|
+
let inCode = false;
|
|
993
|
+
let first = true;
|
|
994
|
+
let table = [];
|
|
995
|
+
const isRow = (l) => /^\s*\|.*\|\s*$/.test(l);
|
|
996
|
+
const flushTable = () => {
|
|
997
|
+
if (table.length) {
|
|
998
|
+
write(renderTable(table));
|
|
999
|
+
table = [];
|
|
842
1000
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1001
|
+
};
|
|
1002
|
+
function emit(line) {
|
|
1003
|
+
const fence = line.match(/^\s*```(\w*)\s*$/);
|
|
1004
|
+
if (first && line.trim()) {
|
|
1005
|
+
first = false;
|
|
1006
|
+
const block = !!fence || isRow(line) || HR_RE.test(line) || /^#{1,6}\s/.test(line);
|
|
1007
|
+
if (block) write("\n");
|
|
1008
|
+
}
|
|
1009
|
+
if (fence) {
|
|
1010
|
+
flushTable();
|
|
1011
|
+
inCode = !inCode;
|
|
1012
|
+
write(color.dim("\u2504".repeat(40)) + "\n");
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (inCode) {
|
|
1016
|
+
flushTable();
|
|
1017
|
+
write(color.dim(" " + line) + "\n");
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (isRow(line)) {
|
|
1021
|
+
table.push(line);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
flushTable();
|
|
1025
|
+
write(blockLine(line) + "\n");
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
push(text) {
|
|
1029
|
+
buf += text;
|
|
1030
|
+
let i;
|
|
1031
|
+
while ((i = buf.indexOf("\n", scanFrom)) >= 0) {
|
|
1032
|
+
emit(buf.slice(0, i));
|
|
1033
|
+
buf = buf.slice(i + 1);
|
|
1034
|
+
scanFrom = 0;
|
|
847
1035
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1036
|
+
scanFrom = buf.length;
|
|
1037
|
+
},
|
|
1038
|
+
end() {
|
|
1039
|
+
if (buf) {
|
|
1040
|
+
emit(buf);
|
|
1041
|
+
buf = "";
|
|
853
1042
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
settled = true;
|
|
862
|
-
cleanup();
|
|
863
|
-
if (aborted) reject(Object.assign(new Error("cancelled"), { stdout, stderr }));
|
|
864
|
-
else if (timedOut) reject(Object.assign(new Error(`timed out after ${opts.timeout}ms`), { stdout, stderr }));
|
|
865
|
-
else if (exitCode !== 0 && exitCode !== null) reject(Object.assign(new Error(`exited with code ${exitCode}`), { stdout, stderr }));
|
|
866
|
-
else resolve2({ stdout, stderr });
|
|
867
|
-
};
|
|
868
|
-
child.on("error", (err) => {
|
|
869
|
-
if (settled) return;
|
|
870
|
-
settled = true;
|
|
871
|
-
cleanup();
|
|
872
|
-
reject(Object.assign(err, { stdout, stderr }));
|
|
873
|
-
});
|
|
874
|
-
child.on("close", finalize);
|
|
875
|
-
child.on("exit", (code) => {
|
|
876
|
-
exitCode = code;
|
|
877
|
-
setTimeout(finalize, 100);
|
|
878
|
-
});
|
|
879
|
-
});
|
|
1043
|
+
flushTable();
|
|
1044
|
+
if (inCode) {
|
|
1045
|
+
write(color.dim("\u2504".repeat(40)) + "\n");
|
|
1046
|
+
inCode = false;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
880
1050
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1051
|
+
|
|
1052
|
+
// src/tools.ts
|
|
1053
|
+
import { readFile as readFile2, writeFile as writeFile2, appendFile, readdir, mkdir as mkdir2, stat, rename, chmod } from "node:fs/promises";
|
|
1054
|
+
import { createReadStream } from "node:fs";
|
|
1055
|
+
import { createInterface as createLineReader } from "node:readline";
|
|
1056
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
1057
|
+
import { join as join2 } from "node:path";
|
|
1058
|
+
import { lookup as dnsLookup } from "node:dns";
|
|
1059
|
+
import { request as httpRequest } from "node:http";
|
|
1060
|
+
import { request as httpsRequest } from "node:https";
|
|
1061
|
+
import { isIP } from "node:net";
|
|
1062
|
+
|
|
1063
|
+
// src/tasks.ts
|
|
1064
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1065
|
+
var tasks = /* @__PURE__ */ new Map();
|
|
1066
|
+
var counter = 0;
|
|
1067
|
+
var unix = process.platform !== "win32";
|
|
1068
|
+
function appendTail(buffer, chunk, cap) {
|
|
1069
|
+
const combined = buffer + chunk;
|
|
1070
|
+
if (combined.length <= cap) return { buffer: combined, dropped: false };
|
|
1071
|
+
return { buffer: combined.slice(combined.length - cap), dropped: true };
|
|
1072
|
+
}
|
|
1073
|
+
function readSince(buffer, totalLen, readLen) {
|
|
1074
|
+
const fresh = totalLen - readLen;
|
|
1075
|
+
if (fresh <= 0) return { output: "", dropped: false };
|
|
1076
|
+
if (fresh >= buffer.length) return { output: buffer, dropped: fresh > buffer.length };
|
|
1077
|
+
return { output: buffer.slice(buffer.length - fresh), dropped: false };
|
|
1078
|
+
}
|
|
1079
|
+
function killTask(task) {
|
|
885
1080
|
try {
|
|
886
|
-
|
|
1081
|
+
if (unix && task.child.pid) process.kill(-task.child.pid, "SIGKILL");
|
|
1082
|
+
else task.child.kill("SIGKILL");
|
|
887
1083
|
} catch {
|
|
888
|
-
}
|
|
889
|
-
await rename(tmp, abs);
|
|
890
|
-
}
|
|
891
|
-
var todos = [];
|
|
892
|
-
function httpGet(rawUrl, maxBytes, signal) {
|
|
893
|
-
return new Promise((resolve2, reject) => {
|
|
894
|
-
let u;
|
|
895
1084
|
try {
|
|
896
|
-
|
|
1085
|
+
task.child.kill("SIGKILL");
|
|
897
1086
|
} catch {
|
|
898
|
-
reject(new Error(`invalid URL: ${rawUrl}`));
|
|
899
|
-
return;
|
|
900
1087
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function startTask(command) {
|
|
1091
|
+
const running = [...tasks.values()].filter((t) => t.status === "running").length;
|
|
1092
|
+
if (running >= config.maxBackgroundTasks) {
|
|
1093
|
+
return { error: `too many background tasks (${running}/${config.maxBackgroundTasks}) \u2014 stop one with stop_task first.` };
|
|
1094
|
+
}
|
|
1095
|
+
const child = spawn2(command, { shell: true, detached: unix, stdio: ["ignore", "pipe", "pipe"] });
|
|
1096
|
+
const id = `bg_${++counter}`;
|
|
1097
|
+
const task = { id, command, child, buffer: "", totalLen: 0, readLen: 0, status: "running", exitCode: null, startedAt: Date.now() };
|
|
1098
|
+
const onData = (d) => {
|
|
1099
|
+
const chunk = d.toString();
|
|
1100
|
+
task.totalLen += chunk.length;
|
|
1101
|
+
task.buffer = appendTail(task.buffer, chunk, config.backgroundTailChars).buffer;
|
|
1102
|
+
};
|
|
1103
|
+
child.stdout?.on("data", onData);
|
|
1104
|
+
child.stderr?.on("data", onData);
|
|
1105
|
+
child.on("exit", (code) => {
|
|
1106
|
+
task.status = "exited";
|
|
1107
|
+
task.exitCode = code;
|
|
1108
|
+
});
|
|
1109
|
+
child.on("error", () => {
|
|
1110
|
+
task.status = "exited";
|
|
1111
|
+
task.exitCode = task.exitCode ?? -1;
|
|
1112
|
+
});
|
|
1113
|
+
tasks.set(id, task);
|
|
1114
|
+
return { id };
|
|
1115
|
+
}
|
|
1116
|
+
function checkTask(id) {
|
|
1117
|
+
const task = tasks.get(id);
|
|
1118
|
+
if (!task) return `Error: no background task with id "${id}". (Ids look like bg_1.)`;
|
|
1119
|
+
const { output, dropped } = readSince(task.buffer, task.totalLen, task.readLen);
|
|
1120
|
+
task.readLen = task.totalLen;
|
|
1121
|
+
const header = task.status === "running" ? `Task ${id} is running (${task.command}).` : `Task ${id} has exited (code ${task.exitCode ?? "unknown"}): ${task.command}`;
|
|
1122
|
+
const body = output ? `${dropped ? "\u2026[earlier output dropped]\n" : ""}${output}` : "(no new output since last check)";
|
|
1123
|
+
return `${header}
|
|
1124
|
+
${body}`;
|
|
1125
|
+
}
|
|
1126
|
+
function stopTask(id) {
|
|
1127
|
+
const task = tasks.get(id);
|
|
1128
|
+
if (!task) return `Error: no background task with id "${id}". (Ids look like bg_1.)`;
|
|
1129
|
+
if (task.status === "exited") return `Task ${id} had already exited (code ${task.exitCode ?? "unknown"}).`;
|
|
1130
|
+
killTask(task);
|
|
1131
|
+
task.status = "exited";
|
|
1132
|
+
return `Stopped background task ${id}.`;
|
|
1133
|
+
}
|
|
1134
|
+
function runningTaskCount() {
|
|
1135
|
+
return [...tasks.values()].filter((t) => t.status === "running").length;
|
|
1136
|
+
}
|
|
1137
|
+
function killAllTasks() {
|
|
1138
|
+
for (const task of tasks.values()) {
|
|
1139
|
+
if (task.status === "running") {
|
|
1140
|
+
killTask(task);
|
|
1141
|
+
task.status = "exited";
|
|
906
1142
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// src/subagent.ts
|
|
1147
|
+
var EXPLORER_TOOLS = /* @__PURE__ */ new Set(["read_file", "search", "list_dir", "web_fetch", "web_search"]);
|
|
1148
|
+
var EMPTY_SET = /* @__PURE__ */ new Set();
|
|
1149
|
+
var EXPLORER_SYSTEM_PROMPT = `You are a focused code explorer \u2014 a read-only sub-agent spawned to investigate ONE question and report back. Your only tools are read_file, search, list_dir, web_fetch, and web_search. You CANNOT edit files, run commands, or ask the user anything.
|
|
1150
|
+
|
|
1151
|
+
- Use search to locate code, list_dir to orient, read_file to read it for yourself; web_search + web_fetch when the question needs external/library docs. Follow the trail until you can answer.
|
|
1152
|
+
- Be efficient \u2014 you have a limited step budget. Go straight for the answer, don't wander.
|
|
1153
|
+
- When you have enough, STOP calling tools and write your findings.
|
|
1154
|
+
|
|
1155
|
+
Your FINAL message must be a concise, self-contained summary that answers the task: the key files with line references, how the relevant pieces fit together, and anything the parent agent needs to act. Prefer exact paths and symbol names over prose. Do not ask questions \u2014 give your best answer from what you found.`;
|
|
1156
|
+
var hasContent = (m) => Boolean(m.content) || (m.tool_calls?.length ?? 0) > 0;
|
|
1157
|
+
async function exploreLoop(deps, task, focus, signal) {
|
|
1158
|
+
const messages = [
|
|
1159
|
+
{ role: "system", content: EXPLORER_SYSTEM_PROMPT },
|
|
1160
|
+
{ role: "user", content: `Task: ${task}` + (focus ? `
|
|
1161
|
+
|
|
1162
|
+
Start by looking at: ${focus}` : "") }
|
|
1163
|
+
];
|
|
1164
|
+
for (let step = 0; step < deps.maxSteps && !signal?.aborted; step++) {
|
|
1165
|
+
const message = await deps.call(messages, true, signal);
|
|
1166
|
+
if (!hasContent(message)) break;
|
|
1167
|
+
messages.push(message);
|
|
1168
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
1169
|
+
for (const call of message.tool_calls) {
|
|
1170
|
+
if (signal?.aborted) break;
|
|
1171
|
+
let args = {};
|
|
1172
|
+
try {
|
|
1173
|
+
args = JSON.parse(call.function.arguments || "{}");
|
|
1174
|
+
} catch {
|
|
935
1175
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
if (status >= 300 && status < 400 && location) {
|
|
942
|
-
res.resume();
|
|
943
|
-
resolve2({ status, location, contentType, body: "" });
|
|
944
|
-
return;
|
|
1176
|
+
const g = deps.gate(call.function.name, args);
|
|
1177
|
+
if (!g.ok) {
|
|
1178
|
+
messages.push({ role: "tool", tool_call_id: call.id, content: `Denied: ${g.reason}. You are a read-only explorer confined to the project \u2014 work with what you can read in-root.` });
|
|
1179
|
+
deps.onStep?.(call.function.name, args, null, g.reason);
|
|
1180
|
+
continue;
|
|
945
1181
|
}
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
if (total < maxBytes) {
|
|
950
|
-
chunks.push(d);
|
|
951
|
-
total += d.length;
|
|
952
|
-
}
|
|
953
|
-
if (total >= maxBytes) res.destroy();
|
|
954
|
-
});
|
|
955
|
-
const done = () => resolve2({ status, location, contentType, body: Buffer.concat(chunks).toString("utf8").slice(0, maxBytes) });
|
|
956
|
-
res.on("end", done);
|
|
957
|
-
res.on("close", done);
|
|
958
|
-
res.on("error", reject);
|
|
1182
|
+
const result = await deps.dispatch(call, signal);
|
|
1183
|
+
messages.push({ role: "tool", tool_call_id: call.id, content: result });
|
|
1184
|
+
deps.onStep?.(call.function.name, args, result);
|
|
959
1185
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1186
|
+
} else {
|
|
1187
|
+
return message.content ?? "(the explorer returned no findings)";
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (signal?.aborted) return "(exploration cancelled)";
|
|
1191
|
+
const wrap = await deps.call(
|
|
1192
|
+
[...messages, { role: "system", content: "Stop exploring now. Write your findings summary from what you've gathered so far; do not call any tools." }],
|
|
1193
|
+
false,
|
|
1194
|
+
signal
|
|
1195
|
+
);
|
|
1196
|
+
return wrap.content ?? "(the explorer reached its step budget without a conclusion)";
|
|
965
1197
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
if (items.length >= cap) return;
|
|
971
|
-
let entries;
|
|
972
|
-
try {
|
|
973
|
-
entries = await readdir(abs, { withFileTypes: true });
|
|
974
|
-
} catch {
|
|
1198
|
+
function narrate(name, args, result, blocked) {
|
|
1199
|
+
const arg = name === "read_file" || name === "list_dir" ? String(args.path ?? "") : name === "search" ? String(args.pattern ?? "") : name === "web_fetch" ? String(args.url ?? "") : name === "web_search" ? String(args.query ?? "") : "";
|
|
1200
|
+
if (blocked) {
|
|
1201
|
+
console.log(color.dim(` ${name} ${stripControl(arg)} \u2014 denied`));
|
|
975
1202
|
return;
|
|
976
1203
|
}
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1204
|
+
const lines = result ? result.split("\n").length : 0;
|
|
1205
|
+
console.log(color.dim(` ${name} ${stripControl(arg)}${lines ? ` \xB7 ${lines} lines` : ""}`));
|
|
1206
|
+
}
|
|
1207
|
+
async function runExplorer(task, focus, signal) {
|
|
1208
|
+
const defs = toolDefs.filter((t) => EXPLORER_TOOLS.has(t.name));
|
|
1209
|
+
const childSchema = defs.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } }));
|
|
1210
|
+
const childByName = new Map(defs.map((t) => [t.name, t]));
|
|
1211
|
+
const cap = config.maxToolResultChars;
|
|
1212
|
+
const deps = {
|
|
1213
|
+
call: (m, incl, sig) => callModel(m, incl, sig, { tools: childSchema, quiet: true }),
|
|
1214
|
+
dispatch: async (c, sig) => {
|
|
1215
|
+
const r = await runTool(c, sig, childByName);
|
|
1216
|
+
return r.length > cap ? r.slice(0, cap) + `
|
|
1217
|
+
\u2026[truncated ${r.length - cap} chars]` : r;
|
|
1218
|
+
},
|
|
1219
|
+
gate: (name, args) => {
|
|
1220
|
+
const d = decideApproval(childByName.get(name), args, { mode: "readonly", autoApprove: true, approvedTools: EMPTY_SET, toolName: name });
|
|
1221
|
+
return d.action === "run" ? { ok: true } : { ok: false, reason: d.reason ?? "not allowed for the read-only explorer" };
|
|
1222
|
+
},
|
|
1223
|
+
onStep: narrate,
|
|
1224
|
+
maxSteps: config.subAgentMaxSteps
|
|
1225
|
+
};
|
|
1226
|
+
try {
|
|
1227
|
+
console.log(color.dim(` \u21B3 exploring: ${stripControl(task)}`));
|
|
1228
|
+
const findings = await exploreLoop(deps, task, focus, signal);
|
|
1229
|
+
console.log(color.dim(` \u21B3 findings ready`));
|
|
1230
|
+
return findings;
|
|
1231
|
+
} catch (err) {
|
|
1232
|
+
if (signal?.aborted || err?.name === "AbortError") return "(exploration cancelled)";
|
|
1233
|
+
return `Error exploring: ${err.message}`;
|
|
984
1234
|
}
|
|
985
1235
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1236
|
+
|
|
1237
|
+
// src/safety.ts
|
|
1238
|
+
import { homedir as homedir3 } from "node:os";
|
|
1239
|
+
import { basename } from "node:path";
|
|
1240
|
+
function pathGuard(args) {
|
|
1241
|
+
const { abs, inRoot } = resolveInRoot(String(args.path ?? "."));
|
|
1242
|
+
return inRoot ? {} : { needsApproval: true, reason: `path is outside the project root: ${abs}` };
|
|
989
1243
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1244
|
+
var SECRET_FILE = /(^|\/)(\.env(\.[\w.-]+)?|[\w.-]*\.(env|pem|key|secret|pfx|p12|jks|keystore)|id_(rsa|ed25519|ecdsa|dsa)|credentials|\.git-credentials|\.pgpass|\.npmrc|\.netrc)$/i;
|
|
1245
|
+
function isSecretPath(userPath) {
|
|
1246
|
+
const { abs } = resolveInRoot(userPath);
|
|
1247
|
+
return SECRET_FILE.test(abs) || SECRET_FILE.test(basename(abs));
|
|
1248
|
+
}
|
|
1249
|
+
function secretGuard(args) {
|
|
1250
|
+
const p = pathGuard(args);
|
|
1251
|
+
if (p.needsApproval) return p;
|
|
1252
|
+
const path = String(args.path ?? "");
|
|
1253
|
+
return isSecretPath(path) ? { needsApproval: true, reason: `this looks like a secrets file (${path}) \u2014 approve before continuing` } : {};
|
|
1254
|
+
}
|
|
1255
|
+
var readGuard = secretGuard;
|
|
1256
|
+
var writeGuard = secretGuard;
|
|
1257
|
+
var DANGEROUS_BASH = [
|
|
1258
|
+
/\brm\b[\s\S]*\s(\/|~|\$\{?HOME\}?)\/?(\s*$|\*|\/\*)/,
|
|
1259
|
+
// rm of / ~ $HOME (and their immediate /*)
|
|
1260
|
+
/\brm\b[\s\S]*\s\/(etc|usr|bin|sbin|lib|var|boot|dev|sys|proc|root|System|Library|Applications)(\/|\s|$|\*)/,
|
|
1261
|
+
// rm of a system root
|
|
1262
|
+
/:\s*\(\s*\)\s*\{[^}]*\}\s*;\s*:/,
|
|
1263
|
+
// fork bomb :(){ :|:& };:
|
|
1264
|
+
/\bmkfs\.?\w*/,
|
|
1265
|
+
// format a filesystem
|
|
1266
|
+
/\bdd\b[^\n]*\bof=\/dev\//,
|
|
1267
|
+
// dd to a raw device
|
|
1268
|
+
/\b(curl|wget)\b[^|]*\|\s*(sudo\s+)?(sh|bash|zsh)\b/,
|
|
1269
|
+
// pipe-to-shell
|
|
1270
|
+
/>\s*\/dev\/(sd|nvme|disk)/
|
|
1271
|
+
// overwrite a disk device
|
|
1272
|
+
];
|
|
1273
|
+
var RISKY_BASH = [
|
|
1274
|
+
/\b(rm|rmdir|shred|unlink)\b/,
|
|
1275
|
+
// deleting files
|
|
1276
|
+
/\bfind\b[\s\S]*\s-(delete|exec)\b/,
|
|
1277
|
+
// find -delete / -exec (mass mutate without "rm")
|
|
1278
|
+
/\btruncate\b/,
|
|
1279
|
+
// truncate files to a size
|
|
1280
|
+
/(^|[\s;&|])(:|true)\s*>\s*\S/,
|
|
1281
|
+
// `: > file` / `true > file` truncation idiom
|
|
1282
|
+
/\b(dd|fdisk|parted|wipefs|sgdisk)\b/,
|
|
1283
|
+
// raw disk tools
|
|
1284
|
+
/\bmkfs\.?\w*/,
|
|
1285
|
+
// make a filesystem
|
|
1286
|
+
/\bsudo\b/,
|
|
1287
|
+
// privilege escalation
|
|
1288
|
+
/[<>]\s*\/dev\/\w/,
|
|
1289
|
+
// raw device I/O
|
|
1290
|
+
/\|\s*(sudo\s+)?(sh|bash|zsh|python\d?|node|perl|ruby|php)\b/,
|
|
1291
|
+
// pipe INTO an interpreter
|
|
1292
|
+
/\b(eval|source)\b[\s\S]*\$\(\s*(curl|wget|fetch)\b/
|
|
1293
|
+
// eval/source of a download
|
|
1294
|
+
];
|
|
1295
|
+
function refsOutsideRoot(cmd) {
|
|
1296
|
+
const norm = cmd.replace(/\$\{?IFS\}?/g, " ").replace(/\$\{?HOME\}?/g, homedir3()).replace(/\$\{?PWD\}?/g, process.cwd()).replace(/(^|[\s"'`=(])~(?=\/|$)/g, (_m, p) => p + homedir3());
|
|
1297
|
+
if (/(^|[\s"'`=(])(\.\.\/|~(\/|$))/.test(norm)) return true;
|
|
1298
|
+
for (const m of norm.matchAll(/(?:^|[\s"'`=(])(\/[^\s"'`;|&()<>]*)/g)) {
|
|
1299
|
+
if (!resolveInRoot(m[1]).inRoot) return true;
|
|
1010
1300
|
}
|
|
1011
|
-
return
|
|
1301
|
+
return false;
|
|
1012
1302
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1303
|
+
function bashGuard(args) {
|
|
1304
|
+
const cmd = String(args.command ?? "");
|
|
1305
|
+
const risky = RISKY_BASH.find((re) => re.test(cmd));
|
|
1306
|
+
if (risky) return { needsApproval: true, reason: `this shell command looks risky (matched ${risky})` };
|
|
1307
|
+
if (refsOutsideRoot(cmd)) return { needsApproval: true, reason: "this shell command references a path outside the project root" };
|
|
1308
|
+
return {};
|
|
1309
|
+
}
|
|
1310
|
+
function isPrivateAddr(ip) {
|
|
1311
|
+
const m = ip.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
|
1312
|
+
if (m) {
|
|
1313
|
+
const a = +m[1], b = +m[2];
|
|
1314
|
+
return a === 0 || a === 127 || a === 10 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127;
|
|
1315
|
+
}
|
|
1316
|
+
const ip6 = ip.toLowerCase();
|
|
1317
|
+
const dotted = ip6.match(/(?:^|:)(\d+\.\d+\.\d+\.\d+)$/);
|
|
1318
|
+
if (dotted) return isPrivateAddr(dotted[1]);
|
|
1319
|
+
const g = expandIPv6(ip6);
|
|
1320
|
+
if (!g) return true;
|
|
1321
|
+
if (g.every((h) => h === 0)) return true;
|
|
1322
|
+
if (g.slice(0, 7).every((h) => h === 0) && g[7] === 1) return true;
|
|
1323
|
+
const embeddedV4 = () => `${g[6] >> 8 & 255}.${g[6] & 255}.${g[7] >> 8 & 255}.${g[7] & 255}`;
|
|
1324
|
+
if (g[0] === 0 && g[1] === 0 && g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 65535) {
|
|
1325
|
+
return isPrivateAddr(embeddedV4());
|
|
1326
|
+
}
|
|
1327
|
+
if (g[0] === 100 && g[1] === 65435 && g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0 || g[0] === 0 && g[1] === 0 && g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0) {
|
|
1328
|
+
return isPrivateAddr(embeddedV4());
|
|
1329
|
+
}
|
|
1330
|
+
if ((g[0] & 65472) === 65152) return true;
|
|
1331
|
+
if ((g[0] & 65024) === 64512) return true;
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
function expandIPv6(ip) {
|
|
1335
|
+
const halves = ip.split("::");
|
|
1336
|
+
if (halves.length > 2) return null;
|
|
1337
|
+
const parse = (s) => s ? s.split(":").map((h) => parseInt(h, 16)) : [];
|
|
1338
|
+
const head = parse(halves[0]);
|
|
1339
|
+
if (halves.length === 1) return head.length === 8 && head.every((n) => n >= 0 && n <= 65535) ? head : null;
|
|
1340
|
+
const tail = parse(halves[1]);
|
|
1341
|
+
const fill = 8 - head.length - tail.length;
|
|
1342
|
+
if (fill < 0) return null;
|
|
1343
|
+
const g = [...head, ...Array(fill).fill(0), ...tail];
|
|
1344
|
+
return g.length === 8 && g.every((n) => Number.isFinite(n) && n >= 0 && n <= 65535) ? g : null;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/html.ts
|
|
1348
|
+
function htmlToText(html) {
|
|
1349
|
+
let s = html;
|
|
1350
|
+
s = s.replace(/<(script|style|noscript|template|svg|head)[\s\S]*?<\/\1>/gi, " ");
|
|
1351
|
+
s = s.replace(/<!--[\s\S]*?-->/g, " ");
|
|
1352
|
+
s = s.replace(/<br\s*\/?>/gi, "\n");
|
|
1353
|
+
s = s.replace(/<\/(p|div|li|tr|h[1-6]|section|article|header|footer|ul|ol|table|blockquote)\s*>/gi, "\n");
|
|
1354
|
+
s = s.replace(/<[^>]+>/g, " ");
|
|
1355
|
+
s = decodeEntities(s);
|
|
1356
|
+
s = s.replace(/[ \t\f\v]+/g, " ").replace(/\n[ \t]+/g, "\n").replace(/\n{3,}/g, "\n\n");
|
|
1357
|
+
return s.trim();
|
|
1358
|
+
}
|
|
1359
|
+
function decodeEntities(s) {
|
|
1360
|
+
const named = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
|
|
1361
|
+
return s.replace(/&(#x?[0-9a-f]+|[a-z][a-z0-9]*);/gi, (m, code) => {
|
|
1362
|
+
if (code[0] === "#") {
|
|
1363
|
+
const n = code[1].toLowerCase() === "x" ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10);
|
|
1364
|
+
return Number.isFinite(n) && n >= 0 && n <= 1114111 && !(n >= 55296 && n <= 57343) ? String.fromCodePoint(n) : m;
|
|
1040
1365
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
required: ["path"]
|
|
1054
|
-
},
|
|
1055
|
-
guard: readGuard,
|
|
1056
|
-
// Returns a tagged payload (\x01file\x01… / \x01dir\x01…) that the agent loop
|
|
1057
|
-
// renders for the user; the model gets a short note instead (see ui.renderShow).
|
|
1058
|
-
run: async (args) => {
|
|
1366
|
+
return named[code.toLowerCase()] ?? m;
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// src/tools.ts
|
|
1371
|
+
function runShell(command, opts) {
|
|
1372
|
+
return new Promise((resolve2, reject) => {
|
|
1373
|
+
const unix2 = process.platform !== "win32";
|
|
1374
|
+
const child = spawn3(command, { shell: true, detached: unix2, stdio: ["ignore", "pipe", "pipe"] });
|
|
1375
|
+
let stdout = "", stderr = "", outLen = 0, errLen = 0, timedOut = false, aborted = false;
|
|
1376
|
+
let settled = false, exitCode = null;
|
|
1377
|
+
const kill = () => {
|
|
1059
1378
|
try {
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
return showPayload("tree", { path: String(args.path), items, truncated: items.length >= TREE_CAP });
|
|
1067
|
-
}
|
|
1068
|
-
const entries = sortDirents(await readdir(abs, { withFileTypes: true }));
|
|
1069
|
-
const names = entries.map((e) => e.isDirectory() ? e.name + "/" : e.name);
|
|
1070
|
-
return showPayload("dir", { path: String(args.path), names });
|
|
1379
|
+
if (unix2 && child.pid) process.kill(-child.pid, "SIGKILL");
|
|
1380
|
+
else child.kill("SIGKILL");
|
|
1381
|
+
} catch {
|
|
1382
|
+
try {
|
|
1383
|
+
child.kill("SIGKILL");
|
|
1384
|
+
} catch {
|
|
1071
1385
|
}
|
|
1072
|
-
const { off, lim } = parseRange(args, 80);
|
|
1073
|
-
const { lines, startLine, hasMore } = await readLineWindow(abs, off, lim);
|
|
1074
|
-
return showPayload("file", { path: String(args.path), startLine, lines, hasMore });
|
|
1075
|
-
} catch (err) {
|
|
1076
|
-
return fail("showing", err);
|
|
1077
1386
|
}
|
|
1387
|
+
};
|
|
1388
|
+
const timer = setTimeout(() => {
|
|
1389
|
+
timedOut = true;
|
|
1390
|
+
kill();
|
|
1391
|
+
}, opts.timeout);
|
|
1392
|
+
const onAbort = () => {
|
|
1393
|
+
aborted = true;
|
|
1394
|
+
kill();
|
|
1395
|
+
};
|
|
1396
|
+
if (opts.signal) {
|
|
1397
|
+
if (opts.signal.aborted) onAbort();
|
|
1398
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
1078
1399
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
parameters: {
|
|
1084
|
-
type: "object",
|
|
1085
|
-
properties: {
|
|
1086
|
-
pattern: { type: "string", description: "Regular expression to search for." },
|
|
1087
|
-
path: { type: "string", description: "Directory to search. Defaults to the current directory." }
|
|
1088
|
-
},
|
|
1089
|
-
required: ["pattern"]
|
|
1090
|
-
},
|
|
1091
|
-
guard: pathGuard,
|
|
1092
|
-
run: async (args) => {
|
|
1093
|
-
if (/\([^()]*[+*{][^()]*\)\s*[+*{]/.test(String(args.pattern ?? ""))) {
|
|
1094
|
-
return `Error: that pattern has nested quantifiers that can hang the search (catastrophic backtracking). Simplify it.`;
|
|
1400
|
+
child.stdout?.on("data", (d) => {
|
|
1401
|
+
if (outLen < opts.maxBuffer) {
|
|
1402
|
+
stdout += d;
|
|
1403
|
+
outLen += d.length;
|
|
1095
1404
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1405
|
+
});
|
|
1406
|
+
child.stderr?.on("data", (d) => {
|
|
1407
|
+
if (errLen < opts.maxBuffer) {
|
|
1408
|
+
stderr += d;
|
|
1409
|
+
errLen += d.length;
|
|
1101
1410
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1411
|
+
});
|
|
1412
|
+
const cleanup = () => {
|
|
1413
|
+
clearTimeout(timer);
|
|
1414
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
1415
|
+
};
|
|
1416
|
+
const finalize = () => {
|
|
1417
|
+
if (settled) return;
|
|
1418
|
+
settled = true;
|
|
1419
|
+
cleanup();
|
|
1420
|
+
if (aborted) reject(Object.assign(new Error("cancelled"), { stdout, stderr }));
|
|
1421
|
+
else if (timedOut) reject(Object.assign(new Error(`timed out after ${opts.timeout}ms`), { stdout, stderr }));
|
|
1422
|
+
else if (exitCode !== 0 && exitCode !== null) reject(Object.assign(new Error(`exited with code ${exitCode}`), { stdout, stderr }));
|
|
1423
|
+
else resolve2({ stdout, stderr });
|
|
1424
|
+
};
|
|
1425
|
+
child.on("error", (err) => {
|
|
1426
|
+
if (settled) return;
|
|
1427
|
+
settled = true;
|
|
1428
|
+
cleanup();
|
|
1429
|
+
reject(Object.assign(err, { stdout, stderr }));
|
|
1430
|
+
});
|
|
1431
|
+
child.on("close", finalize);
|
|
1432
|
+
child.on("exit", (code) => {
|
|
1433
|
+
exitCode = code;
|
|
1434
|
+
setTimeout(finalize, 100);
|
|
1435
|
+
});
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
var fail = (verb, err) => `Error ${verb}: ${err.message}`;
|
|
1439
|
+
async function atomicWrite(abs, content) {
|
|
1440
|
+
const tmp = `${abs}.beecork-${process.pid}.tmp`;
|
|
1441
|
+
await writeFile2(tmp, content, "utf8");
|
|
1442
|
+
try {
|
|
1443
|
+
await chmod(tmp, (await stat(abs)).mode);
|
|
1444
|
+
} catch {
|
|
1445
|
+
}
|
|
1446
|
+
await rename(tmp, abs);
|
|
1447
|
+
}
|
|
1448
|
+
var READ_PREFIX = /^ *\d+ {2}/;
|
|
1449
|
+
function allIndexOf(hay, needle) {
|
|
1450
|
+
const out2 = [];
|
|
1451
|
+
let i = hay.indexOf(needle);
|
|
1452
|
+
while (i !== -1) {
|
|
1453
|
+
out2.push(i);
|
|
1454
|
+
i = hay.indexOf(needle, i + 1);
|
|
1455
|
+
}
|
|
1456
|
+
return out2;
|
|
1457
|
+
}
|
|
1458
|
+
function stripReadPrefix(text) {
|
|
1459
|
+
const lines = text.split("\n");
|
|
1460
|
+
const nonBlank = lines.filter((l) => l.trim() !== "");
|
|
1461
|
+
if (nonBlank.length === 0 || !nonBlank.every((l) => READ_PREFIX.test(l))) return null;
|
|
1462
|
+
return lines.map((l) => l.replace(READ_PREFIX, "")).join("\n");
|
|
1463
|
+
}
|
|
1464
|
+
var leadWs = (l) => l.match(/^[ \t]*/)[0];
|
|
1465
|
+
function lineOffsets(text) {
|
|
1466
|
+
const starts = [0];
|
|
1467
|
+
for (let i = 0; i < text.length; i++) if (text[i] === "\n") starts.push(i + 1);
|
|
1468
|
+
return starts;
|
|
1469
|
+
}
|
|
1470
|
+
function matchWhitespace(file, oldText, newText) {
|
|
1471
|
+
const fileLines = file.split("\n");
|
|
1472
|
+
const oldLines = oldText.split("\n");
|
|
1473
|
+
const n = oldLines.length;
|
|
1474
|
+
const trim = (l) => l.trim();
|
|
1475
|
+
const oldTrim = oldLines.map(trim);
|
|
1476
|
+
const starts = [];
|
|
1477
|
+
for (let s2 = 0; s2 + n <= fileLines.length; s2++) {
|
|
1478
|
+
let hit = true;
|
|
1479
|
+
for (let i = 0; i < n; i++) if (trim(fileLines[s2 + i]) !== oldTrim[i]) {
|
|
1480
|
+
hit = false;
|
|
1481
|
+
break;
|
|
1482
|
+
}
|
|
1483
|
+
if (hit) starts.push(s2);
|
|
1484
|
+
}
|
|
1485
|
+
if (starts.length === 0) return null;
|
|
1486
|
+
if (starts.length > 1) return { ok: false, reason: "ambiguous", count: starts.length };
|
|
1487
|
+
const s = starts[0];
|
|
1488
|
+
let shift = null;
|
|
1489
|
+
let mode = "same";
|
|
1490
|
+
for (let i = 0; i < n; i++) {
|
|
1491
|
+
if (oldTrim[i] === "") continue;
|
|
1492
|
+
const fLead = leadWs(fileLines[s + i]);
|
|
1493
|
+
const oLead = leadWs(oldLines[i]);
|
|
1494
|
+
let thisShift, thisMode;
|
|
1495
|
+
if (fLead === oLead) {
|
|
1496
|
+
thisShift = "";
|
|
1497
|
+
thisMode = "same";
|
|
1498
|
+
} else if (fLead.endsWith(oLead)) {
|
|
1499
|
+
thisShift = fLead.slice(0, fLead.length - oLead.length);
|
|
1500
|
+
thisMode = "add";
|
|
1501
|
+
} else if (oLead.endsWith(fLead)) {
|
|
1502
|
+
thisShift = oLead.slice(0, oLead.length - fLead.length);
|
|
1503
|
+
thisMode = "strip";
|
|
1504
|
+
} else return null;
|
|
1505
|
+
if (shift === null) {
|
|
1506
|
+
shift = thisShift;
|
|
1507
|
+
mode = thisMode;
|
|
1508
|
+
} else if (thisShift !== shift || thisMode !== mode && thisShift !== "") return null;
|
|
1509
|
+
}
|
|
1510
|
+
shift = shift ?? "";
|
|
1511
|
+
const reindented = newText.split("\n").map((l) => {
|
|
1512
|
+
if (l.trim() === "") return l;
|
|
1513
|
+
if (mode === "add") return shift + l;
|
|
1514
|
+
if (mode === "strip") return l.startsWith(shift) ? l.slice(shift.length) : l;
|
|
1515
|
+
return l;
|
|
1516
|
+
}).join("\n");
|
|
1517
|
+
const offs = lineOffsets(file);
|
|
1518
|
+
const start = offs[s];
|
|
1519
|
+
const end = s + n < offs.length ? offs[s + n] - 1 : file.length;
|
|
1520
|
+
return { ok: true, start, end, after: reindented, healedVia: "whitespace" };
|
|
1521
|
+
}
|
|
1522
|
+
function closestRegion(file, oldText) {
|
|
1523
|
+
const anchor = oldText.split("\n").map((l) => l.trim()).find((l) => l !== "");
|
|
1524
|
+
if (!anchor) return void 0;
|
|
1525
|
+
const fileLines = file.split("\n");
|
|
1526
|
+
const fmt = (i) => `${String(i + 1).padStart(5)} ${fileLines[i]}`;
|
|
1527
|
+
const exact = [];
|
|
1528
|
+
for (let i = 0; i < fileLines.length && exact.length < 3; i++) {
|
|
1529
|
+
if (fileLines[i].trim() === anchor) exact.push(fmt(i));
|
|
1530
|
+
}
|
|
1531
|
+
if (exact.length) return exact.join("\n");
|
|
1532
|
+
const words = anchor.split(/\W+/).filter((w) => w.length >= 2);
|
|
1533
|
+
if (words.length === 0) return void 0;
|
|
1534
|
+
let best = -1;
|
|
1535
|
+
let bestScore = 0;
|
|
1536
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
1537
|
+
let score = 0;
|
|
1538
|
+
for (const w of words) if (fileLines[i].includes(w)) score++;
|
|
1539
|
+
if (score > bestScore) {
|
|
1540
|
+
bestScore = score;
|
|
1541
|
+
best = i;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return best >= 0 && bestScore >= Math.max(2, Math.ceil(words.length / 2)) ? fmt(best) : void 0;
|
|
1545
|
+
}
|
|
1546
|
+
function resolveEdit(file, oldText, newText) {
|
|
1547
|
+
if (oldText === "") return { ok: false, reason: "not_found" };
|
|
1548
|
+
const exact = allIndexOf(file, oldText);
|
|
1549
|
+
if (exact.length === 1) return { ok: true, start: exact[0], end: exact[0] + oldText.length, after: newText, healedVia: "exact" };
|
|
1550
|
+
if (exact.length > 1) return { ok: false, reason: "ambiguous", count: exact.length };
|
|
1551
|
+
const strippedOld = stripReadPrefix(oldText);
|
|
1552
|
+
if (strippedOld !== null && strippedOld !== oldText) {
|
|
1553
|
+
const hits = allIndexOf(file, strippedOld);
|
|
1554
|
+
if (hits.length === 1) {
|
|
1555
|
+
const strippedNew = newText.split("\n").map((l) => l.replace(READ_PREFIX, "")).join("\n");
|
|
1556
|
+
return { ok: true, start: hits[0], end: hits[0] + strippedOld.length, after: strippedNew, healedVia: "prefix" };
|
|
1557
|
+
}
|
|
1558
|
+
if (hits.length > 1) return { ok: false, reason: "ambiguous", count: hits.length };
|
|
1559
|
+
}
|
|
1560
|
+
const ws = matchWhitespace(file, oldText, newText);
|
|
1561
|
+
if (ws) return ws;
|
|
1562
|
+
return { ok: false, reason: "not_found", closest: closestRegion(file, oldText) };
|
|
1563
|
+
}
|
|
1564
|
+
var todos = [];
|
|
1565
|
+
function httpGet(rawUrl, maxBytes, signal) {
|
|
1566
|
+
return new Promise((resolve2, reject) => {
|
|
1567
|
+
let u;
|
|
1568
|
+
try {
|
|
1569
|
+
u = new URL(rawUrl);
|
|
1570
|
+
} catch {
|
|
1571
|
+
reject(new Error(`invalid URL: ${rawUrl}`));
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const isHttps = u.protocol === "https:";
|
|
1575
|
+
const reqFn = isHttps ? httpsRequest : httpRequest;
|
|
1576
|
+
if (isIP(u.hostname) && isPrivateAddr(u.hostname)) {
|
|
1577
|
+
reject(new Error(`refused: ${u.hostname} is a private/internal address`));
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
const lookup = (hostname, options, cb) => {
|
|
1581
|
+
dnsLookup(hostname, options, (err, address, family) => {
|
|
1582
|
+
if (err) return cb(err, "", 0);
|
|
1583
|
+
if (Array.isArray(address)) {
|
|
1584
|
+
for (const a of address) {
|
|
1585
|
+
if (isPrivateAddr(a.address)) return cb(new Error(`refused: ${hostname} \u2192 private/internal address (${a.address})`), "", 0);
|
|
1586
|
+
}
|
|
1587
|
+
return cb(null, address);
|
|
1588
|
+
}
|
|
1589
|
+
const addr = String(address);
|
|
1590
|
+
if (isPrivateAddr(addr)) return cb(new Error(`refused: ${hostname} \u2192 private/internal address (${addr})`), "", 0);
|
|
1591
|
+
cb(null, addr, family);
|
|
1592
|
+
});
|
|
1593
|
+
};
|
|
1594
|
+
const req = reqFn(
|
|
1595
|
+
{
|
|
1596
|
+
protocol: u.protocol,
|
|
1597
|
+
hostname: u.hostname,
|
|
1598
|
+
port: Number(u.port) || (isHttps ? 443 : 80),
|
|
1599
|
+
path: u.pathname + u.search,
|
|
1600
|
+
method: "GET",
|
|
1601
|
+
lookup,
|
|
1602
|
+
signal,
|
|
1603
|
+
// user cancel (Ctrl-C) aborts the request
|
|
1604
|
+
headers: {
|
|
1605
|
+
"User-Agent": "beecork (+https://github.com/beecork/beecork)",
|
|
1606
|
+
Accept: "text/html,text/plain,*/*",
|
|
1607
|
+
"Accept-Encoding": "identity"
|
|
1608
|
+
}
|
|
1609
|
+
},
|
|
1610
|
+
(res) => {
|
|
1611
|
+
const status = res.statusCode ?? 0;
|
|
1612
|
+
const location = res.headers.location ?? null;
|
|
1613
|
+
const contentType = String(res.headers["content-type"] ?? "");
|
|
1614
|
+
if (status >= 300 && status < 400 && location) {
|
|
1615
|
+
res.resume();
|
|
1616
|
+
resolve2({ status, location, contentType, body: "" });
|
|
1117
1617
|
return;
|
|
1118
1618
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
if (IGNORE.has(e.name)) continue;
|
|
1126
|
-
const full = `${dir}/${e.name}`;
|
|
1127
|
-
if (e.isDirectory()) {
|
|
1128
|
-
await walk(full);
|
|
1129
|
-
} else if (e.isFile()) {
|
|
1130
|
-
if (SECRET_FILE.test(e.name)) continue;
|
|
1131
|
-
const info = await stat(full).catch(() => null);
|
|
1132
|
-
if (info && info.size > config.searchMaxFileBytes) continue;
|
|
1133
|
-
let lines;
|
|
1134
|
-
try {
|
|
1135
|
-
lines = (await readFile2(full, "utf8")).split("\n");
|
|
1136
|
-
} catch {
|
|
1137
|
-
continue;
|
|
1138
|
-
}
|
|
1139
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1140
|
-
if (lines[i].length > 1e4) continue;
|
|
1141
|
-
if (regex.test(lines[i])) {
|
|
1142
|
-
results.push(`${full}:${i + 1}: ${lines[i].trim()}`);
|
|
1143
|
-
if (results.length >= MAX) break;
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1619
|
+
const chunks = [];
|
|
1620
|
+
let total = 0;
|
|
1621
|
+
res.on("data", (d) => {
|
|
1622
|
+
if (total < maxBytes) {
|
|
1623
|
+
chunks.push(d);
|
|
1624
|
+
total += d.length;
|
|
1146
1625
|
}
|
|
1147
|
-
|
|
1626
|
+
if (total >= maxBytes) res.destroy();
|
|
1627
|
+
});
|
|
1628
|
+
const done = () => resolve2({ status, location, contentType, body: Buffer.concat(chunks).toString("utf8").slice(0, maxBytes) });
|
|
1629
|
+
res.on("end", done);
|
|
1630
|
+
res.on("close", done);
|
|
1631
|
+
res.on("error", reject);
|
|
1148
1632
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1633
|
+
);
|
|
1634
|
+
req.setTimeout(config.webTimeoutMs, () => req.destroy(new Error(`timed out after ${config.webTimeoutMs}ms`)));
|
|
1635
|
+
req.on("error", reject);
|
|
1636
|
+
req.end();
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next"]);
|
|
1640
|
+
var TREE_CAP = 400;
|
|
1641
|
+
var sortDirents = (entries) => entries.sort((a, b) => a.isDirectory() === b.isDirectory() ? a.name.localeCompare(b.name) : a.isDirectory() ? -1 : 1);
|
|
1642
|
+
async function walkTree(abs, prefix, items, cap) {
|
|
1643
|
+
if (items.length >= cap) return;
|
|
1644
|
+
let entries;
|
|
1645
|
+
try {
|
|
1646
|
+
entries = await readdir(abs, { withFileTypes: true });
|
|
1647
|
+
} catch {
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
const kept = sortDirents(entries.filter((e) => !SKIP_DIRS.has(e.name)));
|
|
1651
|
+
for (let i = 0; i < kept.length; i++) {
|
|
1652
|
+
if (items.length >= cap) return;
|
|
1653
|
+
const e = kept[i];
|
|
1654
|
+
const last = i === kept.length - 1;
|
|
1655
|
+
items.push({ prefix: prefix + (last ? "\u2514\u2500 " : "\u251C\u2500 "), name: e.name + (e.isDirectory() ? "/" : ""), isDir: e.isDirectory() });
|
|
1656
|
+
if (e.isDirectory()) await walkTree(join2(abs, e.name), prefix + (last ? " " : "\u2502 "), items, cap);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
function parseRange(args, defLimit) {
|
|
1660
|
+
const o = Number(args.offset), l = Number(args.limit);
|
|
1661
|
+
return { off: Number.isFinite(o) && o > 0 ? o : 1, lim: Number.isFinite(l) && l > 0 ? l : defLimit };
|
|
1662
|
+
}
|
|
1663
|
+
async function readLineWindow(abs, offset1, limit) {
|
|
1664
|
+
const start = Math.max(0, offset1 - 1);
|
|
1665
|
+
const end = start + Math.max(1, limit);
|
|
1666
|
+
const lines = [];
|
|
1667
|
+
let i = 0;
|
|
1668
|
+
let hasMore = false;
|
|
1669
|
+
const stream = createReadStream(abs, { encoding: "utf8" });
|
|
1670
|
+
const rl = createLineReader({ input: stream, crlfDelay: Infinity });
|
|
1671
|
+
try {
|
|
1672
|
+
for await (const line of rl) {
|
|
1673
|
+
if (i >= end) {
|
|
1674
|
+
hasMore = true;
|
|
1675
|
+
break;
|
|
1676
|
+
}
|
|
1677
|
+
if (i >= start) lines.push(line);
|
|
1678
|
+
i++;
|
|
1155
1679
|
}
|
|
1156
|
-
}
|
|
1680
|
+
} finally {
|
|
1681
|
+
rl.close();
|
|
1682
|
+
stream.destroy();
|
|
1683
|
+
}
|
|
1684
|
+
return { lines, startLine: start + 1, hasMore, empty: i === 0 };
|
|
1685
|
+
}
|
|
1686
|
+
var toolDefs = [
|
|
1157
1687
|
{
|
|
1158
|
-
name: "
|
|
1159
|
-
description: "
|
|
1160
|
-
needsApproval: true,
|
|
1161
|
-
guard: writeGuard,
|
|
1162
|
-
// out-of-root OR a secrets file (.env/.npmrc/key…) → per-call prompt, never cached
|
|
1688
|
+
name: "read_file",
|
|
1689
|
+
description: "Read a text file, returned WITH line numbers (for reference only). For large files, pass offset (1-based start line) and limit (number of lines) to read a range.",
|
|
1163
1690
|
parameters: {
|
|
1164
1691
|
type: "object",
|
|
1165
1692
|
properties: {
|
|
1166
|
-
path: { type: "string", description: "Path to the file
|
|
1167
|
-
|
|
1693
|
+
path: { type: "string", description: "Path to the file." },
|
|
1694
|
+
offset: { type: "number", description: "1-based line to start from (optional)." },
|
|
1695
|
+
limit: { type: "number", description: "Max number of lines to return (optional)." }
|
|
1168
1696
|
},
|
|
1169
|
-
required: ["path"
|
|
1697
|
+
required: ["path"]
|
|
1170
1698
|
},
|
|
1699
|
+
guard: readGuard,
|
|
1171
1700
|
run: async (args) => {
|
|
1172
1701
|
try {
|
|
1173
1702
|
const { abs } = resolveInRoot(String(args.path ?? "."));
|
|
1174
|
-
const
|
|
1175
|
-
await
|
|
1176
|
-
return `
|
|
1703
|
+
const { off, lim } = parseRange(args, 1e5);
|
|
1704
|
+
const { lines, startLine, hasMore, empty } = await readLineWindow(abs, off, lim);
|
|
1705
|
+
if (lines.length === 0) return empty ? "(empty file)" : `(offset ${off} is past the end of the file)`;
|
|
1706
|
+
const numbered = lines.map((line, i) => `${String(startLine + i).padStart(5)} ${line}`).join("\n");
|
|
1707
|
+
const more = hasMore ? `
|
|
1708
|
+
\u2026(more lines; read again with offset ${startLine + lines.length})` : "";
|
|
1709
|
+
return numbered + more;
|
|
1177
1710
|
} catch (err) {
|
|
1178
|
-
return fail("
|
|
1711
|
+
return fail("reading file", err);
|
|
1179
1712
|
}
|
|
1180
1713
|
}
|
|
1181
1714
|
},
|
|
1182
1715
|
{
|
|
1183
|
-
name: "
|
|
1184
|
-
description: "
|
|
1185
|
-
needsApproval: true,
|
|
1186
|
-
guard: writeGuard,
|
|
1187
|
-
// out-of-root OR a secrets file → per-call prompt, never cached
|
|
1716
|
+
name: "show",
|
|
1717
|
+
description: "Display a file's contents or a directory's contents to the USER in a clean view. Use this whenever the user asks to see a file or list a folder \u2014 instead of pasting or describing them in your reply. For a whole/recursive listing or the project structure, pass recursive:true (a tree). It returns only a confirmation; the user sees the rendered view.",
|
|
1188
1718
|
parameters: {
|
|
1189
1719
|
type: "object",
|
|
1190
1720
|
properties: {
|
|
1191
|
-
path: { type: "string", description: "
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
},
|
|
1196
|
-
new_text: { type: "string", description: "The text to replace old_text with." }
|
|
1721
|
+
path: { type: "string", description: "File or directory to show." },
|
|
1722
|
+
recursive: { type: "boolean", description: "For a directory, show the full nested tree (folders + files). Use for a whole/recursive/full listing." },
|
|
1723
|
+
offset: { type: "number", description: "1-based start line (files only, optional)." },
|
|
1724
|
+
limit: { type: "number", description: "Max lines to show (files only, optional; default 80)." }
|
|
1197
1725
|
},
|
|
1198
|
-
required: ["path"
|
|
1726
|
+
required: ["path"]
|
|
1199
1727
|
},
|
|
1728
|
+
guard: readGuard,
|
|
1729
|
+
// Returns a tagged payload (\x01file\x01… / \x01dir\x01…) that the agent loop
|
|
1730
|
+
// renders for the user; the model gets a short note instead (see ui.renderShow).
|
|
1200
1731
|
run: async (args) => {
|
|
1201
1732
|
try {
|
|
1202
1733
|
const { abs } = resolveInRoot(String(args.path ?? "."));
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1734
|
+
const st = await stat(abs);
|
|
1735
|
+
if (st.isDirectory()) {
|
|
1736
|
+
if (args.recursive) {
|
|
1737
|
+
const items = [];
|
|
1738
|
+
await walkTree(abs, "", items, TREE_CAP);
|
|
1739
|
+
return showPayload("tree", { path: String(args.path), items, truncated: items.length >= TREE_CAP });
|
|
1740
|
+
}
|
|
1741
|
+
const entries = sortDirents(await readdir(abs, { withFileTypes: true }));
|
|
1742
|
+
const names = entries.map((e) => e.isDirectory() ? e.name + "/" : e.name);
|
|
1743
|
+
return showPayload("dir", { path: String(args.path), names });
|
|
1744
|
+
}
|
|
1745
|
+
const { off, lim } = parseRange(args, 80);
|
|
1746
|
+
const { lines, startLine, hasMore } = await readLineWindow(abs, off, lim);
|
|
1747
|
+
return showPayload("file", { path: String(args.path), startLine, lines, hasMore });
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
return fail("showing", err);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
},
|
|
1753
|
+
{
|
|
1754
|
+
name: "search",
|
|
1755
|
+
description: "Search for a regular-expression pattern across files in a directory (recursively), returning matching 'path:line: text'. Read-only. Use this to find where a name is defined or used.",
|
|
1756
|
+
parameters: {
|
|
1757
|
+
type: "object",
|
|
1758
|
+
properties: {
|
|
1759
|
+
pattern: { type: "string", description: "Regular expression to search for." },
|
|
1760
|
+
path: { type: "string", description: "Directory to search. Defaults to the current directory." }
|
|
1761
|
+
},
|
|
1762
|
+
required: ["pattern"]
|
|
1763
|
+
},
|
|
1764
|
+
guard: pathGuard,
|
|
1765
|
+
run: async (args) => {
|
|
1766
|
+
if (/\([^()]*[+*{][^()]*\)\s*[+*{]/.test(String(args.pattern ?? ""))) {
|
|
1767
|
+
return `Error: that pattern has nested quantifiers that can hang the search (catastrophic backtracking). Simplify it.`;
|
|
1768
|
+
}
|
|
1769
|
+
let regex;
|
|
1770
|
+
try {
|
|
1771
|
+
regex = new RegExp(args.pattern);
|
|
1772
|
+
} catch {
|
|
1773
|
+
return `Error: invalid regular expression: ${args.pattern}`;
|
|
1774
|
+
}
|
|
1775
|
+
const IGNORE = SKIP_DIRS;
|
|
1776
|
+
const results = [];
|
|
1777
|
+
const MAX = config.searchMaxResults;
|
|
1778
|
+
const deadline = Date.now() + config.searchTimeoutMs;
|
|
1779
|
+
let truncated = false;
|
|
1780
|
+
async function walk(dir) {
|
|
1781
|
+
if (results.length >= MAX) return;
|
|
1782
|
+
if (Date.now() > deadline) {
|
|
1783
|
+
truncated = true;
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
let entries;
|
|
1787
|
+
try {
|
|
1788
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1789
|
+
} catch {
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
for (const e of entries) {
|
|
1793
|
+
if (results.length >= MAX || truncated) return;
|
|
1794
|
+
if (Date.now() > deadline) {
|
|
1795
|
+
truncated = true;
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (IGNORE.has(e.name)) continue;
|
|
1799
|
+
const full = `${dir}/${e.name}`;
|
|
1800
|
+
if (e.isDirectory()) {
|
|
1801
|
+
await walk(full);
|
|
1802
|
+
} else if (e.isFile()) {
|
|
1803
|
+
if (SECRET_FILE.test(e.name)) continue;
|
|
1804
|
+
const info = await stat(full).catch(() => null);
|
|
1805
|
+
if (info && info.size > config.searchMaxFileBytes) continue;
|
|
1806
|
+
let lines;
|
|
1807
|
+
try {
|
|
1808
|
+
lines = (await readFile2(full, "utf8")).split("\n");
|
|
1809
|
+
} catch {
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1813
|
+
if (lines[i].length > 1e4) continue;
|
|
1814
|
+
if (regex.test(lines[i])) {
|
|
1815
|
+
results.push(`${full}:${i + 1}: ${lines[i].trim()}`);
|
|
1816
|
+
if (results.length >= MAX) break;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
await walk(resolveInRoot(String(args.path ?? ".")).abs);
|
|
1823
|
+
if (results.length === 0) return truncated ? `No matches yet \u2014 search stopped at the time budget. Narrow the path or pattern.` : `No matches for "${args.pattern}".`;
|
|
1824
|
+
const note = results.length >= MAX ? `
|
|
1825
|
+
\u2026(showing first ${MAX} matches)` : truncated ? `
|
|
1826
|
+
\u2026(search stopped at the time budget \u2014 results may be incomplete)` : "";
|
|
1827
|
+
return results.join("\n") + note;
|
|
1828
|
+
}
|
|
1829
|
+
},
|
|
1830
|
+
{
|
|
1831
|
+
name: "write_file",
|
|
1832
|
+
description: "Create a NEW file (or fully overwrite an existing one) with the given content. To change PART of an existing file, prefer edit_file instead.",
|
|
1833
|
+
needsApproval: true,
|
|
1834
|
+
guard: writeGuard,
|
|
1835
|
+
// out-of-root OR a secrets file (.env/.npmrc/key…) → per-call prompt, never cached
|
|
1836
|
+
parameters: {
|
|
1837
|
+
type: "object",
|
|
1838
|
+
properties: {
|
|
1839
|
+
path: { type: "string", description: "Path to the file to write." },
|
|
1840
|
+
content: { type: "string", description: "The full text to write into the file." }
|
|
1841
|
+
},
|
|
1842
|
+
required: ["path", "content"]
|
|
1843
|
+
},
|
|
1844
|
+
run: async (args) => {
|
|
1845
|
+
try {
|
|
1846
|
+
const { abs } = resolveInRoot(String(args.path ?? "."));
|
|
1847
|
+
const content = String(args.content ?? "");
|
|
1848
|
+
await atomicWrite(abs, content);
|
|
1849
|
+
return `Wrote ${content.length} characters to ${args.path}`;
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
return fail("writing file", err);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
},
|
|
1855
|
+
{
|
|
1856
|
+
name: "edit_file",
|
|
1857
|
+
description: "Make a precise edit to an EXISTING file: replace an exact snippet (old_text) with new_text. old_text must match the file exactly (including whitespace) and appear EXACTLY ONCE. Use the file's RAW text \u2014 do NOT include the line-number prefixes that read_file shows. Prefer this over write_file when changing existing files.",
|
|
1858
|
+
needsApproval: true,
|
|
1859
|
+
guard: writeGuard,
|
|
1860
|
+
// out-of-root OR a secrets file → per-call prompt, never cached
|
|
1861
|
+
parameters: {
|
|
1862
|
+
type: "object",
|
|
1863
|
+
properties: {
|
|
1864
|
+
path: { type: "string", description: "Path to the file to edit." },
|
|
1865
|
+
old_text: {
|
|
1866
|
+
type: "string",
|
|
1867
|
+
description: "The exact text to find and replace. Must match the file exactly and appear once. Include enough surrounding lines to make it unique."
|
|
1868
|
+
},
|
|
1869
|
+
new_text: { type: "string", description: "The text to replace old_text with." }
|
|
1870
|
+
},
|
|
1871
|
+
required: ["path", "old_text", "new_text"]
|
|
1872
|
+
},
|
|
1873
|
+
run: async (args) => {
|
|
1874
|
+
try {
|
|
1875
|
+
const { abs } = resolveInRoot(String(args.path ?? "."));
|
|
1876
|
+
const original = await readFile2(abs, "utf8");
|
|
1877
|
+
const res = resolveEdit(original, String(args.old_text ?? ""), String(args.new_text ?? ""));
|
|
1878
|
+
if (!res.ok) {
|
|
1879
|
+
if (res.reason === "ambiguous") {
|
|
1880
|
+
return `Error: old_text matches ${res.count} places in ${args.path}. Include more surrounding context so it matches exactly once.`;
|
|
1881
|
+
}
|
|
1882
|
+
return `Error: old_text not found in ${args.path}. ` + (res.closest ? `The closest matching text in the file is (copy it EXACTLY, without the line-number prefix):
|
|
1883
|
+
${res.closest}` : `Re-read the file and copy the exact text (including whitespace/indentation).`);
|
|
1884
|
+
}
|
|
1885
|
+
if (original.slice(res.start, res.end) === res.after) {
|
|
1886
|
+
return `Error: old_text and new_text are identical in ${args.path} \u2014 nothing to change.`;
|
|
1887
|
+
}
|
|
1888
|
+
await atomicWrite(abs, original.slice(0, res.start) + res.after + original.slice(res.end));
|
|
1889
|
+
const healed = res.healedVia === "prefix" ? " (auto-healed: stripped read_file line-number prefixes)" : res.healedVia === "whitespace" ? " (auto-healed: normalized whitespace/indentation)" : "";
|
|
1890
|
+
return `Edited ${args.path} \u2014 replaced 1 occurrence.${healed}`;
|
|
1891
|
+
} catch (err) {
|
|
1892
|
+
return fail("editing file", err);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
},
|
|
1218
1896
|
{
|
|
1219
1897
|
name: "list_dir",
|
|
1220
1898
|
description: "List the files and folders in a directory. Use this to list or COUNT files \u2014 do NOT shell out to run_bash/ls for that.",
|
|
@@ -1249,7 +1927,8 @@ var toolDefs = [
|
|
|
1249
1927
|
type: "object",
|
|
1250
1928
|
properties: {
|
|
1251
1929
|
command: { type: "string", description: "The shell command to run." },
|
|
1252
|
-
explanation: { type: "string", description: "One sentence: WHAT this command does and WHY you need it now. Shown to the user before they approve." }
|
|
1930
|
+
explanation: { type: "string", description: "One sentence: WHAT this command does and WHY you need it now. Shown to the user before they approve." },
|
|
1931
|
+
background: { type: "boolean", description: "Run detached in the background (dev servers, watchers, long tasks). Returns a task id immediately; poll it with check_task and stop it with stop_task. Do NOT use for commands whose output you need right now." }
|
|
1253
1932
|
},
|
|
1254
1933
|
required: ["command", "explanation"]
|
|
1255
1934
|
},
|
|
@@ -1258,6 +1937,10 @@ var toolDefs = [
|
|
|
1258
1937
|
if (danger) {
|
|
1259
1938
|
return `Error: refused \u2014 the command matches a known-catastrophic pattern (${danger}). If this is genuinely intended, the user must run it manually.`;
|
|
1260
1939
|
}
|
|
1940
|
+
if (args.background) {
|
|
1941
|
+
const { id, error } = startTask(String(args.command));
|
|
1942
|
+
return error ? `Error: ${error}` : `Started background task ${id} \u2014 running detached. Poll new output with check_task("${id}"), stop it with stop_task("${id}"). Stop it once you no longer need it.`;
|
|
1943
|
+
}
|
|
1261
1944
|
try {
|
|
1262
1945
|
const { stdout, stderr } = await runShell(args.command, { timeout: config.execTimeoutMs, maxBuffer: config.maxToolBuffer, signal });
|
|
1263
1946
|
return (stdout || "") + (stderr ? `
|
|
@@ -1407,6 +2090,77 @@ ${list}`;
|
|
|
1407
2090
|
return fail("saving memory", err);
|
|
1408
2091
|
}
|
|
1409
2092
|
}
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
name: "check_task",
|
|
2096
|
+
description: "Read status + new output from a background task started by run_bash (background:true). Returns only output produced since your last check. Use to see if a dev server started, a build finished, etc.",
|
|
2097
|
+
parameters: {
|
|
2098
|
+
type: "object",
|
|
2099
|
+
properties: { task_id: { type: "string", description: "The bg_\u2026 id returned by run_bash." } },
|
|
2100
|
+
required: ["task_id"]
|
|
2101
|
+
},
|
|
2102
|
+
run: async (args) => checkTask(String(args.task_id ?? ""))
|
|
2103
|
+
},
|
|
2104
|
+
{
|
|
2105
|
+
name: "stop_task",
|
|
2106
|
+
description: "Stop (kill) a background task started by run_bash (background:true). Stop tasks you no longer need.",
|
|
2107
|
+
parameters: {
|
|
2108
|
+
type: "object",
|
|
2109
|
+
properties: { task_id: { type: "string", description: "The bg_\u2026 id to stop." } },
|
|
2110
|
+
required: ["task_id"]
|
|
2111
|
+
},
|
|
2112
|
+
run: async (args) => stopTask(String(args.task_id ?? ""))
|
|
2113
|
+
},
|
|
2114
|
+
{
|
|
2115
|
+
name: "explore",
|
|
2116
|
+
description: "Delegate a focused, READ-ONLY investigation to a sub-agent. It explores on its own (reading, searching, listing, and browsing the web) and returns a concise written summary \u2014 so open-ended questions ('how does X work', 'where is Y handled', 'trace Z', 'research library W') get answered in a SEPARATE context, keeping yours clean. It cannot modify anything, run commands, or ask questions.",
|
|
2117
|
+
parameters: {
|
|
2118
|
+
type: "object",
|
|
2119
|
+
properties: {
|
|
2120
|
+
task: { type: "string", description: "What to find out \u2014 one clear, self-contained question." },
|
|
2121
|
+
focus: { type: "string", description: "Optional starting point: files, directories, symbols, or a URL to look at first." }
|
|
2122
|
+
},
|
|
2123
|
+
required: ["task"]
|
|
2124
|
+
},
|
|
2125
|
+
run: async (args, signal) => {
|
|
2126
|
+
const task = String(args.task ?? "").trim();
|
|
2127
|
+
if (!task) return 'Error: explore needs a non-empty "task".';
|
|
2128
|
+
return runExplorer(task, args.focus ? String(args.focus) : void 0, signal);
|
|
2129
|
+
}
|
|
2130
|
+
},
|
|
2131
|
+
{
|
|
2132
|
+
name: "ask_user",
|
|
2133
|
+
description: "Ask the user to choose between concrete options when the task is genuinely ambiguous or has several valid approaches with different outcomes AND you can't pick a sensible default. Provide 2\u20134 clear options. Use SPARINGLY \u2014 for low-stakes choices, just proceed with a reasonable default.",
|
|
2134
|
+
parameters: {
|
|
2135
|
+
type: "object",
|
|
2136
|
+
properties: {
|
|
2137
|
+
question: { type: "string", description: "One specific question to ask." },
|
|
2138
|
+
options: {
|
|
2139
|
+
type: "array",
|
|
2140
|
+
description: "2\u20134 concrete choices.",
|
|
2141
|
+
items: {
|
|
2142
|
+
type: "object",
|
|
2143
|
+
properties: {
|
|
2144
|
+
label: { type: "string", description: "Short label for the choice." },
|
|
2145
|
+
description: { type: "string", description: "Optional one-line explanation of this choice." }
|
|
2146
|
+
},
|
|
2147
|
+
required: ["label"]
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
},
|
|
2151
|
+
required: ["question", "options"]
|
|
2152
|
+
},
|
|
2153
|
+
// The turn loop intercepts ask_user to show beecork's native picker on a TTY (tools don't get
|
|
2154
|
+
// the keyboard). Reaching run() means no interactive session (headless / a direct call), so we
|
|
2155
|
+
// just validate and tell the model to proceed on its own.
|
|
2156
|
+
run: async (args) => {
|
|
2157
|
+
const options = Array.isArray(args.options) ? args.options : [];
|
|
2158
|
+
const question = String(args.question ?? "").trim();
|
|
2159
|
+
if (!question || options.length === 0) {
|
|
2160
|
+
return `Error: ask_user needs a "question" and a non-empty "options" array (2\u20134 concrete choices, each with a label).`;
|
|
2161
|
+
}
|
|
2162
|
+
return `No interactive user is available (headless run). Proceed with the most reasonable option for "${question}" and state the assumption you made.`;
|
|
2163
|
+
}
|
|
1410
2164
|
}
|
|
1411
2165
|
];
|
|
1412
2166
|
var TOOLS = toolDefs.map((t) => ({
|
|
@@ -1414,8 +2168,8 @@ var TOOLS = toolDefs.map((t) => ({
|
|
|
1414
2168
|
function: { name: t.name, description: t.description, parameters: t.parameters }
|
|
1415
2169
|
}));
|
|
1416
2170
|
var toolsByName = new Map(toolDefs.map((t) => [t.name, t]));
|
|
1417
|
-
async function runTool(call, signal) {
|
|
1418
|
-
const tool =
|
|
2171
|
+
async function runTool(call, signal, byName = toolsByName) {
|
|
2172
|
+
const tool = byName.get(call.function.name);
|
|
1419
2173
|
if (!tool) return `Error: unknown tool "${call.function.name}".`;
|
|
1420
2174
|
let args;
|
|
1421
2175
|
try {
|
|
@@ -1489,19 +2243,52 @@ function parseSSELine(line) {
|
|
|
1489
2243
|
const out2 = {};
|
|
1490
2244
|
if (delta.content) out2.content = delta.content;
|
|
1491
2245
|
if (delta.tool_calls) out2.toolCalls = delta.tool_calls;
|
|
2246
|
+
if (typeof delta.reasoning === "string" && delta.reasoning) out2.reasoning = delta.reasoning;
|
|
2247
|
+
if (Array.isArray(delta.reasoning_details) && delta.reasoning_details.length) out2.reasoningDetails = delta.reasoning_details;
|
|
1492
2248
|
return out2;
|
|
1493
2249
|
}
|
|
1494
|
-
|
|
1495
|
-
const
|
|
2250
|
+
function buildRequestBody(opts) {
|
|
2251
|
+
const { model, messages, includeTools, effort, reasoningSupported, extra, tools } = opts;
|
|
2252
|
+
const body = { ...extra };
|
|
2253
|
+
body.model = model;
|
|
2254
|
+
body.messages = messages;
|
|
2255
|
+
body.stream = true;
|
|
2256
|
+
if (includeTools) body.tools = tools ?? TOOLS;
|
|
2257
|
+
if (reasoningSupported && !("reasoning" in extra)) {
|
|
2258
|
+
body.reasoning = effort === "off" ? { enabled: false } : { effort };
|
|
2259
|
+
}
|
|
2260
|
+
return body;
|
|
2261
|
+
}
|
|
2262
|
+
function pruneReasoningForSend(messages) {
|
|
2263
|
+
let lastUser = -1;
|
|
2264
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2265
|
+
if (messages[i].role === "user") {
|
|
2266
|
+
lastUser = i;
|
|
2267
|
+
break;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
return messages.map((m, i) => {
|
|
2271
|
+
if (i > lastUser || m.reasoning === void 0 && m.reasoning_details === void 0) return m;
|
|
2272
|
+
const { reasoning, reasoning_details, ...rest } = m;
|
|
2273
|
+
return rest;
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
async function callModel(messages, includeTools = true, signal, opts) {
|
|
2277
|
+
const quiet = opts?.quiet ?? false;
|
|
2278
|
+
const body = buildRequestBody({
|
|
1496
2279
|
model: state.model,
|
|
1497
|
-
messages,
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
2280
|
+
messages: pruneReasoningForSend(messages),
|
|
2281
|
+
includeTools,
|
|
2282
|
+
effort: state.reasoningEffort,
|
|
2283
|
+
reasoningSupported: shouldSendReasoning(state.model),
|
|
2284
|
+
extra: config.openRouterExtra,
|
|
2285
|
+
tools: opts?.tools
|
|
2286
|
+
});
|
|
1501
2287
|
const tries = config.retryAttempts;
|
|
1502
2288
|
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
1503
2289
|
const sig = signal ? AbortSignal.any([signal, AbortSignal.timeout(config.apiTimeoutMs)]) : AbortSignal.timeout(config.apiTimeoutMs);
|
|
1504
|
-
const stopSpinner =
|
|
2290
|
+
const stopSpinner = quiet ? () => {
|
|
2291
|
+
} : startSpinner("thinking\u2026");
|
|
1505
2292
|
let response;
|
|
1506
2293
|
try {
|
|
1507
2294
|
response = await openRouterChat(body, sig);
|
|
@@ -1528,13 +2315,37 @@ async function callModel(messages, includeTools = true, signal) {
|
|
|
1528
2315
|
}
|
|
1529
2316
|
let content = "";
|
|
1530
2317
|
const toolCalls = [];
|
|
2318
|
+
let reasoning = "";
|
|
2319
|
+
const reasoningDetails = [];
|
|
2320
|
+
let printedReasoning = false;
|
|
1531
2321
|
let printedText = false;
|
|
1532
2322
|
let streamBroke = false;
|
|
1533
2323
|
let streamError = null;
|
|
1534
2324
|
let streamErrorCode;
|
|
1535
2325
|
const decoder = new TextDecoder();
|
|
1536
2326
|
let buffer = "";
|
|
1537
|
-
const md = process.stdout.isTTY ? createMarkdownStream((s) => process.stdout.write(s)) : null;
|
|
2327
|
+
const md = process.stdout.isTTY && !quiet ? createMarkdownStream((s) => process.stdout.write(s)) : null;
|
|
2328
|
+
const displayThinking = (s) => {
|
|
2329
|
+
if (quiet || !process.stdout.isTTY || !s) return;
|
|
2330
|
+
stopSpinner();
|
|
2331
|
+
if (!printedReasoning) {
|
|
2332
|
+
process.stdout.write("\n" + color.dim("thinking: "));
|
|
2333
|
+
printedReasoning = true;
|
|
2334
|
+
}
|
|
2335
|
+
process.stdout.write(color.dim(stripControl(s)));
|
|
2336
|
+
};
|
|
2337
|
+
const mergeDetail = (d) => {
|
|
2338
|
+
const i = typeof d.index === "number" ? d.index : reasoningDetails.length ? reasoningDetails.length - 1 : 0;
|
|
2339
|
+
const slot = reasoningDetails[i] ??= {};
|
|
2340
|
+
if (d.type) slot.type = d.type;
|
|
2341
|
+
if (d.format) slot.format = d.format;
|
|
2342
|
+
if (d.id) slot.id = d.id;
|
|
2343
|
+
if (d.signature) slot.signature = d.signature;
|
|
2344
|
+
if (typeof d.index === "number") slot.index = d.index;
|
|
2345
|
+
if (typeof d.text === "string") slot.text = (slot.text ?? "") + d.text;
|
|
2346
|
+
if (typeof d.summary === "string") slot.summary = (slot.summary ?? "") + d.summary;
|
|
2347
|
+
if (typeof d.data === "string") slot.data = (slot.data ?? "") + d.data;
|
|
2348
|
+
};
|
|
1538
2349
|
const handleLine = (line) => {
|
|
1539
2350
|
const ev = parseSSELine(line);
|
|
1540
2351
|
if (!ev) return;
|
|
@@ -1543,15 +2354,27 @@ async function callModel(messages, includeTools = true, signal) {
|
|
|
1543
2354
|
streamErrorCode = ev.errorCode;
|
|
1544
2355
|
return;
|
|
1545
2356
|
}
|
|
2357
|
+
if (ev.reasoning) {
|
|
2358
|
+
reasoning += ev.reasoning;
|
|
2359
|
+
displayThinking(ev.reasoning);
|
|
2360
|
+
}
|
|
2361
|
+
if (ev.reasoningDetails) {
|
|
2362
|
+
for (const d of ev.reasoningDetails) {
|
|
2363
|
+
mergeDetail(d);
|
|
2364
|
+
if (!ev.reasoning) displayThinking(typeof d.text === "string" ? d.text : typeof d.summary === "string" ? d.summary : "");
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
1546
2367
|
if (ev.content) {
|
|
1547
2368
|
stopSpinner();
|
|
1548
|
-
if (!
|
|
1549
|
-
|
|
1550
|
-
|
|
2369
|
+
if (!quiet) {
|
|
2370
|
+
if (!printedText) {
|
|
2371
|
+
process.stdout.write("\n" + color.cyan("bee: "));
|
|
2372
|
+
printedText = true;
|
|
2373
|
+
}
|
|
2374
|
+
const safe = stripControl(ev.content);
|
|
2375
|
+
if (md) md.push(safe);
|
|
2376
|
+
else process.stdout.write(safe);
|
|
1551
2377
|
}
|
|
1552
|
-
const safe = stripControl(ev.content);
|
|
1553
|
-
if (md) md.push(safe);
|
|
1554
|
-
else process.stdout.write(safe);
|
|
1555
2378
|
content += ev.content;
|
|
1556
2379
|
}
|
|
1557
2380
|
if (ev.toolCalls) {
|
|
@@ -1590,6 +2413,8 @@ async function callModel(messages, includeTools = true, signal) {
|
|
|
1590
2413
|
md.end();
|
|
1591
2414
|
process.stdout.write("\n");
|
|
1592
2415
|
} else process.stdout.write("\n\n");
|
|
2416
|
+
} else if (printedReasoning) {
|
|
2417
|
+
process.stdout.write("\n");
|
|
1593
2418
|
}
|
|
1594
2419
|
if (streamError) {
|
|
1595
2420
|
const transient = streamErrorCode !== void 0 && Number.isFinite(streamErrorCode) && isTransientStatus(streamErrorCode) || /rate.?limit|overloaded|temporar|timeout|try again|\b(429|500|502|503|504)\b/i.test(streamError);
|
|
@@ -1609,6 +2434,9 @@ async function callModel(messages, includeTools = true, signal) {
|
|
|
1609
2434
|
const message = { role: "assistant", content: content || null };
|
|
1610
2435
|
const calls = toolCalls.filter(Boolean).map((c, i) => c.id ? c : { ...c, id: `call_${i}` });
|
|
1611
2436
|
if (calls.length > 0) message.tool_calls = calls;
|
|
2437
|
+
if (reasoning) message.reasoning = reasoning;
|
|
2438
|
+
const details = reasoningDetails.filter(Boolean);
|
|
2439
|
+
if (details.length > 0) message.reasoning_details = details;
|
|
1612
2440
|
return message;
|
|
1613
2441
|
}
|
|
1614
2442
|
return { role: "assistant", content: null };
|
|
@@ -1637,7 +2465,7 @@ async function summarize(old, signal) {
|
|
|
1637
2465
|
messages: [
|
|
1638
2466
|
{
|
|
1639
2467
|
role: "system",
|
|
1640
|
-
content: "You
|
|
2468
|
+
content: "You are compacting a long coding session to fit the context window. Summarize the transcript below into structured notes the assistant can continue from WITHOUT losing important context. Use exactly these headings:\n- Goal: what the user ultimately wants (and any explicit instructions/preferences).\n- Done: key steps taken, decisions made, and files created or edited \u2014 keep the essential code/exact changes.\n- Facts: important things discovered about the codebase (structure, conventions, file contents that matter).\n- Errors & fixes: problems hit and how they were resolved, plus any user corrections.\n- Pending: what still remains to do.\nBe concise but specific \u2014 keep names, signatures, and paths; omit chit-chat."
|
|
1641
2469
|
},
|
|
1642
2470
|
{ role: "user", content: transcript(old) }
|
|
1643
2471
|
]
|
|
@@ -1755,18 +2583,20 @@ async function readJsonFile(path) {
|
|
|
1755
2583
|
async function loadSettings() {
|
|
1756
2584
|
const paths = beecorkPaths("settings.json");
|
|
1757
2585
|
let model;
|
|
2586
|
+
let reasoningEffort;
|
|
1758
2587
|
let alwaysAllow = [];
|
|
1759
2588
|
let projectAlwaysAllowIgnored = false;
|
|
1760
2589
|
for (let i = 0; i < paths.length; i++) {
|
|
1761
2590
|
const parsed = await readJsonFile(paths[i]);
|
|
1762
2591
|
if (!parsed) continue;
|
|
1763
2592
|
if (typeof parsed.model === "string") model = parsed.model;
|
|
2593
|
+
if (typeof parsed.reasoningEffort === "string") reasoningEffort = normalizeEffort(parsed.reasoningEffort) ?? reasoningEffort;
|
|
1764
2594
|
if (Array.isArray(parsed.alwaysAllow)) {
|
|
1765
2595
|
if (i === 0) alwaysAllow = parsed.alwaysAllow.map(String);
|
|
1766
2596
|
else projectAlwaysAllowIgnored = true;
|
|
1767
2597
|
}
|
|
1768
2598
|
}
|
|
1769
|
-
return { model, alwaysAllow, projectAlwaysAllowIgnored };
|
|
2599
|
+
return { model, reasoningEffort, alwaysAllow, projectAlwaysAllowIgnored };
|
|
1770
2600
|
}
|
|
1771
2601
|
function userConfigPath() {
|
|
1772
2602
|
return join3(homedir4(), BEECORK, "config.json");
|
|
@@ -1793,6 +2623,15 @@ async function saveModelPreference(model) {
|
|
|
1793
2623
|
} catch {
|
|
1794
2624
|
}
|
|
1795
2625
|
}
|
|
2626
|
+
async function saveReasoningPreference(reasoningEffort) {
|
|
2627
|
+
try {
|
|
2628
|
+
const file = join3(homedir4(), BEECORK, "settings.json");
|
|
2629
|
+
await mkdir3(dirname2(file), { recursive: true });
|
|
2630
|
+
const current = await readJsonFile(file) ?? {};
|
|
2631
|
+
await writeFile3(file, JSON.stringify({ ...current, reasoningEffort }, null, 2), "utf8");
|
|
2632
|
+
} catch {
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
1796
2635
|
var sessionsDir = () => join3(process.cwd(), BEECORK, "sessions");
|
|
1797
2636
|
async function saveSession(messages) {
|
|
1798
2637
|
try {
|
|
@@ -1894,13 +2733,10 @@ async function addProjectApproval(tool) {
|
|
|
1894
2733
|
// src/agent.ts
|
|
1895
2734
|
var SYSTEM_PROMPT = `You are beecork, a coding assistant working in a terminal on the user's machine.
|
|
1896
2735
|
|
|
1897
|
-
Environment:
|
|
1898
|
-
- Working directory: ${process.cwd()}
|
|
1899
|
-
- Platform: ${process.platform}
|
|
1900
|
-
|
|
1901
2736
|
# How to work
|
|
1902
2737
|
- For multi-step tasks, call \`update_todos\` to write a short plan, then work through it \u2014 mark each item in_progress when you start it and completed when done. Keep the list current.
|
|
1903
2738
|
- Keep going until the task is FULLY complete. Don't stop after one step or hand back a half-finished task to ask what's next \u2014 unless you are genuinely blocked or need a real decision from the user.
|
|
2739
|
+
- When you genuinely need a decision (an ambiguous request, or several valid approaches with different outcomes) and can't pick a sensible default, call \`ask_user\` with 2\u20134 concrete options instead of guessing. Use it SPARINGLY \u2014 for low-stakes choices, just proceed with a reasonable default.
|
|
1904
2740
|
- Work in small steps and VERIFY: after changes, run the relevant test/build/command. An automatic check may also run after each edit \u2014 if it reports FAILED, fix the problem before continuing. Read the output and fix anything that broke.
|
|
1905
2741
|
- Use your tools to find facts instead of guessing.
|
|
1906
2742
|
- When the user shares a durable preference, project convention, or fact worth keeping across sessions, call \`remember\` to save it.
|
|
@@ -1960,7 +2796,33 @@ function decideApproval(tool, args, ctx) {
|
|
|
1960
2796
|
}
|
|
1961
2797
|
return { action: "run" };
|
|
1962
2798
|
}
|
|
1963
|
-
var
|
|
2799
|
+
var hasContent2 = (m) => Boolean(m.content) || (m.tool_calls?.length ?? 0) > 0;
|
|
2800
|
+
function askUserMessage(question, choice, interactive) {
|
|
2801
|
+
if (!interactive) {
|
|
2802
|
+
return `No interactive user is available (headless run). Proceed with the most reasonable option for "${question}" and state the assumption you made.`;
|
|
2803
|
+
}
|
|
2804
|
+
if (!choice) {
|
|
2805
|
+
return `The user dismissed "${question}" without choosing. Use your best judgment to proceed, or ask again only if you are truly blocked.`;
|
|
2806
|
+
}
|
|
2807
|
+
return `The user selected: "${choice.label}"${choice.description ? ` \u2014 ${choice.description}` : ""}. Continue with this choice.`;
|
|
2808
|
+
}
|
|
2809
|
+
async function handleAskUser(args) {
|
|
2810
|
+
const question = String(args.question ?? "").trim();
|
|
2811
|
+
const raw = Array.isArray(args.options) ? args.options : [];
|
|
2812
|
+
const options = raw.map((o) => ({ label: String(o?.label ?? "").trim(), description: o?.description ? String(o.description) : "" })).filter((o) => o.label);
|
|
2813
|
+
if (!question || options.length === 0) {
|
|
2814
|
+
return `Error: ask_user needs a "question" and a non-empty "options" array (2\u20134 concrete choices, each with a label).`;
|
|
2815
|
+
}
|
|
2816
|
+
if (!process.stdin.isTTY) return askUserMessage(question, null, false);
|
|
2817
|
+
console.log();
|
|
2818
|
+
const choice = await selectMenu({
|
|
2819
|
+
title: stripControl(question),
|
|
2820
|
+
// question is model-supplied — never let it carry raw escapes
|
|
2821
|
+
items: options.map((o) => ({ label: stripControl(o.label), value: o, hint: o.description ? stripControl(o.description) : void 0 }))
|
|
2822
|
+
});
|
|
2823
|
+
if (choice) console.log(color.green(` \u2192 ${stripControl(choice.label)}`) + "\n");
|
|
2824
|
+
return askUserMessage(question, choice, true);
|
|
2825
|
+
}
|
|
1964
2826
|
async function handleToolCall(call, messages, step, deps) {
|
|
1965
2827
|
const { approvedTools, callCounts, ask, signal } = deps;
|
|
1966
2828
|
const pushTool = (content) => messages.push({ role: "tool", tool_call_id: call.id, content });
|
|
@@ -1978,6 +2840,11 @@ async function handleToolCall(call, messages, step, deps) {
|
|
|
1978
2840
|
callArgs = JSON.parse(call.function.arguments);
|
|
1979
2841
|
} catch {
|
|
1980
2842
|
}
|
|
2843
|
+
if (call.function.name === "ask_user") {
|
|
2844
|
+
if (config.traceFile) trace.push({ tool: call.function.name, args: call.function.arguments, step });
|
|
2845
|
+
pushTool(await handleAskUser(callArgs));
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
1981
2848
|
const decision = decideApproval(tool, callArgs, {
|
|
1982
2849
|
mode: state.mode,
|
|
1983
2850
|
autoApprove: config.autoApprove,
|
|
@@ -1999,484 +2866,221 @@ async function handleToolCall(call, messages, step, deps) {
|
|
|
1999
2866
|
if (answer === "deny") {
|
|
2000
2867
|
console.log(color.red(" \u21B3 denied") + "\n");
|
|
2001
2868
|
pushTool(decision.cacheable ? "The user DENIED permission to run this tool. Do not retry it." : `The user DENIED this (${decision.reason}). Do not retry, and do not route around it with run_bash/cat or another tool.`);
|
|
2002
|
-
return;
|
|
2003
|
-
}
|
|
2004
|
-
if (decision.cacheable && answer === "always") {
|
|
2005
|
-
approvedTools.add(call.function.name);
|
|
2006
|
-
void addProjectApproval(call.function.name);
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
if (config.traceFile) trace.push({ tool: call.function.name, args: call.function.arguments, step });
|
|
2010
|
-
const isTodo = call.function.name === "update_todos";
|
|
2011
|
-
const isShow = call.function.name === "show";
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
${
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
} else {
|
|
2076
|
-
answered = true;
|
|
2077
|
-
}
|
|
2078
|
-
}
|
|
2079
|
-
if (signal?.aborted) {
|
|
2080
|
-
console.log(color.dim("\n[cancelled]") + "\n");
|
|
2081
|
-
return snapshot;
|
|
2082
|
-
}
|
|
2083
|
-
if (!answered) {
|
|
2084
|
-
console.log(color.dim(`
|
|
2085
|
-
[reached the ${config.maxSteps}-step limit \u2014 wrapping up]`));
|
|
2086
|
-
const wrapPrompt = {
|
|
2087
|
-
role: "system",
|
|
2088
|
-
content: `You have reached the ${config.maxSteps}-step limit for this turn. Do not call any more tools. Briefly tell the user what you accomplished and what still remains.`
|
|
2089
|
-
};
|
|
2090
|
-
const wrap = await callModel([...messages, wrapPrompt], false, signal);
|
|
2091
|
-
if (hasContent(wrap)) messages.push(wrap);
|
|
2092
|
-
}
|
|
2093
|
-
return messages;
|
|
2094
|
-
} catch (err) {
|
|
2095
|
-
if (signal?.aborted || err?.name === "AbortError") {
|
|
2096
|
-
console.log(color.dim("\n[cancelled]") + "\n");
|
|
2097
|
-
return snapshot;
|
|
2098
|
-
}
|
|
2099
|
-
console.error(color.red(`
|
|
2100
|
-
[error] ${err.message}`) + "\n");
|
|
2101
|
-
return snapshot;
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
// src/commands.ts
|
|
2106
|
-
import { writeFile as writeFile4, mkdir as mkdir4, chmod as chmod3 } from "node:fs/promises";
|
|
2107
|
-
|
|
2108
|
-
// src/skills.ts
|
|
2109
|
-
import { readdir as readdir3, readFile as readFile5 } from "node:fs/promises";
|
|
2110
|
-
import { join as join4 } from "node:path";
|
|
2111
|
-
import { homedir as homedir5 } from "node:os";
|
|
2112
|
-
var registry = /* @__PURE__ */ new Map();
|
|
2113
|
-
function skillNames() {
|
|
2114
|
-
return [...registry.keys()];
|
|
2115
|
-
}
|
|
2116
|
-
function getSkill(name) {
|
|
2117
|
-
return registry.get(name);
|
|
2118
|
-
}
|
|
2119
|
-
async function loadSkills() {
|
|
2120
|
-
registry.clear();
|
|
2121
|
-
const dirs = [
|
|
2122
|
-
[join4(homedir5(), ".beecork", "skills"), "global"],
|
|
2123
|
-
[join4(process.cwd(), ".beecork", "skills"), "project"]
|
|
2124
|
-
];
|
|
2125
|
-
for (const [dir, source] of dirs) {
|
|
2126
|
-
let entries;
|
|
2127
|
-
try {
|
|
2128
|
-
entries = await readdir3(dir, { withFileTypes: true });
|
|
2129
|
-
} catch {
|
|
2130
|
-
continue;
|
|
2131
|
-
}
|
|
2132
|
-
for (const e of entries) {
|
|
2133
|
-
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
2134
|
-
const name = e.name.slice(0, -3);
|
|
2135
|
-
if (!/^[a-z0-9][a-z0-9_-]*$/i.test(name)) continue;
|
|
2136
|
-
try {
|
|
2137
|
-
const content = (await readFile5(join4(dir, e.name), "utf8")).trim();
|
|
2138
|
-
if (!content) continue;
|
|
2139
|
-
if (source === "project" && registry.has(name)) {
|
|
2140
|
-
console.error(color.yellow(`\u26A0 project skill /${name} ignored \u2014 a global skill of that name takes precedence`));
|
|
2141
|
-
continue;
|
|
2142
|
-
}
|
|
2143
|
-
registry.set(name, { name, content, source });
|
|
2144
|
-
} catch {
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
return [...registry.values()];
|
|
2149
|
-
}
|
|
2150
|
-
function expandSkill(skill, extra) {
|
|
2151
|
-
return skill.content.includes("$ARGUMENTS") ? skill.content.replaceAll("$ARGUMENTS", extra) : skill.content + (extra ? `
|
|
2152
|
-
|
|
2153
|
-
${extra}` : "");
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
// src/input.ts
|
|
2157
|
-
import { emitKeypressEvents } from "node:readline";
|
|
2158
|
-
var out = (s) => process.stdout.write(s);
|
|
2159
|
-
var active = null;
|
|
2160
|
-
var started = false;
|
|
2161
|
-
function initInput() {
|
|
2162
|
-
if (started || !process.stdin.isTTY) return;
|
|
2163
|
-
started = true;
|
|
2164
|
-
process.stdin.setRawMode(true);
|
|
2165
|
-
out("\x1B[?2004l");
|
|
2166
|
-
emitKeypressEvents(process.stdin);
|
|
2167
|
-
process.stdin.on("keypress", (str, key) => active?.(str, key));
|
|
2168
|
-
}
|
|
2169
|
-
function teardownInput() {
|
|
2170
|
-
if (started && process.stdin.isTTY) {
|
|
2171
|
-
out("\x1B[?2004h\x1B[?25h");
|
|
2172
|
-
process.stdin.setRawMode(false);
|
|
2173
|
-
process.stdin.pause();
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
function pushKeyHandler(h) {
|
|
2177
|
-
const prev = active;
|
|
2178
|
-
active = h;
|
|
2179
|
-
return () => {
|
|
2180
|
-
active = prev;
|
|
2181
|
-
};
|
|
2182
|
-
}
|
|
2183
|
-
var isEnter = (k) => k?.name === "return" || k?.name === "enter";
|
|
2184
|
-
var MENU_MAX = 8;
|
|
2185
|
-
function readPrompt(opts) {
|
|
2186
|
-
if (!process.stdin.isTTY) return Promise.resolve({ type: "eof" });
|
|
2187
|
-
const history = opts.history ?? [];
|
|
2188
|
-
const all = [
|
|
2189
|
-
...opts.commands ?? [],
|
|
2190
|
-
...(opts.skills ?? []).map((n) => ({ name: "/" + n, desc: "skill" }))
|
|
2191
|
-
];
|
|
2192
|
-
return new Promise((resolve2) => {
|
|
2193
|
-
let buf = "";
|
|
2194
|
-
let cur = 0;
|
|
2195
|
-
let hist = history.length;
|
|
2196
|
-
let sel = 0;
|
|
2197
|
-
let menuHidden = false;
|
|
2198
|
-
const BURST_IDLE_MS = 8;
|
|
2199
|
-
let burstLen = 0;
|
|
2200
|
-
let burstTimer = null;
|
|
2201
|
-
let pendingRender = false;
|
|
2202
|
-
const matches = () => {
|
|
2203
|
-
if (opts.mask) return [];
|
|
2204
|
-
const m = buf.match(/^\/(\S*)$/);
|
|
2205
|
-
if (!m) return [];
|
|
2206
|
-
const pre = "/" + m[1];
|
|
2207
|
-
return all.filter((c) => c.name.startsWith(pre)).slice(0, MENU_MAX);
|
|
2208
|
-
};
|
|
2209
|
-
const menu = () => menuHidden ? [] : matches();
|
|
2210
|
-
const highlight = () => {
|
|
2211
|
-
if (opts.mask) return "*".repeat(buf.length);
|
|
2212
|
-
const m = buf.match(/^(\/\S*)([\s\S]*)$/);
|
|
2213
|
-
if (!m) return buf;
|
|
2214
|
-
const [, token, rest] = m;
|
|
2215
|
-
const known = all.some((c) => c.name === token);
|
|
2216
|
-
const partial = all.some((c) => c.name.startsWith(token));
|
|
2217
|
-
const styled = known ? color.green(token) : partial ? color.cyan(token) : color.red(token);
|
|
2218
|
-
return styled + rest;
|
|
2219
|
-
};
|
|
2220
|
-
let lastCurRow = 0;
|
|
2221
|
-
const rowColOf = (s) => ({ row: (s.match(/\n/g) || []).length, col: s.length - (s.lastIndexOf("\n") + 1) });
|
|
2222
|
-
function drawBlock() {
|
|
2223
|
-
const prompt = opts.promptString();
|
|
2224
|
-
const promptW = stripAnsi(prompt).length;
|
|
2225
|
-
const indent = " ".repeat(promptW);
|
|
2226
|
-
const lines = highlight().split("\n");
|
|
2227
|
-
out(prompt + lines[0]);
|
|
2228
|
-
for (let i = 1; i < lines.length; i++) out("\n" + indent + lines[i]);
|
|
2229
|
-
return { promptW };
|
|
2230
|
-
}
|
|
2231
|
-
function render() {
|
|
2232
|
-
const mm = menu();
|
|
2233
|
-
if (sel >= mm.length) sel = Math.max(0, mm.length - 1);
|
|
2234
|
-
out("\x1B[?25l");
|
|
2235
|
-
if (lastCurRow > 0) out(`\x1B[${lastCurRow}A`);
|
|
2236
|
-
out("\r\x1B[J");
|
|
2237
|
-
const { promptW } = drawBlock();
|
|
2238
|
-
const cols = Math.max(1, process.stdout.columns || 80);
|
|
2239
|
-
for (let i = 0; i < mm.length; i++) {
|
|
2240
|
-
const m = mm[i];
|
|
2241
|
-
const name = m.name.padEnd(10);
|
|
2242
|
-
const maxDesc = Math.max(0, cols - name.length - 4);
|
|
2243
|
-
const desc = m.desc.length > maxDesc ? m.desc.slice(0, Math.max(0, maxDesc - 1)) + "\u2026" : m.desc;
|
|
2244
|
-
out("\n" + (i === sel ? color.green("\u203A " + name) + " " + color.dim(desc) : color.dim(" " + name + " " + desc)));
|
|
2245
|
-
}
|
|
2246
|
-
const text = opts.mask ? "*".repeat(buf.length) : buf;
|
|
2247
|
-
const physRows = text.split("\n").map((l) => Math.max(1, Math.ceil((promptW + displayWidth(l)) / cols)));
|
|
2248
|
-
const totalInputPhys = physRows.reduce((a, b) => a + b, 0);
|
|
2249
|
-
const before = opts.mask ? text : buf.slice(0, cur);
|
|
2250
|
-
const curLogicalRow = (before.match(/\n/g) || []).length;
|
|
2251
|
-
const curCol = promptW + displayWidth(before.slice(before.lastIndexOf("\n") + 1));
|
|
2252
|
-
const physBefore = physRows.slice(0, curLogicalRow).reduce((a, b) => a + b, 0);
|
|
2253
|
-
const curPhysRow = physBefore + Math.floor(curCol / cols);
|
|
2254
|
-
const curPhysCol = curCol % cols;
|
|
2255
|
-
const lastDrawnRow = totalInputPhys - 1 + mm.length;
|
|
2256
|
-
if (lastDrawnRow > curPhysRow) out(`\x1B[${lastDrawnRow - curPhysRow}A`);
|
|
2257
|
-
out("\r" + (curPhysCol > 0 ? `\x1B[${curPhysCol}C` : "") + "\x1B[?25h");
|
|
2258
|
-
lastCurRow = curPhysRow;
|
|
2259
|
-
}
|
|
2260
|
-
function finish(result) {
|
|
2261
|
-
restore();
|
|
2262
|
-
if (burstTimer) {
|
|
2263
|
-
clearTimeout(burstTimer);
|
|
2264
|
-
burstTimer = null;
|
|
2265
|
-
}
|
|
2266
|
-
pendingRender = false;
|
|
2267
|
-
out("\x1B[?25l");
|
|
2268
|
-
if (lastCurRow > 0) out(`\x1B[${lastCurRow}A`);
|
|
2269
|
-
out("\r\x1B[J");
|
|
2270
|
-
drawBlock();
|
|
2271
|
-
out("\n");
|
|
2272
|
-
if (result.type === "line" && result.value.trim() && !opts.mask) history.push(result.value);
|
|
2273
|
-
resolve2(result);
|
|
2274
|
-
}
|
|
2275
|
-
function insert(s) {
|
|
2276
|
-
buf = buf.slice(0, cur) + s + buf.slice(cur);
|
|
2277
|
-
cur += s.length;
|
|
2278
|
-
menuHidden = false;
|
|
2279
|
-
sel = 0;
|
|
2280
|
-
pendingRender = true;
|
|
2281
|
-
}
|
|
2282
|
-
function onKey(str, key) {
|
|
2283
|
-
const mm = menu();
|
|
2284
|
-
burstLen++;
|
|
2285
|
-
if (burstTimer) clearTimeout(burstTimer);
|
|
2286
|
-
burstTimer = setTimeout(() => {
|
|
2287
|
-
burstTimer = null;
|
|
2288
|
-
burstLen = 0;
|
|
2289
|
-
if (pendingRender) {
|
|
2290
|
-
pendingRender = false;
|
|
2291
|
-
render();
|
|
2292
|
-
}
|
|
2293
|
-
}, BURST_IDLE_MS);
|
|
2294
|
-
if (isEnter(key)) {
|
|
2295
|
-
if (key?.shift || key?.meta || burstLen > 1) {
|
|
2296
|
-
insert("\n");
|
|
2297
|
-
return;
|
|
2298
|
-
}
|
|
2299
|
-
return finish({ type: "line", value: buf });
|
|
2300
|
-
}
|
|
2301
|
-
if (key?.ctrl && key.name === "c") {
|
|
2302
|
-
if (buf) {
|
|
2303
|
-
buf = "";
|
|
2304
|
-
cur = 0;
|
|
2305
|
-
render();
|
|
2306
|
-
} else finish({ type: "quit" });
|
|
2307
|
-
return;
|
|
2308
|
-
}
|
|
2309
|
-
if (key?.ctrl && key.name === "d") {
|
|
2310
|
-
if (!buf) finish({ type: "eof" });
|
|
2311
|
-
return;
|
|
2312
|
-
}
|
|
2313
|
-
if (key?.name === "tab" && key.shift) {
|
|
2314
|
-
opts.onShiftTab?.();
|
|
2315
|
-
render();
|
|
2316
|
-
return;
|
|
2317
|
-
}
|
|
2318
|
-
if (key?.name === "tab") {
|
|
2319
|
-
if (mm.length) {
|
|
2320
|
-
buf = mm[sel].name + " ";
|
|
2321
|
-
cur = buf.length;
|
|
2322
|
-
menuHidden = false;
|
|
2323
|
-
render();
|
|
2324
|
-
}
|
|
2325
|
-
return;
|
|
2326
|
-
}
|
|
2327
|
-
if (key?.name === "up") {
|
|
2328
|
-
if (mm.length) {
|
|
2329
|
-
sel = (sel - 1 + mm.length) % mm.length;
|
|
2330
|
-
render();
|
|
2331
|
-
} else if (buf.includes("\n")) moveVert(-1);
|
|
2332
|
-
else histPrev();
|
|
2333
|
-
return;
|
|
2334
|
-
}
|
|
2335
|
-
if (key?.name === "down") {
|
|
2336
|
-
if (mm.length) {
|
|
2337
|
-
sel = (sel + 1) % mm.length;
|
|
2338
|
-
render();
|
|
2339
|
-
} else if (buf.includes("\n")) moveVert(1);
|
|
2340
|
-
else histNext();
|
|
2341
|
-
return;
|
|
2342
|
-
}
|
|
2343
|
-
if (key?.name === "left") {
|
|
2344
|
-
if (cur > 0) cur--;
|
|
2345
|
-
render();
|
|
2346
|
-
return;
|
|
2347
|
-
}
|
|
2348
|
-
if (key?.name === "right") {
|
|
2349
|
-
if (cur < buf.length) cur++;
|
|
2350
|
-
render();
|
|
2351
|
-
return;
|
|
2352
|
-
}
|
|
2353
|
-
if (key?.name === "home" || key?.ctrl && key.name === "a") {
|
|
2354
|
-
cur = 0;
|
|
2355
|
-
render();
|
|
2356
|
-
return;
|
|
2357
|
-
}
|
|
2358
|
-
if (key?.name === "end" || key?.ctrl && key.name === "e") {
|
|
2359
|
-
cur = buf.length;
|
|
2360
|
-
render();
|
|
2361
|
-
return;
|
|
2362
|
-
}
|
|
2363
|
-
if (key?.ctrl && key.name === "u") {
|
|
2364
|
-
buf = buf.slice(cur);
|
|
2365
|
-
cur = 0;
|
|
2366
|
-
menuHidden = false;
|
|
2367
|
-
render();
|
|
2368
|
-
return;
|
|
2369
|
-
}
|
|
2370
|
-
if (key?.name === "backspace") {
|
|
2371
|
-
if (cur > 0) {
|
|
2372
|
-
buf = buf.slice(0, cur - 1) + buf.slice(cur);
|
|
2373
|
-
cur--;
|
|
2374
|
-
menuHidden = false;
|
|
2375
|
-
render();
|
|
2376
|
-
}
|
|
2377
|
-
return;
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
if (decision.cacheable && answer === "always") {
|
|
2872
|
+
approvedTools.add(call.function.name);
|
|
2873
|
+
void addProjectApproval(call.function.name);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
if (config.traceFile) trace.push({ tool: call.function.name, args: call.function.arguments, step });
|
|
2877
|
+
const isTodo = call.function.name === "update_todos";
|
|
2878
|
+
const isShow = call.function.name === "show";
|
|
2879
|
+
const isExplore = call.function.name === "explore";
|
|
2880
|
+
process.stdout.write(" " + renderToolCall(call.function.name, callArgs) + (isTodo || isShow || isExplore ? "\n" : ""));
|
|
2881
|
+
let result = await runTool(call, signal);
|
|
2882
|
+
if (isShow) {
|
|
2883
|
+
const shown = renderShow(result);
|
|
2884
|
+
if (shown) {
|
|
2885
|
+
process.stdout.write(shown.display);
|
|
2886
|
+
pushTool(shown.note);
|
|
2887
|
+
} else {
|
|
2888
|
+
process.stdout.write(" " + color.red(stripControl(result)) + "\n");
|
|
2889
|
+
pushTool(result);
|
|
2890
|
+
}
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
const summary = summarizeResult(call.function.name, callArgs, result);
|
|
2894
|
+
let verifyOut = "";
|
|
2895
|
+
if (config.verifyCommand && (call.function.name === "write_file" || call.function.name === "edit_file")) {
|
|
2896
|
+
verifyOut = await runVerify(signal);
|
|
2897
|
+
result += `
|
|
2898
|
+
|
|
2899
|
+
[auto-check: ${config.verifyCommand}]
|
|
2900
|
+
${verifyOut}`;
|
|
2901
|
+
}
|
|
2902
|
+
if (result.length > config.maxToolResultChars) {
|
|
2903
|
+
result = result.slice(0, config.maxToolResultChars) + `
|
|
2904
|
+
\u2026[truncated ${result.length - config.maxToolResultChars} chars]`;
|
|
2905
|
+
}
|
|
2906
|
+
if (isTodo) {
|
|
2907
|
+
console.log(color.cyan(stripControl(result).split("\n").map((l) => " " + l).join("\n")));
|
|
2908
|
+
} else {
|
|
2909
|
+
process.stdout.write(summary + "\n");
|
|
2910
|
+
}
|
|
2911
|
+
if (verifyOut) {
|
|
2912
|
+
const ok = verifyOut.startsWith("passed");
|
|
2913
|
+
console.log(" " + color.dim(`auto-check ${config.verifyCommand}`) + " " + (ok ? color.green("\u2713 passed") : color.red("\u2717 failed")));
|
|
2914
|
+
}
|
|
2915
|
+
pushTool(result);
|
|
2916
|
+
}
|
|
2917
|
+
var STEER_PREAMBLE = "[The user sent this while you were working \u2014 treat it as additional guidance and keep going on the current task too, don't discard the work in progress]\n";
|
|
2918
|
+
function applySteering(messages, notes) {
|
|
2919
|
+
if (notes.length === 0) return messages;
|
|
2920
|
+
const content = STEER_PREAMBLE + notes.join("\n");
|
|
2921
|
+
const last = messages[messages.length - 1];
|
|
2922
|
+
if (last?.role === "user") {
|
|
2923
|
+
return [...messages.slice(0, -1), { ...last, content: `${last.content ?? ""}
|
|
2924
|
+
|
|
2925
|
+
${content}` }];
|
|
2926
|
+
}
|
|
2927
|
+
return [...messages, { role: "user", content }];
|
|
2928
|
+
}
|
|
2929
|
+
async function runTurn(messages, userInput, ask, approvedTools, signal, steering) {
|
|
2930
|
+
messages.push({ role: "user", content: userInput });
|
|
2931
|
+
const snapshot = messages.slice();
|
|
2932
|
+
try {
|
|
2933
|
+
let answered = false;
|
|
2934
|
+
const callCounts = /* @__PURE__ */ new Map();
|
|
2935
|
+
const deps = { approvedTools, callCounts, ask, signal };
|
|
2936
|
+
for (let step = 0; step < config.maxSteps && !answered && !signal?.aborted; step++) {
|
|
2937
|
+
try {
|
|
2938
|
+
messages = await compactIfNeeded(messages, signal);
|
|
2939
|
+
} catch (err) {
|
|
2940
|
+
console.error(color.red(`
|
|
2941
|
+
[compaction failed: ${err.message} \u2014 continuing]`) + "\n");
|
|
2378
2942
|
}
|
|
2379
|
-
if (
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2943
|
+
if (steering?.length) messages = applySteering(messages, steering.splice(0));
|
|
2944
|
+
const message = await callModel(messages, true, signal);
|
|
2945
|
+
messages.push(message);
|
|
2946
|
+
if (!hasContent2(message)) {
|
|
2947
|
+
messages.pop();
|
|
2948
|
+
console.log(color.dim("\n[the model returned an empty response \u2014 ending the turn]") + "\n");
|
|
2949
|
+
break;
|
|
2386
2950
|
}
|
|
2387
|
-
if (
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2951
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
2952
|
+
for (const call of message.tool_calls) {
|
|
2953
|
+
if (signal?.aborted) break;
|
|
2954
|
+
await handleToolCall(call, messages, step, deps);
|
|
2391
2955
|
}
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
if (str && !key?.ctrl && !key?.meta && [...str].length === 1) {
|
|
2395
|
-
if (isPrintableCodePoint(str.codePointAt(0))) insert(str);
|
|
2956
|
+
} else {
|
|
2957
|
+
answered = !steering?.length;
|
|
2396
2958
|
}
|
|
2397
2959
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
const target = row + dir;
|
|
2402
|
-
if (target < 0 || target >= lines.length) return;
|
|
2403
|
-
let idx = 0;
|
|
2404
|
-
for (let i = 0; i < target; i++) idx += lines[i].length + 1;
|
|
2405
|
-
cur = idx + Math.min(col, lines[target].length);
|
|
2406
|
-
render();
|
|
2960
|
+
if (signal?.aborted) {
|
|
2961
|
+
console.log(color.dim("\n[cancelled]") + "\n");
|
|
2962
|
+
return snapshot;
|
|
2407
2963
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2964
|
+
if (!answered) {
|
|
2965
|
+
console.log(color.dim(`
|
|
2966
|
+
[reached the ${config.maxSteps}-step limit \u2014 wrapping up]`));
|
|
2967
|
+
const wrapPrompt = {
|
|
2968
|
+
role: "system",
|
|
2969
|
+
content: `You have reached the ${config.maxSteps}-step limit for this turn. Do not call any more tools. Briefly tell the user what you accomplished and what still remains.`
|
|
2970
|
+
};
|
|
2971
|
+
const wrap = await callModel([...messages, wrapPrompt], false, signal);
|
|
2972
|
+
if (hasContent2(wrap)) messages.push(wrap);
|
|
2414
2973
|
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
render();
|
|
2974
|
+
return messages;
|
|
2975
|
+
} catch (err) {
|
|
2976
|
+
if (signal?.aborted || err?.name === "AbortError") {
|
|
2977
|
+
console.log(color.dim("\n[cancelled]") + "\n");
|
|
2978
|
+
return snapshot;
|
|
2421
2979
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2980
|
+
console.error(color.red(`
|
|
2981
|
+
[error] ${err.message}`) + "\n");
|
|
2982
|
+
return snapshot;
|
|
2983
|
+
}
|
|
2425
2984
|
}
|
|
2426
|
-
|
|
2427
|
-
|
|
2985
|
+
|
|
2986
|
+
// src/env.ts
|
|
2987
|
+
import { execFile } from "node:child_process";
|
|
2988
|
+
import { platform, arch } from "node:os";
|
|
2989
|
+
function tryExec(cmd, args, timeout = 2e3) {
|
|
2428
2990
|
return new Promise((resolve2) => {
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
else return;
|
|
2435
|
-
restore();
|
|
2436
|
-
out(ch + "\n");
|
|
2437
|
-
resolve2(ch);
|
|
2438
|
-
});
|
|
2991
|
+
try {
|
|
2992
|
+
execFile(cmd, args, { timeout, windowsHide: true }, (err, stdout) => resolve2(err ? null : String(stdout).trim()));
|
|
2993
|
+
} catch {
|
|
2994
|
+
resolve2(null);
|
|
2995
|
+
}
|
|
2439
2996
|
});
|
|
2440
2997
|
}
|
|
2441
|
-
function
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2998
|
+
async function gitStatus() {
|
|
2999
|
+
const branch = await tryExec("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
3000
|
+
if (branch === null) return "not a git repo";
|
|
3001
|
+
const porcelain = await tryExec("git", ["status", "--porcelain"]);
|
|
3002
|
+
if (porcelain === null) return `branch ${branch}`;
|
|
3003
|
+
const dirty = porcelain ? porcelain.split("\n").filter(Boolean).length : 0;
|
|
3004
|
+
return `branch ${branch} (${dirty ? `${dirty} uncommitted change${dirty === 1 ? "" : "s"}` : "clean"})`;
|
|
3005
|
+
}
|
|
3006
|
+
function formatRuntimeContext(f) {
|
|
3007
|
+
return [
|
|
3008
|
+
"# Environment",
|
|
3009
|
+
`- Date: ${f.date}`,
|
|
3010
|
+
`- Working directory: ${f.cwd}`,
|
|
3011
|
+
`- Platform: ${f.platform}`,
|
|
3012
|
+
`- Node: ${f.node}`,
|
|
3013
|
+
`- Git: ${f.git}`,
|
|
3014
|
+
`- ripgrep (rg): ${f.ripgrep ? "available" : "not installed"}`
|
|
3015
|
+
].join("\n");
|
|
3016
|
+
}
|
|
3017
|
+
async function runtimeContext() {
|
|
3018
|
+
const [git, rg] = await Promise.all([gitStatus(), tryExec("rg", ["--version"])]);
|
|
3019
|
+
return formatRuntimeContext({
|
|
3020
|
+
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
3021
|
+
cwd: process.cwd(),
|
|
3022
|
+
platform: `${platform()} ${arch()}`,
|
|
3023
|
+
node: process.version,
|
|
3024
|
+
git,
|
|
3025
|
+
ripgrep: rg !== null
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
// src/commands.ts
|
|
3030
|
+
import { writeFile as writeFile4, mkdir as mkdir4, chmod as chmod3 } from "node:fs/promises";
|
|
3031
|
+
|
|
3032
|
+
// src/skills.ts
|
|
3033
|
+
import { readdir as readdir3, readFile as readFile5 } from "node:fs/promises";
|
|
3034
|
+
import { join as join4 } from "node:path";
|
|
3035
|
+
import { homedir as homedir5 } from "node:os";
|
|
3036
|
+
var registry = /* @__PURE__ */ new Map();
|
|
3037
|
+
function skillNames() {
|
|
3038
|
+
return [...registry.keys()];
|
|
3039
|
+
}
|
|
3040
|
+
function getSkill(name) {
|
|
3041
|
+
return registry.get(name);
|
|
3042
|
+
}
|
|
3043
|
+
async function loadSkills() {
|
|
3044
|
+
registry.clear();
|
|
3045
|
+
const dirs = [
|
|
3046
|
+
[join4(homedir5(), ".beecork", "skills"), "global"],
|
|
3047
|
+
[join4(process.cwd(), ".beecork", "skills"), "project"]
|
|
3048
|
+
];
|
|
3049
|
+
for (const [dir, source] of dirs) {
|
|
3050
|
+
let entries;
|
|
3051
|
+
try {
|
|
3052
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
3053
|
+
} catch {
|
|
3054
|
+
continue;
|
|
2456
3055
|
}
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
3056
|
+
for (const e of entries) {
|
|
3057
|
+
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
3058
|
+
const name = e.name.slice(0, -3);
|
|
3059
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/i.test(name)) continue;
|
|
3060
|
+
try {
|
|
3061
|
+
const content = (await readFile5(join4(dir, e.name), "utf8")).trim();
|
|
3062
|
+
if (!content) continue;
|
|
3063
|
+
if (source === "project" && registry.has(name)) {
|
|
3064
|
+
console.error(color.yellow(`\u26A0 project skill /${name} ignored \u2014 a global skill of that name takes precedence`));
|
|
3065
|
+
continue;
|
|
3066
|
+
}
|
|
3067
|
+
registry.set(name, { name, content, source });
|
|
3068
|
+
} catch {
|
|
3069
|
+
}
|
|
2462
3070
|
}
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
} else if (isEnter(key)) finish(opts.items[sel].value);
|
|
2471
|
-
else if (key?.name === "escape" || key?.name === "q" || key?.ctrl && key.name === "c") finish(null);
|
|
2472
|
-
});
|
|
2473
|
-
render();
|
|
2474
|
-
});
|
|
3071
|
+
}
|
|
3072
|
+
return [...registry.values()];
|
|
3073
|
+
}
|
|
3074
|
+
function expandSkill(skill, extra) {
|
|
3075
|
+
return skill.content.includes("$ARGUMENTS") ? skill.content.replaceAll("$ARGUMENTS", extra) : skill.content + (extra ? `
|
|
3076
|
+
|
|
3077
|
+
${extra}` : "");
|
|
2475
3078
|
}
|
|
2476
3079
|
|
|
2477
3080
|
// src/commands.ts
|
|
2478
3081
|
var SLASH_COMMANDS = [
|
|
2479
3082
|
{ name: "/model", desc: "switch model (menu; /model <term> searches)" },
|
|
3083
|
+
{ name: "/effort", desc: "set reasoning effort (off/low/medium/high/max)" },
|
|
2480
3084
|
{ name: "/context", desc: "conversation size in tokens" },
|
|
2481
3085
|
{ name: "/clear", desc: "clear the conversation" },
|
|
2482
3086
|
{ name: "/key", desc: "set + save your OpenRouter API key" },
|
|
@@ -2492,6 +3096,10 @@ function applyModel(slug) {
|
|
|
2492
3096
|
void saveModelPreference(slug);
|
|
2493
3097
|
return slug;
|
|
2494
3098
|
}
|
|
3099
|
+
function applyEffort(level) {
|
|
3100
|
+
state.reasoningEffort = level;
|
|
3101
|
+
void saveReasoningPreference(level);
|
|
3102
|
+
}
|
|
2495
3103
|
function ago(ms) {
|
|
2496
3104
|
const s = Math.floor((Date.now() - ms) / 1e3);
|
|
2497
3105
|
if (!ms || s < 0) return "unknown";
|
|
@@ -2520,6 +3128,16 @@ async function handleCommand(input, messages) {
|
|
|
2520
3128
|
} else {
|
|
2521
3129
|
await searchModels(arg);
|
|
2522
3130
|
}
|
|
3131
|
+
} else if (cmd === "/effort") {
|
|
3132
|
+
if (!arg) await pickEffort();
|
|
3133
|
+
else {
|
|
3134
|
+
const level = normalizeEffort(arg);
|
|
3135
|
+
if (!level) console.log(color.red(`unknown effort "${arg}" \u2014 use: ${EFFORTS.join(" / ")}`) + "\n");
|
|
3136
|
+
else {
|
|
3137
|
+
applyEffort(level);
|
|
3138
|
+
console.log(color.green(`reasoning effort: ${level}`) + (level === "off" ? color.dim(" (thinking disabled)") : "") + "\n");
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
2523
3141
|
} else if (cmd === "/key") {
|
|
2524
3142
|
if (!arg) {
|
|
2525
3143
|
console.log(color.dim("usage: /key <your-openrouter-key> (saved to ~/.beecork/config.json)") + "\n");
|
|
@@ -2614,6 +3232,32 @@ async function pickModel() {
|
|
|
2614
3232
|
});
|
|
2615
3233
|
if (choice) console.log(color.green(`switched to: ${applyModel(choice)}`) + "\n");
|
|
2616
3234
|
}
|
|
3235
|
+
var EFFORT_NOTES = {
|
|
3236
|
+
off: "no thinking \u2014 fastest, cheapest",
|
|
3237
|
+
low: "a light nudge to think first",
|
|
3238
|
+
medium: "balanced (default)",
|
|
3239
|
+
high: "deeper reasoning, more tokens",
|
|
3240
|
+
max: "maximum reasoning depth"
|
|
3241
|
+
};
|
|
3242
|
+
async function pickEffort() {
|
|
3243
|
+
if (!process.stdin.isTTY) {
|
|
3244
|
+
console.log(color.cyan(`reasoning effort: ${state.reasoningEffort}`) + color.dim(` (levels: ${EFFORTS.join(" / ")})`) + "\n");
|
|
3245
|
+
return;
|
|
3246
|
+
}
|
|
3247
|
+
const choice = await selectMenu({
|
|
3248
|
+
title: "reasoning effort \u2014 \u2191/\u2193 then Enter (Esc to cancel)",
|
|
3249
|
+
initial: Math.max(0, EFFORTS.indexOf(state.reasoningEffort)),
|
|
3250
|
+
items: EFFORTS.map((e) => ({
|
|
3251
|
+
label: (e === state.reasoningEffort ? "\u25CF " : " ") + e,
|
|
3252
|
+
value: e,
|
|
3253
|
+
hint: EFFORT_NOTES[e]
|
|
3254
|
+
}))
|
|
3255
|
+
});
|
|
3256
|
+
if (choice) {
|
|
3257
|
+
applyEffort(choice);
|
|
3258
|
+
console.log(color.green(`reasoning effort: ${choice}`) + (choice === "off" ? color.dim(" (thinking disabled)") : "") + "\n");
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
2617
3261
|
function showRecommended() {
|
|
2618
3262
|
console.log(color.cyan("recommended models (all support tools):") + "\n");
|
|
2619
3263
|
for (const m of RECOMMENDED_MODELS) {
|
|
@@ -2664,6 +3308,11 @@ function completer(line) {
|
|
|
2664
3308
|
const hits = all.filter((c) => c.startsWith(line));
|
|
2665
3309
|
return [hits.length ? hits : all, line];
|
|
2666
3310
|
}
|
|
3311
|
+
if (line.startsWith("/effort ")) {
|
|
3312
|
+
const all = EFFORTS.map((e) => `/effort ${e}`);
|
|
3313
|
+
const hits = all.filter((c) => c.startsWith(line));
|
|
3314
|
+
return [hits.length ? hits : all, line];
|
|
3315
|
+
}
|
|
2667
3316
|
if (line.startsWith("/")) {
|
|
2668
3317
|
const all = [...COMMANDS, ...skillNames().map((n) => "/" + n)];
|
|
2669
3318
|
const hits = all.filter((c) => c.startsWith(line));
|
|
@@ -2685,7 +3334,7 @@ async function main() {
|
|
|
2685
3334
|
}
|
|
2686
3335
|
return;
|
|
2687
3336
|
}
|
|
2688
|
-
const [userCfg, instr, settings, skills, projectApprovals] = await Promise.all([
|
|
3337
|
+
const [userCfg, instr, settings, skills, projectApprovals, runtimeCtx] = await Promise.all([
|
|
2689
3338
|
loadUserConfig(),
|
|
2690
3339
|
// ~/.beecork/config.json (API keys)
|
|
2691
3340
|
loadInstructions(),
|
|
@@ -2694,14 +3343,19 @@ async function main() {
|
|
|
2694
3343
|
// settings.json (model pref + global alwaysAllow)
|
|
2695
3344
|
loadSkills(),
|
|
2696
3345
|
// user-defined slash commands from .beecork/skills/
|
|
2697
|
-
loadProjectApprovals()
|
|
3346
|
+
loadProjectApprovals(),
|
|
2698
3347
|
// per-project "always" from past sessions
|
|
3348
|
+
runtimeContext()
|
|
3349
|
+
// real environment facts (date, git state, tool availability)
|
|
2699
3350
|
]);
|
|
2700
3351
|
const savedKey = String(userCfg.OPENROUTER_API_KEY ?? "");
|
|
2701
3352
|
let apiKey = API_KEY || savedKey;
|
|
2702
3353
|
state.braveKey = process.env.BRAVE_API_KEY || String(userCfg.BRAVE_API_KEY ?? "");
|
|
2703
3354
|
if (settings.model && !process.env.OPENROUTER_MODEL) state.model = settings.model;
|
|
2704
|
-
|
|
3355
|
+
if (settings.reasoningEffort && !process.env.REASONING_EFFORT) state.reasoningEffort = settings.reasoningEffort;
|
|
3356
|
+
let systemContent = `${SYSTEM_PROMPT}
|
|
3357
|
+
|
|
3358
|
+
${runtimeCtx}`;
|
|
2705
3359
|
if (instr.trusted) {
|
|
2706
3360
|
systemContent += `
|
|
2707
3361
|
|
|
@@ -2732,6 +3386,7 @@ ${instr.trusted}`;
|
|
|
2732
3386
|
}
|
|
2733
3387
|
};
|
|
2734
3388
|
const tty = !!process.stdin.isTTY;
|
|
3389
|
+
process.on("exit", killAllTasks);
|
|
2735
3390
|
if (tty) {
|
|
2736
3391
|
initInput();
|
|
2737
3392
|
process.on("exit", teardownInput);
|
|
@@ -2838,21 +3493,56 @@ ${instr.trusted}`;
|
|
|
2838
3493
|
}
|
|
2839
3494
|
activeTurn = new AbortController();
|
|
2840
3495
|
let modeChangedMidTurn = false;
|
|
2841
|
-
const
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
3496
|
+
const steering = [];
|
|
3497
|
+
let typed = "";
|
|
3498
|
+
const redrawSteer = () => process.stdout.write("\r\x1B[2K" + color.cyan("\xBB ") + color.dim(typed));
|
|
3499
|
+
const clearSteer = () => {
|
|
3500
|
+
process.stdout.write("\r\x1B[2K");
|
|
3501
|
+
setSteeringActive(false);
|
|
3502
|
+
};
|
|
3503
|
+
const restoreKeys = tty ? pushKeyHandler((str, key) => {
|
|
3504
|
+
if (key && (key.name === "escape" || key.ctrl && key.name === "c")) {
|
|
3505
|
+
activeTurn?.abort();
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3508
|
+
if (key && key.name === "tab" && key.shift) {
|
|
2845
3509
|
state.mode = nextMode(state.mode);
|
|
2846
3510
|
modeChangedMidTurn = true;
|
|
3511
|
+
return;
|
|
3512
|
+
}
|
|
3513
|
+
if (key && (key.name === "return" || key.name === "enter")) {
|
|
3514
|
+
const note = typed.trim();
|
|
3515
|
+
typed = "";
|
|
3516
|
+
clearSteer();
|
|
3517
|
+
if (note) {
|
|
3518
|
+
steering.push(note);
|
|
3519
|
+
console.log(color.dim(`\xBB queued for next step: ${note}`));
|
|
3520
|
+
}
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
3523
|
+
if (key && (key.name === "backspace" || key.name === "delete")) {
|
|
3524
|
+
if (typed) {
|
|
3525
|
+
typed = typed.slice(0, -1);
|
|
3526
|
+
typed ? redrawSteer() : clearSteer();
|
|
3527
|
+
}
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
if (str && !key?.ctrl && !key?.meta && [...str].length === 1 && isPrintableCodePoint(str.codePointAt(0))) {
|
|
3531
|
+
typed += str;
|
|
3532
|
+
setSteeringActive(true);
|
|
3533
|
+
redrawSteer();
|
|
2847
3534
|
}
|
|
2848
3535
|
}) : () => {
|
|
2849
3536
|
};
|
|
2850
3537
|
try {
|
|
2851
|
-
messages = await runTurn(messages, userInput, ask, approvedTools, activeTurn.signal);
|
|
3538
|
+
messages = await runTurn(messages, userInput, ask, approvedTools, activeTurn.signal, steering);
|
|
2852
3539
|
} finally {
|
|
2853
3540
|
restoreKeys();
|
|
3541
|
+
setSteeringActive(false);
|
|
2854
3542
|
activeTurn = null;
|
|
2855
3543
|
if (modeChangedMidTurn) console.log(color.yellow(`\u25B8 mode: ${modeLabel(state.mode)}`));
|
|
3544
|
+
const bg = runningTaskCount();
|
|
3545
|
+
if (bg > 0) console.log(color.dim(`\u25B8 ${bg} background task${bg === 1 ? "" : "s"} running \u2014 check_task / stop_task`));
|
|
2856
3546
|
}
|
|
2857
3547
|
}
|
|
2858
3548
|
teardownInput();
|