@tiens.nguyen/gonext-local-worker 1.0.174 → 1.0.177

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.
Files changed (2) hide show
  1. package/gonext-repl.mjs +107 -106
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -8,13 +8,15 @@
8
8
  *
9
9
  * It offers to register the CURRENT folder as an agent workspace (with a trust prompt
10
10
  * that maps to --allow-run), authenticates with the worker key from ~/.gonext/worker.env
11
- * (the same file `gonext-local-worker set` / the Wizard write), pulls your agent/coding
12
- * model + budgets + RAG config FROM YOUR USER SETTINGS via POST /api/worker/agent-payload,
13
- * then loops on a `>> ` prompt. Each question executes gonext_agent_chat.py DIRECTLY
14
- * (no job queueno race with a polling worker daemon), streaming the agent's steps
15
- * and thinking into the terminal.
11
+ * (the same file `gonext-local-worker set` / the Wizard write), and loops on a `>> `
12
+ * prompt. Each question is ENQUEUED as a normal agent_chat job (models/budgets/RAG
13
+ * resolved from your user settings by POST /api/worker/agent-ask) and executed by the
14
+ * RUNNING gonext-local-worker daemonso its terminal shows the full [gonext-agent]
15
+ * logs, exactly like a web question. The REPL polls the job and streams the persisted
16
+ * chunks live into the terminal.
16
17
  *
17
- * Commands: /exit /revert [runId] /help Ctrl-C aborts the current run.
18
+ * Requires the worker daemon to be running (it claims the job).
19
+ * Commands: /exit /revert [runId] /help Ctrl-C stops following the current run.
18
20
  */
19
21
  import { readFile, mkdir, writeFile } from "node:fs/promises";
20
22
  import { existsSync, statSync } from "node:fs";
@@ -34,6 +36,25 @@ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
34
36
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
35
37
  const red = (s) => `\x1b[31m${s}\x1b[0m`;
36
38
 
39
+ // ---------- flags ----------
40
+ const argv = process.argv.slice(2);
41
+ if (argv.includes("--help") || argv.includes("-h")) {
42
+ console.log(
43
+ "gonext — terminal agent REPL for the current folder\n\n" +
44
+ "Usage: gonext [-v|--verbose]\n\n" +
45
+ " -v, --verbose show the agent's diagnostic logs ([gonext-agent] …) and python stderr\n" +
46
+ " -h, --help this help\n\n" +
47
+ "Auth comes from ~/.gonext/worker.env; models/budgets/RAG come from your web Settings.\n" +
48
+ "Inside the REPL: /exit · /revert [runId] · /help — Ctrl-C stops following a turn.\n" +
49
+ "Questions are enqueued like web questions and executed by the RUNNING\n" +
50
+ "gonext-local-worker daemon — its terminal shows the full [gonext-agent] logs."
51
+ );
52
+ process.exit(0);
53
+ }
54
+ if (argv.includes("-v") || argv.includes("--verbose")) {
55
+ process.env.GONEXT_REPL_VERBOSE = "1";
56
+ }
57
+
37
58
  // ---------- auth: worker key + API base from ~/.gonext/worker.env ----------
38
59
  dotenv.config({ path: ENV_FILE });
39
60
  dotenv.config();
@@ -157,104 +178,86 @@ async function fetchAgentPayload(messages) {
157
178
  return data;
158
179
  }
159
180
 
160
- // ---------- run one agent turn by executing gonext_agent_chat.py directly ----------
161
- let child = null;
181
+ // ---------- run one agent turn via the JOB QUEUE (the daemon executes it) ----------
182
+ // The question is enqueued exactly like a web question, so the RUNNING
183
+ // gonext-local-worker daemon claims + executes it — its terminal shows the full
184
+ // [gonext-agent] logs. We poll the job here and stream the persisted chunks live.
185
+ let following = false;
186
+ let followAborted = false;
162
187
 
