happy-coder 0.1.10 → 0.1.11
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/README.md +2 -0
- package/dist/index-B2GqfEZV.cjs +1564 -0
- package/dist/index-QItBXhux.mjs +1540 -0
- package/dist/index.cjs +420 -258
- package/dist/index.mjs +410 -248
- package/dist/install-B0DnBGS_.mjs +29 -0
- package/dist/install-B2r_gX72.cjs +109 -0
- package/dist/install-C809w0Cj.cjs +31 -0
- package/dist/install-DEPy62QN.mjs +97 -0
- package/dist/install-GZIzyuIE.cjs +99 -0
- package/dist/install-HKe7dyS4.mjs +107 -0
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +8 -3
- package/dist/lib.d.mts +8 -3
- package/dist/lib.mjs +1 -1
- package/dist/run-BmEaINbl.cjs +250 -0
- package/dist/run-DMbKhYfb.mjs +247 -0
- package/dist/run-FBXkmmN7.mjs +32 -0
- package/dist/run-q2To6b-c.cjs +34 -0
- package/dist/types-BRICSarm.mjs +870 -0
- package/dist/types-BTQRfIr3.cjs +892 -0
- package/dist/types-CEvzGLMI.cjs +882 -0
- package/dist/{types-DnQGY77F.mjs → types-D39L8JSd.mjs} +55 -23
- package/dist/types-DYBiuNUQ.cjs +883 -0
- package/dist/types-Df5dlWLV.mjs +871 -0
- package/dist/types-fXgEaaqP.mjs +861 -0
- package/dist/{types-B2JzqUiU.cjs → types-hotUTaWz.cjs} +53 -21
- package/dist/types-mykDX2xe.cjs +872 -0
- package/dist/types-tLWMaptR.mjs +879 -0
- package/dist/uninstall-BGgl5V8F.mjs +29 -0
- package/dist/uninstall-BWHglipH.mjs +40 -0
- package/dist/uninstall-C42CoSCI.cjs +53 -0
- package/dist/uninstall-CLkTtlMv.mjs +51 -0
- package/dist/uninstall-CdHMb6wi.cjs +31 -0
- package/dist/uninstall-FXyyAuGU.cjs +42 -0
- package/package.json +8 -2
- package/ripgrep/COPYING +3 -0
- package/ripgrep/arm64-darwin/rg +0 -0
- package/ripgrep/arm64-darwin/ripgrep.node +0 -0
- package/ripgrep/arm64-linux/rg +0 -0
- package/ripgrep/arm64-linux/ripgrep.node +0 -0
- package/ripgrep/x64-darwin/rg +0 -0
- package/ripgrep/x64-darwin/ripgrep.node +0 -0
- package/ripgrep/x64-linux/rg +0 -0
- package/ripgrep/x64-linux/ripgrep.node +0 -0
- package/ripgrep/x64-win32/rg.exe +0 -0
- package/ripgrep/x64-win32/ripgrep.node +0 -0
- package/scripts/ripgrep_launcher.cjs +57 -0
package/dist/index.mjs
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { l as logger, d as backoff, R as RawJSONLinesSchema, A as ApiClient, c as configuration,
|
|
2
|
+
import { l as logger, d as delay, e as backoff, R as RawJSONLinesSchema, A as ApiClient, c as configuration, f as encodeBase64, g as encodeBase64Url, h as decodeBase64, j as encrypt, k as decrypt, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-D39L8JSd.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';
|
|
6
|
-
import os, { homedir } from 'node:os';
|
|
7
6
|
import { resolve, join, dirname } from 'node:path';
|
|
7
|
+
import os, { homedir } from 'node:os';
|
|
8
|
+
import { access, watch as watch$1, readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
|
|
8
9
|
import { spawn } from 'node:child_process';
|
|
9
10
|
import { createInterface } from 'node:readline';
|
|
10
|
-
import { fileURLToPath
|
|
11
|
-
import {
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { readFile, mkdir, writeFile as writeFile$1 } from 'node:fs/promises';
|
|
12
13
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
|
-
import { createServer,
|
|
14
|
+
import { createServer, ServerResponse } from 'node:http';
|
|
14
15
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
15
16
|
import * as z from 'zod';
|
|
16
17
|
import { z as z$1 } from 'zod';
|
|
17
|
-
import {
|
|
18
|
-
import net from 'node:net';
|
|
19
|
-
import { exec, spawn as spawn$1, execSync } from 'child_process';
|
|
18
|
+
import { spawn as spawn$1, exec, execSync } from 'child_process';
|
|
20
19
|
import { promisify } from 'util';
|
|
21
|
-
import { readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
|
|
22
20
|
import crypto, { createHash } from 'crypto';
|
|
23
|
-
import { join as join$1 } from 'path';
|
|
21
|
+
import { dirname as dirname$1, join as join$1 } from 'path';
|
|
22
|
+
import { fileURLToPath as fileURLToPath$1 } from 'url';
|
|
23
|
+
import httpProxy from 'http-proxy';
|
|
24
24
|
import tweetnacl from 'tweetnacl';
|
|
25
25
|
import axios from 'axios';
|
|
26
26
|
import qrcode from 'qrcode-terminal';
|
|
27
27
|
import { EventEmitter } from 'node:events';
|
|
28
28
|
import { io } from 'socket.io-client';
|
|
29
|
-
import { homedir as homedir$1
|
|
29
|
+
import { hostname, homedir as homedir$1 } from 'os';
|
|
30
30
|
import { existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync, mkdirSync as mkdirSync$1, writeFileSync, chmodSync } from 'fs';
|
|
31
31
|
import 'expo-server-sdk';
|
|
32
32
|
|
|
@@ -150,9 +150,13 @@ function printDivider() {
|
|
|
150
150
|
console.log(chalk.gray("\u2550".repeat(60)));
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
function getProjectPath(workingDirectory) {
|
|
154
|
+
const projectId = resolve(workingDirectory).replace(/[\\\/\.:]/g, "-");
|
|
155
|
+
return join(homedir(), ".claude", "projects", projectId);
|
|
156
|
+
}
|
|
157
|
+
|
|
153
158
|
function claudeCheckSession(sessionId, path) {
|
|
154
|
-
const
|
|
155
|
-
const projectDir = join(homedir(), ".claude", "projects", projectName);
|
|
159
|
+
const projectDir = getProjectPath(path);
|
|
156
160
|
const sessionFile = join(projectDir, `${sessionId}.jsonl`);
|
|
157
161
|
const sessionExists = existsSync(sessionFile);
|
|
158
162
|
if (!sessionExists) {
|
|
@@ -170,11 +174,29 @@ function claudeCheckSession(sessionId, path) {
|
|
|
170
174
|
return hasGoodMessage;
|
|
171
175
|
}
|
|
172
176
|
|
|
177
|
+
async function awaitFileExist(file, timeout = 1e4) {
|
|
178
|
+
const startTime = Date.now();
|
|
179
|
+
while (Date.now() - startTime < timeout) {
|
|
180
|
+
try {
|
|
181
|
+
await access(file);
|
|
182
|
+
return true;
|
|
183
|
+
} catch (e) {
|
|
184
|
+
await delay(1e3);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
173
190
|
async function claudeRemote(opts) {
|
|
174
191
|
let startFrom = opts.sessionId;
|
|
175
192
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
176
193
|
startFrom = null;
|
|
177
194
|
}
|
|
195
|
+
if (opts.claudeEnvVars) {
|
|
196
|
+
Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
|
|
197
|
+
process.env[key] = value;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
178
200
|
const abortController = new AbortController();
|
|
179
201
|
const sdkOptions = {
|
|
180
202
|
cwd: opts.path,
|
|
@@ -184,6 +206,9 @@ async function claudeRemote(opts) {
|
|
|
184
206
|
executable: "node",
|
|
185
207
|
abortController
|
|
186
208
|
};
|
|
209
|
+
if (opts.claudeArgs && opts.claudeArgs.length > 0) {
|
|
210
|
+
sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
|
|
211
|
+
}
|
|
187
212
|
let aborted = false;
|
|
188
213
|
let response;
|
|
189
214
|
opts.abort.addEventListener("abort", () => {
|
|
@@ -221,15 +246,11 @@ async function claudeRemote(opts) {
|
|
|
221
246
|
logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
|
|
222
247
|
formatClaudeMessage(message, opts.onAssistantResult);
|
|
223
248
|
if (message.type === "system" && message.subtype === "init") {
|
|
224
|
-
|
|
225
|
-
const projectDir =
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
opts.onSessionFound(message.session_id);
|
|
230
|
-
watcher.close();
|
|
231
|
-
}
|
|
232
|
-
});
|
|
249
|
+
logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${message.session_id}`);
|
|
250
|
+
const projectDir = getProjectPath(opts.path);
|
|
251
|
+
const found = await awaitFileExist(join(projectDir, `${message.session_id}.jsonl`));
|
|
252
|
+
logger.debug(`[claudeRemote] Session file found: ${message.session_id} ${found}`);
|
|
253
|
+
opts.onSessionFound(message.session_id);
|
|
233
254
|
}
|
|
234
255
|
}
|
|
235
256
|
logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
@@ -251,10 +272,9 @@ async function claudeRemote(opts) {
|
|
|
251
272
|
logger.debug(`[claudeRemote] Function completed`);
|
|
252
273
|
}
|
|
253
274
|
|
|
254
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
275
|
+
const __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
255
276
|
async function claudeLocal(opts) {
|
|
256
|
-
const
|
|
257
|
-
const projectDir = join(homedir(), ".claude", "projects", projectName);
|
|
277
|
+
const projectDir = getProjectPath(opts.path);
|
|
258
278
|
mkdirSync(projectDir, { recursive: true });
|
|
259
279
|
const watcher = watch(projectDir);
|
|
260
280
|
let resolvedSessionId = null;
|
|
@@ -288,11 +308,19 @@ async function claudeLocal(opts) {
|
|
|
288
308
|
if (startFrom) {
|
|
289
309
|
args.push("--resume", startFrom);
|
|
290
310
|
}
|
|
291
|
-
|
|
311
|
+
if (opts.claudeArgs) {
|
|
312
|
+
args.push(...opts.claudeArgs);
|
|
313
|
+
}
|
|
314
|
+
const claudeCliPath = process.env.HAPPY_CLAUDE_CLI_PATH || resolve(join(__dirname$1, "..", "scripts", "claudeInteractiveLaunch.cjs"));
|
|
315
|
+
const env = {
|
|
316
|
+
...process.env,
|
|
317
|
+
...opts.claudeEnvVars
|
|
318
|
+
};
|
|
292
319
|
const child = spawn("node", [claudeCliPath, ...args], {
|
|
293
320
|
stdio: ["inherit", "inherit", "inherit", "pipe"],
|
|
294
321
|
signal: opts.abort,
|
|
295
|
-
cwd: opts.path
|
|
322
|
+
cwd: opts.path,
|
|
323
|
+
env
|
|
296
324
|
});
|
|
297
325
|
if (child.stdio[3]) {
|
|
298
326
|
const rl = createInterface({
|
|
@@ -517,16 +545,44 @@ class InvalidateSync {
|
|
|
517
545
|
};
|
|
518
546
|
}
|
|
519
547
|
|
|
548
|
+
function startFileWatcher(file, onFileChange) {
|
|
549
|
+
const abortController = new AbortController();
|
|
550
|
+
void (async () => {
|
|
551
|
+
while (true) {
|
|
552
|
+
try {
|
|
553
|
+
logger.debug(`[FILE_WATCHER] Starting watcher for ${file}`);
|
|
554
|
+
const watcher = watch$1(file, { persistent: true, signal: abortController.signal });
|
|
555
|
+
for await (const event of watcher) {
|
|
556
|
+
if (abortController.signal.aborted) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
logger.debug(`[FILE_WATCHER] File changed: ${file}`);
|
|
560
|
+
onFileChange(file);
|
|
561
|
+
}
|
|
562
|
+
} catch (e) {
|
|
563
|
+
if (abortController.signal.aborted) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
logger.debug(`[FILE_WATCHER] Watch error: ${e.message}, restarting watcher in a second`);
|
|
567
|
+
await delay(1e3);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
})();
|
|
571
|
+
return () => {
|
|
572
|
+
abortController.abort();
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
520
576
|
function createSessionScanner(opts) {
|
|
521
|
-
const
|
|
522
|
-
const projectDir = join(homedir(), ".claude", "projects", projectName);
|
|
577
|
+
const projectDir = getProjectPath(opts.workingDirectory);
|
|
523
578
|
let finishedSessions = /* @__PURE__ */ new Set();
|
|
524
579
|
let pendingSessions = /* @__PURE__ */ new Set();
|
|
525
580
|
let currentSessionId = null;
|
|
526
|
-
let
|
|
581
|
+
let watchers = /* @__PURE__ */ new Map();
|
|
527
582
|
let processedMessages = /* @__PURE__ */ new Set();
|
|
528
583
|
let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
|
|
529
584
|
const sync = new InvalidateSync(async () => {
|
|
585
|
+
logger.debug(`[SESSION_SCANNER] Syncing...`);
|
|
530
586
|
let sessions = [];
|
|
531
587
|
for (let p of pendingSessions) {
|
|
532
588
|
sessions.push(p);
|
|
@@ -540,11 +596,15 @@ function createSessionScanner(opts) {
|
|
|
540
596
|
try {
|
|
541
597
|
file = await readFile(expectedSessionFile, "utf-8");
|
|
542
598
|
} catch (error) {
|
|
599
|
+
logger.debug(`[SESSION_SCANNER] Session file not found: ${expectedSessionFile}`);
|
|
543
600
|
return;
|
|
544
601
|
}
|
|
545
602
|
let lines = file.split("\n");
|
|
546
603
|
for (let l of lines) {
|
|
547
604
|
try {
|
|
605
|
+
if (l.trim() === "") {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
548
608
|
let message = JSON.parse(l);
|
|
549
609
|
let parsed = RawJSONLinesSchema.safeParse(message);
|
|
550
610
|
if (!parsed.success) {
|
|
@@ -567,6 +627,7 @@ function createSessionScanner(opts) {
|
|
|
567
627
|
}
|
|
568
628
|
opts.onMessage(message);
|
|
569
629
|
} catch (e) {
|
|
630
|
+
logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
|
|
570
631
|
continue;
|
|
571
632
|
}
|
|
572
633
|
}
|
|
@@ -580,40 +641,37 @@ function createSessionScanner(opts) {
|
|
|
580
641
|
finishedSessions.add(p);
|
|
581
642
|
}
|
|
582
643
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
try {
|
|
589
|
-
for await (const change of watch$1(sessionFile, { persistent: true, signal: currentSessionWatcherAbortController.signal })) {
|
|
590
|
-
await processSessionFile(currentSessionId);
|
|
591
|
-
}
|
|
592
|
-
} catch (error) {
|
|
593
|
-
if (error.name !== "AbortError") {
|
|
594
|
-
logger.debug(`[SESSION_SCANNER] Watch error: ${error.message}`);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
644
|
+
for (let p of sessions) {
|
|
645
|
+
if (!watchers.has(p)) {
|
|
646
|
+
watchers.set(p, startFileWatcher(join(projectDir, `${p}.jsonl`), () => {
|
|
647
|
+
sync.invalidate();
|
|
648
|
+
}));
|
|
597
649
|
}
|
|
598
|
-
}
|
|
650
|
+
}
|
|
599
651
|
});
|
|
652
|
+
sync.invalidate();
|
|
600
653
|
const intervalId = setInterval(() => {
|
|
601
654
|
sync.invalidate();
|
|
602
655
|
}, 3e3);
|
|
603
656
|
return {
|
|
604
|
-
refresh: () => sync.invalidate(),
|
|
605
657
|
cleanup: () => {
|
|
606
658
|
clearInterval(intervalId);
|
|
607
|
-
|
|
659
|
+
for (let w of watchers.values()) {
|
|
660
|
+
w();
|
|
661
|
+
}
|
|
662
|
+
watchers.clear();
|
|
608
663
|
},
|
|
609
664
|
onNewSession: (sessionId) => {
|
|
610
665
|
if (currentSessionId === sessionId) {
|
|
666
|
+
logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
|
|
611
667
|
return;
|
|
612
668
|
}
|
|
613
669
|
if (finishedSessions.has(sessionId)) {
|
|
670
|
+
logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
|
|
614
671
|
return;
|
|
615
672
|
}
|
|
616
673
|
if (pendingSessions.has(sessionId)) {
|
|
674
|
+
logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
|
|
617
675
|
return;
|
|
618
676
|
}
|
|
619
677
|
if (currentSessionId) {
|
|
@@ -658,7 +716,7 @@ function sortKeys(value) {
|
|
|
658
716
|
}
|
|
659
717
|
|
|
660
718
|
async function loop(opts) {
|
|
661
|
-
let mode = opts.startingMode ?? "
|
|
719
|
+
let mode = opts.startingMode ?? "local";
|
|
662
720
|
let currentMessageQueue = new MessageQueue();
|
|
663
721
|
let sessionId = null;
|
|
664
722
|
let onMessage = null;
|
|
@@ -682,23 +740,38 @@ async function loop(opts) {
|
|
|
682
740
|
};
|
|
683
741
|
while (true) {
|
|
684
742
|
if (currentMessageQueue.size() > 0) {
|
|
685
|
-
mode
|
|
743
|
+
if (mode !== "remote") {
|
|
744
|
+
mode = "remote";
|
|
745
|
+
if (opts.onModeChange) {
|
|
746
|
+
opts.onModeChange(mode);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
686
749
|
continue;
|
|
687
750
|
}
|
|
688
|
-
if (mode === "
|
|
751
|
+
if (mode === "local") {
|
|
689
752
|
let abortedOutside = false;
|
|
690
753
|
const interactiveAbortController = new AbortController();
|
|
691
754
|
opts.session.setHandler("switch", () => {
|
|
692
755
|
if (!interactiveAbortController.signal.aborted) {
|
|
693
756
|
abortedOutside = true;
|
|
694
|
-
mode
|
|
757
|
+
if (mode !== "remote") {
|
|
758
|
+
mode = "remote";
|
|
759
|
+
if (opts.onModeChange) {
|
|
760
|
+
opts.onModeChange(mode);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
695
763
|
interactiveAbortController.abort();
|
|
696
764
|
}
|
|
697
765
|
});
|
|
698
766
|
onMessage = () => {
|
|
699
767
|
if (!interactiveAbortController.signal.aborted) {
|
|
700
768
|
abortedOutside = true;
|
|
701
|
-
mode
|
|
769
|
+
if (mode !== "remote") {
|
|
770
|
+
mode = "remote";
|
|
771
|
+
if (opts.onModeChange) {
|
|
772
|
+
opts.onModeChange(mode);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
702
775
|
interactiveAbortController.abort();
|
|
703
776
|
}
|
|
704
777
|
onMessage = null;
|
|
@@ -707,13 +780,15 @@ async function loop(opts) {
|
|
|
707
780
|
path: opts.path,
|
|
708
781
|
sessionId,
|
|
709
782
|
onSessionFound,
|
|
710
|
-
abort: interactiveAbortController.signal
|
|
783
|
+
abort: interactiveAbortController.signal,
|
|
784
|
+
claudeEnvVars: opts.claudeEnvVars,
|
|
785
|
+
claudeArgs: opts.claudeArgs
|
|
711
786
|
});
|
|
712
787
|
onMessage = null;
|
|
713
788
|
if (!abortedOutside) {
|
|
714
789
|
return;
|
|
715
790
|
}
|
|
716
|
-
if (mode !== "
|
|
791
|
+
if (mode !== "local") {
|
|
717
792
|
console.log("Switching to remote mode...");
|
|
718
793
|
}
|
|
719
794
|
}
|
|
@@ -727,7 +802,12 @@ async function loop(opts) {
|
|
|
727
802
|
});
|
|
728
803
|
const abortHandler = () => {
|
|
729
804
|
if (!remoteAbortController.signal.aborted) {
|
|
730
|
-
mode
|
|
805
|
+
if (mode !== "local") {
|
|
806
|
+
mode = "local";
|
|
807
|
+
if (opts.onModeChange) {
|
|
808
|
+
opts.onModeChange(mode);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
731
811
|
remoteAbortController.abort();
|
|
732
812
|
}
|
|
733
813
|
if (process.stdin.isTTY) {
|
|
@@ -751,7 +831,9 @@ async function loop(opts) {
|
|
|
751
831
|
onSessionFound,
|
|
752
832
|
messages: currentMessageQueue,
|
|
753
833
|
onAssistantResult: opts.onAssistantResult,
|
|
754
|
-
interruptController: opts.interruptController
|
|
834
|
+
interruptController: opts.interruptController,
|
|
835
|
+
claudeEnvVars: opts.claudeEnvVars,
|
|
836
|
+
claudeArgs: opts.claudeArgs
|
|
755
837
|
});
|
|
756
838
|
} finally {
|
|
757
839
|
process.stdin.off("data", abortHandler);
|
|
@@ -868,159 +950,41 @@ class InterruptController {
|
|
|
868
950
|
}
|
|
869
951
|
}
|
|
870
952
|
|
|
871
|
-
var version = "0.1.
|
|
953
|
+
var version = "0.1.11";
|
|
872
954
|
var packageJson = {
|
|
873
955
|
version: version};
|
|
874
956
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
const timeout = requestTimeouts.get(requestId);
|
|
883
|
-
if (timeout) {
|
|
884
|
-
clearTimeout(timeout);
|
|
885
|
-
requestTimeouts.delete(requestId);
|
|
886
|
-
logger.debug(`[AnthropicProxy #${requestId}] Cleaned up (${reason}), active requests: ${requestTimeouts.size}`);
|
|
887
|
-
claudeDidSomeWork();
|
|
888
|
-
}
|
|
889
|
-
};
|
|
890
|
-
const claudeDidSomeWork = () => {
|
|
891
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
892
|
-
if (requestTimeouts.size === 0) {
|
|
893
|
-
idleTimer = setTimeout(() => {
|
|
894
|
-
logger.debug(`[AnthropicProxy] Idle for ${maxTimeBeforeIdle}ms, active requests: ${requestTimeouts.size}`);
|
|
895
|
-
onClaudeActivity("idle");
|
|
896
|
-
}, maxTimeBeforeIdle);
|
|
897
|
-
}
|
|
898
|
-
};
|
|
899
|
-
const server = createServer((req, res) => {
|
|
900
|
-
const requestId = ++requestCounter;
|
|
901
|
-
const isAnthropicRequest = req.headers.host === "api.anthropic.com" || req.url?.includes("anthropic.com");
|
|
902
|
-
if (isAnthropicRequest) {
|
|
903
|
-
const timeout = setTimeout(() => {
|
|
904
|
-
logger.debug(`[AnthropicProxy #${requestId}] Request timeout after ${requestTimeout}ms`);
|
|
905
|
-
cleanupRequest(requestId, "timeout");
|
|
906
|
-
}, requestTimeout);
|
|
907
|
-
requestTimeouts.set(requestId, timeout);
|
|
908
|
-
onClaudeActivity("working");
|
|
909
|
-
logger.debug(`[AnthropicProxy #${requestId}] Anthropic request: ${req.method} ${req.url}, active requests: ${requestTimeouts.size}`);
|
|
910
|
-
}
|
|
911
|
-
const chunks = [];
|
|
912
|
-
req.on("data", (chunk) => {
|
|
913
|
-
chunks.push(chunk);
|
|
914
|
-
if (isAnthropicRequest) {
|
|
915
|
-
claudeDidSomeWork();
|
|
916
|
-
}
|
|
957
|
+
const __dirname = dirname$1(fileURLToPath$1(import.meta.url));
|
|
958
|
+
const RUNNER_PATH = join$1(__dirname, "..", "..", "scripts", "ripgrep_launcher.cjs");
|
|
959
|
+
function run(args, options) {
|
|
960
|
+
return new Promise((resolve, reject) => {
|
|
961
|
+
const child = spawn$1("node", [RUNNER_PATH, JSON.stringify(args)], {
|
|
962
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
963
|
+
cwd: options?.cwd
|
|
917
964
|
});
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
targetUrl = new URL$1(req.url || "/", "https://api.anthropic.com");
|
|
923
|
-
} else {
|
|
924
|
-
const protocol = req.headers["x-forwarded-proto"] || "https";
|
|
925
|
-
const host = req.headers.host || "localhost";
|
|
926
|
-
targetUrl = new URL$1(req.url || "/", `${protocol}://${host}`);
|
|
927
|
-
}
|
|
928
|
-
const options = {
|
|
929
|
-
hostname: targetUrl.hostname,
|
|
930
|
-
port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
|
|
931
|
-
path: targetUrl.pathname + targetUrl.search,
|
|
932
|
-
method: req.method,
|
|
933
|
-
headers: {
|
|
934
|
-
...req.headers,
|
|
935
|
-
host: targetUrl.hostname
|
|
936
|
-
}
|
|
937
|
-
};
|
|
938
|
-
const requestMethod = targetUrl.protocol === "https:" ? request : request$1;
|
|
939
|
-
const proxyReq = requestMethod(options, (proxyRes) => {
|
|
940
|
-
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
941
|
-
proxyRes.pipe(res);
|
|
942
|
-
proxyRes.on("end", () => {
|
|
943
|
-
if (isAnthropicRequest) {
|
|
944
|
-
cleanupRequest(requestId, "completed");
|
|
945
|
-
}
|
|
946
|
-
});
|
|
947
|
-
});
|
|
948
|
-
proxyReq.on("error", (error) => {
|
|
949
|
-
if (isAnthropicRequest) {
|
|
950
|
-
cleanupRequest(requestId, `error: ${error.message}`);
|
|
951
|
-
} else {
|
|
952
|
-
logger.debug(`[AnthropicProxy #${requestId}] Error:`, error.message);
|
|
953
|
-
}
|
|
954
|
-
res.writeHead(502);
|
|
955
|
-
res.end("Bad Gateway");
|
|
956
|
-
});
|
|
957
|
-
if (body.length > 0) {
|
|
958
|
-
proxyReq.write(body);
|
|
959
|
-
}
|
|
960
|
-
proxyReq.end();
|
|
965
|
+
let stdout = "";
|
|
966
|
+
let stderr = "";
|
|
967
|
+
child.stdout.on("data", (data) => {
|
|
968
|
+
stdout += data.toString();
|
|
961
969
|
});
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const requestId = ++requestCounter;
|
|
965
|
-
const [hostname, port] = req.url?.split(":") || ["", "443"];
|
|
966
|
-
const isAnthropicRequest = hostname === "api.anthropic.com";
|
|
967
|
-
if (isAnthropicRequest) {
|
|
968
|
-
const timeout = setTimeout(() => {
|
|
969
|
-
logger.debug(`[AnthropicProxy #${requestId}] CONNECT timeout after ${requestTimeout}ms`);
|
|
970
|
-
cleanupRequest(requestId, "timeout");
|
|
971
|
-
}, requestTimeout);
|
|
972
|
-
requestTimeouts.set(requestId, timeout);
|
|
973
|
-
onClaudeActivity("working");
|
|
974
|
-
logger.debug(`[AnthropicProxy #${requestId}] CONNECT to api.anthropic.com, active requests: ${requestTimeouts.size}`);
|
|
975
|
-
}
|
|
976
|
-
const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
|
|
977
|
-
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
978
|
-
serverSocket.write(head);
|
|
979
|
-
serverSocket.pipe(clientSocket);
|
|
980
|
-
clientSocket.pipe(serverSocket);
|
|
970
|
+
child.stderr.on("data", (data) => {
|
|
971
|
+
stderr += data.toString();
|
|
981
972
|
});
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
logger.debug(`[AnthropicProxy #${requestId}] CONNECT error:`, err.message);
|
|
989
|
-
clientSocket.end();
|
|
990
|
-
cleanup();
|
|
973
|
+
child.on("close", (code) => {
|
|
974
|
+
resolve({
|
|
975
|
+
exitCode: code || 0,
|
|
976
|
+
stdout,
|
|
977
|
+
stderr
|
|
978
|
+
});
|
|
991
979
|
});
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
serverSocket.on("end", cleanup);
|
|
995
|
-
});
|
|
996
|
-
const url = await new Promise((resolve) => {
|
|
997
|
-
server.listen(0, "127.0.0.1", () => {
|
|
998
|
-
const addr = server.address();
|
|
999
|
-
if (addr && typeof addr === "object") {
|
|
1000
|
-
resolve(`http://127.0.0.1:${addr.port}`);
|
|
1001
|
-
}
|
|
980
|
+
child.on("error", (err) => {
|
|
981
|
+
reject(err);
|
|
1002
982
|
});
|
|
1003
983
|
});
|
|
1004
|
-
logger.debug(`[AnthropicProxy] Started at ${url}`);
|
|
1005
|
-
return {
|
|
1006
|
-
url,
|
|
1007
|
-
cleanup: () => {
|
|
1008
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
1009
|
-
for (const [requestId, timeout] of requestTimeouts) {
|
|
1010
|
-
clearTimeout(timeout);
|
|
1011
|
-
logger.debug(`[AnthropicProxy] Cleaning up timeout for request #${requestId}`);
|
|
1012
|
-
}
|
|
1013
|
-
requestTimeouts.clear();
|
|
1014
|
-
if (requestTimeouts.size > 0) {
|
|
1015
|
-
logger.debug(`[AnthropicProxy] Warning: ${requestTimeouts.size} active requests still pending at cleanup:`, Array.from(requestTimeouts.keys()));
|
|
1016
|
-
}
|
|
1017
|
-
server.close();
|
|
1018
|
-
}
|
|
1019
|
-
};
|
|
1020
984
|
}
|
|
1021
985
|
|
|
1022
986
|
const execAsync = promisify(exec);
|
|
1023
|
-
function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
987
|
+
function registerHandlers(session, interruptController, permissionCallbacks, onSwitchRemoteRequested) {
|
|
1024
988
|
session.setHandler("abort", async () => {
|
|
1025
989
|
logger.info("Abort request - interrupting Claude");
|
|
1026
990
|
await interruptController.interrupt();
|
|
@@ -1240,6 +1204,121 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
|
1240
1204
|
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
|
|
1241
1205
|
}
|
|
1242
1206
|
});
|
|
1207
|
+
session.setHandler("ripgrep", async (data) => {
|
|
1208
|
+
logger.info("Ripgrep request with args:", data.args, "cwd:", data.cwd);
|
|
1209
|
+
try {
|
|
1210
|
+
const result = await run(data.args, { cwd: data.cwd });
|
|
1211
|
+
return {
|
|
1212
|
+
success: true,
|
|
1213
|
+
exitCode: result.exitCode,
|
|
1214
|
+
stdout: result.stdout,
|
|
1215
|
+
stderr: result.stderr
|
|
1216
|
+
};
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
logger.debug("Failed to run ripgrep:", error);
|
|
1219
|
+
return {
|
|
1220
|
+
success: false,
|
|
1221
|
+
error: error instanceof Error ? error.message : "Failed to run ripgrep"
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
async function startHTTPDirectProxy(options) {
|
|
1228
|
+
const proxy = httpProxy.createProxyServer({
|
|
1229
|
+
target: options.target,
|
|
1230
|
+
changeOrigin: true,
|
|
1231
|
+
secure: false
|
|
1232
|
+
});
|
|
1233
|
+
proxy.on("error", (err, req, res) => {
|
|
1234
|
+
logger.debug(`[HTTPProxy] Proxy error: ${err.message} for ${req.method} ${req.url}`);
|
|
1235
|
+
if (res instanceof ServerResponse && !res.headersSent) {
|
|
1236
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1237
|
+
res.end("Proxy error");
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
proxy.on("proxyReq", (proxyReq, req, res) => {
|
|
1241
|
+
if (options.onRequest) {
|
|
1242
|
+
options.onRequest(req, proxyReq);
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
proxy.on("proxyRes", (proxyRes, req, res) => {
|
|
1246
|
+
if (options.onResponse) {
|
|
1247
|
+
options.onResponse(req, proxyRes);
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
const server = createServer((req, res) => {
|
|
1251
|
+
proxy.web(req, res);
|
|
1252
|
+
});
|
|
1253
|
+
const url = await new Promise((resolve, reject) => {
|
|
1254
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1255
|
+
const addr = server.address();
|
|
1256
|
+
if (addr && typeof addr === "object") {
|
|
1257
|
+
const proxyUrl = `http://127.0.0.1:${addr.port}`;
|
|
1258
|
+
logger.debug(`[HTTPProxy] Started on ${proxyUrl} --> ${options.target}`);
|
|
1259
|
+
resolve(proxyUrl);
|
|
1260
|
+
} else {
|
|
1261
|
+
reject(new Error("Failed to get server address"));
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
return url;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
async function startClaudeActivityTracker(onThinking) {
|
|
1269
|
+
let requestCounter = 0;
|
|
1270
|
+
const activeRequests = /* @__PURE__ */ new Set();
|
|
1271
|
+
let stopThinkingTimeout = null;
|
|
1272
|
+
let isThinking = false;
|
|
1273
|
+
const proxyUrl = await startHTTPDirectProxy({
|
|
1274
|
+
target: process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
|
|
1275
|
+
onRequest: (req, proxyReq) => {
|
|
1276
|
+
if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
|
|
1277
|
+
const requestId = ++requestCounter;
|
|
1278
|
+
activeRequests.add(requestId);
|
|
1279
|
+
req._requestId = requestId;
|
|
1280
|
+
if (stopThinkingTimeout) {
|
|
1281
|
+
clearTimeout(stopThinkingTimeout);
|
|
1282
|
+
stopThinkingTimeout = null;
|
|
1283
|
+
}
|
|
1284
|
+
if (!isThinking) {
|
|
1285
|
+
logger.debug(`[ClaudeActivityTracker] Thinking started`);
|
|
1286
|
+
isThinking = true;
|
|
1287
|
+
onThinking(true);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1291
|
+
onResponse: (req, proxyRes) => {
|
|
1292
|
+
if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
|
|
1293
|
+
const requestId = req._requestId;
|
|
1294
|
+
proxyRes.on("end", () => {
|
|
1295
|
+
activeRequests.delete(requestId);
|
|
1296
|
+
if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
|
|
1297
|
+
stopThinkingTimeout = setTimeout(() => {
|
|
1298
|
+
if (isThinking) {
|
|
1299
|
+
isThinking = false;
|
|
1300
|
+
logger.debug(`[ClaudeActivityTracker] Thinking stopped`);
|
|
1301
|
+
onThinking(false);
|
|
1302
|
+
}
|
|
1303
|
+
}, 500);
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
proxyRes.on("error", () => {
|
|
1307
|
+
activeRequests.delete(requestId);
|
|
1308
|
+
if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
|
|
1309
|
+
stopThinkingTimeout = setTimeout(() => {
|
|
1310
|
+
if (isThinking) {
|
|
1311
|
+
isThinking = false;
|
|
1312
|
+
logger.debug(`[ClaudeActivityTracker] Thinking stopped`);
|
|
1313
|
+
onThinking(false);
|
|
1314
|
+
}
|
|
1315
|
+
}, 500);
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
return proxyUrl;
|
|
1243
1322
|
}
|
|
1244
1323
|
|
|
1245
1324
|
async function start(credentials, options = {}) {
|
|
@@ -1253,22 +1332,15 @@ async function start(credentials, options = {}) {
|
|
|
1253
1332
|
const session = api.session(response);
|
|
1254
1333
|
const pushClient = api.push();
|
|
1255
1334
|
let thinking = false;
|
|
1335
|
+
let mode = "local";
|
|
1256
1336
|
let pingInterval = setInterval(() => {
|
|
1257
|
-
session.keepAlive(thinking);
|
|
1337
|
+
session.keepAlive(thinking, mode);
|
|
1258
1338
|
}, 2e3);
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
logger.debug(`[PING] Thinking state changed: ${thinking}`);
|
|
1265
|
-
session.keepAlive(thinking);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
);
|
|
1269
|
-
process.env.HTTP_PROXY = antropicActivityProxy.url;
|
|
1270
|
-
process.env.HTTPS_PROXY = antropicActivityProxy.url;
|
|
1271
|
-
logger.debug(`[AnthropicProxy] Set HTTP_PROXY and HTTPS_PROXY to ${antropicActivityProxy.url}`);
|
|
1339
|
+
const proxyUrl = await startClaudeActivityTracker((newThinking) => {
|
|
1340
|
+
thinking = newThinking;
|
|
1341
|
+
session.keepAlive(thinking, mode);
|
|
1342
|
+
});
|
|
1343
|
+
process.env.ANTHROPIC_BASE_URL = proxyUrl;
|
|
1272
1344
|
const logPath = await logger.logFilePathPromise;
|
|
1273
1345
|
logger.infoDeveloper(`Session: ${response.id}`);
|
|
1274
1346
|
logger.infoDeveloper(`Logs: ${logPath}`);
|
|
@@ -1280,10 +1352,10 @@ async function start(credentials, options = {}) {
|
|
|
1280
1352
|
requests.set(id, resolve);
|
|
1281
1353
|
});
|
|
1282
1354
|
let timeout = setTimeout(async () => {
|
|
1283
|
-
logger.
|
|
1355
|
+
logger.debug("Permission timeout - attempting to interrupt Claude");
|
|
1284
1356
|
const interrupted = await interruptController.interrupt();
|
|
1285
1357
|
if (interrupted) {
|
|
1286
|
-
logger.
|
|
1358
|
+
logger.debug("Claude interrupted successfully");
|
|
1287
1359
|
}
|
|
1288
1360
|
requests.delete(id);
|
|
1289
1361
|
session.updateAgentState((currentState) => {
|
|
@@ -1295,7 +1367,7 @@ async function start(credentials, options = {}) {
|
|
|
1295
1367
|
};
|
|
1296
1368
|
});
|
|
1297
1369
|
}, 1e3 * 60 * 4.5);
|
|
1298
|
-
logger.
|
|
1370
|
+
logger.debug("Permission request" + id + " " + JSON.stringify(request));
|
|
1299
1371
|
try {
|
|
1300
1372
|
await pushClient.sendToAllDevices(
|
|
1301
1373
|
"Permission Request",
|
|
@@ -1307,7 +1379,7 @@ async function start(credentials, options = {}) {
|
|
|
1307
1379
|
type: "permission_request"
|
|
1308
1380
|
}
|
|
1309
1381
|
);
|
|
1310
|
-
logger.
|
|
1382
|
+
logger.debug("Push notification sent for permission request");
|
|
1311
1383
|
} catch (error) {
|
|
1312
1384
|
logger.debug("Failed to send push notification:", error);
|
|
1313
1385
|
}
|
|
@@ -1349,6 +1421,15 @@ async function start(credentials, options = {}) {
|
|
|
1349
1421
|
model: options.model,
|
|
1350
1422
|
permissionMode: options.permissionMode,
|
|
1351
1423
|
startingMode: options.startingMode,
|
|
1424
|
+
onModeChange: (newMode) => {
|
|
1425
|
+
mode = newMode;
|
|
1426
|
+
session.sendSessionEvent({ type: "switch", mode: newMode });
|
|
1427
|
+
session.keepAlive(thinking, mode);
|
|
1428
|
+
session.updateAgentState((currentState) => ({
|
|
1429
|
+
...currentState,
|
|
1430
|
+
controlledByUser: newMode === "local" ? true : false
|
|
1431
|
+
}));
|
|
1432
|
+
},
|
|
1352
1433
|
mcpServers: {
|
|
1353
1434
|
"permission": {
|
|
1354
1435
|
type: "http",
|
|
@@ -1358,13 +1439,11 @@ async function start(credentials, options = {}) {
|
|
|
1358
1439
|
permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
|
|
1359
1440
|
session,
|
|
1360
1441
|
onAssistantResult,
|
|
1361
|
-
interruptController
|
|
1442
|
+
interruptController,
|
|
1443
|
+
claudeEnvVars: options.claudeEnvVars,
|
|
1444
|
+
claudeArgs: options.claudeArgs
|
|
1362
1445
|
});
|
|
1363
1446
|
clearInterval(pingInterval);
|
|
1364
|
-
if (antropicActivityProxy) {
|
|
1365
|
-
logger.debug("[AnthropicProxy] Shutting down thinking activity monitoring proxy");
|
|
1366
|
-
antropicActivityProxy.cleanup();
|
|
1367
|
-
}
|
|
1368
1447
|
process.exit(0);
|
|
1369
1448
|
}
|
|
1370
1449
|
|
|
@@ -1614,17 +1693,25 @@ class ApiDaemonSession extends EventEmitter {
|
|
|
1614
1693
|
}
|
|
1615
1694
|
}
|
|
1616
1695
|
|
|
1617
|
-
const DAEMON_PID_FILE = join$1(homedir$1(), ".happy", "daemon-pid");
|
|
1618
1696
|
async function startDaemon() {
|
|
1619
|
-
|
|
1697
|
+
console.log("[DAEMON] Starting daemon process...");
|
|
1698
|
+
if (await isDaemonRunning()) {
|
|
1620
1699
|
console.log("Happy daemon is already running");
|
|
1621
1700
|
process.exit(0);
|
|
1622
1701
|
}
|
|
1623
|
-
|
|
1702
|
+
console.log("[DAEMON] Writing PID file with PID:", process.pid);
|
|
1624
1703
|
writePidFile();
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
process.on("
|
|
1704
|
+
console.log("[DAEMON] PID file written successfully");
|
|
1705
|
+
logger.info("Happy CLI daemon started successfully");
|
|
1706
|
+
process.on("SIGINT", () => {
|
|
1707
|
+
stopDaemon().catch(console.error);
|
|
1708
|
+
});
|
|
1709
|
+
process.on("SIGTERM", () => {
|
|
1710
|
+
stopDaemon().catch(console.error);
|
|
1711
|
+
});
|
|
1712
|
+
process.on("exit", () => {
|
|
1713
|
+
stopDaemon().catch(console.error);
|
|
1714
|
+
});
|
|
1628
1715
|
try {
|
|
1629
1716
|
const settings = await readSettings() || { onboardingCompleted: false };
|
|
1630
1717
|
if (!settings.machineId) {
|
|
@@ -1675,22 +1762,37 @@ async function startDaemon() {
|
|
|
1675
1762
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1676
1763
|
}
|
|
1677
1764
|
}
|
|
1678
|
-
function isDaemonRunning() {
|
|
1765
|
+
async function isDaemonRunning() {
|
|
1679
1766
|
try {
|
|
1680
|
-
if
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1767
|
+
console.log("[isDaemonRunning] Checking if daemon is running...");
|
|
1768
|
+
if (existsSync$1(configuration.daemonPidFile)) {
|
|
1769
|
+
console.log("[isDaemonRunning] PID file exists");
|
|
1770
|
+
const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
|
|
1771
|
+
console.log("[isDaemonRunning] PID from file:", pid);
|
|
1772
|
+
try {
|
|
1773
|
+
process.kill(pid, 0);
|
|
1774
|
+
console.log("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
|
|
1775
|
+
const isHappyDaemon = await isProcessHappyDaemon(pid);
|
|
1776
|
+
console.log("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
|
|
1777
|
+
if (isHappyDaemon) {
|
|
1778
|
+
return true;
|
|
1779
|
+
} else {
|
|
1780
|
+
console.log("[isDaemonRunning] PID is not a happy daemon, cleaning up");
|
|
1781
|
+
logger.debug(`[DAEMON] PID ${pid} is not a happy daemon, cleaning up`);
|
|
1782
|
+
unlinkSync(configuration.daemonPidFile);
|
|
1783
|
+
}
|
|
1784
|
+
} catch (error) {
|
|
1785
|
+
console.log("[isDaemonRunning] Process not running, cleaning up stale PID file");
|
|
1786
|
+
logger.debug("[DAEMON] Process not running, cleaning up stale PID file");
|
|
1787
|
+
unlinkSync(configuration.daemonPidFile);
|
|
1788
|
+
}
|
|
1789
|
+
} else {
|
|
1790
|
+
console.log("[isDaemonRunning] No PID file found");
|
|
1692
1791
|
}
|
|
1693
|
-
|
|
1792
|
+
return false;
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
console.log("[isDaemonRunning] Error:", error);
|
|
1795
|
+
logger.debug("[DAEMON] Error checking daemon status", error);
|
|
1694
1796
|
return false;
|
|
1695
1797
|
}
|
|
1696
1798
|
}
|
|
@@ -1699,19 +1801,54 @@ function writePidFile() {
|
|
|
1699
1801
|
if (!existsSync$1(happyDir)) {
|
|
1700
1802
|
mkdirSync$1(happyDir, { recursive: true });
|
|
1701
1803
|
}
|
|
1702
|
-
|
|
1804
|
+
try {
|
|
1805
|
+
writeFileSync(configuration.daemonPidFile, process.pid.toString(), { flag: "wx" });
|
|
1806
|
+
} catch (error) {
|
|
1807
|
+
if (error.code === "EEXIST") {
|
|
1808
|
+
logger.debug("[DAEMON] PID file already exists, another daemon may be starting");
|
|
1809
|
+
throw new Error("Daemon PID file already exists");
|
|
1810
|
+
}
|
|
1811
|
+
throw error;
|
|
1812
|
+
}
|
|
1703
1813
|
}
|
|
1704
|
-
function stopDaemon() {
|
|
1814
|
+
async function stopDaemon() {
|
|
1705
1815
|
try {
|
|
1706
|
-
if (existsSync$1(
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1816
|
+
if (existsSync$1(configuration.daemonPidFile)) {
|
|
1817
|
+
const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
|
|
1818
|
+
logger.debug(`[DAEMON] Stopping daemon with PID ${pid}`);
|
|
1819
|
+
try {
|
|
1820
|
+
process.kill(pid, "SIGTERM");
|
|
1821
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1822
|
+
try {
|
|
1823
|
+
process.kill(pid, 0);
|
|
1824
|
+
process.kill(pid, "SIGKILL");
|
|
1825
|
+
} catch {
|
|
1826
|
+
}
|
|
1827
|
+
} catch (error) {
|
|
1828
|
+
logger.debug("[DAEMON] Process already dead or inaccessible", error);
|
|
1829
|
+
}
|
|
1830
|
+
unlinkSync(configuration.daemonPidFile);
|
|
1710
1831
|
}
|
|
1711
1832
|
} catch (error) {
|
|
1712
|
-
logger.debug("[DAEMON] Error
|
|
1833
|
+
logger.debug("[DAEMON] Error stopping daemon", error);
|
|
1713
1834
|
}
|
|
1714
1835
|
}
|
|
1836
|
+
async function isProcessHappyDaemon(pid) {
|
|
1837
|
+
return new Promise((resolve) => {
|
|
1838
|
+
const ps = spawn$1("ps", ["-p", pid.toString(), "-o", "command="]);
|
|
1839
|
+
let output = "";
|
|
1840
|
+
ps.stdout.on("data", (data) => {
|
|
1841
|
+
output += data.toString();
|
|
1842
|
+
});
|
|
1843
|
+
ps.on("close", () => {
|
|
1844
|
+
const isHappyDaemon = output.includes("daemon start") && (output.includes("happy") || output.includes("src/index"));
|
|
1845
|
+
resolve(isHappyDaemon);
|
|
1846
|
+
});
|
|
1847
|
+
ps.on("error", () => {
|
|
1848
|
+
resolve(false);
|
|
1849
|
+
});
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1715
1852
|
|
|
1716
1853
|
function trimIdent(text) {
|
|
1717
1854
|
const lines = text.split("\n");
|
|
@@ -1913,7 +2050,18 @@ Currently only supported on macOS.
|
|
|
1913
2050
|
} else if (arg === "--local") {
|
|
1914
2051
|
i++;
|
|
1915
2052
|
} else if (arg === "--happy-starting-mode") {
|
|
1916
|
-
options.startingMode = z$1.enum(["
|
|
2053
|
+
options.startingMode = z$1.enum(["local", "remote"]).parse(args[++i]);
|
|
2054
|
+
} else if (arg === "--claude-env") {
|
|
2055
|
+
const envVar = args[++i];
|
|
2056
|
+
const [key, value] = envVar.split("=", 2);
|
|
2057
|
+
if (!key || value === void 0) {
|
|
2058
|
+
console.error(chalk.red(`Invalid environment variable format: ${envVar}. Use KEY=VALUE`));
|
|
2059
|
+
process.exit(1);
|
|
2060
|
+
}
|
|
2061
|
+
options.claudeEnvVars = { ...options.claudeEnvVars, [key]: value };
|
|
2062
|
+
} else if (arg === "--claude-arg") {
|
|
2063
|
+
const claudeArg = args[++i];
|
|
2064
|
+
options.claudeArgs = [...options.claudeArgs || [], claudeArg];
|
|
1917
2065
|
} else {
|
|
1918
2066
|
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
1919
2067
|
process.exit(1);
|
|
@@ -1926,6 +2074,7 @@ ${chalk.bold("happy")} - Claude Code session sharing
|
|
|
1926
2074
|
${chalk.bold("Usage:")}
|
|
1927
2075
|
happy [options]
|
|
1928
2076
|
happy logout Logs out of your account and removes data directory
|
|
2077
|
+
happy daemon Manage the background daemon (macOS only)
|
|
1929
2078
|
|
|
1930
2079
|
${chalk.bold("Options:")}
|
|
1931
2080
|
-h, --help Show this help message
|
|
@@ -1933,6 +2082,14 @@ ${chalk.bold("Options:")}
|
|
|
1933
2082
|
-m, --model <model> Claude model to use (default: sonnet)
|
|
1934
2083
|
-p, --permission-mode Permission mode: auto, default, or plan
|
|
1935
2084
|
--auth, --login Force re-authentication
|
|
2085
|
+
--claude-env KEY=VALUE Set environment variable for Claude Code
|
|
2086
|
+
--claude-arg ARG Pass additional argument to Claude CLI
|
|
2087
|
+
|
|
2088
|
+
[Daemon Management]
|
|
2089
|
+
--happy-daemon-start Start the daemon in background
|
|
2090
|
+
--happy-daemon-stop Stop the daemon
|
|
2091
|
+
--happy-daemon-install Install daemon to run on startup
|
|
2092
|
+
--happy-daemon-uninstall Uninstall daemon from startup
|
|
1936
2093
|
|
|
1937
2094
|
[Advanced]
|
|
1938
2095
|
--local < global | local >
|
|
@@ -1946,6 +2103,10 @@ ${chalk.bold("Examples:")}
|
|
|
1946
2103
|
happy -m opus Use Claude Opus model
|
|
1947
2104
|
happy -p plan Use plan permission mode
|
|
1948
2105
|
happy --auth Force re-authentication before starting session
|
|
2106
|
+
happy --claude-env KEY=VALUE
|
|
2107
|
+
Set environment variable for Claude Code
|
|
2108
|
+
happy --claude-arg --option
|
|
2109
|
+
Pass argument to Claude CLI
|
|
1949
2110
|
happy logout Logs out of your account and removes data directory
|
|
1950
2111
|
`);
|
|
1951
2112
|
process.exit(0);
|
|
@@ -1962,6 +2123,7 @@ ${chalk.bold("Examples:")}
|
|
|
1962
2123
|
}
|
|
1963
2124
|
credentials = res;
|
|
1964
2125
|
}
|
|
2126
|
+
await readSettings() || { };
|
|
1965
2127
|
try {
|
|
1966
2128
|
await start(credentials, options);
|
|
1967
2129
|
} catch (error) {
|