@noobdemon/noob-cli 1.10.20 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,12 +41,12 @@ Trong phiên: `/login <key>`, `/usage`, `/logout`.
41
41
 
42
42
  **Các gói:**
43
43
 
44
- | Gói | Hạn mức |
45
- |---|---|
46
- | `pro` | 5 000 request / 5 giờ (cửa sổ trượt) |
47
- | `proplus` | 10 000 request / 5 giờ |
48
- | `admin` | không giới hạn |
49
- | `trial` | 200 request dùng thử, hết là key **dead** |
44
+ | Gói | Hạn mức |
45
+ | --------- | ----------------------------------------- |
46
+ | `pro` | 5 000 request / 5 giờ (cửa sổ trượt) |
47
+ | `proplus` | 10 000 request / 5 giờ |
48
+ | `admin` | không giới hạn |
49
+ | `trial` | 200 request dùng thử, hết là key **dead** |
50
50
 
51
51
  > Mỗi lệnh gọi mô hình (kể cả từng bước tool trong một tác vụ) tính là 1 request.
52
52
 
@@ -71,17 +71,17 @@ Without `npm link`, run `node bin/noob.js …`.
71
71
 
72
72
  ### In-session commands
73
73
 
74
- | Command | What it does |
75
- |---|---|
76
- | `/model [name]` | switch model (fuzzy match), or list all |
77
- | `/models` | list every model grouped by provider |
78
- | `/merge` | toggle **Merge AI** — synthesizes GPT-5.5 + Claude + Gemini via o3 |
79
- | `/search` | toggle **web search** mode |
80
- | `/chat` | back to normal chat mode |
81
- | `/yolo` | toggle auto-approve for file/command tools (or **Shift+Tab**) |
82
- | `/clear` `/new` | wipe conversation context |
83
- | `/status` | show current model + working directory |
84
- | `/exit` | quit (Ctrl+C once = stop turn, twice = quit) |
74
+ | Command | What it does |
75
+ | --------------- | ------------------------------------------------------------------ |
76
+ | `/model [name]` | switch model (fuzzy match), or list all |
77
+ | `/models` | list every model grouped by provider |
78
+ | `/merge` | toggle **Merge AI** — synthesizes GPT-5.5 + Claude + Gemini via o3 |
79
+ | `/search` | toggle **web search** mode |
80
+ | `/chat` | back to normal chat mode |
81
+ | `/yolo` | toggle auto-approve for file/command tools (or **Shift+Tab**) |
82
+ | `/clear` `/new` | wipe conversation context |
83
+ | `/status` | show current model + working directory |
84
+ | `/exit` | quit (Ctrl+C once = stop turn, twice = quit) |
85
85
 
86
86
  ## How it works
87
87
 
@@ -94,6 +94,26 @@ function-calling. noob layers an agent on top:
94
94
  3. noob parses the tool call, asks permission if it's destructive, executes it, and
95
95
  feeds the result back — looping until the model answers without a tool block.
96
96
 
97
+ ### Architecture (high-level)
98
+
99
+ ```mermaid
100
+ flowchart LR
101
+ User[User<br/>terminal] -->|prompt| REPL[repl.js<br/>input loop + slash commands]
102
+ REPL -->|message + history| Agent[agent.js<br/>tool-loop driver]
103
+ Agent -->|/api/chat| API[api.js<br/>gateway client]
104
+ API -->|HTTPS stream| GW[(Noob Demon<br/>Gateway)]
105
+ GW -->|delta tokens| API
106
+ API -->|onDelta callbacks| Agent
107
+ Agent -->|parse tool block| Tools[tools.js<br/>read/write/edit/run]
108
+ Tools -->|fs / spawn| FS[(workspace)]
109
+ Tools -->|result text| Agent
110
+ Agent -->|final answer| REPL
111
+ REPL -->|persist| Sessions[(~/.noob/sessions/)]
112
+ REPL -->|memory| Memory[(noob.md per project)]
113
+ Agent -.optional.-> Subagent[subagent.js<br/>spawn_agent]
114
+ Subagent -->|isolated context| API
115
+ ```
116
+
97
117
  ### Tools the agent can call
98
118
 
99
119
  `read_file` · `write_file` · `edit_file` · `list_dir` · `glob` · `grep` · `run_command`
@@ -103,10 +123,10 @@ Destructive tools (`write_file`, `edit_file`, `run_command`) prompt for approval
103
123
 
104
124
  ## Configuration
105
125
 
