@noobdemon/noob-cli 1.7.1 → 1.7.2
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/bin/noob.js +18 -6
- package/package.json +1 -1
- package/src/agent.js +13 -12
- package/src/api.js +11 -1
- package/src/subagent.js +7 -1
- package/src/tokens.js +48 -15
- package/src/tools.js +47 -23
package/bin/noob.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { startRepl } from "../src/repl.js";
|
|
3
3
|
import { config } from "../src/config.js";
|
|
4
|
-
import { usage, ApiError } from "../src/api.js";
|
|
4
|
+
import { usage, ApiError, applyInsecureTLS } from "../src/api.js";
|
|
5
5
|
import { c } from "../src/ui.js";
|
|
6
6
|
import { t } from "../src/i18n.js";
|
|
7
7
|
import { checkLatest, runUpdate, CURRENT } from "../src/update.js";
|
|
@@ -27,6 +27,10 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
27
27
|
|
|
28
28
|
const sub = positional[0];
|
|
29
29
|
|
|
30
|
+
// Áp dụng --insecure-tls SAU khi parse argv (ESM import hoist khiến top-level
|
|
31
|
+
// check trong api.js chạy trước khi flag được set).
|
|
32
|
+
applyInsecureTLS();
|
|
33
|
+
|
|
30
34
|
// ── subcommands ──────────────────────────────────────────────────────────
|
|
31
35
|
if (sub === "login") {
|
|
32
36
|
const key = positional[1];
|
|
@@ -38,12 +42,20 @@ if (sub === "login") {
|
|
|
38
42
|
console.log(c.ok("✓ ") + t.loginSaved(config.path));
|
|
39
43
|
try {
|
|
40
44
|
const u = await usage();
|
|
41
|
-
if (u.ok)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
if (u.ok) {
|
|
46
|
+
console.log(c.ok(t.loginOk({ pro: "Pro", proplus: "Pro+", admin: "Admin", trial: "Trial" }[u.plan] || u.plan)));
|
|
47
|
+
process.exit(0);
|
|
48
|
+
} else {
|
|
49
|
+
// Key đã lưu xuống đĩa nhưng gateway từ chối — exit != 0 để user biết, script CI cũng bắt được.
|
|
50
|
+
console.log(c.err("✗ " + t.errInvalidKey));
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// Mạng/gateway down: giữ key (user có thể đang offline), nhưng KHÔNG im lặng.
|
|
55
|
+
console.log(c.err("⚠ không verify được key (mạng/gateway): " + (err?.message || err)));
|
|
56
|
+
console.log(c.dim(" key vẫn được lưu — chạy `noob usage` khi có mạng để xác nhận."));
|
|
57
|
+
process.exit(3);
|
|
45
58
|
}
|
|
46
|
-
process.exit(0);
|
|
47
59
|
} else if (sub === "logout") {
|
|
48
60
|
config.clearKey();
|
|
49
61
|
console.log(c.ok(t.loggedOut));
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -17,21 +17,22 @@ To call a tool, emit EXACTLY ONE fenced code block tagged \`tool\` containing a
|
|
|
17
17
|
|
|
18
18
|
Then STOP and wait — the runtime executes the tool and replies with a TOOL RESULT. Use one tool per step. When the task is complete (or you are only answering a question), reply normally in Markdown with NO tool block.
|
|
19
19
|
|
|
20
|
-
Available tools:
|
|
21
|
-
- read_file {"path": str, "offset"?: int, "limit"?: int} — read a file. The "N " line-number prefix in
|
|
22
|
-
- write_file {"path": str, "content": str} — create/overwrite a file
|
|
20
|
+
Available tools (each is self-contained; pick the SMALLEST tool that answers the question):
|
|
21
|
+
- read_file {"path": str, "offset"?: int, "limit"?: int} — read a file. Default reads whole file. For files you suspect are LARGE (>500 lines), first check size via list_dir/glob, then read with offset+limit (e.g. 200 lines at a time) instead of slurping. The "N " line-number prefix in output is DISPLAY ONLY — never copy it into edit_file.
|
|
22
|
+
- write_file {"path": str, "content": str} — create/overwrite a file. Use ONLY for new files or full rewrites; otherwise prefer edit_file.
|
|
23
23
|
- edit_file {"path": str, "old_string": str, "new_string": str, "replace_all"?: bool} — exact string replace. old_string must match the file's RAW text byte-for-byte (indentation/whitespace included, NO line-number prefix) and be unique unless replace_all. If a replace fails, re-read the file and copy the exact text.
|
|
24
|
-
- list_dir {"path"?: str} — list a directory
|
|
25
|
-
- glob {"pattern": str} — find files by glob (supports ** and *)
|
|
26
|
-
- grep {"pattern": str, "path"?: str, "glob"?: str} — regex search file contents
|
|
27
|
-
- run_command {"command": str, "timeout"?: int, "background"?: bool} — run a shell command in the cwd.
|
|
28
|
-
- bg_output {"id"?: int} — no id: list background processes + status; with id: show that process's captured output so far (poll
|
|
29
|
-
- kill_bg {"id": int} — stop a background process started with run_command background:true
|
|
24
|
+
- list_dir {"path"?: str} — list a directory. Use to map an unfamiliar project before reading anything.
|
|
25
|
+
- glob {"pattern": str} — find files by glob (supports ** and *). Use when you know the filename pattern but not its location.
|
|
26
|
+
- grep {"pattern": str, "path"?: str, "glob"?: str} — regex search file contents. Use to LOCATE code before reading; cheaper than reading whole files when hunting a symbol/string.
|
|
27
|
+
- run_command {"command": str, "timeout"?: int, "background"?: bool} — run a shell command in the cwd. Foreground commands are killed after ~60s (override with "timeout" ms). For long-running processes — dev servers, watchers, \`python -m http.server\`, \`npm run dev\`, \`flask run\` — set "background": true: starts the process, returns immediately, keeps running WITHOUT blocking next steps. Never start a server in the foreground (it will hang then be killed).
|
|
28
|
+
- bg_output {"id"?: int} — no id: list background processes + status; with id: show that process's captured output so far (poll after starting a server to confirm it came up).
|
|
29
|
+
- kill_bg {"id": int} — stop a background process started with run_command background:true.
|
|
30
|
+
|
|
31
|
+
# Retrieval strategy (just-in-time, not bulk)
|
|
32
|
+
Context is finite. Don't slurp the whole repo up front. Discover information progressively: list_dir/glob to map → grep to locate → read_file (with offset+limit for big files) to inspect only what matters. Each tool result spends your attention budget — make every call earn it. When a tool returns a huge blob, extract the few facts you need, then move on; don't re-read it later (the result stays in history).
|
|
30
33
|
|
|
31
34
|
# Rules
|
|
32
|
-
- GROUND TRUTH =
|
|
33
|
-
- NO FABRICATED VERIFICATION. Never claim a result you did not actually get from a real TOOL RESULT in THIS conversation. Do NOT say "tests pass" / "300/300" / "verified" / "done" / "100%" / "it works" unless a run_command TOOL RESULT above truly shows it. Do NOT reference or quote output that does not appear above — if you have not run the check, say so and RUN it; never narrate a result you only imagine.
|
|
34
|
-
- BEFORE any "finished/summary" message, RE-CHECK reality against the FILES CHANGED list: every file you are about to say you created/edited MUST appear there. If it is not there, you did NOT write it — emit the write_file/edit_file (or list_dir/read_file to confirm) NOW instead of claiming completion. When in doubt, list_dir/read_file the workspace and verify before asserting — do not assert from memory.
|
|
35
|
+
- GROUND TRUTH = real TOOL RESULTs in this conversation, not your memory or what you intended to do. A file changed only if a write_file/edit_file result confirms it (see the FILES CHANGED list). A test passed / build succeeded / command worked only if a run_command result above shows it. Never narrate outcomes you didn't observe; if you haven't checked, say so and check now (read_file / list_dir / run the command). Before any "done/summary" reply, reconcile every file and result you're about to claim against the actual tool results above — if it isn't there, you didn't do it yet.
|
|
35
36
|
- Investigate before editing: read the relevant files first; never invent file contents.
|
|
36
37
|
- Make the smallest change that fully solves the task. Match the surrounding code style.
|
|
37
38
|
- Prefer edit_file over write_file for existing files.
|
package/src/api.js
CHANGED
|
@@ -5,9 +5,19 @@ import { config } from "./config.js";
|
|
|
5
5
|
|
|
6
6
|
// Opt-in TLS escape hatch for machines behind a TLS-intercepting / broken-
|
|
7
7
|
// revocation proxy. Off by default. Prefer fixing the trust store.
|
|
8
|
-
|
|
8
|
+
// LƯU Ý: gọi từ bin/noob.js SAU khi parse argv — vì ESM import được hoist nên
|
|
9
|
+
// nếu chỉ check ở top-level, flag --insecure-tls set sau import sẽ không kịp.
|
|
10
|
+
let _tlsWarned = false;
|
|
11
|
+
export function applyInsecureTLS() {
|
|
12
|
+
if (process.env.NOOB_INSECURE_TLS !== "1") return;
|
|
9
13
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
14
|
+
if (_tlsWarned) return;
|
|
15
|
+
_tlsWarned = true;
|
|
16
|
+
// Cảnh báo rõ ràng: flag này tắt verify TLS TOÀN PROCESS. MITM-able.
|
|
17
|
+
console.warn("\x1b[33m⚠ NOOB_INSECURE_TLS=1: TLS verification DISABLED for this process. MITM-vulnerable. Unset when done.\x1b[0m");
|
|
10
18
|
}
|
|
19
|
+
// Vẫn áp dụng ngay nếu env có sẵn từ shell (export NOOB_INSECURE_TLS=1).
|
|
20
|
+
applyInsecureTLS();
|
|
11
21
|
|
|
12
22
|
function authHeaders() {
|
|
13
23
|
const h = { "Content-Type": "application/json" };
|
package/src/subagent.js
CHANGED
|
@@ -32,7 +32,13 @@ Ví dụ phân cấp: cha giao "build full app" → đẻ 1 sub-agent "build bac
|
|
|
32
32
|
// Chạy một sub-agent. dispatchTool: hàm để thực thi tool con (chia sẻ với cha).
|
|
33
33
|
// model: dùng chung model của cha. onLog: callback để log tiến độ ra UI cha.
|
|
34
34
|
export async function runSubAgent({ task, context, model, signal, dispatchTool, depth = 1, onLog, tokenMeter }) {
|
|
35
|
-
const sys = `Bạn là SUB-AGENT (depth=${depth}) được agent cha ủy thác MỘT nhiệm vụ cụ thể.
|
|
35
|
+
const sys = `Bạn là SUB-AGENT (depth=${depth}) được agent cha ủy thác MỘT nhiệm vụ cụ thể.
|
|
36
|
+
|
|
37
|
+
# Cách làm việc
|
|
38
|
+
- Tự quyết với thông tin được cấp + tự khám phá filesystem (list_dir/glob/grep/read_file). KHÔNG hỏi lại cha.
|
|
39
|
+
- History của bạn TÁCH BIỆT với cha. Cha CHỈ thấy chuỗi trả lời cuối của bạn → hãy là một bản tóm tắt cô đọng (mục tiêu 1–2k token): mọi file đã đụng, phát hiện then chốt, lỗi/cảnh báo, và các đầu mối cha cần để hành động tiếp. Bỏ chi tiết quá trình thừa.
|
|
40
|
+
- Làm điều nhỏ nhất giải quyết trọn vẹn nhiệm vụ. Không drive-by refactor.
|
|
41
|
+
- Verify khi hợp lý (chạy build/test/lint). Báo trung thực phần đã/chưa verify.
|
|
36
42
|
|
|
37
43
|
# NHIỆM VỤ
|
|
38
44
|
${task}
|
package/src/tokens.js
CHANGED
|
@@ -25,33 +25,64 @@ export function countMessages(messages = []) {
|
|
|
25
25
|
|
|
26
26
|
// Bộ đếm cộng dồn cho 1 phiên: input (prompt gửi đi) + output (text stream về).
|
|
27
27
|
// Hỗ trợ cộng dồn theo delta để hiển thị realtime trong lúc stream.
|
|
28
|
+
//
|
|
29
|
+
// Vấn đề cũ: pushOutputDelta encode TOÀN buffer mỗi delta → O(N²) trên stream dài.
|
|
30
|
+
// Fix: sliding window. BPE chỉ phụ thuộc context cục bộ (vài chục byte) nên ta
|
|
31
|
+
// chỉ cần encode đoạn TAIL gần đây, phần trước đó "commit" dứt khoát. Safety
|
|
32
|
+
// window đủ rộng (256 chars) để qua mọi ranh giới token thực tế của cl100k_base
|
|
33
|
+
// (token dài nhất ~ vài chục byte).
|
|
34
|
+
const TAIL_WINDOW = 256;
|
|
35
|
+
|
|
28
36
|
export class TokenMeter {
|
|
29
37
|
constructor() {
|
|
30
38
|
this.input = 0;
|
|
31
39
|
this.output = 0;
|
|
32
|
-
|
|
33
|
-
this.
|
|
40
|
+
// Phần đầu output đã "commit" — không đụng vào nữa.
|
|
41
|
+
this._committedChars = 0; // số ký tự đã đẩy qua khỏi tail window
|
|
42
|
+
this._committedTokens = 0; // tổng token tương ứng đã cộng vào this.output
|
|
43
|
+
// Tail buffer: TAIL_WINDOW ký tự cuối, dùng để re-encode khi có delta mới.
|
|
44
|
+
this._tail = "";
|
|
45
|
+
this._tailTokens = 0; // token count hiện tại của _tail (đã cộng vào this.output)
|
|
34
46
|
}
|
|
35
47
|
addInput(n) {
|
|
36
48
|
this.input += Math.max(0, n | 0);
|
|
37
49
|
}
|
|
38
|
-
// Mỗi delta text từ stream:
|
|
39
|
-
//
|
|
40
|
-
// gộp các byte qua ranh giới delta).
|
|
50
|
+
// Mỗi delta text từ stream: append vào tail, re-encode CHỈ tail (không toàn buffer).
|
|
51
|
+
// Nếu tail vượt 2×TAIL_WINDOW → commit nửa đầu, giữ lại nửa sau.
|
|
41
52
|
pushOutputDelta(text) {
|
|
42
53
|
if (!text) return;
|
|
43
|
-
this.
|
|
44
|
-
// Re-encode
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
this._tail += text;
|
|
55
|
+
// Re-encode tail → cập nhật phần token của tail.
|
|
56
|
+
const newTailTokens = countTokens(this._tail);
|
|
57
|
+
this.output += newTailTokens - this._tailTokens;
|
|
58
|
+
this._tailTokens = newTailTokens;
|
|
59
|
+
// Nếu tail quá lớn, commit nửa đầu để giữ tail bounded.
|
|
60
|
+
// Cắt tại ranh giới whitespace gần TAIL_WINDOW — BPE align tốt ở đây nên
|
|
61
|
+
// sai số head+tail vs encode liền mạch ≈ 0 (thay vì +1 token mỗi cut).
|
|
62
|
+
if (this._tail.length > TAIL_WINDOW * 2) {
|
|
63
|
+
let cutAt = this._tail.length - TAIL_WINDOW;
|
|
64
|
+
// Lùi cutAt về ranh giới whitespace gần nhất (tối đa 64 chars).
|
|
65
|
+
const minCut = Math.max(0, cutAt - 64);
|
|
66
|
+
while (cutAt > minCut && !/\s/.test(this._tail[cutAt])) cutAt--;
|
|
67
|
+
const head = this._tail.slice(0, cutAt);
|
|
68
|
+
const newTail = this._tail.slice(cutAt);
|
|
69
|
+
const headTokens = countTokens(head);
|
|
70
|
+
const newTailTokensAfterCut = countTokens(newTail);
|
|
71
|
+
// Điều chỉnh this.output để giữ tổng nhất quán với re-encode rời.
|
|
72
|
+
const delta = headTokens + newTailTokensAfterCut - this._tailTokens;
|
|
73
|
+
this.output += delta;
|
|
74
|
+
this._committedChars += head.length;
|
|
75
|
+
this._committedTokens += headTokens;
|
|
76
|
+
this._tail = newTail;
|
|
77
|
+
this._tailTokens = newTailTokensAfterCut;
|
|
49
78
|
}
|
|
50
79
|
}
|
|
51
80
|
// Kết thúc một lượt output → reset buffer (bắt đầu lượt mới).
|
|
52
81
|
endOutput() {
|
|
53
|
-
this.
|
|
54
|
-
this.
|
|
82
|
+
this._committedChars = 0;
|
|
83
|
+
this._committedTokens = 0;
|
|
84
|
+
this._tail = "";
|
|
85
|
+
this._tailTokens = 0;
|
|
55
86
|
}
|
|
56
87
|
get total() {
|
|
57
88
|
return this.input + this.output;
|
|
@@ -64,7 +95,9 @@ export class TokenMeter {
|
|
|
64
95
|
reset() {
|
|
65
96
|
this.input = 0;
|
|
66
97
|
this.output = 0;
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
98
|
+
this._committedChars = 0;
|
|
99
|
+
this._committedTokens = 0;
|
|
100
|
+
this._tail = "";
|
|
101
|
+
this._tailTokens = 0;
|
|
69
102
|
}
|
|
70
103
|
}
|
package/src/tools.js
CHANGED
|
@@ -29,8 +29,16 @@ function killBgTree(child) {
|
|
|
29
29
|
} catch {}
|
|
30
30
|
}
|
|
31
31
|
// Đừng để tiến trình nền sống mồ côi sau khi CLI thoát.
|
|
32
|
-
process.
|
|
32
|
+
// - 'exit' bắt được normal exit + process.exit() (gồm cả nhánh Ctrl+C lần 2 ở repl).
|
|
33
|
+
// - SIGTERM mặc định KHÔNG trigger 'exit' → process chết im, bg leak. Bắt riêng.
|
|
34
|
+
// - SIGINT đã có handler ở repl.js, không đụng vào để không phá UX "Ctrl+C lần 1 = abort".
|
|
35
|
+
function cleanupBg() {
|
|
33
36
|
for (const p of bg.values()) killBgTree(p.child);
|
|
37
|
+
}
|
|
38
|
+
process.on("exit", cleanupBg);
|
|
39
|
+
process.on("SIGTERM", () => {
|
|
40
|
+
cleanupBg();
|
|
41
|
+
process.exit(143);
|
|
34
42
|
});
|
|
35
43
|
|
|
36
44
|
export const TOOLS = {
|
|
@@ -74,7 +82,9 @@ export const TOOLS = {
|
|
|
74
82
|
if (count === 0) continue;
|
|
75
83
|
if (count > 1 && !replace_all)
|
|
76
84
|
throw new Error(`old_string is not unique (${count} matches) in ${rel(file)}; set replace_all, or add surrounding lines to make it unique`);
|
|
77
|
-
|
|
85
|
+
// LUÔN adapt new_string về line ending của file (kể cả khi cand match raw old_string),
|
|
86
|
+
// tránh tạo file mix CRLF/LF làm git/editor hiển thị diff lạ → user tưởng 'Edited nhưng không apply'.
|
|
87
|
+
await fs.writeFile(file, applyExact(cand, adapt(new_string)), "utf8");
|
|
78
88
|
return `Edited ${rel(file)} (${replace_all ? count : 1} replacement(s))`;
|
|
79
89
|
}
|
|
80
90
|
|
|
@@ -131,34 +141,48 @@ export const TOOLS = {
|
|
|
131
141
|
const rx = new RegExp(pattern, "i");
|
|
132
142
|
const gRx = g ? globToRegExp(g) : null;
|
|
133
143
|
const out = [];
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
function scanFile(full) {
|
|
145
|
+
const relp = rel(full).split(path.sep).join("/");
|
|
146
|
+
if (gRx && !gRx.test(relp)) return;
|
|
147
|
+
let txt;
|
|
136
148
|
try {
|
|
137
|
-
|
|
149
|
+
txt = fssync.readFileSync(full, "utf8");
|
|
138
150
|
} catch {
|
|
139
151
|
return;
|
|
140
152
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
if (txt.includes("\u0000")) return; // skip binary files
|
|
154
|
+
txt.split("\n").forEach((l, idx) => {
|
|
155
|
+
if (rx.test(l) && out.length < 200) out.push(`${relp}:${idx + 1}: ${l.trim().slice(0, 200)}`);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// path có thể là FILE hoặc DIR — stat trước để không nuốt câm khi user trỏ thẳng vào file.
|
|
159
|
+
let st;
|
|
160
|
+
try {
|
|
161
|
+
st = fssync.statSync(abs(p));
|
|
162
|
+
} catch {
|
|
163
|
+
return "No matches.";
|
|
164
|
+
}
|
|
165
|
+
if (st.isFile()) {
|
|
166
|
+
scanFile(abs(p));
|
|
167
|
+
} else {
|
|
168
|
+
(function walk(dir) {
|
|
169
|
+
let ents;
|
|
151
170
|
try {
|
|
152
|
-
|
|
171
|
+
ents = fssync.readdirSync(dir, { withFileTypes: true });
|
|
153
172
|
} catch {
|
|
154
|
-
|
|
173
|
+
return;
|
|
155
174
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
175
|
+
for (const e of ents) {
|
|
176
|
+
if (e.name === "node_modules" || e.name.startsWith(".git")) continue;
|
|
177
|
+
const full = path.join(dir, e.name);
|
|
178
|
+
if (e.isDirectory()) {
|
|
179
|
+
walk(full);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
scanFile(full);
|
|
183
|
+
}
|
|
184
|
+
})(abs(p));
|
|
185
|
+
}
|
|
162
186
|
return out.length ? clip(out.join("\n")) : "No matches.";
|
|
163
187
|
},
|
|
164
188
|
|