@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/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 "node:fs";
5
- import path from "node:path";
6
- import os from "node:os";
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(), ".noob", "sessions");
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, "0");
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 === "user");
27
- if (!first) return "";
28
- return String(first.content).replace(/\s+/g, " ").trim().slice(0, 60);
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 + ".json"), JSON.stringify(session), "utf8");
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 + ".json"), "utf8"));
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 === "win32" ? r.toLowerCase() : r;
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(".json"));
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), "utf8"));
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 === "user").length,
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 + ".json"));
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 "node:fs";
18
- import path from "node:path";
19
- import { fileURLToPath } from "node:url";
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(path.dirname(fileURLToPath(import.meta.url)), "..", "skills");
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(), "skills");
30
+ return path.join(process.cwd(), 'skills');
27
31
  }
28
32
 
29
33
  function candidatePaths(name) {
30
34
  return [
31
- path.join(userSkillsDir(), name, "SKILL.md"), // user override trước
32
- path.join(PACKAGE_SKILLS_DIR, name, "SKILL.md"), // built-in của CLI
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, "utf8");
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, "SKILL.md")))
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) onLog(`▶ sub-agent depth=${depth} bắt đầu: ${task.slice(0, 80)}${task.length > 80 ? '…' : ''}`);
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 bộ bằng gpt-tokenizer (cl100k_base - tokenizer của GPT-4).
2
- // CHÍNH XÁC cho GPT, XẤP XỈ cho Claude/Gemini/khác (sai số ~5-15%) — đủ để
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 "gpt-tokenizer";
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 encode(String(text)).length;
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 || "") + 4;
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 cl100k_base
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) + "k" : String(n));
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 !== "object") return;
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
  }