@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/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 "./config.js";
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 "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
13
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
14
14
  const r = (Math.random() * 16) | 0;
15
- const v = c === "x" ? r : (r & 0x3) | 0x8;
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 !== "1") return;
40
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
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("\x1b[33m⚠ NOOB_INSECURE_TLS=1: TLS verification DISABLED for this process. MITM-vulnerable. Unset when done.\x1b[0m");
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 = { "Content-Type": "application/json" };
51
- if (config.apiKey) h["Authorization"] = "Bearer " + config.apiKey;
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 = "ApiError";
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 === "timeout") return true;
75
- if (code === "plan_limit") return false;
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(/^#{1,3}\s+Web\s+Search\s+(Results|Summary)\s*[\s\S]*?(?=^#{1,3}|\n# |$)/gim, '');
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({ mode = "chat", message, model, system, conversation, effort, signal, onDelta, onReasoning, onStatus, maxContinues = Infinity }) {
141
- const endpoint = mode === "search" ? "/api/search" : mode === "merge" ? "/api/merge" : "/api/chat";
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 = "stop"; // stop | truncated | tool_unclosed | empty | network_drop
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({ endpoint, mode, message: prompt, model, system, conversation, effort, signal, onStatus, onDelta, onReasoning });
151
- fullText = mode === "chat" ? fullText + r.text : r.text; // chat: ghép các đoạn nối tiếp; mode khác: thay thế
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 === "chat" && hasUnclosedToolBlock(fullText);
185
+ const toolUnclosed = mode === 'chat' && hasUnclosedToolBlock(fullText);
156
186
  const truncated = r.truncated || toolUnclosed;
157
187
 
158
- if (!truncated) { lastFinishReason = "stop"; break; }
159
- if (mode !== "chat") { lastFinishReason = "truncated"; break; }
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) { lastFinishReason = "empty"; break; }
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 ? "tool_unclosed" : "truncated";
209
+ lastFinishReason = toolUnclosed ? 'tool_unclosed' : 'truncated';
171
210
  break;
172
211
  }
173
212
  prompt = continuationPrompt(message, fullText);
174
213
  }
175
214
 
176
- return { text: cleanResponseText(fullText.trim()), reasoning: reasoning.trim(), finishReason: lastFinishReason };
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 = "=".repeat(60);
225
+ const bar = '='.repeat(60);
183
226
  return (
184
227
  message +
185
- "\n\n" + bar +
186
- "\n## ASSISTANT (bị ngắt giữa chừng — phần trả lời dưới đây CHƯA hoàn tất)\n" +
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
- "\n\n" + bar +
189
- "\n# SYSTEM: Phần trả lời ngay trên bị mạng/timeout cắt ngang trước khi xong. " +
190
- "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. " +
191
- "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."
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 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({ endpoint, mode, message, model, system, conversation, effort, signal, onStatus, onDelta, onReasoning }) {
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 === "search") body = { query: message };
204
- else if (mode === "merge") body = { message };
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("abort", onUserAbort, { once: true });
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("data:")) return;
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 === "stop") sawDone = true;
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: "POST",
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("\n")) !== -1) {
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 === "chat" && !sawDone && text) truncated = true;
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 === "chat" && text) return { text, reasoning, truncated: true };
342
+ if (mode === 'chat' && text) return { text, reasoning, truncated: true };
286
343
  throw err;
287
344
  } finally {
288
- signal?.removeEventListener("abort", onUserAbort);
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 + "/api/usage", { headers: authHeaders() });
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 "node:fs";
2
- import path from "node:path";
3
- import os from "node:os";
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 = "https://claude-code-proxy.lohieuky678.workers.dev";
7
+ const DEFAULT_GATEWAY = 'https://claude-code-proxy.lohieuky678.workers.dev';
8
8
 
9
- const DIR = path.join(os.homedir(), ".noob");
10
- const FILE = path.join(DIR, "config.json");
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, "utf8"));
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), "utf8");
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
- let cache = read();
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;