orchestrating 0.2.0 → 0.3.0

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