orchestrating 0.1.2 → 0.1.4

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 +93 -100
  2. package/package.json +1 -1
package/bin/orch CHANGED
@@ -89,8 +89,8 @@ async function handleLogin() {
89
89
  refresh_token: refreshToken || "",
90
90
  expires_at: expiresAt ? Number(expiresAt) : 0,
91
91
  });
92
- res.writeHead(302, { Location: "https://app.orchestrat.ing" });
93
- res.end();
92
+ res.writeHead(200, { "Content-Type": "text/html" });
93
+ res.end('<html><head><meta http-equiv="refresh" content="0;url=https://app.orchestrat.ing"></head><body><p>Redirecting to dashboard...</p></body></html>');
94
94
  console.log("\x1b[32mLogged in successfully.\x1b[0m");
95
95
  } else {
96
96
  res.writeHead(400, { "Content-Type": "text/html" });
@@ -98,8 +98,7 @@ async function handleLogin() {
98
98
  console.error("Authentication failed — no token received.");
99
99
  }
100
100
 
101
- server.close();
102
- resolve();
101
+ setTimeout(() => { server.close(); resolve(); }, 500);
103
102
  } else {
104
103
  res.writeHead(404);
105
104
  res.end();
@@ -152,7 +151,6 @@ const ADAPTERS = {
152
151
  buildArgs(prompt, flags) {
153
152
  const args = [
154
153
  "--output-format", "stream-json",
155
- "--input-format", "stream-json",
156
154
  "--verbose",
157
155
  ];
158
156
  if (flags.continue) {
@@ -308,6 +306,7 @@ if (!adapter) {
308
306
 
309
307
  let child;
310
308
  let handleServerMessage; // set per-mode
309
+ let exitRequested = false;
311
310
 
312
311
  if (adapter) {
313
312
  // ======== STRUCTURED MODE (Claude Code JSON streaming) ========
@@ -329,84 +328,90 @@ if (adapter) {
329
328
  process.exit(1);
330
329
  }
331
330
 
332
- const claudeArgs = adapter.buildArgs(prompt, adapterFlags);
333
- if (yoloMode) {
334
- claudeArgs.push("--dangerously-skip-permissions");
335
- }
336
- child = spawn(command, claudeArgs, {
337
- stdio: ["pipe", "pipe", "pipe"],
338
- cwd: process.cwd(),
339
- env: (() => {
340
- const e = { ...process.env };
341
- delete e.CLAUDECODE;
342
- delete e.ANTHROPIC_API_KEY;
343
- return e;
344
- })(),
345
- });
346
-
347
- // Parse NDJSON from stdout line-by-line
348
- const rl = readline.createInterface({ input: child.stdout });
331
+ // Confirmation-type tools — these need "yes" response, not permission grants
332
+ const CONFIRMATION_TOOLS = new Set(["ExitPlanMode", "EnterPlanMode"]);
333
+ let childRunning = false;
334
+
335
+ // Build clean env for child processes (no nesting detection, no leaked keys)
336
+ const childEnv = (() => {
337
+ const e = { ...process.env };
338
+ delete e.CLAUDECODE;
339
+ delete e.ANTHROPIC_API_KEY;
340
+ return e;
341
+ })();
342
+
343
+ // Spawn a claude process and wire up output handling
344
+ function spawnClaude(claudeArgs) {
345
+ const proc = spawn(command, claudeArgs, {
346
+ stdio: ["pipe", "pipe", "pipe"],
347
+ cwd: process.cwd(),
348
+ env: childEnv,
349
+ });
350
+ childRunning = true;
351
+ child = proc;
349
352
 
350
- // Broadcast initial permission state once connected
351
- let permissionsSent = false;
353
+ // Parse NDJSON from stdout line-by-line
354
+ const rl = readline.createInterface({ input: proc.stdout });
355
+ let permissionsSent = false;
352
356
 
353
- rl.on("line", (line) => {
354
- if (!permissionsSent) {
355
- permissionsSent = true;
356
- broadcastPermissions();
357
- }
358
- if (!line.trim()) return;
359
- let raw;
360
- try {
361
- raw = JSON.parse(line);
362
- } catch {
363
- return;
364
- }
357
+ rl.on("line", (line) => {
358
+ if (!permissionsSent) {
359
+ permissionsSent = true;
360
+ broadcastPermissions();
361
+ }
362
+ if (!line.trim()) return;
363
+ let raw;
364
+ try {
365
+ raw = JSON.parse(line);
366
+ } catch {
367
+ return;
368
+ }
365
369
 
366
- // Normalize and relay each event
367
- const events = normalizeClaudeEvent(raw);
368
- for (const event of events) {
369
- printLocalEvent(event);
370
- sendToServer({ type: "agent_event", sessionId, event });
370
+ // Normalize and relay each event
371
+ const events = normalizeClaudeEvent(raw);
372
+ for (const event of events) {
373
+ printLocalEvent(event);
374
+ sendToServer({ type: "agent_event", sessionId, event });
371
375
 
372
- // Yolo mode: auto-approve permission denials immediately
373
- if (yoloMode && event.kind === "permission_denied") {
374
- process.stderr.write(`${GREEN}[yolo] Auto-approving: ${event.toolName}${RESET}\n`);
375
- approvePermission(event.toolName, "session");
376
+ // Yolo mode: auto-approve permission denials immediately
377
+ if (yoloMode && event.kind === "permission_denied") {
378
+ process.stderr.write(`${GREEN}[yolo] Auto-approving: ${event.toolName}${RESET}\n`);
379
+ approvePermission(event.toolName, "session");
380
+ }
376
381
  }
377
- }
378
- });
382
+ });
379
383
 
380
- child.stderr.on("data", (buf) => {
381
- process.stderr.write(buf);
382
- });
384
+ proc.stderr.on("data", (buf) => {
385
+ process.stderr.write(buf);
386
+ });
383
387
 
384
- child.on("exit", (code) => {
385
- const exitCode = code ?? 0;
386
- sendToServer({ type: "exit", sessionId, exitCode });
387
- setTimeout(() => process.exit(exitCode), 200);
388
- });
388
+ proc.on("exit", (code) => {
389
+ childRunning = false;
390
+ const exitCode = code ?? 0;
391
+ if (exitCode !== 0 || exitRequested) {
392
+ // Non-zero exit or user interrupted — shut down
393
+ sendToServer({ type: "exit", sessionId, exitCode });
394
+ setTimeout(() => process.exit(exitCode), 200);
395
+ } else {
396
+ // Successful completion — stay alive for follow-up from dashboard
397
+ sendToServer({
398
+ type: "agent_event", sessionId,
399
+ event: { kind: "status", status: "idle" },
400
+ });
401
+ }
402
+ });
389
403
 
390
- child.on("error", (err) => {
391
- console.error("Failed to start:", err.message);
392
- process.exit(1);
393
- });
404
+ proc.on("error", (err) => {
405
+ childRunning = false;
406
+ console.error("Failed to start:", err.message);
407
+ process.exit(1);
408
+ });
394
409
 
395
- // Confirmation-type tools — these need "yes" response, not permission grants
396
- const CONFIRMATION_TOOLS = new Set(["ExitPlanMode", "EnterPlanMode"]);
410
+ return proc;
411
+ }
397
412
 
398
413
  // Auto-approve a tool permission (used by yolo mode and manual approval)
399
414
  function approvePermission(toolName, scope) {
400
- // For confirmation prompts, just send "yes" — no settings change needed
401
- if (CONFIRMATION_TOOLS.has(toolName)) {
402
- const confirmMsg = JSON.stringify({
403
- type: "user",
404
- message: { role: "user", content: "yes" },
405
- });
406
- child.stdin.write(confirmMsg + "\n");
407
- return;
408
- }
409
-
410
415
  let permEntry = toolName;
411
416
  const mcpMatch = toolName.match(/^(mcp__[^_]+(?:__[^_]+)?)__/);
412
417
  if (mcpMatch) {
@@ -443,41 +448,28 @@ if (adapter) {
443
448
  }
444
449
 
445
450
  broadcastPermissions();
446
-
447
- const retryMsg = JSON.stringify({
448
- type: "user",
449
- message: { role: "user", content: `Permission granted for ${toolName}. Please retry.` },
450
- });
451
- child.stdin.write(retryMsg + "\n");
452
451
  }
453
452
 
454
- // No local stdin forwarding in structured mode — input comes from server only
455
- // (stdin is reserved for stream-json messages to Claude)
453
+ // Start the initial claude process
454
+ const initialArgs = adapter.buildArgs(prompt, adapterFlags);
455
+ if (yoloMode) {
456
+ initialArgs.push("--dangerously-skip-permissions");
457
+ }
458
+ spawnClaude(initialArgs);
456
459
 
457
460
  handleServerMessage = (msg) => {
458
461
  if (msg.type === "agent_input" && (msg.text || msg.images)) {
459
- // Forward user message from dashboard to Claude's stdin as stream-json
460
- let content;
461
- if (msg.images && msg.images.length > 0) {
462
- // Build content array with images + optional text
463
- content = [];
464
- for (const img of msg.images) {
465
- content.push({
466
- type: "image",
467
- source: { type: "base64", media_type: img.mediaType, data: img.data },
468
- });
469
- }
470
- if (msg.text) {
471
- content.push({ type: "text", text: msg.text });
472
- }
473
- } else {
474
- content = msg.text;
462
+ // Follow-up from dashboard spawn new claude with -c (continue)
463
+ if (childRunning) {
464
+ // Child still running, queue or ignore
465
+ process.stderr.write(`${DIM}[orch] Ignoring input claude is still running${RESET}\n`);
466
+ return;
467
+ }
468
+ const followUpArgs = ["--output-format", "stream-json", "--verbose", "-c", "-p", msg.text || "continue"];
469
+ if (yoloMode) {
470
+ followUpArgs.push("--dangerously-skip-permissions");
475
471
  }
476
- const inputMsg = JSON.stringify({
477
- type: "user",
478
- message: { role: "user", content },
479
- });
480
- child.stdin.write(inputMsg + "\n");
472
+ spawnClaude(followUpArgs);
481
473
  } else if (msg.type === "agent_permission" && msg.tool && msg.action === "allow") {
482
474
  const scope = msg.scope || "session";
483
475
  approvePermission(msg.tool, scope);
@@ -856,6 +848,7 @@ function cleanup() {
856
848
 
857
849
  process.on("SIGINT", () => {
858
850
  if (adapter) {
851
+ exitRequested = true;
859
852
  child.kill("SIGINT");
860
853
  } else {
861
854
  child.stdin.write("\x03");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrating",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Stream terminal sessions to the orchestrat.ing dashboard",
5
5
  "type": "module",
6
6
  "bin": {