@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 +9 -0
- package/package.json +1 -1
- package/src/repl/todos.js +65 -24
- package/src/repl.js +17 -17
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
package/src/repl/todos.js
CHANGED
|
@@ -1,38 +1,79 @@
|
|
|
1
|
-
// Parse danh sách todo từ history hội thoại.
|
|
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
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Quan trọng: chỉ parse list todo CUỐI cùng — assistant message GẦN NHẤT có
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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ữ
|
|
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
|
|
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+/);
|