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