@noobdemon/noob-cli 1.12.14 → 1.12.16

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,12 @@
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.15] - 2026-06-16
6
+
7
+ ### Fixed
8
+ - **Todo display nuốt thanh status + đè ô nhập** (`src/tui.js` `topRow()`): todo branch return sớm vô điều kiện — lúc rảnh vẫn vẽ full progress + task + ctx (2-3 dòng) đè vùng trên ô nhập; lúc busy mất animated spinner. Fix: gỡ early-return, dựng `progress` (1 dòng) + `taskLine` sẵn rồi ghép theo trạng thái. Rảnh → 1 dòng progress gọn (📋 done/total bar %), không task/ctx line → không nuốt ô nhập. Busy → spinner + progress chung dòng 1, task line ▸ dòng 2 → giữ cả spinner LẪN tiến độ. Smoke `smoke-tui-render` tách 4a (rảnh: progress, KHÔNG task line) + 4b (busy: task line hiện) — 8/8 pass.
9
+ - **Test stale `tests/models.test.js` + leak isolation `tests/tools.test.js`**: models test hardcode catalog cũ (`gateway-claude-opus-4-7`, provider `google`) → cập nhật theo catalog hiện tại (`gateway-claude-opus-4-8`, `deepseek`). grep test fail do `extraRoots` module-global leak từ `loadWorkspaceRoots()` (nạp `.noob/dirs.json` chứa Temp dir) → thêm export `clearRoots()` gọi trong `beforeEach`. grep 17s → 997ms. 98/98 vitest pass.
10
+
5
11
  ## [1.12.14] - 2026-06-14
6
12
 
7
13
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.12.14",
3
+ "version": "1.12.16",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/agent.js CHANGED
@@ -170,6 +170,25 @@ function goalBlock(goal) {
170
170
  ].join('\n');
171
171
  }
172
172
 
173
+ // Mode hint (build/plan/compose) — chỉ NHẮC mềm, KHÔNG hard-gate tool. build =
174
+ // default (full, không thêm khối). plan/compose chèn 1 đoạn hướng dẫn nhẹ vào
175
+ // system prompt để model điều chỉnh phong cách làm việc.
176
+ function modeBlock(uiMode) {
177
+ if (uiMode === 'plan') {
178
+ return [
179
+ '# CHẾ ĐỘ PLAN',
180
+ 'Ưu tiên phân tích & đọc code trước. Đề xuất kế hoạch rõ ràng TRƯỚC khi sửa file. Tránh edit_file/write_file/run_command (thao tác thay đổi) trừ khi user đã xác nhận hướng đi. Mục tiêu: hiểu sâu & lên kế hoạch, không vội thực thi.',
181
+ ].join('\n');
182
+ }
183
+ if (uiMode === 'compose') {
184
+ return [
185
+ '# CHẾ ĐỘ COMPOSE',
186
+ 'Làm việc theo workflow có cấu trúc: (1) làm rõ specs/yêu cầu → (2) lập kế hoạch → (3) thực thi từng bước → (4) báo cáo kết quả. Giữ kỷ luật từng giai đoạn, không nhảy cóc.',
187
+ ].join('\n');
188
+ }
189
+ return '';
190
+ }
191
+
173
192
  // Môi trường chạy thực: model cần biết OS + shell để emit lệnh ĐÚNG. Không có
174
193
  // khối này, trên Windows model hay emit lệnh Unix (wc/ls/cat/grep) → run_command
175
194
  // (PowerShell) báo lỗi.
