orchestrating 0.1.23 → 0.1.26

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/bin/orch +202 -3
  2. package/package.json +1 -1
package/bin/orch CHANGED
@@ -178,6 +178,200 @@ if (firstArg === "logout") {
178
178
  process.exit(0);
179
179
  }
180
180
 
181
+ if (firstArg === "daemon") {
182
+ await handleDaemon();
183
+ // handleDaemon runs forever (or exits on fatal error)
184
+ }
185
+
186
+ // --- Daemon mode ---
187
+ async function handleDaemon() {
188
+ const DIM = "\x1b[2m";
189
+ const GREEN = "\x1b[32m";
190
+ const RED = "\x1b[31m";
191
+ const RESET = "\x1b[0m";
192
+ const PREFIX = "[daemon]";
193
+
194
+ let token = getAuthToken();
195
+ if (!token) {
196
+ console.error(`${RED}No credentials found. Run 'orch login' to authenticate.${RESET}`);
197
+ process.exit(1);
198
+ }
199
+ if (isTokenExpired()) {
200
+ const refreshed = await refreshAuthToken();
201
+ if (refreshed) {
202
+ token = refreshed;
203
+ } else {
204
+ console.error(`${RED}Session expired. Run 'orch login' to re-authenticate.${RESET}`);
205
+ process.exit(1);
206
+ }
207
+ }
208
+
209
+ const hostname = os.hostname().replace(/\.(lan|local|home|internal)$/i, "");
210
+ const cliVersion = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")).version;
211
+ const serverUrl = process.env.ORC_URL || process.env.CAST_URL || "wss://api.orchestrat.ing/ws";
212
+ const orchPath = fileURLToPath(import.meta.url);
213
+
214
+ const PING_INTERVAL_MS = 30_000;
215
+ const PONG_TIMEOUT_MS = 10_000;
216
+ let ws = null;
217
+ let pingTimer = null;
218
+ let pongTimer = null;
219
+ let reconnectTimer = null;
220
+
221
+ process.stderr.write(`${GREEN}${PREFIX} Listening for remote sessions as "${hostname}"${RESET}\n`);
222
+ process.stderr.write(`${DIM}${PREFIX} Tip: Use 'nohup orch daemon &' to run in background${RESET}\n`);
223
+
224
+ let reconnecting = false;
225
+
226
+ function scheduleReconnect(delaySec) {
227
+ if (reconnecting) return;
228
+ reconnecting = true;
229
+ if (reconnectTimer) clearTimeout(reconnectTimer);
230
+ reconnectTimer = setTimeout(() => {
231
+ reconnecting = false;
232
+ connect();
233
+ }, delaySec * 1000);
234
+ }
235
+
236
+ async function connect() {
237
+ // Refresh token if needed before connecting
238
+ if (isTokenExpired()) {
239
+ const refreshed = await refreshAuthToken();
240
+ if (refreshed) {
241
+ token = refreshed;
242
+ process.stderr.write(`${DIM}${PREFIX} Token refreshed${RESET}\n`);
243
+ } else {
244
+ process.stderr.write(`${RED}${PREFIX} Token refresh failed — retrying in 10s${RESET}\n`);
245
+ scheduleReconnect(10);
246
+ return;
247
+ }
248
+ }
249
+
250
+ if (ws) {
251
+ ws.removeAllListeners();
252
+ ws.close();
253
+ }
254
+ const sock = new WebSocket(serverUrl);
255
+ ws = sock;
256
+
257
+ sock.on("open", () => {
258
+ sock.send(JSON.stringify({
259
+ type: "register_daemon",
260
+ token,
261
+ hostname,
262
+ cliVersion,
263
+ }));
264
+ process.stderr.write(`${GREEN}${PREFIX} Connected${RESET}\n`);
265
+
266
+ // Keepalive pings
267
+ if (pingTimer) clearInterval(pingTimer);
268
+ if (pongTimer) clearTimeout(pongTimer);
269
+ pingTimer = setInterval(() => {
270
+ if (sock.readyState === WebSocket.OPEN) {
271
+ sock.ping();
272
+ pongTimer = setTimeout(() => {
273
+ sock.terminate();
274
+ }, PONG_TIMEOUT_MS);
275
+ }
276
+ }, PING_INTERVAL_MS);
277
+ });
278
+
279
+ sock.on("pong", () => {
280
+ if (pongTimer) { clearTimeout(pongTimer); pongTimer = null; }
281
+ });
282
+
283
+ sock.on("message", (raw) => {
284
+ let msg;
285
+ try { msg = JSON.parse(raw.toString()); } catch { return; }
286
+
287
+ if (msg.type === "error") {
288
+ process.stderr.write(`${RED}${PREFIX} Server: ${msg.error}${RESET}\n`);
289
+ if (/unauthorized|auth|token/i.test(msg.error || "")) {
290
+ // Don't reconnect from here — let the close handler do it after refreshing
291
+ refreshAuthToken().then((refreshed) => {
292
+ if (refreshed) {
293
+ token = refreshed;
294
+ process.stderr.write(`${DIM}${PREFIX} Token refreshed — will reconnect${RESET}\n`);
295
+ } else {
296
+ process.stderr.write(`${RED}${PREFIX} Auth failed. Run 'orch login'.${RESET}\n`);
297
+ process.exit(1);
298
+ }
299
+ });
300
+ }
301
+ return;
302
+ }
303
+
304
+ if (msg.type === "start_session") {
305
+ const { requestId, command: cmd, args: cmdArgs, cwd, label: lbl, yolo } = msg;
306
+ const cmdStr = [cmd, ...cmdArgs].join(" ");
307
+ process.stderr.write(`${GREEN}${PREFIX} Spawning: ${cmdStr}${RESET}\n`);
308
+
309
+ try {
310
+ const childArgs = [];
311
+ if (lbl) childArgs.push("-l", lbl);
312
+ if (yolo) childArgs.push("-y");
313
+ childArgs.push(cmd, ...cmdArgs);
314
+
315
+ const child = spawn(process.execPath, [orchPath, ...childArgs], {
316
+ stdio: "ignore",
317
+ detached: true,
318
+ cwd: cwd || process.cwd(),
319
+ env: { ...process.env },
320
+ });
321
+ child.unref();
322
+
323
+ if (sock.readyState === WebSocket.OPEN) {
324
+ sock.send(JSON.stringify({
325
+ type: "session_started",
326
+ requestId,
327
+ success: true,
328
+ }));
329
+ }
330
+ } catch (err) {
331
+ process.stderr.write(`${RED}${PREFIX} Spawn failed: ${err.message}${RESET}\n`);
332
+ if (sock.readyState === WebSocket.OPEN) {
333
+ sock.send(JSON.stringify({
334
+ type: "session_started",
335
+ requestId,
336
+ success: false,
337
+ error: err.message,
338
+ }));
339
+ }
340
+ }
341
+ }
342
+ });
343
+
344
+ sock.on("close", () => {
345
+ if (ws === sock) ws = null;
346
+ if (pingTimer) { clearInterval(pingTimer); pingTimer = null; }
347
+ if (pongTimer) { clearTimeout(pongTimer); pongTimer = null; }
348
+ process.stderr.write(`${DIM}${PREFIX} Disconnected — reconnecting in 2s${RESET}\n`);
349
+ scheduleReconnect(2);
350
+ });
351
+
352
+ sock.on("error", () => {
353
+ // Will trigger close
354
+ });
355
+ }
356
+
357
+ connect();
358
+
359
+ // Graceful shutdown
360
+ const shutdown = () => {
361
+ process.stderr.write(`\n${DIM}${PREFIX} Shutting down${RESET}\n`);
362
+ if (pingTimer) clearInterval(pingTimer);
363
+ if (pongTimer) clearTimeout(pongTimer);
364
+ if (reconnectTimer) clearTimeout(reconnectTimer);
365
+ if (ws) ws.close();
366
+ process.exit(0);
367
+ };
368
+ process.on("SIGINT", shutdown);
369
+ process.on("SIGTERM", shutdown);
370
+
371
+ // Keep alive forever
372
+ await new Promise(() => {});
373
+ }
374
+
181
375
  // --- Structured adapters ---
