happy-coder 0.9.1 → 0.10.0-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,8 +12,15 @@ var node_crypto = require('node:crypto');
12
12
  var tweetnacl = require('tweetnacl');
13
13
  var node_events = require('node:events');
14
14
  var socket_ioClient = require('socket.io-client');
15
+ var child_process = require('child_process');
16
+ var util = require('util');
17
+ var fs$1 = require('fs/promises');
18
+ var crypto = require('crypto');
19
+ var path = require('path');
20
+ var url = require('url');
15
21
  var expoServerSdk = require('expo-server-sdk');
16
22
 
23
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
17
24
  function _interopNamespaceDefault(e) {
18
25
  var n = Object.create(null);
19
26
  if (e) {
@@ -34,7 +41,7 @@ function _interopNamespaceDefault(e) {
34
41
  var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
35
42
 
36
43
  var name = "happy-coder";
37
- var version = "0.9.1";
44
+ var version = "0.10.0-1";
38
45
  var description = "Claude Code session sharing CLI";
39
46
  var author = "Kirill Dubovitskiy";
40
47
  var license = "MIT";
@@ -97,8 +104,11 @@ var dependencies = {
97
104
  "@types/http-proxy": "^1.17.16",
98
105
  "@types/qrcode-terminal": "^0.12.2",
99
106
  "@types/react": "^19.1.9",
107
+ "@types/ps-list": "^6.2.1",
108
+ "@types/cross-spawn": "^6.0.6",
100
109
  axios: "^1.10.0",
101
110
  chalk: "^5.4.1",
111
+ "cross-spawn": "^7.0.6",
102
112
  "expo-server-sdk": "^3.15.0",
103
113
  fastify: "^5.5.0",
104
114
  "fastify-type-provider-zod": "4.0.2",
@@ -106,6 +116,7 @@ var dependencies = {
106
116
  "http-proxy-middleware": "^3.0.5",
107
117
  ink: "^6.1.0",
108
118
  open: "^10.2.0",
119
+ "ps-list": "^8.1.1",
109
120
  "qrcode-terminal": "^0.12.0",
110
121
  react: "^19.1.1",
111
122
  "socket.io-client": "^4.8.1",
@@ -852,6 +863,340 @@ class AsyncLock {
852
863
  }
853
864
  }
854
865
 
866
+ class RpcHandlerManager {
867
+ handlers = /* @__PURE__ */ new Map();
868
+ scopePrefix;
869
+ secret;
870
+ logger;
871
+ socket = null;
872
+ constructor(config) {
873
+ this.scopePrefix = config.scopePrefix;
874
+ this.secret = config.secret;
875
+ this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
876
+ }
877
+ /**
878
+ * Register an RPC handler for a specific method
879
+ * @param method - The method name (without prefix)
880
+ * @param handler - The handler function
881
+ */
882
+ registerHandler(method, handler) {
883
+ const prefixedMethod = this.getPrefixedMethod(method);
884
+ this.handlers.set(prefixedMethod, handler);
885
+ if (this.socket) {
886
+ this.socket.emit("rpc-register", { method: prefixedMethod });
887
+ }
888
+ }
889
+ /**
890
+ * Handle an incoming RPC request
891
+ * @param request - The RPC request data
892
+ * @param callback - The response callback
893
+ */
894
+ async handleRequest(request) {
895
+ try {
896
+ const handler = this.handlers.get(request.method);
897
+ if (!handler) {
898
+ this.logger("[RPC] [ERROR] Method not found", { method: request.method });
899
+ const errorResponse = { error: "Method not found" };
900
+ const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
901
+ return encryptedError;
902
+ }
903
+ const decryptedParams = decrypt(decodeBase64(request.params), this.secret);
904
+ const result = await handler(decryptedParams);
905
+ const encryptedResponse = encodeBase64(encrypt(result, this.secret));
906
+ return encryptedResponse;
907
+ } catch (error) {
908
+ this.logger("[RPC] [ERROR] Error handling request", { error });
909
+ const errorResponse = {
910
+ error: error instanceof Error ? error.message : "Unknown error"
911
+ };
912
+ const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
913
+ return encryptedError;
914
+ }
915
+ }
916
+ onSocketConnect(socket) {
917
+ this.socket = socket;
918
+ for (const [prefixedMethod] of this.handlers) {
919
+ socket.emit("rpc-register", { method: prefixedMethod });
920
+ }
921
+ }
922
+ onSocketDisconnect() {
923
+ this.socket = null;
924
+ }
925
+ /**
926
+ * Get the number of registered handlers
927
+ */
928
+ getHandlerCount() {
929
+ return this.handlers.size;
930
+ }
931
+ /**
932
+ * Check if a handler is registered
933
+ * @param method - The method name (without prefix)
934
+ */
935
+ hasHandler(method) {
936
+ const prefixedMethod = this.getPrefixedMethod(method);
937
+ return this.handlers.has(prefixedMethod);
938
+ }
939
+ /**
940
+ * Clear all handlers
941
+ */
942
+ clearHandlers() {
943
+ this.handlers.clear();
944
+ this.logger("Cleared all RPC handlers");
945
+ }
946
+ /**
947
+ * Get the prefixed method name
948
+ * @param method - The method name
949
+ */
950
+ getPrefixedMethod(method) {
951
+ return `${this.scopePrefix}:${method}`;
952
+ }
953
+ }
954
+
955
+ const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-D9P2bndj.cjs', document.baseURI).href))));
956
+ function projectPath() {
957
+ const path$1 = path.resolve(__dirname$1, "..");
958
+ return path$1;
959
+ }
960
+
961
+ function run(args, options) {
962
+ const RUNNER_PATH = path.resolve(path.join(projectPath(), "scripts", "ripgrep_launcher.cjs"));
963
+ return new Promise((resolve2, reject) => {
964
+ const child = child_process.spawn("node", [RUNNER_PATH, JSON.stringify(args)], {
965
+ stdio: ["pipe", "pipe", "pipe"],
966
+ cwd: options?.cwd
967
+ });
968
+ let stdout = "";
969
+ let stderr = "";
970
+ child.stdout.on("data", (data) => {
971
+ stdout += data.toString();
972
+ });
973
+ child.stderr.on("data", (data) => {
974
+ stderr += data.toString();
975
+ });
976
+ child.on("close", (code) => {
977
+ resolve2({
978
+ exitCode: code || 0,
979
+ stdout,
980
+ stderr
981
+ });
982
+ });
983
+ child.on("error", (err) => {
984
+ reject(err);
985
+ });
986
+ });
987
+ }
988
+
989
+ const execAsync = util.promisify(child_process.exec);
990
+ function registerCommonHandlers(rpcHandlerManager) {
991
+ rpcHandlerManager.registerHandler("bash", async (data) => {
992
+ logger.debug("Shell command request:", data.command);
993
+ try {
994
+ const options = {
995
+ cwd: data.cwd,
996
+ timeout: data.timeout || 3e4
997
+ // Default 30 seconds timeout
998
+ };
999
+ const { stdout, stderr } = await execAsync(data.command, options);
1000
+ return {
1001
+ success: true,
1002
+ stdout: stdout ? stdout.toString() : "",
1003
+ stderr: stderr ? stderr.toString() : "",
1004
+ exitCode: 0
1005
+ };
1006
+ } catch (error) {
1007
+ const execError = error;
1008
+ if (execError.code === "ETIMEDOUT" || execError.killed) {
1009
+ return {
1010
+ success: false,
1011
+ stdout: execError.stdout || "",
1012
+ stderr: execError.stderr || "",
1013
+ exitCode: typeof execError.code === "number" ? execError.code : -1,
1014
+ error: "Command timed out"
1015
+ };
1016
+ }
1017
+ return {
1018
+ success: false,
1019
+ stdout: execError.stdout ? execError.stdout.toString() : "",
1020
+ stderr: execError.stderr ? execError.stderr.toString() : execError.message || "Command failed",
1021
+ exitCode: typeof execError.code === "number" ? execError.code : 1,
1022
+ error: execError.message || "Command failed"
1023
+ };
1024
+ }
1025
+ });
1026
+ rpcHandlerManager.registerHandler("readFile", async (data) => {
1027
+ logger.debug("Read file request:", data.path);
1028
+ try {
1029
+ const buffer = await fs$1.readFile(data.path);
1030
+ const content = buffer.toString("base64");
1031
+ return { success: true, content };
1032
+ } catch (error) {
1033
+ logger.debug("Failed to read file:", error);
1034
+ return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
1035
+ }
1036
+ });
1037
+ rpcHandlerManager.registerHandler("writeFile", async (data) => {
1038
+ logger.debug("Write file request:", data.path);
1039
+ try {
1040
+ if (data.expectedHash !== null && data.expectedHash !== void 0) {
1041
+ try {
1042
+ const existingBuffer = await fs$1.readFile(data.path);
1043
+ const existingHash = crypto.createHash("sha256").update(existingBuffer).digest("hex");
1044
+ if (existingHash !== data.expectedHash) {
1045
+ return {
1046
+ success: false,
1047
+ error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
1048
+ };
1049
+ }
1050
+ } catch (error) {
1051
+ const nodeError = error;
1052
+ if (nodeError.code !== "ENOENT") {
1053
+ throw error;
1054
+ }
1055
+ return {
1056
+ success: false,
1057
+ error: "File does not exist but hash was provided"
1058
+ };
1059
+ }
1060
+ } else {
1061
+ try {
1062
+ await fs$1.stat(data.path);
1063
+ return {
1064
+ success: false,
1065
+ error: "File already exists but was expected to be new"
1066
+ };
1067
+ } catch (error) {
1068
+ const nodeError = error;
1069
+ if (nodeError.code !== "ENOENT") {
1070
+ throw error;
1071
+ }
1072
+ }
1073
+ }
1074
+ const buffer = Buffer.from(data.content, "base64");
1075
+ await fs$1.writeFile(data.path, buffer);
1076
+ const hash = crypto.createHash("sha256").update(buffer).digest("hex");
1077
+ return { success: true, hash };
1078
+ } catch (error) {
1079
+ logger.debug("Failed to write file:", error);
1080
+ return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
1081
+ }
1082
+ });
1083
+ rpcHandlerManager.registerHandler("listDirectory", async (data) => {
1084
+ logger.debug("List directory request:", data.path);
1085
+ try {
1086
+ const entries = await fs$1.readdir(data.path, { withFileTypes: true });
1087
+ const directoryEntries = await Promise.all(
1088
+ entries.map(async (entry) => {
1089
+ const fullPath = path.join(data.path, entry.name);
1090
+ let type = "other";
1091
+ let size;
1092
+ let modified;
1093
+ if (entry.isDirectory()) {
1094
+ type = "directory";
1095
+ } else if (entry.isFile()) {
1096
+ type = "file";
1097
+ }
1098
+ try {
1099
+ const stats = await fs$1.stat(fullPath);
1100
+ size = stats.size;
1101
+ modified = stats.mtime.getTime();
1102
+ } catch (error) {
1103
+ logger.debug(`Failed to stat ${fullPath}:`, error);
1104
+ }
1105
+ return {
1106
+ name: entry.name,
1107
+ type,
1108
+ size,
1109
+ modified
1110
+ };
1111
+ })
1112
+ );
1113
+ directoryEntries.sort((a, b) => {
1114
+ if (a.type === "directory" && b.type !== "directory") return -1;
1115
+ if (a.type !== "directory" && b.type === "directory") return 1;
1116
+ return a.name.localeCompare(b.name);
1117
+ });
1118
+ return { success: true, entries: directoryEntries };
1119
+ } catch (error) {
1120
+ logger.debug("Failed to list directory:", error);
1121
+ return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
1122
+ }
1123
+ });
1124
+ rpcHandlerManager.registerHandler("getDirectoryTree", async (data) => {
1125
+ logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
1126
+ async function buildTree(path$1, name, currentDepth) {
1127
+ try {
1128
+ const stats = await fs$1.stat(path$1);
1129
+ const node = {
1130
+ name,
1131
+ path: path$1,
1132
+ type: stats.isDirectory() ? "directory" : "file",
1133
+ size: stats.size,
1134
+ modified: stats.mtime.getTime()
1135
+ };
1136
+ if (stats.isDirectory() && currentDepth < data.maxDepth) {
1137
+ const entries = await fs$1.readdir(path$1, { withFileTypes: true });
1138
+ const children = [];
1139
+ await Promise.all(
1140
+ entries.map(async (entry) => {
1141
+ if (entry.isSymbolicLink()) {
1142
+ logger.debug(`Skipping symlink: ${path.join(path$1, entry.name)}`);
1143
+ return;
1144
+ }
1145
+ const childPath = path.join(path$1, entry.name);
1146
+ const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
1147
+ if (childNode) {
1148
+ children.push(childNode);
1149
+ }
1150
+ })
1151
+ );
1152
+ children.sort((a, b) => {
1153
+ if (a.type === "directory" && b.type !== "directory") return -1;
1154
+ if (a.type !== "directory" && b.type === "directory") return 1;
1155
+ return a.name.localeCompare(b.name);
1156
+ });
1157
+ node.children = children;
1158
+ }
1159
+ return node;
1160
+ } catch (error) {
1161
+ logger.debug(`Failed to process ${path$1}:`, error instanceof Error ? error.message : String(error));
1162
+ return null;
1163
+ }
1164
+ }
1165
+ try {
1166
+ if (data.maxDepth < 0) {
1167
+ return { success: false, error: "maxDepth must be non-negative" };
1168
+ }
1169
+ const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
1170
+ const tree = await buildTree(data.path, baseName, 0);
1171
+ if (!tree) {
1172
+ return { success: false, error: "Failed to access the specified path" };
1173
+ }
1174
+ return { success: true, tree };
1175
+ } catch (error) {
1176
+ logger.debug("Failed to get directory tree:", error);
1177
+ return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
1178
+ }
1179
+ });
1180
+ rpcHandlerManager.registerHandler("ripgrep", async (data) => {
1181
+ logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
1182
+ try {
1183
+ const result = await run(data.args, { cwd: data.cwd });
1184
+ return {
1185
+ success: true,
1186
+ exitCode: result.exitCode,
1187
+ stdout: result.stdout.toString(),
1188
+ stderr: result.stderr.toString()
1189
+ };
1190
+ } catch (error) {
1191
+ logger.debug("Failed to run ripgrep:", error);
1192
+ return {
1193
+ success: false,
1194
+ error: error instanceof Error ? error.message : "Failed to run ripgrep"
1195
+ };
1196
+ }
1197
+ });
1198
+ }
1199
+
855
1200
  class ApiSessionClient extends node_events.EventEmitter {
856
1201
  token;
857
1202
  secret;
@@ -863,7 +1208,7 @@ class ApiSessionClient extends node_events.EventEmitter {
863
1208
  socket;
864
1209
  pendingMessages = [];
865
1210
  pendingMessageCallback = null;
866
- rpcHandlers = /* @__PURE__ */ new Map();
1211
+ rpcHandlerManager;
867
1212
  agentStateLock = new AsyncLock();
868
1213
  metadataLock = new AsyncLock();
869
1214
  constructor(token, secret, session) {
@@ -875,6 +1220,12 @@ class ApiSessionClient extends node_events.EventEmitter {
875
1220
  this.metadataVersion = session.metadataVersion;
876
1221
  this.agentState = session.agentState;
877
1222
  this.agentStateVersion = session.agentStateVersion;
1223
+ this.rpcHandlerManager = new RpcHandlerManager({
1224
+ scopePrefix: this.sessionId,
1225
+ secret: this.secret,
1226
+ logger: (msg, data) => logger.debug(msg, data)
1227
+ });
1228
+ registerCommonHandlers(this.rpcHandlerManager);
878
1229
  this.socket = socket_ioClient.io(configuration.serverUrl, {
879
1230
  auth: {
880
1231
  token: this.token,
@@ -892,35 +1243,18 @@ class ApiSessionClient extends node_events.EventEmitter {
892
1243
  });
893
1244
  this.socket.on("connect", () => {
894
1245
  logger.debug("Socket connected successfully");
895
- this.reregisterHandlers();
1246
+ this.rpcHandlerManager.onSocketConnect(this.socket);
896
1247
  });
897
1248
  this.socket.on("rpc-request", async (data, callback) => {
898
- try {
899
- const method = data.method;
900
- const handler = this.rpcHandlers.get(method);
901
- if (!handler) {
902
- logger.debug("[SOCKET] [RPC] [ERROR] method not found", { method });
903
- const errorResponse = { error: "Method not found" };
904
- const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
905
- callback(encryptedError);
906
- return;
907
- }
908
- const decryptedParams = decrypt(decodeBase64(data.params), this.secret);
909
- const result = await handler(decryptedParams);
910
- const encryptedResponse = encodeBase64(encrypt(result, this.secret));
911
- callback(encryptedResponse);
912
- } catch (error) {
913
- logger.debug("[SOCKET] [RPC] [ERROR] Error handling RPC request", { error });
914
- const errorResponse = { error: error instanceof Error ? error.message : "Unknown error" };
915
- const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
916
- callback(encryptedError);
917
- }
1249
+ callback(await this.rpcHandlerManager.handleRequest(data));
918
1250
  });
919
1251
  this.socket.on("disconnect", (reason) => {
920
1252
  logger.debug("[API] Socket disconnected:", reason);
1253
+ this.rpcHandlerManager.onSocketDisconnect();
921
1254
  });
922
1255
  this.socket.on("connect_error", (error) => {
923
1256
  logger.debug("[API] Socket connection error:", error);
1257
+ this.rpcHandlerManager.onSocketDisconnect();
924
1258
  });
925
1259
  this.socket.on("update", (data) => {
926
1260
  try {
@@ -1131,29 +1465,6 @@ class ApiSessionClient extends node_events.EventEmitter {
1131
1465
  });
1132
1466
  });
1133
1467
  }
1134
- /**
1135
- * Set a custom RPC handler for a specific method with encrypted arguments and responses
1136
- * @param method - The method name to handle
1137
- * @param handler - The handler function to call when the method is invoked
1138
- */
1139
- setHandler(method, handler) {
1140
- const prefixedMethod = `${this.sessionId}:${method}`;
1141
- this.rpcHandlers.set(prefixedMethod, handler);
1142
- this.socket.emit("rpc-register", { method: prefixedMethod });
1143
- logger.debug("Registered RPC handler", { method, prefixedMethod });
1144
- }
1145
- /**
1146
- * Re-register all RPC handlers after reconnection
1147
- */
1148
- reregisterHandlers() {
1149
- logger.debug("Re-registering RPC handlers after reconnection", {
1150
- totalMethods: this.rpcHandlers.size
1151
- });
1152
- for (const [prefixedMethod] of this.rpcHandlers) {
1153
- this.socket.emit("rpc-register", { method: prefixedMethod });
1154
- logger.debug("Re-registered method", { prefixedMethod });
1155
- }
1156
- }
1157
1468
  /**
1158
1469
  * Wait for socket buffer to flush
1159
1470
  */
@@ -1180,21 +1491,58 @@ class ApiMachineClient {
1180
1491
  this.token = token;
1181
1492
  this.secret = secret;
1182
1493
  this.machine = machine;
1494
+ this.rpcHandlerManager = new RpcHandlerManager({
1495
+ scopePrefix: this.machine.id,
1496
+ secret: this.secret,
1497
+ logger: (msg, data) => logger.debug(msg, data)
1498
+ });
1499
+ registerCommonHandlers(this.rpcHandlerManager);
1183
1500
  }
1184
1501
  socket;
1185
1502
  keepAliveInterval = null;
1186
- // RPC handlers
1187
- spawnSession;
1188
- stopSession;
1189
- requestShutdown;
1503
+ rpcHandlerManager;
1190
1504
  setRPCHandlers({
1191
1505
  spawnSession,
1192
1506
  stopSession,
1193
1507
  requestShutdown
1194
1508
  }) {
1195
- this.spawnSession = spawnSession;
1196
- this.stopSession = stopSession;
1197
- this.requestShutdown = requestShutdown;
1509
+ this.rpcHandlerManager.registerHandler("spawn-happy-session", async (params) => {
1510
+ const { directory, sessionId, machineId, approvedNewDirectoryCreation } = params || {};
1511
+ if (!directory) {
1512
+ throw new Error("Directory is required");
1513
+ }
1514
+ const result = await spawnSession({ directory, sessionId, machineId, approvedNewDirectoryCreation });
1515
+ switch (result.type) {
1516
+ case "success":
1517
+ logger.debug(`[API MACHINE] Spawned session ${result.sessionId}`);
1518
+ return { type: "success", sessionId: result.sessionId };
1519
+ case "requestToApproveDirectoryCreation":
1520
+ logger.debug(`[API MACHINE] Requesting directory creation approval for: ${result.directory}`);
1521
+ return { type: "requestToApproveDirectoryCreation", directory: result.directory };
1522
+ case "error":
1523
+ throw new Error(result.errorMessage);
1524
+ }
1525
+ });
1526
+ this.rpcHandlerManager.registerHandler("stop-session", (params) => {
1527
+ const { sessionId } = params || {};
1528
+ if (!sessionId) {
1529
+ throw new Error("Session ID is required");
1530
+ }
1531
+ const success = stopSession(sessionId);
1532
+ if (!success) {
1533
+ throw new Error("Session not found or failed to stop");
1534
+ }
1535
+ logger.debug(`[API MACHINE] Stopped session ${sessionId}`);
1536
+ return { message: "Session stopped" };
1537
+ });
1538
+ this.rpcHandlerManager.registerHandler("stop-daemon", () => {
1539
+ logger.debug("[API MACHINE] Received stop-daemon RPC request");
1540
+ setTimeout(() => {
1541
+ logger.debug("[API MACHINE] Initiating daemon shutdown from RPC");
1542
+ requestShutdown();
1543
+ }, 100);
1544
+ return { message: "Daemon stop request acknowledged, starting shutdown sequence..." };
1545
+ });
1198
1546
  }
1199
1547
  /**
1200
1548
  * Update machine metadata
@@ -1262,9 +1610,6 @@ class ApiMachineClient {
1262
1610
  reconnectionDelay: 1e3,
1263
1611
  reconnectionDelayMax: 5e3
1264
1612
  });
1265
- const spawnMethod = `${this.machine.id}:spawn-happy-session`;
1266
- const stopMethod = `${this.machine.id}:stop-session`;
1267
- const stopDaemonMethod = `${this.machine.id}:stop-daemon`;
1268
1613
  this.socket.on("connect", () => {
1269
1614
  logger.debug("[API MACHINE] Connected to server");
1270
1615
  this.updateDaemonState((state) => ({
@@ -1274,84 +1619,17 @@ class ApiMachineClient {
1274
1619
  httpPort: this.machine.daemonState?.httpPort,
1275
1620
  startedAt: Date.now()
1276
1621
  }));
1277
- this.socket.emit("rpc-register", { method: spawnMethod });
1278
- this.socket.emit("rpc-register", { method: stopMethod });
1279
- this.socket.emit("rpc-register", { method: stopDaemonMethod });
1280
- logger.debug(`[API MACHINE] Registered RPC methods: ${spawnMethod}, ${stopMethod}, ${stopDaemonMethod}`);
1622
+ this.rpcHandlerManager.onSocketConnect(this.socket);
1281
1623
  this.startKeepAlive();
1282
1624
  });
1625
+ this.socket.on("disconnect", () => {
1626
+ logger.debug("[API MACHINE] Disconnected from server");
1627
+ this.rpcHandlerManager.onSocketDisconnect();
1628
+ this.stopKeepAlive();
1629
+ });
1283
1630
  this.socket.on("rpc-request", async (data, callback) => {
1284
1631
  logger.debugLargeJson(`[API MACHINE] Received RPC request:`, data);
1285
- try {
1286
- const spawnMethod2 = `${this.machine.id}:spawn-happy-session`;
1287
- const stopMethod2 = `${this.machine.id}:stop-session`;
1288
- const stopDaemonMethod2 = `${this.machine.id}:stop-daemon`;
1289
- if (data.method === spawnMethod2) {
1290
- if (!this.spawnSession) {
1291
- throw new Error("Spawn session handler not set");
1292
- }
1293
- const { directory, sessionId } = decrypt(decodeBase64(data.params), this.secret) || {};
1294
- if (!directory) {
1295
- throw new Error("Directory is required");
1296
- }
1297
- const session = await this.spawnSession(directory, sessionId);
1298
- if (!session) {
1299
- throw new Error("Failed to spawn session");
1300
- }
1301
- if (session.error) {
1302
- throw new Error(session.error);
1303
- }
1304
- logger.debug(`[API MACHINE] Spawned session ${session.happySessionId || "WARNING - not session Id recieved in webhook"} with PID ${session.pid}`);
1305
- if (!session.happySessionId) {
1306
- throw new Error(`Session spawned (PID ${session.pid}) but no sessionId received from webhook. The session process may still be initializing.`);
1307
- }
1308
- const response = {
1309
- sessionId: session.happySessionId,
1310
- message: session.message
1311
- };
1312
- logger.debug(`[API MACHINE] Sending RPC response:`, response);
1313
- callback(encodeBase64(encrypt(response, this.secret)));
1314
- return;
1315
- }
1316
- if (data.method === stopMethod2) {
1317
- logger.debug("[API MACHINE] Received stop-session RPC request");
1318
- const decryptedParams = decrypt(decodeBase64(data.params), this.secret);
1319
- const { sessionId } = decryptedParams || {};
1320
- if (!this.stopSession) {
1321
- throw new Error("Stop session handler not set");
1322
- }
1323
- if (!sessionId) {
1324
- throw new Error("Session ID is required");
1325
- }
1326
- const success = this.stopSession(sessionId);
1327
- if (!success) {
1328
- throw new Error("Session not found or failed to stop");
1329
- }
1330
- logger.debug(`[API MACHINE] Stopped session ${sessionId}`);
1331
- const response = { message: "Session stopped" };
1332
- const encryptedResponse = encodeBase64(encrypt(response, this.secret));
1333
- callback(encryptedResponse);
1334
- return;
1335
- }
1336
- if (data.method === stopDaemonMethod2) {
1337
- logger.debug("[API MACHINE] Received stop-daemon RPC request");
1338
- callback(encodeBase64(encrypt({
1339
- message: "Daemon stop request acknowledged, starting shutdown sequence..."
1340
- }, this.secret)));
1341
- setTimeout(() => {
1342
- logger.debug("[API MACHINE] Initiating daemon shutdown from RPC");
1343
- if (this.requestShutdown) {
1344
- this.requestShutdown();
1345
- }
1346
- }, 100);
1347
- return;
1348
- }
1349
- throw new Error(`Unknown RPC method: ${data.method}`);
1350
- } catch (error) {
1351
- logger.debug(`[API MACHINE] RPC handler failed:`, error.message || error);
1352
- logger.debug(`[API MACHINE] Error stack:`, error.stack);
1353
- callback(encodeBase64(encrypt({ error: error.message || String(error) }, this.secret)));
1354
- }
1632
+ callback(await this.rpcHandlerManager.handleRequest(data));
1355
1633
  });
1356
1634
  this.socket.on("update", (data) => {
1357
1635
  if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
@@ -1370,16 +1648,6 @@ class ApiMachineClient {
1370
1648
  logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
1371
1649
  }
1372
1650
  });
1373
- this.socket.on("disconnect", () => {
1374
- logger.debug("[API MACHINE] Disconnected from server");
1375
- this.stopKeepAlive();
1376
- });
1377
- this.socket.io.on("reconnect", () => {
1378
- logger.debug("[API MACHINE] Reconnected to server");
1379
- this.socket.emit("rpc-register", { method: spawnMethod });
1380
- this.socket.emit("rpc-register", { method: stopMethod });
1381
- this.socket.emit("rpc-register", { method: stopDaemonMethod });
1382
- });
1383
1651
  this.socket.on("connect_error", (error) => {
1384
1652
  logger.debug(`[API MACHINE] Connection error: ${error.message}`);
1385
1653
  });
@@ -1626,7 +1894,7 @@ class ApiClient {
1626
1894
  * Register or update machine with the server
1627
1895
  * Returns the current machine state from the server with decrypted metadata and daemonState
1628
1896
  */
1629
- async createMachineOrGetExistingAsIs(opts) {
1897
+ async getOrCreateMachine(opts) {
1630
1898
  const response = await axios.post(
1631
1899
  `${configuration.serverUrl}/v1/machines`,
1632
1900
  {
@@ -1671,6 +1939,34 @@ class ApiClient {
1671
1939
  push() {
1672
1940
  return this.pushClient;
1673
1941
  }
1942
+ /**
1943
+ * Register a vendor API token with the server
1944
+ * The token is sent as a JSON string - server handles encryption
1945
+ */
1946
+ async registerVendorToken(vendor, apiKey) {
1947
+ try {
1948
+ const response = await axios.post(
1949
+ `${configuration.serverUrl}/v1/connect/${vendor}/register`,
1950
+ {
1951
+ token: JSON.stringify(apiKey)
1952
+ },
1953
+ {
1954
+ headers: {
1955
+ "Authorization": `Bearer ${this.token}`,
1956
+ "Content-Type": "application/json"
1957
+ },
1958
+ timeout: 5e3
1959
+ }
1960
+ );
1961
+ if (response.status !== 200 && response.status !== 201) {
1962
+ throw new Error(`Server returned status ${response.status}`);
1963
+ }
1964
+ logger.debug(`[API] Vendor token for ${vendor} registered successfully`);
1965
+ } catch (error) {
1966
+ logger.debug(`[API] [ERROR] Failed to register vendor token:`, error);
1967
+ throw new Error(`Failed to register vendor token: ${error instanceof Error ? error.message : "Unknown error"}`);
1968
+ }
1969
+ }
1674
1970
  }
1675
1971
 
1676
1972
  const UsageSchema = z.z.object({
@@ -1738,6 +2034,7 @@ exports.encodeBase64Url = encodeBase64Url;
1738
2034
  exports.getLatestDaemonLog = getLatestDaemonLog;
1739
2035
  exports.logger = logger;
1740
2036
  exports.packageJson = packageJson;
2037
+ exports.projectPath = projectPath;
1741
2038
  exports.readCredentials = readCredentials;
1742
2039
  exports.readDaemonState = readDaemonState;
1743
2040
  exports.readSettings = readSettings;