@tiens.nguyen/gonext-local-worker 1.0.205 → 1.0.206
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-local-worker.mjs +69 -0
- package/gonext-repl.mjs +86 -5
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -2020,6 +2020,71 @@ function normalizeBaseUrl(raw) {
|
|
|
2020
2020
|
return typeof raw === "string" ? raw.trim().replace(/\/+$/, "") : "";
|
|
2021
2021
|
}
|
|
2022
2022
|
|
|
2023
|
+
/**
|
|
2024
|
+
* "Load Models" (task #44): probe the agent coding-model server's model list. Tries the
|
|
2025
|
+
* OpenAI-compatible /v1/models (MLX + Ollama both serve it) and falls back to Ollama's
|
|
2026
|
+
* /api/tags. The server is on the user's network, so only this worker can reach it. The
|
|
2027
|
+
* job's resultText is JSON {models:[...]} the web parses into checkboxes.
|
|
2028
|
+
*/
|
|
2029
|
+
async function runListModelsJob(job) {
|
|
2030
|
+
const { jobId, payload } = job;
|
|
2031
|
+
const start = Date.now();
|
|
2032
|
+
const runRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
2033
|
+
method: "PATCH",
|
|
2034
|
+
body: JSON.stringify({ jobStatus: "running" }),
|
|
2035
|
+
});
|
|
2036
|
+
await ensureWorkerOk(runRes, `mark running list_models jobId=${jobId}`);
|
|
2037
|
+
try {
|
|
2038
|
+
const base = normalizeBaseUrl(payload?.url).replace(/\/v1$/i, "");
|
|
2039
|
+
if (!base) throw new Error("No coding-model URL provided.");
|
|
2040
|
+
const models = new Set();
|
|
2041
|
+
// 1) OpenAI-compatible /v1/models → { data: [{ id }] }
|
|
2042
|
+
try {
|
|
2043
|
+
const r = await fetch(`${base}/v1/models`, { method: "GET" });
|
|
2044
|
+
if (r.ok) {
|
|
2045
|
+
const j = await r.json().catch(() => ({}));
|
|
2046
|
+
for (const m of Array.isArray(j?.data) ? j.data : []) {
|
|
2047
|
+
if (typeof m?.id === "string" && m.id.trim()) models.add(m.id.trim());
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
} catch {
|
|
2051
|
+
/* try the Ollama shape next */
|
|
2052
|
+
}
|
|
2053
|
+
// 2) Ollama /api/tags → { models: [{ name }] }
|
|
2054
|
+
if (models.size === 0) {
|
|
2055
|
+
const r = await fetch(`${base}/api/tags`, { method: "GET" });
|
|
2056
|
+
if (r.ok) {
|
|
2057
|
+
const j = await r.json().catch(() => ({}));
|
|
2058
|
+
for (const m of Array.isArray(j?.models) ? j.models : []) {
|
|
2059
|
+
if (typeof m?.name === "string" && m.name.trim()) models.add(m.name.trim());
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
const list = Array.from(models).sort();
|
|
2064
|
+
const doneRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
2065
|
+
method: "PATCH",
|
|
2066
|
+
body: JSON.stringify({
|
|
2067
|
+
jobStatus: "completed",
|
|
2068
|
+
resultText: JSON.stringify({ models: list }),
|
|
2069
|
+
totalTimeSeconds: (Date.now() - start) / 1000,
|
|
2070
|
+
}),
|
|
2071
|
+
});
|
|
2072
|
+
await ensureWorkerOk(doneRes, `complete list_models jobId=${jobId}`);
|
|
2073
|
+
console.log(`[gonext-worker] list_models ${jobId}: ${list.length} model(s) from ${base}`);
|
|
2074
|
+
} catch (e) {
|
|
2075
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
2076
|
+
await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
2077
|
+
method: "PATCH",
|
|
2078
|
+
body: JSON.stringify({
|
|
2079
|
+
jobStatus: "failed",
|
|
2080
|
+
errorMessage: `Could not list models: ${message}`,
|
|
2081
|
+
totalTimeSeconds: (Date.now() - start) / 1000,
|
|
2082
|
+
}),
|
|
2083
|
+
}).catch(() => {});
|
|
2084
|
+
console.error(`[gonext-worker] failed list_models ${jobId}:`, message);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2023
2088
|
function normalizeOpenAiV1Root(raw) {
|
|
2024
2089
|
const base = normalizeBaseUrl(raw);
|
|
2025
2090
|
if (!base) return "";
|
|
@@ -2601,6 +2666,10 @@ async function pollOnce() {
|
|
|
2601
2666
|
await runHttpProbeJob(job);
|
|
2602
2667
|
return;
|
|
2603
2668
|
}
|
|
2669
|
+
if (job.jobType === "list_models") {
|
|
2670
|
+
await runListModelsJob(job);
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2604
2673
|
if (job.jobType === "agent_chat") {
|
|
2605
2674
|
await runAgentChatJob(job);
|
|
2606
2675
|
return;
|
package/gonext-repl.mjs
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* interrupted investigation actually resumes it). /reset clears it.
|
|
22
22
|
*
|
|
23
23
|
* Requires the worker daemon to be running (it claims the job).
|
|
24
|
-
* Commands: /exit /reset /revert [runId] /help
|
|
24
|
+
* Commands: /exit /model /reset /revert [runId] /help Ctrl-C stops following the current run.
|
|
25
25
|
*/
|
|
26
26
|
import { readFile, mkdir, writeFile, unlink } from "node:fs/promises";
|
|
27
27
|
import { existsSync, statSync, readFileSync } from "node:fs";
|
|
@@ -119,7 +119,7 @@ if (argv.includes("--help") || argv.includes("-h")) {
|
|
|
119
119
|
" -v, --verbose show the agent's diagnostic logs ([gonext-agent] …) and python stderr\n" +
|
|
120
120
|
" -h, --help this help\n\n" +
|
|
121
121
|
"Auth comes from ~/.gonext/worker.env; models/budgets/RAG come from your web Settings.\n" +
|
|
122
|
-
"Inside the REPL: /exit · /reset · /revert [runId] · /help — Ctrl-C stops following a turn.\n" +
|
|
122
|
+
"Inside the REPL: /exit · /model · /reset · /revert [runId] · /help — Ctrl-C stops following a turn.\n" +
|
|
123
123
|
"Questions are enqueued like web questions and executed by the RUNNING\n" +
|
|
124
124
|
"gonext-local-worker daemon — its terminal shows the full [gonext-agent] logs.\n" +
|
|
125
125
|
"Conversation history is saved per-folder in ~/.gonext/sessions and resumed next time\n" +
|
|
@@ -193,6 +193,19 @@ rl.on("line", (l) => {
|
|
|
193
193
|
_lineQueue.push(clean);
|
|
194
194
|
}
|
|
195
195
|
});
|
|
196
|
+
// Neutralize navigation keys (↑/↓ history recall, ←/→ cursor, Home/End, etc.) during a
|
|
197
|
+
// turn: echo is already muted, but readline still recalls history into the invisible
|
|
198
|
+
// buffer on ↑/↓ — reset the line state after every keypress so nothing accumulates or
|
|
199
|
+
// moves. Ctrl+C is unaffected (readline emits its SIGINT before this handler runs).
|
|
200
|
+
if (process.stdin.isTTY) {
|
|
201
|
+
process.stdin.on("keypress", () => {
|
|
202
|
+
if (following) {
|
|
203
|
+
rl.line = "";
|
|
204
|
+
rl.cursor = 0;
|
|
205
|
+
rl.historyIndex = -1;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
196
209
|
rl.on("close", () => {
|
|
197
210
|
_stdinClosed = true;
|
|
198
211
|
if (_lineWaiter) {
|
|
@@ -338,6 +351,53 @@ async function clearSession(cwd) {
|
|
|
338
351
|
}
|
|
339
352
|
|
|
340
353
|
// ---------- fetch the ready-to-run agent payload from user settings ----------
|
|
354
|
+
// /model command: fetch the allowed coding models (default + web-curated whitelist),
|
|
355
|
+
// show a numbered picker, and set sessionCodingModel to the choice for this session.
|
|
356
|
+
async function chooseModel() {
|
|
357
|
+
let probe;
|
|
358
|
+
try {
|
|
359
|
+
probe = await fetchAgentPayload([{ role: "user", content: "ping" }]);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
console.log(red(` couldn't load models: ${err.message}\n`));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const allowed = Array.isArray(probe?.codingAllowed) ? probe.codingAllowed : [];
|
|
365
|
+
const def = (probe?.codingDefault ?? "").trim();
|
|
366
|
+
if (allowed.length === 0) {
|
|
367
|
+
console.log(dim(" No coding model configured. Set one in the web app → Settings → Agent.\n"));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (allowed.length === 1) {
|
|
371
|
+
console.log(
|
|
372
|
+
dim(` Only one coding model is available: ${allowed[0]}.\n`) +
|
|
373
|
+
dim(" Add more in the web app → Settings → Agent → Load Models (checkboxes).\n")
|
|
374
|
+
);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const active = sessionCodingModel || def;
|
|
378
|
+
console.log(dim(" Choose the coding model for this session:"));
|
|
379
|
+
allowed.forEach((m, i) => {
|
|
380
|
+
const mark = m === active ? green(" ●") : " ";
|
|
381
|
+
const tag = m === def ? dim(" (default)") : "";
|
|
382
|
+
console.log(` ${mark} ${i + 1}. ${m}${tag}`);
|
|
383
|
+
});
|
|
384
|
+
const pick = (await ask(dim(" number (or Enter to keep current): "))).trim();
|
|
385
|
+
if (!pick) {
|
|
386
|
+
console.log("");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const idx = Number.parseInt(pick, 10) - 1;
|
|
390
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= allowed.length) {
|
|
391
|
+
console.log(red(" invalid choice.\n"));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const chosen = allowed[idx];
|
|
395
|
+
// Empty override means "use the account default" — so don't send an override when the
|
|
396
|
+
// user picks the default (keeps the payload clean and lets a later default change win).
|
|
397
|
+
sessionCodingModel = chosen === def ? "" : chosen;
|
|
398
|
+
console.log(green(` ✓ coding model → ${chosen}${chosen === def ? " (default)" : ""}\n`));
|
|
399
|
+
}
|
|
400
|
+
|
|
341
401
|
async function fetchAgentPayload(messages) {
|
|
342
402
|
const res = await fetch(`${apiBase}/api/worker/agent-payload`, {
|
|
343
403
|
method: "POST",
|
|
@@ -370,6 +430,9 @@ let following = false;
|
|
|
370
430
|
let followAborted = false;
|
|
371
431
|
let currentJobId = null; // the running turn's jobId — so Ctrl+C can cancel it server-side
|
|
372
432
|
let cancelRequested = false; // a cancel POST has been sent for the current turn
|
|
433
|
+
// Per-session coding-model override chosen via /model (empty = use the account default).
|
|
434
|
+
// Sent to /agent-ask as codingModelOverride; the API validates it against the whitelist.
|
|
435
|
+
let sessionCodingModel = "";
|
|
373
436
|
|
|
374
437
|
// While a turn runs, mute readline's own echo/redraws so keystrokes can't corrupt the
|
|
375
438
|
// live status display and there's no way to type a new question — only Ctrl+C works.
|
|
@@ -381,6 +444,15 @@ rl._writeToOutput = (s) => {
|
|
|
381
444
|
if (_origWriteToOutput) _origWriteToOutput(s);
|
|
382
445
|
else process.stdout.write(s);
|
|
383
446
|
};
|
|
447
|
+
// _writeToOutput only mutes echoed TEXT; readline's line refresh writes cursor-position
|
|
448
|
+
// + erase escapes (e.g. ESC[1G, ESC[0J) straight to output on every keypress, which
|
|
449
|
+
// would corrupt our live status display. No-op it during a turn so a keypress produces
|
|
450
|
+
// ZERO terminal output.
|
|
451
|
+
const _origRefreshLine = rl._refreshLine ? rl._refreshLine.bind(rl) : null;
|
|
452
|
+
rl._refreshLine = () => {
|
|
453
|
+
if (following) return;
|
|
454
|
+
if (_origRefreshLine) _origRefreshLine();
|
|
455
|
+
};
|
|
384
456
|
|
|
385
457
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
386
458
|
|
|
@@ -397,8 +469,12 @@ async function runAgentTurn(history) {
|
|
|
397
469
|
method: "POST",
|
|
398
470
|
headers: { "Content-Type": "application/json", "X-Worker-Key": workerKey },
|
|
399
471
|
// cwd = this terminal's folder → agent puts download/unzip output here (when it's
|
|
400
|
-
// a registered workspace).
|
|
401
|
-
body: JSON.stringify({
|
|
472
|
+
// a registered workspace). codingModelOverride = the /model choice for this session.
|
|
473
|
+
body: JSON.stringify({
|
|
474
|
+
messages: history,
|
|
475
|
+
cwd: resolve(process.cwd()),
|
|
476
|
+
...(sessionCodingModel ? { codingModelOverride: sessionCodingModel } : {}),
|
|
477
|
+
}),
|
|
402
478
|
});
|
|
403
479
|
const data = await res.json().catch(() => ({}));
|
|
404
480
|
if (!res.ok) {
|
|
@@ -813,12 +889,17 @@ async function main() {
|
|
|
813
889
|
if (line === "/help") {
|
|
814
890
|
console.log(
|
|
815
891
|
dim(
|
|
816
|
-
" /exit — quit · /
|
|
892
|
+
" /exit — quit · /model — switch the coding model for this session · " +
|
|
893
|
+
"/revert [runId] — undo the agent's file edits (latest run) · " +
|
|
817
894
|
"/reset — forget this folder's saved conversation and start fresh"
|
|
818
895
|
)
|
|
819
896
|
);
|
|
820
897
|
continue;
|
|
821
898
|
}
|
|
899
|
+
if (line === "/model") {
|
|
900
|
+
await chooseModel();
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
822
903
|
if (line === "/reset" || line === "/new") {
|
|
823
904
|
history.length = 0;
|
|
824
905
|
await clearSession(cwd);
|
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.206",
|
|
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",
|