163
- async function runAgentTurn(payload) {
164
- // Map the job payload to the python cfg the same way the worker daemon does in
165
- // runAgentChatJob (gonext-local-worker.mjs) — KEEP IN SYNC with that block.
166
- const input = JSON.stringify({
167
- messages: payload?.messages ?? [],
168
- agentBaseURL: payload?.agentBaseURL ?? "",
169
- agentApiKey: payload?.agentApiKey ?? "",
170
- agentModelId: payload?.agentModelId ?? "",
171
- codingBaseURL: payload?.codingBaseURL ?? "",
172
- codingModelId: payload?.codingModelId ?? "",
173
- tools: payload?.tools ?? ["http_request"],
174
- maxSteps: payload?.maxSteps ?? 0,
175
- researchBudget: payload?.researchBudget ?? 10,
176
- agentToolMode: payload?.agentToolMode ?? "code",
177
- apiBaseURL: apiBase,
178
- workerKey,
179
- emailEnabled: payload?.emailEnabled ?? false,
180
- emailApiUrl: payload?.emailApiUrl ?? "",
181
- emailApiMethod: payload?.emailApiMethod ?? "POST",
182
- emailApiHeaders: payload?.emailApiHeaders ?? "",
183
- emailBodyTemplate: payload?.emailBodyTemplate ?? "",
184
- emailFrom: payload?.emailFrom ?? "",
185
- emailAllowList: payload?.emailAllowList ?? "",
186
- ragEnabled: payload?.ragEnabled ?? false,
187
- ragS3Location: payload?.ragS3Location ?? "",
188
- ragAwsRegion: payload?.ragAwsRegion ?? "",
189
- ragAwsAccessKeyId: payload?.ragAwsAccessKeyId ?? "",
190
- ragAwsSecretAccessKey: payload?.ragAwsSecretAccessKey ?? "",
191
- ragEmbedModel: payload?.ragEmbedModel ?? "",
192
- ragEmbedUrl: payload?.ragEmbedUrl ?? "",
193
- ragTopK: payload?.ragTopK ?? 6,
194
- workspaces: await readWorkspaces(),
195
- });
188
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
196
189
 
197
- const python =
198
- (process.env.GONEXT_PROBE_PYTHON ?? process.env.GONEXT_MLX_LM_PYTHON ?? "").trim() ||
199
- "python3";
200
- // WeasyPrint (create_pdf) needs Homebrew libs on macOS — same env the worker sets.
201
- let extraEnv;
202
- if (process.platform === "darwin") {
203
- const existing = process.env.DYLD_FALLBACK_LIBRARY_PATH ?? "";
204
- extraEnv = {
205
- DYLD_FALLBACK_LIBRARY_PATH: [existing, "/opt/homebrew/lib", "/usr/local/lib"]
206
- .filter(Boolean)
207
- .join(":"),
208
- };
190
+ /** Strip <think> markers for live display, and whole think blocks for the answer. */
191
+ const stripThinkTags = (s) => s.replace(/<\/?think>/gi, "");
192
+ const answerFrom = (s) =>
193
+ s
194
+ .replace(/<think>[\s\S]*?<\/think>/gi, "")
195
+ .replace(/<think>[\s\S]*$/i, "")
196
+ .trim();
197
+
198
+ async function runAgentTurn(history) {
199
+ const res = await fetch(`${apiBase}/api/worker/agent-ask`, {
200
+ method: "POST",
201
+ headers: { "Content-Type": "application/json", "X-Worker-Key": workerKey },
202
+ body: JSON.stringify({ messages: history }),
203
+ });
204
+ const data = await res.json().catch(() => ({}));
205
+ if (!res.ok) {
206
+ if (res.status === 404) {
207
+ throw new Error(
208
+ "the API does not have POST /api/worker/agent-ask yet — deploy the latest API."
209
+ );
210
+ }
211
+ throw new Error(data?.error || `agent-ask failed (HTTP ${res.status})`);
209
212
  }
213
+ const jobId = data?.jobId;
214
+ if (!jobId) throw new Error("agent-ask returned no jobId.");
210
215
 
211
- return new Promise((resolveTurn) => {
212
- let finalText = "";
213
- let buf = "";
214
- child = spawn(python, [join(DIR, "gonext_agent_chat.py")], {
215
- env: { ...process.env, ...(extraEnv ?? {}) },
216
- });
217
- child.stdin.write(input);
218
- child.stdin.end();
219
- child.stdout.on("data", (chunk) => {
220
- buf += chunk.toString();
221
- let nl;
222
- while ((nl = buf.indexOf("\n")) >= 0) {
223
- const line = buf.slice(0, nl);
224
- buf = buf.slice(nl + 1);
225
- if (!line.trim()) continue;
226
- let ev;
227
- try {
228
- ev = JSON.parse(line);
229
- } catch {
230
- continue;
231
- }
232
- if (ev.type === "step" && typeof ev.text === "string") {
233
- process.stdout.write(dim(`\n· ${ev.text}`));
234
- } else if (ev.type === "stream" && typeof ev.text === "string") {
235
- process.stdout.write(dim(ev.text));
236
- } else if (ev.type === "final" && typeof ev.text === "string") {
237
- finalText = ev.text;
238
- } else if (ev.type === "log" && process.env.GONEXT_REPL_VERBOSE === "1") {
239
- process.stdout.write(dim(`\n[log] ${ev.text}`));
240
- }
216
+ following = true;
217
+ followAborted = false;
218
+ const startedAt = Date.now();
219
+ let shownChars = 0;
220
+ let warnedPending = false;
221
+ try {
222
+ for (;;) {
223
+ if (followAborted) {
224
+ process.stdout.write(
225
+ dim("\n(stopped following — the job keeps running on the worker)\n")
226
+ );
227
+ return "";
241
228
  }
242
- });
243
- child.stderr.on("data", (chunk) => {
244
- if (process.env.GONEXT_REPL_VERBOSE === "1") {
245
- process.stderr.write(dim(chunk.toString()));
229
+ const jr = await fetch(`${apiBase}/api/worker/jobs/${jobId}`, {
230
+ headers: { "X-Worker-Key": workerKey },
231
+ });
232
+ const job = await jr.json().catch(() => ({}));
233
+ if (!jr.ok) throw new Error(job?.error || `job poll failed (HTTP ${jr.status})`);
234
+ const text = typeof job.resultText === "string" ? job.resultText : "";
235
+ if (job.jobStatus === "completed") {
236
+ // Don't stream the final delta dim — the answer prints bright right after.
237
+ return answerFrom(text);
246
238
  }
247
- });
248
- child.on("error", (err) => {
249
- console.error(red(`\ngonext: failed to start ${python}: ${err.message}`));
250
- child = null;
251
- resolveTurn("");
252
- });
253
- child.on("exit", () => {
254
- child = null;
255
- resolveTurn(finalText);
256
- });
257
- });
239
+ if (text.length > shownChars) {
240
+ process.stdout.write(dim(stripThinkTags(text.slice(shownChars))));
241
+ shownChars = text.length;
242
+ }
243
+ if (job.jobStatus === "failed" || job.jobStatus === "cancelled") {
244
+ throw new Error(job.errorMessage || `job ${job.jobStatus}`);
245
+ }
246
+ if (
247
+ job.jobStatus === "pending" &&
248
+ !warnedPending &&
249
+ Date.now() - startedAt > 6000
250
+ ) {
251
+ warnedPending = true;
252
+ process.stdout.write(
253
+ red("\n⚠ no worker has claimed the job — is `gonext-local-worker` running?\n")
254
+ );
255
+ }
256
+ await sleep(700);
257
+ }
258
+ } finally {
259
+ following = false;
260
+ }
258
261
  }
259
262
 
260
263
  // ---------- REPL ----------
@@ -280,10 +283,9 @@ async function main() {
280
283
 
281
284
  const history = [];
282
285
  rl.on("SIGINT", () => {
283
- if (child) {
284
- child.kill("SIGKILL");
285
- child = null;
286
- process.stdout.write(red("\n^C run aborted\n"));
286
+ if (following) {
287
+ followAborted = true;
288
+ process.stdout.write(red("\n^C "));
287
289
  } else {
288
290
  process.stdout.write(dim("\n(/exit to quit)\n") + cyan(">> "));
289
291
  }
@@ -315,14 +317,13 @@ async function main() {
315
317
  }
316
318
  history.push({ role: "user", content: line });
317
319
  try {
318
- const { payload } = await fetchAgentPayload(history);
319
- const answer = await runAgentTurn(payload);
320
+ const answer = await runAgentTurn(history);
320
321
  if (answer) {
321
322
  history.push({ role: "assistant", content: answer });
322
323
  console.log(`\n\n${answer}\n`);
323
324
  } else {
324
- history.pop(); // failed turn — don't poison the next request's history
325
- console.log(red("\n(no answer — run failed or was aborted)\n"));
325
+ history.pop(); // aborted/failed turn — don't poison the next request's history
326
+ console.log(red("\n(no answer — run aborted or failed)\n"));
326
327
  }
327
328
  } catch (err) {
328
329
  history.pop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.174",
3
+ "version": "1.0.177",
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",