orchestrating 0.2.0 → 0.3.1

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 +56 -57
  2. package/package.json +1 -1
package/bin/orch CHANGED
@@ -750,14 +750,14 @@ const ADAPTERS = {
750
750
  buildArgs(prompt, flags) {
751
751
  const args = [
752
752
  "--output-format", "stream-json",
753
+ "--input-format", "stream-json",
753
754
  "--verbose",
754
755
  ];
755
756
  if (flags.continue) {
756
757
  args.push("-c");
757
- if (prompt) args.push("-p", prompt);
758
- } else {
759
- args.push("-p", prompt);
760
758
  }
759
+ // With --input-format stream-json, the initial prompt is sent via stdin
760
+ // so we don't use -p here. The prompt is sent after spawn.
761
761
  return args;
762
762
  },
763
763
  mode: "structured",
@@ -829,6 +829,7 @@ if (commandArgs.length === 0) {
829
829
  }
830
830
 
831
831
  // Resolve claude binary: prefer bundled @anthropic-ai/claude-code, fall back to system PATH
832
+ const commandName = commandArgs[0]; // original name for adapter lookup
832
833
  let command = commandArgs[0];
833
834
  let claudeBundled = false;
834
835
  if (command === "claude") {
@@ -952,7 +953,7 @@ function removePermission(tool) {
952
953
  }
953
954
 
954
955
  // --- Determine mode: structured adapter or PTY ---
955
- const adapter = ADAPTERS[command];
956
+ const adapter = ADAPTERS[commandName];
956
957
 
957
958
  // Check for python3 in PTY mode
958
959
  if (!adapter) {
@@ -1016,10 +1017,13 @@ if (adapter) {
1016
1017
  return e;
1017
1018
  })();
1018
1019
 
1019
- // Spawn a claude process and wire up output handling
1020
+ // Spawn claude with bidirectional stdin/stdout (stream-json mode)
1021
+ // Follow-ups are sent via stdin — no more kill/respawn.
1022
+ let claudeSessionId = null; // Claude's internal session ID (from init event)
1023
+
1020
1024
  function spawnClaude(claudeArgs) {
1021
1025
  const proc = spawn(command, claudeArgs, {
1022
- stdio: ["ignore", "pipe", "pipe"],
1026
+ stdio: ["pipe", "pipe", "pipe"],
1023
1027
  cwd: process.cwd(),
1024
1028
  env: childEnv,
1025
1029
  });
@@ -1043,18 +1047,28 @@ if (adapter) {
1043
1047
  return;
1044
1048
  }
1045
1049
 
1050
+ // Capture Claude's session ID for follow-ups
1051
+ if (raw.type === "system" && raw.subtype === "init" && raw.session_id) {
1052
+ claudeSessionId = raw.session_id;
1053
+ }
1054
+
1055
+ // Detect turn completion — Claude finished responding, ready for next input
1056
+ if (raw.type === "result") {
1057
+ childRunning = false; // "idle" — accepting input
1058
+ }
1059
+
1046
1060
  // Normalize and relay each event
1047
1061
  const events = normalizeClaudeEvent(raw);
1048
1062
  for (const event of events) {
1049
1063
  printLocalEvent(event);
1050
1064
  sendToServer({ type: "agent_event", sessionId, event });
1051
1065
 
1052
- // Yolo mode: auto-approve permission denials — save to settings,
1053
- // claude will pick it up on respawn via pendingPermissionGrant
1066
+ // Yolo mode: auto-approve permission denials
1054
1067
  if (yoloMode && event.kind === "permission_denied") {
1055
1068
  process.stderr.write(`${GREEN}[yolo] Auto-approving: ${event.toolName}${RESET}\n`);
1056
1069
  approvePermission(event.toolName, "session");
1057
- pendingPermissionGrant = event.toolName;
1070
+ // Send follow-up via stdin to retry (no respawn needed)
1071
+ sendFollowUp(`Permission for ${event.toolName} was granted. Please retry your last action.`);
1058
1072
  }
1059
1073
  }
1060
1074
  });
@@ -1066,21 +1080,8 @@ if (adapter) {
1066
1080
  proc.on("exit", (code) => {
1067
1081
  childRunning = false;
1068
1082
  const exitCode = code ?? 0;
1069
- if (exitCode !== 0 || exitRequested) {
1070
- sendToServer({ type: "exit", sessionId, exitCode });
1071
- setTimeout(() => process.exit(exitCode), 200);
1072
- } else if (pendingPermissionGrant) {
1073
- // Permission was granted while claude was running — respawn to retry
1074
- const tool = pendingPermissionGrant;
1075
- pendingPermissionGrant = null;
1076
- process.stderr.write(`${GREEN}Retrying with new permission: ${tool}${RESET}\n`);
1077
- respawnWithContinue(`Permission for ${tool} was granted. Please retry your last action.`);
1078
- } else {
1079
- sendToServer({
1080
- type: "agent_event", sessionId,
1081
- event: { kind: "status", status: "idle" },
1082
- });
1083
- }
1083
+ sendToServer({ type: "exit", sessionId, exitCode });
1084
+ setTimeout(() => process.exit(exitCode), 200);
1084
1085
  });
1085
1086
 
1086
1087
  proc.on("error", (err) => {
@@ -1092,6 +1093,24 @@ if (adapter) {
1092
1093
  return proc;
1093
1094
  }
1094
1095
 
1096
+ // Send a follow-up message via stdin (no respawn!)
1097
+ function sendFollowUp(text) {
1098
+ if (!child || child.exitCode !== null) return;
1099
+ const msg = {
1100
+ type: "user",
1101
+ message: {
1102
+ role: "user",
1103
+ content: text,
1104
+ },
1105
+ };
1106
+ try {
1107
+ child.stdin.write(JSON.stringify(msg) + "\n");
1108
+ childRunning = true; // Claude is now processing
1109
+ } catch (err) {
1110
+ process.stderr.write(`${RED}[orch] Failed to send via stdin: ${err.message}${RESET}\n`);
1111
+ }
1112
+ }
1113
+
1095
1114
  // Auto-approve a tool permission (used by yolo mode and manual approval)
1096
1115
  function approvePermission(toolName, scope) {
1097
1116
  let permEntry = toolName;
@@ -1194,37 +1213,29 @@ if (adapter) {
1194
1213
  }
1195
1214
  }
1196
1215
 
1197
- // Start the initial claude process
1216
+ // Start the claude process (single process for entire session — no respawning)
1198
1217
  const initialArgs = adapter.buildArgs(prompt, adapterFlags);
1199
1218
  if (yoloMode) {
1200
1219
  initialArgs.push("--dangerously-skip-permissions");
1201
1220
  }
1202
1221
  spawnClaude(initialArgs);
1203
1222
 
1204
- // Emit the initial prompt as a user message so the dashboard shows it
1223
+ // Send initial prompt via stdin (not -p flag)
1205
1224
  if (prompt) {
1225
+ sendFollowUp(prompt);
1226
+ // Emit the initial prompt as a user message so the dashboard shows it
1206
1227
  sendToServer({
1207
1228
  type: "agent_event", sessionId,
1208
1229
  event: { kind: "user_message", text: prompt },
1209
1230
  });
1210
1231
  }
1211
1232
 
1212
- // Respawn claude with -c to continue after permission grants or follow-up
1213
- function respawnWithContinue(prompt) {
1214
- const args = ["--output-format", "stream-json", "--verbose", "-c"];
1215
- if (prompt) args.push("-p", prompt);
1216
- if (yoloMode) args.push("--dangerously-skip-permissions");
1217
- spawnClaude(args);
1218
- }
1219
-
1220
- // Queue of permission grants received while claude is still running
1221
- let pendingPermissionGrant = null;
1222
-
1223
1233
  handleServerMessage = (msg) => {
1224
1234
  if (msg.type === "agent_input" && (msg.text || msg.images)) {
1225
- // Follow-up from dashboard — spawn new claude with -c (continue)
1235
+ // Follow-up from dashboard — send via stdin (no respawn!)
1226
1236
  if (childRunning) {
1227
- process.stderr.write(`${DIM}[orch] Ignoring input — claude is still running${RESET}\n`);
1237
+ process.stderr.write(`${DIM}[orch] Queuing input — claude is still processing${RESET}\n`);
1238
+ // TODO: queue and send after current turn completes
1228
1239
  return;
1229
1240
  }
1230
1241
  let promptText = msg.text || "continue";
@@ -1241,28 +1252,16 @@ if (adapter) {
1241
1252
  const fileList = imagePaths.map((p) => `[Attached image: ${p} — use Read tool to view]`).join("\n");
1242
1253
  promptText = `${fileList}\n\n${promptText}`;
1243
1254
  }
1244
- respawnWithContinue(promptText);
1255
+ sendFollowUp(promptText);
1245
1256
  } else if (msg.type === "agent_permission" && msg.tool && msg.action === "allow") {
1246
1257
  const scope = msg.scope || "session";
1247
1258
  approvePermission(msg.tool, scope);
1248
1259
  process.stderr.write(`${GREEN}Permission granted (${scope}): ${msg.tool}${RESET}\n`);
1249
-
1250
- if (childRunning) {
1251
- // Claude is still running — remember the grant and respawn when it exits
1252
- pendingPermissionGrant = msg.tool;
1253
- } else {
1254
- // Claude already finished — respawn with -c to retry with new permission
1255
- respawnWithContinue(`Permission for ${msg.tool} was granted. Please retry your last action.`);
1256
- }
1260
+ // Send permission grant as follow-up via stdin
1261
+ sendFollowUp(`Permission for ${msg.tool} was granted. Please retry your last action.`);
1257
1262
  } else if (msg.type === "agent_permission" && msg.tool && msg.action === "deny") {
1258
1263
  process.stderr.write(`${RED}Permission denied: ${msg.tool}${RESET}\n`);
1259
-
1260
- if (childRunning) {
1261
- // Claude is still running — remember and respawn when it exits
1262
- pendingPermissionGrant = null;
1263
- } else {
1264
- respawnWithContinue(`Permission for ${msg.tool} was denied by the user. Do not retry this tool — find an alternative approach or skip this step.`);
1265
- }
1264
+ sendFollowUp(`Permission for ${msg.tool} was denied by the user. Do not retry this tool — find an alternative approach or skip this step.`);
1266
1265
  } else if (msg.type === "agent_permission" && msg.tool && msg.action === "revoke") {
1267
1266
  removePermission(msg.tool);
1268
1267
  broadcastPermissions();
@@ -1274,7 +1273,7 @@ if (adapter) {
1274
1273
  } else if (msg.type === "stop_session") {
1275
1274
  process.stderr.write(`${RED}[orch] Stopped from dashboard${RESET}\n`);
1276
1275
  exitRequested = true;
1277
- if (childRunning) {
1276
+ if (child && child.exitCode === null) {
1278
1277
  child.kill("SIGINT");
1279
1278
  } else {
1280
1279
  cleanup();
@@ -1586,7 +1585,7 @@ async function connectWs() {
1586
1585
  token: authToken,
1587
1586
  sessionId,
1588
1587
  label: effectiveLabel,
1589
- command,
1588
+ command: commandName,
1590
1589
  args: spawnArgs,
1591
1590
  cwd: process.cwd(),
1592
1591
  hostname,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrating",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Stream terminal sessions to the orchestrat.ing dashboard",
5
5
  "type": "module",
6
6
  "bin": {