@@ -417,7 +436,7 @@ function relTime(ts) {
417
436
  // chèn ngay sau SYSTEM để model biết và dùng được.
418
437
  // recentSessions: breadcrumbs các phiên trước cùng workspace (repl.js cung cấp)
419
438
  // → chèn ngay sau memoryBlock() để model "thấy" lịch sử dù chưa /resume.
420
- export function buildSystem(history, extraToolsDoc, goal, recentSessions) {
439
+ export function buildSystem(history, extraToolsDoc, goal, recentSessions, uiMode) {
421
440
  const parts = [SYSTEM, '', memoryBlock()];
422
441
  // Auto-active skills (frontmatter `auto: true`) — luôn ON, không cần slash.
423
442
  // Đặt sau memoryBlock để model thấy convention skill TRƯỚC khi vào tool/goal.
@@ -427,6 +446,8 @@ export function buildSystem(history, extraToolsDoc, goal, recentSessions) {
427
446
  parts.push('', recentSessionsBlock(recentSessions));
428
447
  }
429
448
  if (goal && goal.trim()) parts.push('', goalBlock(goal));
449
+ const mode = modeBlock(uiMode);
450
+ if (mode) parts.push('', mode);
430
451
  if (extraToolsDoc) parts.push('', extraToolsDoc);
431
452
  parts.push('', runtimeContext());
432
453
  return parts.join('\n');
@@ -545,6 +566,7 @@ export async function runAgent({
545
566
  goal,
546
567
  recentSessions,
547
568
  pendingTasks,
569
+ uiMode,
548
570
  }) {
549
571
  // [GỠ BUDGET 2026-06-06] Không còn token budget enforcement. Agent/loop/sub-agent
550
572
  // chạy không giới hạn token. Dừng theo: GOAL đạt, <<LOOP_DONE>>, <<ULTRA_DONE>>,
@@ -573,7 +595,7 @@ export async function runAgent({
573
595
  await maybeSummarize(history, { model, signal });
574
596
  } catch {}
575
597
 
576
- const system = buildSystem(history, extraToolsDoc, goal, recentSessions);
598
+ const system = buildSystem(history, extraToolsDoc, goal, recentSessions, uiMode);
577
599
  const message = buildUserMessage(history);
578
600
  tokenMeter?.addInput(countTokens(system) + countTokens(message));
579
601
  tokenMeter?.setContext(tokenMeter.total);
package/src/api.js CHANGED
@@ -391,8 +391,7 @@ async function streamOnce({
391
391
  // UND_ERR_*). Tự throw nguyên dạng → streamWithRetry KHÔNG retry (vì check
392
392
  // err.name === 'ApiError'). Bọc thành ApiError retryable để được backoff.
393
393
  const causeCode = err?.cause?.code || err?.code;
394
- const isNetworkDrop =
395
- err?.name === 'TypeError' && /fetch failed/i.test(err?.message || '');
394
+ const isNetworkDrop = err?.name === 'TypeError' && /fetch failed/i.test(err?.message || '');
396
395
  const isSocketErr =
397
396
  typeof causeCode === 'string' &&
398
397
  /^(ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|ENOTFOUND|EPIPE|UND_ERR_)/i.test(
@@ -422,7 +421,12 @@ const USAGE_CACHE_MS = 90_000;
422
421
  export async function usage({ force = false } = {}) {
423
422
  const now = Date.now();
424
423
  const key = config.apiKey || '';
425
- if (!force && _usageCache.data && _usageCache.key === key && now - _usageCache.at < USAGE_CACHE_MS) {
424
+ if (
425
+ !force &&
426
+ _usageCache.data &&
427
+ _usageCache.key === key &&
428
+ now - _usageCache.at < USAGE_CACHE_MS
429
+ ) {
426
430
  return _usageCache.data;
427
431
  }
428
432
  const resp = await fetch(config.gatewayUrl + '/api/usage', { headers: authHeaders() });
package/src/diff.js CHANGED
@@ -32,7 +32,8 @@ function diffLines(a, b) {
32
32
  while (i > 0 && j > 0) {
33
33
  if (a[i - 1] === b[j - 1]) {
34
34
  ops.push({ type: 'eq', text: a[i - 1] });
35
- i--; j--;
35
+ i--;
36
+ j--;
36
37
  } else if (t[i - 1][j] >= t[i][j - 1]) {
37
38
  ops.push({ type: 'del', text: a[i - 1] });
38
39
  i--;
@@ -41,8 +42,12 @@ function diffLines(a, b) {
41
42
  j--;
42
43
  }
43
44
  }
44
- while (i > 0) { ops.push({ type: 'del', text: a[--i] }); }
45
- while (j > 0) { ops.push({ type: 'add', text: b[--j] }); }
45
+ while (i > 0) {
46
+ ops.push({ type: 'del', text: a[--i] });
47
+ }
48
+ while (j > 0) {
49
+ ops.push({ type: 'add', text: b[--j] });
50
+ }
46
51
  return ops.reverse();
47
52
  }
48
53
 
@@ -98,7 +103,11 @@ function groupHunks(ops, context = 3) {
98
103
 
99
104
  // Render unified diff với màu. `label` là tiêu đề (vd path file).
100
105
  // `maxLines` cap tổng số dòng render (mặc định 60) — diff quá dài hiện cuối `... +N dòng`.
101
- export function renderUnifiedDiff(oldText, newText, { label = '', context = 3, maxLines = 60 } = {}) {
106
+ export function renderUnifiedDiff(
107
+ oldText,
108
+ newText,
109
+ { label = '', context = 3, maxLines = 60 } = {}
110
+ ) {
102
111
  const a = oldText.split('\n');
103
112
  const b = newText.split('\n');
104
113
  const ops = diffLines(a, b);
package/src/i18n.js CHANGED
@@ -95,7 +95,8 @@ export const t = {
95
95
  ` noob.md : ${lines} dòng · ${bytes} · ${rules} rules · ${notes} notes · cập nhật ${ago}`,
96
96
  memoryStatsArchive: (lines, bytes, ago) =>
97
97
  ` noob-archive.md : ${lines} dòng · ${bytes} · cập nhật ${ago} (không inject vào prompt)`,
98
- memoryStatsArchiveMissing: ' noob-archive.md : (chưa có — tạo bằng tay nếu cần lưu Notes lịch sử)',
98
+ memoryStatsArchiveMissing:
99
+ ' noob-archive.md : (chưa có — tạo bằng tay nếu cần lưu Notes lịch sử)',
99
100
  memoryStatsOk: ' ✓ trong ngưỡng khuyến nghị (≤ 200 dòng / ≤ 20KB)',
100
101
  memoryStatsWarnLines: (n) =>
101
102
  ` ⚠ noob.md ${n} dòng > 200 — cân nhắc archive Notes cũ sang noob-archive.md để giảm token bloat mỗi turn`,
@@ -33,6 +33,22 @@ import { t } from '../i18n.js';
33
33
  */
34
34
  export function createAgentDispatcher(deps) {
35
35
  const { state, abort, tokenMeter, stopSpin, startSpin, execTool, tui, c } = deps;
36
+ // bgAgentLog: true → log sub-agent đi vào ring buffer (tui.pushAgentLog, xem
37
+ // bằng Ctrl+O), KHÔNG in ra UI chính. Dùng cho workflow nền (headless). False/
38
+ // thiếu → behavior cũ: in console.log ra UI chính (sub-agent foreground).
39
+ const bgAgentLog = !!deps.bgAgentLog;
40
+ // Route 1 dòng log sub-agent: nền → ring buffer; foreground → console.log.
41
+ const emitAgentLog = (line) => {
42
+ if (bgAgentLog) {
43
+ try {
44
+ tui?.pushAgentLog?.(line);
45
+ } catch {}
46
+ } else {
47
+ stopSpin?.();
48
+ console.log(chalk.hex('#8b5cf6')(' ' + line));
49
+ startSpin?.(t.thinking);
50
+ }
51
+ };
36
52
  // bgRegistry: tuỳ chọn — chỉ wire ở dispatcher của TURN (repl.js), KHÔNG ở bg
37
53
  // dispatcher (tránh đệ quy: workflow nền spawn workflow nền). Thiếu → tool báo lỗi.
38
54
  const bgRegistry = deps.bgRegistry || null;
@@ -68,7 +84,9 @@ export function createAgentDispatcher(deps) {
68
84
  // Flag: lượt này model đã dùng write_todos → repl skip parse markdown để
69
85
  // không overwrite structured state bằng parser fragile. Reset đầu mỗi turn.
70
86
  state._todosFromTool = true;
71
- try { tui?.setTodos?.(todos); } catch {}
87
+ try {
88
+ tui?.setTodos?.(todos);
89
+ } catch {}
72
90
  const done = todos.filter((t) => t.done).length;
73
91
  // In compact: lần đầu (prev rỗng) hoặc list thay đổi tập text → in full.
74
92
  // Nếu cùng tập text + chỉ khác trạng thái done → in diff (dòng vừa toggle).
@@ -105,7 +123,8 @@ export function createAgentDispatcher(deps) {
105
123
  if (!bgRegistry)
106
124
  return {
107
125
  allow: true,
108
- result: 'ERROR: workflow nền không khả dụng ở ngữ cảnh này (vd đang trong 1 workflow nền khác — không lồng).',
126
+ result:
127
+ 'ERROR: workflow nền không khả dụng ở ngữ cảnh này (vd đang trong 1 workflow nền khác — không lồng).',
109
128
  };
110
129
  if (!state.agentMode)
111
130
  return {
@@ -122,13 +141,14 @@ export function createAgentDispatcher(deps) {
122
141
  result: `ERROR: đã đạt ${bgRegistry.MAX_BG} workflow nền song song — chờ một run xong hoặc /workflow stop <id>.`,
123
142
  };
124
143
  if (r.error === 'empty_prompt')
125
- return { allow: true, result: 'ERROR: workflow cần field "prompt" (yêu cầu cho workflow).' };
144
+ return {
145
+ allow: true,
146
+ result: 'ERROR: workflow cần field "prompt" (yêu cầu cho workflow).',
147
+ };
126
148
  return { allow: true, result: 'ERROR khởi tạo workflow nền: ' + r.error };
127
149
  }
128
150
  stopSpin();
129
- console.log(
130
- chalk.hex('#8b5cf6')(` 🎼 workflow nền bắt đầu — run ${r.id}`)
131
- );
151
+ console.log(chalk.hex('#8b5cf6')(` 🎼 workflow nền bắt đầu — run ${r.id}`));
132
152
  startSpin(t.thinking);
133
153
  return {
134
154
  allow: true,
@@ -172,8 +192,7 @@ export function createAgentDispatcher(deps) {
172
192
  if (m) {
173
193
  subModel = m.id;
174
194
  modelTag = ` [${m.name}]`;
175
- } else
176
- modelTag = ` [model "${task.model}" không khớp — dùng ${state.model.name}]`;
195
+ } else modelTag = ` [model "${task.model}" không khớp — dùng ${state.model.name}]`;
177
196
  }
178
197
  const taskBody = task?.task || task?.prompt || '';
179
198
  const taskCtx = task?.context || '';
@@ -190,9 +209,7 @@ export function createAgentDispatcher(deps) {
190
209
  )
191
210
  );
192
211
  startSpin(t.thinking);
193
- return Promise.resolve(
194
- `── sub-agent #${i + 1}${modelTag} (cached) ──\n${cached}`
195
- );
212
+ return Promise.resolve(`── sub-agent #${i + 1}${modelTag} (cached) ──\n${cached}`);
196
213
  }
197
214
  recordWorkflowTaskStart(runData, {
198
215
  hash,
@@ -209,11 +226,7 @@ export function createAgentDispatcher(deps) {
209
226
  signal: abort.signal,
210
227
  tokenMeter,
211
228
  dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
212
- onLog: (msg) => {
213
- stopSpin();
214
- console.log(chalk.hex('#8b5cf6')(' ' + msg + modelTag));
215
- startSpin(t.thinking);
216
- },
229
+ onLog: (msg) => emitAgentLog(msg + modelTag),
217
230
  })
218
231
  .then((r) => {
219
232
  recordWorkflowTaskDone(runData, hash, r);
@@ -233,11 +246,7 @@ export function createAgentDispatcher(deps) {
233
246
  signal: abort.signal,
234
247
  tokenMeter,
235
248
  dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
236
- onLog: (msg) => {
237
- stopSpin();
238
- console.log(chalk.hex('#8b5cf6')(' ' + msg + modelTag));
239
- startSpin(t.thinking);
240
- },
249
+ onLog: (msg) => emitAgentLog(msg + modelTag),
241
250
  })
242
251
  .then((r) => `── sub-agent #${i + 1}${modelTag} ──\n${r}`)
243
252
  .catch(
@@ -22,6 +22,7 @@ export const SLASH = [
22
22
  { name: '/improve', desc: 'phân tích workspace & gợi ý tính năng cải thiện' },
23
23
  { name: '/ultra', desc: 'tự hành: tự nghĩ & làm nhiệm vụ' },
24
24
  { name: '/agent', desc: 'bật/tắt agent mode (spawn sub-agent)' },
25
+ { name: '/mode', desc: 'build|plan|compose — đổi chế độ agent (Ctrl+T cycle)' },
25
26
  { name: '/goal', desc: 'đặt HARD GOAL — model phải hướng tới tới khi /goal clear' },
26
27
  { name: '/loop', desc: 'chạy task định kỳ (vd: /loop 5m kiểm tra log) · /loop stop để dừng' },
27
28
  { name: '/tokens', desc: 'xem số token đã dùng phiên này' },
@@ -102,8 +102,7 @@ export async function askWorkflowAgentMode({ tui, ask, pending, c, t, truncate }
102
102
  console.log(
103
103
  c.tool(
104
104
  ' ' +
105
- (t.workflowAgentAskHint ||
106
- '🎼 /workflow cần spawn sub-agent — agent mode hiện đang TẮT.')
105
+ (t.workflowAgentAskHint || '🎼 /workflow cần spawn sub-agent — agent mode hiện đang TẮT.')
107
106
  )
108
107
  );
109
108
  try {
package/src/repl/state.js CHANGED
@@ -18,6 +18,7 @@ export function createState(opts = {}, config) {
18
18
  return {
19
19
  model: findModel(opts.model) || findModel(config.model) || findModel(DEFAULT_MODEL),
20
20
  mode: 'chat', // chat | merge | search
21
+ agentUiMode: 'build', // build | plan | compose — UI hint mode (Ctrl+T / /mode). KHÔNG đụng `mode`.
21
22
  history: [],
22
23
  autoApprove: new Set(), // tool name → 'a' (always, phiên)
23
24
  autoApproveTurn: new Set(), // tool name → 't' (this turn, reset sau mỗi runAgent)
@@ -15,12 +15,7 @@
15
15
  // đa dòng — flushCompleteLines phải HOLD các dòng cuối là table-row/sep
16
16
  // candidate để chờ đủ data trước khi parse.
17
17
  import chalk from 'chalk';
18
- import {
19
- renderMarkdown,
20
- renderInline,
21
- renderHeadingLine,
22
- renderBulletPrefix,
23
- } from '../ui.js';
18
+ import { renderMarkdown, renderInline, renderHeadingLine, renderBulletPrefix } from '../ui.js';
24
19
 
25
20
  function renderStreamLine(line, inCodeFence) {
26
21
  if (inCodeFence) return line; // code fence body: in raw, không parse markdown.
@@ -62,11 +57,7 @@ export function renderStreamBlock(lines, fenceState) {
62
57
  continue;
63
58
  }
64
59
  // Detect table: dòng hiện tại là row + dòng kế tiếp là separator.
65
- if (
66
- looksLikeTableRow(ln) &&
67
- i + 1 < lines.length &&
68
- TABLE_SEP_RE.test(lines[i + 1])
69
- ) {
60
+ if (looksLikeTableRow(ln) && i + 1 < lines.length && TABLE_SEP_RE.test(lines[i + 1])) {
70
61
  // Gom hết các dòng row liên tiếp.
71
62
  const tableLines = [ln, lines[i + 1]];
72
63
  let j = i + 2;
@@ -55,7 +55,9 @@ export function workflowHelp({ c, t }) {
55
55
  console.log(' /workflow run <name> [extra] chạy (built-in HOẶC saved, có thể thêm ngữ cảnh)');
56
56
  console.log(' /workflow runs list run journal đã chạy (resume-able)');
57
57
  console.log(' /workflow log <id> xem chi tiết 1 run (sub-agent task + kết quả)');
58
- console.log(' /workflow resume <id> chạy lại run, skip task đã done → tiết kiệm token');
58
+ console.log(
59
+ ' /workflow resume <id> chạy lại run, skip task đã done → tiết kiệm token'
60
+ );
59
61
  console.log(' /workflow active workflow nền đang chạy');
60
62
  console.log(' /workflow stop <id> | all huỷ workflow nền (resume được)');
61
63
  console.log(' /workflow delete|rm <name> xoá workflow đã lưu');