@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 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
- A Claude-Code-style coding agent that lives in your terminal, powered by
4
- **Noob Demon** (34 models across 8 providers GPT, Claude, Gemini, DeepSeek,
5
- Grok, Qwen, Kimi, Llama).
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 hình flagship (Claude Opus 4.8, GPT-5.5,
5
+ DeepSeek V4 Pro).
6
6
 
7
- It reads and edits files, runs shell commands, and iterates on a taskasking
8
- permission before anything destructiveall inside a polished terminal UI.
7
+ đọ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 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 # optional: makes `noob` available globally
24
+ npm link # tùy chọn: cho phép gõ `noob` toàn cục
25
25
  ```
26
26
 
27
- Requires Node.js ≥ 18 (tested on 22).
27
+ Yêu cầu Node.js ≥ 18 (test trên 22).
28
28
 
29
- ## Xác thực & gói cước (Authentication)
29
+ ## Xác thực & gói cước
30
30
 
31
- noob đi qua một **gateway** (Cloudflare Worker `claude-code-proxy`) để ẩn backend
32
- thật và quản lý API key qua Supabase. Bạn cần một API key để dùng:
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 # interactive session
65
- noob "add input validation to api.js" # start with a request
66
- noob -m gateway-claude-opus-4-8 # pick a model
67
- noob --yolo # auto-approve edits & commands
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 hình
59
+ noob --yolo # tự duyệt edit & lệnh
68
60
  ```
69
61
 
70
- Without `npm link`, run `node bin/noob.js …`.
71
-
72
- ### In-session commands
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) |
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 hình (fuzzy match), hoặc liệt kê tất cả |
73
+ | `/models` | liệt mọi 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
- The Noob Demon gateway is a **stateless single-message** endpoint with no native
89
- function-calling. noob layers an agent on top:
108
+ Gateway Noob Demon một endpoint **stateless single-message** không
109
+ function-calling gốc. noob phủ một lớp agent lên trên:
90
110
 
91
- 1. The full transcript (system prompt + history + tool results) is serialized into
92
- one `message` and streamed to the gateway's `/api/chat`.
93
- 2. The model replies with either a final answer or a single \`\`\`tool JSON block.
94
- 3. noob parses the tool call, asks permission if it's destructive, executes it, and
95
- feeds the result back looping until the model answers without a tool block.
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 có rủi ro, thực thi, rồi feed kết quả
115
+ lạilặp tới khi model trả lời không kèm tool block.
96
116
 
97
- ### Architecture (high-level)
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
- ### Tools the agent can call
137
+ ### Tool agent gọi được
118
138
 
119
- `read_file` · `write_file` · `edit_file` · `list_dir` · `glob` · `grep` · `run_command`
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
- Destructive tools (`write_file`, `edit_file`, `run_command`) prompt for approval
122
- (`y` / `n` / `a`lways) unless `/yolo` is on.
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 | Effect |
127
- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
128
- | `NOOB_API_BASE` | override the gateway URL |
129
- | `NOOB_API_KEY` | API key (overrides `~/.noob/config.json`) |
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. |
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
- The agent drives tools through a text protocol (the proxy has no native
135
- function-calling). Models differ in how willingly they follow it:
164
+ Agent điều khiển tool qua một text protocol (gateway không 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 | Notes |
167
+ | Provider | Agentic tools | Ghi chú |
138
168
  | ---------------------- | ---------------- | ---------------------------------------- |
139
- | **Anthropic** (Claude) | ✅ best | default — `gateway-claude-opus-4-8` |
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 |
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
- Stick with Claude or DeepSeek for file edits & commands. Any model is fine for
145
- plain chat, `/merge`, and `/search`.
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`, `/search`.
146
175
 
147
176
  ## Project structure
148
177
 
149
178
  ```
150
179
  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)
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
- │ ├── 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)
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
- ├── tui.js # terminal UI (input, status bar, prompt rendering)
162
- ├── ui.js # color helpers, markdown renderer, banner
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 # ~/.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)
205
+ ├── skills.js # skill loader (cwd package)
168
206
  ├── 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
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 # run vitest unit tests (96 tests across 7 files)
184
- npm run lint # eslint --check
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 gate)
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 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.
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 bạn sau proxy chặn TLS (Zscaler, Cisco
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/Gemini từ chối tool"** — bình thường. GPT-o-series 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).
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
- - Free shared proxy: rate limits and occasional hiccups are expected.
216
- - Context is sent in full each turn, so very long sessions cost more tokensuse
217
- `/clear` to reset.
218
- - This is an unofficial tool built against a public community endpoint; it is not
219
- affiliated with Anthropic or the model providers.
248
+ - Proxy chia sẻ miễn phí: rate limit đô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 tokendùng `/compact`
250
+ hoặc `/clear` để gọn lại.
251
+ - Đây 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.0",
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": "npm run lint && npm run format:check && npm run test",
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
- if (tasks.length > 0) {
674
- const next = tasks[0];
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
- if (image) body.image = image;
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;