happy-coder 0.1.10 → 0.1.12
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/bin/happy +1 -0
- package/bin/happy.cmd +1 -0
- package/dist/index-B2GqfEZV.cjs +1564 -0
- package/dist/index-QItBXhux.mjs +1540 -0
- package/dist/index.cjs +585 -279
- package/dist/index.mjs +575 -269
- 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 +22 -3
- package/dist/lib.d.mts +22 -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 +9 -3
- 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", () => {
|
|
@@ -218,18 +243,14 @@ async function claudeRemote(opts) {
|
|
|
218
243
|
try {
|
|
219
244
|
logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
220
245
|
for await (const message of response) {
|
|
221
|
-
logger.
|
|
246
|
+
logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
|
|
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
|
|
527
|
-
let
|
|
528
|
-
let
|
|
581
|
+
let watchers = /* @__PURE__ */ new Map();
|
|
582
|
+
let processedMessageKeys = /* @__PURE__ */ new Set();
|
|
583
|
+
let unmatchedServerMessageContents = /* @__PURE__ */ new Set();
|
|
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) {
|
|
@@ -552,21 +612,22 @@ function createSessionScanner(opts) {
|
|
|
552
612
|
continue;
|
|
553
613
|
}
|
|
554
614
|
let key = getMessageKey(parsed.data);
|
|
555
|
-
if (
|
|
615
|
+
if (processedMessageKeys.has(key)) {
|
|
556
616
|
continue;
|
|
557
617
|
}
|
|
558
|
-
|
|
618
|
+
processedMessageKeys.add(key);
|
|
559
619
|
logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
|
|
560
620
|
logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
|
|
561
621
|
if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
622
|
+
if (unmatchedServerMessageContents.has(parsed.data.message.content)) {
|
|
623
|
+
logger.debug(`[SESSION_SCANNER] Matched server message echo: ${parsed.data.uuid}`);
|
|
624
|
+
unmatchedServerMessageContents.delete(parsed.data.message.content);
|
|
565
625
|
continue;
|
|
566
626
|
}
|
|
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) {
|
|
@@ -624,13 +682,18 @@ function createSessionScanner(opts) {
|
|
|
624
682
|
sync.invalidate();
|
|
625
683
|
},
|
|
626
684
|
onRemoteUserMessageForDeduplication: (messageContent) => {
|
|
627
|
-
|
|
685
|
+
logger.debug(`[SESSION_SCANNER] Adding unmatched server message content: ${messageContent.substring(0, 50)}...`);
|
|
686
|
+
unmatchedServerMessageContents.add(messageContent);
|
|
628
687
|
}
|
|
629
688
|
};
|
|
630
689
|
}
|
|
631
690
|
function getMessageKey(message) {
|
|
632
691
|
if (message.type === "user") {
|
|
633
|
-
|
|
692
|
+
if (Array.isArray(message.message.content) && message.message.content.length > 0 && typeof message.message.content[0] === "object" && "text" in message.message.content[0]) {
|
|
693
|
+
return `user-message-content:${stableStringify(message.message.content[0].text)}`;
|
|
694
|
+
} else {
|
|
695
|
+
return `user-message-content:${stableStringify(message.message.content)}`;
|
|
696
|
+
}
|
|
634
697
|
} else if (message.type === "assistant") {
|
|
635
698
|
const { usage, ...messageWithoutUsage } = message.message;
|
|
636
699
|
return stableStringify(messageWithoutUsage);
|
|
@@ -642,6 +705,9 @@ function getMessageKey(message) {
|
|
|
642
705
|
return `unknown:<error, this should be unreachable>`;
|
|
643
706
|
}
|
|
644
707
|
function stableStringify(obj) {
|
|
708
|
+
if (!obj) {
|
|
709
|
+
return "null";
|
|
710
|
+
}
|
|
645
711
|
return JSON.stringify(sortKeys(obj), null, 2);
|
|
646
712
|
}
|
|
647
713
|
function sortKeys(value) {
|
|
@@ -658,7 +724,7 @@ function sortKeys(value) {
|
|
|
658
724
|
}
|
|
659
725
|
|
|
660
726
|
async function loop(opts) {
|
|
661
|
-
let mode = opts.startingMode ?? "
|
|
727
|
+
let mode = opts.startingMode ?? "local";
|
|
662
728
|
let currentMessageQueue = new MessageQueue();
|
|
663
729
|
let sessionId = null;
|
|
664
730
|
let onMessage = null;
|
|
@@ -682,38 +748,74 @@ async function loop(opts) {
|
|
|
682
748
|
};
|
|
683
749
|
while (true) {
|
|
684
750
|
if (currentMessageQueue.size() > 0) {
|
|
685
|
-
mode
|
|
751
|
+
if (mode !== "remote") {
|
|
752
|
+
mode = "remote";
|
|
753
|
+
if (opts.onModeChange) {
|
|
754
|
+
opts.onModeChange(mode);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
686
757
|
continue;
|
|
687
758
|
}
|
|
688
|
-
if (mode === "
|
|
759
|
+
if (mode === "local") {
|
|
689
760
|
let abortedOutside = false;
|
|
690
761
|
const interactiveAbortController = new AbortController();
|
|
691
762
|
opts.session.setHandler("switch", () => {
|
|
692
763
|
if (!interactiveAbortController.signal.aborted) {
|
|
693
764
|
abortedOutside = true;
|
|
694
|
-
mode
|
|
765
|
+
if (mode !== "remote") {
|
|
766
|
+
mode = "remote";
|
|
767
|
+
if (opts.onModeChange) {
|
|
768
|
+
opts.onModeChange(mode);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
695
771
|
interactiveAbortController.abort();
|
|
696
772
|
}
|
|
697
773
|
});
|
|
774
|
+
opts.session.setHandler("abort", () => {
|
|
775
|
+
if (onMessage) {
|
|
776
|
+
onMessage();
|
|
777
|
+
}
|
|
778
|
+
});
|
|
698
779
|
onMessage = () => {
|
|
699
780
|
if (!interactiveAbortController.signal.aborted) {
|
|
700
781
|
abortedOutside = true;
|
|
701
|
-
mode
|
|
782
|
+
if (mode !== "remote") {
|
|
783
|
+
mode = "remote";
|
|
784
|
+
if (opts.onModeChange) {
|
|
785
|
+
opts.onModeChange(mode);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
opts.session.sendSessionEvent({ type: "message", message: "Inference aborted" });
|
|
702
789
|
interactiveAbortController.abort();
|
|
703
790
|
}
|
|
704
791
|
onMessage = null;
|
|
705
792
|
};
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
793
|
+
try {
|
|
794
|
+
if (opts.onProcessStart) {
|
|
795
|
+
opts.onProcessStart("local");
|
|
796
|
+
}
|
|
797
|
+
await claudeLocal({
|
|
798
|
+
path: opts.path,
|
|
799
|
+
sessionId,
|
|
800
|
+
onSessionFound,
|
|
801
|
+
abort: interactiveAbortController.signal,
|
|
802
|
+
claudeEnvVars: opts.claudeEnvVars,
|
|
803
|
+
claudeArgs: opts.claudeArgs
|
|
804
|
+
});
|
|
805
|
+
} catch (e) {
|
|
806
|
+
if (!interactiveAbortController.signal.aborted) {
|
|
807
|
+
opts.session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
|
|
808
|
+
}
|
|
809
|
+
} finally {
|
|
810
|
+
if (opts.onProcessStop) {
|
|
811
|
+
opts.onProcessStop("local");
|
|
812
|
+
}
|
|
813
|
+
}
|
|
712
814
|
onMessage = null;
|
|
713
815
|
if (!abortedOutside) {
|
|
714
816
|
return;
|
|
715
817
|
}
|
|
716
|
-
if (mode !== "
|
|
818
|
+
if (mode !== "local") {
|
|
717
819
|
console.log("Switching to remote mode...");
|
|
718
820
|
}
|
|
719
821
|
}
|
|
@@ -727,7 +829,13 @@ async function loop(opts) {
|
|
|
727
829
|
});
|
|
728
830
|
const abortHandler = () => {
|
|
729
831
|
if (!remoteAbortController.signal.aborted) {
|
|
730
|
-
mode
|
|
832
|
+
if (mode !== "local") {
|
|
833
|
+
mode = "local";
|
|
834
|
+
if (opts.onModeChange) {
|
|
835
|
+
opts.onModeChange(mode);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
opts.session.sendSessionEvent({ type: "message", message: "Inference aborted" });
|
|
731
839
|
remoteAbortController.abort();
|
|
732
840
|
}
|
|
733
841
|
if (process.stdin.isTTY) {
|
|
@@ -742,6 +850,9 @@ async function loop(opts) {
|
|
|
742
850
|
process.stdin.on("data", abortHandler);
|
|
743
851
|
try {
|
|
744
852
|
logger.debug(`Starting claudeRemote with messages: ${currentMessageQueue.size()}`);
|
|
853
|
+
if (opts.onProcessStart) {
|
|
854
|
+
opts.onProcessStart("remote");
|
|
855
|
+
}
|
|
745
856
|
await claudeRemote({
|
|
746
857
|
abort: remoteAbortController.signal,
|
|
747
858
|
sessionId,
|
|
@@ -751,9 +862,18 @@ async function loop(opts) {
|
|
|
751
862
|
onSessionFound,
|
|
752
863
|
messages: currentMessageQueue,
|
|
753
864
|
onAssistantResult: opts.onAssistantResult,
|
|
754
|
-
interruptController: opts.interruptController
|
|
865
|
+
interruptController: opts.interruptController,
|
|
866
|
+
claudeEnvVars: opts.claudeEnvVars,
|
|
867
|
+
claudeArgs: opts.claudeArgs
|
|
755
868
|
});
|
|
869
|
+
} catch (e) {
|
|
870
|
+
if (!remoteAbortController.signal.aborted) {
|
|
871
|
+
opts.session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
|
|
872
|
+
}
|
|
756
873
|
} finally {
|
|
874
|
+
if (opts.onProcessStop) {
|
|
875
|
+
opts.onProcessStop("remote");
|
|
876
|
+
}
|
|
757
877
|
process.stdin.off("data", abortHandler);
|
|
758
878
|
if (process.stdin.isTTY) {
|
|
759
879
|
process.stdin.setRawMode(false);
|
|
@@ -868,159 +988,41 @@ class InterruptController {
|
|
|
868
988
|
}
|
|
869
989
|
}
|
|
870
990
|
|
|
871
|
-
var version = "0.1.
|
|
991
|
+
var version = "0.1.12";
|
|
872
992
|
var packageJson = {
|
|
873
993
|
version: version};
|
|
874
994
|
|
|
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
|
-
}
|
|
995
|
+
const __dirname = dirname$1(fileURLToPath$1(import.meta.url));
|
|
996
|
+
const RUNNER_PATH = join$1(__dirname, "..", "..", "scripts", "ripgrep_launcher.cjs");
|
|
997
|
+
function run(args, options) {
|
|
998
|
+
return new Promise((resolve, reject) => {
|
|
999
|
+
const child = spawn$1("node", [RUNNER_PATH, JSON.stringify(args)], {
|
|
1000
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1001
|
+
cwd: options?.cwd
|
|
917
1002
|
});
|
|
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();
|
|
1003
|
+
let stdout = "";
|
|
1004
|
+
let stderr = "";
|
|
1005
|
+
child.stdout.on("data", (data) => {
|
|
1006
|
+
stdout += data.toString();
|
|
961
1007
|
});
|
|
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);
|
|
1008
|
+
child.stderr.on("data", (data) => {
|
|
1009
|
+
stderr += data.toString();
|
|
981
1010
|
});
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
logger.debug(`[AnthropicProxy #${requestId}] CONNECT error:`, err.message);
|
|
989
|
-
clientSocket.end();
|
|
990
|
-
cleanup();
|
|
1011
|
+
child.on("close", (code) => {
|
|
1012
|
+
resolve({
|
|
1013
|
+
exitCode: code || 0,
|
|
1014
|
+
stdout,
|
|
1015
|
+
stderr
|
|
1016
|
+
});
|
|
991
1017
|
});
|
|
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
|
-
}
|
|
1018
|
+
child.on("error", (err) => {
|
|
1019
|
+
reject(err);
|
|
1002
1020
|
});
|
|
1003
1021
|
});
|
|
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
1022
|
}
|
|
1021
1023
|
|
|
1022
1024
|
const execAsync = promisify(exec);
|
|
1023
|
-
function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
1025
|
+
function registerHandlers(session, interruptController, permissionCallbacks, onSwitchRemoteRequested) {
|
|
1024
1026
|
session.setHandler("abort", async () => {
|
|
1025
1027
|
logger.info("Abort request - interrupting Claude");
|
|
1026
1028
|
await interruptController.interrupt();
|
|
@@ -1042,11 +1044,22 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
|
1042
1044
|
return;
|
|
1043
1045
|
}
|
|
1044
1046
|
session.updateAgentState((currentState) => {
|
|
1047
|
+
const request = currentState.requests?.[id];
|
|
1048
|
+
if (!request) return currentState;
|
|
1045
1049
|
let r = { ...currentState.requests };
|
|
1046
1050
|
delete r[id];
|
|
1047
1051
|
return {
|
|
1048
1052
|
...currentState,
|
|
1049
|
-
requests: r
|
|
1053
|
+
requests: r,
|
|
1054
|
+
completedRequests: {
|
|
1055
|
+
...currentState.completedRequests,
|
|
1056
|
+
[id]: {
|
|
1057
|
+
...request,
|
|
1058
|
+
completedAt: Date.now(),
|
|
1059
|
+
status: message.approved ? "approved" : "denied",
|
|
1060
|
+
reason: message.reason
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1050
1063
|
};
|
|
1051
1064
|
});
|
|
1052
1065
|
});
|
|
@@ -1240,6 +1253,158 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
|
1240
1253
|
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
|
|
1241
1254
|
}
|
|
1242
1255
|
});
|
|
1256
|
+
session.setHandler("ripgrep", async (data) => {
|
|
1257
|
+
logger.info("Ripgrep request with args:", data.args, "cwd:", data.cwd);
|
|
1258
|
+
try {
|
|
1259
|
+
const result = await run(data.args, { cwd: data.cwd });
|
|
1260
|
+
return {
|
|
1261
|
+
success: true,
|
|
1262
|
+
exitCode: result.exitCode,
|
|
1263
|
+
stdout: result.stdout,
|
|
1264
|
+
stderr: result.stderr
|
|
1265
|
+
};
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
logger.debug("Failed to run ripgrep:", error);
|
|
1268
|
+
return {
|
|
1269
|
+
success: false,
|
|
1270
|
+
error: error instanceof Error ? error.message : "Failed to run ripgrep"
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
async function startHTTPDirectProxy(options) {
|
|
1277
|
+
const proxy = httpProxy.createProxyServer({
|
|
1278
|
+
target: options.target,
|
|
1279
|
+
changeOrigin: true,
|
|
1280
|
+
secure: false
|
|
1281
|
+
});
|
|
1282
|
+
proxy.on("error", (err, req, res) => {
|
|
1283
|
+
logger.debug(`[HTTPProxy] Proxy error: ${err.message} for ${req.method} ${req.url}`);
|
|
1284
|
+
if (res instanceof ServerResponse && !res.headersSent) {
|
|
1285
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1286
|
+
res.end("Proxy error");
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
proxy.on("proxyReq", (proxyReq, req, res) => {
|
|
1290
|
+
if (options.onRequest) {
|
|
1291
|
+
options.onRequest(req, proxyReq);
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
proxy.on("proxyRes", (proxyRes, req, res) => {
|
|
1295
|
+
if (options.onResponse) {
|
|
1296
|
+
options.onResponse(req, proxyRes);
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
const server = createServer((req, res) => {
|
|
1300
|
+
proxy.web(req, res);
|
|
1301
|
+
});
|
|
1302
|
+
const url = await new Promise((resolve, reject) => {
|
|
1303
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1304
|
+
const addr = server.address();
|
|
1305
|
+
if (addr && typeof addr === "object") {
|
|
1306
|
+
const proxyUrl = `http://127.0.0.1:${addr.port}`;
|
|
1307
|
+
logger.debug(`[HTTPProxy] Started on ${proxyUrl} --> ${options.target}`);
|
|
1308
|
+
resolve(proxyUrl);
|
|
1309
|
+
} else {
|
|
1310
|
+
reject(new Error("Failed to get server address"));
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
return url;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
async function startClaudeActivityTracker(onThinking) {
|
|
1318
|
+
logger.debug(`[ClaudeActivityTracker] Starting activity tracker`);
|
|
1319
|
+
let requestCounter = 0;
|
|
1320
|
+
const activeRequests = /* @__PURE__ */ new Map();
|
|
1321
|
+
let stopThinkingTimeout = null;
|
|
1322
|
+
let isThinking = false;
|
|
1323
|
+
const REQUEST_TIMEOUT = 5 * 60 * 1e3;
|
|
1324
|
+
const checkAndStopThinking = () => {
|
|
1325
|
+
if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
|
|
1326
|
+
stopThinkingTimeout = setTimeout(() => {
|
|
1327
|
+
if (isThinking && activeRequests.size === 0) {
|
|
1328
|
+
isThinking = false;
|
|
1329
|
+
onThinking(false);
|
|
1330
|
+
}
|
|
1331
|
+
stopThinkingTimeout = null;
|
|
1332
|
+
}, 500);
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
const proxyUrl = await startHTTPDirectProxy({
|
|
1336
|
+
target: process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
|
|
1337
|
+
onRequest: (req, proxyReq) => {
|
|
1338
|
+
if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
|
|
1339
|
+
const requestId = ++requestCounter;
|
|
1340
|
+
req._requestId = requestId;
|
|
1341
|
+
if (stopThinkingTimeout) {
|
|
1342
|
+
clearTimeout(stopThinkingTimeout);
|
|
1343
|
+
stopThinkingTimeout = null;
|
|
1344
|
+
}
|
|
1345
|
+
const timeout = setTimeout(() => {
|
|
1346
|
+
activeRequests.delete(requestId);
|
|
1347
|
+
checkAndStopThinking();
|
|
1348
|
+
}, REQUEST_TIMEOUT);
|
|
1349
|
+
activeRequests.set(requestId, timeout);
|
|
1350
|
+
if (!isThinking) {
|
|
1351
|
+
isThinking = true;
|
|
1352
|
+
onThinking(true);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
},
|
|
1356
|
+
onResponse: (req, proxyRes) => {
|
|
1357
|
+
proxyRes.headers["connection"] = "close";
|
|
1358
|
+
if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
|
|
1359
|
+
const requestId = req._requestId;
|
|
1360
|
+
const timeout = activeRequests.get(requestId);
|
|
1361
|
+
if (timeout) {
|
|
1362
|
+
clearTimeout(timeout);
|
|
1363
|
+
}
|
|
1364
|
+
let cleaned = false;
|
|
1365
|
+
const cleanupRequest = () => {
|
|
1366
|
+
if (!cleaned) {
|
|
1367
|
+
cleaned = true;
|
|
1368
|
+
activeRequests.delete(requestId);
|
|
1369
|
+
checkAndStopThinking();
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
proxyRes.on("end", () => {
|
|
1373
|
+
cleanupRequest();
|
|
1374
|
+
});
|
|
1375
|
+
proxyRes.on("error", (err) => {
|
|
1376
|
+
cleanupRequest();
|
|
1377
|
+
});
|
|
1378
|
+
proxyRes.on("aborted", () => {
|
|
1379
|
+
cleanupRequest();
|
|
1380
|
+
});
|
|
1381
|
+
proxyRes.on("close", () => {
|
|
1382
|
+
cleanupRequest();
|
|
1383
|
+
});
|
|
1384
|
+
req.on("close", () => {
|
|
1385
|
+
cleanupRequest();
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
const reset = () => {
|
|
1391
|
+
for (const [requestId, timeout] of activeRequests) {
|
|
1392
|
+
clearTimeout(timeout);
|
|
1393
|
+
}
|
|
1394
|
+
activeRequests.clear();
|
|
1395
|
+
if (stopThinkingTimeout) {
|
|
1396
|
+
clearTimeout(stopThinkingTimeout);
|
|
1397
|
+
stopThinkingTimeout = null;
|
|
1398
|
+
}
|
|
1399
|
+
if (isThinking) {
|
|
1400
|
+
isThinking = false;
|
|
1401
|
+
onThinking(false);
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
return {
|
|
1405
|
+
proxyUrl,
|
|
1406
|
+
reset
|
|
1407
|
+
};
|
|
1243
1408
|
}
|
|
1244
1409
|
|
|
1245
1410
|
async function start(credentials, options = {}) {
|
|
@@ -1253,22 +1418,15 @@ async function start(credentials, options = {}) {
|
|
|
1253
1418
|
const session = api.session(response);
|
|
1254
1419
|
const pushClient = api.push();
|
|
1255
1420
|
let thinking = false;
|
|
1421
|
+
let mode = "local";
|
|
1256
1422
|
let pingInterval = setInterval(() => {
|
|
1257
|
-
session.keepAlive(thinking);
|
|
1423
|
+
session.keepAlive(thinking, mode);
|
|
1258
1424
|
}, 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}`);
|
|
1425
|
+
const activityTracker = await startClaudeActivityTracker((newThinking) => {
|
|
1426
|
+
thinking = newThinking;
|
|
1427
|
+
session.keepAlive(thinking, mode);
|
|
1428
|
+
});
|
|
1429
|
+
process.env.ANTHROPIC_BASE_URL = activityTracker.proxyUrl;
|
|
1272
1430
|
const logPath = await logger.logFilePathPromise;
|
|
1273
1431
|
logger.infoDeveloper(`Session: ${response.id}`);
|
|
1274
1432
|
logger.infoDeveloper(`Logs: ${logPath}`);
|
|
@@ -1280,22 +1438,33 @@ async function start(credentials, options = {}) {
|
|
|
1280
1438
|
requests.set(id, resolve);
|
|
1281
1439
|
});
|
|
1282
1440
|
let timeout = setTimeout(async () => {
|
|
1283
|
-
logger.
|
|
1441
|
+
logger.debug("Permission timeout - attempting to interrupt Claude");
|
|
1284
1442
|
const interrupted = await interruptController.interrupt();
|
|
1285
1443
|
if (interrupted) {
|
|
1286
|
-
logger.
|
|
1444
|
+
logger.debug("Claude interrupted successfully");
|
|
1287
1445
|
}
|
|
1288
1446
|
requests.delete(id);
|
|
1289
1447
|
session.updateAgentState((currentState) => {
|
|
1448
|
+
const request2 = currentState.requests?.[id];
|
|
1449
|
+
if (!request2) return currentState;
|
|
1290
1450
|
let r = { ...currentState.requests };
|
|
1291
1451
|
delete r[id];
|
|
1292
1452
|
return {
|
|
1293
1453
|
...currentState,
|
|
1294
|
-
requests: r
|
|
1454
|
+
requests: r,
|
|
1455
|
+
completedRequests: {
|
|
1456
|
+
...currentState.completedRequests,
|
|
1457
|
+
[id]: {
|
|
1458
|
+
...request2,
|
|
1459
|
+
completedAt: Date.now(),
|
|
1460
|
+
status: "canceled",
|
|
1461
|
+
reason: "Timeout"
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1295
1464
|
};
|
|
1296
1465
|
});
|
|
1297
1466
|
}, 1e3 * 60 * 4.5);
|
|
1298
|
-
logger.
|
|
1467
|
+
logger.debug("Permission request" + id + " " + JSON.stringify(request));
|
|
1299
1468
|
try {
|
|
1300
1469
|
await pushClient.sendToAllDevices(
|
|
1301
1470
|
"Permission Request",
|
|
@@ -1307,7 +1476,7 @@ async function start(credentials, options = {}) {
|
|
|
1307
1476
|
type: "permission_request"
|
|
1308
1477
|
}
|
|
1309
1478
|
);
|
|
1310
|
-
logger.
|
|
1479
|
+
logger.debug("Push notification sent for permission request");
|
|
1311
1480
|
} catch (error) {
|
|
1312
1481
|
logger.debug("Failed to send push notification:", error);
|
|
1313
1482
|
}
|
|
@@ -1317,7 +1486,8 @@ async function start(credentials, options = {}) {
|
|
|
1317
1486
|
...currentState.requests,
|
|
1318
1487
|
[id]: {
|
|
1319
1488
|
tool: request.name,
|
|
1320
|
-
arguments: request.arguments
|
|
1489
|
+
arguments: request.arguments,
|
|
1490
|
+
createdAt: Date.now()
|
|
1321
1491
|
}
|
|
1322
1492
|
}
|
|
1323
1493
|
}));
|
|
@@ -1349,6 +1519,63 @@ async function start(credentials, options = {}) {
|
|
|
1349
1519
|
model: options.model,
|
|
1350
1520
|
permissionMode: options.permissionMode,
|
|
1351
1521
|
startingMode: options.startingMode,
|
|
1522
|
+
onModeChange: (newMode) => {
|
|
1523
|
+
mode = newMode;
|
|
1524
|
+
session.sendSessionEvent({ type: "switch", mode: newMode });
|
|
1525
|
+
session.keepAlive(thinking, mode);
|
|
1526
|
+
if (newMode === "local") {
|
|
1527
|
+
logger.debug("Switching to local mode - clearing pending permission requests");
|
|
1528
|
+
for (const [id, resolve] of requests) {
|
|
1529
|
+
logger.debug(`Rejecting pending permission request: ${id}`);
|
|
1530
|
+
resolve({ approved: false, reason: "Session switched to local mode" });
|
|
1531
|
+
}
|
|
1532
|
+
requests.clear();
|
|
1533
|
+
session.updateAgentState((currentState) => {
|
|
1534
|
+
const pendingRequests = currentState.requests || {};
|
|
1535
|
+
const completedRequests = { ...currentState.completedRequests };
|
|
1536
|
+
for (const [id, request] of Object.entries(pendingRequests)) {
|
|
1537
|
+
completedRequests[id] = {
|
|
1538
|
+
...request,
|
|
1539
|
+
completedAt: Date.now(),
|
|
1540
|
+
status: "canceled",
|
|
1541
|
+
reason: "Session switched to local mode"
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
return {
|
|
1545
|
+
...currentState,
|
|
1546
|
+
controlledByUser: true,
|
|
1547
|
+
requests: {},
|
|
1548
|
+
// Clear all pending requests
|
|
1549
|
+
completedRequests
|
|
1550
|
+
};
|
|
1551
|
+
});
|
|
1552
|
+
} else {
|
|
1553
|
+
session.updateAgentState((currentState) => ({
|
|
1554
|
+
...currentState,
|
|
1555
|
+
controlledByUser: false
|
|
1556
|
+
}));
|
|
1557
|
+
}
|
|
1558
|
+
},
|
|
1559
|
+
onProcessStart: (processMode) => {
|
|
1560
|
+
logger.debug(`[Process Lifecycle] Starting ${processMode} mode`);
|
|
1561
|
+
activityTracker.reset();
|
|
1562
|
+
logger.debug("Starting process - clearing any stale permission requests");
|
|
1563
|
+
for (const [id, resolve] of requests) {
|
|
1564
|
+
logger.debug(`Rejecting stale permission request: ${id}`);
|
|
1565
|
+
resolve({ approved: false, reason: "Process restarted" });
|
|
1566
|
+
}
|
|
1567
|
+
requests.clear();
|
|
1568
|
+
},
|
|
1569
|
+
onProcessStop: (processMode) => {
|
|
1570
|
+
logger.debug(`[Process Lifecycle] Stopped ${processMode} mode`);
|
|
1571
|
+
activityTracker.reset();
|
|
1572
|
+
logger.debug("Stopping process - clearing any stale permission requests");
|
|
1573
|
+
for (const [id, resolve] of requests) {
|
|
1574
|
+
logger.debug(`Rejecting stale permission request: ${id}`);
|
|
1575
|
+
resolve({ approved: false, reason: "Process restarted" });
|
|
1576
|
+
}
|
|
1577
|
+
requests.clear();
|
|
1578
|
+
},
|
|
1352
1579
|
mcpServers: {
|
|
1353
1580
|
"permission": {
|
|
1354
1581
|
type: "http",
|
|
@@ -1358,13 +1585,11 @@ async function start(credentials, options = {}) {
|
|
|
1358
1585
|
permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
|
|
1359
1586
|
session,
|
|
1360
1587
|
onAssistantResult,
|
|
1361
|
-
interruptController
|
|
1588
|
+
interruptController,
|
|
1589
|
+
claudeEnvVars: options.claudeEnvVars,
|
|
1590
|
+
claudeArgs: options.claudeArgs
|
|
1362
1591
|
});
|
|
1363
1592
|
clearInterval(pingInterval);
|
|
1364
|
-
if (antropicActivityProxy) {
|
|
1365
|
-
logger.debug("[AnthropicProxy] Shutting down thinking activity monitoring proxy");
|
|
1366
|
-
antropicActivityProxy.cleanup();
|
|
1367
|
-
}
|
|
1368
1593
|
process.exit(0);
|
|
1369
1594
|
}
|
|
1370
1595
|
|
|
@@ -1614,17 +1839,25 @@ class ApiDaemonSession extends EventEmitter {
|
|
|
1614
1839
|
}
|
|
1615
1840
|
}
|
|
1616
1841
|
|
|
1617
|
-
const DAEMON_PID_FILE = join$1(homedir$1(), ".happy", "daemon-pid");
|
|
1618
1842
|
async function startDaemon() {
|
|
1619
|
-
|
|
1843
|
+
console.log("[DAEMON] Starting daemon process...");
|
|
1844
|
+
if (await isDaemonRunning()) {
|
|
1620
1845
|
console.log("Happy daemon is already running");
|
|
1621
1846
|
process.exit(0);
|
|
1622
1847
|
}
|
|
1623
|
-
|
|
1848
|
+
console.log("[DAEMON] Writing PID file with PID:", process.pid);
|
|
1624
1849
|
writePidFile();
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
process.on("
|
|
1850
|
+
console.log("[DAEMON] PID file written successfully");
|
|
1851
|
+
logger.info("Happy CLI daemon started successfully");
|
|
1852
|
+
process.on("SIGINT", () => {
|
|
1853
|
+
stopDaemon().catch(console.error);
|
|
1854
|
+
});
|
|
1855
|
+
process.on("SIGTERM", () => {
|
|
1856
|
+
stopDaemon().catch(console.error);
|
|
1857
|
+
});
|
|
1858
|
+
process.on("exit", () => {
|
|
1859
|
+
stopDaemon().catch(console.error);
|
|
1860
|
+
});
|
|
1628
1861
|
try {
|
|
1629
1862
|
const settings = await readSettings() || { onboardingCompleted: false };
|
|
1630
1863
|
if (!settings.machineId) {
|
|
@@ -1675,22 +1908,37 @@ async function startDaemon() {
|
|
|
1675
1908
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1676
1909
|
}
|
|
1677
1910
|
}
|
|
1678
|
-
function isDaemonRunning() {
|
|
1911
|
+
async function isDaemonRunning() {
|
|
1679
1912
|
try {
|
|
1680
|
-
if
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1913
|
+
console.log("[isDaemonRunning] Checking if daemon is running...");
|
|
1914
|
+
if (existsSync$1(configuration.daemonPidFile)) {
|
|
1915
|
+
console.log("[isDaemonRunning] PID file exists");
|
|
1916
|
+
const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
|
|
1917
|
+
console.log("[isDaemonRunning] PID from file:", pid);
|
|
1918
|
+
try {
|
|
1919
|
+
process.kill(pid, 0);
|
|
1920
|
+
console.log("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
|
|
1921
|
+
const isHappyDaemon = await isProcessHappyDaemon(pid);
|
|
1922
|
+
console.log("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
|
|
1923
|
+
if (isHappyDaemon) {
|
|
1924
|
+
return true;
|
|
1925
|
+
} else {
|
|
1926
|
+
console.log("[isDaemonRunning] PID is not a happy daemon, cleaning up");
|
|
1927
|
+
logger.debug(`[DAEMON] PID ${pid} is not a happy daemon, cleaning up`);
|
|
1928
|
+
unlinkSync(configuration.daemonPidFile);
|
|
1929
|
+
}
|
|
1930
|
+
} catch (error) {
|
|
1931
|
+
console.log("[isDaemonRunning] Process not running, cleaning up stale PID file");
|
|
1932
|
+
logger.debug("[DAEMON] Process not running, cleaning up stale PID file");
|
|
1933
|
+
unlinkSync(configuration.daemonPidFile);
|
|
1934
|
+
}
|
|
1935
|
+
} else {
|
|
1936
|
+
console.log("[isDaemonRunning] No PID file found");
|
|
1692
1937
|
}
|
|
1693
|
-
|
|
1938
|
+
return false;
|
|
1939
|
+
} catch (error) {
|
|
1940
|
+
console.log("[isDaemonRunning] Error:", error);
|
|
1941
|
+
logger.debug("[DAEMON] Error checking daemon status", error);
|
|
1694
1942
|
return false;
|
|
1695
1943
|
}
|
|
1696
1944
|
}
|
|
@@ -1699,19 +1947,54 @@ function writePidFile() {
|
|
|
1699
1947
|
if (!existsSync$1(happyDir)) {
|
|
1700
1948
|
mkdirSync$1(happyDir, { recursive: true });
|
|
1701
1949
|
}
|
|
1702
|
-
|
|
1950
|
+
try {
|
|
1951
|
+
writeFileSync(configuration.daemonPidFile, process.pid.toString(), { flag: "wx" });
|
|
1952
|
+
} catch (error) {
|
|
1953
|
+
if (error.code === "EEXIST") {
|
|
1954
|
+
logger.debug("[DAEMON] PID file already exists, another daemon may be starting");
|
|
1955
|
+
throw new Error("Daemon PID file already exists");
|
|
1956
|
+
}
|
|
1957
|
+
throw error;
|
|
1958
|
+
}
|
|
1703
1959
|
}
|
|
1704
|
-
function stopDaemon() {
|
|
1960
|
+
async function stopDaemon() {
|
|
1705
1961
|
try {
|
|
1706
|
-
if (existsSync$1(
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1962
|
+
if (existsSync$1(configuration.daemonPidFile)) {
|
|
1963
|
+
const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
|
|
1964
|
+
logger.debug(`[DAEMON] Stopping daemon with PID ${pid}`);
|
|
1965
|
+
try {
|
|
1966
|
+
process.kill(pid, "SIGTERM");
|
|
1967
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1968
|
+
try {
|
|
1969
|
+
process.kill(pid, 0);
|
|
1970
|
+
process.kill(pid, "SIGKILL");
|
|
1971
|
+
} catch {
|
|
1972
|
+
}
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
logger.debug("[DAEMON] Process already dead or inaccessible", error);
|
|
1975
|
+
}
|
|
1976
|
+
unlinkSync(configuration.daemonPidFile);
|
|
1710
1977
|
}
|
|
1711
1978
|
} catch (error) {
|
|
1712
|
-
logger.debug("[DAEMON] Error
|
|
1979
|
+
logger.debug("[DAEMON] Error stopping daemon", error);
|
|
1713
1980
|
}
|
|
1714
1981
|
}
|
|
1982
|
+
async function isProcessHappyDaemon(pid) {
|
|
1983
|
+
return new Promise((resolve) => {
|
|
1984
|
+
const ps = spawn$1("ps", ["-p", pid.toString(), "-o", "command="]);
|
|
1985
|
+
let output = "";
|
|
1986
|
+
ps.stdout.on("data", (data) => {
|
|
1987
|
+
output += data.toString();
|
|
1988
|
+
});
|
|
1989
|
+
ps.on("close", () => {
|
|
1990
|
+
const isHappyDaemon = output.includes("daemon start") && (output.includes("happy") || output.includes("src/index"));
|
|
1991
|
+
resolve(isHappyDaemon);
|
|
1992
|
+
});
|
|
1993
|
+
ps.on("error", () => {
|
|
1994
|
+
resolve(false);
|
|
1995
|
+
});
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1715
1998
|
|
|
1716
1999
|
function trimIdent(text) {
|
|
1717
2000
|
const lines = text.split("\n");
|
|
@@ -1910,10 +2193,19 @@ Currently only supported on macOS.
|
|
|
1910
2193
|
options.model = args[++i];
|
|
1911
2194
|
} else if (arg === "-p" || arg === "--permission-mode") {
|
|
1912
2195
|
options.permissionMode = z$1.enum(["auto", "default", "plan"]).parse(args[++i]);
|
|
1913
|
-
} else if (arg === "--local") {
|
|
1914
|
-
i
|
|
1915
|
-
} else if (arg === "--
|
|
1916
|
-
|
|
2196
|
+
} else if (arg === "--local") ; else if (arg === "--happy-starting-mode") {
|
|
2197
|
+
options.startingMode = z$1.enum(["local", "remote"]).parse(args[++i]);
|
|
2198
|
+
} else if (arg === "--claude-env") {
|
|
2199
|
+
const envVar = args[++i];
|
|
2200
|
+
const [key, value] = envVar.split("=", 2);
|
|
2201
|
+
if (!key || value === void 0) {
|
|
2202
|
+
console.error(chalk.red(`Invalid environment variable format: ${envVar}. Use KEY=VALUE`));
|
|
2203
|
+
process.exit(1);
|
|
2204
|
+
}
|
|
2205
|
+
options.claudeEnvVars = { ...options.claudeEnvVars, [key]: value };
|
|
2206
|
+
} else if (arg === "--claude-arg") {
|
|
2207
|
+
const claudeArg = args[++i];
|
|
2208
|
+
options.claudeArgs = [...options.claudeArgs || [], claudeArg];
|
|
1917
2209
|
} else {
|
|
1918
2210
|
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
1919
2211
|
process.exit(1);
|
|
@@ -1926,6 +2218,7 @@ ${chalk.bold("happy")} - Claude Code session sharing
|
|
|
1926
2218
|
${chalk.bold("Usage:")}
|
|
1927
2219
|
happy [options]
|
|
1928
2220
|
happy logout Logs out of your account and removes data directory
|
|
2221
|
+
happy daemon Manage the background daemon (macOS only)
|
|
1929
2222
|
|
|
1930
2223
|
${chalk.bold("Options:")}
|
|
1931
2224
|
-h, --help Show this help message
|
|
@@ -1933,6 +2226,14 @@ ${chalk.bold("Options:")}
|
|
|
1933
2226
|
-m, --model <model> Claude model to use (default: sonnet)
|
|
1934
2227
|
-p, --permission-mode Permission mode: auto, default, or plan
|
|
1935
2228
|
--auth, --login Force re-authentication
|
|
2229
|
+
--claude-env KEY=VALUE Set environment variable for Claude Code
|
|
2230
|
+
--claude-arg ARG Pass additional argument to Claude CLI
|
|
2231
|
+
|
|
2232
|
+
[Daemon Management]
|
|
2233
|
+
--happy-daemon-start Start the daemon in background
|
|
2234
|
+
--happy-daemon-stop Stop the daemon
|
|
2235
|
+
--happy-daemon-install Install daemon to run on startup
|
|
2236
|
+
--happy-daemon-uninstall Uninstall daemon from startup
|
|
1936
2237
|
|
|
1937
2238
|
[Advanced]
|
|
1938
2239
|
--local < global | local >
|
|
@@ -1946,6 +2247,10 @@ ${chalk.bold("Examples:")}
|
|
|
1946
2247
|
happy -m opus Use Claude Opus model
|
|
1947
2248
|
happy -p plan Use plan permission mode
|
|
1948
2249
|
happy --auth Force re-authentication before starting session
|
|
2250
|
+
happy --claude-env KEY=VALUE
|
|
2251
|
+
Set environment variable for Claude Code
|
|
2252
|
+
happy --claude-arg --option
|
|
2253
|
+
Pass argument to Claude CLI
|
|
1949
2254
|
happy logout Logs out of your account and removes data directory
|
|
1950
2255
|
`);
|
|
1951
2256
|
process.exit(0);
|
|
@@ -1962,6 +2267,7 @@ ${chalk.bold("Examples:")}
|
|
|
1962
2267
|
}
|
|
1963
2268
|
credentials = res;
|
|
1964
2269
|
}
|
|
2270
|
+
await readSettings() || { };
|
|
1965
2271
|
try {
|
|
1966
2272
|
await start(credentials, options);
|
|
1967
2273
|
} catch (error) {
|