happy-coder 0.1.13 → 0.1.14
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 +440 -297
- package/dist/index.mjs +442 -299
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +7 -2
- package/dist/lib.d.mts +7 -2
- package/dist/lib.mjs +1 -1
- package/dist/types-Cg4664gs.cjs +879 -0
- package/dist/types-DD9P_5rj.mjs +868 -0
- package/package.json +3 -3
- package/scripts/claudeInteractiveLaunch.cjs +72 -13
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { l as logger, d as delay, e as backoff, R as RawJSONLinesSchema,
|
|
2
|
+
import { l as logger, d as delay, e as backoff, R as RawJSONLinesSchema, c as configuration, f as encodeBase64, A as ApiClient, g as encodeBase64Url, h as decodeBase64, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-DD9P_5rj.mjs';
|
|
3
3
|
import { randomUUID, randomBytes } from 'node:crypto';
|
|
4
4
|
import { query, AbortError } from '@anthropic-ai/claude-code';
|
|
5
5
|
import { existsSync, readFileSync, mkdirSync, watch, rmSync } from 'node:fs';
|
|
@@ -11,7 +11,7 @@ import { createInterface } from 'node:readline';
|
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { readFile, mkdir, writeFile as writeFile$1 } from 'node:fs/promises';
|
|
13
13
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
14
|
-
import { createServer
|
|
14
|
+
import { createServer } from 'node:http';
|
|
15
15
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
16
16
|
import * as z from 'zod';
|
|
17
17
|
import { z as z$1 } from 'zod';
|
|
@@ -20,14 +20,13 @@ import { promisify } from 'util';
|
|
|
20
20
|
import crypto, { createHash } from 'crypto';
|
|
21
21
|
import { dirname as dirname$1, join as join$1 } from 'path';
|
|
22
22
|
import { fileURLToPath as fileURLToPath$1 } from 'url';
|
|
23
|
-
import httpProxy from 'http-proxy';
|
|
24
23
|
import tweetnacl from 'tweetnacl';
|
|
25
24
|
import axios from 'axios';
|
|
26
25
|
import qrcode from 'qrcode-terminal';
|
|
27
26
|
import { EventEmitter } from 'node:events';
|
|
28
27
|
import { io } from 'socket.io-client';
|
|
29
28
|
import { hostname, homedir as homedir$1 } from 'os';
|
|
30
|
-
import { existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync, mkdirSync as mkdirSync$1, writeFileSync, chmodSync } from 'fs';
|
|
29
|
+
import { closeSync, existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync, mkdirSync as mkdirSync$1, openSync, writeSync, writeFileSync, chmodSync } from 'fs';
|
|
31
30
|
import 'expo-server-sdk';
|
|
32
31
|
|
|
33
32
|
function formatClaudeMessage(message, onAssistantResult) {
|
|
@@ -240,18 +239,32 @@ async function claudeRemote(opts) {
|
|
|
240
239
|
});
|
|
241
240
|
}
|
|
242
241
|
printDivider();
|
|
242
|
+
let thinking = false;
|
|
243
|
+
const updateThinking = (newThinking) => {
|
|
244
|
+
if (thinking !== newThinking) {
|
|
245
|
+
thinking = newThinking;
|
|
246
|
+
logger.debug(`[claudeRemote] Thinking state changed to: ${thinking}`);
|
|
247
|
+
if (opts.onThinkingChange) {
|
|
248
|
+
opts.onThinkingChange(thinking);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
243
252
|
try {
|
|
244
253
|
logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
245
254
|
for await (const message of response) {
|
|
246
255
|
logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
|
|
247
256
|
formatClaudeMessage(message, opts.onAssistantResult);
|
|
248
257
|
if (message.type === "system" && message.subtype === "init") {
|
|
258
|
+
updateThinking(true);
|
|
249
259
|
logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${message.session_id}`);
|
|
250
260
|
const projectDir = getProjectPath(opts.path);
|
|
251
261
|
const found = await awaitFileExist(join(projectDir, `${message.session_id}.jsonl`));
|
|
252
262
|
logger.debug(`[claudeRemote] Session file found: ${message.session_id} ${found}`);
|
|
253
263
|
opts.onSessionFound(message.session_id);
|
|
254
264
|
}
|
|
265
|
+
if (message.type === "result") {
|
|
266
|
+
updateThinking(false);
|
|
267
|
+
}
|
|
255
268
|
}
|
|
256
269
|
logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
257
270
|
} catch (e) {
|
|
@@ -264,6 +277,7 @@ async function claudeRemote(opts) {
|
|
|
264
277
|
throw e;
|
|
265
278
|
}
|
|
266
279
|
} finally {
|
|
280
|
+
updateThinking(false);
|
|
267
281
|
if (opts.interruptController) {
|
|
268
282
|
opts.interruptController.unregister();
|
|
269
283
|
}
|
|
@@ -327,22 +341,70 @@ async function claudeLocal(opts) {
|
|
|
327
341
|
input: child.stdio[3],
|
|
328
342
|
crlfDelay: Infinity
|
|
329
343
|
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
344
|
+
const activeFetches = /* @__PURE__ */ new Map();
|
|
345
|
+
let thinking = false;
|
|
346
|
+
let stopThinkingTimeout = null;
|
|
347
|
+
const updateThinking = (newThinking) => {
|
|
348
|
+
if (thinking !== newThinking) {
|
|
349
|
+
thinking = newThinking;
|
|
350
|
+
logger.debug(`[ClaudeLocal] Thinking state changed to: ${thinking}`);
|
|
351
|
+
if (opts.onThinkingChange) {
|
|
352
|
+
opts.onThinkingChange(thinking);
|
|
336
353
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
rl.on("line", (line) => {
|
|
357
|
+
try {
|
|
358
|
+
const message = JSON.parse(line);
|
|
359
|
+
switch (message.type) {
|
|
360
|
+
case "uuid":
|
|
361
|
+
detectedIdsRandomUUID.add(message.value);
|
|
362
|
+
if (!resolvedSessionId && detectedIdsFileSystem.has(message.value)) {
|
|
363
|
+
resolvedSessionId = message.value;
|
|
364
|
+
opts.onSessionFound(message.value);
|
|
365
|
+
}
|
|
366
|
+
break;
|
|
367
|
+
case "fetch-start":
|
|
368
|
+
logger.debug(`[ClaudeLocal] Fetch start: ${message.method} ${message.hostname}${message.path} (id: ${message.id})`);
|
|
369
|
+
activeFetches.set(message.id, {
|
|
370
|
+
hostname: message.hostname,
|
|
371
|
+
path: message.path,
|
|
372
|
+
startTime: message.timestamp
|
|
373
|
+
});
|
|
374
|
+
if (stopThinkingTimeout) {
|
|
375
|
+
clearTimeout(stopThinkingTimeout);
|
|
376
|
+
stopThinkingTimeout = null;
|
|
377
|
+
}
|
|
378
|
+
updateThinking(true);
|
|
379
|
+
break;
|
|
380
|
+
case "fetch-end":
|
|
381
|
+
logger.debug(`[ClaudeLocal] Fetch end: id ${message.id}`);
|
|
382
|
+
activeFetches.delete(message.id);
|
|
383
|
+
if (activeFetches.size === 0 && thinking && !stopThinkingTimeout) {
|
|
384
|
+
stopThinkingTimeout = setTimeout(() => {
|
|
385
|
+
if (activeFetches.size === 0) {
|
|
386
|
+
updateThinking(false);
|
|
387
|
+
}
|
|
388
|
+
stopThinkingTimeout = null;
|
|
389
|
+
}, 500);
|
|
390
|
+
}
|
|
391
|
+
break;
|
|
392
|
+
default:
|
|
393
|
+
logger.debug(`[ClaudeLocal] Unknown message type: ${message.type}`);
|
|
340
394
|
}
|
|
395
|
+
} catch (e) {
|
|
396
|
+
logger.debug(`[ClaudeLocal] Non-JSON line from fd3: ${line}`);
|
|
341
397
|
}
|
|
342
398
|
});
|
|
343
399
|
rl.on("error", (err) => {
|
|
344
400
|
console.error("Error reading from fd 3:", err);
|
|
345
401
|
});
|
|
402
|
+
child.on("exit", () => {
|
|
403
|
+
if (stopThinkingTimeout) {
|
|
404
|
+
clearTimeout(stopThinkingTimeout);
|
|
405
|
+
}
|
|
406
|
+
updateThinking(false);
|
|
407
|
+
});
|
|
346
408
|
}
|
|
347
409
|
child.on("error", (error) => {
|
|
348
410
|
});
|
|
@@ -798,6 +860,7 @@ async function loop(opts) {
|
|
|
798
860
|
path: opts.path,
|
|
799
861
|
sessionId,
|
|
800
862
|
onSessionFound,
|
|
863
|
+
onThinkingChange: opts.onThinkingChange,
|
|
801
864
|
abort: interactiveAbortController.signal,
|
|
802
865
|
claudeEnvVars: opts.claudeEnvVars,
|
|
803
866
|
claudeArgs: opts.claudeArgs
|
|
@@ -860,6 +923,7 @@ async function loop(opts) {
|
|
|
860
923
|
mcpServers: opts.mcpServers,
|
|
861
924
|
permissionPromptToolName: opts.permissionPromptToolName,
|
|
862
925
|
onSessionFound,
|
|
926
|
+
onThinkingChange: opts.onThinkingChange,
|
|
863
927
|
messages: currentMessageQueue,
|
|
864
928
|
onAssistantResult: opts.onAssistantResult,
|
|
865
929
|
interruptController: opts.interruptController,
|
|
@@ -988,7 +1052,7 @@ class InterruptController {
|
|
|
988
1052
|
}
|
|
989
1053
|
}
|
|
990
1054
|
|
|
991
|
-
var version = "0.1.
|
|
1055
|
+
var version = "0.1.14";
|
|
992
1056
|
var packageJson = {
|
|
993
1057
|
version: version};
|
|
994
1058
|
|
|
@@ -1273,148 +1337,77 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
|
|
|
1273
1337
|
});
|
|
1274
1338
|
}
|
|
1275
1339
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
proxy.on("proxyReq", (proxyReq, req, res) => {
|
|
1290
|
-
if (options.onRequest) {
|
|
1291
|
-
options.onRequest(req, proxyReq);
|
|
1292
|
-
}
|
|
1293
|
-
});
|
|
1294
|
-
proxy.on("proxyRes", (proxyRes, req, res) => {
|
|
1295
|
-
if (options.onResponse) {
|
|
1296
|
-
options.onResponse(req, proxyRes);
|
|
1297
|
-
}
|
|
1298
|
-
});
|
|
1299
|
-
const server = createServer((req, res) => {
|
|
1300
|
-
proxy.web(req, res);
|
|
1301
|
-
});
|
|
1302
|
-
const url = await new Promise((resolve, reject) => {
|
|
1303
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1304
|
-
const addr = server.address();
|
|
1305
|
-
if (addr && typeof addr === "object") {
|
|
1306
|
-
const proxyUrl = `http://127.0.0.1:${addr.port}`;
|
|
1307
|
-
logger.debug(`[HTTPProxy] Started on ${proxyUrl} --> ${options.target}`);
|
|
1308
|
-
resolve(proxyUrl);
|
|
1309
|
-
} else {
|
|
1310
|
-
reject(new Error("Failed to get server address"));
|
|
1311
|
-
}
|
|
1312
|
-
});
|
|
1313
|
-
});
|
|
1314
|
-
return url;
|
|
1340
|
+
const defaultSettings = {
|
|
1341
|
+
onboardingCompleted: false
|
|
1342
|
+
};
|
|
1343
|
+
async function readSettings() {
|
|
1344
|
+
if (!existsSync(configuration.settingsFile)) {
|
|
1345
|
+
return { ...defaultSettings };
|
|
1346
|
+
}
|
|
1347
|
+
try {
|
|
1348
|
+
const content = await readFile(configuration.settingsFile, "utf8");
|
|
1349
|
+
return JSON.parse(content);
|
|
1350
|
+
} catch {
|
|
1351
|
+
return { ...defaultSettings };
|
|
1352
|
+
}
|
|
1315
1353
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
activeRequests.set(requestId, timeout);
|
|
1350
|
-
if (!isThinking) {
|
|
1351
|
-
isThinking = true;
|
|
1352
|
-
onThinking(true);
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
},
|
|
1356
|
-
onResponse: (req, proxyRes) => {
|
|
1357
|
-
proxyRes.headers["connection"] = "close";
|
|
1358
|
-
if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
|
|
1359
|
-
const requestId = req._requestId;
|
|
1360
|
-
const timeout = activeRequests.get(requestId);
|
|
1361
|
-
if (timeout) {
|
|
1362
|
-
clearTimeout(timeout);
|
|
1363
|
-
}
|
|
1364
|
-
let cleaned = false;
|
|
1365
|
-
const cleanupRequest = () => {
|
|
1366
|
-
if (!cleaned) {
|
|
1367
|
-
cleaned = true;
|
|
1368
|
-
activeRequests.delete(requestId);
|
|
1369
|
-
checkAndStopThinking();
|
|
1370
|
-
}
|
|
1371
|
-
};
|
|
1372
|
-
proxyRes.on("end", () => {
|
|
1373
|
-
cleanupRequest();
|
|
1374
|
-
});
|
|
1375
|
-
proxyRes.on("error", (err) => {
|
|
1376
|
-
cleanupRequest();
|
|
1377
|
-
});
|
|
1378
|
-
proxyRes.on("aborted", () => {
|
|
1379
|
-
cleanupRequest();
|
|
1380
|
-
});
|
|
1381
|
-
proxyRes.on("close", () => {
|
|
1382
|
-
cleanupRequest();
|
|
1383
|
-
});
|
|
1384
|
-
req.on("close", () => {
|
|
1385
|
-
cleanupRequest();
|
|
1386
|
-
});
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
});
|
|
1390
|
-
const reset = () => {
|
|
1391
|
-
for (const [requestId, timeout] of activeRequests) {
|
|
1392
|
-
clearTimeout(timeout);
|
|
1393
|
-
}
|
|
1394
|
-
activeRequests.clear();
|
|
1395
|
-
if (stopThinkingTimeout) {
|
|
1396
|
-
clearTimeout(stopThinkingTimeout);
|
|
1397
|
-
stopThinkingTimeout = null;
|
|
1398
|
-
}
|
|
1399
|
-
if (isThinking) {
|
|
1400
|
-
isThinking = false;
|
|
1401
|
-
onThinking(false);
|
|
1402
|
-
}
|
|
1403
|
-
};
|
|
1404
|
-
return {
|
|
1405
|
-
proxyUrl,
|
|
1406
|
-
reset
|
|
1407
|
-
};
|
|
1354
|
+
async function writeSettings(settings) {
|
|
1355
|
+
if (!existsSync(configuration.happyDir)) {
|
|
1356
|
+
await mkdir(configuration.happyDir, { recursive: true });
|
|
1357
|
+
}
|
|
1358
|
+
await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1359
|
+
}
|
|
1360
|
+
const credentialsSchema = z.object({
|
|
1361
|
+
secret: z.string().base64(),
|
|
1362
|
+
token: z.string()
|
|
1363
|
+
});
|
|
1364
|
+
async function readCredentials() {
|
|
1365
|
+
if (!existsSync(configuration.privateKeyFile)) {
|
|
1366
|
+
return null;
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
|
|
1370
|
+
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
1371
|
+
return {
|
|
1372
|
+
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
1373
|
+
token: credentials.token
|
|
1374
|
+
};
|
|
1375
|
+
} catch {
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
async function writeCredentials(credentials) {
|
|
1380
|
+
if (!existsSync(configuration.happyDir)) {
|
|
1381
|
+
await mkdir(configuration.happyDir, { recursive: true });
|
|
1382
|
+
}
|
|
1383
|
+
await writeFile$1(configuration.privateKeyFile, JSON.stringify({
|
|
1384
|
+
secret: encodeBase64(credentials.secret),
|
|
1385
|
+
token: credentials.token
|
|
1386
|
+
}, null, 2));
|
|
1408
1387
|
}
|
|
1409
1388
|
|
|
1410
1389
|
async function start(credentials, options = {}) {
|
|
1411
1390
|
const workingDirectory = process.cwd();
|
|
1412
1391
|
const sessionTag = randomUUID();
|
|
1392
|
+
if (options.daemonSpawn && options.startingMode === "local") {
|
|
1393
|
+
logger.debug("Daemon spawn requested with local mode - forcing remote mode");
|
|
1394
|
+
options.startingMode = "remote";
|
|
1395
|
+
}
|
|
1413
1396
|
const api = new ApiClient(credentials.token, credentials.secret);
|
|
1414
1397
|
let state = {};
|
|
1415
|
-
|
|
1398
|
+
const settings = await readSettings() || { };
|
|
1399
|
+
let metadata = {
|
|
1400
|
+
path: workingDirectory,
|
|
1401
|
+
host: os.hostname(),
|
|
1402
|
+
version: packageJson.version,
|
|
1403
|
+
os: os.platform(),
|
|
1404
|
+
machineId: settings.machineId
|
|
1405
|
+
};
|
|
1416
1406
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
1417
1407
|
logger.debug(`Session created: ${response.id}`);
|
|
1408
|
+
if (options.daemonSpawn) {
|
|
1409
|
+
console.log(`daemon:sessionIdCreated:${response.id}`);
|
|
1410
|
+
}
|
|
1418
1411
|
const session = api.session(response);
|
|
1419
1412
|
const pushClient = api.push();
|
|
1420
1413
|
let thinking = false;
|
|
@@ -1422,11 +1415,6 @@ async function start(credentials, options = {}) {
|
|
|
1422
1415
|
let pingInterval = setInterval(() => {
|
|
1423
1416
|
session.keepAlive(thinking, mode);
|
|
1424
1417
|
}, 2e3);
|
|
1425
|
-
const activityTracker = await startClaudeActivityTracker((newThinking) => {
|
|
1426
|
-
thinking = newThinking;
|
|
1427
|
-
session.keepAlive(thinking, mode);
|
|
1428
|
-
});
|
|
1429
|
-
process.env.ANTHROPIC_BASE_URL = activityTracker.proxyUrl;
|
|
1430
1418
|
const logPath = await logger.logFilePathPromise;
|
|
1431
1419
|
logger.infoDeveloper(`Session: ${response.id}`);
|
|
1432
1420
|
logger.infoDeveloper(`Logs: ${logPath}`);
|
|
@@ -1558,7 +1546,6 @@ async function start(credentials, options = {}) {
|
|
|
1558
1546
|
},
|
|
1559
1547
|
onProcessStart: (processMode) => {
|
|
1560
1548
|
logger.debug(`[Process Lifecycle] Starting ${processMode} mode`);
|
|
1561
|
-
activityTracker.reset();
|
|
1562
1549
|
logger.debug("Starting process - clearing any stale permission requests");
|
|
1563
1550
|
for (const [id, resolve] of requests) {
|
|
1564
1551
|
logger.debug(`Rejecting stale permission request: ${id}`);
|
|
@@ -1568,13 +1555,14 @@ async function start(credentials, options = {}) {
|
|
|
1568
1555
|
},
|
|
1569
1556
|
onProcessStop: (processMode) => {
|
|
1570
1557
|
logger.debug(`[Process Lifecycle] Stopped ${processMode} mode`);
|
|
1571
|
-
activityTracker.reset();
|
|
1572
1558
|
logger.debug("Stopping process - clearing any stale permission requests");
|
|
1573
1559
|
for (const [id, resolve] of requests) {
|
|
1574
1560
|
logger.debug(`Rejecting stale permission request: ${id}`);
|
|
1575
1561
|
resolve({ approved: false, reason: "Process restarted" });
|
|
1576
1562
|
}
|
|
1577
1563
|
requests.clear();
|
|
1564
|
+
thinking = false;
|
|
1565
|
+
session.keepAlive(thinking, mode);
|
|
1578
1566
|
},
|
|
1579
1567
|
mcpServers: {
|
|
1580
1568
|
"permission": {
|
|
@@ -1587,61 +1575,16 @@ async function start(credentials, options = {}) {
|
|
|
1587
1575
|
onAssistantResult,
|
|
1588
1576
|
interruptController,
|
|
1589
1577
|
claudeEnvVars: options.claudeEnvVars,
|
|
1590
|
-
claudeArgs: options.claudeArgs
|
|
1578
|
+
claudeArgs: options.claudeArgs,
|
|
1579
|
+
onThinkingChange: (newThinking) => {
|
|
1580
|
+
thinking = newThinking;
|
|
1581
|
+
session.keepAlive(thinking, mode);
|
|
1582
|
+
}
|
|
1591
1583
|
});
|
|
1592
1584
|
clearInterval(pingInterval);
|
|
1593
1585
|
process.exit(0);
|
|
1594
1586
|
}
|
|
1595
1587
|
|
|
1596
|
-
const defaultSettings = {
|
|
1597
|
-
onboardingCompleted: false
|
|
1598
|
-
};
|
|
1599
|
-
async function readSettings() {
|
|
1600
|
-
if (!existsSync(configuration.settingsFile)) {
|
|
1601
|
-
return { ...defaultSettings };
|
|
1602
|
-
}
|
|
1603
|
-
try {
|
|
1604
|
-
const content = await readFile(configuration.settingsFile, "utf8");
|
|
1605
|
-
return JSON.parse(content);
|
|
1606
|
-
} catch {
|
|
1607
|
-
return { ...defaultSettings };
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
async function writeSettings(settings) {
|
|
1611
|
-
if (!existsSync(configuration.happyDir)) {
|
|
1612
|
-
await mkdir(configuration.happyDir, { recursive: true });
|
|
1613
|
-
}
|
|
1614
|
-
await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1615
|
-
}
|
|
1616
|
-
const credentialsSchema = z.object({
|
|
1617
|
-
secret: z.string().base64(),
|
|
1618
|
-
token: z.string()
|
|
1619
|
-
});
|
|
1620
|
-
async function readCredentials() {
|
|
1621
|
-
if (!existsSync(configuration.privateKeyFile)) {
|
|
1622
|
-
return null;
|
|
1623
|
-
}
|
|
1624
|
-
try {
|
|
1625
|
-
const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
|
|
1626
|
-
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
1627
|
-
return {
|
|
1628
|
-
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
1629
|
-
token: credentials.token
|
|
1630
|
-
};
|
|
1631
|
-
} catch {
|
|
1632
|
-
return null;
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
async function writeCredentials(credentials) {
|
|
1636
|
-
if (!existsSync(configuration.happyDir)) {
|
|
1637
|
-
await mkdir(configuration.happyDir, { recursive: true });
|
|
1638
|
-
}
|
|
1639
|
-
await writeFile$1(configuration.privateKeyFile, JSON.stringify({
|
|
1640
|
-
secret: encodeBase64(credentials.secret),
|
|
1641
|
-
token: credentials.token
|
|
1642
|
-
}, null, 2));
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
1588
|
function displayQRCode(url) {
|
|
1646
1589
|
console.log("=".repeat(80));
|
|
1647
1590
|
console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
|
|
@@ -1669,10 +1612,8 @@ async function doAuth() {
|
|
|
1669
1612
|
console.log("Please, authenticate using mobile app");
|
|
1670
1613
|
const authUrl = "happy://terminal?" + encodeBase64Url(keypair.publicKey);
|
|
1671
1614
|
displayQRCode(authUrl);
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
console.log(authUrl);
|
|
1675
|
-
}
|
|
1615
|
+
console.log("\n\u{1F4CB} For manual entry, copy this URL:");
|
|
1616
|
+
console.log(authUrl);
|
|
1676
1617
|
let credentials = null;
|
|
1677
1618
|
while (true) {
|
|
1678
1619
|
try {
|
|
@@ -1720,18 +1661,20 @@ class ApiDaemonSession extends EventEmitter {
|
|
|
1720
1661
|
keepAliveInterval = null;
|
|
1721
1662
|
token;
|
|
1722
1663
|
secret;
|
|
1664
|
+
spawnedProcesses = /* @__PURE__ */ new Set();
|
|
1723
1665
|
constructor(token, secret, machineIdentity) {
|
|
1724
1666
|
super();
|
|
1725
1667
|
this.token = token;
|
|
1726
1668
|
this.secret = secret;
|
|
1727
1669
|
this.machineIdentity = machineIdentity;
|
|
1670
|
+
logger.daemonDebug(`Connecting to server: ${configuration.serverUrl}`);
|
|
1728
1671
|
const socket = io(configuration.serverUrl, {
|
|
1729
1672
|
auth: {
|
|
1730
1673
|
token: this.token,
|
|
1731
1674
|
clientType: "machine-scoped",
|
|
1732
1675
|
machineId: this.machineIdentity.machineId
|
|
1733
1676
|
},
|
|
1734
|
-
path: "/v1/
|
|
1677
|
+
path: "/v1/updates",
|
|
1735
1678
|
reconnection: true,
|
|
1736
1679
|
reconnectionAttempts: Infinity,
|
|
1737
1680
|
reconnectionDelay: 1e3,
|
|
@@ -1741,68 +1684,146 @@ class ApiDaemonSession extends EventEmitter {
|
|
|
1741
1684
|
autoConnect: false
|
|
1742
1685
|
});
|
|
1743
1686
|
socket.on("connect", () => {
|
|
1744
|
-
logger.
|
|
1687
|
+
logger.daemonDebug("Socket connected");
|
|
1688
|
+
logger.daemonDebug(`Connected with auth - token: ${this.token.substring(0, 10)}..., machineId: ${this.machineIdentity.machineId}`);
|
|
1689
|
+
const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
1690
|
+
socket.emit("rpc-register", { method: rpcMethod });
|
|
1691
|
+
logger.daemonDebug(`Emitted RPC registration: ${rpcMethod}`);
|
|
1745
1692
|
this.emit("connected");
|
|
1746
|
-
socket.emit("machine-connect", {
|
|
1747
|
-
token: this.token,
|
|
1748
|
-
machineIdentity: encodeBase64(encrypt(this.machineIdentity, this.secret))
|
|
1749
|
-
});
|
|
1750
1693
|
this.startKeepAlive();
|
|
1751
1694
|
});
|
|
1752
|
-
socket.on("
|
|
1753
|
-
logger.
|
|
1754
|
-
this.
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
const child = spawn$1("happy", args, {
|
|
1695
|
+
socket.on("rpc-request", async (data, callback) => {
|
|
1696
|
+
logger.daemonDebug(`Received RPC request: ${JSON.stringify(data)}`);
|
|
1697
|
+
const expectedMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
1698
|
+
if (data.method === expectedMethod) {
|
|
1699
|
+
logger.daemonDebug("Processing spawn-happy-session RPC");
|
|
1700
|
+
try {
|
|
1701
|
+
const { directory } = data.params || {};
|
|
1702
|
+
if (!directory) {
|
|
1703
|
+
throw new Error("Directory is required");
|
|
1704
|
+
}
|
|
1705
|
+
const args = [
|
|
1706
|
+
"--daemon-spawn",
|
|
1707
|
+
"--happy-starting-mode",
|
|
1708
|
+
"remote"
|
|
1709
|
+
// ALWAYS force remote mode for daemon spawns
|
|
1710
|
+
];
|
|
1711
|
+
if (configuration.installationLocation === "local") {
|
|
1712
|
+
args.push("--local");
|
|
1713
|
+
}
|
|
1714
|
+
if (configuration.serverUrl !== "https://handy-api.korshakov.org") {
|
|
1715
|
+
args.push("--happy-server-url", configuration.serverUrl);
|
|
1716
|
+
}
|
|
1717
|
+
logger.daemonDebug(`Spawning happy in directory: ${directory} with args: ${args.join(" ")}`);
|
|
1718
|
+
const happyPath = process.argv[1];
|
|
1719
|
+
const isTypeScript = happyPath.endsWith(".ts");
|
|
1720
|
+
const happyProcess = isTypeScript ? spawn$1("npx", ["tsx", happyPath, ...args], {
|
|
1721
|
+
cwd: directory,
|
|
1722
|
+
env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
|
|
1781
1723
|
detached: true,
|
|
1782
|
-
stdio: "ignore",
|
|
1783
|
-
|
|
1724
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1725
|
+
// We need stdout
|
|
1726
|
+
}) : spawn$1(process.argv[0], [happyPath, ...args], {
|
|
1727
|
+
cwd: directory,
|
|
1728
|
+
env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
|
|
1729
|
+
detached: true,
|
|
1730
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1731
|
+
// We need stdout
|
|
1732
|
+
});
|
|
1733
|
+
this.spawnedProcesses.add(happyProcess);
|
|
1734
|
+
let sessionId = null;
|
|
1735
|
+
let output = "";
|
|
1736
|
+
let timeoutId = null;
|
|
1737
|
+
const cleanup = () => {
|
|
1738
|
+
happyProcess.stdout.removeAllListeners("data");
|
|
1739
|
+
happyProcess.stderr.removeAllListeners("data");
|
|
1740
|
+
happyProcess.removeAllListeners("error");
|
|
1741
|
+
happyProcess.removeAllListeners("exit");
|
|
1742
|
+
if (timeoutId) {
|
|
1743
|
+
clearTimeout(timeoutId);
|
|
1744
|
+
timeoutId = null;
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
happyProcess.stdout.on("data", (data2) => {
|
|
1748
|
+
output += data2.toString();
|
|
1749
|
+
const match = output.match(/daemon:sessionIdCreated:(.+?)[\n\r]/);
|
|
1750
|
+
if (match && !sessionId) {
|
|
1751
|
+
sessionId = match[1];
|
|
1752
|
+
logger.daemonDebug(`Session spawned successfully: ${sessionId}`);
|
|
1753
|
+
callback({ sessionId });
|
|
1754
|
+
cleanup();
|
|
1755
|
+
happyProcess.unref();
|
|
1756
|
+
}
|
|
1784
1757
|
});
|
|
1785
|
-
|
|
1758
|
+
happyProcess.stderr.on("data", (data2) => {
|
|
1759
|
+
logger.daemonDebug(`Spawned process stderr: ${data2.toString()}`);
|
|
1760
|
+
});
|
|
1761
|
+
happyProcess.on("error", (error) => {
|
|
1762
|
+
logger.daemonDebug("Error spawning session:", error);
|
|
1763
|
+
if (!sessionId) {
|
|
1764
|
+
callback({ error: `Failed to spawn: ${error.message}` });
|
|
1765
|
+
cleanup();
|
|
1766
|
+
this.spawnedProcesses.delete(happyProcess);
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
happyProcess.on("exit", (code, signal) => {
|
|
1770
|
+
logger.daemonDebug(`Spawned process exited with code ${code}, signal ${signal}`);
|
|
1771
|
+
this.spawnedProcesses.delete(happyProcess);
|
|
1772
|
+
if (!sessionId) {
|
|
1773
|
+
callback({ error: `Process exited before session ID received` });
|
|
1774
|
+
cleanup();
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1777
|
+
timeoutId = setTimeout(() => {
|
|
1778
|
+
if (!sessionId) {
|
|
1779
|
+
logger.daemonDebug("Timeout waiting for session ID");
|
|
1780
|
+
callback({ error: "Timeout waiting for session" });
|
|
1781
|
+
cleanup();
|
|
1782
|
+
happyProcess.kill();
|
|
1783
|
+
this.spawnedProcesses.delete(happyProcess);
|
|
1784
|
+
}
|
|
1785
|
+
}, 1e4);
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
logger.daemonDebug("Error spawning session:", error);
|
|
1788
|
+
callback({ error: error instanceof Error ? error.message : "Unknown error" });
|
|
1786
1789
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
result: encodeBase64(encrypt(result, this.secret))
|
|
1791
|
-
});
|
|
1792
|
-
callback(encodeBase64(encrypt({ success: true }, this.secret)));
|
|
1793
|
-
} catch (error) {
|
|
1794
|
-
logger.debug("[DAEMON] Failed to spawn session", error);
|
|
1795
|
-
const errorResult = {
|
|
1796
|
-
success: false,
|
|
1797
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
1798
|
-
};
|
|
1799
|
-
socket.emit("session-spawn-result", {
|
|
1800
|
-
requestId: requestData?.requestId || "",
|
|
1801
|
-
result: encodeBase64(encrypt(errorResult, this.secret))
|
|
1802
|
-
});
|
|
1803
|
-
callback(encodeBase64(encrypt(errorResult, this.secret)));
|
|
1790
|
+
} else {
|
|
1791
|
+
logger.daemonDebug(`Unknown RPC method: ${data.method}`);
|
|
1792
|
+
callback({ error: `Unknown method: ${data.method}` });
|
|
1804
1793
|
}
|
|
1805
1794
|
});
|
|
1795
|
+
socket.on("disconnect", (reason) => {
|
|
1796
|
+
logger.daemonDebug(`Disconnected from server. Reason: ${reason}`);
|
|
1797
|
+
this.emit("disconnected");
|
|
1798
|
+
this.stopKeepAlive();
|
|
1799
|
+
});
|
|
1800
|
+
socket.on("reconnect", () => {
|
|
1801
|
+
logger.daemonDebug("Reconnected to server");
|
|
1802
|
+
const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
1803
|
+
socket.emit("rpc-register", { method: rpcMethod });
|
|
1804
|
+
logger.daemonDebug(`Re-registered RPC method: ${rpcMethod}`);
|
|
1805
|
+
});
|
|
1806
|
+
socket.on("rpc-registered", (data) => {
|
|
1807
|
+
logger.daemonDebug(`RPC registration confirmed: ${data.method}`);
|
|
1808
|
+
});
|
|
1809
|
+
socket.on("rpc-unregistered", (data) => {
|
|
1810
|
+
logger.daemonDebug(`RPC unregistered: ${data.method}`);
|
|
1811
|
+
});
|
|
1812
|
+
socket.on("rpc-error", (data) => {
|
|
1813
|
+
logger.daemonDebug(`RPC error: ${JSON.stringify(data)}`);
|
|
1814
|
+
});
|
|
1815
|
+
socket.onAny((event, ...args) => {
|
|
1816
|
+
if (!event.startsWith("machine-alive")) {
|
|
1817
|
+
logger.daemonDebug(`Socket event: ${event}, args: ${JSON.stringify(args)}`);
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
socket.on("connect_error", (error) => {
|
|
1821
|
+
logger.daemonDebug(`Connection error: ${error.message}`);
|
|
1822
|
+
logger.daemonDebug(`Error: ${JSON.stringify(error, null, 2)}`);
|
|
1823
|
+
});
|
|
1824
|
+
socket.on("error", (error) => {
|
|
1825
|
+
logger.daemonDebug(`Socket error: ${error}`);
|
|
1826
|
+
});
|
|
1806
1827
|
socket.on("daemon-command", (data) => {
|
|
1807
1828
|
switch (data.command) {
|
|
1808
1829
|
case "shutdown":
|
|
@@ -1818,6 +1839,7 @@ class ApiDaemonSession extends EventEmitter {
|
|
|
1818
1839
|
startKeepAlive() {
|
|
1819
1840
|
this.stopKeepAlive();
|
|
1820
1841
|
this.keepAliveInterval = setInterval(() => {
|
|
1842
|
+
logger.daemonDebug("Sending keep-alive ping");
|
|
1821
1843
|
this.socket.volatile.emit("machine-alive", {
|
|
1822
1844
|
time: Date.now()
|
|
1823
1845
|
});
|
|
@@ -1833,22 +1855,42 @@ class ApiDaemonSession extends EventEmitter {
|
|
|
1833
1855
|
this.socket.connect();
|
|
1834
1856
|
}
|
|
1835
1857
|
shutdown() {
|
|
1858
|
+
logger.daemonDebug(`Shutting down daemon, killing ${this.spawnedProcesses.size} spawned processes`);
|
|
1859
|
+
for (const process2 of this.spawnedProcesses) {
|
|
1860
|
+
try {
|
|
1861
|
+
logger.daemonDebug(`Killing spawned process with PID: ${process2.pid}`);
|
|
1862
|
+
process2.kill("SIGTERM");
|
|
1863
|
+
setTimeout(() => {
|
|
1864
|
+
try {
|
|
1865
|
+
process2.kill("SIGKILL");
|
|
1866
|
+
} catch (e) {
|
|
1867
|
+
}
|
|
1868
|
+
}, 1e3);
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
logger.daemonDebug(`Error killing process: ${error}`);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
this.spawnedProcesses.clear();
|
|
1836
1874
|
this.stopKeepAlive();
|
|
1837
1875
|
this.socket.close();
|
|
1838
1876
|
this.emit("shutdown");
|
|
1839
1877
|
}
|
|
1840
1878
|
}
|
|
1841
1879
|
|
|
1880
|
+
let pidFileFd = null;
|
|
1842
1881
|
async function startDaemon() {
|
|
1843
|
-
|
|
1882
|
+
if (process.platform !== "darwin") {
|
|
1883
|
+
console.error("ERROR: Daemon is only supported on macOS");
|
|
1884
|
+
process.exit(1);
|
|
1885
|
+
}
|
|
1886
|
+
logger.daemonDebug("Starting daemon process...");
|
|
1887
|
+
logger.daemonDebug(`Server URL: ${configuration.serverUrl}`);
|
|
1844
1888
|
if (await isDaemonRunning()) {
|
|
1845
|
-
|
|
1889
|
+
logger.daemonDebug("Happy daemon is already running");
|
|
1846
1890
|
process.exit(0);
|
|
1847
1891
|
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
console.log("[DAEMON] PID file written successfully");
|
|
1851
|
-
logger.info("Happy CLI daemon started successfully");
|
|
1892
|
+
pidFileFd = writePidFile();
|
|
1893
|
+
logger.daemonDebug("PID file written");
|
|
1852
1894
|
process.on("SIGINT", () => {
|
|
1853
1895
|
stopDaemon().catch(console.error);
|
|
1854
1896
|
});
|
|
@@ -1873,7 +1915,7 @@ async function startDaemon() {
|
|
|
1873
1915
|
};
|
|
1874
1916
|
let credentials = await readCredentials();
|
|
1875
1917
|
if (!credentials) {
|
|
1876
|
-
logger.
|
|
1918
|
+
logger.daemonDebug("No credentials found, running auth");
|
|
1877
1919
|
await doAuth();
|
|
1878
1920
|
credentials = await readCredentials();
|
|
1879
1921
|
if (!credentials) {
|
|
@@ -1881,64 +1923,64 @@ async function startDaemon() {
|
|
|
1881
1923
|
}
|
|
1882
1924
|
}
|
|
1883
1925
|
const { token, secret } = credentials;
|
|
1884
|
-
const daemon = new ApiDaemonSession(
|
|
1926
|
+
const daemon = new ApiDaemonSession(
|
|
1927
|
+
token,
|
|
1928
|
+
secret,
|
|
1929
|
+
machineIdentity
|
|
1930
|
+
);
|
|
1885
1931
|
daemon.on("connected", () => {
|
|
1886
|
-
logger.
|
|
1932
|
+
logger.daemonDebug("Connected to server event received");
|
|
1887
1933
|
});
|
|
1888
1934
|
daemon.on("disconnected", () => {
|
|
1889
|
-
logger.
|
|
1935
|
+
logger.daemonDebug("Disconnected from server event received");
|
|
1890
1936
|
});
|
|
1891
1937
|
daemon.on("shutdown", () => {
|
|
1892
|
-
logger.
|
|
1938
|
+
logger.daemonDebug("Shutdown requested");
|
|
1893
1939
|
stopDaemon();
|
|
1894
1940
|
process.exit(0);
|
|
1895
1941
|
});
|
|
1896
1942
|
daemon.connect();
|
|
1897
|
-
|
|
1898
|
-
}, 1e3);
|
|
1943
|
+
logger.daemonDebug("Daemon started successfully");
|
|
1899
1944
|
} catch (error) {
|
|
1900
|
-
logger.
|
|
1945
|
+
logger.daemonDebug("Failed to start daemon", error);
|
|
1901
1946
|
stopDaemon();
|
|
1902
1947
|
process.exit(1);
|
|
1903
1948
|
}
|
|
1904
|
-
process.on("SIGINT", () => process.exit(0));
|
|
1905
|
-
process.on("SIGTERM", () => process.exit(0));
|
|
1906
|
-
process.on("exit", () => process.exit(0));
|
|
1907
1949
|
while (true) {
|
|
1908
1950
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1909
1951
|
}
|
|
1910
1952
|
}
|
|
1911
1953
|
async function isDaemonRunning() {
|
|
1912
1954
|
try {
|
|
1913
|
-
|
|
1955
|
+
logger.daemonDebug("[isDaemonRunning] Checking if daemon is running...");
|
|
1914
1956
|
if (existsSync$1(configuration.daemonPidFile)) {
|
|
1915
|
-
|
|
1957
|
+
logger.daemonDebug("[isDaemonRunning] PID file exists");
|
|
1916
1958
|
const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
|
|
1917
|
-
|
|
1959
|
+
logger.daemonDebug("[isDaemonRunning] PID from file:", pid);
|
|
1918
1960
|
try {
|
|
1919
1961
|
process.kill(pid, 0);
|
|
1920
|
-
|
|
1962
|
+
logger.daemonDebug("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
|
|
1921
1963
|
const isHappyDaemon = await isProcessHappyDaemon(pid);
|
|
1922
|
-
|
|
1964
|
+
logger.daemonDebug("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
|
|
1923
1965
|
if (isHappyDaemon) {
|
|
1924
1966
|
return true;
|
|
1925
1967
|
} else {
|
|
1926
|
-
|
|
1927
|
-
logger.debug(`
|
|
1968
|
+
logger.daemonDebug("[isDaemonRunning] PID is not a happy daemon, cleaning up");
|
|
1969
|
+
logger.debug(`PID ${pid} is not a happy daemon, cleaning up`);
|
|
1928
1970
|
unlinkSync(configuration.daemonPidFile);
|
|
1929
1971
|
}
|
|
1930
1972
|
} catch (error) {
|
|
1931
|
-
|
|
1932
|
-
logger.debug("
|
|
1973
|
+
logger.daemonDebug("[isDaemonRunning] Process not running, cleaning up stale PID file");
|
|
1974
|
+
logger.debug("Process not running, cleaning up stale PID file");
|
|
1933
1975
|
unlinkSync(configuration.daemonPidFile);
|
|
1934
1976
|
}
|
|
1935
1977
|
} else {
|
|
1936
|
-
|
|
1978
|
+
logger.daemonDebug("[isDaemonRunning] No PID file found");
|
|
1937
1979
|
}
|
|
1938
1980
|
return false;
|
|
1939
1981
|
} catch (error) {
|
|
1940
|
-
|
|
1941
|
-
logger.debug("
|
|
1982
|
+
logger.daemonDebug("[isDaemonRunning] Error:", error);
|
|
1983
|
+
logger.debug("Error checking daemon status", error);
|
|
1942
1984
|
return false;
|
|
1943
1985
|
}
|
|
1944
1986
|
}
|
|
@@ -1948,20 +1990,46 @@ function writePidFile() {
|
|
|
1948
1990
|
mkdirSync$1(happyDir, { recursive: true });
|
|
1949
1991
|
}
|
|
1950
1992
|
try {
|
|
1951
|
-
|
|
1993
|
+
const fd = openSync(configuration.daemonPidFile, "wx");
|
|
1994
|
+
writeSync(fd, process.pid.toString());
|
|
1995
|
+
return fd;
|
|
1952
1996
|
} catch (error) {
|
|
1953
1997
|
if (error.code === "EEXIST") {
|
|
1954
|
-
|
|
1955
|
-
|
|
1998
|
+
try {
|
|
1999
|
+
const fd = openSync(configuration.daemonPidFile, "r+");
|
|
2000
|
+
const existingPid = readFileSync$1(configuration.daemonPidFile, "utf-8").trim();
|
|
2001
|
+
closeSync(fd);
|
|
2002
|
+
try {
|
|
2003
|
+
process.kill(parseInt(existingPid), 0);
|
|
2004
|
+
logger.daemonDebug("PID file exists and process is running");
|
|
2005
|
+
logger.daemonDebug("Happy daemon is already running");
|
|
2006
|
+
process.exit(0);
|
|
2007
|
+
} catch {
|
|
2008
|
+
logger.daemonDebug("PID file exists but process is dead, cleaning up");
|
|
2009
|
+
unlinkSync(configuration.daemonPidFile);
|
|
2010
|
+
return writePidFile();
|
|
2011
|
+
}
|
|
2012
|
+
} catch (lockError) {
|
|
2013
|
+
logger.daemonDebug("Cannot acquire write lock on PID file, daemon is running");
|
|
2014
|
+
logger.daemonDebug("Happy daemon is already running");
|
|
2015
|
+
process.exit(0);
|
|
2016
|
+
}
|
|
1956
2017
|
}
|
|
1957
2018
|
throw error;
|
|
1958
2019
|
}
|
|
1959
2020
|
}
|
|
1960
2021
|
async function stopDaemon() {
|
|
1961
2022
|
try {
|
|
2023
|
+
if (pidFileFd !== null) {
|
|
2024
|
+
try {
|
|
2025
|
+
closeSync(pidFileFd);
|
|
2026
|
+
} catch {
|
|
2027
|
+
}
|
|
2028
|
+
pidFileFd = null;
|
|
2029
|
+
}
|
|
1962
2030
|
if (existsSync$1(configuration.daemonPidFile)) {
|
|
1963
2031
|
const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
|
|
1964
|
-
logger.debug(`
|
|
2032
|
+
logger.debug(`Stopping daemon with PID ${pid}`);
|
|
1965
2033
|
try {
|
|
1966
2034
|
process.kill(pid, "SIGTERM");
|
|
1967
2035
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
@@ -1971,12 +2039,12 @@ async function stopDaemon() {
|
|
|
1971
2039
|
} catch {
|
|
1972
2040
|
}
|
|
1973
2041
|
} catch (error) {
|
|
1974
|
-
logger.debug("
|
|
2042
|
+
logger.debug("Process already dead or inaccessible", error);
|
|
1975
2043
|
}
|
|
1976
2044
|
unlinkSync(configuration.daemonPidFile);
|
|
1977
2045
|
}
|
|
1978
2046
|
} catch (error) {
|
|
1979
|
-
logger.debug("
|
|
2047
|
+
logger.debug("Error stopping daemon", error);
|
|
1980
2048
|
}
|
|
1981
2049
|
}
|
|
1982
2050
|
async function isProcessHappyDaemon(pid) {
|
|
@@ -2124,7 +2192,12 @@ async function uninstall() {
|
|
|
2124
2192
|
(async () => {
|
|
2125
2193
|
const args = process.argv.slice(2);
|
|
2126
2194
|
let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
|
|
2127
|
-
|
|
2195
|
+
let serverUrl;
|
|
2196
|
+
const serverUrlIndex = args.indexOf("--happy-server-url");
|
|
2197
|
+
if (serverUrlIndex !== -1 && serverUrlIndex + 1 < args.length) {
|
|
2198
|
+
serverUrl = args[serverUrlIndex + 1];
|
|
2199
|
+
}
|
|
2200
|
+
initializeConfiguration(installationLocation, serverUrl);
|
|
2128
2201
|
initLoggerWithGlobalConfiguration();
|
|
2129
2202
|
logger.debug("Starting happy CLI with args: ", process.argv);
|
|
2130
2203
|
const subcommand = args[0];
|
|
@@ -2206,6 +2279,10 @@ Currently only supported on macOS.
|
|
|
2206
2279
|
} else if (arg === "--claude-arg") {
|
|
2207
2280
|
const claudeArg = args[++i];
|
|
2208
2281
|
options.claudeArgs = [...options.claudeArgs || [], claudeArg];
|
|
2282
|
+
} else if (arg === "--daemon-spawn") {
|
|
2283
|
+
options.daemonSpawn = true;
|
|
2284
|
+
} else if (arg === "--happy-server-url") {
|
|
2285
|
+
i++;
|
|
2209
2286
|
} else {
|
|
2210
2287
|
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
2211
2288
|
process.exit(1);
|
|
@@ -2241,6 +2318,8 @@ ${chalk.bold("Options:")}
|
|
|
2241
2318
|
You will require re-login each time you run this in a new directory.
|
|
2242
2319
|
--happy-starting-mode <interactive|remote>
|
|
2243
2320
|
Set the starting mode for new sessions (default: remote)
|
|
2321
|
+
--happy-server-url <url>
|
|
2322
|
+
Set the server URL (overrides HANDY_SERVER_URL environment variable)
|
|
2244
2323
|
|
|
2245
2324
|
${chalk.bold("Examples:")}
|
|
2246
2325
|
happy Start a session with default settings
|
|
@@ -2267,7 +2346,71 @@ ${chalk.bold("Examples:")}
|
|
|
2267
2346
|
}
|
|
2268
2347
|
credentials = res;
|
|
2269
2348
|
}
|
|
2270
|
-
await readSettings() || { };
|
|
2349
|
+
const settings = await readSettings() || { onboardingCompleted: false };
|
|
2350
|
+
process.env.EXPERIMENTAL_FEATURES !== void 0;
|
|
2351
|
+
if (settings.daemonAutoStartWhenRunningHappy === void 0) {
|
|
2352
|
+
console.log(chalk.cyan("\n\u{1F680} Happy Daemon Setup\n"));
|
|
2353
|
+
const rl = createInterface({
|
|
2354
|
+
input: process.stdin,
|
|
2355
|
+
output: process.stdout
|
|
2356
|
+
});
|
|
2357
|
+
console.log(chalk.cyan("\n\u{1F4F1} Happy can run a background service that allows you to:"));
|
|
2358
|
+
console.log(chalk.cyan(" \u2022 Spawn new conversations from your phone"));
|
|
2359
|
+
console.log(chalk.cyan(" \u2022 Continue closed conversations remotely"));
|
|
2360
|
+
console.log(chalk.cyan(" \u2022 Work with Claude while your computer has internet\n"));
|
|
2361
|
+
const answer = await new Promise((resolve) => {
|
|
2362
|
+
rl.question(chalk.green("Would you like Happy to start this service automatically? (recommended) [Y/n]: "), resolve);
|
|
2363
|
+
});
|
|
2364
|
+
rl.close();
|
|
2365
|
+
const shouldAutoStart = answer.toLowerCase() !== "n";
|
|
2366
|
+
settings.daemonAutoStartWhenRunningHappy = shouldAutoStart;
|
|
2367
|
+
if (shouldAutoStart) {
|
|
2368
|
+
console.log(chalk.green("\u2713 Happy will start the background service automatically"));
|
|
2369
|
+
console.log(chalk.gray(" The service will run whenever you use the happy command"));
|
|
2370
|
+
} else {
|
|
2371
|
+
console.log(chalk.yellow(" You can enable this later by running: happy daemon install"));
|
|
2372
|
+
}
|
|
2373
|
+
await writeSettings(settings);
|
|
2374
|
+
}
|
|
2375
|
+
if (settings.daemonAutoStartWhenRunningHappy) {
|
|
2376
|
+
console.log("Starting Happy background service...");
|
|
2377
|
+
if (!await isDaemonRunning()) {
|
|
2378
|
+
const happyPath = process.argv[1];
|
|
2379
|
+
const isBuiltBinary = happyPath.endsWith("/bin/happy") || happyPath.endsWith("\\bin\\happy");
|
|
2380
|
+
const daemonArgs = ["daemon", "start"];
|
|
2381
|
+
if (serverUrl) {
|
|
2382
|
+
daemonArgs.push("--happy-server-url", serverUrl);
|
|
2383
|
+
}
|
|
2384
|
+
if (installationLocation === "local") {
|
|
2385
|
+
daemonArgs.push("--local");
|
|
2386
|
+
}
|
|
2387
|
+
const daemonProcess = isBuiltBinary ? spawn$1(happyPath, daemonArgs, {
|
|
2388
|
+
detached: true,
|
|
2389
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
2390
|
+
// Show stdout/stderr for debugging
|
|
2391
|
+
env: {
|
|
2392
|
+
...process.env,
|
|
2393
|
+
HANDY_SERVER_URL: serverUrl || process.env.HANDY_SERVER_URL,
|
|
2394
|
+
// Pass through server URL
|
|
2395
|
+
HANDY_LOCAL: process.env.HANDY_LOCAL
|
|
2396
|
+
// Pass through local flag
|
|
2397
|
+
}
|
|
2398
|
+
}) : spawn$1("npx", ["tsx", happyPath, ...daemonArgs], {
|
|
2399
|
+
detached: true,
|
|
2400
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
2401
|
+
// Show stdout/stderr for debugging
|
|
2402
|
+
env: {
|
|
2403
|
+
...process.env,
|
|
2404
|
+
HANDY_SERVER_URL: serverUrl || process.env.HANDY_SERVER_URL,
|
|
2405
|
+
// Pass through server URL
|
|
2406
|
+
HANDY_LOCAL: process.env.HANDY_LOCAL
|
|
2407
|
+
// Pass through local flag
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
daemonProcess.unref();
|
|
2411
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2271
2414
|
try {
|
|
2272
2415
|
await start(credentials, options);
|
|
2273
2416
|
} catch (error) {
|