@noobdemon/noob-cli 1.13.0 → 1.13.2
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 +26 -0
- package/README.md +139 -106
- package/package.json +3 -2
- package/src/agent.js +66 -9
- package/src/api.js +11 -1
- package/src/diff.js +40 -6
- package/src/i18n.js +0 -14
- package/src/repl/commands/goal.js +37 -0
- package/src/repl/commands/kg.js +147 -0
- package/src/repl.js +36 -147
- package/src/tools.js +22 -6
- package/src/tui.js +128 -29
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
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.13.2] - 2026-06-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Hỗ trợ nhiều ảnh trong một tin nhắn**: paste hoặc nhắc nhiều ảnh `@file` trong cùng một lượt — tất cả được đính kèm cho mô hình vision, mỗi ảnh là một phần riêng. Trước đây chỉ ảnh đầu tiên được gửi đi.
|
|
9
|
+
- **Smoke test chống README drift** (`scripts/smoke-readme-drift.mjs`): đối chiếu README với danh mục mô hình + tool thật — fail nếu README quảng cáo số mô hình lớn hơn thực tế hoặc thiếu tool. 20/20 pass.
|
|
10
|
+
- **Gợi ý trang động cho `web_fetch`** (`src/tools.js`): khi tải một trang nhiều HTML nhưng bóc ra rất ít chữ (thường là trang render bằng JS), trả thêm gợi ý thử chế độ `raw` thay vì để mô hình tưởng trang trống. 16/16 pass.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **README viết lại khớp thực tế**: số lượng mô hình, danh sách tool và lệnh trong phiên, cấu trúc thư mục `src/` đều cập nhật cho đúng với mã hiện tại.
|
|
14
|
+
|
|
15
|
+
### Internal
|
|
16
|
+
- **Tách bớt `src/repl.js`** (`src/repl/commands/kg.js` + `src/repl/commands/goal.js`): khối `/kg` và `/goal` chuyển thành hàm riêng nhận dependency injected, không truy cập closure `startRepl`. Verify: import sạch, `smoke-kg` 45/45, `npm test` 109/109.
|
|
17
|
+
- **Luồng ảnh dạng mảng** (`src/api.js` + `src/agent.js` + `src/repl.js`): tham số `images` (mảng) luồng xuyên `runAgent → streamWithRetry → stream → streamOnce`, giữ `image` đơn làm alias tương thích ngược.
|
|
18
|
+
|
|
19
|
+
## [1.13.1] - 2026-06-28
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **Text-loop detection** (`src/agent.js`): model lặp cùng 1 câu liên tiếp mà không gọi tool (vd "Tôi sẽ bắt đầu bằng việc khảo sát workspace" 40 lần) — phát hiện ≥2 lần trùng → nudge `[TEXT LOOP]`, ≥3 lần → force stop `[LOOP STOPPED]`. Reset khi model gọi tool thành công.
|
|
23
|
+
- **Duplicate tool call block trước execution** (`src/agent.js`): kiểm tra `duplicateToolGuidance` chạy TRƯỚC `onTool()` (chỉ post-execution trước đó).
|
|
24
|
+
- **Edit-file retry guidance** (`src/tools.js` + `src/agent.js`): lỗi `old_string not found` giờ inject `NEXT REQUIRED TOOL: read_file {path,offset,limit}` + `toolErrorGuidance` system nudge ép model đọc lại file trước khi retry.
|
|
25
|
+
- **Todo nudge phân biệt success/failure** (`src/agent.js`): `todoContinuationMessage` nói "vừa LỖI" khi tool trả `ERROR:`, không đẩy model lặp lại thao tác thất bại.
|
|
26
|
+
- **nearbyContext mở rộng** (`src/tools.js`): context lines khi edit_file lỗi scales với độ dài `old_string` (`Math.max(8, oldLineCount + 5)`).
|
|
27
|
+
|
|
28
|
+
### Verified
|
|
29
|
+
- `npm test` 109/109 pass.
|
|
30
|
+
|
|
5
31
|
## [1.13.0] - 2026-06-27
|
|
6
32
|
|
|
7
33
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# noob — agentic coding CLI
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
**Noob Demon**
|
|
5
|
-
|
|
3
|
+
Một coding agent kiểu Claude Code sống trong terminal, giao tiếp **tiếng Việt**,
|
|
4
|
+
chạy qua gateway **Noob Demon** với 3 mô hình flagship (Claude Opus 4.8, GPT-5.5,
|
|
5
|
+
DeepSeek V4 Pro).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
Nó đọc & sửa file, chạy lệnh shell, tự lặp để hoàn thành tác vụ — xin phép trước
|
|
8
|
+
mọi thao tác có rủi ro — tất cả trong một giao diện terminal gọn gàng.
|
|
9
9
|
|
|
10
10
|
```
|
|
11
11
|
███╗ ██╗ ██████╗ ██████╗ ██████╗
|
|
@@ -21,15 +21,15 @@ permission before anything destructive — all inside a polished terminal UI.
|
|
|
21
21
|
```bash
|
|
22
22
|
cd "noob cli"
|
|
23
23
|
npm install
|
|
24
|
-
npm link #
|
|
24
|
+
npm link # tùy chọn: cho phép gõ `noob` toàn cục
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Yêu cầu Node.js ≥ 18 (test trên 22).
|
|
28
28
|
|
|
29
|
-
## Xác thực & gói cước
|
|
29
|
+
## Xác thực & gói cước
|
|
30
30
|
|
|
31
|
-
noob đi qua một **gateway** (Cloudflare Worker
|
|
32
|
-
|
|
31
|
+
noob đi qua một **gateway** (Cloudflare Worker) để ẩn backend thật và quản lý API
|
|
32
|
+
key. Bạn cần một API key để dùng:
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
noob login nk_xxx_xxxxxxxx # đăng nhập, key lưu ở ~/.noob/config.json
|
|
@@ -50,51 +50,71 @@ Trong phiên: `/login <key>`, `/usage`, `/logout`.
|
|
|
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
|
|
|
53
|
-
Cấp/huỷ key (admin) — trong repo gateway `worker/`:
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
node scripts/admin.mjs create pro "khách A"
|
|
57
|
-
node scripts/admin.mjs list
|
|
58
|
-
node scripts/admin.mjs revoke nk_pro_xxx
|
|
59
|
-
```
|
|
60
|
-
|
|
61
53
|
## Use
|
|
62
54
|
|
|
63
55
|
```bash
|
|
64
|
-
noob #
|
|
65
|
-
noob "add input validation to api.js" #
|
|
66
|
-
noob -m gateway-claude-opus-4-8 #
|
|
67
|
-
noob --yolo #
|
|
56
|
+
noob # phiên tương tác
|
|
57
|
+
noob "add input validation to api.js" # bắt đầu kèm yêu cầu
|
|
58
|
+
noob -m gateway-claude-opus-4-8 # chọn mô hình
|
|
59
|
+
noob --yolo # tự duyệt edit & lệnh
|
|
68
60
|
```
|
|
69
61
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
| `/
|
|
80
|
-
| `/
|
|
81
|
-
| `/
|
|
82
|
-
| `/
|
|
83
|
-
| `/
|
|
84
|
-
| `/
|
|
62
|
+
Không `npm link` thì chạy `node bin/noob.js …`.
|
|
63
|
+
|
|
64
|
+
Cờ dòng lệnh: `--yolo`, `--ultra`, `--model <name>`, `--continue`, `--resume`,
|
|
65
|
+
`--insecure-tls`.
|
|
66
|
+
|
|
67
|
+
### Lệnh trong phiên
|
|
68
|
+
|
|
69
|
+
| Lệnh | Tác dụng |
|
|
70
|
+
| ----------------- | ------------------------------------------------------------- |
|
|
71
|
+
| `/help` | danh sách lệnh |
|
|
72
|
+
| `/model [name]` | đổi mô hình (fuzzy match), hoặc liệt kê tất cả |
|
|
73
|
+
| `/models` | liệt kê mọi mô hình theo provider |
|
|
74
|
+
| `/merge` | bật/tắt **Merge AI** (tổng hợp đa mô hình) |
|
|
75
|
+
| `/search` | bật/tắt chế độ tìm web |
|
|
76
|
+
| `/chat` | quay lại chế độ chat thường |
|
|
77
|
+
| `/agent on\|off` | bật/tắt agent mode (đẻ sub-agent song song/tuần tự/phân cấp) |
|
|
78
|
+
| `/mode` | build\|plan\|compose — đổi chế độ agent (Ctrl+T cycle) |
|
|
79
|
+
| `/goal [text]` | đặt HARD GOAL cho phiên · `/goal clear` để xoá |
|
|
80
|
+
| `/loop <iv> <task>` | chạy task định kỳ · `/loop stop` để dừng |
|
|
81
|
+
| `/ultra` | chế độ tự hành: model tự nghĩ & làm tới khi xong |
|
|
82
|
+
| `/workflow` | orchestrate workflow đa-agent (xem `/workflow help`) |
|
|
83
|
+
| `/kg` | knowledge graph của project (xem `/kg help`) |
|
|
84
|
+
| `/compact` | tóm tắt phiên ngay để gọn ngữ cảnh |
|
|
85
|
+
| `/tokens` | xem token đã dùng trong phiên |
|
|
86
|
+
| `/memory` | xem bộ nhớ `noob.md` · `/memory stats` xem kích thước |
|
|
87
|
+
| `/learn` | chưng cất bài học vào `noob.md` |
|
|
88
|
+
| `/init` | quét dự án & tạo `noob.md` |
|
|
89
|
+
| `/karpathy` | rà soát code theo nguyên tắc Karpathy |
|
|
90
|
+
| `/frontend-design`| skill thiết kế UI frontend chất lượng cao |
|
|
91
|
+
| `/improve` | phân tích workspace & gợi ý tính năng cải thiện |
|
|
92
|
+
| `/add-dir <path>` | thêm thư mục ngoài cwd vào phạm vi · `remove` để gỡ |
|
|
93
|
+
| `/cwd` | thư mục làm việc hiện tại |
|
|
94
|
+
| `/yolo` | bật/tắt tự duyệt file/lệnh (hoặc **Shift+Tab**) |
|
|
95
|
+
| `/auto-yolo` | lưu yolo làm mặc định mỗi lần chạy |
|
|
96
|
+
| `/clear` `/new` | xoá ngữ cảnh / phiên mới |
|
|
97
|
+
| `/continue` | tiếp tục phiên gần nhất |
|
|
98
|
+
| `/resume` | chọn & tiếp tục một phiên cũ |
|
|
99
|
+
| `/sessions` | liệt kê phiên đã lưu |
|
|
100
|
+
| `/status` | mô hình hiện tại + thư mục làm việc |
|
|
101
|
+
| `/version` | phiên bản |
|
|
102
|
+
| `/login` `/logout` `/usage` | xác thực & hạn mức |
|
|
103
|
+
| `/update` | tự cập nhật noob |
|
|
104
|
+
| `/exit` | thoát (Ctrl+C một lần = dừng turn, hai lần = thoát) |
|
|
85
105
|
|
|
86
106
|
## How it works
|
|
87
107
|
|
|
88
|
-
|
|
89
|
-
function-calling. noob
|
|
108
|
+
Gateway Noob Demon là một endpoint **stateless single-message** không có
|
|
109
|
+
function-calling gốc. noob phủ một lớp agent lên trên:
|
|
90
110
|
|
|
91
|
-
1.
|
|
92
|
-
|
|
93
|
-
2.
|
|
94
|
-
3. noob
|
|
95
|
-
|
|
111
|
+
1. Toàn bộ transcript (system prompt + history + kết quả tool) được serialize
|
|
112
|
+
thành một `message` rồi stream tới `/api/chat`.
|
|
113
|
+
2. Model trả về hoặc câu trả lời cuối, hoặc một khối ```tool JSON duy nhất.
|
|
114
|
+
3. noob parse tool call, xin phép nếu nó có rủi ro, thực thi, rồi feed kết quả
|
|
115
|
+
lại — lặp tới khi model trả lời không kèm tool block.
|
|
96
116
|
|
|
97
|
-
###
|
|
117
|
+
### Kiến trúc (tổng quan)
|
|
98
118
|
|
|
99
119
|
```mermaid
|
|
100
120
|
flowchart LR
|
|
@@ -104,8 +124,8 @@ flowchart LR
|
|
|
104
124
|
API -->|HTTPS stream| GW[(Noob Demon<br/>Gateway)]
|
|
105
125
|
GW -->|delta tokens| API
|
|
106
126
|
API -->|onDelta callbacks| Agent
|
|
107
|
-
Agent -->|parse tool block| Tools[tools.js<br/>read/write/edit/run]
|
|
108
|
-
Tools -->|fs / spawn| FS[(workspace)]
|
|
127
|
+
Agent -->|parse tool block| Tools[tools.js<br/>read/write/edit/run/fetch]
|
|
128
|
+
Tools -->|fs / spawn / http| FS[(workspace)]
|
|
109
129
|
Tools -->|result text| Agent
|
|
110
130
|
Agent -->|final answer| REPL
|
|
111
131
|
REPL -->|persist| Sessions[(~/.noob/sessions/)]
|
|
@@ -114,109 +134,122 @@ flowchart LR
|
|
|
114
134
|
Subagent -->|isolated context| API
|
|
115
135
|
```
|
|
116
136
|
|
|
117
|
-
###
|
|
137
|
+
### Tool agent gọi được
|
|
118
138
|
|
|
119
|
-
|
|
139
|
+
Đọc/khám phá (chạy tự do, không xin phép):
|
|
140
|
+
`read_file` · `list_dir` · `glob` · `grep` · `web_fetch`
|
|
120
141
|
|
|
121
|
-
|
|
122
|
-
|
|
142
|
+
Thao tác có rủi ro (xin phép `y`/`n`/`a`, thêm `t`=hết turn, `f`=file này khi
|
|
143
|
+
bật rich TTY):
|
|
144
|
+
`write_file` · `edit_file` · `run_command`
|
|
145
|
+
|
|
146
|
+
Quản lý tiến trình nền & điều phối:
|
|
147
|
+
`bg_output` · `kill_bg` · `write_todos` · `spawn_agent` / `spawn_agents` (khi
|
|
148
|
+
`/agent` bật) · `kg_search` / `kg_add` / `kg_link` / `kg_obs` (knowledge graph)
|
|
149
|
+
|
|
150
|
+
> `web_fetch` truy cập mạng (http/https) nhưng read-only nên không xin phép.
|
|
151
|
+
> Nó strip HTML bằng regex; với trang render bằng JS (SPA) sẽ trả về gần rỗng
|
|
152
|
+
> kèm gợi ý thử `raw:true`.
|
|
123
153
|
|
|
124
154
|
## Configuration
|
|
125
155
|
|
|
126
|
-
| Env var |
|
|
127
|
-
| --------------------- |
|
|
128
|
-
| `NOOB_API_BASE` |
|
|
129
|
-
| `NOOB_API_KEY` | API key (
|
|
130
|
-
| `NOOB_INSECURE_TLS=1` |
|
|
156
|
+
| Env var | Tác dụng |
|
|
157
|
+
| --------------------- | ------------------------------------------------------------------ |
|
|
158
|
+
| `NOOB_API_BASE` | ghi đè URL gateway |
|
|
159
|
+
| `NOOB_API_KEY` | API key (ghi đè `~/.noob/config.json`) |
|
|
160
|
+
| `NOOB_INSECURE_TLS=1` | tắt verify TLS — **giải pháp cuối** cho máy sau proxy chặn TLS. |
|
|
131
161
|
|
|
132
162
|
## Model compatibility
|
|
133
163
|
|
|
134
|
-
|
|
135
|
-
|
|
164
|
+
Agent điều khiển tool qua một text protocol (gateway không có function-calling
|
|
165
|
+
gốc). Các model khác nhau về mức độ sẵn lòng tuân theo:
|
|
136
166
|
|
|
137
|
-
| Provider | Agentic tools |
|
|
167
|
+
| Provider | Agentic tools | Ghi chú |
|
|
138
168
|
| ---------------------- | ---------------- | ---------------------------------------- |
|
|
139
|
-
| **Anthropic** (Claude) | ✅
|
|
140
|
-
| **DeepSeek** | ✅
|
|
141
|
-
| OpenAI (GPT
|
|
142
|
-
| Google (Gemini) | ⚠️ often refuses | same |
|
|
169
|
+
| **Anthropic** (Claude) | ✅ tốt nhất | mặc định — `gateway-claude-opus-4-8` |
|
|
170
|
+
| **DeepSeek** | ✅ chạy tốt | lựa chọn thay thế tốt |
|
|
171
|
+
| OpenAI (GPT) | ⚠️ hay từ chối | trả "I can't access your filesystem" |
|
|
143
172
|
|
|
144
|
-
|
|
145
|
-
|
|
173
|
+
Dùng Claude hoặc DeepSeek cho edit file & chạy lệnh. Mọi model đều ổn cho chat
|
|
174
|
+
thường, `/merge`, và `/search`.
|
|
146
175
|
|
|
147
176
|
## Project structure
|
|
148
177
|
|
|
149
178
|
```
|
|
150
179
|
src/
|
|
151
|
-
├── api.js # gateway client + stream parser +
|
|
152
|
-
├── agent.js # tool-loop driver,
|
|
153
|
-
├── tools.js # read/write/edit/list_dir/glob/grep/run_command
|
|
154
|
-
├── repl.js # input loop, slash commands, session state
|
|
180
|
+
├── api.js # gateway client + stream parser + quota warning
|
|
181
|
+
├── agent.js # tool-loop driver, SYSTEM prompt, summarization
|
|
182
|
+
├── tools.js # read/write/edit/list_dir/glob/grep/run_command/web_fetch/bg/kg
|
|
183
|
+
├── repl.js # input loop, slash commands, session state
|
|
155
184
|
├── repl/
|
|
156
|
-
│ ├──
|
|
157
|
-
│ ├──
|
|
158
|
-
│ ├──
|
|
159
|
-
│
|
|
185
|
+
│ ├── agent-dispatch.js # spawn_agent + write_todos dispatcher
|
|
186
|
+
│ ├── complete.js # SLASH catalog + autocomplete
|
|
187
|
+
│ ├── permission.js # askPermission (y/n/a/t/f scopes)
|
|
188
|
+
│ ├── state.js # createState — session state shape
|
|
189
|
+
│ ├── stream-printer.js # render stream theo dòng (heading/table/code)
|
|
190
|
+
│ ├── todos.js # parseTodosFromHistory (pure)
|
|
191
|
+
│ ├── ultra.js # ULTRA mode prompt templates
|
|
192
|
+
│ ├── workflow-commands.js # /workflow help/list/load/delete (pure)
|
|
193
|
+
│ └── commands/ # nhóm slash command tách rời
|
|
160
194
|
├── subagent.js # spawn_agent / spawn_agents — sub-agent isolation
|
|
161
|
-
├──
|
|
162
|
-
├──
|
|
195
|
+
├── workflow-bg.js # workflow chạy nền
|
|
196
|
+
├── workflow-runs.js # lưu/khôi phục lần chạy workflow
|
|
197
|
+
├── workflows.js # ~/.noob/workflows/ — workflow user lưu
|
|
198
|
+
├── workflows-builtin.js # workflow ship-sẵn (deep-research, verify-claims, triage)
|
|
199
|
+
├── kg.js # knowledge graph (.noob/kg.jsonl per project)
|
|
200
|
+
├── tui.js # terminal UI (input, status bar, todo progress)
|
|
201
|
+
├── ui.js # color, markdown renderer, banner
|
|
202
|
+
├── diff.js # render diff khi edit_file
|
|
163
203
|
├── config.js # ~/.noob/config.json (api key, gateway, model)
|
|
164
204
|
├── sessions.js # ~/.noob/sessions/ — save/list/resume
|
|
165
|
-
├── skills.js #
|
|
166
|
-
├── workflows.js # ~/.noob/workflows/ — user-saved workflows
|
|
167
|
-
├── workflows-builtin.js # ship-with-binary workflows (deep-research, etc)
|
|
205
|
+
├── skills.js # skill loader (cwd → package)
|
|
168
206
|
├── memory.js # noob.md per-project agent memory
|
|
169
|
-
├── tokens.js # local token counter (
|
|
170
|
-
├── models.js #
|
|
171
|
-
├── i18n.js #
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
tests/ # vitest unit tests (96 tests)
|
|
177
|
-
scripts/ # check-imports, notify-discord, release.ps1
|
|
207
|
+
├── tokens.js # local token counter (gpt-tokenizer)
|
|
208
|
+
├── models.js # 3-model flagship catalog + fuzzy resolver
|
|
209
|
+
├── i18n.js # bảng chuỗi tiếng Việt
|
|
210
|
+
└── update.js # `noob update` self-updater
|
|
211
|
+
|
|
212
|
+
tests/ # vitest unit tests
|
|
213
|
+
scripts/ # smoke tests + notify-discord
|
|
178
214
|
```
|
|
179
215
|
|
|
180
216
|
## Development
|
|
181
217
|
|
|
182
218
|
```bash
|
|
183
|
-
npm test #
|
|
184
|
-
npm run lint # eslint
|
|
219
|
+
npm test # chạy vitest unit tests
|
|
220
|
+
npm run lint # eslint
|
|
185
221
|
npm run lint:fix # eslint --fix
|
|
186
222
|
npm run format # prettier --write
|
|
187
223
|
npm run format:check # prettier --check
|
|
188
|
-
npm run check # lint + format:check + test (pre-commit
|
|
224
|
+
npm run check # lint + format:check + test (cổng pre-commit)
|
|
189
225
|
```
|
|
190
226
|
|
|
191
227
|
Husky chạy `lint-staged` + `npm test` trước mỗi commit. Bỏ qua bằng
|
|
192
228
|
`git commit --no-verify` nếu thực sự cần (vd commit WIP).
|
|
193
229
|
|
|
230
|
+
Ngoài unit test, `scripts/` chứa nhiều smoke test chạy bằng
|
|
231
|
+
`node scripts/smoke-<name>.mjs` (offline, không cần gateway).
|
|
232
|
+
|
|
194
233
|
## Troubleshooting
|
|
195
234
|
|
|
196
235
|
**"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
|
|
198
|
-
|
|
236
|
+
kiện `close` từ tiến trình con. Đã có workaround trong `repl.js`. Nếu còn xảy ra,
|
|
237
|
+
mở issue kèm OS + Node version.
|
|
199
238
|
|
|
200
|
-
**"TLS error / certificate"** — máy
|
|
239
|
+
**"TLS error / certificate"** — máy sau proxy chặn TLS (Zscaler, Cisco
|
|
201
240
|
Umbrella…). Ưu tiên add CA của proxy vào trust store. Cuối cùng mới dùng
|
|
202
241
|
`NOOB_INSECURE_TLS=1` (tắt verify TLS toàn process, MITM-vulnerable).
|
|
203
242
|
|
|
204
|
-
**"Model OpenAI
|
|
205
|
-
|
|
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).
|
|
243
|
+
**"Model OpenAI từ chối tool"** — GPT thường refuse access filesystem qua text
|
|
244
|
+
protocol. Dùng `/model claude` hoặc `/model deepseek` cho task cần tool.
|
|
212
245
|
|
|
213
246
|
## Notes & limits
|
|
214
247
|
|
|
215
|
-
-
|
|
216
|
-
- Context
|
|
217
|
-
`/clear`
|
|
218
|
-
-
|
|
219
|
-
|
|
248
|
+
- Proxy chia sẻ miễn phí: rate limit và đôi lúc trục trặc là bình thường.
|
|
249
|
+
- Context gửi đầy đủ mỗi lượt, nên phiên rất dài tốn nhiều token — dùng `/compact`
|
|
250
|
+
hoặc `/clear` để gọn lại.
|
|
251
|
+
- Đây là công cụ không chính thức, không liên kết với Anthropic hay các nhà cung
|
|
252
|
+
cấp model.
|
|
220
253
|
|
|
221
254
|
## License
|
|
222
255
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noobdemon/noob-cli",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"format:check": "prettier --check .",
|
|
26
26
|
"test": "vitest run",
|
|
27
27
|
"test:watch": "vitest",
|
|
28
|
-
"check": "
|
|
28
|
+
"i18n:check": "node scripts/check-i18n-orphans.mjs",
|
|
29
|
+
"check": "npm run lint && npm run format:check && npm run i18n:check && npm run test",
|
|
29
30
|
"postpublish": "node scripts/notify-discord.js",
|
|
30
31
|
"prepare": "husky"
|
|
31
32
|
},
|
package/src/agent.js
CHANGED
|
@@ -475,6 +475,24 @@ export function buildUserMessage(history) {
|
|
|
475
475
|
return parts.join('\n');
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
+
export function todoContinuationMessage(toolName, ok, tasks) {
|
|
479
|
+
if (!tasks?.length) return null;
|
|
480
|
+
const next = tasks[0];
|
|
481
|
+
const status = ok ? `đã hoàn thành` : `vừa LỖI`;
|
|
482
|
+
return `[SYSTEM] Việc "${toolName}" ${status}. Còn ${tasks.length} việc: ${tasks.map((t) => `"${t}"`).join(', ')}. Việc tiếp theo BẮT BUỘC phải làm ngay: "${next}". Gọi tool (write_file/edit_file/run_command) để làm việc này. KHÔNG dừng, KHÔNG tóm tắt.`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function toolErrorGuidance(name, { result } = {}) {
|
|
486
|
+
if (name !== 'edit_file' || !String(result || '').includes('old_string not found')) return null;
|
|
487
|
+
const m = String(result).match(/NEXT REQUIRED TOOL:\s*(read_file \{[^\n]+\})/);
|
|
488
|
+
if (!m) return null;
|
|
489
|
+
return `[SYSTEM] edit_file thất bại vì old_string sai. Bạn PHẢI gọi đúng tool này ngay: ${m[1]}. KHÔNG gọi edit_file lại trước khi đọc file. Sau đó copy đúng text từ read_file mới để retry.`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function duplicateToolGuidance(name, input) {
|
|
493
|
+
return `[SYSTEM] Tool call vừa rồi TRÙNG HỆT lần trước: ${name} ${JSON.stringify(input || {})}. KHÔNG chạy tool này lại. Dùng kết quả đã có trong history; nếu cần tiến thêm thì gọi tool khác (grep/read_file/edit_file/run_command) hoặc trả lời Markdown nếu xong.`;
|
|
494
|
+
}
|
|
495
|
+
|
|
478
496
|
// Detect câu trả lời bị cắt giữa chừng — KHÔNG phải câu hoàn chỉnh.
|
|
479
497
|
// Trả true nếu text kết thúc đột ngột (thiếu dấu câu, list chưa đóng, v.v.).
|
|
480
498
|
function isIncompleteResponse(text) {
|
|
@@ -563,6 +581,7 @@ export async function runAgent({
|
|
|
563
581
|
history,
|
|
564
582
|
model,
|
|
565
583
|
image,
|
|
584
|
+
images,
|
|
566
585
|
signal,
|
|
567
586
|
onTool,
|
|
568
587
|
onStatus,
|
|
@@ -578,9 +597,13 @@ export async function runAgent({
|
|
|
578
597
|
// [GỠ BUDGET 2026-06-06] Không còn token budget enforcement. Agent/loop/sub-agent
|
|
579
598
|
// chạy không giới hạn token. Dừng theo: GOAL đạt, <<LOOP_DONE>>, <<ULTRA_DONE>>,
|
|
580
599
|
// model tự kết thúc reply không có tool block, hoặc user Ctrl+C.
|
|
581
|
-
const recentCalls = []; // {name, inputStr} — theo dõi vòng lặp
|
|
600
|
+
const recentCalls = []; // {name, inputStr} — theo dõi vòng lặp tool call
|
|
582
601
|
let loopDetectedCount = 0; // số lần loop detection liên tiếp — reset khi model gọi tool khác
|
|
583
602
|
const MAX_LOOP_DETECTIONS = 3; // sau 3 lần loop detection liên tiếp → force stop
|
|
603
|
+
// ponytail: text-loop detection — model lặp cùng 1 câu mà không gọi tool
|
|
604
|
+
let lastTextHash = '';
|
|
605
|
+
let textRepeatCount = 0;
|
|
606
|
+
const MAX_TEXT_REPEATS = 3; // cùng text 3 lần liên tiếp → force stop
|
|
584
607
|
// Effort classifier: phân loại task từ user message gốc → set effort level.
|
|
585
608
|
// Chỉ classify 1 lần ở bước đầu, giữ nguyên suốt task (thay đổi giữa chừng gây bất ổn).
|
|
586
609
|
const effort = classifyEffort(history.find((m) => m.role === 'user')?.content || '');
|
|
@@ -616,6 +639,7 @@ export async function runAgent({
|
|
|
616
639
|
model,
|
|
617
640
|
message,
|
|
618
641
|
image,
|
|
642
|
+
images,
|
|
619
643
|
system,
|
|
620
644
|
signal,
|
|
621
645
|
tokenMeter,
|
|
@@ -654,29 +678,59 @@ export async function runAgent({
|
|
|
654
678
|
});
|
|
655
679
|
continue;
|
|
656
680
|
}
|
|
681
|
+
// ponytail: text-loop detection — model lặp cùng 1 câu không tool
|
|
682
|
+
const textHash = text.trim().slice(0, 200);
|
|
683
|
+
if (textHash === lastTextHash) {
|
|
684
|
+
textRepeatCount++;
|
|
685
|
+
if (textRepeatCount >= MAX_TEXT_REPEATS) {
|
|
686
|
+
history.push({
|
|
687
|
+
role: 'tool',
|
|
688
|
+
name: 'loop_detection',
|
|
689
|
+
content: `[TEXT LOOP × ${textRepeatCount}] Bạn vừa nói cùng 1 câu ${textRepeatCount} lần liên tiếp mà KHÔNG gọi tool. DỪNG NGAY. Gọi read_file/list_dir/grep để thực sự bắt đầu, hoặc trả lời Markdown nếu đã xong.`,
|
|
690
|
+
});
|
|
691
|
+
return `[LOOP STOPPED] Model kẹt trong vòng lặp text: "${text.slice(0, 80)}…"`;
|
|
692
|
+
}
|
|
693
|
+
history.push({
|
|
694
|
+
role: 'tool',
|
|
695
|
+
name: 'loop_detection',
|
|
696
|
+
content: `[TEXT LOOP × ${textRepeatCount}] Bạn vừa lặp lại câu trả lời giống hệt. Hãy gọi tool (read_file/grep/list_dir) hoặc trả lời Markdown khác đi. KHÔNG nói lại câu cũ.`,
|
|
697
|
+
});
|
|
698
|
+
continue; // quay lại loop, ép model gọi tool
|
|
699
|
+
}
|
|
700
|
+
lastTextHash = textHash;
|
|
701
|
+
textRepeatCount = 0;
|
|
657
702
|
// Model dừng (không tool call, không incomplete) → return để repl quyết định tiếp tục hay không
|
|
658
703
|
return text; // final answer
|
|
659
704
|
}
|
|
660
705
|
|
|
706
|
+
const inputStr = JSON.stringify(call.input || {});
|
|
707
|
+
const prev = recentCalls[recentCalls.length - 1];
|
|
708
|
+
if (prev && prev.name === call.name && prev.inputStr === inputStr) {
|
|
709
|
+
history.push({
|
|
710
|
+
role: 'tool',
|
|
711
|
+
name: 'loop_detection',
|
|
712
|
+
content: duplicateToolGuidance(call.name, call.input),
|
|
713
|
+
});
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
|
|
661
717
|
const { allow, result } = await onTool(call.name, call.input);
|
|
662
718
|
history.push({
|
|
663
719
|
role: 'tool',
|
|
664
720
|
name: call.name,
|
|
665
721
|
content: allow ? result : t.toolDenied,
|
|
666
722
|
});
|
|
723
|
+
const toolOk = allow && !String(result || '').startsWith('ERROR:');
|
|
724
|
+
const errorNudge = allow ? toolErrorGuidance(call.name, { result }) : null;
|
|
725
|
+
if (errorNudge) history.push({ role: 'user', content: errorNudge });
|
|
667
726
|
|
|
668
727
|
// ── Todo continuation nudge ──────────────────────────────────────────
|
|
669
728
|
// Sau mỗi tool result, inject nudge nếu còn task chưa xong.
|
|
670
729
|
// Dùng pendingTasks (caller gửi vào) thay vì parse output của model.
|
|
671
730
|
{
|
|
672
731
|
const tasks = pendingTasks || [];
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
history.push({
|
|
676
|
-
role: 'user',
|
|
677
|
-
content: `[SYSTEM] Việc "${call.name}" đã hoàn thành. Còn ${tasks.length} việc: ${tasks.map((t) => `"${t}"`).join(', ')}. Việc tiếp theo BẮT BUỘC phải làm ngay: "${next}". Gọi tool (write_file/edit_file/run_command) để làm việc này. KHÔNG dừng, KHÔNG tóm tắt.`,
|
|
678
|
-
});
|
|
679
|
-
}
|
|
732
|
+
const msg = todoContinuationMessage(call.name, toolOk, tasks);
|
|
733
|
+
if (msg) history.push({ role: 'user', content: msg });
|
|
680
734
|
}
|
|
681
735
|
|
|
682
736
|
// ── Loop detection ──────────────────────────────────────────────────
|
|
@@ -685,7 +739,6 @@ export async function runAgent({
|
|
|
685
739
|
// (B) Pattern vòng (A-B-A-B, A-B-C-A-B-C) — model xen kẽ 2-3 tool
|
|
686
740
|
// khác nhau để tránh phát hiện cũ. So nửa đầu vs nửa cuối window.
|
|
687
741
|
// Nếu phát hiện → inject cảnh báo. Nếu tái diễn → force stop.
|
|
688
|
-
const inputStr = JSON.stringify(call.input || {});
|
|
689
742
|
recentCalls.push({ name: call.name, inputStr });
|
|
690
743
|
if (recentCalls.length > LOOP_DETECT_WINDOW) recentCalls.shift();
|
|
691
744
|
let loopType = null; // 'consecutive' | 'pattern' | null
|
|
@@ -749,6 +802,8 @@ export async function runAgent({
|
|
|
749
802
|
});
|
|
750
803
|
} else {
|
|
751
804
|
loopDetectedCount = 0; // tool khác → reset
|
|
805
|
+
lastTextHash = ''; // text-loop: reset sau tool call thành công
|
|
806
|
+
textRepeatCount = 0;
|
|
752
807
|
}
|
|
753
808
|
}
|
|
754
809
|
return t.maxSteps;
|
|
@@ -765,6 +820,7 @@ async function streamWithRetry({
|
|
|
765
820
|
model,
|
|
766
821
|
message,
|
|
767
822
|
image,
|
|
823
|
+
images,
|
|
768
824
|
system,
|
|
769
825
|
signal,
|
|
770
826
|
tokenMeter,
|
|
@@ -781,6 +837,7 @@ async function streamWithRetry({
|
|
|
781
837
|
model,
|
|
782
838
|
message,
|
|
783
839
|
image,
|
|
840
|
+
images,
|
|
784
841
|
system,
|
|
785
842
|
signal,
|
|
786
843
|
effort,
|
package/src/api.js
CHANGED
|
@@ -158,6 +158,7 @@ export async function stream({
|
|
|
158
158
|
mode = 'chat',
|
|
159
159
|
message,
|
|
160
160
|
image,
|
|
161
|
+
images,
|
|
161
162
|
model,
|
|
162
163
|
system,
|
|
163
164
|
conversation,
|
|
@@ -188,6 +189,7 @@ export async function stream({
|
|
|
188
189
|
mode,
|
|
189
190
|
message: prompt,
|
|
190
191
|
image,
|
|
192
|
+
images,
|
|
191
193
|
model,
|
|
192
194
|
system,
|
|
193
195
|
conversation,
|
|
@@ -265,6 +267,7 @@ async function streamOnce({
|
|
|
265
267
|
mode,
|
|
266
268
|
message,
|
|
267
269
|
image,
|
|
270
|
+
images,
|
|
268
271
|
model,
|
|
269
272
|
system,
|
|
270
273
|
conversation,
|
|
@@ -281,7 +284,14 @@ async function streamOnce({
|
|
|
281
284
|
else if (mode === 'merge') body = { message };
|
|
282
285
|
else {
|
|
283
286
|
body = { message, model, remember: true, memoryToken: getMemoryToken() };
|
|
284
|
-
|
|
287
|
+
// Ảnh: chấp nhận cả `images` (mảng, multi-image) lẫn `image` (1 ảnh, cũ).
|
|
288
|
+
// Gửi `images` (mảng data URL) cho worker; giữ `image` = ảnh đầu cho
|
|
289
|
+
// tương thích ngược với worker/upstream cũ.
|
|
290
|
+
const imgList = Array.isArray(images) ? images.filter(Boolean) : image ? [image] : [];
|
|
291
|
+
if (imgList.length) {
|
|
292
|
+
body.images = imgList;
|
|
293
|
+
body.image = imgList[0];
|
|
294
|
+
}
|
|
285
295
|
if (system) body.customInstructions = system;
|
|
286
296
|
if (Array.isArray(conversation) && conversation.length) body.conversation = conversation;
|
|
287
297
|
if (effort) body.effort = effort;
|