happy-coder 0.3.1-beta.1 → 0.4.0

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.
Files changed (49) hide show
  1. package/dist/index.cjs +76 -27
  2. package/dist/index.mjs +76 -27
  3. package/dist/lib.cjs +1 -1
  4. package/dist/lib.d.cts +1 -0
  5. package/dist/lib.d.mts +1 -0
  6. package/dist/lib.mjs +1 -1
  7. package/dist/{types-CzYKFAYa.mjs → types-VkaGP8up.mjs} +1 -1
  8. package/dist/{types-D7u2DxfV.cjs → types-eN-YHsuj.cjs} +1 -0
  9. package/package.json +8 -6
  10. package/dist/index-B2GqfEZV.cjs +0 -1564
  11. package/dist/index-QItBXhux.mjs +0 -1540
  12. package/dist/install-B0DnBGS_.mjs +0 -29
  13. package/dist/install-B2r_gX72.cjs +0 -109
  14. package/dist/install-C809w0Cj.cjs +0 -31
  15. package/dist/install-DEPy62QN.mjs +0 -97
  16. package/dist/install-GZIzyuIE.cjs +0 -99
  17. package/dist/install-HKe7dyS4.mjs +0 -107
  18. package/dist/run-BmEaINbl.cjs +0 -250
  19. package/dist/run-DMbKhYfb.mjs +0 -247
  20. package/dist/run-FBXkmmN7.mjs +0 -32
  21. package/dist/run-q2To6b-c.cjs +0 -34
  22. package/dist/types-BG9AgCI4.mjs +0 -875
  23. package/dist/types-BRICSarm.mjs +0 -870
  24. package/dist/types-BTQRfIr3.cjs +0 -892
  25. package/dist/types-BX4xv8Ty.mjs +0 -881
  26. package/dist/types-BeUppqJU.cjs +0 -886
  27. package/dist/types-C6Wx_bRW.cjs +0 -886
  28. package/dist/types-CEvzGLMI.cjs +0 -882
  29. package/dist/types-CKUdOV6c.mjs +0 -875
  30. package/dist/types-CNuBtNA5.cjs +0 -884
  31. package/dist/types-Cg4664gs.cjs +0 -879
  32. package/dist/types-CkPUFpfr.cjs +0 -885
  33. package/dist/types-D39L8JSd.mjs +0 -850
  34. package/dist/types-DD9P_5rj.mjs +0 -868
  35. package/dist/types-DNu8okOb.mjs +0 -874
  36. package/dist/types-DXK5YldG.cjs +0 -892
  37. package/dist/types-DYBiuNUQ.cjs +0 -883
  38. package/dist/types-Df5dlWLV.mjs +0 -871
  39. package/dist/types-fXgEaaqP.mjs +0 -861
  40. package/dist/types-hotUTaWz.cjs +0 -863
  41. package/dist/types-ikrrEcJm.mjs +0 -873
  42. package/dist/types-mykDX2xe.cjs +0 -872
  43. package/dist/types-tLWMaptR.mjs +0 -879
  44. package/dist/uninstall-BGgl5V8F.mjs +0 -29
  45. package/dist/uninstall-BWHglipH.mjs +0 -40
  46. package/dist/uninstall-C42CoSCI.cjs +0 -53
  47. package/dist/uninstall-CLkTtlMv.mjs +0 -51
  48. package/dist/uninstall-CdHMb6wi.cjs +0 -31
  49. package/dist/uninstall-FXyyAuGU.cjs +0 -42
