@noobdemon/noob-cli 1.11.1 → 1.12.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 +30 -0
- package/package.json +1 -1
- package/src/agent.js +7 -4
- package/src/api.js +61 -5
- package/src/diff.js +152 -0
- package/src/repl/commands/prompts.js +45 -0
- package/src/repl/permission.js +116 -0
- package/src/repl/state.js +33 -0
- package/src/repl/utils.js +64 -0
- package/src/repl/workflow-commands.js +3 -0
- package/src/repl.js +372 -202
- package/src/tokens.js +2 -2
- package/src/ui.js +92 -5
- package/src/workflow-runs.js +222 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
Tất cả thay đổi đáng kể của `@noobdemon/noob-cli` được ghi vào file này.
|
|
4
4
|
|
|
5
|
+
## [1.12.0] - 2026-06-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Diff preview trước khi approve `edit_file`/`write_file`** (`src/diff.js`): permission prompt hiện unified diff màu (`-` đỏ, `+` xanh, context dim) thay vì tóm tắt raw. `edit_file` đọc file gốc + locate `old_string` → diff vùng thay đổi với context xung quanh. `write_file` overwrite → diff cũ vs mới; file mới → `(file MỚI · N dòng)` xanh top 20 dòng. Bắt model bịa nội dung sớm.
|
|
9
|
+
- **Workflow resume + per-run journal** (`src/workflow-runs.js`): mỗi `/workflow run` ghi journal vào `<cwd>/.noob/workflow-runs/<id>.json` (mỗi sub-agent task: hash crc32(task+context+model), status, result). Ctrl+C / mất mạng giữa fan-out → `/workflow resume <id>` skip task đã done, chỉ chạy lại pending. 3 sub-command mới: `/workflow runs|log|resume`.
|
|
10
|
+
- **Quota usage trong status bar** (`src/api.js` + `src/ui.js`): meta line append `· 4.7k/5k req` cạnh token meter, màu theo % used (dim <70%, vàng 70-85%, đỏ >85%). `/api/usage` cache TTL 90s + background refresh — không spam gateway. Cache bust khi `/login`/`/logout`. Plan admin skip hoàn toàn.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **Render markdown trong stream mode** (`src/ui.js` + `src/repl.js`): trước đây streaming path in raw `**bold**`, `` `code` ``, `## heading` không màu. Giờ `makeStreamPrinter` flush theo dòng hoàn chỉnh, render inline:
|
|
14
|
+
- `**bold**` → vàng `#fbbf24` đậm (nhãn `**Vấn đề/cơ hội:**` đập vào mắt)
|
|
15
|
+
- `` `inline code` `` → xanh lá `#34d399` (path/file.ts:23 tách rõ)
|
|
16
|
+
- Heading phân tầng: `#` brand gradient, `##` tím, `###` cyan, `####+` xám sáng
|
|
17
|
+
- Bullet `*`/`-` → `•` xanh accent; code fence (```lang ... ```) pass-through raw
|
|
18
|
+
- **Refactor `src/repl.js` −8.1%** (102KB → 93.8KB): tách 4 module mới với dependency injection rõ ràng — `src/repl/permission.js` (askPermission/askAddRoot/askWorkflowAgentMode), `src/repl/commands/prompts.js` (runImprove/runKarpathy), `src/repl/state.js` (createState factory), `src/repl/utils.js` (shortCwd/relTime/truncate/fmtK/fmtTime/preview/firstLine/shortPath). Mỗi module test được riêng bằng mock object thay vì bám closure.
|
|
19
|
+
- **Context window tăng 300k → 2M tokens** (`src/tokens.js`) + tinh chỉnh ngưỡng auto-compact (`src/repl.js` + `src/agent.js`):
|
|
20
|
+
- Ngưỡng warning kéo xuống cho context lớn: 40% (800k) nhắc nhẹ, 60% (1.2M) cảnh báo mạnh, 75% (1.5M) auto-compact. Trước đây 60/70/80% → với 2M context sẽ để tới 1.6M mới compact, quá trễ vì mỗi lượt cuối có thể ăn 200k+ tokens.
|
|
21
|
+
- `SUMMARIZE_THRESHOLD_CHARS` 1M → 6M chars (~1.5M tokens), match với mốc 75% auto-compact.
|
|
22
|
+
- `keepTail` tăng 4/8 → 16/24 messages: với phiên dài 2M tokens, giữ 4 message cuối là quá ít — model mất hết context tool result gần nhất (vd chuỗi edit_file + run_command đang dở).
|
|
23
|
+
|
|
24
|
+
## [1.11.2] - 2026-06-10
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- **Hết tình trạng "timeout nhiều hơn done"** — task chưa xong đã ngủm: chuyển backend gateway sang upstream mới ổn định hơn (upstream cũ hay cắt stream giữa chừng ở phút thứ 4-5 với câu trả lời dài). User không phải đổi gì, chạy `noob update` là dùng được.
|
|
28
|
+
- **Thấy progress khi model đang nghĩ lâu**: gateway giờ phát status `"đang nghĩ… (Ns)"` mỗi 30s khi upstream im lặng → khỏi tưởng treo và Ctrl+C oan. Heartbeat connection cũng đẩy về 10s liên tục để Cloudflare không đóng socket.
|
|
29
|
+
- **CLI tự bắt được khi gateway chết im lặng** (`src/api.js`): idle timeout 60s — nếu không nhận được byte nào (kể cả keepalive) trong 60s thì coi như chết, retry lại thay vì treo vô tận.
|
|
30
|
+
- **Chống vòng lặp "nối tiếp vô hạn"** (`src/api.js`): `maxContinues` cap ở 5 lần thay vì `Infinity`. Trước đây nếu upstream cứ trả truncated, CLI nối tiếp mãi → user thấy treo.
|
|
31
|
+
|
|
32
|
+
### Chore
|
|
33
|
+
- ESLint ignore `worker/**` (sub-project Cloudflare Worker riêng).
|
|
34
|
+
|
|
5
35
|
## [1.11.1] - 2026-06-10
|
|
6
36
|
|
|
7
37
|
### Fixed
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -146,7 +146,7 @@ const LOOP_DETECT_THRESHOLD = 2;
|
|
|
146
146
|
const MAX_PROMPT_CHARS = 1200000; // ~300k tokens (ngang context window) — compact() KHÔNG chạy trước auto-compact 80% (240k token) của repl.js
|
|
147
147
|
// Khi history vượt ngưỡng này, gọi model phụ tóm tắt các lượt cũ thay vì cắt cụt
|
|
148
148
|
// → giữ được "trí nhớ dài hạn" trong phiên mà không nổ context.
|
|
149
|
-
const SUMMARIZE_THRESHOLD_CHARS =
|
|
149
|
+
const SUMMARIZE_THRESHOLD_CHARS = 6000000; // ~1.5M tokens (75% window) — summarize chỉ chạy sau auto-compact 75% với CONTEXT_WINDOW=2M
|
|
150
150
|
|
|
151
151
|
// HARD GOAL block (do /goal <text> set): chèn ngay sau memoryBlock, attention
|
|
152
152
|
// cao. Mục đích — chống 3 failure mode bài "dynamic workflows" của Anthropic
|
|
@@ -258,9 +258,12 @@ export async function maybeSummarize(history, { model, signal, force = false } =
|
|
|
258
258
|
if (!history?.length) return false;
|
|
259
259
|
const totalChars = history.reduce((s, m) => s + (m.content?.length || 0), 0);
|
|
260
260
|
if (!force && totalChars < SUMMARIZE_THRESHOLD_CHARS) return false;
|
|
261
|
-
// Giữ
|
|
262
|
-
//
|
|
263
|
-
|
|
261
|
+
// Giữ tail nguyên vẹn; tóm tắt phần trước.
|
|
262
|
+
// Với CONTEXT_WINDOW = 2M tokens, tail cần đủ lớn để giữ context tool result
|
|
263
|
+
// gần nhất (vd 10 lượt cuối có thể là chuỗi edit_file + run_command đang dở).
|
|
264
|
+
// force (gọi từ /compact hoặc auto-compact 75%): giữ 16 tail.
|
|
265
|
+
// non-force: giữ 24 tail (rộng tay hơn vì phiên rất dài mới trigger).
|
|
266
|
+
const keepTail = force ? 16 : 24;
|
|
264
267
|
if (history.length <= keepTail + 2) return false;
|
|
265
268
|
// Nếu lượt đầu đã là summary (role=system, name=summary) → tóm tắt thêm.
|
|
266
269
|
const head = history.slice(0, history.length - keepTail);
|
package/src/api.js
CHANGED
|
@@ -165,7 +165,12 @@ export async function stream({
|
|
|
165
165
|
onDelta,
|
|
166
166
|
onReasoning,
|
|
167
167
|
onStatus,
|
|
168
|
-
|
|
168
|
+
// Cap continuation: trước đây Infinity → khi upstream cứ trả truncated liên
|
|
169
|
+
// tục (effort:high bị Vercel cắt 300s mỗi lần, hoặc CF wall-clock cắt fetch
|
|
170
|
+
// upstream), CLI nối tiếp vô hạn → user thấy như treo. 5 lần là rộng rãi cho
|
|
171
|
+
// câu trả lời rất dài + vài lần bị cắt; nhiều hơn nữa = upstream/gateway
|
|
172
|
+
// hỏng thật, dừng tốt hơn.
|
|
173
|
+
maxContinues = 5,
|
|
169
174
|
}) {
|
|
170
175
|
const endpoint =
|
|
171
176
|
mode === 'search' ? '/api/search' : mode === 'merge' ? '/api/merge' : '/api/chat';
|
|
@@ -287,6 +292,17 @@ async function streamOnce({
|
|
|
287
292
|
let sawDone = false; // thấy {done} = stream kết thúc tử tế (không bị cắt)
|
|
288
293
|
let truncated = false; // gateway báo upstream bị cắt giữa chừng
|
|
289
294
|
|
|
295
|
+
// Idle timeout client-side: gateway phát keepalive (`: keepalive\n\n`) mỗi 10s.
|
|
296
|
+
// Nếu 60s liên tiếp không nhận được gì (kể cả keepalive comment), nghĩa là
|
|
297
|
+
// gateway/CF connection đã chết im lặng. Throw retryable thay vì treo vô tận.
|
|
298
|
+
const IDLE_MS = 60000;
|
|
299
|
+
let lastByteAt = Date.now();
|
|
300
|
+
const idleTimer = setInterval(() => {
|
|
301
|
+
if (Date.now() - lastByteAt > IDLE_MS) {
|
|
302
|
+
ctrl.abort(new Error('idle_timeout'));
|
|
303
|
+
}
|
|
304
|
+
}, 5000);
|
|
305
|
+
|
|
290
306
|
// Một dòng SSE → cập nhật text/reasoning. Tách ra để dùng lại khi flush dòng cuối.
|
|
291
307
|
const processLine = (rawLine) => {
|
|
292
308
|
const line = rawLine.trim();
|
|
@@ -332,6 +348,7 @@ async function streamOnce({
|
|
|
332
348
|
while (true) {
|
|
333
349
|
const { done, value } = await reader.read();
|
|
334
350
|
if (done) break;
|
|
351
|
+
lastByteAt = Date.now(); // bất kỳ byte nào (kể cả keepalive comment) → reset idle timer
|
|
335
352
|
buf += decoder.decode(value, { stream: true });
|
|
336
353
|
// Chuẩn hoá CRLF→LF: một số reverse proxy (Cloudflare, nginx config khác)
|
|
337
354
|
// gửi line endings \r\n. processLine() có trim() nên \r trailing tự rụng,
|
|
@@ -353,21 +370,60 @@ async function streamOnce({
|
|
|
353
370
|
|
|
354
371
|
return { text, reasoning, truncated };
|
|
355
372
|
} catch (err) {
|
|
373
|
+
// Phân biệt user abort (Ctrl+C — signal cha) với idle_timeout (ctrl.abort
|
|
374
|
+
// bên trong khi gateway im lặng quá lâu). Cả hai cùng làm fetch throw
|
|
375
|
+
// AbortError nên check signal cha trước.
|
|
356
376
|
if (signal?.aborted) throw err; // người dùng bấm Ctrl+C → huỷ thật, không nối tiếp
|
|
357
|
-
|
|
358
|
-
//
|
|
377
|
+
const isIdle = ctrl.signal.aborted && err?.name === 'AbortError';
|
|
378
|
+
// Rớt mạng giữa chừng (không phải huỷ user): với chat, nếu đã có chữ thì
|
|
379
|
+
// trả phần đã nhận + cờ truncated để lớp trên nối tiếp.
|
|
359
380
|
if (mode === 'chat' && text) return { text, reasoning, truncated: true };
|
|
381
|
+
// Chưa có chữ nào và là idle_timeout → throw lỗi retryable rõ ràng (thay
|
|
382
|
+
// vì AbortError chung chung) để streamWithRetry biết cần thử lại.
|
|
383
|
+
if (isIdle) {
|
|
384
|
+
throw new ApiError('idle_timeout: gateway im lặng quá lâu', {
|
|
385
|
+
code: 'idle_timeout',
|
|
386
|
+
retryable: true,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
360
389
|
throw err;
|
|
361
390
|
} finally {
|
|
391
|
+
clearInterval(idleTimer);
|
|
362
392
|
signal?.removeEventListener('abort', onUserAbort);
|
|
363
393
|
}
|
|
364
394
|
}
|
|
365
395
|
|
|
396
|
+
// Cache /api/usage trong 90s để gọi từ status bar không spam gateway. Key cache
|
|
397
|
+
// theo apiKey — đổi key (login khác) thì cache bust ngay. Bypass cache bằng
|
|
398
|
+
// usage({ force: true }) cho /usage command (user gõ tay → muốn realtime).
|
|
399
|
+
let _usageCache = { key: null, at: 0, data: null };
|
|
400
|
+
const USAGE_CACHE_MS = 90_000;
|
|
401
|
+
|
|
366
402
|
/** Fetch the current key's quota/usage from the gateway (no request consumed). */
|
|
367
|
-
export async function usage() {
|
|
403
|
+
export async function usage({ force = false } = {}) {
|
|
404
|
+
const now = Date.now();
|
|
405
|
+
const key = config.apiKey || '';
|
|
406
|
+
if (!force && _usageCache.data && _usageCache.key === key && now - _usageCache.at < USAGE_CACHE_MS) {
|
|
407
|
+
return _usageCache.data;
|
|
408
|
+
}
|
|
368
409
|
const resp = await fetch(config.gatewayUrl + '/api/usage', { headers: authHeaders() });
|
|
369
410
|
if (!resp.ok) throw await parseError(resp);
|
|
370
|
-
|
|
411
|
+
const data = await resp.json();
|
|
412
|
+
_usageCache = { key, at: now, data };
|
|
413
|
+
return data;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Trả về usage đã cache mà KHÔNG fetch (cho status bar render sync). Null nếu chưa có. */
|
|
417
|
+
export function cachedUsage() {
|
|
418
|
+
const key = config.apiKey || '';
|
|
419
|
+
if (!_usageCache.data || _usageCache.key !== key) return null;
|
|
420
|
+
if (Date.now() - _usageCache.at > USAGE_CACHE_MS) return null;
|
|
421
|
+
return _usageCache.data;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Bust cache (gọi sau khi đổi key / logout). */
|
|
425
|
+
export function resetUsageCache() {
|
|
426
|
+
_usageCache = { key: null, at: 0, data: null };
|
|
371
427
|
}
|
|
372
428
|
|
|
373
429
|
export { ApiError };
|
package/src/diff.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Minimal line-based unified diff cho permission preview. KHÔNG phải implementation
|
|
2
|
+
// đầy đủ của Myers — chỉ cần LCS + emit context để show user trước khi approve.
|
|
3
|
+
//
|
|
4
|
+
// Mục tiêu: user thấy CHÍNH XÁC dòng nào bị xoá / thêm, không bị model bịa nội dung.
|
|
5
|
+
// Output dạng unified diff (` @@`, `-`, `+`, ` `), colorize bằng chalk ở caller.
|
|
6
|
+
//
|
|
7
|
+
// Performance: O(M*N) LCS table. M, N ≤ ~vài trăm dòng cho 1 edit_file → đủ nhanh.
|
|
8
|
+
// File lớn hơn (write_file overwrite 1k+ dòng) → caller truncate trước khi gọi.
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
|
|
11
|
+
// LCS table giữa 2 mảng lines. Trả về matrix (M+1) x (N+1).
|
|
12
|
+
function lcsTable(a, b) {
|
|
13
|
+
const m = a.length;
|
|
14
|
+
const n = b.length;
|
|
15
|
+
const t = Array.from({ length: m + 1 }, () => new Uint32Array(n + 1));
|
|
16
|
+
for (let i = 1; i <= m; i++) {
|
|
17
|
+
for (let j = 1; j <= n; j++) {
|
|
18
|
+
if (a[i - 1] === b[j - 1]) t[i][j] = t[i - 1][j - 1] + 1;
|
|
19
|
+
else t[i][j] = t[i - 1][j] >= t[i][j - 1] ? t[i - 1][j] : t[i][j - 1];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return t;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Backtrack LCS → mảng các op { type: 'eq'|'del'|'add', text }.
|
|
26
|
+
// Output theo thứ tự xuất hiện trong file (top-down).
|
|
27
|
+
function diffLines(a, b) {
|
|
28
|
+
const t = lcsTable(a, b);
|
|
29
|
+
const ops = [];
|
|
30
|
+
let i = a.length;
|
|
31
|
+
let j = b.length;
|
|
32
|
+
while (i > 0 && j > 0) {
|
|
33
|
+
if (a[i - 1] === b[j - 1]) {
|
|
34
|
+
ops.push({ type: 'eq', text: a[i - 1] });
|
|
35
|
+
i--; j--;
|
|
36
|
+
} else if (t[i - 1][j] >= t[i][j - 1]) {
|
|
37
|
+
ops.push({ type: 'del', text: a[i - 1] });
|
|
38
|
+
i--;
|
|
39
|
+
} else {
|
|
40
|
+
ops.push({ type: 'add', text: b[j - 1] });
|
|
41
|
+
j--;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
while (i > 0) { ops.push({ type: 'del', text: a[--i] }); }
|
|
45
|
+
while (j > 0) { ops.push({ type: 'add', text: b[--j] }); }
|
|
46
|
+
return ops.reverse();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Group ops thành hunks (cụm thay đổi + context). `context` = số dòng eq trước/sau
|
|
50
|
+
// mỗi cụm del/add. Mặc định 3 — chuẩn unified diff.
|
|
51
|
+
function groupHunks(ops, context = 3) {
|
|
52
|
+
const hunks = [];
|
|
53
|
+
let cur = null;
|
|
54
|
+
let trailingEq = 0;
|
|
55
|
+
for (let k = 0; k < ops.length; k++) {
|
|
56
|
+
const op = ops[k];
|
|
57
|
+
if (op.type === 'eq') {
|
|
58
|
+
if (!cur) {
|
|
59
|
+
// Chưa có hunk đang mở → giữ trong sliding window context để mở hunk kế tiếp.
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
cur.ops.push(op);
|
|
63
|
+
trailingEq++;
|
|
64
|
+
if (trailingEq > context * 2) {
|
|
65
|
+
// Đã đủ context sau → đóng hunk, trim eq thừa.
|
|
66
|
+
const trimCount = trailingEq - context;
|
|
67
|
+
cur.ops.splice(cur.ops.length - trimCount, trimCount);
|
|
68
|
+
hunks.push(cur);
|
|
69
|
+
cur = null;
|
|
70
|
+
trailingEq = 0;
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
trailingEq = 0;
|
|
74
|
+
if (!cur) {
|
|
75
|
+
// Mở hunk mới: thêm context trước (eq trong window vừa rồi).
|
|
76
|
+
cur = { ops: [] };
|
|
77
|
+
// Lấy `context` eq lùi từ vị trí hiện tại.
|
|
78
|
+
const start = Math.max(0, k - context);
|
|
79
|
+
for (let p = start; p < k; p++) {
|
|
80
|
+
if (ops[p].type === 'eq') cur.ops.push(ops[p]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
cur.ops.push(op);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (cur) {
|
|
87
|
+
// Trim trailing eq nếu vượt context.
|
|
88
|
+
while (cur.ops.length > 0 && cur.ops[cur.ops.length - 1].type === 'eq') {
|
|
89
|
+
let trail = 0;
|
|
90
|
+
for (let p = cur.ops.length - 1; p >= 0 && cur.ops[p].type === 'eq'; p--) trail++;
|
|
91
|
+
if (trail > context) cur.ops.splice(cur.ops.length - (trail - context), trail - context);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
hunks.push(cur);
|
|
95
|
+
}
|
|
96
|
+
return hunks;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Render unified diff với màu. `label` là tiêu đề (vd path file).
|
|
100
|
+
// `maxLines` cap tổng số dòng render (mặc định 60) — diff quá dài hiện cuối `... +N dòng`.
|
|
101
|
+
export function renderUnifiedDiff(oldText, newText, { label = '', context = 3, maxLines = 60 } = {}) {
|
|
102
|
+
const a = oldText.split('\n');
|
|
103
|
+
const b = newText.split('\n');
|
|
104
|
+
const ops = diffLines(a, b);
|
|
105
|
+
const hunks = groupHunks(ops, context);
|
|
106
|
+
const out = [];
|
|
107
|
+
out.push(chalk.dim(' ┌─ ' + label));
|
|
108
|
+
if (!hunks.length) {
|
|
109
|
+
out.push(chalk.dim(' │ ') + chalk.dim('(không có thay đổi)'));
|
|
110
|
+
out.push(chalk.dim(' └─'));
|
|
111
|
+
return out.join('\n');
|
|
112
|
+
}
|
|
113
|
+
let printed = 0;
|
|
114
|
+
let truncated = 0;
|
|
115
|
+
for (let h = 0; h < hunks.length; h++) {
|
|
116
|
+
if (h > 0) out.push(chalk.dim(' │ ' + chalk.dim('@@')));
|
|
117
|
+
for (const op of hunks[h].ops) {
|
|
118
|
+
if (printed >= maxLines) {
|
|
119
|
+
truncated++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const text = String(op.text).slice(0, 200);
|
|
123
|
+
let line;
|
|
124
|
+
if (op.type === 'add') line = chalk.green('+ ') + chalk.green(text);
|
|
125
|
+
else if (op.type === 'del') line = chalk.red('- ') + chalk.red(text);
|
|
126
|
+
else line = chalk.dim(' ') + chalk.dim(text);
|
|
127
|
+
out.push(chalk.dim(' │ ') + line);
|
|
128
|
+
printed++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (truncated) {
|
|
132
|
+
out.push(chalk.dim(' │ ') + chalk.dim(`… +${truncated} dòng nữa (truncated)`));
|
|
133
|
+
}
|
|
134
|
+
out.push(chalk.dim(' └─'));
|
|
135
|
+
return out.join('\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Render preview cho file mới (write_file path chưa tồn tại).
|
|
139
|
+
// Hiển thị top N dòng + count tổng. Không có concept diff.
|
|
140
|
+
export function renderNewFilePreview(content, { label = '', maxLines = 20 } = {}) {
|
|
141
|
+
const lines = String(content || '').split('\n');
|
|
142
|
+
const head = lines.slice(0, maxLines);
|
|
143
|
+
const more = lines.length - head.length;
|
|
144
|
+
const out = [];
|
|
145
|
+
out.push(chalk.dim(' ┌─ ' + label + ' ') + chalk.green(`(file MỚI · ${lines.length} dòng)`));
|
|
146
|
+
for (const l of head) {
|
|
147
|
+
out.push(chalk.dim(' │ ') + chalk.green('+ ') + chalk.green(l.slice(0, 200)));
|
|
148
|
+
}
|
|
149
|
+
if (more > 0) out.push(chalk.dim(' │ ') + chalk.dim(`… +${more} dòng nữa`));
|
|
150
|
+
out.push(chalk.dim(' └─'));
|
|
151
|
+
return out.join('\n');
|
|
152
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Slash commands chạy 1 prompt cố định qua handle() + persist().
|
|
2
|
+
// Tách khỏi src/repl.js — cả 2 cùng pattern, đơn giản, không động state phức tạp.
|
|
3
|
+
//
|
|
4
|
+
// Caller truyền:
|
|
5
|
+
// - config: để check apiKey
|
|
6
|
+
// - c, t: color + i18n
|
|
7
|
+
// - handle: async function (prompt) → đẩy 1 lượt user tới agent loop
|
|
8
|
+
// - persist: lưu session sau lượt
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* /improve [hint] — model rà soát workspace & đề xuất tính năng/cải tiến.
|
|
12
|
+
* KHÔNG sửa code, chỉ phân tích & đề xuất.
|
|
13
|
+
*/
|
|
14
|
+
export async function runImprove(arg, { config, c, t, handle, persist }) {
|
|
15
|
+
if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
|
|
16
|
+
const focus = arg
|
|
17
|
+
? `\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.`
|
|
18
|
+
: '';
|
|
19
|
+
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.`;
|
|
20
|
+
console.log(c.tool(' ✨ ' + t.improveRunning));
|
|
21
|
+
await handle(prompt);
|
|
22
|
+
persist();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* /karpathy [path] — bắt noob tự rà soát code theo 4 nguyên tắc Karpathy.
|
|
27
|
+
* Không có path → soát các file đã đổi trong phiên (model thấy qua FILES CHANGED).
|
|
28
|
+
*/
|
|
29
|
+
export async function runKarpathy(arg, { config, c, t, handle, persist }) {
|
|
30
|
+
if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
|
|
31
|
+
const target = arg
|
|
32
|
+
? `file/đường dẫn: ${arg}`
|
|
33
|
+
: 'các file bạn đã tạo/sửa trong phiên này (xem mục FILES CHANGED)';
|
|
34
|
+
const prompt = `Đóng vai reviewer khó tính. Rà soát ${target} theo 4 nguyên tắc code của Karpathy.
|
|
35
|
+
ĐỌC nội dung file thật bằng read_file trước — KHÔNG dựa vào trí nhớ.
|
|
36
|
+
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":
|
|
37
|
+
1. THINK FIRST — giả định ẩn nào chưa nêu? chỗ nào thiếu kiểm chứng?
|
|
38
|
+
2. KEEP IT SIMPLE — over-engineer, abstraction thừa, lồng quá sâu, hàm quá dài?
|
|
39
|
+
3. SURGICAL — thay đổi lạc đề, refactor tiện tay, đổi style/format vô cớ?
|
|
40
|
+
4. VERIFIABLE GOAL — mục tiêu có kiểm chứng được? đã chạy build/test chưa?
|
|
41
|
+
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.`;
|
|
42
|
+
console.log(c.tool(' ⚖ Karpathy check…'));
|
|
43
|
+
await handle(prompt);
|
|
44
|
+
persist();
|
|
45
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Permission prompts (y/n/a) cho tool calls, add-root, và workflow agent mode.
|
|
2
|
+
//
|
|
3
|
+
// Tách khỏi src/repl.js (refactor 1.11.4+): 3 hàm này dùng chung pattern
|
|
4
|
+
// "paste-spam protection" — nếu user paste/gõ nhầm tin nhắn lạ (dòng dài > 3 char,
|
|
5
|
+
// không khớp y/n/a) vào đúng lúc prompt hiện, thay vì coi như từ chối, ta XẾP
|
|
6
|
+
// HÀNG tin đó (pending) và hỏi lại. Đây là thủ phạm cũ làm CLI "tự tắt" hoặc
|
|
7
|
+
// quyết định nhầm trước đây.
|
|
8
|
+
//
|
|
9
|
+
// Mỗi hàm:
|
|
10
|
+
// - tui.setBusy(false) trước prompt (tắt spinner để prompt nổi bật).
|
|
11
|
+
// - try/finally khôi phục busy=true với label thinking.
|
|
12
|
+
// - stdin đóng (raw == null) → return 'n' (mặc định an toàn).
|
|
13
|
+
//
|
|
14
|
+
// Caller phải truyền:
|
|
15
|
+
// - tui: TUI instance (setBusy, ask qua chính TUI hoặc inject `ask` riêng).
|
|
16
|
+
// - ask: hàm async đọc 1 dòng từ stdin (đã có sẵn trong repl.js).
|
|
17
|
+
// - pending: mảng tin nhắn xếp hàng dùng chung với REPL loop.
|
|
18
|
+
// - c: color palette (chalk hex).
|
|
19
|
+
// - t: i18n strings.
|
|
20
|
+
// - truncate: helper cắt chuỗi dài.
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hỏi quyền chung cho 1 tool call. Trả về 'y' | 'n' | 'a'.
|
|
24
|
+
* 'a' = auto-approve mọi lần gọi tool name này trong phiên còn lại.
|
|
25
|
+
*/
|
|
26
|
+
export async function askPermission(name, { tui, ask, pending, c, t, truncate }) {
|
|
27
|
+
tui.setBusy(false);
|
|
28
|
+
console.log(
|
|
29
|
+
c.tool(' ⏸ Cần quyền: ' + name) +
|
|
30
|
+
c.dim(' — gõ y (đồng ý) / n (từ chối) / a (luôn cho phép)')
|
|
31
|
+
);
|
|
32
|
+
try {
|
|
33
|
+
while (true) {
|
|
34
|
+
const raw = await ask(
|
|
35
|
+
c.tool(' cho phép? ') + c.dim('[y] có / [n] không / [a] luôn ' + name + ' › ')
|
|
36
|
+
);
|
|
37
|
+
if (raw == null) return 'n';
|
|
38
|
+
const a = raw.trim().toLowerCase();
|
|
39
|
+
if (a === '' || a === 'y' || a === 'yes' || a === 'có') return 'y';
|
|
40
|
+
if (a === 'n' || a === 'no' || a === 'không') return 'n';
|
|
41
|
+
if (a === 'a' || a === 'always' || a === 'luôn') return 'a';
|
|
42
|
+
if (raw.trim().length > 3) {
|
|
43
|
+
pending.push(raw);
|
|
44
|
+
console.log(c.dim(' ' + t.queued(pending.length, truncate(raw, 60))));
|
|
45
|
+
}
|
|
46
|
+
console.log(c.dim(' ' + t.permRetry));
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
tui.setBusy(true, t.thinking);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Hỏi user có muốn add folder ngoài workspace cho 1 tool call cụ thể.
|
|
55
|
+
* `targetPath` = path tool đang muốn truy cập (để user thấy rõ context).
|
|
56
|
+
* Trả về 'y' | 'n' | 'a'.
|
|
57
|
+
*/
|
|
58
|
+
export async function askAddRoot(root, targetPath, { tui, ask, pending, c, t, truncate }) {
|
|
59
|
+
tui.setBusy(false);
|
|
60
|
+
console.log(c.tool(' ⏸ Cần cấp quyền folder: ') + c.accent(root));
|
|
61
|
+
console.log(c.dim(' (model muốn truy cập: ' + targetPath + ')'));
|
|
62
|
+
try {
|
|
63
|
+
while (true) {
|
|
64
|
+
const raw = await ask(
|
|
65
|
+
c.tool(' cho phép? ') +
|
|
66
|
+
c.dim('[y] thêm vào scope lần này / [a] luôn thêm / [n] từ chối › ')
|
|
67
|
+
);
|
|
68
|
+
if (raw == null) return 'n';
|
|
69
|
+
const a = raw.trim().toLowerCase();
|
|
70
|
+
if (a === '' || a === 'y' || a === 'yes' || a === 'có') return 'y';
|
|
71
|
+
if (a === 'n' || a === 'no' || a === 'không') return 'n';
|
|
72
|
+
if (a === 'a' || a === 'always' || a === 'luôn') return 'a';
|
|
73
|
+
if (raw.trim().length > 3) {
|
|
74
|
+
pending.push(raw);
|
|
75
|
+
console.log(c.dim(' ' + t.queued(pending.length, truncate(raw, 60))));
|
|
76
|
+
}
|
|
77
|
+
console.log(c.dim(' → gõ y / n / a'));
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
tui.setBusy(true, t.thinking);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Hỏi user có muốn bật agent mode để chạy /workflow. Chỉ y/n (Enter=yes).
|
|
86
|
+
* Trả về 'y' | 'n'.
|
|
87
|
+
*/
|
|
88
|
+
export async function askWorkflowAgentMode({ tui, ask, pending, c, t, truncate }) {
|
|
89
|
+
tui.setBusy(false);
|
|
90
|
+
console.log(
|
|
91
|
+
c.tool(
|
|
92
|
+
' ' +
|
|
93
|
+
(t.workflowAgentAskHint ||
|
|
94
|
+
'🎼 /workflow cần spawn sub-agent — agent mode hiện đang TẮT.')
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
try {
|
|
98
|
+
while (true) {
|
|
99
|
+
const raw = await ask(
|
|
100
|
+
c.tool(' bật agent mode và chạy workflow? ') +
|
|
101
|
+
c.dim('[y] có, bật & chạy / [n] huỷ (gõ /agent rồi chạy lại nếu muốn) › ')
|
|
102
|
+
);
|
|
103
|
+
if (raw == null) return 'n';
|
|
104
|
+
const a = raw.trim().toLowerCase();
|
|
105
|
+
if (a === '' || a === 'y' || a === 'yes' || a === 'có') return 'y';
|
|
106
|
+
if (a === 'n' || a === 'no' || a === 'không') return 'n';
|
|
107
|
+
if (raw.trim().length > 3) {
|
|
108
|
+
pending.push(raw);
|
|
109
|
+
console.log(c.dim(' ' + t.queued(pending.length, truncate(raw, 60))));
|
|
110
|
+
}
|
|
111
|
+
console.log(c.dim(' → gõ y hoặc n'));
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
tui.setBusy(true, t.thinking);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// State phiên REPL — factory pure, không side effect.
|
|
2
|
+
//
|
|
3
|
+
// Tách khỏi src/repl.js (refactor): trước đây inline trong startRepl(). Tách ra
|
|
4
|
+
// để (1) test được riêng — pass opts vào, assert shape; (2) thấy ngay shape state
|
|
5
|
+
// đầy đủ thay vì cuộn 50 dòng init.
|
|
6
|
+
//
|
|
7
|
+
// Lưu ý: state chỉ chứa primitive + collection rỗng. Mọi reference động (model,
|
|
8
|
+
// tokenMeter, abort, session) vẫn ở startRepl scope — vì chúng có lifecycle khác
|
|
9
|
+
// state và cần closure binding với tui/api.
|
|
10
|
+
import { findModel, DEFAULT_MODEL } from '../models.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} opts - CLI flags từ bin/noob.js
|
|
14
|
+
* @param {Object} config - config singleton (đọc model mặc định + yoloDefault)
|
|
15
|
+
* @returns {Object} state phiên — mutable, share giữa REPL loop + commands
|
|
16
|
+
*/
|
|
17
|
+
export function createState(opts = {}, config) {
|
|
18
|
+
return {
|
|
19
|
+
model: findModel(opts.model) || findModel(config.model) || findModel(DEFAULT_MODEL),
|
|
20
|
+
mode: 'chat', // chat | merge | search
|
|
21
|
+
history: [],
|
|
22
|
+
autoApprove: new Set(),
|
|
23
|
+
yolo: !!opts.yolo || config.yoloDefault,
|
|
24
|
+
ultra: false, // /ultra chế độ tự hành đang chạy
|
|
25
|
+
agentMode: false, // /agent on → cho phép spawn_agent / spawn_agents
|
|
26
|
+
goal: null, // HARD GOAL set qua /goal — inject vào mọi prompt
|
|
27
|
+
loop: null, // /loop — {intervalMs, intervalStr, task, timer, ticks, startedAt} | null
|
|
28
|
+
extraRoots: [], // /add-dir — UX display only; source of truth ở tools.js
|
|
29
|
+
_longSessionWarned: false, // đã in cảnh báo phiên dài chưa (reset khi /clear)
|
|
30
|
+
todos: [], // [{text, done}] — todo list parse từ model output, render trên status bar
|
|
31
|
+
workflowRun: null, // {id, data, path} — active workflow run journal (resume + log)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Pure utility helpers cho REPL — không bám state, không I/O.
|
|
2
|
+
//
|
|
3
|
+
// Tách khỏi src/repl.js (refactor): cuối file gốc đè đầy các const/function nhỏ
|
|
4
|
+
// (shortCwd, shortPath, relTime, firstLine, truncate, fmtTime, fmtK, preview).
|
|
5
|
+
// Đưa ra module riêng để test được không cần mock toàn REPL.
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
// Cắt cwd cho prompt — quá dài thì giữ phần đuôi.
|
|
9
|
+
export const shortCwd = () => {
|
|
10
|
+
const p = process.cwd();
|
|
11
|
+
return p.length > 48 ? '…' + p.slice(-47) : p;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Cắt path để hiển thị ngắn (vd trong session list).
|
|
15
|
+
export const shortPath = (p = '') => (p.length > 30 ? '…' + p.slice(-29) : p);
|
|
16
|
+
|
|
17
|
+
// Hiển thị thời gian tương đối tiếng Việt: "vừa xong" / "N phút trước" / "N giờ trước" / "N ngày trước".
|
|
18
|
+
export const relTime = (ts) => {
|
|
19
|
+
const m = Math.round((Date.now() - ts) / 60000);
|
|
20
|
+
if (m < 1) return 'vừa xong';
|
|
21
|
+
if (m < 60) return m + ' phút trước';
|
|
22
|
+
const h = Math.round(m / 60);
|
|
23
|
+
if (h < 24) return h + ' giờ trước';
|
|
24
|
+
return Math.round(h / 24) + ' ngày trước';
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Lấy dòng đầu của output tool (clip 100 char) cho status line.
|
|
28
|
+
export const firstLine = (s) => (s.split('\n')[0] || '').slice(0, 100);
|
|
29
|
+
|
|
30
|
+
// Cắt chuỗi dài + thay \n bằng ⏎ (cho hiển thị 1 dòng trong queued/status).
|
|
31
|
+
export const truncate = (s = '', n = 120) =>
|
|
32
|
+
(s.length > n ? s.slice(0, n) + '…' : s).replace(/\n/g, '⏎');
|
|
33
|
+
|
|
34
|
+
// Format ISO time → locale vi-VN. Fallback raw string nếu parse fail.
|
|
35
|
+
export const fmtTime = (iso) => {
|
|
36
|
+
try {
|
|
37
|
+
return new Date(iso).toLocaleString('vi-VN');
|
|
38
|
+
} catch {
|
|
39
|
+
return iso;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Format số: 1234 → "1.2k", 1234567 → "1.2M". Dưới 1000 → string thường.
|
|
44
|
+
export function fmtK(n) {
|
|
45
|
+
return n >= 1000000
|
|
46
|
+
? (n / 1000000).toFixed(1) + 'M'
|
|
47
|
+
: n >= 1000
|
|
48
|
+
? (n / 1000).toFixed(1) + 'k'
|
|
49
|
+
: String(n);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Preview generic cho non-diff content (vd run_command output cho permission prompt).
|
|
53
|
+
// Cho file/diff dùng src/diff.js (renderUnifiedDiff / renderNewFilePreview).
|
|
54
|
+
export function preview(content, label) {
|
|
55
|
+
const lines = content.split('\n').slice(0, 12);
|
|
56
|
+
const more = content.split('\n').length - lines.length;
|
|
57
|
+
console.log(
|
|
58
|
+
chalk.gray(' ┌─ ' + (label || '')) +
|
|
59
|
+
'\n' +
|
|
60
|
+
lines.map((l) => chalk.gray(' │ ') + l.slice(0, 110)).join('\n') +
|
|
61
|
+
(more > 0 ? chalk.gray(`\n │ … +${more} dòng nữa`) : '') +
|
|
62
|
+
chalk.gray('\n └─')
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -53,6 +53,9 @@ export function workflowHelp({ c, t }) {
|
|
|
53
53
|
console.log(' /workflow save <name> <req> lưu prompt template → ~/.noob/workflows/');
|
|
54
54
|
console.log(' /workflow load <name> xem nội dung (saved hoặc built-in)');
|
|
55
55
|
console.log(' /workflow run <name> [extra] chạy (built-in HOẶC saved, có thể thêm ngữ cảnh)');
|
|
56
|
+
console.log(' /workflow runs list run journal đã chạy (resume-able)');
|
|
57
|
+
console.log(' /workflow log <id> xem chi tiết 1 run (sub-agent task + kết quả)');
|
|
58
|
+
console.log(' /workflow resume <id> chạy lại run, skip task đã done → tiết kiệm token');
|
|
56
59
|
console.log(' /workflow delete|rm <name> xoá workflow đã lưu');
|
|
57
60
|
console.log('');
|
|
58
61
|
console.log(c.accent(' Nhanh nhất để thử:'));
|