@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/sessions.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Lưu & khôi phục lịch sử hội thoại (giống --continue / --resume của Claude Code).
|
|
2
2
|
// Mỗi phiên là một file JSON trong ~/.noob/sessions/. Ghi sau mỗi lượt nên có rớt
|
|
3
3
|
// mạng / tắt máy vẫn resume được.
|
|
4
|
-
import fs from
|
|
5
|
-
import path from
|
|
6
|
-
import os from
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
7
|
|
|
8
|
-
const DIR = path.join(os.homedir(),
|
|
8
|
+
const DIR = path.join(os.homedir(), '.noob', 'sessions');
|
|
9
9
|
|
|
10
10
|
function ensure() {
|
|
11
11
|
try {
|
|
@@ -17,15 +17,15 @@ function ensure() {
|
|
|
17
17
|
|
|
18
18
|
function genId() {
|
|
19
19
|
const d = new Date();
|
|
20
|
-
const p = (n) => String(n).padStart(2,
|
|
20
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
21
21
|
const stamp = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
22
22
|
return `${stamp}-${Math.random().toString(36).slice(2, 6)}`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function titleFrom(history = []) {
|
|
26
|
-
const first = history.find((m) => m.role ===
|
|
27
|
-
if (!first) return
|
|
28
|
-
return String(first.content).replace(/\s+/g,
|
|
26
|
+
const first = history.find((m) => m.role === 'user');
|
|
27
|
+
if (!first) return '';
|
|
28
|
+
return String(first.content).replace(/\s+/g, ' ').trim().slice(0, 60);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function newSession({ cwd, model } = {}) {
|
|
@@ -35,8 +35,8 @@ export function newSession({ cwd, model } = {}) {
|
|
|
35
35
|
createdAt: now,
|
|
36
36
|
updatedAt: now,
|
|
37
37
|
cwd: cwd || process.cwd(),
|
|
38
|
-
model: model ||
|
|
39
|
-
title:
|
|
38
|
+
model: model || '',
|
|
39
|
+
title: '',
|
|
40
40
|
history: [],
|
|
41
41
|
};
|
|
42
42
|
}
|
|
@@ -47,7 +47,7 @@ export function save(session) {
|
|
|
47
47
|
session.updatedAt = Date.now();
|
|
48
48
|
if (!session.title) session.title = titleFrom(session.history);
|
|
49
49
|
try {
|
|
50
|
-
fs.writeFileSync(path.join(DIR, session.id +
|
|
50
|
+
fs.writeFileSync(path.join(DIR, session.id + '.json'), JSON.stringify(session), 'utf8');
|
|
51
51
|
return true;
|
|
52
52
|
} catch {
|
|
53
53
|
return false;
|
|
@@ -57,7 +57,7 @@ export function save(session) {
|
|
|
57
57
|
export function load(id) {
|
|
58
58
|
if (!id) return null;
|
|
59
59
|
try {
|
|
60
|
-
return JSON.parse(fs.readFileSync(path.join(DIR, id +
|
|
60
|
+
return JSON.parse(fs.readFileSync(path.join(DIR, id + '.json'), 'utf8'));
|
|
61
61
|
} catch {
|
|
62
62
|
return null;
|
|
63
63
|
}
|
|
@@ -66,8 +66,8 @@ export function load(id) {
|
|
|
66
66
|
// Chuẩn hoá thư mục workspace để so khớp: resolve + lowercase trên Windows nên
|
|
67
67
|
// "D:\x", "D:\x\" và "d:\x" coi là một.
|
|
68
68
|
const normDir = (p) => {
|
|
69
|
-
const r = path.resolve(p ||
|
|
70
|
-
return process.platform ===
|
|
69
|
+
const r = path.resolve(p || '');
|
|
70
|
+
return process.platform === 'win32' ? r.toLowerCase() : r;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
/**
|
|
@@ -79,22 +79,22 @@ export function list(limit = 30, cwd = null) {
|
|
|
79
79
|
const want = cwd != null ? normDir(cwd) : null;
|
|
80
80
|
let files;
|
|
81
81
|
try {
|
|
82
|
-
files = fs.readdirSync(DIR).filter((f) => f.endsWith(
|
|
82
|
+
files = fs.readdirSync(DIR).filter((f) => f.endsWith('.json'));
|
|
83
83
|
} catch {
|
|
84
84
|
return [];
|
|
85
85
|
}
|
|
86
86
|
const out = [];
|
|
87
87
|
for (const f of files) {
|
|
88
88
|
try {
|
|
89
|
-
const s = JSON.parse(fs.readFileSync(path.join(DIR, f),
|
|
89
|
+
const s = JSON.parse(fs.readFileSync(path.join(DIR, f), 'utf8'));
|
|
90
90
|
if (want != null && normDir(s.cwd) !== want) continue; // khác workspace → bỏ
|
|
91
91
|
out.push({
|
|
92
92
|
id: s.id,
|
|
93
93
|
updatedAt: s.updatedAt || s.createdAt || 0,
|
|
94
|
-
cwd: s.cwd ||
|
|
95
|
-
model: s.model ||
|
|
94
|
+
cwd: s.cwd || '',
|
|
95
|
+
model: s.model || '',
|
|
96
96
|
title: s.title || titleFrom(s.history || []),
|
|
97
|
-
turns: (s.history || []).filter((m) => m.role ===
|
|
97
|
+
turns: (s.history || []).filter((m) => m.role === 'user').length,
|
|
98
98
|
});
|
|
99
99
|
} catch {
|
|
100
100
|
/* bỏ qua file hỏng */
|
|
@@ -113,7 +113,7 @@ export function latest(cwd = null) {
|
|
|
113
113
|
export function remove(id) {
|
|
114
114
|
if (!id) return false;
|
|
115
115
|
try {
|
|
116
|
-
fs.unlinkSync(path.join(DIR, id +
|
|
116
|
+
fs.unlinkSync(path.join(DIR, id + '.json'));
|
|
117
117
|
return true;
|
|
118
118
|
} catch {
|
|
119
119
|
return false;
|
package/src/skills.js
CHANGED
|
@@ -14,22 +14,26 @@
|
|
|
14
14
|
// listSkills() -> string[] (tên skill có sẵn, gộp 2 nguồn)
|
|
15
15
|
// skillPath(name) -> string | null (đường dẫn tuyệt đối SKILL.md đã resolve)
|
|
16
16
|
|
|
17
|
-
import fs from
|
|
18
|
-
import path from
|
|
19
|
-
import { fileURLToPath } from
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
20
|
|
|
21
21
|
// Thư mục `skills/` đi kèm package (cùng cấp với `src/`, `bin/`).
|
|
22
22
|
// `import.meta.url` -> .../noob-cli/src/skills.js => ../skills
|
|
23
|
-
const PACKAGE_SKILLS_DIR = path.resolve(
|
|
23
|
+
const PACKAGE_SKILLS_DIR = path.resolve(
|
|
24
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
25
|
+
'..',
|
|
26
|
+
'skills'
|
|
27
|
+
);
|
|
24
28
|
|
|
25
29
|
function userSkillsDir() {
|
|
26
|
-
return path.join(process.cwd(),
|
|
30
|
+
return path.join(process.cwd(), 'skills');
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
function candidatePaths(name) {
|
|
30
34
|
return [
|
|
31
|
-
path.join(userSkillsDir(), name,
|
|
32
|
-
path.join(PACKAGE_SKILLS_DIR, name,
|
|
35
|
+
path.join(userSkillsDir(), name, 'SKILL.md'), // user override trước
|
|
36
|
+
path.join(PACKAGE_SKILLS_DIR, name, 'SKILL.md'), // built-in của CLI
|
|
33
37
|
];
|
|
34
38
|
}
|
|
35
39
|
|
|
@@ -44,7 +48,7 @@ export function loadSkill(name) {
|
|
|
44
48
|
try {
|
|
45
49
|
const p = skillPath(name);
|
|
46
50
|
if (!p) return null;
|
|
47
|
-
return fs.readFileSync(p,
|
|
51
|
+
return fs.readFileSync(p, 'utf8');
|
|
48
52
|
} catch {
|
|
49
53
|
return null;
|
|
50
54
|
}
|
|
@@ -55,7 +59,7 @@ function listIn(dir) {
|
|
|
55
59
|
if (!fs.existsSync(dir)) return [];
|
|
56
60
|
return fs
|
|
57
61
|
.readdirSync(dir, { withFileTypes: true })
|
|
58
|
-
.filter((e) => e.isDirectory() && fs.existsSync(path.join(dir, e.name,
|
|
62
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(dir, e.name, 'SKILL.md')))
|
|
59
63
|
.map((e) => e.name);
|
|
60
64
|
} catch {
|
|
61
65
|
return [];
|
package/src/subagent.js
CHANGED
|
@@ -47,14 +47,14 @@ export async function runSubAgent({
|
|
|
47
47
|
const history = [{ role: 'user', content: userPrompt }];
|
|
48
48
|
|
|
49
49
|
// Nếu chưa chạm trần depth, cho phép spawn cháu. Chạm trần → bỏ doc.
|
|
50
|
-
const extraToolsDoc =
|
|
51
|
-
depth < MAX_SUBAGENT_DEPTH ? spawnAgentToolsDoc(depth) : '';
|
|
50
|
+
const extraToolsDoc = depth < MAX_SUBAGENT_DEPTH ? spawnAgentToolsDoc(depth) : '';
|
|
52
51
|
|
|
53
52
|
// onTool: forward thẳng dispatcher cha. Cha sẽ tự tăng depth khi spawn lồng
|
|
54
53
|
// (xem repl.js line 1027: `(n, inp) => dispatchTool(n, inp, depth + 1)`).
|
|
55
54
|
const onTool = async (name, input) => dispatchTool(name, input);
|
|
56
55
|
|
|
57
|
-
if (onLog)
|
|
56
|
+
if (onLog)
|
|
57
|
+
onLog(`▶ sub-agent depth=${depth} bắt đầu: ${task.slice(0, 80)}${task.length > 80 ? '…' : ''}`);
|
|
58
58
|
|
|
59
59
|
const result = await runAgent({
|
|
60
60
|
history,
|
package/src/tokens.js
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
|
-
// Đếm token cục
|
|
2
|
-
//
|
|
1
|
+
// Đếm token cục bộ. Chọn encoder theo model:
|
|
2
|
+
// - o200k_base: GPT-4o, GPT-5*, o1/o3/o4 (OpenAI thế hệ mới)
|
|
3
|
+
// - cl100k_base: GPT-4/3.5 legacy + fallback cho Claude/Gemini/khác (xấp xỉ).
|
|
4
|
+
// CHÍNH XÁC cho OpenAI, XẤP XỈ cho provider khác (sai số ~5-15%) — đủ để
|
|
3
5
|
// hiển thị mang tính tham khảo realtime trong CLI.
|
|
4
|
-
import { encode } from
|
|
6
|
+
import { encode as encodeCl100k } from 'gpt-tokenizer/encoding/cl100k_base';
|
|
7
|
+
import { encode as encodeO200k } from 'gpt-tokenizer/encoding/o200k_base';
|
|
8
|
+
|
|
9
|
+
// Encoder hiện tại — mặc định cl100k để giữ tương thích ngược với mọi model
|
|
10
|
+
// không phải OpenAI. setEncodingForModel() đổi sang o200k khi cần.
|
|
11
|
+
let currentEncode = encodeCl100k;
|
|
12
|
+
|
|
13
|
+
// Map model id → encoder. Heuristic theo id (không phải provider) vì cùng OpenAI
|
|
14
|
+
// nhưng GPT-3.5 vẫn dùng cl100k. ID pattern khớp `MODELS` trong src/models.js.
|
|
15
|
+
export function pickEncoderForModel(modelId) {
|
|
16
|
+
if (!modelId || typeof modelId !== 'string') return encodeCl100k;
|
|
17
|
+
const id = modelId.toLowerCase();
|
|
18
|
+
// OpenAI thế hệ mới: GPT-4o, GPT-5*, o1/o3/o4 → o200k_base.
|
|
19
|
+
// Khớp các id gateway: gateway-gpt-4o, gateway-gpt-5*, gateway-gpt-o3*, gateway-gpt-o4*.
|
|
20
|
+
if (/gpt-?4o/.test(id) || /gpt-?5/.test(id) || /gpt-?o[1-9]/.test(id) || /\bo[1-9]\b/.test(id)) {
|
|
21
|
+
return encodeO200k;
|
|
22
|
+
}
|
|
23
|
+
// Còn lại (Claude, Gemini, DeepSeek, Grok, Qwen, Kimi, Llama, GPT-4.1/3.5) → cl100k.
|
|
24
|
+
return encodeCl100k;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function setEncodingForModel(modelId) {
|
|
28
|
+
currentEncode = pickEncoderForModel(modelId);
|
|
29
|
+
}
|
|
5
30
|
|
|
6
31
|
export function countTokens(text) {
|
|
7
32
|
if (!text) return 0;
|
|
8
33
|
try {
|
|
9
|
-
return
|
|
34
|
+
return currentEncode(String(text)).length;
|
|
10
35
|
} catch {
|
|
11
36
|
// Fallback heuristic nếu encoder lỗi: ~4 ký tự / token.
|
|
12
37
|
return Math.ceil(String(text).length / 4);
|
|
@@ -18,7 +43,7 @@ export function countTokens(text) {
|
|
|
18
43
|
export function countMessages(messages = []) {
|
|
19
44
|
let n = 0;
|
|
20
45
|
for (const m of messages) {
|
|
21
|
-
n += countTokens(m?.content ||
|
|
46
|
+
n += countTokens(m?.content || '') + 4;
|
|
22
47
|
}
|
|
23
48
|
return n;
|
|
24
49
|
}
|
|
@@ -29,7 +54,7 @@ export function countMessages(messages = []) {
|
|
|
29
54
|
// Vấn đề cũ: pushOutputDelta encode TOÀN buffer mỗi delta → O(N²) trên stream dài.
|
|
30
55
|
// Fix: sliding window. BPE chỉ phụ thuộc context cục bộ (vài chục byte) nên ta
|
|
31
56
|
// chỉ cần encode đoạn TAIL gần đây, phần trước đó "commit" dứt khoát. Safety
|
|
32
|
-
// window đủ rộng (256 chars) để qua mọi ranh giới token thực tế của
|
|
57
|
+
// window đủ rộng (256 chars) để qua mọi ranh giới token thực tế của cl100k/o200k
|
|
33
58
|
// (token dài nhất ~ vài chục byte).
|
|
34
59
|
const TAIL_WINDOW = 256;
|
|
35
60
|
// Context window tối đa của model (300k tokens). Dùng để tính % usage realtime.
|
|
@@ -46,7 +71,7 @@ export class TokenMeter {
|
|
|
46
71
|
this._committedChars = 0; // số ký tự đã đẩy qua khỏi tail window
|
|
47
72
|
this._committedTokens = 0; // tổng token tương ứng đã cộng vào this.output
|
|
48
73
|
// Tail buffer: TAIL_WINDOW ký tự cuối, dùng để re-encode khi có delta mới.
|
|
49
|
-
this._tail =
|
|
74
|
+
this._tail = '';
|
|
50
75
|
this._tailTokens = 0; // token count hiện tại của _tail (đã cộng vào this.output)
|
|
51
76
|
}
|
|
52
77
|
addInput(n) {
|
|
@@ -86,7 +111,7 @@ export class TokenMeter {
|
|
|
86
111
|
endOutput() {
|
|
87
112
|
this._committedChars = 0;
|
|
88
113
|
this._committedTokens = 0;
|
|
89
|
-
this._tail =
|
|
114
|
+
this._tail = '';
|
|
90
115
|
this._tailTokens = 0;
|
|
91
116
|
}
|
|
92
117
|
get total() {
|
|
@@ -94,7 +119,7 @@ export class TokenMeter {
|
|
|
94
119
|
}
|
|
95
120
|
// Định dạng ngắn để hiển thị ở status bar: "↑1.2k ↓340 (1.5k)".
|
|
96
121
|
format() {
|
|
97
|
-
const fmt = (n) => (n >= 1000 ? (n / 1000).toFixed(1) +
|
|
122
|
+
const fmt = (n) => (n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n));
|
|
98
123
|
return `↑${fmt(this.input)} ↓${fmt(this.output)} (${fmt(this.total)})`;
|
|
99
124
|
}
|
|
100
125
|
// Định dạng kèm context usage: "↑1.2k ↓340 (1.5k) · ctx 45%".
|
|
@@ -119,7 +144,7 @@ export class TokenMeter {
|
|
|
119
144
|
this.contextTokens = 0;
|
|
120
145
|
this._committedChars = 0;
|
|
121
146
|
this._committedTokens = 0;
|
|
122
|
-
this._tail =
|
|
147
|
+
this._tail = '';
|
|
123
148
|
this._tailTokens = 0;
|
|
124
149
|
}
|
|
125
150
|
// Serialize counter để persist qua --continue/--resume. Tail buffer là transient
|
|
@@ -130,12 +155,12 @@ export class TokenMeter {
|
|
|
130
155
|
}
|
|
131
156
|
// Khôi phục từ snapshot — chỉ set 2 counter, tail giữ rỗng (lượt mới bắt đầu).
|
|
132
157
|
restore(data) {
|
|
133
|
-
if (!data || typeof data !==
|
|
158
|
+
if (!data || typeof data !== 'object') return;
|
|
134
159
|
this.input = Math.max(0, data.input | 0);
|
|
135
160
|
this.output = Math.max(0, data.output | 0);
|
|
136
161
|
this._committedChars = 0;
|
|
137
162
|
this._committedTokens = 0;
|
|
138
|
-
this._tail =
|
|
163
|
+
this._tail = '';
|
|
139
164
|
this._tailTokens = 0;
|
|
140
165
|
}
|
|
141
166
|
}
|