182
376
  // Commands that match an adapter key get spawned with structured JSON I/O
183
377
  // instead of PTY wrapping. Future adapters (codex, gemini) go here.
@@ -220,6 +414,7 @@ while (i < args.length) {
220
414
  console.error("Usage: orch [-l label] [-y] <command> [args...]");
221
415
  console.error(" orch login — Authenticate with orchestrat.ing");
222
416
  console.error(" orch logout — Clear stored credentials");
417
+ console.error(" orch daemon — Run background daemon for remote session launching");
223
418
  console.error("");
224
419
  console.error(" -l <label> Optional human-readable session label");
225
420
  console.error(" -y, --yolo Skip all permission prompts (auto-approve everything)");
@@ -229,6 +424,7 @@ while (i < args.length) {
229
424
  console.error(' orch -y claude "build a website"');
230
425
  console.error(' orch -l "deploy fix" codex');
231
426
  console.error(" orch bash");
427
+ console.error(" orch daemon");
232
428
  console.error("");
233
429
  console.error("Environment:");
234
430
  console.error(" ORC_URL WebSocket server URL (default: wss://api.orchestrat.ing/ws)");
@@ -268,13 +464,15 @@ if (authToken && isTokenExpired()) {
268
464
  authToken = refreshed;
269
465
  process.stderr.write(`${DIM}[orch] Token refreshed${RESET}\n`);
270
466
  } else {
271
- process.stderr.write("\x1b[33mToken expired. Run 'orch login' to re-authenticate.\x1b[0m\n");
467
+ process.stderr.write(`${RED}Session expired. Run 'orch login' to re-authenticate.${RESET}\n`);
468
+ process.exit(1);
272
469
  }
273
470
  }
274
471
 
275
472
  // Warn if no auth and connecting to remote server
276
473
  if (!authToken && !serverUrl.includes("localhost") && !serverUrl.includes("127.0.0.1")) {
277
- console.error("\x1b[33mNo credentials found. Run 'orch login' to authenticate.\x1b[0m");
474
+ console.error(`${RED}No credentials found. Run 'orch login' to authenticate.${RESET}`);
475
+ process.exit(1);
278
476
  }
279
477
 
280
478
  // --- WebSocket connection with reconnect ---
@@ -988,8 +1186,9 @@ async function connectWs() {
988
1186
  authFailed = false;
989
1187
  connectWs();
990
1188
  } else {
991
- process.stderr.write(`${RED}Run 'orch login' to authenticate.${RESET}\n`);
1189
+ process.stderr.write(`${RED}Session expired. Run 'orch login' to re-authenticate.${RESET}\n`);
992
1190
  authFailed = true;
1191
+ process.exit(1);
993
1192
  }
994
1193
  });
995
1194
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrating",
3
- "version": "0.1.23",
3
+ "version": "0.1.26",
4
4
  "description": "Stream terminal sessions to the orchestrat.ing dashboard",
5
5
  "type": "module",
6
6
  "bin": {