happy-coder 0.1.6 → 0.1.9

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 (57) hide show
  1. package/dist/index.cjs +354 -917
  2. package/dist/index.mjs +280 -843
  3. package/dist/install-B2r_gX72.cjs +109 -0
  4. package/dist/install-HKe7dyS4.mjs +107 -0
  5. package/dist/lib.cjs +32 -0
  6. package/dist/lib.d.cts +727 -0
  7. package/dist/lib.d.mts +727 -0
  8. package/dist/lib.mjs +14 -0
  9. package/dist/run-FBXkmmN7.mjs +32 -0
  10. package/dist/run-q2To6b-c.cjs +34 -0
  11. package/dist/types-fXgEaaqP.mjs +861 -0
  12. package/dist/types-mykDX2xe.cjs +872 -0
  13. package/dist/uninstall-C42CoSCI.cjs +53 -0
  14. package/dist/uninstall-CLkTtlMv.mjs +51 -0
  15. package/package.json +28 -13
  16. package/dist/auth/auth.d.ts +0 -38
  17. package/dist/auth/auth.js +0 -76
  18. package/dist/auth/auth.test.d.ts +0 -7
  19. package/dist/auth/auth.test.js +0 -96
  20. package/dist/auth/crypto.d.ts +0 -25
  21. package/dist/auth/crypto.js +0 -36
  22. package/dist/claude/claude.d.ts +0 -54
  23. package/dist/claude/claude.js +0 -170
  24. package/dist/claude/claude.test.d.ts +0 -7
  25. package/dist/claude/claude.test.js +0 -130
  26. package/dist/claude/types.d.ts +0 -37
  27. package/dist/claude/types.js +0 -7
  28. package/dist/commands/start.d.ts +0 -38
  29. package/dist/commands/start.js +0 -161
  30. package/dist/commands/start.test.d.ts +0 -7
  31. package/dist/commands/start.test.js +0 -307
  32. package/dist/handlers/message-handler.d.ts +0 -65
  33. package/dist/handlers/message-handler.js +0 -187
  34. package/dist/index.d.ts +0 -1
  35. package/dist/index.js +0 -1
  36. package/dist/session/service.d.ts +0 -27
  37. package/dist/session/service.js +0 -93
  38. package/dist/session/service.test.d.ts +0 -7
  39. package/dist/session/service.test.js +0 -71
  40. package/dist/session/types.d.ts +0 -44
  41. package/dist/session/types.js +0 -4
  42. package/dist/socket/client.d.ts +0 -50
  43. package/dist/socket/client.js +0 -136
  44. package/dist/socket/client.test.d.ts +0 -7
  45. package/dist/socket/client.test.js +0 -74
  46. package/dist/socket/types.d.ts +0 -80
  47. package/dist/socket/types.js +0 -12
  48. package/dist/utils/config.d.ts +0 -22
  49. package/dist/utils/config.js +0 -23
  50. package/dist/utils/logger.d.ts +0 -26
  51. package/dist/utils/logger.js +0 -60
  52. package/dist/utils/paths.d.ts +0 -18
  53. package/dist/utils/paths.js +0 -24
  54. package/dist/utils/qrcode.d.ts +0 -19
  55. package/dist/utils/qrcode.js +0 -37
  56. package/dist/utils/qrcode.test.d.ts +0 -7
  57. package/dist/utils/qrcode.test.js +0 -14
package/dist/index.mjs CHANGED
@@ -1,716 +1,28 @@
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, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-fXgEaaqP.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 } 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 tweetnacl from 'tweetnacl';
20
+ import axios from 'axios';
22
21
  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
- }
22
+ import 'fs';
23
+ import 'node:events';
24
+ import 'socket.io-client';
25
+ import 'expo-server-sdk';
714
26
 
715
27
  function formatClaudeMessage(message, onAssistantResult) {
716
28
  logger.debugLargeJson("[CLAUDE] Message from non interactive & remote mode:", message);
@@ -1199,105 +511,6 @@ class InvalidateSync {
1199
511
  };
1200
512
  }
