@noobdemon/noob-cli 1.0.7 → 1.0.9
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/package.json +1 -1
- package/src/agent.js +76 -8
- package/src/api.js +73 -41
- package/src/repl.js +112 -20
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os from "node:os";
|
|
1
2
|
import { stream } from "./api.js";
|
|
2
3
|
import { t } from "./i18n.js";
|
|
3
4
|
|
|
@@ -32,27 +33,86 @@ Available tools:
|
|
|
32
33
|
- JSON in the tool block must be valid: escape newlines as \\n inside string values.
|
|
33
34
|
- LANGUAGE: Always write your prose answers to the user in Vietnamese (tiếng Việt), unless the user explicitly writes in another language. Keep code, file paths, commands, and tool JSON unchanged.
|
|
34
35
|
|
|
36
|
+
# Coding principles (Karpathy) — apply to EVERY code change
|
|
37
|
+
1. THINK FIRST: state the key assumptions before you code. If a requirement is ambiguous or a step is hard to reverse, ask ONE sharp question instead of guessing.
|
|
38
|
+
2. KEEP IT SIMPLE: write the simplest thing that works. No speculative abstractions, no extra flags/config/layers "for later". Prefer deleting code to adding it.
|
|
39
|
+
3. SURGICAL: change only what the task needs. No drive-by refactors, renames, reformatting, or comment churn in unrelated code.
|
|
40
|
+
4. VERIFIABLE GOAL: decide how you'll know it works, then check it (run the build/test, read the output). Report what you verified — and honestly state what you did NOT verify.
|
|
41
|
+
|
|
35
42
|
# Example interaction
|
|
36
43
|
## USER
|
|
37
|
-
|
|
44
|
+
do the tests pass?
|
|
38
45
|
## ASSISTANT
|
|
39
46
|
\`\`\`tool
|
|
40
|
-
{"name": "run_command", "input": {"command": "
|
|
47
|
+
{"name": "run_command", "input": {"command": "npm test"}}
|
|
41
48
|
\`\`\`
|
|
42
49
|
## TOOL RESULT (run_command)
|
|
43
|
-
|
|
50
|
+
12 passing (340ms)
|
|
44
51
|
[exit code 0]
|
|
45
52
|
## ASSISTANT
|
|
46
|
-
|
|
53
|
+
Có — cả 12 test đều pass.
|
|
47
54
|
|
|
48
55
|
Follow this pattern exactly. Your very first response to a task that needs the filesystem MUST be a tool block — do not refuse or explain limitations.`;
|
|
49
56
|
|
|
50
57
|
const MAX_STEPS = 30;
|
|
58
|
+
const MAX_PROMPT_CHARS = 80000; // ngân sách ký tự cho phần hội thoại gửi lên model
|
|
59
|
+
|
|
60
|
+
// Môi trường chạy thực: model cần biết OS + shell để emit lệnh ĐÚNG. Không có
|
|
61
|
+
// khối này, trên Windows model hay emit lệnh Unix (wc/ls/cat/grep) → run_command
|
|
62
|
+
// (PowerShell) báo lỗi.
|
|
63
|
+
function runtimeContext() {
|
|
64
|
+
const isWin = process.platform === "win32";
|
|
65
|
+
const lines = [
|
|
66
|
+
"# ENVIRONMENT",
|
|
67
|
+
`- OS: ${process.platform} (${os.release()})`,
|
|
68
|
+
`- Shell for run_command: ${isWin ? "Windows PowerShell (powershell.exe)" : "bash"}`,
|
|
69
|
+
`- Current working directory: ${process.cwd()}`,
|
|
70
|
+
];
|
|
71
|
+
if (isWin) {
|
|
72
|
+
lines.push(
|
|
73
|
+
"- IMPORTANT: run_command runs in PowerShell on Windows — do NOT use Unix tools.",
|
|
74
|
+
" Use: Get-Content (not cat), Get-ChildItem (not ls), Select-String (not grep),",
|
|
75
|
+
" (Get-Content f | Measure-Object -Line) (not wc -l). Paths use backslashes.",
|
|
76
|
+
"- Prefer the dedicated tools (read_file / list_dir / grep / glob) over shell commands;",
|
|
77
|
+
" they are cross-platform. Use run_command mainly for builds/tests/installs.",
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
lines.push("- Prefer the dedicated tools (read_file/list_dir/grep/glob) over shell when possible.");
|
|
81
|
+
}
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Lược ngữ cảnh để không vượt context khi phiên dài. KHÔNG đụng vào history thật
|
|
86
|
+
// (vẫn lưu/đầy đủ để resume) — chỉ thu gọn BẢN SAO dùng cho prompt.
|
|
87
|
+
function compact(history, budget) {
|
|
88
|
+
const len = (m) => (m.content || "").length + 24;
|
|
89
|
+
let total = history.reduce((s, m) => s + len(m), 0);
|
|
90
|
+
if (total <= budget) return history;
|
|
91
|
+
const out = history.map((m) => ({ ...m }));
|
|
92
|
+
// (1) Rút gọn các TOOL RESULT cũ trước (giữ 5 cái gần nhất) — đây là phần phình
|
|
93
|
+
// nhất (đọc file lớn) và model đã xử lý xong rồi.
|
|
94
|
+
const toolIdx = out.map((m, i) => (m.role === "tool" ? i : -1)).filter((i) => i >= 0);
|
|
95
|
+
for (const i of toolIdx.slice(0, Math.max(0, toolIdx.length - 5))) {
|
|
96
|
+
if (total <= budget) break;
|
|
97
|
+
const before = len(out[i]);
|
|
98
|
+
out[i].content = "[kết quả công cụ cũ đã được lược bớt để tiết kiệm ngữ cảnh]";
|
|
99
|
+
total -= before - len(out[i]);
|
|
100
|
+
}
|
|
101
|
+
if (total <= budget) return out;
|
|
102
|
+
// (2) Vẫn dài → bỏ các lượt cũ nhất, giữ USER đầu tiên (mô tả nhiệm vụ gốc) +
|
|
103
|
+
// 12 message gần nhất.
|
|
104
|
+
const firstUser = out.findIndex((m) => m.role === "user");
|
|
105
|
+
const head = firstUser >= 0 ? [out[firstUser]] : [];
|
|
106
|
+
const tailStart = Math.max(firstUser + 1, out.length - 12);
|
|
107
|
+
const elided = { role: "tool", name: "context", content: "[… các lượt trước đã được lược bớt …]" };
|
|
108
|
+
return [...head, elided, ...out.slice(tailStart)];
|
|
109
|
+
}
|
|
51
110
|
|
|
52
111
|
// The proxy is stateless, so we serialize the whole transcript into one prompt.
|
|
53
112
|
function buildPrompt(history) {
|
|
54
|
-
const
|
|
55
|
-
|
|
113
|
+
const msgs = compact(history, MAX_PROMPT_CHARS);
|
|
114
|
+
const parts = [SYSTEM, "", runtimeContext(), "", "=".repeat(60), "# CONVERSATION", ""];
|
|
115
|
+
for (const m of msgs) {
|
|
56
116
|
if (m.role === "user") parts.push(`## USER\n${m.content}`);
|
|
57
117
|
else if (m.role === "assistant") parts.push(`## ASSISTANT\n${m.content}`);
|
|
58
118
|
else if (m.role === "tool") parts.push(`## TOOL RESULT (${m.name})\n${m.content}`);
|
|
@@ -95,11 +155,19 @@ export function parseToolCall(text) {
|
|
|
95
155
|
* @param {(msg:string)=>void} opts.onStatus thinking/streaming status
|
|
96
156
|
* @returns {Promise<string>} the final assistant answer (no tool block)
|
|
97
157
|
*/
|
|
98
|
-
export async function runAgent({ history, model, signal, onTool, onStatus }) {
|
|
158
|
+
export async function runAgent({ history, model, signal, onTool, onStatus, onDelta }) {
|
|
99
159
|
for (let step = 0; step < MAX_STEPS; step++) {
|
|
100
160
|
const prompt = buildPrompt(history);
|
|
101
161
|
onStatus?.("thinking");
|
|
102
|
-
|
|
162
|
+
onDelta?.({ type: "step-start" });
|
|
163
|
+
const { text } = await stream({
|
|
164
|
+
mode: "chat",
|
|
165
|
+
model,
|
|
166
|
+
message: prompt,
|
|
167
|
+
signal,
|
|
168
|
+
onDelta: (d) => onDelta?.({ type: "delta", text: d }),
|
|
169
|
+
});
|
|
170
|
+
onDelta?.({ type: "step-end" });
|
|
103
171
|
history.push({ role: "assistant", content: text });
|
|
104
172
|
|
|
105
173
|
const call = parseToolCall(text);
|
package/src/api.js
CHANGED
|
@@ -43,57 +43,89 @@ async function parseError(resp) {
|
|
|
43
43
|
* Stream a chat/merge/search request from the gateway.
|
|
44
44
|
* @returns {Promise<{text:string, reasoning:string}>}
|
|
45
45
|
*/
|
|
46
|
-
export async function stream({ mode = "chat", message, model, signal, onDelta, onReasoning, onStatus }) {
|
|
46
|
+
export async function stream({ mode = "chat", message, model, signal, onDelta, onReasoning, onStatus, idleMs = 120000 }) {
|
|
47
47
|
const endpoint = mode === "search" ? "/api/search" : mode === "merge" ? "/api/merge" : "/api/chat";
|
|
48
48
|
const body = mode === "search" ? { query: message } : mode === "merge" ? { message } : { message, model };
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
// Idle-timeout: nếu KHÔNG nhận được byte nào trong idleMs (kết nối treo), tự
|
|
51
|
+
// huỷ và báo lỗi rõ ràng — thay vì spinner quay vô tận. Vẫn tôn trọng signal
|
|
52
|
+
// của người dùng (Ctrl+C). Phân biệt 2 trường hợp qua cờ `timedOut`.
|
|
53
|
+
const ctrl = new AbortController();
|
|
54
|
+
let timedOut = false;
|
|
55
|
+
const onUserAbort = () => ctrl.abort();
|
|
56
|
+
signal?.addEventListener("abort", onUserAbort, { once: true });
|
|
57
|
+
let idle;
|
|
58
|
+
const arm = () => {
|
|
59
|
+
clearTimeout(idle);
|
|
60
|
+
idle = setTimeout(() => {
|
|
61
|
+
timedOut = true;
|
|
62
|
+
ctrl.abort();
|
|
63
|
+
}, idleMs);
|
|
64
|
+
};
|
|
58
65
|
|
|
59
|
-
const reader = resp.body.getReader();
|
|
60
|
-
const decoder = new TextDecoder();
|
|
61
|
-
let buf = "";
|
|
62
66
|
let text = "";
|
|
63
67
|
let reasoning = "";
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
69
|
+
// Một dòng SSE → cập nhật text/reasoning. Tách ra để dùng lại khi flush dòng cuối.
|
|
70
|
+
const processLine = (rawLine) => {
|
|
71
|
+
const line = rawLine.trim();
|
|
72
|
+
if (!line.startsWith("data:")) return;
|
|
73
|
+
const data = line.slice(5).trim();
|
|
74
|
+
if (!data) return;
|
|
75
|
+
let p;
|
|
76
|
+
try {
|
|
77
|
+
p = JSON.parse(data);
|
|
78
|
+
} catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (p.status && onStatus) onStatus(p.status);
|
|
82
|
+
if (p.delta) {
|
|
83
|
+
text += p.delta;
|
|
84
|
+
onDelta?.(p.delta);
|
|
85
|
+
}
|
|
86
|
+
if (p.reasoning) {
|
|
87
|
+
reasoning = p.reasoning;
|
|
88
|
+
onReasoning?.(p.reasoning);
|
|
89
|
+
if (p.answer) text = p.answer;
|
|
90
|
+
}
|
|
91
|
+
if (p.error) throw new ApiError(p.error, {});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
arm();
|
|
96
|
+
const resp = await fetch(config.gatewayUrl + endpoint, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: authHeaders(),
|
|
99
|
+
body: JSON.stringify(body),
|
|
100
|
+
signal: ctrl.signal,
|
|
101
|
+
});
|
|
102
|
+
if (!resp.ok || !resp.body) throw await parseError(resp);
|
|
103
|
+
|
|
104
|
+
const reader = resp.body.getReader();
|
|
105
|
+
const decoder = new TextDecoder();
|
|
106
|
+
let buf = "";
|
|
107
|
+
while (true) {
|
|
108
|
+
const { done, value } = await reader.read();
|
|
109
|
+
arm(); // có hoạt động → gia hạn idle
|
|
110
|
+
if (done) break;
|
|
111
|
+
buf += decoder.decode(value, { stream: true });
|
|
112
|
+
let nl;
|
|
113
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
114
|
+
processLine(buf.slice(0, nl));
|
|
115
|
+
buf = buf.slice(nl + 1);
|
|
91
116
|
}
|
|
92
|
-
if (p.error) throw new ApiError(p.error, { status: resp.status });
|
|
93
117
|
}
|
|
94
|
-
|
|
118
|
+
buf += decoder.decode(); // flush decoder
|
|
119
|
+
if (buf.trim()) processLine(buf); // dòng SSE cuối không có '\n' — đừng bỏ sót
|
|
95
120
|
|
|
96
|
-
|
|
121
|
+
return { text: text.trim(), reasoning: reasoning.trim() };
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (timedOut) throw new ApiError("Kết nối tới máy chủ quá thời gian chờ (treo).", { code: "timeout" });
|
|
124
|
+
throw err;
|
|
125
|
+
} finally {
|
|
126
|
+
clearTimeout(idle);
|
|
127
|
+
signal?.removeEventListener("abort", onUserAbort);
|
|
128
|
+
}
|
|
97
129
|
}
|
|
98
130
|
|
|
99
131
|
/** Fetch the current key's quota/usage from the gateway (no request consumed). */
|
package/src/repl.js
CHANGED
|
@@ -133,6 +133,14 @@ export async function startRepl(opts = {}) {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
// NOOB_DEBUG=1: vạch trần đường thoát thật sự (xác nhận/loại bỏ giả thuyết
|
|
137
|
+
// "event loop cạn"). 'beforeExit' nổ = loop cạn (stdin chết). 'exit' = thoát.
|
|
138
|
+
if (process.env.NOOB_DEBUG === "1") {
|
|
139
|
+
process.stderr.write(` [debug] isTTY=${process.stdin.isTTY} platform=${process.platform} node=${process.version}\n`);
|
|
140
|
+
process.on("beforeExit", (code) => process.stderr.write(` [debug] beforeExit code=${code} — EVENT LOOP CẠN (stdin chết)\n`));
|
|
141
|
+
process.on("exit", (code) => process.stderr.write(` [debug] exit code=${code} closed=${closed} exiting=${exiting}\n`));
|
|
142
|
+
}
|
|
143
|
+
|
|
136
144
|
let abort = null; // active turn controller
|
|
137
145
|
let sigintArmed = false;
|
|
138
146
|
process.on("SIGINT", () => {
|
|
@@ -317,55 +325,89 @@ export async function startRepl(opts = {}) {
|
|
|
317
325
|
return;
|
|
318
326
|
}
|
|
319
327
|
abort = new AbortController();
|
|
320
|
-
|
|
328
|
+
// discardStdin:false — TỐI QUAN TRỌNG. Mặc định ora chiếm stdin (raw mode +
|
|
329
|
+
// pause) để "nuốt" input khi quay. Trên Windows nó KHÔNG khôi phục sạch khi
|
|
330
|
+
// stop → stdin chết → prompt "cho phép?" hiện ra rồi event loop cạn → CLI tự
|
|
331
|
+
// out; và Ctrl+C không thành SIGINT (phải spam mới thoát). Tắt hẳn để ora
|
|
332
|
+
// đừng đụng stdin — readline tự quản.
|
|
333
|
+
const spinner = ora({ color: "magenta", spinner: "dots", discardStdin: false });
|
|
321
334
|
const t0 = Date.now();
|
|
322
335
|
let timer = null;
|
|
323
336
|
const tick = (label) => {
|
|
324
337
|
const elapsed = ((Date.now() - t0) / 1000).toFixed(0);
|
|
325
338
|
spinner.text = c.dim(`${label}… ${elapsed}s`);
|
|
326
339
|
};
|
|
340
|
+
const stopSpin = () => {
|
|
341
|
+
if (timer) {
|
|
342
|
+
clearInterval(timer);
|
|
343
|
+
timer = null;
|
|
344
|
+
}
|
|
345
|
+
if (spinner.isSpinning) spinner.stop();
|
|
346
|
+
};
|
|
347
|
+
const startSpin = (label) => {
|
|
348
|
+
if (!spinner.isSpinning) spinner.start();
|
|
349
|
+
if (!timer) timer = setInterval(() => tick(label), 200);
|
|
350
|
+
};
|
|
327
351
|
|
|
328
352
|
try {
|
|
329
353
|
if (state.mode !== "chat") {
|
|
330
|
-
|
|
331
|
-
|
|
354
|
+
const name = state.mode === "search" ? "Tìm web" : "Merge AI";
|
|
355
|
+
const label = state.mode === "search" ? t.searching : t.merging;
|
|
356
|
+
startSpin(label);
|
|
357
|
+
const printer = makeStreamPrinter(name, "#f59e0b");
|
|
332
358
|
const { text: answer } = await stream({
|
|
333
359
|
mode: state.mode,
|
|
334
360
|
message: text,
|
|
335
361
|
signal: abort.signal,
|
|
336
|
-
onStatus: (s) =>
|
|
362
|
+
onStatus: (s) => {
|
|
363
|
+
if (!printer.started) spinner.text = c.dim(" " + s);
|
|
364
|
+
},
|
|
365
|
+
onDelta: (d) => {
|
|
366
|
+
stopSpin();
|
|
367
|
+
printer.push(d);
|
|
368
|
+
},
|
|
337
369
|
});
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
printAnswer(answer,
|
|
370
|
+
stopSpin();
|
|
371
|
+
printer.flush();
|
|
372
|
+
if (!printer.started) printAnswer(answer, name, "#f59e0b");
|
|
341
373
|
return;
|
|
342
374
|
}
|
|
343
375
|
|
|
344
376
|
state.history.push({ role: "user", content: text });
|
|
345
|
-
|
|
346
|
-
|
|
377
|
+
startSpin(t.thinking);
|
|
378
|
+
let printer = null;
|
|
347
379
|
|
|
348
380
|
const answer = await runAgent({
|
|
349
381
|
history: state.history,
|
|
350
382
|
model: state.model.id,
|
|
351
383
|
signal: abort.signal,
|
|
352
384
|
onStatus: () => tick(t.thinking),
|
|
385
|
+
onDelta: (ev) => {
|
|
386
|
+
if (ev.type === "step-start") {
|
|
387
|
+
printer = makeStreamPrinter(state.model.name, providerColor(state.model.provider));
|
|
388
|
+
} else if (ev.type === "delta") {
|
|
389
|
+
if (printer.suppressing) return printer.push(ev.text); // nuốt tool JSON → để spinner chạy
|
|
390
|
+
stopSpin();
|
|
391
|
+
printer.push(ev.text);
|
|
392
|
+
if (printer.suppressing) startSpin(t.thinking); // vừa chuyển sang soạn tool
|
|
393
|
+
} else if (ev.type === "step-end") {
|
|
394
|
+
printer?.flush();
|
|
395
|
+
}
|
|
396
|
+
},
|
|
353
397
|
onTool: async (name, input) => {
|
|
354
|
-
|
|
355
|
-
spinner.stop();
|
|
398
|
+
stopSpin();
|
|
356
399
|
const res = await execTool(name, input);
|
|
357
|
-
|
|
358
|
-
timer = setInterval(() => tick(t.thinking), 200);
|
|
400
|
+
startSpin(t.thinking);
|
|
359
401
|
return res;
|
|
360
402
|
},
|
|
361
403
|
});
|
|
362
404
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
405
|
+
stopSpin();
|
|
406
|
+
// Lượt cuối không stream ra gì (vd model suy luận trả 1 cục) → in fallback.
|
|
407
|
+
if ((!printer || !printer.started) && answer?.trim())
|
|
408
|
+
printAnswer(answer, state.model.name, providerColor(state.model.provider));
|
|
366
409
|
} catch (err) {
|
|
367
|
-
|
|
368
|
-
spinner.stop();
|
|
410
|
+
stopSpin();
|
|
369
411
|
if (err.name === "AbortError") return;
|
|
370
412
|
printError(err);
|
|
371
413
|
} finally {
|
|
@@ -390,7 +432,7 @@ export async function startRepl(opts = {}) {
|
|
|
390
432
|
}
|
|
391
433
|
}
|
|
392
434
|
|
|
393
|
-
const sp = ora({ text: c.dim(" " + t.running), color: "yellow" }).start();
|
|
435
|
+
const sp = ora({ text: c.dim(" " + t.running), color: "yellow", discardStdin: false }).start();
|
|
394
436
|
try {
|
|
395
437
|
const result = await runTool(name, input);
|
|
396
438
|
sp.stop();
|
|
@@ -527,7 +569,7 @@ export async function startRepl(opts = {}) {
|
|
|
527
569
|
|
|
528
570
|
async function showUsage() {
|
|
529
571
|
if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
|
|
530
|
-
const sp = ora({ text: c.dim(" ..."), color: "magenta" }).start();
|
|
572
|
+
const sp = ora({ text: c.dim(" ..."), color: "magenta", discardStdin: false }).start();
|
|
531
573
|
try {
|
|
532
574
|
const u = await usage();
|
|
533
575
|
sp.stop();
|
|
@@ -586,6 +628,56 @@ function printAnswer(text, name, color) {
|
|
|
586
628
|
);
|
|
587
629
|
}
|
|
588
630
|
|
|
631
|
+
// In câu trả lời theo dòng token thời gian thực. Vì model emit lời + (tuỳ chọn)
|
|
632
|
+
// MỘT khối ```tool ở cuối, ta giấu mọi thứ từ ```tool trở đi (người dùng chỉ
|
|
633
|
+
// thấy phần lời + hoạt động công cụ riêng). Giữ lại đuôi vài ký tự để không in
|
|
634
|
+
// nửa vời "```to" trước khi kịp nhận ra đó là fence.
|
|
635
|
+
function makeStreamPrinter(name, color) {
|
|
636
|
+
let buf = "";
|
|
637
|
+
let printed = 0;
|
|
638
|
+
let suppress = false;
|
|
639
|
+
let started = false;
|
|
640
|
+
let header = false;
|
|
641
|
+
const HOLD = 8;
|
|
642
|
+
const write = (s) => {
|
|
643
|
+
if (!s) return;
|
|
644
|
+
if (!header) {
|
|
645
|
+
process.stdout.write("\n" + chalk.hex(color).bold(" ● " + name) + "\n ");
|
|
646
|
+
header = true;
|
|
647
|
+
}
|
|
648
|
+
process.stdout.write(s.replace(/\n/g, "\n "));
|
|
649
|
+
started = true;
|
|
650
|
+
};
|
|
651
|
+
return {
|
|
652
|
+
get started() {
|
|
653
|
+
return started;
|
|
654
|
+
},
|
|
655
|
+
get suppressing() {
|
|
656
|
+
return suppress;
|
|
657
|
+
},
|
|
658
|
+
push(delta) {
|
|
659
|
+
buf += delta;
|
|
660
|
+
if (suppress) return;
|
|
661
|
+
const f = buf.indexOf("```tool");
|
|
662
|
+
if (f !== -1) {
|
|
663
|
+
write(buf.slice(printed, f));
|
|
664
|
+
printed = buf.length;
|
|
665
|
+
suppress = true;
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const safeEnd = Math.max(printed, buf.length - HOLD);
|
|
669
|
+
if (safeEnd > printed) {
|
|
670
|
+
write(buf.slice(printed, safeEnd));
|
|
671
|
+
printed = safeEnd;
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
flush() {
|
|
675
|
+
if (!suppress && printed < buf.length) write(buf.slice(printed));
|
|
676
|
+
if (started) process.stdout.write("\n");
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
589
681
|
function printError(err) {
|
|
590
682
|
const map = {
|
|
591
683
|
missing_key: t.errMissingKey,
|