happy-coder 0.2.2 → 0.2.3-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,134 +1,832 @@
1
1
  import chalk from 'chalk';
2
- import { l as logger, d as delay, e as backoff, R as RawJSONLinesSchema, c as configuration, f as encodeBase64, A as ApiClient, g as encodeBase64Url, h as decodeBase64, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-DNu8okOb.mjs';
2
+ import { l as logger, d as backoff, e as delay, R as RawJSONLinesSchema, c as configuration, f as encodeBase64, A as ApiClient, g as encodeBase64Url, h as decodeBase64, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-BX4xv8Ty.mjs';
3
3
  import { randomUUID, randomBytes } from 'node:crypto';
4
- import { spawn } from 'node:child_process';
4
+ import { spawn, execSync } from 'node:child_process';
5
+ import { resolve, join, dirname } from 'node:path';
5
6
  import { createInterface } from 'node:readline';
6
- import { existsSync, readFileSync, mkdirSync, watch, rmSync } from 'node:fs';
7
- import { join, resolve, dirname } from 'node:path';
8
7
  import { fileURLToPath } from 'node:url';
8
+ import { existsSync, readFileSync, mkdirSync, watch, rmSync } from 'node:fs';
9
9
  import os, { homedir } from 'node:os';
10
- import { access, watch as watch$1, readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
11
10
  import { readFile, mkdir, writeFile as writeFile$1 } from 'node:fs/promises';
11
+ import { watch as watch$1, access, readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
12
+ import { useStdout, useInput, Box, Text, render } from 'ink';
13
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
14
+ import axios from 'axios';
15
+ import { EventEmitter } from 'node:events';
16
+ import { io } from 'socket.io-client';
17
+ import tweetnacl from 'tweetnacl';
18
+ import 'expo-server-sdk';
12
19
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
20
  import { createServer } from 'node:http';
14
21
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
15
22
  import * as z from 'zod';
16
23
  import { z as z$1 } from 'zod';
17
- import { spawn as spawn$1, exec, execSync } from 'child_process';
24
+ import { spawn as spawn$1, exec, execSync as execSync$1 } from 'child_process';
18
25
  import { promisify } from 'util';
19
26
  import crypto, { createHash } from 'crypto';
20
27
  import { dirname as dirname$1, join as join$1 } from 'path';
21
28
  import { fileURLToPath as fileURLToPath$1 } from 'url';
22
- import tweetnacl from 'tweetnacl';
23
- import axios from 'axios';
24
29
  import qrcode from 'qrcode-terminal';
25
- import { EventEmitter } from 'node:events';
26
- import { io } from 'socket.io-client';
30
+ import { existsSync as existsSync$1, readFileSync as readFileSync$1, writeFileSync, unlinkSync, mkdirSync as mkdirSync$1, chmodSync } from 'fs';
27
31
  import { hostname, homedir as homedir$1 } from 'os';
28
- import { closeSync, existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync, mkdirSync as mkdirSync$1, openSync, writeSync, writeFileSync, chmodSync } from 'fs';
29
- import 'expo-server-sdk';
30
32
 
31
- class Stream {
32
- constructor(returned) {
33
- this.returned = returned;
34
- }
35
- queue = [];
36
- readResolve;
37
- readReject;
38
- isDone = false;
39
- hasError;
40
- started = false;
41
- /**
42
- * Implements async iterable protocol
43
- */
44
- [Symbol.asyncIterator]() {
45
- if (this.started) {
46
- throw new Error("Stream can only be iterated once");
47
- }
48
- this.started = true;
49
- return this;
33
+ class Session {
34
+ path;
35
+ logPath;
36
+ api;
37
+ client;
38
+ queue;
39
+ claudeEnvVars;
40
+ claudeArgs;
41
+ mcpServers;
42
+ _onModeChange;
43
+ sessionId;
44
+ mode = "local";
45
+ thinking = false;
46
+ constructor(opts) {
47
+ this.path = opts.path;
48
+ this.api = opts.api;
49
+ this.client = opts.client;
50
+ this.logPath = opts.logPath;
51
+ this.sessionId = opts.sessionId;
52
+ this.queue = opts.messageQueue;
53
+ this.claudeEnvVars = opts.claudeEnvVars;
54
+ this.claudeArgs = opts.claudeArgs;
55
+ this.mcpServers = opts.mcpServers;
56
+ this._onModeChange = opts.onModeChange;
57
+ this.client.keepAlive(this.thinking, this.mode);
58
+ setInterval(() => {
59
+ this.client.keepAlive(this.thinking, this.mode);
60
+ }, 2e3);
61
+ }
62
+ onThinkingChange = (thinking) => {
63
+ this.thinking = thinking;
64
+ this.client.keepAlive(thinking, this.mode);
65
+ };
66
+ onModeChange = (mode) => {
67
+ this.mode = mode;
68
+ this.client.keepAlive(this.thinking, mode);
69
+ this._onModeChange(mode);
70
+ };
71
+ onSessionFound = (sessionId) => {
72
+ this.sessionId = sessionId;
73
+ };
74
+ }
75
+
76
+ function getProjectPath(workingDirectory) {
77
+ const projectId = resolve(workingDirectory).replace(/[\\\/\.:]/g, "-");
78
+ return join(homedir(), ".claude", "projects", projectId);
79
+ }
80
+
81
+ function claudeCheckSession(sessionId, path) {
82
+ const projectDir = getProjectPath(path);
83
+ const sessionFile = join(projectDir, `${sessionId}.jsonl`);
84
+ const sessionExists = existsSync(sessionFile);
85
+ if (!sessionExists) {
86
+ logger.debug(`[claudeCheckSession] Path ${sessionFile} does not exist`);
87
+ return false;
50
88
  }
51
- /**
52
- * Gets the next value from the stream
53
- */
54
- async next() {
55
- if (this.queue.length > 0) {
56
- return Promise.resolve({
57
- done: false,
58
- value: this.queue.shift()
59
- });
89
+ const sessionData = readFileSync(sessionFile, "utf-8").split("\n");
90
+ const hasGoodMessage = !!sessionData.find((v) => {
91
+ try {
92
+ return typeof JSON.parse(v).uuid === "string";
93
+ } catch (e) {
94
+ return false;
60
95
  }
61
- if (this.isDone) {
62
- return Promise.resolve({ done: true, value: void 0 });
96
+ });
97
+ return hasGoodMessage;
98
+ }
99
+
100
+ const __dirname$2 = dirname(fileURLToPath(import.meta.url));
101
+ async function claudeLocal(opts) {
102
+ const projectDir = getProjectPath(opts.path);
103
+ mkdirSync(projectDir, { recursive: true });
104
+ const watcher = watch(projectDir);
105
+ let resolvedSessionId = null;
106
+ const detectedIdsRandomUUID = /* @__PURE__ */ new Set();
107
+ const detectedIdsFileSystem = /* @__PURE__ */ new Set();
108
+ watcher.on("change", (event, filename) => {
109
+ if (typeof filename === "string" && filename.toLowerCase().endsWith(".jsonl")) {
110
+ logger.debug("change", event, filename);
111
+ const sessionId = filename.replace(".jsonl", "");
112
+ if (detectedIdsFileSystem.has(sessionId)) {
113
+ return;
114
+ }
115
+ detectedIdsFileSystem.add(sessionId);
116
+ if (resolvedSessionId) {
117
+ return;
118
+ }
119
+ if (detectedIdsRandomUUID.has(sessionId)) {
120
+ resolvedSessionId = sessionId;
121
+ opts.onSessionFound(sessionId);
122
+ }
63
123
  }
64
- if (this.hasError) {
65
- return Promise.reject(this.hasError);
124
+ });
125
+ let startFrom = opts.sessionId;
126
+ if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
127
+ startFrom = null;
128
+ }
129
+ let thinking = false;
130
+ let stopThinkingTimeout = null;
131
+ const updateThinking = (newThinking) => {
132
+ if (thinking !== newThinking) {
133
+ thinking = newThinking;
134
+ logger.debug(`[ClaudeLocal] Thinking state changed to: ${thinking}`);
135
+ if (opts.onThinkingChange) {
136
+ opts.onThinkingChange(thinking);
137
+ }
66
138
  }
67
- return new Promise((resolve, reject) => {
68
- this.readResolve = resolve;
69
- this.readReject = reject;
139
+ };
140
+ try {
141
+ process.stdin.pause();
142
+ await new Promise((r, reject) => {
143
+ const args = [];
144
+ if (startFrom) {
145
+ args.push("--resume", startFrom);
146
+ }
147
+ if (opts.claudeArgs) {
148
+ args.push(...opts.claudeArgs);
149
+ }
150
+ const claudeCliPath = process.env.HAPPY_CLAUDE_CLI_PATH || resolve(join(__dirname$2, "..", "scripts", "claudeInteractiveLaunch.cjs"));
151
+ const env = {
152
+ ...process.env,
153
+ ...opts.claudeEnvVars
154
+ };
155
+ const child = spawn("node", [claudeCliPath, ...args], {
156
+ stdio: ["inherit", "inherit", "inherit", "pipe"],
157
+ signal: opts.abort,
158
+ cwd: opts.path,
159
+ env
160
+ });
161
+ if (child.stdio[3]) {
162
+ const rl = createInterface({
163
+ input: child.stdio[3],
164
+ crlfDelay: Infinity
165
+ });
166
+ const activeFetches = /* @__PURE__ */ new Map();
167
+ rl.on("line", (line) => {
168
+ try {
169
+ const message = JSON.parse(line);
170
+ switch (message.type) {
171
+ case "uuid":
172
+ detectedIdsRandomUUID.add(message.value);
173
+ if (!resolvedSessionId && detectedIdsFileSystem.has(message.value)) {
174
+ resolvedSessionId = message.value;
175
+ opts.onSessionFound(message.value);
176
+ }
177
+ break;
178
+ case "fetch-start":
179
+ activeFetches.set(message.id, {
180
+ hostname: message.hostname,
181
+ path: message.path,
182
+ startTime: message.timestamp
183
+ });
184
+ if (stopThinkingTimeout) {
185
+ clearTimeout(stopThinkingTimeout);
186
+ stopThinkingTimeout = null;
187
+ }
188
+ updateThinking(true);
189
+ break;
190
+ case "fetch-end":
191
+ activeFetches.delete(message.id);
192
+ if (activeFetches.size === 0 && thinking && !stopThinkingTimeout) {
193
+ stopThinkingTimeout = setTimeout(() => {
194
+ if (activeFetches.size === 0) {
195
+ updateThinking(false);
196
+ }
197
+ stopThinkingTimeout = null;
198
+ }, 500);
199
+ }
200
+ break;
201
+ default:
202
+ logger.debug(`[ClaudeLocal] Unknown message type: ${message.type}`);
203
+ }
204
+ } catch (e) {
205
+ logger.debug(`[ClaudeLocal] Non-JSON line from fd3: ${line}`);
206
+ }
207
+ });
208
+ rl.on("error", (err) => {
209
+ console.error("Error reading from fd 3:", err);
210
+ });
211
+ child.on("exit", () => {
212
+ if (stopThinkingTimeout) {
213
+ clearTimeout(stopThinkingTimeout);
214
+ }
215
+ updateThinking(false);
216
+ });
217
+ }
218
+ child.on("error", (error) => {
219
+ });
220
+ child.on("exit", (code, signal) => {
221
+ if (signal === "SIGTERM" && opts.abort.aborted) {
222
+ r();
223
+ } else if (signal) {
224
+ reject(new Error(`Process terminated with signal: ${signal}`));
225
+ } else {
226
+ r();
227
+ }
228
+ });
70
229
  });
71
- }
72
- /**
73
- * Adds a value to the stream
74
- */
75
- enqueue(value) {
76
- if (this.readResolve) {
77
- const resolve = this.readResolve;
78
- this.readResolve = void 0;
79
- this.readReject = void 0;
80
- resolve({ done: false, value });
81
- } else {
82
- this.queue.push(value);
230
+ } finally {
231
+ watcher.close();
232
+ process.stdin.resume();
233
+ if (stopThinkingTimeout) {
234
+ clearTimeout(stopThinkingTimeout);
235
+ stopThinkingTimeout = null;
83
236
  }
237
+ updateThinking(false);
84
238
  }
85
- /**
86
- * Marks the stream as complete
87
- */
88
- done() {
89
- this.isDone = true;
90
- if (this.readResolve) {
91
- const resolve = this.readResolve;
92
- this.readResolve = void 0;
93
- this.readReject = void 0;
94
- resolve({ done: true, value: void 0 });
95
- }
239
+ return resolvedSessionId;
240
+ }
241
+
242
+ class Future {
243
+ _resolve;
244
+ _reject;
245
+ _promise;
246
+ constructor() {
247
+ this._promise = new Promise((resolve, reject) => {
248
+ this._resolve = resolve;
249
+ this._reject = reject;
250
+ });
96
251
  }
97
- /**
98
- * Propagates an error through the stream
99
- */
100
- error(error) {
101
- this.hasError = error;
102
- if (this.readReject) {
103
- const reject = this.readReject;
104
- this.readResolve = void 0;
105
- this.readReject = void 0;
106
- reject(error);
107
- }
252
+ resolve(value) {
253
+ this._resolve(value);
108
254
  }
109
- /**
110
- * Implements async iterator cleanup
111
- */
112
- async return() {
113
- this.isDone = true;
114
- if (this.returned) {
115
- this.returned();
116
- }
117
- return Promise.resolve({ done: true, value: void 0 });
255
+ reject(reason) {
256
+ this._reject(reason);
118
257
  }
119
- }
120
-
121
- class AbortError extends Error {
122
- constructor(message) {
123
- super(message);
124
- this.name = "AbortError";
258
+ get promise() {
259
+ return this._promise;
125
260
  }
126
261
  }
127
262
 
128
- const __filename = fileURLToPath(import.meta.url);
129
- const __dirname$2 = join(__filename, "..");
130
- function getDefaultClaudeCodePath() {
131
- return join(__dirname$2, "..", "..", "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
263
+ class InvalidateSync {
264
+ _invalidated = false;
265
+ _invalidatedDouble = false;
266
+ _stopped = false;
267
+ _command;
268
+ _pendings = [];
269
+ constructor(command) {
270
+ this._command = command;
271
+ }
272
+ invalidate() {
273
+ if (this._stopped) {
274
+ return;
275
+ }
276
+ if (!this._invalidated) {
277
+ this._invalidated = true;
278
+ this._invalidatedDouble = false;
279
+ this._doSync();
280
+ } else {
281
+ if (!this._invalidatedDouble) {
282
+ this._invalidatedDouble = true;
283
+ }
284
+ }
285
+ }
286
+ async invalidateAndAwait() {
287
+ if (this._stopped) {
288
+ return;
289
+ }
290
+ await new Promise((resolve) => {
291
+ this._pendings.push(resolve);
292
+ this.invalidate();
293
+ });
294
+ }
295
+ stop() {
296
+ if (this._stopped) {
297
+ return;
298
+ }
299
+ this._notifyPendings();
300
+ this._stopped = true;
301
+ }
302
+ _notifyPendings = () => {
303
+ for (let pending of this._pendings) {
304
+ pending();
305
+ }
306
+ this._pendings = [];
307
+ };
308
+ _doSync = async () => {
309
+ await backoff(async () => {
310
+ if (this._stopped) {
311
+ return;
312
+ }
313
+ await this._command();
314
+ });
315
+ if (this._stopped) {
316
+ this._notifyPendings();
317
+ return;
318
+ }
319
+ if (this._invalidatedDouble) {
320
+ this._invalidatedDouble = false;
321
+ this._doSync();
322
+ } else {
323
+ this._invalidated = false;
324
+ this._notifyPendings();
325
+ }
326
+ };
327
+ }
328
+
329
+ function startFileWatcher(file, onFileChange) {
330
+ const abortController = new AbortController();
331
+ void (async () => {
332
+ while (true) {
333
+ try {
334
+ logger.debug(`[FILE_WATCHER] Starting watcher for ${file}`);
335
+ const watcher = watch$1(file, { persistent: true, signal: abortController.signal });
336
+ for await (const event of watcher) {
337
+ if (abortController.signal.aborted) {
338
+ return;
339
+ }
340
+ logger.debug(`[FILE_WATCHER] File changed: ${file}`);
341
+ onFileChange(file);
342
+ }
343
+ } catch (e) {
344
+ if (abortController.signal.aborted) {
345
+ return;
346
+ }
347
+ logger.debug(`[FILE_WATCHER] Watch error: ${e.message}, restarting watcher in a second`);
348
+ await delay(1e3);
349
+ }
350
+ }
351
+ })();
352
+ return () => {
353
+ abortController.abort();
354
+ };
355
+ }
356
+
357
+ async function createSessionScanner(opts) {
358
+ const projectDir = getProjectPath(opts.workingDirectory);
359
+ let finishedSessions = /* @__PURE__ */ new Set();
360
+ let pendingSessions = /* @__PURE__ */ new Set();
361
+ let currentSessionId = null;
362
+ let watchers = /* @__PURE__ */ new Map();
363
+ let processedMessageKeys = /* @__PURE__ */ new Set();
364
+ if (opts.sessionId) {
365
+ let messages = await readSessionLog(projectDir, opts.sessionId);
366
+ for (let m of messages) {
367
+ processedMessageKeys.add(messageKey(m));
368
+ }
369
+ }
370
+ const sync = new InvalidateSync(async () => {
371
+ let sessions = [];
372
+ for (let p of pendingSessions) {
373
+ sessions.push(p);
374
+ }
375
+ if (currentSessionId) {
376
+ sessions.push(currentSessionId);
377
+ }
378
+ for (let session of sessions) {
379
+ for (let file of await readSessionLog(projectDir, session)) {
380
+ let key = messageKey(file);
381
+ if (processedMessageKeys.has(key)) {
382
+ continue;
383
+ }
384
+ processedMessageKeys.add(key);
385
+ opts.onMessage(file);
386
+ }
387
+ }
388
+ for (let p of sessions) {
389
+ if (pendingSessions.has(p)) {
390
+ pendingSessions.delete(p);
391
+ finishedSessions.add(p);
392
+ }
393
+ }
394
+ for (let p of sessions) {
395
+ if (!watchers.has(p)) {
396
+ watchers.set(p, startFileWatcher(join(projectDir, `${p}.jsonl`), () => {
397
+ sync.invalidate();
398
+ }));
399
+ }
400
+ }
401
+ });
402
+ await sync.invalidateAndAwait();
403
+ const intervalId = setInterval(() => {
404
+ sync.invalidate();
405
+ }, 3e3);
406
+ return {
407
+ cleanup: async () => {
408
+ clearInterval(intervalId);
409
+ for (let w of watchers.values()) {
410
+ w();
411
+ }
412
+ watchers.clear();
413
+ await sync.invalidateAndAwait();
414
+ sync.stop();
415
+ },
416
+ onNewSession: (sessionId) => {
417
+ if (currentSessionId === sessionId) {
418
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
419
+ return;
420
+ }
421
+ if (finishedSessions.has(sessionId)) {
422
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
423
+ return;
424
+ }
425
+ if (pendingSessions.has(sessionId)) {
426
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
427
+ return;
428
+ }
429
+ if (currentSessionId) {
430
+ pendingSessions.add(currentSessionId);
431
+ }
432
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId}`);
433
+ currentSessionId = sessionId;
434
+ sync.invalidate();
435
+ }
436
+ };
437
+ }
438
+ function messageKey(message) {
439
+ if (message.type === "user") {
440
+ return message.uuid;
441
+ } else if (message.type === "assistant") {
442
+ return message.uuid;
443
+ } else if (message.type === "summary") {
444
+ return "summary: " + message.leafUuid + ": " + message.summary;
445
+ } else if (message.type === "system") {
446
+ return message.uuid;
447
+ } else {
448
+ throw Error();
449
+ }
450
+ }
451
+ async function readSessionLog(projectDir, sessionId) {
452
+ const expectedSessionFile = join(projectDir, `${sessionId}.jsonl`);
453
+ let file;
454
+ try {
455
+ file = await readFile(expectedSessionFile, "utf-8");
456
+ } catch (error) {
457
+ logger.debug(`[SESSION_SCANNER] Session file not found: ${expectedSessionFile}`);
458
+ return [];
459
+ }
460
+ let lines = file.split("\n");
461
+ let messages = [];
462
+ for (let l of lines) {
463
+ try {
464
+ if (l.trim() === "") {
465
+ continue;
466
+ }
467
+ let message = JSON.parse(l);
468
+ let parsed = RawJSONLinesSchema.safeParse(message);
469
+ if (!parsed.success) {
470
+ logger.debugLargeJson(`[SESSION_SCANNER] Failed to parse message`, message);
471
+ continue;
472
+ }
473
+ messages.push(parsed.data);
474
+ } catch (e) {
475
+ logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
476
+ continue;
477
+ }
478
+ }
479
+ return messages;
480
+ }
481
+
482
+ async function claudeLocalLauncher(session) {
483
+ const scanner = await createSessionScanner({
484
+ sessionId: session.sessionId,
485
+ workingDirectory: session.path,
486
+ onMessage: (message) => {
487
+ session.client.sendClaudeSessionMessage(message);
488
+ }
489
+ });
490
+ let exitReason = null;
491
+ const processAbortController = new AbortController();
492
+ let exutFuture = new Future();
493
+ try {
494
+ async function abort() {
495
+ if (!processAbortController.signal.aborted) {
496
+ processAbortController.abort();
497
+ }
498
+ await exutFuture.promise;
499
+ }
500
+ async function doAbort() {
501
+ logger.debug("[local]: doAbort");
502
+ if (!exitReason) {
503
+ exitReason = "switch";
504
+ }
505
+ session.queue.reset();
506
+ await abort();
507
+ }
508
+ async function doSwitch() {
509
+ logger.debug("[local]: doSwitch");
510
+ if (!exitReason) {
511
+ exitReason = "switch";
512
+ }
513
+ await abort();
514
+ }
515
+ session.client.setHandler("abort", doAbort);
516
+ session.client.setHandler("switch", doSwitch);
517
+ session.queue.setOnMessage(doSwitch);
518
+ if (session.queue.size() > 0) {
519
+ return "switch";
520
+ }
521
+ const handleSessionStart = (sessionId) => {
522
+ session.onSessionFound(sessionId);
523
+ scanner.onNewSession(sessionId);
524
+ };
525
+ while (true) {
526
+ if (exitReason) {
527
+ return exitReason;
528
+ }
529
+ logger.debug("[local]: launch");
530
+ try {
531
+ await claudeLocal({
532
+ path: session.path,
533
+ sessionId: session.sessionId,
534
+ onSessionFound: handleSessionStart,
535
+ onThinkingChange: session.onThinkingChange,
536
+ abort: processAbortController.signal,
537
+ claudeEnvVars: session.claudeEnvVars,
538
+ claudeArgs: session.claudeArgs
539
+ });
540
+ if (!exitReason) {
541
+ exitReason = "exit";
542
+ break;
543
+ }
544
+ } catch (e) {
545
+ logger.debug("[local]: launch error", e);
546
+ if (!exitReason) {
547
+ session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
548
+ continue;
549
+ } else {
550
+ break;
551
+ }
552
+ }
553
+ logger.debug("[local]: launch done");
554
+ }
555
+ } finally {
556
+ exutFuture.resolve(void 0);
557
+ session.client.setHandler("abort", async () => {
558
+ });
559
+ session.client.setHandler("switch", async () => {
560
+ });
561
+ session.queue.setOnMessage(null);
562
+ await scanner.cleanup();
563
+ }
564
+ return exitReason || "exit";
565
+ }
566
+
567
+ class MessageBuffer {
568
+ messages = [];
569
+ listeners = [];
570
+ nextId = 1;
571
+ addMessage(content, type = "assistant") {
572
+ const message = {
573
+ id: `msg-${this.nextId++}`,
574
+ timestamp: /* @__PURE__ */ new Date(),
575
+ content,
576
+ type
577
+ };
578
+ this.messages.push(message);
579
+ this.notifyListeners();
580
+ }
581
+ getMessages() {
582
+ return [...this.messages];
583
+ }
584
+ clear() {
585
+ this.messages = [];
586
+ this.nextId = 1;
587
+ this.notifyListeners();
588
+ }
589
+ onUpdate(listener) {
590
+ this.listeners.push(listener);
591
+ return () => {
592
+ const index = this.listeners.indexOf(listener);
593
+ if (index > -1) {
594
+ this.listeners.splice(index, 1);
595
+ }
596
+ };
597
+ }
598
+ notifyListeners() {
599
+ const messages = this.getMessages();
600
+ this.listeners.forEach((listener) => listener(messages));
601
+ }
602
+ }
603
+
604
+ const RemoteModeDisplay = ({ messageBuffer, logPath, onExit, onSwitchToLocal }) => {
605
+ const [messages, setMessages] = useState([]);
606
+ const [confirmationMode, setConfirmationMode] = useState(null);
607
+ const [actionInProgress, setActionInProgress] = useState(null);
608
+ const confirmationTimeoutRef = useRef(null);
609
+ const { stdout } = useStdout();
610
+ const terminalWidth = stdout.columns || 80;
611
+ const terminalHeight = stdout.rows || 24;
612
+ useEffect(() => {
613
+ setMessages(messageBuffer.getMessages());
614
+ const unsubscribe = messageBuffer.onUpdate((newMessages) => {
615
+ setMessages(newMessages);
616
+ });
617
+ return () => {
618
+ unsubscribe();
619
+ if (confirmationTimeoutRef.current) {
620
+ clearTimeout(confirmationTimeoutRef.current);
621
+ }
622
+ };
623
+ }, [messageBuffer]);
624
+ const resetConfirmation = useCallback(() => {
625
+ setConfirmationMode(null);
626
+ if (confirmationTimeoutRef.current) {
627
+ clearTimeout(confirmationTimeoutRef.current);
628
+ confirmationTimeoutRef.current = null;
629
+ }
630
+ }, []);
631
+ const setConfirmationWithTimeout = useCallback((mode) => {
632
+ setConfirmationMode(mode);
633
+ if (confirmationTimeoutRef.current) {
634
+ clearTimeout(confirmationTimeoutRef.current);
635
+ }
636
+ confirmationTimeoutRef.current = setTimeout(() => {
637
+ resetConfirmation();
638
+ }, 15e3);
639
+ }, [resetConfirmation]);
640
+ useInput(useCallback(async (input, key) => {
641
+ if (actionInProgress) return;
642
+ if (key.ctrl && input === "c") {
643
+ if (confirmationMode === "exit") {
644
+ resetConfirmation();
645
+ setActionInProgress("exiting");
646
+ await new Promise((resolve) => setTimeout(resolve, 100));
647
+ onExit?.();
648
+ } else {
649
+ setConfirmationWithTimeout("exit");
650
+ }
651
+ return;
652
+ }
653
+ if (input === " ") {
654
+ if (confirmationMode === "switch") {
655
+ resetConfirmation();
656
+ setActionInProgress("switching");
657
+ await new Promise((resolve) => setTimeout(resolve, 100));
658
+ onSwitchToLocal?.();
659
+ } else {
660
+ setConfirmationWithTimeout("switch");
661
+ }
662
+ return;
663
+ }
664
+ if (confirmationMode) {
665
+ resetConfirmation();
666
+ }
667
+ }, [confirmationMode, actionInProgress, onExit, onSwitchToLocal, setConfirmationWithTimeout, resetConfirmation]));
668
+ const getMessageColor = (type) => {
669
+ switch (type) {
670
+ case "user":
671
+ return "magenta";
672
+ case "assistant":
673
+ return "cyan";
674
+ case "system":
675
+ return "blue";
676
+ case "tool":
677
+ return "yellow";
678
+ case "result":
679
+ return "green";
680
+ case "status":
681
+ return "gray";
682
+ default:
683
+ return "white";
684
+ }
685
+ };
686
+ const formatMessage = (msg) => {
687
+ const lines = msg.content.split("\n");
688
+ const maxLineLength = terminalWidth - 10;
689
+ return lines.map((line) => {
690
+ if (line.length <= maxLineLength) return line;
691
+ const chunks = [];
692
+ for (let i = 0; i < line.length; i += maxLineLength) {
693
+ chunks.push(line.slice(i, i + maxLineLength));
694
+ }
695
+ return chunks.join("\n");
696
+ }).join("\n");
697
+ };
698
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
699
+ Box,
700
+ {
701
+ flexDirection: "column",
702
+ width: terminalWidth,
703
+ height: terminalHeight - 4,
704
+ borderStyle: "round",
705
+ borderColor: "gray",
706
+ paddingX: 1,
707
+ overflow: "hidden"
708
+ },
709
+ /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "gray", bold: true }, "\u{1F4E1} Remote Mode - Claude Messages"), /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "\u2500".repeat(Math.min(terminalWidth - 4, 60)))),
710
+ /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", height: terminalHeight - 10, overflow: "hidden" }, messages.length === 0 ? /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "Waiting for messages...") : (
711
+ // Show only the last messages that fit in the available space
712
+ messages.slice(-Math.max(1, terminalHeight - 10)).map((msg) => /* @__PURE__ */ React.createElement(Box, { key: msg.id, flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: getMessageColor(msg.type), dimColor: true }, formatMessage(msg))))
713
+ ))
714
+ ), /* @__PURE__ */ React.createElement(
715
+ Box,
716
+ {
717
+ width: terminalWidth,
718
+ borderStyle: "round",
719
+ borderColor: actionInProgress ? "gray" : confirmationMode === "exit" ? "red" : confirmationMode === "switch" ? "yellow" : "green",
720
+ paddingX: 2,
721
+ justifyContent: "center",
722
+ alignItems: "center",
723
+ flexDirection: "column"
724
+ },
725
+ /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", alignItems: "center" }, actionInProgress === "exiting" ? /* @__PURE__ */ React.createElement(Text, { color: "gray", bold: true }, "Exiting...") : actionInProgress === "switching" ? /* @__PURE__ */ React.createElement(Text, { color: "gray", bold: true }, "Switching to local mode...") : confirmationMode === "exit" ? /* @__PURE__ */ React.createElement(Text, { color: "red", bold: true }, "\u26A0\uFE0F Press Ctrl-C again to exit completely") : confirmationMode === "switch" ? /* @__PURE__ */ React.createElement(Text, { color: "yellow", bold: true }, "\u23F8\uFE0F Press space again to switch to local mode") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { color: "green", bold: true }, "\u{1F4F1} Press space to switch to local mode \u2022 Ctrl-C to exit")), process.env.DEBUG && logPath && /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
726
+ ));
727
+ };
728
+
729
+ class Stream {
730
+ constructor(returned) {
731
+ this.returned = returned;
732
+ }
733
+ queue = [];
734
+ readResolve;
735
+ readReject;
736
+ isDone = false;
737
+ hasError;
738
+ started = false;
739
+ /**
740
+ * Implements async iterable protocol
741
+ */
742
+ [Symbol.asyncIterator]() {
743
+ if (this.started) {
744
+ throw new Error("Stream can only be iterated once");
745
+ }
746
+ this.started = true;
747
+ return this;
748
+ }
749
+ /**
750
+ * Gets the next value from the stream
751
+ */
752
+ async next() {
753
+ if (this.queue.length > 0) {
754
+ return Promise.resolve({
755
+ done: false,
756
+ value: this.queue.shift()
757
+ });
758
+ }
759
+ if (this.isDone) {
760
+ return Promise.resolve({ done: true, value: void 0 });
761
+ }
762
+ if (this.hasError) {
763
+ return Promise.reject(this.hasError);
764
+ }
765
+ return new Promise((resolve, reject) => {
766
+ this.readResolve = resolve;
767
+ this.readReject = reject;
768
+ });
769
+ }
770
+ /**
771
+ * Adds a value to the stream
772
+ */
773
+ enqueue(value) {
774
+ if (this.readResolve) {
775
+ const resolve = this.readResolve;
776
+ this.readResolve = void 0;
777
+ this.readReject = void 0;
778
+ resolve({ done: false, value });
779
+ } else {
780
+ this.queue.push(value);
781
+ }
782
+ }
783
+ /**
784
+ * Marks the stream as complete
785
+ */
786
+ done() {
787
+ this.isDone = true;
788
+ if (this.readResolve) {
789
+ const resolve = this.readResolve;
790
+ this.readResolve = void 0;
791
+ this.readReject = void 0;
792
+ resolve({ done: true, value: void 0 });
793
+ }
794
+ }
795
+ /**
796
+ * Propagates an error through the stream
797
+ */
798
+ error(error) {
799
+ this.hasError = error;
800
+ if (this.readReject) {
801
+ const reject = this.readReject;
802
+ this.readResolve = void 0;
803
+ this.readReject = void 0;
804
+ reject(error);
805
+ }
806
+ }
807
+ /**
808
+ * Implements async iterator cleanup
809
+ */
810
+ async return() {
811
+ this.isDone = true;
812
+ if (this.returned) {
813
+ this.returned();
814
+ }
815
+ return Promise.resolve({ done: true, value: void 0 });
816
+ }
817
+ }
818
+
819
+ class AbortError extends Error {
820
+ constructor(message) {
821
+ super(message);
822
+ this.name = "AbortError";
823
+ }
824
+ }
825
+
826
+ const __filename = fileURLToPath(import.meta.url);
827
+ const __dirname$1 = join(__filename, "..");
828
+ function getDefaultClaudeCodePath() {
829
+ return join(__dirname$1, "..", "..", "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
132
830
  }
133
831
  function logDebug(message) {
134
832
  if (process.env.DEBUG) {
@@ -136,9 +834,9 @@ function logDebug(message) {
136
834
  console.log(message);
137
835
  }
138
836
  }
139
- async function streamToStdin(stream, stdin, abortController) {
837
+ async function streamToStdin(stream, stdin, abort) {
140
838
  for await (const message of stream) {
141
- if (abortController.signal.aborted) break;
839
+ if (abort?.aborted) break;
142
840
  stdin.write(JSON.stringify(message) + "\n");
143
841
  }
144
842
  stdin.end();
@@ -254,7 +952,6 @@ class Query {
254
952
  function query(config) {
255
953
  const {
256
954
  prompt,
257
- abortController = config.abortController || new AbortController(),
258
955
  options: {
259
956
  allowedTools = [],
260
957
  appendSystemPrompt,
@@ -311,7 +1008,7 @@ function query(config) {
311
1008
  const child = spawn(executable, [...executableArgs, pathToClaudeCodeExecutable, ...args], {
312
1009
  cwd,
313
1010
  stdio: ["pipe", "pipe", "pipe"],
314
- signal: abortController.signal,
1011
+ signal: config.options?.abort,
315
1012
  env: {
316
1013
  ...process.env
317
1014
  }
@@ -320,7 +1017,7 @@ function query(config) {
320
1017
  if (typeof prompt === "string") {
321
1018
  child.stdin.end();
322
1019
  } else {
323
- streamToStdin(prompt, child.stdin, abortController);
1020
+ streamToStdin(prompt, child.stdin, config.options?.abort);
324
1021
  childStdin = child.stdin;
325
1022
  }
326
1023
  if (process.env.DEBUG) {
@@ -333,11 +1030,11 @@ function query(config) {
333
1030
  child.kill("SIGTERM");
334
1031
  }
335
1032
  };
336
- abortController.signal.addEventListener("abort", cleanup);
1033
+ config.options?.abort?.addEventListener("abort", cleanup);
337
1034
  process.on("exit", cleanup);
338
1035
  const processExitPromise = new Promise((resolve) => {
339
1036
  child.on("close", (code) => {
340
- if (abortController.signal.aborted) {
1037
+ if (config.options?.abort?.aborted) {
341
1038
  query2.setError(new AbortError("Claude Code process aborted by user"));
342
1039
  }
343
1040
  if (code !== 0) {
@@ -349,163 +1046,20 @@ function query(config) {
349
1046
  });
350
1047
  const query2 = new Query(childStdin, child.stdout, processExitPromise);
351
1048
  child.on("error", (error) => {
352
- if (abortController.signal.aborted) {
1049
+ if (config.options?.abort?.aborted) {
353
1050
  query2.setError(new AbortError("Claude Code process aborted by user"));
354
1051
  } else {
355
1052
  query2.setError(new Error(`Failed to spawn Claude Code process: ${error.message}`));
356
1053
  }
357
- });
358
- processExitPromise.finally(() => {
359
- cleanup();
360
- abortController.signal.removeEventListener("abort", cleanup);
361
- if (process.env.CLAUDE_SDK_MCP_SERVERS) {
362
- delete process.env.CLAUDE_SDK_MCP_SERVERS;
363
- }
364
- });
365
- return query2;
366
- }
367
-
368
- function formatClaudeMessage(message, onAssistantResult) {
369
- logger.debugLargeJson("[CLAUDE] Message from non interactive & remote mode:", message);
370
- switch (message.type) {
371
- case "system": {
372
- const sysMsg = message;
373
- if (sysMsg.subtype === "init") {
374
- console.log(chalk.gray("\u2500".repeat(60)));
375
- console.log(chalk.blue.bold("\u{1F680} Session initialized:"), chalk.cyan(sysMsg.session_id));
376
- console.log(chalk.gray(` Model: ${sysMsg.model}`));
377
- console.log(chalk.gray(` CWD: ${sysMsg.cwd}`));
378
- if (sysMsg.tools && sysMsg.tools.length > 0) {
379
- console.log(chalk.gray(` Tools: ${sysMsg.tools.join(", ")}`));
380
- }
381
- console.log(chalk.gray("\u2500".repeat(60)));
382
- }
383
- break;
384
- }
385
- case "user": {
386
- const userMsg = message;
387
- if (userMsg.message && typeof userMsg.message === "object" && "content" in userMsg.message) {
388
- const content = userMsg.message.content;
389
- if (typeof content === "string") {
390
- console.log(chalk.magenta.bold("\n\u{1F464} User:"), content);
391
- } else if (Array.isArray(content)) {
392
- for (const block of content) {
393
- if (block.type === "text") {
394
- console.log(chalk.magenta.bold("\n\u{1F464} User:"), block.text);
395
- } else if (block.type === "tool_result") {
396
- console.log(chalk.green.bold("\n\u2705 Tool Result:"), chalk.gray(`(Tool ID: ${block.tool_use_id})`));
397
- if (block.content) {
398
- const outputStr = typeof block.content === "string" ? block.content : JSON.stringify(block.content, null, 2);
399
- const maxLength = 200;
400
- if (outputStr.length > maxLength) {
401
- console.log(outputStr.substring(0, maxLength) + chalk.gray("\n... (truncated)"));
402
- } else {
403
- console.log(outputStr);
404
- }
405
- }
406
- }
407
- }
408
- } else {
409
- console.log(chalk.magenta.bold("\n\u{1F464} User:"), JSON.stringify(content, null, 2));
410
- }
411
- }
412
- break;
413
- }
414
- case "assistant": {
415
- const assistantMsg = message;
416
- if (assistantMsg.message && assistantMsg.message.content) {
417
- console.log(chalk.cyan.bold("\n\u{1F916} Assistant:"));
418
- for (const block of assistantMsg.message.content) {
419
- if (block.type === "text") {
420
- console.log(block.text);
421
- } else if (block.type === "tool_use") {
422
- console.log(chalk.yellow.bold(`
423
- \u{1F527} Tool: ${block.name}`));
424
- if (block.input) {
425
- const inputStr = JSON.stringify(block.input, null, 2);
426
- const maxLength = 500;
427
- if (inputStr.length > maxLength) {
428
- console.log(chalk.gray("Input:"), inputStr.substring(0, maxLength) + chalk.gray("\n... (truncated)"));
429
- } else {
430
- console.log(chalk.gray("Input:"), inputStr);
431
- }
432
- }
433
- }
434
- }
435
- }
436
- break;
437
- }
438
- case "result": {
439
- const resultMsg = message;
440
- if (resultMsg.subtype === "success") {
441
- if ("result" in resultMsg && resultMsg.result) {
442
- console.log(chalk.green.bold("\n\u2728 Summary:"));
443
- console.log(resultMsg.result);
444
- }
445
- if (resultMsg.usage) {
446
- console.log(chalk.gray("\n\u{1F4CA} Session Stats:"));
447
- console.log(chalk.gray(` \u2022 Turns: ${resultMsg.num_turns}`));
448
- console.log(chalk.gray(` \u2022 Input tokens: ${resultMsg.usage.input_tokens}`));
449
- console.log(chalk.gray(` \u2022 Output tokens: ${resultMsg.usage.output_tokens}`));
450
- if (resultMsg.usage.cache_read_input_tokens) {
451
- console.log(chalk.gray(` \u2022 Cache read tokens: ${resultMsg.usage.cache_read_input_tokens}`));
452
- }
453
- if (resultMsg.usage.cache_creation_input_tokens) {
454
- console.log(chalk.gray(` \u2022 Cache creation tokens: ${resultMsg.usage.cache_creation_input_tokens}`));
455
- }
456
- console.log(chalk.gray(` \u2022 Cost: $${resultMsg.total_cost_usd.toFixed(4)}`));
457
- console.log(chalk.gray(` \u2022 Duration: ${resultMsg.duration_ms}ms`));
458
- console.log(chalk.gray("\n\u{1F440} Back already?"));
459
- console.log(chalk.green("\u{1F449} Press any key to continue your session in `claude`"));
460
- if (onAssistantResult) {
461
- Promise.resolve(onAssistantResult(resultMsg)).catch((err) => {
462
- logger.debug("Error in onAssistantResult callback:", err);
463
- });
464
- }
465
- }
466
- } else if (resultMsg.subtype === "error_max_turns") {
467
- console.log(chalk.red.bold("\n\u274C Error: Maximum turns reached"));
468
- console.log(chalk.gray(`Completed ${resultMsg.num_turns} turns`));
469
- } else if (resultMsg.subtype === "error_during_execution") {
470
- console.log(chalk.red.bold("\n\u274C Error during execution"));
471
- console.log(chalk.gray(`Completed ${resultMsg.num_turns} turns before error`));
472
- logger.debugLargeJson("[RESULT] Error during execution", resultMsg);
473
- }
474
- break;
475
- }
476
- default: {
477
- if (process.env.DEBUG) {
478
- console.log(chalk.gray(`[Unknown message type: ${message.type}]`));
479
- }
480
- }
481
- }
482
- }
483
- function printDivider() {
484
- console.log(chalk.gray("\u2550".repeat(60)));
485
- }
486
-
487
- function getProjectPath(workingDirectory) {
488
- const projectId = resolve(workingDirectory).replace(/[\\\/\.:]/g, "-");
489
- return join(homedir(), ".claude", "projects", projectId);
490
- }
491
-
492
- function claudeCheckSession(sessionId, path) {
493
- const projectDir = getProjectPath(path);
494
- const sessionFile = join(projectDir, `${sessionId}.jsonl`);
495
- const sessionExists = existsSync(sessionFile);
496
- if (!sessionExists) {
497
- logger.debug(`[claudeCheckSession] Path ${sessionFile} does not exist`);
498
- return false;
499
- }
500
- const sessionData = readFileSync(sessionFile, "utf-8").split("\n");
501
- const hasGoodMessage = !!sessionData.find((v) => {
502
- try {
503
- return typeof JSON.parse(v).uuid === "string";
504
- } catch (e) {
505
- return false;
1054
+ });
1055
+ processExitPromise.finally(() => {
1056
+ cleanup();
1057
+ config.options?.abort?.removeEventListener("abort", cleanup);
1058
+ if (process.env.CLAUDE_SDK_MCP_SERVERS) {
1059
+ delete process.env.CLAUDE_SDK_MCP_SERVERS;
506
1060
  }
507
1061
  });
508
- return hasGoodMessage;
1062
+ return query2;
509
1063
  }
510
1064
 
511
1065
  async function awaitFileExist(file, timeout = 1e4) {
@@ -521,1057 +1075,1251 @@ async function awaitFileExist(file, timeout = 1e4) {
521
1075
  return false;
522
1076
  }
523
1077
 
524
- function deepEqual(a, b) {
525
- if (a === b) return true;
526
- if (a == null || b == null) return false;
527
- if (typeof a !== "object" || typeof b !== "object") return false;
528
- const keysA = Object.keys(a);
529
- const keysB = Object.keys(b);
530
- if (keysA.length !== keysB.length) return false;
531
- for (const key of keysA) {
532
- if (!keysB.includes(key)) return false;
533
- if (!deepEqual(a[key], b[key])) return false;
534
- }
535
- return true;
536
- }
537
- async function claudeRemote(opts) {
538
- let startFrom = opts.sessionId;
539
- if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
540
- startFrom = null;
541
- }
542
- if (opts.claudeEnvVars) {
543
- Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
544
- process.env[key] = value;
545
- });
546
- }
547
- const abortController = new AbortController();
548
- const sdkOptions = {
549
- cwd: opts.path,
550
- resume: startFrom ?? void 0,
551
- mcpServers: opts.mcpServers,
552
- permissionPromptToolName: opts.permissionPromptToolName,
553
- permissionMode: opts.permissionMode,
554
- executable: "node",
555
- abortController
556
- };
557
- if (opts.claudeArgs && opts.claudeArgs.length > 0) {
558
- sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
559
- }
560
- let aborted = false;
561
- let response;
562
- opts.abort.addEventListener("abort", () => {
563
- if (!aborted) {
564
- aborted = true;
565
- if (response) {
566
- (async () => {
567
- try {
568
- await response.interrupt();
569
- } catch (e) {
570
- }
571
- abortController.abort();
572
- })();
573
- } else {
574
- abortController.abort();
575
- }
576
- }
577
- });
578
- logger.debug(`[claudeRemote] Starting query with permission mode: ${opts.permissionMode}`);
579
- response = query({
580
- prompt: opts.message,
581
- options: sdkOptions
582
- });
583
- if (opts.interruptController) {
584
- opts.interruptController.register(async () => {
585
- logger.debug("[claudeRemote] Interrupting Claude via SDK");
586
- await response.interrupt();
587
- });
588
- }
589
- printDivider();
590
- let thinking = false;
591
- const updateThinking = (newThinking) => {
592
- if (thinking !== newThinking) {
593
- thinking = newThinking;
594
- logger.debug(`[claudeRemote] Thinking state changed to: ${thinking}`);
595
- if (opts.onThinkingChange) {
596
- opts.onThinkingChange(thinking);
597
- }
598
- }
599
- };
600
- const toolCalls = [];
601
- const resolveToolCallId = (name, args) => {
602
- for (let i = toolCalls.length - 1; i >= 0; i--) {
603
- const call = toolCalls[i];
604
- if (call.name === name && deepEqual(call.input, args)) {
605
- if (call.used) {
606
- logger.debug("[claudeRemote] Warning: Permission request matched an already-used tool call");
607
- return null;
608
- }
609
- call.used = true;
610
- logger.debug(`[claudeRemote] Resolved tool call ID: ${call.id} for ${name}`);
611
- return call.id;
612
- }
613
- }
614
- logger.debug(`[claudeRemote] No matching tool call found for permission request: ${name}`);
615
- return null;
616
- };
617
- if (opts.onToolCallResolver) {
618
- opts.onToolCallResolver(resolveToolCallId);
1078
+ class PushableAsyncIterable {
1079
+ queue = [];
1080
+ waiters = [];
1081
+ isDone = false;
1082
+ error = null;
1083
+ started = false;
1084
+ constructor() {
619
1085
  }
620
- try {
621
- logger.debug(`[claudeRemote] Starting to iterate over response`);
622
- for await (const message of response) {
623
- logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
624
- formatClaudeMessage(message, opts.onAssistantResult);
625
- if (message.type === "assistant") {
626
- const assistantMsg = message;
627
- if (assistantMsg.message && assistantMsg.message.content) {
628
- for (const block of assistantMsg.message.content) {
629
- if (block.type === "tool_use") {
630
- toolCalls.push({
631
- id: block.id,
632
- name: block.name,
633
- input: block.input,
634
- used: false
635
- });
636
- logger.debug(`[claudeRemote] Tracked tool call: ${block.id} - ${block.name}`);
637
- }
638
- }
639
- }
640
- }
641
- if (message.type === "user") {
642
- const userMsg = message;
643
- if (userMsg.message && userMsg.message.content && Array.isArray(userMsg.message.content)) {
644
- for (const block of userMsg.message.content) {
645
- if (block.type === "tool_result" && block.tool_use_id) {
646
- const toolCall = toolCalls.find((tc) => tc.id === block.tool_use_id);
647
- if (toolCall && !toolCall.used) {
648
- toolCall.used = true;
649
- logger.debug(`[claudeRemote] Tool completed execution, marked as used: ${block.tool_use_id}`);
650
- }
651
- }
652
- }
653
- }
654
- }
655
- if (message.type === "system" && message.subtype === "init") {
656
- updateThinking(true);
657
- const systemInit = message;
658
- if (systemInit.session_id) {
659
- logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${systemInit.session_id}`);
660
- const projectDir = getProjectPath(opts.path);
661
- const found = await awaitFileExist(join(projectDir, `${systemInit.session_id}.jsonl`));
662
- logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
663
- opts.onSessionFound(systemInit.session_id);
664
- }
665
- }
666
- if (message.type === "result") {
667
- updateThinking(false);
668
- logger.debug("[claudeRemote] Result received, exiting claudeRemote");
669
- break;
670
- }
1086
+ /**
1087
+ * Push a value to the iterable
1088
+ */
1089
+ push(value) {
1090
+ if (this.isDone) {
1091
+ throw new Error("Cannot push to completed iterable");
671
1092
  }
672
- logger.debug(`[claudeRemote] Finished iterating over response`);
673
- } catch (e) {
674
- if (abortController.signal.aborted) {
675
- logger.debug(`[claudeRemote] Aborted`);
1093
+ if (this.error) {
1094
+ throw this.error;
676
1095
  }
677
- if (e instanceof AbortError) {
678
- logger.debug(`[claudeRemote] Aborted`);
1096
+ const waiter = this.waiters.shift();
1097
+ if (waiter) {
1098
+ waiter.resolve({ done: false, value });
679
1099
  } else {
680
- throw e;
681
- }
682
- } finally {
683
- updateThinking(false);
684
- toolCalls.length = 0;
685
- if (opts.onToolCallResolver) {
686
- opts.onToolCallResolver(null);
687
- }
688
- if (opts.interruptController) {
689
- opts.interruptController.unregister();
690
- }
691
- }
692
- printDivider();
693
- logger.debug(`[claudeRemote] Function completed`);
694
- }
695
-
696
- const __dirname$1 = dirname(fileURLToPath(import.meta.url));
697
- async function claudeLocal(opts) {
698
- const projectDir = getProjectPath(opts.path);
699
- mkdirSync(projectDir, { recursive: true });
700
- const watcher = watch(projectDir);
701
- let resolvedSessionId = null;
702
- const detectedIdsRandomUUID = /* @__PURE__ */ new Set();
703
- const detectedIdsFileSystem = /* @__PURE__ */ new Set();
704
- watcher.on("change", (event, filename) => {
705
- if (typeof filename === "string" && filename.toLowerCase().endsWith(".jsonl")) {
706
- logger.debug("change", event, filename);
707
- const sessionId = filename.replace(".jsonl", "");
708
- if (detectedIdsFileSystem.has(sessionId)) {
709
- return;
710
- }
711
- detectedIdsFileSystem.add(sessionId);
712
- if (resolvedSessionId) {
713
- return;
714
- }
715
- if (detectedIdsRandomUUID.has(sessionId)) {
716
- resolvedSessionId = sessionId;
717
- opts.onSessionFound(sessionId);
718
- }
1100
+ this.queue.push(value);
719
1101
  }
720
- });
721
- let startFrom = opts.sessionId;
722
- if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
723
- startFrom = null;
724
- }
725
- try {
726
- process.stdin.pause();
727
- await new Promise((r, reject) => {
728
- const args = [];
729
- if (startFrom) {
730
- args.push("--resume", startFrom);
731
- }
732
- if (opts.claudeArgs) {
733
- args.push(...opts.claudeArgs);
734
- }
735
- const claudeCliPath = process.env.HAPPY_CLAUDE_CLI_PATH || resolve(join(__dirname$1, "..", "scripts", "claudeInteractiveLaunch.cjs"));
736
- const env = {
737
- ...process.env,
738
- ...opts.claudeEnvVars
739
- };
740
- const child = spawn("node", [claudeCliPath, ...args], {
741
- stdio: ["inherit", "inherit", "inherit", "pipe"],
742
- signal: opts.abort,
743
- cwd: opts.path,
744
- env
745
- });
746
- if (child.stdio[3]) {
747
- const rl = createInterface({
748
- input: child.stdio[3],
749
- crlfDelay: Infinity
750
- });
751
- const activeFetches = /* @__PURE__ */ new Map();
752
- let thinking = false;
753
- let stopThinkingTimeout = null;
754
- const updateThinking = (newThinking) => {
755
- if (thinking !== newThinking) {
756
- thinking = newThinking;
757
- logger.debug(`[ClaudeLocal] Thinking state changed to: ${thinking}`);
758
- if (opts.onThinkingChange) {
759
- opts.onThinkingChange(thinking);
760
- }
761
- }
762
- };
763
- rl.on("line", (line) => {
764
- try {
765
- const message = JSON.parse(line);
766
- switch (message.type) {
767
- case "uuid":
768
- detectedIdsRandomUUID.add(message.value);
769
- if (!resolvedSessionId && detectedIdsFileSystem.has(message.value)) {
770
- resolvedSessionId = message.value;
771
- opts.onSessionFound(message.value);
772
- }
773
- break;
774
- case "fetch-start":
775
- logger.debug(`[ClaudeLocal] Fetch start: ${message.method} ${message.hostname}${message.path} (id: ${message.id})`);
776
- activeFetches.set(message.id, {
777
- hostname: message.hostname,
778
- path: message.path,
779
- startTime: message.timestamp
780
- });
781
- if (stopThinkingTimeout) {
782
- clearTimeout(stopThinkingTimeout);
783
- stopThinkingTimeout = null;
784
- }
785
- updateThinking(true);
786
- break;
787
- case "fetch-end":
788
- logger.debug(`[ClaudeLocal] Fetch end: id ${message.id}`);
789
- activeFetches.delete(message.id);
790
- if (activeFetches.size === 0 && thinking && !stopThinkingTimeout) {
791
- stopThinkingTimeout = setTimeout(() => {
792
- if (activeFetches.size === 0) {
793
- updateThinking(false);
794
- }
795
- stopThinkingTimeout = null;
796
- }, 500);
797
- }
798
- break;
799
- default:
800
- logger.debug(`[ClaudeLocal] Unknown message type: ${message.type}`);
801
- }
802
- } catch (e) {
803
- logger.debug(`[ClaudeLocal] Non-JSON line from fd3: ${line}`);
804
- }
805
- });
806
- rl.on("error", (err) => {
807
- console.error("Error reading from fd 3:", err);
808
- });
809
- child.on("exit", () => {
810
- if (stopThinkingTimeout) {
811
- clearTimeout(stopThinkingTimeout);
812
- }
813
- updateThinking(false);
814
- });
815
- }
816
- child.on("error", (error) => {
817
- });
818
- child.on("exit", (code, signal) => {
819
- if (signal === "SIGTERM" && opts.abort.aborted) {
820
- r();
821
- } else if (signal) {
822
- reject(new Error(`Process terminated with signal: ${signal}`));
823
- } else {
824
- r();
825
- }
826
- });
827
- });
828
- } finally {
829
- watcher.close();
830
- process.stdin.resume();
831
1102
  }
832
- return resolvedSessionId;
833
- }
834
-
835
- class MessageQueue2 {
836
- constructor(modeHasher) {
837
- this.modeHasher = modeHasher;
838
- logger.debug(`[MessageQueue2] Initialized`);
1103
+ /**
1104
+ * Mark the iterable as complete
1105
+ */
1106
+ end() {
1107
+ if (this.isDone) {
1108
+ return;
1109
+ }
1110
+ this.isDone = true;
1111
+ this.cleanup();
839
1112
  }
840
- queue = [];
841
- waiter = null;
842
- closed = false;
843
1113
  /**
844
- * Push a message to the queue with a mode.
1114
+ * Set an error on the iterable
845
1115
  */
846
- push(message, mode) {
847
- if (this.closed) {
848
- throw new Error("Cannot push to closed queue");
1116
+ setError(err) {
1117
+ if (this.isDone) {
1118
+ return;
849
1119
  }
850
- const modeHash = this.modeHasher(mode);
851
- logger.debug(`[MessageQueue2] push() called with mode hash: ${modeHash}`);
852
- this.queue.push({
853
- message,
854
- mode,
855
- modeHash
856
- });
857
- if (this.waiter) {
858
- logger.debug(`[MessageQueue2] Notifying waiter`);
859
- const waiter = this.waiter;
860
- this.waiter = null;
861
- waiter(true);
1120
+ this.error = err;
1121
+ this.isDone = true;
1122
+ this.cleanup();
1123
+ }
1124
+ /**
1125
+ * Cleanup waiting consumers
1126
+ */
1127
+ cleanup() {
1128
+ while (this.waiters.length > 0) {
1129
+ const waiter = this.waiters.shift();
1130
+ if (this.error) {
1131
+ waiter.reject(this.error);
1132
+ } else {
1133
+ waiter.resolve({ done: true, value: void 0 });
1134
+ }
862
1135
  }
863
- logger.debug(`[MessageQueue2] push() completed. Queue size: ${this.queue.length}`);
864
1136
  }
865
1137
  /**
866
- * Push a message to the beginning of the queue with a mode.
1138
+ * AsyncIterableIterator implementation
867
1139
  */
868
- unshift(message, mode) {
869
- if (this.closed) {
870
- throw new Error("Cannot unshift to closed queue");
1140
+ async next() {
1141
+ if (this.queue.length > 0) {
1142
+ return { done: false, value: this.queue.shift() };
871
1143
  }
872
- const modeHash = this.modeHasher(mode);
873
- logger.debug(`[MessageQueue2] unshift() called with mode hash: ${modeHash}`);
874
- this.queue.unshift({
875
- message,
876
- mode,
877
- modeHash
878
- });
879
- if (this.waiter) {
880
- logger.debug(`[MessageQueue2] Notifying waiter`);
881
- const waiter = this.waiter;
882
- this.waiter = null;
883
- waiter(true);
1144
+ if (this.isDone) {
1145
+ if (this.error) {
1146
+ throw this.error;
1147
+ }
1148
+ return { done: true, value: void 0 };
884
1149
  }
885
- logger.debug(`[MessageQueue2] unshift() completed. Queue size: ${this.queue.length}`);
1150
+ return new Promise((resolve, reject) => {
1151
+ this.waiters.push({ resolve, reject });
1152
+ });
886
1153
  }
887
1154
  /**
888
- * Close the queue - no more messages can be pushed
1155
+ * AsyncIterableIterator return implementation
889
1156
  */
890
- close() {
891
- logger.debug(`[MessageQueue2] close() called`);
892
- this.closed = true;
893
- if (this.waiter) {
894
- const waiter = this.waiter;
895
- this.waiter = null;
896
- waiter(false);
1157
+ async return(_value) {
1158
+ this.end();
1159
+ return { done: true, value: void 0 };
1160
+ }
1161
+ /**
1162
+ * AsyncIterableIterator throw implementation
1163
+ */
1164
+ async throw(e) {
1165
+ this.setError(e instanceof Error ? e : new Error(String(e)));
1166
+ throw this.error;
1167
+ }
1168
+ /**
1169
+ * Make this iterable
1170
+ */
1171
+ [Symbol.asyncIterator]() {
1172
+ if (this.started) {
1173
+ throw new Error("PushableAsyncIterable can only be iterated once");
897
1174
  }
1175
+ this.started = true;
1176
+ return this;
898
1177
  }
899
1178
  /**
900
- * Check if the queue is closed
1179
+ * Check if the iterable is done
901
1180
  */
902
- isClosed() {
903
- return this.closed;
1181
+ get done() {
1182
+ return this.isDone;
1183
+ }
1184
+ /**
1185
+ * Check if the iterable has an error
1186
+ */
1187
+ get hasError() {
1188
+ return this.error !== null;
904
1189
  }
905
1190
  /**
906
1191
  * Get the current queue size
907
1192
  */
908
- size() {
1193
+ get queueSize() {
909
1194
  return this.queue.length;
910
1195
  }
911
1196
  /**
912
- * Wait for messages and return all messages with the same mode as a single string
913
- * Returns { message: string, mode: T } or null if aborted/closed
1197
+ * Get the number of waiting consumers
914
1198
  */
915
- async waitForMessagesAndGetAsString(abortSignal) {
916
- if (this.queue.length > 0) {
917
- return this.collectBatch();
1199
+ get waiterCount() {
1200
+ return this.waiters.length;
1201
+ }
1202
+ }
1203
+
1204
+ async function claudeRemote(opts) {
1205
+ let startFrom = opts.sessionId;
1206
+ if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
1207
+ startFrom = null;
1208
+ }
1209
+ if (opts.claudeEnvVars) {
1210
+ Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
1211
+ process.env[key] = value;
1212
+ });
1213
+ }
1214
+ let response;
1215
+ const sdkOptions = {
1216
+ cwd: opts.path,
1217
+ resume: startFrom ?? void 0,
1218
+ mcpServers: opts.mcpServers,
1219
+ permissionPromptToolName: opts.permissionPromptToolName,
1220
+ permissionMode: opts.permissionMode,
1221
+ executable: "node",
1222
+ abort: opts.signal
1223
+ };
1224
+ if (opts.claudeArgs && opts.claudeArgs.length > 0) {
1225
+ sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
1226
+ }
1227
+ logger.debug(`[claudeRemote] Starting query with permission mode: ${opts.permissionMode}`);
1228
+ let message = new PushableAsyncIterable();
1229
+ message.push({
1230
+ type: "user",
1231
+ message: {
1232
+ role: "user",
1233
+ content: opts.message
918
1234
  }
919
- if (this.closed || abortSignal?.aborted) {
920
- return null;
1235
+ });
1236
+ message.end();
1237
+ response = query({
1238
+ prompt: message,
1239
+ options: sdkOptions
1240
+ });
1241
+ let thinking = false;
1242
+ const updateThinking = (newThinking) => {
1243
+ if (thinking !== newThinking) {
1244
+ thinking = newThinking;
1245
+ logger.debug(`[claudeRemote] Thinking state changed to: ${thinking}`);
1246
+ if (opts.onThinkingChange) {
1247
+ opts.onThinkingChange(thinking);
1248
+ }
921
1249
  }
922
- const hasMessages = await this.waitForMessages(abortSignal);
923
- if (!hasMessages) {
924
- return null;
1250
+ };
1251
+ try {
1252
+ logger.debug(`[claudeRemote] Starting to iterate over response`);
1253
+ for await (const message2 of response) {
1254
+ logger.debugLargeJson(`[claudeRemote] Message ${message2.type}`, message2);
1255
+ opts.onMessage(message2);
1256
+ if (message2.type === "system" && message2.subtype === "init") {
1257
+ updateThinking(true);
1258
+ const systemInit = message2;
1259
+ if (systemInit.session_id) {
1260
+ logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${systemInit.session_id}`);
1261
+ const projectDir = getProjectPath(opts.path);
1262
+ const found = await awaitFileExist(join(projectDir, `${systemInit.session_id}.jsonl`));
1263
+ logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
1264
+ opts.onSessionFound(systemInit.session_id);
1265
+ }
1266
+ }
1267
+ if (message2.type === "result") {
1268
+ updateThinking(false);
1269
+ logger.debug("[claudeRemote] Result received, exiting claudeRemote");
1270
+ return;
1271
+ }
1272
+ if (message2.type === "user") {
1273
+ const msg = message2;
1274
+ if (msg.message.role === "user" && Array.isArray(msg.message.content)) {
1275
+ for (let c of msg.message.content) {
1276
+ if (c.type === "tool_result" && (c.name === "exit_plan_mode" || c.name === "ExitPlanMode")) {
1277
+ logger.debug("[claudeRemote] Plan result received, exiting claudeRemote");
1278
+ return;
1279
+ }
1280
+ if (c.type === "tool_result" && c.tool_use_id && opts.responses.has(c.tool_use_id) && !opts.responses.get(c.tool_use_id).approved) {
1281
+ logger.debug("[claudeRemote] Tool rejected, exiting claudeRemote");
1282
+ return;
1283
+ }
1284
+ }
1285
+ }
1286
+ }
925
1287
  }
926
- return this.collectBatch();
927
- }
928
- /**
929
- * Collect a batch of messages with the same mode
930
- */
931
- collectBatch() {
932
- if (this.queue.length === 0) {
933
- return null;
1288
+ logger.debug(`[claudeRemote] Finished iterating over response`);
1289
+ } catch (e) {
1290
+ if (e instanceof AbortError) {
1291
+ logger.debug(`[claudeRemote] Aborted`);
1292
+ } else {
1293
+ throw e;
934
1294
  }
935
- const firstItem = this.queue[0];
936
- const sameModeMessages = [];
937
- let mode = firstItem.mode;
938
- const targetModeHash = firstItem.modeHash;
939
- while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash) {
940
- const item = this.queue.shift();
941
- sameModeMessages.push(item.message);
1295
+ } finally {
1296
+ updateThinking(false);
1297
+ }
1298
+ logger.debug(`[claudeRemote] Function completed`);
1299
+ }
1300
+
1301
+ const PLAN_FAKE_REJECT = `User approved plan, but you need to be restarted. STOP IMMEDIATELY TO SWITCH FROM PLAN MODE. DO NOT REPLY TO THIS MESSAGE.`;
1302
+ const PLAN_FAKE_RESTART = `PlEaZe Continue with plan.`;
1303
+
1304
+ async function startPermissionServerV2(handler) {
1305
+ const mcp = new McpServer({
1306
+ name: "Permission Server",
1307
+ version: "1.0.0",
1308
+ description: "A server that allows you to request permissions from the user"
1309
+ });
1310
+ mcp.registerTool("ask_permission", {
1311
+ description: "Request permission to execute a tool",
1312
+ title: "Request Permission",
1313
+ inputSchema: {
1314
+ tool_name: z$1.string().describe("The tool that needs permission"),
1315
+ input: z$1.any().describe("The arguments for the tool")
942
1316
  }
943
- const combinedMessage = sameModeMessages.join("\n");
944
- logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
1317
+ }, async (args) => {
1318
+ const response = await handler({ name: args.tool_name, arguments: args.input });
1319
+ logger.debugLargeJson("[permissionServerV2] Response", response);
1320
+ const result = response.approved ? { behavior: "allow", updatedInput: args.input || {} } : { behavior: "deny", message: response.reason || `The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.` };
945
1321
  return {
946
- message: combinedMessage,
947
- mode
1322
+ content: [
1323
+ {
1324
+ type: "text",
1325
+ text: JSON.stringify(result)
1326
+ }
1327
+ ],
1328
+ isError: false
948
1329
  };
1330
+ });
1331
+ const transport = new StreamableHTTPServerTransport({
1332
+ // NOTE: Returning session id here will result in claude
1333
+ // sdk spawn to fail with `Invalid Request: Server already initialized`
1334
+ sessionIdGenerator: void 0
1335
+ });
1336
+ await mcp.connect(transport);
1337
+ const server = createServer(async (req, res) => {
1338
+ try {
1339
+ await transport.handleRequest(req, res);
1340
+ } catch (error) {
1341
+ logger.debug("Error handling request:", error);
1342
+ if (!res.headersSent) {
1343
+ res.writeHead(500).end();
1344
+ }
1345
+ }
1346
+ });
1347
+ const baseUrl = await new Promise((resolve) => {
1348
+ server.listen(0, "127.0.0.1", () => {
1349
+ const addr = server.address();
1350
+ resolve(new URL(`http://127.0.0.1:${addr.port}`));
1351
+ });
1352
+ });
1353
+ return {
1354
+ url: baseUrl.toString(),
1355
+ toolName: "ask_permission",
1356
+ stop: () => {
1357
+ mcp.close();
1358
+ server.close();
1359
+ }
1360
+ };
1361
+ }
1362
+
1363
+ function deepEqual(a, b) {
1364
+ if (a === b) return true;
1365
+ if (a == null || b == null) return false;
1366
+ if (typeof a !== "object" || typeof b !== "object") return false;
1367
+ const keysA = Object.keys(a);
1368
+ const keysB = Object.keys(b);
1369
+ if (keysA.length !== keysB.length) return false;
1370
+ for (const key of keysA) {
1371
+ if (!keysB.includes(key)) return false;
1372
+ if (!deepEqual(a[key], b[key])) return false;
949
1373
  }
950
- /**
951
- * Wait for messages to arrive
952
- */
953
- waitForMessages(abortSignal) {
954
- return new Promise((resolve) => {
955
- let abortHandler = null;
956
- if (abortSignal) {
957
- abortHandler = () => {
958
- logger.debug("[MessageQueue2] Wait aborted");
959
- if (this.waiter === waiterFunc) {
960
- this.waiter = null;
1374
+ return true;
1375
+ }
1376
+
1377
+ async function startPermissionResolver(session) {
1378
+ let toolCalls = [];
1379
+ let responses = /* @__PURE__ */ new Map();
1380
+ let requests = /* @__PURE__ */ new Map();
1381
+ let pendingPermissionRequests = [];
1382
+ const server = await startPermissionServerV2(async (request) => {
1383
+ const id = resolveToolCallId(request.name, request.arguments);
1384
+ if (!id) {
1385
+ logger.debug(`Tool call ID not yet available for ${request.name}, queueing request`);
1386
+ return new Promise((resolve, reject) => {
1387
+ const timeout = setTimeout(() => {
1388
+ const idx = pendingPermissionRequests.findIndex((p) => p.request === request);
1389
+ if (idx !== -1) {
1390
+ pendingPermissionRequests.splice(idx, 1);
1391
+ reject(new Error(`Timeout: Tool call ID never arrived for ${request.name}`));
1392
+ }
1393
+ }, 3e4);
1394
+ pendingPermissionRequests.push({ request, resolve, reject, timeout });
1395
+ });
1396
+ }
1397
+ return handlePermissionRequest(id, request);
1398
+ });
1399
+ function handlePermissionRequest(id, request) {
1400
+ let promise = new Promise((resolve) => {
1401
+ if (request.name === "exit_plan_mode" || request.name === "ExitPlanMode") {
1402
+ const wrappedResolve = (response) => {
1403
+ if (response.approved) {
1404
+ logger.debug("Plan approved - injecting PLAN_FAKE_RESTART");
1405
+ if (response.mode && ["default", "acceptEdits", "bypassPermissions"].includes(response.mode)) {
1406
+ session.queue.unshift(PLAN_FAKE_RESTART, response.mode);
1407
+ } else {
1408
+ session.queue.unshift(PLAN_FAKE_RESTART, "default");
1409
+ }
1410
+ resolve({ approved: false, reason: PLAN_FAKE_REJECT });
1411
+ } else {
1412
+ resolve(response);
961
1413
  }
962
- resolve(false);
963
1414
  };
964
- abortSignal.addEventListener("abort", abortHandler);
1415
+ requests.set(id, wrappedResolve);
1416
+ } else {
1417
+ requests.set(id, resolve);
965
1418
  }
966
- const waiterFunc = (hasMessages) => {
967
- if (abortHandler && abortSignal) {
968
- abortSignal.removeEventListener("abort", abortHandler);
969
- }
970
- resolve(hasMessages);
971
- };
972
- if (this.queue.length > 0) {
973
- if (abortHandler && abortSignal) {
974
- abortSignal.removeEventListener("abort", abortHandler);
975
- }
976
- resolve(true);
977
- return;
1419
+ });
1420
+ let timeout = setTimeout(async () => {
1421
+ logger.debug("Permission timeout - attempting to interrupt Claude");
1422
+ requests.delete(id);
1423
+ session.client.updateAgentState((currentState) => {
1424
+ const request2 = currentState.requests?.[id];
1425
+ if (!request2) return currentState;
1426
+ let r = { ...currentState.requests };
1427
+ delete r[id];
1428
+ return {
1429
+ ...currentState,
1430
+ requests: r,
1431
+ completedRequests: {
1432
+ ...currentState.completedRequests,
1433
+ [id]: {
1434
+ ...request2,
1435
+ completedAt: Date.now(),
1436
+ status: "canceled",
1437
+ reason: "Timeout"
1438
+ }
1439
+ }
1440
+ };
1441
+ });
1442
+ }, 1e3 * 60 * 4.5);
1443
+ logger.debug("Permission request" + id + " " + JSON.stringify(request));
1444
+ session.api.push().sendToAllDevices(
1445
+ "Permission Request",
1446
+ `Claude wants to use ${request.name}`,
1447
+ {
1448
+ sessionId: session.client.sessionId,
1449
+ requestId: id,
1450
+ tool: request.name,
1451
+ type: "permission_request"
978
1452
  }
979
- if (this.closed || abortSignal?.aborted) {
980
- if (abortHandler && abortSignal) {
981
- abortSignal.removeEventListener("abort", abortHandler);
1453
+ );
1454
+ session.client.updateAgentState((currentState) => ({
1455
+ ...currentState,
1456
+ requests: {
1457
+ ...currentState.requests,
1458
+ [id]: {
1459
+ tool: request.name,
1460
+ arguments: request.arguments,
1461
+ createdAt: Date.now()
982
1462
  }
983
- resolve(false);
984
- return;
985
1463
  }
986
- this.waiter = waiterFunc;
987
- logger.debug("[MessageQueue2] Waiting for messages...");
988
- });
989
- }
990
- }
991
-
992
- var MessageQueue2$1 = /*#__PURE__*/Object.freeze({
993
- __proto__: null,
994
- MessageQueue2: MessageQueue2
995
- });
996
-
997
- class InvalidateSync {
998
- _invalidated = false;
999
- _invalidatedDouble = false;
1000
- _stopped = false;
1001
- _command;
1002
- _pendings = [];
1003
- constructor(command) {
1004
- this._command = command;
1464
+ }));
1465
+ promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
1466
+ return promise;
1005
1467
  }
1006
- invalidate() {
1007
- if (this._stopped) {
1008
- return;
1009
- }
1010
- if (!this._invalidated) {
1011
- this._invalidated = true;
1012
- this._invalidatedDouble = false;
1013
- this._doSync();
1468
+ session.client.setHandler("permission", async (message) => {
1469
+ logger.debug("Permission response" + JSON.stringify(message));
1470
+ const id = message.id;
1471
+ const resolve = requests.get(id);
1472
+ if (resolve) {
1473
+ responses.set(id, message);
1474
+ resolve({ approved: message.approved, reason: message.reason, mode: message.mode });
1475
+ requests.delete(id);
1014
1476
  } else {
1015
- if (!this._invalidatedDouble) {
1016
- this._invalidatedDouble = true;
1017
- }
1018
- }
1019
- }
1020
- async invalidateAndAwait() {
1021
- if (this._stopped) {
1477
+ logger.debug("Permission request stale, likely timed out");
1022
1478
  return;
1023
1479
  }
1024
- await new Promise((resolve) => {
1025
- this._pendings.push(resolve);
1026
- this.invalidate();
1480
+ session.client.updateAgentState((currentState) => {
1481
+ const request = currentState.requests?.[id];
1482
+ if (!request) return currentState;
1483
+ let r = { ...currentState.requests };
1484
+ delete r[id];
1485
+ const isExitPlanModeSuccess = request.tool === "exit_plan_mode" && !message.approved && message.reason === PLAN_FAKE_REJECT;
1486
+ return {
1487
+ ...currentState,
1488
+ requests: r,
1489
+ completedRequests: {
1490
+ ...currentState.completedRequests,
1491
+ [id]: {
1492
+ ...request,
1493
+ completedAt: Date.now(),
1494
+ status: isExitPlanModeSuccess ? "approved" : message.approved ? "approved" : "denied",
1495
+ reason: isExitPlanModeSuccess ? "Plan approved" : message.reason
1496
+ }
1497
+ }
1498
+ };
1027
1499
  });
1028
- }
1029
- stop() {
1030
- if (this._stopped) {
1031
- return;
1032
- }
1033
- this._notifyPendings();
1034
- this._stopped = true;
1035
- }
1036
- _notifyPendings = () => {
1037
- for (let pending of this._pendings) {
1038
- pending();
1500
+ });
1501
+ const resolveToolCallId = (name, args) => {
1502
+ for (let i = toolCalls.length - 1; i >= 0; i--) {
1503
+ const call = toolCalls[i];
1504
+ if (call.name === name && deepEqual(call.input, args)) {
1505
+ if (call.used) {
1506
+ return null;
1507
+ }
1508
+ call.used = true;
1509
+ return call.id;
1510
+ }
1039
1511
  }
1040
- this._pendings = [];
1512
+ return null;
1041
1513
  };
1042
- _doSync = async () => {
1043
- await backoff(async () => {
1044
- if (this._stopped) {
1045
- return;
1514
+ function reset() {
1515
+ toolCalls = [];
1516
+ requests.clear();
1517
+ responses.clear();
1518
+ for (const pending of pendingPermissionRequests) {
1519
+ clearTimeout(pending.timeout);
1520
+ }
1521
+ pendingPermissionRequests = [];
1522
+ session.client.updateAgentState((currentState) => {
1523
+ const pendingRequests = currentState.requests || {};
1524
+ const completedRequests = { ...currentState.completedRequests };
1525
+ for (const [id, request] of Object.entries(pendingRequests)) {
1526
+ completedRequests[id] = {
1527
+ ...request,
1528
+ completedAt: Date.now(),
1529
+ status: "canceled",
1530
+ reason: "Session switched to local mode"
1531
+ };
1046
1532
  }
1047
- await this._command();
1533
+ return {
1534
+ ...currentState,
1535
+ requests: {},
1536
+ // Clear all pending requests
1537
+ completedRequests
1538
+ };
1048
1539
  });
1049
- if (this._stopped) {
1050
- this._notifyPendings();
1051
- return;
1052
- }
1053
- if (this._invalidatedDouble) {
1054
- this._invalidatedDouble = false;
1055
- this._doSync();
1056
- } else {
1057
- this._invalidated = false;
1058
- this._notifyPendings();
1059
- }
1060
- };
1061
- }
1062
-
1063
- function startFileWatcher(file, onFileChange) {
1064
- const abortController = new AbortController();
1065
- void (async () => {
1066
- while (true) {
1067
- try {
1068
- logger.debug(`[FILE_WATCHER] Starting watcher for ${file}`);
1069
- const watcher = watch$1(file, { persistent: true, signal: abortController.signal });
1070
- for await (const event of watcher) {
1071
- if (abortController.signal.aborted) {
1072
- return;
1540
+ }
1541
+ function onMessage(message) {
1542
+ if (message.type === "assistant") {
1543
+ const assistantMsg = message;
1544
+ if (assistantMsg.message && assistantMsg.message.content) {
1545
+ for (const block of assistantMsg.message.content) {
1546
+ if (block.type === "tool_use") {
1547
+ toolCalls.push({
1548
+ id: block.id,
1549
+ name: block.name,
1550
+ input: block.input,
1551
+ used: false
1552
+ });
1553
+ for (let i = pendingPermissionRequests.length - 1; i >= 0; i--) {
1554
+ const pending = pendingPermissionRequests[i];
1555
+ if (pending.request.name === block.name && deepEqual(pending.request.arguments, block.input)) {
1556
+ logger.debug(`Resolving pending permission request for ${block.name} with ID ${block.id}`);
1557
+ clearTimeout(pending.timeout);
1558
+ pendingPermissionRequests.splice(i, 1);
1559
+ handlePermissionRequest(block.id, pending.request).then(
1560
+ pending.resolve,
1561
+ pending.reject
1562
+ );
1563
+ break;
1564
+ }
1565
+ }
1073
1566
  }
1074
- logger.debug(`[FILE_WATCHER] File changed: ${file}`);
1075
- onFileChange(file);
1076
1567
  }
1077
- } catch (e) {
1078
- if (abortController.signal.aborted) {
1079
- return;
1568
+ }
1569
+ }
1570
+ if (message.type === "user") {
1571
+ const userMsg = message;
1572
+ if (userMsg.message && userMsg.message.content && Array.isArray(userMsg.message.content)) {
1573
+ for (const block of userMsg.message.content) {
1574
+ if (block.type === "tool_result" && block.tool_use_id) {
1575
+ const toolCall = toolCalls.find((tc) => tc.id === block.tool_use_id);
1576
+ if (toolCall && !toolCall.used) {
1577
+ toolCall.used = true;
1578
+ }
1579
+ }
1080
1580
  }
1081
- logger.debug(`[FILE_WATCHER] Watch error: ${e.message}, restarting watcher in a second`);
1082
- await delay(1e3);
1083
1581
  }
1084
1582
  }
1085
- })();
1086
- return () => {
1087
- abortController.abort();
1583
+ }
1584
+ return {
1585
+ server,
1586
+ reset,
1587
+ onMessage,
1588
+ responses
1088
1589
  };
1089
1590
  }
1090
1591
 
1091
- const PLAN_FAKE_REJECT = `User approved plan, but you need to be restarted. STOP IMMEDIATELY TO SWITCH FROM PLAN MODE. DO NOT REPLY TO THIS MESSAGE.`;
1092
- const PLAN_FAKE_RESTART = `PlEaZe Continue with plan.`;
1093
-
1094
- function hackToolResponse(message) {
1095
- logger.debug("hackToolResponse", JSON.stringify(message, null, 2));
1096
- if (message.type === "user" && message.message?.role === "user" && message.message?.content && Array.isArray(message.message.content)) {
1097
- let modified = false;
1098
- const hackedContent = message.message.content.map((item) => {
1099
- if (item.type === "tool_result" && item.is_error === true) {
1100
- if (item.content === PLAN_FAKE_REJECT) {
1101
- logger.debug(`[SESSION_SCANNER] Hacking exit_plan_mode tool_result: flipping is_error from true to false`);
1102
- modified = true;
1103
- return {
1104
- ...item,
1105
- is_error: false,
1106
- content: "Plan approved"
1107
- };
1592
+ function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
1593
+ logger.debugLargeJson("[CLAUDE INK] Message from remote mode:", message);
1594
+ switch (message.type) {
1595
+ case "system": {
1596
+ const sysMsg = message;
1597
+ if (sysMsg.subtype === "init") {
1598
+ messageBuffer.addMessage("\u2500".repeat(40), "status");
1599
+ messageBuffer.addMessage(`\u{1F680} Session initialized: ${sysMsg.session_id}`, "system");
1600
+ messageBuffer.addMessage(` Model: ${sysMsg.model}`, "status");
1601
+ messageBuffer.addMessage(` CWD: ${sysMsg.cwd}`, "status");
1602
+ if (sysMsg.tools && sysMsg.tools.length > 0) {
1603
+ messageBuffer.addMessage(` Tools: ${sysMsg.tools.join(", ")}`, "status");
1108
1604
  }
1605
+ messageBuffer.addMessage("\u2500".repeat(40), "status");
1109
1606
  }
1110
- return item;
1111
- });
1112
- if (modified) {
1113
- return {
1114
- ...message,
1115
- message: {
1116
- ...message.message,
1117
- content: hackedContent
1118
- }
1119
- };
1120
- }
1121
- }
1122
- return message;
1123
- }
1124
- function createSessionScanner(opts) {
1125
- const projectDir = getProjectPath(opts.workingDirectory);
1126
- let finishedSessions = /* @__PURE__ */ new Set();
1127
- let pendingSessions = /* @__PURE__ */ new Set();
1128
- let currentSessionId = null;
1129
- let watchers = /* @__PURE__ */ new Map();
1130
- let processedMessageKeys = /* @__PURE__ */ new Set();
1131
- let unmatchedServerMessageContents = /* @__PURE__ */ new Set();
1132
- const sync = new InvalidateSync(async () => {
1133
- logger.debug(`[SESSION_SCANNER] Syncing...`);
1134
- let sessions = [];
1135
- for (let p of pendingSessions) {
1136
- sessions.push(p);
1137
- }
1138
- if (currentSessionId) {
1139
- sessions.push(currentSessionId);
1607
+ break;
1140
1608
  }
1141
- let processSessionFile = async (sessionId) => {
1142
- const expectedSessionFile = join(projectDir, `${sessionId}.jsonl`);
1143
- let file;
1144
- try {
1145
- file = await readFile(expectedSessionFile, "utf-8");
1146
- } catch (error) {
1147
- logger.debug(`[SESSION_SCANNER] Session file not found: ${expectedSessionFile}`);
1148
- return;
1149
- }
1150
- let lines = file.split("\n");
1151
- for (let l of lines) {
1152
- try {
1153
- if (l.trim() === "") {
1154
- continue;
1155
- }
1156
- let message = JSON.parse(l);
1157
- let parsed = RawJSONLinesSchema.safeParse(message);
1158
- if (!parsed.success) {
1159
- logger.debugLargeJson(`[SESSION_SCANNER] Failed to parse message`, message);
1160
- continue;
1161
- }
1162
- let key = getMessageKey(parsed.data);
1163
- if (processedMessageKeys.has(key)) {
1164
- continue;
1609
+ case "user": {
1610
+ const userMsg = message;
1611
+ if (userMsg.message && typeof userMsg.message === "object" && "content" in userMsg.message) {
1612
+ const content = userMsg.message.content;
1613
+ if (typeof content === "string") {
1614
+ messageBuffer.addMessage(`\u{1F464} User: ${content}`, "user");
1615
+ } else if (Array.isArray(content)) {
1616
+ for (const block of content) {
1617
+ if (block.type === "text") {
1618
+ messageBuffer.addMessage(`\u{1F464} User: ${block.text}`, "user");
1619
+ } else if (block.type === "tool_result") {
1620
+ messageBuffer.addMessage(`\u2705 Tool Result (ID: ${block.tool_use_id})`, "result");
1621
+ if (block.content) {
1622
+ const outputStr = typeof block.content === "string" ? block.content : JSON.stringify(block.content, null, 2);
1623
+ const maxLength = 200;
1624
+ if (outputStr.length > maxLength) {
1625
+ messageBuffer.addMessage(outputStr.substring(0, maxLength) + "... (truncated)", "result");
1626
+ } else {
1627
+ messageBuffer.addMessage(outputStr, "result");
1628
+ }
1629
+ }
1630
+ }
1165
1631
  }
1166
- processedMessageKeys.add(key);
1167
- logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
1168
- logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
1169
- if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
1170
- if (unmatchedServerMessageContents.has(parsed.data.message.content)) {
1171
- logger.debug(`[SESSION_SCANNER] Matched server message echo: ${parsed.data.uuid}`);
1172
- unmatchedServerMessageContents.delete(parsed.data.message.content);
1173
- continue;
1632
+ } else {
1633
+ messageBuffer.addMessage(`\u{1F464} User: ${JSON.stringify(content, null, 2)}`, "user");
1634
+ }
1635
+ }
1636
+ break;
1637
+ }
1638
+ case "assistant": {
1639
+ const assistantMsg = message;
1640
+ if (assistantMsg.message && assistantMsg.message.content) {
1641
+ messageBuffer.addMessage("\u{1F916} Assistant:", "assistant");
1642
+ for (const block of assistantMsg.message.content) {
1643
+ if (block.type === "text") {
1644
+ messageBuffer.addMessage(block.text || "", "assistant");
1645
+ } else if (block.type === "tool_use") {
1646
+ messageBuffer.addMessage(`\u{1F527} Tool: ${block.name}`, "tool");
1647
+ if (block.input) {
1648
+ const inputStr = JSON.stringify(block.input, null, 2);
1649
+ const maxLength = 500;
1650
+ if (inputStr.length > maxLength) {
1651
+ messageBuffer.addMessage(`Input: ${inputStr.substring(0, maxLength)}... (truncated)`, "tool");
1652
+ } else {
1653
+ messageBuffer.addMessage(`Input: ${inputStr}`, "tool");
1654
+ }
1174
1655
  }
1175
1656
  }
1176
- const hackedMessage = hackToolResponse(message);
1177
- opts.onMessage(hackedMessage);
1178
- } catch (e) {
1179
- logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
1180
- continue;
1181
1657
  }
1182
1658
  }
1183
- };
1184
- for (let session of sessions) {
1185
- await processSessionFile(session);
1659
+ break;
1186
1660
  }
1187
- for (let p of sessions) {
1188
- if (pendingSessions.has(p)) {
1189
- pendingSessions.delete(p);
1190
- finishedSessions.add(p);
1661
+ case "result": {
1662
+ const resultMsg = message;
1663
+ if (resultMsg.subtype === "success") {
1664
+ if ("result" in resultMsg && resultMsg.result) {
1665
+ messageBuffer.addMessage("\u2728 Summary:", "result");
1666
+ messageBuffer.addMessage(resultMsg.result || "", "result");
1667
+ }
1668
+ if (resultMsg.usage) {
1669
+ messageBuffer.addMessage("\u{1F4CA} Session Stats:", "status");
1670
+ messageBuffer.addMessage(` \u2022 Turns: ${resultMsg.num_turns}`, "status");
1671
+ messageBuffer.addMessage(` \u2022 Input tokens: ${resultMsg.usage.input_tokens}`, "status");
1672
+ messageBuffer.addMessage(` \u2022 Output tokens: ${resultMsg.usage.output_tokens}`, "status");
1673
+ if (resultMsg.usage.cache_read_input_tokens) {
1674
+ messageBuffer.addMessage(` \u2022 Cache read tokens: ${resultMsg.usage.cache_read_input_tokens}`, "status");
1675
+ }
1676
+ if (resultMsg.usage.cache_creation_input_tokens) {
1677
+ messageBuffer.addMessage(` \u2022 Cache creation tokens: ${resultMsg.usage.cache_creation_input_tokens}`, "status");
1678
+ }
1679
+ messageBuffer.addMessage(` \u2022 Cost: $${resultMsg.total_cost_usd.toFixed(4)}`, "status");
1680
+ messageBuffer.addMessage(` \u2022 Duration: ${resultMsg.duration_ms}ms`, "status");
1681
+ }
1682
+ } else if (resultMsg.subtype === "error_max_turns") {
1683
+ messageBuffer.addMessage("\u274C Error: Maximum turns reached", "result");
1684
+ messageBuffer.addMessage(`Completed ${resultMsg.num_turns} turns`, "status");
1685
+ } else if (resultMsg.subtype === "error_during_execution") {
1686
+ messageBuffer.addMessage("\u274C Error during execution", "result");
1687
+ messageBuffer.addMessage(`Completed ${resultMsg.num_turns} turns before error`, "status");
1688
+ logger.debugLargeJson("[RESULT] Error during execution", resultMsg);
1191
1689
  }
1690
+ break;
1192
1691
  }
1193
- for (let p of sessions) {
1194
- if (!watchers.has(p)) {
1195
- watchers.set(p, startFileWatcher(join(projectDir, `${p}.jsonl`), () => {
1196
- sync.invalidate();
1197
- }));
1692
+ default: {
1693
+ if (process.env.DEBUG) {
1694
+ messageBuffer.addMessage(`[Unknown message type: ${message.type}]`, "status");
1198
1695
  }
1199
1696
  }
1200
- });
1201
- sync.invalidate();
1202
- const intervalId = setInterval(() => {
1203
- sync.invalidate();
1204
- }, 3e3);
1205
- return {
1206
- cleanup: () => {
1207
- clearInterval(intervalId);
1208
- for (let w of watchers.values()) {
1209
- w();
1697
+ }
1698
+ }
1699
+
1700
+ function getGitBranch(cwd) {
1701
+ try {
1702
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
1703
+ cwd,
1704
+ encoding: "utf8",
1705
+ stdio: ["ignore", "pipe", "ignore"]
1706
+ }).trim();
1707
+ return branch || void 0;
1708
+ } catch {
1709
+ return void 0;
1710
+ }
1711
+ }
1712
+ class SDKToLogConverter {
1713
+ lastUuid = null;
1714
+ context;
1715
+ responses;
1716
+ sidechainLastUUID = /* @__PURE__ */ new Map();
1717
+ constructor(context, responses) {
1718
+ this.context = {
1719
+ ...context,
1720
+ gitBranch: context.gitBranch ?? getGitBranch(context.cwd),
1721
+ version: context.version ?? process.env.npm_package_version ?? "0.0.0",
1722
+ parentUuid: null
1723
+ };
1724
+ this.responses = responses;
1725
+ }
1726
+ /**
1727
+ * Update session ID (for when session changes during resume)
1728
+ */
1729
+ updateSessionId(sessionId) {
1730
+ this.context.sessionId = sessionId;
1731
+ }
1732
+ /**
1733
+ * Reset parent chain (useful when starting new conversation)
1734
+ */
1735
+ resetParentChain() {
1736
+ this.lastUuid = null;
1737
+ this.context.parentUuid = null;
1738
+ }
1739
+ /**
1740
+ * Convert SDK message to log format
1741
+ */
1742
+ convert(sdkMessage) {
1743
+ const uuid = randomUUID();
1744
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1745
+ let parentUuid = this.lastUuid;
1746
+ let isSidechain = false;
1747
+ if (sdkMessage.parent_tool_use_id) {
1748
+ isSidechain = true;
1749
+ parentUuid = this.sidechainLastUUID.get(sdkMessage.parent_tool_use_id) ?? null;
1750
+ this.sidechainLastUUID.set(sdkMessage.parent_tool_use_id, uuid);
1751
+ }
1752
+ const baseFields = {
1753
+ parentUuid,
1754
+ isSidechain,
1755
+ userType: "external",
1756
+ cwd: this.context.cwd,
1757
+ sessionId: this.context.sessionId,
1758
+ version: this.context.version,
1759
+ gitBranch: this.context.gitBranch,
1760
+ uuid,
1761
+ timestamp
1762
+ };
1763
+ let logMessage = null;
1764
+ switch (sdkMessage.type) {
1765
+ case "user": {
1766
+ const userMsg = sdkMessage;
1767
+ logMessage = {
1768
+ ...baseFields,
1769
+ type: "user",
1770
+ message: userMsg.message
1771
+ };
1772
+ if (Array.isArray(userMsg.message.content)) {
1773
+ for (const content of userMsg.message.content) {
1774
+ if (content.type === "tool_result" && content.tool_use_id && this.responses?.has(content.tool_use_id)) {
1775
+ const response = this.responses.get(content.tool_use_id);
1776
+ if (response?.mode) {
1777
+ logMessage.mode = response.mode;
1778
+ }
1779
+ }
1780
+ }
1781
+ } else if (typeof userMsg.message.content === "string") ;
1782
+ break;
1210
1783
  }
1211
- watchers.clear();
1212
- },
1213
- onNewSession: (sessionId) => {
1214
- if (currentSessionId === sessionId) {
1215
- logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
1216
- return;
1784
+ case "assistant": {
1785
+ const assistantMsg = sdkMessage;
1786
+ logMessage = {
1787
+ ...baseFields,
1788
+ type: "assistant",
1789
+ message: assistantMsg.message,
1790
+ // Assistant messages often have additional fields
1791
+ requestId: assistantMsg.requestId
1792
+ };
1793
+ break;
1217
1794
  }
1218
- if (finishedSessions.has(sessionId)) {
1219
- logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
1220
- return;
1795
+ case "system": {
1796
+ const systemMsg = sdkMessage;
1797
+ if (systemMsg.subtype === "init" && systemMsg.session_id) {
1798
+ this.updateSessionId(systemMsg.session_id);
1799
+ }
1800
+ logMessage = {
1801
+ ...baseFields,
1802
+ type: "system",
1803
+ subtype: systemMsg.subtype,
1804
+ model: systemMsg.model,
1805
+ tools: systemMsg.tools,
1806
+ // Include all other fields
1807
+ ...systemMsg
1808
+ };
1809
+ break;
1221
1810
  }
1222
- if (pendingSessions.has(sessionId)) {
1223
- logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
1224
- return;
1811
+ case "result": {
1812
+ break;
1225
1813
  }
1226
- if (currentSessionId) {
1227
- pendingSessions.add(currentSessionId);
1814
+ // Handle tool use results (often comes as user messages)
1815
+ case "tool_result": {
1816
+ const toolMsg = sdkMessage;
1817
+ const baseLogMessage = {
1818
+ ...baseFields,
1819
+ type: "user",
1820
+ message: {
1821
+ role: "user",
1822
+ content: [{
1823
+ type: "tool_result",
1824
+ tool_use_id: toolMsg.tool_use_id,
1825
+ content: toolMsg.content
1826
+ }]
1827
+ },
1828
+ toolUseResult: toolMsg.content
1829
+ };
1830
+ if (toolMsg.tool_use_id && this.responses?.has(toolMsg.tool_use_id)) {
1831
+ const response = this.responses.get(toolMsg.tool_use_id);
1832
+ if (response?.mode) {
1833
+ baseLogMessage.mode = response.mode;
1834
+ }
1835
+ }
1836
+ logMessage = baseLogMessage;
1837
+ break;
1228
1838
  }
1229
- logger.debug(`[SESSION_SCANNER] New session: ${sessionId}`);
1230
- currentSessionId = sessionId;
1231
- sync.invalidate();
1232
- },
1233
- onRemoteUserMessageForDeduplication: (messageContent) => {
1234
- logger.debug(`[SESSION_SCANNER] Adding unmatched server message content: ${messageContent.substring(0, 50)}...`);
1235
- unmatchedServerMessageContents.add(messageContent);
1839
+ default:
1840
+ logMessage = {
1841
+ ...baseFields,
1842
+ ...sdkMessage,
1843
+ type: sdkMessage.type
1844
+ // Override type last to ensure it's set
1845
+ };
1236
1846
  }
1237
- };
1238
- }
1239
- function getMessageKey(message) {
1240
- if (message.type === "user") {
1241
- if (Array.isArray(message.message.content) && message.message.content.length > 0 && typeof message.message.content[0] === "object" && "text" in message.message.content[0]) {
1242
- return `user-message-content:${stableStringify(message.message.content[0].text)}`;
1243
- } else {
1244
- return `user-message-content:${stableStringify(message.message.content)}`;
1847
+ if (logMessage && logMessage.type !== "summary") {
1848
+ this.lastUuid = uuid;
1245
1849
  }
1246
- } else if (message.type === "assistant") {
1247
- const { usage, ...messageWithoutUsage } = message.message;
1248
- return stableStringify(messageWithoutUsage);
1249
- } else if (message.type === "summary") {
1250
- return `summary:${message.leafUuid}`;
1251
- } else if (message.type === "system") {
1252
- return `system:${message.uuid}`;
1850
+ return logMessage;
1253
1851
  }
1254
- return `unknown:<error, this should be unreachable>`;
1255
- }
1256
- function stableStringify(obj) {
1257
- if (!obj) {
1258
- return "null";
1852
+ /**
1853
+ * Convert multiple SDK messages to log format
1854
+ */
1855
+ convertMany(sdkMessages) {
1856
+ return sdkMessages.map((msg) => this.convert(msg)).filter((msg) => msg !== null);
1259
1857
  }
1260
- return JSON.stringify(sortKeys(obj), null, 2);
1261
- }
1262
- function sortKeys(value) {
1263
- if (Array.isArray(value)) {
1264
- return value.map(sortKeys);
1265
- } else if (value && typeof value === "object" && value.constructor === Object) {
1266
- return Object.keys(value).sort().reduce((result, key) => {
1267
- result[key] = sortKeys(value[key]);
1268
- return result;
1269
- }, {});
1270
- } else {
1271
- return value;
1858
+ /**
1859
+ * Convert a simple string content to a sidechain user message
1860
+ * Used for Task tool sub-agent prompts
1861
+ */
1862
+ convertSidechainUserMessage(toolUseId, content) {
1863
+ const uuid = randomUUID();
1864
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1865
+ this.sidechainLastUUID.set(toolUseId, uuid);
1866
+ return {
1867
+ parentUuid: null,
1868
+ isSidechain: true,
1869
+ userType: "external",
1870
+ cwd: this.context.cwd,
1871
+ sessionId: this.context.sessionId,
1872
+ version: this.context.version,
1873
+ gitBranch: this.context.gitBranch,
1874
+ type: "user",
1875
+ message: {
1876
+ role: "user",
1877
+ content
1878
+ },
1879
+ uuid,
1880
+ timestamp
1881
+ };
1882
+ }
1883
+ /**
1884
+ * Generate an interrupted tool result message
1885
+ * Used when a tool call is interrupted by the user
1886
+ * @param toolUseId - The ID of the tool that was interrupted
1887
+ * @param parentToolUseId - Optional parent tool ID if this is a sidechain tool
1888
+ */
1889
+ generateInterruptedToolResult(toolUseId, parentToolUseId) {
1890
+ const uuid = randomUUID();
1891
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1892
+ const errorMessage = "[Request interrupted by user for tool use]";
1893
+ let isSidechain = false;
1894
+ let parentUuid = this.lastUuid;
1895
+ if (parentToolUseId) {
1896
+ isSidechain = true;
1897
+ parentUuid = this.sidechainLastUUID.get(parentToolUseId) ?? null;
1898
+ this.sidechainLastUUID.set(parentToolUseId, uuid);
1899
+ }
1900
+ const logMessage = {
1901
+ type: "user",
1902
+ isSidechain,
1903
+ uuid,
1904
+ message: {
1905
+ role: "user",
1906
+ content: [
1907
+ {
1908
+ type: "tool_result",
1909
+ content: errorMessage,
1910
+ is_error: true,
1911
+ tool_use_id: toolUseId
1912
+ }
1913
+ ]
1914
+ },
1915
+ parentUuid,
1916
+ userType: "external",
1917
+ cwd: this.context.cwd,
1918
+ sessionId: this.context.sessionId,
1919
+ version: this.context.version,
1920
+ gitBranch: this.context.gitBranch,
1921
+ timestamp,
1922
+ toolUseResult: `Error: ${errorMessage}`
1923
+ };
1924
+ this.lastUuid = uuid;
1925
+ return logMessage;
1272
1926
  }
1273
1927
  }
1274
1928
 
1275
- async function loop(opts) {
1276
- let mode = opts.startingMode ?? "local";
1277
- let currentPermissionMode = opts.permissionMode ?? "default";
1278
- logger.debug(`[loop] Starting with permission mode: ${currentPermissionMode}`);
1279
- let currentMessageQueue = opts.messageQueue || new MessageQueue2(
1280
- (mode2) => mode2
1281
- // Simple string hasher since modes are already strings
1282
- );
1283
- let sessionId = null;
1284
- let onMessage = null;
1285
- const sessionScanner = opts.sessionScanner || createSessionScanner({
1286
- workingDirectory: opts.path,
1287
- onMessage: (message) => {
1288
- opts.session.sendClaudeSessionMessage(message);
1929
+ async function claudeRemoteLauncher(session) {
1930
+ let messageBuffer = new MessageBuffer();
1931
+ console.clear();
1932
+ let inkInstance = render(React.createElement(RemoteModeDisplay, {
1933
+ messageBuffer,
1934
+ logPath: process.env.DEBUG ? session.logPath : void 0,
1935
+ onExit: async () => {
1936
+ logger.debug("[remote]: Exiting client via Ctrl-C");
1937
+ if (!exitReason) {
1938
+ exitReason = "exit";
1939
+ }
1940
+ await abort();
1941
+ },
1942
+ onSwitchToLocal: () => {
1943
+ logger.debug("[remote]: Switching to local mode via double space");
1944
+ doSwitch();
1289
1945
  }
1946
+ }), {
1947
+ exitOnCtrlC: false,
1948
+ patchConsole: false
1290
1949
  });
1291
- opts.session.onUserMessage((message) => {
1292
- sessionScanner.onRemoteUserMessageForDeduplication(message.content.text);
1293
- let messagePermissionMode = currentPermissionMode;
1294
- if (message.meta?.permissionMode) {
1295
- const validModes = ["default", "acceptEdits", "bypassPermissions", "plan"];
1296
- if (validModes.includes(message.meta.permissionMode)) {
1297
- messagePermissionMode = message.meta.permissionMode;
1298
- currentPermissionMode = messagePermissionMode;
1299
- logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
1300
- } else {
1301
- logger.info(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
1950
+ process.stdin.resume();
1951
+ if (process.stdin.isTTY) {
1952
+ process.stdin.setRawMode(true);
1953
+ }
1954
+ process.stdin.setEncoding("utf8");
1955
+ const scanner = await createSessionScanner({
1956
+ sessionId: session.sessionId,
1957
+ workingDirectory: session.path,
1958
+ onMessage: (message) => {
1959
+ if (message.type === "summary") {
1960
+ session.client.sendClaudeSessionMessage(message);
1302
1961
  }
1303
- } else {
1304
- logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
1305
- }
1306
- currentMessageQueue.push(message.content.text, messagePermissionMode);
1307
- logger.debugLargeJson("User message pushed to queue:", message);
1308
- if (onMessage) {
1309
- onMessage();
1310
1962
  }
1311
1963
  });
1312
- let onSessionFound = (newSessionId) => {
1313
- sessionId = newSessionId;
1314
- sessionScanner.onNewSession(newSessionId);
1315
- };
1316
- while (true) {
1317
- logger.debug(`[loop] Starting loop iteration, queue size: ${currentMessageQueue.size()}, mode: ${mode}`);
1318
- if (currentMessageQueue.size() > 0) {
1319
- if (mode !== "remote") {
1320
- mode = "remote";
1321
- if (opts.onModeChange) {
1322
- opts.onModeChange(mode);
1964
+ let exitReason = null;
1965
+ let abortController = null;
1966
+ let abortFuture = null;
1967
+ async function abort() {
1968
+ if (abortController && !abortController.signal.aborted) {
1969
+ abortController.abort();
1970
+ }
1971
+ await abortFuture?.promise;
1972
+ }
1973
+ async function doAbort() {
1974
+ logger.debug("[remote]: doAbort");
1975
+ await abort();
1976
+ }
1977
+ async function doSwitch() {
1978
+ logger.debug("[remote]: doSwitch");
1979
+ if (!exitReason) {
1980
+ exitReason = "switch";
1981
+ }
1982
+ await abort();
1983
+ }
1984
+ session.client.setHandler("abort", doAbort);
1985
+ session.client.setHandler("switch", doSwitch);
1986
+ const permissions = await startPermissionResolver(session);
1987
+ const sdkToLogConverter = new SDKToLogConverter({
1988
+ sessionId: session.sessionId || "unknown",
1989
+ cwd: session.path,
1990
+ version: process.env.npm_package_version
1991
+ }, permissions.responses);
1992
+ let planModeToolCalls = /* @__PURE__ */ new Set();
1993
+ let ongoingToolCalls = /* @__PURE__ */ new Map();
1994
+ function onMessage(message) {
1995
+ formatClaudeMessageForInk(message, messageBuffer);
1996
+ permissions.onMessage(message);
1997
+ if (message.type === "assistant") {
1998
+ let umessage = message;
1999
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2000
+ for (let c of umessage.message.content) {
2001
+ if (c.type === "tool_use" && (c.name === "exit_plan_mode" || c.name === "ExitPlanMode")) {
2002
+ logger.debug("[remote]: detected plan mode tool call " + c.id);
2003
+ planModeToolCalls.add(c.id);
2004
+ }
1323
2005
  }
1324
2006
  }
1325
2007
  }
1326
- if (mode === "local") {
1327
- let abortedOutside = false;
1328
- const interactiveAbortController = new AbortController();
1329
- opts.session.setHandler("switch", () => {
1330
- if (!interactiveAbortController.signal.aborted) {
1331
- abortedOutside = true;
1332
- if (mode !== "remote") {
1333
- mode = "remote";
1334
- if (opts.onModeChange) {
1335
- opts.onModeChange(mode);
1336
- }
2008
+ if (message.type === "assistant") {
2009
+ let umessage = message;
2010
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2011
+ for (let c of umessage.message.content) {
2012
+ if (c.type === "tool_use") {
2013
+ logger.debug("[remote]: detected tool use " + c.id + " parent: " + umessage.parent_tool_use_id);
2014
+ ongoingToolCalls.set(c.id, { parentToolCallId: umessage.parent_tool_use_id ?? null });
1337
2015
  }
1338
- interactiveAbortController.abort();
1339
- }
1340
- });
1341
- opts.session.setHandler("abort", () => {
1342
- if (onMessage) {
1343
- onMessage();
1344
2016
  }
1345
- });
1346
- onMessage = () => {
1347
- if (!interactiveAbortController.signal.aborted) {
1348
- abortedOutside = true;
1349
- if (mode !== "remote") {
1350
- mode = "remote";
1351
- if (opts.onModeChange) {
1352
- opts.onModeChange(mode);
1353
- }
2017
+ }
2018
+ }
2019
+ if (message.type === "user") {
2020
+ let umessage = message;
2021
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2022
+ for (let c of umessage.message.content) {
2023
+ if (c.type === "tool_result" && c.tool_use_id) {
2024
+ ongoingToolCalls.delete(c.tool_use_id);
1354
2025
  }
1355
- opts.session.sendSessionEvent({ type: "message", message: "Inference aborted" });
1356
- interactiveAbortController.abort();
1357
- }
1358
- onMessage = null;
1359
- };
1360
- try {
1361
- if (opts.onProcessStart) {
1362
- opts.onProcessStart("local");
1363
- }
1364
- await claudeLocal({
1365
- path: opts.path,
1366
- sessionId,
1367
- onSessionFound,
1368
- onThinkingChange: opts.onThinkingChange,
1369
- abort: interactiveAbortController.signal,
1370
- claudeEnvVars: opts.claudeEnvVars,
1371
- claudeArgs: opts.claudeArgs
1372
- });
1373
- } catch (e) {
1374
- if (!interactiveAbortController.signal.aborted) {
1375
- opts.session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
1376
- }
1377
- } finally {
1378
- if (opts.onProcessStop) {
1379
- opts.onProcessStop("local");
1380
2026
  }
1381
2027
  }
1382
- onMessage = null;
1383
- if (!abortedOutside) {
1384
- return;
2028
+ }
2029
+ let msg = message;
2030
+ if (message.type === "user") {
2031
+ let umessage = message;
2032
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2033
+ msg = {
2034
+ ...umessage,
2035
+ message: {
2036
+ ...umessage.message,
2037
+ content: umessage.message.content.map((c) => {
2038
+ if (c.type === "tool_result" && c.tool_use_id && planModeToolCalls.has(c.tool_use_id)) {
2039
+ if (c.content === PLAN_FAKE_REJECT) {
2040
+ logger.debug("[remote]: hack plan mode exit");
2041
+ logger.debugLargeJson("[remote]: hack plan mode exit", c);
2042
+ return {
2043
+ ...c,
2044
+ is_error: false,
2045
+ content: "Plan approved",
2046
+ mode: c.mode
2047
+ };
2048
+ } else {
2049
+ return c;
2050
+ }
2051
+ }
2052
+ return c;
2053
+ })
2054
+ }
2055
+ };
1385
2056
  }
1386
- if (mode !== "local") {
1387
- console.log("Switching to remote mode...");
2057
+ }
2058
+ const logMessage = sdkToLogConverter.convert(msg);
2059
+ if (logMessage) {
2060
+ if (logMessage.type !== "system") {
2061
+ session.client.sendClaudeSessionMessage(logMessage);
1388
2062
  }
1389
2063
  }
1390
- if (mode === "remote") {
1391
- console.log("Starting remote mode...");
1392
- logger.debug("Starting " + sessionId);
1393
- const remoteAbortController = new AbortController();
1394
- opts.session.setHandler("abort", () => {
1395
- if (remoteAbortController && !remoteAbortController.signal.aborted) {
1396
- remoteAbortController.abort();
1397
- }
1398
- });
1399
- const abortHandler = () => {
1400
- if (remoteAbortController && !remoteAbortController.signal.aborted) {
1401
- if (mode !== "local") {
1402
- mode = "local";
1403
- if (opts.onModeChange) {
1404
- opts.onModeChange(mode);
2064
+ if (message.type === "assistant") {
2065
+ let umessage = message;
2066
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2067
+ for (let c of umessage.message.content) {
2068
+ if (c.type === "tool_use" && c.name === "Task" && c.input && typeof c.input.prompt === "string") {
2069
+ const logMessage2 = sdkToLogConverter.convertSidechainUserMessage(c.id, c.input.prompt);
2070
+ if (logMessage2) {
2071
+ session.client.sendClaudeSessionMessage(logMessage2);
1405
2072
  }
1406
2073
  }
1407
- opts.session.sendSessionEvent({ type: "message", message: "Inference aborted" });
1408
- remoteAbortController.abort();
1409
- }
1410
- if (process.stdin.isTTY) {
1411
- process.stdin.setRawMode(false);
1412
2074
  }
1413
- };
1414
- process.stdin.resume();
1415
- if (process.stdin.isTTY) {
1416
- process.stdin.setRawMode(true);
1417
2075
  }
1418
- process.stdin.setEncoding("utf8");
1419
- process.stdin.on("data", abortHandler);
1420
- try {
1421
- logger.debug(`Starting claudeRemote with messages: ${currentMessageQueue.size()}`);
1422
- logger.debug("[loop] Waiting for messages before starting claudeRemote...");
1423
- const messageData = await currentMessageQueue.waitForMessagesAndGetAsString(remoteAbortController.signal);
1424
- if (!messageData) {
1425
- console.log("[LOOP] No message received (queue closed or aborted), continuing loop");
1426
- logger.debug("[loop] No message received (queue closed or aborted), skipping remote mode");
2076
+ }
2077
+ }
2078
+ try {
2079
+ while (!exitReason) {
2080
+ logger.debug("[remote]: fetch next message");
2081
+ abortController = new AbortController();
2082
+ abortFuture = new Future();
2083
+ const messageData = await session.queue.waitForMessagesAndGetAsString(abortController.signal);
2084
+ if (!messageData || abortController.signal.aborted) {
2085
+ logger.debug("[remote]: fetch next message done: no message or aborted");
2086
+ abortFuture?.resolve(void 0);
2087
+ if (exitReason) {
2088
+ return exitReason;
2089
+ } else {
1427
2090
  continue;
1428
2091
  }
1429
- currentPermissionMode = messageData.mode;
1430
- logger.debug(`[loop] Using permission mode from queue: ${currentPermissionMode}`);
1431
- if (opts.onProcessStart) {
1432
- opts.onProcessStart("remote");
1433
- }
1434
- opts.session.sendSessionEvent({ type: "permission-mode-changed", mode: currentPermissionMode });
1435
- logger.debug(`[loop] Sent permission-mode-changed event to app: ${currentPermissionMode}`);
2092
+ }
2093
+ logger.debug("[remote]: fetch next message done: message received");
2094
+ abortFuture?.resolve(void 0);
2095
+ abortFuture = null;
2096
+ abortController = null;
2097
+ logger.debug("[remote]: launch");
2098
+ messageBuffer.addMessage("\u2550".repeat(40), "status");
2099
+ messageBuffer.addMessage("Starting new Claude session...", "status");
2100
+ abortController = new AbortController();
2101
+ abortFuture = new Future();
2102
+ permissions.reset();
2103
+ sdkToLogConverter.resetParentChain();
2104
+ try {
1436
2105
  await claudeRemote({
1437
- abort: remoteAbortController.signal,
1438
- sessionId,
1439
- path: opts.path,
1440
- mcpServers: opts.mcpServers,
1441
- permissionPromptToolName: opts.permissionPromptToolName,
1442
- permissionMode: currentPermissionMode,
1443
- onSessionFound,
1444
- onThinkingChange: opts.onThinkingChange,
2106
+ sessionId: session.sessionId,
2107
+ path: session.path,
2108
+ responses: permissions.responses,
2109
+ mcpServers: {
2110
+ ...session.mcpServers,
2111
+ permission: {
2112
+ type: "http",
2113
+ url: permissions.server.url
2114
+ }
2115
+ },
2116
+ permissionPromptToolName: "mcp__permission__" + permissions.server.toolName,
2117
+ permissionMode: messageData.mode,
2118
+ onSessionFound: (sessionId) => {
2119
+ sdkToLogConverter.updateSessionId(sessionId);
2120
+ session.onSessionFound(sessionId);
2121
+ scanner.onNewSession(sessionId);
2122
+ },
2123
+ onThinkingChange: session.onThinkingChange,
1445
2124
  message: messageData.message,
1446
- onAssistantResult: opts.onAssistantResult,
1447
- interruptController: opts.interruptController,
1448
- claudeEnvVars: opts.claudeEnvVars,
1449
- claudeArgs: opts.claudeArgs,
1450
- onToolCallResolver: opts.onToolCallResolver
2125
+ claudeEnvVars: session.claudeEnvVars,
2126
+ claudeArgs: session.claudeArgs,
2127
+ onMessage,
2128
+ signal: abortController.signal
1451
2129
  });
2130
+ if (!exitReason && abortController.signal.aborted) {
2131
+ session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
2132
+ }
1452
2133
  } catch (e) {
1453
- if (!remoteAbortController.signal.aborted) {
1454
- opts.session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
2134
+ if (!exitReason) {
2135
+ session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
2136
+ continue;
1455
2137
  }
1456
2138
  } finally {
1457
- if (opts.onProcessStop) {
1458
- opts.onProcessStop("remote");
1459
- }
1460
- process.stdin.off("data", abortHandler);
1461
- if (process.stdin.isTTY) {
1462
- process.stdin.setRawMode(false);
1463
- }
1464
- }
1465
- if (mode !== "remote") {
1466
- console.log("Switching back to good old claude...");
1467
- }
1468
- }
1469
- }
1470
- }
1471
-
1472
- async function startPermissionServerV2(handler) {
1473
- const mcp = new McpServer({
1474
- name: "Permission Server",
1475
- version: "1.0.0",
1476
- description: "A server that allows you to request permissions from the user"
1477
- });
1478
- mcp.registerTool("ask_permission", {
1479
- description: "Request permission to execute a tool",
1480
- title: "Request Permission",
1481
- inputSchema: {
1482
- tool_name: z$1.string().describe("The tool that needs permission"),
1483
- input: z$1.any().describe("The arguments for the tool")
1484
- }
1485
- // outputSchema: {
1486
- // approved: z.boolean().describe('Whether the tool was approved'),
1487
- // reason: z.string().describe('The reason for the approval or denial'),
1488
- // },
1489
- }, async (args) => {
1490
- const response = await handler({ name: args.tool_name, arguments: args.input });
1491
- const result = response.approved ? { behavior: "allow", updatedInput: args.input || {} } : { behavior: "deny", message: response.reason || "Permission denied by user" };
1492
- return {
1493
- content: [
1494
- {
1495
- type: "text",
1496
- text: JSON.stringify(result)
2139
+ for (let [toolCallId, { parentToolCallId }] of ongoingToolCalls) {
2140
+ const converted = sdkToLogConverter.generateInterruptedToolResult(toolCallId, parentToolCallId);
2141
+ if (converted) {
2142
+ logger.debug("[remote]: terminating tool call " + toolCallId + " parent: " + parentToolCallId);
2143
+ session.client.sendClaudeSessionMessage(converted);
2144
+ }
1497
2145
  }
1498
- ],
1499
- isError: false
1500
- };
1501
- });
1502
- const transport = new StreamableHTTPServerTransport({
1503
- // NOTE: Returning session id here will result in claude
1504
- // sdk spawn to fail with `Invalid Request: Server already initialized`
1505
- sessionIdGenerator: void 0
1506
- });
1507
- await mcp.connect(transport);
1508
- const server = createServer(async (req, res) => {
1509
- try {
1510
- await transport.handleRequest(req, res);
1511
- } catch (error) {
1512
- logger.debug("Error handling request:", error);
1513
- if (!res.headersSent) {
1514
- res.writeHead(500).end();
2146
+ ongoingToolCalls.clear();
2147
+ abortController = null;
2148
+ abortFuture?.resolve(void 0);
2149
+ abortFuture = null;
2150
+ logger.debug("[remote]: launch done");
2151
+ permissions.reset();
1515
2152
  }
1516
2153
  }
1517
- });
1518
- const baseUrl = await new Promise((resolve) => {
1519
- server.listen(0, "127.0.0.1", () => {
1520
- const addr = server.address();
1521
- resolve(new URL(`http://127.0.0.1:${addr.port}`));
1522
- });
1523
- });
1524
- return {
1525
- url: baseUrl.toString(),
1526
- toolName: "ask_permission"
1527
- };
1528
- }
1529
-
1530
- class InterruptController {
1531
- interruptFn;
1532
- isInterrupting = false;
1533
- /**
1534
- * Register an interrupt function from claudeRemote
1535
- */
1536
- register(fn) {
1537
- this.interruptFn = fn;
1538
- }
1539
- /**
1540
- * Unregister the interrupt function (cleanup)
1541
- */
1542
- unregister() {
1543
- this.interruptFn = void 0;
1544
- this.isInterrupting = false;
1545
- }
1546
- /**
1547
- * Trigger the interrupt - can be called from anywhere
1548
- */
1549
- async interrupt() {
1550
- if (!this.interruptFn || this.isInterrupting) {
1551
- return false;
2154
+ } finally {
2155
+ permissions.server.stop();
2156
+ process.stdin.off("data", abort);
2157
+ if (process.stdin.isTTY) {
2158
+ process.stdin.setRawMode(false);
1552
2159
  }
1553
- this.isInterrupting = true;
1554
- try {
1555
- await this.interruptFn();
1556
- return true;
1557
- } catch (error) {
1558
- logger.debug("Failed to interrupt Claude:", error);
1559
- return false;
1560
- } finally {
1561
- this.isInterrupting = false;
2160
+ inkInstance.unmount();
2161
+ messageBuffer.clear();
2162
+ if (abortFuture) {
2163
+ abortFuture.resolve(void 0);
1562
2164
  }
2165
+ await scanner.cleanup();
1563
2166
  }
1564
- /**
1565
- * Check if interrupt is available
1566
- */
1567
- canInterrupt() {
1568
- return !!this.interruptFn && !this.isInterrupting;
2167
+ return exitReason || "exit";
2168
+ }
2169
+
2170
+ async function loop(opts) {
2171
+ const logPath = await logger.logFilePathPromise;
2172
+ let session = new Session({
2173
+ api: opts.api,
2174
+ client: opts.session,
2175
+ path: opts.path,
2176
+ sessionId: null,
2177
+ claudeEnvVars: opts.claudeEnvVars,
2178
+ claudeArgs: opts.claudeArgs,
2179
+ mcpServers: opts.mcpServers,
2180
+ logPath,
2181
+ messageQueue: opts.messageQueue,
2182
+ onModeChange: opts.onModeChange
2183
+ });
2184
+ let mode = opts.startingMode ?? "local";
2185
+ while (true) {
2186
+ logger.debug(`[loop] Iteration with mode: ${mode}`);
2187
+ if (mode === "local") {
2188
+ let reason = await claudeLocalLauncher(session);
2189
+ if (reason === "exit") {
2190
+ return;
2191
+ }
2192
+ mode = "remote";
2193
+ if (opts.onModeChange) {
2194
+ opts.onModeChange(mode);
2195
+ }
2196
+ continue;
2197
+ }
2198
+ if (mode === "remote") {
2199
+ let reason = await claudeRemoteLauncher(session);
2200
+ if (reason === "exit") {
2201
+ return;
2202
+ }
2203
+ mode = "local";
2204
+ if (opts.onModeChange) {
2205
+ opts.onModeChange(mode);
2206
+ }
2207
+ continue;
2208
+ }
1569
2209
  }
1570
2210
  }
1571
2211
 
1572
- var version = "0.2.2";
2212
+ var name = "happy-coder";
2213
+ var version = "0.2.3-beta.0";
2214
+ var description = "Claude Code session sharing CLI";
2215
+ var author = "Kirill Dubovitskiy";
2216
+ var license = "MIT";
2217
+ var type = "module";
2218
+ var homepage = "https://github.com/slopus/happy-cli";
2219
+ var bugs = "https://github.com/slopus/happy-cli/issues";
2220
+ var repository = "slopus/happy-cli";
2221
+ var bin = {
2222
+ happy: "./bin/happy"
2223
+ };
2224
+ var main = "./dist/index.cjs";
2225
+ var module = "./dist/index.mjs";
2226
+ var types = "./dist/index.d.cts";
2227
+ var exports = {
2228
+ ".": {
2229
+ require: {
2230
+ types: "./dist/index.d.cts",
2231
+ "default": "./dist/index.cjs"
2232
+ },
2233
+ "import": {
2234
+ types: "./dist/index.d.mts",
2235
+ "default": "./dist/index.mjs"
2236
+ }
2237
+ },
2238
+ "./lib": {
2239
+ require: {
2240
+ types: "./dist/lib.d.cts",
2241
+ "default": "./dist/lib.cjs"
2242
+ },
2243
+ "import": {
2244
+ types: "./dist/lib.d.mts",
2245
+ "default": "./dist/lib.mjs"
2246
+ }
2247
+ }
2248
+ };
2249
+ var files = [
2250
+ "dist",
2251
+ "bin",
2252
+ "scripts",
2253
+ "ripgrep",
2254
+ "package.json"
2255
+ ];
2256
+ var scripts = {
2257
+ test: "vitest run",
2258
+ "test:watch": "vitest",
2259
+ build: "tsc --noEmit && pkgroll",
2260
+ prepublishOnly: "yarn build && yarn test",
2261
+ typecheck: "tsc --noEmit",
2262
+ dev: "npx tsx --env-file .env.sample src/index.ts",
2263
+ "dev:local-server": "HANDY_SERVER_URL=http://localhost:3005 npx tsx --env-file .env.sample src/index.ts",
2264
+ prerelease: "npm version prerelease --preid=beta"
2265
+ };
2266
+ var dependencies = {
2267
+ "@anthropic-ai/claude-code": "^1.0.72",
2268
+ "@anthropic-ai/sdk": "^0.56.0",
2269
+ "@modelcontextprotocol/sdk": "^1.15.1",
2270
+ "@stablelib/base64": "^2.0.1",
2271
+ "@types/http-proxy": "^1.17.16",
2272
+ "@types/qrcode-terminal": "^0.12.2",
2273
+ "@types/react": "^19.1.9",
2274
+ axios: "^1.10.0",
2275
+ chalk: "^5.4.1",
2276
+ "expo-server-sdk": "^3.15.0",
2277
+ "http-proxy": "^1.18.1",
2278
+ "http-proxy-middleware": "^3.0.5",
2279
+ ink: "^6.1.0",
2280
+ "ink-box": "^2.0.0",
2281
+ "qrcode-terminal": "^0.12.0",
2282
+ react: "^19.1.1",
2283
+ "socket.io-client": "^4.8.1",
2284
+ tweetnacl: "^1.0.3",
2285
+ zod: "^3.23.8"
2286
+ };
2287
+ var devDependencies = {
2288
+ "@eslint/compat": "^1",
2289
+ "@types/node": ">=18",
2290
+ eslint: "^9",
2291
+ "eslint-config-prettier": "^10",
2292
+ pkgroll: "^2.14.2",
2293
+ shx: "^0.3.3",
2294
+ "ts-node": "^10",
2295
+ tsx: "^4.20.3",
2296
+ typescript: "^5",
2297
+ vitest: "^3.2.4"
2298
+ };
2299
+ var overrides = {
2300
+ "whatwg-url": "14.2.0"
2301
+ };
1573
2302
  var packageJson = {
1574
- version: version};
2303
+ name: name,
2304
+ version: version,
2305
+ description: description,
2306
+ author: author,
2307
+ license: license,
2308
+ type: type,
2309
+ homepage: homepage,
2310
+ bugs: bugs,
2311
+ repository: repository,
2312
+ bin: bin,
2313
+ main: main,
2314
+ module: module,
2315
+ types: types,
2316
+ exports: exports,
2317
+ files: files,
2318
+ scripts: scripts,
2319
+ dependencies: dependencies,
2320
+ devDependencies: devDependencies,
2321
+ overrides: overrides
2322
+ };
1575
2323
 
1576
2324
  const __dirname = dirname$1(fileURLToPath$1(import.meta.url));
1577
2325
  const RUNNER_PATH = join$1(__dirname, "..", "..", "scripts", "ripgrep_launcher.cjs");
@@ -1603,51 +2351,9 @@ function run(args, options) {
1603
2351
  }
1604
2352
 
1605
2353
  const execAsync = promisify(exec);
1606
- function registerHandlers(session, interruptController, permissionCallbacks, onSwitchRemoteRequested) {
1607
- session.setHandler("abort", async () => {
1608
- logger.info("Abort request - interrupting Claude");
1609
- await interruptController.interrupt();
1610
- });
1611
- if (permissionCallbacks) {
1612
- session.setHandler("permission", async (message) => {
1613
- logger.info("Permission response" + JSON.stringify(message));
1614
- const id = message.id;
1615
- const resolve = permissionCallbacks.requests.get(id);
1616
- if (resolve) {
1617
- if (!message.approved) {
1618
- logger.debug("Permission denied, interrupting Claude");
1619
- await interruptController.interrupt();
1620
- }
1621
- resolve({ approved: message.approved, reason: message.reason });
1622
- permissionCallbacks.requests.delete(id);
1623
- } else {
1624
- logger.info("Permission request stale, likely timed out");
1625
- return;
1626
- }
1627
- session.updateAgentState((currentState) => {
1628
- const request = currentState.requests?.[id];
1629
- if (!request) return currentState;
1630
- let r = { ...currentState.requests };
1631
- delete r[id];
1632
- const isExitPlanModeSuccess = request.tool === "exit_plan_mode" && !message.approved && message.reason === PLAN_FAKE_REJECT;
1633
- return {
1634
- ...currentState,
1635
- requests: r,
1636
- completedRequests: {
1637
- ...currentState.completedRequests,
1638
- [id]: {
1639
- ...request,
1640
- completedAt: Date.now(),
1641
- status: isExitPlanModeSuccess ? "approved" : message.approved ? "approved" : "denied",
1642
- reason: isExitPlanModeSuccess ? "Plan approved" : message.reason
1643
- }
1644
- }
1645
- };
1646
- });
1647
- });
1648
- }
2354
+ function registerHandlers(session) {
1649
2355
  session.setHandler("bash", async (data) => {
1650
- logger.info("Shell command request:", data.command);
2356
+ logger.debug("Shell command request:", data.command);
1651
2357
  try {
1652
2358
  const options = {
1653
2359
  cwd: data.cwd,
@@ -1682,7 +2388,7 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1682
2388
  }
1683
2389
  });
1684
2390
  session.setHandler("readFile", async (data) => {
1685
- logger.info("Read file request:", data.path);
2391
+ logger.debug("Read file request:", data.path);
1686
2392
  try {
1687
2393
  const buffer = await readFile$1(data.path);
1688
2394
  const content = buffer.toString("base64");
@@ -1693,7 +2399,7 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1693
2399
  }
1694
2400
  });
1695
2401
  session.setHandler("writeFile", async (data) => {
1696
- logger.info("Write file request:", data.path);
2402
+ logger.debug("Write file request:", data.path);
1697
2403
  try {
1698
2404
  if (data.expectedHash !== null && data.expectedHash !== void 0) {
1699
2405
  try {
@@ -1739,7 +2445,7 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1739
2445
  }
1740
2446
  });
1741
2447
  session.setHandler("listDirectory", async (data) => {
1742
- logger.info("List directory request:", data.path);
2448
+ logger.debug("List directory request:", data.path);
1743
2449
  try {
1744
2450
  const entries = await readdir(data.path, { withFileTypes: true });
1745
2451
  const directoryEntries = await Promise.all(
@@ -1780,7 +2486,7 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1780
2486
  }
1781
2487
  });
1782
2488
  session.setHandler("getDirectoryTree", async (data) => {
1783
- logger.info("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
2489
+ logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
1784
2490
  async function buildTree(path, name, currentDepth) {
1785
2491
  try {
1786
2492
  const stats = await stat(path);
@@ -1836,7 +2542,7 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1836
2542
  }
1837
2543
  });
1838
2544
  session.setHandler("ripgrep", async (data) => {
1839
- logger.info("Ripgrep request with args:", data.args, "cwd:", data.cwd);
2545
+ logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
1840
2546
  try {
1841
2547
  const result = await run(data.args, { cwd: data.cwd });
1842
2548
  return {
@@ -1855,53 +2561,306 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1855
2561
  });
1856
2562
  }
1857
2563
 
1858
- const defaultSettings = {
1859
- onboardingCompleted: false
1860
- };
1861
- async function readSettings() {
1862
- if (!existsSync(configuration.settingsFile)) {
1863
- return { ...defaultSettings };
2564
+ const defaultSettings = {
2565
+ onboardingCompleted: false
2566
+ };
2567
+ async function readSettings() {
2568
+ if (!existsSync(configuration.settingsFile)) {
2569
+ return { ...defaultSettings };
2570
+ }
2571
+ try {
2572
+ const content = await readFile(configuration.settingsFile, "utf8");
2573
+ return JSON.parse(content);
2574
+ } catch {
2575
+ return { ...defaultSettings };
2576
+ }
2577
+ }
2578
+ async function writeSettings(settings) {
2579
+ if (!existsSync(configuration.happyDir)) {
2580
+ await mkdir(configuration.happyDir, { recursive: true });
2581
+ }
2582
+ await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
2583
+ }
2584
+ const credentialsSchema = z.object({
2585
+ secret: z.string().base64(),
2586
+ token: z.string()
2587
+ });
2588
+ async function readCredentials() {
2589
+ if (!existsSync(configuration.privateKeyFile)) {
2590
+ return null;
2591
+ }
2592
+ try {
2593
+ const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
2594
+ const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
2595
+ return {
2596
+ secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
2597
+ token: credentials.token
2598
+ };
2599
+ } catch {
2600
+ return null;
2601
+ }
2602
+ }
2603
+ async function writeCredentials(credentials) {
2604
+ if (!existsSync(configuration.happyDir)) {
2605
+ await mkdir(configuration.happyDir, { recursive: true });
2606
+ }
2607
+ await writeFile$1(configuration.privateKeyFile, JSON.stringify({
2608
+ secret: encodeBase64(credentials.secret),
2609
+ token: credentials.token
2610
+ }, null, 2));
2611
+ }
2612
+
2613
+ class MessageQueue2 {
2614
+ constructor(modeHasher) {
2615
+ this.modeHasher = modeHasher;
2616
+ logger.debug(`[MessageQueue2] Initialized`);
2617
+ }
2618
+ queue = [];
2619
+ waiter = null;
2620
+ closed = false;
2621
+ onMessageHandler = null;
2622
+ /**
2623
+ * Set a handler that will be called when a message arrives
2624
+ */
2625
+ setOnMessage(handler) {
2626
+ this.onMessageHandler = handler;
2627
+ }
2628
+ /**
2629
+ * Push a message to the queue with a mode.
2630
+ */
2631
+ push(message, mode) {
2632
+ if (this.closed) {
2633
+ throw new Error("Cannot push to closed queue");
2634
+ }
2635
+ const modeHash = this.modeHasher(mode);
2636
+ logger.debug(`[MessageQueue2] push() called with mode hash: ${modeHash}`);
2637
+ this.queue.push({
2638
+ message,
2639
+ mode,
2640
+ modeHash
2641
+ });
2642
+ if (this.onMessageHandler) {
2643
+ this.onMessageHandler(message, mode);
2644
+ }
2645
+ if (this.waiter) {
2646
+ logger.debug(`[MessageQueue2] Notifying waiter`);
2647
+ const waiter = this.waiter;
2648
+ this.waiter = null;
2649
+ waiter(true);
2650
+ }
2651
+ logger.debug(`[MessageQueue2] push() completed. Queue size: ${this.queue.length}`);
2652
+ }
2653
+ /**
2654
+ * Push a message to the beginning of the queue with a mode.
2655
+ */
2656
+ unshift(message, mode) {
2657
+ if (this.closed) {
2658
+ throw new Error("Cannot unshift to closed queue");
2659
+ }
2660
+ const modeHash = this.modeHasher(mode);
2661
+ logger.debug(`[MessageQueue2] unshift() called with mode hash: ${modeHash}`);
2662
+ this.queue.unshift({
2663
+ message,
2664
+ mode,
2665
+ modeHash
2666
+ });
2667
+ if (this.onMessageHandler) {
2668
+ this.onMessageHandler(message, mode);
2669
+ }
2670
+ if (this.waiter) {
2671
+ logger.debug(`[MessageQueue2] Notifying waiter`);
2672
+ const waiter = this.waiter;
2673
+ this.waiter = null;
2674
+ waiter(true);
2675
+ }
2676
+ logger.debug(`[MessageQueue2] unshift() completed. Queue size: ${this.queue.length}`);
2677
+ }
2678
+ /**
2679
+ * Reset the queue - clears all messages and resets to empty state
2680
+ */
2681
+ reset() {
2682
+ logger.debug(`[MessageQueue2] reset() called. Clearing ${this.queue.length} messages`);
2683
+ this.queue = [];
2684
+ this.closed = false;
2685
+ this.waiter = null;
2686
+ }
2687
+ /**
2688
+ * Close the queue - no more messages can be pushed
2689
+ */
2690
+ close() {
2691
+ logger.debug(`[MessageQueue2] close() called`);
2692
+ this.closed = true;
2693
+ if (this.waiter) {
2694
+ const waiter = this.waiter;
2695
+ this.waiter = null;
2696
+ waiter(false);
2697
+ }
2698
+ }
2699
+ /**
2700
+ * Check if the queue is closed
2701
+ */
2702
+ isClosed() {
2703
+ return this.closed;
2704
+ }
2705
+ /**
2706
+ * Get the current queue size
2707
+ */
2708
+ size() {
2709
+ return this.queue.length;
2710
+ }
2711
+ /**
2712
+ * Wait for messages and return all messages with the same mode as a single string
2713
+ * Returns { message: string, mode: T } or null if aborted/closed
2714
+ */
2715
+ async waitForMessagesAndGetAsString(abortSignal) {
2716
+ if (this.queue.length > 0) {
2717
+ return this.collectBatch();
2718
+ }
2719
+ if (this.closed || abortSignal?.aborted) {
2720
+ return null;
2721
+ }
2722
+ const hasMessages = await this.waitForMessages(abortSignal);
2723
+ if (!hasMessages) {
2724
+ return null;
2725
+ }
2726
+ return this.collectBatch();
2727
+ }
2728
+ /**
2729
+ * Collect a batch of messages with the same mode
2730
+ */
2731
+ collectBatch() {
2732
+ if (this.queue.length === 0) {
2733
+ return null;
2734
+ }
2735
+ const firstItem = this.queue[0];
2736
+ const sameModeMessages = [];
2737
+ let mode = firstItem.mode;
2738
+ const targetModeHash = firstItem.modeHash;
2739
+ while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash) {
2740
+ const item = this.queue.shift();
2741
+ sameModeMessages.push(item.message);
2742
+ }
2743
+ const combinedMessage = sameModeMessages.join("\n");
2744
+ logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
2745
+ return {
2746
+ message: combinedMessage,
2747
+ mode
2748
+ };
2749
+ }
2750
+ /**
2751
+ * Wait for messages to arrive
2752
+ */
2753
+ waitForMessages(abortSignal) {
2754
+ return new Promise((resolve) => {
2755
+ let abortHandler = null;
2756
+ if (abortSignal) {
2757
+ abortHandler = () => {
2758
+ logger.debug("[MessageQueue2] Wait aborted");
2759
+ if (this.waiter === waiterFunc) {
2760
+ this.waiter = null;
2761
+ }
2762
+ resolve(false);
2763
+ };
2764
+ abortSignal.addEventListener("abort", abortHandler);
2765
+ }
2766
+ const waiterFunc = (hasMessages) => {
2767
+ if (abortHandler && abortSignal) {
2768
+ abortSignal.removeEventListener("abort", abortHandler);
2769
+ }
2770
+ resolve(hasMessages);
2771
+ };
2772
+ if (this.queue.length > 0) {
2773
+ if (abortHandler && abortSignal) {
2774
+ abortSignal.removeEventListener("abort", abortHandler);
2775
+ }
2776
+ resolve(true);
2777
+ return;
2778
+ }
2779
+ if (this.closed || abortSignal?.aborted) {
2780
+ if (abortHandler && abortSignal) {
2781
+ abortSignal.removeEventListener("abort", abortHandler);
2782
+ }
2783
+ resolve(false);
2784
+ return;
2785
+ }
2786
+ this.waiter = waiterFunc;
2787
+ logger.debug("[MessageQueue2] Waiting for messages...");
2788
+ });
2789
+ }
2790
+ }
2791
+
2792
+ let caffeinateProcess = null;
2793
+ function startCaffeinate() {
2794
+ if (process.platform !== "darwin") {
2795
+ logger.debug("[caffeinate] Not on macOS, skipping caffeinate");
2796
+ return false;
1864
2797
  }
1865
- try {
1866
- const content = await readFile(configuration.settingsFile, "utf8");
1867
- return JSON.parse(content);
1868
- } catch {
1869
- return { ...defaultSettings };
2798
+ if (caffeinateProcess && !caffeinateProcess.killed) {
2799
+ logger.debug("[caffeinate] Caffeinate already running");
2800
+ return true;
1870
2801
  }
1871
- }
1872
- async function writeSettings(settings) {
1873
- if (!existsSync(configuration.happyDir)) {
1874
- await mkdir(configuration.happyDir, { recursive: true });
2802
+ try {
2803
+ caffeinateProcess = spawn$1("caffeinate", ["-dim"], {
2804
+ stdio: "ignore",
2805
+ detached: false
2806
+ });
2807
+ caffeinateProcess.on("error", (error) => {
2808
+ logger.debug("[caffeinate] Error starting caffeinate:", error);
2809
+ caffeinateProcess = null;
2810
+ });
2811
+ caffeinateProcess.on("exit", (code, signal) => {
2812
+ logger.debug(`[caffeinate] Process exited with code ${code}, signal ${signal}`);
2813
+ caffeinateProcess = null;
2814
+ });
2815
+ logger.debug(`[caffeinate] Started with PID ${caffeinateProcess.pid}`);
2816
+ setupCleanupHandlers();
2817
+ return true;
2818
+ } catch (error) {
2819
+ logger.debug("[caffeinate] Failed to start caffeinate:", error);
2820
+ return false;
1875
2821
  }
1876
- await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
1877
2822
  }
1878
- const credentialsSchema = z.object({
1879
- secret: z.string().base64(),
1880
- token: z.string()
1881
- });
1882
- async function readCredentials() {
1883
- if (!existsSync(configuration.privateKeyFile)) {
1884
- return null;
1885
- }
1886
- try {
1887
- const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
1888
- const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
1889
- return {
1890
- secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
1891
- token: credentials.token
1892
- };
1893
- } catch {
1894
- return null;
2823
+ function stopCaffeinate() {
2824
+ if (caffeinateProcess && !caffeinateProcess.killed) {
2825
+ logger.debug(`[caffeinate] Stopping caffeinate process PID ${caffeinateProcess.pid}`);
2826
+ try {
2827
+ caffeinateProcess.kill("SIGTERM");
2828
+ setTimeout(() => {
2829
+ if (caffeinateProcess && !caffeinateProcess.killed) {
2830
+ logger.debug("[caffeinate] Force killing caffeinate process");
2831
+ caffeinateProcess.kill("SIGKILL");
2832
+ }
2833
+ caffeinateProcess = null;
2834
+ }, 1e3);
2835
+ } catch (error) {
2836
+ logger.debug("[caffeinate] Error stopping caffeinate:", error);
2837
+ }
1895
2838
  }
1896
2839
  }
1897
- async function writeCredentials(credentials) {
1898
- if (!existsSync(configuration.happyDir)) {
1899
- await mkdir(configuration.happyDir, { recursive: true });
2840
+ let cleanupHandlersSet = false;
2841
+ function setupCleanupHandlers() {
2842
+ if (cleanupHandlersSet) {
2843
+ return;
1900
2844
  }
1901
- await writeFile$1(configuration.privateKeyFile, JSON.stringify({
1902
- secret: encodeBase64(credentials.secret),
1903
- token: credentials.token
1904
- }, null, 2));
2845
+ cleanupHandlersSet = true;
2846
+ const cleanup = () => {
2847
+ stopCaffeinate();
2848
+ };
2849
+ process.on("exit", cleanup);
2850
+ process.on("SIGINT", cleanup);
2851
+ process.on("SIGTERM", cleanup);
2852
+ process.on("SIGUSR1", cleanup);
2853
+ process.on("SIGUSR2", cleanup);
2854
+ process.on("uncaughtException", (error) => {
2855
+ logger.debug("[caffeinate] Uncaught exception, cleaning up:", error);
2856
+ cleanup();
2857
+ process.exit(1);
2858
+ });
2859
+ process.on("unhandledRejection", (reason, promise) => {
2860
+ logger.debug("[caffeinate] Unhandled rejection, cleaning up:", reason);
2861
+ cleanup();
2862
+ process.exit(1);
2863
+ });
1905
2864
  }
1906
2865
 
1907
2866
  async function start(credentials, options = {}) {
@@ -1927,227 +2886,59 @@ async function start(credentials, options = {}) {
1927
2886
  console.log(`daemon:sessionIdCreated:${response.id}`);
1928
2887
  }
1929
2888
  const session = api.session(response);
1930
- const pushClient = api.push();
1931
- let thinking = false;
1932
- let mode = "local";
1933
- let pingInterval = setInterval(() => {
1934
- session.keepAlive(thinking, mode);
1935
- }, 2e3);
1936
2889
  const logPath = await logger.logFilePathPromise;
1937
2890
  logger.infoDeveloper(`Session: ${response.id}`);
1938
2891
  logger.infoDeveloper(`Logs: ${logPath}`);
1939
- const interruptController = new InterruptController();
1940
- const { MessageQueue2 } = await Promise.resolve().then(function () { return MessageQueue2$1; });
1941
- const messageQueue = new MessageQueue2(
1942
- (mode2) => mode2
1943
- // Simple string hasher since modes are already strings
1944
- );
1945
- let requests = /* @__PURE__ */ new Map();
1946
- let toolCallResolver = null;
1947
- const sessionScanner = createSessionScanner({
1948
- workingDirectory,
1949
- onMessage: (message) => {
1950
- session.sendClaudeSessionMessage(message);
1951
- }
1952
- });
1953
- const permissionServer = await startPermissionServerV2(async (request) => {
1954
- if (!toolCallResolver) {
1955
- const error = `Tool call resolver not available for permission request: ${request.name}`;
1956
- logger.info(`ERROR: ${error}`);
1957
- throw new Error(error);
1958
- }
1959
- const toolCallId = toolCallResolver(request.name, request.arguments);
1960
- if (!toolCallId) {
1961
- const error = `Could not resolve tool call ID for permission request: ${request.name}`;
1962
- logger.info(`ERROR: ${error}`);
1963
- throw new Error(error);
1964
- }
1965
- const id = toolCallId;
1966
- logger.debug(`Using tool call ID as permission request ID: ${id} for ${request.name}`);
1967
- let promise = new Promise((resolve) => {
1968
- if (request.name === "exit_plan_mode") {
1969
- const wrappedResolve = (response2) => {
1970
- if (response2.approved) {
1971
- logger.debug("[HACK] exit_plan_mode approved - injecting approval message and denying");
1972
- sessionScanner.onRemoteUserMessageForDeduplication(PLAN_FAKE_RESTART);
1973
- messageQueue.unshift(PLAN_FAKE_RESTART, "default");
1974
- logger.debug(`[HACK] Message queue size after unshift: ${messageQueue.size()}`);
1975
- resolve({
1976
- approved: false,
1977
- reason: PLAN_FAKE_REJECT
1978
- });
1979
- } else {
1980
- resolve(response2);
1981
- }
1982
- };
1983
- requests.set(id, wrappedResolve);
2892
+ const caffeinateStarted = startCaffeinate();
2893
+ if (caffeinateStarted) {
2894
+ logger.infoDeveloper("Sleep prevention enabled (macOS)");
2895
+ }
2896
+ const messageQueue = new MessageQueue2((mode) => mode);
2897
+ registerHandlers(session);
2898
+ let currentPermissionMode = options.permissionMode;
2899
+ session.onUserMessage((message) => {
2900
+ let messagePermissionMode = currentPermissionMode;
2901
+ if (message.meta?.permissionMode) {
2902
+ const validModes = ["default", "acceptEdits", "bypassPermissions", "plan"];
2903
+ if (validModes.includes(message.meta.permissionMode)) {
2904
+ messagePermissionMode = message.meta.permissionMode;
2905
+ currentPermissionMode = messagePermissionMode;
2906
+ logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
1984
2907
  } else {
1985
- requests.set(id, resolve);
1986
- }
1987
- });
1988
- let timeout = setTimeout(async () => {
1989
- logger.debug("Permission timeout - attempting to interrupt Claude");
1990
- const interrupted = await interruptController.interrupt();
1991
- if (interrupted) {
1992
- logger.debug("Claude interrupted successfully");
2908
+ logger.debug(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
1993
2909
  }
1994
- requests.delete(id);
1995
- session.updateAgentState((currentState) => {
1996
- const request2 = currentState.requests?.[id];
1997
- if (!request2) return currentState;
1998
- let r = { ...currentState.requests };
1999
- delete r[id];
2000
- return {
2001
- ...currentState,
2002
- requests: r,
2003
- completedRequests: {
2004
- ...currentState.completedRequests,
2005
- [id]: {
2006
- ...request2,
2007
- completedAt: Date.now(),
2008
- status: "canceled",
2009
- reason: "Timeout"
2010
- }
2011
- }
2012
- };
2013
- });
2014
- }, 1e3 * 60 * 4.5);
2015
- logger.debug("Permission request" + id + " " + JSON.stringify(request));
2016
- try {
2017
- await pushClient.sendToAllDevices(
2018
- "Permission Request",
2019
- `Claude wants to use ${request.name}`,
2020
- {
2021
- sessionId: response.id,
2022
- requestId: id,
2023
- tool: request.name,
2024
- type: "permission_request"
2025
- }
2026
- );
2027
- logger.debug("Push notification sent for permission request");
2028
- } catch (error) {
2029
- logger.debug("Failed to send push notification:", error);
2910
+ } else {
2911
+ logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
2030
2912
  }
2031
- session.updateAgentState((currentState) => ({
2032
- ...currentState,
2033
- requests: {
2034
- ...currentState.requests,
2035
- [id]: {
2036
- tool: request.name,
2037
- arguments: request.arguments,
2038
- createdAt: Date.now()
2039
- }
2040
- }
2041
- }));
2042
- promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
2043
- return promise;
2913
+ messageQueue.push(message.content.text, messagePermissionMode || "default");
2914
+ logger.debugLargeJson("User message pushed to queue:", message);
2044
2915
  });
2045
- registerHandlers(session, interruptController, { requests });
2046
- const onAssistantResult = async (result) => {
2047
- try {
2048
- const summary = "result" in result && result.result ? result.result.substring(0, 100) + (result.result.length > 100 ? "..." : "") : "";
2049
- await pushClient.sendToAllDevices(
2050
- "Your move :D",
2051
- summary,
2052
- {
2053
- sessionId: response.id,
2054
- type: "assistant_result",
2055
- turns: result.num_turns,
2056
- duration_ms: result.duration_ms,
2057
- cost_usd: result.total_cost_usd
2058
- }
2059
- );
2060
- logger.debug("Push notification sent: Assistant result");
2061
- } catch (error) {
2062
- logger.debug("Failed to send assistant result push notification:", error);
2063
- }
2064
- };
2065
2916
  await loop({
2066
2917
  path: workingDirectory,
2067
2918
  model: options.model,
2068
2919
  permissionMode: options.permissionMode,
2069
2920
  startingMode: options.startingMode,
2070
2921
  messageQueue,
2071
- sessionScanner,
2922
+ api,
2072
2923
  onModeChange: (newMode) => {
2073
- mode = newMode;
2074
2924
  session.sendSessionEvent({ type: "switch", mode: newMode });
2075
- session.keepAlive(thinking, mode);
2076
- if (newMode === "local") {
2077
- logger.debug("Switching to local mode - clearing pending permission requests");
2078
- toolCallResolver = null;
2079
- for (const [id, resolve] of requests) {
2080
- logger.debug(`Rejecting pending permission request: ${id}`);
2081
- resolve({ approved: false, reason: "Session switched to local mode" });
2082
- }
2083
- requests.clear();
2084
- session.updateAgentState((currentState) => {
2085
- const pendingRequests = currentState.requests || {};
2086
- const completedRequests = { ...currentState.completedRequests };
2087
- for (const [id, request] of Object.entries(pendingRequests)) {
2088
- completedRequests[id] = {
2089
- ...request,
2090
- completedAt: Date.now(),
2091
- status: "canceled",
2092
- reason: "Session switched to local mode"
2093
- };
2094
- }
2095
- return {
2096
- ...currentState,
2097
- controlledByUser: true,
2098
- requests: {},
2099
- // Clear all pending requests
2100
- completedRequests
2101
- };
2102
- });
2103
- } else {
2104
- session.updateAgentState((currentState) => ({
2105
- ...currentState,
2106
- controlledByUser: false
2107
- }));
2108
- }
2109
- },
2110
- onProcessStart: (processMode) => {
2111
- logger.debug(`[Process Lifecycle] Starting ${processMode} mode`);
2112
- logger.debug("Starting process - clearing any stale permission requests");
2113
- for (const [id, resolve] of requests) {
2114
- logger.debug(`Rejecting stale permission request: ${id}`);
2115
- resolve({ approved: false, reason: "Process restarted" });
2116
- }
2117
- requests.clear();
2925
+ session.updateAgentState((currentState) => ({
2926
+ ...currentState,
2927
+ controlledByUser: false
2928
+ }));
2118
2929
  },
2119
- onProcessStop: (processMode) => {
2120
- logger.debug(`[Process Lifecycle] Stopped ${processMode} mode`);
2121
- logger.debug("Stopping process - clearing any stale permission requests");
2122
- for (const [id, resolve] of requests) {
2123
- logger.debug(`Rejecting stale permission request: ${id}`);
2124
- resolve({ approved: false, reason: "Process restarted" });
2125
- }
2126
- requests.clear();
2127
- thinking = false;
2128
- session.keepAlive(thinking, mode);
2129
- },
2130
- mcpServers: {
2131
- "permission": {
2132
- type: "http",
2133
- url: permissionServer.url
2134
- }
2135
- },
2136
- permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
2930
+ mcpServers: {},
2137
2931
  session,
2138
- onAssistantResult,
2139
- interruptController,
2140
2932
  claudeEnvVars: options.claudeEnvVars,
2141
- claudeArgs: options.claudeArgs,
2142
- onThinkingChange: (newThinking) => {
2143
- thinking = newThinking;
2144
- session.keepAlive(thinking, mode);
2145
- },
2146
- onToolCallResolver: (resolver) => {
2147
- toolCallResolver = resolver;
2148
- }
2933
+ claudeArgs: options.claudeArgs
2149
2934
  });
2150
- clearInterval(pingInterval);
2935
+ session.sendSessionDeath();
2936
+ logger.debug("Waiting for socket to flush...");
2937
+ await session.flush();
2938
+ logger.debug("Closing session...");
2939
+ await session.close();
2940
+ stopCaffeinate();
2941
+ logger.debug("Stopped sleep prevention");
2151
2942
  process.exit(0);
2152
2943
  }
2153
2944
 
@@ -2233,7 +3024,7 @@ class ApiDaemonSession extends EventEmitter {
2233
3024
  this.token = token;
2234
3025
  this.secret = secret;
2235
3026
  this.machineIdentity = machineIdentity;
2236
- logger.daemonDebug(`Connecting to server: ${configuration.serverUrl}`);
3027
+ logger.debug(`[DAEMON SESSION] Connecting to server: ${configuration.serverUrl}`);
2237
3028
  const socket = io(configuration.serverUrl, {
2238
3029
  auth: {
2239
3030
  token: this.token,
@@ -2250,19 +3041,19 @@ class ApiDaemonSession extends EventEmitter {
2250
3041
  autoConnect: false
2251
3042
  });
2252
3043
  socket.on("connect", () => {
2253
- logger.daemonDebug("Socket connected");
2254
- logger.daemonDebug(`Connected with auth - token: ${this.token.substring(0, 10)}..., machineId: ${this.machineIdentity.machineId}`);
3044
+ logger.debug("[DAEMON SESSION] Socket connected");
3045
+ logger.debug(`[DAEMON SESSION] Connected with auth - token: ${this.token.substring(0, 10)}..., machineId: ${this.machineIdentity.machineId}`);
2255
3046
  const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
2256
3047
  socket.emit("rpc-register", { method: rpcMethod });
2257
- logger.daemonDebug(`Emitted RPC registration: ${rpcMethod}`);
3048
+ logger.debug(`[DAEMON SESSION] Emitted RPC registration: ${rpcMethod}`);
2258
3049
  this.emit("connected");
2259
3050
  this.startKeepAlive();
2260
3051
  });
2261
3052
  socket.on("rpc-request", async (data, callback) => {
2262
- logger.daemonDebug(`Received RPC request: ${JSON.stringify(data)}`);
3053
+ logger.debug(`[DAEMON SESSION] Received RPC request: ${JSON.stringify(data)}`);
2263
3054
  const expectedMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
2264
3055
  if (data.method === expectedMethod) {
2265
- logger.daemonDebug("Processing spawn-happy-session RPC");
3056
+ logger.debug("[DAEMON SESSION] Processing spawn-happy-session RPC");
2266
3057
  try {
2267
3058
  const { directory } = data.params || {};
2268
3059
  if (!directory) {
@@ -2277,26 +3068,25 @@ class ApiDaemonSession extends EventEmitter {
2277
3068
  if (configuration.installationLocation === "local") {
2278
3069
  args.push("--local");
2279
3070
  }
2280
- if (configuration.serverUrl !== "https://handy-api.korshakov.org") {
2281
- args.push("--happy-server-url", configuration.serverUrl);
2282
- }
2283
- logger.daemonDebug(`Spawning happy in directory: ${directory} with args: ${args.join(" ")}`);
3071
+ logger.debug(`[DAEMON SESSION] Spawning happy in directory: ${directory} with args: ${args.join(" ")}`);
2284
3072
  const happyPath = process.argv[1];
2285
- const isTypeScript = happyPath.endsWith(".ts");
2286
- const happyProcess = isTypeScript ? spawn$1("npx", ["tsx", happyPath, ...args], {
2287
- cwd: directory,
2288
- env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
2289
- detached: true,
2290
- stdio: ["ignore", "pipe", "pipe"]
2291
- // We need stdout
2292
- }) : spawn$1(process.argv[0], [happyPath, ...args], {
3073
+ const runningFromBuiltBinary = happyPath.endsWith("happy") || happyPath.endsWith("happy.cmd");
3074
+ let executable, spawnArgs;
3075
+ if (runningFromBuiltBinary) {
3076
+ executable = happyPath;
3077
+ spawnArgs = args;
3078
+ } else {
3079
+ executable = "npx";
3080
+ spawnArgs = ["tsx", happyPath, ...args];
3081
+ }
3082
+ const happyProcess = spawn$1(executable, spawnArgs, {
2293
3083
  cwd: directory,
2294
- env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
2295
3084
  detached: true,
2296
3085
  stdio: ["ignore", "pipe", "pipe"]
2297
3086
  // We need stdout
2298
3087
  });
2299
3088
  this.spawnedProcesses.add(happyProcess);
3089
+ this.updateChildPidsInMetadata();
2300
3090
  let sessionId = null;
2301
3091
  let output = "";
2302
3092
  let timeoutId = null;
@@ -2315,17 +3105,17 @@ class ApiDaemonSession extends EventEmitter {
2315
3105
  const match = output.match(/daemon:sessionIdCreated:(.+?)[\n\r]/);
2316
3106
  if (match && !sessionId) {
2317
3107
  sessionId = match[1];
2318
- logger.daemonDebug(`Session spawned successfully: ${sessionId}`);
3108
+ logger.debug(`[DAEMON SESSION] Session spawned successfully: ${sessionId}`);
2319
3109
  callback({ sessionId });
2320
3110
  cleanup();
2321
3111
  happyProcess.unref();
2322
3112
  }
2323
3113
  });
2324
3114
  happyProcess.stderr.on("data", (data2) => {
2325
- logger.daemonDebug(`Spawned process stderr: ${data2.toString()}`);
3115
+ logger.debug(`[DAEMON SESSION] Spawned process stderr: ${data2.toString()}`);
2326
3116
  });
2327
3117
  happyProcess.on("error", (error) => {
2328
- logger.daemonDebug("Error spawning session:", error);
3118
+ logger.debug("[DAEMON SESSION] Error spawning session:", error);
2329
3119
  if (!sessionId) {
2330
3120
  callback({ error: `Failed to spawn: ${error.message}` });
2331
3121
  cleanup();
@@ -2333,8 +3123,9 @@ class ApiDaemonSession extends EventEmitter {
2333
3123
  }
2334
3124
  });
2335
3125
  happyProcess.on("exit", (code, signal) => {
2336
- logger.daemonDebug(`Spawned process exited with code ${code}, signal ${signal}`);
3126
+ logger.debug(`[DAEMON SESSION] Spawned process exited with code ${code}, signal ${signal}`);
2337
3127
  this.spawnedProcesses.delete(happyProcess);
3128
+ this.updateChildPidsInMetadata();
2338
3129
  if (!sessionId) {
2339
3130
  callback({ error: `Process exited before session ID received` });
2340
3131
  cleanup();
@@ -2342,53 +3133,54 @@ class ApiDaemonSession extends EventEmitter {
2342
3133
  });
2343
3134
  timeoutId = setTimeout(() => {
2344
3135
  if (!sessionId) {
2345
- logger.daemonDebug("Timeout waiting for session ID");
3136
+ logger.debug("[DAEMON SESSION] Timeout waiting for session ID");
2346
3137
  callback({ error: "Timeout waiting for session" });
2347
3138
  cleanup();
2348
3139
  happyProcess.kill();
2349
3140
  this.spawnedProcesses.delete(happyProcess);
3141
+ this.updateChildPidsInMetadata();
2350
3142
  }
2351
3143
  }, 1e4);
2352
3144
  } catch (error) {
2353
- logger.daemonDebug("Error spawning session:", error);
3145
+ logger.debug("[DAEMON SESSION] Error spawning session:", error);
2354
3146
  callback({ error: error instanceof Error ? error.message : "Unknown error" });
2355
3147
  }
2356
3148
  } else {
2357
- logger.daemonDebug(`Unknown RPC method: ${data.method}`);
3149
+ logger.debug(`[DAEMON SESSION] Unknown RPC method: ${data.method}`);
2358
3150
  callback({ error: `Unknown method: ${data.method}` });
2359
3151
  }
2360
3152
  });
2361
3153
  socket.on("disconnect", (reason) => {
2362
- logger.daemonDebug(`Disconnected from server. Reason: ${reason}`);
3154
+ logger.debug(`[DAEMON SESSION] Disconnected from server. Reason: ${reason}`);
2363
3155
  this.emit("disconnected");
2364
3156
  this.stopKeepAlive();
2365
3157
  });
2366
3158
  socket.on("reconnect", () => {
2367
- logger.daemonDebug("Reconnected to server");
3159
+ logger.debug("[DAEMON SESSION] Reconnected to server");
2368
3160
  const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
2369
3161
  socket.emit("rpc-register", { method: rpcMethod });
2370
- logger.daemonDebug(`Re-registered RPC method: ${rpcMethod}`);
3162
+ logger.debug(`[DAEMON SESSION] Re-registered RPC method: ${rpcMethod}`);
2371
3163
  });
2372
3164
  socket.on("rpc-registered", (data) => {
2373
- logger.daemonDebug(`RPC registration confirmed: ${data.method}`);
3165
+ logger.debug(`[DAEMON SESSION] RPC registration confirmed: ${data.method}`);
2374
3166
  });
2375
3167
  socket.on("rpc-unregistered", (data) => {
2376
- logger.daemonDebug(`RPC unregistered: ${data.method}`);
3168
+ logger.debug(`[DAEMON SESSION] RPC unregistered: ${data.method}`);
2377
3169
  });
2378
3170
  socket.on("rpc-error", (data) => {
2379
- logger.daemonDebug(`RPC error: ${JSON.stringify(data)}`);
3171
+ logger.debug(`[DAEMON SESSION] RPC error: ${JSON.stringify(data)}`);
2380
3172
  });
2381
3173
  socket.onAny((event, ...args) => {
2382
3174
  if (!event.startsWith("machine-alive")) {
2383
- logger.daemonDebug(`Socket event: ${event}, args: ${JSON.stringify(args)}`);
3175
+ logger.debug(`[DAEMON SESSION] Socket event: ${event}, args: ${JSON.stringify(args)}`);
2384
3176
  }
2385
3177
  });
2386
3178
  socket.on("connect_error", (error) => {
2387
- logger.daemonDebug(`Connection error: ${error.message}`);
2388
- logger.daemonDebug(`Error: ${JSON.stringify(error, null, 2)}`);
3179
+ logger.debug(`[DAEMON SESSION] Connection error: ${error.message}`);
3180
+ logger.debug(`[DAEMON SESSION] Error: ${JSON.stringify(error, null, 2)}`);
2389
3181
  });
2390
3182
  socket.on("error", (error) => {
2391
- logger.daemonDebug(`Socket error: ${error}`);
3183
+ logger.debug(`[DAEMON SESSION] Socket error: ${error}`);
2392
3184
  });
2393
3185
  socket.on("daemon-command", (data) => {
2394
3186
  switch (data.command) {
@@ -2416,14 +3208,27 @@ class ApiDaemonSession extends EventEmitter {
2416
3208
  this.keepAliveInterval = null;
2417
3209
  }
2418
3210
  }
3211
+ updateChildPidsInMetadata() {
3212
+ try {
3213
+ if (existsSync$1(configuration.daemonMetadataFile)) {
3214
+ const content = readFileSync$1(configuration.daemonMetadataFile, "utf-8");
3215
+ const metadata = JSON.parse(content);
3216
+ const childPids = Array.from(this.spawnedProcesses).map((proc) => proc.pid).filter((pid) => pid !== void 0);
3217
+ metadata.childPids = childPids;
3218
+ writeFileSync(configuration.daemonMetadataFile, JSON.stringify(metadata, null, 2));
3219
+ }
3220
+ } catch (error) {
3221
+ logger.debug("[DAEMON SESSION] Error updating child PIDs in metadata:", error);
3222
+ }
3223
+ }
2419
3224
  connect() {
2420
3225
  this.socket.connect();
2421
3226
  }
2422
3227
  shutdown() {
2423
- logger.daemonDebug(`Shutting down daemon, killing ${this.spawnedProcesses.size} spawned processes`);
3228
+ logger.debug(`[DAEMON SESSION] Shutting down daemon, killing ${this.spawnedProcesses.size} spawned processes`);
2424
3229
  for (const process2 of this.spawnedProcesses) {
2425
3230
  try {
2426
- logger.daemonDebug(`Killing spawned process with PID: ${process2.pid}`);
3231
+ logger.debug(`[DAEMON SESSION] Killing spawned process with PID: ${process2.pid}`);
2427
3232
  process2.kill("SIGTERM");
2428
3233
  setTimeout(() => {
2429
3234
  try {
@@ -2432,39 +3237,66 @@ class ApiDaemonSession extends EventEmitter {
2432
3237
  }
2433
3238
  }, 1e3);
2434
3239
  } catch (error) {
2435
- logger.daemonDebug(`Error killing process: ${error}`);
3240
+ logger.debug(`[DAEMON SESSION] Error killing process: ${error}`);
2436
3241
  }
2437
3242
  }
2438
3243
  this.spawnedProcesses.clear();
3244
+ this.updateChildPidsInMetadata();
2439
3245
  this.stopKeepAlive();
2440
3246
  this.socket.close();
2441
3247
  this.emit("shutdown");
2442
3248
  }
2443
3249
  }
2444
3250
 
2445
- let pidFileFd = null;
2446
3251
  async function startDaemon() {
2447
3252
  if (process.platform !== "darwin") {
2448
3253
  console.error("ERROR: Daemon is only supported on macOS");
2449
3254
  process.exit(1);
2450
3255
  }
2451
- logger.daemonDebug("Starting daemon process...");
2452
- logger.daemonDebug(`Server URL: ${configuration.serverUrl}`);
2453
- if (await isDaemonRunning()) {
2454
- logger.daemonDebug("Happy daemon is already running");
2455
- process.exit(0);
3256
+ logger.debug("[DAEMON RUN] Starting daemon process...");
3257
+ logger.debug(`[DAEMON RUN] Server URL: ${configuration.serverUrl}`);
3258
+ const runningDaemon = await getDaemonMetadata();
3259
+ if (runningDaemon) {
3260
+ if (runningDaemon.version !== packageJson.version) {
3261
+ logger.debug(`[DAEMON RUN] Daemon version mismatch (running: ${runningDaemon.version}, current: ${packageJson.version}), restarting...`);
3262
+ await stopDaemon();
3263
+ await new Promise((resolve) => setTimeout(resolve, 500));
3264
+ } else if (await isDaemonProcessRunning(runningDaemon.pid)) {
3265
+ logger.debug("[DAEMON RUN] Happy daemon is already running with correct version");
3266
+ process.exit(0);
3267
+ } else {
3268
+ logger.debug("[DAEMON RUN] Stale daemon metadata found, cleaning up");
3269
+ await cleanupDaemonMetadata();
3270
+ }
3271
+ }
3272
+ const oldMetadata = await getDaemonMetadata();
3273
+ if (oldMetadata && oldMetadata.childPids && oldMetadata.childPids.length > 0) {
3274
+ logger.debug(`[DAEMON RUN] Found ${oldMetadata.childPids.length} potential orphaned child processes from previous run`);
3275
+ for (const childPid of oldMetadata.childPids) {
3276
+ try {
3277
+ process.kill(childPid, 0);
3278
+ const isHappy = await isProcessHappyChild(childPid);
3279
+ if (isHappy) {
3280
+ logger.debug(`[DAEMON RUN] Killing orphaned happy process ${childPid}`);
3281
+ process.kill(childPid, "SIGTERM");
3282
+ await new Promise((resolve) => setTimeout(resolve, 500));
3283
+ try {
3284
+ process.kill(childPid, 0);
3285
+ process.kill(childPid, "SIGKILL");
3286
+ } catch {
3287
+ }
3288
+ }
3289
+ } catch {
3290
+ logger.debug(`[DAEMON RUN] Process ${childPid} doesn't exist (already dead)`);
3291
+ }
3292
+ }
3293
+ }
3294
+ writeDaemonMetadata();
3295
+ logger.debug("[DAEMON RUN] Daemon metadata written");
3296
+ const caffeinateStarted = startCaffeinate();
3297
+ if (caffeinateStarted) {
3298
+ logger.debug("[DAEMON RUN] Sleep prevention enabled for daemon");
2456
3299
  }
2457
- pidFileFd = writePidFile();
2458
- logger.daemonDebug("PID file written");
2459
- process.on("SIGINT", () => {
2460
- stopDaemon().catch(console.error);
2461
- });
2462
- process.on("SIGTERM", () => {
2463
- stopDaemon().catch(console.error);
2464
- });
2465
- process.on("exit", () => {
2466
- stopDaemon().catch(console.error);
2467
- });
2468
3300
  try {
2469
3301
  const settings = await readSettings() || { onboardingCompleted: false };
2470
3302
  if (!settings.machineId) {
@@ -2476,11 +3308,11 @@ async function startDaemon() {
2476
3308
  machineId: settings.machineId,
2477
3309
  machineHost: settings.machineHost || hostname(),
2478
3310
  platform: process.platform,
2479
- version: process.env.npm_package_version || "unknown"
3311
+ version: packageJson.version
2480
3312
  };
2481
3313
  let credentials = await readCredentials();
2482
3314
  if (!credentials) {
2483
- logger.daemonDebug("No credentials found, running auth");
3315
+ logger.debug("[DAEMON RUN] No credentials found, running auth");
2484
3316
  await doAuth();
2485
3317
  credentials = await readCredentials();
2486
3318
  if (!credentials) {
@@ -2494,20 +3326,37 @@ async function startDaemon() {
2494
3326
  machineIdentity
2495
3327
  );
2496
3328
  daemon.on("connected", () => {
2497
- logger.daemonDebug("Connected to server event received");
3329
+ logger.debug("[DAEMON RUN] Connected to server event received");
2498
3330
  });
2499
3331
  daemon.on("disconnected", () => {
2500
- logger.daemonDebug("Disconnected from server event received");
3332
+ logger.debug("[DAEMON RUN] Disconnected from server event received");
2501
3333
  });
2502
3334
  daemon.on("shutdown", () => {
2503
- logger.daemonDebug("Shutdown requested");
2504
- stopDaemon();
3335
+ logger.debug("[DAEMON RUN] Shutdown requested");
3336
+ daemon?.shutdown();
3337
+ cleanupDaemonMetadata();
2505
3338
  process.exit(0);
2506
3339
  });
2507
3340
  daemon.connect();
2508
- logger.daemonDebug("Daemon started successfully");
3341
+ logger.debug("[DAEMON RUN] Daemon started successfully");
3342
+ process.on("SIGINT", async () => {
3343
+ logger.debug("[DAEMON RUN] Received SIGINT, shutting down...");
3344
+ if (daemon) {
3345
+ daemon.shutdown();
3346
+ }
3347
+ await cleanupDaemonMetadata();
3348
+ process.exit(0);
3349
+ });
3350
+ process.on("SIGTERM", async () => {
3351
+ logger.debug("[DAEMON RUN] Received SIGTERM, shutting down...");
3352
+ if (daemon) {
3353
+ daemon.shutdown();
3354
+ }
3355
+ await cleanupDaemonMetadata();
3356
+ process.exit(0);
3357
+ });
2509
3358
  } catch (error) {
2510
- logger.daemonDebug("Failed to start daemon", error);
3359
+ logger.debug("[DAEMON RUN] Failed to start daemon", error);
2511
3360
  stopDaemon();
2512
3361
  process.exit(1);
2513
3362
  }
@@ -2517,96 +3366,114 @@ async function startDaemon() {
2517
3366
  }
2518
3367
  async function isDaemonRunning() {
2519
3368
  try {
2520
- logger.daemonDebug("[isDaemonRunning] Checking if daemon is running...");
2521
- if (existsSync$1(configuration.daemonPidFile)) {
2522
- logger.daemonDebug("[isDaemonRunning] PID file exists");
2523
- const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
2524
- logger.daemonDebug("[isDaemonRunning] PID from file:", pid);
2525
- try {
2526
- process.kill(pid, 0);
2527
- logger.daemonDebug("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
2528
- const isHappyDaemon = await isProcessHappyDaemon(pid);
2529
- logger.daemonDebug("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
2530
- if (isHappyDaemon) {
2531
- return true;
2532
- } else {
2533
- logger.daemonDebug("[isDaemonRunning] PID is not a happy daemon, cleaning up");
2534
- logger.debug(`PID ${pid} is not a happy daemon, cleaning up`);
2535
- unlinkSync(configuration.daemonPidFile);
2536
- }
2537
- } catch (error) {
2538
- logger.daemonDebug("[isDaemonRunning] Process not running, cleaning up stale PID file");
2539
- logger.debug("Process not running, cleaning up stale PID file");
2540
- unlinkSync(configuration.daemonPidFile);
2541
- }
2542
- } else {
2543
- logger.daemonDebug("[isDaemonRunning] No PID file found");
3369
+ logger.debug("[DAEMON RUN] [isDaemonRunning] Checking if daemon is running...");
3370
+ const metadata = await getDaemonMetadata();
3371
+ if (!metadata) {
3372
+ logger.debug("[DAEMON RUN] [isDaemonRunning] No daemon metadata found");
3373
+ return false;
2544
3374
  }
2545
- return false;
3375
+ logger.debug("[DAEMON RUN] [isDaemonRunning] Daemon metadata exists");
3376
+ logger.debug("[DAEMON RUN] [isDaemonRunning] PID from metadata:", metadata.pid);
3377
+ const isRunning = await isDaemonProcessRunning(metadata.pid);
3378
+ if (!isRunning) {
3379
+ logger.debug("[DAEMON RUN] [isDaemonRunning] Process not running, cleaning up stale metadata");
3380
+ await cleanupDaemonMetadata();
3381
+ return false;
3382
+ }
3383
+ return true;
2546
3384
  } catch (error) {
2547
- logger.daemonDebug("[isDaemonRunning] Error:", error);
3385
+ logger.debug("[DAEMON RUN] [isDaemonRunning] Error:", error);
2548
3386
  logger.debug("Error checking daemon status", error);
2549
3387
  return false;
2550
3388
  }
2551
3389
  }
2552
- function writePidFile() {
3390
+ async function isDaemonProcessRunning(pid) {
3391
+ try {
3392
+ process.kill(pid, 0);
3393
+ logger.debug("[DAEMON RUN] Process exists, checking if it's a happy daemon...");
3394
+ const isHappyDaemon = await isProcessHappyDaemon(pid);
3395
+ logger.debug("[DAEMON RUN] isHappyDaemon:", isHappyDaemon);
3396
+ return isHappyDaemon;
3397
+ } catch (error) {
3398
+ return false;
3399
+ }
3400
+ }
3401
+ function writeDaemonMetadata(childPids) {
2553
3402
  const happyDir = join$1(homedir$1(), ".happy");
2554
3403
  if (!existsSync$1(happyDir)) {
2555
3404
  mkdirSync$1(happyDir, { recursive: true });
2556
3405
  }
3406
+ const metadata = {
3407
+ pid: process.pid,
3408
+ startTime: (/* @__PURE__ */ new Date()).toISOString(),
3409
+ version: packageJson.version,
3410
+ ...childPids
3411
+ };
3412
+ writeFileSync(configuration.daemonMetadataFile, JSON.stringify(metadata, null, 2));
3413
+ }
3414
+ async function getDaemonMetadata() {
2557
3415
  try {
2558
- const fd = openSync(configuration.daemonPidFile, "wx");
2559
- writeSync(fd, process.pid.toString());
2560
- return fd;
3416
+ if (!existsSync$1(configuration.daemonMetadataFile)) {
3417
+ return null;
3418
+ }
3419
+ const content = readFileSync$1(configuration.daemonMetadataFile, "utf-8");
3420
+ return JSON.parse(content);
2561
3421
  } catch (error) {
2562
- if (error.code === "EEXIST") {
2563
- try {
2564
- const fd = openSync(configuration.daemonPidFile, "r+");
2565
- const existingPid = readFileSync$1(configuration.daemonPidFile, "utf-8").trim();
2566
- closeSync(fd);
2567
- try {
2568
- process.kill(parseInt(existingPid), 0);
2569
- logger.daemonDebug("PID file exists and process is running");
2570
- logger.daemonDebug("Happy daemon is already running");
2571
- process.exit(0);
2572
- } catch {
2573
- logger.daemonDebug("PID file exists but process is dead, cleaning up");
2574
- unlinkSync(configuration.daemonPidFile);
2575
- return writePidFile();
2576
- }
2577
- } catch (lockError) {
2578
- logger.daemonDebug("Cannot acquire write lock on PID file, daemon is running");
2579
- logger.daemonDebug("Happy daemon is already running");
2580
- process.exit(0);
2581
- }
3422
+ logger.debug("Error reading daemon metadata", error);
3423
+ return null;
3424
+ }
3425
+ }
3426
+ async function cleanupDaemonMetadata() {
3427
+ try {
3428
+ if (existsSync$1(configuration.daemonMetadataFile)) {
3429
+ unlinkSync(configuration.daemonMetadataFile);
2582
3430
  }
2583
- throw error;
3431
+ } catch (error) {
3432
+ logger.debug("Error cleaning up daemon metadata", error);
2584
3433
  }
2585
3434
  }
2586
3435
  async function stopDaemon() {
2587
3436
  try {
2588
- if (pidFileFd !== null) {
3437
+ stopCaffeinate();
3438
+ logger.debug("Stopped sleep prevention");
3439
+ const metadata = await getDaemonMetadata();
3440
+ if (metadata) {
3441
+ logger.debug(`Stopping daemon with PID ${metadata.pid}`);
2589
3442
  try {
2590
- closeSync(pidFileFd);
2591
- } catch {
2592
- }
2593
- pidFileFd = null;
2594
- }
2595
- if (existsSync$1(configuration.daemonPidFile)) {
2596
- const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
2597
- logger.debug(`Stopping daemon with PID ${pid}`);
2598
- try {
2599
- process.kill(pid, "SIGTERM");
2600
- await new Promise((resolve) => setTimeout(resolve, 1e3));
3443
+ process.kill(metadata.pid, "SIGTERM");
3444
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
2601
3445
  try {
2602
- process.kill(pid, 0);
2603
- process.kill(pid, "SIGKILL");
3446
+ process.kill(metadata.pid, 0);
3447
+ logger.debug("Daemon still running, force killing...");
3448
+ process.kill(metadata.pid, "SIGKILL");
2604
3449
  } catch {
3450
+ logger.debug("Daemon exited cleanly");
2605
3451
  }
2606
3452
  } catch (error) {
2607
- logger.debug("Process already dead or inaccessible", error);
3453
+ logger.debug("Daemon process already dead or inaccessible", error);
3454
+ }
3455
+ await new Promise((resolve) => setTimeout(resolve, 500));
3456
+ if (metadata.childPids && metadata.childPids.length > 0) {
3457
+ logger.debug(`Checking for ${metadata.childPids.length} potential orphaned child processes...`);
3458
+ for (const childPid of metadata.childPids) {
3459
+ try {
3460
+ process.kill(childPid, 0);
3461
+ const isHappy = await isProcessHappyChild(childPid);
3462
+ if (isHappy) {
3463
+ logger.debug(`Killing orphaned happy process ${childPid}`);
3464
+ process.kill(childPid, "SIGTERM");
3465
+ await new Promise((resolve) => setTimeout(resolve, 500));
3466
+ try {
3467
+ process.kill(childPid, 0);
3468
+ process.kill(childPid, "SIGKILL");
3469
+ } catch {
3470
+ }
3471
+ }
3472
+ } catch {
3473
+ }
3474
+ }
2608
3475
  }
2609
- unlinkSync(configuration.daemonPidFile);
3476
+ await cleanupDaemonMetadata();
2610
3477
  }
2611
3478
  } catch (error) {
2612
3479
  logger.debug("Error stopping daemon", error);
@@ -2628,6 +3495,22 @@ async function isProcessHappyDaemon(pid) {
2628
3495
  });
2629
3496
  });
2630
3497
  }
3498
+ async function isProcessHappyChild(pid) {
3499
+ return new Promise((resolve) => {
3500
+ const ps = spawn$1("ps", ["-p", pid.toString(), "-o", "command="]);
3501
+ let output = "";
3502
+ ps.stdout.on("data", (data) => {
3503
+ output += data.toString();
3504
+ });
3505
+ ps.on("close", () => {
3506
+ const isHappyChild = output.includes("--daemon-spawn") && (output.includes("happy") || output.includes("src/index"));
3507
+ resolve(isHappyChild);
3508
+ });
3509
+ ps.on("error", () => {
3510
+ resolve(false);
3511
+ });
3512
+ });
3513
+ }
2631
3514
 
2632
3515
  function trimIdent(text) {
2633
3516
  const lines = text.split("\n");
@@ -2655,7 +3538,7 @@ async function install$1() {
2655
3538
  try {
2656
3539
  if (existsSync$1(PLIST_FILE$1)) {
2657
3540
  logger.info("Daemon plist already exists. Uninstalling first...");
2658
- execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
3541
+ execSync$1(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
2659
3542
  }
2660
3543
  const happyPath = process.argv[0];
2661
3544
  const scriptPath = process.argv[1];
@@ -2700,7 +3583,7 @@ async function install$1() {
2700
3583
  writeFileSync(PLIST_FILE$1, plistContent);
2701
3584
  chmodSync(PLIST_FILE$1, 420);
2702
3585
  logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
2703
- execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
3586
+ execSync$1(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
2704
3587
  logger.info("Daemon installed and started successfully");
2705
3588
  logger.info("Check logs at ~/.happy/daemon.log");
2706
3589
  } catch (error) {
@@ -2729,7 +3612,7 @@ async function uninstall$1() {
2729
3612
  return;
2730
3613
  }
2731
3614
  try {
2732
- execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
3615
+ execSync$1(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
2733
3616
  logger.info("Daemon stopped successfully");
2734
3617
  } catch (error) {
2735
3618
  logger.info("Failed to unload daemon (it might not be running)");
@@ -2757,12 +3640,7 @@ async function uninstall() {
2757
3640
  (async () => {
2758
3641
  const args = process.argv.slice(2);
2759
3642
  let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
2760
- let serverUrl;
2761
- const serverUrlIndex = args.indexOf("--happy-server-url");
2762
- if (serverUrlIndex !== -1 && serverUrlIndex + 1 < args.length) {
2763
- serverUrl = args[serverUrlIndex + 1];
2764
- }
2765
- initializeConfiguration(installationLocation, serverUrl);
3643
+ initializeConfiguration(installationLocation);
2766
3644
  initLoggerWithGlobalConfiguration();
2767
3645
  logger.debug("Starting happy CLI with args: ", process.argv);
2768
3646
  const subcommand = args[0];
@@ -2846,8 +3724,6 @@ Currently only supported on macOS.
2846
3724
  options.claudeArgs = [...options.claudeArgs || [], claudeArg];
2847
3725
  } else if (arg === "--daemon-spawn") {
2848
3726
  options.daemonSpawn = true;
2849
- } else if (arg === "--happy-server-url") {
2850
- i++;
2851
3727
  } else {
2852
3728
  console.error(chalk.red(`Unknown argument: ${arg}`));
2853
3729
  process.exit(1);
@@ -2855,7 +3731,7 @@ Currently only supported on macOS.
2855
3731
  }
2856
3732
  if (showHelp) {
2857
3733
  console.log(`
2858
- ${chalk.bold("happy")} - Claude Code session sharing
3734
+ ${chalk.bold("happy")} - Claude Code On the Go
2859
3735
 
2860
3736
  ${chalk.bold("Usage:")}
2861
3737
  happy [options]
@@ -2896,6 +3772,10 @@ ${chalk.bold("Examples:")}
2896
3772
  happy --claude-arg --option
2897
3773
  Pass argument to Claude CLI
2898
3774
  happy logout Logs out of your account and removes data directory
3775
+
3776
+ [TODO: add after steve's refactor lands]
3777
+ ${chalk.bold("Happy is a seamless passthrough to Claude CLI - so any commands that Claude CLI supports will work with Happy. Here is the help for Claude CLI:")}
3778
+ TODO: exec cluade --help and show inline here
2899
3779
  `);
2900
3780
  process.exit(0);
2901
3781
  }
@@ -2912,7 +3792,6 @@ ${chalk.bold("Examples:")}
2912
3792
  credentials = res;
2913
3793
  }
2914
3794
  const settings = await readSettings() || { onboardingCompleted: false };
2915
- process.env.EXPERIMENTAL_FEATURES !== void 0;
2916
3795
  if (settings.daemonAutoStartWhenRunningHappy === void 0) {
2917
3796
  console.log(chalk.cyan("\n\u{1F680} Happy Daemon Setup\n"));
2918
3797
  const rl = createInterface({
@@ -2938,39 +3817,26 @@ ${chalk.bold("Examples:")}
2938
3817
  await writeSettings(settings);
2939
3818
  }
2940
3819
  if (settings.daemonAutoStartWhenRunningHappy) {
2941
- console.debug("Starting Happy background service...");
3820
+ logger.debug("Starting Happy background service...");
2942
3821
  if (!await isDaemonRunning()) {
2943
3822
  const happyPath = process.argv[1];
2944
- const isBuiltBinary = happyPath.endsWith("/bin/happy") || happyPath.endsWith("\\bin\\happy");
3823
+ const runningFromBuiltBinary = happyPath.endsWith("happy") || happyPath.endsWith("happy.cmd");
2945
3824
  const daemonArgs = ["daemon", "start"];
2946
- if (serverUrl) {
2947
- daemonArgs.push("--happy-server-url", serverUrl);
2948
- }
2949
3825
  if (installationLocation === "local") {
2950
3826
  daemonArgs.push("--local");
2951
3827
  }
2952
- const daemonProcess = isBuiltBinary ? spawn$1(happyPath, daemonArgs, {
2953
- detached: true,
2954
- stdio: ["ignore", "inherit", "inherit"],
2955
- // Show stdout/stderr for debugging
2956
- env: {
2957
- ...process.env,
2958
- HANDY_SERVER_URL: serverUrl || process.env.HANDY_SERVER_URL,
2959
- // Pass through server URL
2960
- HANDY_LOCAL: process.env.HANDY_LOCAL
2961
- // Pass through local flag
2962
- }
2963
- }) : spawn$1("npx", ["tsx", happyPath, ...daemonArgs], {
3828
+ let executable, args2;
3829
+ if (runningFromBuiltBinary) {
3830
+ executable = happyPath;
3831
+ args2 = daemonArgs;
3832
+ } else {
3833
+ executable = "npx";
3834
+ args2 = ["tsx", happyPath, ...daemonArgs];
3835
+ }
3836
+ const daemonProcess = spawn$1(executable, args2, {
2964
3837
  detached: true,
2965
- stdio: ["ignore", "inherit", "inherit"],
3838
+ stdio: ["ignore", "inherit", "inherit"]
2966
3839
  // Show stdout/stderr for debugging
2967
- env: {
2968
- ...process.env,
2969
- HANDY_SERVER_URL: serverUrl || process.env.HANDY_SERVER_URL,
2970
- // Pass through server URL
2971
- HANDY_LOCAL: process.env.HANDY_LOCAL
2972
- // Pass through local flag
2973
- }
2974
3840
  });
2975
3841
  daemonProcess.unref();
2976
3842
  await new Promise((resolve) => setTimeout(resolve, 200));