happy-coder 0.1.7 → 0.1.10

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 (51) hide show
  1. package/dist/index.cjs +950 -938
  2. package/dist/index.mjs +880 -868
  3. package/dist/lib.cjs +32 -0
  4. package/dist/lib.d.cts +527 -0
  5. package/dist/lib.d.mts +527 -0
  6. package/dist/lib.mjs +14 -0
  7. package/dist/types-B2JzqUiU.cjs +831 -0
  8. package/dist/types-DnQGY77F.mjs +818 -0
  9. package/package.json +25 -10
  10. package/dist/auth/auth.d.ts +0 -38
  11. package/dist/auth/auth.js +0 -76
  12. package/dist/auth/auth.test.d.ts +0 -7
  13. package/dist/auth/auth.test.js +0 -96
  14. package/dist/auth/crypto.d.ts +0 -25
  15. package/dist/auth/crypto.js +0 -36
  16. package/dist/claude/claude.d.ts +0 -54
  17. package/dist/claude/claude.js +0 -170
  18. package/dist/claude/claude.test.d.ts +0 -7
  19. package/dist/claude/claude.test.js +0 -130
  20. package/dist/claude/types.d.ts +0 -37
  21. package/dist/claude/types.js +0 -7
  22. package/dist/commands/start.d.ts +0 -38
  23. package/dist/commands/start.js +0 -161
  24. package/dist/commands/start.test.d.ts +0 -7
  25. package/dist/commands/start.test.js +0 -307
  26. package/dist/handlers/message-handler.d.ts +0 -65
  27. package/dist/handlers/message-handler.js +0 -187
  28. package/dist/index.d.ts +0 -1
  29. package/dist/index.js +0 -1
  30. package/dist/session/service.d.ts +0 -27
  31. package/dist/session/service.js +0 -93
  32. package/dist/session/service.test.d.ts +0 -7
  33. package/dist/session/service.test.js +0 -71
  34. package/dist/session/types.d.ts +0 -44
  35. package/dist/session/types.js +0 -4
  36. package/dist/socket/client.d.ts +0 -50
  37. package/dist/socket/client.js +0 -136
  38. package/dist/socket/client.test.d.ts +0 -7
  39. package/dist/socket/client.test.js +0 -74
  40. package/dist/socket/types.d.ts +0 -80
  41. package/dist/socket/types.js +0 -12
  42. package/dist/utils/config.d.ts +0 -22
  43. package/dist/utils/config.js +0 -23
  44. package/dist/utils/logger.d.ts +0 -26
  45. package/dist/utils/logger.js +0 -60
  46. package/dist/utils/paths.d.ts +0 -18
  47. package/dist/utils/paths.js +0 -24
  48. package/dist/utils/qrcode.d.ts +0 -19
  49. package/dist/utils/qrcode.js +0 -37
  50. package/dist/utils/qrcode.test.d.ts +0 -7
  51. package/dist/utils/qrcode.test.js +0 -14
package/dist/index.mjs CHANGED
@@ -1,716 +1,34 @@
1
1
  import chalk from 'chalk';
