@noobdemon/noob-cli 1.10.20 → 1.11.0

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/src/repl.js CHANGED
@@ -1,142 +1,54 @@
1
- import process from "node:process";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import chalk from "chalk";
5
- import { createTui } from "./tui.js";
6
- import { runAgent, maybeSummarize, buildSystem, buildUserMessage } from "./agent.js";
7
- import { runSubAgent, spawnAgentToolsDoc, MAX_SUBAGENT_DEPTH } from "./subagent.js";
8
- import { TokenMeter, countMessages, CONTEXT_WINDOW, countTokens } from "./tokens.js";
9
- import { stream, usage, ApiError, resetMemoryToken } from "./api.js";
10
- import { runTool, describe, DESTRUCTIVE, addRoot, removeRoot, listRoots, OutOfScopeError, nearestExistingDir } from "./tools.js";
11
- import { MODELS, PROVIDERS, findModel, providerColor, DEFAULT_MODEL } from "./models.js";
12
- import { c, banner, modelBadge, renderMarkdown, box } from "./ui.js";
13
- import { config } from "./config.js";
14
- import { loadMemory, memoryPath, memoryStats } from "./memory.js";
15
- import { t } from "./i18n.js";
16
- import { checkLatest, runUpdate, CURRENT } from "./update.js";
17
- import * as sessions from "./sessions.js";
18
- import { loadSkill, listSkills } from "./skills.js";
19
- import { saveWorkflow, loadWorkflow, listWorkflows, deleteWorkflow, workflowsDir } from "./workflows.js";
20
- import { getBuiltinWorkflow, listBuiltinWorkflows, loadBuiltinPrompt } from "./workflows-builtin.js";
21
-
22
-
23
- // Lệnh dùng cho autocomplete. "/l" → lọc các lệnh có "l" (login, logout,
24
- // clear, models, yolo…); ↑/↓ chọn, Tab điền, Enter chạy mục đang sáng.
25
- const SLASH = [
26
- { name: "/help", desc: "danh sách lệnh" },
27
- { name: "/model", desc: "đổi hình" },
28
- { name: "/models", desc: "liệt mô hình" },
29
- { name: "/merge", desc: "bật/tắt Merge AI" },
30
- { name: "/search", desc: "bật/tắt tìm web" },
31
- { name: "/chat", desc: "chế độ chat thường" },
32
- { name: "/yolo", desc: "bật/tắt tự duyệt" },
33
- { name: "/auto-yolo", desc: "lưu yolo làm mặc định (cần xác nhận)" },
34
- { name: "/init", desc: "quét dự án & tạo noob.md" },
35
- { name: "/karpathy", desc: "rà soát code (Karpathy)" },
36
- { name: "/frontend-design", desc: "thiết kế UI frontend chất lượng cao (skill)" },
37
- { name: "/workflow", desc: "orchestrate multi-agent dynamic workflow (skill)" },
38
- { name: "/improve", desc: "phân tích workspace & gợi ý tính năng cải thiện" },
39
- { name: "/ultra", desc: "tự hành: tự nghĩ & làm nhiệm vụ" },
40
- { name: "/agent", desc: "bật/tắt agent mode (spawn sub-agent)" },
41
- { name: "/goal", desc: "đặt HARD GOAL — model phải hướng tới tới khi /goal clear" },
42
- { name: "/loop", desc: "chạy task định kỳ (vd: /loop 5m kiểm tra log) · /loop stop để dừng" },
43
- { name: "/tokens", desc: "xem số token đã dùng phiên này" },
44
- { name: "/learn", desc: "chưng cất bài học vào noob.md" },
45
- { name: "/memory", desc: "xem bộ nhớ noob.md" },
46
- { name: "/login", desc: "đăng nhập bằng API key" },
47
- { name: "/logout", desc: "đăng xuất" },
48
- { name: "/usage", desc: "xem hạn mức key" },
49
- { name: "/update", desc: "cập nhật noob" },
50
- { name: "/clear", desc: "xoá ngữ cảnh / phiên mới" },
51
- { name: "/resume", desc: "tiếp tục phiên cũ" },
52
- { name: "/continue", desc: "tiếp tục phiên gần nhất" },
53
- { name: "/sessions", desc: "liệt kê phiên đã lưu" },
54
- { name: "/cwd", desc: "thư mục hiện tại" },
55
- { name: "/add-dir", desc: "thêm thư mục ngoài cwd vào phạm vi" },
56
- { name: "/status", desc: "trạng thái" },
57
- { name: "/version", desc: "phiên bản" },
58
- { name: "/exit", desc: "thoát" },
59
- ];
60
- // Danh sách file trong cwd, cache 5s (gõ @ mỗi phím KHÔNG quét lại đĩa).
61
- let fileCache = { at: 0, list: [] };
62
- function allFiles() {
63
- if (Date.now() - fileCache.at < 5000) return fileCache.list;
64
- const out = [];
65
- const root = process.cwd();
66
- (function walk(dir, depth) {
67
- if (out.length > 4000 || depth > 8) return;
68
- let ents;
69
- try {
70
- ents = fs.readdirSync(dir, { withFileTypes: true });
71
- } catch {
72
- return;
73
- }
74
- for (const e of ents) {
75
- if (e.name === "node_modules" || e.name.startsWith(".git")) continue;
76
- const full = path.join(dir, e.name);
77
- if (e.isDirectory()) walk(full, depth + 1);
78
- else out.push(path.relative(root, full).split(path.sep).join("/"));
79
- if (out.length > 4000) return;
80
- }
81
- })(root, 0);
82
- fileCache = { at: Date.now(), list: out };
83
- return out;
84
- }
85
- // Xếp hạng: tên file khớp đầu > đầu một đoạn path > chứa trong tên > chứa bất kỳ.
86
- function fileMatches(frag) {
87
- const all = allFiles();
88
- const q = frag.toLowerCase();
89
- if (!q) return all.slice(0, 12);
90
- const scored = [];
91
- for (const f of all) {
92
- const lf = f.toLowerCase();
93
- const base = lf.split("/").pop();
94
- let s = -1;
95
- if (base.startsWith(q)) s = 0;
96
- else if (lf.includes("/" + q)) s = 1;
97
- else if (base.includes(q)) s = 2;
98
- else if (lf.includes(q)) s = 3;
99
- if (s >= 0) scored.push([s, f]);
100
- }
101
- scored.sort((a, b) => a[0] - b[0] || a[1].length - b[1].length);
102
- return scored.slice(0, 12).map((x) => x[1]);
103
- }
104
-
105
- // Gợi ý cho thanh nhập: /lệnh (điền-rồi-gửi) hoặc @file (chỉ chèn, gõ tiếp).
106
- function completeInput(text) {
107
- if (text.startsWith("/") && !/\s/.test(text)) {
108
- const q = text.slice(1).toLowerCase();
109
- const items = SLASH.filter((cmd) => cmd.name.slice(1).toLowerCase().includes(q));
110
- return items.length ? { items, start: 0, fill: "submit" } : null;
111
- }
112
- // @file: token CUỐI bắt đầu bằng @ (đầu dòng hoặc sau khoảng trắng).
113
- const m = text.match(/(?:^|\s)@([^\s]*)$/);
114
- if (m) {
115
- const start = text.length - m[1].length - 1; // vị trí dấu '@'
116
- const items = fileMatches(m[1]).map((p) => ({ name: "@" + p, desc: "file" }));
117
- return items.length ? { items, start, fill: "insert" } : null;
118
- }
119
- return null;
120
- }
121
-
122
- // File thật được nhắc bằng @ trong tin nhắn → thêm chú thích để model đọc nhanh,
123
- // đúng chỗ (bỏ qua @ không trỏ tới file có thật, vd @tên người).
124
- function mentionedFiles(text) {
125
- const out = new Set();
126
- const re = /(?:^|\s)@([^\s]+)/g;
127
- let m;
128
- while ((m = re.exec(text))) {
129
- try {
130
- if (fs.existsSync(path.resolve(process.cwd(), m[1]))) out.add(m[1]);
131
- } catch {}
132
- }
133
- return [...out];
134
- }
135
-
1
+ import process from 'node:process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import chalk from 'chalk';
5
+ import { createTui } from './tui.js';
6
+ import { runAgent, maybeSummarize, buildSystem, buildUserMessage } from './agent.js';
7
+ import { runSubAgent, spawnAgentToolsDoc, MAX_SUBAGENT_DEPTH } from './subagent.js';
8
+ import { TokenMeter, countMessages, CONTEXT_WINDOW, countTokens } from './tokens.js';
9
+ import { stream, usage, ApiError, resetMemoryToken } from './api.js';
10
+ import {
11
+ runTool,
12
+ describe,
13
+ DESTRUCTIVE,
14
+ addRoot,
15
+ removeRoot,
16
+ listRoots,
17
+ OutOfScopeError,
18
+ nearestExistingDir,
19
+ } from './tools.js';
20
+ import { MODELS, PROVIDERS, findModel, providerColor, DEFAULT_MODEL } from './models.js';
21
+ import { c, banner, modelBadge, renderMarkdown, box } from './ui.js';
22
+ import { config } from './config.js';
23
+ import { loadMemory, memoryPath, memoryStats } from './memory.js';
24
+ import { t } from './i18n.js';
25
+ import { checkLatest, runUpdate, CURRENT } from './update.js';
26
+ import * as sessions from './sessions.js';
27
+ import { loadSkill, listSkills } from './skills.js';
28
+ import { saveWorkflow, loadWorkflow } from './workflows.js';
29
+ import { getBuiltinWorkflow, loadBuiltinPrompt } from './workflows-builtin.js';
30
+ import { SLASH, completeInput, mentionedFiles } from './repl/complete.js';
31
+ import { parseTodosFromHistory } from './repl/todos.js';
32
+ import {
33
+ ULTRA_DONE,
34
+ MAX_QUESTS,
35
+ ultraIsDone,
36
+ ultraLooksStuck,
37
+ ultraStart,
38
+ ultraContinue,
39
+ } from './repl/ultra.js';
40
+ import {
41
+ workflowHelp as _workflowHelp,
42
+ workflowPatterns as _workflowPatterns,
43
+ workflowBuiltins as _workflowBuiltins,
44
+ workflowList as _workflowList,
45
+ workflowLoad as _workflowLoad,
46
+ workflowDelete as _workflowDelete,
47
+ } from './repl/workflow-commands.js';
136
48
  export async function startRepl(opts = {}) {
137
49
  const state = {
138
50
  model: findModel(opts.model) || findModel(config.model) || findModel(DEFAULT_MODEL),
139
- mode: "chat", // chat | merge | search
51
+ mode: 'chat', // chat | merge | search
140
52
  history: [],
141
53
  autoApprove: new Set(),
142
54
  yolo: !!opts.yolo || config.yoloDefault, // cờ --yolo HOẶC mặc định đã lưu (/auto-yolo)
@@ -145,7 +57,7 @@ export async function startRepl(opts = {}) {
145
57
  goal: null, // HARD GOAL (set qua /goal <text>) — inject vào mọi prompt tới khi /goal clear
146
58
  loop: null, // /loop — {intervalMs, intervalStr, task, timer, ticks, startedAt} hoặc null
147
59
  extraRoots: [], // thư mục ngoài cwd được /add-dir cấp quyền (UX display only;
148
- // source of truth là extraRoots trong src/tools.js)
60
+ // source of truth là extraRoots trong src/tools.js)
149
61
  _longSessionWarned: false, // đã in cảnh báo phiên dài chưa (reset khi /clear)
150
62
  todos: [], // [{text, done}] — todo list parse từ model output, render trên status bar
151
63
  };
@@ -155,7 +67,7 @@ export async function startRepl(opts = {}) {
155
67
  // session chưa được khai báo ở đây → updateTitle() gọi SAU khi session = null (dòng ~295).
156
68
  const updateTitle = () => {
157
69
  const name = session?.title
158
- ? session.title.slice(0, 40) + (session.title.length > 40 ? "" : "")
70
+ ? session.title.slice(0, 40) + (session.title.length > 40 ? '' : '')
159
71
  : state.model.name;
160
72
  process.title = `noob — ${name}`;
161
73
  };
@@ -163,9 +75,9 @@ export async function startRepl(opts = {}) {
163
75
  // Prompt = dòng trạng thái sống. Luôn phản ánh yolo + version theo thời gian
164
76
  // thực (vẽ lại mỗi lượt và ngay khi Shift+Tab), nên không cần gõ /status.
165
77
  const promptStr = (lead = true) => {
166
- const nl = lead ? "\n" : "";
167
- const yolo = state.yolo ? c.err("⚡ yolo ") : "";
168
- return c.user(nl + t.promptYou) + yolo + c.dim("v" + CURRENT + "");
78
+ const nl = lead ? '\n' : '';
79
+ const yolo = state.yolo ? c.err('⚡ yolo ') : '';
80
+ return c.user(nl + t.promptYou) + yolo + c.dim('v' + CURRENT + '');
169
81
  };
170
82
 
171
83
  // ── Input layer — KHÔNG ĐƯỢC tự tắt ───────────────────────────────────────
@@ -191,7 +103,11 @@ export async function startRepl(opts = {}) {
191
103
  // Submit khi KHÔNG có read() đang chờ = tin xếp hàng. Đang chạy task → sẽ
192
104
  // CHÈN cho AI ở bước kế tiếp (steering); rảnh → gửi như lượt mới.
193
105
  pending.push(line);
194
- tui.print(abort ? c.user(" " + t.steerWillInject(truncate(line, 60))) : c.dim(" " + t.queued(pending.length, truncate(line, 60))));
106
+ tui.print(
107
+ abort
108
+ ? c.user(' ' + t.steerWillInject(truncate(line, 60)))
109
+ : c.dim(' ' + t.queued(pending.length, truncate(line, 60)))
110
+ );
195
111
  },
196
112
  onInterrupt: () => interrupt(),
197
113
  onEOF: () => {
@@ -199,7 +115,7 @@ export async function startRepl(opts = {}) {
199
115
  },
200
116
  onShiftTab: () => {
201
117
  state.yolo = !state.yolo;
202
- tui.print(state.yolo ? c.err(" " + t.yoloOn) : c.ok(" " + t.yoloOff));
118
+ tui.print(state.yolo ? c.err(' ' + t.yoloOn) : c.ok(' ' + t.yoloOff));
203
119
  tui.setPrompt(promptStr(false));
204
120
  },
205
121
  completer: completeInput,
@@ -213,10 +129,16 @@ export async function startRepl(opts = {}) {
213
129
 
214
130
  // NOOB_DEBUG=1: vạch trần đường thoát thật sự (xác nhận/loại bỏ giả thuyết
215
131
  // "event loop cạn"). 'beforeExit' nổ = loop cạn (stdin chết). 'exit' = thoát.
216
- if (process.env.NOOB_DEBUG === "1") {
217
- process.stderr.write(` [debug] isTTY=${process.stdin.isTTY} platform=${process.platform} node=${process.version}\n`);
218
- process.on("beforeExit", (code) => process.stderr.write(` [debug] beforeExit code=${code} — EVENT LOOP CẠN (stdin chết)\n`));
219
- process.on("exit", (code) => process.stderr.write(` [debug] exit code=${code} closed=${closed} exiting=${exiting}\n`));
132
+ if (process.env.NOOB_DEBUG === '1') {
133
+ process.stderr.write(
134
+ ` [debug] isTTY=${process.stdin.isTTY} platform=${process.platform} node=${process.version}\n`
135
+ );
136
+ process.on('beforeExit', (code) =>
137
+ process.stderr.write(` [debug] beforeExit code=${code} — EVENT LOOP CẠN (stdin chết)\n`)
138
+ );
139
+ process.on('exit', (code) =>
140
+ process.stderr.write(` [debug] exit code=${code} closed=${closed} exiting=${exiting}\n`)
141
+ );
220
142
  }
221
143
 
222
144
  let abort = null; // active turn controller
@@ -239,14 +161,14 @@ export async function startRepl(opts = {}) {
239
161
  abort = null;
240
162
  if (state.ultra) {
241
163
  state.ultra = false; // Ctrl+C cũng dừng vòng tự hành, không chỉ lượt hiện tại
242
- console.log(c.tool(" " + t.ultraStopped));
164
+ console.log(c.tool(' ' + t.ultraStopped));
243
165
  }
244
166
  if (pending.length) {
245
167
  const n = pending.length;
246
168
  pending.length = 0; // ngắt lượt → xoá luôn tin đang xếp hàng
247
- console.log(c.dim(" " + t.queueCleared(n)));
169
+ console.log(c.dim(' ' + t.queueCleared(n)));
248
170
  }
249
- console.log(c.err("\n ✗ " + t.interrupted));
171
+ console.log(c.err('\n ✗ ' + t.interrupted));
250
172
  sigintArmed = false;
251
173
  if (sigintTimer) {
252
174
  clearTimeout(sigintTimer);
@@ -261,32 +183,35 @@ export async function startRepl(opts = {}) {
261
183
  if (sigintTimer) clearTimeout(sigintTimer);
262
184
  exiting = true;
263
185
  persist();
264
- console.log(c.dim("\n " + t.bye));
186
+ console.log(c.dim('\n ' + t.bye));
265
187
  tui.close(); // khôi phục terminal (raw mode/paste/stdout) trước khi thoát
266
188
  process.exit(0);
267
189
  }
268
190
  // lần 1 → vũ trang, đếm 1.5s
269
191
  sigintArmed = true;
270
- console.log(c.dim("\n " + t.pressAgainToExit));
192
+ console.log(c.dim('\n ' + t.pressAgainToExit));
271
193
  sigintTimer = setTimeout(() => {
272
194
  sigintArmed = false;
273
195
  sigintTimer = null;
274
196
  }, 1500);
275
197
  if (!closed) tui.setPrompt(promptStr(false));
276
198
  }
277
- process.on("SIGINT", interrupt);
199
+ process.on('SIGINT', interrupt);
278
200
 
279
201
  // Đừng để một lỗi bất ngờ làm "tự động tắt" CLI. Nguyên nhân hay gặp:
280
202
  // tiến trình cập nhật nền (spawn npm) phát sự kiện 'error' không ai bắt,
281
203
  // hoặc lỗi async trong một lượt → Node thoát ngay. Ở đây bắt lại, in ra,
282
204
  // rồi vẽ lại prompt để phiên làm việc vẫn sống.
283
- process.on("uncaughtException", (err) => {
284
- if (abort) { abort.abort(); abort = null; }
285
- console.log(c.err("\n ✗ lỗi: " + (err?.message || err)));
205
+ process.on('uncaughtException', (err) => {
206
+ if (abort) {
207
+ abort.abort();
208
+ abort = null;
209
+ }
210
+ console.log(c.err('\n ✗ lỗi: ' + (err?.message || err)));
286
211
  if (!closed) tui.setPrompt(promptStr(false));
287
212
  });
288
- process.on("unhandledRejection", (err) => {
289
- console.log(c.err("\n ✗ lỗi nền: " + (err?.message || err)));
213
+ process.on('unhandledRejection', (err) => {
214
+ console.log(c.err('\n ✗ lỗi nền: ' + (err?.message || err)));
290
215
  if (!closed) tui.setPrompt(promptStr(false));
291
216
  });
292
217
 
@@ -311,7 +236,7 @@ export async function startRepl(opts = {}) {
311
236
  async function restore(s) {
312
237
  session = s;
313
238
  state.history = s.history || [];
314
- state.mode = "chat";
239
+ state.mode = 'chat';
315
240
  state.goal = s.goal || null; // khôi phục HARD GOAL nếu phiên cũ có
316
241
  if (s.model) {
317
242
  const m = findModel(s.model);
@@ -328,34 +253,44 @@ export async function startRepl(opts = {}) {
328
253
  lastTickAt: Date.now(), // reset baseline để tick đầu chạy sau intervalMs
329
254
  };
330
255
  state.loop.timer = setInterval(makeLoopTick(s.loop.task), s.loop.intervalMs);
331
- console.log(c.accent(" ↻ " + t.loopStatus(s.loop.intervalStr || fmtMs(s.loop.intervalMs), s.loop.task, s.loop.ticks || 0, fmtMs(s.loop.intervalMs))));
256
+ console.log(
257
+ c.accent(
258
+ ' ↻ ' +
259
+ t.loopStatus(
260
+ s.loop.intervalStr || fmtMs(s.loop.intervalMs),
261
+ s.loop.task,
262
+ s.loop.ticks || 0,
263
+ fmtMs(s.loop.intervalMs)
264
+ )
265
+ )
266
+ );
332
267
  }
333
- console.log(c.ok("" + t.sessionResumed(s.id)));
334
- const turns = state.history.filter((m) => m.role === "user");
268
+ console.log(c.ok('' + t.sessionResumed(s.id)));
269
+ const turns = state.history.filter((m) => m.role === 'user');
335
270
  const tail = turns.slice(-5);
336
271
  const base = turns.length - tail.length;
337
272
  tail.forEach((m, i) => console.log(c.dim(` ${base + i + 1}. `) + truncate(m.content, 70)));
338
- console.log("");
273
+ console.log('');
339
274
  }
340
275
  async function pickSession() {
341
276
  const items = sessions.list(20, process.cwd()); // chỉ phiên của workspace hiện tại
342
277
  if (!items.length) {
343
- console.log(c.dim(" " + t.sessionEmpty) + "\n");
278
+ console.log(c.dim(' ' + t.sessionEmpty) + '\n');
344
279
  return null;
345
280
  }
346
- console.log("\n" + chalk.bold(" " + t.sessionPickTitle));
281
+ console.log('\n' + chalk.bold(' ' + t.sessionPickTitle));
347
282
  items.forEach((s, i) =>
348
283
  console.log(
349
284
  c.accent(` ${String(i + 1).padStart(2)}. `) +
350
- chalk.bold(s.title || "(trống)") +
351
- c.dim(` · ${s.turns} lượt · ${relTime(s.updatedAt)} · ${shortPath(s.cwd)}`),
352
- ),
285
+ chalk.bold(s.title || '(trống)') +
286
+ c.dim(` · ${s.turns} lượt · ${relTime(s.updatedAt)} · ${shortPath(s.cwd)}`)
287
+ )
353
288
  );
354
- const ans = ((await ask(c.tool(" " + t.sessionPickPrompt(items.length)))) ?? "").trim();
289
+ const ans = ((await ask(c.tool(' ' + t.sessionPickPrompt(items.length)))) ?? '').trim();
355
290
  if (!ans) return null;
356
291
  const idx = parseInt(ans, 10) - 1;
357
292
  if (Number.isNaN(idx) || idx < 0 || idx >= items.length) {
358
- console.log(c.err(" " + t.sessionPickBad) + "\n");
293
+ console.log(c.err(' ' + t.sessionPickBad) + '\n');
359
294
  return null;
360
295
  }
361
296
  const full = sessions.load(items[idx].id);
@@ -364,16 +299,16 @@ export async function startRepl(opts = {}) {
364
299
  }
365
300
  function listSessions() {
366
301
  const items = sessions.list(20, process.cwd()); // chỉ phiên của workspace hiện tại
367
- if (!items.length) return console.log(c.dim(" " + t.sessionEmpty));
368
- console.log("\n" + chalk.bold(" " + t.sessionListTitle));
302
+ if (!items.length) return console.log(c.dim(' ' + t.sessionEmpty));
303
+ console.log('\n' + chalk.bold(' ' + t.sessionListTitle));
369
304
  items.forEach((s) =>
370
305
  console.log(
371
- c.dim(" " + s.id.padEnd(20)) +
372
- chalk.bold(s.title || "(trống)") +
373
- c.dim(` · ${s.turns} lượt · ${relTime(s.updatedAt)}`),
374
- ),
306
+ c.dim(' ' + s.id.padEnd(20)) +
307
+ chalk.bold(s.title || '(trống)') +
308
+ c.dim(` · ${s.turns} lượt · ${relTime(s.updatedAt)}`)
309
+ )
375
310
  );
376
- console.log(c.dim("\n " + t.sessionResumeHint) + "\n");
311
+ console.log(c.dim('\n ' + t.sessionResumeHint) + '\n');
377
312
  }
378
313
  const startFresh = () => {
379
314
  session = sessions.newSession({ cwd: process.cwd(), model: state.model.id });
@@ -387,10 +322,10 @@ export async function startRepl(opts = {}) {
387
322
  // /frontend-design <yêu cầu> — vận dụng skill frontend-design (skills/frontend-design/SKILL.md)
388
323
  // để model tạo UI frontend chất lượng cao, tránh "AI slop" aesthetic.
389
324
  async function runFrontendDesign(arg) {
390
- if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
391
- if (!arg) return console.log(c.err(" " + t.frontendDesignNeedReq));
392
- const skill = loadSkill("frontend-design");
393
- if (!skill) return console.log(c.err(" " + t.frontendDesignNoSkill));
325
+ if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
326
+ if (!arg) return console.log(c.err(' ' + t.frontendDesignNeedReq));
327
+ const skill = loadSkill('frontend-design');
328
+ if (!skill) return console.log(c.err(' ' + t.frontendDesignNoSkill));
394
329
  const prompt = `Bạn đang thực thi SKILL "frontend-design". Đọc kỹ hướng dẫn skill dưới đây và TUÂN THỦ khi xây dựng UI.
395
330
 
396
331
  === SKILL: frontend-design ===
@@ -401,7 +336,7 @@ YÊU CẦU NGƯỜI DÙNG:
401
336
  ${arg}
402
337
 
403
338
  Thực thi: đọc/tạo file cần thiết bằng tool, viết code production-grade theo đúng tinh thần skill (typography đặc sắc, color/theme có cam kết, motion có chủ đích, layout bất ngờ, tránh AI slop). Báo cáo ngắn gọn các file đã tạo và lựa chọn thẩm mỹ chính.`;
404
- console.log(c.tool(" 🎨 " + t.frontendDesignRunning));
339
+ console.log(c.tool(' 🎨 ' + t.frontendDesignRunning));
405
340
  await handle(prompt);
406
341
  persist();
407
342
  }
@@ -420,7 +355,7 @@ Thực thi: đọc/tạo file cần thiết bằng tool, viết code production-
420
355
  // built-in), L147-153 (triage + quarantine + pair-with-/loop), L177
421
356
  // (repeatable workflow + /goal + /loop integration).
422
357
  async function runWorkflow(arg) {
423
- if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
358
+ if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
424
359
  // Empty arg → KHÔNG báo lỗi "need arg" nữa, mà show menu trợ giúp — user
425
360
  // mới gõ /workflow có thể chưa biết phải gì. Thay vì đuổi đi, show help +
426
361
  // builtins + saved → user thấy luôn có gì để chạy.
@@ -431,18 +366,20 @@ Thực thi: đọc/tạo file cần thiết bằng tool, viết code production-
431
366
  // Detect sub-command. Sub-command tách bằng khoảng trắng đầu tiên. Thứ tự
432
367
  // match quan trọng: `help` / `?` / `patterns` / `builtins` / `list|ls` /
433
368
  // `load` / `delete|rm` / `save` / `run`. Ad-hoc default = phần còn lại.
434
- const m = trimmed.match(/^(help|\?|patterns|builtins|list|ls|load|delete|rm|save|run)\b\s*([\s\S]*)$/i);
369
+ const m = trimmed.match(
370
+ /^(help|\?|patterns|builtins|list|ls|load|delete|rm|save|run)\b\s*([\s\S]*)$/i
371
+ );
435
372
  if (m) {
436
373
  const sub = m[1].toLowerCase();
437
374
  const rest = m[2].trim();
438
- if (sub === "help" || sub === "?") return workflowHelp();
439
- if (sub === "patterns") return workflowPatterns();
440
- if (sub === "builtins") return workflowBuiltins();
441
- if (sub === "list" || sub === "ls") return workflowList();
442
- if (sub === "load") return workflowLoad(rest);
443
- if (sub === "delete" || sub === "rm") return workflowDelete(rest);
444
- if (sub === "save") return workflowSave(rest);
445
- if (sub === "run") return workflowRun(rest);
375
+ if (sub === 'help' || sub === '?') return workflowHelp();
376
+ if (sub === 'patterns') return workflowPatterns();
377
+ if (sub === 'builtins') return workflowBuiltins();
378
+ if (sub === 'list' || sub === 'ls') return workflowList();
379
+ if (sub === 'load') return workflowLoad(rest);
380
+ if (sub === 'delete' || sub === 'rm') return workflowDelete(rest);
381
+ if (sub === 'save') return workflowSave(rest);
382
+ if (sub === 'run') return workflowRun(rest);
446
383
  }
447
384
  // Default: ad-hoc workflow (giữ behavior cũ — model design workflow từ request).
448
385
  await workflowExecute(trimmed);
@@ -453,19 +390,27 @@ Thực thi: đọc/tạo file cần thiết bằng tool, viết code production-
453
390
  // askPermission / askAddRoot) để user khỏi phải gõ lại /workflow.
454
391
  async function askWorkflowAgentMode() {
455
392
  tui.setBusy(false);
456
- console.log(c.tool(" " + (t.workflowAgentAskHint || "🎼 /workflow cần spawn sub-agent — agent mode hiện đang TẮT.")));
393
+ console.log(
394
+ c.tool(
395
+ ' ' +
396
+ (t.workflowAgentAskHint || '🎼 /workflow cần spawn sub-agent — agent mode hiện đang TẮT.')
397
+ )
398
+ );
457
399
  try {
458
400
  while (true) {
459
- const raw = await ask(c.tool(" bật agent mode và chạy workflow? ") + c.dim("[y] có, bật & chạy / [n] huỷ (gõ /agent rồi chạy lại nếu muốn) › "));
460
- if (raw == null) return "n"; // stdin đóng thật
401
+ const raw = await ask(
402
+ c.tool(' bật agent mode chạy workflow? ') +
403
+ c.dim('[y] có, bật & chạy / [n] huỷ (gõ /agent rồi chạy lại nếu muốn) › ')
404
+ );
405
+ if (raw == null) return 'n'; // stdin đóng thật
461
406
  const a = raw.trim().toLowerCase();
462
- if (a === "" || a === "y" || a === "yes" || a === "") return "y";
463
- if (a === "n" || a === "no" || a === "không") return "n";
407
+ if (a === '' || a === 'y' || a === 'yes' || a === '') return 'y';
408
+ if (a === 'n' || a === 'no' || a === 'không') return 'n';
464
409
  if (raw.trim().length > 3) {
465
410
  pending.push(raw);
466
- console.log(c.dim(" " + t.queued(pending.length, truncate(raw, 60))));
411
+ console.log(c.dim(' ' + t.queued(pending.length, truncate(raw, 60))));
467
412
  }
468
- console.log(c.dim(" → gõ y hoặc n"));
413
+ console.log(c.dim(' → gõ y hoặc n'));
469
414
  }
470
415
  } finally {
471
416
  tui.setBusy(true, t.thinking);
@@ -481,8 +426,11 @@ Thực thi: đọc/tạo file cần thiết bằng tool, viết code production-
481
426
  // Built-in prompt đã đầy đủ PLAN + 4 bước thực thi — KHÔNG wrap thêm skill.
482
427
  prompt = userRequest;
483
428
  } else {
484
- const skill = loadSkill("dynamic-workflows");
485
- if (!skill) return console.log(c.err(" " + (t.workflowNoSkill || "Không tìm thấy skill dynamic-workflows")));
429
+ const skill = loadSkill('dynamic-workflows');
430
+ if (!skill)
431
+ return console.log(
432
+ c.err(' ' + (t.workflowNoSkill || 'Không tìm thấy skill dynamic-workflows'))
433
+ );
486
434
  // Enforce PLAN xuất hiện TRƯỚC khi spawn bằng cách gộp vào bước 1 và yêu cầu
487
435
  // output. Model hay skip bước này → user mất visibility vào plan.
488
436
  prompt = `Bạn đang thực thi SKILL "dynamic-workflows". Đọc kỹ playbook dưới đây và TUÂN THỦ khi orchestrate multi-agent workflow.
@@ -510,13 +458,21 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
510
458
  // tool độc lập). Hỏi 1 lần, user chọn y thì bật & chạy, n thì huỷ sạch +
511
459
  // gợi ý /agent. Tránh buộc user gõ lại /workflow sau khi /agent.
512
460
  const choice = await askWorkflowAgentMode();
513
- if (choice !== "y") {
514
- return console.log(c.dim(" " + (t.workflowAgentDenied || "đã huỷ /workflow — agent mode vẫn TẮT. Gõ /agent rồi chạy lại lệnh nếu muốn.")));
461
+ if (choice !== 'y') {
462
+ return console.log(
463
+ c.dim(
464
+ ' ' +
465
+ (t.workflowAgentDenied ||
466
+ 'đã huỷ /workflow — agent mode vẫn TẮT. Gõ /agent rồi chạy lại lệnh nếu muốn.')
467
+ )
468
+ );
515
469
  }
516
470
  state.agent = true;
517
- console.log(c.tool(" ✓ " + (t.workflowAgentEnabled || "đã bật agent mode cho workflow này.")));
471
+ console.log(
472
+ c.tool(' ✓ ' + (t.workflowAgentEnabled || 'đã bật agent mode cho workflow này.'))
473
+ );
518
474
  }
519
- console.log(c.tool(" 🎼 " + (t.workflowRunning || "Dynamic workflow running…")));
475
+ console.log(c.tool(' 🎼 ' + (t.workflowRunning || 'Dynamic workflow running…')));
520
476
  await handle(prompt);
521
477
  persist();
522
478
  }
@@ -525,142 +481,74 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
525
481
  // Trước đó /workflow không có gì để khám phá: user phải biết syntax sẵn. Giờ
526
482
  // empty /workflow hoặc `/workflow help` show menu đầy đủ.
527
483
  function workflowHelp() {
528
- console.log(c.tool(" " + (t.workflowHelpTitle || "🎼 /workflow — orchestrate multi-agent workflow")));
529
- console.log(c.dim(" " + (t.workflowHelpSub || "Workflow chia task lớn thành sub-agent chạy song song/độc lập → chống 3 failure mode của single-context: agentic laziness, self-preferential bias, goal drift.")));
530
- console.log("");
531
- console.log(c.accent(" Cú pháp:"));
532
- console.log(" /workflow <yêu cầu> chạy ad-hoc (model tự design workflow)");
533
- console.log(" /workflow help | ? menu này");
534
- console.log(" /workflow patterns 6 pattern workflow (theo article Thariq)");
535
- console.log(" /workflow builtins 3 workflow built-in có sẵn");
536
- console.log(" /workflow list workflow đã lưu");
537
- console.log(" /workflow save <name> <req> lưu prompt template → ~/.noob/workflows/");
538
- console.log(" /workflow load <name> xem nội dung (saved hoặc built-in)");
539
- console.log(" /workflow run <name> [extra] chạy (built-in HOẶC saved, có thể thêm ngữ cảnh)");
540
- console.log(" /workflow delete|rm <name> xoá workflow đã lưu");
541
- console.log("");
542
- console.log(c.accent(" Nhanh nhất để thử:"));
543
- console.log(" " + c.dim("/workflow builtins ") + "xem có sẵn cái nào");
544
- console.log(" " + c.dim("/workflow run deep-research \"async-await trong Python\"") + " chạy ngay");
545
- console.log(" " + c.dim("/workflow run verify-claims README.md ") + "verify claim trong tài liệu");
546
- console.log("");
547
- console.log(c.dim(" 💡 Repeatable workflow (triage, research, verify) — pair với /loop <interval> + /goal <text>"));
548
- console.log("");
549
- workflowBuiltins({ compact: true });
484
+ return _workflowHelp({ c, t });
550
485
  }
551
486
 
552
487
  // Liệt kê 6 pattern từ article Thariq (L83-109). Mỗi pattern 1 dòng — user
553
488
  // scan nhanh chọn pattern phù hợp task. Trước đó tôi liệt kê 7 (thêm
554
489
  // "Diverse-Hypothesis Debug" tự bịa) → fix về 6 theo article.
555
490
  function workflowPatterns() {
556
- const PATTERNS = [
557
- ["Classify-and-act", "classifier phân loại task → route tới sub-agent chuyên dụng. HOẶC classifier ở cuối để check output. Khi: input không đồng nhất, mỗi loại cần chiến lược khác."],
558
- ["Fan-out-and-synthesize", "task lớn chia N nhánh độc lập song song → gom kết quả. SYNTHESIZE STEP LÀ BARRIER (article L93): đợi tất cả fan-out xong mới merge. Khi: partition rõ theo file/module/khía cạnh."],
559
- ["Adversarial verification", "1 agent LÀM, 1 agent KHÁC verify output chống rubric/criteria. Khi: claim cần verify, code rủi ro, quyết định khó đảo."],
560
- ["Generate-and-filter", "sinh nhiều phương án song song → 1 agent lọc theo rubric/verify → dedupe → trả về top. Khi: bài toán mở, cần đa dạng giải pháp đã verify."],
561
- ["Tournament", "N agents CÙNG LÀM 1 task với approach khác nhau → judge pairwise cho tới khi có winner. Pairwise comparison reliable hơn absolute scoring. Khi: cần ranking/rubric, hoặc bài toán 'taste' (naming, design)."],
562
- ["Loop-until-done", "sub-agent làm 1 vòng, parent check stop condition (no new findings / no more errors), chưa đạt → spawn lại. Khi: lượng work không biết trước, có metric đo được."],
563
- ];
564
- console.log(c.tool(" " + (t.workflowPatternsTitle || "🎼 6 pattern workflow (theo article Thariq — A harness for every task)")));
565
- PATTERNS.forEach(([name, desc], i) => {
566
- console.log(" " + c.accent(`${i + 1}. ${name}`));
567
- console.log(" " + c.dim(desc));
568
- });
569
- console.log("");
570
- console.log(c.dim(" Tổ hợp: 1 workflow có thể compose nhiều pattern (vd triage = classify-and-act + loop-until-done + quarantine)."));
571
- console.log(c.dim(" Lưu ý (article L165-167): workflow KHÔNG cần cho mọi task — tốn nhiều token. Việc < vài file → tự làm."));
491
+ return _workflowPatterns({ c, t });
572
492
  }
573
493
 
574
494
  // Liệt kê built-in workflow có sẵn trong source. Built-in là 3 mẫu ship sẵn
575
495
  // để user `run` ngay, không phải tự viết. `compact=true` dùng trong help để
576
496
  // khỏi tốn dòng; default = list đầy đủ pattern + description.
577
- function workflowBuiltins({ compact = false } = {}) {
578
- const items = listBuiltinWorkflows();
579
- if (!compact) {
580
- console.log(c.tool(" " + (t.workflowBuiltinsTitle || `🎼 Workflow built-in (${items.length} mẫu ship sẵn):`)));
581
- } else {
582
- console.log(c.accent(" Built-in workflow:"));
583
- }
584
- for (const w of items) {
585
- console.log(" " + c.accent("/workflow run " + w.name) + c.dim(" · " + w.pattern));
586
- if (!compact) console.log(" " + c.dim(w.description));
587
- }
588
- if (!compact) {
589
- console.log("");
590
- console.log(c.dim(" Chạy: /workflow run <name> [input]. VD: /workflow run verify-claims README.md"));
591
- }
497
+ function workflowBuiltins(opts = {}) {
498
+ return _workflowBuiltins({ c, t, ...opts });
592
499
  }
593
500
 
594
501
  function workflowList() {
595
- const saved = listWorkflows();
596
- const builtins = listBuiltinWorkflows();
597
- // Luôn show cả 2 nhóm — built-in quan trọng vì user quên chúng có sẵn.
598
- console.log(c.tool(" " + (t.workflowListHeader ? t.workflowListHeader(workflowsDir()) : `Workflow đã lưu (${workflowsDir()}):`)));
599
- if (saved.length) {
600
- for (const it of saved) {
601
- const desc = it.description ? c.dim(" — " + it.description) : "";
602
- const date = it.updated ? c.dim(" [" + it.updated.slice(0, 10) + "]") : "";
603
- console.log(" " + c.accent(it.name) + desc + date);
604
- }
605
- } else {
606
- console.log(c.dim(" (chưa có — /workflow save <name> <yêu cầu> để tạo)"));
607
- }
608
- console.log("");
609
- console.log(c.accent(" Built-in workflow (chạy ngay, không cần save):"));
610
- for (const w of builtins) {
611
- console.log(" " + c.accent("/workflow run " + w.name) + c.dim(" · " + w.title));
612
- }
613
- console.log("");
614
- console.log(c.dim(" Dùng: /workflow <yêu cầu> hoặc /workflow run <name> [input] hoặc /workflow help"));
502
+ return _workflowList({ c, t });
615
503
  }
616
504
 
617
505
  function workflowLoad(name) {
618
- if (!name) return console.log(c.err(" " + (t.workflowLoadNeedName || "Cách dùng: /workflow load <name>")));
619
- // Check built-in trước — user có thể quên chúng có sẵn.
620
- const builtin = getBuiltinWorkflow(name);
621
- if (builtin) {
622
- console.log(c.tool(" " + `🎼 Built-in workflow '${builtin.name}' — ${builtin.title}`));
623
- console.log(c.dim(" pattern: " + builtin.pattern));
624
- console.log(c.dim(" " + builtin.description));
625
- console.log("");
626
- console.log(c.dim(" ── prompt template (chạy bằng /workflow run " + builtin.name + " <input>) ──"));
627
- console.log(builtin.buildPrompt("<input>"));
628
- return;
629
- }
630
- const r = loadWorkflow(name);
631
- if (!r.ok) return console.log(c.err(" " + (t.workflowLoadError ? t.workflowLoadError(name, r.error) : `Không nạp được workflow '${name}': ${r.error}`)));
632
- console.log(c.tool(" " + (t.workflowLoadOk ? t.workflowLoadOk(r.name, r.path) : `Workflow '${r.name}' (${r.path}):`)));
633
- if (r.meta.description) console.log(c.dim(" " + r.meta.description));
634
- if (r.meta.updated) console.log(c.dim(" updated: " + r.meta.updated));
635
- console.log("");
636
- console.log(r.prompt);
506
+ return _workflowLoad(name, { c, t });
637
507
  }
638
508
 
639
509
  function workflowDelete(name) {
640
- if (!name) return console.log(c.err(" " + (t.workflowDeleteNeedName || "Cách dùng: /workflow delete <name>")));
641
- // Chỉ xoá saved — built-in không xoá được.
642
- const builtin = getBuiltinWorkflow(name);
643
- if (builtin) return console.log(c.err(" " + (t.workflowDeleteBuiltIn ? t.workflowDeleteBuiltIn(name) : `'${name}' là built-in workflow, không xoá được.`)));
644
- const r = deleteWorkflow(name);
645
- if (!r.ok) return console.log(c.err(" " + (t.workflowDeleteError ? t.workflowDeleteError(name, r.error) : `Không xoá được workflow '${name}': ${r.error}`)));
646
- console.log(c.tool(" " + (t.workflowDeleteOk ? t.workflowDeleteOk(name) : `Đã xoá workflow '${name}'.`)));
510
+ return _workflowDelete(name, { c, t });
647
511
  }
648
512
 
649
513
  function workflowSave(rest) {
650
514
  // /workflow save <name> <yêu cầu...>
651
515
  const m = rest.match(/^(\S+)\s+([\s\S]+)$/);
652
- if (!m) return console.log(c.err(" " + (t.workflowSaveNeedArgs || "Cách dùng: /workflow save <name> <yêu cầu workflow>")));
516
+ if (!m)
517
+ return console.log(
518
+ c.err(
519
+ ' ' + (t.workflowSaveNeedArgs || 'Cách dùng: /workflow save <name> <yêu cầu workflow>')
520
+ )
521
+ );
653
522
  const name = m[1];
654
523
  const prompt = m[2].trim();
655
- if (!prompt) return console.log(c.err(" " + (t.workflowSaveEmptyPrompt || "Thiếu yêu cầu workflow. VD: /workflow save code-audit-security \"audit src/ tìm SQL injection\"")));
524
+ if (!prompt)
525
+ return console.log(
526
+ c.err(
527
+ ' ' +
528
+ (t.workflowSaveEmptyPrompt ||
529
+ 'Thiếu yêu cầu workflow. VD: /workflow save code-audit-security "audit src/ tìm SQL injection"')
530
+ )
531
+ );
656
532
  const r = saveWorkflow(name, prompt);
657
533
  if (!r.ok) {
658
- const msg = r.error === "invalid_name"
659
- ? (t.workflowSaveBadName ? t.workflowSaveBadName(name) : `Tên workflow không hợp lệ: '${name}'. Chỉ chấp nhận [a-z0-9_-], bắt đầu bằng chữ/số, tối đa 64 ký tự.`)
660
- : (t.workflowSaveError ? t.workflowSaveError(name, r.error) : `Không lưu được workflow '${name}': ${r.error}`);
661
- return console.log(c.err(" " + msg));
534
+ const msg =
535
+ r.error === 'invalid_name'
536
+ ? t.workflowSaveBadName
537
+ ? t.workflowSaveBadName(name)
538
+ : `Tên workflow không hợp lệ: '${name}'. Chỉ chấp nhận [a-z0-9_-], bắt đầu bằng chữ/số, tối đa 64 ký tự.`
539
+ : t.workflowSaveError
540
+ ? t.workflowSaveError(name, r.error)
541
+ : `Không lưu được workflow '${name}': ${r.error}`;
542
+ return console.log(c.err(' ' + msg));
662
543
  }
663
- console.log(c.tool(" 💾 " + (t.workflowSaveOk ? t.workflowSaveOk(name, r.path) : `Đã lưu workflow '${name}' → ${r.path}`)));
544
+ console.log(
545
+ c.tool(
546
+ ' 💾 ' +
547
+ (t.workflowSaveOk
548
+ ? t.workflowSaveOk(name, r.path)
549
+ : `Đã lưu workflow '${name}' → ${r.path}`)
550
+ )
551
+ );
664
552
  // Hỏi thêm description (1 dòng) — list/load sau này có ích, user nhìn 1 dòng
665
553
  // là biết workflow này làm gì. Không bắt buộc: n / Enter = skip.
666
554
  return maybeAskWorkflowDescription(name, prompt);
@@ -672,64 +560,117 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
672
560
  async function maybeAskWorkflowDescription(name, currentPrompt) {
673
561
  tui.setBusy(false);
674
562
  try {
675
- const raw = await ask(c.tool(" " + (t.workflowSaveAskDesc || "thêm mô tả ngắn để dễ tìm sau này? [y/n] › ")) + c.dim("(y = hỏi 1 dòng, phím khác = bỏ qua) "));
563
+ const raw = await ask(
564
+ c.tool(' ' + (t.workflowSaveAskDesc || 'thêm mô tả ngắn để dễ tìm sau này? [y/n] › ')) +
565
+ c.dim('(y = hỏi 1 dòng, phím khác = bỏ qua) ')
566
+ );
676
567
  if (raw == null) return; // stdin đóng
677
568
  const a = raw.trim().toLowerCase();
678
- if (a !== "y" && a !== "yes" && a !== "") {
679
- console.log(c.dim(" " + (t.workflowSaveDescSkipped || "(bỏ qua description — có thể thêm sau bằng cách save lại)")));
569
+ if (a !== 'y' && a !== 'yes' && a !== '') {
570
+ console.log(
571
+ c.dim(
572
+ ' ' +
573
+ (t.workflowSaveDescSkipped ||
574
+ '(bỏ qua description — có thể thêm sau bằng cách save lại)')
575
+ )
576
+ );
680
577
  return;
681
578
  }
682
- const descRaw = await ask(c.tool(" " + (t.workflowSaveDescPrompt || "mô tả (1 dòng): ")));
579
+ const descRaw = await ask(c.tool(' ' + (t.workflowSaveDescPrompt || 'mô tả (1 dòng): ')));
683
580
  if (descRaw == null) return;
684
581
  const desc = descRaw.trim();
685
582
  if (!desc) {
686
- console.log(c.dim(" (description trống — bỏ qua)"));
583
+ console.log(c.dim(' (description trống — bỏ qua)'));
687
584
  return;
688
585
  }
689
586
  const r2 = saveWorkflow(name, currentPrompt, { description: desc });
690
- if (r2.ok) console.log(c.ok(" ✓ " + (t.workflowSaveDescOk ? t.workflowSaveDescOk(name, desc) : `Đã thêm mô tả cho '${name}': ${desc}`)));
587
+ if (r2.ok)
588
+ console.log(
589
+ c.ok(
590
+ ' ✓ ' +
591
+ (t.workflowSaveDescOk
592
+ ? t.workflowSaveDescOk(name, desc)
593
+ : `Đã thêm mô tả cho '${name}': ${desc}`)
594
+ )
595
+ );
691
596
  } catch (e) {
692
- console.log(c.dim(" (lỗi thêm description, workflow vẫn được lưu)"));
597
+ console.log(c.dim(' (lỗi thêm description, workflow vẫn được lưu)'));
693
598
  } finally {
694
599
  tui.setBusy(true, t.thinking);
695
600
  }
696
601
  }
697
602
 
698
603
  async function workflowRun(rest) {
699
- if (!rest) return console.log(c.err(" " + (t.workflowRunNeedName || "Cách dùng: /workflow run <name> [thêm ngữ cảnh]")));
604
+ if (!rest)
605
+ return console.log(
606
+ c.err(' ' + (t.workflowRunNeedName || 'Cách dùng: /workflow run <name> [thêm ngữ cảnh]'))
607
+ );
700
608
  // Tách name (1 từ, kebab-case theo sanitize) + extra context phần còn lại.
701
609
  const m = rest.match(/^(\S+)(?:\s+([\s\S]+))?$/);
702
610
  const name = m[1];
703
- const extra = (m[2] || "").trim();
611
+ const extra = (m[2] || '').trim();
704
612
  // Built-in trước — user có thể gõ `run deep-research ...` mà quên đó là built-in.
705
613
  const builtin = getBuiltinWorkflow(name);
706
614
  if (builtin) {
707
- const userInput = extra || c.dim("(không có input — workflow sẽ chạy với placeholder)");
615
+ const userInput = extra || c.dim('(không có input — workflow sẽ chạy với placeholder)');
708
616
  const prompt = builtin.buildPrompt(userInput);
709
- console.log(c.tool(" ▶️ " + (t.workflowRunPreviewBuiltin ? t.workflowRunPreviewBuiltin(name, builtin.title) : `Built-in workflow '${name}' (${builtin.title}) — pattern: ${builtin.pattern}`)));
710
- console.log(c.dim(" input: " + truncate(userInput, 80)));
711
- console.log(c.dim(" prompt: " + prompt.length + " chars"));
617
+ console.log(
618
+ c.tool(
619
+ ' ▶️ ' +
620
+ (t.workflowRunPreviewBuiltin
621
+ ? t.workflowRunPreviewBuiltin(name, builtin.title)
622
+ : `Built-in workflow '${name}' (${builtin.title}) — pattern: ${builtin.pattern}`)
623
+ )
624
+ );
625
+ console.log(c.dim(' input: ' + truncate(userInput, 80)));
626
+ console.log(c.dim(' prompt: ' + prompt.length + ' chars'));
712
627
  return await workflowExecute(prompt, { builtInName: name });
713
628
  }
714
629
  const r = loadWorkflow(name);
715
- if (!r.ok) return console.log(c.err(" " + (t.workflowRunError ? t.workflowRunError(name, r.error) : `Không nạp được workflow '${name}': ${r.error}`)));
716
- const userRequest = extra ? `${r.prompt}\n\nNgữ cảnh bổ sung cho lần chạy này:\n${extra}` : r.prompt;
630
+ if (!r.ok)
631
+ return console.log(
632
+ c.err(
633
+ ' ' +
634
+ (t.workflowRunError
635
+ ? t.workflowRunError(name, r.error)
636
+ : `Không nạp được workflow '${name}': ${r.error}`)
637
+ )
638
+ );
639
+ const userRequest = extra
640
+ ? `${r.prompt}\n\nNgữ cảnh bổ sung cho lần chạy này:\n${extra}`
641
+ : r.prompt;
717
642
  // Preview banner trước khi execute — user verify "đúng cái mình muốn" trước
718
643
  // khi bỏ 30s+ chờ. Tránh case user gõ nhầm `run my-workflow` thành
719
644
  // `run my-workflw` (saved khác) và mất 1 phút mới biết.
720
- console.log(c.tool(" ▶️ " + (t.workflowRunPreviewSaved ? t.workflowRunPreviewSaved(name) : `Workflow đã lưu '${name}'`)));
721
- console.log(c.dim(" prompt: " + r.prompt.length + " chars · " + (r.meta.description || "(chưa có description)")));
722
- if (extra) console.log(c.dim(" extra context: " + truncate(extra, 80)));
645
+ console.log(
646
+ c.tool(
647
+ ' ▶️ ' +
648
+ (t.workflowRunPreviewSaved
649
+ ? t.workflowRunPreviewSaved(name)
650
+ : `Workflow đã lưu '${name}'`)
651
+ )
652
+ );
653
+ console.log(
654
+ c.dim(
655
+ ' prompt: ' +
656
+ r.prompt.length +
657
+ ' chars · ' +
658
+ (r.meta.description || '(chưa có description)')
659
+ )
660
+ );
661
+ if (extra) console.log(c.dim(' extra context: ' + truncate(extra, 80)));
723
662
  await workflowExecute(userRequest);
724
663
  }
725
664
 
726
665
  // /improve [hint] — model rà soát workspace & đề xuất tính năng/cải tiến.
727
666
  // KHÔNG sửa code, chỉ phân tích & đề xuất.
728
667
  async function runImprove(arg) {
729
- if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
730
- const focus = arg ? `\nNgười dùng nhấn mạnh: "${arg}". Ưu tiên theo hướng đó nhưng vẫn nêu gợi ý quan trọng khác.` : "";
668
+ if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
669
+ const focus = arg
670
+ ? `\nNgười dùng nhấn mạnh: "${arg}". Ưu tiên theo hướng đó nhưng vẫn nêu gợi ý quan trọng khác.`
671
+ : '';
731
672
  const prompt = `Đóng vai senior engineer & product reviewer. KHẢO SÁT workspace hiện tại và đề xuất TÍNH NĂNG / CẢI TIẾN cho dự án.${focus}\n\nQUY TRÌNH (dùng tool, không nói suông):\n1. list_dir thư mục gốc để nắm cấu trúc.\n2. Đọc README.md, package.json, noob.md, CHANGELOG.md (nếu có) để hiểu mục đích & trạng thái.\n3. list_dir/glob các thư mục mã chính. KHÔNG đọc hết file — chỉ đủ để nắm kiến trúc.\n4. grep TODO/FIXME/HACK/XXX để biết chỗ tác giả đã ghi nhận.\n5. Ghi nhận thiếu test/lint/CI nếu có.\n\nSAU KHẢO SÁT, viết báo cáo Markdown TIẾNG VIỆT theo cấu trúc:\n\n## Tóm tắt dự án\n2–4 dòng: làm gì, tech gì, trạng thái.\n\n## Điểm mạnh hiện tại\n3–6 gạch đầu dòng.\n\n## Gợi ý cải thiện\n5–10 đề xuất, MỖI cái:\n### N. <Tên>\n- **Vấn đề/cơ hội:** quan sát cụ thể (kèm tên_file:dòng nếu được).\n- **Đề xuất:** mô tả tính năng/cải tiến.\n- **Lợi ích:** UX/hiệu năng/độ tin cậy/mở rộng.\n- **Công sức:** S (vài giờ) / M (1–2 ngày) / L (>2 ngày).\n- **Ưu tiên:** P0 / P1 / P2.\n\n## Đề xuất ưu tiên hàng đầu\n1–3 mục P0 nên làm trước, kèm lý do.\n\nQUY TẮC: bám observation từ code thật, KHÔNG gợi ý chung chung, thẳng thắn không nịnh, KHÔNG sửa code, KHÔNG ghi noob.md.`;
732
- console.log(c.tool("" + t.improveRunning));
673
+ console.log(c.tool('' + t.improveRunning));
733
674
  await handle(prompt);
734
675
  persist();
735
676
  }
@@ -737,8 +678,10 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
737
678
  // /karpathy [path] — bắt noob tự rà soát code theo 4 nguyên tắc Karpathy.
738
679
  // Không có path → soát các file đã đổi trong phiên (model thấy qua FILES CHANGED).
739
680
  async function runKarpathy(arg) {
740
- if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
741
- const target = arg ? `file/đường dẫn: ${arg}` : "các file bạn đã tạo/sửa trong phiên này (xem mục FILES CHANGED)";
681
+ if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
682
+ const target = arg
683
+ ? `file/đường dẫn: ${arg}`
684
+ : 'các file bạn đã tạo/sửa trong phiên này (xem mục FILES CHANGED)';
742
685
  const prompt = `Đóng vai reviewer khó tính. Rà soát ${target} theo 4 nguyên tắc code của Karpathy.
743
686
  ĐỌC nội dung file thật bằng read_file trước — KHÔNG dựa vào trí nhớ.
744
687
  Với MỖI nguyên tắc, cho verdict (✅ đạt / ⚠️ cảnh báo / ❌ vi phạm) + phát hiện cụ thể kèm "tên_file:dòng":
@@ -747,62 +690,21 @@ Với MỖI nguyên tắc, cho verdict (✅ đạt / ⚠️ cảnh báo / ❌ vi
747
690
  3. SURGICAL — thay đổi lạc đề, refactor tiện tay, đổi style/format vô cớ?
748
691
  4. VERIFIABLE GOAL — mục tiêu có kiểm chứng được? đã chạy build/test chưa?
749
692
  Cuối cùng: tóm tắt + danh sách sửa theo thứ tự ưu tiên. Thẳng thắn, KHÔNG nịnh.`;
750
- console.log(c.tool(" ⚖ Karpathy check…"));
693
+ console.log(c.tool(' ⚖ Karpathy check…'));
751
694
  await handle(prompt);
752
695
  persist();
753
696
  }
754
697
 
755
698
  // ── ULTRA: chế độ tự hành (self-quest) ────────────────────────────────────
756
- // noob tự lập kế hoạch, TỰ chọn nhiệm vụ con kế tiếp, tự thực hiện & tự kiểm
757
- // chứng lặp tới khi model phát token ULTRA_DONE, chạm giới hạn vòng, hoặc
758
- // người dùng Ctrl+C. Mỗi vòng là một lượt agent đầy đủ (dùng lại handle()).
759
- const ULTRA_DONE = "<<ULTRA_DONE>>";
760
- const MAX_QUESTS = 40;
761
- // Chỉ coi là HOÀN THÀNH khi token nằm ở CUỐI câu trả lời (dòng riêng) — tránh
762
- // bắt nhầm khi model chỉ NHẮC tới token giữa văn xuôi. Không bao giờ chấp nhận
763
- // ở lượt lập kế hoạch (xem vòng lặp bên dưới).
764
- const ultraIsDone = (a) => a.trimEnd().endsWith(ULTRA_DONE);
765
- // Detect "stuck": model bối rối, không nhận task, chỉ hỏi lại user hoặc spam
766
- // list_dir/ls vô nghĩa. Xảy ra khi goal trống nghĩa / bị paste system prompt /
767
- // model mất ngữ cảnh. 2 vòng stuck liên tiếp → auto-exit để không loop vô hạn.
768
- const STUCK_PHRASES = [
769
- "chưa giao task", "chưa nêu tác vụ", "chưa có yêu cầu", "chưa có task",
770
- "không nhận task", "không thể nhận vai", "bạn muốn mình làm gì",
771
- "chưa rõ yêu cầu", "cần mục tiêu rõ", "vui lòng cho biết",
772
- "please provide", "what would you like", "no task", "clarify",
773
- ];
774
- const ultraLooksStuck = (a) => {
775
- if (!a) return true;
776
- const s = a.toLowerCase();
777
- return STUCK_PHRASES.some((p) => s.includes(p));
778
- };
779
- const ultraStart = (goal) => `# CHẾ ĐỘ ULTRA (tự hành)
780
- Mục tiêu tổng: ${goal}
781
-
782
- Bạn TỰ lập kế hoạch và TỰ làm từng bước tới khi HOÀN THÀNH THẬT, không chờ người dùng xác nhận giữa chừng.
783
- LƯỢT NÀY (lập kế hoạch + khởi động):
784
- - Viết kế hoạch ngắn 3–7 gạch đầu dòng.
785
- - Rồi BẮT TAY làm bước đầu bằng tool (đọc/sửa file, chạy lệnh) — không nói suông.
786
- - TUYỆT ĐỐI KHÔNG phát token ${ULTRA_DONE} ở lượt này, dù mục tiêu trông nhỏ. Lượt lập kế hoạch KHÔNG bao giờ là lượt kết thúc.
787
- Nguyên tắc xuyên suốt: chỉ KẾT QUẢ TOOL mới tính là "đã làm"; nói "đã xong/đã sửa" trong văn xuôi mà KHÔNG có tool result thì KHÔNG tính.`;
788
- const ultraContinue = (goal) => `Tiếp tục ULTRA — mục tiêu: ${goal}
789
- Tự đánh giá: còn THIẾU gì để đạt mục tiêu? Chọn 1 nhiệm vụ con kế tiếp và LÀM bằng tool (đừng chỉ mô tả). Cập nhật noob.md nếu học được điều mới.
790
-
791
- ĐIỀU KIỆN KẾT THÚC — chỉ được dừng khi ĐỦ CẢ 3; thiếu bất kỳ điều nào thì LÀM TIẾP, đừng dừng:
792
- 1. Mọi phần của mục tiêu đã thực sự làm xong, có TOOL RESULT xác nhận (đối chiếu mục FILES CHANGED) — không chỉ nói trong văn xuôi.
793
- 2. ĐÃ KIỂM CHỨNG: chạy build/test/lint hoặc chạy thử phần vừa làm bằng run_command và ĐỌC output thấy ĐẠT. Nếu dự án không có cách kiểm chứng tự động → nêu rõ đã kiểm tra bằng cách nào.
794
- 3. Đã rà lại, không còn việc dở hay lỗi.
795
-
796
- • ĐỦ cả 3 → viết tóm tắt NGẮN việc đã làm + BẰNG CHỨNG kiểm chứng (lệnh đã chạy & kết quả thật), rồi đặt token ${ULTRA_DONE} TRÊN MỘT DÒNG RIÊNG ở CUỐI CÙNG.
797
- • CHƯA đủ (còn việc, hoặc chưa chạy kiểm chứng) → ĐỪNG phát token, tiếp tục bước kế.
798
- • Gặp việc nguy hiểm/không đảo ngược hoặc thật sự bí → DỪNG hỏi 1 câu rõ ràng (đừng phát token).`;
699
+ // Constants + helpers thuần + prompt templates đã tách sang src/repl/ultra.js.
700
+ // Phần state-heavy (runUltra loop) dưới giữ đây cần closure handle/persist/state.
799
701
 
800
702
  async function runUltra(goal) {
801
- if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
802
- if (!goal) return console.log(c.err(" " + t.ultraNeedGoal));
803
- state.mode = "chat"; // tự hành chỉ chạy ở chế độ agent
703
+ if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
704
+ if (!goal) return console.log(c.err(' ' + t.ultraNeedGoal));
705
+ state.mode = 'chat'; // tự hành chỉ chạy ở chế độ agent
804
706
  state.ultra = true;
805
- console.log(c.accent(" 🚀 " + t.ultraOn));
707
+ console.log(c.accent(' 🚀 ' + t.ultraOn));
806
708
  // Mốc history TRƯỚC khi ULTRA bơm prompt — kết thúc thì cắt về để các lượt sau không bị "dính" mục tiêu cũ.
807
709
  const baseLen = state.history.length;
808
710
  try {
@@ -820,22 +722,28 @@ Tự đánh giá: còn THIẾU gì để đạt mục tiêu? Chọn 1 nhiệm v
820
722
  if (ultraLooksStuck(answer)) {
821
723
  stuckStreak++;
822
724
  if (stuckStreak >= STUCK_MAX) {
823
- console.log(c.err(" ⚠ ULTRA stuck: model không nhận task " + stuckStreak + " vòng liên tiếp. Thoát. Gõ /ultra <mục tiêu rõ> để thử lại."));
725
+ console.log(
726
+ c.err(
727
+ ' ⚠ ULTRA stuck: model không nhận task ' +
728
+ stuckStreak +
729
+ ' vòng liên tiếp. Thoát. Gõ /ultra <mục tiêu rõ> để thử lại.'
730
+ )
731
+ );
824
732
  break;
825
733
  }
826
734
  } else {
827
735
  stuckStreak = 0;
828
736
  }
829
737
  i++;
830
- console.log(c.accent("" + t.ultraQuest(i)));
738
+ console.log(c.accent('' + t.ultraQuest(i)));
831
739
  answer = await handle(ultraContinue(goal));
832
740
  persist();
833
741
  if (answer && ultraIsDone(answer)) {
834
- console.log(c.ok("" + t.ultraDone));
742
+ console.log(c.ok('' + t.ultraDone));
835
743
  break;
836
744
  }
837
745
  }
838
- if (state.ultra && i >= MAX_QUESTS) console.log(c.tool(" " + t.ultraMax));
746
+ if (state.ultra && i >= MAX_QUESTS) console.log(c.tool(' ' + t.ultraMax));
839
747
  } finally {
840
748
  // Dọn dấu vết ULTRA khỏi history (prompt khởi động, các lượt "tiếp tục",
841
749
  // token <<ULTRA_DONE>>…) để các yêu cầu SAU đó không bị model coi như vẫn
@@ -843,10 +751,15 @@ Tự đánh giá: còn THIẾU gì để đạt mục tiêu? Chọn 1 nhiệm v
843
751
  state.ultra = false;
844
752
  if (state.history.length > baseLen) state.history.length = baseLen;
845
753
  state.history.push({
846
- role: "user",
847
- content: "[Phiên ULTRA đã KẾT THÚC — mục tiêu cũ: " + goal + ". Bỏ qua mọi chỉ dẫn ULTRA trước đó, KHÔNG tự hành tiếp, KHÔNG phát token " + ULTRA_DONE + ". Chờ yêu cầu mới.]",
754
+ role: 'user',
755
+ content:
756
+ '[Phiên ULTRA đã KẾT THÚC — mục tiêu cũ: ' +
757
+ goal +
758
+ '. Bỏ qua mọi chỉ dẫn ULTRA trước đó, KHÔNG tự hành tiếp, KHÔNG phát token ' +
759
+ ULTRA_DONE +
760
+ '. Chờ yêu cầu mới.]',
848
761
  });
849
- state.history.push({ role: "assistant", content: "OK, đã thoát chế độ ULTRA." });
762
+ state.history.push({ role: 'assistant', content: 'OK, đã thoát chế độ ULTRA.' });
850
763
  persist();
851
764
  }
852
765
  }
@@ -862,25 +775,29 @@ Tự đánh giá: còn THIẾU gì để đạt mục tiêu? Chọn 1 nhiệm v
862
775
  function parseInterval(s) {
863
776
  if (!s) return null;
864
777
  const re = /(\d+)\s*(h|m|s)/gi;
865
- let total = 0, matched = false, m;
778
+ let total = 0,
779
+ matched = false,
780
+ m;
866
781
  while ((m = re.exec(s)) !== null) {
867
782
  matched = true;
868
783
  const n = parseInt(m[1], 10);
869
784
  const u = m[2].toLowerCase();
870
- if (u === "h") total += n * 3600_000;
871
- else if (u === "m") total += n * 60_000;
785
+ if (u === 'h') total += n * 3600_000;
786
+ else if (u === 'm') total += n * 60_000;
872
787
  else total += n * 1000;
873
788
  }
874
789
  if (!matched || total < 5000) return null; // tối thiểu 5s — tránh hammer
875
790
  return total;
876
791
  }
877
792
  function fmtMs(ms) {
878
- if (ms < 60_000) return Math.round(ms / 1000) + "s";
793
+ if (ms < 60_000) return Math.round(ms / 1000) + 's';
879
794
  if (ms < 3600_000) {
880
- const m = Math.floor(ms / 60_000), s = Math.round((ms % 60_000) / 1000);
795
+ const m = Math.floor(ms / 60_000),
796
+ s = Math.round((ms % 60_000) / 1000);
881
797
  return s ? `${m}m${s}s` : `${m}m`;
882
798
  }
883
- const h = Math.floor(ms / 3600_000), mm = Math.round((ms % 3600_000) / 60_000);
799
+ const h = Math.floor(ms / 3600_000),
800
+ mm = Math.round((ms % 3600_000) / 60_000);
884
801
  return mm ? `${h}h${mm}m` : `${h}h`;
885
802
  }
886
803
  function stopLoop() {
@@ -899,17 +816,17 @@ Tự đánh giá: còn THIẾU gì để đạt mục tiêu? Chọn 1 nhiệm v
899
816
  state.loop.ticks++;
900
817
  state.loop.lastTickAt = Date.now();
901
818
  try {
902
- console.log(c.dim(" " + t.loopTick(state.loop.ticks)));
819
+ console.log(c.dim(' ' + t.loopTick(state.loop.ticks)));
903
820
  const answer = await handle(loopTickPrompt(task, state.loop.ticks));
904
821
  persist();
905
822
  if (loopIsDone(answer)) {
906
- console.log(c.ok("" + t.loopAutoStop(state.loop.ticks)));
823
+ console.log(c.ok('' + t.loopAutoStop(state.loop.ticks)));
907
824
  stopLoop();
908
825
  }
909
826
  // [GỠ BUDGET 2026-06-06] Không còn cap token cho /loop — loop dừng theo
910
827
  // <<LOOP_DONE>> hoặc /loop stop, không bị cắt giữa chừng vì "hết token".
911
828
  } catch (e) {
912
- console.log(c.err(" loop tick lỗi: " + (e?.message || e)));
829
+ console.log(c.err(' loop tick lỗi: ' + (e?.message || e)));
913
830
  } finally {
914
831
  if (state.loop) state.loop.running = false;
915
832
  }
@@ -918,7 +835,7 @@ Tự đánh giá: còn THIẾU gì để đạt mục tiêu? Chọn 1 nhiệm v
918
835
  // /loop auto-stop: model phát token <<LOOP_DONE>> ở CUỐI reply khi thấy không
919
836
  // còn việc / goal đã đạt → loop tự dừng. Combo tự nhiên với /goal: gõ /goal
920
837
  // trước rồi /loop, model tự đánh giá goal mỗi tick và phát LOOP_DONE khi đủ.
921
- const LOOP_DONE = "<<LOOP_DONE>>";
838
+ const LOOP_DONE = '<<LOOP_DONE>>';
922
839
  const loopIsDone = (a) => a && a.trimEnd().endsWith(LOOP_DONE);
923
840
  const loopTickPrompt = (task, n) => `[LOOP tick #${n}] ${task}
924
841
 
@@ -928,32 +845,35 @@ Sau khi xong tick này, TỰ ĐÁNH GIÁ:
928
845
  - Nếu task này đã hoàn tất hẳn (mọi điều cần làm đều đã làm, hoặc goal nếu có đã đạt) và không còn lý do để tick tiếp → đặt token ${LOOP_DONE} TRÊN MỘT DÒNG RIÊNG ở CUỐI reply để dừng loop.`;
929
846
 
930
847
  async function runLoop(arg) {
931
- const a = (arg || "").trim();
848
+ const a = (arg || '').trim();
932
849
  // /loop (no arg) → status
933
850
  if (!a) {
934
- if (!state.loop) return console.log(c.dim(" " + t.loopNotRunning) + c.dim(" " + t.loopNeedArgs));
851
+ if (!state.loop)
852
+ return console.log(c.dim(' ' + t.loopNotRunning) + c.dim(' ' + t.loopNeedArgs));
935
853
  const L = state.loop;
936
854
  const elapsed = Date.now() - L.lastTickAt;
937
855
  const nextIn = Math.max(0, L.intervalMs - elapsed);
938
- return console.log(c.accent(" " + t.loopStatus(L.intervalStr, L.task, L.ticks, fmtMs(nextIn))));
856
+ return console.log(
857
+ c.accent(' ' + t.loopStatus(L.intervalStr, L.task, L.ticks, fmtMs(nextIn)))
858
+ );
939
859
  }
940
860
  // /loop stop
941
861
  if (/^(stop|off|dừng|dung|tắt|tat)$/i.test(a)) {
942
- if (stopLoop()) console.log(c.ok(" " + t.loopStopped));
943
- else console.log(c.dim(" " + t.loopNotRunning));
862
+ if (stopLoop()) console.log(c.ok(' ' + t.loopStopped));
863
+ else console.log(c.dim(' ' + t.loopNotRunning));
944
864
  return;
945
865
  }
946
- if (state.loop) return console.log(c.err(" " + t.loopAlreadyRunning));
947
- if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
866
+ if (state.loop) return console.log(c.err(' ' + t.loopAlreadyRunning));
867
+ if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
948
868
  // parse <interval> <task>
949
869
  // [GỠ BUDGET 2026-06-06] Cú pháp đơn giản: /loop <interval> <task>. Không còn cap token.
950
870
  const firstSpace = a.search(/\s/);
951
- if (firstSpace < 0) return console.log(c.err(" " + t.loopNeedArgs));
871
+ if (firstSpace < 0) return console.log(c.err(' ' + t.loopNeedArgs));
952
872
  const intervalStr = a.slice(0, firstSpace).trim();
953
873
  const task = a.slice(firstSpace + 1).trim();
954
- if (!task) return console.log(c.err(" " + t.loopNeedArgs));
874
+ if (!task) return console.log(c.err(' ' + t.loopNeedArgs));
955
875
  const intervalMs = parseInterval(intervalStr);
956
- if (!intervalMs) return console.log(c.err(" " + t.loopBadInterval(intervalStr)));
876
+ if (!intervalMs) return console.log(c.err(' ' + t.loopBadInterval(intervalStr)));
957
877
  const normInterval = fmtMs(intervalMs);
958
878
  state.loop = {
959
879
  intervalMs,
@@ -965,7 +885,7 @@ Sau khi xong tick này, TỰ ĐÁNH GIÁ:
965
885
  running: false, // chống re-entrant (tick trước chưa xong, tick sau tới)
966
886
  timer: null,
967
887
  };
968
- console.log(c.accent(" " + t.loopStarted(normInterval, task)));
888
+ console.log(c.accent(' ' + t.loopStarted(normInterval, task)));
969
889
  state.loop.timer = setInterval(makeLoopTick(task), intervalMs);
970
890
  // KHÔNG tick ngay — user có thể muốn gõ thêm lệnh khác trước khi tick đầu chạy.
971
891
  }
@@ -973,13 +893,13 @@ Sau khi xong tick này, TỰ ĐÁNH GIÁ:
973
893
  // /init — quét dự án & sinh noob.md tổng quan (giống `/init` của Claude Code).
974
894
  // Nếu noob.md đã có: hỏi xác nhận ghi đè trước khi giao việc cho model.
975
895
  async function runInit() {
976
- if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
896
+ if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
977
897
  const mem = loadMemory();
978
898
  if (mem) {
979
- console.log(c.err(" " + t.initOverwriteWarn(memoryPath())));
980
- const ans = ((await ask(c.tool(" " + t.initOverwriteConfirm))) ?? "").trim().toLowerCase();
981
- if (ans !== "y" && ans !== "yes" && ans !== "") {
982
- console.log(c.dim(" " + t.initCancel));
899
+ console.log(c.err(' ' + t.initOverwriteWarn(memoryPath())));
900
+ const ans = ((await ask(c.tool(' ' + t.initOverwriteConfirm))) ?? '').trim().toLowerCase();
901
+ if (ans !== 'y' && ans !== 'yes' && ans !== '') {
902
+ console.log(c.dim(' ' + t.initCancel));
983
903
  return;
984
904
  }
985
905
  }
@@ -1017,22 +937,22 @@ NGUYÊN TẮC:
1017
937
  - Chỉ ghi sự thật rút ra từ file thật. KHÔNG bịa lệnh/quy ước không có cơ sở.
1018
938
  - Ngắn gọn (~80–150 dòng), mỗi ý một gạch đầu dòng.
1019
939
  - Khi xong, in 1 đoạn tóm tắt rất ngắn về những gì đã ghi vào noob.md.`;
1020
- console.log(c.tool(" 📋 " + t.initRunning));
940
+ console.log(c.tool(' 📋 ' + t.initRunning));
1021
941
  await handle(prompt);
1022
942
  persist();
1023
943
  }
1024
944
 
1025
945
  // /learn [ghi chú] — bắt noob chưng cất điều đáng nhớ của phiên vào noob.md.
1026
946
  async function runLearn(arg) {
1027
- if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
1028
- const note = arg ? `Người dùng nhấn mạnh cần nhớ: "${arg}"\n` : "";
947
+ if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
948
+ const note = arg ? `Người dùng nhấn mạnh cần nhớ: "${arg}"\n` : '';
1029
949
  const prompt = `${note}Hãy CHƯNG CẤT những điều đáng nhớ lâu dài từ phiên này và cập nhật noob.md ở thư mục gốc dự án.
1030
950
  - Đọc noob.md hiện có trước (chưa có thì tạo bằng write_file). noob.md có 2 mục: "## Rules" (quy ước đã chốt — bắt buộc tuân theo) và "## Notes" (quan sát chưa chốt).
1031
951
  - Ghi cái mới vào Notes: lệnh build/test/run, quy ước code, kiến trúc, sở thích người dùng, quyết định quan trọng, việc còn dang dở.
1032
952
  - Note nào đã đúng/lặp lại ~2–3 lần → CHUYỂN lên Rules và xoá Note trùng (vòng tự cải thiện).
1033
953
  - Mỗi ý 1 gạch đầu dòng, ngắn gọn, đúng sự thật. Giữ noob.md gọn (~200 dòng): cắt mục cũ/sai, đừng chỉ thêm.
1034
954
  - Chỉ ghi qua tool (write_file/edit_file). Xong thì tóm tắt ngắn bạn đã thêm/sửa/chuyển gì.`;
1035
- console.log(c.tool(" 🧠 " + t.learning));
955
+ console.log(c.tool(' 🧠 ' + t.learning));
1036
956
  await handle(prompt);
1037
957
  persist();
1038
958
  }
@@ -1040,24 +960,41 @@ NGUYÊN TẮC:
1040
960
  // /compact — chủ động tóm tắt phiên ngay để gọn ngữ cảnh, giữ trí nhớ dài hạn.
1041
961
  // Khác /clear (xoá sạch) và khác auto-summarize (chỉ chạy khi vượt ngưỡng).
1042
962
  async function runCompact() {
1043
- if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
1044
- if (!state.history?.length) return console.log(c.dim(" " + t.compactEmpty));
1045
- const beforeChars = state.history.reduce((a, m) => a + (typeof m.content === "string" ? m.content.length : 0), 0);
963
+ if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
964
+ if (!state.history?.length) return console.log(c.dim(' ' + t.compactEmpty));
965
+ const beforeChars = state.history.reduce(
966
+ (a, m) => a + (typeof m.content === 'string' ? m.content.length : 0),
967
+ 0
968
+ );
1046
969
  const beforeMsgs = state.history.length;
1047
- console.log(c.tool(" 🗜 " + t.compactRunning));
970
+ console.log(c.tool(' 🗜 ' + t.compactRunning));
1048
971
  tui.setBusy(true, t.compactRunning);
1049
972
  try {
1050
973
  const ok = await maybeSummarize(state.history, { model: state.model, force: true });
1051
974
  tui.setBusy(false);
1052
975
  if (!ok) {
1053
- console.log(c.dim(" " + t.compactSkipped));
976
+ console.log(c.dim(' ' + t.compactSkipped));
1054
977
  return;
1055
978
  }
1056
- const afterChars = state.history.reduce((a, m) => a + (typeof m.content === "string" ? m.content.length : 0), 0);
979
+ const afterChars = state.history.reduce(
980
+ (a, m) => a + (typeof m.content === 'string' ? m.content.length : 0),
981
+ 0
982
+ );
1057
983
  const afterMsgs = state.history.length;
1058
984
  const saved = Math.max(0, beforeChars - afterChars);
1059
985
  const pct = beforeChars > 0 ? Math.round((saved / beforeChars) * 100) : 0;
1060
- console.log(c.ok(" ✓ " + t.compactDone(beforeMsgs, afterMsgs, Math.round(beforeChars / 1000), Math.round(afterChars / 1000), pct)));
986
+ console.log(
987
+ c.ok(
988
+ ' ✓ ' +
989
+ t.compactDone(
990
+ beforeMsgs,
991
+ afterMsgs,
992
+ Math.round(beforeChars / 1000),
993
+ Math.round(afterChars / 1000),
994
+ pct
995
+ )
996
+ )
997
+ );
1061
998
  state._longSessionWarned = false; // reset để có thể cảnh báo lại nếu lại phình
1062
999
  persist();
1063
1000
  } catch (err) {
@@ -1068,9 +1005,9 @@ NGUYÊN TẮC:
1068
1005
 
1069
1006
  function showMemory() {
1070
1007
  const mem = loadMemory();
1071
- if (!mem) return console.log(c.dim(" " + t.memoryEmpty(memoryPath())));
1072
- console.log(box(mem.length > 1800 ? mem.slice(0, 1800) + "\n…" : mem, "noob.md", "#10b981"));
1073
- console.log(c.dim(" " + memoryPath() + t.memoryStat(mem.split("\n").length)));
1008
+ if (!mem) return console.log(c.dim(' ' + t.memoryEmpty(memoryPath())));
1009
+ console.log(box(mem.length > 1800 ? mem.slice(0, 1800) + '\n…' : mem, 'noob.md', '#10b981'));
1010
+ console.log(c.dim(' ' + memoryPath() + t.memoryStat(mem.split('\n').length)));
1074
1011
  }
1075
1012
 
1076
1013
  // /auto-yolo — lưu/bỏ yolo làm MẶC ĐỊNH (mỗi lần mở noob tự bật). Vì yolo tự
@@ -1078,17 +1015,17 @@ NGUYÊN TẮC:
1078
1015
  async function toggleAutoYolo() {
1079
1016
  if (config.yoloDefault) {
1080
1017
  config.setYolo(false);
1081
- return console.log(c.ok(" " + t.autoYoloOff));
1018
+ return console.log(c.ok(' ' + t.autoYoloOff));
1082
1019
  }
1083
- console.log(c.err(" " + t.autoYoloWarn));
1084
- const ans = ((await ask(c.tool(" " + t.autoYoloConfirm))) ?? "").trim().toLowerCase();
1085
- if (ans === "y" || ans === "yes" || ans === "") {
1020
+ console.log(c.err(' ' + t.autoYoloWarn));
1021
+ const ans = ((await ask(c.tool(' ' + t.autoYoloConfirm))) ?? '').trim().toLowerCase();
1022
+ if (ans === 'y' || ans === 'yes' || ans === '') {
1086
1023
  config.setYolo(true);
1087
1024
  state.yolo = true; // áp dụng ngay cho phiên hiện tại
1088
1025
  if (!closed) tui.setPrompt(promptStr(false));
1089
- console.log(c.err(" " + t.autoYoloOn));
1026
+ console.log(c.err(' ' + t.autoYoloOn));
1090
1027
  } else {
1091
- console.log(c.dim(" " + t.autoYoloCancel));
1028
+ console.log(c.dim(' ' + t.autoYoloCancel));
1092
1029
  }
1093
1030
  }
1094
1031
 
@@ -1101,22 +1038,26 @@ NGUYÊN TẮC:
1101
1038
  {
1102
1039
  const stats = memoryStats();
1103
1040
  if (stats) {
1104
- console.log(c.dim(` 📝 noob.md: ${stats.lines} dòng (${stats.rules} rules, ${stats.notes} notes) · cập nhật ${relTime(stats.mtime)}`));
1041
+ console.log(
1042
+ c.dim(
1043
+ ` 📝 noob.md: ${stats.lines} dòng (${stats.rules} rules, ${stats.notes} notes) · cập nhật ${relTime(stats.mtime)}`
1044
+ )
1045
+ );
1105
1046
  } else {
1106
1047
  console.log(c.dim(` 📝 noob.md: chưa có — gõ /init để tạo từ dự án.`));
1107
1048
  }
1108
1049
  }
1109
- if (!config.apiKey) console.log("\n" + c.tool(" " + t.notLoggedIn) + "\n");
1110
- else console.log(c.dim(" " + t.ready + "\n"));
1050
+ if (!config.apiKey) console.log('\n' + c.tool(' ' + t.notLoggedIn) + '\n');
1051
+ else console.log(c.dim(' ' + t.ready + '\n'));
1111
1052
 
1112
1053
  // Auto-update: non-blocking startup check; if newer, update in the background.
1113
- if (process.env.NOOB_NO_AUTOUPDATE !== "1") {
1054
+ if (process.env.NOOB_NO_AUTOUPDATE !== '1') {
1114
1055
  checkLatest()
1115
1056
  .then((v) => {
1116
1057
  if (!v) return;
1117
- console.log(c.tool(" " + t.updateFound(CURRENT, v)));
1058
+ console.log(c.tool(' ' + t.updateFound(CURRENT, v)));
1118
1059
  runUpdate({ background: true });
1119
- console.log(c.dim(" " + t.updateBgDone));
1060
+ console.log(c.dim(' ' + t.updateBgDone));
1120
1061
  })
1121
1062
  .catch(() => {});
1122
1063
  }
@@ -1127,15 +1068,15 @@ NGUYÊN TẮC:
1127
1068
  if (s) await restore(s);
1128
1069
  else {
1129
1070
  startFresh();
1130
- console.log(c.dim(" " + t.sessionNonePrev) + "\n");
1071
+ console.log(c.dim(' ' + t.sessionNonePrev) + '\n');
1131
1072
  }
1132
1073
  } else if (opts.resume === true) {
1133
1074
  if (!(await pickSession())) startFresh();
1134
- } else if (typeof opts.resume === "string") {
1075
+ } else if (typeof opts.resume === 'string') {
1135
1076
  const s = sessions.load(opts.resume);
1136
1077
  if (s) await restore(s);
1137
1078
  else {
1138
- console.log(c.err(" " + t.sessionNotFound(opts.resume)) + "\n");
1079
+ console.log(c.err(' ' + t.sessionNotFound(opts.resume)) + '\n');
1139
1080
  startFresh();
1140
1081
  }
1141
1082
  } else {
@@ -1143,7 +1084,7 @@ NGUYÊN TẮC:
1143
1084
  }
1144
1085
 
1145
1086
  if (opts.prompt) {
1146
- console.log(c.user(t.promptYou) + c.dim("") + opts.prompt);
1087
+ console.log(c.user(t.promptYou) + c.dim('') + opts.prompt);
1147
1088
  if (opts.ultra) await runUltra(opts.prompt);
1148
1089
  else {
1149
1090
  await handle(opts.prompt);
@@ -1156,7 +1097,7 @@ NGUYÊN TẮC:
1156
1097
  let input;
1157
1098
  if (pending.length) {
1158
1099
  // Có tin xếp hàng → tự gửi câu kế tiếp (không chờ gõ).
1159
- input = (pending.shift() ?? "").trim();
1100
+ input = (pending.shift() ?? '').trim();
1160
1101
  } else {
1161
1102
  const raw = await ask(promptStr(false));
1162
1103
  if (raw == null) break; // stdin fully closed and drained
@@ -1167,7 +1108,7 @@ NGUYÊN TẮC:
1167
1108
  // ngoài vòng lặp (sẽ rơi vào .catch ở bin/noob.js → process.exit(1) =
1168
1109
  // "tự động tắt"). Bắt ở đây, in lỗi, rồi tiếp tục vòng lặp.
1169
1110
  try {
1170
- if (input.startsWith("/")) {
1111
+ if (input.startsWith('/')) {
1171
1112
  const done = await command(input);
1172
1113
  if (done) break;
1173
1114
  continue;
@@ -1185,7 +1126,7 @@ NGUYÊN TẮC:
1185
1126
  // ── turn handler ─────────────────────────────────────────────────────────
1186
1127
  async function handle(text) {
1187
1128
  if (!config.apiKey) {
1188
- console.log(c.tool(" " + t.notLoggedIn));
1129
+ console.log(c.tool(' ' + t.notLoggedIn));
1189
1130
  return;
1190
1131
  }
1191
1132
  abort = new AbortController();
@@ -1219,11 +1160,11 @@ NGUYÊN TẮC:
1219
1160
  };
1220
1161
 
1221
1162
  try {
1222
- if (state.mode !== "chat") {
1223
- const name = state.mode === "search" ? "Tìm web" : "Merge AI";
1224
- const label = state.mode === "search" ? t.searching : t.merging;
1163
+ if (state.mode !== 'chat') {
1164
+ const name = state.mode === 'search' ? 'Tìm web' : 'Merge AI';
1165
+ const label = state.mode === 'search' ? t.searching : t.merging;
1225
1166
  startSpin(label);
1226
- const printer = makeStreamPrinter(name, "#f59e0b");
1167
+ const printer = makeStreamPrinter(name, '#f59e0b');
1227
1168
  const { text: answer } = await stream({
1228
1169
  mode: state.mode,
1229
1170
  message: text,
@@ -1241,26 +1182,32 @@ NGUYÊN TẮC:
1241
1182
  });
1242
1183
  stopSpin();
1243
1184
  printer.flush();
1244
- if (!printer.started) printAnswer(answer, name, "#f59e0b");
1185
+ if (!printer.started) printAnswer(answer, name, '#f59e0b');
1245
1186
  return;
1246
1187
  }
1247
1188
 
1248
1189
  const files = mentionedFiles(text);
1249
1190
  const content = files.length
1250
- ? text + `\n\n[File người dùng nhắc tới bằng @: ${files.join(", ")} — đọc bằng read_file nếu cần.]`
1191
+ ? text +
1192
+ `\n\n[File người dùng nhắc tới bằng @: ${files.join(', ')} — đọc bằng read_file nếu cần.]`
1251
1193
  : text;
1252
- state.history.push({ role: "user", content });
1194
+ state.history.push({ role: 'user', content });
1253
1195
  // Update terminal title với session name (trích từ message đầu).
1254
1196
  if (session && !session.title) {
1255
- session.title = content.replace(/\s+/g, " ").trim().slice(0, 60);
1197
+ session.title = content.replace(/\s+/g, ' ').trim().slice(0, 60);
1256
1198
  updateTitle();
1257
1199
  }
1258
1200
  // Tính context tokens realtime — đếm system prompt + history trước khi gửi.
1259
- const systemPrompt = buildSystem(state.history, state.agentMode ? spawnAgentToolsDoc(0) : "", state.goal, sessions.list(5, process.cwd()).filter((s) => s.id !== session?.id));
1201
+ const systemPrompt = buildSystem(
1202
+ state.history,
1203
+ state.agentMode ? spawnAgentToolsDoc(0) : '',
1204
+ state.goal,
1205
+ sessions.list(5, process.cwd()).filter((s) => s.id !== session?.id)
1206
+ );
1260
1207
  const userMessage = buildUserMessage(state.history);
1261
1208
  tokenMeter.setContext(countTokens(systemPrompt) + countTokens(userMessage));
1262
1209
  if (process.stdin.isTTY && !state.steerHintShown) {
1263
- console.log(c.dim(" " + t.steerHint));
1210
+ console.log(c.dim(' ' + t.steerHint));
1264
1211
  state.steerHintShown = true;
1265
1212
  }
1266
1213
  startSpin(t.thinking);
@@ -1269,42 +1216,66 @@ NGUYÊN TẮC:
1269
1216
  const dispatchTool = async (name, input, depth = 0) => {
1270
1217
  // spawn_agent / spawn_agents chỉ được phép khi agentMode bật; depth giới hạn
1271
1218
  // bởi MAX_SUBAGENT_DEPTH để tránh đệ quy nổ.
1272
- if (name === "spawn_agent" || name === "spawn_agents") {
1219
+ if (name === 'spawn_agent' || name === 'spawn_agents') {
1273
1220
  if (!state.agentMode)
1274
- return { allow: true, result: "ERROR: agent mode đang TẮT — gõ /agent on để bật trước khi spawn." };
1221
+ return {
1222
+ allow: true,
1223
+ result: 'ERROR: agent mode đang TẮT — gõ /agent on để bật trước khi spawn.',
1224
+ };
1275
1225
  if (depth >= MAX_SUBAGENT_DEPTH)
1276
- return { allow: true, result: `ERROR: đã đạt depth tối đa (${MAX_SUBAGENT_DEPTH}) — không spawn thêm.` };
1277
- const tasks = name === "spawn_agent" ? [input] : (Array.isArray(input?.agents) ? input.agents : []);
1278
- if (!tasks.length) return { allow: true, result: "ERROR: thiếu task cho sub-agent." };
1226
+ return {
1227
+ allow: true,
1228
+ result: `ERROR: đã đạt depth tối đa (${MAX_SUBAGENT_DEPTH}) không spawn thêm.`,
1229
+ };
1230
+ const tasks =
1231
+ name === 'spawn_agent' ? [input] : Array.isArray(input?.agents) ? input.agents : [];
1232
+ if (!tasks.length) return { allow: true, result: 'ERROR: thiếu task cho sub-agent.' };
1279
1233
  stopSpin();
1280
- console.log(chalk.hex("#8b5cf6")(` ⊕ spawn ${tasks.length} sub-agent (depth ${depth + 1}/${MAX_SUBAGENT_DEPTH})`));
1234
+ console.log(
1235
+ chalk.hex('#8b5cf6')(
1236
+ ` ⊕ spawn ${tasks.length} sub-agent (depth ${depth + 1}/${MAX_SUBAGENT_DEPTH})`
1237
+ )
1238
+ );
1281
1239
  startSpin(t.thinking);
1282
1240
  try {
1283
- const results = await Promise.all(tasks.map((task, i) => {
1284
- // Per-sub-agent model routing: task.model thể là id model hoặc tên thân thiện.
1285
- // findModel() resolve cả hai; nếu không match thì fallback model của cha.
1286
- let subModel = state.model.id;
1287
- let modelTag = "";
1288
- if (task?.model) {
1289
- const m = findModel(task.model);
1290
- if (m) { subModel = m.id; modelTag = ` [${m.name}]`; }
1291
- else modelTag = ` [model "${task.model}" không khớp — dùng ${state.model.name}]`;
1292
- }
1293
- // [GỠ BUDGET 2026-06-06] Sub-agent chạy không giới hạn token.
1294
- return runSubAgent({
1295
- task: task?.task || task?.prompt || "",
1296
- context: task?.context || "",
1297
- depth: depth + 1,
1298
- model: subModel,
1299
- signal: abort.signal,
1300
- tokenMeter,
1301
- dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
1302
- onLog: (msg) => { stopSpin(); console.log(chalk.hex("#8b5cf6")(" " + msg + modelTag)); startSpin(t.thinking); },
1303
- }).then((r) => `── sub-agent #${i + 1}${modelTag} ──\n${r}`).catch((e) => `── sub-agent #${i + 1}${modelTag} (LỖI) ──\n${e?.message || String(e)}`);
1304
- }));
1305
- return { allow: true, result: results.join("\n\n") };
1241
+ const results = await Promise.all(
1242
+ tasks.map((task, i) => {
1243
+ // Per-sub-agent model routing: task.model thể id model hoặc tên thân thiện.
1244
+ // findModel() resolve cả hai; nếu không match thì fallback model của cha.
1245
+ let subModel = state.model.id;
1246
+ let modelTag = '';
1247
+ if (task?.model) {
1248
+ const m = findModel(task.model);
1249
+ if (m) {
1250
+ subModel = m.id;
1251
+ modelTag = ` [${m.name}]`;
1252
+ } else
1253
+ modelTag = ` [model "${task.model}" không khớp — dùng ${state.model.name}]`;
1254
+ }
1255
+ // [GỠ BUDGET 2026-06-06] Sub-agent chạy không giới hạn token.
1256
+ return runSubAgent({
1257
+ task: task?.task || task?.prompt || '',
1258
+ context: task?.context || '',
1259
+ depth: depth + 1,
1260
+ model: subModel,
1261
+ signal: abort.signal,
1262
+ tokenMeter,
1263
+ dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
1264
+ onLog: (msg) => {
1265
+ stopSpin();
1266
+ console.log(chalk.hex('#8b5cf6')(' ' + msg + modelTag));
1267
+ startSpin(t.thinking);
1268
+ },
1269
+ })
1270
+ .then((r) => `── sub-agent #${i + 1}${modelTag} ──\n${r}`)
1271
+ .catch(
1272
+ (e) => `── sub-agent #${i + 1}${modelTag} (LỖI) ──\n${e?.message || String(e)}`
1273
+ );
1274
+ })
1275
+ );
1276
+ return { allow: true, result: results.join('\n\n') };
1306
1277
  } catch (err) {
1307
- return { allow: true, result: "ERROR sub-agent: " + (err?.message || String(err)) };
1278
+ return { allow: true, result: 'ERROR sub-agent: ' + (err?.message || String(err)) };
1308
1279
  }
1309
1280
  }
1310
1281
  stopSpin();
@@ -1313,35 +1284,34 @@ NGUYÊN TẮC:
1313
1284
  return res;
1314
1285
  };
1315
1286
 
1316
- let answer = await runAgent({
1287
+ const answer = await runAgent({
1317
1288
  history: state.history,
1318
1289
  model: state.model.id,
1319
1290
  signal: abort.signal,
1320
1291
  tokenMeter,
1321
1292
  goal: state.goal,
1322
1293
  recentSessions: sessions.list(5, process.cwd()).filter((s) => s.id !== session?.id),
1323
- extraToolsDoc: state.agentMode ? spawnAgentToolsDoc(0) : "",
1294
+ extraToolsDoc: state.agentMode ? spawnAgentToolsDoc(0) : '',
1324
1295
  // Pending tasks: todo items chưa hoàn thành từ lượt trước → model tiếp tục ngay.
1325
1296
  pendingTasks: (state.todos || []).filter((t) => !t.done).map((t) => t.text),
1326
- extraToolsDoc: state.agentMode ? spawnAgentToolsDoc(0) : "",
1327
1297
  onStatus: () => tick(t.thinking),
1328
1298
  onSteer: () => {
1329
1299
  if (!pending.length) return [];
1330
1300
  const msgs = pending.splice(0);
1331
1301
  stopSpin(); // in sạch dòng chèn rồi cho spinner chạy lại
1332
- for (const msg of msgs) console.log(c.user(" " + t.steerInject(truncate(msg, 70))));
1302
+ for (const msg of msgs) console.log(c.user(' ' + t.steerInject(truncate(msg, 70))));
1333
1303
  startSpin(t.thinking);
1334
1304
  return msgs;
1335
1305
  },
1336
1306
  onDelta: (ev) => {
1337
- if (ev.type === "step-start") {
1307
+ if (ev.type === 'step-start') {
1338
1308
  printer = makeStreamPrinter(state.model.name, providerColor(state.model.provider));
1339
- } else if (ev.type === "delta") {
1309
+ } else if (ev.type === 'delta') {
1340
1310
  if (printer.suppressing) return printer.push(ev.text); // nuốt tool JSON → để spinner chạy
1341
1311
  stopSpin();
1342
1312
  printer.push(ev.text);
1343
1313
  if (printer.suppressing) startSpin(t.thinking); // vừa chuyển sang soạn tool
1344
- } else if (ev.type === "step-end") {
1314
+ } else if (ev.type === 'step-end') {
1345
1315
  printer?.flush();
1346
1316
  }
1347
1317
  },
@@ -1359,7 +1329,7 @@ NGUYÊN TẮC:
1359
1329
  return answer; // vòng ULTRA cần text này để dò token hoàn thành
1360
1330
  } catch (err) {
1361
1331
  stopSpin();
1362
- if (err.name === "AbortError") return;
1332
+ if (err.name === 'AbortError') return;
1363
1333
  printError(err);
1364
1334
  } finally {
1365
1335
  abort = null;
@@ -1383,16 +1353,21 @@ NGUYÊN TẮC:
1383
1353
  if (ok) {
1384
1354
  const afterTokens = countMessages(state.history);
1385
1355
  const aK = Math.round(afterTokens / 1000);
1386
- const saved = totalTokens > 0 ? Math.round(((totalTokens - afterTokens) / totalTokens) * 100) : 0;
1387
- console.log(c.ok(` ${t.autoCompactDone(k, aK, saved)} (${Math.round((afterTokens / CONTEXT_WINDOW) * 100)}% context)`));
1356
+ const saved =
1357
+ totalTokens > 0 ? Math.round(((totalTokens - afterTokens) / totalTokens) * 100) : 0;
1358
+ console.log(
1359
+ c.ok(
1360
+ ` ${t.autoCompactDone(k, aK, saved)} (${Math.round((afterTokens / CONTEXT_WINDOW) * 100)}% context)`
1361
+ )
1362
+ );
1388
1363
  state._longSessionWarned = false;
1389
1364
  persist();
1390
1365
  } else {
1391
- console.log(c.err(" " + t.autoCompactFail));
1366
+ console.log(c.err(' ' + t.autoCompactFail));
1392
1367
  }
1393
1368
  } catch (e) {
1394
1369
  tui.setBusy(false);
1395
- console.log(c.err(" " + t.autoCompactFail));
1370
+ console.log(c.err(' ' + t.autoCompactFail));
1396
1371
  } finally {
1397
1372
  state._autoCompacting = false;
1398
1373
  }
@@ -1410,41 +1385,23 @@ NGUYÊN TẮC:
1410
1385
  }
1411
1386
 
1412
1387
  // ── Todo parser ────────────────────────────────────────────────────────────
1413
- // Scan history assistant messages cho pattern `- [ ] task` và `- [x] task`.
1414
- // Trả về [{text, done}] items mới nhất cuối. Dùng state mới nhất (cuối
1415
- // history) để反映todo hiện tại của model.
1416
- function parseTodosFromHistory(history) {
1417
- const todos = [];
1418
- for (const m of history) {
1419
- if (m.role !== "assistant" || typeof m.content !== "string") continue;
1420
- // Match todo items: `- [ ] task` hoặc `- [x] task` (case-insensitive)
1421
- const lines = m.content.split("\n");
1422
- for (const line of lines) {
1423
- const doneMatch = line.match(/^[\s]*-\s*\[x\]\s+(.+)/i);
1424
- if (doneMatch) { todos.push({ text: doneMatch[1].trim(), done: true }); continue; }
1425
- const todoMatch = line.match(/^[\s]*-\s*\[\s?\]\s+(.+)/);
1426
- if (todoMatch) { todos.push({ text: todoMatch[1].trim(), done: false }); }
1427
- }
1428
- }
1429
- // Dedupe: giữ item CUỐI cùng cho mỗi text (model có thể lặp todo)
1430
- const seen = new Map();
1431
- for (const t of todos) seen.set(t.text, t);
1432
- return [...seen.values()];
1433
- }
1388
+ // Implementation đã tách sang src/repl/todos.js (pure function, test).
1389
+ // Import đầu file shadow function này khối comment giữ lại như landmark.
1434
1390
 
1435
1391
  async function execTool(name, input) {
1436
1392
  const desc = describe(name, input);
1437
- const color = name === "run_command" ? "#ef4444" : "#f59e0b";
1438
- console.log("\n" + chalk.hex(color)("" + name) + c.dim(" " + desc));
1393
+ const color = name === 'run_command' ? '#ef4444' : '#f59e0b';
1394
+ console.log('\n' + chalk.hex(color)('' + name) + c.dim(' ' + desc));
1439
1395
 
1440
- if (name === "write_file" && input.content) preview(input.content, input.path);
1441
- else if (name === "edit_file") preview(`- ${truncate(input.old_string)}\n+ ${truncate(input.new_string)}`, input.path);
1396
+ if (name === 'write_file' && input.content) preview(input.content, input.path);
1397
+ else if (name === 'edit_file')
1398
+ preview(`- ${truncate(input.old_string)}\n+ ${truncate(input.new_string)}`, input.path);
1442
1399
 
1443
1400
  if (DESTRUCTIVE.has(name) && !state.yolo && !state.autoApprove.has(name)) {
1444
1401
  const a = await askPermission(name);
1445
- if (a === "a") state.autoApprove.add(name);
1446
- else if (a === "n") {
1447
- console.log(c.err(" " + t.denied));
1402
+ if (a === 'a') state.autoApprove.add(name);
1403
+ else if (a === 'n') {
1404
+ console.log(c.err(' ' + t.denied));
1448
1405
  return { allow: false };
1449
1406
  }
1450
1407
  }
@@ -1459,34 +1416,34 @@ NGUYÊN TẮC:
1459
1416
  // tuyệt đối + có suggestedRoot hợp lệ (folder tồn tại) — tương đối escape cwd
1460
1417
  // thường là model tính sai, để model tự sửa.
1461
1418
  async function execToolCore(name, input, { retried }) {
1462
- tui.status(c.dim(" " + t.running));
1419
+ tui.status(c.dim(' ' + t.running));
1463
1420
  try {
1464
1421
  const result = await runTool(name, input, { signal: abort?.signal });
1465
1422
  tui.status(null);
1466
- console.log(c.ok("") + c.dim(firstLine(result)));
1423
+ console.log(c.ok('') + c.dim(firstLine(result)));
1467
1424
  return { allow: true, result };
1468
1425
  } catch (err) {
1469
1426
  tui.status(null);
1470
1427
  if (err instanceof OutOfScopeError && !retried && err.suggestedRoot) {
1471
1428
  const root = err.suggestedRoot;
1472
1429
  const a = await askAddRoot(root, err.path);
1473
- if (a === "n") {
1474
- console.log(c.err(" " + t.outOfScopeRejected(root)));
1475
- return { allow: true, result: "ERROR: " + err.message };
1430
+ if (a === 'n') {
1431
+ console.log(c.err(' ' + t.outOfScopeRejected(root)));
1432
+ return { allow: true, result: 'ERROR: ' + err.message };
1476
1433
  }
1477
1434
  try {
1478
1435
  addRoot(root);
1479
1436
  if (!state.extraRoots.includes(root)) state.extraRoots.push(root);
1480
- if (a === "a") state.autoApprove.add("add-root");
1481
- console.log(c.ok(" " + t.outOfScopeAdded(root)));
1437
+ if (a === 'a') state.autoApprove.add('add-root');
1438
+ console.log(c.ok(' ' + t.outOfScopeAdded(root)));
1482
1439
  } catch (e) {
1483
- console.log(c.err("" + (e?.message || String(e))));
1484
- return { allow: true, result: "ERROR: " + err.message };
1440
+ console.log(c.err('' + (e?.message || String(e))));
1441
+ return { allow: true, result: 'ERROR: ' + err.message };
1485
1442
  }
1486
1443
  return await execToolCore(name, input, { retried: true });
1487
1444
  }
1488
- console.log(c.err("" + err.message));
1489
- return { allow: true, result: "ERROR: " + err.message };
1445
+ console.log(c.err('' + err.message));
1446
+ return { allow: true, result: 'ERROR: ' + err.message };
1490
1447
  }
1491
1448
  }
1492
1449
 
@@ -1495,21 +1452,24 @@ NGUYÊN TẮC:
1495
1452
  // mọi add-root trong phiên. Cùng style retry với askPermission (lạc → queue).
1496
1453
  async function askAddRoot(root, targetPath) {
1497
1454
  tui.setBusy(false);
1498
- console.log(c.tool(" ⏸ Cần cấp quyền folder: ") + c.accent(root));
1499
- console.log(c.dim(" (model muốn truy cập: " + targetPath + ")"));
1455
+ console.log(c.tool(' ⏸ Cần cấp quyền folder: ') + c.accent(root));
1456
+ console.log(c.dim(' (model muốn truy cập: ' + targetPath + ')'));
1500
1457
  try {
1501
1458
  while (true) {
1502
- const raw = await ask(c.tool(" cho phép? ") + c.dim("[y] thêm vào scope lần này / [a] luôn thêm / [n] từ chối › "));
1503
- if (raw == null) return "n";
1459
+ const raw = await ask(
1460
+ c.tool(' cho phép? ') +
1461
+ c.dim('[y] thêm vào scope lần này / [a] luôn thêm / [n] từ chối › ')
1462
+ );
1463
+ if (raw == null) return 'n';
1504
1464
  const a = raw.trim().toLowerCase();
1505
- if (a === "" || a === "y" || a === "yes" || a === "") return "y";
1506
- if (a === "n" || a === "no" || a === "không") return "n";
1507
- if (a === "a" || a === "always" || a === "luôn") return "a";
1465
+ if (a === '' || a === 'y' || a === 'yes' || a === '') return 'y';
1466
+ if (a === 'n' || a === 'no' || a === 'không') return 'n';
1467
+ if (a === 'a' || a === 'always' || a === 'luôn') return 'a';
1508
1468
  if (raw.trim().length > 3) {
1509
1469
  pending.push(raw);
1510
- console.log(c.dim(" " + t.queued(pending.length, truncate(raw, 60))));
1470
+ console.log(c.dim(' ' + t.queued(pending.length, truncate(raw, 60))));
1511
1471
  }
1512
- console.log(c.dim(" → gõ y / n / a"));
1472
+ console.log(c.dim(' → gõ y / n / a'));
1513
1473
  }
1514
1474
  } finally {
1515
1475
  tui.setBusy(true, t.thinking);
@@ -1527,20 +1487,24 @@ NGUYÊN TẮC:
1527
1487
  // y/n nên lượt TREO. Báo bằng 1 dòng cố định (vào scrollback, không bị vẽ đè)
1528
1488
  // + bỏ spinner để prompt nổi bật. finally khôi phục trạng thái chạy.
1529
1489
  tui.setBusy(false);
1530
- console.log(c.tool(" ⏸ Cần quyền: " + name) + c.dim(" — gõ y (đồng ý) / n (từ chối) / a (luôn cho phép)"));
1490
+ console.log(
1491
+ c.tool(' ⏸ Cần quyền: ' + name) + c.dim(' — gõ y (đồng ý) / n (từ chối) / a (luôn cho phép)')
1492
+ );
1531
1493
  try {
1532
1494
  while (true) {
1533
- const raw = await ask(c.tool(" cho phép? ") + c.dim("[y] có / [n] không / [a] luôn " + name + " › "));
1534
- if (raw == null) return "n"; // stdin đóng thật
1495
+ const raw = await ask(
1496
+ c.tool(' cho phép? ') + c.dim('[y] có / [n] không / [a] luôn ' + name + ' › ')
1497
+ );
1498
+ if (raw == null) return 'n'; // stdin đóng thật
1535
1499
  const a = raw.trim().toLowerCase();
1536
- if (a === "" || a === "y" || a === "yes" || a === "") return "y";
1537
- if (a === "n" || a === "no" || a === "không") return "n";
1538
- if (a === "a" || a === "always" || a === "luôn") return "a";
1500
+ if (a === '' || a === 'y' || a === 'yes' || a === '') return 'y';
1501
+ if (a === 'n' || a === 'no' || a === 'không') return 'n';
1502
+ if (a === 'a' || a === 'always' || a === 'luôn') return 'a';
1539
1503
  if (raw.trim().length > 3) {
1540
1504
  pending.push(raw); // tin nhắn lạc → xếp hàng, gửi sau khi xong lượt
1541
- console.log(c.dim(" " + t.queued(pending.length, truncate(raw, 60))));
1505
+ console.log(c.dim(' ' + t.queued(pending.length, truncate(raw, 60))));
1542
1506
  }
1543
- console.log(c.dim(" " + t.permRetry));
1507
+ console.log(c.dim(' ' + t.permRetry));
1544
1508
  }
1545
1509
  } finally {
1546
1510
  tui.setBusy(true, t.thinking); // khôi phục "đang chạy" cho phần còn lại của lượt
@@ -1550,128 +1514,151 @@ NGUYÊN TẮC:
1550
1514
  // ── slash commands ─────────────────────────────────────────────────────
1551
1515
  async function command(input) {
1552
1516
  const [cmd, ...rest] = input.slice(1).split(/\s+/);
1553
- const arg = rest.join(" ").trim();
1517
+ const arg = rest.join(' ').trim();
1554
1518
  switch (cmd) {
1555
- case "help":
1519
+ case 'help':
1556
1520
  printHelp();
1557
1521
  break;
1558
- case "model":
1522
+ case 'model':
1559
1523
  arg ? selectModel(arg) : listModels();
1560
1524
  break;
1561
- case "models":
1525
+ case 'models':
1562
1526
  listModels();
1563
1527
  break;
1564
- case "merge":
1565
- state.mode = state.mode === "merge" ? "chat" : "merge";
1566
- console.log(c.tool(" " + (state.mode === "merge" ? t.mergeOn : t.mergeOff)));
1528
+ case 'merge':
1529
+ state.mode = state.mode === 'merge' ? 'chat' : 'merge';
1530
+ console.log(c.tool(' ' + (state.mode === 'merge' ? t.mergeOn : t.mergeOff)));
1567
1531
  break;
1568
- case "search":
1569
- state.mode = state.mode === "search" ? "chat" : "search";
1570
- console.log(c.accent(" " + (state.mode === "search" ? t.searchOn : t.searchOff)));
1532
+ case 'search':
1533
+ state.mode = state.mode === 'search' ? 'chat' : 'search';
1534
+ console.log(c.accent(' ' + (state.mode === 'search' ? t.searchOn : t.searchOff)));
1571
1535
  break;
1572
- case "chat":
1573
- state.mode = "chat";
1574
- console.log(c.dim(" " + t.backToChat));
1536
+ case 'chat':
1537
+ state.mode = 'chat';
1538
+ console.log(c.dim(' ' + t.backToChat));
1575
1539
  break;
1576
- case "yolo":
1540
+ case 'yolo':
1577
1541
  state.yolo = !state.yolo;
1578
- console.log((state.yolo ? c.err : c.ok)(" " + (state.yolo ? t.yoloOn : t.yoloOff)));
1542
+ console.log((state.yolo ? c.err : c.ok)(' ' + (state.yolo ? t.yoloOn : t.yoloOff)));
1579
1543
  break;
1580
- case "agent": {
1544
+ case 'agent': {
1581
1545
  const v = arg.toLowerCase();
1582
- if (v === "on" || v === "bật" || v === "bat") state.agentMode = true;
1583
- else if (v === "off" || v === "tắt" || v === "tat") state.agentMode = false;
1546
+ if (v === 'on' || v === 'bật' || v === 'bat') state.agentMode = true;
1547
+ else if (v === 'off' || v === 'tắt' || v === 'tat') state.agentMode = false;
1584
1548
  else state.agentMode = !state.agentMode;
1585
- console.log((state.agentMode ? c.accent : c.dim)(" agent mode: " + (state.agentMode ? "BẬT (spawn_agent / spawn_agents khả dụng, depth tối đa " + MAX_SUBAGENT_DEPTH + ")" : "tắt")));
1549
+ console.log(
1550
+ (state.agentMode ? c.accent : c.dim)(
1551
+ ' agent mode: ' +
1552
+ (state.agentMode
1553
+ ? 'BẬT (spawn_agent / spawn_agents khả dụng, depth tối đa ' +
1554
+ MAX_SUBAGENT_DEPTH +
1555
+ ')'
1556
+ : 'tắt')
1557
+ )
1558
+ );
1586
1559
  break;
1587
1560
  }
1588
- case "goal": {
1561
+ case 'goal': {
1589
1562
  // HARD GOAL = completion requirement (xem tweet_dump.txt mục "Combine
1590
1563
  // with /goal and /loop"). Set xong sẽ inject vào MỌI prompt tới khi clear.
1591
1564
  const v = arg.trim();
1592
1565
  if (!v) {
1593
- if (state.goal) console.log(c.accent(" 🎯 goal: ") + state.goal);
1594
- else console.log(c.dim(" chưa đặt goal. Cú pháp: /goal <mục tiêu> · /goal clear để xoá"));
1595
- } else if (v.toLowerCase() === "clear" || v.toLowerCase() === "off" || v.toLowerCase() === "xoá" || v.toLowerCase() === "xoa") {
1566
+ if (state.goal) console.log(c.accent(' 🎯 goal: ') + state.goal);
1567
+ else
1568
+ console.log(c.dim(' chưa đặt goal. pháp: /goal <mục tiêu> · /goal clear để xoá'));
1569
+ } else if (
1570
+ v.toLowerCase() === 'clear' ||
1571
+ v.toLowerCase() === 'off' ||
1572
+ v.toLowerCase() === 'xoá' ||
1573
+ v.toLowerCase() === 'xoa'
1574
+ ) {
1596
1575
  state.goal = null;
1597
- console.log(c.dim(" đã xoá goal"));
1576
+ console.log(c.dim(' đã xoá goal'));
1598
1577
  persist();
1599
1578
  } else {
1600
1579
  state.goal = v;
1601
- console.log(c.accent(" 🎯 đã đặt goal: ") + v);
1580
+ console.log(c.accent(' 🎯 đã đặt goal: ') + v);
1602
1581
  persist();
1603
1582
  }
1604
1583
  break;
1605
1584
  }
1606
- case "tokens": {
1607
- console.log(c.dim(` tokens — input: ${tokenMeter.input.toLocaleString("vi-VN")} · output: ${tokenMeter.output.toLocaleString("vi-VN")} · tổng: ${tokenMeter.total.toLocaleString("vi-VN")} · ${tokenMeter.format()}`));
1585
+ case 'tokens': {
1586
+ console.log(
1587
+ c.dim(
1588
+ ` tokens — input: ${tokenMeter.input.toLocaleString('vi-VN')} · output: ${tokenMeter.output.toLocaleString('vi-VN')} · tổng: ${tokenMeter.total.toLocaleString('vi-VN')} · ${tokenMeter.format()}`
1589
+ )
1590
+ );
1608
1591
  break;
1609
1592
  }
1610
- case "auto-yolo":
1611
- case "autoyolo":
1593
+ case 'auto-yolo':
1594
+ case 'autoyolo':
1612
1595
  await toggleAutoYolo();
1613
1596
  break;
1614
- case "karpathy":
1615
- case "kcheck":
1616
- case "kc":
1597
+ case 'karpathy':
1598
+ case 'kcheck':
1599
+ case 'kc':
1617
1600
  await runKarpathy(arg);
1618
1601
  break;
1619
- case "frontend-design":
1620
- case "frontend":
1621
- case "fd":
1602
+ case 'frontend-design':
1603
+ case 'frontend':
1604
+ case 'fd':
1622
1605
  await runFrontendDesign(arg);
1623
1606
  break;
1624
- case "workflow":
1625
- case "wf":
1626
- case "ultracode":
1607
+ case 'workflow':
1608
+ case 'wf':
1609
+ case 'ultracode':
1627
1610
  await runWorkflow(arg);
1628
1611
  break;
1629
- case "improve":
1630
- case "imp":
1612
+ case 'improve':
1613
+ case 'imp':
1631
1614
  await runImprove(arg);
1632
1615
  break;
1633
- case "ultra":
1634
- case "u":
1616
+ case 'ultra':
1617
+ case 'u':
1635
1618
  await runUltra(arg);
1636
1619
  break;
1637
- case "loop":
1620
+ case 'loop':
1638
1621
  await runLoop(arg);
1639
1622
  break;
1640
- case "init":
1623
+ case 'init':
1641
1624
  await runInit();
1642
1625
  break;
1643
- case "learn":
1626
+ case 'learn':
1644
1627
  await runLearn(arg);
1645
1628
  break;
1646
- case "compact":
1629
+ case 'compact':
1647
1630
  await runCompact();
1648
1631
  break;
1649
- case "memory":
1650
- case "mem":
1632
+ case 'memory':
1633
+ case 'mem':
1651
1634
  showMemory();
1652
1635
  break;
1653
- case "login":
1636
+ case 'login':
1654
1637
  doLogin(arg);
1655
1638
  break;
1656
- case "logout":
1639
+ case 'logout':
1657
1640
  config.clearKey();
1658
- console.log(c.ok(" " + t.loggedOut));
1641
+ console.log(c.ok(' ' + t.loggedOut));
1659
1642
  break;
1660
- case "usage":
1643
+ case 'usage':
1661
1644
  await showUsage();
1662
1645
  break;
1663
- case "update":
1646
+ case 'update':
1664
1647
  await doUpdate();
1665
1648
  break;
1666
- case "clear":
1667
- case "new":
1649
+ case 'clear':
1650
+ case 'new':
1668
1651
  // Nếu phiên hiện tại có nhiều lượt (≥ 5 user turns) → nhắc /learn TRƯỚC
1669
1652
  // khi xoá, vì sau khi clear thì history mất và /learn sẽ chạy trên
1670
1653
  // history rỗng. In hint để user tự quyết định; không block (UX).
1671
1654
  {
1672
- const userTurns = state.history.filter((m) => m.role === "user").length;
1655
+ const userTurns = state.history.filter((m) => m.role === 'user').length;
1673
1656
  if (userTurns >= 5) {
1674
- console.log(c.dim(` 💡 Phiên này có ${userTurns} lượt. Gõ /learn trước để chưng cất bài học vào noob.md (sau khi /new thì history sẽ mất).`));
1657
+ console.log(
1658
+ c.dim(
1659
+ ` 💡 Phiên này có ${userTurns} lượt. Gõ /learn trước để chưng cất bài học vào noob.md (sau khi /new thì history sẽ mất).`
1660
+ )
1661
+ );
1675
1662
  }
1676
1663
  }
1677
1664
  persist(); // giữ lại phiên cũ trên đĩa
@@ -1685,101 +1672,111 @@ NGUYÊN TẮC:
1685
1672
  {
1686
1673
  const stats = memoryStats();
1687
1674
  if (stats) {
1688
- console.log(c.dim(` 📝 noob.md: ${stats.lines} dòng (${stats.rules} rules, ${stats.notes} notes) · cập nhật ${relTime(stats.mtime)}`));
1675
+ console.log(
1676
+ c.dim(
1677
+ ` 📝 noob.md: ${stats.lines} dòng (${stats.rules} rules, ${stats.notes} notes) · cập nhật ${relTime(stats.mtime)}`
1678
+ )
1679
+ );
1689
1680
  }
1690
1681
  }
1691
- console.log(c.dim(" " + t.ctxCleared + "\n"));
1682
+ console.log(c.dim(' ' + t.ctxCleared + '\n'));
1692
1683
  break;
1693
- case "resume":
1684
+ case 'resume':
1694
1685
  if (arg) {
1695
1686
  const s = sessions.load(arg);
1696
1687
  if (s) await restore(s);
1697
- else console.log(c.err(" " + t.sessionNotFound(arg)));
1688
+ else console.log(c.err(' ' + t.sessionNotFound(arg)));
1698
1689
  } else {
1699
1690
  await pickSession();
1700
1691
  }
1701
1692
  break;
1702
- case "continue": {
1693
+ case 'continue': {
1703
1694
  const s = sessions.latest();
1704
1695
  if (s) await restore(s);
1705
- else console.log(c.dim(" " + t.sessionNonePrev));
1696
+ else console.log(c.dim(' ' + t.sessionNonePrev));
1706
1697
  break;
1707
1698
  }
1708
- case "sessions":
1699
+ case 'sessions':
1709
1700
  listSessions();
1710
1701
  break;
1711
- case "cwd":
1712
- console.log(c.dim(" " + process.cwd()));
1702
+ case 'cwd':
1703
+ console.log(c.dim(' ' + process.cwd()));
1713
1704
  break;
1714
- case "adddir":
1715
- case "add-dir": {
1705
+ case 'adddir':
1706
+ case 'add-dir': {
1716
1707
  // /add-dir remove|rm <path> — gỡ khỏi scope (xóa cả trong file persist).
1717
1708
  if (/^(remove|rm)\b/i.test(arg)) {
1718
- const target = arg.replace(/^(remove|rm)\s*/i, "").trim();
1709
+ const target = arg.replace(/^(remove|rm)\s*/i, '').trim();
1719
1710
  if (!target) {
1720
- console.log(c.err(" " + t.addDirRemoveNeedArg));
1711
+ console.log(c.err(' ' + t.addDirRemoveNeedArg));
1721
1712
  break;
1722
1713
  }
1723
1714
  const full = path.resolve(process.cwd(), target);
1724
1715
  if (removeRoot(full)) {
1725
1716
  state.extraRoots = state.extraRoots.filter((r) => r !== full);
1726
- console.log(c.ok("") + c.dim("đã gỡ khỏi phạm vi: ") + full);
1717
+ console.log(c.ok('') + c.dim('đã gỡ khỏi phạm vi: ') + full);
1727
1718
  } else {
1728
- console.log(c.err(" " + t.addDirNotInScope(full)));
1719
+ console.log(c.err(' ' + t.addDirNotInScope(full)));
1729
1720
  }
1730
1721
  break;
1731
1722
  }
1732
1723
  if (!arg) {
1733
1724
  // Không arg → liệt kê roots hiện tại (cwd + các thư mục đã /add-dir).
1734
1725
  const roots = listRoots();
1735
- console.log(c.dim(" Phạm vi truy cập:"));
1726
+ console.log(c.dim(' Phạm vi truy cập:'));
1736
1727
  for (const r of roots) {
1737
1728
  const isCwd = r === process.cwd();
1738
- console.log(" " + (isCwd ? c.accent("• ") : c.ok("+ ")) + r + (isCwd ? c.dim(" (cwd)") : ""));
1729
+ console.log(
1730
+ ' ' + (isCwd ? c.accent('• ') : c.ok('+ ')) + r + (isCwd ? c.dim(' (cwd)') : '')
1731
+ );
1739
1732
  }
1740
- console.log(c.dim(" Dùng: /add-dir <đường-dẫn> hoặc /add-dir remove <đường-dẫn>"));
1733
+ console.log(c.dim(' Dùng: /add-dir <đường-dẫn> hoặc /add-dir remove <đường-dẫn>'));
1741
1734
  break;
1742
1735
  }
1743
1736
  try {
1744
1737
  const full = addRoot(path.resolve(process.cwd(), arg));
1745
1738
  if (!state.extraRoots.includes(full)) state.extraRoots.push(full);
1746
- console.log(c.ok("") + c.dim("đã thêm vào phạm vi: ") + full);
1747
- console.log(c.dim(" (đã lưu vào .noob/dirs.json — lần sau mở lại tự động áp dụng)"));
1739
+ console.log(c.ok('') + c.dim('đã thêm vào phạm vi: ') + full);
1740
+ console.log(c.dim(' (đã lưu vào .noob/dirs.json — lần sau mở lại tự động áp dụng)'));
1748
1741
  } catch (e) {
1749
- console.log(c.err("") + (e?.message || String(e)));
1742
+ console.log(c.err('') + (e?.message || String(e)));
1750
1743
  }
1751
1744
  break;
1752
1745
  }
1753
- case "status":
1746
+ case 'status':
1754
1747
  printStatus(state);
1755
1748
  break;
1756
- case "version":
1757
- case "v":
1758
- console.log(c.dim(" noob ") + c.accent("v" + CURRENT) + (state.yolo ? c.err(" ⚡ yolo: BẬT") : c.dim(" yolo: tắt")));
1749
+ case 'version':
1750
+ case 'v':
1751
+ console.log(
1752
+ c.dim(' noob ') +
1753
+ c.accent('v' + CURRENT) +
1754
+ (state.yolo ? c.err(' ⚡ yolo: BẬT') : c.dim(' yolo: tắt'))
1755
+ );
1759
1756
  break;
1760
- case "exit":
1761
- case "quit":
1762
- case "q":
1757
+ case 'exit':
1758
+ case 'quit':
1759
+ case 'q':
1763
1760
  persist();
1764
1761
  exiting = true;
1765
- console.log(c.dim(" " + t.bye));
1762
+ console.log(c.dim(' ' + t.bye));
1766
1763
  return true;
1767
1764
  default:
1768
- console.log(c.err(" " + t.unknownCmd(cmd)) + c.dim(" " + t.tryHelp));
1765
+ console.log(c.err(' ' + t.unknownCmd(cmd)) + c.dim(' ' + t.tryHelp));
1769
1766
  }
1770
1767
  return false;
1771
1768
  }
1772
1769
 
1773
1770
  function doLogin(key) {
1774
- if (!key) return console.log(c.err(" " + t.needKeyArg));
1771
+ if (!key) return console.log(c.err(' ' + t.needKeyArg));
1775
1772
  config.setKey(key);
1776
- console.log(c.ok("") + c.dim(t.loginSaved(config.path)));
1773
+ console.log(c.ok('') + c.dim(t.loginSaved(config.path)));
1777
1774
  showUsage().catch(() => {});
1778
1775
  }
1779
1776
 
1780
1777
  async function showUsage() {
1781
- if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
1782
- tui.status(c.dim(" ..."));
1778
+ if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
1779
+ tui.status(c.dim(' ...'));
1783
1780
  try {
1784
1781
  const u = await usage();
1785
1782
  tui.status(null);
@@ -1792,12 +1789,12 @@ NGUYÊN TẮC:
1792
1789
  }
1793
1790
 
1794
1791
  async function doUpdate() {
1795
- console.log(c.dim(" " + t.updateChecking));
1792
+ console.log(c.dim(' ' + t.updateChecking));
1796
1793
  const v = await checkLatest({ throttle: false });
1797
- if (!v) return console.log(c.ok(" " + t.updateLatest(CURRENT)));
1798
- console.log(c.tool(" " + t.updateFound(CURRENT, v)));
1794
+ if (!v) return console.log(c.ok(' ' + t.updateLatest(CURRENT)));
1795
+ console.log(c.tool(' ' + t.updateFound(CURRENT, v)));
1799
1796
  const ok = await runUpdate({ background: false });
1800
- console.log(ok ? c.ok(" " + t.updateOk) : c.err(" " + t.updateFail));
1797
+ console.log(ok ? c.ok(' ' + t.updateOk) : c.err(' ' + t.updateFail));
1801
1798
  }
1802
1799
 
1803
1800
  function selectModel(q) {
@@ -1806,61 +1803,76 @@ NGUYÊN TẮC:
1806
1803
  MODELS.find((x) => x.id === q) ||
1807
1804
  MODELS.find((x) => x.name.toLowerCase() === s) ||
1808
1805
  MODELS.find((x) => x.name.toLowerCase().includes(s) || x.id.includes(s));
1809
- if (!m) return console.log(c.err(" " + t.noModelMatch(q)) + c.dim(" /models"));
1806
+ if (!m) return console.log(c.err(' ' + t.noModelMatch(q)) + c.dim(' /models'));
1810
1807
  state.model = m;
1811
- state.mode = "chat";
1808
+ state.mode = 'chat';
1812
1809
  config.setModel(m.id);
1813
1810
  updateTitle();
1814
- console.log(c.ok(" " + t.switchTo + " ") + modelBadge(m));
1815
- if (m.provider === "openai" || m.provider === "google")
1816
- console.log(c.dim(" ") + c.tool(t.providerRefuses(PROVIDERS[m.provider].name)));
1811
+ console.log(c.ok(' ' + t.switchTo + ' ') + modelBadge(m));
1812
+ if (m.provider === 'openai' || m.provider === 'google')
1813
+ console.log(c.dim(' ') + c.tool(t.providerRefuses(PROVIDERS[m.provider].name)));
1817
1814
  }
1818
1815
 
1819
1816
  function printStatus(s) {
1820
1817
  const mode =
1821
- s.mode === "merge" ? c.tool("Merge AI") : s.mode === "search" ? c.accent("Tìm web") : modelBadge(s.model);
1822
- const key = config.apiKey ? c.ok(" 🔑") : c.err(" 🔒");
1823
- const yolo = s.yolo ? c.err(" ⚡ yolo: BẬT") : c.dim(" yolo: tắt");
1818
+ s.mode === 'merge'
1819
+ ? c.tool('Merge AI')
1820
+ : s.mode === 'search'
1821
+ ? c.accent('Tìm web')
1822
+ : modelBadge(s.model);
1823
+ const key = config.apiKey ? c.ok(' 🔑') : c.err(' 🔒');
1824
+ const yolo = s.yolo ? c.err(' ⚡ yolo: BẬT') : c.dim(' yolo: tắt');
1824
1825
  // Context % — dùng token meter nếu có data, fallback chars.
1825
1826
  const ctxPct = tokenMeter.contextPct();
1826
- const ctxColor = ctxPct !== null
1827
- ? (ctxPct >= 80 ? c.err : ctxPct >= 60 ? c.accent : ctxPct >= 40 ? c.tool : c.dim)
1828
- : c.dim;
1829
- const ctxLabel = ctxPct !== null
1830
- ? ctxColor(` ctx: ${ctxPct}%`)
1831
- : c.dim(" ctx: —");
1827
+ const ctxColor =
1828
+ ctxPct !== null
1829
+ ? ctxPct >= 80
1830
+ ? c.err
1831
+ : ctxPct >= 60
1832
+ ? c.accent
1833
+ : ctxPct >= 40
1834
+ ? c.tool
1835
+ : c.dim
1836
+ : c.dim;
1837
+ const ctxLabel = ctxPct !== null ? ctxColor(` ctx: ${ctxPct}%`) : c.dim(' ctx: —');
1832
1838
  // Token usage.
1833
- const tokLabel = c.dim(` ↑${fmtK(tokenMeter.input)} ↓${fmtK(tokenMeter.output)} (${fmtK(tokenMeter.total)})`);
1839
+ const tokLabel = c.dim(
1840
+ ` ↑${fmtK(tokenMeter.input)} ↓${fmtK(tokenMeter.output)} (${fmtK(tokenMeter.total)})`
1841
+ );
1834
1842
  // Todo progress.
1835
1843
  const todos = s.todos || [];
1836
- let todoLabel = "";
1844
+ let todoLabel = '';
1837
1845
  if (todos.length) {
1838
1846
  const done = todos.filter((t) => t.done).length;
1839
1847
  todoLabel = c.ok(` ✓ ${done}/${todos.length}`);
1840
1848
  }
1841
1849
  // Session ID.
1842
- const sidLabel = session?.id ? c.dim(` 📋 ${session.id}`) : "";
1850
+ const sidLabel = session?.id ? c.dim(` 📋 ${session.id}`) : '';
1843
1851
  // CWD.
1844
- const cwdLabel = c.dim(" 📁 " + shortCwd());
1852
+ const cwdLabel = c.dim(' 📁 ' + shortCwd());
1845
1853
  console.log(
1846
- ` ${mode}${key}${yolo}${ctxLabel}${tokLabel}${todoLabel}\n ${c.dim("v" + CURRENT)}${sidLabel}${cwdLabel}`,
1854
+ ` ${mode}${key}${yolo}${ctxLabel}${tokLabel}${todoLabel}\n ${c.dim('v' + CURRENT)}${sidLabel}${cwdLabel}`
1847
1855
  );
1848
1856
  }
1849
1857
  }
1850
1858
 
1851
1859
  // ── presentation helpers ───────────────────────────────────────────────────
1852
1860
  function fmtK(n) {
1853
- return n >= 1000000 ? (n / 1000000).toFixed(1) + "M" : n >= 1000 ? (n / 1000).toFixed(1) + "k" : String(n);
1861
+ return n >= 1000000
1862
+ ? (n / 1000000).toFixed(1) + 'M'
1863
+ : n >= 1000
1864
+ ? (n / 1000).toFixed(1) + 'k'
1865
+ : String(n);
1854
1866
  }
1855
- function printAnswer(text, name, color) {
1856
- if (!text?.trim()) return;
1857
- console.log("\n" + chalk.hex(color).bold("" + name));
1858
- console.log(
1859
- renderMarkdown(text)
1860
- .split("\n")
1861
- .map((l) => " " + l)
1862
- .join("\n") + "\n",
1863
- );
1867
+ function printAnswer(text, name, color) {
1868
+ if (!text?.trim()) return;
1869
+ console.log('\n' + chalk.hex(color).bold('' + name));
1870
+ console.log(
1871
+ renderMarkdown(text)
1872
+ .split('\n')
1873
+ .map((l) => ' ' + l)
1874
+ .join('\n') + '\n'
1875
+ );
1864
1876
  }
1865
1877
 
1866
1878
  // In câu trả lời theo dòng token thời gian thực. Vì model emit lời + (tuỳ chọn)
@@ -1868,7 +1880,7 @@ function printAnswer(text, name, color) {
1868
1880
  // thấy phần lời + hoạt động công cụ riêng). Giữ lại đuôi vài ký tự để không in
1869
1881
  // nửa vời "```to" trước khi kịp nhận ra đó là fence.
1870
1882
  function makeStreamPrinter(name, color) {
1871
- let buf = "";
1883
+ let buf = '';
1872
1884
  let printed = 0;
1873
1885
  let suppress = false;
1874
1886
  let started = false;
@@ -1877,10 +1889,10 @@ function makeStreamPrinter(name, color) {
1877
1889
  const write = (s) => {
1878
1890
  if (!s) return;
1879
1891
  if (!header) {
1880
- process.stdout.write("\n" + chalk.hex(color).bold("" + name) + "\n ");
1892
+ process.stdout.write('\n' + chalk.hex(color).bold('' + name) + '\n ');
1881
1893
  header = true;
1882
1894
  }
1883
- process.stdout.write(s.replace(/\n/g, "\n "));
1895
+ process.stdout.write(s.replace(/\n/g, '\n '));
1884
1896
  started = true;
1885
1897
  };
1886
1898
  return {
@@ -1890,25 +1902,25 @@ function makeStreamPrinter(name, color) {
1890
1902
  get suppressing() {
1891
1903
  return suppress;
1892
1904
  },
1893
- push(delta) {
1894
- buf += delta;
1895
- if (suppress) return;
1896
- const f = buf.indexOf("```tool");
1897
- if (f !== -1) {
1898
- write(buf.slice(printed, f));
1899
- printed = buf.length;
1900
- suppress = true;
1901
- return;
1902
- }
1903
- const safeEnd = Math.max(printed, buf.length - HOLD);
1904
- if (safeEnd > printed) {
1905
- write(buf.slice(printed, safeEnd));
1906
- printed = safeEnd;
1907
- }
1908
- },
1909
- flush() {
1910
- if (!suppress && printed < buf.length) write(buf.slice(printed));
1911
- if (started) process.stdout.write("\n");
1905
+ push(delta) {
1906
+ buf += delta;
1907
+ if (suppress) return;
1908
+ const f = buf.indexOf('```tool');
1909
+ if (f !== -1) {
1910
+ write(buf.slice(printed, f));
1911
+ printed = buf.length;
1912
+ suppress = true;
1913
+ return;
1914
+ }
1915
+ const safeEnd = Math.max(printed, buf.length - HOLD);
1916
+ if (safeEnd > printed) {
1917
+ write(buf.slice(printed, safeEnd));
1918
+ printed = safeEnd;
1919
+ }
1920
+ },
1921
+ flush() {
1922
+ if (!suppress && printed < buf.length) write(buf.slice(printed));
1923
+ if (started) process.stdout.write('\n');
1912
1924
  },
1913
1925
  };
1914
1926
  }
@@ -1920,26 +1932,35 @@ function printError(err) {
1920
1932
  key_dead: t.errKeyDead,
1921
1933
  trial_exhausted: t.errTrialExhausted,
1922
1934
  key_disabled: t.errDisabled,
1923
- rate_limited: t.errRateLimited(err.reset_at ? fmtTime(err.reset_at) : ""),
1935
+ rate_limited: t.errRateLimited(err.reset_at ? fmtTime(err.reset_at) : ''),
1924
1936
  };
1925
1937
  const msg = (err instanceof ApiError && map[err.code]) || err.message || t.errConn;
1926
- console.log(c.err("" + msg));
1927
- if (err instanceof ApiError && (err.code === "missing_key" || err.code === "invalid_key" || err.status === 401))
1928
- console.log(c.dim(" → noob login <api-key>"));
1938
+ console.log(c.err('' + msg));
1939
+ if (
1940
+ err instanceof ApiError &&
1941
+ (err.code === 'missing_key' || err.code === 'invalid_key' || err.status === 401)
1942
+ )
1943
+ console.log(c.dim(' → noob login <api-key>'));
1929
1944
  }
1930
1945
 
1931
1946
  function printUsage(u) {
1932
- const planName = { pro: "Pro", proplus: "Pro+", ultra: "Ultra", admin: "Admin", trial: "Trial" }[u.plan] || u.plan;
1947
+ const planName =
1948
+ { pro: 'Pro', proplus: 'Pro+', ultra: 'Ultra', admin: 'Admin', trial: 'Trial' }[u.plan] ||
1949
+ u.plan;
1933
1950
  const lines = [
1934
1951
  chalk.bold(t.usageTitle),
1935
- ` ${t.plan}: ${chalk.bold(planName)} ${t.status}: ${u.status === "active" ? c.ok(u.status) : c.err(u.status)}`,
1952
+ ` ${t.plan}: ${chalk.bold(planName)} ${t.status}: ${u.status === 'active' ? c.ok(u.status) : c.err(u.status)}`,
1936
1953
  ];
1937
- if (u.plan === "admin") lines.push(` ${t.remaining}: ${c.ok(t.unlimited)}`);
1938
- else if (u.plan === "trial") lines.push(` ${t.remaining}: ${c.accent(t.trialLeft(u.remaining ?? 0))}`);
1939
- else lines.push(` ${t.remaining}: ${c.accent(String(u.remaining))} / ${u.limit} (${t.windowInfo(u.window_count ?? 0, u.limit)})`);
1954
+ if (u.plan === 'admin') lines.push(` ${t.remaining}: ${c.ok(t.unlimited)}`);
1955
+ else if (u.plan === 'trial')
1956
+ lines.push(` ${t.remaining}: ${c.accent(t.trialLeft(u.remaining ?? 0))}`);
1957
+ else
1958
+ lines.push(
1959
+ ` ${t.remaining}: ${c.accent(String(u.remaining))} / ${u.limit} (${t.windowInfo(u.window_count ?? 0, u.limit)})`
1960
+ );
1940
1961
  if (u.reset_at) lines.push(c.dim(` ${t.resetAt}: ${fmtTime(u.reset_at)}`));
1941
1962
  if (u.total_used != null) lines.push(c.dim(` ${t.used} (tổng): ${u.total_used}`));
1942
- console.log(box(lines.join("\n"), t.usageTitle, "#a78bfa"));
1963
+ console.log(box(lines.join('\n'), t.usageTitle, '#a78bfa'));
1943
1964
  }
1944
1965
 
1945
1966
  function printHelp() {
@@ -1947,47 +1968,47 @@ function printHelp() {
1947
1968
  box(
1948
1969
  [
1949
1970
  chalk.bold(t.helpCommands),
1950
- " " + t.cmdModel,
1951
- " " + t.cmdModels,
1952
- " " + t.cmdMerge,
1953
- " " + t.cmdSearch,
1954
- " " + t.cmdChat,
1955
- " " + t.cmdYolo,
1956
- " " + t.cmdAgent,
1957
- " " + t.cmdTokens,
1958
- " " + t.cmdAutoYolo,
1959
- " " + t.cmdInit,
1960
- " " + t.cmdKarpathy,
1961
- " " + t.cmdFrontendDesign,
1962
- " " + t.cmdImprove,
1963
- " " + t.cmdUltra,
1964
- " " + t.cmdWorkflow,
1965
- " " + t.cmdGoal,
1966
- " " + t.cmdLoop,
1967
- " " + t.cmdLearn,
1968
- " " + t.cmdCompact,
1969
- " " + t.cmdMemory,
1970
- " " + t.cmdAddDir,
1971
- " " + t.cmdLogin,
1972
- " " + t.cmdLogout,
1973
- " " + t.cmdUsage,
1974
- " " + t.cmdUpdate,
1975
- " " + t.cmdClear,
1976
- " " + t.cmdResume,
1977
- " " + t.cmdSessions,
1978
- " " + t.cmdStatus,
1979
- " " + t.cmdVersion,
1980
- " " + t.cmdExit,
1981
- "",
1971
+ ' ' + t.cmdModel,
1972
+ ' ' + t.cmdModels,
1973
+ ' ' + t.cmdMerge,
1974
+ ' ' + t.cmdSearch,
1975
+ ' ' + t.cmdChat,
1976
+ ' ' + t.cmdYolo,
1977
+ ' ' + t.cmdAgent,
1978
+ ' ' + t.cmdTokens,
1979
+ ' ' + t.cmdAutoYolo,
1980
+ ' ' + t.cmdInit,
1981
+ ' ' + t.cmdKarpathy,
1982
+ ' ' + t.cmdFrontendDesign,
1983
+ ' ' + t.cmdImprove,
1984
+ ' ' + t.cmdUltra,
1985
+ ' ' + t.cmdWorkflow,
1986
+ ' ' + t.cmdGoal,
1987
+ ' ' + t.cmdLoop,
1988
+ ' ' + t.cmdLearn,
1989
+ ' ' + t.cmdCompact,
1990
+ ' ' + t.cmdMemory,
1991
+ ' ' + t.cmdAddDir,
1992
+ ' ' + t.cmdLogin,
1993
+ ' ' + t.cmdLogout,
1994
+ ' ' + t.cmdUsage,
1995
+ ' ' + t.cmdUpdate,
1996
+ ' ' + t.cmdClear,
1997
+ ' ' + t.cmdResume,
1998
+ ' ' + t.cmdSessions,
1999
+ ' ' + t.cmdStatus,
2000
+ ' ' + t.cmdVersion,
2001
+ ' ' + t.cmdExit,
2002
+ '',
1982
2003
  chalk.bold(t.helpTips),
1983
- c.dim(" " + t.tip1),
1984
- c.dim(" " + t.tip2),
1985
- c.dim(" " + t.tip3),
1986
- c.dim(" " + t.tip4),
1987
- ].join("\n"),
2004
+ c.dim(' ' + t.tip1),
2005
+ c.dim(' ' + t.tip2),
2006
+ c.dim(' ' + t.tip3),
2007
+ c.dim(' ' + t.tip4),
2008
+ ].join('\n'),
1988
2009
  t.helpTitle,
1989
- "#a78bfa",
1990
- ),
2010
+ '#a78bfa'
2011
+ )
1991
2012
  );
1992
2013
  }
1993
2014
 
@@ -1997,42 +2018,46 @@ function listModels() {
1997
2018
  const lines = [];
1998
2019
  for (const [pk, ms] of Object.entries(byProv)) {
1999
2020
  lines.push(chalk.hex(providerColor(pk)).bold(PROVIDERS[pk]?.name || pk));
2000
- lines.push(ms.map((m) => " " + chalk.hex(providerColor(pk))("●") + " " + m.name + c.dim(` (${m.tier})`)).join("\n"));
2021
+ lines.push(
2022
+ ms
2023
+ .map((m) => ' ' + chalk.hex(providerColor(pk))('●') + ' ' + m.name + c.dim(` (${m.tier})`))
2024
+ .join('\n')
2025
+ );
2001
2026
  }
2002
- console.log("\n" + lines.join("\n") + c.dim("\n\n " + t.modelListHint) + "\n");
2027
+ console.log('\n' + lines.join('\n') + c.dim('\n\n ' + t.modelListHint) + '\n');
2003
2028
  }
2004
2029
 
2005
2030
  const shortCwd = () => {
2006
2031
  const p = process.cwd();
2007
- return p.length > 48 ? "" + p.slice(-47) : p;
2032
+ return p.length > 48 ? '' + p.slice(-47) : p;
2008
2033
  };
2009
- const shortPath = (p = "") => (p.length > 30 ? "" + p.slice(-29) : p);
2034
+ const shortPath = (p = '') => (p.length > 30 ? '' + p.slice(-29) : p);
2010
2035
  const relTime = (ts) => {
2011
2036
  const m = Math.round((Date.now() - ts) / 60000);
2012
- if (m < 1) return "vừa xong";
2013
- if (m < 60) return m + " phút trước";
2037
+ if (m < 1) return 'vừa xong';
2038
+ if (m < 60) return m + ' phút trước';
2014
2039
  const h = Math.round(m / 60);
2015
- if (h < 24) return h + " giờ trước";
2016
- return Math.round(h / 24) + " ngày trước";
2040
+ if (h < 24) return h + ' giờ trước';
2041
+ return Math.round(h / 24) + ' ngày trước';
2017
2042
  };
2018
- const firstLine = (s) => (s.split("\n")[0] || "").slice(0, 100);
2019
- const truncate = (s = "", n = 120) => (s.length > n ? s.slice(0, n) + "" : s).replace(/\n/g, "");
2043
+ const firstLine = (s) => (s.split('\n')[0] || '').slice(0, 100);
2044
+ const truncate = (s = '', n = 120) => (s.length > n ? s.slice(0, n) + '' : s).replace(/\n/g, '');
2020
2045
  const fmtTime = (iso) => {
2021
2046
  try {
2022
- return new Date(iso).toLocaleString("vi-VN");
2047
+ return new Date(iso).toLocaleString('vi-VN');
2023
2048
  } catch {
2024
2049
  return iso;
2025
2050
  }
2026
2051
  };
2027
2052
 
2028
2053
  function preview(content, label) {
2029
- const lines = content.split("\n").slice(0, 12);
2030
- const more = content.split("\n").length - lines.length;
2054
+ const lines = content.split('\n').slice(0, 12);
2055
+ const more = content.split('\n').length - lines.length;
2031
2056
  console.log(
2032
- c.dim(" ┌─ " + (label || "")) +
2033
- "\n" +
2034
- lines.map((l) => c.dim("") + l.slice(0, 110)).join("\n") +
2035
- (more > 0 ? c.dim(`\n │ … +${more} dòng nữa`) : "") +
2036
- c.dim("\n └─"),
2057
+ c.dim(' ┌─ ' + (label || '')) +
2058
+ '\n' +
2059
+ lines.map((l) => c.dim('') + l.slice(0, 110)).join('\n') +
2060
+ (more > 0 ? c.dim(`\n │ … +${more} dòng nữa`) : '') +
2061
+ c.dim('\n └─')
2037
2062
  );
2038
2063
  }