orchestrating 0.1.23 → 0.1.25

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 +184 -3
  2. package/package.json +1 -1
package/bin/orch CHANGED
@@ -178,6 +178,182 @@ 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
+ async function connect() {
225
+ // Refresh token if needed before connecting
226
+ if (isTokenExpired()) {
227
+ const refreshed = await refreshAuthToken();
228
+ if (refreshed) {
229
+ token = refreshed;
230
+ process.stderr.write(`${DIM}${PREFIX} Token refreshed${RESET}\n`);
231
+ } else {
232
+ process.stderr.write(`${RED}${PREFIX} Token refresh failed — retrying in 10s${RESET}\n`);
233
+ reconnectTimer = setTimeout(connect, 10_000);
234
+ return;
235
+ }
236
+ }
237
+
238
+ ws = new WebSocket(serverUrl);
239
+
240
+ ws.on("open", () => {
241
+ ws.send(JSON.stringify({
242
+ type: "register_daemon",
243
+ token,
244
+ hostname,
245
+ cliVersion,
246
+ }));
247
+ process.stderr.write(`${GREEN}${PREFIX} Connected${RESET}\n`);
248
+
249
+ // Keepalive pings
250
+ if (pingTimer) clearInterval(pingTimer);
251
+ if (pongTimer) clearTimeout(pongTimer);
252
+ pingTimer = setInterval(() => {
253
+ if (ws && ws.readyState === WebSocket.OPEN) {
254
+ ws.ping();
255
+ pongTimer = setTimeout(() => {
256
+ if (ws) ws.terminate();
257
+ }, PONG_TIMEOUT_MS);
258
+ }
259
+ }, PING_INTERVAL_MS);
260
+ });
261
+
262
+ ws.on("pong", () => {
263
+ if (pongTimer) { clearTimeout(pongTimer); pongTimer = null; }
264
+ });
265
+
266
+ ws.on("message", (raw) => {
267
+ let msg;
268
+ try { msg = JSON.parse(raw.toString()); } catch { return; }
269
+
270
+ if (msg.type === "error") {
271
+ process.stderr.write(`${RED}${PREFIX} Server: ${msg.error}${RESET}\n`);
272
+ if (/unauthorized|auth|token/i.test(msg.error || "")) {
273
+ refreshAuthToken().then((refreshed) => {
274
+ if (refreshed) {
275
+ token = refreshed;
276
+ connect();
277
+ } else {
278
+ process.stderr.write(`${RED}${PREFIX} Auth failed. Run 'orch login'.${RESET}\n`);
279
+ process.exit(1);
280
+ }
281
+ });
282
+ }
283
+ return;
284
+ }
285
+
286
+ if (msg.type === "start_session") {
287
+ const { requestId, command, args, cwd, label, yolo } = msg;
288
+ const cmdStr = [command, ...args].join(" ");
289
+ process.stderr.write(`${GREEN}${PREFIX} Spawning: ${cmdStr}${RESET}\n`);
290
+
291
+ try {
292
+ const childArgs = [];
293
+ if (label) childArgs.push("-l", label);
294
+ if (yolo) childArgs.push("-y");
295
+ childArgs.push(command, ...args);
296
+
297
+ const child = spawn(process.execPath, [orchPath, ...childArgs], {
298
+ stdio: "ignore",
299
+ detached: true,
300
+ cwd: cwd || process.cwd(),
301
+ env: { ...process.env },
302
+ });
303
+ child.unref();
304
+
305
+ if (ws && ws.readyState === WebSocket.OPEN) {
306
+ ws.send(JSON.stringify({
307
+ type: "session_started",
308
+ requestId,
309
+ success: true,
310
+ }));
311
+ }
312
+ } catch (err) {
313
+ process.stderr.write(`${RED}${PREFIX} Spawn failed: ${err.message}${RESET}\n`);
314
+ if (ws && ws.readyState === WebSocket.OPEN) {
315
+ ws.send(JSON.stringify({
316
+ type: "session_started",
317
+ requestId,
318
+ success: false,
319
+ error: err.message,
320
+ }));
321
+ }
322
+ }
323
+ }
324
+ });
325
+
326
+ ws.on("close", () => {
327
+ ws = null;
328
+ if (pingTimer) { clearInterval(pingTimer); pingTimer = null; }
329
+ if (pongTimer) { clearTimeout(pongTimer); pongTimer = null; }
330
+ process.stderr.write(`${DIM}${PREFIX} Disconnected — reconnecting in 2s${RESET}\n`);
331
+ reconnectTimer = setTimeout(connect, 2000);
332
+ });
333
+
334
+ ws.on("error", () => {
335
+ // Will trigger close
336
+ });
337
+ }
338
+
339
+ connect();
340
+
341
+ // Graceful shutdown
342
+ const shutdown = () => {
343
+ process.stderr.write(`\n${DIM}${PREFIX} Shutting down${RESET}\n`);
344
+ if (pingTimer) clearInterval(pingTimer);
345
+ if (pongTimer) clearTimeout(pongTimer);
346
+ if (reconnectTimer) clearTimeout(reconnectTimer);
347
+ if (ws) ws.close();
348
+ process.exit(0);
349
+ };
350
+ process.on("SIGINT", shutdown);
351
+ process.on("SIGTERM", shutdown);
352
+
353
+ // Keep alive forever
354
+ await new Promise(() => {});
355
+ }
356
+
181
357
  // --- Structured adapters ---