2
- import axios from 'axios';
3
- import { appendFileSync } from 'fs';
4
- import os, { homedir } from 'node:os';
5
- import { join, resolve, dirname } from 'node:path';
6
- import { mkdir, watch as watch$1, readFile, writeFile } from 'node:fs/promises';
7
- import { existsSync, readFileSync, mkdirSync, watch, rmSync } from 'node:fs';
8
- import { EventEmitter } from 'node:events';
9
- import { io } from 'socket.io-client';
10
- import * as z from 'zod';
11
- import { z as z$1 } from 'zod';
12
- import { randomBytes, randomUUID } from 'node:crypto';
13
- import tweetnacl from 'tweetnacl';
14
- import { Expo } from 'expo-server-sdk';
2
+ import { l as logger, d as backoff, R as RawJSONLinesSchema, A as ApiClient, c as configuration, e as encodeBase64, f as encodeBase64Url, g as decodeBase64, h as delay, j as encrypt, k as decrypt, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-DnQGY77F.mjs';
3
+ import { randomUUID, randomBytes } from 'node:crypto';
15
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';
16
8
  import { spawn } from 'node:child_process';
17
9
  import { createInterface } from 'node:readline';
18
- import { fileURLToPath } from 'node:url';
10
+ import { fileURLToPath, URL as URL$1 } from 'node:url';
11
+ import { watch as watch$1, readFile, mkdir, writeFile as writeFile$1 } from 'node:fs/promises';
19
12
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
20
- import { createServer } from 'node:http';
13
+ import { createServer, request as request$1 } from 'node:http';
21
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 { exec, spawn as spawn$1, execSync } from 'child_process';
20
+ import { promisify } from 'util';
21
+ import { readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
22
+ import crypto, { createHash } from 'crypto';
23
+ import { join as join$1 } from 'path';
24
+ import tweetnacl from 'tweetnacl';
25
+ import axios from 'axios';
22
26
  import qrcode from 'qrcode-terminal';
23
-
24
- class Configuration {
25
- serverUrl;
26
- // Directories and paths (from persistence)
27
- happyDir;
28
- logsDir;
29
- settingsFile;
30
- privateKeyFile;
31
- constructor(location) {
32
- this.serverUrl = process.env.HANDY_SERVER_URL || "https://handy-api.korshakov.org";
33
- if (location === "local") {
34
- this.happyDir = join(process.cwd(), ".happy");
35
- } else {
36
- this.happyDir = join(homedir(), ".happy");
37
- }
38
- this.logsDir = join(this.happyDir, "logs");
39
- this.settingsFile = join(this.happyDir, "settings.json");
40
- this.privateKeyFile = join(this.happyDir, "access.key");
41
- }
42
- }
43
- let configuration = void 0;
44
- function initializeConfiguration(location) {
45
- configuration = new Configuration(location);
46
- }
47
-
48
- async function getSessionLogPath() {
49
- if (!existsSync(configuration.logsDir)) {
50
- await mkdir(configuration.logsDir, { recursive: true });
51
- }
52
- const now = /* @__PURE__ */ new Date();
53
- const timestamp = now.toLocaleString("sv-SE", {
54
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
55
- year: "numeric",
56
- month: "2-digit",
57
- day: "2-digit",
58
- hour: "2-digit",
59
- minute: "2-digit",
60
- second: "2-digit"
61
- }).replace(/[: ]/g, "-").replace(/,/g, "");
62
- return join(configuration.logsDir, `${timestamp}.log`);
63
- }
64
- class Logger {
65
- constructor(logFilePathPromise = getSessionLogPath()) {
66
- this.logFilePathPromise = logFilePathPromise;
67
- }
68
- // Use local timezone for simplicity of locating the logs,
69
- // in practice you will not need absolute timestamps
70
- localTimezoneTimestamp() {
71
- return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", {
72
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
73
- hour12: false,
74
- hour: "2-digit",
75
- minute: "2-digit",
76
- second: "2-digit",
77
- fractionalSecondDigits: 3
78
- });
79
- }
80
- debug(message, ...args) {
81
- this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, ...args);
82
- }
83
- debugLargeJson(message, object, maxStringLength = 100, maxArrayLength = 10) {
84
- if (!process.env.DEBUG) {
85
- this.debug(`In production, skipping message inspection`);
86
- }
87
- const truncateStrings = (obj) => {
88
- if (typeof obj === "string") {
89
- return obj.length > maxStringLength ? obj.substring(0, maxStringLength) + "... [truncated for logs]" : obj;
90
- }
91
- if (Array.isArray(obj)) {
92
- const truncatedArray = obj.map((item) => truncateStrings(item)).slice(0, maxArrayLength);
93
- if (obj.length > maxArrayLength) {
94
- truncatedArray.push(`... [truncated array for logs up to ${maxArrayLength} items]`);
95
- }
96
- return truncatedArray;
97
- }
98
- if (obj && typeof obj === "object") {
99
- const result = {};
100
- for (const [key, value] of Object.entries(obj)) {
101
- if (key === "usage") {
102
- continue;
103
- }
104
- result[key] = truncateStrings(value);
105
- }
106
- return result;
107
- }
108
- return obj;
109
- };
110
- const truncatedObject = truncateStrings(object);
111
- const json = JSON.stringify(truncatedObject, null, 2);
112
- this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, "\n", json);
113
- }
114
- info(message, ...args) {
115
- this.logToConsole("info", "", message, ...args);
116
- this.debug(message, args);
117
- }
118
- logToConsole(level, prefix, message, ...args) {
119
- switch (level) {
120
- case "debug": {
121
- console.log(chalk.gray(prefix), message, ...args);
122
- break;
123
- }
124
- case "error": {
125
- console.error(chalk.red(prefix), message, ...args);
126
- break;
127
- }
128
- case "info": {
129
- console.log(chalk.blue(prefix), message, ...args);
130
- break;
131
- }
132
- case "warn": {
133
- console.log(chalk.yellow(prefix), message, ...args);
134
- break;
135
- }
136
- default: {
137
- this.debug("Unknown log level:", level);
138
- console.log(chalk.blue(prefix), message, ...args);
139
- break;
140
- }
141
- }
142
- }
143
- logToFile(prefix, message, ...args) {
144
- const logLine = `${prefix} ${message} ${args.map(
145
- (arg) => typeof arg === "string" ? arg : JSON.stringify(arg)
146
- ).join(" ")}
147
- `;
148
- this.logFilePathPromise.then((logFilePath) => {
149
- try {
150
- appendFileSync(logFilePath, logLine);
151
- } catch (appendError) {
152
- if (process.env.DEBUG) {
153
- console.error("Failed to append to log file:", appendError);
154
- throw appendError;
155
- }
156
- }
157
- }).catch((error) => {
158
- if (process.env.DEBUG) {
159
- console.log("This message only visible in DEBUG mode, not in production");
160
- console.error("Failed to resolve log file path:", error);
161
- console.log(prefix, message, ...args);
162
- }
163
- });
164
- }
165
- }
166
- let logger;
167
- function initLoggerWithGlobalConfiguration() {
168
- logger = new Logger();
169
- }
170
-
171
- const SessionMessageContentSchema = z$1.object({
172
- c: z$1.string(),
173
- // Base64 encoded encrypted content
174
- t: z$1.literal("encrypted")
175
- });
176
- const UpdateBodySchema = z$1.object({
177
- message: z$1.object({
178
- id: z$1.string(),
179
- seq: z$1.number(),
180
- content: SessionMessageContentSchema
181
- }),
182
- sid: z$1.string(),
183
- // Session ID
184
- t: z$1.literal("new-message")
185
- });
186
- const UpdateSessionBodySchema = z$1.object({
187
- t: z$1.literal("update-session"),
188
- sid: z$1.string(),
189
- metadata: z$1.object({
190
- version: z$1.number(),
191
- metadata: z$1.string()
192
- }).nullish(),
193
- agentState: z$1.object({
194
- version: z$1.number(),
195
- agentState: z$1.string()
196
- }).nullish()
197
- });
198
- z$1.object({
199
- id: z$1.string(),
200
- seq: z$1.number(),
201
- body: z$1.union([UpdateBodySchema, UpdateSessionBodySchema]),
202
- createdAt: z$1.number()
203
- });
204
- z$1.object({
205
- createdAt: z$1.number(),
206
- id: z$1.string(),
207
- seq: z$1.number(),
208
- updatedAt: z$1.number(),
209
- metadata: z$1.any(),
210
- metadataVersion: z$1.number(),
211
- agentState: z$1.any().nullable(),
212
- agentStateVersion: z$1.number()
213
- });
214
- z$1.object({
215
- content: SessionMessageContentSchema,
216
- createdAt: z$1.number(),
217
- id: z$1.string(),
218
- seq: z$1.number(),
219
- updatedAt: z$1.number()
220
- });
221
- z$1.object({
222
- session: z$1.object({
223
- id: z$1.string(),
224
- tag: z$1.string(),
225
- seq: z$1.number(),
226
- createdAt: z$1.number(),
227
- updatedAt: z$1.number(),
228
- metadata: z$1.string(),
229
- metadataVersion: z$1.number(),
230
- agentState: z$1.string().nullable(),
231
- agentStateVersion: z$1.number()
232
- })
233
- });
234
- const UserMessageSchema$1 = z$1.object({
235
- role: z$1.literal("user"),
236
- content: z$1.object({
237
- type: z$1.literal("text"),
238
- text: z$1.string()
239
- }),
240
- localKey: z$1.string().optional(),
241
- // Mobile messages include this
242
- sentFrom: z$1.enum(["mobile", "cli"]).optional()
243
- // Source identifier
244
- });
245
- const AgentMessageSchema = z$1.object({
246
- role: z$1.literal("agent"),
247
- content: z$1.object({
248
- type: z$1.literal("output"),
249
- data: z$1.any()
250
- })
251
- });
252
- z$1.union([UserMessageSchema$1, AgentMessageSchema]);
253
-
254
- function encodeBase64(buffer) {
255
- return Buffer.from(buffer).toString("base64");
256
- }
257
- function encodeBase64Url(buffer) {
258
- return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
259
- }
260
- function decodeBase64(base64) {
261
- return new Uint8Array(Buffer.from(base64, "base64"));
262
- }
263
- function getRandomBytes(size) {
264
- return new Uint8Array(randomBytes(size));
265
- }
266
- function encrypt(data, secret) {
267
- const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
268
- const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
269
- const result = new Uint8Array(nonce.length + encrypted.length);
270
- result.set(nonce);
271
- result.set(encrypted, nonce.length);
272
- return result;
273
- }
274
- function decrypt(data, secret) {
275
- const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
276
- const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
277
- const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
278
- if (!decrypted) {
279
- return null;
280
- }
281
- return JSON.parse(new TextDecoder().decode(decrypted));
282
- }
283
-
284
- async function delay(ms) {
285
- return new Promise((resolve) => setTimeout(resolve, ms));
286
- }
287
- function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
288
- let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.max(currentFailureCount, maxFailureCount);
289
- return Math.round(Math.random() * maxDelayRet);
290
- }
291
- function createBackoff(opts) {
292
- return async (callback) => {
293
- let currentFailureCount = 0;
294
- const minDelay = 250;
295
- const maxDelay = 1e3;
296
- const maxFailureCount = 50;
297
- while (true) {
298
- try {
299
- return await callback();
300
- } catch (e) {
301
- if (currentFailureCount < maxFailureCount) {
302
- currentFailureCount++;
303
- }
304
- let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
305
- await delay(waitForRequest);
306
- }
307
- }
308
- };
309
- }
310
- let backoff = createBackoff();
311
-
312
- class ApiSessionClient extends EventEmitter {
313
- token;
314
- secret;
315
- sessionId;
316
- metadata;
317
- metadataVersion;
318
- agentState;
319
- agentStateVersion;
320
- socket;
321
- pendingMessages = [];
322
- pendingMessageCallback = null;
323
- rpcHandlers = /* @__PURE__ */ new Map();
324
- constructor(token, secret, session) {
325
- super();
326
- this.token = token;
327
- this.secret = secret;
328
- this.sessionId = session.id;
329
- this.metadata = session.metadata;
330
- this.metadataVersion = session.metadataVersion;
331
- this.agentState = session.agentState;
332
- this.agentStateVersion = session.agentStateVersion;
333
- this.socket = io(configuration.serverUrl, {
334
- auth: {
335
- token: this.token,
336
- clientType: "session-scoped",
337
- sessionId: this.sessionId
338
- },
339
- path: "/v1/updates",
340
- reconnection: true,
341
- reconnectionAttempts: Infinity,
342
- reconnectionDelay: 1e3,
343
- reconnectionDelayMax: 5e3,
344
- transports: ["websocket"],
345
- withCredentials: true,
346
- autoConnect: false
347
- });
348
- this.socket.on("connect", () => {
349
- logger.debug("Socket connected successfully");
350
- this.reregisterHandlers();
351
- });
352
- this.socket.on("rpc-request", async (data, callback) => {
353
- try {
354
- const method = data.method;
355
- const handler = this.rpcHandlers.get(method);
356
- if (!handler) {
357
- logger.debug("[SOCKET] [RPC] [ERROR] method not found", { method });
358
- const errorResponse = { error: "Method not found" };
359
- const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
360
- callback(encryptedError);
361
- return;
362
- }
363
- const decryptedParams = decrypt(decodeBase64(data.params), this.secret);
364
- const result = await handler(decryptedParams);
365
- const encryptedResponse = encodeBase64(encrypt(result, this.secret));
366
- callback(encryptedResponse);
367
- } catch (error) {
368
- logger.debug("[SOCKET] [RPC] [ERROR] Error handling RPC request", { error });
369
- const errorResponse = { error: error instanceof Error ? error.message : "Unknown error" };
370
- const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
371
- callback(encryptedError);
372
- }
373
- });
374
- this.socket.on("disconnect", (reason) => {
375
- logger.debug("[API] Socket disconnected:", reason);
376
- });
377
- this.socket.on("connect_error", (error) => {
378
- logger.debug("[API] Socket connection error:", error);
379
- });
380
- this.socket.on("update", (data) => {
381
- if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
382
- const body = decrypt(decodeBase64(data.body.message.content.c), this.secret);
383
- logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
384
- const userResult = UserMessageSchema$1.safeParse(body);
385
- if (userResult.success) {
386
- if (this.pendingMessageCallback) {
387
- this.pendingMessageCallback(userResult.data);
388
- } else {
389
- this.pendingMessages.push(userResult.data);
390
- }
391
- } else {
392
- this.emit("message", body);
393
- }
394
- } else if (data.body.t === "update-session") {
395
- if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
396
- this.metadata = decrypt(decodeBase64(data.body.metadata.metadata), this.secret);
397
- this.metadataVersion = data.body.metadata.version;
398
- }
399
- if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
400
- this.agentState = data.body.agentState.agentState ? decrypt(decodeBase64(data.body.agentState.agentState), this.secret) : null;
401
- this.agentStateVersion = data.body.agentState.version;
402
- }
403
- }
404
- });
405
- this.socket.on("error", (error) => {
406
- logger.debug("[API] Socket error:", error);
407
- });
408
- this.socket.connect();
409
- }
410
- onUserMessage(callback) {
411
- this.pendingMessageCallback = callback;
412
- while (this.pendingMessages.length > 0) {
413
- callback(this.pendingMessages.shift());
414
- }
415
- }
416
- /**
417
- * Send message to session
418
- * @param body - Message body (can be MessageContent or raw content for agent messages)
419
- */
420
- sendClaudeSessionMessage(body) {
421
- let content;
422
- if (body.type === "user" && typeof body.message.content === "string") {
423
- content = {
424
- role: "user",
425
- content: {
426
- type: "text",
427
- text: body.message.content
428
- }
429
- };
430
- } else {
431
- content = {
432
- role: "agent",
433
- content: {
434
- type: "output",
435
- data: body
436
- // This wraps the entire Claude message
437
- }
438
- };
439
- }
440
- logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
441
- const encrypted = encodeBase64(encrypt(content, this.secret));
442
- this.socket.emit("message", {
443
- sid: this.sessionId,
444
- message: encrypted
445
- });
446
- }
447
- /**
448
- * Send a ping message to keep the connection alive
449
- */
450
- keepAlive(thinking) {
451
- this.socket.volatile.emit("session-alive", { sid: this.sessionId, time: Date.now(), thinking });
452
- }
453
- /**
454
- * Send session death message
455
- */
456
- sendSessionDeath() {
457
- this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
458
- }
459
- /**
460
- * Update session metadata
461
- * @param handler - Handler function that returns the updated metadata
462
- */
463
- updateMetadata(handler) {
464
- backoff(async () => {
465
- let updated = handler(this.metadata);
466
- const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(updated, this.secret)) });
467
- if (answer.result === "success") {
468
- this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
469
- this.metadataVersion = answer.version;
470
- } else if (answer.result === "version-mismatch") {
471
- if (answer.version > this.metadataVersion) {
472
- this.metadataVersion = answer.version;
473
- this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
474
- }
475
- throw new Error("Metadata version mismatch");
476
- } else if (answer.result === "error") ;
477
- });
478
- }
479
- /**
480
- * Update session agent state
481
- * @param handler - Handler function that returns the updated agent state
482
- */
483
- updateAgentState(handler) {
484
- console.log("Updating agent state", this.agentState);
485
- backoff(async () => {
486
- let updated = handler(this.agentState || {});
487
- const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(updated, this.secret)) : null });
488
- if (answer.result === "success") {
489
- this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
490
- this.agentStateVersion = answer.version;
491
- console.log("Agent state updated", this.agentState);
492
- } else if (answer.result === "version-mismatch") {
493
- if (answer.version > this.agentStateVersion) {
494
- this.agentStateVersion = answer.version;
495
- this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
496
- }
497
- throw new Error("Agent state version mismatch");
498
- } else if (answer.result === "error") {
499
- console.error("Agent state update error", answer);
500
- }
501
- });
502
- }
503
- /**
504
- * Set a custom RPC handler for a specific method with encrypted arguments and responses
505
- * @param method - The method name to handle
506
- * @param handler - The handler function to call when the method is invoked
507
- */
508
- setHandler(method, handler) {
509
- const prefixedMethod = `${this.sessionId}:${method}`;
510
- this.rpcHandlers.set(prefixedMethod, handler);
511
- this.socket.emit("rpc-register", { method: prefixedMethod });
512
- logger.debug("Registered RPC handler", { method, prefixedMethod });
513
- }
514
- /**
515
- * Re-register all RPC handlers after reconnection
516
- */
517
- reregisterHandlers() {
518
- logger.debug("Re-registering RPC handlers after reconnection", {
519
- totalMethods: this.rpcHandlers.size
520
- });
521
- for (const [prefixedMethod] of this.rpcHandlers) {
522
- this.socket.emit("rpc-register", { method: prefixedMethod });
523
- logger.debug("Re-registered method", { prefixedMethod });
524
- }
525
- }
526
- /**
527
- * Wait for socket buffer to flush
528
- */
529
- async flush() {
530
- if (!this.socket.connected) {
531
- return;
532
- }
533
- return new Promise((resolve) => {
534
- this.socket.emit("ping", () => {
535
- resolve();
536
- });
537
- setTimeout(() => {
538
- resolve();
539
- }, 1e4);
540
- });
541
- }
542
- async close() {
543
- this.socket.close();
544
- }
545
- }
546
-
547
- class PushNotificationClient {
548
- token;
549
- baseUrl;
550
- expo;
551
- constructor(token, baseUrl = "https://handy-api.korshakov.org") {
552
- this.token = token;
553
- this.baseUrl = baseUrl;
554
- this.expo = new Expo();
555
- }
556
- /**
557
- * Fetch all push tokens for the authenticated user
558
- */
559
- async fetchPushTokens() {
560
- try {
561
- const response = await axios.get(
562
- `${this.baseUrl}/v1/push-tokens`,
563
- {
564
- headers: {
565
- "Authorization": `Bearer ${this.token}`,
566
- "Content-Type": "application/json"
567
- }
568
- }
569
- );
570
- logger.info(`Fetched ${response.data.tokens.length} push tokens`);
571
- return response.data.tokens;
572
- } catch (error) {
573
- logger.debug("[PUSH] [ERROR] Failed to fetch push tokens:", error);
574
- throw new Error(`Failed to fetch push tokens: ${error instanceof Error ? error.message : "Unknown error"}`);
575
- }
576
- }
577
- /**
578
- * Send push notification via Expo Push API with retry
579
- * @param messages - Array of push messages to send
580
- */
581
- async sendPushNotifications(messages) {
582
- logger.info(`Sending ${messages.length} push notifications`);
583
- const validMessages = messages.filter((message) => {
584
- if (Array.isArray(message.to)) {
585
- return message.to.every((token) => Expo.isExpoPushToken(token));
586
- }
587
- return Expo.isExpoPushToken(message.to);
588
- });
589
- if (validMessages.length === 0) {
590
- logger.info("No valid Expo push tokens found");
591
- return;
592
- }
593
- const chunks = this.expo.chunkPushNotifications(validMessages);
594
- for (const chunk of chunks) {
595
- const startTime = Date.now();
596
- const timeout = 3e5;
597
- let attempt = 0;
598
- while (true) {
599
- try {
600
- const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
601
- const errors = ticketChunk.filter((ticket) => ticket.status === "error");
602
- if (errors.length > 0) {
603
- logger.debug("[PUSH] Some notifications failed:", errors);
604
- }
605
- if (errors.length === ticketChunk.length) {
606
- throw new Error("All push notifications in chunk failed");
607
- }
608
- break;
609
- } catch (error) {
610
- const elapsed = Date.now() - startTime;
611
- if (elapsed >= timeout) {
612
- logger.debug("[PUSH] Timeout reached after 5 minutes, giving up on chunk");
613
- break;
614
- }
615
- attempt++;
616
- const delay = Math.min(1e3 * Math.pow(2, attempt), 3e4);
617
- const remainingTime = timeout - elapsed;
618
- const waitTime = Math.min(delay, remainingTime);
619
- if (waitTime > 0) {
620
- logger.debug(`[PUSH] Retrying in ${waitTime}ms (attempt ${attempt})`);
621
- await new Promise((resolve) => setTimeout(resolve, waitTime));
622
- }
623
- }
624
- }
625
- }
626
- logger.info(`Push notifications sent successfully`);
627
- }
628
- /**
629
- * Send a push notification to all registered devices for the user
630
- * @param title - Notification title
631
- * @param body - Notification body
632
- * @param data - Additional data to send with the notification
633
- */
634
- async sendToAllDevices(title, body, data) {
635
- const tokens = await this.fetchPushTokens();
636
- if (tokens.length === 0) {
637
- logger.info("No push tokens found for user");
638
- return;
639
- }
640
- const messages = tokens.map((token) => ({
641
- to: token.token,
642
- title,
643
- body,
644
- data,
645
- sound: "default",
646
- priority: "high"
647
- }));
648
- await this.sendPushNotifications(messages);
649
- }
650
- }
651
-
652
- class ApiClient {
653
- token;
654
- secret;
655
- pushClient;
656
- constructor(token, secret) {
657
- this.token = token;
658
- this.secret = secret;
659
- this.pushClient = new PushNotificationClient(token);
660
- }
661
- /**
662
- * Create a new session or load existing one with the given tag
663
- */
664
- async getOrCreateSession(opts) {
665
- try {
666
- const response = await axios.post(
667
- `${configuration.serverUrl}/v1/sessions`,
668
- {
669
- tag: opts.tag,
670
- metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
671
- agentState: opts.state ? encodeBase64(encrypt(opts.state, this.secret)) : null
672
- },
673
- {
674
- headers: {
675
- "Authorization": `Bearer ${this.token}`,
676
- "Content-Type": "application/json"
677
- }
678
- }
679
- );
680
- logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
681
- let raw = response.data.session;
682
- let session = {
683
- id: raw.id,
684
- createdAt: raw.createdAt,
685
- updatedAt: raw.updatedAt,
686
- seq: raw.seq,
687
- metadata: decrypt(decodeBase64(raw.metadata), this.secret),
688
- metadataVersion: raw.metadataVersion,
689
- agentState: raw.agentState ? decrypt(decodeBase64(raw.agentState), this.secret) : null,
690
- agentStateVersion: raw.agentStateVersion
691
- };
692
- return session;
693
- } catch (error) {
694
- logger.debug("[API] [ERROR] Failed to get or create session:", error);
695
- throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
696
- }
697
- }
698
- /**
699
- * Start realtime session client
700
- * @param id - Session ID
701
- * @returns Session client
702
- */
703
- session(session) {
704
- return new ApiSessionClient(this.token, this.secret, session);
705
- }
706
- /**
707
- * Get push notification client
708
- * @returns Push notification client
709
- */
710
- push() {
711
- return this.pushClient;
712
- }
713
- }
27
+ import { EventEmitter } from 'node:events';
28
+ import { io } from 'socket.io-client';
29
+ import { homedir as homedir$1, hostname } from 'os';
30
+ import { existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync, mkdirSync as mkdirSync$1, writeFileSync, chmodSync } from 'fs';
31
+ import 'expo-server-sdk';
714
32
 
