@noobdemon/noob-cli 1.10.19 → 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/CHANGELOG.md +465 -0
- package/README.md +113 -27
- package/bin/noob.js +40 -27
- package/package.json +30 -2
- package/src/agent.js +223 -139
- package/src/api.js +105 -48
- package/src/config.js +11 -11
- package/src/i18n.js +171 -148
- package/src/memory.js +24 -13
- package/src/models.js +96 -46
- package/src/prompts/system.md +85 -0
- package/src/repl/complete.js +120 -0
- package/src/repl/todos.js +38 -0
- package/src/repl/ultra.js +62 -0
- package/src/repl/workflow-commands.js +238 -0
- package/src/repl.js +794 -769
- package/src/sessions.js +20 -20
- package/src/skills.js +13 -9
- package/src/subagent.js +3 -3
- package/src/tokens.js +37 -12
- package/src/tools.js +202 -121
- package/src/tui.js +240 -124
- package/src/ui.js +44 -44
- package/src/update.js +21 -21
- package/src/workflows-builtin.js +16 -14
- package/src/workflows.js +29 -27
package/src/repl.js
CHANGED
|
@@ -1,142 +1,54 @@
|
|
|
1
|
-
import process from
|
|
2
|
-
import fs from
|
|
3
|
-
import path from
|
|
4
|
-
import chalk from
|
|
5
|
-
import { createTui } from
|
|
6
|
-
import { runAgent, maybeSummarize, buildSystem, buildUserMessage } from
|
|
7
|
-
import { runSubAgent, spawnAgentToolsDoc, MAX_SUBAGENT_DEPTH } from
|
|
8
|
-
import { TokenMeter, countMessages, CONTEXT_WINDOW, countTokens } from
|
|
9
|
-
import { stream, usage, ApiError, resetMemoryToken } from
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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:
|
|
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
|
-
|
|
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 ?
|
|
167
|
-
const yolo = state.yolo ? c.err(
|
|
168
|
-
return c.user(nl + t.promptYou) + yolo + c.dim(
|
|
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(
|
|
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(
|
|
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 ===
|
|
217
|
-
process.stderr.write(
|
|
218
|
-
|
|
219
|
-
|
|
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(
|
|
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(
|
|
169
|
+
console.log(c.dim(' ' + t.queueCleared(n)));
|
|
248
170
|
}
|
|
249
|
-
console.log(c.err(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
284
|
-
if (abort) {
|
|
285
|
-
|
|
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(
|
|
289
|
-
console.log(c.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 =
|
|
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(
|
|
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(
|
|
334
|
-
const turns = state.history.filter((m) => m.role ===
|
|
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(
|
|
278
|
+
console.log(c.dim(' ' + t.sessionEmpty) + '\n');
|
|
344
279
|
return null;
|
|
345
280
|
}
|
|
346
|
-
console.log(
|
|
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 ||
|
|
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(
|
|
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(
|
|
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(
|
|
368
|
-
console.log(
|
|
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(
|
|
372
|
-
chalk.bold(s.title ||
|
|
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(
|
|
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(
|
|
391
|
-
if (!arg) return console.log(c.err(
|
|
392
|
-
const skill = loadSkill(
|
|
393
|
-
if (!skill) return console.log(c.err(
|
|
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(
|
|
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(
|
|
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(
|
|
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 ===
|
|
439
|
-
if (sub ===
|
|
440
|
-
if (sub ===
|
|
441
|
-
if (sub ===
|
|
442
|
-
if (sub ===
|
|
443
|
-
if (sub ===
|
|
444
|
-
if (sub ===
|
|
445
|
-
if (sub ===
|
|
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(
|
|
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(
|
|
460
|
-
|
|
401
|
+
const raw = await ask(
|
|
402
|
+
c.tool(' bật agent mode và 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 ===
|
|
463
|
-
if (a ===
|
|
407
|
+
if (a === '' || a === 'y' || a === 'yes' || a === 'có') 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(
|
|
411
|
+
console.log(c.dim(' ' + t.queued(pending.length, truncate(raw, 60))));
|
|
467
412
|
}
|
|
468
|
-
console.log(c.dim(
|
|
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(
|
|
485
|
-
if (!skill)
|
|
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 !==
|
|
514
|
-
return console.log(
|
|
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(
|
|
471
|
+
console.log(
|
|
472
|
+
c.tool(' ✓ ' + (t.workflowAgentEnabled || 'đã bật agent mode cho workflow này.'))
|
|
473
|
+
);
|
|
518
474
|
}
|
|
519
|
-
console.log(c.tool(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
578
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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 =
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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(
|
|
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(
|
|
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 !==
|
|
679
|
-
console.log(
|
|
569
|
+
if (a !== 'y' && a !== 'yes' && a !== 'có') {
|
|
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(
|
|
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(
|
|
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)
|
|
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(
|
|
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)
|
|
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] ||
|
|
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(
|
|
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(
|
|
710
|
-
|
|
711
|
-
|
|
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)
|
|
716
|
-
|
|
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(
|
|
721
|
-
|
|
722
|
-
|
|
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(
|
|
730
|
-
const focus = arg
|
|
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(
|
|
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(
|
|
741
|
-
const target = arg
|
|
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(
|
|
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
|
-
//
|
|
757
|
-
//
|
|
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 vì cần closure handle/persist/state.
|
|
799
701
|
|
|
800
702
|
async function runUltra(goal) {
|
|
801
|
-
if (!config.apiKey) return console.log(c.tool(
|
|
802
|
-
if (!goal) return console.log(c.err(
|
|
803
|
-
state.mode =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
847
|
-
content:
|
|
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:
|
|
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,
|
|
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 ===
|
|
871
|
-
else if (u ===
|
|
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) +
|
|
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),
|
|
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),
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 ||
|
|
848
|
+
const a = (arg || '').trim();
|
|
932
849
|
// /loop (no arg) → status
|
|
933
850
|
if (!a) {
|
|
934
|
-
if (!state.loop)
|
|
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(
|
|
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(
|
|
943
|
-
else console.log(c.dim(
|
|
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(
|
|
947
|
-
if (!config.apiKey) return console.log(c.tool(
|
|
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(
|
|
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(
|
|
874
|
+
if (!task) return console.log(c.err(' ' + t.loopNeedArgs));
|
|
955
875
|
const intervalMs = parseInterval(intervalStr);
|
|
956
|
-
if (!intervalMs) return console.log(c.err(
|
|
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(
|
|
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(
|
|
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(
|
|
980
|
-
const ans = ((await ask(c.tool(
|
|
981
|
-
if (ans !==
|
|
982
|
-
console.log(c.dim(
|
|
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 !== 'có') {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1044
|
-
if (!state.history?.length) return console.log(c.dim(
|
|
1045
|
-
const beforeChars = state.history.reduce(
|
|
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(
|
|
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(
|
|
976
|
+
console.log(c.dim(' ' + t.compactSkipped));
|
|
1054
977
|
return;
|
|
1055
978
|
}
|
|
1056
|
-
const afterChars = state.history.reduce(
|
|
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(
|
|
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(
|
|
1072
|
-
console.log(box(mem.length > 1800 ? mem.slice(0, 1800) +
|
|
1073
|
-
console.log(c.dim(
|
|
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(
|
|
1018
|
+
return console.log(c.ok(' ' + t.autoYoloOff));
|
|
1082
1019
|
}
|
|
1083
|
-
console.log(c.err(
|
|
1084
|
-
const ans = ((await ask(c.tool(
|
|
1085
|
-
if (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 === 'có') {
|
|
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(
|
|
1026
|
+
console.log(c.err(' ' + t.autoYoloOn));
|
|
1090
1027
|
} else {
|
|
1091
|
-
console.log(c.dim(
|
|
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(
|
|
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(
|
|
1110
|
-
else console.log(c.dim(
|
|
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 !==
|
|
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(
|
|
1058
|
+
console.log(c.tool(' ' + t.updateFound(CURRENT, v)));
|
|
1118
1059
|
runUpdate({ background: true });
|
|
1119
|
-
console.log(c.dim(
|
|
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(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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() ??
|
|
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(
|
|
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 !==
|
|
1223
|
-
const name = state.mode ===
|
|
1224
|
-
const label = state.mode ===
|
|
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,
|
|
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,
|
|
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 +
|
|
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:
|
|
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,
|
|
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(
|
|
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(
|
|
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 ===
|
|
1219
|
+
if (name === 'spawn_agent' || name === 'spawn_agents') {
|
|
1273
1220
|
if (!state.agentMode)
|
|
1274
|
-
return {
|
|
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 {
|
|
1277
|
-
|
|
1278
|
-
|
|
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(
|
|
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(
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1241
|
+
const results = await Promise.all(
|
|
1242
|
+
tasks.map((task, i) => {
|
|
1243
|
+
// Per-sub-agent model routing: task.model có thể là 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:
|
|
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
|
-
|
|
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(
|
|
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 ===
|
|
1307
|
+
if (ev.type === 'step-start') {
|
|
1338
1308
|
printer = makeStreamPrinter(state.model.name, providerColor(state.model.provider));
|
|
1339
|
-
} else if (ev.type ===
|
|
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 ===
|
|
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 ===
|
|
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 =
|
|
1387
|
-
|
|
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(
|
|
1366
|
+
console.log(c.err(' ' + t.autoCompactFail));
|
|
1392
1367
|
}
|
|
1393
1368
|
} catch (e) {
|
|
1394
1369
|
tui.setBusy(false);
|
|
1395
|
-
console.log(c.err(
|
|
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
|
-
//
|
|
1414
|
-
//
|
|
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, có 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 ===
|
|
1438
|
-
console.log(
|
|
1393
|
+
const color = name === 'run_command' ? '#ef4444' : '#f59e0b';
|
|
1394
|
+
console.log('\n' + chalk.hex(color)(' ⚙ ' + name) + c.dim(' ' + desc));
|
|
1439
1395
|
|
|
1440
|
-
if (name ===
|
|
1441
|
-
else if (name ===
|
|
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 ===
|
|
1446
|
-
else if (a ===
|
|
1447
|
-
console.log(c.err(
|
|
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(
|
|
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(
|
|
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 ===
|
|
1474
|
-
console.log(c.err(
|
|
1475
|
-
return { allow: true, result:
|
|
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 ===
|
|
1481
|
-
console.log(c.ok(
|
|
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(
|
|
1484
|
-
return { allow: true, result:
|
|
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(
|
|
1489
|
-
return { allow: true, result:
|
|
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(
|
|
1499
|
-
console.log(c.dim(
|
|
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(
|
|
1503
|
-
|
|
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 ===
|
|
1506
|
-
if (a ===
|
|
1507
|
-
if (a ===
|
|
1465
|
+
if (a === '' || a === 'y' || a === 'yes' || a === 'có') 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(
|
|
1470
|
+
console.log(c.dim(' ' + t.queued(pending.length, truncate(raw, 60))));
|
|
1511
1471
|
}
|
|
1512
|
-
console.log(c.dim(
|
|
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(
|
|
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(
|
|
1534
|
-
|
|
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 ===
|
|
1537
|
-
if (a ===
|
|
1538
|
-
if (a ===
|
|
1500
|
+
if (a === '' || a === 'y' || a === 'yes' || a === 'có') 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(
|
|
1505
|
+
console.log(c.dim(' ' + t.queued(pending.length, truncate(raw, 60))));
|
|
1542
1506
|
}
|
|
1543
|
-
console.log(c.dim(
|
|
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(
|
|
1517
|
+
const arg = rest.join(' ').trim();
|
|
1554
1518
|
switch (cmd) {
|
|
1555
|
-
case
|
|
1519
|
+
case 'help':
|
|
1556
1520
|
printHelp();
|
|
1557
1521
|
break;
|
|
1558
|
-
case
|
|
1522
|
+
case 'model':
|
|
1559
1523
|
arg ? selectModel(arg) : listModels();
|
|
1560
1524
|
break;
|
|
1561
|
-
case
|
|
1525
|
+
case 'models':
|
|
1562
1526
|
listModels();
|
|
1563
1527
|
break;
|
|
1564
|
-
case
|
|
1565
|
-
state.mode = state.mode ===
|
|
1566
|
-
console.log(c.tool(
|
|
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
|
|
1569
|
-
state.mode = state.mode ===
|
|
1570
|
-
console.log(c.accent(
|
|
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
|
|
1573
|
-
state.mode =
|
|
1574
|
-
console.log(c.dim(
|
|
1536
|
+
case 'chat':
|
|
1537
|
+
state.mode = 'chat';
|
|
1538
|
+
console.log(c.dim(' ' + t.backToChat));
|
|
1575
1539
|
break;
|
|
1576
|
-
case
|
|
1540
|
+
case 'yolo':
|
|
1577
1541
|
state.yolo = !state.yolo;
|
|
1578
|
-
console.log((state.yolo ? c.err : c.ok)(
|
|
1542
|
+
console.log((state.yolo ? c.err : c.ok)(' ' + (state.yolo ? t.yoloOn : t.yoloOff)));
|
|
1579
1543
|
break;
|
|
1580
|
-
case
|
|
1544
|
+
case 'agent': {
|
|
1581
1545
|
const v = arg.toLowerCase();
|
|
1582
|
-
if (v ===
|
|
1583
|
-
else if (v ===
|
|
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(
|
|
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
|
|
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(
|
|
1594
|
-
else
|
|
1595
|
-
|
|
1566
|
+
if (state.goal) console.log(c.accent(' 🎯 goal: ') + state.goal);
|
|
1567
|
+
else
|
|
1568
|
+
console.log(c.dim(' chưa đặt goal. Cú 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(
|
|
1576
|
+
console.log(c.dim(' đã xoá goal'));
|
|
1598
1577
|
persist();
|
|
1599
1578
|
} else {
|
|
1600
1579
|
state.goal = v;
|
|
1601
|
-
console.log(c.accent(
|
|
1580
|
+
console.log(c.accent(' 🎯 đã đặt goal: ') + v);
|
|
1602
1581
|
persist();
|
|
1603
1582
|
}
|
|
1604
1583
|
break;
|
|
1605
1584
|
}
|
|
1606
|
-
case
|
|
1607
|
-
console.log(
|
|
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
|
|
1611
|
-
case
|
|
1593
|
+
case 'auto-yolo':
|
|
1594
|
+
case 'autoyolo':
|
|
1612
1595
|
await toggleAutoYolo();
|
|
1613
1596
|
break;
|
|
1614
|
-
case
|
|
1615
|
-
case
|
|
1616
|
-
case
|
|
1597
|
+
case 'karpathy':
|
|
1598
|
+
case 'kcheck':
|
|
1599
|
+
case 'kc':
|
|
1617
1600
|
await runKarpathy(arg);
|
|
1618
1601
|
break;
|
|
1619
|
-
case
|
|
1620
|
-
case
|
|
1621
|
-
case
|
|
1602
|
+
case 'frontend-design':
|
|
1603
|
+
case 'frontend':
|
|
1604
|
+
case 'fd':
|
|
1622
1605
|
await runFrontendDesign(arg);
|
|
1623
1606
|
break;
|
|
1624
|
-
case
|
|
1625
|
-
case
|
|
1626
|
-
case
|
|
1607
|
+
case 'workflow':
|
|
1608
|
+
case 'wf':
|
|
1609
|
+
case 'ultracode':
|
|
1627
1610
|
await runWorkflow(arg);
|
|
1628
1611
|
break;
|
|
1629
|
-
case
|
|
1630
|
-
case
|
|
1612
|
+
case 'improve':
|
|
1613
|
+
case 'imp':
|
|
1631
1614
|
await runImprove(arg);
|
|
1632
1615
|
break;
|
|
1633
|
-
case
|
|
1634
|
-
case
|
|
1616
|
+
case 'ultra':
|
|
1617
|
+
case 'u':
|
|
1635
1618
|
await runUltra(arg);
|
|
1636
1619
|
break;
|
|
1637
|
-
case
|
|
1620
|
+
case 'loop':
|
|
1638
1621
|
await runLoop(arg);
|
|
1639
1622
|
break;
|
|
1640
|
-
case
|
|
1623
|
+
case 'init':
|
|
1641
1624
|
await runInit();
|
|
1642
1625
|
break;
|
|
1643
|
-
case
|
|
1626
|
+
case 'learn':
|
|
1644
1627
|
await runLearn(arg);
|
|
1645
1628
|
break;
|
|
1646
|
-
case
|
|
1629
|
+
case 'compact':
|
|
1647
1630
|
await runCompact();
|
|
1648
1631
|
break;
|
|
1649
|
-
case
|
|
1650
|
-
case
|
|
1632
|
+
case 'memory':
|
|
1633
|
+
case 'mem':
|
|
1651
1634
|
showMemory();
|
|
1652
1635
|
break;
|
|
1653
|
-
case
|
|
1636
|
+
case 'login':
|
|
1654
1637
|
doLogin(arg);
|
|
1655
1638
|
break;
|
|
1656
|
-
case
|
|
1639
|
+
case 'logout':
|
|
1657
1640
|
config.clearKey();
|
|
1658
|
-
console.log(c.ok(
|
|
1641
|
+
console.log(c.ok(' ' + t.loggedOut));
|
|
1659
1642
|
break;
|
|
1660
|
-
case
|
|
1643
|
+
case 'usage':
|
|
1661
1644
|
await showUsage();
|
|
1662
1645
|
break;
|
|
1663
|
-
case
|
|
1646
|
+
case 'update':
|
|
1664
1647
|
await doUpdate();
|
|
1665
1648
|
break;
|
|
1666
|
-
case
|
|
1667
|
-
case
|
|
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 ===
|
|
1655
|
+
const userTurns = state.history.filter((m) => m.role === 'user').length;
|
|
1673
1656
|
if (userTurns >= 5) {
|
|
1674
|
-
console.log(
|
|
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(
|
|
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(
|
|
1682
|
+
console.log(c.dim(' ' + t.ctxCleared + '\n'));
|
|
1692
1683
|
break;
|
|
1693
|
-
case
|
|
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(
|
|
1688
|
+
else console.log(c.err(' ' + t.sessionNotFound(arg)));
|
|
1698
1689
|
} else {
|
|
1699
1690
|
await pickSession();
|
|
1700
1691
|
}
|
|
1701
1692
|
break;
|
|
1702
|
-
case
|
|
1693
|
+
case 'continue': {
|
|
1703
1694
|
const s = sessions.latest();
|
|
1704
1695
|
if (s) await restore(s);
|
|
1705
|
-
else console.log(c.dim(
|
|
1696
|
+
else console.log(c.dim(' ' + t.sessionNonePrev));
|
|
1706
1697
|
break;
|
|
1707
1698
|
}
|
|
1708
|
-
case
|
|
1699
|
+
case 'sessions':
|
|
1709
1700
|
listSessions();
|
|
1710
1701
|
break;
|
|
1711
|
-
case
|
|
1712
|
-
console.log(c.dim(
|
|
1702
|
+
case 'cwd':
|
|
1703
|
+
console.log(c.dim(' ' + process.cwd()));
|
|
1713
1704
|
break;
|
|
1714
|
-
case
|
|
1715
|
-
case
|
|
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,
|
|
1709
|
+
const target = arg.replace(/^(remove|rm)\s*/i, '').trim();
|
|
1719
1710
|
if (!target) {
|
|
1720
|
-
console.log(c.err(
|
|
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(
|
|
1717
|
+
console.log(c.ok(' ✓ ') + c.dim('đã gỡ khỏi phạm vi: ') + full);
|
|
1727
1718
|
} else {
|
|
1728
|
-
console.log(c.err(
|
|
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(
|
|
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(
|
|
1729
|
+
console.log(
|
|
1730
|
+
' ' + (isCwd ? c.accent('• ') : c.ok('+ ')) + r + (isCwd ? c.dim(' (cwd)') : '')
|
|
1731
|
+
);
|
|
1739
1732
|
}
|
|
1740
|
-
console.log(c.dim(
|
|
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(
|
|
1747
|
-
console.log(c.dim(
|
|
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(
|
|
1742
|
+
console.log(c.err(' ✗ ') + (e?.message || String(e)));
|
|
1750
1743
|
}
|
|
1751
1744
|
break;
|
|
1752
1745
|
}
|
|
1753
|
-
case
|
|
1746
|
+
case 'status':
|
|
1754
1747
|
printStatus(state);
|
|
1755
1748
|
break;
|
|
1756
|
-
case
|
|
1757
|
-
case
|
|
1758
|
-
console.log(
|
|
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
|
|
1761
|
-
case
|
|
1762
|
-
case
|
|
1757
|
+
case 'exit':
|
|
1758
|
+
case 'quit':
|
|
1759
|
+
case 'q':
|
|
1763
1760
|
persist();
|
|
1764
1761
|
exiting = true;
|
|
1765
|
-
console.log(c.dim(
|
|
1762
|
+
console.log(c.dim(' ' + t.bye));
|
|
1766
1763
|
return true;
|
|
1767
1764
|
default:
|
|
1768
|
-
console.log(c.err(
|
|
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(
|
|
1771
|
+
if (!key) return console.log(c.err(' ' + t.needKeyArg));
|
|
1775
1772
|
config.setKey(key);
|
|
1776
|
-
console.log(c.ok(
|
|
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(
|
|
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(
|
|
1792
|
+
console.log(c.dim(' ' + t.updateChecking));
|
|
1796
1793
|
const v = await checkLatest({ throttle: false });
|
|
1797
|
-
if (!v) return console.log(c.ok(
|
|
1798
|
-
console.log(c.tool(
|
|
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(
|
|
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(
|
|
1806
|
+
if (!m) return console.log(c.err(' ' + t.noModelMatch(q)) + c.dim(' /models'));
|
|
1810
1807
|
state.model = m;
|
|
1811
|
-
state.mode =
|
|
1808
|
+
state.mode = 'chat';
|
|
1812
1809
|
config.setModel(m.id);
|
|
1813
1810
|
updateTitle();
|
|
1814
|
-
console.log(c.ok(
|
|
1815
|
-
if (m.provider ===
|
|
1816
|
-
console.log(c.dim(
|
|
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 ===
|
|
1822
|
-
|
|
1823
|
-
|
|
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 =
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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(
|
|
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(
|
|
1852
|
+
const cwdLabel = c.dim(' 📁 ' + shortCwd());
|
|
1845
1853
|
console.log(
|
|
1846
|
-
` ${mode}${key}${yolo}${ctxLabel}${tokLabel}${todoLabel}\n ${c.dim(
|
|
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
|
|
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(
|
|
1858
|
-
console.log(
|
|
1859
|
-
renderMarkdown(text)
|
|
1860
|
-
.split(
|
|
1861
|
-
.map((l) =>
|
|
1862
|
-
.join(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
1927
|
-
if (
|
|
1928
|
-
|
|
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 =
|
|
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 ===
|
|
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 ===
|
|
1938
|
-
else if (u.plan ===
|
|
1939
|
-
|
|
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(
|
|
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
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
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(
|
|
1984
|
-
c.dim(
|
|
1985
|
-
c.dim(
|
|
1986
|
-
c.dim(
|
|
1987
|
-
].join(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 ?
|
|
2032
|
+
return p.length > 48 ? '…' + p.slice(-47) : p;
|
|
2008
2033
|
};
|
|
2009
|
-
const shortPath = (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
|
|
2013
|
-
if (m < 60) return m +
|
|
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 +
|
|
2016
|
-
return Math.round(h / 24) +
|
|
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(
|
|
2019
|
-
const truncate = (s =
|
|
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(
|
|
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(
|
|
2030
|
-
const more = content.split(
|
|
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(
|
|
2033
|
-
|
|
2034
|
-
lines.map((l) => c.dim(
|
|
2035
|
-
(more > 0 ? c.dim(`\n │ … +${more} dòng nữa`) :
|
|
2036
|
-
c.dim(
|
|
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
|
}
|