@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/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
|
|
45
|
-
|
|
46
|
-
| `pro`
|
|
47
|
-
| `proplus` | 10 000 request / 5 giờ
|
|
48
|
-
| `admin`
|
|
49
|
-
| `trial`
|
|
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
|
|
75
|
-
|
|
76
|
-
| `/model [name]` | switch model (fuzzy match), or list all
|
|
77
|
-
| `/models`
|
|
78
|
-
| `/merge`
|
|
79
|
-
| `/search`
|
|
80
|
-
| `/chat`
|
|
81
|
-
| `/yolo`
|
|
82
|
-
| `/clear` `/new` | wipe conversation context
|
|
83
|
-
| `/status`
|
|
84
|
-
| `/exit`
|
|
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
|
|
107
|
-
|
|
108
|
-
| `NOOB_API_BASE`
|
|
109
|
-
| `NOOB_API_KEY`
|
|
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
|
|
118
|
-
|
|
119
|
-
| **Anthropic** (Claude) | ✅ best
|
|
120
|
-
| **DeepSeek**
|
|
121
|
-
| OpenAI (GPT/o-series)
|
|
122
|
-
| Google (Gemini)
|
|
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
|
|
3
|
-
import { config } from
|
|
4
|
-
import { usage, ApiError, applyInsecureTLS } from
|
|
5
|
-
import { c } from
|
|
6
|
-
import { t } from
|
|
7
|
-
import { checkLatest, runUpdate, CURRENT } from
|
|
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 = {
|
|
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 ===
|
|
16
|
-
else if (a ===
|
|
17
|
-
else if (a ===
|
|
18
|
-
else if (a ===
|
|
19
|
-
else if (a ===
|
|
20
|
-
else if (a ===
|
|
21
|
-
else if (a.startsWith(
|
|
22
|
-
else if (a ===
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
56
|
-
console.log(c.dim(
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
-
"
|
|
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
|
}
|