happy-coder 0.1.9 → 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 +997 -260
- package/dist/index.mjs +996 -259
- package/dist/install-B0DnBGS_.mjs +29 -0
- package/dist/install-C809w0Cj.cjs +31 -0
- package/dist/install-DEPy62QN.mjs +97 -0
- package/dist/install-GZIzyuIE.cjs +99 -0
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +265 -460
- package/dist/lib.d.mts +265 -460
- package/dist/lib.mjs +1 -1
- package/dist/run-BmEaINbl.cjs +250 -0
- package/dist/run-DMbKhYfb.mjs +247 -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-D39L8JSd.mjs +850 -0
- package/dist/types-DYBiuNUQ.cjs +883 -0
- package/dist/types-Df5dlWLV.mjs +871 -0
- package/dist/types-hotUTaWz.cjs +863 -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-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,27 +1,33 @@
|
|
|
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
|
|
18
|
+
import { spawn as spawn$1, exec, execSync } from 'child_process';
|
|
19
|
+
import { promisify } from 'util';
|
|
20
|
+
import crypto, { createHash } from 'crypto';
|
|
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';
|
|
19
24
|
import tweetnacl from 'tweetnacl';
|
|
20
25
|
import axios from 'axios';
|
|
21
26
|
import qrcode from 'qrcode-terminal';
|
|
22
|
-
import '
|
|
23
|
-
import '
|
|
24
|
-
import '
|
|
27
|
+
import { EventEmitter } from 'node:events';
|
|
28
|
+
import { io } from 'socket.io-client';
|
|
29
|
+
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';
|
|
25
31
|
import 'expo-server-sdk';
|
|
26
32
|
|
|
27
33
|
function formatClaudeMessage(message, onAssistantResult) {
|
|
@@ -144,9 +150,13 @@ function printDivider() {
|
|
|
144
150
|
console.log(chalk.gray("\u2550".repeat(60)));
|
|
145
151
|
}
|
|
146
152
|
|
|
153
|
+
function getProjectPath(workingDirectory) {
|
|
154
|
+
const projectId = resolve(workingDirectory).replace(/[\\\/\.:]/g, "-");
|
|
155
|
+
return join(homedir(), ".claude", "projects", projectId);
|
|
156
|
+
}
|
|
157
|
+
|
|
147
158
|
function claudeCheckSession(sessionId, path) {
|
|
148
|
-
const
|
|
149
|
-
const projectDir = join(homedir(), ".claude", "projects", projectName);
|
|
159
|
+
const projectDir = getProjectPath(path);
|
|
150
160
|
const sessionFile = join(projectDir, `${sessionId}.jsonl`);
|
|
151
161
|
const sessionExists = existsSync(sessionFile);
|
|
152
162
|
if (!sessionExists) {
|
|
@@ -164,11 +174,29 @@ function claudeCheckSession(sessionId, path) {
|
|
|
164
174
|
return hasGoodMessage;
|
|
165
175
|
}
|
|
166
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
|
+
|
|
167
190
|
async function claudeRemote(opts) {
|
|
168
191
|
let startFrom = opts.sessionId;
|
|
169
192
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
170
193
|
startFrom = null;
|
|
171
194
|
}
|
|
195
|
+
if (opts.claudeEnvVars) {
|
|
196
|
+
Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
|
|
197
|
+
process.env[key] = value;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
172
200
|
const abortController = new AbortController();
|
|
173
201
|
const sdkOptions = {
|
|
174
202
|
cwd: opts.path,
|
|
@@ -178,6 +206,9 @@ async function claudeRemote(opts) {
|
|
|
178
206
|
executable: "node",
|
|
179
207
|
abortController
|
|
180
208
|
};
|
|
209
|
+
if (opts.claudeArgs && opts.claudeArgs.length > 0) {
|
|
210
|
+
sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
|
|
211
|
+
}
|
|
181
212
|
let aborted = false;
|
|
182
213
|
let response;
|
|
183
214
|
opts.abort.addEventListener("abort", () => {
|
|
@@ -215,15 +246,11 @@ async function claudeRemote(opts) {
|
|
|
215
246
|
logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
|
|
216
247
|
formatClaudeMessage(message, opts.onAssistantResult);
|
|
217
248
|
if (message.type === "system" && message.subtype === "init") {
|
|
218
|
-
|
|
219
|
-
const projectDir =
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
opts.onSessionFound(message.session_id);
|
|
224
|
-
watcher.close();
|
|
225
|
-
}
|
|
226
|
-
});
|
|
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);
|
|
227
254
|
}
|
|
228
255
|
}
|
|
229
256
|
logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
@@ -245,10 +272,9 @@ async function claudeRemote(opts) {
|
|
|
245
272
|
logger.debug(`[claudeRemote] Function completed`);
|
|
246
273
|
}
|
|
247
274
|
|
|
248
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
275
|
+
const __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
249
276
|
async function claudeLocal(opts) {
|
|
250
|
-
const
|
|
251
|
-
const projectDir = join(homedir(), ".claude", "projects", projectName);
|
|
277
|
+
const projectDir = getProjectPath(opts.path);
|
|
252
278
|
mkdirSync(projectDir, { recursive: true });
|
|
253
279
|
const watcher = watch(projectDir);
|
|
254
280
|
let resolvedSessionId = null;
|
|
@@ -282,11 +308,19 @@ async function claudeLocal(opts) {
|
|
|
282
308
|
if (startFrom) {
|
|
283
309
|
args.push("--resume", startFrom);
|
|
284
310
|
}
|
|
285
|
-
|
|
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
|
+
};
|
|
286
319
|
const child = spawn("node", [claudeCliPath, ...args], {
|
|
287
320
|
stdio: ["inherit", "inherit", "inherit", "pipe"],
|
|
288
321
|
signal: opts.abort,
|
|
289
|
-
cwd: opts.path
|
|
322
|
+
cwd: opts.path,
|
|
323
|
+
env
|
|
290
324
|
});
|
|
291
325
|
if (child.stdio[3]) {
|
|
292
326
|
const rl = createInterface({
|
|
@@ -511,16 +545,44 @@ class InvalidateSync {
|
|
|
511
545
|
};
|
|
512
546
|
}
|
|
513
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
|
+
|
|
514
576
|
function createSessionScanner(opts) {
|
|
515
|
-
const
|
|
516
|
-
const projectDir = join(homedir(), ".claude", "projects", projectName);
|
|
577
|
+
const projectDir = getProjectPath(opts.workingDirectory);
|
|
517
578
|
let finishedSessions = /* @__PURE__ */ new Set();
|
|
518
579
|
let pendingSessions = /* @__PURE__ */ new Set();
|
|
519
580
|
let currentSessionId = null;
|
|
520
|
-
let
|
|
581
|
+
let watchers = /* @__PURE__ */ new Map();
|
|
521
582
|
let processedMessages = /* @__PURE__ */ new Set();
|
|
522
583
|
let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
|
|
523
584
|
const sync = new InvalidateSync(async () => {
|
|
585
|
+
logger.debug(`[SESSION_SCANNER] Syncing...`);
|
|
524
586
|
let sessions = [];
|
|
525
587
|
for (let p of pendingSessions) {
|
|
526
588
|
sessions.push(p);
|
|
@@ -534,11 +596,15 @@ function createSessionScanner(opts) {
|
|
|
534
596
|
try {
|
|
535
597
|
file = await readFile(expectedSessionFile, "utf-8");
|
|
536
598
|
} catch (error) {
|
|
599
|
+
logger.debug(`[SESSION_SCANNER] Session file not found: ${expectedSessionFile}`);
|
|
537
600
|
return;
|
|
538
601
|
}
|
|
539
602
|
let lines = file.split("\n");
|
|
540
603
|
for (let l of lines) {
|
|
541
604
|
try {
|
|
605
|
+
if (l.trim() === "") {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
542
608
|
let message = JSON.parse(l);
|
|
543
609
|
let parsed = RawJSONLinesSchema.safeParse(message);
|
|
544
610
|
if (!parsed.success) {
|
|
@@ -552,15 +618,16 @@ function createSessionScanner(opts) {
|
|
|
552
618
|
processedMessages.add(key);
|
|
553
619
|
logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
|
|
554
620
|
logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
|
|
555
|
-
if (parsed.data.type === "user" && typeof parsed.data.message.content === "string") {
|
|
621
|
+
if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
|
|
556
622
|
const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
|
|
557
623
|
if (currentCounter && currentCounter > 0) {
|
|
558
624
|
seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
|
|
559
625
|
continue;
|
|
560
626
|
}
|
|
561
627
|
}
|
|
562
|
-
opts.onMessage(
|
|
628
|
+
opts.onMessage(message);
|
|
563
629
|
} catch (e) {
|
|
630
|
+
logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
|
|
564
631
|
continue;
|
|
565
632
|
}
|
|
566
633
|
}
|
|
@@ -574,40 +641,37 @@ function createSessionScanner(opts) {
|
|
|
574
641
|
finishedSessions.add(p);
|
|
575
642
|
}
|
|
576
643
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
try {
|
|
583
|
-
for await (const change of watch$1(sessionFile, { persistent: true, signal: currentSessionWatcherAbortController.signal })) {
|
|
584
|
-
await processSessionFile(currentSessionId);
|
|
585
|
-
}
|
|
586
|
-
} catch (error) {
|
|
587
|
-
if (error.name !== "AbortError") {
|
|
588
|
-
logger.debug(`[SESSION_SCANNER] Watch error: ${error.message}`);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
644
|
+
for (let p of sessions) {
|
|
645
|
+
if (!watchers.has(p)) {
|
|
646
|
+
watchers.set(p, startFileWatcher(join(projectDir, `${p}.jsonl`), () => {
|
|
647
|
+
sync.invalidate();
|
|
648
|
+
}));
|
|
591
649
|
}
|
|
592
|
-
}
|
|
650
|
+
}
|
|
593
651
|
});
|
|
652
|
+
sync.invalidate();
|
|
594
653
|
const intervalId = setInterval(() => {
|
|
595
654
|
sync.invalidate();
|
|
596
655
|
}, 3e3);
|
|
597
656
|
return {
|
|
598
|
-
refresh: () => sync.invalidate(),
|
|
599
657
|
cleanup: () => {
|
|
600
658
|
clearInterval(intervalId);
|
|
601
|
-
|
|
659
|
+
for (let w of watchers.values()) {
|
|
660
|
+
w();
|
|
661
|
+
}
|
|
662
|
+
watchers.clear();
|
|
602
663
|
},
|
|
603
664
|
onNewSession: (sessionId) => {
|
|
604
665
|
if (currentSessionId === sessionId) {
|
|
666
|
+
logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
|
|
605
667
|
return;
|
|
606
668
|
}
|
|
607
669
|
if (finishedSessions.has(sessionId)) {
|
|
670
|
+
logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
|
|
608
671
|
return;
|
|
609
672
|
}
|
|
610
673
|
if (pendingSessions.has(sessionId)) {
|
|
674
|
+
logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
|
|
611
675
|
return;
|
|
612
676
|
}
|
|
613
677
|
if (currentSessionId) {
|
|
@@ -652,7 +716,7 @@ function sortKeys(value) {
|
|
|
652
716
|
}
|
|
653
717
|
|
|
654
718
|
async function loop(opts) {
|
|
655
|
-
let mode = opts.startingMode ?? "
|
|
719
|
+
let mode = opts.startingMode ?? "local";
|
|
656
720
|
let currentMessageQueue = new MessageQueue();
|
|
657
721
|
let sessionId = null;
|
|
658
722
|
let onMessage = null;
|
|
@@ -676,23 +740,38 @@ async function loop(opts) {
|
|
|
676
740
|
};
|
|
677
741
|
while (true) {
|
|
678
742
|
if (currentMessageQueue.size() > 0) {
|
|
679
|
-
mode
|
|
743
|
+
if (mode !== "remote") {
|
|
744
|
+
mode = "remote";
|
|
745
|
+
if (opts.onModeChange) {
|
|
746
|
+
opts.onModeChange(mode);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
680
749
|
continue;
|
|
681
750
|
}
|
|
682
|
-
if (mode === "
|
|
751
|
+
if (mode === "local") {
|
|
683
752
|
let abortedOutside = false;
|
|
684
753
|
const interactiveAbortController = new AbortController();
|
|
685
754
|
opts.session.setHandler("switch", () => {
|
|
686
755
|
if (!interactiveAbortController.signal.aborted) {
|
|
687
756
|
abortedOutside = true;
|
|
688
|
-
mode
|
|
757
|
+
if (mode !== "remote") {
|
|
758
|
+
mode = "remote";
|
|
759
|
+
if (opts.onModeChange) {
|
|
760
|
+
opts.onModeChange(mode);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
689
763
|
interactiveAbortController.abort();
|
|
690
764
|
}
|
|
691
765
|
});
|
|
692
766
|
onMessage = () => {
|
|
693
767
|
if (!interactiveAbortController.signal.aborted) {
|
|
694
768
|
abortedOutside = true;
|
|
695
|
-
mode
|
|
769
|
+
if (mode !== "remote") {
|
|
770
|
+
mode = "remote";
|
|
771
|
+
if (opts.onModeChange) {
|
|
772
|
+
opts.onModeChange(mode);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
696
775
|
interactiveAbortController.abort();
|
|
697
776
|
}
|
|
698
777
|
onMessage = null;
|
|
@@ -701,13 +780,15 @@ async function loop(opts) {
|
|
|
701
780
|
path: opts.path,
|
|
702
781
|
sessionId,
|
|
703
782
|
onSessionFound,
|
|
704
|
-
abort: interactiveAbortController.signal
|
|
783
|
+
abort: interactiveAbortController.signal,
|
|
784
|
+
claudeEnvVars: opts.claudeEnvVars,
|
|
785
|
+
claudeArgs: opts.claudeArgs
|
|
705
786
|
});
|
|
706
787
|
onMessage = null;
|
|
707
788
|
if (!abortedOutside) {
|
|
708
789
|
return;
|
|
709
790
|
}
|
|
710
|
-
if (mode !== "
|
|
791
|
+
if (mode !== "local") {
|
|
711
792
|
console.log("Switching to remote mode...");
|
|
712
793
|
}
|
|
713
794
|
}
|
|
@@ -721,7 +802,12 @@ async function loop(opts) {
|
|
|
721
802
|
});
|
|
722
803
|
const abortHandler = () => {
|
|
723
804
|
if (!remoteAbortController.signal.aborted) {
|
|
724
|
-
mode
|
|
805
|
+
if (mode !== "local") {
|
|
806
|
+
mode = "local";
|
|
807
|
+
if (opts.onModeChange) {
|
|
808
|
+
opts.onModeChange(mode);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
725
811
|
remoteAbortController.abort();
|
|
726
812
|
}
|
|
727
813
|
if (process.stdin.isTTY) {
|
|
@@ -745,7 +831,9 @@ async function loop(opts) {
|
|
|
745
831
|
onSessionFound,
|
|
746
832
|
messages: currentMessageQueue,
|
|
747
833
|
onAssistantResult: opts.onAssistantResult,
|
|
748
|
-
interruptController: opts.interruptController
|
|
834
|
+
interruptController: opts.interruptController,
|
|
835
|
+
claudeEnvVars: opts.claudeEnvVars,
|
|
836
|
+
claudeArgs: opts.claudeArgs
|
|
749
837
|
});
|
|
750
838
|
} finally {
|
|
751
839
|
process.stdin.off("data", abortHandler);
|
|
@@ -862,155 +950,375 @@ class InterruptController {
|
|
|
862
950
|
}
|
|
863
951
|
}
|
|
864
952
|
|
|
865
|
-
var version = "0.1.
|
|
953
|
+
var version = "0.1.11";
|
|
866
954
|
var packageJson = {
|
|
867
955
|
version: version};
|
|
868
956
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
const requestId = ++requestCounter;
|
|
895
|
-
const isAnthropicRequest = req.headers.host === "api.anthropic.com" || req.url?.includes("anthropic.com");
|
|
896
|
-
if (isAnthropicRequest) {
|
|
897
|
-
const timeout = setTimeout(() => {
|
|
898
|
-
logger.debug(`[AnthropicProxy #${requestId}] Request timeout after ${requestTimeout}ms`);
|
|
899
|
-
cleanupRequest(requestId, "timeout");
|
|
900
|
-
}, requestTimeout);
|
|
901
|
-
requestTimeouts.set(requestId, timeout);
|
|
902
|
-
onClaudeActivity("working");
|
|
903
|
-
logger.debug(`[AnthropicProxy #${requestId}] Anthropic request: ${req.method} ${req.url}, active requests: ${requestTimeouts.size}`);
|
|
904
|
-
}
|
|
905
|
-
const chunks = [];
|
|
906
|
-
req.on("data", (chunk) => {
|
|
907
|
-
chunks.push(chunk);
|
|
908
|
-
if (isAnthropicRequest) {
|
|
909
|
-
claudeDidSomeWork();
|
|
910
|
-
}
|
|
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
|
|
964
|
+
});
|
|
965
|
+
let stdout = "";
|
|
966
|
+
let stderr = "";
|
|
967
|
+
child.stdout.on("data", (data) => {
|
|
968
|
+
stdout += data.toString();
|
|
969
|
+
});
|
|
970
|
+
child.stderr.on("data", (data) => {
|
|
971
|
+
stderr += data.toString();
|
|
972
|
+
});
|
|
973
|
+
child.on("close", (code) => {
|
|
974
|
+
resolve({
|
|
975
|
+
exitCode: code || 0,
|
|
976
|
+
stdout,
|
|
977
|
+
stderr
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
child.on("error", (err) => {
|
|
981
|
+
reject(err);
|
|
911
982
|
});
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const execAsync = promisify(exec);
|
|
987
|
+
function registerHandlers(session, interruptController, permissionCallbacks, onSwitchRemoteRequested) {
|
|
988
|
+
session.setHandler("abort", async () => {
|
|
989
|
+
logger.info("Abort request - interrupting Claude");
|
|
990
|
+
await interruptController.interrupt();
|
|
991
|
+
});
|
|
992
|
+
if (permissionCallbacks) {
|
|
993
|
+
session.setHandler("permission", async (message) => {
|
|
994
|
+
logger.info("Permission response" + JSON.stringify(message));
|
|
995
|
+
const id = message.id;
|
|
996
|
+
const resolve = permissionCallbacks.requests.get(id);
|
|
997
|
+
if (resolve) {
|
|
998
|
+
if (!message.approved) {
|
|
999
|
+
logger.debug("Permission denied, interrupting Claude");
|
|
1000
|
+
await interruptController.interrupt();
|
|
1001
|
+
}
|
|
1002
|
+
resolve({ approved: message.approved, reason: message.reason });
|
|
1003
|
+
permissionCallbacks.requests.delete(id);
|
|
917
1004
|
} else {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
targetUrl = new URL$1(req.url || "/", `${protocol}://${host}`);
|
|
1005
|
+
logger.info("Permission request stale, likely timed out");
|
|
1006
|
+
return;
|
|
921
1007
|
}
|
|
1008
|
+
session.updateAgentState((currentState) => {
|
|
1009
|
+
let r = { ...currentState.requests };
|
|
1010
|
+
delete r[id];
|
|
1011
|
+
return {
|
|
1012
|
+
...currentState,
|
|
1013
|
+
requests: r
|
|
1014
|
+
};
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
session.setHandler("bash", async (data) => {
|
|
1019
|
+
logger.info("Shell command request:", data.command);
|
|
1020
|
+
try {
|
|
922
1021
|
const options = {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
method: req.method,
|
|
927
|
-
headers: {
|
|
928
|
-
...req.headers,
|
|
929
|
-
host: targetUrl.hostname
|
|
930
|
-
}
|
|
1022
|
+
cwd: data.cwd,
|
|
1023
|
+
timeout: data.timeout || 3e4
|
|
1024
|
+
// Default 30 seconds timeout
|
|
931
1025
|
};
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1026
|
+
const { stdout, stderr } = await execAsync(data.command, options);
|
|
1027
|
+
return {
|
|
1028
|
+
success: true,
|
|
1029
|
+
stdout: stdout || "",
|
|
1030
|
+
stderr: stderr || "",
|
|
1031
|
+
exitCode: 0
|
|
1032
|
+
};
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
const execError = error;
|
|
1035
|
+
if (execError.code === "ETIMEDOUT" || execError.killed) {
|
|
1036
|
+
return {
|
|
1037
|
+
success: false,
|
|
1038
|
+
stdout: execError.stdout || "",
|
|
1039
|
+
stderr: execError.stderr || "",
|
|
1040
|
+
exitCode: typeof execError.code === "number" ? execError.code : -1,
|
|
1041
|
+
error: "Command timed out"
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
return {
|
|
1045
|
+
success: false,
|
|
1046
|
+
stdout: execError.stdout || "",
|
|
1047
|
+
stderr: execError.stderr || execError.message || "Command failed",
|
|
1048
|
+
exitCode: typeof execError.code === "number" ? execError.code : 1,
|
|
1049
|
+
error: execError.message || "Command failed"
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
session.setHandler("readFile", async (data) => {
|
|
1054
|
+
logger.info("Read file request:", data.path);
|
|
1055
|
+
try {
|
|
1056
|
+
const buffer = await readFile$1(data.path);
|
|
1057
|
+
const content = buffer.toString("base64");
|
|
1058
|
+
return { success: true, content };
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
logger.debug("Failed to read file:", error);
|
|
1061
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
session.setHandler("writeFile", async (data) => {
|
|
1065
|
+
logger.info("Write file request:", data.path);
|
|
1066
|
+
try {
|
|
1067
|
+
if (data.expectedHash !== null && data.expectedHash !== void 0) {
|
|
1068
|
+
try {
|
|
1069
|
+
const existingBuffer = await readFile$1(data.path);
|
|
1070
|
+
const existingHash = createHash("sha256").update(existingBuffer).digest("hex");
|
|
1071
|
+
if (existingHash !== data.expectedHash) {
|
|
1072
|
+
return {
|
|
1073
|
+
success: false,
|
|
1074
|
+
error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
const nodeError = error;
|
|
1079
|
+
if (nodeError.code !== "ENOENT") {
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
return {
|
|
1083
|
+
success: false,
|
|
1084
|
+
error: "File does not exist but hash was provided"
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
} else {
|
|
1088
|
+
try {
|
|
1089
|
+
await stat(data.path);
|
|
1090
|
+
return {
|
|
1091
|
+
success: false,
|
|
1092
|
+
error: "File already exists but was expected to be new"
|
|
1093
|
+
};
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
const nodeError = error;
|
|
1096
|
+
if (nodeError.code !== "ENOENT") {
|
|
1097
|
+
throw error;
|
|
939
1098
|
}
|
|
940
|
-
});
|
|
941
|
-
});
|
|
942
|
-
proxyReq.on("error", (error) => {
|
|
943
|
-
if (isAnthropicRequest) {
|
|
944
|
-
cleanupRequest(requestId, `error: ${error.message}`);
|
|
945
|
-
} else {
|
|
946
|
-
logger.debug(`[AnthropicProxy #${requestId}] Error:`, error.message);
|
|
947
1099
|
}
|
|
948
|
-
res.writeHead(502);
|
|
949
|
-
res.end("Bad Gateway");
|
|
950
|
-
});
|
|
951
|
-
if (body.length > 0) {
|
|
952
|
-
proxyReq.write(body);
|
|
953
1100
|
}
|
|
954
|
-
|
|
955
|
-
|
|
1101
|
+
const buffer = Buffer.from(data.content, "base64");
|
|
1102
|
+
await writeFile(data.path, buffer);
|
|
1103
|
+
const hash = createHash("sha256").update(buffer).digest("hex");
|
|
1104
|
+
return { success: true, hash };
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
logger.debug("Failed to write file:", error);
|
|
1107
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
|
|
1108
|
+
}
|
|
956
1109
|
});
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1110
|
+
session.setHandler("listDirectory", async (data) => {
|
|
1111
|
+
logger.info("List directory request:", data.path);
|
|
1112
|
+
try {
|
|
1113
|
+
const entries = await readdir(data.path, { withFileTypes: true });
|
|
1114
|
+
const directoryEntries = await Promise.all(
|
|
1115
|
+
entries.map(async (entry) => {
|
|
1116
|
+
const fullPath = join$1(data.path, entry.name);
|
|
1117
|
+
let type = "other";
|
|
1118
|
+
let size;
|
|
1119
|
+
let modified;
|
|
1120
|
+
if (entry.isDirectory()) {
|
|
1121
|
+
type = "directory";
|
|
1122
|
+
} else if (entry.isFile()) {
|
|
1123
|
+
type = "file";
|
|
1124
|
+
}
|
|
1125
|
+
try {
|
|
1126
|
+
const stats = await stat(fullPath);
|
|
1127
|
+
size = stats.size;
|
|
1128
|
+
modified = stats.mtime.getTime();
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
logger.debug(`Failed to stat ${fullPath}:`, error);
|
|
1131
|
+
}
|
|
1132
|
+
return {
|
|
1133
|
+
name: entry.name,
|
|
1134
|
+
type,
|
|
1135
|
+
size,
|
|
1136
|
+
modified
|
|
1137
|
+
};
|
|
1138
|
+
})
|
|
1139
|
+
);
|
|
1140
|
+
directoryEntries.sort((a, b) => {
|
|
1141
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
1142
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
1143
|
+
return a.name.localeCompare(b.name);
|
|
1144
|
+
});
|
|
1145
|
+
return { success: true, entries: directoryEntries };
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
logger.debug("Failed to list directory:", error);
|
|
1148
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
session.setHandler("getDirectoryTree", async (data) => {
|
|
1152
|
+
logger.info("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
|
|
1153
|
+
async function buildTree(path, name, currentDepth) {
|
|
1154
|
+
try {
|
|
1155
|
+
const stats = await stat(path);
|
|
1156
|
+
const node = {
|
|
1157
|
+
name,
|
|
1158
|
+
path,
|
|
1159
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
1160
|
+
size: stats.size,
|
|
1161
|
+
modified: stats.mtime.getTime()
|
|
1162
|
+
};
|
|
1163
|
+
if (stats.isDirectory() && currentDepth < data.maxDepth) {
|
|
1164
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
1165
|
+
const children = [];
|
|
1166
|
+
await Promise.all(
|
|
1167
|
+
entries.map(async (entry) => {
|
|
1168
|
+
if (entry.isSymbolicLink()) {
|
|
1169
|
+
logger.debug(`Skipping symlink: ${join$1(path, entry.name)}`);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const childPath = join$1(path, entry.name);
|
|
1173
|
+
const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
|
|
1174
|
+
if (childNode) {
|
|
1175
|
+
children.push(childNode);
|
|
1176
|
+
}
|
|
1177
|
+
})
|
|
1178
|
+
);
|
|
1179
|
+
children.sort((a, b) => {
|
|
1180
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
1181
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
1182
|
+
return a.name.localeCompare(b.name);
|
|
1183
|
+
});
|
|
1184
|
+
node.children = children;
|
|
1185
|
+
}
|
|
1186
|
+
return node;
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
logger.debug(`Failed to process ${path}:`, error instanceof Error ? error.message : String(error));
|
|
1189
|
+
return null;
|
|
979
1190
|
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1191
|
+
}
|
|
1192
|
+
try {
|
|
1193
|
+
if (data.maxDepth < 0) {
|
|
1194
|
+
return { success: false, error: "maxDepth must be non-negative" };
|
|
1195
|
+
}
|
|
1196
|
+
const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
|
|
1197
|
+
const tree = await buildTree(data.path, baseName, 0);
|
|
1198
|
+
if (!tree) {
|
|
1199
|
+
return { success: false, error: "Failed to access the specified path" };
|
|
1200
|
+
}
|
|
1201
|
+
return { success: true, tree };
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
logger.debug("Failed to get directory tree:", error);
|
|
1204
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
|
|
1205
|
+
}
|
|
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
|
+
}
|
|
989
1224
|
});
|
|
990
|
-
|
|
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) => {
|
|
991
1254
|
server.listen(0, "127.0.0.1", () => {
|
|
992
1255
|
const addr = server.address();
|
|
993
1256
|
if (addr && typeof addr === "object") {
|
|
994
|
-
|
|
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"));
|
|
995
1262
|
}
|
|
996
1263
|
});
|
|
997
1264
|
});
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
+
}
|
|
1006
1289
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
+
});
|
|
1010
1318
|
}
|
|
1011
|
-
server.close();
|
|
1012
1319
|
}
|
|
1013
|
-
};
|
|
1320
|
+
});
|
|
1321
|
+
return proxyUrl;
|
|
1014
1322
|
}
|
|
1015
1323
|
|
|
1016
1324
|
async function start(credentials, options = {}) {
|
|
@@ -1018,30 +1326,23 @@ async function start(credentials, options = {}) {
|
|
|
1018
1326
|
const sessionTag = randomUUID();
|
|
1019
1327
|
const api = new ApiClient(credentials.token, credentials.secret);
|
|
1020
1328
|
let state = {};
|
|
1021
|
-
let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version };
|
|
1329
|
+
let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version, os: os.platform() };
|
|
1022
1330
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
1023
1331
|
logger.debug(`Session created: ${response.id}`);
|
|
1024
1332
|
const session = api.session(response);
|
|
1025
1333
|
const pushClient = api.push();
|
|
1026
1334
|
let thinking = false;
|
|
1335
|
+
let mode = "local";
|
|
1027
1336
|
let pingInterval = setInterval(() => {
|
|
1028
|
-
session.keepAlive(thinking);
|
|
1337
|
+
session.keepAlive(thinking, mode);
|
|
1029
1338
|
}, 2e3);
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
logger.debug(`[PING] Thinking state changed: ${thinking}`);
|
|
1036
|
-
session.keepAlive(thinking);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
);
|
|
1040
|
-
process.env.HTTP_PROXY = antropicActivityProxy.url;
|
|
1041
|
-
process.env.HTTPS_PROXY = antropicActivityProxy.url;
|
|
1042
|
-
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;
|
|
1043
1344
|
const logPath = await logger.logFilePathPromise;
|
|
1044
|
-
logger.
|
|
1345
|
+
logger.infoDeveloper(`Session: ${response.id}`);
|
|
1045
1346
|
logger.infoDeveloper(`Logs: ${logPath}`);
|
|
1046
1347
|
const interruptController = new InterruptController();
|
|
1047
1348
|
let requests = /* @__PURE__ */ new Map();
|
|
@@ -1051,10 +1352,10 @@ async function start(credentials, options = {}) {
|
|
|
1051
1352
|
requests.set(id, resolve);
|
|
1052
1353
|
});
|
|
1053
1354
|
let timeout = setTimeout(async () => {
|
|
1054
|
-
logger.
|
|
1355
|
+
logger.debug("Permission timeout - attempting to interrupt Claude");
|
|
1055
1356
|
const interrupted = await interruptController.interrupt();
|
|
1056
1357
|
if (interrupted) {
|
|
1057
|
-
logger.
|
|
1358
|
+
logger.debug("Claude interrupted successfully");
|
|
1058
1359
|
}
|
|
1059
1360
|
requests.delete(id);
|
|
1060
1361
|
session.updateAgentState((currentState) => {
|
|
@@ -1066,7 +1367,7 @@ async function start(credentials, options = {}) {
|
|
|
1066
1367
|
};
|
|
1067
1368
|
});
|
|
1068
1369
|
}, 1e3 * 60 * 4.5);
|
|
1069
|
-
logger.
|
|
1370
|
+
logger.debug("Permission request" + id + " " + JSON.stringify(request));
|
|
1070
1371
|
try {
|
|
1071
1372
|
await pushClient.sendToAllDevices(
|
|
1072
1373
|
"Permission Request",
|
|
@@ -1078,7 +1379,7 @@ async function start(credentials, options = {}) {
|
|
|
1078
1379
|
type: "permission_request"
|
|
1079
1380
|
}
|
|
1080
1381
|
);
|
|
1081
|
-
logger.
|
|
1382
|
+
logger.debug("Push notification sent for permission request");
|
|
1082
1383
|
} catch (error) {
|
|
1083
1384
|
logger.debug("Failed to send push notification:", error);
|
|
1084
1385
|
}
|
|
@@ -1095,29 +1396,7 @@ async function start(credentials, options = {}) {
|
|
|
1095
1396
|
promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
|
|
1096
1397
|
return promise;
|
|
1097
1398
|
});
|
|
1098
|
-
session
|
|
1099
|
-
logger.info("Permission response" + JSON.stringify(message));
|
|
1100
|
-
const id = message.id;
|
|
1101
|
-
const resolve = requests.get(id);
|
|
1102
|
-
if (resolve) {
|
|
1103
|
-
resolve({ approved: message.approved, reason: message.reason });
|
|
1104
|
-
} else {
|
|
1105
|
-
logger.info("Permission request stale, likely timed out");
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
session.updateAgentState((currentState) => {
|
|
1109
|
-
let r = { ...currentState.requests };
|
|
1110
|
-
delete r[id];
|
|
1111
|
-
return {
|
|
1112
|
-
...currentState,
|
|
1113
|
-
requests: r
|
|
1114
|
-
};
|
|
1115
|
-
});
|
|
1116
|
-
});
|
|
1117
|
-
session.setHandler("abort", async () => {
|
|
1118
|
-
logger.info("Abort request - interrupting Claude");
|
|
1119
|
-
await interruptController.interrupt();
|
|
1120
|
-
});
|
|
1399
|
+
registerHandlers(session, interruptController, { requests });
|
|
1121
1400
|
const onAssistantResult = async (result) => {
|
|
1122
1401
|
try {
|
|
1123
1402
|
const summary = "result" in result && result.result ? result.result.substring(0, 100) + (result.result.length > 100 ? "..." : "") : "";
|
|
@@ -1142,6 +1421,15 @@ async function start(credentials, options = {}) {
|
|
|
1142
1421
|
model: options.model,
|
|
1143
1422
|
permissionMode: options.permissionMode,
|
|
1144
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
|
+
},
|
|
1145
1433
|
mcpServers: {
|
|
1146
1434
|
"permission": {
|
|
1147
1435
|
type: "http",
|
|
@@ -1151,16 +1439,34 @@ async function start(credentials, options = {}) {
|
|
|
1151
1439
|
permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
|
|
1152
1440
|
session,
|
|
1153
1441
|
onAssistantResult,
|
|
1154
|
-
interruptController
|
|
1442
|
+
interruptController,
|
|
1443
|
+
claudeEnvVars: options.claudeEnvVars,
|
|
1444
|
+
claudeArgs: options.claudeArgs
|
|
1155
1445
|
});
|
|
1156
1446
|
clearInterval(pingInterval);
|
|
1157
|
-
if (antropicActivityProxy) {
|
|
1158
|
-
logger.info("[AnthropicProxy] Shutting down activity monitoring proxy");
|
|
1159
|
-
antropicActivityProxy.cleanup();
|
|
1160
|
-
}
|
|
1161
1447
|
process.exit(0);
|
|
1162
1448
|
}
|
|
1163
1449
|
|
|
1450
|
+
const defaultSettings = {
|
|
1451
|
+
onboardingCompleted: false
|
|
1452
|
+
};
|
|
1453
|
+
async function readSettings() {
|
|
1454
|
+
if (!existsSync(configuration.settingsFile)) {
|
|
1455
|
+
return { ...defaultSettings };
|
|
1456
|
+
}
|
|
1457
|
+
try {
|
|
1458
|
+
const content = await readFile(configuration.settingsFile, "utf8");
|
|
1459
|
+
return JSON.parse(content);
|
|
1460
|
+
} catch {
|
|
1461
|
+
return { ...defaultSettings };
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
async function writeSettings(settings) {
|
|
1465
|
+
if (!existsSync(configuration.happyDir)) {
|
|
1466
|
+
await mkdir(configuration.happyDir, { recursive: true });
|
|
1467
|
+
}
|
|
1468
|
+
await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1469
|
+
}
|
|
1164
1470
|
const credentialsSchema = z.object({
|
|
1165
1471
|
secret: z.string().base64(),
|
|
1166
1472
|
token: z.string()
|
|
@@ -1184,7 +1490,7 @@ async function writeCredentials(credentials) {
|
|
|
1184
1490
|
if (!existsSync(configuration.happyDir)) {
|
|
1185
1491
|
await mkdir(configuration.happyDir, { recursive: true });
|
|
1186
1492
|
}
|
|
1187
|
-
await writeFile(configuration.privateKeyFile, JSON.stringify({
|
|
1493
|
+
await writeFile$1(configuration.privateKeyFile, JSON.stringify({
|
|
1188
1494
|
secret: encodeBase64(credentials.secret),
|
|
1189
1495
|
token: credentials.token
|
|
1190
1496
|
}, null, 2));
|
|
@@ -1262,6 +1568,413 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
1262
1568
|
return decrypted;
|
|
1263
1569
|
}
|
|
1264
1570
|
|
|
1571
|
+
class ApiDaemonSession extends EventEmitter {
|
|
1572
|
+
socket;
|
|
1573
|
+
machineIdentity;
|
|
1574
|
+
keepAliveInterval = null;
|
|
1575
|
+
token;
|
|
1576
|
+
secret;
|
|
1577
|
+
constructor(token, secret, machineIdentity) {
|
|
1578
|
+
super();
|
|
1579
|
+
this.token = token;
|
|
1580
|
+
this.secret = secret;
|
|
1581
|
+
this.machineIdentity = machineIdentity;
|
|
1582
|
+
const socket = io(configuration.serverUrl, {
|
|
1583
|
+
auth: {
|
|
1584
|
+
token: this.token,
|
|
1585
|
+
clientType: "machine-scoped",
|
|
1586
|
+
machineId: this.machineIdentity.machineId
|
|
1587
|
+
},
|
|
1588
|
+
path: "/v1/user-machine-daemon",
|
|
1589
|
+
reconnection: true,
|
|
1590
|
+
reconnectionAttempts: Infinity,
|
|
1591
|
+
reconnectionDelay: 1e3,
|
|
1592
|
+
reconnectionDelayMax: 5e3,
|
|
1593
|
+
transports: ["websocket"],
|
|
1594
|
+
withCredentials: true,
|
|
1595
|
+
autoConnect: false
|
|
1596
|
+
});
|
|
1597
|
+
socket.on("connect", () => {
|
|
1598
|
+
logger.debug("[DAEMON] Connected to server");
|
|
1599
|
+
this.emit("connected");
|
|
1600
|
+
socket.emit("machine-connect", {
|
|
1601
|
+
token: this.token,
|
|
1602
|
+
machineIdentity: encodeBase64(encrypt(this.machineIdentity, this.secret))
|
|
1603
|
+
});
|
|
1604
|
+
this.startKeepAlive();
|
|
1605
|
+
});
|
|
1606
|
+
socket.on("disconnect", () => {
|
|
1607
|
+
logger.debug("[DAEMON] Disconnected from server");
|
|
1608
|
+
this.emit("disconnected");
|
|
1609
|
+
this.stopKeepAlive();
|
|
1610
|
+
});
|
|
1611
|
+
socket.on("spawn-session", async (encryptedData, callback) => {
|
|
1612
|
+
let requestData;
|
|
1613
|
+
try {
|
|
1614
|
+
requestData = decrypt(decodeBase64(encryptedData), this.secret);
|
|
1615
|
+
logger.debug("[DAEMON] Received spawn-session request", requestData);
|
|
1616
|
+
const args = [
|
|
1617
|
+
"--directory",
|
|
1618
|
+
requestData.directory,
|
|
1619
|
+
"--happy-starting-mode",
|
|
1620
|
+
requestData.startingMode
|
|
1621
|
+
];
|
|
1622
|
+
if (requestData.metadata) {
|
|
1623
|
+
args.push("--metadata", requestData.metadata);
|
|
1624
|
+
}
|
|
1625
|
+
if (requestData.startingMode === "interactive" && process.platform === "darwin") {
|
|
1626
|
+
const script = `
|
|
1627
|
+
tell application "Terminal"
|
|
1628
|
+
activate
|
|
1629
|
+
do script "cd ${requestData.directory} && happy ${args.join(" ")}"
|
|
1630
|
+
end tell
|
|
1631
|
+
`;
|
|
1632
|
+
spawn$1("osascript", ["-e", script], { detached: true });
|
|
1633
|
+
} else {
|
|
1634
|
+
const child = spawn$1("happy", args, {
|
|
1635
|
+
detached: true,
|
|
1636
|
+
stdio: "ignore",
|
|
1637
|
+
cwd: requestData.directory
|
|
1638
|
+
});
|
|
1639
|
+
child.unref();
|
|
1640
|
+
}
|
|
1641
|
+
const result = { success: true };
|
|
1642
|
+
socket.emit("session-spawn-result", {
|
|
1643
|
+
requestId: requestData.requestId,
|
|
1644
|
+
result: encodeBase64(encrypt(result, this.secret))
|
|
1645
|
+
});
|
|
1646
|
+
callback(encodeBase64(encrypt({ success: true }, this.secret)));
|
|
1647
|
+
} catch (error) {
|
|
1648
|
+
logger.debug("[DAEMON] Failed to spawn session", error);
|
|
1649
|
+
const errorResult = {
|
|
1650
|
+
success: false,
|
|
1651
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1652
|
+
};
|
|
1653
|
+
socket.emit("session-spawn-result", {
|
|
1654
|
+
requestId: requestData?.requestId || "",
|
|
1655
|
+
result: encodeBase64(encrypt(errorResult, this.secret))
|
|
1656
|
+
});
|
|
1657
|
+
callback(encodeBase64(encrypt(errorResult, this.secret)));
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
socket.on("daemon-command", (data) => {
|
|
1661
|
+
switch (data.command) {
|
|
1662
|
+
case "shutdown":
|
|
1663
|
+
this.shutdown();
|
|
1664
|
+
break;
|
|
1665
|
+
case "status":
|
|
1666
|
+
this.emit("status-request");
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
this.socket = socket;
|
|
1671
|
+
}
|
|
1672
|
+
startKeepAlive() {
|
|
1673
|
+
this.stopKeepAlive();
|
|
1674
|
+
this.keepAliveInterval = setInterval(() => {
|
|
1675
|
+
this.socket.volatile.emit("machine-alive", {
|
|
1676
|
+
time: Date.now()
|
|
1677
|
+
});
|
|
1678
|
+
}, 2e4);
|
|
1679
|
+
}
|
|
1680
|
+
stopKeepAlive() {
|
|
1681
|
+
if (this.keepAliveInterval) {
|
|
1682
|
+
clearInterval(this.keepAliveInterval);
|
|
1683
|
+
this.keepAliveInterval = null;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
connect() {
|
|
1687
|
+
this.socket.connect();
|
|
1688
|
+
}
|
|
1689
|
+
shutdown() {
|
|
1690
|
+
this.stopKeepAlive();
|
|
1691
|
+
this.socket.close();
|
|
1692
|
+
this.emit("shutdown");
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
async function startDaemon() {
|
|
1697
|
+
console.log("[DAEMON] Starting daemon process...");
|
|
1698
|
+
if (await isDaemonRunning()) {
|
|
1699
|
+
console.log("Happy daemon is already running");
|
|
1700
|
+
process.exit(0);
|
|
1701
|
+
}
|
|
1702
|
+
console.log("[DAEMON] Writing PID file with PID:", process.pid);
|
|
1703
|
+
writePidFile();
|
|
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
|
+
});
|
|
1715
|
+
try {
|
|
1716
|
+
const settings = await readSettings() || { onboardingCompleted: false };
|
|
1717
|
+
if (!settings.machineId) {
|
|
1718
|
+
settings.machineId = crypto.randomUUID();
|
|
1719
|
+
settings.machineHost = hostname();
|
|
1720
|
+
await writeSettings(settings);
|
|
1721
|
+
}
|
|
1722
|
+
const machineIdentity = {
|
|
1723
|
+
machineId: settings.machineId,
|
|
1724
|
+
machineHost: settings.machineHost || hostname(),
|
|
1725
|
+
platform: process.platform,
|
|
1726
|
+
version: process.env.npm_package_version || "unknown"
|
|
1727
|
+
};
|
|
1728
|
+
let credentials = await readCredentials();
|
|
1729
|
+
if (!credentials) {
|
|
1730
|
+
logger.debug("[DAEMON] No credentials found, running auth");
|
|
1731
|
+
await doAuth();
|
|
1732
|
+
credentials = await readCredentials();
|
|
1733
|
+
if (!credentials) {
|
|
1734
|
+
throw new Error("Failed to authenticate");
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const { token, secret } = credentials;
|
|
1738
|
+
const daemon = new ApiDaemonSession(token, secret, machineIdentity);
|
|
1739
|
+
daemon.on("connected", () => {
|
|
1740
|
+
logger.debug("[DAEMON] Successfully connected to server");
|
|
1741
|
+
});
|
|
1742
|
+
daemon.on("disconnected", () => {
|
|
1743
|
+
logger.debug("[DAEMON] Disconnected from server");
|
|
1744
|
+
});
|
|
1745
|
+
daemon.on("shutdown", () => {
|
|
1746
|
+
logger.debug("[DAEMON] Shutdown requested");
|
|
1747
|
+
stopDaemon();
|
|
1748
|
+
process.exit(0);
|
|
1749
|
+
});
|
|
1750
|
+
daemon.connect();
|
|
1751
|
+
setInterval(() => {
|
|
1752
|
+
}, 1e3);
|
|
1753
|
+
} catch (error) {
|
|
1754
|
+
logger.debug("[DAEMON] Failed to start daemon", error);
|
|
1755
|
+
stopDaemon();
|
|
1756
|
+
process.exit(1);
|
|
1757
|
+
}
|
|
1758
|
+
process.on("SIGINT", () => process.exit(0));
|
|
1759
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
1760
|
+
process.on("exit", () => process.exit(0));
|
|
1761
|
+
while (true) {
|
|
1762
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
async function isDaemonRunning() {
|
|
1766
|
+
try {
|
|
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");
|
|
1791
|
+
}
|
|
1792
|
+
return false;
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
console.log("[isDaemonRunning] Error:", error);
|
|
1795
|
+
logger.debug("[DAEMON] Error checking daemon status", error);
|
|
1796
|
+
return false;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
function writePidFile() {
|
|
1800
|
+
const happyDir = join$1(homedir$1(), ".happy");
|
|
1801
|
+
if (!existsSync$1(happyDir)) {
|
|
1802
|
+
mkdirSync$1(happyDir, { recursive: true });
|
|
1803
|
+
}
|
|
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
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
async function stopDaemon() {
|
|
1815
|
+
try {
|
|
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);
|
|
1831
|
+
}
|
|
1832
|
+
} catch (error) {
|
|
1833
|
+
logger.debug("[DAEMON] Error stopping daemon", error);
|
|
1834
|
+
}
|
|
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
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function trimIdent(text) {
|
|
1854
|
+
const lines = text.split("\n");
|
|
1855
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
1856
|
+
lines.shift();
|
|
1857
|
+
}
|
|
1858
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
1859
|
+
lines.pop();
|
|
1860
|
+
}
|
|
1861
|
+
const minSpaces = lines.reduce((min, line) => {
|
|
1862
|
+
if (line.trim() === "") {
|
|
1863
|
+
return min;
|
|
1864
|
+
}
|
|
1865
|
+
const leadingSpaces = line.match(/^\s*/)[0].length;
|
|
1866
|
+
return Math.min(min, leadingSpaces);
|
|
1867
|
+
}, Infinity);
|
|
1868
|
+
const trimmedLines = lines.map((line) => line.slice(minSpaces));
|
|
1869
|
+
return trimmedLines.join("\n");
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
const PLIST_LABEL$1 = "com.happy-cli.daemon";
|
|
1873
|
+
const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
|
|
1874
|
+
const USER_HOME = process.env.HOME || process.env.USERPROFILE;
|
|
1875
|
+
async function install$1() {
|
|
1876
|
+
try {
|
|
1877
|
+
if (existsSync$1(PLIST_FILE$1)) {
|
|
1878
|
+
logger.info("Daemon plist already exists. Uninstalling first...");
|
|
1879
|
+
execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
1880
|
+
}
|
|
1881
|
+
const happyPath = process.argv[0];
|
|
1882
|
+
const scriptPath = process.argv[1];
|
|
1883
|
+
const plistContent = trimIdent(`
|
|
1884
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
1885
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1886
|
+
<plist version="1.0">
|
|
1887
|
+
<dict>
|
|
1888
|
+
<key>Label</key>
|
|
1889
|
+
<string>${PLIST_LABEL$1}</string>
|
|
1890
|
+
|
|
1891
|
+
<key>ProgramArguments</key>
|
|
1892
|
+
<array>
|
|
1893
|
+
<string>${happyPath}</string>
|
|
1894
|
+
<string>${scriptPath}</string>
|
|
1895
|
+
<string>happy-daemon</string>
|
|
1896
|
+
</array>
|
|
1897
|
+
|
|
1898
|
+
<key>EnvironmentVariables</key>
|
|
1899
|
+
<dict>
|
|
1900
|
+
<key>HAPPY_DAEMON_MODE</key>
|
|
1901
|
+
<string>true</string>
|
|
1902
|
+
</dict>
|
|
1903
|
+
|
|
1904
|
+
<key>RunAtLoad</key>
|
|
1905
|
+
<true/>
|
|
1906
|
+
|
|
1907
|
+
<key>KeepAlive</key>
|
|
1908
|
+
<true/>
|
|
1909
|
+
|
|
1910
|
+
<key>StandardErrorPath</key>
|
|
1911
|
+
<string>${USER_HOME}/.happy/daemon.err</string>
|
|
1912
|
+
|
|
1913
|
+
<key>StandardOutPath</key>
|
|
1914
|
+
<string>${USER_HOME}/.happy/daemon.log</string>
|
|
1915
|
+
|
|
1916
|
+
<key>WorkingDirectory</key>
|
|
1917
|
+
<string>/tmp</string>
|
|
1918
|
+
</dict>
|
|
1919
|
+
</plist>
|
|
1920
|
+
`);
|
|
1921
|
+
writeFileSync(PLIST_FILE$1, plistContent);
|
|
1922
|
+
chmodSync(PLIST_FILE$1, 420);
|
|
1923
|
+
logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
|
|
1924
|
+
execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
1925
|
+
logger.info("Daemon installed and started successfully");
|
|
1926
|
+
logger.info("Check logs at ~/.happy/daemon.log");
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
logger.debug("Failed to install daemon:", error);
|
|
1929
|
+
throw error;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
async function install() {
|
|
1934
|
+
if (process.platform !== "darwin") {
|
|
1935
|
+
throw new Error("Daemon installation is currently only supported on macOS");
|
|
1936
|
+
}
|
|
1937
|
+
if (process.getuid && process.getuid() !== 0) {
|
|
1938
|
+
throw new Error("Daemon installation requires sudo privileges. Please run with sudo.");
|
|
1939
|
+
}
|
|
1940
|
+
logger.info("Installing Happy CLI daemon for macOS...");
|
|
1941
|
+
await install$1();
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
const PLIST_LABEL = "com.happy-cli.daemon";
|
|
1945
|
+
const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
|
|
1946
|
+
async function uninstall$1() {
|
|
1947
|
+
try {
|
|
1948
|
+
if (!existsSync$1(PLIST_FILE)) {
|
|
1949
|
+
logger.info("Daemon plist not found. Nothing to uninstall.");
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
try {
|
|
1953
|
+
execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
|
|
1954
|
+
logger.info("Daemon stopped successfully");
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
logger.info("Failed to unload daemon (it might not be running)");
|
|
1957
|
+
}
|
|
1958
|
+
unlinkSync(PLIST_FILE);
|
|
1959
|
+
logger.info(`Removed daemon plist from ${PLIST_FILE}`);
|
|
1960
|
+
logger.info("Daemon uninstalled successfully");
|
|
1961
|
+
} catch (error) {
|
|
1962
|
+
logger.debug("Failed to uninstall daemon:", error);
|
|
1963
|
+
throw error;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
async function uninstall() {
|
|
1968
|
+
if (process.platform !== "darwin") {
|
|
1969
|
+
throw new Error("Daemon uninstallation is currently only supported on macOS");
|
|
1970
|
+
}
|
|
1971
|
+
if (process.getuid && process.getuid() !== 0) {
|
|
1972
|
+
throw new Error("Daemon uninstallation requires sudo privileges. Please run with sudo.");
|
|
1973
|
+
}
|
|
1974
|
+
logger.info("Uninstalling Happy CLI daemon for macOS...");
|
|
1975
|
+
await uninstall$1();
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1265
1978
|
(async () => {
|
|
1266
1979
|
const args = process.argv.slice(2);
|
|
1267
1980
|
let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
|
|
@@ -1281,39 +1994,40 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
1281
1994
|
}
|
|
1282
1995
|
return;
|
|
1283
1996
|
} else if (subcommand === "daemon") {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
await
|
|
1997
|
+
const daemonSubcommand = args[1];
|
|
1998
|
+
if (daemonSubcommand === "start") {
|
|
1999
|
+
await startDaemon();
|
|
2000
|
+
process.exit(0);
|
|
2001
|
+
} else if (daemonSubcommand === "stop") {
|
|
2002
|
+
await stopDaemon();
|
|
2003
|
+
process.exit(0);
|
|
2004
|
+
} else if (daemonSubcommand === "install") {
|
|
2005
|
+
try {
|
|
2006
|
+
await install();
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
2009
|
+
process.exit(1);
|
|
2010
|
+
}
|
|
2011
|
+
} else if (daemonSubcommand === "uninstall") {
|
|
2012
|
+
try {
|
|
2013
|
+
await uninstall();
|
|
2014
|
+
} catch (error) {
|
|
2015
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
2016
|
+
process.exit(1);
|
|
2017
|
+
}
|
|
1287
2018
|
} else {
|
|
1288
|
-
|
|
1289
|
-
if (daemonSubcommand === "install") {
|
|
1290
|
-
const { install } = await import('./install-HKe7dyS4.mjs');
|
|
1291
|
-
try {
|
|
1292
|
-
await install();
|
|
1293
|
-
} catch (error) {
|
|
1294
|
-
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1295
|
-
process.exit(1);
|
|
1296
|
-
}
|
|
1297
|
-
} else if (daemonSubcommand === "uninstall") {
|
|
1298
|
-
const { uninstall } = await import('./uninstall-CLkTtlMv.mjs');
|
|
1299
|
-
try {
|
|
1300
|
-
await uninstall();
|
|
1301
|
-
} catch (error) {
|
|
1302
|
-
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1303
|
-
process.exit(1);
|
|
1304
|
-
}
|
|
1305
|
-
} else {
|
|
1306
|
-
console.log(`
|
|
2019
|
+
console.log(`
|
|
1307
2020
|
${chalk.bold("happy daemon")} - Daemon management
|
|
1308
2021
|
|
|
1309
2022
|
${chalk.bold("Usage:")}
|
|
2023
|
+
happy daemon start Start the daemon
|
|
2024
|
+
happy daemon stop Stop the daemon
|
|
1310
2025
|
sudo happy daemon install Install the daemon (requires sudo)
|
|
1311
2026
|
sudo happy daemon uninstall Uninstall the daemon (requires sudo)
|
|
1312
2027
|
|
|
1313
2028
|
${chalk.bold("Note:")} The daemon runs in the background and provides persistent services.
|
|
1314
2029
|
Currently only supported on macOS.
|
|
1315
2030
|
`);
|
|
1316
|
-
}
|
|
1317
2031
|
}
|
|
1318
2032
|
return;
|
|
1319
2033
|
} else {
|
|
@@ -1336,7 +2050,18 @@ Currently only supported on macOS.
|
|
|
1336
2050
|
} else if (arg === "--local") {
|
|
1337
2051
|
i++;
|
|
1338
2052
|
} else if (arg === "--happy-starting-mode") {
|
|
1339
|
-
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];
|
|
1340
2065
|
} else {
|
|
1341
2066
|
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
1342
2067
|
process.exit(1);
|
|
@@ -1357,20 +2082,31 @@ ${chalk.bold("Options:")}
|
|
|
1357
2082
|
-m, --model <model> Claude model to use (default: sonnet)
|
|
1358
2083
|
-p, --permission-mode Permission mode: auto, default, or plan
|
|
1359
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
|
|
1360
2093
|
|
|
1361
2094
|
[Advanced]
|
|
1362
2095
|
--local < global | local >
|
|
1363
2096
|
Will use .happy folder in the current directory for storing your private key and debug logs.
|
|
1364
2097
|
You will require re-login each time you run this in a new directory.
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
Default: interactive
|
|
2098
|
+
--happy-starting-mode <interactive|remote>
|
|
2099
|
+
Set the starting mode for new sessions (default: remote)
|
|
1368
2100
|
|
|
1369
2101
|
${chalk.bold("Examples:")}
|
|
1370
2102
|
happy Start a session with default settings
|
|
1371
2103
|
happy -m opus Use Claude Opus model
|
|
1372
2104
|
happy -p plan Use plan permission mode
|
|
1373
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
|
|
1374
2110
|
happy logout Logs out of your account and removes data directory
|
|
1375
2111
|
`);
|
|
1376
2112
|
process.exit(0);
|
|
@@ -1387,6 +2123,7 @@ ${chalk.bold("Examples:")}
|
|
|
1387
2123
|
}
|
|
1388
2124
|
credentials = res;
|
|
1389
2125
|
}
|
|
2126
|
+
await readSettings() || { };
|
|
1390
2127
|
try {
|
|
1391
2128
|
await start(credentials, options);
|
|
1392
2129
|
} catch (error) {
|