orchestrating 0.1.22 → 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 +202 -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,19 +446,22 @@ 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 ---
281
461
  const BUFFER_MAX = 50 * 1024; // 50KB reconnect buffer
282
462
  const PING_INTERVAL_MS = 30_000; // 30s keepalive ping
283
463
  const PONG_TIMEOUT_MS = 10_000; // 10s to receive pong before assuming dead
464
+ const MAX_EVENT_HISTORY = 500; // max events to replay on reconnect
284
465
  let ws = null;
285
466
  let wsReady = false;
286
467
  let reconnectTimer = null;
@@ -289,8 +470,16 @@ let pongTimer = null;
289
470
  let authFailed = false;
290
471
  const sendBuffer = [];
291
472
  let bufferSize = 0;
473
+ const eventHistory = []; // all agent events for replay on reconnect
292
474
 
293
475
  function sendToServer(msg) {
476
+ // Track agent events for reconnect replay
477
+ if (msg.type === "agent_event" && msg.event) {
478
+ eventHistory.push(msg.event);
479
+ while (eventHistory.length > MAX_EVENT_HISTORY) {
480
+ eventHistory.shift();
481
+ }
482
+ }
294
483
  if (wsReady && ws && ws.readyState === WebSocket.OPEN) {
295
484
  ws.send(JSON.stringify(msg));
296
485
  } else {
@@ -938,6 +1127,15 @@ async function connectWs() {
938
1127
  broadcastPermissions();
939
1128
  }
940
1129
 
1130
+ // Replay event history so server can rebuild conversation after restarts
1131
+ if (eventHistory.length > 0) {
1132
+ ws.send(JSON.stringify({
1133
+ type: "agent_history_replay",
1134
+ sessionId,
1135
+ events: eventHistory,
1136
+ }));
1137
+ }
1138
+
941
1139
  // Keepalive: send WebSocket ping frames every 30s to prevent idle disconnects
942
1140
  if (pingTimer) clearInterval(pingTimer);
943
1141
  if (pongTimer) clearTimeout(pongTimer);
@@ -970,8 +1168,9 @@ async function connectWs() {
970
1168
  authFailed = false;
971
1169
  connectWs();
972
1170
  } else {
973
- 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`);
974
1172
  authFailed = true;
1173
+ process.exit(1);
975
1174
  }
976
1175
  });
977
1176
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrating",
3
- "version": "0.1.22",
3
+ "version": "0.1.25",
4
4
  "description": "Stream terminal sessions to the orchestrat.ing dashboard",
5
5
  "type": "module",
6
6
  "bin": {