106
- | Env var | Effect |
107
- |---|---|
108
- | `NOOB_API_BASE` | override the gateway URL |
109
- | `NOOB_API_KEY` | API key (overrides `~/.noob/config.json`) |
126
+ | Env var | Effect |
127
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
128
+ | `NOOB_API_BASE` | override the gateway URL |
129
+ | `NOOB_API_KEY` | API key (overrides `~/.noob/config.json`) |
110
130
  | `NOOB_INSECURE_TLS=1` | disable TLS verification — **last resort** for machines behind a TLS-intercepting proxy. Prefer adding your proxy CA to the trust store. |
111
131
 
112
132
  ## Model compatibility
@@ -114,16 +134,82 @@ Destructive tools (`write_file`, `edit_file`, `run_command`) prompt for approval
114
134
  The agent drives tools through a text protocol (the proxy has no native
115
135
  function-calling). Models differ in how willingly they follow it:
116
136
 
117
- | Provider | Agentic tools | Notes |
118
- |---|---|---|
119
- | **Anthropic** (Claude) | ✅ best | default — `gateway-claude-opus-4-7` |
120
- | **DeepSeek** | ✅ works | good free alternative |
121
- | OpenAI (GPT/o-series) | ⚠️ often refuses | replies "I can't access your filesystem" |
122
- | Google (Gemini) | ⚠️ often refuses | same |
137
+ | Provider | Agentic tools | Notes |
138
+ | ---------------------- | ---------------- | ---------------------------------------- |
139
+ | **Anthropic** (Claude) | ✅ best | default — `gateway-claude-opus-4-7` |
140
+ | **DeepSeek** | ✅ works | good free alternative |
141
+ | OpenAI (GPT/o-series) | ⚠️ often refuses | replies "I can't access your filesystem" |
142
+ | Google (Gemini) | ⚠️ often refuses | same |
123
143
 
124
144
  Stick with Claude or DeepSeek for file edits & commands. Any model is fine for
125
145
  plain chat, `/merge`, and `/search`.
126
146
 
147
+ ## Project structure
148
+
149
+ ```
150
+ src/
151
+ ├── api.js # gateway client + stream parser + memory token
152
+ ├── agent.js # tool-loop driver, summarization, prompt assembly
153
+ ├── tools.js # read/write/edit/list_dir/glob/grep/run_command + bg
154
+ ├── repl.js # input loop, slash commands, session state (88 KB)
155
+ ├── repl/
156
+ │ ├── complete.js # SLASH catalog + autocomplete
157
+ │ ├── todos.js # parseTodosFromHistory (pure)
158
+ │ ├── ultra.js # ULTRA mode constants + prompt templates
159
+ │ └── workflow-commands.js # /workflow help/list/load/delete (pure)
160
+ ├── subagent.js # spawn_agent / spawn_agents — sub-agent isolation
161
+ ├── tui.js # terminal UI (input, status bar, prompt rendering)
162
+ ├── ui.js # color helpers, markdown renderer, banner
163
+ ├── config.js # ~/.noob/config.json (api key, gateway, model)
164
+ ├── sessions.js # ~/.noob/sessions/ — save/list/resume
165
+ ├── skills.js # ~/.noob/skills/ — user-defined skill loader
166
+ ├── workflows.js # ~/.noob/workflows/ — user-saved workflows
167
+ ├── workflows-builtin.js # ship-with-binary workflows (deep-research, etc)
168
+ ├── memory.js # noob.md per-project agent memory
169
+ ├── tokens.js # local token counter (o200k_base / cl100k_base)
170
+ ├── models.js # 34-model catalog + fuzzy resolver
171
+ ├── i18n.js # Vietnamese string table
172
+ ├── update.js # `noob update` self-updater
173
+ └── prompts/
174
+ └── system.md # base system prompt template
175
+
176
+ tests/ # vitest unit tests (96 tests)
177
+ scripts/ # check-imports, notify-discord, release.ps1
178
+ ```
179
+
180
+ ## Development
181
+
182
+ ```bash
183
+ npm test # run vitest unit tests (96 tests across 7 files)
184
+ npm run lint # eslint --check
185
+ npm run lint:fix # eslint --fix
186
+ npm run format # prettier --write
187
+ npm run format:check # prettier --check
188
+ npm run check # lint + format:check + test (pre-commit gate)
189
+ ```
190
+
191
+ Husky chạy `lint-staged` + `npm test` trước mỗi commit. Bỏ qua bằng
192
+ `git commit --no-verify` nếu thực sự cần (vd commit WIP).
193
+
194
+ ## Troubleshooting
195
+
196
+ **"CLI tự thoát sau khi hỏi quyền"** — thường là readline trên Windows phát sự
197
+ kiện `close` từ tiến trình con chạm vào console. Đã có workaround trong
198
+ `repl.js` (input layer tự re-arm). Nếu còn xảy ra, mở issue kèm OS + Node version.
199
+
200
+ **"TLS error / certificate"** — máy bạn sau proxy chặn TLS (Zscaler, Cisco
201
+ Umbrella…). Ưu tiên add CA của proxy vào trust store. Cuối cùng mới dùng
202
+ `NOOB_INSECURE_TLS=1` (tắt verify TLS toàn process, MITM-vulnerable).
203
+
204
+ **"Model OpenAI/Gemini từ chối tool"** — bình thường. GPT-o-series và Gemini
205
+ thường refuse access filesystem qua text protocol. Dùng `/model claude-opus` hoặc
206
+ `/model deepseek-v3` cho tasks cần tool.
207
+
208
+ **"Token count có vẻ sai"** — `tokens.js` chọn encoder theo model id
209
+ (`o200k_base` cho GPT-4o/5/o-series, `cl100k_base` cho phần còn lại). Đếm chính
210
+ xác cho OpenAI, xấp xỉ ±5-15% cho Claude/Gemini/khác (các provider này không
211
+ publish tokenizer public).
212
+
127
213
  ## Notes & limits
