@tiens.nguyen/gonext-local-worker 1.0.7 → 1.0.9

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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @tiens.nguyen/gonext-local-worker
2
+ Run:
3
+ GONEXT_API_BASE=... GONEXT_WORKER_KEY=... npx -y --package @tiens.nguyen/gonext-local-worker gonext-local-worker
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GoNext local worker:
4
+ * - `gonext-local-worker set <workerKey> [--api-base URL] [--poll-ms 1500]`
5
+ * writes ~/.gonext/worker.env
6
+ * - `gonext-local-worker` starts polling loop
7
+ */
8
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ import dotenv from "dotenv";
12
+ import OpenAI from "openai";
13
+
14
+ const ENV_FILE = join(homedir(), ".gonext", "worker.env");
15
+ dotenv.config({ path: ENV_FILE });
16
+ dotenv.config();
17
+
18
+ const args = process.argv.slice(2);
19
+
20
+ function printHelp() {
21
+ console.log(`
22
+ gonext-local-worker
23
+
24
+ Usage:
25
+ gonext-local-worker
26
+ gonext-local-worker set <workerKey> [--api-base <url>] [--poll-ms <ms>]
27
+
28
+ Examples:
29
+ gonext-local-worker set abc123 --api-base https://hwohu56e8d.execute-api.ap-southeast-1.amazonaws.com
30
+ gonext-local-worker
31
+ `);
32
+ }
33
+
34
+ function parseFlag(name) {
35
+ const idx = args.indexOf(name);
36
+ if (idx >= 0 && args[idx + 1]) {
37
+ return args[idx + 1];
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ async function setConfig() {
43
+ const workerKey = args[1]?.trim();
44
+ if (!workerKey) {
45
+ console.error("Missing worker key. Usage: gonext-local-worker set <workerKey>");
46
+ process.exit(1);
47
+ }
48
+ const currentRaw = await readFile(ENV_FILE, "utf8").catch(() => "");
49
+ const current = dotenv.parse(currentRaw);
50
+ const apiBaseFromFlag = parseFlag("--api-base");
51
+ const pollMsFromFlag = parseFlag("--poll-ms");
52
+ const next = {
53
+ GONEXT_API_BASE: (
54
+ apiBaseFromFlag ??
55
+ current.GONEXT_API_BASE ??
56
+ process.env.GONEXT_API_BASE ??
57
+ ""
58
+ ).replace(/\/+$/, ""),
59
+ GONEXT_WORKER_KEY: workerKey,
60
+ GONEXT_POLL_MS:
61
+ pollMsFromFlag ?? current.GONEXT_POLL_MS ?? process.env.GONEXT_POLL_MS ?? "1500",
62
+ };
63
+ await mkdir(join(homedir(), ".gonext"), { recursive: true });
64
+ await writeFile(
65
+ ENV_FILE,
66
+ `GONEXT_API_BASE=${next.GONEXT_API_BASE}\nGONEXT_WORKER_KEY=${next.GONEXT_WORKER_KEY}\nGONEXT_POLL_MS=${next.GONEXT_POLL_MS}\n`,
67
+ "utf8"
68
+ );
69
+ console.log(`Saved ${ENV_FILE}`);
70
+ if (!next.GONEXT_API_BASE) {
71
+ console.log("Tip: set API base too: gonext-local-worker set <workerKey> --api-base <https-url>");
72
+ }
73
+ return;
74
+ }
75
+
76
+ if (args.includes("--help") || args.includes("-h")) {
77
+ printHelp();
78
+ process.exit(0);
79
+ }
80
+ if (args[0] === "set") {
81
+ await setConfig();
82
+ process.exit(0);
83
+ }
84
+
85
+ const apiBase = (process.env.GONEXT_API_BASE ?? "").replace(/\/+$/, "");
86
+ const workerKey = process.env.GONEXT_WORKER_KEY ?? "";
87
+ const pollMs = Number(process.env.GONEXT_POLL_MS ?? "1500") || 1500;
88
+
89
+ if (!apiBase || !workerKey) {
90
+ console.error(
91
+ "Set GONEXT_API_BASE (HTTP API origin, no /api suffix) and GONEXT_WORKER_KEY."
92
+ );
93
+ process.exit(1);
94
+ }
95
+
96
+ function toOpenAIMessages(messages) {
97
+ return messages.map((m) => {
98
+ if (m.role === "user" && m.attachments?.length) {
99
+ return {
100
+ role: m.role,
101
+ content: [
102
+ { type: "text", text: m.content },
103
+ ...m.attachments.map((a) => ({
104
+ type: "image_url",
105
+ image_url: { url: `data:${a.mimeType};base64,${a.data}` },
106
+ })),
107
+ ],
108
+ };
109
+ }
110
+ return { role: m.role, content: m.content };
111
+ });
112
+ }
113
+
114
+ async function workerFetch(path, init = {}) {
115
+ const url = `${apiBase}${path.startsWith("/") ? path : `/${path}`}`;
116
+ const headers = {
117
+ "Content-Type": "application/json",
118
+ "X-Worker-Key": workerKey,
119
+ ...(init.headers ?? {}),
120
+ };
121
+ return fetch(url, { ...init, headers });
122
+ }
123
+
124
+ async function runChatJob(job) {
125
+ const { jobId, payload } = job;
126
+ const start = Date.now();
127
+ await workerFetch(`/api/worker/jobs/${jobId}`, {
128
+ method: "PATCH",
129
+ body: JSON.stringify({ jobStatus: "running" }),
130
+ });
131
+
132
+ const client = new OpenAI({
133
+ baseURL: payload.baseURL,
134
+ apiKey: payload.apiKey || "ollama",
135
+ });
136
+
137
+ try {
138
+ const completion = await client.chat.completions.create({
139
+ model: payload.modelId,
140
+ messages: toOpenAIMessages(payload.messages),
141
+ temperature: 0,
142
+ });
143
+ const text = completion.choices[0]?.message?.content ?? "";
144
+ const totalTimeSeconds = (Date.now() - start) / 1000;
145
+ await workerFetch(`/api/worker/jobs/${jobId}`, {
146
+ method: "PATCH",
147
+ body: JSON.stringify({
148
+ jobStatus: "completed",
149
+ resultText: text,
150
+ tokenCount: 1,
151
+ totalTimeSeconds,
152
+ }),
153
+ });
154
+ console.log(`[gonext-worker] completed ${jobId} (${totalTimeSeconds.toFixed(1)}s)`);
155
+ } catch (e) {
156
+ const message = e instanceof Error ? e.message : String(e);
157
+ await workerFetch(`/api/worker/jobs/${jobId}`, {
158
+ method: "PATCH",
159
+ body: JSON.stringify({
160
+ jobStatus: "failed",
161
+ errorMessage: message,
162
+ totalTimeSeconds: (Date.now() - start) / 1000,
163
+ }),
164
+ });
165
+ console.error(`[gonext-worker] failed ${jobId}:`, message);
166
+ }
167
+ }
168
+
169
+ async function pollOnce() {
170
+ const res = await workerFetch("/api/worker/jobs/next", { method: "POST" });
171
+ if (res.status === 204) return;
172
+ if (!res.ok) {
173
+ const t = await res.text().catch(() => "");
174
+ throw new Error(`next failed ${res.status}: ${t}`);
175
+ }
176
+ const job = await res.json();
177
+ if (job?.jobId) {
178
+ await runChatJob(job);
179
+ }
180
+ }
181
+
182
+ async function main() {
183
+ console.log(`[gonext-worker] polling ${apiBase} every ${pollMs}ms`);
184
+ for (;;) {
185
+ try {
186
+ await pollOnce();
187
+ } catch (e) {
188
+ console.error("[gonext-worker] poll error:", e);
189
+ }
190
+ await new Promise((r) => setTimeout(r, pollMs));
191
+ }
192
+ }
193
+
194
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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",