happy-coder 0.2.2 → 0.2.3-beta.1

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,1252 @@ 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
+ updateThinking(true);
1252
+ try {
1253
+ logger.debug(`[claudeRemote] Starting to iterate over response`);
1254
+ for await (const message2 of response) {
1255
+ logger.debugLargeJson(`[claudeRemote] Message ${message2.type}`, message2);
1256
+ opts.onMessage(message2);
1257
+ if (message2.type === "system" && message2.subtype === "init") {
1258
+ updateThinking(true);
1259
+ const systemInit = message2;
1260
+ if (systemInit.session_id) {
1261
+ logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${systemInit.session_id}`);
1262
+ const projectDir = getProjectPath(opts.path);
1263
+ const found = await awaitFileExist(join(projectDir, `${systemInit.session_id}.jsonl`));
1264
+ logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
1265
+ opts.onSessionFound(systemInit.session_id);
1266
+ }
1267
+ }
1268
+ if (message2.type === "result") {
1269
+ updateThinking(false);
1270
+ logger.debug("[claudeRemote] Result received, exiting claudeRemote");
1271
+ return;
1272
+ }
1273
+ if (message2.type === "user") {
1274
+ const msg = message2;
1275
+ if (msg.message.role === "user" && Array.isArray(msg.message.content)) {
1276
+ for (let c of msg.message.content) {
1277
+ if (c.type === "tool_result" && (c.name === "exit_plan_mode" || c.name === "ExitPlanMode")) {
1278
+ logger.debug("[claudeRemote] Plan result received, exiting claudeRemote");
1279
+ return;
1280
+ }
1281
+ if (c.type === "tool_result" && c.tool_use_id && opts.responses.has(c.tool_use_id) && !opts.responses.get(c.tool_use_id).approved) {
1282
+ logger.debug("[claudeRemote] Tool rejected, exiting claudeRemote");
1283
+ return;
1284
+ }
1285
+ }
1286
+ }
1287
+ }
925
1288
  }
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;
1289
+ logger.debug(`[claudeRemote] Finished iterating over response`);
1290
+ } catch (e) {
1291
+ if (e instanceof AbortError) {
1292
+ logger.debug(`[claudeRemote] Aborted`);
1293
+ } else {
1294
+ throw e;
934
1295
  }
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);
1296
+ } finally {
1297
+ updateThinking(false);
1298
+ }
1299
+ logger.debug(`[claudeRemote] Function completed`);
1300
+ }
1301
+
1302
+ 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.`;
1303
+ const PLAN_FAKE_RESTART = `PlEaZe Continue with plan.`;
1304
+
1305
+ async function startPermissionServerV2(handler) {
1306
+ const mcp = new McpServer({
1307
+ name: "Permission Server",
1308
+ version: "1.0.0",
1309
+ description: "A server that allows you to request permissions from the user"
1310
+ });
1311
+ mcp.registerTool("ask_permission", {
1312
+ description: "Request permission to execute a tool",
1313
+ title: "Request Permission",
1314
+ inputSchema: {
1315
+ tool_name: z$1.string().describe("The tool that needs permission"),
1316
+ input: z$1.any().describe("The arguments for the tool")
942
1317
  }
943
- const combinedMessage = sameModeMessages.join("\n");
944
- logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
1318
+ }, async (args) => {
1319
+ const response = await handler({ name: args.tool_name, arguments: args.input });
1320
+ logger.debugLargeJson("[permissionServerV2] Response", response);
1321
+ 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
1322
  return {
946
- message: combinedMessage,
947
- mode
1323
+ content: [
1324
+ {
1325
+ type: "text",
1326
+ text: JSON.stringify(result)
1327
+ }
1328
+ ],
1329
+ isError: false
948
1330
  };
1331
+ });
1332
+ const transport = new StreamableHTTPServerTransport({
1333
+ // NOTE: Returning session id here will result in claude
1334
+ // sdk spawn to fail with `Invalid Request: Server already initialized`
1335
+ sessionIdGenerator: void 0
1336
+ });
1337
+ await mcp.connect(transport);
1338
+ const server = createServer(async (req, res) => {
1339
+ try {
1340
+ await transport.handleRequest(req, res);
1341
+ } catch (error) {
1342
+ logger.debug("Error handling request:", error);
1343
+ if (!res.headersSent) {
1344
+ res.writeHead(500).end();
1345
+ }
1346
+ }
1347
+ });
1348
+ const baseUrl = await new Promise((resolve) => {
1349
+ server.listen(0, "127.0.0.1", () => {
1350
+ const addr = server.address();
1351
+ resolve(new URL(`http://127.0.0.1:${addr.port}`));
1352
+ });
1353
+ });
1354
+ return {
1355
+ url: baseUrl.toString(),
1356
+ toolName: "ask_permission",
1357
+ stop: () => {
1358
+ mcp.close();
1359
+ server.close();
1360
+ }
1361
+ };
1362
+ }
1363
+
1364
+ function deepEqual(a, b) {
1365
+ if (a === b) return true;
1366
+ if (a == null || b == null) return false;
1367
+ if (typeof a !== "object" || typeof b !== "object") return false;
1368
+ const keysA = Object.keys(a);
1369
+ const keysB = Object.keys(b);
1370
+ if (keysA.length !== keysB.length) return false;
1371
+ for (const key of keysA) {
1372
+ if (!keysB.includes(key)) return false;
1373
+ if (!deepEqual(a[key], b[key])) return false;
949
1374
  }
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;
1375
+ return true;
1376
+ }
1377
+
1378
+ async function startPermissionResolver(session) {
1379
+ let toolCalls = [];
1380
+ let responses = /* @__PURE__ */ new Map();
1381
+ let requests = /* @__PURE__ */ new Map();
1382
+ let pendingPermissionRequests = [];
1383
+ const server = await startPermissionServerV2(async (request) => {
1384
+ const id = resolveToolCallId(request.name, request.arguments);
1385
+ if (!id) {
1386
+ logger.debug(`Tool call ID not yet available for ${request.name}, queueing request`);
1387
+ return new Promise((resolve, reject) => {
1388
+ const timeout = setTimeout(() => {
1389
+ const idx = pendingPermissionRequests.findIndex((p) => p.request === request);
1390
+ if (idx !== -1) {
1391
+ pendingPermissionRequests.splice(idx, 1);
1392
+ reject(new Error(`Timeout: Tool call ID never arrived for ${request.name}`));
1393
+ }
1394
+ }, 3e4);
1395
+ pendingPermissionRequests.push({ request, resolve, reject, timeout });
1396
+ });
1397
+ }
1398
+ return handlePermissionRequest(id, request);
1399
+ });
1400
+ function handlePermissionRequest(id, request) {
1401
+ let promise = new Promise((resolve) => {
1402
+ if (request.name === "exit_plan_mode" || request.name === "ExitPlanMode") {
1403
+ const wrappedResolve = (response) => {
1404
+ if (response.approved) {
1405
+ logger.debug("Plan approved - injecting PLAN_FAKE_RESTART");
1406
+ if (response.mode && ["default", "acceptEdits", "bypassPermissions"].includes(response.mode)) {
1407
+ session.queue.unshift(PLAN_FAKE_RESTART, response.mode);
1408
+ } else {
1409
+ session.queue.unshift(PLAN_FAKE_RESTART, "default");
1410
+ }
1411
+ resolve({ approved: false, reason: PLAN_FAKE_REJECT });
1412
+ } else {
1413
+ resolve(response);
961
1414
  }
962
- resolve(false);
963
1415
  };
964
- abortSignal.addEventListener("abort", abortHandler);
1416
+ requests.set(id, wrappedResolve);
1417
+ } else {
1418
+ requests.set(id, resolve);
965
1419
  }
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;
1420
+ });
1421
+ let timeout = setTimeout(async () => {
1422
+ logger.debug("Permission timeout - attempting to interrupt Claude");
1423
+ requests.delete(id);
1424
+ session.client.updateAgentState((currentState) => {
1425
+ const request2 = currentState.requests?.[id];
1426
+ if (!request2) return currentState;
1427
+ let r = { ...currentState.requests };
1428
+ delete r[id];
1429
+ return {
1430
+ ...currentState,
1431
+ requests: r,
1432
+ completedRequests: {
1433
+ ...currentState.completedRequests,
1434
+ [id]: {
1435
+ ...request2,
1436
+ completedAt: Date.now(),
1437
+ status: "canceled",
1438
+ reason: "Timeout"
1439
+ }
1440
+ }
1441
+ };
1442
+ });
1443
+ }, 1e3 * 60 * 4.5);
1444
+ logger.debug("Permission request" + id + " " + JSON.stringify(request));
1445
+ session.api.push().sendToAllDevices(
1446
+ "Permission Request",
1447
+ `Claude wants to use ${request.name}`,
1448
+ {
1449
+ sessionId: session.client.sessionId,
1450
+ requestId: id,
1451
+ tool: request.name,
1452
+ type: "permission_request"
978
1453
  }
979
- if (this.closed || abortSignal?.aborted) {
980
- if (abortHandler && abortSignal) {
981
- abortSignal.removeEventListener("abort", abortHandler);
1454
+ );
1455
+ session.client.updateAgentState((currentState) => ({
1456
+ ...currentState,
1457
+ requests: {
1458
+ ...currentState.requests,
1459
+ [id]: {
1460
+ tool: request.name,
1461
+ arguments: request.arguments,
1462
+ createdAt: Date.now()
982
1463
  }
983
- resolve(false);
984
- return;
985
1464
  }
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;
1465
+ }));
1466
+ promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
1467
+ return promise;
1005
1468
  }
1006
- invalidate() {
1007
- if (this._stopped) {
1008
- return;
1009
- }
1010
- if (!this._invalidated) {
1011
- this._invalidated = true;
1012
- this._invalidatedDouble = false;
1013
- this._doSync();
1469
+ session.client.setHandler("permission", async (message) => {
1470
+ logger.debug("Permission response" + JSON.stringify(message));
1471
+ const id = message.id;
1472
+ const resolve = requests.get(id);
1473
+ if (resolve) {
1474
+ responses.set(id, message);
1475
+ resolve({ approved: message.approved, reason: message.reason, mode: message.mode });
1476
+ requests.delete(id);
1014
1477
  } else {
1015
- if (!this._invalidatedDouble) {
1016
- this._invalidatedDouble = true;
1017
- }
1018
- }
1019
- }
1020
- async invalidateAndAwait() {
1021
- if (this._stopped) {
1478
+ logger.debug("Permission request stale, likely timed out");
1022
1479
  return;
1023
1480
  }
1024
- await new Promise((resolve) => {
1025
- this._pendings.push(resolve);
1026
- this.invalidate();
1481
+ session.client.updateAgentState((currentState) => {
1482
+ const request = currentState.requests?.[id];
1483
+ if (!request) return currentState;
1484
+ let r = { ...currentState.requests };
1485
+ delete r[id];
1486
+ const isExitPlanModeSuccess = request.tool === "exit_plan_mode" && !message.approved && message.reason === PLAN_FAKE_REJECT;
1487
+ return {
1488
+ ...currentState,
1489
+ requests: r,
1490
+ completedRequests: {
1491
+ ...currentState.completedRequests,
1492
+ [id]: {
1493
+ ...request,
1494
+ completedAt: Date.now(),
1495
+ status: isExitPlanModeSuccess ? "approved" : message.approved ? "approved" : "denied",
1496
+ reason: isExitPlanModeSuccess ? "Plan approved" : message.reason
1497
+ }
1498
+ }
1499
+ };
1027
1500
  });
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();
1501
+ });
1502
+ const resolveToolCallId = (name, args) => {
1503
+ for (let i = toolCalls.length - 1; i >= 0; i--) {
1504
+ const call = toolCalls[i];
1505
+ if (call.name === name && deepEqual(call.input, args)) {
1506
+ if (call.used) {
1507
+ return null;
1508
+ }
1509
+ call.used = true;
1510
+ return call.id;
1511
+ }
1039
1512
  }
1040
- this._pendings = [];
1513
+ return null;
1041
1514
  };
1042
- _doSync = async () => {
1043
- await backoff(async () => {
1044
- if (this._stopped) {
1045
- return;
1515
+ function reset() {
1516
+ toolCalls = [];
1517
+ requests.clear();
1518
+ responses.clear();
1519
+ for (const pending of pendingPermissionRequests) {
1520
+ clearTimeout(pending.timeout);
1521
+ }
1522
+ pendingPermissionRequests = [];
1523
+ session.client.updateAgentState((currentState) => {
1524
+ const pendingRequests = currentState.requests || {};
1525
+ const completedRequests = { ...currentState.completedRequests };
1526
+ for (const [id, request] of Object.entries(pendingRequests)) {
1527
+ completedRequests[id] = {
1528
+ ...request,
1529
+ completedAt: Date.now(),
1530
+ status: "canceled",
1531
+ reason: "Session switched to local mode"
1532
+ };
1046
1533
  }
1047
- await this._command();
1534
+ return {
1535
+ ...currentState,
1536
+ requests: {},
1537
+ // Clear all pending requests
1538
+ completedRequests
1539
+ };
1048
1540
  });
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;
1541
+ }
1542
+ function onMessage(message) {
1543
+ if (message.type === "assistant") {
1544
+ const assistantMsg = message;
1545
+ if (assistantMsg.message && assistantMsg.message.content) {
1546
+ for (const block of assistantMsg.message.content) {
1547
+ if (block.type === "tool_use") {
1548
+ toolCalls.push({
1549
+ id: block.id,
1550
+ name: block.name,
1551
+ input: block.input,
1552
+ used: false
1553
+ });
1554
+ for (let i = pendingPermissionRequests.length - 1; i >= 0; i--) {
1555
+ const pending = pendingPermissionRequests[i];
1556
+ if (pending.request.name === block.name && deepEqual(pending.request.arguments, block.input)) {
1557
+ logger.debug(`Resolving pending permission request for ${block.name} with ID ${block.id}`);
1558
+ clearTimeout(pending.timeout);
1559
+ pendingPermissionRequests.splice(i, 1);
1560
+ handlePermissionRequest(block.id, pending.request).then(
1561
+ pending.resolve,
1562
+ pending.reject
1563
+ );
1564
+ break;
1565
+ }
1566
+ }
1073
1567
  }
1074
- logger.debug(`[FILE_WATCHER] File changed: ${file}`);
1075
- onFileChange(file);
1076
1568
  }
1077
- } catch (e) {
1078
- if (abortController.signal.aborted) {
1079
- return;
1569
+ }
1570
+ }
1571
+ if (message.type === "user") {
1572
+ const userMsg = message;
1573
+ if (userMsg.message && userMsg.message.content && Array.isArray(userMsg.message.content)) {
1574
+ for (const block of userMsg.message.content) {
1575
+ if (block.type === "tool_result" && block.tool_use_id) {
1576
+ const toolCall = toolCalls.find((tc) => tc.id === block.tool_use_id);
1577
+ if (toolCall && !toolCall.used) {
1578
+ toolCall.used = true;
1579
+ }
1580
+ }
1080
1581
  }
1081
- logger.debug(`[FILE_WATCHER] Watch error: ${e.message}, restarting watcher in a second`);
1082
- await delay(1e3);
1083
1582
  }
1084
1583
  }
1085
- })();
1086
- return () => {
1087
- abortController.abort();
1584
+ }
1585
+ return {
1586
+ server,
1587
+ reset,
1588
+ onMessage,
1589
+ responses
1088
1590
  };
1089
1591
  }
1090
1592
 
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
- };
1593
+ function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
1594
+ logger.debugLargeJson("[CLAUDE INK] Message from remote mode:", message);
1595
+ switch (message.type) {
1596
+ case "system": {
1597
+ const sysMsg = message;
1598
+ if (sysMsg.subtype === "init") {
1599
+ messageBuffer.addMessage("\u2500".repeat(40), "status");
1600
+ messageBuffer.addMessage(`\u{1F680} Session initialized: ${sysMsg.session_id}`, "system");
1601
+ messageBuffer.addMessage(` Model: ${sysMsg.model}`, "status");
1602
+ messageBuffer.addMessage(` CWD: ${sysMsg.cwd}`, "status");
1603
+ if (sysMsg.tools && sysMsg.tools.length > 0) {
1604
+ messageBuffer.addMessage(` Tools: ${sysMsg.tools.join(", ")}`, "status");
1108
1605
  }
1606
+ messageBuffer.addMessage("\u2500".repeat(40), "status");
1109
1607
  }
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);
1608
+ break;
1140
1609
  }
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;
1610
+ case "user": {
1611
+ const userMsg = message;
1612
+ if (userMsg.message && typeof userMsg.message === "object" && "content" in userMsg.message) {
1613
+ const content = userMsg.message.content;
1614
+ if (typeof content === "string") {
1615
+ messageBuffer.addMessage(`\u{1F464} User: ${content}`, "user");
1616
+ } else if (Array.isArray(content)) {
1617
+ for (const block of content) {
1618
+ if (block.type === "text") {
1619
+ messageBuffer.addMessage(`\u{1F464} User: ${block.text}`, "user");
1620
+ } else if (block.type === "tool_result") {
1621
+ messageBuffer.addMessage(`\u2705 Tool Result (ID: ${block.tool_use_id})`, "result");
1622
+ if (block.content) {
1623
+ const outputStr = typeof block.content === "string" ? block.content : JSON.stringify(block.content, null, 2);
1624
+ const maxLength = 200;
1625
+ if (outputStr.length > maxLength) {
1626
+ messageBuffer.addMessage(outputStr.substring(0, maxLength) + "... (truncated)", "result");
1627
+ } else {
1628
+ messageBuffer.addMessage(outputStr, "result");
1629
+ }
1630
+ }
1631
+ }
1165
1632
  }
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;
1633
+ } else {
1634
+ messageBuffer.addMessage(`\u{1F464} User: ${JSON.stringify(content, null, 2)}`, "user");
1635
+ }
1636
+ }
1637
+ break;
1638
+ }
1639
+ case "assistant": {
1640
+ const assistantMsg = message;
1641
+ if (assistantMsg.message && assistantMsg.message.content) {
1642
+ messageBuffer.addMessage("\u{1F916} Assistant:", "assistant");
1643
+ for (const block of assistantMsg.message.content) {
1644
+ if (block.type === "text") {
1645
+ messageBuffer.addMessage(block.text || "", "assistant");
1646
+ } else if (block.type === "tool_use") {
1647
+ messageBuffer.addMessage(`\u{1F527} Tool: ${block.name}`, "tool");
1648
+ if (block.input) {
1649
+ const inputStr = JSON.stringify(block.input, null, 2);
1650
+ const maxLength = 500;
1651
+ if (inputStr.length > maxLength) {
1652
+ messageBuffer.addMessage(`Input: ${inputStr.substring(0, maxLength)}... (truncated)`, "tool");
1653
+ } else {
1654
+ messageBuffer.addMessage(`Input: ${inputStr}`, "tool");
1655
+ }
1174
1656
  }
1175
1657
  }
1176
- const hackedMessage = hackToolResponse(message);
1177
- opts.onMessage(hackedMessage);
1178
- } catch (e) {
1179
- logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
1180
- continue;
1181
1658
  }
1182
1659
  }
1183
- };
1184
- for (let session of sessions) {
1185
- await processSessionFile(session);
1660
+ break;
1186
1661
  }
1187
- for (let p of sessions) {
1188
- if (pendingSessions.has(p)) {
1189
- pendingSessions.delete(p);
1190
- finishedSessions.add(p);
1662
+ case "result": {
1663
+ const resultMsg = message;
1664
+ if (resultMsg.subtype === "success") {
1665
+ if ("result" in resultMsg && resultMsg.result) {
1666
+ messageBuffer.addMessage("\u2728 Summary:", "result");
1667
+ messageBuffer.addMessage(resultMsg.result || "", "result");
1668
+ }
1669
+ if (resultMsg.usage) {
1670
+ messageBuffer.addMessage("\u{1F4CA} Session Stats:", "status");
1671
+ messageBuffer.addMessage(` \u2022 Turns: ${resultMsg.num_turns}`, "status");
1672
+ messageBuffer.addMessage(` \u2022 Input tokens: ${resultMsg.usage.input_tokens}`, "status");
1673
+ messageBuffer.addMessage(` \u2022 Output tokens: ${resultMsg.usage.output_tokens}`, "status");
1674
+ if (resultMsg.usage.cache_read_input_tokens) {
1675
+ messageBuffer.addMessage(` \u2022 Cache read tokens: ${resultMsg.usage.cache_read_input_tokens}`, "status");
1676
+ }
1677
+ if (resultMsg.usage.cache_creation_input_tokens) {
1678
+ messageBuffer.addMessage(` \u2022 Cache creation tokens: ${resultMsg.usage.cache_creation_input_tokens}`, "status");
1679
+ }
1680
+ messageBuffer.addMessage(` \u2022 Cost: $${resultMsg.total_cost_usd.toFixed(4)}`, "status");
1681
+ messageBuffer.addMessage(` \u2022 Duration: ${resultMsg.duration_ms}ms`, "status");
1682
+ }
1683
+ } else if (resultMsg.subtype === "error_max_turns") {
1684
+ messageBuffer.addMessage("\u274C Error: Maximum turns reached", "result");
1685
+ messageBuffer.addMessage(`Completed ${resultMsg.num_turns} turns`, "status");
1686
+ } else if (resultMsg.subtype === "error_during_execution") {
1687
+ messageBuffer.addMessage("\u274C Error during execution", "result");
1688
+ messageBuffer.addMessage(`Completed ${resultMsg.num_turns} turns before error`, "status");
1689
+ logger.debugLargeJson("[RESULT] Error during execution", resultMsg);
1191
1690
  }
1691
+ break;
1192
1692
  }
1193
- for (let p of sessions) {
1194
- if (!watchers.has(p)) {
1195
- watchers.set(p, startFileWatcher(join(projectDir, `${p}.jsonl`), () => {
1196
- sync.invalidate();
1197
- }));
1693
+ default: {
1694
+ if (process.env.DEBUG) {
1695
+ messageBuffer.addMessage(`[Unknown message type: ${message.type}]`, "status");
1198
1696
  }
1199
1697
  }
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();
1698
+ }
1699
+ }
1700
+
1701
+ function getGitBranch(cwd) {
1702
+ try {
1703
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
1704
+ cwd,
1705
+ encoding: "utf8",
1706
+ stdio: ["ignore", "pipe", "ignore"]
1707
+ }).trim();
1708
+ return branch || void 0;
1709
+ } catch {
1710
+ return void 0;
1711
+ }
1712
+ }
1713
+ class SDKToLogConverter {
1714
+ lastUuid = null;
1715
+ context;
1716
+ responses;
1717
+ sidechainLastUUID = /* @__PURE__ */ new Map();
1718
+ constructor(context, responses) {
1719
+ this.context = {
1720
+ ...context,
1721
+ gitBranch: context.gitBranch ?? getGitBranch(context.cwd),
1722
+ version: context.version ?? process.env.npm_package_version ?? "0.0.0",
1723
+ parentUuid: null
1724
+ };
1725
+ this.responses = responses;
1726
+ }
1727
+ /**
1728
+ * Update session ID (for when session changes during resume)
1729
+ */
1730
+ updateSessionId(sessionId) {
1731
+ this.context.sessionId = sessionId;
1732
+ }
1733
+ /**
1734
+ * Reset parent chain (useful when starting new conversation)
1735
+ */
1736
+ resetParentChain() {
1737
+ this.lastUuid = null;
1738
+ this.context.parentUuid = null;
1739
+ }
1740
+ /**
1741
+ * Convert SDK message to log format
1742
+ */
1743
+ convert(sdkMessage) {
1744
+ const uuid = randomUUID();
1745
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1746
+ let parentUuid = this.lastUuid;
1747
+ let isSidechain = false;
1748
+ if (sdkMessage.parent_tool_use_id) {
1749
+ isSidechain = true;
1750
+ parentUuid = this.sidechainLastUUID.get(sdkMessage.parent_tool_use_id) ?? null;
1751
+ this.sidechainLastUUID.set(sdkMessage.parent_tool_use_id, uuid);
1752
+ }
1753
+ const baseFields = {
1754
+ parentUuid,
1755
+ isSidechain,
1756
+ userType: "external",
1757
+ cwd: this.context.cwd,
1758
+ sessionId: this.context.sessionId,
1759
+ version: this.context.version,
1760
+ gitBranch: this.context.gitBranch,
1761
+ uuid,
1762
+ timestamp
1763
+ };
1764
+ let logMessage = null;
1765
+ switch (sdkMessage.type) {
1766
+ case "user": {
1767
+ const userMsg = sdkMessage;
1768
+ logMessage = {
1769
+ ...baseFields,
1770
+ type: "user",
1771
+ message: userMsg.message
1772
+ };
1773
+ if (Array.isArray(userMsg.message.content)) {
1774
+ for (const content of userMsg.message.content) {
1775
+ if (content.type === "tool_result" && content.tool_use_id && this.responses?.has(content.tool_use_id)) {
1776
+ const response = this.responses.get(content.tool_use_id);
1777
+ if (response?.mode) {
1778
+ logMessage.mode = response.mode;
1779
+ }
1780
+ }
1781
+ }
1782
+ } else if (typeof userMsg.message.content === "string") ;
1783
+ break;
1210
1784
  }
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;
1785
+ case "assistant": {
1786
+ const assistantMsg = sdkMessage;
1787
+ logMessage = {
1788
+ ...baseFields,
1789
+ type: "assistant",
1790
+ message: assistantMsg.message,
1791
+ // Assistant messages often have additional fields
1792
+ requestId: assistantMsg.requestId
1793
+ };
1794
+ break;
1217
1795
  }
1218
- if (finishedSessions.has(sessionId)) {
1219
- logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
1220
- return;
1796
+ case "system": {
1797
+ const systemMsg = sdkMessage;
1798
+ if (systemMsg.subtype === "init" && systemMsg.session_id) {
1799
+ this.updateSessionId(systemMsg.session_id);
1800
+ }
1801
+ logMessage = {
1802
+ ...baseFields,
1803
+ type: "system",
1804
+ subtype: systemMsg.subtype,
1805
+ model: systemMsg.model,
1806
+ tools: systemMsg.tools,
1807
+ // Include all other fields
1808
+ ...systemMsg
1809
+ };
1810
+ break;
1221
1811
  }
1222
- if (pendingSessions.has(sessionId)) {
1223
- logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
1224
- return;
1812
+ case "result": {
1813
+ break;
1225
1814
  }
1226
- if (currentSessionId) {
1227
- pendingSessions.add(currentSessionId);
1815
+ // Handle tool use results (often comes as user messages)
1816
+ case "tool_result": {
1817
+ const toolMsg = sdkMessage;
1818
+ const baseLogMessage = {
1819
+ ...baseFields,
1820
+ type: "user",
1821
+ message: {
1822
+ role: "user",
1823
+ content: [{
1824
+ type: "tool_result",
1825
+ tool_use_id: toolMsg.tool_use_id,
1826
+ content: toolMsg.content
1827
+ }]
1828
+ },
1829
+ toolUseResult: toolMsg.content
1830
+ };
1831
+ if (toolMsg.tool_use_id && this.responses?.has(toolMsg.tool_use_id)) {
1832
+ const response = this.responses.get(toolMsg.tool_use_id);
1833
+ if (response?.mode) {
1834
+ baseLogMessage.mode = response.mode;
1835
+ }
1836
+ }
1837
+ logMessage = baseLogMessage;
1838
+ break;
1228
1839
  }
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);
1840
+ default:
1841
+ logMessage = {
1842
+ ...baseFields,
1843
+ ...sdkMessage,
1844
+ type: sdkMessage.type
1845
+ // Override type last to ensure it's set
1846
+ };
1236
1847
  }
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)}`;
1848
+ if (logMessage && logMessage.type !== "summary") {
1849
+ this.lastUuid = uuid;
1245
1850
  }
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}`;
1851
+ return logMessage;
1253
1852
  }
1254
- return `unknown:<error, this should be unreachable>`;
1255
- }
1256
- function stableStringify(obj) {
1257
- if (!obj) {
1258
- return "null";
1853
+ /**
1854
+ * Convert multiple SDK messages to log format
1855
+ */
1856
+ convertMany(sdkMessages) {
1857
+ return sdkMessages.map((msg) => this.convert(msg)).filter((msg) => msg !== null);
1259
1858
  }
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;
1859
+ /**
1860
+ * Convert a simple string content to a sidechain user message
1861
+ * Used for Task tool sub-agent prompts
1862
+ */
1863
+ convertSidechainUserMessage(toolUseId, content) {
1864
+ const uuid = randomUUID();
1865
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1866
+ this.sidechainLastUUID.set(toolUseId, uuid);
1867
+ return {
1868
+ parentUuid: null,
1869
+ isSidechain: true,
1870
+ userType: "external",
1871
+ cwd: this.context.cwd,
1872
+ sessionId: this.context.sessionId,
1873
+ version: this.context.version,
1874
+ gitBranch: this.context.gitBranch,
1875
+ type: "user",
1876
+ message: {
1877
+ role: "user",
1878
+ content
1879
+ },
1880
+ uuid,
1881
+ timestamp
1882
+ };
1883
+ }
1884
+ /**
1885
+ * Generate an interrupted tool result message
1886
+ * Used when a tool call is interrupted by the user
1887
+ * @param toolUseId - The ID of the tool that was interrupted
1888
+ * @param parentToolUseId - Optional parent tool ID if this is a sidechain tool
1889
+ */
1890
+ generateInterruptedToolResult(toolUseId, parentToolUseId) {
1891
+ const uuid = randomUUID();
1892
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1893
+ const errorMessage = "[Request interrupted by user for tool use]";
1894
+ let isSidechain = false;
1895
+ let parentUuid = this.lastUuid;
1896
+ if (parentToolUseId) {
1897
+ isSidechain = true;
1898
+ parentUuid = this.sidechainLastUUID.get(parentToolUseId) ?? null;
1899
+ this.sidechainLastUUID.set(parentToolUseId, uuid);
1900
+ }
1901
+ const logMessage = {
1902
+ type: "user",
1903
+ isSidechain,
1904
+ uuid,
1905
+ message: {
1906
+ role: "user",
1907
+ content: [
1908
+ {
1909
+ type: "tool_result",
1910
+ content: errorMessage,
1911
+ is_error: true,
1912
+ tool_use_id: toolUseId
1913
+ }
1914
+ ]
1915
+ },
1916
+ parentUuid,
1917
+ userType: "external",
1918
+ cwd: this.context.cwd,
1919
+ sessionId: this.context.sessionId,
1920
+ version: this.context.version,
1921
+ gitBranch: this.context.gitBranch,
1922
+ timestamp,
1923
+ toolUseResult: `Error: ${errorMessage}`
1924
+ };
1925
+ this.lastUuid = uuid;
1926
+ return logMessage;
1272
1927
  }
1273
1928
  }
1274
1929
 
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);
1930
+ async function claudeRemoteLauncher(session) {
1931
+ let messageBuffer = new MessageBuffer();
1932
+ console.clear();
1933
+ let inkInstance = render(React.createElement(RemoteModeDisplay, {
1934
+ messageBuffer,
1935
+ logPath: process.env.DEBUG ? session.logPath : void 0,
1936
+ onExit: async () => {
1937
+ logger.debug("[remote]: Exiting client via Ctrl-C");
1938
+ if (!exitReason) {
1939
+ exitReason = "exit";
1940
+ }
1941
+ await abort();
1942
+ },
1943
+ onSwitchToLocal: () => {
1944
+ logger.debug("[remote]: Switching to local mode via double space");
1945
+ doSwitch();
1289
1946
  }
1947
+ }), {
1948
+ exitOnCtrlC: false,
1949
+ patchConsole: false
1290
1950
  });
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}`);
1951
+ process.stdin.resume();
1952
+ if (process.stdin.isTTY) {
1953
+ process.stdin.setRawMode(true);
1954
+ }
1955
+ process.stdin.setEncoding("utf8");
1956
+ const scanner = await createSessionScanner({
1957
+ sessionId: session.sessionId,
1958
+ workingDirectory: session.path,
1959
+ onMessage: (message) => {
1960
+ if (message.type === "summary") {
1961
+ session.client.sendClaudeSessionMessage(message);
1302
1962
  }
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
1963
  }
1311
1964
  });
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);
1965
+ let exitReason = null;
1966
+ let abortController = null;
1967
+ let abortFuture = null;
1968
+ async function abort() {
1969
+ if (abortController && !abortController.signal.aborted) {
1970
+ abortController.abort();
1971
+ }
1972
+ await abortFuture?.promise;
1973
+ }
1974
+ async function doAbort() {
1975
+ logger.debug("[remote]: doAbort");
1976
+ await abort();
1977
+ }
1978
+ async function doSwitch() {
1979
+ logger.debug("[remote]: doSwitch");
1980
+ if (!exitReason) {
1981
+ exitReason = "switch";
1982
+ }
1983
+ await abort();
1984
+ }
1985
+ session.client.setHandler("abort", doAbort);
1986
+ session.client.setHandler("switch", doSwitch);
1987
+ const permissions = await startPermissionResolver(session);
1988
+ const sdkToLogConverter = new SDKToLogConverter({
1989
+ sessionId: session.sessionId || "unknown",
1990
+ cwd: session.path,
1991
+ version: process.env.npm_package_version
1992
+ }, permissions.responses);
1993
+ let planModeToolCalls = /* @__PURE__ */ new Set();
1994
+ let ongoingToolCalls = /* @__PURE__ */ new Map();
1995
+ function onMessage(message) {
1996
+ formatClaudeMessageForInk(message, messageBuffer);
1997
+ permissions.onMessage(message);
1998
+ if (message.type === "assistant") {
1999
+ let umessage = message;
2000
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2001
+ for (let c of umessage.message.content) {
2002
+ if (c.type === "tool_use" && (c.name === "exit_plan_mode" || c.name === "ExitPlanMode")) {
2003
+ logger.debug("[remote]: detected plan mode tool call " + c.id);
2004
+ planModeToolCalls.add(c.id);
2005
+ }
1323
2006
  }
1324
2007
  }
1325
2008
  }
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
- }
2009
+ if (message.type === "assistant") {
2010
+ let umessage = message;
2011
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2012
+ for (let c of umessage.message.content) {
2013
+ if (c.type === "tool_use") {
2014
+ logger.debug("[remote]: detected tool use " + c.id + " parent: " + umessage.parent_tool_use_id);
2015
+ ongoingToolCalls.set(c.id, { parentToolCallId: umessage.parent_tool_use_id ?? null });
1337
2016
  }
1338
- interactiveAbortController.abort();
1339
- }
1340
- });
1341
- opts.session.setHandler("abort", () => {
1342
- if (onMessage) {
1343
- onMessage();
1344
2017
  }
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
- }
2018
+ }
2019
+ }
2020
+ if (message.type === "user") {
2021
+ let umessage = message;
2022
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2023
+ for (let c of umessage.message.content) {
2024
+ if (c.type === "tool_result" && c.tool_use_id) {
2025
+ ongoingToolCalls.delete(c.tool_use_id);
1354
2026
  }
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
2027
  }
1381
2028
  }
1382
- onMessage = null;
1383
- if (!abortedOutside) {
1384
- return;
2029
+ }
2030
+ let msg = message;
2031
+ if (message.type === "user") {
2032
+ let umessage = message;
2033
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2034
+ msg = {
2035
+ ...umessage,
2036
+ message: {
2037
+ ...umessage.message,
2038
+ content: umessage.message.content.map((c) => {
2039
+ if (c.type === "tool_result" && c.tool_use_id && planModeToolCalls.has(c.tool_use_id)) {
2040
+ if (c.content === PLAN_FAKE_REJECT) {
2041
+ logger.debug("[remote]: hack plan mode exit");
2042
+ logger.debugLargeJson("[remote]: hack plan mode exit", c);
2043
+ return {
2044
+ ...c,
2045
+ is_error: false,
2046
+ content: "Plan approved",
2047
+ mode: c.mode
2048
+ };
2049
+ } else {
2050
+ return c;
2051
+ }
2052
+ }
2053
+ return c;
2054
+ })
2055
+ }
2056
+ };
1385
2057
  }
1386
- if (mode !== "local") {
1387
- console.log("Switching to remote mode...");
2058
+ }
2059
+ const logMessage = sdkToLogConverter.convert(msg);
2060
+ if (logMessage) {
2061
+ if (logMessage.type !== "system") {
2062
+ session.client.sendClaudeSessionMessage(logMessage);
1388
2063
  }
1389
2064
  }
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);
2065
+ if (message.type === "assistant") {
2066
+ let umessage = message;
2067
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2068
+ for (let c of umessage.message.content) {
2069
+ if (c.type === "tool_use" && c.name === "Task" && c.input && typeof c.input.prompt === "string") {
2070
+ const logMessage2 = sdkToLogConverter.convertSidechainUserMessage(c.id, c.input.prompt);
2071
+ if (logMessage2) {
2072
+ session.client.sendClaudeSessionMessage(logMessage2);
1405
2073
  }
1406
2074
  }
1407
- opts.session.sendSessionEvent({ type: "message", message: "Inference aborted" });
1408
- remoteAbortController.abort();
1409
- }
1410
- if (process.stdin.isTTY) {
1411
- process.stdin.setRawMode(false);
1412
2075
  }
1413
- };
1414
- process.stdin.resume();
1415
- if (process.stdin.isTTY) {
1416
- process.stdin.setRawMode(true);
1417
2076
  }
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");
2077
+ }
2078
+ }
2079
+ try {
2080
+ while (!exitReason) {
2081
+ logger.debug("[remote]: fetch next message");
2082
+ abortController = new AbortController();
2083
+ abortFuture = new Future();
2084
+ const messageData = await session.queue.waitForMessagesAndGetAsString(abortController.signal);
2085
+ if (!messageData || abortController.signal.aborted) {
2086
+ logger.debug("[remote]: fetch next message done: no message or aborted");
2087
+ abortFuture?.resolve(void 0);
2088
+ if (exitReason) {
2089
+ return exitReason;
2090
+ } else {
1427
2091
  continue;
1428
2092
  }
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}`);
2093
+ }
2094
+ logger.debug("[remote]: fetch next message done: message received");
2095
+ abortFuture?.resolve(void 0);
2096
+ abortFuture = null;
2097
+ abortController = null;
2098
+ logger.debug("[remote]: launch");
2099
+ messageBuffer.addMessage("\u2550".repeat(40), "status");
2100
+ messageBuffer.addMessage("Starting new Claude session...", "status");
2101
+ abortController = new AbortController();
2102
+ abortFuture = new Future();
2103
+ permissions.reset();
2104
+ sdkToLogConverter.resetParentChain();
2105
+ try {
1436
2106
  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,
2107
+ sessionId: session.sessionId,
2108
+ path: session.path,
2109
+ responses: permissions.responses,
2110
+ mcpServers: {
2111
+ ...session.mcpServers,
2112
+ permission: {
2113
+ type: "http",
2114
+ url: permissions.server.url
2115
+ }
2116
+ },
2117
+ permissionPromptToolName: "mcp__permission__" + permissions.server.toolName,
2118
+ permissionMode: messageData.mode,
2119
+ onSessionFound: (sessionId) => {
2120
+ sdkToLogConverter.updateSessionId(sessionId);
2121
+ session.onSessionFound(sessionId);
2122
+ scanner.onNewSession(sessionId);
2123
+ },
2124
+ onThinkingChange: session.onThinkingChange,
1445
2125
  message: messageData.message,
1446
- onAssistantResult: opts.onAssistantResult,
1447
- interruptController: opts.interruptController,
1448
- claudeEnvVars: opts.claudeEnvVars,
1449
- claudeArgs: opts.claudeArgs,
1450
- onToolCallResolver: opts.onToolCallResolver
2126
+ claudeEnvVars: session.claudeEnvVars,
2127
+ claudeArgs: session.claudeArgs,
2128
+ onMessage,
2129
+ signal: abortController.signal
1451
2130
  });
2131
+ if (!exitReason && abortController.signal.aborted) {
2132
+ session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
2133
+ }
1452
2134
  } catch (e) {
1453
- if (!remoteAbortController.signal.aborted) {
1454
- opts.session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
2135
+ if (!exitReason) {
2136
+ session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
2137
+ continue;
1455
2138
  }
1456
2139
  } 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)
2140
+ for (let [toolCallId, { parentToolCallId }] of ongoingToolCalls) {
2141
+ const converted = sdkToLogConverter.generateInterruptedToolResult(toolCallId, parentToolCallId);
2142
+ if (converted) {
2143
+ logger.debug("[remote]: terminating tool call " + toolCallId + " parent: " + parentToolCallId);
2144
+ session.client.sendClaudeSessionMessage(converted);
2145
+ }
1497
2146
  }
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();
2147
+ ongoingToolCalls.clear();
2148
+ abortController = null;
2149
+ abortFuture?.resolve(void 0);
2150
+ abortFuture = null;
2151
+ logger.debug("[remote]: launch done");
2152
+ permissions.reset();
1515
2153
  }
1516
2154
  }
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;
2155
+ } finally {
2156
+ permissions.server.stop();
2157
+ process.stdin.off("data", abort);
2158
+ if (process.stdin.isTTY) {
2159
+ process.stdin.setRawMode(false);
1552
2160
  }
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;
2161
+ inkInstance.unmount();
2162
+ messageBuffer.clear();
2163
+ if (abortFuture) {
2164
+ abortFuture.resolve(void 0);
1562
2165
  }
2166
+ await scanner.cleanup();
1563
2167
  }
1564
- /**
1565
- * Check if interrupt is available
1566
- */
1567
- canInterrupt() {
1568
- return !!this.interruptFn && !this.isInterrupting;
2168
+ return exitReason || "exit";
2169
+ }
2170
+
2171
+ async function loop(opts) {
2172
+ const logPath = await logger.logFilePathPromise;
2173
+ let session = new Session({
2174
+ api: opts.api,
2175
+ client: opts.session,
2176
+ path: opts.path,
2177
+ sessionId: null,
2178
+ claudeEnvVars: opts.claudeEnvVars,
2179
+ claudeArgs: opts.claudeArgs,
2180
+ mcpServers: opts.mcpServers,
2181
+ logPath,
2182
+ messageQueue: opts.messageQueue,
2183
+ onModeChange: opts.onModeChange
2184
+ });
2185
+ let mode = opts.startingMode ?? "local";
2186
+ while (true) {
2187
+ logger.debug(`[loop] Iteration with mode: ${mode}`);
2188
+ if (mode === "local") {
2189
+ let reason = await claudeLocalLauncher(session);
2190
+ if (reason === "exit") {
2191
+ return;
2192
+ }
2193
+ mode = "remote";
2194
+ if (opts.onModeChange) {
2195
+ opts.onModeChange(mode);
2196
+ }
2197
+ continue;
2198
+ }
2199
+ if (mode === "remote") {
2200
+ let reason = await claudeRemoteLauncher(session);
2201
+ if (reason === "exit") {
2202
+ return;
2203
+ }
2204
+ mode = "local";
2205
+ if (opts.onModeChange) {
2206
+ opts.onModeChange(mode);
2207
+ }
2208
+ continue;
2209
+ }
1569
2210
  }
1570
2211
  }
1571
2212
 
1572
- var version = "0.2.2";
2213
+ var name = "happy-coder";
2214
+ var version = "0.2.3-beta.1";
2215
+ var description = "Claude Code session sharing CLI";
2216
+ var author = "Kirill Dubovitskiy";
2217
+ var license = "MIT";
2218
+ var type = "module";
2219
+ var homepage = "https://github.com/slopus/happy-cli";
2220
+ var bugs = "https://github.com/slopus/happy-cli/issues";
2221
+ var repository = "slopus/happy-cli";
2222
+ var bin = {
2223
+ happy: "./bin/happy"
2224
+ };
2225
+ var main = "./dist/index.cjs";
2226
+ var module = "./dist/index.mjs";
2227
+ var types = "./dist/index.d.cts";
2228
+ var exports = {
2229
+ ".": {
2230
+ require: {
2231
+ types: "./dist/index.d.cts",
2232
+ "default": "./dist/index.cjs"
2233
+ },
2234
+ "import": {
2235
+ types: "./dist/index.d.mts",
2236
+ "default": "./dist/index.mjs"
2237
+ }
2238
+ },
2239
+ "./lib": {
2240
+ require: {
2241
+ types: "./dist/lib.d.cts",
2242
+ "default": "./dist/lib.cjs"
2243
+ },
2244
+ "import": {
2245
+ types: "./dist/lib.d.mts",
2246
+ "default": "./dist/lib.mjs"
2247
+ }
2248
+ }
2249
+ };
2250
+ var files = [
2251
+ "dist",
2252
+ "bin",
2253
+ "scripts",
2254
+ "ripgrep",
2255
+ "package.json"
2256
+ ];
2257
+ var scripts = {
2258
+ test: "vitest run",
2259
+ "test:watch": "vitest",
2260
+ build: "tsc --noEmit && pkgroll",
2261
+ prepublishOnly: "yarn build && yarn test",
2262
+ typecheck: "tsc --noEmit",
2263
+ dev: "npx tsx --env-file .env.sample src/index.ts",
2264
+ "dev:local-server": "HANDY_SERVER_URL=http://localhost:3005 npx tsx --env-file .env.sample src/index.ts",
2265
+ prerelease: "npm version prerelease --preid=beta"
2266
+ };
2267
+ var dependencies = {
2268
+ "@anthropic-ai/claude-code": "^1.0.72",
2269
+ "@anthropic-ai/sdk": "^0.56.0",
2270
+ "@modelcontextprotocol/sdk": "^1.15.1",
2271
+ "@stablelib/base64": "^2.0.1",
2272
+ "@types/http-proxy": "^1.17.16",
2273
+ "@types/qrcode-terminal": "^0.12.2",
2274
+ "@types/react": "^19.1.9",
2275
+ axios: "^1.10.0",
2276
+ chalk: "^5.4.1",
2277
+ "expo-server-sdk": "^3.15.0",
2278
+ "http-proxy": "^1.18.1",
2279
+ "http-proxy-middleware": "^3.0.5",
2280
+ ink: "^6.1.0",
2281
+ "ink-box": "^2.0.0",
2282
+ "qrcode-terminal": "^0.12.0",
2283
+ react: "^19.1.1",
2284
+ "socket.io-client": "^4.8.1",
2285
+ tweetnacl: "^1.0.3",
2286
+ zod: "^3.23.8"
2287
+ };
2288
+ var devDependencies = {
2289
+ "@eslint/compat": "^1",
2290
+ "@types/node": ">=18",
2291
+ eslint: "^9",
2292
+ "eslint-config-prettier": "^10",
2293
+ pkgroll: "^2.14.2",
2294
+ shx: "^0.3.3",
2295
+ "ts-node": "^10",
2296
+ tsx: "^4.20.3",
2297
+ typescript: "^5",
2298
+ vitest: "^3.2.4"
2299
+ };
2300
+ var overrides = {
2301
+ "whatwg-url": "14.2.0"
2302
+ };
1573
2303
  var packageJson = {
1574
- version: version};
2304
+ name: name,
2305
+ version: version,
2306
+ description: description,
2307
+ author: author,
2308
+ license: license,
2309
+ type: type,
2310
+ homepage: homepage,
2311
+ bugs: bugs,
2312
+ repository: repository,
2313
+ bin: bin,
2314
+ main: main,
2315
+ module: module,
2316
+ types: types,
2317
+ exports: exports,
2318
+ files: files,
2319
+ scripts: scripts,
2320
+ dependencies: dependencies,
2321
+ devDependencies: devDependencies,
2322
+ overrides: overrides
2323
+ };
1575
2324
 
1576
2325
  const __dirname = dirname$1(fileURLToPath$1(import.meta.url));
1577
2326
  const RUNNER_PATH = join$1(__dirname, "..", "..", "scripts", "ripgrep_launcher.cjs");
@@ -1603,51 +2352,9 @@ function run(args, options) {
1603
2352
  }
1604
2353
 
1605
2354
  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
- }
2355
+ function registerHandlers(session) {
1649
2356
  session.setHandler("bash", async (data) => {
1650
- logger.info("Shell command request:", data.command);
2357
+ logger.debug("Shell command request:", data.command);
1651
2358
  try {
1652
2359
  const options = {
1653
2360
  cwd: data.cwd,
@@ -1682,7 +2389,7 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1682
2389
  }
1683
2390
  });
1684
2391
  session.setHandler("readFile", async (data) => {
1685
- logger.info("Read file request:", data.path);
2392
+ logger.debug("Read file request:", data.path);
1686
2393
  try {
1687
2394
  const buffer = await readFile$1(data.path);
1688
2395
  const content = buffer.toString("base64");
@@ -1693,7 +2400,7 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1693
2400
  }
1694
2401
  });
1695
2402
  session.setHandler("writeFile", async (data) => {
1696
- logger.info("Write file request:", data.path);
2403
+ logger.debug("Write file request:", data.path);
1697
2404
  try {
1698
2405
  if (data.expectedHash !== null && data.expectedHash !== void 0) {
1699
2406
  try {
@@ -1739,7 +2446,7 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1739
2446
  }
1740
2447
  });
1741
2448
  session.setHandler("listDirectory", async (data) => {
1742
- logger.info("List directory request:", data.path);
2449
+ logger.debug("List directory request:", data.path);
1743
2450
  try {
1744
2451
  const entries = await readdir(data.path, { withFileTypes: true });
1745
2452
  const directoryEntries = await Promise.all(
@@ -1780,7 +2487,7 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1780
2487
  }
1781
2488
  });
1782
2489
  session.setHandler("getDirectoryTree", async (data) => {
1783
- logger.info("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
2490
+ logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
1784
2491
  async function buildTree(path, name, currentDepth) {
1785
2492
  try {
1786
2493
  const stats = await stat(path);
@@ -1836,7 +2543,7 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1836
2543
  }
1837
2544
  });
1838
2545
  session.setHandler("ripgrep", async (data) => {
1839
- logger.info("Ripgrep request with args:", data.args, "cwd:", data.cwd);
2546
+ logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
1840
2547
  try {
1841
2548
  const result = await run(data.args, { cwd: data.cwd });
1842
2549
  return {
@@ -1855,53 +2562,306 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1855
2562
  });
1856
2563
  }
1857
2564
 
1858
- const defaultSettings = {
1859
- onboardingCompleted: false
1860
- };
1861
- async function readSettings() {
1862
- if (!existsSync(configuration.settingsFile)) {
1863
- return { ...defaultSettings };
2565
+ const defaultSettings = {
2566
+ onboardingCompleted: false
2567
+ };
2568
+ async function readSettings() {
2569
+ if (!existsSync(configuration.settingsFile)) {
2570
+ return { ...defaultSettings };
2571
+ }
2572
+ try {
2573
+ const content = await readFile(configuration.settingsFile, "utf8");
2574
+ return JSON.parse(content);
2575
+ } catch {
2576
+ return { ...defaultSettings };
2577
+ }
2578
+ }
2579
+ async function writeSettings(settings) {
2580
+ if (!existsSync(configuration.happyDir)) {
2581
+ await mkdir(configuration.happyDir, { recursive: true });
2582
+ }
2583
+ await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
2584
+ }
2585
+ const credentialsSchema = z.object({
2586
+ secret: z.string().base64(),
2587
+ token: z.string()
2588
+ });
2589
+ async function readCredentials() {
2590
+ if (!existsSync(configuration.privateKeyFile)) {
2591
+ return null;
2592
+ }
2593
+ try {
2594
+ const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
2595
+ const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
2596
+ return {
2597
+ secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
2598
+ token: credentials.token
2599
+ };
2600
+ } catch {
2601
+ return null;
2602
+ }
2603
+ }
2604
+ async function writeCredentials(credentials) {
2605
+ if (!existsSync(configuration.happyDir)) {
2606
+ await mkdir(configuration.happyDir, { recursive: true });
2607
+ }
2608
+ await writeFile$1(configuration.privateKeyFile, JSON.stringify({
2609
+ secret: encodeBase64(credentials.secret),
2610
+ token: credentials.token
2611
+ }, null, 2));
2612
+ }
2613
+
2614
+ class MessageQueue2 {
2615
+ constructor(modeHasher) {
2616
+ this.modeHasher = modeHasher;
2617
+ logger.debug(`[MessageQueue2] Initialized`);
2618
+ }
2619
+ queue = [];
2620
+ waiter = null;
2621
+ closed = false;
2622
+ onMessageHandler = null;
2623
+ /**
2624
+ * Set a handler that will be called when a message arrives
2625
+ */
2626
+ setOnMessage(handler) {
2627
+ this.onMessageHandler = handler;
2628
+ }
2629
+ /**
2630
+ * Push a message to the queue with a mode.
2631
+ */
2632
+ push(message, mode) {
2633
+ if (this.closed) {
2634
+ throw new Error("Cannot push to closed queue");
2635
+ }
2636
+ const modeHash = this.modeHasher(mode);
2637
+ logger.debug(`[MessageQueue2] push() called with mode hash: ${modeHash}`);
2638
+ this.queue.push({
2639
+ message,
2640
+ mode,
2641
+ modeHash
2642
+ });
2643
+ if (this.onMessageHandler) {
2644
+ this.onMessageHandler(message, mode);
2645
+ }
2646
+ if (this.waiter) {
2647
+ logger.debug(`[MessageQueue2] Notifying waiter`);
2648
+ const waiter = this.waiter;
2649
+ this.waiter = null;
2650
+ waiter(true);
2651
+ }
2652
+ logger.debug(`[MessageQueue2] push() completed. Queue size: ${this.queue.length}`);
2653
+ }
2654
+ /**
2655
+ * Push a message to the beginning of the queue with a mode.
2656
+ */
2657
+ unshift(message, mode) {
2658
+ if (this.closed) {
2659
+ throw new Error("Cannot unshift to closed queue");
2660
+ }
2661
+ const modeHash = this.modeHasher(mode);
2662
+ logger.debug(`[MessageQueue2] unshift() called with mode hash: ${modeHash}`);
2663
+ this.queue.unshift({
2664
+ message,
2665
+ mode,
2666
+ modeHash
2667
+ });
2668
+ if (this.onMessageHandler) {
2669
+ this.onMessageHandler(message, mode);
2670
+ }
2671
+ if (this.waiter) {
2672
+ logger.debug(`[MessageQueue2] Notifying waiter`);
2673
+ const waiter = this.waiter;
2674
+ this.waiter = null;
2675
+ waiter(true);
2676
+ }
2677
+ logger.debug(`[MessageQueue2] unshift() completed. Queue size: ${this.queue.length}`);
2678
+ }
2679
+ /**
2680
+ * Reset the queue - clears all messages and resets to empty state
2681
+ */
2682
+ reset() {
2683
+ logger.debug(`[MessageQueue2] reset() called. Clearing ${this.queue.length} messages`);
2684
+ this.queue = [];
2685
+ this.closed = false;
2686
+ this.waiter = null;
2687
+ }
2688
+ /**
2689
+ * Close the queue - no more messages can be pushed
2690
+ */
2691
+ close() {
2692
+ logger.debug(`[MessageQueue2] close() called`);
2693
+ this.closed = true;
2694
+ if (this.waiter) {
2695
+ const waiter = this.waiter;
2696
+ this.waiter = null;
2697
+ waiter(false);
2698
+ }
2699
+ }
2700
+ /**
2701
+ * Check if the queue is closed
2702
+ */
2703
+ isClosed() {
2704
+ return this.closed;
2705
+ }
2706
+ /**
2707
+ * Get the current queue size
2708
+ */
2709
+ size() {
2710
+ return this.queue.length;
2711
+ }
2712
+ /**
2713
+ * Wait for messages and return all messages with the same mode as a single string
2714
+ * Returns { message: string, mode: T } or null if aborted/closed
2715
+ */
2716
+ async waitForMessagesAndGetAsString(abortSignal) {
2717
+ if (this.queue.length > 0) {
2718
+ return this.collectBatch();
2719
+ }
2720
+ if (this.closed || abortSignal?.aborted) {
2721
+ return null;
2722
+ }
2723
+ const hasMessages = await this.waitForMessages(abortSignal);
2724
+ if (!hasMessages) {
2725
+ return null;
2726
+ }
2727
+ return this.collectBatch();
2728
+ }
2729
+ /**
2730
+ * Collect a batch of messages with the same mode
2731
+ */
2732
+ collectBatch() {
2733
+ if (this.queue.length === 0) {
2734
+ return null;
2735
+ }
2736
+ const firstItem = this.queue[0];
2737
+ const sameModeMessages = [];
2738
+ let mode = firstItem.mode;
2739
+ const targetModeHash = firstItem.modeHash;
2740
+ while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash) {
2741
+ const item = this.queue.shift();
2742
+ sameModeMessages.push(item.message);
2743
+ }
2744
+ const combinedMessage = sameModeMessages.join("\n");
2745
+ logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
2746
+ return {
2747
+ message: combinedMessage,
2748
+ mode
2749
+ };
2750
+ }
2751
+ /**
2752
+ * Wait for messages to arrive
2753
+ */
2754
+ waitForMessages(abortSignal) {
2755
+ return new Promise((resolve) => {
2756
+ let abortHandler = null;
2757
+ if (abortSignal) {
2758
+ abortHandler = () => {
2759
+ logger.debug("[MessageQueue2] Wait aborted");
2760
+ if (this.waiter === waiterFunc) {
2761
+ this.waiter = null;
2762
+ }
2763
+ resolve(false);
2764
+ };
2765
+ abortSignal.addEventListener("abort", abortHandler);
2766
+ }
2767
+ const waiterFunc = (hasMessages) => {
2768
+ if (abortHandler && abortSignal) {
2769
+ abortSignal.removeEventListener("abort", abortHandler);
2770
+ }
2771
+ resolve(hasMessages);
2772
+ };
2773
+ if (this.queue.length > 0) {
2774
+ if (abortHandler && abortSignal) {
2775
+ abortSignal.removeEventListener("abort", abortHandler);
2776
+ }
2777
+ resolve(true);
2778
+ return;
2779
+ }
2780
+ if (this.closed || abortSignal?.aborted) {
2781
+ if (abortHandler && abortSignal) {
2782
+ abortSignal.removeEventListener("abort", abortHandler);
2783
+ }
2784
+ resolve(false);
2785
+ return;
2786
+ }
2787
+ this.waiter = waiterFunc;
2788
+ logger.debug("[MessageQueue2] Waiting for messages...");
2789
+ });
2790
+ }
2791
+ }
2792
+
2793
+ let caffeinateProcess = null;
2794
+ function startCaffeinate() {
2795
+ if (process.platform !== "darwin") {
2796
+ logger.debug("[caffeinate] Not on macOS, skipping caffeinate");
2797
+ return false;
1864
2798
  }
1865
- try {
1866
- const content = await readFile(configuration.settingsFile, "utf8");
1867
- return JSON.parse(content);
1868
- } catch {
1869
- return { ...defaultSettings };
2799
+ if (caffeinateProcess && !caffeinateProcess.killed) {
2800
+ logger.debug("[caffeinate] Caffeinate already running");
2801
+ return true;
1870
2802
  }
1871
- }
1872
- async function writeSettings(settings) {
1873
- if (!existsSync(configuration.happyDir)) {
1874
- await mkdir(configuration.happyDir, { recursive: true });
2803
+ try {
2804
+ caffeinateProcess = spawn$1("caffeinate", ["-dim"], {
2805
+ stdio: "ignore",
2806
+ detached: false
2807
+ });
2808
+ caffeinateProcess.on("error", (error) => {
2809
+ logger.debug("[caffeinate] Error starting caffeinate:", error);
2810
+ caffeinateProcess = null;
2811
+ });
2812
+ caffeinateProcess.on("exit", (code, signal) => {
2813
+ logger.debug(`[caffeinate] Process exited with code ${code}, signal ${signal}`);
2814
+ caffeinateProcess = null;
2815
+ });
2816
+ logger.debug(`[caffeinate] Started with PID ${caffeinateProcess.pid}`);
2817
+ setupCleanupHandlers();
2818
+ return true;
2819
+ } catch (error) {
2820
+ logger.debug("[caffeinate] Failed to start caffeinate:", error);
2821
+ return false;
1875
2822
  }
1876
- await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
1877
2823
  }
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;
2824
+ function stopCaffeinate() {
2825
+ if (caffeinateProcess && !caffeinateProcess.killed) {
2826
+ logger.debug(`[caffeinate] Stopping caffeinate process PID ${caffeinateProcess.pid}`);
2827
+ try {
2828
+ caffeinateProcess.kill("SIGTERM");
2829
+ setTimeout(() => {
2830
+ if (caffeinateProcess && !caffeinateProcess.killed) {
2831
+ logger.debug("[caffeinate] Force killing caffeinate process");
2832
+ caffeinateProcess.kill("SIGKILL");
2833
+ }
2834
+ caffeinateProcess = null;
2835
+ }, 1e3);
2836
+ } catch (error) {
2837
+ logger.debug("[caffeinate] Error stopping caffeinate:", error);
2838
+ }
1895
2839
  }
1896
2840
  }
1897
- async function writeCredentials(credentials) {
1898
- if (!existsSync(configuration.happyDir)) {
1899
- await mkdir(configuration.happyDir, { recursive: true });
2841
+ let cleanupHandlersSet = false;
2842
+ function setupCleanupHandlers() {
2843
+ if (cleanupHandlersSet) {
2844
+ return;
1900
2845
  }
1901
- await writeFile$1(configuration.privateKeyFile, JSON.stringify({
1902
- secret: encodeBase64(credentials.secret),
1903
- token: credentials.token
1904
- }, null, 2));
2846
+ cleanupHandlersSet = true;
2847
+ const cleanup = () => {
2848
+ stopCaffeinate();
2849
+ };
2850
+ process.on("exit", cleanup);
2851
+ process.on("SIGINT", cleanup);
2852
+ process.on("SIGTERM", cleanup);
2853
+ process.on("SIGUSR1", cleanup);
2854
+ process.on("SIGUSR2", cleanup);
2855
+ process.on("uncaughtException", (error) => {
2856
+ logger.debug("[caffeinate] Uncaught exception, cleaning up:", error);
2857
+ cleanup();
2858
+ process.exit(1);
2859
+ });
2860
+ process.on("unhandledRejection", (reason, promise) => {
2861
+ logger.debug("[caffeinate] Unhandled rejection, cleaning up:", reason);
2862
+ cleanup();
2863
+ process.exit(1);
2864
+ });
1905
2865
  }
1906
2866
 
1907
2867
  async function start(credentials, options = {}) {
@@ -1927,227 +2887,59 @@ async function start(credentials, options = {}) {
1927
2887
  console.log(`daemon:sessionIdCreated:${response.id}`);
1928
2888
  }
1929
2889
  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
2890
  const logPath = await logger.logFilePathPromise;
1937
2891
  logger.infoDeveloper(`Session: ${response.id}`);
1938
2892
  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);
2893
+ const caffeinateStarted = startCaffeinate();
2894
+ if (caffeinateStarted) {
2895
+ logger.infoDeveloper("Sleep prevention enabled (macOS)");
2896
+ }
2897
+ const messageQueue = new MessageQueue2((mode) => mode);
2898
+ registerHandlers(session);
2899
+ let currentPermissionMode = options.permissionMode;
2900
+ session.onUserMessage((message) => {
2901
+ let messagePermissionMode = currentPermissionMode;
2902
+ if (message.meta?.permissionMode) {
2903
+ const validModes = ["default", "acceptEdits", "bypassPermissions", "plan"];
2904
+ if (validModes.includes(message.meta.permissionMode)) {
2905
+ messagePermissionMode = message.meta.permissionMode;
2906
+ currentPermissionMode = messagePermissionMode;
2907
+ logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
1984
2908
  } 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");
2909
+ logger.debug(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
1993
2910
  }
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);
2911
+ } else {
2912
+ logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
2030
2913
  }
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;
2914
+ messageQueue.push(message.content.text, messagePermissionMode || "default");
2915
+ logger.debugLargeJson("User message pushed to queue:", message);
2044
2916
  });
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
2917
  await loop({
2066
2918
  path: workingDirectory,
2067
2919
  model: options.model,
2068
2920
  permissionMode: options.permissionMode,
2069
2921
  startingMode: options.startingMode,
2070
2922
  messageQueue,
2071
- sessionScanner,
2923
+ api,
2072
2924
  onModeChange: (newMode) => {
2073
- mode = newMode;
2074
2925
  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();
2926
+ session.updateAgentState((currentState) => ({
2927
+ ...currentState,
2928
+ controlledByUser: false
2929
+ }));
2118
2930
  },
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,
2931
+ mcpServers: {},
2137
2932
  session,
2138
- onAssistantResult,
2139
- interruptController,
2140
2933
  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
- }
2934
+ claudeArgs: options.claudeArgs
2149
2935
  });
2150
- clearInterval(pingInterval);
2936
+ session.sendSessionDeath();
2937
+ logger.debug("Waiting for socket to flush...");
2938
+ await session.flush();
2939
+ logger.debug("Closing session...");
2940
+ await session.close();
2941
+ stopCaffeinate();
2942
+ logger.debug("Stopped sleep prevention");
2151
2943
  process.exit(0);
2152
2944
  }
2153
2945
 
@@ -2233,7 +3025,7 @@ class ApiDaemonSession extends EventEmitter {
2233
3025
  this.token = token;
2234
3026
  this.secret = secret;
2235
3027
  this.machineIdentity = machineIdentity;
2236
- logger.daemonDebug(`Connecting to server: ${configuration.serverUrl}`);
3028
+ logger.debug(`[DAEMON SESSION] Connecting to server: ${configuration.serverUrl}`);
2237
3029
  const socket = io(configuration.serverUrl, {
2238
3030
  auth: {
2239
3031
  token: this.token,
@@ -2250,19 +3042,19 @@ class ApiDaemonSession extends EventEmitter {
2250
3042
  autoConnect: false
2251
3043
  });
2252
3044
  socket.on("connect", () => {
2253
- logger.daemonDebug("Socket connected");
2254
- logger.daemonDebug(`Connected with auth - token: ${this.token.substring(0, 10)}..., machineId: ${this.machineIdentity.machineId}`);
3045
+ logger.debug("[DAEMON SESSION] Socket connected");
3046
+ logger.debug(`[DAEMON SESSION] Connected with auth - token: ${this.token.substring(0, 10)}..., machineId: ${this.machineIdentity.machineId}`);
2255
3047
  const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
2256
3048
  socket.emit("rpc-register", { method: rpcMethod });
2257
- logger.daemonDebug(`Emitted RPC registration: ${rpcMethod}`);
3049
+ logger.debug(`[DAEMON SESSION] Emitted RPC registration: ${rpcMethod}`);
2258
3050
  this.emit("connected");
2259
3051
  this.startKeepAlive();
2260
3052
  });
2261
3053
  socket.on("rpc-request", async (data, callback) => {
2262
- logger.daemonDebug(`Received RPC request: ${JSON.stringify(data)}`);
3054
+ logger.debug(`[DAEMON SESSION] Received RPC request: ${JSON.stringify(data)}`);
2263
3055
  const expectedMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
2264
3056
  if (data.method === expectedMethod) {
2265
- logger.daemonDebug("Processing spawn-happy-session RPC");
3057
+ logger.debug("[DAEMON SESSION] Processing spawn-happy-session RPC");
2266
3058
  try {
2267
3059
  const { directory } = data.params || {};
2268
3060
  if (!directory) {
@@ -2277,26 +3069,25 @@ class ApiDaemonSession extends EventEmitter {
2277
3069
  if (configuration.installationLocation === "local") {
2278
3070
  args.push("--local");
2279
3071
  }
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(" ")}`);
3072
+ logger.debug(`[DAEMON SESSION] Spawning happy in directory: ${directory} with args: ${args.join(" ")}`);
2284
3073
  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], {
3074
+ const runningFromBuiltBinary = happyPath.endsWith("happy") || happyPath.endsWith("happy.cmd");
3075
+ let executable, spawnArgs;
3076
+ if (runningFromBuiltBinary) {
3077
+ executable = happyPath;
3078
+ spawnArgs = args;
3079
+ } else {
3080
+ executable = "npx";
3081
+ spawnArgs = ["tsx", happyPath, ...args];
3082
+ }
3083
+ const happyProcess = spawn$1(executable, spawnArgs, {
2293
3084
  cwd: directory,
2294
- env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
2295
3085
  detached: true,
2296
3086
  stdio: ["ignore", "pipe", "pipe"]
2297
3087
  // We need stdout
2298
3088
  });
2299
3089
  this.spawnedProcesses.add(happyProcess);
3090
+ this.updateChildPidsInMetadata();
2300
3091
  let sessionId = null;
2301
3092
  let output = "";
2302
3093
  let timeoutId = null;
@@ -2315,17 +3106,17 @@ class ApiDaemonSession extends EventEmitter {
2315
3106
  const match = output.match(/daemon:sessionIdCreated:(.+?)[\n\r]/);
2316
3107
  if (match && !sessionId) {
2317
3108
  sessionId = match[1];
2318
- logger.daemonDebug(`Session spawned successfully: ${sessionId}`);
3109
+ logger.debug(`[DAEMON SESSION] Session spawned successfully: ${sessionId}`);
2319
3110
  callback({ sessionId });
2320
3111
  cleanup();
2321
3112
  happyProcess.unref();
2322
3113
  }
2323
3114
  });
2324
3115
  happyProcess.stderr.on("data", (data2) => {
2325
- logger.daemonDebug(`Spawned process stderr: ${data2.toString()}`);
3116
+ logger.debug(`[DAEMON SESSION] Spawned process stderr: ${data2.toString()}`);
2326
3117
  });
2327
3118
  happyProcess.on("error", (error) => {
2328
- logger.daemonDebug("Error spawning session:", error);
3119
+ logger.debug("[DAEMON SESSION] Error spawning session:", error);
2329
3120
  if (!sessionId) {
2330
3121
  callback({ error: `Failed to spawn: ${error.message}` });
2331
3122
  cleanup();
@@ -2333,8 +3124,9 @@ class ApiDaemonSession extends EventEmitter {
2333
3124
  }
2334
3125
  });
2335
3126
  happyProcess.on("exit", (code, signal) => {
2336
- logger.daemonDebug(`Spawned process exited with code ${code}, signal ${signal}`);
3127
+ logger.debug(`[DAEMON SESSION] Spawned process exited with code ${code}, signal ${signal}`);
2337
3128
  this.spawnedProcesses.delete(happyProcess);
3129
+ this.updateChildPidsInMetadata();
2338
3130
  if (!sessionId) {
2339
3131
  callback({ error: `Process exited before session ID received` });
2340
3132
  cleanup();
@@ -2342,53 +3134,54 @@ class ApiDaemonSession extends EventEmitter {
2342
3134
  });
2343
3135
  timeoutId = setTimeout(() => {
2344
3136
  if (!sessionId) {
2345
- logger.daemonDebug("Timeout waiting for session ID");
3137
+ logger.debug("[DAEMON SESSION] Timeout waiting for session ID");
2346
3138
  callback({ error: "Timeout waiting for session" });
2347
3139
  cleanup();
2348
3140
  happyProcess.kill();
2349
3141
  this.spawnedProcesses.delete(happyProcess);
3142
+ this.updateChildPidsInMetadata();
2350
3143
  }
2351
3144
  }, 1e4);
2352
3145
  } catch (error) {
2353
- logger.daemonDebug("Error spawning session:", error);
3146
+ logger.debug("[DAEMON SESSION] Error spawning session:", error);
2354
3147
  callback({ error: error instanceof Error ? error.message : "Unknown error" });
2355
3148
  }
2356
3149
  } else {
2357
- logger.daemonDebug(`Unknown RPC method: ${data.method}`);
3150
+ logger.debug(`[DAEMON SESSION] Unknown RPC method: ${data.method}`);
2358
3151
  callback({ error: `Unknown method: ${data.method}` });
2359
3152
  }
2360
3153
  });
2361
3154
  socket.on("disconnect", (reason) => {
2362
- logger.daemonDebug(`Disconnected from server. Reason: ${reason}`);
3155
+ logger.debug(`[DAEMON SESSION] Disconnected from server. Reason: ${reason}`);
2363
3156
  this.emit("disconnected");
2364
3157
  this.stopKeepAlive();
2365
3158
  });
2366
3159
  socket.on("reconnect", () => {
2367
- logger.daemonDebug("Reconnected to server");
3160
+ logger.debug("[DAEMON SESSION] Reconnected to server");
2368
3161
  const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
2369
3162
  socket.emit("rpc-register", { method: rpcMethod });
2370
- logger.daemonDebug(`Re-registered RPC method: ${rpcMethod}`);
3163
+ logger.debug(`[DAEMON SESSION] Re-registered RPC method: ${rpcMethod}`);
2371
3164
  });
2372
3165
  socket.on("rpc-registered", (data) => {
2373
- logger.daemonDebug(`RPC registration confirmed: ${data.method}`);
3166
+ logger.debug(`[DAEMON SESSION] RPC registration confirmed: ${data.method}`);
2374
3167
  });
2375
3168
  socket.on("rpc-unregistered", (data) => {
2376
- logger.daemonDebug(`RPC unregistered: ${data.method}`);
3169
+ logger.debug(`[DAEMON SESSION] RPC unregistered: ${data.method}`);
2377
3170
  });
2378
3171
  socket.on("rpc-error", (data) => {
2379
- logger.daemonDebug(`RPC error: ${JSON.stringify(data)}`);
3172
+ logger.debug(`[DAEMON SESSION] RPC error: ${JSON.stringify(data)}`);
2380
3173
  });
2381
3174
  socket.onAny((event, ...args) => {
2382
3175
  if (!event.startsWith("machine-alive")) {
2383
- logger.daemonDebug(`Socket event: ${event}, args: ${JSON.stringify(args)}`);
3176
+ logger.debug(`[DAEMON SESSION] Socket event: ${event}, args: ${JSON.stringify(args)}`);
2384
3177
  }
2385
3178
  });
2386
3179
  socket.on("connect_error", (error) => {
2387
- logger.daemonDebug(`Connection error: ${error.message}`);
2388
- logger.daemonDebug(`Error: ${JSON.stringify(error, null, 2)}`);
3180
+ logger.debug(`[DAEMON SESSION] Connection error: ${error.message}`);
3181
+ logger.debug(`[DAEMON SESSION] Error: ${JSON.stringify(error, null, 2)}`);
2389
3182
  });
2390
3183
  socket.on("error", (error) => {
2391
- logger.daemonDebug(`Socket error: ${error}`);
3184
+ logger.debug(`[DAEMON SESSION] Socket error: ${error}`);
2392
3185
  });
2393
3186
  socket.on("daemon-command", (data) => {
2394
3187
  switch (data.command) {
@@ -2416,14 +3209,27 @@ class ApiDaemonSession extends EventEmitter {
2416
3209
  this.keepAliveInterval = null;
2417
3210
  }
2418
3211
  }
3212
+ updateChildPidsInMetadata() {
3213
+ try {
3214
+ if (existsSync$1(configuration.daemonMetadataFile)) {
3215
+ const content = readFileSync$1(configuration.daemonMetadataFile, "utf-8");
3216
+ const metadata = JSON.parse(content);
3217
+ const childPids = Array.from(this.spawnedProcesses).map((proc) => proc.pid).filter((pid) => pid !== void 0);
3218
+ metadata.childPids = childPids;
3219
+ writeFileSync(configuration.daemonMetadataFile, JSON.stringify(metadata, null, 2));
3220
+ }
3221
+ } catch (error) {
3222
+ logger.debug("[DAEMON SESSION] Error updating child PIDs in metadata:", error);
3223
+ }
3224
+ }
2419
3225
  connect() {
2420
3226
  this.socket.connect();
2421
3227
  }
2422
3228
  shutdown() {
2423
- logger.daemonDebug(`Shutting down daemon, killing ${this.spawnedProcesses.size} spawned processes`);
3229
+ logger.debug(`[DAEMON SESSION] Shutting down daemon, killing ${this.spawnedProcesses.size} spawned processes`);
2424
3230
  for (const process2 of this.spawnedProcesses) {
2425
3231
  try {
2426
- logger.daemonDebug(`Killing spawned process with PID: ${process2.pid}`);
3232
+ logger.debug(`[DAEMON SESSION] Killing spawned process with PID: ${process2.pid}`);
2427
3233
  process2.kill("SIGTERM");
2428
3234
  setTimeout(() => {
2429
3235
  try {
@@ -2432,39 +3238,66 @@ class ApiDaemonSession extends EventEmitter {
2432
3238
  }
2433
3239
  }, 1e3);
2434
3240
  } catch (error) {
2435
- logger.daemonDebug(`Error killing process: ${error}`);
3241
+ logger.debug(`[DAEMON SESSION] Error killing process: ${error}`);
2436
3242
  }
2437
3243
  }
2438
3244
  this.spawnedProcesses.clear();
3245
+ this.updateChildPidsInMetadata();
2439
3246
  this.stopKeepAlive();
2440
3247
  this.socket.close();
2441
3248
  this.emit("shutdown");
2442
3249
  }
2443
3250
  }
2444
3251
 
2445
- let pidFileFd = null;
2446
3252
  async function startDaemon() {
2447
3253
  if (process.platform !== "darwin") {
2448
3254
  console.error("ERROR: Daemon is only supported on macOS");
2449
3255
  process.exit(1);
2450
3256
  }
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);
3257
+ logger.debug("[DAEMON RUN] Starting daemon process...");
3258
+ logger.debug(`[DAEMON RUN] Server URL: ${configuration.serverUrl}`);
3259
+ const runningDaemon = await getDaemonMetadata();
3260
+ if (runningDaemon) {
3261
+ if (runningDaemon.version !== packageJson.version) {
3262
+ logger.debug(`[DAEMON RUN] Daemon version mismatch (running: ${runningDaemon.version}, current: ${packageJson.version}), restarting...`);
3263
+ await stopDaemon();
3264
+ await new Promise((resolve) => setTimeout(resolve, 500));
3265
+ } else if (await isDaemonProcessRunning(runningDaemon.pid)) {
3266
+ logger.debug("[DAEMON RUN] Happy daemon is already running with correct version");
3267
+ process.exit(0);
3268
+ } else {
3269
+ logger.debug("[DAEMON RUN] Stale daemon metadata found, cleaning up");
3270
+ await cleanupDaemonMetadata();
3271
+ }
3272
+ }
3273
+ const oldMetadata = await getDaemonMetadata();
3274
+ if (oldMetadata && oldMetadata.childPids && oldMetadata.childPids.length > 0) {
3275
+ logger.debug(`[DAEMON RUN] Found ${oldMetadata.childPids.length} potential orphaned child processes from previous run`);
3276
+ for (const childPid of oldMetadata.childPids) {
3277
+ try {
3278
+ process.kill(childPid, 0);
3279
+ const isHappy = await isProcessHappyChild(childPid);
3280
+ if (isHappy) {
3281
+ logger.debug(`[DAEMON RUN] Killing orphaned happy process ${childPid}`);
3282
+ process.kill(childPid, "SIGTERM");
3283
+ await new Promise((resolve) => setTimeout(resolve, 500));
3284
+ try {
3285
+ process.kill(childPid, 0);
3286
+ process.kill(childPid, "SIGKILL");
3287
+ } catch {
3288
+ }
3289
+ }
3290
+ } catch {
3291
+ logger.debug(`[DAEMON RUN] Process ${childPid} doesn't exist (already dead)`);
3292
+ }
3293
+ }
3294
+ }
3295
+ writeDaemonMetadata();
3296
+ logger.debug("[DAEMON RUN] Daemon metadata written");
3297
+ const caffeinateStarted = startCaffeinate();
3298
+ if (caffeinateStarted) {
3299
+ logger.debug("[DAEMON RUN] Sleep prevention enabled for daemon");
2456
3300
  }
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
3301
  try {
2469
3302
  const settings = await readSettings() || { onboardingCompleted: false };
2470
3303
  if (!settings.machineId) {
@@ -2476,11 +3309,11 @@ async function startDaemon() {
2476
3309
  machineId: settings.machineId,
2477
3310
  machineHost: settings.machineHost || hostname(),
2478
3311
  platform: process.platform,
2479
- version: process.env.npm_package_version || "unknown"
3312
+ version: packageJson.version
2480
3313
  };
2481
3314
  let credentials = await readCredentials();
2482
3315
  if (!credentials) {
2483
- logger.daemonDebug("No credentials found, running auth");
3316
+ logger.debug("[DAEMON RUN] No credentials found, running auth");
2484
3317
  await doAuth();
2485
3318
  credentials = await readCredentials();
2486
3319
  if (!credentials) {
@@ -2494,20 +3327,37 @@ async function startDaemon() {
2494
3327
  machineIdentity
2495
3328
  );
2496
3329
  daemon.on("connected", () => {
2497
- logger.daemonDebug("Connected to server event received");
3330
+ logger.debug("[DAEMON RUN] Connected to server event received");
2498
3331
  });
2499
3332
  daemon.on("disconnected", () => {
2500
- logger.daemonDebug("Disconnected from server event received");
3333
+ logger.debug("[DAEMON RUN] Disconnected from server event received");
2501
3334
  });
2502
3335
  daemon.on("shutdown", () => {
2503
- logger.daemonDebug("Shutdown requested");
2504
- stopDaemon();
3336
+ logger.debug("[DAEMON RUN] Shutdown requested");
3337
+ daemon?.shutdown();
3338
+ cleanupDaemonMetadata();
2505
3339
  process.exit(0);
2506
3340
  });
2507
3341
  daemon.connect();
2508
- logger.daemonDebug("Daemon started successfully");
3342
+ logger.debug("[DAEMON RUN] Daemon started successfully");
3343
+ process.on("SIGINT", async () => {
3344
+ logger.debug("[DAEMON RUN] Received SIGINT, shutting down...");
3345
+ if (daemon) {
3346
+ daemon.shutdown();
3347
+ }
3348
+ await cleanupDaemonMetadata();
3349
+ process.exit(0);
3350
+ });
3351
+ process.on("SIGTERM", async () => {
3352
+ logger.debug("[DAEMON RUN] Received SIGTERM, shutting down...");
3353
+ if (daemon) {
3354
+ daemon.shutdown();
3355
+ }
3356
+ await cleanupDaemonMetadata();
3357
+ process.exit(0);
3358
+ });
2509
3359
  } catch (error) {
2510
- logger.daemonDebug("Failed to start daemon", error);
3360
+ logger.debug("[DAEMON RUN] Failed to start daemon", error);
2511
3361
  stopDaemon();
2512
3362
  process.exit(1);
2513
3363
  }
@@ -2517,96 +3367,114 @@ async function startDaemon() {
2517
3367
  }
2518
3368
  async function isDaemonRunning() {
2519
3369
  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");
3370
+ logger.debug("[DAEMON RUN] [isDaemonRunning] Checking if daemon is running...");
3371
+ const metadata = await getDaemonMetadata();
3372
+ if (!metadata) {
3373
+ logger.debug("[DAEMON RUN] [isDaemonRunning] No daemon metadata found");
3374
+ return false;
2544
3375
  }
2545
- return false;
3376
+ logger.debug("[DAEMON RUN] [isDaemonRunning] Daemon metadata exists");
3377
+ logger.debug("[DAEMON RUN] [isDaemonRunning] PID from metadata:", metadata.pid);
3378
+ const isRunning = await isDaemonProcessRunning(metadata.pid);
3379
+ if (!isRunning) {
3380
+ logger.debug("[DAEMON RUN] [isDaemonRunning] Process not running, cleaning up stale metadata");
3381
+ await cleanupDaemonMetadata();
3382
+ return false;
3383
+ }
3384
+ return true;
2546
3385
  } catch (error) {
2547
- logger.daemonDebug("[isDaemonRunning] Error:", error);
3386
+ logger.debug("[DAEMON RUN] [isDaemonRunning] Error:", error);
2548
3387
  logger.debug("Error checking daemon status", error);
2549
3388
  return false;
2550
3389
  }
2551
3390
  }
2552
- function writePidFile() {
3391
+ async function isDaemonProcessRunning(pid) {
3392
+ try {
3393
+ process.kill(pid, 0);
3394
+ logger.debug("[DAEMON RUN] Process exists, checking if it's a happy daemon...");
3395
+ const isHappyDaemon = await isProcessHappyDaemon(pid);
3396
+ logger.debug("[DAEMON RUN] isHappyDaemon:", isHappyDaemon);
3397
+ return isHappyDaemon;
3398
+ } catch (error) {
3399
+ return false;
3400
+ }
3401
+ }
3402
+ function writeDaemonMetadata(childPids) {
2553
3403
  const happyDir = join$1(homedir$1(), ".happy");
2554
3404
  if (!existsSync$1(happyDir)) {
2555
3405
  mkdirSync$1(happyDir, { recursive: true });
2556
3406
  }
3407
+ const metadata = {
3408
+ pid: process.pid,
3409
+ startTime: (/* @__PURE__ */ new Date()).toISOString(),
3410
+ version: packageJson.version,
3411
+ ...childPids
3412
+ };
3413
+ writeFileSync(configuration.daemonMetadataFile, JSON.stringify(metadata, null, 2));
3414
+ }
3415
+ async function getDaemonMetadata() {
2557
3416
  try {
2558
- const fd = openSync(configuration.daemonPidFile, "wx");
2559
- writeSync(fd, process.pid.toString());
2560
- return fd;
3417
+ if (!existsSync$1(configuration.daemonMetadataFile)) {
3418
+ return null;
3419
+ }
3420
+ const content = readFileSync$1(configuration.daemonMetadataFile, "utf-8");
3421
+ return JSON.parse(content);
2561
3422
  } 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
- }
3423
+ logger.debug("Error reading daemon metadata", error);
3424
+ return null;
3425
+ }
3426
+ }
3427
+ async function cleanupDaemonMetadata() {
3428
+ try {
3429
+ if (existsSync$1(configuration.daemonMetadataFile)) {
3430
+ unlinkSync(configuration.daemonMetadataFile);
2582
3431
  }
2583
- throw error;
3432
+ } catch (error) {
3433
+ logger.debug("Error cleaning up daemon metadata", error);
2584
3434
  }
2585
3435
  }
2586
3436
  async function stopDaemon() {
2587
3437
  try {
2588
- if (pidFileFd !== null) {
3438
+ stopCaffeinate();
3439
+ logger.debug("Stopped sleep prevention");
3440
+ const metadata = await getDaemonMetadata();
3441
+ if (metadata) {
3442
+ logger.debug(`Stopping daemon with PID ${metadata.pid}`);
2589
3443
  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));
3444
+ process.kill(metadata.pid, "SIGTERM");
3445
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
2601
3446
  try {
2602
- process.kill(pid, 0);
2603
- process.kill(pid, "SIGKILL");
3447
+ process.kill(metadata.pid, 0);
3448
+ logger.debug("Daemon still running, force killing...");
3449
+ process.kill(metadata.pid, "SIGKILL");
2604
3450
  } catch {
3451
+ logger.debug("Daemon exited cleanly");
2605
3452
  }
2606
3453
  } catch (error) {
2607
- logger.debug("Process already dead or inaccessible", error);
3454
+ logger.debug("Daemon process already dead or inaccessible", error);
3455
+ }
3456
+ await new Promise((resolve) => setTimeout(resolve, 500));
3457
+ if (metadata.childPids && metadata.childPids.length > 0) {
3458
+ logger.debug(`Checking for ${metadata.childPids.length} potential orphaned child processes...`);
3459
+ for (const childPid of metadata.childPids) {
3460
+ try {
3461
+ process.kill(childPid, 0);
3462
+ const isHappy = await isProcessHappyChild(childPid);
3463
+ if (isHappy) {
3464
+ logger.debug(`Killing orphaned happy process ${childPid}`);
3465
+ process.kill(childPid, "SIGTERM");
3466
+ await new Promise((resolve) => setTimeout(resolve, 500));
3467
+ try {
3468
+ process.kill(childPid, 0);
3469
+ process.kill(childPid, "SIGKILL");
3470
+ } catch {
3471
+ }
3472
+ }
3473
+ } catch {
3474
+ }
3475
+ }
2608
3476
  }
2609
- unlinkSync(configuration.daemonPidFile);
3477
+ await cleanupDaemonMetadata();
2610
3478
  }
2611
3479
  } catch (error) {
2612
3480
  logger.debug("Error stopping daemon", error);
@@ -2628,6 +3496,22 @@ async function isProcessHappyDaemon(pid) {
2628
3496
  });
2629
3497
  });
2630
3498
  }
3499
+ async function isProcessHappyChild(pid) {
3500
+ return new Promise((resolve) => {
3501
+ const ps = spawn$1("ps", ["-p", pid.toString(), "-o", "command="]);
3502
+ let output = "";
3503
+ ps.stdout.on("data", (data) => {
3504
+ output += data.toString();
3505
+ });
3506
+ ps.on("close", () => {
3507
+ const isHappyChild = output.includes("--daemon-spawn") && (output.includes("happy") || output.includes("src/index"));
3508
+ resolve(isHappyChild);
3509
+ });
3510
+ ps.on("error", () => {
3511
+ resolve(false);
3512
+ });
3513
+ });
3514
+ }
2631
3515
 
2632
3516
  function trimIdent(text) {
2633
3517
  const lines = text.split("\n");
@@ -2655,7 +3539,7 @@ async function install$1() {
2655
3539
  try {
2656
3540
  if (existsSync$1(PLIST_FILE$1)) {
2657
3541
  logger.info("Daemon plist already exists. Uninstalling first...");
2658
- execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
3542
+ execSync$1(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
2659
3543
  }
2660
3544
  const happyPath = process.argv[0];
2661
3545
  const scriptPath = process.argv[1];
@@ -2700,7 +3584,7 @@ async function install$1() {
2700
3584
  writeFileSync(PLIST_FILE$1, plistContent);
2701
3585
  chmodSync(PLIST_FILE$1, 420);
2702
3586
  logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
2703
- execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
3587
+ execSync$1(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
2704
3588
  logger.info("Daemon installed and started successfully");
2705
3589
  logger.info("Check logs at ~/.happy/daemon.log");
2706
3590
  } catch (error) {
@@ -2729,7 +3613,7 @@ async function uninstall$1() {
2729
3613
  return;
2730
3614
  }
2731
3615
  try {
2732
- execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
3616
+ execSync$1(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
2733
3617
  logger.info("Daemon stopped successfully");
2734
3618
  } catch (error) {
2735
3619
  logger.info("Failed to unload daemon (it might not be running)");
@@ -2757,12 +3641,7 @@ async function uninstall() {
2757
3641
  (async () => {
2758
3642
  const args = process.argv.slice(2);
2759
3643
  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);
3644
+ initializeConfiguration(installationLocation);
2766
3645
  initLoggerWithGlobalConfiguration();
2767
3646
  logger.debug("Starting happy CLI with args: ", process.argv);
2768
3647
  const subcommand = args[0];
@@ -2846,8 +3725,6 @@ Currently only supported on macOS.
2846
3725
  options.claudeArgs = [...options.claudeArgs || [], claudeArg];
2847
3726
  } else if (arg === "--daemon-spawn") {
2848
3727
  options.daemonSpawn = true;
2849
- } else if (arg === "--happy-server-url") {
2850
- i++;
2851
3728
  } else {
2852
3729
  console.error(chalk.red(`Unknown argument: ${arg}`));
2853
3730
  process.exit(1);
@@ -2855,7 +3732,7 @@ Currently only supported on macOS.
2855
3732
  }
2856
3733
  if (showHelp) {
2857
3734
  console.log(`
2858
- ${chalk.bold("happy")} - Claude Code session sharing
3735
+ ${chalk.bold("happy")} - Claude Code On the Go
2859
3736
 
2860
3737
  ${chalk.bold("Usage:")}
2861
3738
  happy [options]
@@ -2896,6 +3773,10 @@ ${chalk.bold("Examples:")}
2896
3773
  happy --claude-arg --option
2897
3774
  Pass argument to Claude CLI
2898
3775
  happy logout Logs out of your account and removes data directory
3776
+
3777
+ [TODO: add after steve's refactor lands]
3778
+ ${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:")}
3779
+ TODO: exec cluade --help and show inline here
2899
3780
  `);
2900
3781
  process.exit(0);
2901
3782
  }
@@ -2912,7 +3793,6 @@ ${chalk.bold("Examples:")}
2912
3793
  credentials = res;
2913
3794
  }
2914
3795
  const settings = await readSettings() || { onboardingCompleted: false };
2915
- process.env.EXPERIMENTAL_FEATURES !== void 0;
2916
3796
  if (settings.daemonAutoStartWhenRunningHappy === void 0) {
2917
3797
  console.log(chalk.cyan("\n\u{1F680} Happy Daemon Setup\n"));
2918
3798
  const rl = createInterface({
@@ -2938,39 +3818,26 @@ ${chalk.bold("Examples:")}
2938
3818
  await writeSettings(settings);
2939
3819
  }
2940
3820
  if (settings.daemonAutoStartWhenRunningHappy) {
2941
- console.debug("Starting Happy background service...");
3821
+ logger.debug("Starting Happy background service...");
2942
3822
  if (!await isDaemonRunning()) {
2943
3823
  const happyPath = process.argv[1];
2944
- const isBuiltBinary = happyPath.endsWith("/bin/happy") || happyPath.endsWith("\\bin\\happy");
3824
+ const runningFromBuiltBinary = happyPath.endsWith("happy") || happyPath.endsWith("happy.cmd");
2945
3825
  const daemonArgs = ["daemon", "start"];
2946
- if (serverUrl) {
2947
- daemonArgs.push("--happy-server-url", serverUrl);
2948
- }
2949
3826
  if (installationLocation === "local") {
2950
3827
  daemonArgs.push("--local");
2951
3828
  }
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], {
3829
+ let executable, args2;
3830
+ if (runningFromBuiltBinary) {
3831
+ executable = happyPath;
3832
+ args2 = daemonArgs;
3833
+ } else {
3834
+ executable = "npx";
3835
+ args2 = ["tsx", happyPath, ...daemonArgs];
3836
+ }
3837
+ const daemonProcess = spawn$1(executable, args2, {
2964
3838
  detached: true,
2965
- stdio: ["ignore", "inherit", "inherit"],
3839
+ stdio: ["ignore", "inherit", "inherit"]
2966
3840
  // 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
3841
  });
2975
3842
  daemonProcess.unref();
2976
3843
  await new Promise((resolve) => setTimeout(resolve, 200));