182
358
  // Commands that match an adapter key get spawned with structured JSON I/O
183
359
  // instead of PTY wrapping. Future adapters (codex, gemini) go here.
@@ -220,6 +396,7 @@ while (i < args.length) {
220
396
  console.error("Usage: orch [-l label] [-y] <command> [args...]");
221
397
  console.error(" orch login — Authenticate with orchestrat.ing");
222
398
  console.error(" orch logout — Clear stored credentials");
399
+ console.error(" orch daemon — Run background daemon for remote session launching");
223
400
  console.error("");
224
401
  console.error(" -l <label> Optional human-readable session label");
225
402
  console.error(" -y, --yolo Skip all permission prompts (auto-approve everything)");
@@ -229,6 +406,7 @@ while (i < args.length) {
229
406
  console.error(' orch -y claude "build a website"');
230
407
  console.error(' orch -l "deploy fix" codex');
231
408
  console.error(" orch bash");
409
+ console.error(" orch daemon");
232
410
  console.error("");
233
411
  console.error("Environment:");
234
412
  console.error(" ORC_URL WebSocket server URL (default: wss://api.orchestrat.ing/ws)");
@@ -268,13 +446,15 @@ if (authToken && isTokenExpired()) {
268
446
  authToken = refreshed;
269
447
  process.stderr.write(`${DIM}[orch] Token refreshed${RESET}\n`);
270
448
  } else {
271
- process.stderr.write("\x1b[33mToken expired. Run 'orch login' to re-authenticate.\x1b[0m\n");
449
+ process.stderr.write(`${RED}Session expired. Run 'orch login' to re-authenticate.${RESET}\n`);
450
+ process.exit(1);
272
451
  }
273
452
  }
274
453
 
275
454
  // Warn if no auth and connecting to remote server
276
455
  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");
456
+ console.error(`${RED}No credentials found. Run 'orch login' to authenticate.${RESET}`);
457
+ process.exit(1);
278
458
  }
279
459
 
280
460
  // --- WebSocket connection with reconnect ---
@@ -988,8 +1168,9 @@ async function connectWs() {
988
1168
  authFailed = false;
989
1169
  connectWs();
990
1170
  } else {
991
- process.stderr.write(`${RED}Run 'orch login' to authenticate.${RESET}\n`);
1171
+ process.stderr.write(`${RED}Session expired. Run 'orch login' to re-authenticate.${RESET}\n`);
992
1172
  authFailed = true;
1173
+ process.exit(1);
993
1174
  }
994
1175
  });
995
1176
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrating",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Stream terminal sessions to the orchestrat.ing dashboard",
5
5
  "type": "module",
6
6
  "bin": {