715
33
  function formatClaudeMessage(message, onAssistantResult) {
716
34
  logger.debugLargeJson("[CLAUDE] Message from non interactive & remote mode:", message);
@@ -1199,105 +517,6 @@ class InvalidateSync {
1199
517
  };
1200
518
  }
1201
519
 
1202
- const UsageSchema = z$1.object({
1203
- input_tokens: z$1.number().int().nonnegative(),
1204
- cache_creation_input_tokens: z$1.number().int().nonnegative().optional(),
1205
- cache_read_input_tokens: z$1.number().int().nonnegative().optional(),
1206
- output_tokens: z$1.number().int().nonnegative(),
1207
- service_tier: z$1.string().optional()
1208
- });
1209
- const TextContentSchema = z$1.object({
1210
- type: z$1.literal("text"),
1211
- text: z$1.string()
1212
- });
1213
- const ThinkingContentSchema = z$1.object({
1214
- type: z$1.literal("thinking"),
1215
- thinking: z$1.string(),
1216
- signature: z$1.string()
1217
- });
1218
- const ToolUseContentSchema = z$1.object({
1219
- type: z$1.literal("tool_use"),
1220
- id: z$1.string(),
1221
- name: z$1.string(),
1222
- input: z$1.unknown()
1223
- // Tool-specific input parameters
1224
- });
1225
- const ToolResultContentSchema = z$1.object({
1226
- tool_use_id: z$1.string(),
1227
- type: z$1.literal("tool_result"),
1228
- content: z$1.union([
1229
- z$1.string(),
1230
- // For simple string responses
1231
- z$1.array(TextContentSchema)
1232
- // For structured content blocks (typically text)
1233
- ]),
1234
- is_error: z$1.boolean().optional()
1235
- });
1236
- const ContentSchema = z$1.union([
1237
- TextContentSchema,
1238
- ThinkingContentSchema,
1239
- ToolUseContentSchema,
1240
- ToolResultContentSchema
1241
- ]);
1242
- const UserMessageSchema = z$1.object({
1243
- role: z$1.literal("user"),
1244
- content: z$1.union([
1245
- z$1.string(),
1246
- // Simple string content
1247
- z$1.array(z$1.union([ToolResultContentSchema, TextContentSchema]))
1248
- ])
1249
- });
1250
- const AssistantMessageSchema = z$1.object({
1251
- id: z$1.string(),
1252
- type: z$1.literal("message"),
1253
- role: z$1.literal("assistant"),
1254
- model: z$1.string(),
1255
- content: z$1.array(ContentSchema),
1256
- stop_reason: z$1.string().nullable(),
1257
- stop_sequence: z$1.string().nullable(),
1258
- usage: UsageSchema
1259
- });
1260
- const BaseEntrySchema = z$1.object({
1261
- cwd: z$1.string(),
1262
- sessionId: z$1.string(),
1263
- version: z$1.string(),
1264
- uuid: z$1.string(),
1265
- timestamp: z$1.string().datetime(),
1266
- parent_tool_use_id: z$1.string().nullable().optional()
1267
- });
1268
- const SummaryEntrySchema = z$1.object({
1269
- type: z$1.literal("summary"),
1270
- summary: z$1.string(),
1271
- leafUuid: z$1.string()
1272
- });
1273
- const UserEntrySchema = BaseEntrySchema.extend({
1274
- type: z$1.literal("user"),
1275
- message: UserMessageSchema,
1276
- isMeta: z$1.boolean().optional(),
1277
- toolUseResult: z$1.unknown().optional()
1278
- // Present when user responds to tool use
1279
- });
1280
- const AssistantEntrySchema = BaseEntrySchema.extend({
1281
- type: z$1.literal("assistant"),
1282
- message: AssistantMessageSchema,
1283
- requestId: z$1.string().optional()
1284
- });
1285
- const SystemEntrySchema = BaseEntrySchema.extend({
1286
- type: z$1.literal("system"),
1287
- content: z$1.string(),
1288
- isMeta: z$1.boolean().optional(),
1289
- level: z$1.string().optional(),
1290
- parentUuid: z$1.string().optional(),
1291
- isSidechain: z$1.boolean().optional(),
1292
- userType: z$1.string().optional()
1293
- });
1294
- const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
1295
- UserEntrySchema,
1296
- AssistantEntrySchema,
1297
- SummaryEntrySchema,
1298
- SystemEntrySchema
1299
- ]);
1300
-
1301
520
  function createSessionScanner(opts) {
1302
521
  const projectName = resolve(opts.workingDirectory).replace(/\//g, "-");
1303
522
  const projectDir = join(homedir(), ".claude", "projects", projectName);
@@ -1306,6 +525,7 @@ function createSessionScanner(opts) {
1306
525
  let currentSessionId = null;
1307
526
  let currentSessionWatcherAbortController = null;
1308
527
  let processedMessages = /* @__PURE__ */ new Set();
528
+ let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
1309
529
  const sync = new InvalidateSync(async () => {
1310
530
  let sessions = [];
1311
531
  for (let p of pendingSessions) {
@@ -1338,7 +558,14 @@ function createSessionScanner(opts) {
1338
558
  processedMessages.add(key);
1339
559
  logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
1340
560
  logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
1341
- opts.onMessage(parsed.data);
561
+ if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
562
+ const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
563
+ if (currentCounter && currentCounter > 0) {
564
+ seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
565
+ continue;
566
+ }
567
+ }
568
+ opts.onMessage(message);
1342
569
  } catch (e) {
1343
570
  continue;
1344
571
  }
@@ -1392,8 +619,12 @@ function createSessionScanner(opts) {
1392
619
  if (currentSessionId) {
1393
620
  pendingSessions.add(currentSessionId);
1394
621
  }
622
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId}`);
1395
623
  currentSessionId = sessionId;
1396
624
  sync.invalidate();
625
+ },
626
+ onRemoteUserMessageForDeduplication: (messageContent) => {
627
+ seenRemoteUserMessageCounters.set(messageContent, (seenRemoteUserMessageCounters.get(messageContent) || 0) + 1);
1397
628
  }
1398
629
  };
1399
630
  }
@@ -1427,32 +658,24 @@ function sortKeys(value) {
1427
658
  }
1428
659
 
1429
660
  async function loop(opts) {
1430
- let mode = "interactive";
661
+ let mode = opts.startingMode ?? "interactive";
1431
662
  let currentMessageQueue = new MessageQueue();
1432
663
  let sessionId = null;
1433
664
  let onMessage = null;
1434
- let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
1435
- opts.session.onUserMessage((message) => {
1436
- logger.debugLargeJson("User message pushed to queue:", message);
1437
- currentMessageQueue.push(message.content.text);
1438
- seenRemoteUserMessageCounters.set(message.content.text, (seenRemoteUserMessageCounters.get(message.content.text) || 0) + 1);
1439
- if (onMessage) {
1440
- onMessage();
1441
- }
1442
- });
1443
665
  const sessionScanner = createSessionScanner({
1444
666
  workingDirectory: opts.path,
1445
667
  onMessage: (message) => {
1446
- if (message.type === "user" && typeof message.message.content === "string") {
1447
- const currentCounter = seenRemoteUserMessageCounters.get(message.message.content);
1448
- if (currentCounter && currentCounter > 0) {
1449
- seenRemoteUserMessageCounters.set(message.message.content, currentCounter - 1);
1450
- return;
1451
- }
1452
- }
1453
668
  opts.session.sendClaudeSessionMessage(message);
1454
669
  }
1455
670
  });
671
+ opts.session.onUserMessage((message) => {
672
+ sessionScanner.onRemoteUserMessageForDeduplication(message.content.text);
673
+ currentMessageQueue.push(message.content.text);
674
+ logger.debugLargeJson("User message pushed to queue:", message);
675
+ if (onMessage) {
676
+ onMessage();
677
+ }
678
+ });
1456
679
  let onSessionFound = (newSessionId) => {
1457
680
  sessionId = newSessionId;
1458
681
  sessionScanner.onNewSession(newSessionId);
@@ -1507,10 +730,14 @@ async function loop(opts) {
1507
730
  mode = "interactive";
1508
731
  remoteAbortController.abort();
1509
732
  }
1510
- process.stdin.setRawMode(false);
733
+ if (process.stdin.isTTY) {
734
+ process.stdin.setRawMode(false);
735
+ }
1511
736
  };
1512
737
  process.stdin.resume();
1513
- process.stdin.setRawMode(true);
738
+ if (process.stdin.isTTY) {
739
+ process.stdin.setRawMode(true);
740
+ }
1514
741
  process.stdin.setEncoding("utf8");
1515
742
  process.stdin.on("data", abortHandler);
1516
743
  try {
@@ -1528,7 +755,9 @@ async function loop(opts) {
1528
755
  });
1529
756
  } finally {
1530
757
  process.stdin.off("data", abortHandler);
1531
- process.stdin.setRawMode(false);
758
+ if (process.stdin.isTTY) {
759
+ process.stdin.setRawMode(false);
760
+ }
1532
761
  currentMessageQueue.close();
1533
762
  currentMessageQueue = new MessageQueue();
1534
763
  }
@@ -1639,16 +868,410 @@ class InterruptController {
1639
868
  }
1640
869
  }
1641
870
 
871
+ var version = "0.1.10";
872
+ var packageJson = {
873
+ version: version};
874
+
875
+ async function startAnthropicActivityProxy(onClaudeActivity) {
876
+ const requestTimeouts = /* @__PURE__ */ new Map();
877
+ let requestCounter = 0;
878
+ let idleTimer = null;
879
+ const maxTimeBeforeIdle = 50;
880
+ const requestTimeout = 5 * 60 * 1e3;
881
+ const cleanupRequest = (requestId, reason) => {
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
+ }
917
+ });
918
+ req.on("end", () => {
919
+ const body = Buffer.concat(chunks);
920
+ let targetUrl;
921
+ if (isAnthropicRequest) {
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();
961
+ });
962
+ });
963
+ server.on("connect", (req, clientSocket, head) => {
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);
981
+ });
982
+ const cleanup = () => {
983
+ if (isAnthropicRequest) {
984
+ cleanupRequest(requestId, "CONNECT closed");
985
+ }
986
+ };
987
+ serverSocket.on("error", (err) => {
988
+ logger.debug(`[AnthropicProxy #${requestId}] CONNECT error:`, err.message);
989
+ clientSocket.end();
990
+ cleanup();
991
+ });
992
+ clientSocket.on("error", cleanup);
993
+ clientSocket.on("end", cleanup);
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
+ }
1002
+ });
1003
+ });
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
+ }
1021
+
1022
+ const execAsync = promisify(exec);
1023
+ function registerHandlers(session, interruptController, permissionCallbacks) {
1024
+ session.setHandler("abort", async () => {
1025
+ logger.info("Abort request - interrupting Claude");
1026
+ await interruptController.interrupt();
1027
+ });
1028
+ if (permissionCallbacks) {
1029
+ session.setHandler("permission", async (message) => {
1030
+ logger.info("Permission response" + JSON.stringify(message));
1031
+ const id = message.id;
1032
+ const resolve = permissionCallbacks.requests.get(id);
1033
+ if (resolve) {
1034
+ if (!message.approved) {
1035
+ logger.debug("Permission denied, interrupting Claude");
1036
+ await interruptController.interrupt();
1037
+ }
1038
+ resolve({ approved: message.approved, reason: message.reason });
1039
+ permissionCallbacks.requests.delete(id);
1040
+ } else {
1041
+ logger.info("Permission request stale, likely timed out");
1042
+ return;
1043
+ }
1044
+ session.updateAgentState((currentState) => {
1045
+ let r = { ...currentState.requests };
1046
+ delete r[id];
1047
+ return {
1048
+ ...currentState,
1049
+ requests: r
1050
+ };
1051
+ });
1052
+ });
1053
+ }
1054
+ session.setHandler("bash", async (data) => {
1055
+ logger.info("Shell command request:", data.command);
1056
+ try {
1057
+ const options = {
1058
+ cwd: data.cwd,
1059
+ timeout: data.timeout || 3e4
1060
+ // Default 30 seconds timeout
1061
+ };
1062
+ const { stdout, stderr } = await execAsync(data.command, options);
1063
+ return {
1064
+ success: true,
1065
+ stdout: stdout || "",
1066
+ stderr: stderr || "",
1067
+ exitCode: 0
1068
+ };
1069
+ } catch (error) {
1070
+ const execError = error;
1071
+ if (execError.code === "ETIMEDOUT" || execError.killed) {
1072
+ return {
1073
+ success: false,
1074
+ stdout: execError.stdout || "",
1075
+ stderr: execError.stderr || "",
1076
+ exitCode: typeof execError.code === "number" ? execError.code : -1,
1077
+ error: "Command timed out"
1078
+ };
1079
+ }
1080
+ return {
1081
+ success: false,
1082
+ stdout: execError.stdout || "",
1083
+ stderr: execError.stderr || execError.message || "Command failed",
1084
+ exitCode: typeof execError.code === "number" ? execError.code : 1,
1085
+ error: execError.message || "Command failed"
1086
+ };
1087
+ }
1088
+ });
1089
+ session.setHandler("readFile", async (data) => {
1090
+ logger.info("Read file request:", data.path);
1091
+ try {
1092
+ const buffer = await readFile$1(data.path);
1093
+ const content = buffer.toString("base64");
1094
+ return { success: true, content };
1095
+ } catch (error) {
1096
+ logger.debug("Failed to read file:", error);
1097
+ return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
1098
+ }
1099
+ });
1100
+ session.setHandler("writeFile", async (data) => {
1101
+ logger.info("Write file request:", data.path);
1102
+ try {
1103
+ if (data.expectedHash !== null && data.expectedHash !== void 0) {
1104
+ try {
1105
+ const existingBuffer = await readFile$1(data.path);
1106
+ const existingHash = createHash("sha256").update(existingBuffer).digest("hex");
1107
+ if (existingHash !== data.expectedHash) {
1108
+ return {
1109
+ success: false,
1110
+ error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
1111
+ };
1112
+ }
1113
+ } catch (error) {
1114
+ const nodeError = error;
1115
+ if (nodeError.code !== "ENOENT") {
1116
+ throw error;
1117
+ }
1118
+ return {
1119
+ success: false,
1120
+ error: "File does not exist but hash was provided"
1121
+ };
1122
+ }
1123
+ } else {
1124
+ try {
1125
+ await stat(data.path);
1126
+ return {
1127
+ success: false,
1128
+ error: "File already exists but was expected to be new"
1129
+ };
1130
+ } catch (error) {
1131
+ const nodeError = error;
1132
+ if (nodeError.code !== "ENOENT") {
1133
+ throw error;
1134
+ }
1135
+ }
1136
+ }
1137
+ const buffer = Buffer.from(data.content, "base64");
1138
+ await writeFile(data.path, buffer);
1139
+ const hash = createHash("sha256").update(buffer).digest("hex");
1140
+ return { success: true, hash };
1141
+ } catch (error) {
1142
+ logger.debug("Failed to write file:", error);
1143
+ return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
1144
+ }
1145
+ });
1146
+ session.setHandler("listDirectory", async (data) => {
1147
+ logger.info("List directory request:", data.path);
1148
+ try {
1149
+ const entries = await readdir(data.path, { withFileTypes: true });
1150
+ const directoryEntries = await Promise.all(
1151
+ entries.map(async (entry) => {
1152
+ const fullPath = join$1(data.path, entry.name);
1153
+ let type = "other";
1154
+ let size;
1155
+ let modified;
1156
+ if (entry.isDirectory()) {
1157
+ type = "directory";
1158
+ } else if (entry.isFile()) {
1159
+ type = "file";
1160
+ }
1161
+ try {
1162
+ const stats = await stat(fullPath);
1163
+ size = stats.size;
1164
+ modified = stats.mtime.getTime();
1165
+ } catch (error) {
1166
+ logger.debug(`Failed to stat ${fullPath}:`, error);
1167
+ }
1168
+ return {
1169
+ name: entry.name,
1170
+ type,
1171
+ size,
1172
+ modified
1173
+ };
1174
+ })
1175
+ );
1176
+ directoryEntries.sort((a, b) => {
1177
+ if (a.type === "directory" && b.type !== "directory") return -1;
1178
+ if (a.type !== "directory" && b.type === "directory") return 1;
1179
+ return a.name.localeCompare(b.name);
1180
+ });
1181
+ return { success: true, entries: directoryEntries };
1182
+ } catch (error) {
1183
+ logger.debug("Failed to list directory:", error);
1184
+ return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
1185
+ }
1186
+ });
1187
+ session.setHandler("getDirectoryTree", async (data) => {
1188
+ logger.info("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
1189
+ async function buildTree(path, name, currentDepth) {
1190
+ try {
1191
+ const stats = await stat(path);
1192
+ const node = {
1193
+ name,
1194
+ path,
1195
+ type: stats.isDirectory() ? "directory" : "file",
1196
+ size: stats.size,
1197
+ modified: stats.mtime.getTime()
1198
+ };
1199
+ if (stats.isDirectory() && currentDepth < data.maxDepth) {
1200
+ const entries = await readdir(path, { withFileTypes: true });
1201
+ const children = [];
1202
+ await Promise.all(
1203
+ entries.map(async (entry) => {
1204
+ if (entry.isSymbolicLink()) {
1205
+ logger.debug(`Skipping symlink: ${join$1(path, entry.name)}`);
1206
+ return;
1207
+ }
1208
+ const childPath = join$1(path, entry.name);
1209
+ const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
1210
+ if (childNode) {
1211
+ children.push(childNode);
1212
+ }
1213
+ })
1214
+ );
1215
+ children.sort((a, b) => {
1216
+ if (a.type === "directory" && b.type !== "directory") return -1;
1217
+ if (a.type !== "directory" && b.type === "directory") return 1;
1218
+ return a.name.localeCompare(b.name);
1219
+ });
1220
+ node.children = children;
1221
+ }
1222
+ return node;
1223
+ } catch (error) {
1224
+ logger.debug(`Failed to process ${path}:`, error instanceof Error ? error.message : String(error));
1225
+ return null;
1226
+ }
1227
+ }
1228
+ try {
1229
+ if (data.maxDepth < 0) {
1230
+ return { success: false, error: "maxDepth must be non-negative" };
1231
+ }
1232
+ const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
1233
+ const tree = await buildTree(data.path, baseName, 0);
1234
+ if (!tree) {
1235
+ return { success: false, error: "Failed to access the specified path" };
1236
+ }
1237
+ return { success: true, tree };
1238
+ } catch (error) {
1239
+ logger.debug("Failed to get directory tree:", error);
1240
+ return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
1241
+ }
1242
+ });
1243
+ }
1244
+
1642
1245
  async function start(credentials, options = {}) {
1643
1246
  const workingDirectory = process.cwd();
1644
1247
  const sessionTag = randomUUID();
1645
1248
  const api = new ApiClient(credentials.token, credentials.secret);
1646
1249
  let state = {};
1647
- let metadata = { path: workingDirectory, host: os.hostname() };
1250
+ let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version, os: os.platform() };
1648
1251
  const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
1649
1252
  logger.debug(`Session created: ${response.id}`);
1650
1253
  const session = api.session(response);
1651
1254
  const pushClient = api.push();
1255
+ let thinking = false;
1256
+ let pingInterval = setInterval(() => {
1257
+ session.keepAlive(thinking);
1258
+ }, 2e3);
1259
+ const antropicActivityProxy = await startAnthropicActivityProxy(
1260
+ (activity) => {
1261
+ const newThinking = activity === "working";
1262
+ if (newThinking !== thinking) {
1263
+ thinking = newThinking;
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}`);
1272
+ const logPath = await logger.logFilePathPromise;
1273
+ logger.infoDeveloper(`Session: ${response.id}`);
1274
+ logger.infoDeveloper(`Logs: ${logPath}`);
1652
1275
  const interruptController = new InterruptController();
1653
1276
  let requests = /* @__PURE__ */ new Map();
1654
1277
  const permissionServer = await startPermissionServerV2(async (request) => {
@@ -1701,33 +1324,7 @@ async function start(credentials, options = {}) {
1701
1324
  promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
1702
1325
  return promise;
1703
1326
  });
1704
- session.setHandler("permission", (message) => {
1705
- logger.info("Permission response" + JSON.stringify(message));
1706
- const id = message.id;
1707
- const resolve = requests.get(id);
1708
- if (resolve) {
1709
- resolve({ approved: message.approved, reason: message.reason });
1710
- } else {
1711
- logger.info("Permission request stale, likely timed out");
1712
- return;
1713
- }
1714
- session.updateAgentState((currentState) => {
1715
- let r = { ...currentState.requests };
1716
- delete r[id];
1717
- return {
1718
- ...currentState,
1719
- requests: r
1720
- };
1721
- });
1722
- });
1723
- session.setHandler("abort", async () => {
1724
- logger.info("Abort request - interrupting Claude");
1725
- await interruptController.interrupt();
1726
- });
1727
- let thinking = false;
1728
- const pingInterval = setInterval(() => {
1729
- session.keepAlive(thinking);
1730
- }, 15e3);
1327
+ registerHandlers(session, interruptController, { requests });
1731
1328
  const onAssistantResult = async (result) => {
1732
1329
  try {
1733
1330
  const summary = "result" in result && result.result ? result.result.substring(0, 100) + (result.result.length > 100 ? "..." : "") : "";
@@ -1751,6 +1348,7 @@ async function start(credentials, options = {}) {
1751
1348
  path: workingDirectory,
1752
1349
  model: options.model,
1753
1350
  permissionMode: options.permissionMode,
1351
+ startingMode: options.startingMode,
1754
1352
  mcpServers: {
1755
1353
  "permission": {
1756
1354
  type: "http",
@@ -1758,18 +1356,38 @@ async function start(credentials, options = {}) {
1758
1356
  }
1759
1357
  },
1760
1358
  permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
1761
- onThinking: (t) => {
1762
- thinking = t;
1763
- session.keepAlive(t);
1764
- },
1765
1359
  session,
1766
1360
  onAssistantResult,
1767
1361
  interruptController
1768
1362
  });
1769
1363
  clearInterval(pingInterval);
1364
+ if (antropicActivityProxy) {
1365
+ logger.debug("[AnthropicProxy] Shutting down thinking activity monitoring proxy");
1366
+ antropicActivityProxy.cleanup();
1367
+ }
1770
1368
  process.exit(0);
1771
1369
  }
1772
1370
 
1371
+ const defaultSettings = {
1372
+ onboardingCompleted: false
1373
+ };
1374
+ async function readSettings() {
1375
+ if (!existsSync(configuration.settingsFile)) {
1376
+ return { ...defaultSettings };
1377
+ }
1378
+ try {
1379
+ const content = await readFile(configuration.settingsFile, "utf8");
1380
+ return JSON.parse(content);
1381
+ } catch {
1382
+ return { ...defaultSettings };
1383
+ }
1384
+ }
1385
+ async function writeSettings(settings) {
1386
+ if (!existsSync(configuration.happyDir)) {
1387
+ await mkdir(configuration.happyDir, { recursive: true });
1388
+ }
1389
+ await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
1390
+ }
1773
1391
  const credentialsSchema = z.object({
1774
1392
  secret: z.string().base64(),
1775
1393
  token: z.string()
@@ -1793,7 +1411,7 @@ async function writeCredentials(credentials) {
1793
1411
  if (!existsSync(configuration.happyDir)) {
1794
1412
  await mkdir(configuration.happyDir, { recursive: true });
1795
1413
  }
1796
- await writeFile(configuration.privateKeyFile, JSON.stringify({
1414
+ await writeFile$1(configuration.privateKeyFile, JSON.stringify({
1797
1415
  secret: encodeBase64(credentials.secret),
1798
1416
  token: credentials.token
1799
1417
  }, null, 2));
@@ -1824,7 +1442,12 @@ async function doAuth() {
1824
1442
  return null;
1825
1443
  }
1826
1444
  console.log("Please, authenticate using mobile app");
1827
- displayQRCode("happy://terminal?" + encodeBase64Url(keypair.publicKey));
1445
+ const authUrl = "happy://terminal?" + encodeBase64Url(keypair.publicKey);
1446
+ displayQRCode(authUrl);
1447
+ if (process.env.DEBUG === "1") {
1448
+ console.log("\n\u{1F4CB} For manual entry, copy this URL:");
1449
+ console.log(authUrl);
1450
+ }
1828
1451
  let credentials = null;
1829
1452
  while (true) {
1830
1453
  try {
@@ -1866,6 +1489,355 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
1866
1489
  return decrypted;
1867
1490
  }
1868
1491
 
1492
+ class ApiDaemonSession extends EventEmitter {
1493
+ socket;
1494
+ machineIdentity;
1495
+ keepAliveInterval = null;
1496
+ token;
1497
+ secret;
1498
+ constructor(token, secret, machineIdentity) {
1499
+ super();
1500
+ this.token = token;
1501
+ this.secret = secret;
1502
+ this.machineIdentity = machineIdentity;
1503
+ const socket = io(configuration.serverUrl, {
1504
+ auth: {
1505
+ token: this.token,
1506
+ clientType: "machine-scoped",
1507
+ machineId: this.machineIdentity.machineId
1508
+ },
1509
+ path: "/v1/user-machine-daemon",
1510
+ reconnection: true,
1511
+ reconnectionAttempts: Infinity,
1512
+ reconnectionDelay: 1e3,
1513
+ reconnectionDelayMax: 5e3,
1514
+ transports: ["websocket"],
1515
+ withCredentials: true,
1516
+ autoConnect: false
1517
+ });
1518
+ socket.on("connect", () => {
1519
+ logger.debug("[DAEMON] Connected to server");
1520
+ this.emit("connected");
1521
+ socket.emit("machine-connect", {
1522
+ token: this.token,
1523
+ machineIdentity: encodeBase64(encrypt(this.machineIdentity, this.secret))
1524
+ });
1525
+ this.startKeepAlive();
1526
+ });
1527
+ socket.on("disconnect", () => {
1528
+ logger.debug("[DAEMON] Disconnected from server");
1529
+ this.emit("disconnected");
1530
+ this.stopKeepAlive();
1531
+ });
1532
+ socket.on("spawn-session", async (encryptedData, callback) => {
1533
+ let requestData;
1534
+ try {
1535
+ requestData = decrypt(decodeBase64(encryptedData), this.secret);
1536
+ logger.debug("[DAEMON] Received spawn-session request", requestData);
1537
+ const args = [
1538
+ "--directory",
1539
+ requestData.directory,
1540
+ "--happy-starting-mode",
1541
+ requestData.startingMode
1542
+ ];
1543
+ if (requestData.metadata) {
1544
+ args.push("--metadata", requestData.metadata);
1545
+ }
1546
+ if (requestData.startingMode === "interactive" && process.platform === "darwin") {
1547
+ const script = `
1548
+ tell application "Terminal"
1549
+ activate
1550
+ do script "cd ${requestData.directory} && happy ${args.join(" ")}"
1551
+ end tell
1552
+ `;
1553
+ spawn$1("osascript", ["-e", script], { detached: true });
1554
+ } else {
1555
+ const child = spawn$1("happy", args, {
1556
+ detached: true,
1557
+ stdio: "ignore",
1558
+ cwd: requestData.directory
1559
+ });
1560
+ child.unref();
1561
+ }
1562
+ const result = { success: true };
1563
+ socket.emit("session-spawn-result", {
1564
+ requestId: requestData.requestId,
1565
+ result: encodeBase64(encrypt(result, this.secret))
1566
+ });
1567
+ callback(encodeBase64(encrypt({ success: true }, this.secret)));
1568
+ } catch (error) {
1569
+ logger.debug("[DAEMON] Failed to spawn session", error);
1570
+ const errorResult = {
1571
+ success: false,
1572
+ error: error instanceof Error ? error.message : "Unknown error"
1573
+ };
1574
+ socket.emit("session-spawn-result", {
1575
+ requestId: requestData?.requestId || "",
1576
+ result: encodeBase64(encrypt(errorResult, this.secret))
1577
+ });
1578
+ callback(encodeBase64(encrypt(errorResult, this.secret)));
1579
+ }
1580
+ });
1581
+ socket.on("daemon-command", (data) => {
1582
+ switch (data.command) {
1583
+ case "shutdown":
1584
+ this.shutdown();
1585
+ break;
1586
+ case "status":
1587
+ this.emit("status-request");
1588
+ break;
1589
+ }
1590
+ });
1591
+ this.socket = socket;
1592
+ }
1593
+ startKeepAlive() {
1594
+ this.stopKeepAlive();
1595
+ this.keepAliveInterval = setInterval(() => {
1596
+ this.socket.volatile.emit("machine-alive", {
1597
+ time: Date.now()
1598
+ });
1599
+ }, 2e4);
1600
+ }
1601
+ stopKeepAlive() {
1602
+ if (this.keepAliveInterval) {
1603
+ clearInterval(this.keepAliveInterval);
1604
+ this.keepAliveInterval = null;
1605
+ }
1606
+ }
1607
+ connect() {
1608
+ this.socket.connect();
1609
+ }
1610
+ shutdown() {
1611
+ this.stopKeepAlive();
1612
+ this.socket.close();
1613
+ this.emit("shutdown");
1614
+ }
1615
+ }
1616
+
1617
+ const DAEMON_PID_FILE = join$1(homedir$1(), ".happy", "daemon-pid");
1618
+ async function startDaemon() {
1619
+ if (isDaemonRunning()) {
1620
+ console.log("Happy daemon is already running");
1621
+ process.exit(0);
1622
+ }
1623
+ logger.info("Happy CLI daemon started successfully");
1624
+ writePidFile();
1625
+ process.on("SIGINT", stopDaemon);
1626
+ process.on("SIGTERM", stopDaemon);
1627
+ process.on("exit", stopDaemon);
1628
+ try {
1629
+ const settings = await readSettings() || { onboardingCompleted: false };
1630
+ if (!settings.machineId) {
1631
+ settings.machineId = crypto.randomUUID();
1632
+ settings.machineHost = hostname();
1633
+ await writeSettings(settings);
1634
+ }
1635
+ const machineIdentity = {
1636
+ machineId: settings.machineId,
1637
+ machineHost: settings.machineHost || hostname(),
1638
+ platform: process.platform,
1639
+ version: process.env.npm_package_version || "unknown"
1640
+ };
1641
+ let credentials = await readCredentials();
1642
+ if (!credentials) {
1643
+ logger.debug("[DAEMON] No credentials found, running auth");
1644
+ await doAuth();
1645
+ credentials = await readCredentials();
1646
+ if (!credentials) {
1647
+ throw new Error("Failed to authenticate");
1648
+ }
1649
+ }
1650
+ const { token, secret } = credentials;
1651
+ const daemon = new ApiDaemonSession(token, secret, machineIdentity);
1652
+ daemon.on("connected", () => {
1653
+ logger.debug("[DAEMON] Successfully connected to server");
1654
+ });
1655
+ daemon.on("disconnected", () => {
1656
+ logger.debug("[DAEMON] Disconnected from server");
1657
+ });
1658
+ daemon.on("shutdown", () => {
1659
+ logger.debug("[DAEMON] Shutdown requested");
1660
+ stopDaemon();
1661
+ process.exit(0);
1662
+ });
1663
+ daemon.connect();
1664
+ setInterval(() => {
1665
+ }, 1e3);
1666
+ } catch (error) {
1667
+ logger.debug("[DAEMON] Failed to start daemon", error);
1668
+ stopDaemon();
1669
+ process.exit(1);
1670
+ }
1671
+ process.on("SIGINT", () => process.exit(0));
1672
+ process.on("SIGTERM", () => process.exit(0));
1673
+ process.on("exit", () => process.exit(0));
1674
+ while (true) {
1675
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1676
+ }
1677
+ }
1678
+ function isDaemonRunning() {
1679
+ try {
1680
+ if (!existsSync$1(DAEMON_PID_FILE)) {
1681
+ console.log("No PID file found");
1682
+ return false;
1683
+ }
1684
+ const pid = parseInt(readFileSync$1(DAEMON_PID_FILE, "utf-8"));
1685
+ try {
1686
+ process.kill(pid, 0);
1687
+ return true;
1688
+ } catch (error) {
1689
+ console.log("Process not running", error);
1690
+ unlinkSync(DAEMON_PID_FILE);
1691
+ return false;
1692
+ }
1693
+ } catch {
1694
+ return false;
1695
+ }
1696
+ }
1697
+ function writePidFile() {
1698
+ const happyDir = join$1(homedir$1(), ".happy");
1699
+ if (!existsSync$1(happyDir)) {
1700
+ mkdirSync$1(happyDir, { recursive: true });
1701
+ }
1702
+ writeFileSync(DAEMON_PID_FILE, process.pid.toString());
1703
+ }
1704
+ function stopDaemon() {
1705
+ try {
1706
+ if (existsSync$1(DAEMON_PID_FILE)) {
1707
+ logger.debug("[DAEMON] Stopping daemon");
1708
+ process.kill(parseInt(readFileSync$1(DAEMON_PID_FILE, "utf-8")), "SIGTERM");
1709
+ unlinkSync(DAEMON_PID_FILE);
1710
+ }
1711
+ } catch (error) {
1712
+ logger.debug("[DAEMON] Error cleaning up PID file", error);
1713
+ }
1714
+ }
1715
+
1716
+ function trimIdent(text) {
1717
+ const lines = text.split("\n");
1718
+ while (lines.length > 0 && lines[0].trim() === "") {
1719
+ lines.shift();
1720
+ }
1721
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
1722
+ lines.pop();
1723
+ }
1724
+ const minSpaces = lines.reduce((min, line) => {
1725
+ if (line.trim() === "") {
1726
+ return min;
1727
+ }
1728
+ const leadingSpaces = line.match(/^\s*/)[0].length;
1729
+ return Math.min(min, leadingSpaces);
1730
+ }, Infinity);
1731
+ const trimmedLines = lines.map((line) => line.slice(minSpaces));
1732
+ return trimmedLines.join("\n");
1733
+ }
1734
+
1735
+ const PLIST_LABEL$1 = "com.happy-cli.daemon";
1736
+ const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
1737
+ const USER_HOME = process.env.HOME || process.env.USERPROFILE;
1738
+ async function install$1() {
1739
+ try {
1740
+ if (existsSync$1(PLIST_FILE$1)) {
1741
+ logger.info("Daemon plist already exists. Uninstalling first...");
1742
+ execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
1743
+ }
1744
+ const happyPath = process.argv[0];
1745
+ const scriptPath = process.argv[1];
1746
+ const plistContent = trimIdent(`
1747
+ <?xml version="1.0" encoding="UTF-8"?>
1748
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1749
+ <plist version="1.0">
1750
+ <dict>
1751
+ <key>Label</key>
1752
+ <string>${PLIST_LABEL$1}</string>
1753
+
1754
+ <key>ProgramArguments</key>
1755
+ <array>
1756
+ <string>${happyPath}</string>
1757
+ <string>${scriptPath}</string>
1758
+ <string>happy-daemon</string>
1759
+ </array>
1760
+
1761
+ <key>EnvironmentVariables</key>
1762
+ <dict>
1763
+ <key>HAPPY_DAEMON_MODE</key>
1764
+ <string>true</string>
1765
+ </dict>
1766
+
1767
+ <key>RunAtLoad</key>
1768
+ <true/>
1769
+
1770
+ <key>KeepAlive</key>
1771
+ <true/>
1772
+
1773
+ <key>StandardErrorPath</key>
1774
+ <string>${USER_HOME}/.happy/daemon.err</string>
1775
+
1776
+ <key>StandardOutPath</key>
1777
+ <string>${USER_HOME}/.happy/daemon.log</string>
1778
+
1779
+ <key>WorkingDirectory</key>
1780
+ <string>/tmp</string>
1781
+ </dict>
1782
+ </plist>
1783
+ `);
1784
+ writeFileSync(PLIST_FILE$1, plistContent);
1785
+ chmodSync(PLIST_FILE$1, 420);
1786
+ logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
1787
+ execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
1788
+ logger.info("Daemon installed and started successfully");
1789
+ logger.info("Check logs at ~/.happy/daemon.log");
1790
+ } catch (error) {
1791
+ logger.debug("Failed to install daemon:", error);
1792
+ throw error;
1793
+ }
1794
+ }
1795
+
1796
+ async function install() {
1797
+ if (process.platform !== "darwin") {
1798
+ throw new Error("Daemon installation is currently only supported on macOS");
1799
+ }
1800
+ if (process.getuid && process.getuid() !== 0) {
1801
+ throw new Error("Daemon installation requires sudo privileges. Please run with sudo.");
1802
+ }
1803
+ logger.info("Installing Happy CLI daemon for macOS...");
1804
+ await install$1();
1805
+ }
1806
+
1807
+ const PLIST_LABEL = "com.happy-cli.daemon";
1808
+ const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
1809
+ async function uninstall$1() {
1810
+ try {
1811
+ if (!existsSync$1(PLIST_FILE)) {
1812
+ logger.info("Daemon plist not found. Nothing to uninstall.");
1813
+ return;
1814
+ }
1815
+ try {
1816
+ execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
1817
+ logger.info("Daemon stopped successfully");
1818
+ } catch (error) {
1819
+ logger.info("Failed to unload daemon (it might not be running)");
1820
+ }
1821
+ unlinkSync(PLIST_FILE);
1822
+ logger.info(`Removed daemon plist from ${PLIST_FILE}`);
1823
+ logger.info("Daemon uninstalled successfully");
1824
+ } catch (error) {
1825
+ logger.debug("Failed to uninstall daemon:", error);
1826
+ throw error;
1827
+ }
1828
+ }
1829
+
1830
+ async function uninstall() {
1831
+ if (process.platform !== "darwin") {
1832
+ throw new Error("Daemon uninstallation is currently only supported on macOS");
1833
+ }
1834
+ if (process.getuid && process.getuid() !== 0) {
1835
+ throw new Error("Daemon uninstallation requires sudo privileges. Please run with sudo.");
1836
+ }
1837
+ logger.info("Uninstalling Happy CLI daemon for macOS...");
1838
+ await uninstall$1();
1839
+ }
1840
+
1869
1841
  (async () => {
1870
1842
  const args = process.argv.slice(2);
1871
1843
  let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
@@ -1884,25 +1856,64 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
1884
1856
  process.exit(1);
1885
1857
  }
1886
1858
  return;
1887
- } else if (subcommand === "login" || subcommand === "auth") {
1888
- await doAuth();
1859
+ } else if (subcommand === "daemon") {
1860
+ const daemonSubcommand = args[1];
1861
+ if (daemonSubcommand === "start") {
1862
+ await startDaemon();
1863
+ process.exit(0);
1864
+ } else if (daemonSubcommand === "stop") {
1865
+ await stopDaemon();
1866
+ process.exit(0);
1867
+ } else if (daemonSubcommand === "install") {
1868
+ try {
1869
+ await install();
1870
+ } catch (error) {
1871
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1872
+ process.exit(1);
1873
+ }
1874
+ } else if (daemonSubcommand === "uninstall") {
1875
+ try {
1876
+ await uninstall();
1877
+ } catch (error) {
1878
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1879
+ process.exit(1);
1880
+ }
1881
+ } else {
1882
+ console.log(`
1883
+ ${chalk.bold("happy daemon")} - Daemon management
1884
+
1885
+ ${chalk.bold("Usage:")}
1886
+ happy daemon start Start the daemon
1887
+ happy daemon stop Stop the daemon
1888
+ sudo happy daemon install Install the daemon (requires sudo)
1889
+ sudo happy daemon uninstall Uninstall the daemon (requires sudo)
1890
+
1891
+ ${chalk.bold("Note:")} The daemon runs in the background and provides persistent services.
1892
+ Currently only supported on macOS.
1893
+ `);
1894
+ }
1889
1895
  return;
1890
1896
  } else {
1891
1897
  const options = {};
1892
1898
  let showHelp = false;
1893
1899
  let showVersion = false;
1900
+ let forceAuth = false;
1894
1901
  for (let i = 0; i < args.length; i++) {
1895
1902
  const arg = args[i];
1896
1903
  if (arg === "-h" || arg === "--help") {
1897
1904
  showHelp = true;
1898
1905
  } else if (arg === "-v" || arg === "--version") {
1899
1906
  showVersion = true;
1907
+ } else if (arg === "--auth" || arg === "--login") {
1908
+ forceAuth = true;
1900
1909
  } else if (arg === "-m" || arg === "--model") {
1901
1910
  options.model = args[++i];
1902
1911
  } else if (arg === "-p" || arg === "--permission-mode") {
1903
- options.permissionMode = args[++i];
1912
+ options.permissionMode = z$1.enum(["auto", "default", "plan"]).parse(args[++i]);
1904
1913
  } else if (arg === "--local") {
1905
1914
  i++;
1915
+ } else if (arg === "--happy-starting-mode") {
1916
+ options.startingMode = z$1.enum(["interactive", "remote"]).parse(args[++i]);
1906
1917
  } else {
1907
1918
  console.error(chalk.red(`Unknown argument: ${arg}`));
1908
1919
  process.exit(1);
@@ -1915,35 +1926,36 @@ ${chalk.bold("happy")} - Claude Code session sharing
1915
1926
  ${chalk.bold("Usage:")}
1916
1927
  happy [options]
1917
1928
  happy logout Logs out of your account and removes data directory
1918
- happy login Show your secret QR code
1919
- happy auth Same as login
1920
1929
 
1921
1930
  ${chalk.bold("Options:")}
1922
1931
  -h, --help Show this help message
1923
1932
  -v, --version Show version
1924
1933
  -m, --model <model> Claude model to use (default: sonnet)
1925
1934
  -p, --permission-mode Permission mode: auto, default, or plan
1935
+ --auth, --login Force re-authentication
1926
1936
 
1927
1937
  [Advanced]
1928
1938
  --local < global | local >
1929
1939
  Will use .happy folder in the current directory for storing your private key and debug logs.
1930
1940
  You will require re-login each time you run this in a new directory.
1931
- Use with login to show either global or local QR code.
1941
+ --happy-starting-mode <interactive|remote>
1942
+ Set the starting mode for new sessions (default: remote)
1932
1943
 
1933
1944
  ${chalk.bold("Examples:")}
1934
1945
  happy Start a session with default settings
1935
1946
  happy -m opus Use Claude Opus model
1936
1947
  happy -p plan Use plan permission mode
1948
+ happy --auth Force re-authentication before starting session
1937
1949
  happy logout Logs out of your account and removes data directory
1938
1950
  `);
1939
1951
  process.exit(0);
1940
1952
  }
1941
1953
  if (showVersion) {
1942
- console.log("0.1.3");
1954
+ console.log(packageJson.version);
1943
1955
  process.exit(0);
1944
1956
  }
1945
1957
  let credentials = await readCredentials();
1946
- if (!credentials) {
1958
+ if (!credentials || forceAuth) {
1947
1959
  let res = await doAuth();
1948
1960
  if (!res) {
1949
1961
  process.exit(1);