happy-coder 0.1.9 → 0.1.10
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 +635 -60
- package/dist/index.mjs +637 -62
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +257 -457
- package/dist/lib.d.mts +257 -457
- package/dist/lib.mjs +1 -1
- package/dist/{types-mykDX2xe.cjs → types-B2JzqUiU.cjs} +57 -98
- package/dist/{types-fXgEaaqP.mjs → types-DnQGY77F.mjs} +56 -99
- package/package.json +1 -1
- package/dist/install-B2r_gX72.cjs +0 -109
- package/dist/install-HKe7dyS4.mjs +0 -107
- package/dist/run-FBXkmmN7.mjs +0 -32
- package/dist/run-q2To6b-c.cjs +0 -34
- package/dist/uninstall-C42CoSCI.cjs +0 -53
- package/dist/uninstall-CLkTtlMv.mjs +0 -51
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chalk = require('chalk');
|
|
4
|
-
var types = require('./types-
|
|
4
|
+
var types = require('./types-B2JzqUiU.cjs');
|
|
5
5
|
var node_crypto = require('node:crypto');
|
|
6
6
|
var claudeCode = require('@anthropic-ai/claude-code');
|
|
7
7
|
var node_fs = require('node:fs');
|
|
@@ -17,12 +17,18 @@ var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp
|
|
|
17
17
|
var z = require('zod');
|
|
18
18
|
var node_https = require('node:https');
|
|
19
19
|
var net = require('node:net');
|
|
20
|
+
var child_process = require('child_process');
|
|
21
|
+
var util = require('util');
|
|
22
|
+
var promises$1 = require('fs/promises');
|
|
23
|
+
var crypto = require('crypto');
|
|
24
|
+
var path = require('path');
|
|
20
25
|
var tweetnacl = require('tweetnacl');
|
|
21
26
|
var axios = require('axios');
|
|
22
27
|
var qrcode = require('qrcode-terminal');
|
|
23
|
-
require('
|
|
24
|
-
require('
|
|
25
|
-
require('
|
|
28
|
+
var node_events = require('node:events');
|
|
29
|
+
var socket_ioClient = require('socket.io-client');
|
|
30
|
+
var os$1 = require('os');
|
|
31
|
+
var fs = require('fs');
|
|
26
32
|
require('expo-server-sdk');
|
|
27
33
|
|
|
28
34
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
@@ -573,14 +579,14 @@ function createSessionScanner(opts) {
|
|
|
573
579
|
processedMessages.add(key);
|
|
574
580
|
types.logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
|
|
575
581
|
types.logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
|
|
576
|
-
if (parsed.data.type === "user" && typeof parsed.data.message.content === "string") {
|
|
582
|
+
if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
|
|
577
583
|
const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
|
|
578
584
|
if (currentCounter && currentCounter > 0) {
|
|
579
585
|
seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
|
|
580
586
|
continue;
|
|
581
587
|
}
|
|
582
588
|
}
|
|
583
|
-
opts.onMessage(
|
|
589
|
+
opts.onMessage(message);
|
|
584
590
|
} catch (e) {
|
|
585
591
|
continue;
|
|
586
592
|
}
|
|
@@ -883,7 +889,7 @@ class InterruptController {
|
|
|
883
889
|
}
|
|
884
890
|
}
|
|
885
891
|
|
|
886
|
-
var version = "0.1.
|
|
892
|
+
var version = "0.1.10";
|
|
887
893
|
var packageJson = {
|
|
888
894
|
version: version};
|
|
889
895
|
|
|
@@ -1034,12 +1040,235 @@ async function startAnthropicActivityProxy(onClaudeActivity) {
|
|
|
1034
1040
|
};
|
|
1035
1041
|
}
|
|
1036
1042
|
|
|
1043
|
+
const execAsync = util.promisify(child_process.exec);
|
|
1044
|
+
function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
1045
|
+
session.setHandler("abort", async () => {
|
|
1046
|
+
types.logger.info("Abort request - interrupting Claude");
|
|
1047
|
+
await interruptController.interrupt();
|
|
1048
|
+
});
|
|
1049
|
+
if (permissionCallbacks) {
|
|
1050
|
+
session.setHandler("permission", async (message) => {
|
|
1051
|
+
types.logger.info("Permission response" + JSON.stringify(message));
|
|
1052
|
+
const id = message.id;
|
|
1053
|
+
const resolve = permissionCallbacks.requests.get(id);
|
|
1054
|
+
if (resolve) {
|
|
1055
|
+
if (!message.approved) {
|
|
1056
|
+
types.logger.debug("Permission denied, interrupting Claude");
|
|
1057
|
+
await interruptController.interrupt();
|
|
1058
|
+
}
|
|
1059
|
+
resolve({ approved: message.approved, reason: message.reason });
|
|
1060
|
+
permissionCallbacks.requests.delete(id);
|
|
1061
|
+
} else {
|
|
1062
|
+
types.logger.info("Permission request stale, likely timed out");
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
session.updateAgentState((currentState) => {
|
|
1066
|
+
let r = { ...currentState.requests };
|
|
1067
|
+
delete r[id];
|
|
1068
|
+
return {
|
|
1069
|
+
...currentState,
|
|
1070
|
+
requests: r
|
|
1071
|
+
};
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
session.setHandler("bash", async (data) => {
|
|
1076
|
+
types.logger.info("Shell command request:", data.command);
|
|
1077
|
+
try {
|
|
1078
|
+
const options = {
|
|
1079
|
+
cwd: data.cwd,
|
|
1080
|
+
timeout: data.timeout || 3e4
|
|
1081
|
+
// Default 30 seconds timeout
|
|
1082
|
+
};
|
|
1083
|
+
const { stdout, stderr } = await execAsync(data.command, options);
|
|
1084
|
+
return {
|
|
1085
|
+
success: true,
|
|
1086
|
+
stdout: stdout || "",
|
|
1087
|
+
stderr: stderr || "",
|
|
1088
|
+
exitCode: 0
|
|
1089
|
+
};
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
const execError = error;
|
|
1092
|
+
if (execError.code === "ETIMEDOUT" || execError.killed) {
|
|
1093
|
+
return {
|
|
1094
|
+
success: false,
|
|
1095
|
+
stdout: execError.stdout || "",
|
|
1096
|
+
stderr: execError.stderr || "",
|
|
1097
|
+
exitCode: typeof execError.code === "number" ? execError.code : -1,
|
|
1098
|
+
error: "Command timed out"
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
success: false,
|
|
1103
|
+
stdout: execError.stdout || "",
|
|
1104
|
+
stderr: execError.stderr || execError.message || "Command failed",
|
|
1105
|
+
exitCode: typeof execError.code === "number" ? execError.code : 1,
|
|
1106
|
+
error: execError.message || "Command failed"
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
session.setHandler("readFile", async (data) => {
|
|
1111
|
+
types.logger.info("Read file request:", data.path);
|
|
1112
|
+
try {
|
|
1113
|
+
const buffer = await promises$1.readFile(data.path);
|
|
1114
|
+
const content = buffer.toString("base64");
|
|
1115
|
+
return { success: true, content };
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
types.logger.debug("Failed to read file:", error);
|
|
1118
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
session.setHandler("writeFile", async (data) => {
|
|
1122
|
+
types.logger.info("Write file request:", data.path);
|
|
1123
|
+
try {
|
|
1124
|
+
if (data.expectedHash !== null && data.expectedHash !== void 0) {
|
|
1125
|
+
try {
|
|
1126
|
+
const existingBuffer = await promises$1.readFile(data.path);
|
|
1127
|
+
const existingHash = crypto.createHash("sha256").update(existingBuffer).digest("hex");
|
|
1128
|
+
if (existingHash !== data.expectedHash) {
|
|
1129
|
+
return {
|
|
1130
|
+
success: false,
|
|
1131
|
+
error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
const nodeError = error;
|
|
1136
|
+
if (nodeError.code !== "ENOENT") {
|
|
1137
|
+
throw error;
|
|
1138
|
+
}
|
|
1139
|
+
return {
|
|
1140
|
+
success: false,
|
|
1141
|
+
error: "File does not exist but hash was provided"
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1145
|
+
try {
|
|
1146
|
+
await promises$1.stat(data.path);
|
|
1147
|
+
return {
|
|
1148
|
+
success: false,
|
|
1149
|
+
error: "File already exists but was expected to be new"
|
|
1150
|
+
};
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
const nodeError = error;
|
|
1153
|
+
if (nodeError.code !== "ENOENT") {
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
const buffer = Buffer.from(data.content, "base64");
|
|
1159
|
+
await promises$1.writeFile(data.path, buffer);
|
|
1160
|
+
const hash = crypto.createHash("sha256").update(buffer).digest("hex");
|
|
1161
|
+
return { success: true, hash };
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
types.logger.debug("Failed to write file:", error);
|
|
1164
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
session.setHandler("listDirectory", async (data) => {
|
|
1168
|
+
types.logger.info("List directory request:", data.path);
|
|
1169
|
+
try {
|
|
1170
|
+
const entries = await promises$1.readdir(data.path, { withFileTypes: true });
|
|
1171
|
+
const directoryEntries = await Promise.all(
|
|
1172
|
+
entries.map(async (entry) => {
|
|
1173
|
+
const fullPath = path.join(data.path, entry.name);
|
|
1174
|
+
let type = "other";
|
|
1175
|
+
let size;
|
|
1176
|
+
let modified;
|
|
1177
|
+
if (entry.isDirectory()) {
|
|
1178
|
+
type = "directory";
|
|
1179
|
+
} else if (entry.isFile()) {
|
|
1180
|
+
type = "file";
|
|
1181
|
+
}
|
|
1182
|
+
try {
|
|
1183
|
+
const stats = await promises$1.stat(fullPath);
|
|
1184
|
+
size = stats.size;
|
|
1185
|
+
modified = stats.mtime.getTime();
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
types.logger.debug(`Failed to stat ${fullPath}:`, error);
|
|
1188
|
+
}
|
|
1189
|
+
return {
|
|
1190
|
+
name: entry.name,
|
|
1191
|
+
type,
|
|
1192
|
+
size,
|
|
1193
|
+
modified
|
|
1194
|
+
};
|
|
1195
|
+
})
|
|
1196
|
+
);
|
|
1197
|
+
directoryEntries.sort((a, b) => {
|
|
1198
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
1199
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
1200
|
+
return a.name.localeCompare(b.name);
|
|
1201
|
+
});
|
|
1202
|
+
return { success: true, entries: directoryEntries };
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
types.logger.debug("Failed to list directory:", error);
|
|
1205
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
session.setHandler("getDirectoryTree", async (data) => {
|
|
1209
|
+
types.logger.info("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
|
|
1210
|
+
async function buildTree(path$1, name, currentDepth) {
|
|
1211
|
+
try {
|
|
1212
|
+
const stats = await promises$1.stat(path$1);
|
|
1213
|
+
const node = {
|
|
1214
|
+
name,
|
|
1215
|
+
path: path$1,
|
|
1216
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
1217
|
+
size: stats.size,
|
|
1218
|
+
modified: stats.mtime.getTime()
|
|
1219
|
+
};
|
|
1220
|
+
if (stats.isDirectory() && currentDepth < data.maxDepth) {
|
|
1221
|
+
const entries = await promises$1.readdir(path$1, { withFileTypes: true });
|
|
1222
|
+
const children = [];
|
|
1223
|
+
await Promise.all(
|
|
1224
|
+
entries.map(async (entry) => {
|
|
1225
|
+
if (entry.isSymbolicLink()) {
|
|
1226
|
+
types.logger.debug(`Skipping symlink: ${path.join(path$1, entry.name)}`);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
const childPath = path.join(path$1, entry.name);
|
|
1230
|
+
const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
|
|
1231
|
+
if (childNode) {
|
|
1232
|
+
children.push(childNode);
|
|
1233
|
+
}
|
|
1234
|
+
})
|
|
1235
|
+
);
|
|
1236
|
+
children.sort((a, b) => {
|
|
1237
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
1238
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
1239
|
+
return a.name.localeCompare(b.name);
|
|
1240
|
+
});
|
|
1241
|
+
node.children = children;
|
|
1242
|
+
}
|
|
1243
|
+
return node;
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
types.logger.debug(`Failed to process ${path$1}:`, error instanceof Error ? error.message : String(error));
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
try {
|
|
1250
|
+
if (data.maxDepth < 0) {
|
|
1251
|
+
return { success: false, error: "maxDepth must be non-negative" };
|
|
1252
|
+
}
|
|
1253
|
+
const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
|
|
1254
|
+
const tree = await buildTree(data.path, baseName, 0);
|
|
1255
|
+
if (!tree) {
|
|
1256
|
+
return { success: false, error: "Failed to access the specified path" };
|
|
1257
|
+
}
|
|
1258
|
+
return { success: true, tree };
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
types.logger.debug("Failed to get directory tree:", error);
|
|
1261
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1037
1266
|
async function start(credentials, options = {}) {
|
|
1038
1267
|
const workingDirectory = process.cwd();
|
|
1039
1268
|
const sessionTag = node_crypto.randomUUID();
|
|
1040
1269
|
const api = new types.ApiClient(credentials.token, credentials.secret);
|
|
1041
1270
|
let state = {};
|
|
1042
|
-
let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version };
|
|
1271
|
+
let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version, os: os.platform() };
|
|
1043
1272
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
1044
1273
|
types.logger.debug(`Session created: ${response.id}`);
|
|
1045
1274
|
const session = api.session(response);
|
|
@@ -1062,7 +1291,7 @@ async function start(credentials, options = {}) {
|
|
|
1062
1291
|
process.env.HTTPS_PROXY = antropicActivityProxy.url;
|
|
1063
1292
|
types.logger.debug(`[AnthropicProxy] Set HTTP_PROXY and HTTPS_PROXY to ${antropicActivityProxy.url}`);
|
|
1064
1293
|
const logPath = await types.logger.logFilePathPromise;
|
|
1065
|
-
types.logger.
|
|
1294
|
+
types.logger.infoDeveloper(`Session: ${response.id}`);
|
|
1066
1295
|
types.logger.infoDeveloper(`Logs: ${logPath}`);
|
|
1067
1296
|
const interruptController = new InterruptController();
|
|
1068
1297
|
let requests = /* @__PURE__ */ new Map();
|
|
@@ -1116,29 +1345,7 @@ async function start(credentials, options = {}) {
|
|
|
1116
1345
|
promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
|
|
1117
1346
|
return promise;
|
|
1118
1347
|
});
|
|
1119
|
-
session
|
|
1120
|
-
types.logger.info("Permission response" + JSON.stringify(message));
|
|
1121
|
-
const id = message.id;
|
|
1122
|
-
const resolve = requests.get(id);
|
|
1123
|
-
if (resolve) {
|
|
1124
|
-
resolve({ approved: message.approved, reason: message.reason });
|
|
1125
|
-
} else {
|
|
1126
|
-
types.logger.info("Permission request stale, likely timed out");
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
|
-
session.updateAgentState((currentState) => {
|
|
1130
|
-
let r = { ...currentState.requests };
|
|
1131
|
-
delete r[id];
|
|
1132
|
-
return {
|
|
1133
|
-
...currentState,
|
|
1134
|
-
requests: r
|
|
1135
|
-
};
|
|
1136
|
-
});
|
|
1137
|
-
});
|
|
1138
|
-
session.setHandler("abort", async () => {
|
|
1139
|
-
types.logger.info("Abort request - interrupting Claude");
|
|
1140
|
-
await interruptController.interrupt();
|
|
1141
|
-
});
|
|
1348
|
+
registerHandlers(session, interruptController, { requests });
|
|
1142
1349
|
const onAssistantResult = async (result) => {
|
|
1143
1350
|
try {
|
|
1144
1351
|
const summary = "result" in result && result.result ? result.result.substring(0, 100) + (result.result.length > 100 ? "..." : "") : "";
|
|
@@ -1176,12 +1383,32 @@ async function start(credentials, options = {}) {
|
|
|
1176
1383
|
});
|
|
1177
1384
|
clearInterval(pingInterval);
|
|
1178
1385
|
if (antropicActivityProxy) {
|
|
1179
|
-
types.logger.
|
|
1386
|
+
types.logger.debug("[AnthropicProxy] Shutting down thinking activity monitoring proxy");
|
|
1180
1387
|
antropicActivityProxy.cleanup();
|
|
1181
1388
|
}
|
|
1182
1389
|
process.exit(0);
|
|
1183
1390
|
}
|
|
1184
1391
|
|
|
1392
|
+
const defaultSettings = {
|
|
1393
|
+
onboardingCompleted: false
|
|
1394
|
+
};
|
|
1395
|
+
async function readSettings() {
|
|
1396
|
+
if (!node_fs.existsSync(types.configuration.settingsFile)) {
|
|
1397
|
+
return { ...defaultSettings };
|
|
1398
|
+
}
|
|
1399
|
+
try {
|
|
1400
|
+
const content = await promises.readFile(types.configuration.settingsFile, "utf8");
|
|
1401
|
+
return JSON.parse(content);
|
|
1402
|
+
} catch {
|
|
1403
|
+
return { ...defaultSettings };
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
async function writeSettings(settings) {
|
|
1407
|
+
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1408
|
+
await promises.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1409
|
+
}
|
|
1410
|
+
await promises.writeFile(types.configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1411
|
+
}
|
|
1185
1412
|
const credentialsSchema = z__namespace.object({
|
|
1186
1413
|
secret: z__namespace.string().base64(),
|
|
1187
1414
|
token: z__namespace.string()
|
|
@@ -1283,6 +1510,355 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
1283
1510
|
return decrypted;
|
|
1284
1511
|
}
|
|
1285
1512
|
|
|
1513
|
+
class ApiDaemonSession extends node_events.EventEmitter {
|
|
1514
|
+
socket;
|
|
1515
|
+
machineIdentity;
|
|
1516
|
+
keepAliveInterval = null;
|
|
1517
|
+
token;
|
|
1518
|
+
secret;
|
|
1519
|
+
constructor(token, secret, machineIdentity) {
|
|
1520
|
+
super();
|
|
1521
|
+
this.token = token;
|
|
1522
|
+
this.secret = secret;
|
|
1523
|
+
this.machineIdentity = machineIdentity;
|
|
1524
|
+
const socket = socket_ioClient.io(types.configuration.serverUrl, {
|
|
1525
|
+
auth: {
|
|
1526
|
+
token: this.token,
|
|
1527
|
+
clientType: "machine-scoped",
|
|
1528
|
+
machineId: this.machineIdentity.machineId
|
|
1529
|
+
},
|
|
1530
|
+
path: "/v1/user-machine-daemon",
|
|
1531
|
+
reconnection: true,
|
|
1532
|
+
reconnectionAttempts: Infinity,
|
|
1533
|
+
reconnectionDelay: 1e3,
|
|
1534
|
+
reconnectionDelayMax: 5e3,
|
|
1535
|
+
transports: ["websocket"],
|
|
1536
|
+
withCredentials: true,
|
|
1537
|
+
autoConnect: false
|
|
1538
|
+
});
|
|
1539
|
+
socket.on("connect", () => {
|
|
1540
|
+
types.logger.debug("[DAEMON] Connected to server");
|
|
1541
|
+
this.emit("connected");
|
|
1542
|
+
socket.emit("machine-connect", {
|
|
1543
|
+
token: this.token,
|
|
1544
|
+
machineIdentity: types.encodeBase64(types.encrypt(this.machineIdentity, this.secret))
|
|
1545
|
+
});
|
|
1546
|
+
this.startKeepAlive();
|
|
1547
|
+
});
|
|
1548
|
+
socket.on("disconnect", () => {
|
|
1549
|
+
types.logger.debug("[DAEMON] Disconnected from server");
|
|
1550
|
+
this.emit("disconnected");
|
|
1551
|
+
this.stopKeepAlive();
|
|
1552
|
+
});
|
|
1553
|
+
socket.on("spawn-session", async (encryptedData, callback) => {
|
|
1554
|
+
let requestData;
|
|
1555
|
+
try {
|
|
1556
|
+
requestData = types.decrypt(types.decodeBase64(encryptedData), this.secret);
|
|
1557
|
+
types.logger.debug("[DAEMON] Received spawn-session request", requestData);
|
|
1558
|
+
const args = [
|
|
1559
|
+
"--directory",
|
|
1560
|
+
requestData.directory,
|
|
1561
|
+
"--happy-starting-mode",
|
|
1562
|
+
requestData.startingMode
|
|
1563
|
+
];
|
|
1564
|
+
if (requestData.metadata) {
|
|
1565
|
+
args.push("--metadata", requestData.metadata);
|
|
1566
|
+
}
|
|
1567
|
+
if (requestData.startingMode === "interactive" && process.platform === "darwin") {
|
|
1568
|
+
const script = `
|
|
1569
|
+
tell application "Terminal"
|
|
1570
|
+
activate
|
|
1571
|
+
do script "cd ${requestData.directory} && happy ${args.join(" ")}"
|
|
1572
|
+
end tell
|
|
1573
|
+
`;
|
|
1574
|
+
child_process.spawn("osascript", ["-e", script], { detached: true });
|
|
1575
|
+
} else {
|
|
1576
|
+
const child = child_process.spawn("happy", args, {
|
|
1577
|
+
detached: true,
|
|
1578
|
+
stdio: "ignore",
|
|
1579
|
+
cwd: requestData.directory
|
|
1580
|
+
});
|
|
1581
|
+
child.unref();
|
|
1582
|
+
}
|
|
1583
|
+
const result = { success: true };
|
|
1584
|
+
socket.emit("session-spawn-result", {
|
|
1585
|
+
requestId: requestData.requestId,
|
|
1586
|
+
result: types.encodeBase64(types.encrypt(result, this.secret))
|
|
1587
|
+
});
|
|
1588
|
+
callback(types.encodeBase64(types.encrypt({ success: true }, this.secret)));
|
|
1589
|
+
} catch (error) {
|
|
1590
|
+
types.logger.debug("[DAEMON] Failed to spawn session", error);
|
|
1591
|
+
const errorResult = {
|
|
1592
|
+
success: false,
|
|
1593
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1594
|
+
};
|
|
1595
|
+
socket.emit("session-spawn-result", {
|
|
1596
|
+
requestId: requestData?.requestId || "",
|
|
1597
|
+
result: types.encodeBase64(types.encrypt(errorResult, this.secret))
|
|
1598
|
+
});
|
|
1599
|
+
callback(types.encodeBase64(types.encrypt(errorResult, this.secret)));
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
socket.on("daemon-command", (data) => {
|
|
1603
|
+
switch (data.command) {
|
|
1604
|
+
case "shutdown":
|
|
1605
|
+
this.shutdown();
|
|
1606
|
+
break;
|
|
1607
|
+
case "status":
|
|
1608
|
+
this.emit("status-request");
|
|
1609
|
+
break;
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
this.socket = socket;
|
|
1613
|
+
}
|
|
1614
|
+
startKeepAlive() {
|
|
1615
|
+
this.stopKeepAlive();
|
|
1616
|
+
this.keepAliveInterval = setInterval(() => {
|
|
1617
|
+
this.socket.volatile.emit("machine-alive", {
|
|
1618
|
+
time: Date.now()
|
|
1619
|
+
});
|
|
1620
|
+
}, 2e4);
|
|
1621
|
+
}
|
|
1622
|
+
stopKeepAlive() {
|
|
1623
|
+
if (this.keepAliveInterval) {
|
|
1624
|
+
clearInterval(this.keepAliveInterval);
|
|
1625
|
+
this.keepAliveInterval = null;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
connect() {
|
|
1629
|
+
this.socket.connect();
|
|
1630
|
+
}
|
|
1631
|
+
shutdown() {
|
|
1632
|
+
this.stopKeepAlive();
|
|
1633
|
+
this.socket.close();
|
|
1634
|
+
this.emit("shutdown");
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
const DAEMON_PID_FILE = path.join(os$1.homedir(), ".happy", "daemon-pid");
|
|
1639
|
+
async function startDaemon() {
|
|
1640
|
+
if (isDaemonRunning()) {
|
|
1641
|
+
console.log("Happy daemon is already running");
|
|
1642
|
+
process.exit(0);
|
|
1643
|
+
}
|
|
1644
|
+
types.logger.info("Happy CLI daemon started successfully");
|
|
1645
|
+
writePidFile();
|
|
1646
|
+
process.on("SIGINT", stopDaemon);
|
|
1647
|
+
process.on("SIGTERM", stopDaemon);
|
|
1648
|
+
process.on("exit", stopDaemon);
|
|
1649
|
+
try {
|
|
1650
|
+
const settings = await readSettings() || { onboardingCompleted: false };
|
|
1651
|
+
if (!settings.machineId) {
|
|
1652
|
+
settings.machineId = crypto.randomUUID();
|
|
1653
|
+
settings.machineHost = os$1.hostname();
|
|
1654
|
+
await writeSettings(settings);
|
|
1655
|
+
}
|
|
1656
|
+
const machineIdentity = {
|
|
1657
|
+
machineId: settings.machineId,
|
|
1658
|
+
machineHost: settings.machineHost || os$1.hostname(),
|
|
1659
|
+
platform: process.platform,
|
|
1660
|
+
version: process.env.npm_package_version || "unknown"
|
|
1661
|
+
};
|
|
1662
|
+
let credentials = await readCredentials();
|
|
1663
|
+
if (!credentials) {
|
|
1664
|
+
types.logger.debug("[DAEMON] No credentials found, running auth");
|
|
1665
|
+
await doAuth();
|
|
1666
|
+
credentials = await readCredentials();
|
|
1667
|
+
if (!credentials) {
|
|
1668
|
+
throw new Error("Failed to authenticate");
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
const { token, secret } = credentials;
|
|
1672
|
+
const daemon = new ApiDaemonSession(token, secret, machineIdentity);
|
|
1673
|
+
daemon.on("connected", () => {
|
|
1674
|
+
types.logger.debug("[DAEMON] Successfully connected to server");
|
|
1675
|
+
});
|
|
1676
|
+
daemon.on("disconnected", () => {
|
|
1677
|
+
types.logger.debug("[DAEMON] Disconnected from server");
|
|
1678
|
+
});
|
|
1679
|
+
daemon.on("shutdown", () => {
|
|
1680
|
+
types.logger.debug("[DAEMON] Shutdown requested");
|
|
1681
|
+
stopDaemon();
|
|
1682
|
+
process.exit(0);
|
|
1683
|
+
});
|
|
1684
|
+
daemon.connect();
|
|
1685
|
+
setInterval(() => {
|
|
1686
|
+
}, 1e3);
|
|
1687
|
+
} catch (error) {
|
|
1688
|
+
types.logger.debug("[DAEMON] Failed to start daemon", error);
|
|
1689
|
+
stopDaemon();
|
|
1690
|
+
process.exit(1);
|
|
1691
|
+
}
|
|
1692
|
+
process.on("SIGINT", () => process.exit(0));
|
|
1693
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
1694
|
+
process.on("exit", () => process.exit(0));
|
|
1695
|
+
while (true) {
|
|
1696
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function isDaemonRunning() {
|
|
1700
|
+
try {
|
|
1701
|
+
if (!fs.existsSync(DAEMON_PID_FILE)) {
|
|
1702
|
+
console.log("No PID file found");
|
|
1703
|
+
return false;
|
|
1704
|
+
}
|
|
1705
|
+
const pid = parseInt(fs.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
1706
|
+
try {
|
|
1707
|
+
process.kill(pid, 0);
|
|
1708
|
+
return true;
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
console.log("Process not running", error);
|
|
1711
|
+
fs.unlinkSync(DAEMON_PID_FILE);
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
} catch {
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
function writePidFile() {
|
|
1719
|
+
const happyDir = path.join(os$1.homedir(), ".happy");
|
|
1720
|
+
if (!fs.existsSync(happyDir)) {
|
|
1721
|
+
fs.mkdirSync(happyDir, { recursive: true });
|
|
1722
|
+
}
|
|
1723
|
+
fs.writeFileSync(DAEMON_PID_FILE, process.pid.toString());
|
|
1724
|
+
}
|
|
1725
|
+
function stopDaemon() {
|
|
1726
|
+
try {
|
|
1727
|
+
if (fs.existsSync(DAEMON_PID_FILE)) {
|
|
1728
|
+
types.logger.debug("[DAEMON] Stopping daemon");
|
|
1729
|
+
process.kill(parseInt(fs.readFileSync(DAEMON_PID_FILE, "utf-8")), "SIGTERM");
|
|
1730
|
+
fs.unlinkSync(DAEMON_PID_FILE);
|
|
1731
|
+
}
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
types.logger.debug("[DAEMON] Error cleaning up PID file", error);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function trimIdent(text) {
|
|
1738
|
+
const lines = text.split("\n");
|
|
1739
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
1740
|
+
lines.shift();
|
|
1741
|
+
}
|
|
1742
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
1743
|
+
lines.pop();
|
|
1744
|
+
}
|
|
1745
|
+
const minSpaces = lines.reduce((min, line) => {
|
|
1746
|
+
if (line.trim() === "") {
|
|
1747
|
+
return min;
|
|
1748
|
+
}
|
|
1749
|
+
const leadingSpaces = line.match(/^\s*/)[0].length;
|
|
1750
|
+
return Math.min(min, leadingSpaces);
|
|
1751
|
+
}, Infinity);
|
|
1752
|
+
const trimmedLines = lines.map((line) => line.slice(minSpaces));
|
|
1753
|
+
return trimmedLines.join("\n");
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
const PLIST_LABEL$1 = "com.happy-cli.daemon";
|
|
1757
|
+
const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
|
|
1758
|
+
const USER_HOME = process.env.HOME || process.env.USERPROFILE;
|
|
1759
|
+
async function install$1() {
|
|
1760
|
+
try {
|
|
1761
|
+
if (fs.existsSync(PLIST_FILE$1)) {
|
|
1762
|
+
types.logger.info("Daemon plist already exists. Uninstalling first...");
|
|
1763
|
+
child_process.execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
1764
|
+
}
|
|
1765
|
+
const happyPath = process.argv[0];
|
|
1766
|
+
const scriptPath = process.argv[1];
|
|
1767
|
+
const plistContent = trimIdent(`
|
|
1768
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
1769
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1770
|
+
<plist version="1.0">
|
|
1771
|
+
<dict>
|
|
1772
|
+
<key>Label</key>
|
|
1773
|
+
<string>${PLIST_LABEL$1}</string>
|
|
1774
|
+
|
|
1775
|
+
<key>ProgramArguments</key>
|
|
1776
|
+
<array>
|
|
1777
|
+
<string>${happyPath}</string>
|
|
1778
|
+
<string>${scriptPath}</string>
|
|
1779
|
+
<string>happy-daemon</string>
|
|
1780
|
+
</array>
|
|
1781
|
+
|
|
1782
|
+
<key>EnvironmentVariables</key>
|
|
1783
|
+
<dict>
|
|
1784
|
+
<key>HAPPY_DAEMON_MODE</key>
|
|
1785
|
+
<string>true</string>
|
|
1786
|
+
</dict>
|
|
1787
|
+
|
|
1788
|
+
<key>RunAtLoad</key>
|
|
1789
|
+
<true/>
|
|
1790
|
+
|
|
1791
|
+
<key>KeepAlive</key>
|
|
1792
|
+
<true/>
|
|
1793
|
+
|
|
1794
|
+
<key>StandardErrorPath</key>
|
|
1795
|
+
<string>${USER_HOME}/.happy/daemon.err</string>
|
|
1796
|
+
|
|
1797
|
+
<key>StandardOutPath</key>
|
|
1798
|
+
<string>${USER_HOME}/.happy/daemon.log</string>
|
|
1799
|
+
|
|
1800
|
+
<key>WorkingDirectory</key>
|
|
1801
|
+
<string>/tmp</string>
|
|
1802
|
+
</dict>
|
|
1803
|
+
</plist>
|
|
1804
|
+
`);
|
|
1805
|
+
fs.writeFileSync(PLIST_FILE$1, plistContent);
|
|
1806
|
+
fs.chmodSync(PLIST_FILE$1, 420);
|
|
1807
|
+
types.logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
|
|
1808
|
+
child_process.execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
1809
|
+
types.logger.info("Daemon installed and started successfully");
|
|
1810
|
+
types.logger.info("Check logs at ~/.happy/daemon.log");
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
types.logger.debug("Failed to install daemon:", error);
|
|
1813
|
+
throw error;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
async function install() {
|
|
1818
|
+
if (process.platform !== "darwin") {
|
|
1819
|
+
throw new Error("Daemon installation is currently only supported on macOS");
|
|
1820
|
+
}
|
|
1821
|
+
if (process.getuid && process.getuid() !== 0) {
|
|
1822
|
+
throw new Error("Daemon installation requires sudo privileges. Please run with sudo.");
|
|
1823
|
+
}
|
|
1824
|
+
types.logger.info("Installing Happy CLI daemon for macOS...");
|
|
1825
|
+
await install$1();
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
const PLIST_LABEL = "com.happy-cli.daemon";
|
|
1829
|
+
const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
|
|
1830
|
+
async function uninstall$1() {
|
|
1831
|
+
try {
|
|
1832
|
+
if (!fs.existsSync(PLIST_FILE)) {
|
|
1833
|
+
types.logger.info("Daemon plist not found. Nothing to uninstall.");
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
try {
|
|
1837
|
+
child_process.execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
|
|
1838
|
+
types.logger.info("Daemon stopped successfully");
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
types.logger.info("Failed to unload daemon (it might not be running)");
|
|
1841
|
+
}
|
|
1842
|
+
fs.unlinkSync(PLIST_FILE);
|
|
1843
|
+
types.logger.info(`Removed daemon plist from ${PLIST_FILE}`);
|
|
1844
|
+
types.logger.info("Daemon uninstalled successfully");
|
|
1845
|
+
} catch (error) {
|
|
1846
|
+
types.logger.debug("Failed to uninstall daemon:", error);
|
|
1847
|
+
throw error;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
async function uninstall() {
|
|
1852
|
+
if (process.platform !== "darwin") {
|
|
1853
|
+
throw new Error("Daemon uninstallation is currently only supported on macOS");
|
|
1854
|
+
}
|
|
1855
|
+
if (process.getuid && process.getuid() !== 0) {
|
|
1856
|
+
throw new Error("Daemon uninstallation requires sudo privileges. Please run with sudo.");
|
|
1857
|
+
}
|
|
1858
|
+
types.logger.info("Uninstalling Happy CLI daemon for macOS...");
|
|
1859
|
+
await uninstall$1();
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1286
1862
|
(async () => {
|
|
1287
1863
|
const args = process.argv.slice(2);
|
|
1288
1864
|
let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
|
|
@@ -1302,39 +1878,40 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
1302
1878
|
}
|
|
1303
1879
|
return;
|
|
1304
1880
|
} else if (subcommand === "daemon") {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
await
|
|
1881
|
+
const daemonSubcommand = args[1];
|
|
1882
|
+
if (daemonSubcommand === "start") {
|
|
1883
|
+
await startDaemon();
|
|
1884
|
+
process.exit(0);
|
|
1885
|
+
} else if (daemonSubcommand === "stop") {
|
|
1886
|
+
await stopDaemon();
|
|
1887
|
+
process.exit(0);
|
|
1888
|
+
} else if (daemonSubcommand === "install") {
|
|
1889
|
+
try {
|
|
1890
|
+
await install();
|
|
1891
|
+
} catch (error) {
|
|
1892
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1893
|
+
process.exit(1);
|
|
1894
|
+
}
|
|
1895
|
+
} else if (daemonSubcommand === "uninstall") {
|
|
1896
|
+
try {
|
|
1897
|
+
await uninstall();
|
|
1898
|
+
} catch (error) {
|
|
1899
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1900
|
+
process.exit(1);
|
|
1901
|
+
}
|
|
1308
1902
|
} else {
|
|
1309
|
-
|
|
1310
|
-
if (daemonSubcommand === "install") {
|
|
1311
|
-
const { install } = await Promise.resolve().then(function () { return require('./install-B2r_gX72.cjs'); });
|
|
1312
|
-
try {
|
|
1313
|
-
await install();
|
|
1314
|
-
} catch (error) {
|
|
1315
|
-
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1316
|
-
process.exit(1);
|
|
1317
|
-
}
|
|
1318
|
-
} else if (daemonSubcommand === "uninstall") {
|
|
1319
|
-
const { uninstall } = await Promise.resolve().then(function () { return require('./uninstall-C42CoSCI.cjs'); });
|
|
1320
|
-
try {
|
|
1321
|
-
await uninstall();
|
|
1322
|
-
} catch (error) {
|
|
1323
|
-
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1324
|
-
process.exit(1);
|
|
1325
|
-
}
|
|
1326
|
-
} else {
|
|
1327
|
-
console.log(`
|
|
1903
|
+
console.log(`
|
|
1328
1904
|
${chalk.bold("happy daemon")} - Daemon management
|
|
1329
1905
|
|
|
1330
1906
|
${chalk.bold("Usage:")}
|
|
1907
|
+
happy daemon start Start the daemon
|
|
1908
|
+
happy daemon stop Stop the daemon
|
|
1331
1909
|
sudo happy daemon install Install the daemon (requires sudo)
|
|
1332
1910
|
sudo happy daemon uninstall Uninstall the daemon (requires sudo)
|
|
1333
1911
|
|
|
1334
1912
|
${chalk.bold("Note:")} The daemon runs in the background and provides persistent services.
|
|
1335
1913
|
Currently only supported on macOS.
|
|
1336
1914
|
`);
|
|
1337
|
-
}
|
|
1338
1915
|
}
|
|
1339
1916
|
return;
|
|
1340
1917
|
} else {
|
|
@@ -1370,7 +1947,6 @@ ${chalk.bold("happy")} - Claude Code session sharing
|
|
|
1370
1947
|
${chalk.bold("Usage:")}
|
|
1371
1948
|
happy [options]
|
|
1372
1949
|
happy logout Logs out of your account and removes data directory
|
|
1373
|
-
happy daemon Manage the background daemon (macOS only)
|
|
1374
1950
|
|
|
1375
1951
|
${chalk.bold("Options:")}
|
|
1376
1952
|
-h, --help Show this help message
|
|
@@ -1383,9 +1959,8 @@ ${chalk.bold("Options:")}
|
|
|
1383
1959
|
--local < global | local >
|
|
1384
1960
|
Will use .happy folder in the current directory for storing your private key and debug logs.
|
|
1385
1961
|
You will require re-login each time you run this in a new directory.
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
Default: interactive
|
|
1962
|
+
--happy-starting-mode <interactive|remote>
|
|
1963
|
+
Set the starting mode for new sessions (default: remote)
|
|
1389
1964
|
|
|
1390
1965
|
${chalk.bold("Examples:")}
|
|
1391
1966
|
happy Start a session with default settings
|