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
|
@@ -11,10 +11,16 @@ import { randomBytes, randomUUID } from 'node:crypto';
|
|
|
11
11
|
import tweetnacl from 'tweetnacl';
|
|
12
12
|
import { EventEmitter } from 'node:events';
|
|
13
13
|
import { io } from 'socket.io-client';
|
|
14
|
+
import { spawn, exec } from 'child_process';
|
|
15
|
+
import { promisify } from 'util';
|
|
16
|
+
import { readFile as readFile$1, stat as stat$1, writeFile as writeFile$1, readdir } from 'fs/promises';
|
|
17
|
+
import { createHash } from 'crypto';
|
|
18
|
+
import { dirname, resolve, join as join$1 } from 'path';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
14
20
|
import { Expo } from 'expo-server-sdk';
|
|
15
21
|
|
|
16
22
|
var name = "happy-coder";
|
|
17
|
-
var version = "0.
|
|
23
|
+
var version = "0.10.0-1";
|
|
18
24
|
var description = "Claude Code session sharing CLI";
|
|
19
25
|
var author = "Kirill Dubovitskiy";
|
|
20
26
|
var license = "MIT";
|
|
@@ -77,8 +83,11 @@ var dependencies = {
|
|
|
77
83
|
"@types/http-proxy": "^1.17.16",
|
|
78
84
|
"@types/qrcode-terminal": "^0.12.2",
|
|
79
85
|
"@types/react": "^19.1.9",
|
|
86
|
+
"@types/ps-list": "^6.2.1",
|
|
87
|
+
"@types/cross-spawn": "^6.0.6",
|
|
80
88
|
axios: "^1.10.0",
|
|
81
89
|
chalk: "^5.4.1",
|
|
90
|
+
"cross-spawn": "^7.0.6",
|
|
82
91
|
"expo-server-sdk": "^3.15.0",
|
|
83
92
|
fastify: "^5.5.0",
|
|
84
93
|
"fastify-type-provider-zod": "4.0.2",
|
|
@@ -86,6 +95,7 @@ var dependencies = {
|
|
|
86
95
|
"http-proxy-middleware": "^3.0.5",
|
|
87
96
|
ink: "^6.1.0",
|
|
88
97
|
open: "^10.2.0",
|
|
98
|
+
"ps-list": "^8.1.1",
|
|
89
99
|
"qrcode-terminal": "^0.12.0",
|
|
90
100
|
react: "^19.1.1",
|
|
91
101
|
"socket.io-client": "^4.8.1",
|
|
@@ -832,6 +842,340 @@ class AsyncLock {
|
|
|
832
842
|
}
|
|
833
843
|
}
|
|
834
844
|
|
|
845
|
+
class RpcHandlerManager {
|
|
846
|
+
handlers = /* @__PURE__ */ new Map();
|
|
847
|
+
scopePrefix;
|
|
848
|
+
secret;
|
|
849
|
+
logger;
|
|
850
|
+
socket = null;
|
|
851
|
+
constructor(config) {
|
|
852
|
+
this.scopePrefix = config.scopePrefix;
|
|
853
|
+
this.secret = config.secret;
|
|
854
|
+
this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Register an RPC handler for a specific method
|
|
858
|
+
* @param method - The method name (without prefix)
|
|
859
|
+
* @param handler - The handler function
|
|
860
|
+
*/
|
|
861
|
+
registerHandler(method, handler) {
|
|
862
|
+
const prefixedMethod = this.getPrefixedMethod(method);
|
|
863
|
+
this.handlers.set(prefixedMethod, handler);
|
|
864
|
+
if (this.socket) {
|
|
865
|
+
this.socket.emit("rpc-register", { method: prefixedMethod });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Handle an incoming RPC request
|
|
870
|
+
* @param request - The RPC request data
|
|
871
|
+
* @param callback - The response callback
|
|
872
|
+
*/
|
|
873
|
+
async handleRequest(request) {
|
|
874
|
+
try {
|
|
875
|
+
const handler = this.handlers.get(request.method);
|
|
876
|
+
if (!handler) {
|
|
877
|
+
this.logger("[RPC] [ERROR] Method not found", { method: request.method });
|
|
878
|
+
const errorResponse = { error: "Method not found" };
|
|
879
|
+
const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
|
|
880
|
+
return encryptedError;
|
|
881
|
+
}
|
|
882
|
+
const decryptedParams = decrypt(decodeBase64(request.params), this.secret);
|
|
883
|
+
const result = await handler(decryptedParams);
|
|
884
|
+
const encryptedResponse = encodeBase64(encrypt(result, this.secret));
|
|
885
|
+
return encryptedResponse;
|
|
886
|
+
} catch (error) {
|
|
887
|
+
this.logger("[RPC] [ERROR] Error handling request", { error });
|
|
888
|
+
const errorResponse = {
|
|
889
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
890
|
+
};
|
|
891
|
+
const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
|
|
892
|
+
return encryptedError;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
onSocketConnect(socket) {
|
|
896
|
+
this.socket = socket;
|
|
897
|
+
for (const [prefixedMethod] of this.handlers) {
|
|
898
|
+
socket.emit("rpc-register", { method: prefixedMethod });
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
onSocketDisconnect() {
|
|
902
|
+
this.socket = null;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Get the number of registered handlers
|
|
906
|
+
*/
|
|
907
|
+
getHandlerCount() {
|
|
908
|
+
return this.handlers.size;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Check if a handler is registered
|
|
912
|
+
* @param method - The method name (without prefix)
|
|
913
|
+
*/
|
|
914
|
+
hasHandler(method) {
|
|
915
|
+
const prefixedMethod = this.getPrefixedMethod(method);
|
|
916
|
+
return this.handlers.has(prefixedMethod);
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Clear all handlers
|
|
920
|
+
*/
|
|
921
|
+
clearHandlers() {
|
|
922
|
+
this.handlers.clear();
|
|
923
|
+
this.logger("Cleared all RPC handlers");
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Get the prefixed method name
|
|
927
|
+
* @param method - The method name
|
|
928
|
+
*/
|
|
929
|
+
getPrefixedMethod(method) {
|
|
930
|
+
return `${this.scopePrefix}:${method}`;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
935
|
+
function projectPath() {
|
|
936
|
+
const path = resolve(__dirname, "..");
|
|
937
|
+
return path;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function run(args, options) {
|
|
941
|
+
const RUNNER_PATH = resolve(join$1(projectPath(), "scripts", "ripgrep_launcher.cjs"));
|
|
942
|
+
return new Promise((resolve2, reject) => {
|
|
943
|
+
const child = spawn("node", [RUNNER_PATH, JSON.stringify(args)], {
|
|
944
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
945
|
+
cwd: options?.cwd
|
|
946
|
+
});
|
|
947
|
+
let stdout = "";
|
|
948
|
+
let stderr = "";
|
|
949
|
+
child.stdout.on("data", (data) => {
|
|
950
|
+
stdout += data.toString();
|
|
951
|
+
});
|
|
952
|
+
child.stderr.on("data", (data) => {
|
|
953
|
+
stderr += data.toString();
|
|
954
|
+
});
|
|
955
|
+
child.on("close", (code) => {
|
|
956
|
+
resolve2({
|
|
957
|
+
exitCode: code || 0,
|
|
958
|
+
stdout,
|
|
959
|
+
stderr
|
|
960
|
+
});
|
|
961
|
+
});
|
|
962
|
+
child.on("error", (err) => {
|
|
963
|
+
reject(err);
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const execAsync = promisify(exec);
|
|
969
|
+
function registerCommonHandlers(rpcHandlerManager) {
|
|
970
|
+
rpcHandlerManager.registerHandler("bash", async (data) => {
|
|
971
|
+
logger.debug("Shell command request:", data.command);
|
|
972
|
+
try {
|
|
973
|
+
const options = {
|
|
974
|
+
cwd: data.cwd,
|
|
975
|
+
timeout: data.timeout || 3e4
|
|
976
|
+
// Default 30 seconds timeout
|
|
977
|
+
};
|
|
978
|
+
const { stdout, stderr } = await execAsync(data.command, options);
|
|
979
|
+
return {
|
|
980
|
+
success: true,
|
|
981
|
+
stdout: stdout ? stdout.toString() : "",
|
|
982
|
+
stderr: stderr ? stderr.toString() : "",
|
|
983
|
+
exitCode: 0
|
|
984
|
+
};
|
|
985
|
+
} catch (error) {
|
|
986
|
+
const execError = error;
|
|
987
|
+
if (execError.code === "ETIMEDOUT" || execError.killed) {
|
|
988
|
+
return {
|
|
989
|
+
success: false,
|
|
990
|
+
stdout: execError.stdout || "",
|
|
991
|
+
stderr: execError.stderr || "",
|
|
992
|
+
exitCode: typeof execError.code === "number" ? execError.code : -1,
|
|
993
|
+
error: "Command timed out"
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
success: false,
|
|
998
|
+
stdout: execError.stdout ? execError.stdout.toString() : "",
|
|
999
|
+
stderr: execError.stderr ? execError.stderr.toString() : execError.message || "Command failed",
|
|
1000
|
+
exitCode: typeof execError.code === "number" ? execError.code : 1,
|
|
1001
|
+
error: execError.message || "Command failed"
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
rpcHandlerManager.registerHandler("readFile", async (data) => {
|
|
1006
|
+
logger.debug("Read file request:", data.path);
|
|
1007
|
+
try {
|
|
1008
|
+
const buffer = await readFile$1(data.path);
|
|
1009
|
+
const content = buffer.toString("base64");
|
|
1010
|
+
return { success: true, content };
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
logger.debug("Failed to read file:", error);
|
|
1013
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
rpcHandlerManager.registerHandler("writeFile", async (data) => {
|
|
1017
|
+
logger.debug("Write file request:", data.path);
|
|
1018
|
+
try {
|
|
1019
|
+
if (data.expectedHash !== null && data.expectedHash !== void 0) {
|
|
1020
|
+
try {
|
|
1021
|
+
const existingBuffer = await readFile$1(data.path);
|
|
1022
|
+
const existingHash = createHash("sha256").update(existingBuffer).digest("hex");
|
|
1023
|
+
if (existingHash !== data.expectedHash) {
|
|
1024
|
+
return {
|
|
1025
|
+
success: false,
|
|
1026
|
+
error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
const nodeError = error;
|
|
1031
|
+
if (nodeError.code !== "ENOENT") {
|
|
1032
|
+
throw error;
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
success: false,
|
|
1036
|
+
error: "File does not exist but hash was provided"
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
} else {
|
|
1040
|
+
try {
|
|
1041
|
+
await stat$1(data.path);
|
|
1042
|
+
return {
|
|
1043
|
+
success: false,
|
|
1044
|
+
error: "File already exists but was expected to be new"
|
|
1045
|
+
};
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
const nodeError = error;
|
|
1048
|
+
if (nodeError.code !== "ENOENT") {
|
|
1049
|
+
throw error;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const buffer = Buffer.from(data.content, "base64");
|
|
1054
|
+
await writeFile$1(data.path, buffer);
|
|
1055
|
+
const hash = createHash("sha256").update(buffer).digest("hex");
|
|
1056
|
+
return { success: true, hash };
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
logger.debug("Failed to write file:", error);
|
|
1059
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
rpcHandlerManager.registerHandler("listDirectory", async (data) => {
|
|
1063
|
+
logger.debug("List directory request:", data.path);
|
|
1064
|
+
try {
|
|
1065
|
+
const entries = await readdir(data.path, { withFileTypes: true });
|
|
1066
|
+
const directoryEntries = await Promise.all(
|
|
1067
|
+
entries.map(async (entry) => {
|
|
1068
|
+
const fullPath = join$1(data.path, entry.name);
|
|
1069
|
+
let type = "other";
|
|
1070
|
+
let size;
|
|
1071
|
+
let modified;
|
|
1072
|
+
if (entry.isDirectory()) {
|
|
1073
|
+
type = "directory";
|
|
1074
|
+
} else if (entry.isFile()) {
|
|
1075
|
+
type = "file";
|
|
1076
|
+
}
|
|
1077
|
+
try {
|
|
1078
|
+
const stats = await stat$1(fullPath);
|
|
1079
|
+
size = stats.size;
|
|
1080
|
+
modified = stats.mtime.getTime();
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
logger.debug(`Failed to stat ${fullPath}:`, error);
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
1085
|
+
name: entry.name,
|
|
1086
|
+
type,
|
|
1087
|
+
size,
|
|
1088
|
+
modified
|
|
1089
|
+
};
|
|
1090
|
+
})
|
|
1091
|
+
);
|
|
1092
|
+
directoryEntries.sort((a, b) => {
|
|
1093
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
1094
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
1095
|
+
return a.name.localeCompare(b.name);
|
|
1096
|
+
});
|
|
1097
|
+
return { success: true, entries: directoryEntries };
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
logger.debug("Failed to list directory:", error);
|
|
1100
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
rpcHandlerManager.registerHandler("getDirectoryTree", async (data) => {
|
|
1104
|
+
logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
|
|
1105
|
+
async function buildTree(path, name, currentDepth) {
|
|
1106
|
+
try {
|
|
1107
|
+
const stats = await stat$1(path);
|
|
1108
|
+
const node = {
|
|
1109
|
+
name,
|
|
1110
|
+
path,
|
|
1111
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
1112
|
+
size: stats.size,
|
|
1113
|
+
modified: stats.mtime.getTime()
|
|
1114
|
+
};
|
|
1115
|
+
if (stats.isDirectory() && currentDepth < data.maxDepth) {
|
|
1116
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
1117
|
+
const children = [];
|
|
1118
|
+
await Promise.all(
|
|
1119
|
+
entries.map(async (entry) => {
|
|
1120
|
+
if (entry.isSymbolicLink()) {
|
|
1121
|
+
logger.debug(`Skipping symlink: ${join$1(path, entry.name)}`);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const childPath = join$1(path, entry.name);
|
|
1125
|
+
const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
|
|
1126
|
+
if (childNode) {
|
|
1127
|
+
children.push(childNode);
|
|
1128
|
+
}
|
|
1129
|
+
})
|
|
1130
|
+
);
|
|
1131
|
+
children.sort((a, b) => {
|
|
1132
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
1133
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
1134
|
+
return a.name.localeCompare(b.name);
|
|
1135
|
+
});
|
|
1136
|
+
node.children = children;
|
|
1137
|
+
}
|
|
1138
|
+
return node;
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
logger.debug(`Failed to process ${path}:`, error instanceof Error ? error.message : String(error));
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
if (data.maxDepth < 0) {
|
|
1146
|
+
return { success: false, error: "maxDepth must be non-negative" };
|
|
1147
|
+
}
|
|
1148
|
+
const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
|
|
1149
|
+
const tree = await buildTree(data.path, baseName, 0);
|
|
1150
|
+
if (!tree) {
|
|
1151
|
+
return { success: false, error: "Failed to access the specified path" };
|
|
1152
|
+
}
|
|
1153
|
+
return { success: true, tree };
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
logger.debug("Failed to get directory tree:", error);
|
|
1156
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
rpcHandlerManager.registerHandler("ripgrep", async (data) => {
|
|
1160
|
+
logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
|
|
1161
|
+
try {
|
|
1162
|
+
const result = await run(data.args, { cwd: data.cwd });
|
|
1163
|
+
return {
|
|
1164
|
+
success: true,
|
|
1165
|
+
exitCode: result.exitCode,
|
|
1166
|
+
stdout: result.stdout.toString(),
|
|
1167
|
+
stderr: result.stderr.toString()
|
|
1168
|
+
};
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
logger.debug("Failed to run ripgrep:", error);
|
|
1171
|
+
return {
|
|
1172
|
+
success: false,
|
|
1173
|
+
error: error instanceof Error ? error.message : "Failed to run ripgrep"
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
|
|
835
1179
|
class ApiSessionClient extends EventEmitter {
|
|
836
1180
|
token;
|
|
837
1181
|
secret;
|
|
@@ -843,7 +1187,7 @@ class ApiSessionClient extends EventEmitter {
|
|
|
843
1187
|
socket;
|
|
844
1188
|
pendingMessages = [];
|
|
845
1189
|
pendingMessageCallback = null;
|
|
846
|
-
|
|
1190
|
+
rpcHandlerManager;
|
|
847
1191
|
agentStateLock = new AsyncLock();
|
|
848
1192
|
metadataLock = new AsyncLock();
|
|
849
1193
|
constructor(token, secret, session) {
|
|
@@ -855,6 +1199,12 @@ class ApiSessionClient extends EventEmitter {
|
|
|
855
1199
|
this.metadataVersion = session.metadataVersion;
|
|
856
1200
|
this.agentState = session.agentState;
|
|
857
1201
|
this.agentStateVersion = session.agentStateVersion;
|
|
1202
|
+
this.rpcHandlerManager = new RpcHandlerManager({
|
|
1203
|
+
scopePrefix: this.sessionId,
|
|
1204
|
+
secret: this.secret,
|
|
1205
|
+
logger: (msg, data) => logger.debug(msg, data)
|
|
1206
|
+
});
|
|
1207
|
+
registerCommonHandlers(this.rpcHandlerManager);
|
|
858
1208
|
this.socket = io(configuration.serverUrl, {
|
|
859
1209
|
auth: {
|
|
860
1210
|
token: this.token,
|
|
@@ -872,35 +1222,18 @@ class ApiSessionClient extends EventEmitter {
|
|
|
872
1222
|
});
|
|
873
1223
|
this.socket.on("connect", () => {
|
|
874
1224
|
logger.debug("Socket connected successfully");
|
|
875
|
-
this.
|
|
1225
|
+
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
876
1226
|
});
|
|
877
1227
|
this.socket.on("rpc-request", async (data, callback) => {
|
|
878
|
-
|
|
879
|
-
const method = data.method;
|
|
880
|
-
const handler = this.rpcHandlers.get(method);
|
|
881
|
-
if (!handler) {
|
|
882
|
-
logger.debug("[SOCKET] [RPC] [ERROR] method not found", { method });
|
|
883
|
-
const errorResponse = { error: "Method not found" };
|
|
884
|
-
const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
|
|
885
|
-
callback(encryptedError);
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
const decryptedParams = decrypt(decodeBase64(data.params), this.secret);
|
|
889
|
-
const result = await handler(decryptedParams);
|
|
890
|
-
const encryptedResponse = encodeBase64(encrypt(result, this.secret));
|
|
891
|
-
callback(encryptedResponse);
|
|
892
|
-
} catch (error) {
|
|
893
|
-
logger.debug("[SOCKET] [RPC] [ERROR] Error handling RPC request", { error });
|
|
894
|
-
const errorResponse = { error: error instanceof Error ? error.message : "Unknown error" };
|
|
895
|
-
const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
|
|
896
|
-
callback(encryptedError);
|
|
897
|
-
}
|
|
1228
|
+
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
898
1229
|
});
|
|
899
1230
|
this.socket.on("disconnect", (reason) => {
|
|
900
1231
|
logger.debug("[API] Socket disconnected:", reason);
|
|
1232
|
+
this.rpcHandlerManager.onSocketDisconnect();
|
|
901
1233
|
});
|
|
902
1234
|
this.socket.on("connect_error", (error) => {
|
|
903
1235
|
logger.debug("[API] Socket connection error:", error);
|
|
1236
|
+
this.rpcHandlerManager.onSocketDisconnect();
|
|
904
1237
|
});
|
|
905
1238
|
this.socket.on("update", (data) => {
|
|
906
1239
|
try {
|
|
@@ -1111,29 +1444,6 @@ class ApiSessionClient extends EventEmitter {
|
|
|
1111
1444
|
});
|
|
1112
1445
|
});
|
|
1113
1446
|
}
|
|
1114
|
-
/**
|
|
1115
|
-
* Set a custom RPC handler for a specific method with encrypted arguments and responses
|
|
1116
|
-
* @param method - The method name to handle
|
|
1117
|
-
* @param handler - The handler function to call when the method is invoked
|
|
1118
|
-
*/
|
|
1119
|
-
setHandler(method, handler) {
|
|
1120
|
-
const prefixedMethod = `${this.sessionId}:${method}`;
|
|
1121
|
-
this.rpcHandlers.set(prefixedMethod, handler);
|
|
1122
|
-
this.socket.emit("rpc-register", { method: prefixedMethod });
|
|
1123
|
-
logger.debug("Registered RPC handler", { method, prefixedMethod });
|
|
1124
|
-
}
|
|
1125
|
-
/**
|
|
1126
|
-
* Re-register all RPC handlers after reconnection
|
|
1127
|
-
*/
|
|
1128
|
-
reregisterHandlers() {
|
|
1129
|
-
logger.debug("Re-registering RPC handlers after reconnection", {
|
|
1130
|
-
totalMethods: this.rpcHandlers.size
|
|
1131
|
-
});
|
|
1132
|
-
for (const [prefixedMethod] of this.rpcHandlers) {
|
|
1133
|
-
this.socket.emit("rpc-register", { method: prefixedMethod });
|
|
1134
|
-
logger.debug("Re-registered method", { prefixedMethod });
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
1447
|
/**
|
|
1138
1448
|
* Wait for socket buffer to flush
|
|
1139
1449
|
*/
|
|
@@ -1160,21 +1470,58 @@ class ApiMachineClient {
|
|
|
1160
1470
|
this.token = token;
|
|
1161
1471
|
this.secret = secret;
|
|
1162
1472
|
this.machine = machine;
|
|
1473
|
+
this.rpcHandlerManager = new RpcHandlerManager({
|
|
1474
|
+
scopePrefix: this.machine.id,
|
|
1475
|
+
secret: this.secret,
|
|
1476
|
+
logger: (msg, data) => logger.debug(msg, data)
|
|
1477
|
+
});
|
|
1478
|
+
registerCommonHandlers(this.rpcHandlerManager);
|
|
1163
1479
|
}
|
|
1164
1480
|
socket;
|
|
1165
1481
|
keepAliveInterval = null;
|
|
1166
|
-
|
|
1167
|
-
spawnSession;
|
|
1168
|
-
stopSession;
|
|
1169
|
-
requestShutdown;
|
|
1482
|
+
rpcHandlerManager;
|
|
1170
1483
|
setRPCHandlers({
|
|
1171
1484
|
spawnSession,
|
|
1172
1485
|
stopSession,
|
|
1173
1486
|
requestShutdown
|
|
1174
1487
|
}) {
|
|
1175
|
-
this.
|
|
1176
|
-
|
|
1177
|
-
|
|
1488
|
+
this.rpcHandlerManager.registerHandler("spawn-happy-session", async (params) => {
|
|
1489
|
+
const { directory, sessionId, machineId, approvedNewDirectoryCreation } = params || {};
|
|
1490
|
+
if (!directory) {
|
|
1491
|
+
throw new Error("Directory is required");
|
|
1492
|
+
}
|
|
1493
|
+
const result = await spawnSession({ directory, sessionId, machineId, approvedNewDirectoryCreation });
|
|
1494
|
+
switch (result.type) {
|
|
1495
|
+
case "success":
|
|
1496
|
+
logger.debug(`[API MACHINE] Spawned session ${result.sessionId}`);
|
|
1497
|
+
return { type: "success", sessionId: result.sessionId };
|
|
1498
|
+
case "requestToApproveDirectoryCreation":
|
|
1499
|
+
logger.debug(`[API MACHINE] Requesting directory creation approval for: ${result.directory}`);
|
|
1500
|
+
return { type: "requestToApproveDirectoryCreation", directory: result.directory };
|
|
1501
|
+
case "error":
|
|
1502
|
+
throw new Error(result.errorMessage);
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
this.rpcHandlerManager.registerHandler("stop-session", (params) => {
|
|
1506
|
+
const { sessionId } = params || {};
|
|
1507
|
+
if (!sessionId) {
|
|
1508
|
+
throw new Error("Session ID is required");
|
|
1509
|
+
}
|
|
1510
|
+
const success = stopSession(sessionId);
|
|
1511
|
+
if (!success) {
|
|
1512
|
+
throw new Error("Session not found or failed to stop");
|
|
1513
|
+
}
|
|
1514
|
+
logger.debug(`[API MACHINE] Stopped session ${sessionId}`);
|
|
1515
|
+
return { message: "Session stopped" };
|
|
1516
|
+
});
|
|
1517
|
+
this.rpcHandlerManager.registerHandler("stop-daemon", () => {
|
|
1518
|
+
logger.debug("[API MACHINE] Received stop-daemon RPC request");
|
|
1519
|
+
setTimeout(() => {
|
|
1520
|
+
logger.debug("[API MACHINE] Initiating daemon shutdown from RPC");
|
|
1521
|
+
requestShutdown();
|
|
1522
|
+
}, 100);
|
|
1523
|
+
return { message: "Daemon stop request acknowledged, starting shutdown sequence..." };
|
|
1524
|
+
});
|
|
1178
1525
|
}
|
|
1179
1526
|
/**
|
|
1180
1527
|
* Update machine metadata
|
|
@@ -1242,9 +1589,6 @@ class ApiMachineClient {
|
|
|
1242
1589
|
reconnectionDelay: 1e3,
|
|
1243
1590
|
reconnectionDelayMax: 5e3
|
|
1244
1591
|
});
|
|
1245
|
-
const spawnMethod = `${this.machine.id}:spawn-happy-session`;
|
|
1246
|
-
const stopMethod = `${this.machine.id}:stop-session`;
|
|
1247
|
-
const stopDaemonMethod = `${this.machine.id}:stop-daemon`;
|
|
1248
1592
|
this.socket.on("connect", () => {
|
|
1249
1593
|
logger.debug("[API MACHINE] Connected to server");
|
|
1250
1594
|
this.updateDaemonState((state) => ({
|
|
@@ -1254,84 +1598,17 @@ class ApiMachineClient {
|
|
|
1254
1598
|
httpPort: this.machine.daemonState?.httpPort,
|
|
1255
1599
|
startedAt: Date.now()
|
|
1256
1600
|
}));
|
|
1257
|
-
this.
|
|
1258
|
-
this.socket.emit("rpc-register", { method: stopMethod });
|
|
1259
|
-
this.socket.emit("rpc-register", { method: stopDaemonMethod });
|
|
1260
|
-
logger.debug(`[API MACHINE] Registered RPC methods: ${spawnMethod}, ${stopMethod}, ${stopDaemonMethod}`);
|
|
1601
|
+
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
1261
1602
|
this.startKeepAlive();
|
|
1262
1603
|
});
|
|
1604
|
+
this.socket.on("disconnect", () => {
|
|
1605
|
+
logger.debug("[API MACHINE] Disconnected from server");
|
|
1606
|
+
this.rpcHandlerManager.onSocketDisconnect();
|
|
1607
|
+
this.stopKeepAlive();
|
|
1608
|
+
});
|
|
1263
1609
|
this.socket.on("rpc-request", async (data, callback) => {
|
|
1264
1610
|
logger.debugLargeJson(`[API MACHINE] Received RPC request:`, data);
|
|
1265
|
-
|
|
1266
|
-
const spawnMethod2 = `${this.machine.id}:spawn-happy-session`;
|
|
1267
|
-
const stopMethod2 = `${this.machine.id}:stop-session`;
|
|
1268
|
-
const stopDaemonMethod2 = `${this.machine.id}:stop-daemon`;
|
|
1269
|
-
if (data.method === spawnMethod2) {
|
|
1270
|
-
if (!this.spawnSession) {
|
|
1271
|
-
throw new Error("Spawn session handler not set");
|
|
1272
|
-
}
|
|
1273
|
-
const { directory, sessionId } = decrypt(decodeBase64(data.params), this.secret) || {};
|
|
1274
|
-
if (!directory) {
|
|
1275
|
-
throw new Error("Directory is required");
|
|
1276
|
-
}
|
|
1277
|
-
const session = await this.spawnSession(directory, sessionId);
|
|
1278
|
-
if (!session) {
|
|
1279
|
-
throw new Error("Failed to spawn session");
|
|
1280
|
-
}
|
|
1281
|
-
if (session.error) {
|
|
1282
|
-
throw new Error(session.error);
|
|
1283
|
-
}
|
|
1284
|
-
logger.debug(`[API MACHINE] Spawned session ${session.happySessionId || "WARNING - not session Id recieved in webhook"} with PID ${session.pid}`);
|
|
1285
|
-
if (!session.happySessionId) {
|
|
1286
|
-
throw new Error(`Session spawned (PID ${session.pid}) but no sessionId received from webhook. The session process may still be initializing.`);
|
|
1287
|
-
}
|
|
1288
|
-
const response = {
|
|
1289
|
-
sessionId: session.happySessionId,
|
|
1290
|
-
message: session.message
|
|
1291
|
-
};
|
|
1292
|
-
logger.debug(`[API MACHINE] Sending RPC response:`, response);
|
|
1293
|
-
callback(encodeBase64(encrypt(response, this.secret)));
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
if (data.method === stopMethod2) {
|
|
1297
|
-
logger.debug("[API MACHINE] Received stop-session RPC request");
|
|
1298
|
-
const decryptedParams = decrypt(decodeBase64(data.params), this.secret);
|
|
1299
|
-
const { sessionId } = decryptedParams || {};
|
|
1300
|
-
if (!this.stopSession) {
|
|
1301
|
-
throw new Error("Stop session handler not set");
|
|
1302
|
-
}
|
|
1303
|
-
if (!sessionId) {
|
|
1304
|
-
throw new Error("Session ID is required");
|
|
1305
|
-
}
|
|
1306
|
-
const success = this.stopSession(sessionId);
|
|
1307
|
-
if (!success) {
|
|
1308
|
-
throw new Error("Session not found or failed to stop");
|
|
1309
|
-
}
|
|
1310
|
-
logger.debug(`[API MACHINE] Stopped session ${sessionId}`);
|
|
1311
|
-
const response = { message: "Session stopped" };
|
|
1312
|
-
const encryptedResponse = encodeBase64(encrypt(response, this.secret));
|
|
1313
|
-
callback(encryptedResponse);
|
|
1314
|
-
return;
|
|
1315
|
-
}
|
|
1316
|
-
if (data.method === stopDaemonMethod2) {
|
|
1317
|
-
logger.debug("[API MACHINE] Received stop-daemon RPC request");
|
|
1318
|
-
callback(encodeBase64(encrypt({
|
|
1319
|
-
message: "Daemon stop request acknowledged, starting shutdown sequence..."
|
|
1320
|
-
}, this.secret)));
|
|
1321
|
-
setTimeout(() => {
|
|
1322
|
-
logger.debug("[API MACHINE] Initiating daemon shutdown from RPC");
|
|
1323
|
-
if (this.requestShutdown) {
|
|
1324
|
-
this.requestShutdown();
|
|
1325
|
-
}
|
|
1326
|
-
}, 100);
|
|
1327
|
-
return;
|
|
1328
|
-
}
|
|
1329
|
-
throw new Error(`Unknown RPC method: ${data.method}`);
|
|
1330
|
-
} catch (error) {
|
|
1331
|
-
logger.debug(`[API MACHINE] RPC handler failed:`, error.message || error);
|
|
1332
|
-
logger.debug(`[API MACHINE] Error stack:`, error.stack);
|
|
1333
|
-
callback(encodeBase64(encrypt({ error: error.message || String(error) }, this.secret)));
|
|
1334
|
-
}
|
|
1611
|
+
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
1335
1612
|
});
|
|
1336
1613
|
this.socket.on("update", (data) => {
|
|
1337
1614
|
if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
|
|
@@ -1350,16 +1627,6 @@ class ApiMachineClient {
|
|
|
1350
1627
|
logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
|
|
1351
1628
|
}
|
|
1352
1629
|
});
|
|
1353
|
-
this.socket.on("disconnect", () => {
|
|
1354
|
-
logger.debug("[API MACHINE] Disconnected from server");
|
|
1355
|
-
this.stopKeepAlive();
|
|
1356
|
-
});
|
|
1357
|
-
this.socket.io.on("reconnect", () => {
|
|
1358
|
-
logger.debug("[API MACHINE] Reconnected to server");
|
|
1359
|
-
this.socket.emit("rpc-register", { method: spawnMethod });
|
|
1360
|
-
this.socket.emit("rpc-register", { method: stopMethod });
|
|
1361
|
-
this.socket.emit("rpc-register", { method: stopDaemonMethod });
|
|
1362
|
-
});
|
|
1363
1630
|
this.socket.on("connect_error", (error) => {
|
|
1364
1631
|
logger.debug(`[API MACHINE] Connection error: ${error.message}`);
|
|
1365
1632
|
});
|
|
@@ -1606,7 +1873,7 @@ class ApiClient {
|
|
|
1606
1873
|
* Register or update machine with the server
|
|
1607
1874
|
* Returns the current machine state from the server with decrypted metadata and daemonState
|
|
1608
1875
|
*/
|
|
1609
|
-
async
|
|
1876
|
+
async getOrCreateMachine(opts) {
|
|
1610
1877
|
const response = await axios.post(
|
|
1611
1878
|
`${configuration.serverUrl}/v1/machines`,
|
|
1612
1879
|
{
|
|
@@ -1651,6 +1918,34 @@ class ApiClient {
|
|
|
1651
1918
|
push() {
|
|
1652
1919
|
return this.pushClient;
|
|
1653
1920
|
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Register a vendor API token with the server
|
|
1923
|
+
* The token is sent as a JSON string - server handles encryption
|
|
1924
|
+
*/
|
|
1925
|
+
async registerVendorToken(vendor, apiKey) {
|
|
1926
|
+
try {
|
|
1927
|
+
const response = await axios.post(
|
|
1928
|
+
`${configuration.serverUrl}/v1/connect/${vendor}/register`,
|
|
1929
|
+
{
|
|
1930
|
+
token: JSON.stringify(apiKey)
|
|
1931
|
+
},
|
|
1932
|
+
{
|
|
1933
|
+
headers: {
|
|
1934
|
+
"Authorization": `Bearer ${this.token}`,
|
|
1935
|
+
"Content-Type": "application/json"
|
|
1936
|
+
},
|
|
1937
|
+
timeout: 5e3
|
|
1938
|
+
}
|
|
1939
|
+
);
|
|
1940
|
+
if (response.status !== 200 && response.status !== 201) {
|
|
1941
|
+
throw new Error(`Server returned status ${response.status}`);
|
|
1942
|
+
}
|
|
1943
|
+
logger.debug(`[API] Vendor token for ${vendor} registered successfully`);
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
logger.debug(`[API] [ERROR] Failed to register vendor token:`, error);
|
|
1946
|
+
throw new Error(`Failed to register vendor token: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1654
1949
|
}
|
|
1655
1950
|
|
|
1656
1951
|
const UsageSchema = z$1.object({
|
|
@@ -1701,4 +1996,4 @@ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
|
|
|
1701
1996
|
}).passthrough()
|
|
1702
1997
|
]);
|
|
1703
1998
|
|
|
1704
|
-
export { ApiClient as A, RawJSONLinesSchema as R, ApiSessionClient as a, backoff as b, configuration as c, delay as d, AsyncLock as e, clearDaemonState as f,
|
|
1999
|
+
export { ApiClient as A, RawJSONLinesSchema as R, ApiSessionClient as a, backoff as b, configuration as c, delay as d, AsyncLock as e, clearDaemonState as f, packageJson as g, readSettings as h, readCredentials as i, encodeBase64 as j, encodeBase64Url as k, logger as l, decodeBase64 as m, acquireDaemonLock as n, writeDaemonState as o, projectPath as p, releaseDaemonLock as q, readDaemonState as r, clearCredentials as s, clearMachineId as t, updateSettings as u, getLatestDaemonLog as v, writeCredentials as w };
|