@noobdemon/noob-cli 1.0.6 → 1.0.7
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/i18n.js +5 -2
- package/src/repl.js +72 -26
package/package.json
CHANGED
package/src/i18n.js
CHANGED
|
@@ -11,6 +11,9 @@ export const t = {
|
|
|
11
11
|
pressAgainToExit: "nhấn Ctrl+C lần nữa để thoát",
|
|
12
12
|
running: "đang chạy…",
|
|
13
13
|
denied: "đã từ chối",
|
|
14
|
+
queued: (n, txt) => `⏎ đã xếp hàng [${n}] · gửi khi model xong: ${txt}`,
|
|
15
|
+
queueCleared: (n) => `(đã xoá ${n} tin đang xếp hàng)`,
|
|
16
|
+
permRetry: "→ gõ y (đồng ý) · n (từ chối) · a (luôn cho phép)",
|
|
14
17
|
|
|
15
18
|
// auth
|
|
16
19
|
notLoggedIn:
|
|
@@ -61,8 +64,8 @@ export const t = {
|
|
|
61
64
|
cmdVersion: "/version /v xem version hiện tại + trạng thái yolo",
|
|
62
65
|
cmdExit: "/exit /quit thoát",
|
|
63
66
|
tip1: "• Mô tả việc cần làm; noob sẽ đọc/sửa file & chạy lệnh giúp bạn.",
|
|
64
|
-
tip2: "•
|
|
65
|
-
tip3: "• Ctrl+C 1 lần = dừng lượt
|
|
67
|
+
tip2: "• Đang chạy vẫn gõ tiếp được — tin sẽ xếp hàng & tự gửi khi model xong.",
|
|
68
|
+
tip3: "• Shift+Tab: bật/tắt yolo nhanh. Ctrl+C 1 lần = dừng lượt, 2 lần = thoát.",
|
|
66
69
|
|
|
67
70
|
// misc
|
|
68
71
|
yoloOn: "⚠ yolo BẬT — tự động duyệt mọi thao tác sửa file & chạy lệnh",
|
package/src/repl.js
CHANGED
|
@@ -44,9 +44,12 @@ export async function startRepl(opts = {}) {
|
|
|
44
44
|
let closed = false; // đã ngừng đọc vĩnh viễn
|
|
45
45
|
let exiting = false; // ta chủ động thoát (/exit, Ctrl+C ×2)
|
|
46
46
|
let lastPrompt = ""; // áp lại sau khi dựng lại interface
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
// Hàng đợi tin nhắn (giống Claude Code): khi model đang chạy, gõ thêm câu hỏi
|
|
48
|
+
// → xếp vào `pending`; xong turn, main loop tự lấy câu kế tiếp gửi lên. CHỈ
|
|
49
|
+
// main loop tiêu thụ `pending`. Câu trả lời permission đi qua `waiter` riêng,
|
|
50
|
+
// nên tin xếp hàng KHÔNG bị nhầm thành câu trả lời "cho phép?".
|
|
51
|
+
const pending = [];
|
|
52
|
+
let waiter = null; // resolver đang chờ một dòng tươi (prompt / permission)
|
|
50
53
|
let closeAt = 0;
|
|
51
54
|
|
|
52
55
|
function deliver(line) {
|
|
@@ -54,9 +57,11 @@ export async function startRepl(opts = {}) {
|
|
|
54
57
|
const w = waiter;
|
|
55
58
|
waiter = null;
|
|
56
59
|
w(line);
|
|
57
|
-
|
|
58
|
-
queue.push(line); // type-ahead / buffered — không bao giờ mất
|
|
60
|
+
return;
|
|
59
61
|
}
|
|
62
|
+
// Không ai đang hỏi → đây là tin xếp hàng cho lượt kế tiếp.
|
|
63
|
+
pending.push(line);
|
|
64
|
+
if (process.stdin.isTTY) console.log(c.dim(" " + t.queued(pending.length, truncate(line, 60))));
|
|
60
65
|
}
|
|
61
66
|
function endInput() {
|
|
62
67
|
closed = true;
|
|
@@ -70,31 +75,41 @@ export async function startRepl(opts = {}) {
|
|
|
70
75
|
const r = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "" });
|
|
71
76
|
r.on("line", deliver);
|
|
72
77
|
r.on("close", () => {
|
|
73
|
-
if (exiting
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
if (++closeBurst > 8) return endInput(); // stdin mất thật → khỏi quay vòng
|
|
80
|
-
// 'close' bất thường: dựng lại interface, hiện lại prompt; reader vẫn chờ.
|
|
81
|
-
rl = buildRl();
|
|
82
|
-
if (lastPrompt) {
|
|
83
|
-
rl.setPrompt(lastPrompt);
|
|
84
|
-
rl.prompt();
|
|
78
|
+
if (exiting) return endInput(); // ta chủ động thoát
|
|
79
|
+
// EOF THẬT: stdin đã end/destroy (Ctrl+Z trên Windows, Ctrl+D trên *nix),
|
|
80
|
+
// hoặc stdin không phải TTY (piped) đã đọc hết. Chỉ lúc đó mới dừng.
|
|
81
|
+
if (!process.stdin.isTTY || process.stdin.readableEnded || process.stdin.destroyed) {
|
|
82
|
+
return endInput();
|
|
85
83
|
}
|
|
84
|
+
// 'close' BẤT THƯỜNG trên một TTY còn sống (tranh chấp console khi
|
|
85
|
+
// paste/đa dòng, tiến trình con, v.v.) → KHÔNG BAO GIỜ thoát. Dựng lại
|
|
86
|
+
// interface và hiện lại prompt; reader đang chờ vẫn được giữ nguyên. Nếu
|
|
87
|
+
// close dồn dập thì hoãn 50ms để khỏi quay CPU — nhưng vẫn sống.
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const fast = now - closeAt < 50;
|
|
90
|
+
closeAt = now;
|
|
91
|
+
const rebuild = () => {
|
|
92
|
+
rl = buildRl();
|
|
93
|
+
if (lastPrompt) {
|
|
94
|
+
rl.setPrompt(lastPrompt);
|
|
95
|
+
rl.prompt();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
if (fast) setTimeout(rebuild, 50);
|
|
99
|
+
else rebuild();
|
|
86
100
|
});
|
|
87
101
|
return r;
|
|
88
102
|
}
|
|
89
103
|
rl = buildRl();
|
|
90
104
|
|
|
91
105
|
function nextLine() {
|
|
92
|
-
if (queue.length) return Promise.resolve(queue.shift());
|
|
93
106
|
if (closed) return Promise.resolve(null);
|
|
94
107
|
return new Promise((res) => (waiter = res));
|
|
95
108
|
}
|
|
109
|
+
// Dòng "tươi": dùng cho prompt chính VÀ cho permission. KHÔNG đụng tới
|
|
110
|
+
// `pending` (hàng đợi tin nhắn) — chỉ main loop mới rút từ `pending`.
|
|
96
111
|
function ask(prompt) {
|
|
97
|
-
if (closed
|
|
112
|
+
if (closed) return Promise.resolve(null);
|
|
98
113
|
lastPrompt = prompt;
|
|
99
114
|
rl.setPrompt(prompt);
|
|
100
115
|
rl.prompt();
|
|
@@ -124,6 +139,11 @@ export async function startRepl(opts = {}) {
|
|
|
124
139
|
if (abort) {
|
|
125
140
|
abort.abort();
|
|
126
141
|
abort = null;
|
|
142
|
+
if (pending.length) {
|
|
143
|
+
const n = pending.length;
|
|
144
|
+
pending.length = 0; // ngắt lượt → xoá luôn tin đang xếp hàng
|
|
145
|
+
console.log(c.dim(" " + t.queueCleared(n)));
|
|
146
|
+
}
|
|
127
147
|
console.log(c.err("\n ✗ " + t.interrupted));
|
|
128
148
|
return; // the main loop will redraw the prompt
|
|
129
149
|
}
|
|
@@ -260,9 +280,16 @@ export async function startRepl(opts = {}) {
|
|
|
260
280
|
|
|
261
281
|
// Main loop — runs until /exit, double Ctrl+C, or EOF. Never exits after a task.
|
|
262
282
|
while (true) {
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
|
|
283
|
+
let input;
|
|
284
|
+
if (pending.length) {
|
|
285
|
+
// Có tin xếp hàng → tự gửi câu kế tiếp (không chờ gõ).
|
|
286
|
+
input = (pending.shift() ?? "").trim();
|
|
287
|
+
if (process.stdin.isTTY && input) console.log(promptStr() + input);
|
|
288
|
+
} else {
|
|
289
|
+
const raw = await ask(promptStr());
|
|
290
|
+
if (raw == null) break; // stdin fully closed and drained
|
|
291
|
+
input = raw.trim();
|
|
292
|
+
}
|
|
266
293
|
if (!input) continue;
|
|
267
294
|
// Bọc cả lượt: một lỗi trong xử lý lệnh/agent không được phép thoát ra
|
|
268
295
|
// ngoài vòng lặp (sẽ rơi vào .catch ở bin/noob.js → process.exit(1) =
|
|
@@ -355,11 +382,9 @@ export async function startRepl(opts = {}) {
|
|
|
355
382
|
else if (name === "edit_file") preview(`- ${truncate(input.old_string)}\n+ ${truncate(input.new_string)}`, input.path);
|
|
356
383
|
|
|
357
384
|
if (DESTRUCTIVE.has(name) && !state.yolo && !state.autoApprove.has(name)) {
|
|
358
|
-
const a =
|
|
359
|
-
.trim()
|
|
360
|
-
.toLowerCase();
|
|
385
|
+
const a = await askPermission(name);
|
|
361
386
|
if (a === "a") state.autoApprove.add(name);
|
|
362
|
-
else if (a
|
|
387
|
+
else if (a === "n") {
|
|
363
388
|
console.log(c.err(" " + t.denied));
|
|
364
389
|
return { allow: false };
|
|
365
390
|
}
|
|
@@ -378,6 +403,27 @@ export async function startRepl(opts = {}) {
|
|
|
378
403
|
}
|
|
379
404
|
}
|
|
380
405
|
|
|
406
|
+
// Đọc câu trả lời cho phép một cách CHẮC CHẮN. Chỉ chấp nhận y/n/a (hoặc
|
|
407
|
+
// Enter = đồng ý). Nếu nhận một dòng lạ & dài — gần như chắc là một tin nhắn
|
|
408
|
+
// bị paste/gõ nhầm vào đúng lúc prompt hiện ra (đây là thủ phạm làm hỏng &
|
|
409
|
+
// "tự tắt" trước đây) — thì KHÔNG coi là từ chối: xếp nó vào hàng đợi tin
|
|
410
|
+
// nhắn rồi HỎI LẠI. Nhờ vậy không thao tác nào bị quyết định bởi rác.
|
|
411
|
+
async function askPermission(name) {
|
|
412
|
+
while (true) {
|
|
413
|
+
const raw = await ask(c.tool(" cho phép? ") + c.dim("[y] có / [n] không / [a] luôn " + name + " › "));
|
|
414
|
+
if (raw == null) return "n"; // stdin đóng thật
|
|
415
|
+
const a = raw.trim().toLowerCase();
|
|
416
|
+
if (a === "" || a === "y" || a === "yes" || a === "có") return "y";
|
|
417
|
+
if (a === "n" || a === "no" || a === "không") return "n";
|
|
418
|
+
if (a === "a" || a === "always" || a === "luôn") return "a";
|
|
419
|
+
if (raw.trim().length > 3) {
|
|
420
|
+
pending.push(raw); // tin nhắn lạc → xếp hàng, gửi sau khi xong lượt
|
|
421
|
+
console.log(c.dim(" " + t.queued(pending.length, truncate(raw, 60))));
|
|
422
|
+
}
|
|
423
|
+
console.log(c.dim(" " + t.permRetry));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
381
427
|
// ── slash commands ─────────────────────────────────────────────────────
|
|
382
428
|
async function command(input) {
|
|
383
429
|
const [cmd, ...rest] = input.slice(1).split(/\s+/);
|