1201
513
 
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
514
  function createSessionScanner(opts) {
1302
515
  const projectName = resolve(opts.workingDirectory).replace(/\//g, "-");
1303
516
  const projectDir = join(homedir(), ".claude", "projects", projectName);
@@ -1306,6 +519,7 @@ function createSessionScanner(opts) {
1306
519
  let currentSessionId = null;
1307
520
  let currentSessionWatcherAbortController = null;
1308
521
  let processedMessages = /* @__PURE__ */ new Set();
522
+ let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
1309
523
  const sync = new InvalidateSync(async () => {
1310
524
  let sessions = [];
1311
525
  for (let p of pendingSessions) {
@@ -1338,6 +552,13 @@ function createSessionScanner(opts) {
1338
552
  processedMessages.add(key);
1339
553
  logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
1340
554
  logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
555
+ if (parsed.data.type === "user" && typeof parsed.data.message.content === "string") {
556
+ const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
557
+ if (currentCounter && currentCounter > 0) {
558
+ seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
559
+ continue;
560
+ }
561
+ }
1341
562
  opts.onMessage(parsed.data);
1342
563
  } catch (e) {
1343
564
  continue;
@@ -1392,8 +613,12 @@ function createSessionScanner(opts) {
1392
613
  if (currentSessionId) {
1393
614
  pendingSessions.add(currentSessionId);
1394
615
  }
616
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId}`);
1395
617
  currentSessionId = sessionId;
1396
618
  sync.invalidate();
619
+ },
620
+ onRemoteUserMessageForDeduplication: (messageContent) => {
621
+ seenRemoteUserMessageCounters.set(messageContent, (seenRemoteUserMessageCounters.get(messageContent) || 0) + 1);
1397
622
  }
1398
623
  };
1399
624
  }
@@ -1427,32 +652,24 @@ function sortKeys(value) {
1427
652
  }
1428
653
 
1429
654
  async function loop(opts) {
1430
- let mode = "interactive";
655
+ let mode = opts.startingMode ?? "interactive";
1431
656
  let currentMessageQueue = new MessageQueue();
1432
657
  let sessionId = null;
1433
658
  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
659
  const sessionScanner = createSessionScanner({
1444
660
  workingDirectory: opts.path,
1445
661
  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
662
  opts.session.sendClaudeSessionMessage(message);
1454
663
  }
1455
664
  });
665
+ opts.session.onUserMessage((message) => {
666
+ sessionScanner.onRemoteUserMessageForDeduplication(message.content.text);
667
+ currentMessageQueue.push(message.content.text);
668
+ logger.debugLargeJson("User message pushed to queue:", message);
669
+ if (onMessage) {
670
+ onMessage();
671
+ }
672
+ });
1456
673
  let onSessionFound = (newSessionId) => {
1457
674
  sessionId = newSessionId;
1458
675
  sessionScanner.onNewSession(newSessionId);
@@ -1507,10 +724,14 @@ async function loop(opts) {
1507
724
  mode = "interactive";
1508
725
  remoteAbortController.abort();
1509
726
  }
1510
- process.stdin.setRawMode(false);
727
+ if (process.stdin.isTTY) {
728
+ process.stdin.setRawMode(false);
729
+ }
1511
730
  };
1512
731
  process.stdin.resume();
1513
- process.stdin.setRawMode(true);
732
+ if (process.stdin.isTTY) {
733
+ process.stdin.setRawMode(true);
734
+ }
1514
735
  process.stdin.setEncoding("utf8");
1515
736
  process.stdin.on("data", abortHandler);
1516
737
  try {
@@ -1528,7 +749,9 @@ async function loop(opts) {
1528
749
  });
1529
750
  } finally {
1530
751
  process.stdin.off("data", abortHandler);
1531
- process.stdin.setRawMode(false);
752
+ if (process.stdin.isTTY) {
753
+ process.stdin.setRawMode(false);
754
+ }
1532
755
  currentMessageQueue.close();
1533
756
  currentMessageQueue = new MessageQueue();
1534
757
  }
@@ -1639,16 +862,187 @@ class InterruptController {
1639
862
  }
1640
863
  }
1641
864
 
865
+ var version = "0.1.9";
866
+ var packageJson = {
867
+ version: version};
868
+
869
+ async function startAnthropicActivityProxy(onClaudeActivity) {
870
+ const requestTimeouts = /* @__PURE__ */ new Map();
871
+ let requestCounter = 0;
872
+ let idleTimer = null;
873
+ const maxTimeBeforeIdle = 50;
874
+ const requestTimeout = 5 * 60 * 1e3;
875
+ const cleanupRequest = (requestId, reason) => {
876
+ const timeout = requestTimeouts.get(requestId);
877
+ if (timeout) {
878
+ clearTimeout(timeout);
879
+ requestTimeouts.delete(requestId);
880
+ logger.debug(`[AnthropicProxy #${requestId}] Cleaned up (${reason}), active requests: ${requestTimeouts.size}`);
881
+ claudeDidSomeWork();
882
+ }
883
+ };
884
+ const claudeDidSomeWork = () => {
885
+ if (idleTimer) clearTimeout(idleTimer);
886
+ if (requestTimeouts.size === 0) {
887
+ idleTimer = setTimeout(() => {
888
+ logger.debug(`[AnthropicProxy] Idle for ${maxTimeBeforeIdle}ms, active requests: ${requestTimeouts.size}`);
889
+ onClaudeActivity("idle");
890
+ }, maxTimeBeforeIdle);
891
+ }
892
+ };
893
+ const server = createServer((req, res) => {
894
+ const requestId = ++requestCounter;
895
+ const isAnthropicRequest = req.headers.host === "api.anthropic.com" || req.url?.includes("anthropic.com");
896
+ if (isAnthropicRequest) {
897
+ const timeout = setTimeout(() => {
898
+ logger.debug(`[AnthropicProxy #${requestId}] Request timeout after ${requestTimeout}ms`);
899
+ cleanupRequest(requestId, "timeout");
900
+ }, requestTimeout);
901
+ requestTimeouts.set(requestId, timeout);
902
+ onClaudeActivity("working");
903
+ logger.debug(`[AnthropicProxy #${requestId}] Anthropic request: ${req.method} ${req.url}, active requests: ${requestTimeouts.size}`);
904
+ }
905
+ const chunks = [];
906
+ req.on("data", (chunk) => {
907
+ chunks.push(chunk);
908
+ if (isAnthropicRequest) {
909
+ claudeDidSomeWork();
910
+ }
911
+ });
912
+ req.on("end", () => {
913
+ const body = Buffer.concat(chunks);
914
+ let targetUrl;
915
+ if (isAnthropicRequest) {
916
+ targetUrl = new URL$1(req.url || "/", "https://api.anthropic.com");
917
+ } else {
918
+ const protocol = req.headers["x-forwarded-proto"] || "https";
919
+ const host = req.headers.host || "localhost";
920
+ targetUrl = new URL$1(req.url || "/", `${protocol}://${host}`);
921
+ }
922
+ const options = {
923
+ hostname: targetUrl.hostname,
924
+ port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
925
+ path: targetUrl.pathname + targetUrl.search,
926
+ method: req.method,
927
+ headers: {
928
+ ...req.headers,
929
+ host: targetUrl.hostname
930
+ }
931
+ };
932
+ const requestMethod = targetUrl.protocol === "https:" ? request : request$1;
933
+ const proxyReq = requestMethod(options, (proxyRes) => {
934
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
935
+ proxyRes.pipe(res);
936
+ proxyRes.on("end", () => {
937
+ if (isAnthropicRequest) {
938
+ cleanupRequest(requestId, "completed");
939
+ }
940
+ });
941
+ });
942
+ proxyReq.on("error", (error) => {
943
+ if (isAnthropicRequest) {
944
+ cleanupRequest(requestId, `error: ${error.message}`);
945
+ } else {
946
+ logger.debug(`[AnthropicProxy #${requestId}] Error:`, error.message);
947
+ }
948
+ res.writeHead(502);
949
+ res.end("Bad Gateway");
950
+ });
951
+ if (body.length > 0) {
952
+ proxyReq.write(body);
953
+ }
954
+ proxyReq.end();
955
+ });
956
+ });
957
+ server.on("connect", (req, clientSocket, head) => {
958
+ const requestId = ++requestCounter;
959
+ const [hostname, port] = req.url?.split(":") || ["", "443"];
960
+ const isAnthropicRequest = hostname === "api.anthropic.com";
961
+ if (isAnthropicRequest) {
962
+ const timeout = setTimeout(() => {
963
+ logger.debug(`[AnthropicProxy #${requestId}] CONNECT timeout after ${requestTimeout}ms`);
964
+ cleanupRequest(requestId, "timeout");
965
+ }, requestTimeout);
966
+ requestTimeouts.set(requestId, timeout);
967
+ onClaudeActivity("working");
968
+ logger.debug(`[AnthropicProxy #${requestId}] CONNECT to api.anthropic.com, active requests: ${requestTimeouts.size}`);
969
+ }
970
+ const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
971
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
972
+ serverSocket.write(head);
973
+ serverSocket.pipe(clientSocket);
974
+ clientSocket.pipe(serverSocket);
975
+ });
976
+ const cleanup = () => {
977
+ if (isAnthropicRequest) {
978
+ cleanupRequest(requestId, "CONNECT closed");
979
+ }
980
+ };
981
+ serverSocket.on("error", (err) => {
982
+ logger.debug(`[AnthropicProxy #${requestId}] CONNECT error:`, err.message);
983
+ clientSocket.end();
984
+ cleanup();
985
+ });
986
+ clientSocket.on("error", cleanup);
987
+ clientSocket.on("end", cleanup);
988
+ serverSocket.on("end", cleanup);
989
+ });
990
+ const url = await new Promise((resolve) => {
991
+ server.listen(0, "127.0.0.1", () => {
992
+ const addr = server.address();
993
+ if (addr && typeof addr === "object") {
994
+ resolve(`http://127.0.0.1:${addr.port}`);
995
+ }
996
+ });
997
+ });
998
+ logger.debug(`[AnthropicProxy] Started at ${url}`);
999
+ return {
1000
+ url,
1001
+ cleanup: () => {
1002
+ if (idleTimer) clearTimeout(idleTimer);
1003
+ for (const [requestId, timeout] of requestTimeouts) {
1004
+ clearTimeout(timeout);
1005
+ logger.debug(`[AnthropicProxy] Cleaning up timeout for request #${requestId}`);
1006
+ }
1007
+ requestTimeouts.clear();
1008
+ if (requestTimeouts.size > 0) {
1009
+ logger.debug(`[AnthropicProxy] Warning: ${requestTimeouts.size} active requests still pending at cleanup:`, Array.from(requestTimeouts.keys()));
1010
+ }
1011
+ server.close();
1012
+ }
1013
+ };
1014
+ }
1015
+
1642
1016
  async function start(credentials, options = {}) {
1643
1017
  const workingDirectory = process.cwd();
1644
1018
  const sessionTag = randomUUID();
1645
1019
  const api = new ApiClient(credentials.token, credentials.secret);
1646
1020
  let state = {};
1647
- let metadata = { path: workingDirectory, host: os.hostname() };
1021
+ let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version };
1648
1022
  const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
1649
1023
  logger.debug(`Session created: ${response.id}`);
1650
1024
  const session = api.session(response);
1651
1025
  const pushClient = api.push();
1026
+ let thinking = false;
1027
+ let pingInterval = setInterval(() => {
1028
+ session.keepAlive(thinking);
1029
+ }, 2e3);
1030
+ const antropicActivityProxy = await startAnthropicActivityProxy(
1031
+ (activity) => {
1032
+ const newThinking = activity === "working";
1033
+ if (newThinking !== thinking) {
1034
+ thinking = newThinking;
1035
+ logger.debug(`[PING] Thinking state changed: ${thinking}`);
1036
+ session.keepAlive(thinking);
1037
+ }
1038
+ }
1039
+ );
1040
+ process.env.HTTP_PROXY = antropicActivityProxy.url;
1041
+ process.env.HTTPS_PROXY = antropicActivityProxy.url;
1042
+ logger.debug(`[AnthropicProxy] Set HTTP_PROXY and HTTPS_PROXY to ${antropicActivityProxy.url}`);
1043
+ const logPath = await logger.logFilePathPromise;
1044
+ logger.info(`Session: ${response.id}`);
1045
+ logger.infoDeveloper(`Logs: ${logPath}`);
1652
1046
  const interruptController = new InterruptController();
1653
1047
  let requests = /* @__PURE__ */ new Map();
1654
1048
  const permissionServer = await startPermissionServerV2(async (request) => {
@@ -1724,10 +1118,6 @@ async function start(credentials, options = {}) {
1724
1118
  logger.info("Abort request - interrupting Claude");
1725
1119
  await interruptController.interrupt();
1726
1120
  });
1727
- let thinking = false;
1728
- const pingInterval = setInterval(() => {
1729
- session.keepAlive(thinking);
1730
- }, 15e3);
1731
1121
  const onAssistantResult = async (result) => {
1732
1122
  try {
1733
1123
  const summary = "result" in result && result.result ? result.result.substring(0, 100) + (result.result.length > 100 ? "..." : "") : "";
@@ -1751,6 +1141,7 @@ async function start(credentials, options = {}) {
1751
1141
  path: workingDirectory,
1752
1142
  model: options.model,
1753
1143
  permissionMode: options.permissionMode,
1144
+ startingMode: options.startingMode,
1754
1145
  mcpServers: {
1755
1146
  "permission": {
1756
1147
  type: "http",
@@ -1758,15 +1149,15 @@ async function start(credentials, options = {}) {
1758
1149
  }
1759
1150
  },
1760
1151
  permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
1761
- onThinking: (t) => {
1762
- thinking = t;
1763
- session.keepAlive(t);
1764
- },
1765
1152
  session,
1766
1153
  onAssistantResult,
1767
1154
  interruptController
1768
1155
  });
1769
1156
  clearInterval(pingInterval);
1157
+ if (antropicActivityProxy) {
1158
+ logger.info("[AnthropicProxy] Shutting down activity monitoring proxy");
1159
+ antropicActivityProxy.cleanup();
1160
+ }
1770
1161
  process.exit(0);
1771
1162
  }
1772
1163
 
@@ -1824,7 +1215,12 @@ async function doAuth() {
1824
1215
  return null;
1825
1216
  }
1826
1217
  console.log("Please, authenticate using mobile app");
1827
- displayQRCode("happy://terminal?" + encodeBase64Url(keypair.publicKey));
1218
+ const authUrl = "happy://terminal?" + encodeBase64Url(keypair.publicKey);
1219
+ displayQRCode(authUrl);
1220
+ if (process.env.DEBUG === "1") {
1221
+ console.log("\n\u{1F4CB} For manual entry, copy this URL:");
1222
+ console.log(authUrl);
1223
+ }
1828
1224
  let credentials = null;
1829
1225
  while (true) {
1830
1226
  try {
@@ -1884,25 +1280,63 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
1884
1280
  process.exit(1);
1885
1281
  }
1886
1282
  return;
1887
- } else if (subcommand === "login" || subcommand === "auth") {
1888
- await doAuth();
1283
+ } else if (subcommand === "daemon") {
1284
+ if (process.env.HAPPY_DAEMON_MODE) {
1285
+ const { run } = await import('./run-FBXkmmN7.mjs');
1286
+ await run();
1287
+ } else {
1288
+ const daemonSubcommand = args[1];
1289
+ if (daemonSubcommand === "install") {
1290
+ const { install } = await import('./install-HKe7dyS4.mjs');
1291
+ try {
1292
+ await install();
1293
+ } catch (error) {
1294
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1295
+ process.exit(1);
1296
+ }
1297
+ } else if (daemonSubcommand === "uninstall") {
1298
+ const { uninstall } = await import('./uninstall-CLkTtlMv.mjs');
1299
+ try {
1300
+ await uninstall();
1301
+ } catch (error) {
1302
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1303
+ process.exit(1);
1304
+ }
1305
+ } else {
1306
+ console.log(`
1307
+ ${chalk.bold("happy daemon")} - Daemon management
1308
+
1309
+ ${chalk.bold("Usage:")}
1310
+ sudo happy daemon install Install the daemon (requires sudo)
1311
+ sudo happy daemon uninstall Uninstall the daemon (requires sudo)
1312
+
1313
+ ${chalk.bold("Note:")} The daemon runs in the background and provides persistent services.
1314
+ Currently only supported on macOS.
1315
+ `);
1316
+ }
1317
+ }
1889
1318
  return;
1890
1319
  } else {
1891
1320
  const options = {};
1892
1321
  let showHelp = false;
1893
1322
  let showVersion = false;
1323
+ let forceAuth = false;
1894
1324
  for (let i = 0; i < args.length; i++) {
1895
1325
  const arg = args[i];
1896
1326
  if (arg === "-h" || arg === "--help") {
1897
1327
  showHelp = true;
1898
1328
  } else if (arg === "-v" || arg === "--version") {
1899
1329
  showVersion = true;
1330
+ } else if (arg === "--auth" || arg === "--login") {
1331
+ forceAuth = true;
1900
1332
  } else if (arg === "-m" || arg === "--model") {
1901
1333
  options.model = args[++i];
1902
1334
  } else if (arg === "-p" || arg === "--permission-mode") {
1903
- options.permissionMode = args[++i];
1335
+ options.permissionMode = z$1.enum(["auto", "default", "plan"]).parse(args[++i]);
1904
1336
  } else if (arg === "--local") {
1905
1337
  i++;
1338
+ } else if (arg === "--happy-starting-mode") {
1339
+ options.startingMode = z$1.enum(["interactive", "remote"]).parse(args[++i]);
1906
1340
  } else {
1907
1341
  console.error(chalk.red(`Unknown argument: ${arg}`));
1908
1342
  process.exit(1);
@@ -1915,35 +1349,38 @@ ${chalk.bold("happy")} - Claude Code session sharing
1915
1349
  ${chalk.bold("Usage:")}
1916
1350
  happy [options]
1917
1351
  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
1352
+ happy daemon Manage the background daemon (macOS only)
1920
1353
 
1921
1354
  ${chalk.bold("Options:")}
1922
1355
  -h, --help Show this help message
1923
1356
  -v, --version Show version
1924
1357
  -m, --model <model> Claude model to use (default: sonnet)
1925
1358
  -p, --permission-mode Permission mode: auto, default, or plan
1359
+ --auth, --login Force re-authentication
1926
1360
 
1927
1361
  [Advanced]
1928
1362
  --local < global | local >
1929
1363
  Will use .happy folder in the current directory for storing your private key and debug logs.
1930
1364
  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.
1365
+
1366
+ --happy-starting-mode <mode> Start in specified mode (interactive or remote)
1367
+ Default: interactive
1932
1368
 
1933
1369
  ${chalk.bold("Examples:")}
1934
1370
  happy Start a session with default settings
1935
1371
  happy -m opus Use Claude Opus model
1936
1372
  happy -p plan Use plan permission mode
1373
+ happy --auth Force re-authentication before starting session
1937
1374
  happy logout Logs out of your account and removes data directory
1938
1375
  `);
1939
1376
  process.exit(0);
1940
1377
  }
1941
1378
  if (showVersion) {
1942
- console.log("0.1.3");
1379
+ console.log(packageJson.version);
1943
1380
  process.exit(0);
1944
1381
  }
1945
1382
  let credentials = await readCredentials();
1946
- if (!credentials) {
1383
+ if (!credentials || forceAuth) {
1947
1384
  let res = await doAuth();
1948
1385
  if (!res) {
1949
1386
  process.exit(1);