@@ -1,1540 +0,0 @@
1
- import chalk from 'chalk';
2
- import { l as logger, h as backoff, R as RawJSONLinesSchema, A as ApiClient, c as configuration, e as encodeBase64, j as encodeBase64Url, g as decodeBase64, k as delay, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-BRICSarm.mjs';
3
- import { randomUUID, randomBytes } from 'node:crypto';
4
- import { query, AbortError } from '@anthropic-ai/claude-code';
5
- import { existsSync, readFileSync, mkdirSync, watch, rmSync } from 'node:fs';
6
- import os, { homedir } from 'node:os';
7
- import { resolve, join, dirname } from 'node:path';
8
- import { spawn } from 'node:child_process';
9
- import { createInterface } from 'node:readline';
10
- import { fileURLToPath, URL as URL$1 } from 'node:url';
11
- import { watch as watch$1, readFile, mkdir, writeFile } from 'node:fs/promises';
12
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
- import { createServer, request as request$1 } from 'node:http';
14
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
15
- import * as z from 'zod';
16
- import { z as z$1 } from 'zod';
17
- import { request } from 'node:https';
18
- import net from 'node:net';
19
- import tweetnacl from 'tweetnacl';
20
- import axios from 'axios';
21
- import qrcode from 'qrcode-terminal';
22
-
23
- function formatClaudeMessage(message, onAssistantResult) {
24
- logger.debugLargeJson("[CLAUDE] Message from non interactive & remote mode:", message);
25
- switch (message.type) {
26
- case "system": {
27
- const sysMsg = message;
28
- if (sysMsg.subtype === "init") {
29
- console.log(chalk.gray("\u2500".repeat(60)));
30
- console.log(chalk.blue.bold("\u{1F680} Session initialized:"), chalk.cyan(sysMsg.session_id));
31
- console.log(chalk.gray(` Model: ${sysMsg.model}`));
32
- console.log(chalk.gray(` CWD: ${sysMsg.cwd}`));
33
- if (sysMsg.tools && sysMsg.tools.length > 0) {
34
- console.log(chalk.gray(` Tools: ${sysMsg.tools.join(", ")}`));
35
- }
36
- console.log(chalk.gray("\u2500".repeat(60)));
37
- }
38
- break;
39
- }
40
- case "user": {
41
- const userMsg = message;
42
- if (userMsg.message && typeof userMsg.message === "object" && "content" in userMsg.message) {
43
- const content = userMsg.message.content;
44
- if (typeof content === "string") {
45
- console.log(chalk.magenta.bold("\n\u{1F464} User:"), content);
46
- } else if (Array.isArray(content)) {
47
- for (const block of content) {
48
- if (block.type === "text") {
49
- console.log(chalk.magenta.bold("\n\u{1F464} User:"), block.text);
50
- } else if (block.type === "tool_result") {
51
- console.log(chalk.green.bold("\n\u2705 Tool Result:"), chalk.gray(`(Tool ID: ${block.tool_use_id})`));
52
- if (block.content) {
53
- const outputStr = typeof block.content === "string" ? block.content : JSON.stringify(block.content, null, 2);
54
- const maxLength = 200;
55
- if (outputStr.length > maxLength) {
56
- console.log(outputStr.substring(0, maxLength) + chalk.gray("\n... (truncated)"));
57
- } else {
58
- console.log(outputStr);
59
- }
60
- }
61
- }
62
- }
63
- } else {
64
- console.log(chalk.magenta.bold("\n\u{1F464} User:"), JSON.stringify(content, null, 2));
65
- }
66
- }
67
- break;
68
- }
69
- case "assistant": {
70
- const assistantMsg = message;
71
- if (assistantMsg.message && assistantMsg.message.content) {
72
- console.log(chalk.cyan.bold("\n\u{1F916} Assistant:"));
73
- for (const block of assistantMsg.message.content) {
74
- if (block.type === "text") {
75
- console.log(block.text);
76
- } else if (block.type === "tool_use") {
77
- console.log(chalk.yellow.bold(`
78
- \u{1F527} Tool: ${block.name}`));
79
- if (block.input) {
80
- const inputStr = JSON.stringify(block.input, null, 2);
81
- const maxLength = 500;
82
- if (inputStr.length > maxLength) {
83
- console.log(chalk.gray("Input:"), inputStr.substring(0, maxLength) + chalk.gray("\n... (truncated)"));
84
- } else {
85
- console.log(chalk.gray("Input:"), inputStr);
86
- }
87
- }
88
- }
89
- }
90
- }
91
- break;
92
- }
93
- case "result": {
94
- const resultMsg = message;
95
- if (resultMsg.subtype === "success") {
96
- if ("result" in resultMsg && resultMsg.result) {
97
- console.log(chalk.green.bold("\n\u2728 Summary:"));
98
- console.log(resultMsg.result);
99
- }
100
- if (resultMsg.usage) {
101
- console.log(chalk.gray("\n\u{1F4CA} Session Stats:"));
102
- console.log(chalk.gray(` \u2022 Turns: ${resultMsg.num_turns}`));
103
- console.log(chalk.gray(` \u2022 Input tokens: ${resultMsg.usage.input_tokens}`));
104
- console.log(chalk.gray(` \u2022 Output tokens: ${resultMsg.usage.output_tokens}`));
105
- if (resultMsg.usage.cache_read_input_tokens) {
106
- console.log(chalk.gray(` \u2022 Cache read tokens: ${resultMsg.usage.cache_read_input_tokens}`));
107
- }
108
- if (resultMsg.usage.cache_creation_input_tokens) {
109
- console.log(chalk.gray(` \u2022 Cache creation tokens: ${resultMsg.usage.cache_creation_input_tokens}`));
110
- }
111
- console.log(chalk.gray(` \u2022 Cost: $${resultMsg.total_cost_usd.toFixed(4)}`));
112
- console.log(chalk.gray(` \u2022 Duration: ${resultMsg.duration_ms}ms`));
113
- console.log(chalk.gray("\n\u{1F440} Back already?"));
114
- console.log(chalk.green("\u{1F449} Press any key to continue your session in `claude`"));
115
- if (onAssistantResult) {
116
- Promise.resolve(onAssistantResult(resultMsg)).catch((err) => {
117
- logger.debug("Error in onAssistantResult callback:", err);
118
- });
119
- }
120
- }
121
- } else if (resultMsg.subtype === "error_max_turns") {
122
- console.log(chalk.red.bold("\n\u274C Error: Maximum turns reached"));
123
- console.log(chalk.gray(`Completed ${resultMsg.num_turns} turns`));
124
- } else if (resultMsg.subtype === "error_during_execution") {
125
- console.log(chalk.red.bold("\n\u274C Error during execution"));
126
- console.log(chalk.gray(`Completed ${resultMsg.num_turns} turns before error`));
127
- logger.debugLargeJson("[RESULT] Error during execution", resultMsg);
128
- }
129
- break;
130
- }
131
- default: {
132
- const exhaustiveCheck = message;
133
- if (process.env.DEBUG) {
134
- console.log(chalk.gray(`[Unknown message type]`), exhaustiveCheck);
135
- }
136
- }
137
- }
138
- }
139
- function printDivider() {
140
- console.log(chalk.gray("\u2550".repeat(60)));
141
- }
142
-
143
- function claudeCheckSession(sessionId, path) {
144
- const projectName = resolve(path).replace(/\//g, "-");
145
- const projectDir = join(homedir(), ".claude", "projects", projectName);
146
- const sessionFile = join(projectDir, `${sessionId}.jsonl`);
147
- const sessionExists = existsSync(sessionFile);
148
- if (!sessionExists) {
149
- logger.debug(`[claudeCheckSession] Path ${sessionFile} does not exist`);
150
- return false;
151
- }
152
- const sessionData = readFileSync(sessionFile, "utf-8").split("\n");
153
- const hasGoodMessage = !!sessionData.find((v) => {
154
- try {
155
- return typeof JSON.parse(v).uuid === "string";
156
- } catch (e) {
157
- return false;
158
- }
159
- });
160
- return hasGoodMessage;
161
- }
162
-
163
- async function claudeRemote(opts) {
164
- let startFrom = opts.sessionId;
165
- if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
166
- startFrom = null;
167
- }
168
- const abortController = new AbortController();
169
- const sdkOptions = {
170
- cwd: opts.path,
171
- resume: startFrom ?? void 0,
172
- mcpServers: opts.mcpServers,
173
- permissionPromptToolName: opts.permissionPromptToolName,
174
- executable: "node",
175
- abortController
176
- };
177
- let aborted = false;
178
- let response;
179
- opts.abort.addEventListener("abort", () => {
180
- if (!aborted) {
181
- aborted = true;
182
- if (response) {
183
- (async () => {
184
- try {
185
- const r = await response.interrupt();
186
- } catch (e) {
187
- }
188
- abortController.abort();
189
- })();
190
- } else {
191
- abortController.abort();
192
- }
193
- }
194
- });
195
- logger.debug(`[claudeRemote] Starting query with messages`);
196
- response = query({
197
- prompt: opts.messages,
198
- abortController,
199
- options: sdkOptions
200
- });
201
- if (opts.interruptController) {
202
- opts.interruptController.register(async () => {
203
- logger.debug("[claudeRemote] Interrupting Claude via SDK");
204
- await response.interrupt();
205
- });
206
- }
207
- printDivider();
208
- try {
209
- logger.debug(`[claudeRemote] Starting to iterate over response`);
210
- for await (const message of response) {
211
- logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
212
- formatClaudeMessage(message, opts.onAssistantResult);
213
- if (message.type === "system" && message.subtype === "init") {
214
- const projectName = resolve(opts.path).replace(/\//g, "-");
215
- const projectDir = join(homedir(), ".claude", "projects", projectName);
216
- mkdirSync(projectDir, { recursive: true });
217
- const watcher = watch(projectDir).on("change", (_, filename) => {
218
- if (filename === `${message.session_id}.jsonl`) {
219
- opts.onSessionFound(message.session_id);
220
- watcher.close();
221
- }
222
- });
223
- }
224
- }
225
- logger.debug(`[claudeRemote] Finished iterating over response`);
226
- } catch (e) {
227
- if (abortController.signal.aborted) {
228
- logger.debug(`[claudeRemote] Aborted`);
229
- }
230
- if (e instanceof AbortError) {
231
- logger.debug(`[claudeRemote] Aborted`);
232
- } else {
233
- throw e;
234
- }
235
- } finally {
236
- if (opts.interruptController) {
237
- opts.interruptController.unregister();
238
- }
239
- }
240
- printDivider();
241
- logger.debug(`[claudeRemote] Function completed`);
242
- }
243
-
244
- const __dirname = dirname(fileURLToPath(import.meta.url));
245
- async function claudeLocal(opts) {
246
- const projectName = resolve(opts.path).replace(/\//g, "-");
247
- const projectDir = join(homedir(), ".claude", "projects", projectName);
248
- mkdirSync(projectDir, { recursive: true });
249
- const watcher = watch(projectDir);
250
- let resolvedSessionId = null;
251
- const detectedIdsRandomUUID = /* @__PURE__ */ new Set();
252
- const detectedIdsFileSystem = /* @__PURE__ */ new Set();
253
- watcher.on("change", (event, filename) => {
254
- if (typeof filename === "string" && filename.toLowerCase().endsWith(".jsonl")) {
255
- logger.debug("change", event, filename);
256
- const sessionId = filename.replace(".jsonl", "");
257
- if (detectedIdsFileSystem.has(sessionId)) {
258
- return;
259
- }
260
- detectedIdsFileSystem.add(sessionId);
261
- if (resolvedSessionId) {
262
- return;
263
- }
264
- if (detectedIdsRandomUUID.has(sessionId)) {
265
- resolvedSessionId = sessionId;
266
- opts.onSessionFound(sessionId);
267
- }
268
- }
269
- });
270
- let startFrom = opts.sessionId;
271
- if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
272
- startFrom = null;
273
- }
274
- try {
275
- process.stdin.pause();
276
- await new Promise((r, reject) => {
277
- const args = [];
278
- if (startFrom) {
279
- args.push("--resume", startFrom);
280
- }
281
- const claudeCliPath = process.env.HAPPY_CLAUDE_CLI_PATH || resolve(join(__dirname, "..", "scripts", "claudeInteractiveLaunch.cjs"));
282
- const child = spawn("node", [claudeCliPath, ...args], {
283
- stdio: ["inherit", "inherit", "inherit", "pipe"],
284
- signal: opts.abort,
285
- cwd: opts.path
286
- });
287
- if (child.stdio[3]) {
288
- const rl = createInterface({
289
- input: child.stdio[3],
290
- crlfDelay: Infinity
291
- });
292
- rl.on("line", (line) => {
293
- const sessionMatch = line.match(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i);
294
- if (sessionMatch) {
295
- detectedIdsRandomUUID.add(sessionMatch[0]);
296
- if (resolvedSessionId) {
297
- return;
298
- }
299
- if (detectedIdsFileSystem.has(sessionMatch[0])) {
300
- resolvedSessionId = sessionMatch[0];
301
- opts.onSessionFound(sessionMatch[0]);
302
- }
303
- }
304
- });
305
- rl.on("error", (err) => {
306
- console.error("Error reading from fd 3:", err);
307
- });
308
- }
309
- child.on("error", (error) => {
310
- });
311
- child.on("exit", (code, signal) => {
312
- if (signal === "SIGTERM" && opts.abort.aborted) {
313
- r();
314
- } else if (signal) {
315
- reject(new Error(`Process terminated with signal: ${signal}`));
316
- } else {
317
- r();
318
- }
319
- });
320
- });
321
- } finally {
322
- watcher.close();
323
- process.stdin.resume();
324
- }
325
- return resolvedSessionId;
326
- }
327
-
328
- class MessageQueue {
329
- queue = [];
330
- waiters = [];
331
- closed = false;
332
- closePromise;
333
- closeResolve;
334
- constructor() {
335
- this.closePromise = new Promise((resolve) => {
336
- this.closeResolve = resolve;
337
- });
338
- }
339
- /**
340
- * Push a message to the queue
341
- */
342
- push(message) {
343
- if (this.closed) {
344
- throw new Error("Cannot push to closed queue");
345
- }
346
- logger.debug(`[MessageQueue] push() called. Waiters: ${this.waiters.length}, Queue size before: ${this.queue.length}`);
347
- const waiter = this.waiters.shift();
348
- if (waiter) {
349
- logger.debug(`[MessageQueue] Found waiter! Delivering message directly: "${message}"`);
350
- waiter({
351
- type: "user",
352
- message: {
353
- role: "user",
354
- content: message
355
- },
356
- parent_tool_use_id: null,
357
- session_id: ""
358
- });
359
- } else {
360
- logger.debug(`[MessageQueue] No waiter found. Adding to queue: "${message}"`);
361
- this.queue.push({
362
- type: "user",
363
- message: {
364
- role: "user",
365
- content: message
366
- },
367
- parent_tool_use_id: null,
368
- session_id: ""
369
- });
370
- }
371
- logger.debug(`[MessageQueue] push() completed. Waiters: ${this.waiters.length}, Queue size after: ${this.queue.length}`);
372
- }
373
- /**
374
- * Close the queue - no more messages can be pushed
375
- */
376
- close() {
377
- logger.debug(`[MessageQueue] close() called. Waiters: ${this.waiters.length}`);
378
- this.closed = true;
379
- this.closeResolve?.();
380
- }
381
- /**
382
- * Check if the queue is closed
383
- */
384
- isClosed() {
385
- return this.closed;
386
- }
387
- /**
388
- * Get the current queue size
389
- */
390
- size() {
391
- return this.queue.length;
392
- }
393
- /**
394
- * Async iterator implementation
395
- */
396
- async *[Symbol.asyncIterator]() {
397
- logger.debug(`[MessageQueue] Iterator started`);
398
- while (true) {
399
- const message = this.queue.shift();
400
- if (message !== void 0) {
401
- logger.debug(`[MessageQueue] Iterator yielding queued message`);
402
- yield message;
403
- continue;
404
- }
405
- if (this.closed) {
406
- logger.debug(`[MessageQueue] Iterator ending - queue closed`);
407
- return;
408
- }
409
- logger.debug(`[MessageQueue] Iterator waiting for next message...`);
410
- const nextMessage = await this.waitForNext();
411
- if (nextMessage === void 0) {
412
- logger.debug(`[MessageQueue] Iterator ending - no more messages`);
413
- return;
414
- }
415
- logger.debug(`[MessageQueue] Iterator yielding waited message`);
416
- yield nextMessage;
417
- }
418
- }
419
- /**
420
- * Wait for the next message or queue closure
421
- */
422
- waitForNext() {
423
- return new Promise((resolve) => {
424
- if (this.closed) {
425
- logger.debug(`[MessageQueue] waitForNext() called but queue is closed`);
426
- resolve(void 0);
427
- return;
428
- }
429
- const waiter = (value) => resolve(value);
430
- this.waiters.push(waiter);
431
- logger.debug(`[MessageQueue] waitForNext() adding waiter. Total waiters: ${this.waiters.length}`);
432
- this.closePromise?.then(() => {
433
- const index = this.waiters.indexOf(waiter);
434
- if (index !== -1) {
435
- this.waiters.splice(index, 1);
436
- logger.debug(`[MessageQueue] waitForNext() waiter removed due to close. Remaining waiters: ${this.waiters.length}`);
437
- resolve(void 0);
438
- }
439
- });
440
- });
441
- }
442
- }
443
-
444
- class InvalidateSync {
445
- _invalidated = false;
446
- _invalidatedDouble = false;
447
- _stopped = false;
448
- _command;
449
- _pendings = [];
450
- constructor(command) {
451
- this._command = command;
452
- }
453
- invalidate() {
454
- if (this._stopped) {
455
- return;
456
- }
457
- if (!this._invalidated) {
458
- this._invalidated = true;
459
- this._invalidatedDouble = false;
460
- this._doSync();
461
- } else {
462
- if (!this._invalidatedDouble) {
463
- this._invalidatedDouble = true;
464
- }
465
- }
466
- }
467
- async invalidateAndAwait() {
468
- if (this._stopped) {
469
- return;
470
- }
471
- await new Promise((resolve) => {
472
- this._pendings.push(resolve);
473
- this.invalidate();
474
- });
475
- }
476
- stop() {
477
- if (this._stopped) {
478
- return;
479
- }
480
- this._notifyPendings();
481
- this._stopped = true;
482
- }
483
- _notifyPendings = () => {
484
- for (let pending of this._pendings) {
485
- pending();
486
- }
487
- this._pendings = [];
488
- };
489
- _doSync = async () => {
490
- await backoff(async () => {
491
- if (this._stopped) {
492
- return;
493
- }
494
- await this._command();
495
- });
496
- if (this._stopped) {
497
- this._notifyPendings();
498
- return;
499
- }
500
- if (this._invalidatedDouble) {
501
- this._invalidatedDouble = false;
502
- this._doSync();
503
- } else {
504
- this._invalidated = false;
505
- this._notifyPendings();
506
- }
507
- };
508
- }
509
-
510
- function createSessionScanner(opts) {
511
- const projectName = resolve(opts.workingDirectory).replace(/\//g, "-");
512
- const projectDir = join(homedir(), ".claude", "projects", projectName);
513
- let finishedSessions = /* @__PURE__ */ new Set();
514
- let pendingSessions = /* @__PURE__ */ new Set();
515
- let currentSessionId = null;
516
- let currentSessionWatcherAbortController = null;
517
- let processedMessages = /* @__PURE__ */ new Set();
518
- let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
519
- const sync = new InvalidateSync(async () => {
520
- let sessions = [];
521
- for (let p of pendingSessions) {
522
- sessions.push(p);
523
- }
524
- if (currentSessionId) {
525
- sessions.push(currentSessionId);
526
- }
527
- let processSessionFile = async (sessionId) => {
528
- const expectedSessionFile = join(projectDir, `${sessionId}.jsonl`);
529
- let file;
530
- try {
531
- file = await readFile(expectedSessionFile, "utf-8");
532
- } catch (error) {
533
- return;
534
- }
535
- let lines = file.split("\n");
536
- for (let l of lines) {
537
- try {
538
- let message = JSON.parse(l);
539
- let parsed = RawJSONLinesSchema.safeParse(message);
540
- if (!parsed.success) {
541
- logger.debugLargeJson(`[SESSION_SCANNER] Failed to parse message`, message);
542
- continue;
543
- }
544
- let key = getMessageKey(parsed.data);
545
- if (processedMessages.has(key)) {
546
- continue;
547
- }
548
- processedMessages.add(key);
549
- logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
550
- logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
551
- if (parsed.data.type === "user" && typeof parsed.data.message.content === "string") {
552
- const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
553
- if (currentCounter && currentCounter > 0) {
554
- seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
555
- continue;
556
- }
557
- }
558
- opts.onMessage(parsed.data);
559
- } catch (e) {
560
- continue;
561
- }
562
- }
563
- };
564
- for (let session of sessions) {
565
- await processSessionFile(session);
566
- }
567
- for (let p of sessions) {
568
- if (pendingSessions.has(p)) {
569
- pendingSessions.delete(p);
570
- finishedSessions.add(p);
571
- }
572
- }
573
- currentSessionWatcherAbortController?.abort();
574
- currentSessionWatcherAbortController = new AbortController();
575
- void (async () => {
576
- if (currentSessionId) {
577
- const sessionFile = join(projectDir, `${currentSessionId}.jsonl`);
578
- try {
579
- for await (const change of watch$1(sessionFile, { persistent: true, signal: currentSessionWatcherAbortController.signal })) {
580
- await processSessionFile(currentSessionId);
581
- }
582
- } catch (error) {
583
- if (error.name !== "AbortError") {
584
- logger.debug(`[SESSION_SCANNER] Watch error: ${error.message}`);
585
- }
586
- }
587
- }
588
- })();
589
- });
590
- const intervalId = setInterval(() => {
591
- sync.invalidate();
592
- }, 3e3);
593
- return {
594
- refresh: () => sync.invalidate(),
595
- cleanup: () => {
596
- clearInterval(intervalId);
597
- currentSessionWatcherAbortController?.abort();
598
- },
599
- onNewSession: (sessionId) => {
600
- if (currentSessionId === sessionId) {
601
- return;
602
- }
603
- if (finishedSessions.has(sessionId)) {
604
- return;
605
- }
606
- if (pendingSessions.has(sessionId)) {
607
- return;
608
- }
609
- if (currentSessionId) {
610
- pendingSessions.add(currentSessionId);
611
- }
612
- logger.debug(`[SESSION_SCANNER] New session: ${sessionId}`);
613
- currentSessionId = sessionId;
614
- sync.invalidate();
615
- },
616
- onRemoteUserMessageForDeduplication: (messageContent) => {
617
- seenRemoteUserMessageCounters.set(messageContent, (seenRemoteUserMessageCounters.get(messageContent) || 0) + 1);
618
- }
619
- };
620
- }
621
- function getMessageKey(message) {
622
- if (message.type === "user") {
623
- return `user:${message.uuid}`;
624
- } else if (message.type === "assistant") {
625
- const { usage, ...messageWithoutUsage } = message.message;
626
- return stableStringify(messageWithoutUsage);
627
- } else if (message.type === "summary") {
628
- return `summary:${message.leafUuid}`;
629
- } else if (message.type === "system") {
630
- return `system:${message.uuid}`;
631
- }
632
- return `unknown:<error, this should be unreachable>`;
633
- }
634
- function stableStringify(obj) {
635
- return JSON.stringify(sortKeys(obj), null, 2);
636
- }
637
- function sortKeys(value) {
638
- if (Array.isArray(value)) {
639
- return value.map(sortKeys);
640
- } else if (value && typeof value === "object" && value.constructor === Object) {
641
- return Object.keys(value).sort().reduce((result, key) => {
642
- result[key] = sortKeys(value[key]);
643
- return result;
644
- }, {});
645
- } else {
646
- return value;
647
- }
648
- }
649
-
650
- async function loop(opts) {
651
- let mode = opts.startingMode ?? "interactive";
652
- let currentMessageQueue = new MessageQueue();
653
- let sessionId = null;
654
- let onMessage = null;
655
- const sessionScanner = createSessionScanner({
656
- workingDirectory: opts.path,
657
- onMessage: (message) => {
658
- opts.session.sendClaudeSessionMessage(message);
659
- }
660
- });
661
- opts.session.onUserMessage((message) => {
662
- sessionScanner.onRemoteUserMessageForDeduplication(message.content.text);
663
- currentMessageQueue.push(message.content.text);
664
- logger.debugLargeJson("User message pushed to queue:", message);
665
- if (onMessage) {
666
- onMessage();
667
- }
668
- });
669
- let onSessionFound = (newSessionId) => {
670
- sessionId = newSessionId;
671
- sessionScanner.onNewSession(newSessionId);
672
- };
673
- while (true) {
674
- if (currentMessageQueue.size() > 0) {
675
- mode = "remote";
676
- continue;
677
- }
678
- if (mode === "interactive") {
679
- let abortedOutside = false;
680
- const interactiveAbortController = new AbortController();
681
- opts.session.setHandler("switch", () => {
682
- if (!interactiveAbortController.signal.aborted) {
683
- abortedOutside = true;
684
- mode = "remote";
685
- interactiveAbortController.abort();
686
- }
687
- });
688
- onMessage = () => {
689
- if (!interactiveAbortController.signal.aborted) {
690
- abortedOutside = true;
691
- mode = "remote";
692
- interactiveAbortController.abort();
693
- }
694
- onMessage = null;
695
- };
696
- await claudeLocal({
697
- path: opts.path,
698
- sessionId,
699
- onSessionFound,
700
- abort: interactiveAbortController.signal
701
- });
702
- onMessage = null;
703
- if (!abortedOutside) {
704
- return;
705
- }
706
- if (mode !== "interactive") {
707
- console.log("Switching to remote mode...");
708
- }
709
- }
710
- if (mode === "remote") {
711
- logger.debug("Starting " + sessionId);
712
- const remoteAbortController = new AbortController();
713
- opts.session.setHandler("abort", () => {
714
- if (!remoteAbortController.signal.aborted) {
715
- remoteAbortController.abort();
716
- }
717
- });
718
- const abortHandler = () => {
719
- if (!remoteAbortController.signal.aborted) {
720
- mode = "interactive";
721
- remoteAbortController.abort();
722
- }
723
- if (process.stdin.isTTY) {
724
- process.stdin.setRawMode(false);
725
- }
726
- };
727
- process.stdin.resume();
728
- if (process.stdin.isTTY) {
729
- process.stdin.setRawMode(true);
730
- }
731
- process.stdin.setEncoding("utf8");
732
- process.stdin.on("data", abortHandler);
733
- try {
734
- logger.debug(`Starting claudeRemote with messages: ${currentMessageQueue.size()}`);
735
- await claudeRemote({
736
- abort: remoteAbortController.signal,
737
- sessionId,
738
- path: opts.path,
739
- mcpServers: opts.mcpServers,
740
- permissionPromptToolName: opts.permissionPromptToolName,
741
- onSessionFound,
742
- messages: currentMessageQueue,
743
- onAssistantResult: opts.onAssistantResult,
744
- interruptController: opts.interruptController
745
- });
746
- } finally {
747
- process.stdin.off("data", abortHandler);
748
- if (process.stdin.isTTY) {
749
- process.stdin.setRawMode(false);
750
- }
751
- currentMessageQueue.close();
752
- currentMessageQueue = new MessageQueue();
753
- }
754
- if (mode !== "remote") {
755
- console.log("Switching back to good old claude...");
756
- }
757
- }
758
- }
759
- }
760
-
761
- async function startPermissionServerV2(handler) {
762
- const mcp = new McpServer({
763
- name: "Permission Server",
764
- version: "1.0.0",
765
- description: "A server that allows you to request permissions from the user"
766
- });
767
- mcp.registerTool("ask_permission", {
768
- description: "Request permission to execute a tool",
769
- title: "Request Permission",
770
- inputSchema: {
771
- tool_name: z$1.string().describe("The tool that needs permission"),
772
- input: z$1.any().describe("The arguments for the tool")
773
- }
774
- // outputSchema: {
775
- // approved: z.boolean().describe('Whether the tool was approved'),
776
- // reason: z.string().describe('The reason for the approval or denial'),
777
- // },
778
- }, async (args) => {
779
- const response = await handler({ name: args.tool_name, arguments: args.input });
780
- const result = response.approved ? { behavior: "allow", updatedInput: args.input || {} } : { behavior: "deny", message: response.reason || "Permission denied by user" };
781
- return {
782
- content: [
783
- {
784
- type: "text",
785
- text: JSON.stringify(result)
786
- }
787
- ],
788
- isError: false
789
- };
790
- });
791
- const transport = new StreamableHTTPServerTransport({
792
- // NOTE: Returning session id here will result in claude
793
- // sdk spawn to fail with `Invalid Request: Server already initialized`
794
- sessionIdGenerator: void 0
795
- });
796
- await mcp.connect(transport);
797
- const server = createServer(async (req, res) => {
798
- try {
799
- await transport.handleRequest(req, res);
800
- } catch (error) {
801
- logger.debug("Error handling request:", error);
802
- if (!res.headersSent) {
803
- res.writeHead(500).end();
804
- }
805
- }
806
- });
807
- const baseUrl = await new Promise((resolve) => {
808
- server.listen(0, "127.0.0.1", () => {
809
- const addr = server.address();
810
- resolve(new URL(`http://127.0.0.1:${addr.port}`));
811
- });
812
- });
813
- return {
814
- url: baseUrl.toString(),
815
- toolName: "ask_permission"
816
- };
817
- }
818
-
819
- class InterruptController {
820
- interruptFn;
821
- isInterrupting = false;
822
- /**
823
- * Register an interrupt function from claudeRemote
824
- */
825
- register(fn) {
826
- this.interruptFn = fn;
827
- }
828
- /**
829
- * Unregister the interrupt function (cleanup)
830
- */
831
- unregister() {
832
- this.interruptFn = void 0;
833
- this.isInterrupting = false;
834
- }
835
- /**
836
- * Trigger the interrupt - can be called from anywhere
837
- */
838
- async interrupt() {
839
- if (!this.interruptFn || this.isInterrupting) {
840
- return false;
841
- }
842
- this.isInterrupting = true;
843
- try {
844
- await this.interruptFn();
845
- return true;
846
- } catch (error) {
847
- logger.debug("Failed to interrupt Claude:", error);
848
- return false;
849
- } finally {
850
- this.isInterrupting = false;
851
- }
852
- }
853
- /**
854
- * Check if interrupt is available
855
- */
856
- canInterrupt() {
857
- return !!this.interruptFn && !this.isInterrupting;
858
- }
859
- }
860
-
861
- var version = "0.1.9";
862
- var packageJson = {
863
- version: version};
864
-
865
- async function startAnthropicActivityProxy(onClaudeActivity) {
866
- const requestTimeouts = /* @__PURE__ */ new Map();
867
- let requestCounter = 0;
868
- let idleTimer = null;
869
- const maxTimeBeforeIdle = 50;
870
- const requestTimeout = 5 * 60 * 1e3;
871
- const cleanupRequest = (requestId, reason) => {
872
- const timeout = requestTimeouts.get(requestId);
873
- if (timeout) {
874
- clearTimeout(timeout);
875
- requestTimeouts.delete(requestId);
876
- logger.debug(`[AnthropicProxy #${requestId}] Cleaned up (${reason}), active requests: ${requestTimeouts.size}`);
877
- claudeDidSomeWork();
878
- }
879
- };
880
- const claudeDidSomeWork = () => {
881
- if (idleTimer) clearTimeout(idleTimer);
882
- if (requestTimeouts.size === 0) {
883
- idleTimer = setTimeout(() => {
884
- logger.debug(`[AnthropicProxy] Idle for ${maxTimeBeforeIdle}ms, active requests: ${requestTimeouts.size}`);
885
- onClaudeActivity("idle");
886
- }, maxTimeBeforeIdle);
887
- }
888
- };
889
- const server = createServer((req, res) => {
890
- const requestId = ++requestCounter;
891
- const isAnthropicRequest = req.headers.host === "api.anthropic.com" || req.url?.includes("anthropic.com");
892
- if (isAnthropicRequest) {
893
- const timeout = setTimeout(() => {
894
- logger.debug(`[AnthropicProxy #${requestId}] Request timeout after ${requestTimeout}ms`);
895
- cleanupRequest(requestId, "timeout");
896
- }, requestTimeout);
897
- requestTimeouts.set(requestId, timeout);
898
- onClaudeActivity("working");
899
- logger.debug(`[AnthropicProxy #${requestId}] Anthropic request: ${req.method} ${req.url}, active requests: ${requestTimeouts.size}`);
900
- }
901
- const chunks = [];
902
- req.on("data", (chunk) => {
903
- chunks.push(chunk);
904
- if (isAnthropicRequest) {
905
- claudeDidSomeWork();
906
- }
907
- });
908
- req.on("end", () => {
909
- const body = Buffer.concat(chunks);
910
- let targetUrl;
911
- if (isAnthropicRequest) {
912
- targetUrl = new URL$1(req.url || "/", "https://api.anthropic.com");
913
- } else {
914
- const protocol = req.headers["x-forwarded-proto"] || "https";
915
- const host = req.headers.host || "localhost";
916
- targetUrl = new URL$1(req.url || "/", `${protocol}://${host}`);
917
- }
918
- const options = {
919
- hostname: targetUrl.hostname,
920
- port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
921
- path: targetUrl.pathname + targetUrl.search,
922
- method: req.method,
923
- headers: {
924
- ...req.headers,
925
- host: targetUrl.hostname
926
- }
927
- };
928
- const requestMethod = targetUrl.protocol === "https:" ? request : request$1;
929
- const proxyReq = requestMethod(options, (proxyRes) => {
930
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
931
- proxyRes.pipe(res);
932
- proxyRes.on("end", () => {
933
- if (isAnthropicRequest) {
934
- cleanupRequest(requestId, "completed");
935
- }
936
- });
937
- });
938
- proxyReq.on("error", (error) => {
939
- if (isAnthropicRequest) {
940
- cleanupRequest(requestId, `error: ${error.message}`);
941
- } else {
942
- logger.debug(`[AnthropicProxy #${requestId}] Error:`, error.message);
943
- }
944
- res.writeHead(502);
945
- res.end("Bad Gateway");
946
- });
947
- if (body.length > 0) {
948
- proxyReq.write(body);
949
- }
950
- proxyReq.end();
951
- });
952
- });
953
- server.on("connect", (req, clientSocket, head) => {
954
- const requestId = ++requestCounter;
955
- const [hostname, port] = req.url?.split(":") || ["", "443"];
956
- const isAnthropicRequest = hostname === "api.anthropic.com";
957
- if (isAnthropicRequest) {
958
- const timeout = setTimeout(() => {
959
- logger.debug(`[AnthropicProxy #${requestId}] CONNECT timeout after ${requestTimeout}ms`);
960
- cleanupRequest(requestId, "timeout");
961
- }, requestTimeout);
962
- requestTimeouts.set(requestId, timeout);
963
- onClaudeActivity("working");
964
- logger.debug(`[AnthropicProxy #${requestId}] CONNECT to api.anthropic.com, active requests: ${requestTimeouts.size}`);
965
- }
966
- const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
967
- clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
968
- serverSocket.write(head);
969
- serverSocket.pipe(clientSocket);
970
- clientSocket.pipe(serverSocket);
971
- });
972
- const cleanup = () => {
973
- if (isAnthropicRequest) {
974
- cleanupRequest(requestId, "CONNECT closed");
975
- }
976
- };
977
- serverSocket.on("error", (err) => {
978
- logger.debug(`[AnthropicProxy #${requestId}] CONNECT error:`, err.message);
979
- clientSocket.end();
980
- cleanup();
981
- });
982
- clientSocket.on("error", cleanup);
983
- clientSocket.on("end", cleanup);
984
- serverSocket.on("end", cleanup);
985
- });
986
- const url = await new Promise((resolve) => {
987
- server.listen(0, "127.0.0.1", () => {
988
- const addr = server.address();
989
- if (addr && typeof addr === "object") {
990
- resolve(`http://127.0.0.1:${addr.port}`);
991
- }
992
- });
993
- });
994
- logger.debug(`[AnthropicProxy] Started at ${url}`);
995
- return {
996
- url,
997
- cleanup: () => {
998
- if (idleTimer) clearTimeout(idleTimer);
999
- for (const [requestId, timeout] of requestTimeouts) {
1000
- clearTimeout(timeout);
1001
- logger.debug(`[AnthropicProxy] Cleaning up timeout for request #${requestId}`);
1002
- }
1003
- requestTimeouts.clear();
1004
- if (requestTimeouts.size > 0) {
1005
- logger.debug(`[AnthropicProxy] Warning: ${requestTimeouts.size} active requests still pending at cleanup:`, Array.from(requestTimeouts.keys()));
1006
- }
1007
- server.close();
1008
- }
1009
- };
1010
- }
1011
-
1012
- async function start(credentials, options = {}) {
1013
- const workingDirectory = process.cwd();
1014
- const sessionTag = randomUUID();
1015
- const api = new ApiClient(credentials.token, credentials.secret);
1016
- let state = {};
1017
- let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version };
1018
- const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
1019
- logger.debug(`Session created: ${response.id}`);
1020
- const session = api.session(response);
1021
- const pushClient = api.push();
1022
- let thinking = false;
1023
- let pingInterval = setInterval(() => {
1024
- session.keepAlive(thinking);
1025
- }, 2e3);
1026
- const antropicActivityProxy = await startAnthropicActivityProxy(
1027
- (activity) => {
1028
- const newThinking = activity === "working";
1029
- if (newThinking !== thinking) {
1030
- thinking = newThinking;
1031
- logger.debug(`[PING] Thinking state changed: ${thinking}`);
1032
- session.keepAlive(thinking);
1033
- }
1034
- }
1035
- );
1036
- process.env.HTTP_PROXY = antropicActivityProxy.url;
1037
- process.env.HTTPS_PROXY = antropicActivityProxy.url;
1038
- logger.debug(`[AnthropicProxy] Set HTTP_PROXY and HTTPS_PROXY to ${antropicActivityProxy.url}`);
1039
- const logPath = await logger.logFilePathPromise;
1040
- logger.info(`Session: ${response.id}`);
1041
- logger.infoDeveloper(`Logs: ${logPath}`);
1042
- const interruptController = new InterruptController();
1043
- let requests = /* @__PURE__ */ new Map();
1044
- const permissionServer = await startPermissionServerV2(async (request) => {
1045
- const id = randomUUID();
1046
- let promise = new Promise((resolve) => {
1047
- requests.set(id, resolve);
1048
- });
1049
- let timeout = setTimeout(async () => {
1050
- logger.info("Permission timeout - attempting to interrupt Claude");
1051
- const interrupted = await interruptController.interrupt();
1052
- if (interrupted) {
1053
- logger.info("Claude interrupted successfully");
1054
- }
1055
- requests.delete(id);
1056
- session.updateAgentState((currentState) => {
1057
- let r = { ...currentState.requests };
1058
- delete r[id];
1059
- return {
1060
- ...currentState,
1061
- requests: r
1062
- };
1063
- });
1064
- }, 1e3 * 60 * 4.5);
1065
- logger.info("Permission request" + id + " " + JSON.stringify(request));
1066
- try {
1067
- await pushClient.sendToAllDevices(
1068
- "Permission Request",
1069
- `Claude wants to use ${request.name}`,
1070
- {
1071
- sessionId: response.id,
1072
- requestId: id,
1073
- tool: request.name,
1074
- type: "permission_request"
1075
- }
1076
- );
1077
- logger.info("Push notification sent for permission request");
1078
- } catch (error) {
1079
- logger.debug("Failed to send push notification:", error);
1080
- }
1081
- session.updateAgentState((currentState) => ({
1082
- ...currentState,
1083
- requests: {
1084
- ...currentState.requests,
1085
- [id]: {
1086
- tool: request.name,
1087
- arguments: request.arguments
1088
- }
1089
- }
1090
- }));
1091
- promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
1092
- return promise;
1093
- });
1094
- session.setHandler("permission", async (message) => {
1095
- logger.info("Permission response" + JSON.stringify(message));
1096
- const id = message.id;
1097
- const resolve = requests.get(id);
1098
- if (resolve) {
1099
- if (!message.approved) {
1100
- logger.debug("Permission denied, interrupting Claude");
1101
- await interruptController.interrupt();
1102
- }
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
- });
1121
- const onAssistantResult = async (result) => {
1122
- try {
1123
- const summary = "result" in result && result.result ? result.result.substring(0, 100) + (result.result.length > 100 ? "..." : "") : "";
1124
- await pushClient.sendToAllDevices(
1125
- "Your move :D",
1126
- summary,
1127
- {
1128
- sessionId: response.id,
1129
- type: "assistant_result",
1130
- turns: result.num_turns,
1131
- duration_ms: result.duration_ms,
1132
- cost_usd: result.total_cost_usd
1133
- }
1134
- );
1135
- logger.debug("Push notification sent: Assistant result");
1136
- } catch (error) {
1137
- logger.debug("Failed to send assistant result push notification:", error);
1138
- }
1139
- };
1140
- await loop({
1141
- path: workingDirectory,
1142
- model: options.model,
1143
- permissionMode: options.permissionMode,
1144
- startingMode: options.startingMode,
1145
- mcpServers: {
1146
- "permission": {
1147
- type: "http",
1148
- url: permissionServer.url
1149
- }
1150
- },
1151
- permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
1152
- session,
1153
- onAssistantResult,
1154
- interruptController
1155
- });
1156
- clearInterval(pingInterval);
1157
- if (antropicActivityProxy) {
1158
- logger.info("[AnthropicProxy] Shutting down activity monitoring proxy");
1159
- antropicActivityProxy.cleanup();
1160
- }
1161
- process.exit(0);
1162
- }
1163
-
1164
- const defaultSettings = {
1165
- onboardingCompleted: false
1166
- };
1167
- async function readSettings() {
1168
- if (!existsSync(configuration.settingsFile)) {
1169
- return { ...defaultSettings };
1170
- }
1171
- try {
1172
- const content = await readFile(configuration.settingsFile, "utf8");
1173
- return JSON.parse(content);
1174
- } catch {
1175
- return { ...defaultSettings };
1176
- }
1177
- }
1178
- async function writeSettings(settings) {
1179
- if (!existsSync(configuration.happyDir)) {
1180
- await mkdir(configuration.happyDir, { recursive: true });
1181
- }
1182
- await writeFile(configuration.settingsFile, JSON.stringify(settings, null, 2));
1183
- }
1184
- const credentialsSchema = z.object({
1185
- secret: z.string().base64(),
1186
- token: z.string()
1187
- });
1188
- async function readCredentials() {
1189
- if (!existsSync(configuration.privateKeyFile)) {
1190
- return null;
1191
- }
1192
- try {
1193
- const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
1194
- const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
1195
- return {
1196
- secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
1197
- token: credentials.token
1198
- };
1199
- } catch {
1200
- return null;
1201
- }
1202
- }
1203
- async function writeCredentials(credentials) {
1204
- if (!existsSync(configuration.happyDir)) {
1205
- await mkdir(configuration.happyDir, { recursive: true });
1206
- }
1207
- await writeFile(configuration.privateKeyFile, JSON.stringify({
1208
- secret: encodeBase64(credentials.secret),
1209
- token: credentials.token
1210
- }, null, 2));
1211
- }
1212
-
1213
- function displayQRCode(url) {
1214
- console.log("=".repeat(80));
1215
- console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
1216
- console.log("=".repeat(80));
1217
- qrcode.generate(url, { small: true }, (qr) => {
1218
- for (let l of qr.split("\n")) {
1219
- console.log(" ".repeat(10) + l);
1220
- }
1221
- });
1222
- console.log("=".repeat(80));
1223
- }
1224
-
1225
- async function doAuth() {
1226
- console.log("Starting authentication...");
1227
- const secret = new Uint8Array(randomBytes(32));
1228
- const keypair = tweetnacl.box.keyPair.fromSecretKey(secret);
1229
- try {
1230
- await axios.post(`${configuration.serverUrl}/v1/auth/request`, {
1231
- publicKey: encodeBase64(keypair.publicKey)
1232
- });
1233
- } catch (error) {
1234
- console.log("Failed to create authentication request, please try again later.");
1235
- return null;
1236
- }
1237
- console.log("Please, authenticate using mobile app");
1238
- const authUrl = "happy://terminal?" + encodeBase64Url(keypair.publicKey);
1239
- displayQRCode(authUrl);
1240
- if (process.env.DEBUG === "1") {
1241
- console.log("\n\u{1F4CB} For manual entry, copy this URL:");
1242
- console.log(authUrl);
1243
- }
1244
- let credentials = null;
1245
- while (true) {
1246
- try {
1247
- const response = await axios.post(`${configuration.serverUrl}/v1/auth/request`, {
1248
- publicKey: encodeBase64(keypair.publicKey)
1249
- });
1250
- if (response.data.state === "authorized") {
1251
- let token = response.data.token;
1252
- let r = decodeBase64(response.data.response);
1253
- let decrypted = decryptWithEphemeralKey(r, keypair.secretKey);
1254
- if (decrypted) {
1255
- credentials = {
1256
- secret: decrypted,
1257
- token
1258
- };
1259
- await writeCredentials(credentials);
1260
- return credentials;
1261
- } else {
1262
- console.log("Failed to decrypt response, please try again later.");
1263
- return null;
1264
- }
1265
- }
1266
- } catch (error) {
1267
- console.log("Failed to create authentication request, please try again later.");
1268
- return null;
1269
- }
1270
- await delay(1e3);
1271
- }
1272
- return null;
1273
- }
1274
- function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
1275
- const ephemeralPublicKey = encryptedBundle.slice(0, 32);
1276
- const nonce = encryptedBundle.slice(32, 32 + tweetnacl.box.nonceLength);
1277
- const encrypted = encryptedBundle.slice(32 + tweetnacl.box.nonceLength);
1278
- const decrypted = tweetnacl.box.open(encrypted, nonce, ephemeralPublicKey, recipientSecretKey);
1279
- if (!decrypted) {
1280
- return null;
1281
- }
1282
- return decrypted;
1283
- }
1284
-
1285
- (async () => {
1286
- const args = process.argv.slice(2);
1287
- let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
1288
- initializeConfiguration(installationLocation);
1289
- initLoggerWithGlobalConfiguration();
1290
- logger.debug("Starting happy CLI with args: ", process.argv);
1291
- const subcommand = args[0];
1292
- if (subcommand === "logout") {
1293
- try {
1294
- await cleanKey();
1295
- } catch (error) {
1296
- console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1297
- if (process.env.DEBUG) {
1298
- console.error(error);
1299
- }
1300
- process.exit(1);
1301
- }
1302
- return;
1303
- } else if (subcommand === "daemon") {
1304
- if (process.env.HAPPY_DAEMON_MODE) {
1305
- const { run } = await import('./run-DMbKhYfb.mjs');
1306
- await run();
1307
- } else {
1308
- const daemonSubcommand = args[1];
1309
- if (daemonSubcommand === "install") {
1310
- const { install } = await import('./install-B0DnBGS_.mjs');
1311
- try {
1312
- await install();
1313
- } catch (error) {
1314
- console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1315
- process.exit(1);
1316
- }
1317
- } else if (daemonSubcommand === "uninstall") {
1318
- const { uninstall } = await import('./uninstall-BGgl5V8F.mjs');
1319
- try {
1320
- await uninstall();
1321
- } catch (error) {
1322
- console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1323
- process.exit(1);
1324
- }
1325
- } else {
1326
- console.log(`
1327
- ${chalk.bold("happy daemon")} - Daemon management
1328
-
1329
- ${chalk.bold("Usage:")}
1330
- sudo happy daemon install Install the daemon (requires sudo)
1331
- sudo happy daemon uninstall Uninstall the daemon (requires sudo)
1332
-
1333
- ${chalk.bold("Note:")} The daemon runs in the background and provides persistent services.
1334
- Currently only supported on macOS.
1335
- `);
1336
- }
1337
- }
1338
- return;
1339
- } else {
1340
- const options = {};
1341
- let showHelp = false;
1342
- let showVersion = false;
1343
- let forceAuth = false;
1344
- for (let i = 0; i < args.length; i++) {
1345
- const arg = args[i];
1346
- if (arg === "-h" || arg === "--help") {
1347
- showHelp = true;
1348
- } else if (arg === "-v" || arg === "--version") {
1349
- showVersion = true;
1350
- } else if (arg === "--auth" || arg === "--login") {
1351
- forceAuth = true;
1352
- } else if (arg === "-m" || arg === "--model") {
1353
- options.model = args[++i];
1354
- } else if (arg === "-p" || arg === "--permission-mode") {
1355
- options.permissionMode = z$1.enum(["auto", "default", "plan"]).parse(args[++i]);
1356
- } else if (arg === "--local") {
1357
- i++;
1358
- } else if (arg === "--happy-starting-mode") {
1359
- options.startingMode = z$1.enum(["interactive", "remote"]).parse(args[++i]);
1360
- } else if (arg === "--happy-daemon") {
1361
- process.env.HAPPY_DAEMON_MODE = "true";
1362
- const { run } = await import('./run-DMbKhYfb.mjs');
1363
- await run();
1364
- return;
1365
- } else if (arg === "--happy-daemon-start") {
1366
- const { spawn } = await import('child_process');
1367
- const { isDaemonRunning } = await import('./run-DMbKhYfb.mjs');
1368
- if (isDaemonRunning()) {
1369
- console.log("Happy daemon is already running");
1370
- return;
1371
- }
1372
- const happyPath = process.argv[1];
1373
- const daemonProcess = spawn("node", [happyPath, "--happy-daemon"], {
1374
- detached: true,
1375
- stdio: "ignore",
1376
- env: process.env
1377
- });
1378
- daemonProcess.unref();
1379
- console.log("Happy daemon started");
1380
- return;
1381
- } else if (arg === "--happy-daemon-stop") {
1382
- const { isDaemonRunning } = await import('./run-DMbKhYfb.mjs');
1383
- const pidFile = join(homedir(), ".happy", "daemon-pid");
1384
- if (!existsSync(pidFile)) {
1385
- console.log("Happy daemon is not running");
1386
- return;
1387
- }
1388
- try {
1389
- const pid = parseInt(await readFile(pidFile, "utf-8"));
1390
- process.kill(pid, "SIGTERM");
1391
- console.log("Happy daemon stopped");
1392
- } catch (error) {
1393
- console.error("Failed to stop daemon:", error);
1394
- }
1395
- return;
1396
- } else if (arg === "--happy-daemon-install") {
1397
- const { install } = await import('./install-DEPy62QN.mjs');
1398
- await install();
1399
- return;
1400
- } else if (arg === "--happy-daemon-uninstall") {
1401
- const { uninstall } = await import('./uninstall-BWHglipH.mjs');
1402
- await uninstall();
1403
- return;
1404
- } else {
1405
- console.error(chalk.red(`Unknown argument: ${arg}`));
1406
- process.exit(1);
1407
- }
1408
- }
1409
- if (showHelp) {
1410
- console.log(`
1411
- ${chalk.bold("happy")} - Claude Code session sharing
1412
-
1413
- ${chalk.bold("Usage:")}
1414
- happy [options]
1415
- happy logout Logs out of your account and removes data directory
1416
- happy daemon Manage the background daemon (macOS only)
1417
-
1418
- ${chalk.bold("Options:")}
1419
- -h, --help Show this help message
1420
- -v, --version Show version
1421
- -m, --model <model> Claude model to use (default: sonnet)
1422
- -p, --permission-mode Permission mode: auto, default, or plan
1423
- --auth, --login Force re-authentication
1424
-
1425
- [Daemon Management]
1426
- --happy-daemon-start Start the daemon in background
1427
- --happy-daemon-stop Stop the daemon
1428
- --happy-daemon-install Install daemon to run on startup
1429
- --happy-daemon-uninstall Uninstall daemon from startup
1430
-
1431
- [Advanced]
1432
- --local < global | local >
1433
- Will use .happy folder in the current directory for storing your private key and debug logs.
1434
- You will require re-login each time you run this in a new directory.
1435
- --happy-starting-mode <interactive|remote>
1436
- Set the starting mode for new sessions (default: remote)
1437
-
1438
- ${chalk.bold("Examples:")}
1439
- happy Start a session with default settings
1440
- happy -m opus Use Claude Opus model
1441
- happy -p plan Use plan permission mode
1442
- happy --auth Force re-authentication before starting session
1443
- happy logout Logs out of your account and removes data directory
1444
- `);
1445
- process.exit(0);
1446
- }
1447
- if (showVersion) {
1448
- console.log(packageJson.version);
1449
- process.exit(0);
1450
- }
1451
- let credentials = await readCredentials();
1452
- if (!credentials || forceAuth) {
1453
- let res = await doAuth();
1454
- if (!res) {
1455
- process.exit(1);
1456
- }
1457
- credentials = res;
1458
- }
1459
- const settings = await readSettings() || { onboardingCompleted: false };
1460
- if (settings.daemonInstalled === void 0 && process.platform === "darwin") {
1461
- console.log(chalk.cyan("\n\u{1F680} Happy Daemon Setup\n"));
1462
- console.log("The Happy daemon runs in the background to enable faster session startup");
1463
- console.log("and remote control from the mobile app.\n");
1464
- const rl = createInterface({
1465
- input: process.stdin,
1466
- output: process.stdout
1467
- });
1468
- const answer = await new Promise((resolve) => {
1469
- rl.question(chalk.green("Would you like to install the daemon to run on startup? (recommended) [Y/n]: "), resolve);
1470
- });
1471
- rl.close();
1472
- const shouldInstall = answer.toLowerCase() !== "n";
1473
- if (shouldInstall) {
1474
- try {
1475
- const { install } = await import('./install-DEPy62QN.mjs');
1476
- await install();
1477
- settings.daemonInstalled = true;
1478
- const { isDaemonRunning } = await import('./run-DMbKhYfb.mjs');
1479
- if (!isDaemonRunning()) {
1480
- const { spawn } = await import('child_process');
1481
- const happyPath = process.argv[1];
1482
- const daemonProcess = spawn("node", [happyPath, "--happy-daemon"], {
1483
- detached: true,
1484
- stdio: "ignore",
1485
- env: process.env
1486
- });
1487
- daemonProcess.unref();
1488
- }
1489
- console.log(chalk.green("\u2713 Daemon installed and started"));
1490
- } catch (error) {
1491
- console.error(chalk.red("Failed to install daemon:"), error);
1492
- console.log(chalk.yellow("You can install the daemon later with: sudo happy daemon install"));
1493
- }
1494
- } else {
1495
- settings.daemonInstalled = false;
1496
- console.log(chalk.yellow("You can install the daemon later with: sudo happy daemon install"));
1497
- }
1498
- await writeSettings(settings);
1499
- }
1500
- try {
1501
- await start(credentials, options);
1502
- } catch (error) {
1503
- console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1504
- if (process.env.DEBUG) {
1505
- console.error(error);
1506
- }
1507
- process.exit(1);
1508
- }
1509
- }
1510
- })();
1511
- async function cleanKey() {
1512
- const happyDir = configuration.happyDir;
1513
- if (!existsSync(happyDir)) {
1514
- console.log(chalk.yellow("No happy data directory found at:"), happyDir);
1515
- return;
1516
- }
1517
- console.log(chalk.blue("Found happy data directory at:"), happyDir);
1518
- console.log(chalk.yellow("\u26A0\uFE0F This will remove all authentication data and require reconnecting your phone."));
1519
- const rl = createInterface({
1520
- input: process.stdin,
1521
- output: process.stdout
1522
- });
1523
- const answer = await new Promise((resolve) => {
1524
- rl.question(chalk.yellow("Are you sure you want to remove the happy data directory? (y/N): "), resolve);
1525
- });
1526
- rl.close();
1527
- if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
1528
- try {
1529
- rmSync(happyDir, { recursive: true, force: true });
1530
- console.log(chalk.green("\u2713 Happy data directory removed successfully"));
1531
- console.log(chalk.blue("\u2139\uFE0F You will need to reconnect your phone on the next session"));
1532
- } catch (error) {
1533
- throw new Error(`Failed to remove data directory: ${error instanceof Error ? error.message : "Unknown error"}`);
1534
- }
1535
- } else {
1536
- console.log(chalk.blue("Operation cancelled"));
1537
- }
1538
- }
1539
-
1540
- export { readCredentials as a, doAuth as d, readSettings as r, writeSettings as w };