happy-coder 0.10.0-0 → 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 +32 -277
- package/dist/index.mjs +29 -274
- package/dist/lib.cjs +7 -1
- package/dist/lib.d.cts +78 -21
- package/dist/lib.d.mts +78 -21
- package/dist/lib.mjs +7 -1
- package/dist/{types-CGbH1LGX.mjs → types-BUXwivpV.mjs} +407 -146
- package/dist/{types-fU2E-jQl.cjs → types-D9P2bndj.cjs} +408 -145
- package/package.json +3 -3
|
@@ -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.10.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,6 +104,8 @@ 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",
|
|
102
111
|
"cross-spawn": "^7.0.6",
|
|
@@ -116,9 +125,7 @@ var dependencies = {
|
|
|
116
125
|
};
|
|
117
126
|
var devDependencies = {
|
|
118
127
|
"@eslint/compat": "^1",
|
|
119
|
-
"@types/cross-spawn": "^6.0.6",
|
|
120
128
|
"@types/node": ">=20",
|
|
121
|
-
"@types/ps-list": "^6.2.1",
|
|
122
129
|
"cross-env": "^10.0.0",
|
|
123
130
|
dotenv: "^16.6.1",
|
|
124
131
|
eslint: "^9",
|
|
@@ -856,6 +863,340 @@ class AsyncLock {
|
|
|
856
863
|
}
|
|
857
864
|
}
|
|
858
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
|
+
|
|
859
1200
|
class ApiSessionClient extends node_events.EventEmitter {
|
|
860
1201
|
token;
|
|
861
1202
|
secret;
|
|
@@ -867,7 +1208,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
867
1208
|
socket;
|
|
868
1209
|
pendingMessages = [];
|
|
869
1210
|
pendingMessageCallback = null;
|
|
870
|
-
|
|
1211
|
+
rpcHandlerManager;
|
|
871
1212
|
agentStateLock = new AsyncLock();
|
|
872
1213
|
metadataLock = new AsyncLock();
|
|
873
1214
|
constructor(token, secret, session) {
|
|
@@ -879,6 +1220,12 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
879
1220
|
this.metadataVersion = session.metadataVersion;
|
|
880
1221
|
this.agentState = session.agentState;
|
|
881
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);
|
|
882
1229
|
this.socket = socket_ioClient.io(configuration.serverUrl, {
|
|
883
1230
|
auth: {
|
|
884
1231
|
token: this.token,
|
|
@@ -896,35 +1243,18 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
896
1243
|
});
|
|
897
1244
|
this.socket.on("connect", () => {
|
|
898
1245
|
logger.debug("Socket connected successfully");
|
|
899
|
-
this.
|
|
1246
|
+
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
900
1247
|
});
|
|
901
1248
|
this.socket.on("rpc-request", async (data, callback) => {
|
|
902
|
-
|
|
903
|
-
const method = data.method;
|
|
904
|
-
const handler = this.rpcHandlers.get(method);
|
|
905
|
-
if (!handler) {
|
|
906
|
-
logger.debug("[SOCKET] [RPC] [ERROR] method not found", { method });
|
|
907
|
-
const errorResponse = { error: "Method not found" };
|
|
908
|
-
const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
|
|
909
|
-
callback(encryptedError);
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
const decryptedParams = decrypt(decodeBase64(data.params), this.secret);
|
|
913
|
-
const result = await handler(decryptedParams);
|
|
914
|
-
const encryptedResponse = encodeBase64(encrypt(result, this.secret));
|
|
915
|
-
callback(encryptedResponse);
|
|
916
|
-
} catch (error) {
|
|
917
|
-
logger.debug("[SOCKET] [RPC] [ERROR] Error handling RPC request", { error });
|
|
918
|
-
const errorResponse = { error: error instanceof Error ? error.message : "Unknown error" };
|
|
919
|
-
const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
|
|
920
|
-
callback(encryptedError);
|
|
921
|
-
}
|
|
1249
|
+
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
922
1250
|
});
|
|
923
1251
|
this.socket.on("disconnect", (reason) => {
|
|
924
1252
|
logger.debug("[API] Socket disconnected:", reason);
|
|
1253
|
+
this.rpcHandlerManager.onSocketDisconnect();
|
|
925
1254
|
});
|
|
926
1255
|
this.socket.on("connect_error", (error) => {
|
|
927
1256
|
logger.debug("[API] Socket connection error:", error);
|
|
1257
|
+
this.rpcHandlerManager.onSocketDisconnect();
|
|
928
1258
|
});
|
|
929
1259
|
this.socket.on("update", (data) => {
|
|
930
1260
|
try {
|
|
@@ -1135,29 +1465,6 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
1135
1465
|
});
|
|
1136
1466
|
});
|
|
1137
1467
|
}
|
|
1138
|
-
/**
|
|
1139
|
-
* Set a custom RPC handler for a specific method with encrypted arguments and responses
|
|
1140
|
-
* @param method - The method name to handle
|
|
1141
|
-
* @param handler - The handler function to call when the method is invoked
|
|
1142
|
-
*/
|
|
1143
|
-
setHandler(method, handler) {
|
|
1144
|
-
const prefixedMethod = `${this.sessionId}:${method}`;
|
|
1145
|
-
this.rpcHandlers.set(prefixedMethod, handler);
|
|
1146
|
-
this.socket.emit("rpc-register", { method: prefixedMethod });
|
|
1147
|
-
logger.debug("Registered RPC handler", { method, prefixedMethod });
|
|
1148
|
-
}
|
|
1149
|
-
/**
|
|
1150
|
-
* Re-register all RPC handlers after reconnection
|
|
1151
|
-
*/
|
|
1152
|
-
reregisterHandlers() {
|
|
1153
|
-
logger.debug("Re-registering RPC handlers after reconnection", {
|
|
1154
|
-
totalMethods: this.rpcHandlers.size
|
|
1155
|
-
});
|
|
1156
|
-
for (const [prefixedMethod] of this.rpcHandlers) {
|
|
1157
|
-
this.socket.emit("rpc-register", { method: prefixedMethod });
|
|
1158
|
-
logger.debug("Re-registered method", { prefixedMethod });
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
1468
|
/**
|
|
1162
1469
|
* Wait for socket buffer to flush
|
|
1163
1470
|
*/
|
|
@@ -1184,21 +1491,58 @@ class ApiMachineClient {
|
|
|
1184
1491
|
this.token = token;
|
|
1185
1492
|
this.secret = secret;
|
|
1186
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);
|
|
1187
1500
|
}
|
|
1188
1501
|
socket;
|
|
1189
1502
|
keepAliveInterval = null;
|
|
1190
|
-
|
|
1191
|
-
spawnSession;
|
|
1192
|
-
stopSession;
|
|
1193
|
-
requestShutdown;
|
|
1503
|
+
rpcHandlerManager;
|
|
1194
1504
|
setRPCHandlers({
|
|
1195
1505
|
spawnSession,
|
|
1196
1506
|
stopSession,
|
|
1197
1507
|
requestShutdown
|
|
1198
1508
|
}) {
|
|
1199
|
-
this.
|
|
1200
|
-
|
|
1201
|
-
|
|
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
|
+
});
|
|
1202
1546
|
}
|
|
1203
1547
|
/**
|
|
1204
1548
|
* Update machine metadata
|
|
@@ -1266,9 +1610,6 @@ class ApiMachineClient {
|
|
|
1266
1610
|
reconnectionDelay: 1e3,
|
|
1267
1611
|
reconnectionDelayMax: 5e3
|
|
1268
1612
|
});
|
|
1269
|
-
const spawnMethod = `${this.machine.id}:spawn-happy-session`;
|
|
1270
|
-
const stopMethod = `${this.machine.id}:stop-session`;
|
|
1271
|
-
const stopDaemonMethod = `${this.machine.id}:stop-daemon`;
|
|
1272
1613
|
this.socket.on("connect", () => {
|
|
1273
1614
|
logger.debug("[API MACHINE] Connected to server");
|
|
1274
1615
|
this.updateDaemonState((state) => ({
|
|
@@ -1278,86 +1619,17 @@ class ApiMachineClient {
|
|
|
1278
1619
|
httpPort: this.machine.daemonState?.httpPort,
|
|
1279
1620
|
startedAt: Date.now()
|
|
1280
1621
|
}));
|
|
1281
|
-
this.
|
|
1282
|
-
this.socket.emit("rpc-register", { method: stopMethod });
|
|
1283
|
-
this.socket.emit("rpc-register", { method: stopDaemonMethod });
|
|
1284
|
-
logger.debug(`[API MACHINE] Registered RPC methods: ${spawnMethod}, ${stopMethod}, ${stopDaemonMethod}`);
|
|
1622
|
+
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
1285
1623
|
this.startKeepAlive();
|
|
1286
1624
|
});
|
|
1625
|
+
this.socket.on("disconnect", () => {
|
|
1626
|
+
logger.debug("[API MACHINE] Disconnected from server");
|
|
1627
|
+
this.rpcHandlerManager.onSocketDisconnect();
|
|
1628
|
+
this.stopKeepAlive();
|
|
1629
|
+
});
|
|
1287
1630
|
this.socket.on("rpc-request", async (data, callback) => {
|
|
1288
1631
|
logger.debugLargeJson(`[API MACHINE] Received RPC request:`, data);
|
|
1289
|
-
|
|
1290
|
-
if (data.method === spawnMethod) {
|
|
1291
|
-
if (!this.spawnSession) {
|
|
1292
|
-
throw new Error("Spawn session handler not set");
|
|
1293
|
-
}
|
|
1294
|
-
const { directory, sessionId, machineId, approvedNewDirectoryCreation } = decrypt(decodeBase64(data.params), this.secret) || {};
|
|
1295
|
-
if (!directory) {
|
|
1296
|
-
throw new Error("Directory is required");
|
|
1297
|
-
}
|
|
1298
|
-
const result = await this.spawnSession({ directory, sessionId, machineId, approvedNewDirectoryCreation });
|
|
1299
|
-
switch (result.type) {
|
|
1300
|
-
case "success": {
|
|
1301
|
-
logger.debug(`[API MACHINE] Spawned session ${result.sessionId}`);
|
|
1302
|
-
const response = {
|
|
1303
|
-
type: "success",
|
|
1304
|
-
sessionId: result.sessionId
|
|
1305
|
-
};
|
|
1306
|
-
logger.debug(`[API MACHINE] Sending RPC response:`, response);
|
|
1307
|
-
callback(encodeBase64(encrypt(response, this.secret)));
|
|
1308
|
-
return;
|
|
1309
|
-
}
|
|
1310
|
-
case "requestToApproveDirectoryCreation":
|
|
1311
|
-
const promptResponse = {
|
|
1312
|
-
type: "requestToApproveDirectoryCreation",
|
|
1313
|
-
directory: result.directory
|
|
1314
|
-
};
|
|
1315
|
-
logger.debug(`[API MACHINE] Requesting directory creation approval for: ${result.directory}`);
|
|
1316
|
-
callback(encodeBase64(encrypt(promptResponse, this.secret)));
|
|
1317
|
-
return;
|
|
1318
|
-
case "error":
|
|
1319
|
-
throw new Error(result.errorMessage);
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
if (data.method === stopMethod) {
|
|
1323
|
-
logger.debug("[API MACHINE] Received stop-session RPC request");
|
|
1324
|
-
const decryptedParams = decrypt(decodeBase64(data.params), this.secret);
|
|
1325
|
-
const { sessionId } = decryptedParams || {};
|
|
1326
|
-
if (!this.stopSession) {
|
|
1327
|
-
throw new Error("Stop session handler not set");
|
|
1328
|
-
}
|
|
1329
|
-
if (!sessionId) {
|
|
1330
|
-
throw new Error("Session ID is required");
|
|
1331
|
-
}
|
|
1332
|
-
const success = this.stopSession(sessionId);
|
|
1333
|
-
if (!success) {
|
|
1334
|
-
throw new Error("Session not found or failed to stop");
|
|
1335
|
-
}
|
|
1336
|
-
logger.debug(`[API MACHINE] Stopped session ${sessionId}`);
|
|
1337
|
-
const response = { message: "Session stopped" };
|
|
1338
|
-
const encryptedResponse = encodeBase64(encrypt(response, this.secret));
|
|
1339
|
-
callback(encryptedResponse);
|
|
1340
|
-
return;
|
|
1341
|
-
}
|
|
1342
|
-
if (data.method === stopDaemonMethod) {
|
|
1343
|
-
logger.debug("[API MACHINE] Received stop-daemon RPC request");
|
|
1344
|
-
callback(encodeBase64(encrypt({
|
|
1345
|
-
message: "Daemon stop request acknowledged, starting shutdown sequence..."
|
|
1346
|
-
}, this.secret)));
|
|
1347
|
-
setTimeout(() => {
|
|
1348
|
-
logger.debug("[API MACHINE] Initiating daemon shutdown from RPC");
|
|
1349
|
-
if (this.requestShutdown) {
|
|
1350
|
-
this.requestShutdown();
|
|
1351
|
-
}
|
|
1352
|
-
}, 100);
|
|
1353
|
-
return;
|
|
1354
|
-
}
|
|
1355
|
-
throw new Error(`Unknown RPC method: ${data.method}`);
|
|
1356
|
-
} catch (error) {
|
|
1357
|
-
logger.debug(`[API MACHINE] RPC handler failed:`, error.message || error);
|
|
1358
|
-
logger.debug(`[API MACHINE] Error stack:`, error.stack);
|
|
1359
|
-
callback(encodeBase64(encrypt({ error: error.message || String(error) }, this.secret)));
|
|
1360
|
-
}
|
|
1632
|
+
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
1361
1633
|
});
|
|
1362
1634
|
this.socket.on("update", (data) => {
|
|
1363
1635
|
if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
|
|
@@ -1376,16 +1648,6 @@ class ApiMachineClient {
|
|
|
1376
1648
|
logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
|
|
1377
1649
|
}
|
|
1378
1650
|
});
|
|
1379
|
-
this.socket.on("disconnect", () => {
|
|
1380
|
-
logger.debug("[API MACHINE] Disconnected from server");
|
|
1381
|
-
this.stopKeepAlive();
|
|
1382
|
-
});
|
|
1383
|
-
this.socket.io.on("reconnect", () => {
|
|
1384
|
-
logger.debug("[API MACHINE] Reconnected to server");
|
|
1385
|
-
this.socket.emit("rpc-register", { method: spawnMethod });
|
|
1386
|
-
this.socket.emit("rpc-register", { method: stopMethod });
|
|
1387
|
-
this.socket.emit("rpc-register", { method: stopDaemonMethod });
|
|
1388
|
-
});
|
|
1389
1651
|
this.socket.on("connect_error", (error) => {
|
|
1390
1652
|
logger.debug(`[API MACHINE] Connection error: ${error.message}`);
|
|
1391
1653
|
});
|
|
@@ -1632,7 +1894,7 @@ class ApiClient {
|
|
|
1632
1894
|
* Register or update machine with the server
|
|
1633
1895
|
* Returns the current machine state from the server with decrypted metadata and daemonState
|
|
1634
1896
|
*/
|
|
1635
|
-
async
|
|
1897
|
+
async getOrCreateMachine(opts) {
|
|
1636
1898
|
const response = await axios.post(
|
|
1637
1899
|
`${configuration.serverUrl}/v1/machines`,
|
|
1638
1900
|
{
|
|
@@ -1772,6 +2034,7 @@ exports.encodeBase64Url = encodeBase64Url;
|
|
|
1772
2034
|
exports.getLatestDaemonLog = getLatestDaemonLog;
|
|
1773
2035
|
exports.logger = logger;
|
|
1774
2036
|
exports.packageJson = packageJson;
|
|
2037
|
+
exports.projectPath = projectPath;
|
|
1775
2038
|
exports.readCredentials = readCredentials;
|
|
1776
2039
|
exports.readDaemonState = readDaemonState;
|
|
1777
2040
|
exports.readSettings = readSettings;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "happy-coder",
|
|
3
|
-
"version": "0.10.0-
|
|
3
|
+
"version": "0.10.0-1",
|
|
4
4
|
"description": "Claude Code session sharing CLI",
|
|
5
5
|
"author": "Kirill Dubovitskiy",
|
|
6
6
|
"license": "MIT",
|
|
@@ -63,6 +63,8 @@
|
|
|
63
63
|
"@types/http-proxy": "^1.17.16",
|
|
64
64
|
"@types/qrcode-terminal": "^0.12.2",
|
|
65
65
|
"@types/react": "^19.1.9",
|
|
66
|
+
"@types/ps-list": "^6.2.1",
|
|
67
|
+
"@types/cross-spawn": "^6.0.6",
|
|
66
68
|
"axios": "^1.10.0",
|
|
67
69
|
"chalk": "^5.4.1",
|
|
68
70
|
"cross-spawn": "^7.0.6",
|
|
@@ -82,9 +84,7 @@
|
|
|
82
84
|
},
|
|
83
85
|
"devDependencies": {
|
|
84
86
|
"@eslint/compat": "^1",
|
|
85
|
-
"@types/cross-spawn": "^6.0.6",
|
|
86
87
|
"@types/node": ">=20",
|
|
87
|
-
"@types/ps-list": "^6.2.1",
|
|
88
88
|
"cross-env": "^10.0.0",
|
|
89
89
|
"dotenv": "^16.6.1",
|
|
90
90
|
"eslint": "^9",
|