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.
- package/dist/index.cjs +785 -521
- package/dist/index.mjs +786 -522
- package/dist/lib.cjs +7 -1
- package/dist/lib.d.cts +99 -34
- package/dist/lib.d.mts +99 -34
- package/dist/lib.mjs +7 -1
- package/dist/{types-BS8Pr3Im.mjs → types-BUXwivpV.mjs} +437 -142
- package/dist/{types-DNUk09Np.cjs → types-D9P2bndj.cjs} +438 -141
- package/package.json +5 -1
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
1246
|
+
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
896
1247
|
});
|
|
897
1248
|
this.socket.on("rpc-request", async (data, callback) => {
|
|
898
|
-
|
|
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
|
-
|
|
1187
|
-
spawnSession;
|
|
1188
|
-
stopSession;
|
|
1189
|
-
requestShutdown;
|
|
1503
|
+
rpcHandlerManager;
|
|
1190
1504
|
setRPCHandlers({
|
|
1191
1505
|
spawnSession,
|
|
1192
1506
|
stopSession,
|
|
1193
1507
|
requestShutdown
|
|
1194
1508
|
}) {
|
|
1195
|
-
this.
|
|
1196
|
-
|
|
1197
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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;
|