@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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/i18n.js +5 -2
  3. package/src/repl.js +72 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
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: "• Thao tác nguy hiểm sẽ hỏi phép, trừ khi bật yolo (Shift+Tab).",
65
- tip3: "• Ctrl+C 1 lần = dừng lượt hiện tại, 2 lần = thoát. CLI không tự tắt sau khi xong.",
67
+ tip2: "• Đang chạy vẫn 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
- const queue = []; // lines đã gõ/pipe, dùng theo thứ tự
48
- let waiter = null; // resolver đang chờ dòng kế tiếp
49
- let closeBurst = 0;
47
+ // Hàng đợi tin nhắn (giống Claude Code): khi model đang chạy, 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
- } else {
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 || !process.stdin.isTTY) return endInput(); // thoát thật
74
- const now = Date.now();
75
- if (now - closeAt > 1500) {
76
- closeAt = now;
77
- closeBurst = 0;
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 && !queue.length) return Promise.resolve(null);
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
- const raw = await ask(promptStr());
264
- if (raw == null) break; // stdin fully closed and drained
265
- const input = raw.trim();
283
+ let input;
284
+ if (pending.length) {
285
+ // 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 = ((await ask(c.tool(" cho phép? ") + c.dim("[y] có / [n] không / [a] luôn " + name + " › "))) ?? "n")
359
- .trim()
360
- .toLowerCase();
385
+ const a = await askPermission(name);
361
386
  if (a === "a") state.autoApprove.add(name);
362
- else if (a !== "y" && 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+/);