128
214
 
129
215
  - Free shared proxy: rate limits and occasional hiccups are expected.
package/bin/noob.js CHANGED
@@ -1,25 +1,32 @@
1
1
  #!/usr/bin/env node
2
- import { startRepl } from "../src/repl.js";
3
- import { config } from "../src/config.js";
4
- import { usage, ApiError, applyInsecureTLS } from "../src/api.js";
5
- import { c } from "../src/ui.js";
6
- import { t } from "../src/i18n.js";
7
- import { checkLatest, runUpdate, CURRENT } from "../src/update.js";
2
+ import { startRepl } from '../src/repl.js';
3
+ import { config } from '../src/config.js';
4
+ import { usage, ApiError, applyInsecureTLS } from '../src/api.js';
5
+ import { c } from '../src/ui.js';
6
+ import { t } from '../src/i18n.js';
7
+ import { checkLatest, runUpdate, CURRENT } from '../src/update.js';
8
8
 
9
9
  const argv = process.argv.slice(2);
10
- const opts = { yolo: false, ultra: false, model: undefined, prompt: undefined, continue: false, resume: false };
10
+ const opts = {
11
+ yolo: false,
12
+ ultra: false,
13
+ model: undefined,
14
+ prompt: undefined,
15
+ continue: false,
16
+ resume: false,
17
+ };
11
18
  const positional = [];
12
19
 
