@tiens.nguyen/gonext-local-worker 1.0.343 → 1.0.347
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/gonext-repl.mjs +79 -1
- package/model-doctor.mjs +231 -2
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -377,6 +377,9 @@ function slashRestoreHistory() {
|
|
|
377
377
|
// which the earlier relative-cursor overlay hit whenever readline refreshed or the
|
|
378
378
|
// menu was near the bottom of the screen and scrolled).
|
|
379
379
|
let slashOverlayActive = false;
|
|
380
|
+
// While true, readline's echo + line-refresh are muted so a typed secret (an API key) never
|
|
381
|
+
// reaches the terminal or scrollback (task #127 — see askSecret + the _writeToOutput guards).
|
|
382
|
+
let secretInput = false;
|
|
380
383
|
|
|
381
384
|
// Visible width of PROMPT (">> ") — used to put the caret back on the input line by
|
|
382
385
|
// COLUMN, since the string itself carries color escapes that don't occupy cells.
|
|
@@ -608,6 +611,20 @@ const ask = async (q) => {
|
|
|
608
611
|
// re-uses the default. This replaces a fragile `(await ask()) || "y"` idiom where any
|
|
609
612
|
// leftover character (a stray escape byte, a stray space) silently defeated the
|
|
610
613
|
// default — the reported "[Y/n] + Enter registered as No" bug.
|
|
614
|
+
// Masked prompt for a secret (an API key). Mutes readline's echo/refresh (secretInput)
|
|
615
|
+
// so the typed value never hits the terminal or scrollback (task #127). Returns "" on a
|
|
616
|
+
// bare Enter (skip).
|
|
617
|
+
const askSecret = async (q) => {
|
|
618
|
+
process.stdout.write(q);
|
|
619
|
+
secretInput = true;
|
|
620
|
+
try {
|
|
621
|
+
const a = await nextLine();
|
|
622
|
+
return (a ?? "").trim();
|
|
623
|
+
} finally {
|
|
624
|
+
secretInput = false;
|
|
625
|
+
process.stdout.write("\n"); // the muted Enter left the cursor mid-line
|
|
626
|
+
}
|
|
627
|
+
};
|
|
611
628
|
const askYesNo = async (question, defaultYes) => {
|
|
612
629
|
const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
|
|
613
630
|
const raw = (await ask(question + suffix)).trim().toLowerCase();
|
|
@@ -768,6 +785,22 @@ async function addOutputTokens(cwd, delta) {
|
|
|
768
785
|
}
|
|
769
786
|
}
|
|
770
787
|
|
|
788
|
+
// Save the agent coding-model API key (task #127): the terminal prompts for the Kimi K3 key
|
|
789
|
+
// when the default cloud coder has none, and POSTs it here (worker-key authed). Returns true
|
|
790
|
+
// on success. Best-effort — a failure just leaves the reminder for next startup.
|
|
791
|
+
async function saveCodingKey(apiKey) {
|
|
792
|
+
try {
|
|
793
|
+
const r = await fetch(`${apiBase}/api/worker/coding-key`, {
|
|
794
|
+
method: "POST",
|
|
795
|
+
headers: { "Content-Type": "application/json", "X-Worker-Key": workerKey },
|
|
796
|
+
body: JSON.stringify({ apiKey }),
|
|
797
|
+
});
|
|
798
|
+
return r.ok;
|
|
799
|
+
} catch {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
771
804
|
async function resetGlobalOutputTokens() {
|
|
772
805
|
try {
|
|
773
806
|
const r = await fetch(`${apiBase}/api/worker/output-tokens/reset-global`, {
|
|
@@ -1395,6 +1428,9 @@ rl._writeToOutput = (s) => {
|
|
|
1395
1428
|
// slashOverlayActive: the slash-command overlay owns drawing (input line + menu), so
|
|
1396
1429
|
// readline's own echo must be swallowed or it fights the overlay (bug #109).
|
|
1397
1430
|
if (following || listPickerActive || slashOverlayActive) return;
|
|
1431
|
+
// secretInput (task #127): the user is typing an API key — swallow the echo so it never
|
|
1432
|
+
// appears in the terminal or scrollback.
|
|
1433
|
+
if (secretInput) return;
|
|
1398
1434
|
if (_origWriteToOutput) _origWriteToOutput(s);
|
|
1399
1435
|
else process.stdout.write(s);
|
|
1400
1436
|
};
|
|
@@ -1407,6 +1443,7 @@ rl._refreshLine = () => {
|
|
|
1407
1443
|
// Suppress readline's line refresh (which does clearScreenDown → would wipe our menu)
|
|
1408
1444
|
// while the slash overlay owns the screen. We redraw the input line ourselves (#109).
|
|
1409
1445
|
if (following || listPickerActive || slashOverlayActive) return;
|
|
1446
|
+
if (secretInput) return; // don't redraw the typed key (task #127)
|
|
1410
1447
|
if (_origRefreshLine) _origRefreshLine();
|
|
1411
1448
|
};
|
|
1412
1449
|
|
|
@@ -2316,9 +2353,50 @@ async function main() {
|
|
|
2316
2353
|
} catch {
|
|
2317
2354
|
/* doctor is best-effort — never block the REPL from starting */
|
|
2318
2355
|
}
|
|
2356
|
+
// Task #127: the default coding model is a cloud coder (Kimi K3) with no API key yet.
|
|
2357
|
+
// Ask for it. If skipped, the account stays in this "waiting" state (server-side:
|
|
2358
|
+
// coder configured, no key) and we ASK AGAIN on every startup until it's set. Until
|
|
2359
|
+
// then the agent falls back to the chat model for coding, so the terminal still works.
|
|
2360
|
+
if (p.codingKeyMissing) {
|
|
2361
|
+
const model = String(p.codingKeyModel || "the coding model").trim();
|
|
2362
|
+
console.log(
|
|
2363
|
+
dim(`\ngonext: your agent coding model (`) + cyan(model) + dim(`) needs an API key.`)
|
|
2364
|
+
);
|
|
2365
|
+
console.log(
|
|
2366
|
+
dim(" Until it's set, coding falls back to your local model. Enter the key now,")
|
|
2367
|
+
);
|
|
2368
|
+
console.log(dim(" or press Enter to skip (I'll ask again next time).\n"));
|
|
2369
|
+
const key = await askSecret(` ${model} API key: `);
|
|
2370
|
+
if (key) {
|
|
2371
|
+
const ok = await saveCodingKey(key);
|
|
2372
|
+
console.log(
|
|
2373
|
+
ok
|
|
2374
|
+
? green(` ✓ saved — ${model} is now your coding model.\n`)
|
|
2375
|
+
: red(" couldn't save the key — try again next start, or set it in web Settings → Agent.\n")
|
|
2376
|
+
);
|
|
2377
|
+
} else {
|
|
2378
|
+
console.log(
|
|
2379
|
+
dim(" ⏳ waiting for your ") + cyan(model) + dim(" API key — I'll ask again next time.\n")
|
|
2380
|
+
);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2319
2383
|
}
|
|
2320
2384
|
} catch (err) {
|
|
2321
|
-
|
|
2385
|
+
// Task #127 (zero-config): a freshly-paired account is seeded with a default local
|
|
2386
|
+
// agent model, so this "no model" path is normally unreachable. An account paired
|
|
2387
|
+
// BEFORE that change has none — point them at re-login, which now seeds it, rather than
|
|
2388
|
+
// only the raw web-Settings instruction.
|
|
2389
|
+
if (/no agent model configured/i.test(err?.message ?? "")) {
|
|
2390
|
+
console.error(
|
|
2391
|
+
red("gonext: no agent model is configured for your account yet.") +
|
|
2392
|
+
dim(
|
|
2393
|
+
"\n Run `gonext-local-worker login` to re-connect — new logins set up a local" +
|
|
2394
|
+
"\n default automatically. Or set the Agent model URL in the web app → Settings → Agent."
|
|
2395
|
+
)
|
|
2396
|
+
);
|
|
2397
|
+
} else {
|
|
2398
|
+
console.error(red(`gonext: ${err.message}`));
|
|
2399
|
+
}
|
|
2322
2400
|
process.exit(1);
|
|
2323
2401
|
}
|
|
2324
2402
|
console.log(dim("Ask about this repo. Type / to see commands (Tab completes). Ctrl-C aborts a running turn.\n"));
|
package/model-doctor.mjs
CHANGED
|
@@ -8,14 +8,52 @@
|
|
|
8
8
|
// remote box). Downloads happen only when the model dir is genuinely absent AND the user
|
|
9
9
|
// opts in.
|
|
10
10
|
import { spawn, execFile } from "node:child_process";
|
|
11
|
-
import { open, mkdir, readdir } from "node:fs/promises";
|
|
11
|
+
import { open, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
12
12
|
import { homedir, totalmem } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
14
15
|
|
|
15
16
|
// Minimum RAM to run the default 14B-4bit chat model comfortably on Apple Silicon.
|
|
16
17
|
const MIN_CHAT_RAM_GB = 16;
|
|
17
18
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
18
19
|
|
|
20
|
+
// The RAG embedding model + server (task #127 — parity with the Wizard's model-embed step).
|
|
21
|
+
// EMBED_PID_FILE / EMBED_LOG_FILE (which need DIR) are defined below, after DIR.
|
|
22
|
+
export const EMBED_REPO = "mlx-community/Qwen3-Embedding-8B-4bit-DWQ";
|
|
23
|
+
export const EMBED_DIR = join(homedir(), "mlx-models", "Qwen3-Embedding-8B-4bit-DWQ");
|
|
24
|
+
export const EMBED_PORT = 8085;
|
|
25
|
+
const EMBED_SCRIPT = fileURLToPath(new URL("./gonext_mlx_embed.py", import.meta.url));
|
|
26
|
+
|
|
27
|
+
/** The python3 the worker uses to run its agent / model tooling (mirrors the daemon). */
|
|
28
|
+
function python3Bin() {
|
|
29
|
+
return (
|
|
30
|
+
(process.env.GONEXT_PROBE_PYTHON ?? process.env.GONEXT_MLX_LM_PYTHON ?? "").trim() ||
|
|
31
|
+
"python3"
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** True when `python3 -c "import <mod>"` succeeds — i.e. the module is installed. */
|
|
36
|
+
function pyHasModule(mod) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
execFile(python3Bin(), ["-c", `import ${mod}`], (err) => resolve(!err));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** pip install -U <pkgs> with live progress (inherit stdio). Rejects on non-zero exit. */
|
|
43
|
+
function pipInstall(pkgs, log) {
|
|
44
|
+
log(` installing ${pkgs.join(" ")} … (pip, this can take a minute)`);
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const child = spawn(python3Bin(), ["-m", "pip", "install", "-U", ...pkgs], {
|
|
47
|
+
stdio: "inherit",
|
|
48
|
+
env: process.env,
|
|
49
|
+
});
|
|
50
|
+
child.on("exit", (code) =>
|
|
51
|
+
code === 0 ? resolve() : reject(new Error(`pip install ${pkgs.join(" ")} exited ${code}`))
|
|
52
|
+
);
|
|
53
|
+
child.on("error", reject);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
19
57
|
/**
|
|
20
58
|
* The yellow RAM notice shown before the multi-GB model download (user request): the
|
|
21
59
|
* default 14B-4bit model wants a Mac with ≥16 GB. When the machine has less, add a plain
|
|
@@ -42,6 +80,8 @@ export function chatRamWarningLines(repo, totalBytes) {
|
|
|
42
80
|
const DIR = join(homedir(), ".gonext");
|
|
43
81
|
export const MODEL_PID_FILE = join(DIR, "model.pid");
|
|
44
82
|
export const MODEL_LOG_FILE = join(DIR, "model.log");
|
|
83
|
+
const EMBED_PID_FILE = join(DIR, "embed.pid");
|
|
84
|
+
const EMBED_LOG_FILE = join(DIR, "embed.log");
|
|
45
85
|
|
|
46
86
|
// The default base chat model (task #127 decision): matches the Wizard's model-chat step.
|
|
47
87
|
export const DEFAULT_CHAT_REPO = "mlx-community/Qwen3-14B-4bit";
|
|
@@ -229,6 +269,13 @@ async function fetchModelConfig(apiBase, workerKey) {
|
|
|
229
269
|
*/
|
|
230
270
|
export async function runDoctor({ apiBase, workerKey, log, confirm }) {
|
|
231
271
|
const p = (await fetchModelConfig(apiBase, workerKey)) ?? {};
|
|
272
|
+
|
|
273
|
+
// Step 0: the agent's PYTHON deps must be present or every agent turn crashes at import
|
|
274
|
+
// — and the model download/start needs `hf`/`mlx-lm`. Ensure these FIRST (task #127:
|
|
275
|
+
// parity with the Wizard's pip-mlx-lm / pip-agent-libs / hf-cli steps). Fatal-ish: if the
|
|
276
|
+
// core framework is missing, say so, but keep going so the model steps still run.
|
|
277
|
+
const deps = await ensureAgentDeps({ log, confirm });
|
|
278
|
+
|
|
232
279
|
// Chat model: prefer the account's configured URL; fall back to the default local one.
|
|
233
280
|
const configuredChat = String(p.agentBaseURL || "").trim();
|
|
234
281
|
const chatUrl = configuredChat || `http://127.0.0.1:${DEFAULT_CHAT_PORT}`;
|
|
@@ -338,5 +385,187 @@ export async function runDoctor({ apiBase, workerKey, log, confirm }) {
|
|
|
338
385
|
log(` answering. Start it there${codeModel ? ` (ollama pull ${codeModel})` : ""}.`);
|
|
339
386
|
}
|
|
340
387
|
}
|
|
341
|
-
|
|
388
|
+
|
|
389
|
+
// RAG (task #127: the user wants the terminal RAG-ready). Ensure the embedding model +
|
|
390
|
+
// server so rag_index / rag_search work. Uses the account's configured embed URL when
|
|
391
|
+
// local, else the default local one on EMBED_PORT.
|
|
392
|
+
const rag = await ensureRag({ p, log, confirm });
|
|
393
|
+
|
|
394
|
+
return { chat, deps, rag };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Ensure the agent's required runtime deps (task #127 — parity with the Wizard's pip steps).
|
|
399
|
+
* python3 can't be auto-installed (guidance only); hf / mlx-lm / smolagents+openai are
|
|
400
|
+
* offered via pip. Each install is confirmed. Returns a status map; missing deps are
|
|
401
|
+
* reported, never silently ignored — the agent hard-fails without smolagents+openai.
|
|
402
|
+
*/
|
|
403
|
+
export async function ensureAgentDeps({ log, confirm }) {
|
|
404
|
+
const out = {};
|
|
405
|
+
log("");
|
|
406
|
+
log(" Checking the agent's dependencies…");
|
|
407
|
+
// python3 — the whole agent runs on it; we can't install it for the user.
|
|
408
|
+
if (!(await pyHasModule("sys"))) {
|
|
409
|
+
log(` ⚠ ${python3Bin()} not found. Install Python 3 (Xcode Command Line Tools or`);
|
|
410
|
+
log(" python.org), then run `gonext-local-worker doctor` again.");
|
|
411
|
+
out.python3 = "missing";
|
|
412
|
+
return out; // nothing else can be installed without python
|
|
413
|
+
}
|
|
414
|
+
out.python3 = "ok";
|
|
415
|
+
|
|
416
|
+
const ensurePkg = async (key, probeMod, pkgs, question) => {
|
|
417
|
+
if (await probeMod()) {
|
|
418
|
+
out[key] = "present";
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (!(await confirm(question, true))) {
|
|
422
|
+
out[key] = "skipped";
|
|
423
|
+
log(` Skipped ${pkgs.join(" ")} — install later: ${python3Bin()} -m pip install -U ${pkgs.join(" ")}`);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
await pipInstall(pkgs, log);
|
|
428
|
+
out[key] = "installed";
|
|
429
|
+
} catch (e) {
|
|
430
|
+
log(` ⚠ ${e instanceof Error ? e.message : e}`);
|
|
431
|
+
out[key] = "failed";
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// hf CLI (model downloads). `huggingface_hub` provides the `hf` command.
|
|
436
|
+
await ensurePkg(
|
|
437
|
+
"hf",
|
|
438
|
+
() => which("hf"),
|
|
439
|
+
["huggingface_hub"],
|
|
440
|
+
"Install the Hugging Face CLI (needed to download models)?"
|
|
441
|
+
);
|
|
442
|
+
// mlx-lm — runs the local chat + embedding model servers.
|
|
443
|
+
await ensurePkg("mlxLm", () => pyHasModule("mlx_lm"), ["mlx-lm"], "Install mlx-lm (runs the local model servers)?");
|
|
444
|
+
// The agent framework itself — WITHOUT this every agent turn crashes at import.
|
|
445
|
+
await ensurePkg(
|
|
446
|
+
"agentLibs",
|
|
447
|
+
async () => (await pyHasModule("smolagents")) && (await pyHasModule("openai")),
|
|
448
|
+
["smolagents", "openai"],
|
|
449
|
+
"Install the agent framework (smolagents + openai) — required to run the agent?"
|
|
450
|
+
);
|
|
451
|
+
if (out.agentLibs === "skipped" || out.agentLibs === "failed") {
|
|
452
|
+
log(yellow(" ⚠ Without smolagents + openai the agent can't run — install them before asking a question."));
|
|
453
|
+
}
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Start the MLX embeddings server (gonext_mlx_embed.py) DETACHED on `port` for `modelDir`,
|
|
459
|
+
* with its own pidfile/log. No-op + { ready:true } when one already answers (BC3). Waits up
|
|
460
|
+
* to waitMs for /v1/models to respond. Mirrors startChatModelServer.
|
|
461
|
+
*/
|
|
462
|
+
export async function startEmbedServer({ modelDir, port, waitMs = 60_000, log = () => {} }) {
|
|
463
|
+
const base = `http://127.0.0.1:${port}`;
|
|
464
|
+
if (await probeModelServer(base, { timeoutMs: 1500 })) {
|
|
465
|
+
return { started: false, ready: true, reason: "already-running" };
|
|
466
|
+
}
|
|
467
|
+
if (!(await pyHasModule("mlx_lm"))) {
|
|
468
|
+
return { started: false, ready: false, reason: "mlx-lm not installed (pip install mlx-lm)" };
|
|
469
|
+
}
|
|
470
|
+
await mkdir(DIR, { recursive: true });
|
|
471
|
+
const outLog = await open(EMBED_LOG_FILE, "a");
|
|
472
|
+
let pid;
|
|
473
|
+
try {
|
|
474
|
+
const child = spawn(
|
|
475
|
+
python3Bin(),
|
|
476
|
+
[EMBED_SCRIPT, "--model", modelDir, "--port", String(port), "--host", "127.0.0.1"],
|
|
477
|
+
{ detached: true, stdio: ["ignore", outLog.fd, outLog.fd], env: process.env }
|
|
478
|
+
);
|
|
479
|
+
pid = child.pid;
|
|
480
|
+
await writeFile(EMBED_PID_FILE, String(pid ?? ""), "utf8");
|
|
481
|
+
child.unref();
|
|
482
|
+
} finally {
|
|
483
|
+
await outLog.close();
|
|
484
|
+
}
|
|
485
|
+
log(` starting embeddings server (pid ${pid}) on port ${port} — waiting for it to load…`);
|
|
486
|
+
const deadline = Date.now() + waitMs;
|
|
487
|
+
while (Date.now() < deadline) {
|
|
488
|
+
await sleep(2500);
|
|
489
|
+
if (await probeModelServer(base, { timeoutMs: 2000 })) return { started: true, ready: true, pid };
|
|
490
|
+
}
|
|
491
|
+
return { started: true, ready: false, pid, reason: `embeddings server still loading (see ${EMBED_LOG_FILE})` };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Ensure RAG is ready for the agent terminal (task #127): boto3 (cloud/S3 storage), the
|
|
496
|
+
* embedding model, and a running embeddings server. Uses the account's configured embed URL
|
|
497
|
+
* when it's local; otherwise the default local server on EMBED_PORT. BC3: an already-running
|
|
498
|
+
* embed server is left alone. Returns a status map.
|
|
499
|
+
*/
|
|
500
|
+
export async function ensureRag({ p, log, confirm }) {
|
|
501
|
+
const out = {};
|
|
502
|
+
log("");
|
|
503
|
+
log(" Checking RAG (agent knowledge base)…");
|
|
504
|
+
// boto3 — needed only for CLOUD (S3) RAG; local RAG stores on disk. Cheap + harmless, so
|
|
505
|
+
// offer it so cloud RAG works later without another setup pass.
|
|
506
|
+
if (!(await pyHasModule("boto3"))) {
|
|
507
|
+
if (await confirm("Install boto3 (needed only if you use cloud/S3 RAG)?", true)) {
|
|
508
|
+
try {
|
|
509
|
+
await pipInstall(["boto3"], log);
|
|
510
|
+
out.boto3 = "installed";
|
|
511
|
+
} catch (e) {
|
|
512
|
+
log(` ⚠ ${e instanceof Error ? e.message : e}`);
|
|
513
|
+
out.boto3 = "failed";
|
|
514
|
+
}
|
|
515
|
+
} else out.boto3 = "skipped";
|
|
516
|
+
} else out.boto3 = "present";
|
|
517
|
+
|
|
518
|
+
// Embed server URL: the account's configured local one, else the default local server.
|
|
519
|
+
const configuredEmbed = String(p?.ragEmbedUrl || "").trim();
|
|
520
|
+
const embedUrl =
|
|
521
|
+
configuredEmbed && isLocalModelUrl(configuredEmbed)
|
|
522
|
+
? configuredEmbed
|
|
523
|
+
: `http://127.0.0.1:${EMBED_PORT}`;
|
|
524
|
+
const embedPort = parsePort(embedUrl) || EMBED_PORT;
|
|
525
|
+
|
|
526
|
+
if (await probeModelServer(embedUrl)) {
|
|
527
|
+
log(` ✓ embeddings server already running at ${modelRoot(embedUrl)} — leaving it alone.`);
|
|
528
|
+
out.embed = "already-running";
|
|
529
|
+
return out;
|
|
530
|
+
}
|
|
531
|
+
if (configuredEmbed && !isLocalModelUrl(configuredEmbed)) {
|
|
532
|
+
log(` ⚠ your embeddings server (${modelRoot(configuredEmbed)}) is remote and isn't`);
|
|
533
|
+
log(" answering. Start it there — this machine can't launch it.");
|
|
534
|
+
out.embed = "remote";
|
|
535
|
+
return out;
|
|
536
|
+
}
|
|
537
|
+
// Download the embed model if missing.
|
|
538
|
+
if (!(await modelDirReady(EMBED_DIR))) {
|
|
539
|
+
if (!(await confirm(`Download the RAG embedding model ${EMBED_REPO.split("/").pop()} (~4 GB)?`, true))) {
|
|
540
|
+
log(" Skipped. To do it yourself:");
|
|
541
|
+
log(` hf download ${EMBED_REPO} --local-dir ${EMBED_DIR}`);
|
|
542
|
+
log(` gonext-local-worker embed --model ${EMBED_DIR} --port ${embedPort}`);
|
|
543
|
+
out.embed = "skipped-model";
|
|
544
|
+
return out;
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
await downloadModel({ repo: EMBED_REPO, dir: EMBED_DIR, log });
|
|
548
|
+
} catch (e) {
|
|
549
|
+
log(` ⚠ ${e instanceof Error ? e.message : e}`);
|
|
550
|
+
out.embed = "download-failed";
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (!(await confirm(`Start the RAG embeddings server on port ${embedPort} now?`, true))) {
|
|
555
|
+
log(` Skipped. Start it later: gonext-local-worker embed --model ${EMBED_DIR} --port ${embedPort}`);
|
|
556
|
+
out.embed = "not-started";
|
|
557
|
+
return out;
|
|
558
|
+
}
|
|
559
|
+
const r = await startEmbedServer({ modelDir: EMBED_DIR, port: embedPort, log });
|
|
560
|
+
if (r.ready) {
|
|
561
|
+
log(` ✓ embeddings server is up on port ${embedPort}.`);
|
|
562
|
+
out.embed = "started";
|
|
563
|
+
} else if (r.started) {
|
|
564
|
+
log(` … embeddings server is loading in the background on port ${embedPort}.`);
|
|
565
|
+
out.embed = "loading";
|
|
566
|
+
} else {
|
|
567
|
+
log(` ⚠ ${r.reason}`);
|
|
568
|
+
out.embed = "start-failed";
|
|
569
|
+
}
|
|
570
|
+
return out;
|
|
342
571
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gonext-local-worker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.347",
|
|
4
4
|
"description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|