@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/src/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Client for the noob gateway (claude-code-proxy worker). The gateway validates
|
|
2
2
|
// the API key against Supabase, enforces plan limits, and hides the real
|
|
3
3
|
// upstream. The CLI only ever sees the gateway URL + the user's key.
|
|
4
|
-
import { config } from
|
|
4
|
+
import { config } from './config.js';
|
|
5
5
|
|
|
6
6
|
// ── memoryToken: per-session random token for upstream conversation state.
|
|
7
7
|
// Browser sends something like "uuid_uuid" (two v4 UUIDs joined by _).
|
|
@@ -10,9 +10,9 @@ import { config } from "./config.js";
|
|
|
10
10
|
let _sessionMemoryToken = null;
|
|
11
11
|
|
|
12
12
|
function makeUUID() {
|
|
13
|
-
return
|
|
13
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
14
14
|
const r = (Math.random() * 16) | 0;
|
|
15
|
-
const v = c ===
|
|
15
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
16
16
|
return v.toString(16);
|
|
17
17
|
});
|
|
18
18
|
}
|
|
@@ -36,26 +36,28 @@ export function resetMemoryToken() {
|
|
|
36
36
|
// nếu chỉ check ở top-level, flag --insecure-tls set sau import sẽ không kịp.
|
|
37
37
|
let _tlsWarned = false;
|
|
38
38
|
export function applyInsecureTLS() {
|
|
39
|
-
if (process.env.NOOB_INSECURE_TLS !==
|
|
40
|
-
process.env.NODE_TLS_REJECT_UNAUTHORIZED =
|
|
39
|
+
if (process.env.NOOB_INSECURE_TLS !== '1') return;
|
|
40
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
41
41
|
if (_tlsWarned) return;
|
|
42
42
|
_tlsWarned = true;
|
|
43
43
|
// Cảnh báo rõ ràng: flag này tắt verify TLS TOÀN PROCESS. MITM-able.
|
|
44
|
-
console.warn(
|
|
44
|
+
console.warn(
|
|
45
|
+
'\x1b[33m⚠ NOOB_INSECURE_TLS=1: TLS verification DISABLED for this process. MITM-vulnerable. Unset when done.\x1b[0m'
|
|
46
|
+
);
|
|
45
47
|
}
|
|
46
48
|
// Vẫn áp dụng ngay nếu env có sẵn từ shell (export NOOB_INSECURE_TLS=1).
|
|
47
49
|
applyInsecureTLS();
|
|
48
50
|
|
|
49
51
|
function authHeaders() {
|
|
50
|
-
const h = {
|
|
51
|
-
if (config.apiKey) h[
|
|
52
|
+
const h = { 'Content-Type': 'application/json' };
|
|
53
|
+
if (config.apiKey) h['Authorization'] = 'Bearer ' + config.apiKey;
|
|
52
54
|
return h;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
class ApiError extends Error {
|
|
56
58
|
constructor(message, { status, code, reset_at, plan, retryable, partial } = {}) {
|
|
57
59
|
super(message);
|
|
58
|
-
this.name =
|
|
60
|
+
this.name = 'ApiError';
|
|
59
61
|
this.status = status;
|
|
60
62
|
this.code = code;
|
|
61
63
|
this.reset_at = reset_at;
|
|
@@ -64,15 +66,15 @@ class ApiError extends Error {
|
|
|
64
66
|
// nếu lỗi 4xx auth/bad-request (retry vô nghĩa). Tự suy ra khi không truyền.
|
|
65
67
|
this.retryable = retryable ?? deriveRetryable({ status, code });
|
|
66
68
|
// partial: phần text đã nhận được trước khi lỗi (cho phép caller continue).
|
|
67
|
-
this.partial = partial ||
|
|
69
|
+
this.partial = partial || '';
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
// Phân loại lỗi gateway: 5xx + 408/429 (không phải plan limit) + timeout/network
|
|
72
74
|
// → retryable. 4xx khác (401 auth, 400 bad request, 429 plan_limit) → KHÔNG retry.
|
|
73
75
|
function deriveRetryable({ status, code }) {
|
|
74
|
-
if (code ===
|
|
75
|
-
if (code ===
|
|
76
|
+
if (code === 'timeout') return true;
|
|
77
|
+
if (code === 'plan_limit') return false;
|
|
76
78
|
if (!status) return true; // không có status = network drop / fetch throw → retry
|
|
77
79
|
if (status >= 500) return true;
|
|
78
80
|
if (status === 408 || status === 429) return true;
|
|
@@ -109,7 +111,10 @@ function cleanResponseText(text) {
|
|
|
109
111
|
cleaned = cleaned.replace(/web_search_results[\s\S]*?web_search_results_end/gi, '');
|
|
110
112
|
cleaned = cleaned.replace(/web_search_summary[\s\S]*?web_search_summary_end/gi, '');
|
|
111
113
|
// Markdown headings: ## Web Search Results / ## Web Search Summary (with content until next heading)
|
|
112
|
-
cleaned = cleaned.replace(
|
|
114
|
+
cleaned = cleaned.replace(
|
|
115
|
+
/^#{1,3}\s+Web\s+Search\s+(Results|Summary)\s*[\s\S]*?(?=^#{1,3}|\n# |$)/gim,
|
|
116
|
+
''
|
|
117
|
+
);
|
|
113
118
|
return cleaned.trim();
|
|
114
119
|
}
|
|
115
120
|
|
|
@@ -137,58 +142,98 @@ function hasUnclosedToolBlock(text) {
|
|
|
137
142
|
*
|
|
138
143
|
* @returns {Promise<{text:string, reasoning:string}>}
|
|
139
144
|
*/
|
|
140
|
-
export async function stream({
|
|
141
|
-
|
|
145
|
+
export async function stream({
|
|
146
|
+
mode = 'chat',
|
|
147
|
+
message,
|
|
148
|
+
model,
|
|
149
|
+
system,
|
|
150
|
+
conversation,
|
|
151
|
+
effort,
|
|
152
|
+
signal,
|
|
153
|
+
onDelta,
|
|
154
|
+
onReasoning,
|
|
155
|
+
onStatus,
|
|
156
|
+
maxContinues = Infinity,
|
|
157
|
+
}) {
|
|
158
|
+
const endpoint =
|
|
159
|
+
mode === 'search' ? '/api/search' : mode === 'merge' ? '/api/merge' : '/api/chat';
|
|
142
160
|
|
|
143
|
-
let fullText =
|
|
144
|
-
let reasoning =
|
|
161
|
+
let fullText = '';
|
|
162
|
+
let reasoning = '';
|
|
145
163
|
let prompt = message; // prompt gửi đi: lần đầu = nguyên bản, các lần sau = nối tiếp
|
|
146
|
-
let lastFinishReason =
|
|
164
|
+
let lastFinishReason = 'stop'; // stop | truncated | tool_unclosed | empty | network_drop
|
|
147
165
|
let emptyStreak = 0; // số lần liên tiếp stream rỗng (chống loop vô tận khi upstream chết hẳn)
|
|
148
166
|
|
|
149
167
|
for (let attempt = 0; ; attempt++) {
|
|
150
|
-
const r = await streamOnce({
|
|
151
|
-
|
|
168
|
+
const r = await streamOnce({
|
|
169
|
+
endpoint,
|
|
170
|
+
mode,
|
|
171
|
+
message: prompt,
|
|
172
|
+
model,
|
|
173
|
+
system,
|
|
174
|
+
conversation,
|
|
175
|
+
effort,
|
|
176
|
+
signal,
|
|
177
|
+
onStatus,
|
|
178
|
+
onDelta,
|
|
179
|
+
onReasoning,
|
|
180
|
+
});
|
|
181
|
+
fullText = mode === 'chat' ? fullText + r.text : r.text; // chat: ghép các đoạn nối tiếp; mode khác: thay thế
|
|
152
182
|
if (r.reasoning) reasoning = r.reasoning;
|
|
153
183
|
|
|
154
184
|
// Phát hiện thêm: tool block mở mà chưa đóng → coi như bị cắt dù gateway báo done.
|
|
155
|
-
const toolUnclosed = mode ===
|
|
185
|
+
const toolUnclosed = mode === 'chat' && hasUnclosedToolBlock(fullText);
|
|
156
186
|
const truncated = r.truncated || toolUnclosed;
|
|
157
187
|
|
|
158
|
-
if (!truncated) {
|
|
159
|
-
|
|
188
|
+
if (!truncated) {
|
|
189
|
+
lastFinishReason = 'stop';
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
if (mode !== 'chat') {
|
|
193
|
+
lastFinishReason = 'truncated';
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
160
196
|
|
|
161
197
|
// Đếm chuỗi rỗng: nếu 3 lần liên tiếp model trả rỗng → upstream chết hẳn, dừng.
|
|
162
198
|
if (!r.text.trim()) {
|
|
163
199
|
emptyStreak++;
|
|
164
|
-
if (emptyStreak >= 3) {
|
|
200
|
+
if (emptyStreak >= 3) {
|
|
201
|
+
lastFinishReason = 'empty';
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
165
204
|
} else {
|
|
166
205
|
emptyStreak = 0;
|
|
167
206
|
}
|
|
168
207
|
|
|
169
208
|
if (attempt >= maxContinues) {
|
|
170
|
-
lastFinishReason = toolUnclosed ?
|
|
209
|
+
lastFinishReason = toolUnclosed ? 'tool_unclosed' : 'truncated';
|
|
171
210
|
break;
|
|
172
211
|
}
|
|
173
212
|
prompt = continuationPrompt(message, fullText);
|
|
174
213
|
}
|
|
175
214
|
|
|
176
|
-
return {
|
|
215
|
+
return {
|
|
216
|
+
text: cleanResponseText(fullText.trim()),
|
|
217
|
+
reasoning: reasoning.trim(),
|
|
218
|
+
finishReason: lastFinishReason,
|
|
219
|
+
};
|
|
177
220
|
}
|
|
178
221
|
|
|
179
222
|
// Dựng prompt "nối tiếp" khi câu trả lời bị cắt giữa chừng: gửi lại nguyên ngữ
|
|
180
223
|
// cảnh gốc + phần model đã viết dở + yêu cầu viết TIẾP đúng chỗ dừng, không lặp.
|
|
181
224
|
function continuationPrompt(message, partial) {
|
|
182
|
-
const bar =
|
|
225
|
+
const bar = '='.repeat(60);
|
|
183
226
|
return (
|
|
184
227
|
message +
|
|
185
|
-
|
|
186
|
-
|
|
228
|
+
'\n\n' +
|
|
229
|
+
bar +
|
|
230
|
+
'\n## ASSISTANT (bị ngắt giữa chừng — phần trả lời dưới đây CHƯA hoàn tất)\n' +
|
|
187
231
|
partial +
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
232
|
+
'\n\n' +
|
|
233
|
+
bar +
|
|
234
|
+
'\n# SYSTEM: Phần trả lời ngay trên bị mạng/timeout cắt ngang trước khi xong. ' +
|
|
235
|
+
'Hãy VIẾT TIẾP liền mạch từ ĐÚNG ký tự cuối cùng ở trên — KHÔNG lặp lại hay diễn đạt lại bất kỳ chữ nào đã hiện, KHÔNG mở đầu lại, KHÔNG thêm lời dẫn. ' +
|
|
236
|
+
'Chỉ xuất phần CÒN LẠI. Nếu đang viết dở một khối tool thì hoàn tất đúng khối đó. Nếu thật ra đã xong, chỉ xuất một dấu cách rồi dừng.'
|
|
192
237
|
);
|
|
193
238
|
}
|
|
194
239
|
|
|
@@ -196,12 +241,24 @@ function continuationPrompt(message, partial) {
|
|
|
196
241
|
* One network attempt of the stream. Returns this attempt's accumulated text +
|
|
197
242
|
* a `truncated` flag telling the caller whether the reply was cut short.
|
|
198
243
|
*/
|
|
199
|
-
async function streamOnce({
|
|
244
|
+
async function streamOnce({
|
|
245
|
+
endpoint,
|
|
246
|
+
mode,
|
|
247
|
+
message,
|
|
248
|
+
model,
|
|
249
|
+
system,
|
|
250
|
+
conversation,
|
|
251
|
+
effort,
|
|
252
|
+
signal,
|
|
253
|
+
onStatus,
|
|
254
|
+
onDelta,
|
|
255
|
+
onReasoning,
|
|
256
|
+
}) {
|
|
200
257
|
// chat body: gửi system + conversation riêng để gateway forward đúng tới upstream.
|
|
201
258
|
// Worker gateway (handleChat) + upstream đều nhận shape này.
|
|
202
259
|
let body;
|
|
203
|
-
if (mode ===
|
|
204
|
-
else if (mode ===
|
|
260
|
+
if (mode === 'search') body = { query: message };
|
|
261
|
+
else if (mode === 'merge') body = { message };
|
|
205
262
|
else {
|
|
206
263
|
body = { message, model, remember: true, memoryToken: getMemoryToken() };
|
|
207
264
|
if (system) body.customInstructions = system;
|
|
@@ -211,17 +268,17 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
211
268
|
|
|
212
269
|
const ctrl = new AbortController();
|
|
213
270
|
const onUserAbort = () => ctrl.abort();
|
|
214
|
-
signal?.addEventListener(
|
|
271
|
+
signal?.addEventListener('abort', onUserAbort, { once: true });
|
|
215
272
|
|
|
216
|
-
let text =
|
|
217
|
-
let reasoning =
|
|
273
|
+
let text = '';
|
|
274
|
+
let reasoning = '';
|
|
218
275
|
let sawDone = false; // thấy {done} = stream kết thúc tử tế (không bị cắt)
|
|
219
276
|
let truncated = false; // gateway báo upstream bị cắt giữa chừng
|
|
220
277
|
|
|
221
278
|
// Một dòng SSE → cập nhật text/reasoning. Tách ra để dùng lại khi flush dòng cuối.
|
|
222
279
|
const processLine = (rawLine) => {
|
|
223
280
|
const line = rawLine.trim();
|
|
224
|
-
if (!line.startsWith(
|
|
281
|
+
if (!line.startsWith('data:')) return;
|
|
225
282
|
const data = line.slice(5).trim();
|
|
226
283
|
if (!data) return;
|
|
227
284
|
let p;
|
|
@@ -243,14 +300,14 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
243
300
|
if (p.truncated) truncated = true;
|
|
244
301
|
// Handle {finish: true, reason: "stop"} — unlimited.surf & some proxies
|
|
245
302
|
// send this as completion signal instead of (or in addition to) {done: true}
|
|
246
|
-
if (p.finish === true && p.reason ===
|
|
303
|
+
if (p.finish === true && p.reason === 'stop') sawDone = true;
|
|
247
304
|
if (p.done) sawDone = true;
|
|
248
305
|
if (p.error) throw new ApiError(p.error, {});
|
|
249
306
|
};
|
|
250
307
|
|
|
251
308
|
try {
|
|
252
309
|
const resp = await fetch(config.gatewayUrl + endpoint, {
|
|
253
|
-
method:
|
|
310
|
+
method: 'POST',
|
|
254
311
|
headers: authHeaders(),
|
|
255
312
|
body: JSON.stringify(body),
|
|
256
313
|
signal: ctrl.signal,
|
|
@@ -259,13 +316,13 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
259
316
|
|
|
260
317
|
const reader = resp.body.getReader();
|
|
261
318
|
const decoder = new TextDecoder();
|
|
262
|
-
let buf =
|
|
319
|
+
let buf = '';
|
|
263
320
|
while (true) {
|
|
264
321
|
const { done, value } = await reader.read();
|
|
265
322
|
if (done) break;
|
|
266
323
|
buf += decoder.decode(value, { stream: true });
|
|
267
324
|
let nl;
|
|
268
|
-
while ((nl = buf.indexOf(
|
|
325
|
+
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
269
326
|
processLine(buf.slice(0, nl));
|
|
270
327
|
buf = buf.slice(nl + 1);
|
|
271
328
|
}
|
|
@@ -275,23 +332,23 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
275
332
|
|
|
276
333
|
// Chat: gateway gửi {done} khi xong sạch. Stream EOF mà chưa thấy {done} dù
|
|
277
334
|
// đã có chữ → kết nối/edge rớt giữa chừng → coi như bị cắt để nối tiếp.
|
|
278
|
-
if (mode ===
|
|
335
|
+
if (mode === 'chat' && !sawDone && text) truncated = true;
|
|
279
336
|
|
|
280
337
|
return { text, reasoning, truncated };
|
|
281
338
|
} catch (err) {
|
|
282
339
|
if (signal?.aborted) throw err; // người dùng bấm Ctrl+C → huỷ thật, không nối tiếp
|
|
283
340
|
// Rớt mạng giữa chừng (không phải huỷ): với chat, nếu đã có
|
|
284
341
|
// chữ thì trả phần đã nhận + cờ truncated để lớp trên nối tiếp.
|
|
285
|
-
if (mode ===
|
|
342
|
+
if (mode === 'chat' && text) return { text, reasoning, truncated: true };
|
|
286
343
|
throw err;
|
|
287
344
|
} finally {
|
|
288
|
-
signal?.removeEventListener(
|
|
345
|
+
signal?.removeEventListener('abort', onUserAbort);
|
|
289
346
|
}
|
|
290
347
|
}
|
|
291
348
|
|
|
292
349
|
/** Fetch the current key's quota/usage from the gateway (no request consumed). */
|
|
293
350
|
export async function usage() {
|
|
294
|
-
const resp = await fetch(config.gatewayUrl +
|
|
351
|
+
const resp = await fetch(config.gatewayUrl + '/api/usage', { headers: authHeaders() });
|
|
295
352
|
if (!resp.ok) throw await parseError(resp);
|
|
296
353
|
return await resp.json();
|
|
297
354
|
}
|
package/src/config.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import os from
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
4
|
|
|
5
5
|
// Default gateway — the Noob Demon endpoint. The real upstream is hidden behind
|
|
6
6
|
// it; the CLI only ever talks to this gateway.
|
|
7
|
-
const DEFAULT_GATEWAY =
|
|
7
|
+
const DEFAULT_GATEWAY = 'https://claude-code-proxy.lohieuky678.workers.dev';
|
|
8
8
|
|
|
9
|
-
const DIR = path.join(os.homedir(),
|
|
10
|
-
const FILE = path.join(DIR,
|
|
9
|
+
const DIR = path.join(os.homedir(), '.noob');
|
|
10
|
+
const FILE = path.join(DIR, 'config.json');
|
|
11
11
|
|
|
12
12
|
function read() {
|
|
13
13
|
try {
|
|
14
|
-
return JSON.parse(fs.readFileSync(FILE,
|
|
14
|
+
return JSON.parse(fs.readFileSync(FILE, 'utf8'));
|
|
15
15
|
} catch {
|
|
16
16
|
return {};
|
|
17
17
|
}
|
|
@@ -20,24 +20,24 @@ function read() {
|
|
|
20
20
|
function write(cfg) {
|
|
21
21
|
try {
|
|
22
22
|
fs.mkdirSync(DIR, { recursive: true });
|
|
23
|
-
fs.writeFileSync(FILE, JSON.stringify(cfg, null, 2),
|
|
23
|
+
fs.writeFileSync(FILE, JSON.stringify(cfg, null, 2), 'utf8');
|
|
24
24
|
return true;
|
|
25
25
|
} catch {
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
const cache = read();
|
|
31
31
|
|
|
32
32
|
export const config = {
|
|
33
33
|
get gatewayUrl() {
|
|
34
34
|
return process.env.NOOB_API_BASE || cache.gatewayUrl || DEFAULT_GATEWAY;
|
|
35
35
|
},
|
|
36
36
|
get apiKey() {
|
|
37
|
-
return process.env.NOOB_API_KEY || cache.apiKey ||
|
|
37
|
+
return process.env.NOOB_API_KEY || cache.apiKey || '';
|
|
38
38
|
},
|
|
39
39
|
get model() {
|
|
40
|
-
return cache.model ||
|
|
40
|
+
return cache.model || '';
|
|
41
41
|
},
|
|
42
42
|
setKey(key) {
|
|
43
43
|
cache.apiKey = key;
|