orchestrating 0.1.39 → 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 +121 -58
  2. package/package.json +3 -2
package/bin/orch CHANGED
@@ -242,6 +242,50 @@ if (firstArg === "logout") {
242
242
  process.exit(0);
243
243
  }
244
244
 
245
+ // --- Config subcommand: manage API key and settings ---
246
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
247
+
248
+ function loadConfig() {
249
+ try {
250
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
251
+ } catch {
252
+ return {};
253
+ }
254
+ }
255
+
256
+ function saveConfig(data) {
257
+ mkdirSync(CONFIG_DIR, { recursive: true });
258
+ writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2) + "\n");
259
+ }
260
+
261
+ if (firstArg === "config") {
262
+ const configArgs = process.argv.slice(3);
263
+ if (configArgs[0] === "set" && configArgs[1] === "api-key" && configArgs[2]) {
264
+ const config = loadConfig();
265
+ config.anthropic_api_key = configArgs[2];
266
+ saveConfig(config);
267
+ console.log("Anthropic API key saved. Claude Code will use this key automatically.");
268
+ } else if (configArgs[0] === "get" && configArgs[1] === "api-key") {
269
+ const config = loadConfig();
270
+ if (config.anthropic_api_key) {
271
+ console.log(`API key: ${config.anthropic_api_key.slice(0, 10)}…${config.anthropic_api_key.slice(-4)}`);
272
+ } else {
273
+ console.log("No API key configured. Run: orch config set api-key <your-anthropic-key>");
274
+ }
275
+ } else if (configArgs[0] === "unset" && configArgs[1] === "api-key") {
276
+ const config = loadConfig();
277
+ delete config.anthropic_api_key;
278
+ saveConfig(config);
279
+ console.log("API key removed.");
280
+ } else {
281
+ console.error("Usage:");
282
+ console.error(" orch config set api-key <key> Save your Anthropic API key");
283
+ console.error(" orch config get api-key Show stored API key");
284
+ console.error(" orch config unset api-key Remove stored API key");
285
+ }
286
+ process.exit(0);
287
+ }
288
+
245
289
  if (firstArg === "daemon") {
246
290
  const daemonArgs = process.argv.slice(3);
247
291
  const pidFile = path.join(os.homedir(), ".orch-daemon.pid");
@@ -706,14 +750,14 @@ const ADAPTERS = {
706
750
  buildArgs(prompt, flags) {
707
751
  const args = [
708
752
  "--output-format", "stream-json",
753
+ "--input-format", "stream-json",
709
754
  "--verbose",
710
755
  ];
711
756
  if (flags.continue) {
712
757
  args.push("-c");
713
- if (prompt) args.push("-p", prompt);
714
- } else {
715
- args.push("-p", prompt);
716
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.
717
761
  return args;
718
762
  },
719
763
  mode: "structured",
@@ -740,6 +784,7 @@ while (i < args.length) {
740
784
  console.error("Usage: orch [-l label] [-y] <command> [args...]");
741
785
  console.error(" orch login — Authenticate with orchestrat.ing");
742
786
  console.error(" orch logout — Clear stored credentials");
787
+ console.error(" orch config — Manage settings (API keys, etc.)");
743
788
  console.error(" orch daemon — Run background daemon for remote session launching");
744
789
  console.error("");
745
790
  console.error(" -l <label> Optional human-readable session label");
@@ -763,6 +808,10 @@ while (i < args.length) {
763
808
  console.error(" orch daemon -b");
764
809
  console.error(" orch daemon --enable");
765
810
  console.error("");
811
+ console.error("Setup (one-time):");
812
+ console.error(" orch login");
813
+ console.error(' orch config set api-key sk-ant-... # Your Anthropic API key');
814
+ console.error("");
766
815
  console.error("Environment:");
767
816
  console.error(" ORC_URL WebSocket server URL (default: wss://api.orchestrat.ing/ws)");
768
817
  console.error(" ORC_TOKEN Auth token (overrides stored credentials)");
@@ -779,7 +828,19 @@ if (commandArgs.length === 0) {
779
828
  process.exit(1);
780
829
  }
781
830
 
782
- const command = commandArgs[0];
831
+ // Resolve claude binary: prefer bundled @anthropic-ai/claude-code, fall back to system PATH
832
+ let command = commandArgs[0];
833
+ let claudeBundled = false;
834
+ if (command === "claude") {
835
+ try {
836
+ // The npm package has a bin entry that npm links globally; for local use, resolve directly
837
+ const bundledBin = path.join(__dirname, "..", "node_modules", ".bin", "claude");
838
+ if (existsSync(bundledBin)) {
839
+ command = bundledBin;
840
+ claudeBundled = true;
841
+ }
842
+ } catch {}
843
+ }
783
844
  const spawnArgs = commandArgs.slice(1);
784
845
  let sessionId;
785
846
  const hostname = os.hostname().replace(/\.(lan|local|home|internal)$/i, "");
@@ -943,18 +1004,25 @@ if (adapter) {
943
1004
  // Confirmation-type tools — these need "yes" response, not permission grants
944
1005
  const CONFIRMATION_TOOLS = new Set(["ExitPlanMode", "EnterPlanMode"]);
945
1006
 
946
- // Build clean env for child processes (no nesting detection, no leaked keys)
1007
+ // Build clean env for child processes
1008
+ // Inject stored Anthropic API key if available (so users don't need separate `claude login`)
947
1009
  const childEnv = (() => {
948
1010
  const e = { ...process.env };
949
1011
  delete e.CLAUDECODE;
950
- delete e.ANTHROPIC_API_KEY;
1012
+ const config = loadConfig();
1013
+ if (config.anthropic_api_key && !e.ANTHROPIC_API_KEY) {
1014
+ e.ANTHROPIC_API_KEY = config.anthropic_api_key;
1015
+ }
951
1016
  return e;
952
1017
  })();
953
1018
 
954
- // 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
+
955
1023
  function spawnClaude(claudeArgs) {
956
1024
  const proc = spawn(command, claudeArgs, {
957
- stdio: ["ignore", "pipe", "pipe"],
1025
+ stdio: ["pipe", "pipe", "pipe"],
958
1026
  cwd: process.cwd(),
959
1027
  env: childEnv,
960
1028
  });
@@ -978,18 +1046,28 @@ if (adapter) {
978
1046
  return;
979
1047
  }
980
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
+
981
1059
  // Normalize and relay each event
982
1060
  const events = normalizeClaudeEvent(raw);
983
1061
  for (const event of events) {
984
1062
  printLocalEvent(event);
985
1063
  sendToServer({ type: "agent_event", sessionId, event });
986
1064
 
987
- // Yolo mode: auto-approve permission denials — save to settings,
988
- // claude will pick it up on respawn via pendingPermissionGrant
1065
+ // Yolo mode: auto-approve permission denials
989
1066
  if (yoloMode && event.kind === "permission_denied") {
990
1067
  process.stderr.write(`${GREEN}[yolo] Auto-approving: ${event.toolName}${RESET}\n`);
991
1068
  approvePermission(event.toolName, "session");
992
- 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.`);
993
1071
  }
994
1072
  }
995
1073
  });
@@ -1001,21 +1079,8 @@ if (adapter) {
1001
1079
  proc.on("exit", (code) => {
1002
1080
  childRunning = false;
1003
1081
  const exitCode = code ?? 0;
1004
- if (exitCode !== 0 || exitRequested) {
1005
- sendToServer({ type: "exit", sessionId, exitCode });
1006
- setTimeout(() => process.exit(exitCode), 200);
1007
- } else if (pendingPermissionGrant) {
1008
- // Permission was granted while claude was running — respawn to retry
1009
- const tool = pendingPermissionGrant;
1010
- pendingPermissionGrant = null;
1011
- process.stderr.write(`${GREEN}Retrying with new permission: ${tool}${RESET}\n`);
1012
- respawnWithContinue(`Permission for ${tool} was granted. Please retry your last action.`);
1013
- } else {
1014
- sendToServer({
1015
- type: "agent_event", sessionId,
1016
- event: { kind: "status", status: "idle" },
1017
- });
1018
- }
1082
+ sendToServer({ type: "exit", sessionId, exitCode });
1083
+ setTimeout(() => process.exit(exitCode), 200);
1019
1084
  });
1020
1085
 
1021
1086
  proc.on("error", (err) => {
@@ -1027,6 +1092,24 @@ if (adapter) {
1027
1092
  return proc;
1028
1093
  }
1029
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
+
1030
1113
  // Auto-approve a tool permission (used by yolo mode and manual approval)
1031
1114
  function approvePermission(toolName, scope) {
1032
1115
  let permEntry = toolName;
@@ -1129,37 +1212,29 @@ if (adapter) {
1129
1212
  }
1130
1213
  }
1131
1214
 
1132
- // Start the initial claude process
1215
+ // Start the claude process (single process for entire session — no respawning)
1133
1216
  const initialArgs = adapter.buildArgs(prompt, adapterFlags);
1134
1217
  if (yoloMode) {
1135
1218
  initialArgs.push("--dangerously-skip-permissions");
1136
1219
  }
1137
1220
  spawnClaude(initialArgs);
1138
1221
 
1139
- // Emit the initial prompt as a user message so the dashboard shows it
1222
+ // Send initial prompt via stdin (not -p flag)
1140
1223
  if (prompt) {
1224
+ sendFollowUp(prompt);
1225
+ // Emit the initial prompt as a user message so the dashboard shows it
1141
1226
  sendToServer({
1142
1227
  type: "agent_event", sessionId,
1143
1228
  event: { kind: "user_message", text: prompt },
1144
1229
  });
1145
1230
  }
1146
1231
 
1147
- // Respawn claude with -c to continue after permission grants or follow-up
1148
- function respawnWithContinue(prompt) {
1149
- const args = ["--output-format", "stream-json", "--verbose", "-c"];
1150
- if (prompt) args.push("-p", prompt);
1151
- if (yoloMode) args.push("--dangerously-skip-permissions");
1152
- spawnClaude(args);
1153
- }
1154
-
1155
- // Queue of permission grants received while claude is still running
1156
- let pendingPermissionGrant = null;
1157
-
1158
1232
  handleServerMessage = (msg) => {
1159
1233
  if (msg.type === "agent_input" && (msg.text || msg.images)) {
1160
- // Follow-up from dashboard — spawn new claude with -c (continue)
1234
+ // Follow-up from dashboard — send via stdin (no respawn!)
1161
1235
  if (childRunning) {
1162
- 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
1163
1238
  return;
1164
1239
  }
1165
1240
  let promptText = msg.text || "continue";
@@ -1176,28 +1251,16 @@ if (adapter) {
1176
1251
  const fileList = imagePaths.map((p) => `[Attached image: ${p} — use Read tool to view]`).join("\n");
1177
1252
  promptText = `${fileList}\n\n${promptText}`;
1178
1253
  }
1179
- respawnWithContinue(promptText);
1254
+ sendFollowUp(promptText);
1180
1255
  } else if (msg.type === "agent_permission" && msg.tool && msg.action === "allow") {
1181
1256
  const scope = msg.scope || "session";
1182
1257
  approvePermission(msg.tool, scope);
1183
1258
  process.stderr.write(`${GREEN}Permission granted (${scope}): ${msg.tool}${RESET}\n`);
1184
-
1185
- if (childRunning) {
1186
- // Claude is still running — remember the grant and respawn when it exits
1187
- pendingPermissionGrant = msg.tool;
1188
- } else {
1189
- // Claude already finished — respawn with -c to retry with new permission
1190
- respawnWithContinue(`Permission for ${msg.tool} was granted. Please retry your last action.`);
1191
- }
1259
+ // Send permission grant as follow-up via stdin
1260
+ sendFollowUp(`Permission for ${msg.tool} was granted. Please retry your last action.`);
1192
1261
  } else if (msg.type === "agent_permission" && msg.tool && msg.action === "deny") {
1193
1262
  process.stderr.write(`${RED}Permission denied: ${msg.tool}${RESET}\n`);
1194
-
1195
- if (childRunning) {
1196
- // Claude is still running — remember and respawn when it exits
1197
- pendingPermissionGrant = null;
1198
- } else {
1199
- respawnWithContinue(`Permission for ${msg.tool} was denied by the user. Do not retry this tool — find an alternative approach or skip this step.`);
1200
- }
1263
+ sendFollowUp(`Permission for ${msg.tool} was denied by the user. Do not retry this tool — find an alternative approach or skip this step.`);
1201
1264
  } else if (msg.type === "agent_permission" && msg.tool && msg.action === "revoke") {
1202
1265
  removePermission(msg.tool);
1203
1266
  broadcastPermissions();
@@ -1209,7 +1272,7 @@ if (adapter) {
1209
1272
  } else if (msg.type === "stop_session") {
1210
1273
  process.stderr.write(`${RED}[orch] Stopped from dashboard${RESET}\n`);
1211
1274
  exitRequested = true;
1212
- if (childRunning) {
1275
+ if (child && child.exitCode === null) {
1213
1276
  child.kill("SIGINT");
1214
1277
  } else {
1215
1278
  cleanup();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrating",
3
- "version": "0.1.39",
3
+ "version": "0.3.0",
4
4
  "description": "Stream terminal sessions to the orchestrat.ing dashboard",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,6 +30,7 @@
30
30
  "dashboard"
31
31
  ],
32
32
  "dependencies": {
33
- "ws": "^8.18.0"
33
+ "ws": "^8.18.0",
34
+ "@anthropic-ai/claude-code": "^2.1.0"
34
35
  }
35
36
  }