@noobdemon/noob-cli 1.10.20 → 1.11.1
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 +476 -0
- package/README.md +113 -27
- package/bin/noob.js +40 -27
- package/package.json +30 -2
- package/src/agent.js +213 -124
- package/src/api.js +126 -52
- 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 +211 -122
- 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,30 @@ 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
|
-
//
|
|
47
|
-
|
|
48
|
+
// KHÔNG gọi applyInsecureTLS() ở top-level: vì ESM hoist, lệnh này sẽ chạy
|
|
49
|
+
// TRƯỚC khi bin/noob.js kịp parse `--insecure-tls` và set env. bin/noob.js đã
|
|
50
|
+
// gọi applyInsecureTLS() sau khi parse argv — đó là điểm duy nhất.
|
|
51
|
+
// Nếu env NOOB_INSECURE_TLS=1 đã có sẵn từ shell, bin/noob.js cũng sẽ áp dụng.
|
|
48
52
|
|
|
49
53
|
function authHeaders() {
|
|
50
|
-
const h = {
|
|
51
|
-
if (config.apiKey) h[
|
|
54
|
+
const h = { 'Content-Type': 'application/json' };
|
|
55
|
+
if (config.apiKey) h['Authorization'] = 'Bearer ' + config.apiKey;
|
|
52
56
|
return h;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
class ApiError extends Error {
|
|
56
60
|
constructor(message, { status, code, reset_at, plan, retryable, partial } = {}) {
|
|
57
61
|
super(message);
|
|
58
|
-
this.name =
|
|
62
|
+
this.name = 'ApiError';
|
|
59
63
|
this.status = status;
|
|
60
64
|
this.code = code;
|
|
61
65
|
this.reset_at = reset_at;
|
|
@@ -64,15 +68,15 @@ class ApiError extends Error {
|
|
|
64
68
|
// nếu lỗi 4xx auth/bad-request (retry vô nghĩa). Tự suy ra khi không truyền.
|
|
65
69
|
this.retryable = retryable ?? deriveRetryable({ status, code });
|
|
66
70
|
// partial: phần text đã nhận được trước khi lỗi (cho phép caller continue).
|
|
67
|
-
this.partial = partial ||
|
|
71
|
+
this.partial = partial || '';
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
// Phân loại lỗi gateway: 5xx + 408/429 (không phải plan limit) + timeout/network
|
|
72
76
|
// → retryable. 4xx khác (401 auth, 400 bad request, 429 plan_limit) → KHÔNG retry.
|
|
73
77
|
function deriveRetryable({ status, code }) {
|
|
74
|
-
if (code ===
|
|
75
|
-
if (code ===
|
|
78
|
+
if (code === 'timeout') return true;
|
|
79
|
+
if (code === 'plan_limit') return false;
|
|
76
80
|
if (!status) return true; // không có status = network drop / fetch throw → retry
|
|
77
81
|
if (status >= 500) return true;
|
|
78
82
|
if (status === 408 || status === 429) return true;
|
|
@@ -96,7 +100,13 @@ async function parseError(resp) {
|
|
|
96
100
|
// Unlimited.surf & similar proxies inject web search results as XML/markdown blocks
|
|
97
101
|
// into the SSE stream. These get appended as regular text and confuse the AI model
|
|
98
102
|
// when seen in subsequent turns (it thinks they are prompt injection attempts).
|
|
99
|
-
|
|
103
|
+
//
|
|
104
|
+
// QUAN TRỌNG: chỉ chạy stripping markdown-heading khi mode === 'search'. Với
|
|
105
|
+
// chat/merge, người dùng có thể đang viết về chính chủ đề "Web Search Results"
|
|
106
|
+
// (vd nội dung SEO/content) — regex tham sẽ nuốt mất câu trả lời thật.
|
|
107
|
+
// Các tag XML/bracket/plain-marker thì hiếm khi xuất hiện tự nhiên nên vẫn an
|
|
108
|
+
// toàn để strip ở mọi mode.
|
|
109
|
+
function cleanResponseText(text, mode = 'chat') {
|
|
100
110
|
if (!text) return text;
|
|
101
111
|
let cleaned = text;
|
|
102
112
|
// XML/SGML style: <web_search_results>...</web_search_results>
|
|
@@ -108,8 +118,15 @@ function cleanResponseText(text) {
|
|
|
108
118
|
// Plain text markers: web_search_results ... web_search_results_end
|
|
109
119
|
cleaned = cleaned.replace(/web_search_results[\s\S]*?web_search_results_end/gi, '');
|
|
110
120
|
cleaned = cleaned.replace(/web_search_summary[\s\S]*?web_search_summary_end/gi, '');
|
|
111
|
-
// Markdown headings:
|
|
112
|
-
|
|
121
|
+
// Markdown headings: chỉ áp dụng cho search mode (nguồn duy nhất bị inject
|
|
122
|
+
// dạng heading bởi proxy). Lookahead bám sát: dừng tại heading kế tiếp HOẶC
|
|
123
|
+
// 2 dòng trống liên tiếp (đoạn văn mới) — tránh nuốt phần còn lại của câu trả lời.
|
|
124
|
+
if (mode === 'search') {
|
|
125
|
+
cleaned = cleaned.replace(
|
|
126
|
+
/^#{1,3}\s+Web\s+Search\s+(Results|Summary)\b[^\n]*\n[\s\S]*?(?=^#{1,3}\s|\n\n\n|$)/gim,
|
|
127
|
+
''
|
|
128
|
+
);
|
|
129
|
+
}
|
|
113
130
|
return cleaned.trim();
|
|
114
131
|
}
|
|
115
132
|
|
|
@@ -137,58 +154,98 @@ function hasUnclosedToolBlock(text) {
|
|
|
137
154
|
*
|
|
138
155
|
* @returns {Promise<{text:string, reasoning:string}>}
|
|
139
156
|
*/
|
|
140
|
-
export async function stream({
|
|
141
|
-
|
|
157
|
+
export async function stream({
|
|
158
|
+
mode = 'chat',
|
|
159
|
+
message,
|
|
160
|
+
model,
|
|
161
|
+
system,
|
|
162
|
+
conversation,
|
|
163
|
+
effort,
|
|
164
|
+
signal,
|
|
165
|
+
onDelta,
|
|
166
|
+
onReasoning,
|
|
167
|
+
onStatus,
|
|
168
|
+
maxContinues = Infinity,
|
|
169
|
+
}) {
|
|
170
|
+
const endpoint =
|
|
171
|
+
mode === 'search' ? '/api/search' : mode === 'merge' ? '/api/merge' : '/api/chat';
|
|
142
172
|
|
|
143
|
-
let fullText =
|
|
144
|
-
let reasoning =
|
|
173
|
+
let fullText = '';
|
|
174
|
+
let reasoning = '';
|
|
145
175
|
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 =
|
|
176
|
+
let lastFinishReason = 'stop'; // stop | truncated | tool_unclosed | empty | network_drop
|
|
147
177
|
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
178
|
|
|
149
179
|
for (let attempt = 0; ; attempt++) {
|
|
150
|
-
const r = await streamOnce({
|
|
151
|
-
|
|
180
|
+
const r = await streamOnce({
|
|
181
|
+
endpoint,
|
|
182
|
+
mode,
|
|
183
|
+
message: prompt,
|
|
184
|
+
model,
|
|
185
|
+
system,
|
|
186
|
+
conversation,
|
|
187
|
+
effort,
|
|
188
|
+
signal,
|
|
189
|
+
onStatus,
|
|
190
|
+
onDelta,
|
|
191
|
+
onReasoning,
|
|
192
|
+
});
|
|
193
|
+
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
194
|
if (r.reasoning) reasoning = r.reasoning;
|
|
153
195
|
|
|
154
196
|
// 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 ===
|
|
197
|
+
const toolUnclosed = mode === 'chat' && hasUnclosedToolBlock(fullText);
|
|
156
198
|
const truncated = r.truncated || toolUnclosed;
|
|
157
199
|
|
|
158
|
-
if (!truncated) {
|
|
159
|
-
|
|
200
|
+
if (!truncated) {
|
|
201
|
+
lastFinishReason = 'stop';
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
if (mode !== 'chat') {
|
|
205
|
+
lastFinishReason = 'truncated';
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
160
208
|
|
|
161
209
|
// Đế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
210
|
if (!r.text.trim()) {
|
|
163
211
|
emptyStreak++;
|
|
164
|
-
if (emptyStreak >= 3) {
|
|
212
|
+
if (emptyStreak >= 3) {
|
|
213
|
+
lastFinishReason = 'empty';
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
165
216
|
} else {
|
|
166
217
|
emptyStreak = 0;
|
|
167
218
|
}
|
|
168
219
|
|
|
169
220
|
if (attempt >= maxContinues) {
|
|
170
|
-
lastFinishReason = toolUnclosed ?
|
|
221
|
+
lastFinishReason = toolUnclosed ? 'tool_unclosed' : 'truncated';
|
|
171
222
|
break;
|
|
172
223
|
}
|
|
173
224
|
prompt = continuationPrompt(message, fullText);
|
|
174
225
|
}
|
|
175
226
|
|
|
176
|
-
return {
|
|
227
|
+
return {
|
|
228
|
+
text: cleanResponseText(fullText.trim(), mode),
|
|
229
|
+
reasoning: reasoning.trim(),
|
|
230
|
+
finishReason: lastFinishReason,
|
|
231
|
+
};
|
|
177
232
|
}
|
|
178
233
|
|
|
179
234
|
// 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
235
|
// 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
236
|
function continuationPrompt(message, partial) {
|
|
182
|
-
const bar =
|
|
237
|
+
const bar = '='.repeat(60);
|
|
183
238
|
return (
|
|
184
239
|
message +
|
|
185
|
-
|
|
186
|
-
|
|
240
|
+
'\n\n' +
|
|
241
|
+
bar +
|
|
242
|
+
'\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
243
|
partial +
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
244
|
+
'\n\n' +
|
|
245
|
+
bar +
|
|
246
|
+
'\n# SYSTEM: Phần trả lời ngay trên bị mạng/timeout cắt ngang trước khi xong. ' +
|
|
247
|
+
'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. ' +
|
|
248
|
+
'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
249
|
);
|
|
193
250
|
}
|
|
194
251
|
|
|
@@ -196,12 +253,24 @@ function continuationPrompt(message, partial) {
|
|
|
196
253
|
* One network attempt of the stream. Returns this attempt's accumulated text +
|
|
197
254
|
* a `truncated` flag telling the caller whether the reply was cut short.
|
|
198
255
|
*/
|
|
199
|
-
async function streamOnce({
|
|
256
|
+
async function streamOnce({
|
|
257
|
+
endpoint,
|
|
258
|
+
mode,
|
|
259
|
+
message,
|
|
260
|
+
model,
|
|
261
|
+
system,
|
|
262
|
+
conversation,
|
|
263
|
+
effort,
|
|
264
|
+
signal,
|
|
265
|
+
onStatus,
|
|
266
|
+
onDelta,
|
|
267
|
+
onReasoning,
|
|
268
|
+
}) {
|
|
200
269
|
// chat body: gửi system + conversation riêng để gateway forward đúng tới upstream.
|
|
201
270
|
// Worker gateway (handleChat) + upstream đều nhận shape này.
|
|
202
271
|
let body;
|
|
203
|
-
if (mode ===
|
|
204
|
-
else if (mode ===
|
|
272
|
+
if (mode === 'search') body = { query: message };
|
|
273
|
+
else if (mode === 'merge') body = { message };
|
|
205
274
|
else {
|
|
206
275
|
body = { message, model, remember: true, memoryToken: getMemoryToken() };
|
|
207
276
|
if (system) body.customInstructions = system;
|
|
@@ -211,17 +280,17 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
211
280
|
|
|
212
281
|
const ctrl = new AbortController();
|
|
213
282
|
const onUserAbort = () => ctrl.abort();
|
|
214
|
-
signal?.addEventListener(
|
|
283
|
+
signal?.addEventListener('abort', onUserAbort, { once: true });
|
|
215
284
|
|
|
216
|
-
let text =
|
|
217
|
-
let reasoning =
|
|
285
|
+
let text = '';
|
|
286
|
+
let reasoning = '';
|
|
218
287
|
let sawDone = false; // thấy {done} = stream kết thúc tử tế (không bị cắt)
|
|
219
288
|
let truncated = false; // gateway báo upstream bị cắt giữa chừng
|
|
220
289
|
|
|
221
290
|
// 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
291
|
const processLine = (rawLine) => {
|
|
223
292
|
const line = rawLine.trim();
|
|
224
|
-
if (!line.startsWith(
|
|
293
|
+
if (!line.startsWith('data:')) return;
|
|
225
294
|
const data = line.slice(5).trim();
|
|
226
295
|
if (!data) return;
|
|
227
296
|
let p;
|
|
@@ -243,14 +312,14 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
243
312
|
if (p.truncated) truncated = true;
|
|
244
313
|
// Handle {finish: true, reason: "stop"} — unlimited.surf & some proxies
|
|
245
314
|
// send this as completion signal instead of (or in addition to) {done: true}
|
|
246
|
-
if (p.finish === true && p.reason ===
|
|
315
|
+
if (p.finish === true && p.reason === 'stop') sawDone = true;
|
|
247
316
|
if (p.done) sawDone = true;
|
|
248
317
|
if (p.error) throw new ApiError(p.error, {});
|
|
249
318
|
};
|
|
250
319
|
|
|
251
320
|
try {
|
|
252
321
|
const resp = await fetch(config.gatewayUrl + endpoint, {
|
|
253
|
-
method:
|
|
322
|
+
method: 'POST',
|
|
254
323
|
headers: authHeaders(),
|
|
255
324
|
body: JSON.stringify(body),
|
|
256
325
|
signal: ctrl.signal,
|
|
@@ -259,13 +328,18 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
259
328
|
|
|
260
329
|
const reader = resp.body.getReader();
|
|
261
330
|
const decoder = new TextDecoder();
|
|
262
|
-
let buf =
|
|
331
|
+
let buf = '';
|
|
263
332
|
while (true) {
|
|
264
333
|
const { done, value } = await reader.read();
|
|
265
334
|
if (done) break;
|
|
266
335
|
buf += decoder.decode(value, { stream: true });
|
|
336
|
+
// Chuẩn hoá CRLF→LF: một số reverse proxy (Cloudflare, nginx config khác)
|
|
337
|
+
// gửi line endings \r\n. processLine() có trim() nên \r trailing tự rụng,
|
|
338
|
+
// nhưng nếu chunk biên giới rơi đúng giữa \r và \n thì vẫn ổn — đây là
|
|
339
|
+
// safety belt cho trường hợp \r đơn lẻ (rất hiếm nhưng có thấy thực tế).
|
|
340
|
+
buf = buf.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
267
341
|
let nl;
|
|
268
|
-
while ((nl = buf.indexOf(
|
|
342
|
+
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
269
343
|
processLine(buf.slice(0, nl));
|
|
270
344
|
buf = buf.slice(nl + 1);
|
|
271
345
|
}
|
|
@@ -275,23 +349,23 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
275
349
|
|
|
276
350
|
// Chat: gateway gửi {done} khi xong sạch. Stream EOF mà chưa thấy {done} dù
|
|
277
351
|
// đã 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 ===
|
|
352
|
+
if (mode === 'chat' && !sawDone && text) truncated = true;
|
|
279
353
|
|
|
280
354
|
return { text, reasoning, truncated };
|
|
281
355
|
} catch (err) {
|
|
282
356
|
if (signal?.aborted) throw err; // người dùng bấm Ctrl+C → huỷ thật, không nối tiếp
|
|
283
357
|
// Rớt mạng giữa chừng (không phải huỷ): với chat, nếu đã có
|
|
284
358
|
// chữ thì trả phần đã nhận + cờ truncated để lớp trên nối tiếp.
|
|
285
|
-
if (mode ===
|
|
359
|
+
if (mode === 'chat' && text) return { text, reasoning, truncated: true };
|
|
286
360
|
throw err;
|
|
287
361
|
} finally {
|
|
288
|
-
signal?.removeEventListener(
|
|
362
|
+
signal?.removeEventListener('abort', onUserAbort);
|
|
289
363
|
}
|
|
290
364
|
}
|
|
291
365
|
|
|
292
366
|
/** Fetch the current key's quota/usage from the gateway (no request consumed). */
|
|
293
367
|
export async function usage() {
|
|
294
|
-
const resp = await fetch(config.gatewayUrl +
|
|
368
|
+
const resp = await fetch(config.gatewayUrl + '/api/usage', { headers: authHeaders() });
|
|
295
369
|
if (!resp.ok) throw await parseError(resp);
|
|
296
370
|
return await resp.json();
|
|
297
371
|
}
|
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;
|