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.
- package/bin/orch +184 -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,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(
|
|
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 ---
|
|
@@ -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
|
}
|