13
20
  for (let i = 0; i < argv.length; i++) {
14
21
  const a = argv[i];
15
- if (a === "--yolo" || a === "-y") opts.yolo = true;
16
- else if (a === "--ultra" || a === "-u") opts.ultra = true;
17
- else if (a === "--insecure-tls") process.env.NOOB_INSECURE_TLS = "1";
18
- else if (a === "--model" || a === "-m") opts.model = argv[++i];
19
- else if (a === "--continue" || a === "-c") opts.continue = true;
20
- else if (a === "--resume" || a === "-r") opts.resume = true;
21
- else if (a.startsWith("--resume=")) opts.resume = a.slice("--resume=".length);
22
- else if (a === "--help" || a === "-h") {
22
+ if (a === '--yolo' || a === '-y') opts.yolo = true;
23
+ else if (a === '--ultra' || a === '-u') opts.ultra = true;
24
+ else if (a === '--insecure-tls') process.env.NOOB_INSECURE_TLS = '1';
25
+ else if (a === '--model' || a === '-m') opts.model = argv[++i];
26
+ else if (a === '--continue' || a === '-c') opts.continue = true;
27
+ else if (a === '--resume' || a === '-r') opts.resume = true;
28
+ else if (a.startsWith('--resume=')) opts.resume = a.slice('--resume='.length);
29
+ else if (a === '--help' || a === '-h') {
23
30
  printHelp();
24
31
  process.exit(0);
25
32
  } else positional.push(a);
@@ -32,35 +39,41 @@ const sub = positional[0];
32
39
  applyInsecureTLS();
33
40
 
34
41
  // ── subcommands ──────────────────────────────────────────────────────────
35
- if (sub === "login") {
42
+ if (sub === 'login') {
36
43
  const key = positional[1];
37
44
  if (!key) {
38
45
  console.log(c.err(t.needKeyArg));
39
46
  process.exit(1);
40
47
  }
41
48
  config.setKey(key);
42
- console.log(c.ok("") + t.loginSaved(config.path));
49
+ console.log(c.ok('') + t.loginSaved(config.path));
43
50
  try {
44
51
  const u = await usage();
45
52
  if (u.ok) {
46
- console.log(c.ok(t.loginOk({ pro: "Pro", proplus: "Pro+", admin: "Admin", trial: "Trial" }[u.plan] || u.plan)));
53
+ console.log(
54
+ c.ok(
55
+ t.loginOk(
56
+ { pro: 'Pro', proplus: 'Pro+', admin: 'Admin', trial: 'Trial' }[u.plan] || u.plan
57
+ )
58
+ )
59
+ );
47
60
  process.exit(0);
48
61
  } else {
49
62
  // Key đã lưu xuống đĩa nhưng gateway từ chối — exit != 0 để user biết, script CI cũng bắt được.
50
- console.log(c.err("" + t.errInvalidKey));
63
+ console.log(c.err('' + t.errInvalidKey));
51
64
  process.exit(2);
52
65
  }
53
66
  } catch (err) {
54
67
  // Mạng/gateway down: giữ key (user có thể đang offline), nhưng KHÔNG im lặng.
55
- console.log(c.err("⚠ không verify được key (mạng/gateway): " + (err?.message || err)));
56
- console.log(c.dim(" key vẫn được lưu — chạy `noob usage` khi có mạng để xác nhận."));
68
+ console.log(c.err('⚠ không verify được key (mạng/gateway): ' + (err?.message || err)));
69
+ console.log(c.dim(' key vẫn được lưu — chạy `noob usage` khi có mạng để xác nhận.'));
57
70
  process.exit(3);
58
71
  }
59
- } else if (sub === "logout") {
72
+ } else if (sub === 'logout') {
60
73
  config.clearKey();
61
74
  console.log(c.ok(t.loggedOut));
62
75
  process.exit(0);
63
- } else if (sub === "update") {
76
+ } else if (sub === 'update') {
64
77
  console.log(c.dim(t.updateChecking));
65
78
  const v = await checkLatest({ throttle: false });
66
79
  if (!v) {
@@ -71,7 +84,7 @@ if (sub === "login") {
71
84
  const ok = await runUpdate({ background: false });
72
85
  console.log(ok ? c.ok(t.updateOk) : c.err(t.updateFail));
73
86
  process.exit(ok ? 0 : 1);
74
- } else if (sub === "usage") {
87
+ } else if (sub === 'usage') {
75
88
  if (!config.apiKey) {
76
89
  console.log(c.tool(t.notLoggedIn));
77
90
  process.exit(1);
@@ -80,16 +93,16 @@ if (sub === "login") {
80
93
  const u = await usage();
81
94
  console.log(JSON.stringify(u, null, 2));
82
95
  } catch (err) {
83
- console.log(c.err("" + (err.message || t.errConn)));
96
+ console.log(c.err('' + (err.message || t.errConn)));
84
97
  process.exit(1);
85
98
  }
86
99
  process.exit(0);
87
100
  }
88
101
 
89
- if (positional.length) opts.prompt = positional.join(" ");
102
+ if (positional.length) opts.prompt = positional.join(' ');
90
103
 
91
104
  startRepl(opts).catch((err) => {
92
- console.error(c.err("lỗi nghiêm trọng: " + (err?.stack || err?.message || err)));
105
+ console.error(c.err('lỗi nghiêm trọng: ' + (err?.stack || err?.message || err)));
93
106
  process.exit(1);
94
107
  });
95
108
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.10.20",
3
+ "version": "1.11.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -14,11 +14,20 @@
14
14
  "src/",
15
15
  "skills/",
16
16
  "README.md",
17
+ "CHANGELOG.md",
17
18
  "LICENSE"
18
19
  ],
19
20
  "scripts": {
20
21
  "start": "node bin/noob.js",
21
- "postpublish": "node scripts/notify-discord.js"
22
+ "lint": "eslint .",
23
+ "lint:fix": "eslint . --fix",
24
+ "format": "prettier --write .",
25
+ "format:check": "prettier --check .",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "check": "npm run lint && npm run format:check && npm run test",
29
+ "postpublish": "node scripts/notify-discord.js",
30
+ "prepare": "husky"
22
31
  },
23
32
  "engines": {
24
33
  "node": ">=18"
@@ -44,5 +53,24 @@
44
53
  "gradient-string": "^3.0.0",
45
54
  "marked": "^15.0.12",
46
55
  "marked-terminal": "^7.3.0"
56
+ },
57
+ "devDependencies": {
58
+ "@eslint/js": "^9.39.4",
59
+ "eslint": "^9.39.4",
60
+ "eslint-config-prettier": "^10.1.8",
61
+ "globals": "^17.6.0",
62
+ "husky": "^9.1.7",
63
+ "lint-staged": "^16.4.0",
64
+ "prettier": "^3.8.4",
65
+ "vitest": "^4.1.8"
66
+ },
67
+ "lint-staged": {
68
+ "*.{js,mjs,cjs}": [
69
+ "eslint --fix",
70
+ "prettier --write"
71
+ ],
72
+ "*.{json,md,yml,yaml}": [
73
+ "prettier --write"
74
+ ]
47
75
  }
48
76
  }