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.
- package/bin/orch +121 -58
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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: ["
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1005
|
-
|
|
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
|
|
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
|
-
//
|
|
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 —
|
|
1234
|
+
// Follow-up from dashboard — send via stdin (no respawn!)
|
|
1161
1235
|
if (childRunning) {
|
|
1162
|
-
process.stderr.write(`${DIM}[orch]
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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
|
}
|