@noobdemon/noob-cli 1.12.0 → 1.12.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  Tất cả thay đổi đáng kể của `@noobdemon/noob-cli` được ghi vào file này.
4
4
 
5
+ ## [1.12.1] - 2026-06-11
6
+
7
+ ### Fixed
8
+ - **TDZ ReferenceError khi workflow / tool out-of-scope ask permission** (`src/repl.js`): 3 helper `askPermission`/`askAddRoot`/`askWorkflowAgentMode` trước đây khai báo `const` rải rác (line ~405/1648/1655) — SAU chỗ gọi đầu tiên trong scope `startRepl` → JS Temporal Dead Zone throw `Cannot access 'askAddRoot' before initialization`. Exception bị `uncaughtException` handler nuốt im lặng → user gõ `y` mà flow chết, workflow approve xong không chạy. Fix: gom 3 const lên line 147 ngay sau `function ask`, trước mọi function khác. Verify bằng E2E smoke spawn bin thật (`scripts/smoke-tdz.mjs`, 3/3 PASS).
9
+ - **Todo progress bar không reset khi chuyển task** (`src/repl/todos.js`): parser cũ scan toàn lịch sử rồi dedupe theo text → todo task A vẫn đếm vào bar dù model đã viết list mới cho task B (bar 5/10 nhưng reply chỉ có 3 todo). Fix: scan reverse, dừng ở assistant message **đầu tiên** có ≥1 dòng `- [ ]`/`- [x]` — coi đó là "todo list hiện tại", bỏ qua mọi message trước. Tự nhiên reset khi model viết list mới.
10
+
11
+ ### Changed
12
+ - **Todo parser hardening** (`src/repl/todos.js`): skip dòng nằm trong code fence ` ``` ` (paste README/snippet có checkbox không bị bắt nhầm), chấp nhận cả `[X]` viết hoa (cờ `/i` cho cả `RE_DONE` và `RE_TODO`), bỏ qua message có content kiểu non-string (multimodal array).
13
+
5
14
  ## [1.12.0] - 2026-06-11
6
15
 
7
16
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.12.0",
3
+ "version": "1.12.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/repl/todos.js CHANGED
@@ -1,38 +1,79 @@
1
- // Parse danh sách todo từ history hội thoại. Scan các assistant message tìm
2
- // pattern markdown `- [ ] task` và `- [x] task` (case-insensitive cho dấu x).
3
- // Pure function: chỉ phụ thuộc input `history`, không closure state.
1
+ // Parse danh sách todo từ history hội thoại.
4
2
  //
5
- // Dedupe: nếu cùng một text xuất hiện nhiều lần (model lặp todo qua các lượt),
6
- // giữ trạng thái CUỐI cùng phản ánh state hiện tại của model.
3
+ // Quan trọng: chỉ parse list todo CUỐI cùng assistant message GẦN NHẤT
4
+ // ít nhất 1 dòng `- [ ]` hoặc `- [x]` được coi là "todo list hiện tại". Mọi
5
+ // message trước đó bị bỏ qua.
6
+ //
7
+ // Lý do: trước đây scan toàn lịch sử rồi dedupe theo text → todo từ task CŨ
8
+ // vẫn đếm vào progress bar dù model đã chuyển sang task mới. Triệu chứng:
9
+ // bar 5/10 nhưng reply mới nhất chỉ có 3 todo. Cách mới tự nhiên reset khi
10
+ // model viết todo list mới, không bao giờ trộn todo từ 2 task khác nhau.
11
+ //
12
+ // Edge case đã xử lý:
13
+ // - Code fence ``` (cả với info string như ```md): bỏ qua dòng bên trong,
14
+ // tránh paste README/snippet có checkbox bị parser bắt nhầm thành todo.
15
+ // - Cả `[x]` và `[X]` (viết hoa) đều coi là done.
16
+ // - Multi-line message lặp cùng todo text: giữ trạng thái CUỐI.
17
+ //
18
+ // Pure function: chỉ phụ thuộc input `history`.
7
19
 
8
20
  /**
9
21
  * @typedef {{ text: string, done: boolean }} TodoItem
10
22
  */
11
23
 
24
+ // Regex tách riêng cho rõ ràng + dễ test. Cả 2 dùng cờ `i` để chấp nhận `[X]`
25
+ // (viết hoa) — markdown checkbox không phân biệt hoa thường trong thực tế.
26
+ const RE_DONE = /^\s*-\s*\[x\]\s+(.+)/i;
27
+ const RE_TODO = /^\s*-\s*\[\s?\]\s+(.+)/i;
28
+ // Code fence: ``` đầu dòng, có thể kèm info string (vd ```js, ```md).
29
+ const RE_FENCE = /^\s*```/;
30
+
12
31
  /**
13
- * Trích todo list từ history.
14
- * @param {Array<{role: string, content: any}>} history
32
+ * Parse các dòng todo trong 1 message content. Trả về [] nếu không có dòng nào.
33
+ * - Bỏ qua dòng nằm trong code fence (``` ... ```).
34
+ * - Trong cùng 1 message, nếu cùng text xuất hiện 2 lần (vd model lặp khi
35
+ * format), giữ trạng thái CUỐI — phản ánh ý định gần nhất.
36
+ * @param {string} content
15
37
  * @returns {TodoItem[]}
16
38
  */
17
- export function parseTodosFromHistory(history) {
18
- const todos = [];
19
- for (const m of history) {
20
- if (m.role !== 'assistant' || typeof m.content !== 'string') continue;
21
- const lines = m.content.split('\n');
22
- for (const line of lines) {
23
- const doneMatch = line.match(/^[\s]*-\s*\[x\]\s+(.+)/i);
24
- if (doneMatch) {
25
- todos.push({ text: doneMatch[1].trim(), done: true });
26
- continue;
27
- }
28
- const todoMatch = line.match(/^[\s]*-\s*\[\s?\]\s+(.+)/);
29
- if (todoMatch) {
30
- todos.push({ text: todoMatch[1].trim(), done: false });
31
- }
39
+ function parseTodosFromMessage(content) {
40
+ const out = [];
41
+ let inFence = false;
42
+ for (const line of content.split('\n')) {
43
+ if (RE_FENCE.test(line)) {
44
+ inFence = !inFence;
45
+ continue;
32
46
  }
47
+ if (inFence) continue;
48
+ const m1 = line.match(RE_DONE);
49
+ if (m1) {
50
+ out.push({ text: m1[1].trim(), done: true });
51
+ continue;
52
+ }
53
+ const m2 = line.match(RE_TODO);
54
+ if (m2) out.push({ text: m2[1].trim(), done: false });
33
55
  }
34
- // Dedupe: giữ item CUỐI cùng cho mỗi text (model thể lặp todo qua các lượt).
56
+ // Dedupe trong cùng message: giữ entry CUỐI cho mỗi text. Map<1 entry rẻ
57
+ // như không có — không cần micro-opt skip khi out.length<=1.
35
58
  const seen = new Map();
36
- for (const t of todos) seen.set(t.text, t);
59
+ for (const it of out) seen.set(it.text, it);
37
60
  return [...seen.values()];
38
61
  }
62
+
63
+ /**
64
+ * Trích todo list từ history. Chỉ trả về todo từ assistant message GẦN NHẤT
65
+ * chứa ít nhất 1 dòng todo. Mọi message trước đó bị bỏ qua.
66
+ * Bỏ qua message có content kiểu non-string (vd multimodal array) — chưa hỗ trợ.
67
+ * @param {Array<{role: string, content: any}>} history
68
+ * @returns {TodoItem[]}
69
+ */
70
+ export function parseTodosFromHistory(history) {
71
+ if (!Array.isArray(history)) return [];
72
+ for (let i = history.length - 1; i >= 0; i--) {
73
+ const m = history[i];
74
+ if (!m || m.role !== 'assistant' || typeof m.content !== 'string') continue;
75
+ const parsed = parseTodosFromMessage(m.content);
76
+ if (parsed.length) return parsed;
77
+ }
78
+ return [];
79
+ }
package/src/repl.js CHANGED
@@ -138,6 +138,23 @@ export async function startRepl(opts = {}) {
138
138
  return tui.read(prompt);
139
139
  }
140
140
 
141
+ // ── permission prompts (y/n/a) ────────────────────────────────────────
142
+ // PHẢI khai báo Ở ĐÂY (ngay sau `ask`), TRƯỚC bất kỳ function nào có thể
143
+ // gọi tới — trước đây 3 const này nằm rải rác (line ~405, ~1648, ~1655)
144
+ // gây TDZ ReferenceError: "Cannot access 'askAddRoot' before initialization"
145
+ // khi execToolCore / workflowExecute chạy lần đầu. Logic vẫn ở permission.js,
146
+ // chỉ wire prompt UI (tui/ask/pending/...) từ scope startRepl.
147
+ //
148
+ // ĐỪNG DI CHUYỂN khối này lên trên: dependencies cần sẵn — `tui` (~line 112),
149
+ // `pending` (~line 107), `ask` (~line 136), `c`/`t`/`truncate` (import top).
150
+ // Nếu dời lên trước `ask` sẽ lại TDZ. Vị trí hiện tại là điểm sớm nhất hợp lệ.
151
+ const askPermission = (name) =>
152
+ _askPermission(name, { tui, ask, pending, c, t, truncate });
153
+ const askAddRoot = (root, targetPath) =>
154
+ _askAddRoot(root, targetPath, { tui, ask, pending, c, t, truncate });
155
+ const askWorkflowAgentMode = () =>
156
+ _askWorkflowAgentMode({ tui, ask, pending, c, t, truncate });
157
+
141
158
  // NOOB_DEBUG=1: vạch trần đường thoát thật sự (xác nhận/loại bỏ giả thuyết
142
159
  // "event loop cạn"). 'beforeExit' nổ = loop cạn (stdin chết). 'exit' = thoát.
143
160
  if (process.env.NOOB_DEBUG === '1') {
@@ -399,11 +416,6 @@ Thực thi: đọc/tạo file cần thiết bằng tool, viết code production-
399
416
  await workflowExecute(trimmed);
400
417
  }
401
418
 
402
- // Hỏi quyền bật agent mode để chạy workflow. CHỈ chấp nhận y/n (Enter = yes).
403
- // Nếu nhận dòng lạ & dài (paste nhầm tin nhắn) → xếp hàng + hỏi lại (y hệt
404
- // askPermission / askAddRoot) để user khỏi phải gõ lại /workflow.
405
- const askWorkflowAgentMode = () => _askWorkflowAgentMode({ tui, ask, pending, c, t, truncate });
406
-
407
419
  // Chạy thật workflow prompt — chia sẻ giữa ad-hoc và `run <name>`.
408
420
  // `builtInName` (optional): nếu có thì SKIP loadSkill dynamic-workflows (prompt
409
421
  // built-in đã hardcode pattern + step cụ thể rồi, không cần model design lại).
@@ -1642,18 +1654,6 @@ NGUYÊN TẮC:
1642
1654
  }
1643
1655
  }
1644
1656
 
1645
- // Hỏi user có muốn cấp quyền folder ngoài workspace cho tool call hiện tại
1646
- // hay không. Trả về "y" | "n" | "a" (luôn). y = chỉ lần này; a = auto-approve
1647
- // mọi add-root trong phiên. Cùng style retry với askPermission (lạc → queue).
1648
- const askAddRoot = (root, targetPath) => _askAddRoot(root, targetPath, { tui, ask, pending, c, t, truncate });
1649
-
1650
- // Đọ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
1651
- // Enter = đồng ý). Nếu nhận một dòng lạ & dài — gần như chắc là một tin nhắn
1652
- // bị paste/gõ nhầm vào đúng lúc prompt hiện ra (đây là thủ phạm làm hỏng &
1653
- // "tự tắt" trước đây) — thì KHÔNG coi là từ chối: xếp nó vào hàng đợi tin
1654
- // 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.
1655
- const askPermission = (name) => _askPermission(name, { tui, ask, pending, c, t, truncate });
1656
-
1657
1657
  // ── slash commands ─────────────────────────────────────────────────────
1658
1658
  async function command(input) {
1659
1659
  const [cmd, ...rest] = input.slice(1).split(/\s+/);