replicas-cli 0.2.29 → 0.2.32

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/dist/index.js +1199 -1179
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -813,1266 +813,1281 @@ Error: ${error instanceof Error ? error.message : "Unknown error"}`));
813
813
  var import_chalk6 = __toESM(require("chalk"));
814
814
  var import_child_process4 = require("child_process");
815
815
 
816
- // src/lib/ssh-config.ts
817
- var import_fs3 = __toESM(require("fs"));
818
- var import_path4 = __toESM(require("path"));
819
- var import_os2 = __toESM(require("os"));
820
- var SSH_CONFIG_PATH = import_path4.default.join(import_os2.default.homedir(), ".ssh", "config");
821
- var REPLICAS_MARKER_START = "# === REPLICAS CLI MANAGED ENTRY START ===";
822
- var REPLICAS_MARKER_END = "# === REPLICAS CLI MANAGED ENTRY END ===";
823
- function ensureSSHDir() {
824
- const sshDir = import_path4.default.join(import_os2.default.homedir(), ".ssh");
825
- if (!import_fs3.default.existsSync(sshDir)) {
826
- import_fs3.default.mkdirSync(sshDir, { recursive: true, mode: 448 });
827
- }
828
- }
829
- function readSSHConfig() {
830
- ensureSSHDir();
831
- if (!import_fs3.default.existsSync(SSH_CONFIG_PATH)) {
832
- return "";
816
+ // ../shared/src/sandbox.ts
817
+ var SANDBOX_LIFECYCLE = {
818
+ AUTO_STOP_MINUTES: 60,
819
+ AUTO_ARCHIVE_MINUTES: 60 * 24 * 7,
820
+ AUTO_DELETE_MINUTES: -1,
821
+ SSH_TOKEN_EXPIRATION_MINUTES: 3 * 60
822
+ };
823
+ var SANDBOX_PATHS = {
824
+ HOME_DIR: "/home/ubuntu",
825
+ WORKSPACES_DIR: "/home/ubuntu/workspaces",
826
+ REPLICAS_FILES_DIR: "/home/ubuntu/.replicas/files",
827
+ REPLICAS_FILES_DISPLAY_DIR: "~/.replicas/files"
828
+ };
829
+
830
+ // ../shared/src/routes/workspaces.ts
831
+ var WORKSPACE_FILE_UPLOAD_MAX_SIZE_BYTES = 20 * 1024 * 1024;
832
+ var WORKSPACE_FILE_CONTENT_MAX_SIZE_BYTES = 1 * 1024 * 1024;
833
+
834
+ // ../shared/src/display-message/parsers/codex-parser.ts
835
+ function safeJsonParse(str, fallback) {
836
+ try {
837
+ return JSON.parse(str);
838
+ } catch {
839
+ return fallback;
833
840
  }
834
- return import_fs3.default.readFileSync(SSH_CONFIG_PATH, "utf-8");
835
841
  }
836
- function writeSSHConfig(content) {
837
- ensureSSHDir();
838
- import_fs3.default.writeFileSync(SSH_CONFIG_PATH, content, { mode: 384 });
842
+ function getStatusFromExitCode(exitCode) {
843
+ return exitCode === 0 ? "completed" : "failed";
839
844
  }
840
- function generateConfigBlock(entry) {
841
- const lines = [
842
- REPLICAS_MARKER_START,
843
- `Host ${entry.host}`,
844
- ` HostName ${entry.hostname}`,
845
- ` User ${entry.user}`,
846
- ` StrictHostKeyChecking no`,
847
- ` UserKnownHostsFile /dev/null`
848
- ];
849
- if (entry.portForwards && entry.portForwards.size > 0) {
850
- for (const [localPort, remotePort] of entry.portForwards) {
851
- lines.push(` LocalForward ${localPort} localhost:${remotePort}`);
852
- }
853
- }
854
- lines.push(REPLICAS_MARKER_END);
855
- return lines.join("\n");
845
+ function parseShellOutput(raw) {
846
+ const exitCodeMatch = raw.match(/^Exit code: (\d+)/);
847
+ const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 0;
848
+ const outputIndex = raw.indexOf("Output:\n");
849
+ const output = outputIndex >= 0 ? raw.slice(outputIndex + "Output:\n".length) : raw;
850
+ return { exitCode, output };
856
851
  }
857
- function addOrUpdateSSHConfigEntry(entry) {
858
- let config2 = readSSHConfig();
859
- const lines = config2.split("\n");
860
- const result = [];
861
- let inTargetBlock = false;
862
- for (const line of lines) {
863
- if (line.trim() === REPLICAS_MARKER_START) {
864
- const nextLines = lines.slice(lines.indexOf(line) + 1, lines.indexOf(line) + 3);
865
- const hostLine = nextLines.find((l) => l.trim().startsWith("Host "));
866
- if (hostLine && hostLine.includes(entry.host)) {
867
- inTargetBlock = true;
868
- continue;
852
+ function parsePatch(input) {
853
+ const operations = [];
854
+ const lines = input.split("\n");
855
+ let currentOp = null;
856
+ let diffLines = [];
857
+ for (let i = 0; i < lines.length; i++) {
858
+ const line = lines[i];
859
+ if (line.startsWith("*** Add File:")) {
860
+ if (currentOp) {
861
+ if (diffLines.length > 0) currentOp.diff = diffLines.join("\n");
862
+ operations.push(currentOp);
863
+ diffLines = [];
869
864
  }
870
- result.push(line);
871
- continue;
872
- }
873
- if (line.trim() === REPLICAS_MARKER_END) {
874
- if (inTargetBlock) {
875
- inTargetBlock = false;
876
- continue;
865
+ currentOp = {
866
+ action: "add",
867
+ path: line.replace("*** Add File:", "").trim()
868
+ };
869
+ } else if (line.startsWith("*** Update File:")) {
870
+ if (currentOp) {
871
+ if (diffLines.length > 0) currentOp.diff = diffLines.join("\n");
872
+ operations.push(currentOp);
873
+ diffLines = [];
877
874
  }
878
- result.push(line);
879
- continue;
880
- }
881
- if (inTargetBlock) {
875
+ currentOp = {
876
+ action: "update",
877
+ path: line.replace("*** Update File:", "").trim()
878
+ };
879
+ } else if (line.startsWith("*** Delete File:")) {
880
+ if (currentOp) {
881
+ if (diffLines.length > 0) currentOp.diff = diffLines.join("\n");
882
+ operations.push(currentOp);
883
+ diffLines = [];
884
+ }
885
+ currentOp = {
886
+ action: "delete",
887
+ path: line.replace("*** Delete File:", "").trim()
888
+ };
889
+ } else if (line.startsWith("*** Move to:")) {
890
+ if (currentOp) {
891
+ currentOp.moveTo = line.replace("*** Move to:", "").trim();
892
+ }
893
+ } else if (line.startsWith("*** Begin Patch") || line.startsWith("*** End Patch")) {
882
894
  continue;
895
+ } else if (currentOp && (line.startsWith("+") || line.startsWith("-") || line.startsWith("@@") || line.trim() === "")) {
896
+ diffLines.push(line);
883
897
  }
884
- result.push(line);
885
898
  }
886
- config2 = result.join("\n");
887
- const newEntry = generateConfigBlock(entry);
888
- let finalConfig = config2.trim();
889
- if (finalConfig.length > 0) {
890
- finalConfig += "\n\n";
899
+ if (currentOp) {
900
+ if (diffLines.length > 0) currentOp.diff = diffLines.join("\n");
901
+ operations.push(currentOp);
891
902
  }
892
- finalConfig += newEntry + "\n";
893
- writeSSHConfig(finalConfig);
903
+ return operations;
894
904
  }
895
- function getWorkspaceHostAlias(workspaceName) {
896
- return `replicas-${workspaceName.toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
905
+ function parseCodexEvents(events) {
906
+ const messages = [];
907
+ const pendingCommands = /* @__PURE__ */ new Map();
908
+ const pendingPatches = /* @__PURE__ */ new Map();
909
+ events.forEach((event, eventIndex) => {
910
+ if (event.type === "event_msg" && event.payload?.type === "user_message") {
911
+ messages.push({
912
+ id: `user-${event.timestamp}-${eventIndex}`,
913
+ type: "user",
914
+ content: event.payload.message || "",
915
+ timestamp: event.timestamp
916
+ });
917
+ }
918
+ if (event.type === "event_msg" && event.payload?.type === "agent_reasoning") {
919
+ messages.push({
920
+ id: `reasoning-${event.timestamp}-${eventIndex}-${messages.length}`,
921
+ type: "reasoning",
922
+ content: event.payload.text || "",
923
+ status: "completed",
924
+ timestamp: event.timestamp
925
+ });
926
+ }
927
+ if (event.type === "response_item") {
928
+ const payloadType = event.payload?.type;
929
+ if (payloadType === "message" && event.payload?.role === "assistant") {
930
+ const content = event.payload.content || [];
931
+ const textContent = content.filter((c) => c.type === "output_text").map((c) => c.text || "").join("\n");
932
+ if (textContent) {
933
+ messages.push({
934
+ id: `agent-${event.timestamp}-${eventIndex}`,
935
+ type: "agent",
936
+ content: textContent,
937
+ timestamp: event.timestamp
938
+ });
939
+ }
940
+ }
941
+ if (payloadType === "function_call" && (event.payload?.name === "shell" || event.payload?.name === "shell_command")) {
942
+ const callId = event.payload.call_id;
943
+ const args = safeJsonParse(event.payload.arguments || "{}", {});
944
+ const command = Array.isArray(args.command) ? args.command.join(" ") : args.command || "";
945
+ const msg = {
946
+ id: `command-${callId || "no-call-id"}-${eventIndex}`,
947
+ type: "command",
948
+ command,
949
+ output: "",
950
+ status: "in_progress",
951
+ timestamp: event.timestamp
952
+ };
953
+ messages.push(msg);
954
+ if (callId) {
955
+ pendingCommands.set(callId, msg);
956
+ }
957
+ }
958
+ if (payloadType === "function_call_output") {
959
+ const callId = event.payload.call_id;
960
+ const rawOutput = event.payload.output || "";
961
+ const commandMsg = callId ? pendingCommands.get(callId) : void 0;
962
+ if (commandMsg) {
963
+ const { exitCode, output } = parseShellOutput(rawOutput);
964
+ commandMsg.output = output;
965
+ commandMsg.exitCode = exitCode;
966
+ commandMsg.status = getStatusFromExitCode(exitCode);
967
+ pendingCommands.delete(callId);
968
+ }
969
+ }
970
+ if (payloadType === "custom_tool_call") {
971
+ const callId = event.payload.call_id;
972
+ const name = event.payload.name;
973
+ const input = event.payload.input || "";
974
+ const status = event.payload.status || "in_progress";
975
+ if (name === "apply_patch") {
976
+ const operations = parsePatch(input);
977
+ pendingPatches.set(callId, { input, status, timestamp: event.timestamp, operations });
978
+ } else {
979
+ messages.push({
980
+ id: `toolcall-${event.timestamp}-${eventIndex}`,
981
+ type: "tool_call",
982
+ server: "custom",
983
+ tool: name,
984
+ status,
985
+ timestamp: event.timestamp
986
+ });
987
+ }
988
+ }
989
+ if (payloadType === "custom_tool_call_output") {
990
+ const callId = event.payload.call_id;
991
+ const output = safeJsonParse(
992
+ event.payload.output || "{}",
993
+ {}
994
+ );
995
+ const pendingPatch = pendingPatches.get(callId);
996
+ if (pendingPatch) {
997
+ messages.push({
998
+ id: `patch-${pendingPatch.timestamp}-${eventIndex}`,
999
+ type: "patch",
1000
+ operations: pendingPatch.operations,
1001
+ output: output.output || "",
1002
+ exitCode: output.metadata?.exit_code,
1003
+ status: getStatusFromExitCode(output.metadata?.exit_code),
1004
+ timestamp: pendingPatch.timestamp
1005
+ });
1006
+ pendingPatches.delete(callId);
1007
+ } else {
1008
+ const toolCallMsg = messages.findLast((m) => m.type === "tool_call");
1009
+ if (toolCallMsg) {
1010
+ toolCallMsg.status = getStatusFromExitCode(output.metadata?.exit_code);
1011
+ }
1012
+ }
1013
+ }
1014
+ if (payloadType === "function_call" && event.payload?.name === "update_plan") {
1015
+ const args = safeJsonParse(
1016
+ event.payload.arguments || "{}",
1017
+ {}
1018
+ );
1019
+ if (args.plan && Array.isArray(args.plan)) {
1020
+ const todoItems = args.plan.map((item) => ({
1021
+ text: item.step,
1022
+ completed: item.status === "completed"
1023
+ }));
1024
+ messages.push({
1025
+ id: `todo-${event.timestamp}-${eventIndex}`,
1026
+ type: "todo_list",
1027
+ items: todoItems,
1028
+ status: "completed",
1029
+ timestamp: event.timestamp
1030
+ });
1031
+ }
1032
+ }
1033
+ }
1034
+ });
1035
+ return messages;
897
1036
  }
898
1037
 
899
- // src/commands/code.ts
900
- async function codeCommand(workspaceName, options) {
901
- if (!isAuthenticated()) {
902
- console.log(import_chalk6.default.red('Not logged in. Please run "replicas login" first.'));
903
- process.exit(1);
904
- }
1038
+ // ../shared/src/display-message/parsers/claude-parser.ts
1039
+ function safeJsonParse2(str, fallback) {
905
1040
  try {
906
- const { workspace, sshToken, sshHost, portMappings, repoName } = await prepareWorkspaceConnection(
907
- workspaceName,
908
- options
909
- );
910
- const hostAlias = getWorkspaceHostAlias(workspace.name);
911
- console.log(import_chalk6.default.blue("\nConfiguring SSH connection..."));
912
- addOrUpdateSSHConfigEntry({
913
- host: hostAlias,
914
- hostname: sshHost,
915
- user: sshToken,
916
- portForwards: portMappings
917
- });
918
- console.log(import_chalk6.default.green(`\u2713 SSH config entry created: ${hostAlias}`));
919
- const remotePath = repoName ? `/home/ubuntu/workspaces/${repoName}` : "/home/ubuntu";
920
- const ideCommand = getIdeCommand();
921
- const fullCommand = `${ideCommand} --remote ssh-remote+${hostAlias} ${remotePath}`;
922
- console.log(import_chalk6.default.blue(`
923
- Opening ${ideCommand} for workspace ${workspace.name}...`));
924
- console.log(import_chalk6.default.gray(`Command: ${fullCommand}`));
925
- if (portMappings.size > 0) {
926
- console.log(import_chalk6.default.cyan("\nPort forwarding configured:"));
927
- for (const [remotePort, localPort] of portMappings) {
928
- console.log(import_chalk6.default.cyan(` \u2022 localhost:${localPort} \u2192 workspace:${remotePort}`));
929
- }
930
- }
931
- console.log(import_chalk6.default.yellow(`
932
- Note: SSH tokens expire after 3 hours. Run this command again to refresh.`));
933
- const ide = (0, import_child_process4.spawn)(ideCommand, [
934
- "--remote",
935
- `ssh-remote+${hostAlias}`,
936
- remotePath
937
- ], {
938
- stdio: "inherit",
939
- detached: false
940
- });
941
- ide.on("error", (error) => {
942
- console.error(import_chalk6.default.red(`
943
- Failed to launch ${ideCommand}: ${error.message}`));
944
- console.log(import_chalk6.default.yellow(`
945
- Make sure ${ideCommand} is installed and available in your PATH.`));
946
- console.log(import_chalk6.default.gray(`You can configure a different IDE with: replicas config set ide <command>`));
947
- process.exit(1);
948
- });
949
- ide.on("close", (code) => {
950
- if (code === 0) {
951
- console.log(import_chalk6.default.green(`
952
- \u2713 ${ideCommand} closed successfully.
953
- `));
954
- }
955
- });
956
- } catch (error) {
957
- console.error(import_chalk6.default.red(`
958
- Error: ${error instanceof Error ? error.message : "Unknown error"}`));
959
- process.exit(1);
960
- }
961
- }
962
-
963
- // src/commands/org.ts
964
- var import_chalk7 = __toESM(require("chalk"));
965
- var import_prompts2 = __toESM(require("prompts"));
966
- async function orgCommand() {
967
- if (!isAuthenticated()) {
968
- console.log(import_chalk7.default.red('Not logged in. Please run "replicas login" first.'));
969
- process.exit(1);
970
- }
971
- try {
972
- const currentOrgId = getOrganizationId();
973
- const organizations = await fetchOrganizations();
974
- if (!currentOrgId) {
975
- console.log(import_chalk7.default.yellow("No organization selected."));
976
- console.log(import_chalk7.default.gray('Run "replicas org switch" to select an organization.\n'));
977
- return;
978
- }
979
- const currentOrg = organizations.find((org2) => org2.id === currentOrgId);
980
- if (!currentOrg) {
981
- console.log(import_chalk7.default.yellow(`Current organization ID (${currentOrgId}) not found.`));
982
- console.log(import_chalk7.default.gray('Run "replicas org switch" to select a new organization.\n'));
983
- return;
984
- }
985
- console.log(import_chalk7.default.green("\nCurrent organization:"));
986
- console.log(import_chalk7.default.gray(` Name: ${currentOrg.name}`));
987
- console.log(import_chalk7.default.gray(` ID: ${currentOrg.id}
988
- `));
989
- } catch (error) {
990
- console.error(import_chalk7.default.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
991
- process.exit(1);
1041
+ return JSON.parse(str);
1042
+ } catch {
1043
+ return fallback;
992
1044
  }
993
1045
  }
994
- async function orgSwitchCommand() {
995
- if (!isAuthenticated()) {
996
- console.log(import_chalk7.default.red('Not logged in. Please run "replicas login" first.'));
997
- process.exit(1);
998
- }
999
- try {
1000
- const organizations = await fetchOrganizations();
1001
- if (organizations.length === 0) {
1002
- console.log(import_chalk7.default.yellow("You are not a member of any organization."));
1003
- console.log(import_chalk7.default.gray("Please contact support.\n"));
1004
- return;
1005
- }
1006
- if (organizations.length === 1) {
1007
- await setActiveOrganization(organizations[0].id);
1008
- console.log(import_chalk7.default.green(`
1009
- \u2713 Organization set to: ${organizations[0].name}
1010
- `));
1011
- return;
1012
- }
1013
- const currentOrgId = getOrganizationId();
1014
- const response = await (0, import_prompts2.default)({
1015
- type: "select",
1016
- name: "organizationId",
1017
- message: "Select an organization:",
1018
- choices: organizations.map((org2) => ({
1019
- title: `${org2.name}${org2.id === currentOrgId ? " (current)" : ""}`,
1020
- value: org2.id,
1021
- description: org2.id
1022
- })),
1023
- initial: organizations.findIndex((org2) => org2.id === currentOrgId)
1024
- });
1025
- if (!response.organizationId) {
1026
- console.log(import_chalk7.default.yellow("\nCancelled."));
1027
- return;
1028
- }
1029
- await setActiveOrganization(response.organizationId);
1030
- const selectedOrg = organizations.find((org2) => org2.id === response.organizationId);
1031
- console.log(import_chalk7.default.green(`
1032
- \u2713 Switched to organization: ${selectedOrg?.name}
1033
- `));
1034
- } catch (error) {
1035
- console.error(import_chalk7.default.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
1036
- process.exit(1);
1046
+ function extractToolResultContent(content) {
1047
+ if (!content) return "";
1048
+ if (typeof content === "string") return content;
1049
+ if (Array.isArray(content)) {
1050
+ return content.filter((item) => item.type === "text").map((item) => item.text || "").join("\n");
1037
1051
  }
1052
+ return "";
1038
1053
  }
1039
-
1040
- // src/commands/config.ts
1041
- var import_chalk8 = __toESM(require("chalk"));
1042
- async function configGetCommand(key) {
1043
- if (!isAuthenticated()) {
1044
- console.log(import_chalk8.default.red('Not logged in. Please run "replicas login" first.'));
1045
- process.exit(1);
1046
- }
1047
- try {
1048
- if (key === "ide") {
1049
- const ideCommand = getIdeCommand();
1050
- console.log(import_chalk8.default.green(`
1051
- IDE command: ${ideCommand}
1052
- `));
1053
- } else {
1054
- console.log(import_chalk8.default.red(`Unknown config key: ${key}`));
1055
- console.log(import_chalk8.default.gray("Available keys: ide"));
1056
- process.exit(1);
1054
+ function parseClaudeEvents(events, parentToolUseId) {
1055
+ const messages = [];
1056
+ const filterValue = parentToolUseId !== void 0 ? parentToolUseId : null;
1057
+ const filteredEvents = events.filter(
1058
+ (e) => e.payload.parent_tool_use_id === filterValue
1059
+ );
1060
+ const toolCallMap = /* @__PURE__ */ new Map();
1061
+ filteredEvents.forEach((event) => {
1062
+ if (event.type === "claude-user") {
1063
+ const content = event.payload.message?.content || [];
1064
+ const toolResult = content.find((c) => c.type === "tool_result");
1065
+ if (toolResult && toolResult.tool_use_id) {
1066
+ return;
1067
+ }
1068
+ const textContent = content.filter((c) => c.type === "text").map((c) => c.text || "").join("\n");
1069
+ const images = content.filter((c) => c.type === "image" && c.source).map((c) => {
1070
+ const source = c.source;
1071
+ return {
1072
+ type: "image",
1073
+ mediaType: source.media_type || "image/png",
1074
+ data: source.data || ""
1075
+ };
1076
+ }).filter((img) => img.data);
1077
+ if (textContent || images.length > 0) {
1078
+ messages.push({
1079
+ id: `user-${event.timestamp}`,
1080
+ type: "user",
1081
+ content: textContent || (images.length > 0 ? `[${images.length} image${images.length > 1 ? "s" : ""} attached]` : ""),
1082
+ images: images.length > 0 ? images : void 0,
1083
+ timestamp: event.timestamp
1084
+ });
1085
+ }
1057
1086
  }
1058
- } catch (error) {
1059
- console.error(import_chalk8.default.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
1060
- process.exit(1);
1061
- }
1062
- }
1063
- async function configSetCommand(key, value) {
1064
- if (!isAuthenticated()) {
1065
- console.log(import_chalk8.default.red('Not logged in. Please run "replicas login" first.'));
1066
- process.exit(1);
1067
- }
1068
- try {
1069
- if (key === "ide") {
1070
- setIdeCommand(value);
1071
- console.log(import_chalk8.default.green(`
1072
- \u2713 IDE command set to: ${value}
1073
- `));
1074
- } else {
1075
- console.log(import_chalk8.default.red(`Unknown config key: ${key}`));
1076
- console.log(import_chalk8.default.gray("Available keys: ide"));
1077
- process.exit(1);
1087
+ if (event.type === "claude-assistant") {
1088
+ const contentBlocks = event.payload.message?.content || [];
1089
+ contentBlocks.forEach((block) => {
1090
+ if (block.type === "text" && block.text) {
1091
+ messages.push({
1092
+ id: `agent-${event.timestamp}-${messages.length}`,
1093
+ type: "agent",
1094
+ content: block.text,
1095
+ timestamp: event.timestamp
1096
+ });
1097
+ }
1098
+ if (block.type === "thinking" && block.text) {
1099
+ messages.push({
1100
+ id: `reasoning-${event.timestamp}-${messages.length}`,
1101
+ type: "reasoning",
1102
+ content: block.text,
1103
+ status: "completed",
1104
+ timestamp: event.timestamp
1105
+ });
1106
+ }
1107
+ if (block.type === "tool_use" && block.id) {
1108
+ const toolName = block.name || "unknown";
1109
+ const toolInput = block.input || {};
1110
+ const toolUseId = block.id;
1111
+ toolCallMap.set(toolUseId, {
1112
+ messageIndex: messages.length,
1113
+ toolName,
1114
+ input: toolInput
1115
+ });
1116
+ if (toolName === "Task") {
1117
+ const inputObj = typeof toolInput === "string" ? safeJsonParse2(toolInput, {}) : toolInput;
1118
+ const nestedEvents = events.filter((e) => e.payload.parent_tool_use_id === toolUseId).map((e) => ({
1119
+ timestamp: e.timestamp,
1120
+ type: e.type,
1121
+ payload: e.payload
1122
+ }));
1123
+ messages.push({
1124
+ id: `subagent-${event.timestamp}-${messages.length}`,
1125
+ type: "subagent",
1126
+ toolUseId,
1127
+ description: inputObj.description || "Subagent Task",
1128
+ prompt: inputObj.prompt || "",
1129
+ subagentType: inputObj.subagent_type || "general",
1130
+ model: inputObj.model,
1131
+ status: "in_progress",
1132
+ nestedEvents,
1133
+ timestamp: event.timestamp
1134
+ });
1135
+ } else if (toolName === "Bash" || toolName === "bash" || toolName === "shell") {
1136
+ const inputObj = typeof toolInput === "string" ? safeJsonParse2(toolInput, {}) : toolInput;
1137
+ const command = inputObj.command || "";
1138
+ messages.push({
1139
+ id: `command-${event.timestamp}-${messages.length}`,
1140
+ type: "command",
1141
+ command,
1142
+ output: "",
1143
+ status: "in_progress",
1144
+ timestamp: event.timestamp
1145
+ });
1146
+ } else if (toolName === "Write" || toolName === "Edit") {
1147
+ const inputObj = typeof toolInput === "string" ? safeJsonParse2(toolInput, {}) : toolInput;
1148
+ const filePath = inputObj.file_path || "";
1149
+ const action = toolName === "Write" ? "add" : "update";
1150
+ messages.push({
1151
+ id: `file-${event.timestamp}-${messages.length}`,
1152
+ type: "file_change",
1153
+ changes: [{
1154
+ path: filePath,
1155
+ kind: action
1156
+ }],
1157
+ status: "in_progress",
1158
+ timestamp: event.timestamp
1159
+ });
1160
+ } else if (toolName === "TodoWrite") {
1161
+ const inputObj = typeof toolInput === "string" ? safeJsonParse2(toolInput, {}) : toolInput;
1162
+ const todos = inputObj.todos || [];
1163
+ const todoItems = todos.map((todo) => ({
1164
+ text: todo.content,
1165
+ completed: todo.status === "completed"
1166
+ }));
1167
+ if (todoItems.length > 0) {
1168
+ messages.push({
1169
+ id: `todo-${event.timestamp}-${messages.length}`,
1170
+ type: "todo_list",
1171
+ items: todoItems,
1172
+ status: "completed",
1173
+ timestamp: event.timestamp
1174
+ });
1175
+ }
1176
+ } else if (toolName === "WebSearch") {
1177
+ const inputObj = typeof toolInput === "string" ? safeJsonParse2(toolInput, {}) : toolInput;
1178
+ const query = inputObj.query || "";
1179
+ messages.push({
1180
+ id: `search-${event.timestamp}-${messages.length}`,
1181
+ type: "web_search",
1182
+ query,
1183
+ status: "in_progress",
1184
+ timestamp: event.timestamp
1185
+ });
1186
+ } else {
1187
+ messages.push({
1188
+ id: `toolcall-${event.timestamp}-${messages.length}`,
1189
+ type: "tool_call",
1190
+ server: "claude",
1191
+ tool: toolName,
1192
+ input: toolInput,
1193
+ status: "in_progress",
1194
+ timestamp: event.timestamp
1195
+ });
1196
+ }
1197
+ }
1198
+ });
1078
1199
  }
1079
- } catch (error) {
1080
- console.error(import_chalk8.default.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
1081
- process.exit(1);
1082
- }
1083
- }
1084
- async function configListCommand() {
1085
- if (!isAuthenticated()) {
1086
- console.log(import_chalk8.default.red('Not logged in. Please run "replicas login" first.'));
1087
- process.exit(1);
1088
- }
1089
- try {
1090
- const config2 = readConfig();
1091
- if (!config2) {
1092
- console.log(import_chalk8.default.red("No config found. Please login first."));
1093
- process.exit(1);
1200
+ if (event.type === "claude-result") {
1201
+ const payload = event.payload;
1202
+ if (payload.is_error || payload.subtype !== "success") {
1203
+ const errorList = payload.errors || [];
1204
+ const errorMessage = errorList.length > 0 ? errorList.join("\n") : "Claude session encountered an unexpected error.";
1205
+ messages.push({
1206
+ id: `error-${event.timestamp}`,
1207
+ type: "error",
1208
+ message: errorMessage,
1209
+ timestamp: event.timestamp
1210
+ });
1211
+ }
1094
1212
  }
1095
- console.log(import_chalk8.default.green("\nCurrent configuration:"));
1096
- console.log(import_chalk8.default.gray(` Organization ID: ${config2.organization_id || "(not set)"}`));
1097
- console.log(import_chalk8.default.gray(` IDE command: ${config2.ide_command || "code (default)"}`));
1098
- console.log();
1099
- } catch (error) {
1100
- console.error(import_chalk8.default.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
1101
- process.exit(1);
1102
- }
1103
- }
1104
-
1105
- // src/commands/tools.ts
1106
- var import_chalk9 = __toESM(require("chalk"));
1107
- var import_figlet = __toESM(require("figlet"));
1108
- async function toolsCommand() {
1109
- const banner = import_figlet.default.textSync("Replicas Machine Image", {
1110
- horizontalLayout: "default",
1111
- verticalLayout: "default",
1112
- whitespaceBreak: true
1113
1213
  });
1114
- console.log(import_chalk9.default.cyan(banner));
1115
- console.log();
1116
- console.log(import_chalk9.default.bold("RMI Version: v0.1.3"));
1117
- console.log();
1118
- console.log(
1119
- import_chalk9.default.gray(
1120
- "The following tooling and dependencies are provided by default on the Replicas Machine Image.\n"
1121
- )
1122
- );
1123
- const sections = [
1124
- {
1125
- title: "Python",
1126
- tools: ["Python 3.12.3", "pip 24", "Poetry 2.2.1", "uv 0.9.5"]
1127
- },
1128
- {
1129
- title: "Node.js",
1130
- tools: [
1131
- "Node.js 22.21.0",
1132
- "npm 10.9.4",
1133
- "nvm 0.40.3",
1134
- "Yarn 1.22.22",
1135
- "pnpm 10.18.3",
1136
- "ESLint 9.38.0",
1137
- "Prettier 3.6.2"
1138
- ]
1139
- },
1140
- {
1141
- title: "C/C++",
1142
- tools: ["gcc 13.3.0", "g++ 13.3.0", "make 4.3"]
1143
- },
1144
- {
1145
- title: "Perl",
1146
- tools: ["Perl 5.38.2"]
1147
- },
1148
- {
1149
- title: "AI Coding Assistants",
1150
- tools: ["Claude Code (@anthropic-ai/claude-code)", "OpenAI Codex (@openai/codex)"]
1151
- },
1152
- {
1153
- title: "Misc",
1154
- tools: [
1155
- "git 2.43.0",
1156
- "vim 9.1.697",
1157
- "nano 7.2",
1158
- "curl",
1159
- "wget",
1160
- "GitHub CLI (gh)",
1161
- "Docker Engine"
1162
- ]
1214
+ filteredEvents.forEach((event) => {
1215
+ if (event.type === "claude-user") {
1216
+ const content = event.payload.message?.content || [];
1217
+ const toolResult = content.find((c) => c.type === "tool_result");
1218
+ if (toolResult && toolResult.tool_use_id) {
1219
+ const toolInfo = toolCallMap.get(toolResult.tool_use_id);
1220
+ if (!toolInfo) return;
1221
+ const resultContent = extractToolResultContent(toolResult.content);
1222
+ const isError = toolResult.is_error || false;
1223
+ const status = isError ? "failed" : "completed";
1224
+ const message = messages[toolInfo.messageIndex];
1225
+ if (!message) return;
1226
+ if (message.type === "command") {
1227
+ message.output = resultContent;
1228
+ message.status = status;
1229
+ const exitCodeMatch = resultContent.match(/exit code:?\s*(\d+)/i);
1230
+ if (exitCodeMatch) {
1231
+ message.exitCode = parseInt(exitCodeMatch[1], 10);
1232
+ } else {
1233
+ message.exitCode = isError ? 1 : 0;
1234
+ }
1235
+ } else if (message.type === "file_change") {
1236
+ message.status = status;
1237
+ } else if (message.type === "web_search") {
1238
+ message.status = status;
1239
+ } else if (message.type === "tool_call") {
1240
+ message.output = resultContent;
1241
+ message.status = status;
1242
+ } else if (message.type === "subagent") {
1243
+ message.output = resultContent;
1244
+ message.status = status;
1245
+ }
1246
+ }
1163
1247
  }
1164
- ];
1165
- sections.forEach((section) => {
1166
- console.log(import_chalk9.default.blue(section.title));
1167
- section.tools.forEach((tool) => {
1168
- console.log(` - ${tool}`);
1169
- });
1170
- console.log();
1171
1248
  });
1249
+ return messages;
1172
1250
  }
1173
1251
 
1174
- // src/commands/codex-auth.ts
1175
- var import_chalk10 = __toESM(require("chalk"));
1176
-
1177
- // src/lib/codex-oauth.ts
1178
- var import_http2 = __toESM(require("http"));
1179
- var import_url2 = require("url");
1180
- var import_open2 = __toESM(require("open"));
1252
+ // ../shared/src/display-message/parsers/index.ts
1253
+ function parseAgentEvents(events, agentType) {
1254
+ if (agentType === "codex") {
1255
+ return parseCodexEvents(events);
1256
+ } else if (agentType === "claude") {
1257
+ return parseClaudeEvents(events);
1258
+ }
1259
+ return [];
1260
+ }
1181
1261
 
1182
- // src/lib/pkce.ts
1183
- var import_crypto = require("crypto");
1184
- function generatePkceCodes() {
1185
- const verifierBytes = (0, import_crypto.randomBytes)(32);
1186
- const codeVerifier = verifierBytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1187
- const hash = (0, import_crypto.createHash)("sha256");
1188
- hash.update(codeVerifier);
1189
- const codeChallenge = hash.digest("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1190
- return {
1191
- codeVerifier,
1192
- codeChallenge
1193
- };
1262
+ // src/lib/ssh-config.ts
1263
+ var import_fs3 = __toESM(require("fs"));
1264
+ var import_path4 = __toESM(require("path"));
1265
+ var import_os2 = __toESM(require("os"));
1266
+ var SSH_CONFIG_PATH = import_path4.default.join(import_os2.default.homedir(), ".ssh", "config");
1267
+ var REPLICAS_MARKER_START = "# === REPLICAS CLI MANAGED ENTRY START ===";
1268
+ var REPLICAS_MARKER_END = "# === REPLICAS CLI MANAGED ENTRY END ===";
1269
+ function ensureSSHDir() {
1270
+ const sshDir = import_path4.default.join(import_os2.default.homedir(), ".ssh");
1271
+ if (!import_fs3.default.existsSync(sshDir)) {
1272
+ import_fs3.default.mkdirSync(sshDir, { recursive: true, mode: 448 });
1273
+ }
1194
1274
  }
1195
- function generateState2() {
1196
- return (0, import_crypto.randomBytes)(16).toString("hex");
1275
+ function readSSHConfig() {
1276
+ ensureSSHDir();
1277
+ if (!import_fs3.default.existsSync(SSH_CONFIG_PATH)) {
1278
+ return "";
1279
+ }
1280
+ return import_fs3.default.readFileSync(SSH_CONFIG_PATH, "utf-8");
1197
1281
  }
1198
-
1199
- // src/lib/codex-oauth.ts
1200
- var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
1201
- var AUTHORIZATION_ENDPOINT = "https://auth.openai.com/oauth/authorize";
1202
- var TOKEN_ENDPOINT = "https://auth.openai.com/oauth/token";
1203
- var CALLBACK_PORT = 1455;
1204
- var CALLBACK_PATH = "/auth/callback";
1205
- var WEB_APP_URL2 = process.env.REPLICAS_WEB_URL || "https://replicas.dev";
1206
- function buildAuthorizationUrl(codeChallenge, state) {
1207
- const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
1208
- const params = new URLSearchParams({
1209
- response_type: "code",
1210
- client_id: CODEX_OAUTH_CLIENT_ID,
1211
- redirect_uri: redirectUri,
1212
- scope: "openid profile email offline_access",
1213
- code_challenge: codeChallenge,
1214
- code_challenge_method: "S256",
1215
- id_token_add_organizations: "true",
1216
- codex_cli_simplified_flow: "true",
1217
- state,
1218
- originator: "codex_cli_rs"
1219
- });
1220
- return `${AUTHORIZATION_ENDPOINT}?${params.toString()}`;
1282
+ function writeSSHConfig(content) {
1283
+ ensureSSHDir();
1284
+ import_fs3.default.writeFileSync(SSH_CONFIG_PATH, content, { mode: 384 });
1221
1285
  }
1222
- async function exchangeCodeForTokens(code, codeVerifier) {
1223
- const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
1224
- const params = new URLSearchParams({
1225
- grant_type: "authorization_code",
1226
- code,
1227
- redirect_uri: redirectUri,
1228
- client_id: CODEX_OAUTH_CLIENT_ID,
1229
- code_verifier: codeVerifier
1230
- });
1231
- const response = await fetch(TOKEN_ENDPOINT, {
1232
- method: "POST",
1233
- headers: {
1234
- "Content-Type": "application/x-www-form-urlencoded"
1235
- },
1236
- body: params.toString()
1237
- });
1238
- if (!response.ok) {
1239
- const errorText = await response.text();
1240
- throw new Error(`Failed to exchange code for tokens: ${response.status} ${errorText}`);
1286
+ function generateConfigBlock(entry) {
1287
+ const lines = [
1288
+ REPLICAS_MARKER_START,
1289
+ `Host ${entry.host}`,
1290
+ ` HostName ${entry.hostname}`,
1291
+ ` User ${entry.user}`,
1292
+ ` StrictHostKeyChecking no`,
1293
+ ` UserKnownHostsFile /dev/null`
1294
+ ];
1295
+ if (entry.portForwards && entry.portForwards.size > 0) {
1296
+ for (const [localPort, remotePort] of entry.portForwards) {
1297
+ lines.push(` LocalForward ${localPort} localhost:${remotePort}`);
1298
+ }
1241
1299
  }
1242
- const data = await response.json();
1243
- return {
1244
- idToken: data.id_token,
1245
- accessToken: data.access_token,
1246
- refreshToken: data.refresh_token
1247
- };
1300
+ lines.push(REPLICAS_MARKER_END);
1301
+ return lines.join("\n");
1248
1302
  }
1249
- function startCallbackServer(expectedState, codeVerifier) {
1250
- return new Promise((resolve, reject) => {
1251
- const server = import_http2.default.createServer(async (req, res) => {
1252
- try {
1253
- if (!req.url) {
1254
- res.writeHead(400);
1255
- res.end("Bad Request");
1256
- return;
1257
- }
1258
- const url = new import_url2.URL(req.url, `http://127.0.0.1:${CALLBACK_PORT}`);
1259
- if (url.pathname === CALLBACK_PATH) {
1260
- const code = url.searchParams.get("code");
1261
- const state = url.searchParams.get("state");
1262
- const error = url.searchParams.get("error");
1263
- const errorDescription = url.searchParams.get("error_description");
1264
- if (error) {
1265
- const errorMessage = encodeURIComponent(`${error}: ${errorDescription || "Unknown error"}`);
1266
- res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/error?message=${errorMessage}` });
1267
- res.end();
1268
- server.close();
1269
- reject(new Error(`OAuth error: ${error} - ${errorDescription}`));
1270
- return;
1271
- }
1272
- if (state !== expectedState) {
1273
- const errorMessage = encodeURIComponent("State parameter mismatch. Possible CSRF attack.");
1274
- res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/error?message=${errorMessage}` });
1275
- res.end();
1276
- server.close();
1277
- reject(new Error("State mismatch - possible CSRF attack"));
1278
- return;
1279
- }
1280
- if (!code) {
1281
- const errorMessage = encodeURIComponent("No authorization code received.");
1282
- res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/error?message=${errorMessage}` });
1283
- res.end();
1284
- server.close();
1285
- reject(new Error("No authorization code received"));
1286
- return;
1287
- }
1288
- try {
1289
- const tokens = await exchangeCodeForTokens(code, codeVerifier);
1290
- res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/success` });
1291
- res.end();
1292
- server.close();
1293
- resolve(tokens);
1294
- } catch (tokenError) {
1295
- const errorMessage = encodeURIComponent(tokenError instanceof Error ? tokenError.message : "Unknown error");
1296
- res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/error?message=${errorMessage}` });
1297
- res.end();
1298
- server.close();
1299
- reject(tokenError);
1300
- }
1301
- } else {
1302
- res.writeHead(404);
1303
- res.end("Not Found");
1304
- }
1305
- } catch (serverError) {
1306
- res.writeHead(500);
1307
- res.end("Internal Server Error");
1308
- server.close();
1309
- reject(serverError);
1303
+ function addOrUpdateSSHConfigEntry(entry) {
1304
+ let config2 = readSSHConfig();
1305
+ const lines = config2.split("\n");
1306
+ const result = [];
1307
+ let inTargetBlock = false;
1308
+ for (const line of lines) {
1309
+ if (line.trim() === REPLICAS_MARKER_START) {
1310
+ const nextLines = lines.slice(lines.indexOf(line) + 1, lines.indexOf(line) + 3);
1311
+ const hostLine = nextLines.find((l) => l.trim().startsWith("Host "));
1312
+ if (hostLine && hostLine.includes(entry.host)) {
1313
+ inTargetBlock = true;
1314
+ continue;
1310
1315
  }
1311
- });
1312
- server.on("error", (err) => {
1313
- if (err.code === "EADDRINUSE") {
1314
- reject(new Error(`Port ${CALLBACK_PORT} is already in use. Please close any other OAuth flows and try again.`));
1315
- } else {
1316
- reject(err);
1316
+ result.push(line);
1317
+ continue;
1318
+ }
1319
+ if (line.trim() === REPLICAS_MARKER_END) {
1320
+ if (inTargetBlock) {
1321
+ inTargetBlock = false;
1322
+ continue;
1317
1323
  }
1318
- });
1319
- server.listen(CALLBACK_PORT, "127.0.0.1");
1320
- });
1321
- }
1322
- async function runCodexOAuthFlow() {
1323
- const pkce = generatePkceCodes();
1324
- const state = generateState2();
1325
- const authUrl = buildAuthorizationUrl(pkce.codeChallenge, state);
1326
- const tokensPromise = startCallbackServer(state, pkce.codeVerifier);
1327
- await new Promise((resolve) => setTimeout(resolve, 500));
1328
- console.log("If the browser does not open automatically, visit:");
1329
- console.log(authUrl);
1330
- console.log();
1331
- try {
1332
- await (0, import_open2.default)(authUrl);
1333
- } catch (openError) {
1334
- console.warn("Failed to open browser automatically. Please open the URL manually.");
1324
+ result.push(line);
1325
+ continue;
1326
+ }
1327
+ if (inTargetBlock) {
1328
+ continue;
1329
+ }
1330
+ result.push(line);
1335
1331
  }
1336
- console.log("Waiting for authentication...");
1337
- const tokens = await tokensPromise;
1338
- console.log("\u2713 Successfully obtained Codex credentials");
1339
- return tokens;
1332
+ config2 = result.join("\n");
1333
+ const newEntry = generateConfigBlock(entry);
1334
+ let finalConfig = config2.trim();
1335
+ if (finalConfig.length > 0) {
1336
+ finalConfig += "\n\n";
1337
+ }
1338
+ finalConfig += newEntry + "\n";
1339
+ writeSSHConfig(finalConfig);
1340
+ }
1341
+ function getWorkspaceHostAlias(workspaceName) {
1342
+ return `replicas-${workspaceName.toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
1340
1343
  }
1341
1344
 
1342
- // src/commands/codex-auth.ts
1343
- async function codexAuthCommand(options = {}) {
1344
- const isUserScoped = options.user === true;
1345
- const scopeLabel = isUserScoped ? "personal" : "organization";
1346
- console.log(import_chalk10.default.cyan(`Authenticating with Codex (${scopeLabel})...
1347
- `));
1345
+ // src/commands/code.ts
1346
+ async function codeCommand(workspaceName, options) {
1347
+ if (!isAuthenticated()) {
1348
+ console.log(import_chalk6.default.red('Not logged in. Please run "replicas login" first.'));
1349
+ process.exit(1);
1350
+ }
1348
1351
  try {
1349
- const tokens = await runCodexOAuthFlow();
1350
- console.log(import_chalk10.default.gray("\nUploading credentials to Replicas..."));
1351
- const endpoint = isUserScoped ? "/v1/codex/user/credentials" : "/v1/codex/credentials";
1352
- const response = await orgAuthenticatedFetch(
1353
- endpoint,
1354
- {
1355
- method: "POST",
1356
- body: {
1357
- access_token: tokens.accessToken,
1358
- refresh_token: tokens.refreshToken,
1359
- id_token: tokens.idToken
1360
- }
1361
- }
1352
+ const { workspace, sshToken, sshHost, portMappings, repoName } = await prepareWorkspaceConnection(
1353
+ workspaceName,
1354
+ options
1362
1355
  );
1363
- if (response.success) {
1364
- console.log(import_chalk10.default.green("\n\u2713 Codex authentication complete!"));
1365
- if (response.email) {
1366
- console.log(import_chalk10.default.gray(` Account: ${response.email}`));
1367
- }
1368
- if (response.planType) {
1369
- console.log(import_chalk10.default.gray(` Plan: ${response.planType}`));
1370
- }
1371
- if (isUserScoped) {
1372
- console.log(import_chalk10.default.gray("\nYour personal Codex credentials have been saved.\n"));
1373
- } else {
1374
- console.log(import_chalk10.default.gray("\nYou can now use Codex in your workspaces.\n"));
1356
+ const hostAlias = getWorkspaceHostAlias(workspace.name);
1357
+ console.log(import_chalk6.default.blue("\nConfiguring SSH connection..."));
1358
+ addOrUpdateSSHConfigEntry({
1359
+ host: hostAlias,
1360
+ hostname: sshHost,
1361
+ user: sshToken,
1362
+ portForwards: portMappings
1363
+ });
1364
+ console.log(import_chalk6.default.green(`\u2713 SSH config entry created: ${hostAlias}`));
1365
+ const remotePath = repoName ? `${SANDBOX_PATHS.WORKSPACES_DIR}/${repoName}` : SANDBOX_PATHS.HOME_DIR;
1366
+ const ideCommand = getIdeCommand();
1367
+ const fullCommand = `${ideCommand} --remote ssh-remote+${hostAlias} ${remotePath}`;
1368
+ console.log(import_chalk6.default.blue(`
1369
+ Opening ${ideCommand} for workspace ${workspace.name}...`));
1370
+ console.log(import_chalk6.default.gray(`Command: ${fullCommand}`));
1371
+ if (portMappings.size > 0) {
1372
+ console.log(import_chalk6.default.cyan("\nPort forwarding configured:"));
1373
+ for (const [remotePort, localPort] of portMappings) {
1374
+ console.log(import_chalk6.default.cyan(` \u2022 localhost:${localPort} \u2192 workspace:${remotePort}`));
1375
1375
  }
1376
- } else {
1377
- throw new Error(response.error || "Failed to upload Codex credentials to Replicas");
1378
1376
  }
1377
+ console.log(import_chalk6.default.yellow(`
1378
+ Note: SSH tokens expire after 3 hours. Run this command again to refresh.`));
1379
+ const ide = (0, import_child_process4.spawn)(ideCommand, [
1380
+ "--remote",
1381
+ `ssh-remote+${hostAlias}`,
1382
+ remotePath
1383
+ ], {
1384
+ stdio: "inherit",
1385
+ detached: false
1386
+ });
1387
+ ide.on("error", (error) => {
1388
+ console.error(import_chalk6.default.red(`
1389
+ Failed to launch ${ideCommand}: ${error.message}`));
1390
+ console.log(import_chalk6.default.yellow(`
1391
+ Make sure ${ideCommand} is installed and available in your PATH.`));
1392
+ console.log(import_chalk6.default.gray(`You can configure a different IDE with: replicas config set ide <command>`));
1393
+ process.exit(1);
1394
+ });
1395
+ ide.on("close", (code) => {
1396
+ if (code === 0) {
1397
+ console.log(import_chalk6.default.green(`
1398
+ \u2713 ${ideCommand} closed successfully.
1399
+ `));
1400
+ }
1401
+ });
1379
1402
  } catch (error) {
1380
- console.log(import_chalk10.default.red("\n\u2717 Error:"), error instanceof Error ? error.message : error);
1403
+ console.error(import_chalk6.default.red(`
1404
+ Error: ${error instanceof Error ? error.message : "Unknown error"}`));
1381
1405
  process.exit(1);
1382
1406
  }
1383
1407
  }
1384
1408
 
1385
- // src/commands/claude-auth.ts
1386
- var import_chalk12 = __toESM(require("chalk"));
1387
-
1388
- // src/lib/claude-oauth.ts
1389
- var import_open3 = __toESM(require("open"));
1390
- var import_readline = __toESM(require("readline"));
1391
- var import_chalk11 = __toESM(require("chalk"));
1392
- var DEFAULT_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
1393
- var CLAUDE_CLIENT_ID = process.env.CLAUDE_OAUTH_CLIENT_ID || DEFAULT_CLIENT_ID;
1394
- var AUTHORIZATION_ENDPOINT2 = "https://claude.ai/oauth/authorize";
1395
- var TOKEN_ENDPOINT2 = "https://console.anthropic.com/v1/oauth/token";
1396
- var REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback";
1397
- var SCOPES = "org:create_api_key user:profile user:inference";
1398
- function buildAuthorizationUrl2(codeChallenge, state) {
1399
- const params = new URLSearchParams({
1400
- code: "true",
1401
- client_id: CLAUDE_CLIENT_ID,
1402
- response_type: "code",
1403
- redirect_uri: REDIRECT_URI,
1404
- scope: SCOPES,
1405
- code_challenge: codeChallenge,
1406
- code_challenge_method: "S256",
1407
- state
1408
- });
1409
- return `${AUTHORIZATION_ENDPOINT2}?${params.toString()}`;
1410
- }
1411
- async function promptForAuthorizationCode(instruction) {
1412
- const rl = import_readline.default.createInterface({
1413
- input: process.stdin,
1414
- output: process.stdout
1415
- });
1416
- return new Promise((resolve) => {
1417
- rl.question(instruction, (answer) => {
1418
- rl.close();
1419
- resolve(answer.trim());
1420
- });
1421
- });
1422
- }
1423
- function parseUserInput(input) {
1424
- if (!input.includes("#") && !input.includes("code=")) {
1425
- return { code: input };
1409
+ // src/commands/org.ts
1410
+ var import_chalk7 = __toESM(require("chalk"));
1411
+ var import_prompts2 = __toESM(require("prompts"));
1412
+ async function orgCommand() {
1413
+ if (!isAuthenticated()) {
1414
+ console.log(import_chalk7.default.red('Not logged in. Please run "replicas login" first.'));
1415
+ process.exit(1);
1426
1416
  }
1427
- if (input.startsWith("http://") || input.startsWith("https://")) {
1428
- try {
1429
- const url = new URL(input);
1430
- const fragment = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
1431
- const params = new URLSearchParams(fragment);
1432
- const code2 = params.get("code") ?? void 0;
1433
- const state2 = params.get("state") ?? void 0;
1434
- if (code2) {
1435
- return { code: code2, state: state2 };
1436
- }
1437
- } catch {
1417
+ try {
1418
+ const currentOrgId = getOrganizationId();
1419
+ const organizations = await fetchOrganizations();
1420
+ if (!currentOrgId) {
1421
+ console.log(import_chalk7.default.yellow("No organization selected."));
1422
+ console.log(import_chalk7.default.gray('Run "replicas org switch" to select an organization.\n'));
1423
+ return;
1438
1424
  }
1425
+ const currentOrg = organizations.find((org2) => org2.id === currentOrgId);
1426
+ if (!currentOrg) {
1427
+ console.log(import_chalk7.default.yellow(`Current organization ID (${currentOrgId}) not found.`));
1428
+ console.log(import_chalk7.default.gray('Run "replicas org switch" to select a new organization.\n'));
1429
+ return;
1430
+ }
1431
+ console.log(import_chalk7.default.green("\nCurrent organization:"));
1432
+ console.log(import_chalk7.default.gray(` Name: ${currentOrg.name}`));
1433
+ console.log(import_chalk7.default.gray(` ID: ${currentOrg.id}
1434
+ `));
1435
+ } catch (error) {
1436
+ console.error(import_chalk7.default.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
1437
+ process.exit(1);
1439
1438
  }
1440
- const [code, state] = input.split("#");
1441
- return { code, state };
1442
- }
1443
- async function exchangeCodeForTokens2(code, state, expectedState, codeVerifier) {
1444
- if (state && state !== expectedState) {
1445
- throw new Error("State mismatch detected. Please restart the authentication flow.");
1446
- }
1447
- const payload = {
1448
- code,
1449
- state: state ?? expectedState,
1450
- grant_type: "authorization_code",
1451
- client_id: CLAUDE_CLIENT_ID,
1452
- redirect_uri: REDIRECT_URI,
1453
- code_verifier: codeVerifier
1454
- };
1455
- const response = await fetch(TOKEN_ENDPOINT2, {
1456
- method: "POST",
1457
- headers: {
1458
- "Content-Type": "application/json"
1459
- },
1460
- body: JSON.stringify(payload)
1461
- });
1462
- if (!response.ok) {
1463
- const text = await response.text();
1464
- throw new Error(`Failed to exchange authorization code: ${response.status} ${text}`);
1465
- }
1466
- const data = await response.json();
1467
- const now = Date.now();
1468
- const expiresAt = new Date(now + data.expires_in * 1e3).toISOString();
1469
- const refreshTokenExpiresAt = data.refresh_token_expires_in ? new Date(now + data.refresh_token_expires_in * 1e3).toISOString() : void 0;
1470
- return {
1471
- accessToken: data.access_token,
1472
- refreshToken: data.refresh_token,
1473
- tokenType: data.token_type,
1474
- scope: data.scope,
1475
- expiresAt,
1476
- refreshTokenExpiresAt
1477
- };
1478
1439
  }
1479
- async function runClaudeOAuthFlow() {
1480
- const pkce = generatePkceCodes();
1481
- const state = generateState2();
1482
- const authUrl = buildAuthorizationUrl2(pkce.codeChallenge, state);
1483
- console.log(import_chalk11.default.gray("We will open your browser so you can authenticate with Claude Code."));
1484
- console.log(import_chalk11.default.gray("If the browser does not open automatically, use the URL below:\n"));
1485
- console.log(authUrl);
1486
- console.log();
1487
- try {
1488
- await (0, import_open3.default)(authUrl);
1489
- } catch {
1490
- console.warn(import_chalk11.default.yellow("Failed to open browser automatically. Please copy and open the URL manually."));
1491
- }
1492
- console.log(import_chalk11.default.cyan("After completing authentication, copy the code shown on the success page."));
1493
- console.log(import_chalk11.default.cyan("You can paste either the full URL, or a value formatted as CODE#STATE.\n"));
1494
- const userInput = await promptForAuthorizationCode("Paste the authorization code (or URL) here: ");
1495
- if (!userInput) {
1496
- throw new Error("No authorization code provided.");
1497
- }
1498
- const { code, state: returnedState } = parseUserInput(userInput);
1499
- if (!code) {
1500
- throw new Error("Unable to parse authorization code. Please try again.");
1440
+ async function orgSwitchCommand() {
1441
+ if (!isAuthenticated()) {
1442
+ console.log(import_chalk7.default.red('Not logged in. Please run "replicas login" first.'));
1443
+ process.exit(1);
1501
1444
  }
1502
- console.log(import_chalk11.default.gray("\nExchanging authorization code for tokens..."));
1503
- const tokens = await exchangeCodeForTokens2(code, returnedState, state, pkce.codeVerifier);
1504
- console.log(import_chalk11.default.green("\u2713 Successfully obtained Claude credentials"));
1505
- return tokens;
1506
- }
1507
-
1508
- // src/commands/claude-auth.ts
1509
- async function claudeAuthCommand(options = {}) {
1510
- const isUserScoped = options.user === true;
1511
- const scopeLabel = isUserScoped ? "personal" : "organization";
1512
- console.log(import_chalk12.default.cyan(`Authenticating with Claude Code (${scopeLabel})...
1513
- `));
1514
1445
  try {
1515
- const tokens = await runClaudeOAuthFlow();
1516
- console.log(import_chalk12.default.gray("\nUploading credentials to Replicas..."));
1517
- const endpoint = isUserScoped ? "/v1/claude/user/credentials" : "/v1/claude/credentials";
1518
- const response = await orgAuthenticatedFetch(
1519
- endpoint,
1520
- {
1521
- method: "POST",
1522
- body: {
1523
- access_token: tokens.accessToken,
1524
- refresh_token: tokens.refreshToken,
1525
- token_type: tokens.tokenType,
1526
- scope: tokens.scope,
1527
- expires_at: tokens.expiresAt,
1528
- refresh_token_expires_at: tokens.refreshTokenExpiresAt
1529
- }
1530
- }
1531
- );
1532
- if (!response.success) {
1533
- throw new Error(response.error || "Failed to upload Claude credentials to Replicas");
1446
+ const organizations = await fetchOrganizations();
1447
+ if (organizations.length === 0) {
1448
+ console.log(import_chalk7.default.yellow("You are not a member of any organization."));
1449
+ console.log(import_chalk7.default.gray("Please contact support.\n"));
1450
+ return;
1534
1451
  }
1535
- console.log(import_chalk12.default.green("\n\u2713 Claude authentication complete!"));
1536
- if (response.email) {
1537
- console.log(import_chalk12.default.gray(` Account: ${response.email}`));
1452
+ if (organizations.length === 1) {
1453
+ await setActiveOrganization(organizations[0].id);
1454
+ console.log(import_chalk7.default.green(`
1455
+ \u2713 Organization set to: ${organizations[0].name}
1456
+ `));
1457
+ return;
1538
1458
  }
1539
- if (response.planType) {
1540
- console.log(import_chalk12.default.gray(` Plan: ${response.planType}`));
1459
+ const currentOrgId = getOrganizationId();
1460
+ const response = await (0, import_prompts2.default)({
1461
+ type: "select",
1462
+ name: "organizationId",
1463
+ message: "Select an organization:",
1464
+ choices: organizations.map((org2) => ({
1465
+ title: `${org2.name}${org2.id === currentOrgId ? " (current)" : ""}`,
1466
+ value: org2.id,
1467
+ description: org2.id
1468
+ })),
1469
+ initial: organizations.findIndex((org2) => org2.id === currentOrgId)
1470
+ });
1471
+ if (!response.organizationId) {
1472
+ console.log(import_chalk7.default.yellow("\nCancelled."));
1473
+ return;
1541
1474
  }
1542
- if (isUserScoped) {
1543
- console.log(import_chalk12.default.gray("\nYour personal Claude credentials have been saved.\n"));
1475
+ await setActiveOrganization(response.organizationId);
1476
+ const selectedOrg = organizations.find((org2) => org2.id === response.organizationId);
1477
+ console.log(import_chalk7.default.green(`
1478
+ \u2713 Switched to organization: ${selectedOrg?.name}
1479
+ `));
1480
+ } catch (error) {
1481
+ console.error(import_chalk7.default.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
1482
+ process.exit(1);
1483
+ }
1484
+ }
1485
+
1486
+ // src/commands/config.ts
1487
+ var import_chalk8 = __toESM(require("chalk"));
1488
+ async function configGetCommand(key) {
1489
+ if (!isAuthenticated()) {
1490
+ console.log(import_chalk8.default.red('Not logged in. Please run "replicas login" first.'));
1491
+ process.exit(1);
1492
+ }
1493
+ try {
1494
+ if (key === "ide") {
1495
+ const ideCommand = getIdeCommand();
1496
+ console.log(import_chalk8.default.green(`
1497
+ IDE command: ${ideCommand}
1498
+ `));
1544
1499
  } else {
1545
- console.log(import_chalk12.default.gray("\nYou can now use Claude Code in your workspaces.\n"));
1500
+ console.log(import_chalk8.default.red(`Unknown config key: ${key}`));
1501
+ console.log(import_chalk8.default.gray("Available keys: ide"));
1502
+ process.exit(1);
1546
1503
  }
1547
1504
  } catch (error) {
1548
- console.log(import_chalk12.default.red("\n\u2717 Error:"), error instanceof Error ? error.message : error);
1505
+ console.error(import_chalk8.default.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
1549
1506
  process.exit(1);
1550
1507
  }
1551
1508
  }
1552
-
1553
- // src/commands/init.ts
1554
- var import_chalk13 = __toESM(require("chalk"));
1555
- var import_fs4 = __toESM(require("fs"));
1556
- var import_path5 = __toESM(require("path"));
1557
- var REPLICAS_CONFIG_FILENAME = "replicas.json";
1558
- function getDefaultConfig() {
1559
- return {
1560
- copy: [],
1561
- ports: [],
1562
- systemPrompt: "",
1563
- startHook: {
1564
- timeout: 3e5,
1565
- commands: []
1566
- }
1567
- };
1568
- }
1569
- function initCommand(options) {
1570
- const configPath = import_path5.default.join(process.cwd(), REPLICAS_CONFIG_FILENAME);
1571
- if (import_fs4.default.existsSync(configPath) && !options.force) {
1572
- console.log(
1573
- import_chalk13.default.yellow(
1574
- `${REPLICAS_CONFIG_FILENAME} already exists in this directory.`
1575
- )
1576
- );
1577
- console.log(import_chalk13.default.gray("Use --force to overwrite the existing file."));
1578
- return;
1509
+ async function configSetCommand(key, value) {
1510
+ if (!isAuthenticated()) {
1511
+ console.log(import_chalk8.default.red('Not logged in. Please run "replicas login" first.'));
1512
+ process.exit(1);
1579
1513
  }
1580
- const defaultConfig = getDefaultConfig();
1581
- const configContent = JSON.stringify(defaultConfig, null, 2);
1582
1514
  try {
1583
- import_fs4.default.writeFileSync(configPath, configContent + "\n", "utf-8");
1584
- console.log(import_chalk13.default.green(`\u2713 Created ${REPLICAS_CONFIG_FILENAME}`));
1585
- console.log("");
1586
- console.log(import_chalk13.default.gray("Configuration options:"));
1587
- console.log(
1588
- import_chalk13.default.gray(' copy - Files to sync to the workspace (e.g., [".env"])')
1589
- );
1590
- console.log(
1591
- import_chalk13.default.gray(" ports - Ports to forward from the workspace (e.g., [3000, 8080])")
1592
- );
1593
- console.log(
1594
- import_chalk13.default.gray(" systemPrompt - Custom instructions for AI coding assistants")
1595
- );
1596
- console.log(
1597
- import_chalk13.default.gray(" startHook - Commands to run on workspace startup")
1598
- );
1515
+ if (key === "ide") {
1516
+ setIdeCommand(value);
1517
+ console.log(import_chalk8.default.green(`
1518
+ \u2713 IDE command set to: ${value}
1519
+ `));
1520
+ } else {
1521
+ console.log(import_chalk8.default.red(`Unknown config key: ${key}`));
1522
+ console.log(import_chalk8.default.gray("Available keys: ide"));
1523
+ process.exit(1);
1524
+ }
1599
1525
  } catch (error) {
1600
- throw new Error(
1601
- `Failed to create ${REPLICAS_CONFIG_FILENAME}: ${error instanceof Error ? error.message : "Unknown error"}`
1602
- );
1526
+ console.error(import_chalk8.default.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
1527
+ process.exit(1);
1603
1528
  }
1604
1529
  }
1605
-
1606
- // src/lib/version-check.ts
1607
- var import_chalk14 = __toESM(require("chalk"));
1608
- var MONOLITH_URL2 = process.env.REPLICAS_MONOLITH_URL || "https://api.replicas.dev";
1609
- var VERSION_CHECK_TIMEOUT = 2e3;
1610
- function compareVersions(v1, v2) {
1611
- const parts1 = v1.split(".").map(Number);
1612
- const parts2 = v2.split(".").map(Number);
1613
- for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
1614
- const num1 = parts1[i] || 0;
1615
- const num2 = parts2[i] || 0;
1616
- if (num1 > num2) return 1;
1617
- if (num1 < num2) return -1;
1530
+ async function configListCommand() {
1531
+ if (!isAuthenticated()) {
1532
+ console.log(import_chalk8.default.red('Not logged in. Please run "replicas login" first.'));
1533
+ process.exit(1);
1618
1534
  }
1619
- return 0;
1620
- }
1621
- async function checkForUpdates(currentVersion) {
1622
1535
  try {
1623
- const controller = new AbortController();
1624
- const timeoutId = setTimeout(() => controller.abort(), VERSION_CHECK_TIMEOUT);
1625
- const response = await fetch(`${MONOLITH_URL2}/v1/cli/version`, {
1626
- signal: controller.signal
1627
- });
1628
- clearTimeout(timeoutId);
1629
- if (!response.ok) return;
1630
- const data = await response.json();
1631
- const latestVersion = data.version;
1632
- if (compareVersions(latestVersion, currentVersion) > 0) {
1633
- console.error(import_chalk14.default.gray(`
1634
- Update available: ${currentVersion} \u2192 ${latestVersion}`));
1635
- console.error(import_chalk14.default.gray(`Run: npm i -g replicas-cli@latest
1636
- `));
1536
+ const config2 = readConfig();
1537
+ if (!config2) {
1538
+ console.log(import_chalk8.default.red("No config found. Please login first."));
1539
+ process.exit(1);
1637
1540
  }
1638
- } catch {
1541
+ console.log(import_chalk8.default.green("\nCurrent configuration:"));
1542
+ console.log(import_chalk8.default.gray(` Organization ID: ${config2.organization_id || "(not set)"}`));
1543
+ console.log(import_chalk8.default.gray(` IDE command: ${config2.ide_command || "code (default)"}`));
1544
+ console.log();
1545
+ } catch (error) {
1546
+ console.error(import_chalk8.default.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
1547
+ process.exit(1);
1639
1548
  }
1640
1549
  }
1641
1550
 
1642
- // src/commands/replica.ts
1643
- var import_chalk15 = __toESM(require("chalk"));
1644
- var import_prompts3 = __toESM(require("prompts"));
1551
+ // src/commands/tools.ts
1552
+ var import_chalk9 = __toESM(require("chalk"));
1553
+ var import_figlet = __toESM(require("figlet"));
1554
+ async function toolsCommand() {
1555
+ const banner = import_figlet.default.textSync("Replicas Machine Image", {
1556
+ horizontalLayout: "default",
1557
+ verticalLayout: "default",
1558
+ whitespaceBreak: true
1559
+ });
1560
+ console.log(import_chalk9.default.cyan(banner));
1561
+ console.log();
1562
+ console.log(import_chalk9.default.bold("RMI Version: v0.1.3"));
1563
+ console.log();
1564
+ console.log(
1565
+ import_chalk9.default.gray(
1566
+ "The following tooling and dependencies are provided by default on the Replicas Machine Image.\n"
1567
+ )
1568
+ );
1569
+ const sections = [
1570
+ {
1571
+ title: "Python",
1572
+ tools: ["Python 3.12.3", "pip 24", "Poetry 2.2.1", "uv 0.9.5"]
1573
+ },
1574
+ {
1575
+ title: "Node.js",
1576
+ tools: [
1577
+ "Node.js 22.21.0",
1578
+ "npm 10.9.4",
1579
+ "nvm 0.40.3",
1580
+ "Yarn 1.22.22",
1581
+ "pnpm 10.18.3",
1582
+ "ESLint 9.38.0",
1583
+ "Prettier 3.6.2"
1584
+ ]
1585
+ },
1586
+ {
1587
+ title: "C/C++",
1588
+ tools: ["gcc 13.3.0", "g++ 13.3.0", "make 4.3"]
1589
+ },
1590
+ {
1591
+ title: "Perl",
1592
+ tools: ["Perl 5.38.2"]
1593
+ },
1594
+ {
1595
+ title: "AI Coding Assistants",
1596
+ tools: ["Claude Code (@anthropic-ai/claude-code)", "OpenAI Codex (@openai/codex)"]
1597
+ },
1598
+ {
1599
+ title: "Misc",
1600
+ tools: [
1601
+ "git 2.43.0",
1602
+ "vim 9.1.697",
1603
+ "nano 7.2",
1604
+ "curl",
1605
+ "wget",
1606
+ "GitHub CLI (gh)",
1607
+ "Docker Engine"
1608
+ ]
1609
+ }
1610
+ ];
1611
+ sections.forEach((section) => {
1612
+ console.log(import_chalk9.default.blue(section.title));
1613
+ section.tools.forEach((tool) => {
1614
+ console.log(` - ${tool}`);
1615
+ });
1616
+ console.log();
1617
+ });
1618
+ }
1645
1619
 
1646
- // ../shared/src/sandbox.ts
1647
- var SANDBOX_LIFECYCLE = {
1648
- AUTO_STOP_MINUTES: 60,
1649
- AUTO_ARCHIVE_MINUTES: 60 * 24 * 7,
1650
- AUTO_DELETE_MINUTES: -1,
1651
- SSH_TOKEN_EXPIRATION_MINUTES: 3 * 60
1652
- };
1620
+ // src/commands/codex-auth.ts
1621
+ var import_chalk10 = __toESM(require("chalk"));
1653
1622
 
1654
- // ../shared/src/display-message/parsers/codex-parser.ts
1655
- function safeJsonParse(str, fallback) {
1656
- try {
1657
- return JSON.parse(str);
1658
- } catch {
1659
- return fallback;
1660
- }
1623
+ // src/lib/codex-oauth.ts
1624
+ var import_http2 = __toESM(require("http"));
1625
+ var import_url2 = require("url");
1626
+ var import_open2 = __toESM(require("open"));
1627
+
1628
+ // src/lib/pkce.ts
1629
+ var import_crypto = require("crypto");
1630
+ function generatePkceCodes() {
1631
+ const verifierBytes = (0, import_crypto.randomBytes)(32);
1632
+ const codeVerifier = verifierBytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1633
+ const hash = (0, import_crypto.createHash)("sha256");
1634
+ hash.update(codeVerifier);
1635
+ const codeChallenge = hash.digest("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1636
+ return {
1637
+ codeVerifier,
1638
+ codeChallenge
1639
+ };
1661
1640
  }
1662
- function getStatusFromExitCode(exitCode) {
1663
- return exitCode === 0 ? "completed" : "failed";
1641
+ function generateState2() {
1642
+ return (0, import_crypto.randomBytes)(16).toString("hex");
1664
1643
  }
1665
- function findLastMessageByType(messages, type) {
1666
- return messages.findLast((m) => m.type === type);
1644
+
1645
+ // src/lib/codex-oauth.ts
1646
+ var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
1647
+ var AUTHORIZATION_ENDPOINT = "https://auth.openai.com/oauth/authorize";
1648
+ var TOKEN_ENDPOINT = "https://auth.openai.com/oauth/token";
1649
+ var CALLBACK_PORT = 1455;
1650
+ var CALLBACK_PATH = "/auth/callback";
1651
+ var WEB_APP_URL2 = process.env.REPLICAS_WEB_URL || "https://replicas.dev";
1652
+ function buildAuthorizationUrl(codeChallenge, state) {
1653
+ const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
1654
+ const params = new URLSearchParams({
1655
+ response_type: "code",
1656
+ client_id: CODEX_OAUTH_CLIENT_ID,
1657
+ redirect_uri: redirectUri,
1658
+ scope: "openid profile email offline_access",
1659
+ code_challenge: codeChallenge,
1660
+ code_challenge_method: "S256",
1661
+ id_token_add_organizations: "true",
1662
+ codex_cli_simplified_flow: "true",
1663
+ state,
1664
+ originator: "codex_cli_rs"
1665
+ });
1666
+ return `${AUTHORIZATION_ENDPOINT}?${params.toString()}`;
1667
1667
  }
1668
- function parsePatch(input) {
1669
- const operations = [];
1670
- const lines = input.split("\n");
1671
- let currentOp = null;
1672
- let diffLines = [];
1673
- for (let i = 0; i < lines.length; i++) {
1674
- const line = lines[i];
1675
- if (line.startsWith("*** Add File:")) {
1676
- if (currentOp) {
1677
- if (diffLines.length > 0) currentOp.diff = diffLines.join("\n");
1678
- operations.push(currentOp);
1679
- diffLines = [];
1680
- }
1681
- currentOp = {
1682
- action: "add",
1683
- path: line.replace("*** Add File:", "").trim()
1684
- };
1685
- } else if (line.startsWith("*** Update File:")) {
1686
- if (currentOp) {
1687
- if (diffLines.length > 0) currentOp.diff = diffLines.join("\n");
1688
- operations.push(currentOp);
1689
- diffLines = [];
1690
- }
1691
- currentOp = {
1692
- action: "update",
1693
- path: line.replace("*** Update File:", "").trim()
1694
- };
1695
- } else if (line.startsWith("*** Delete File:")) {
1696
- if (currentOp) {
1697
- if (diffLines.length > 0) currentOp.diff = diffLines.join("\n");
1698
- operations.push(currentOp);
1699
- diffLines = [];
1700
- }
1701
- currentOp = {
1702
- action: "delete",
1703
- path: line.replace("*** Delete File:", "").trim()
1704
- };
1705
- } else if (line.startsWith("*** Move to:")) {
1706
- if (currentOp) {
1707
- currentOp.moveTo = line.replace("*** Move to:", "").trim();
1708
- }
1709
- } else if (line.startsWith("*** Begin Patch") || line.startsWith("*** End Patch")) {
1710
- continue;
1711
- } else if (currentOp && (line.startsWith("+") || line.startsWith("-") || line.startsWith("@@") || line.trim() === "")) {
1712
- diffLines.push(line);
1713
- }
1714
- }
1715
- if (currentOp) {
1716
- if (diffLines.length > 0) currentOp.diff = diffLines.join("\n");
1717
- operations.push(currentOp);
1668
+ async function exchangeCodeForTokens(code, codeVerifier) {
1669
+ const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
1670
+ const params = new URLSearchParams({
1671
+ grant_type: "authorization_code",
1672
+ code,
1673
+ redirect_uri: redirectUri,
1674
+ client_id: CODEX_OAUTH_CLIENT_ID,
1675
+ code_verifier: codeVerifier
1676
+ });
1677
+ const response = await fetch(TOKEN_ENDPOINT, {
1678
+ method: "POST",
1679
+ headers: {
1680
+ "Content-Type": "application/x-www-form-urlencoded"
1681
+ },
1682
+ body: params.toString()
1683
+ });
1684
+ if (!response.ok) {
1685
+ const errorText = await response.text();
1686
+ throw new Error(`Failed to exchange code for tokens: ${response.status} ${errorText}`);
1718
1687
  }
1719
- return operations;
1688
+ const data = await response.json();
1689
+ return {
1690
+ idToken: data.id_token,
1691
+ accessToken: data.access_token,
1692
+ refreshToken: data.refresh_token
1693
+ };
1720
1694
  }
1721
- function parseCodexEvents(events) {
1722
- const messages = [];
1723
- const pendingPatches = /* @__PURE__ */ new Map();
1724
- events.forEach((event) => {
1725
- if (event.type === "event_msg" && event.payload?.type === "user_message") {
1726
- messages.push({
1727
- id: `user-${event.timestamp}`,
1728
- type: "user",
1729
- content: event.payload.message || "",
1730
- timestamp: event.timestamp
1731
- });
1732
- }
1733
- if (event.type === "event_msg" && event.payload?.type === "agent_reasoning") {
1734
- messages.push({
1735
- id: `reasoning-${event.timestamp}-${messages.length}`,
1736
- type: "reasoning",
1737
- content: event.payload.text || "",
1738
- status: "completed",
1739
- timestamp: event.timestamp
1740
- });
1741
- }
1742
- if (event.type === "response_item") {
1743
- const payloadType = event.payload?.type;
1744
- if (payloadType === "message" && event.payload?.role === "assistant") {
1745
- const content = event.payload.content || [];
1746
- const textContent = content.filter((c) => c.type === "output_text").map((c) => c.text || "").join("\n");
1747
- if (textContent) {
1748
- messages.push({
1749
- id: `agent-${event.timestamp}`,
1750
- type: "agent",
1751
- content: textContent,
1752
- timestamp: event.timestamp
1753
- });
1695
+ function startCallbackServer(expectedState, codeVerifier) {
1696
+ return new Promise((resolve, reject) => {
1697
+ const server = import_http2.default.createServer(async (req, res) => {
1698
+ try {
1699
+ if (!req.url) {
1700
+ res.writeHead(400);
1701
+ res.end("Bad Request");
1702
+ return;
1754
1703
  }
1755
- }
1756
- if (payloadType === "function_call" && event.payload?.name === "shell") {
1757
- const args = safeJsonParse(event.payload.arguments || "{}", {});
1758
- const command = Array.isArray(args.command) ? args.command.join(" ") : args.command || "";
1759
- messages.push({
1760
- id: `command-${event.timestamp}`,
1761
- type: "command",
1762
- command,
1763
- output: "",
1764
- // Will be filled by function_call_output
1765
- status: "in_progress",
1766
- timestamp: event.timestamp
1767
- });
1768
- }
1769
- if (payloadType === "function_call_output") {
1770
- const output = safeJsonParse(
1771
- event.payload.output || "{}",
1772
- {}
1773
- );
1774
- if (output.output !== void 0 && output.metadata) {
1775
- const commandMsg = findLastMessageByType(messages, "command");
1776
- if (commandMsg) {
1777
- commandMsg.output = output.output || "";
1778
- commandMsg.exitCode = output.metadata?.exit_code;
1779
- commandMsg.status = getStatusFromExitCode(output.metadata?.exit_code);
1704
+ const url = new import_url2.URL(req.url, `http://127.0.0.1:${CALLBACK_PORT}`);
1705
+ if (url.pathname === CALLBACK_PATH) {
1706
+ const code = url.searchParams.get("code");
1707
+ const state = url.searchParams.get("state");
1708
+ const error = url.searchParams.get("error");
1709
+ const errorDescription = url.searchParams.get("error_description");
1710
+ if (error) {
1711
+ const errorMessage = encodeURIComponent(`${error}: ${errorDescription || "Unknown error"}`);
1712
+ res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/error?message=${errorMessage}` });
1713
+ res.end();
1714
+ server.close();
1715
+ reject(new Error(`OAuth error: ${error} - ${errorDescription}`));
1716
+ return;
1717
+ }
1718
+ if (state !== expectedState) {
1719
+ const errorMessage = encodeURIComponent("State parameter mismatch. Possible CSRF attack.");
1720
+ res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/error?message=${errorMessage}` });
1721
+ res.end();
1722
+ server.close();
1723
+ reject(new Error("State mismatch - possible CSRF attack"));
1724
+ return;
1725
+ }
1726
+ if (!code) {
1727
+ const errorMessage = encodeURIComponent("No authorization code received.");
1728
+ res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/error?message=${errorMessage}` });
1729
+ res.end();
1730
+ server.close();
1731
+ reject(new Error("No authorization code received"));
1732
+ return;
1733
+ }
1734
+ try {
1735
+ const tokens = await exchangeCodeForTokens(code, codeVerifier);
1736
+ res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/success` });
1737
+ res.end();
1738
+ server.close();
1739
+ resolve(tokens);
1740
+ } catch (tokenError) {
1741
+ const errorMessage = encodeURIComponent(tokenError instanceof Error ? tokenError.message : "Unknown error");
1742
+ res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/error?message=${errorMessage}` });
1743
+ res.end();
1744
+ server.close();
1745
+ reject(tokenError);
1780
1746
  }
1781
- }
1782
- }
1783
- if (payloadType === "custom_tool_call") {
1784
- const callId = event.payload.call_id;
1785
- const name = event.payload.name;
1786
- const input = event.payload.input || "";
1787
- const status = event.payload.status || "in_progress";
1788
- if (name === "apply_patch") {
1789
- const operations = parsePatch(input);
1790
- pendingPatches.set(callId, { input, status, timestamp: event.timestamp, operations });
1791
1747
  } else {
1792
- messages.push({
1793
- id: `toolcall-${event.timestamp}`,
1794
- type: "tool_call",
1795
- server: "custom",
1796
- tool: name,
1797
- status,
1798
- timestamp: event.timestamp
1799
- });
1748
+ res.writeHead(404);
1749
+ res.end("Not Found");
1800
1750
  }
1751
+ } catch (serverError) {
1752
+ res.writeHead(500);
1753
+ res.end("Internal Server Error");
1754
+ server.close();
1755
+ reject(serverError);
1801
1756
  }
1802
- if (payloadType === "custom_tool_call_output") {
1803
- const callId = event.payload.call_id;
1804
- const output = safeJsonParse(
1805
- event.payload.output || "{}",
1806
- {}
1807
- );
1808
- const pendingPatch = pendingPatches.get(callId);
1809
- if (pendingPatch) {
1810
- messages.push({
1811
- id: `patch-${pendingPatch.timestamp}`,
1812
- type: "patch",
1813
- operations: pendingPatch.operations,
1814
- output: output.output || "",
1815
- exitCode: output.metadata?.exit_code,
1816
- status: getStatusFromExitCode(output.metadata?.exit_code),
1817
- timestamp: pendingPatch.timestamp
1818
- });
1819
- pendingPatches.delete(callId);
1820
- } else {
1821
- const toolCallMsg = findLastMessageByType(messages, "tool_call");
1822
- if (toolCallMsg) {
1823
- toolCallMsg.status = getStatusFromExitCode(output.metadata?.exit_code);
1824
- }
1757
+ });
1758
+ server.on("error", (err) => {
1759
+ if (err.code === "EADDRINUSE") {
1760
+ reject(new Error(`Port ${CALLBACK_PORT} is already in use. Please close any other OAuth flows and try again.`));
1761
+ } else {
1762
+ reject(err);
1763
+ }
1764
+ });
1765
+ server.listen(CALLBACK_PORT, "127.0.0.1");
1766
+ });
1767
+ }
1768
+ async function runCodexOAuthFlow() {
1769
+ const pkce = generatePkceCodes();
1770
+ const state = generateState2();
1771
+ const authUrl = buildAuthorizationUrl(pkce.codeChallenge, state);
1772
+ const tokensPromise = startCallbackServer(state, pkce.codeVerifier);
1773
+ await new Promise((resolve) => setTimeout(resolve, 500));
1774
+ console.log("If the browser does not open automatically, visit:");
1775
+ console.log(authUrl);
1776
+ console.log();
1777
+ try {
1778
+ await (0, import_open2.default)(authUrl);
1779
+ } catch (openError) {
1780
+ console.warn("Failed to open browser automatically. Please open the URL manually.");
1781
+ }
1782
+ console.log("Waiting for authentication...");
1783
+ const tokens = await tokensPromise;
1784
+ console.log("\u2713 Successfully obtained Codex credentials");
1785
+ return tokens;
1786
+ }
1787
+
1788
+ // src/commands/codex-auth.ts
1789
+ async function codexAuthCommand(options = {}) {
1790
+ const isUserScoped = options.user === true;
1791
+ const scopeLabel = isUserScoped ? "personal" : "organization";
1792
+ console.log(import_chalk10.default.cyan(`Authenticating with Codex (${scopeLabel})...
1793
+ `));
1794
+ try {
1795
+ const tokens = await runCodexOAuthFlow();
1796
+ console.log(import_chalk10.default.gray("\nUploading credentials to Replicas..."));
1797
+ const endpoint = isUserScoped ? "/v1/codex/user/credentials" : "/v1/codex/credentials";
1798
+ const response = await orgAuthenticatedFetch(
1799
+ endpoint,
1800
+ {
1801
+ method: "POST",
1802
+ body: {
1803
+ access_token: tokens.accessToken,
1804
+ refresh_token: tokens.refreshToken,
1805
+ id_token: tokens.idToken
1825
1806
  }
1826
1807
  }
1827
- if (payloadType === "function_call" && event.payload?.name === "update_plan") {
1828
- const args = safeJsonParse(
1829
- event.payload.arguments || "{}",
1830
- {}
1831
- );
1832
- if (args.plan && Array.isArray(args.plan)) {
1833
- const todoItems = args.plan.map((item) => ({
1834
- text: item.step,
1835
- completed: item.status === "completed"
1836
- }));
1837
- messages.push({
1838
- id: `todo-${event.timestamp}`,
1839
- type: "todo_list",
1840
- items: todoItems,
1841
- status: "completed",
1842
- timestamp: event.timestamp
1843
- });
1844
- }
1808
+ );
1809
+ if (response.success) {
1810
+ console.log(import_chalk10.default.green("\n\u2713 Codex authentication complete!"));
1811
+ if (response.email) {
1812
+ console.log(import_chalk10.default.gray(` Account: ${response.email}`));
1813
+ }
1814
+ if (response.planType) {
1815
+ console.log(import_chalk10.default.gray(` Plan: ${response.planType}`));
1816
+ }
1817
+ if (isUserScoped) {
1818
+ console.log(import_chalk10.default.gray("\nYour personal Codex credentials have been saved.\n"));
1819
+ } else {
1820
+ console.log(import_chalk10.default.gray("\nYou can now use Codex in your workspaces.\n"));
1845
1821
  }
1822
+ } else {
1823
+ throw new Error(response.error || "Failed to upload Codex credentials to Replicas");
1846
1824
  }
1847
- });
1848
- return messages;
1849
- }
1850
-
1851
- // ../shared/src/display-message/parsers/claude-parser.ts
1852
- function safeJsonParse2(str, fallback) {
1853
- try {
1854
- return JSON.parse(str);
1855
- } catch {
1856
- return fallback;
1825
+ } catch (error) {
1826
+ console.log(import_chalk10.default.red("\n\u2717 Error:"), error instanceof Error ? error.message : error);
1827
+ process.exit(1);
1857
1828
  }
1858
1829
  }
1859
- function extractToolResultContent(content) {
1860
- if (!content) return "";
1861
- if (typeof content === "string") return content;
1862
- if (Array.isArray(content)) {
1863
- return content.filter((item) => item.type === "text").map((item) => item.text || "").join("\n");
1864
- }
1865
- return "";
1830
+
1831
+ // src/commands/claude-auth.ts
1832
+ var import_chalk12 = __toESM(require("chalk"));
1833
+
1834
+ // src/lib/claude-oauth.ts
1835
+ var import_open3 = __toESM(require("open"));
1836
+ var import_readline = __toESM(require("readline"));
1837
+ var import_chalk11 = __toESM(require("chalk"));
1838
+ var DEFAULT_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
1839
+ var CLAUDE_CLIENT_ID = process.env.CLAUDE_OAUTH_CLIENT_ID || DEFAULT_CLIENT_ID;
1840
+ var AUTHORIZATION_ENDPOINT2 = "https://claude.ai/oauth/authorize";
1841
+ var TOKEN_ENDPOINT2 = "https://console.anthropic.com/v1/oauth/token";
1842
+ var REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback";
1843
+ var SCOPES = "org:create_api_key user:profile user:inference";
1844
+ function buildAuthorizationUrl2(codeChallenge, state) {
1845
+ const params = new URLSearchParams({
1846
+ code: "true",
1847
+ client_id: CLAUDE_CLIENT_ID,
1848
+ response_type: "code",
1849
+ redirect_uri: REDIRECT_URI,
1850
+ scope: SCOPES,
1851
+ code_challenge: codeChallenge,
1852
+ code_challenge_method: "S256",
1853
+ state
1854
+ });
1855
+ return `${AUTHORIZATION_ENDPOINT2}?${params.toString()}`;
1866
1856
  }
1867
- function parseClaudeEvents(events, parentToolUseId) {
1868
- const messages = [];
1869
- const filterValue = parentToolUseId !== void 0 ? parentToolUseId : null;
1870
- const filteredEvents = events.filter(
1871
- (e) => e.payload.parent_tool_use_id === filterValue
1872
- );
1873
- const toolCallMap = /* @__PURE__ */ new Map();
1874
- filteredEvents.forEach((event) => {
1875
- if (event.type === "claude-user") {
1876
- const content = event.payload.message?.content || [];
1877
- const toolResult = content.find((c) => c.type === "tool_result");
1878
- if (toolResult && toolResult.tool_use_id) {
1879
- return;
1880
- }
1881
- const textContent = content.filter((c) => c.type === "text").map((c) => c.text || "").join("\n");
1882
- const images = content.filter((c) => c.type === "image" && c.source).map((c) => {
1883
- const source = c.source;
1884
- return {
1885
- type: "image",
1886
- mediaType: source.media_type || "image/png",
1887
- data: source.data || ""
1888
- };
1889
- }).filter((img) => img.data);
1890
- if (textContent || images.length > 0) {
1891
- messages.push({
1892
- id: `user-${event.timestamp}`,
1893
- type: "user",
1894
- content: textContent || (images.length > 0 ? `[${images.length} image${images.length > 1 ? "s" : ""} attached]` : ""),
1895
- images: images.length > 0 ? images : void 0,
1896
- timestamp: event.timestamp
1897
- });
1898
- }
1899
- }
1900
- if (event.type === "claude-assistant") {
1901
- const contentBlocks = event.payload.message?.content || [];
1902
- contentBlocks.forEach((block) => {
1903
- if (block.type === "text" && block.text) {
1904
- messages.push({
1905
- id: `agent-${event.timestamp}-${messages.length}`,
1906
- type: "agent",
1907
- content: block.text,
1908
- timestamp: event.timestamp
1909
- });
1910
- }
1911
- if (block.type === "thinking" && block.text) {
1912
- messages.push({
1913
- id: `reasoning-${event.timestamp}-${messages.length}`,
1914
- type: "reasoning",
1915
- content: block.text,
1916
- status: "completed",
1917
- timestamp: event.timestamp
1918
- });
1919
- }
1920
- if (block.type === "tool_use" && block.id) {
1921
- const toolName = block.name || "unknown";
1922
- const toolInput = block.input || {};
1923
- const toolUseId = block.id;
1924
- toolCallMap.set(toolUseId, {
1925
- messageIndex: messages.length,
1926
- toolName,
1927
- input: toolInput
1928
- });
1929
- if (toolName === "Task") {
1930
- const inputObj = typeof toolInput === "string" ? safeJsonParse2(toolInput, {}) : toolInput;
1931
- const nestedEvents = events.filter((e) => e.payload.parent_tool_use_id === toolUseId).map((e) => ({
1932
- timestamp: e.timestamp,
1933
- type: e.type,
1934
- payload: e.payload
1935
- }));
1936
- messages.push({
1937
- id: `subagent-${event.timestamp}-${messages.length}`,
1938
- type: "subagent",
1939
- toolUseId,
1940
- description: inputObj.description || "Subagent Task",
1941
- prompt: inputObj.prompt || "",
1942
- subagentType: inputObj.subagent_type || "general",
1943
- model: inputObj.model,
1944
- status: "in_progress",
1945
- nestedEvents,
1946
- timestamp: event.timestamp
1947
- });
1948
- } else if (toolName === "Bash" || toolName === "bash" || toolName === "shell") {
1949
- const inputObj = typeof toolInput === "string" ? safeJsonParse2(toolInput, {}) : toolInput;
1950
- const command = inputObj.command || "";
1951
- messages.push({
1952
- id: `command-${event.timestamp}-${messages.length}`,
1953
- type: "command",
1954
- command,
1955
- output: "",
1956
- status: "in_progress",
1957
- timestamp: event.timestamp
1958
- });
1959
- } else if (toolName === "Write" || toolName === "Edit") {
1960
- const inputObj = typeof toolInput === "string" ? safeJsonParse2(toolInput, {}) : toolInput;
1961
- const filePath = inputObj.file_path || "";
1962
- const action = toolName === "Write" ? "add" : "update";
1963
- messages.push({
1964
- id: `file-${event.timestamp}-${messages.length}`,
1965
- type: "file_change",
1966
- changes: [{
1967
- path: filePath,
1968
- kind: action
1969
- }],
1970
- status: "in_progress",
1971
- timestamp: event.timestamp
1972
- });
1973
- } else if (toolName === "TodoWrite") {
1974
- const inputObj = typeof toolInput === "string" ? safeJsonParse2(toolInput, {}) : toolInput;
1975
- const todos = inputObj.todos || [];
1976
- const todoItems = todos.map((todo) => ({
1977
- text: todo.content,
1978
- completed: todo.status === "completed"
1979
- }));
1980
- if (todoItems.length > 0) {
1981
- messages.push({
1982
- id: `todo-${event.timestamp}-${messages.length}`,
1983
- type: "todo_list",
1984
- items: todoItems,
1985
- status: "completed",
1986
- timestamp: event.timestamp
1987
- });
1988
- }
1989
- } else if (toolName === "WebSearch") {
1990
- const inputObj = typeof toolInput === "string" ? safeJsonParse2(toolInput, {}) : toolInput;
1991
- const query = inputObj.query || "";
1992
- messages.push({
1993
- id: `search-${event.timestamp}-${messages.length}`,
1994
- type: "web_search",
1995
- query,
1996
- status: "in_progress",
1997
- timestamp: event.timestamp
1998
- });
1999
- } else {
2000
- messages.push({
2001
- id: `toolcall-${event.timestamp}-${messages.length}`,
2002
- type: "tool_call",
2003
- server: "claude",
2004
- tool: toolName,
2005
- input: toolInput,
2006
- status: "in_progress",
2007
- timestamp: event.timestamp
2008
- });
2009
- }
2010
- }
2011
- });
2012
- }
2013
- if (event.type === "claude-result") {
2014
- const payload = event.payload;
2015
- if (payload.is_error || payload.subtype !== "success") {
2016
- const errorList = payload.errors || [];
2017
- const errorMessage = errorList.length > 0 ? errorList.join("\n") : "Claude session encountered an unexpected error.";
2018
- messages.push({
2019
- id: `error-${event.timestamp}`,
2020
- type: "error",
2021
- message: errorMessage,
2022
- timestamp: event.timestamp
2023
- });
1857
+ async function promptForAuthorizationCode(instruction) {
1858
+ const rl = import_readline.default.createInterface({
1859
+ input: process.stdin,
1860
+ output: process.stdout
1861
+ });
1862
+ return new Promise((resolve) => {
1863
+ rl.question(instruction, (answer) => {
1864
+ rl.close();
1865
+ resolve(answer.trim());
1866
+ });
1867
+ });
1868
+ }
1869
+ function parseUserInput(input) {
1870
+ if (!input.includes("#") && !input.includes("code=")) {
1871
+ return { code: input };
1872
+ }
1873
+ if (input.startsWith("http://") || input.startsWith("https://")) {
1874
+ try {
1875
+ const url = new URL(input);
1876
+ const fragment = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
1877
+ const params = new URLSearchParams(fragment);
1878
+ const code2 = params.get("code") ?? void 0;
1879
+ const state2 = params.get("state") ?? void 0;
1880
+ if (code2) {
1881
+ return { code: code2, state: state2 };
2024
1882
  }
1883
+ } catch {
2025
1884
  }
1885
+ }
1886
+ const [code, state] = input.split("#");
1887
+ return { code, state };
1888
+ }
1889
+ async function exchangeCodeForTokens2(code, state, expectedState, codeVerifier) {
1890
+ if (state && state !== expectedState) {
1891
+ throw new Error("State mismatch detected. Please restart the authentication flow.");
1892
+ }
1893
+ const payload = {
1894
+ code,
1895
+ state: state ?? expectedState,
1896
+ grant_type: "authorization_code",
1897
+ client_id: CLAUDE_CLIENT_ID,
1898
+ redirect_uri: REDIRECT_URI,
1899
+ code_verifier: codeVerifier
1900
+ };
1901
+ const response = await fetch(TOKEN_ENDPOINT2, {
1902
+ method: "POST",
1903
+ headers: {
1904
+ "Content-Type": "application/json"
1905
+ },
1906
+ body: JSON.stringify(payload)
2026
1907
  });
2027
- filteredEvents.forEach((event) => {
2028
- if (event.type === "claude-user") {
2029
- const content = event.payload.message?.content || [];
2030
- const toolResult = content.find((c) => c.type === "tool_result");
2031
- if (toolResult && toolResult.tool_use_id) {
2032
- const toolInfo = toolCallMap.get(toolResult.tool_use_id);
2033
- if (!toolInfo) return;
2034
- const resultContent = extractToolResultContent(toolResult.content);
2035
- const isError = toolResult.is_error || false;
2036
- const status = isError ? "failed" : "completed";
2037
- const message = messages[toolInfo.messageIndex];
2038
- if (!message) return;
2039
- if (message.type === "command") {
2040
- message.output = resultContent;
2041
- message.status = status;
2042
- const exitCodeMatch = resultContent.match(/exit code:?\s*(\d+)/i);
2043
- if (exitCodeMatch) {
2044
- message.exitCode = parseInt(exitCodeMatch[1], 10);
2045
- } else {
2046
- message.exitCode = isError ? 1 : 0;
2047
- }
2048
- } else if (message.type === "file_change") {
2049
- message.status = status;
2050
- } else if (message.type === "web_search") {
2051
- message.status = status;
2052
- } else if (message.type === "tool_call") {
2053
- message.output = resultContent;
2054
- message.status = status;
2055
- } else if (message.type === "subagent") {
2056
- message.output = resultContent;
2057
- message.status = status;
1908
+ if (!response.ok) {
1909
+ const text = await response.text();
1910
+ throw new Error(`Failed to exchange authorization code: ${response.status} ${text}`);
1911
+ }
1912
+ const data = await response.json();
1913
+ const now = Date.now();
1914
+ const expiresAt = new Date(now + data.expires_in * 1e3).toISOString();
1915
+ const refreshTokenExpiresAt = data.refresh_token_expires_in ? new Date(now + data.refresh_token_expires_in * 1e3).toISOString() : void 0;
1916
+ return {
1917
+ accessToken: data.access_token,
1918
+ refreshToken: data.refresh_token,
1919
+ tokenType: data.token_type,
1920
+ scope: data.scope,
1921
+ expiresAt,
1922
+ refreshTokenExpiresAt
1923
+ };
1924
+ }
1925
+ async function runClaudeOAuthFlow() {
1926
+ const pkce = generatePkceCodes();
1927
+ const state = generateState2();
1928
+ const authUrl = buildAuthorizationUrl2(pkce.codeChallenge, state);
1929
+ console.log(import_chalk11.default.gray("We will open your browser so you can authenticate with Claude Code."));
1930
+ console.log(import_chalk11.default.gray("If the browser does not open automatically, use the URL below:\n"));
1931
+ console.log(authUrl);
1932
+ console.log();
1933
+ try {
1934
+ await (0, import_open3.default)(authUrl);
1935
+ } catch {
1936
+ console.warn(import_chalk11.default.yellow("Failed to open browser automatically. Please copy and open the URL manually."));
1937
+ }
1938
+ console.log(import_chalk11.default.cyan("After completing authentication, copy the code shown on the success page."));
1939
+ console.log(import_chalk11.default.cyan("You can paste either the full URL, or a value formatted as CODE#STATE.\n"));
1940
+ const userInput = await promptForAuthorizationCode("Paste the authorization code (or URL) here: ");
1941
+ if (!userInput) {
1942
+ throw new Error("No authorization code provided.");
1943
+ }
1944
+ const { code, state: returnedState } = parseUserInput(userInput);
1945
+ if (!code) {
1946
+ throw new Error("Unable to parse authorization code. Please try again.");
1947
+ }
1948
+ console.log(import_chalk11.default.gray("\nExchanging authorization code for tokens..."));
1949
+ const tokens = await exchangeCodeForTokens2(code, returnedState, state, pkce.codeVerifier);
1950
+ console.log(import_chalk11.default.green("\u2713 Successfully obtained Claude credentials"));
1951
+ return tokens;
1952
+ }
1953
+
1954
+ // src/commands/claude-auth.ts
1955
+ async function claudeAuthCommand(options = {}) {
1956
+ const isUserScoped = options.user === true;
1957
+ const scopeLabel = isUserScoped ? "personal" : "organization";
1958
+ console.log(import_chalk12.default.cyan(`Authenticating with Claude Code (${scopeLabel})...
1959
+ `));
1960
+ try {
1961
+ const tokens = await runClaudeOAuthFlow();
1962
+ console.log(import_chalk12.default.gray("\nUploading credentials to Replicas..."));
1963
+ const endpoint = isUserScoped ? "/v1/claude/user/credentials" : "/v1/claude/credentials";
1964
+ const response = await orgAuthenticatedFetch(
1965
+ endpoint,
1966
+ {
1967
+ method: "POST",
1968
+ body: {
1969
+ access_token: tokens.accessToken,
1970
+ refresh_token: tokens.refreshToken,
1971
+ token_type: tokens.tokenType,
1972
+ scope: tokens.scope,
1973
+ expires_at: tokens.expiresAt,
1974
+ refresh_token_expires_at: tokens.refreshTokenExpiresAt
2058
1975
  }
2059
1976
  }
1977
+ );
1978
+ if (!response.success) {
1979
+ throw new Error(response.error || "Failed to upload Claude credentials to Replicas");
2060
1980
  }
2061
- });
2062
- return messages;
1981
+ console.log(import_chalk12.default.green("\n\u2713 Claude authentication complete!"));
1982
+ if (response.email) {
1983
+ console.log(import_chalk12.default.gray(` Account: ${response.email}`));
1984
+ }
1985
+ if (response.planType) {
1986
+ console.log(import_chalk12.default.gray(` Plan: ${response.planType}`));
1987
+ }
1988
+ if (isUserScoped) {
1989
+ console.log(import_chalk12.default.gray("\nYour personal Claude credentials have been saved.\n"));
1990
+ } else {
1991
+ console.log(import_chalk12.default.gray("\nYou can now use Claude Code in your workspaces.\n"));
1992
+ }
1993
+ } catch (error) {
1994
+ console.log(import_chalk12.default.red("\n\u2717 Error:"), error instanceof Error ? error.message : error);
1995
+ process.exit(1);
1996
+ }
2063
1997
  }
2064
1998
 
2065
- // ../shared/src/display-message/parsers/index.ts
2066
- function parseAgentEvents(events, agentType) {
2067
- if (agentType === "codex") {
2068
- return parseCodexEvents(events);
2069
- } else if (agentType === "claude") {
2070
- return parseClaudeEvents(events);
1999
+ // src/commands/init.ts
2000
+ var import_chalk13 = __toESM(require("chalk"));
2001
+ var import_fs4 = __toESM(require("fs"));
2002
+ var import_path5 = __toESM(require("path"));
2003
+ var REPLICAS_CONFIG_FILENAME = "replicas.json";
2004
+ function getDefaultConfig() {
2005
+ return {
2006
+ copy: [],
2007
+ ports: [],
2008
+ systemPrompt: "",
2009
+ startHook: {
2010
+ timeout: 3e5,
2011
+ commands: []
2012
+ }
2013
+ };
2014
+ }
2015
+ function initCommand(options) {
2016
+ const configPath = import_path5.default.join(process.cwd(), REPLICAS_CONFIG_FILENAME);
2017
+ if (import_fs4.default.existsSync(configPath) && !options.force) {
2018
+ console.log(
2019
+ import_chalk13.default.yellow(
2020
+ `${REPLICAS_CONFIG_FILENAME} already exists in this directory.`
2021
+ )
2022
+ );
2023
+ console.log(import_chalk13.default.gray("Use --force to overwrite the existing file."));
2024
+ return;
2025
+ }
2026
+ const defaultConfig = getDefaultConfig();
2027
+ const configContent = JSON.stringify(defaultConfig, null, 2);
2028
+ try {
2029
+ import_fs4.default.writeFileSync(configPath, configContent + "\n", "utf-8");
2030
+ console.log(import_chalk13.default.green(`\u2713 Created ${REPLICAS_CONFIG_FILENAME}`));
2031
+ console.log("");
2032
+ console.log(import_chalk13.default.gray("Configuration options:"));
2033
+ console.log(
2034
+ import_chalk13.default.gray(' copy - Files to sync to the workspace (e.g., [".env"])')
2035
+ );
2036
+ console.log(
2037
+ import_chalk13.default.gray(" ports - Ports to forward from the workspace (e.g., [3000, 8080])")
2038
+ );
2039
+ console.log(
2040
+ import_chalk13.default.gray(" systemPrompt - Custom instructions for AI coding assistants")
2041
+ );
2042
+ console.log(
2043
+ import_chalk13.default.gray(" startHook - Commands to run on workspace startup")
2044
+ );
2045
+ } catch (error) {
2046
+ throw new Error(
2047
+ `Failed to create ${REPLICAS_CONFIG_FILENAME}: ${error instanceof Error ? error.message : "Unknown error"}`
2048
+ );
2049
+ }
2050
+ }
2051
+
2052
+ // src/lib/version-check.ts
2053
+ var import_chalk14 = __toESM(require("chalk"));
2054
+ var MONOLITH_URL2 = process.env.REPLICAS_MONOLITH_URL || "https://api.replicas.dev";
2055
+ var VERSION_CHECK_TIMEOUT = 2e3;
2056
+ function compareVersions(v1, v2) {
2057
+ const parts1 = v1.split(".").map(Number);
2058
+ const parts2 = v2.split(".").map(Number);
2059
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
2060
+ const num1 = parts1[i] || 0;
2061
+ const num2 = parts2[i] || 0;
2062
+ if (num1 > num2) return 1;
2063
+ if (num1 < num2) return -1;
2064
+ }
2065
+ return 0;
2066
+ }
2067
+ async function checkForUpdates(currentVersion) {
2068
+ try {
2069
+ const controller = new AbortController();
2070
+ const timeoutId = setTimeout(() => controller.abort(), VERSION_CHECK_TIMEOUT);
2071
+ const response = await fetch(`${MONOLITH_URL2}/v1/cli/version`, {
2072
+ signal: controller.signal
2073
+ });
2074
+ clearTimeout(timeoutId);
2075
+ if (!response.ok) return;
2076
+ const data = await response.json();
2077
+ const latestVersion = data.version;
2078
+ if (compareVersions(latestVersion, currentVersion) > 0) {
2079
+ console.error(import_chalk14.default.gray(`
2080
+ Update available: ${currentVersion} \u2192 ${latestVersion}`));
2081
+ console.error(import_chalk14.default.gray(`Run: npm i -g replicas-cli@latest
2082
+ `));
2083
+ }
2084
+ } catch {
2071
2085
  }
2072
- return [];
2073
2086
  }
2074
2087
 
2075
2088
  // src/commands/replica.ts
2089
+ var import_chalk15 = __toESM(require("chalk"));
2090
+ var import_prompts3 = __toESM(require("prompts"));
2076
2091
  function formatDate(dateString) {
2077
2092
  return new Date(dateString).toLocaleString();
2078
2093
  }
@@ -2195,15 +2210,15 @@ Replicas (Page ${response.page} of ${response.totalPages}, Total: ${response.tot
2195
2210
  for (const replica of response.replicas) {
2196
2211
  console.log(import_chalk15.default.white(` ${replica.name}`));
2197
2212
  console.log(import_chalk15.default.gray(` ID: ${replica.id}`));
2198
- if (replica.repository) {
2199
- console.log(import_chalk15.default.gray(` Repository: ${replica.repository}`));
2213
+ if (replica.repositories.length > 0) {
2214
+ console.log(import_chalk15.default.gray(` Repositories: ${replica.repositories.map((repository) => repository.name).join(", ")}`));
2200
2215
  }
2201
2216
  console.log(import_chalk15.default.gray(` Status: ${formatStatus(replica.status)}`));
2202
2217
  console.log(import_chalk15.default.gray(` Created: ${formatDate(replica.created_at)}`));
2203
2218
  if (replica.pull_requests && replica.pull_requests.length > 0) {
2204
2219
  console.log(import_chalk15.default.gray(` Pull Requests:`));
2205
2220
  for (const pr of replica.pull_requests) {
2206
- console.log(import_chalk15.default.cyan(` - #${pr.number}: ${pr.url}`));
2221
+ console.log(import_chalk15.default.cyan(` - ${pr.repository} #${pr.number}: ${pr.url}`));
2207
2222
  }
2208
2223
  }
2209
2224
  console.log();
@@ -2225,8 +2240,8 @@ async function replicaGetCommand(id) {
2225
2240
  Replica: ${replica.name}
2226
2241
  `));
2227
2242
  console.log(import_chalk15.default.gray(` ID: ${replica.id}`));
2228
- if (replica.repository) {
2229
- console.log(import_chalk15.default.gray(` Repository: ${replica.repository}`));
2243
+ if (replica.repositories.length > 0) {
2244
+ console.log(import_chalk15.default.gray(` Repositories: ${replica.repositories.map((repository) => repository.name).join(", ")}`));
2230
2245
  }
2231
2246
  console.log(import_chalk15.default.gray(` Status: ${formatStatus(replica.status)}`));
2232
2247
  console.log(import_chalk15.default.gray(` Created: ${formatDate(replica.created_at)}`));
@@ -2236,17 +2251,18 @@ Replica: ${replica.name}
2236
2251
  if (replica.coding_agent) {
2237
2252
  console.log(import_chalk15.default.gray(` Coding Agent: ${replica.coding_agent}`));
2238
2253
  }
2239
- if (replica.branch) {
2240
- console.log(import_chalk15.default.gray(` Branch: ${replica.branch}`));
2241
- }
2242
- if (replica.git_diff) {
2243
- console.log(import_chalk15.default.gray(` Changes: ${import_chalk15.default.green(`+${replica.git_diff.added}`)} / ${import_chalk15.default.red(`-${replica.git_diff.removed}`)}`));
2254
+ if (replica.repository_statuses && replica.repository_statuses.length > 0) {
2255
+ console.log(import_chalk15.default.gray(" Repository Statuses:"));
2256
+ for (const repositoryStatus of replica.repository_statuses) {
2257
+ const changeText = repositoryStatus.git_diff ? ` (${import_chalk15.default.green(`+${repositoryStatus.git_diff.added}`)} / ${import_chalk15.default.red(`-${repositoryStatus.git_diff.removed}`)})` : "";
2258
+ console.log(import_chalk15.default.gray(` - ${repositoryStatus.repository}: ${repositoryStatus.branch || "unknown"}${changeText}`));
2259
+ }
2244
2260
  }
2245
2261
  }
2246
2262
  if (replica.pull_requests && replica.pull_requests.length > 0) {
2247
2263
  console.log(import_chalk15.default.gray(` Pull Requests:`));
2248
2264
  for (const pr of replica.pull_requests) {
2249
- console.log(import_chalk15.default.cyan(` - #${pr.number}: ${pr.url}`));
2265
+ console.log(import_chalk15.default.cyan(` - ${pr.repository} #${pr.number}: ${pr.url}`));
2250
2266
  }
2251
2267
  }
2252
2268
  console.log();
@@ -2269,7 +2285,7 @@ async function replicaCreateCommand(name, options) {
2269
2285
  }
2270
2286
  let replicaName = name;
2271
2287
  let message = options.message;
2272
- let repository = options.repository;
2288
+ let selectedRepositories = options.repositories?.split(",").map((repository) => repository.trim()).filter((repository) => repository.length > 0);
2273
2289
  let codingAgent = options.agent;
2274
2290
  if (replicaName && /\s/.test(replicaName)) {
2275
2291
  console.log(import_chalk15.default.red("Replica name cannot contain spaces."));
@@ -2292,22 +2308,23 @@ async function replicaCreateCommand(name, options) {
2292
2308
  }
2293
2309
  replicaName = response2.name;
2294
2310
  }
2295
- if (!repository) {
2311
+ if (!selectedRepositories || selectedRepositories.length === 0) {
2296
2312
  const response2 = await (0, import_prompts3.default)({
2297
- type: "select",
2298
- name: "repository",
2299
- message: "Select a repository:",
2313
+ type: "multiselect",
2314
+ name: "repositories",
2315
+ message: "Select repositories:",
2300
2316
  choices: repositories.map((repo) => ({
2301
2317
  title: repo.name,
2302
2318
  value: repo.name,
2303
2319
  description: repo.url
2304
- }))
2320
+ })),
2321
+ min: 1
2305
2322
  });
2306
- if (!response2.repository) {
2323
+ if (!response2.repositories || response2.repositories.length === 0) {
2307
2324
  console.log(import_chalk15.default.yellow("\nCancelled."));
2308
2325
  return;
2309
2326
  }
2310
- repository = response2.repository;
2327
+ selectedRepositories = response2.repositories;
2311
2328
  }
2312
2329
  if (!message) {
2313
2330
  const response2 = await (0, import_prompts3.default)({
@@ -2346,7 +2363,7 @@ async function replicaCreateCommand(name, options) {
2346
2363
  const body = {
2347
2364
  name: replicaName,
2348
2365
  message,
2349
- repository,
2366
+ repositories: selectedRepositories,
2350
2367
  coding_agent: codingAgent
2351
2368
  };
2352
2369
  console.log(import_chalk15.default.gray("\nCreating replica..."));
@@ -2359,6 +2376,9 @@ async function replicaCreateCommand(name, options) {
2359
2376
  Created replica: ${replica.name}`));
2360
2377
  console.log(import_chalk15.default.gray(` ID: ${replica.id}`));
2361
2378
  console.log(import_chalk15.default.gray(` Status: ${formatStatus(replica.status)}`));
2379
+ if (replica.repositories.length > 0) {
2380
+ console.log(import_chalk15.default.gray(` Repositories: ${replica.repositories.map((repository) => repository.name).join(", ")}`));
2381
+ }
2362
2382
  console.log();
2363
2383
  } catch (error) {
2364
2384
  console.error(import_chalk15.default.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
@@ -2529,7 +2549,7 @@ Repositories (${response.repositories.length}):
2529
2549
  }
2530
2550
 
2531
2551
  // src/index.ts
2532
- var CLI_VERSION = "0.2.29";
2552
+ var CLI_VERSION = "0.2.32";
2533
2553
  var program = new import_commander.Command();
2534
2554
  program.name("replicas").description("CLI for managing Replicas workspaces").version(CLI_VERSION);
2535
2555
  program.command("login").description("Authenticate with your Replicas account").action(async () => {
@@ -2726,7 +2746,7 @@ program.command("get <id>").description("Get replica details by ID").action(asyn
2726
2746
  process.exit(1);
2727
2747
  }
2728
2748
  });
2729
- program.command("create [name]").description("Create a new replica").option("-m, --message <message>", "Initial message for the replica").option("-r, --repository <repository>", "Repository name").option("-a, --agent <agent>", "Coding agent (claude, codex)").action(async (name, options) => {
2749
+ program.command("create [name]").description("Create a new replica").option("-m, --message <message>", "Initial message for the replica").option("-r, --repositories <repositories>", "Comma-separated repository names").option("-a, --agent <agent>", "Coding agent (claude, codex)").action(async (name, options) => {
2730
2750
  try {
2731
2751
  await replicaCreateCommand(name, options);
2732
2752
  } catch (error) {