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.
- package/bin/orch +202 -3
- 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(
|
|
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(
|
|
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
|
}
|