@zonease/happy 0.6.38 → 0.6.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/{types-BWpRU1VN.mjs → api-CIZKyfSn.mjs} +136 -1725
  2. package/dist/{types-DvTsFaqR.cjs → api-CwOCQeLH.cjs} +317 -1961
  3. package/dist/auth-0VnlXT9f.mjs +181 -0
  4. package/dist/auth-BE8dD20T.mjs +270 -0
  5. package/dist/auth-BIleVCw0.cjs +274 -0
  6. package/dist/auth-Cas9DR3b.cjs +183 -0
  7. package/dist/browser-Cc08m7of.cjs +22 -0
  8. package/dist/browser-Dll5hxOV.mjs +20 -0
  9. package/dist/claudeLocal-CPT3zcIU.mjs +344 -0
  10. package/dist/claudeLocal-WkgLbl1a.cjs +352 -0
  11. package/dist/codex/happyMcpStdioBridge.cjs +4 -1
  12. package/dist/codex/happyMcpStdioBridge.mjs +4 -1
  13. package/dist/common-BdNgghUJ.mjs +75 -0
  14. package/dist/common-CZvSRU4p.cjs +83 -0
  15. package/dist/{config-0Xnc8ARM.cjs → config-Draa-5XC.cjs} +21 -34
  16. package/dist/{config-_GW4YBCP.mjs → config-rTT4qTU5.mjs} +7 -20
  17. package/dist/connect-BfC1tYP_.mjs +679 -0
  18. package/dist/connect-CJZ73syK.cjs +681 -0
  19. package/dist/{constants-B4x1K7by.cjs → constants-E_UWYTo3.cjs} +2 -2
  20. package/dist/{constants-BO9OAFOT.mjs → constants-pmXZGibX.mjs} +1 -1
  21. package/dist/controlClient-CM_QcJnB.mjs +159 -0
  22. package/dist/controlClient-DZMIsKsT.cjs +168 -0
  23. package/dist/deviceExec-CU1R9CRb.cjs +84 -0
  24. package/dist/deviceExec-D93AOVV4.mjs +81 -0
  25. package/dist/diagnose-B3D1a_v0.cjs +60 -0
  26. package/dist/diagnose-CuRKXxt1.mjs +58 -0
  27. package/dist/doctor-C08m5Z9f.cjs +78 -0
  28. package/dist/doctor-Cya9YSA0.cjs +214 -0
  29. package/dist/doctor-DZYB6IVc.mjs +211 -0
  30. package/dist/doctor-ON7pN5G5.mjs +74 -0
  31. package/dist/downloadImage-DfSFLb-h.mjs +887 -0
  32. package/dist/downloadImage-MQMrUUxS.cjs +897 -0
  33. package/dist/enroll-BxlbVftP.cjs +115 -0
  34. package/dist/enroll-C52x8oly.mjs +113 -0
  35. package/dist/evidence-Ccd9iYcj.mjs +127 -0
  36. package/dist/evidence-CjWBG5rX.cjs +129 -0
  37. package/dist/formatImageMessage-B4lSxlk2.mjs +43 -0
  38. package/dist/formatImageMessage-DbyG8c2j.cjs +46 -0
  39. package/dist/github-DKcyF1zp.mjs +57 -0
  40. package/dist/github-h9GhJJ_C.cjs +59 -0
  41. package/dist/index-CDTNgfUf.cjs +578 -0
  42. package/dist/index-CPKdvi7J.mjs +554 -0
  43. package/dist/index.cjs +812 -40
  44. package/dist/index.mjs +813 -40
  45. package/dist/install-BokHRgFW.cjs +98 -0
  46. package/dist/install-C_cp8b2-.cjs +96 -0
  47. package/dist/install-DAeCn8ae.mjs +94 -0
  48. package/dist/install-oRAjVLjP.mjs +95 -0
  49. package/dist/lib.cjs +18 -12
  50. package/dist/lib.mjs +13 -7
  51. package/dist/{loadableToolOutput-va3RLplh.mjs → loadableToolOutput-Cuygmd4K.mjs} +4 -2
  52. package/dist/{loadableToolOutput-5vdQB98M.cjs → loadableToolOutput-DDCJjywt.cjs} +63 -61
  53. package/dist/mcpToolSchemas-CjeTfrAs.mjs +62 -0
  54. package/dist/mcpToolSchemas-DK42yoZ_.cjs +64 -0
  55. package/dist/persistence-BFjvtUad.mjs +944 -0
  56. package/dist/persistence-JLUn_Nou.cjs +996 -0
  57. package/dist/projectPath-BTBIMB57.mjs +10 -0
  58. package/dist/projectPath-ChLhdOyt.cjs +13 -0
  59. package/dist/prompt-BTWDZZO0.cjs +25 -0
  60. package/dist/prompt-BsAZsPbo.mjs +22 -0
  61. package/dist/release-BMEzNONE.mjs +168 -0
  62. package/dist/release-EhOJeAZe.cjs +171 -0
  63. package/dist/run-BDn2aE3Q.cjs +2152 -0
  64. package/dist/run-DBZqN6Kn.mjs +2128 -0
  65. package/dist/runClaude-DNHvERX0.mjs +3763 -0
  66. package/dist/runClaude-uaKxPZPm.cjs +3766 -0
  67. package/dist/{runCodex-skq0j05d.cjs → runCodex-82UrZLme.cjs} +169 -153
  68. package/dist/{runCodex-DiKzBf9q.mjs → runCodex-C-RTM0wA.mjs} +36 -20
  69. package/dist/{runGemini-BeqrVq8R.mjs → runGemini-Bm6-4Xm2.mjs} +34 -17
  70. package/dist/{runGemini-CVp-CEIh.cjs → runGemini-CXMr4Jwb.cjs} +212 -195
  71. package/dist/{runOneShot-BQNkNW61.cjs → runOneShot-1B6RMYT7.cjs} +31 -55
  72. package/dist/{runOneShot-ltCpHbBB.mjs → runOneShot-ioARRHrx.mjs} +14 -38
  73. package/dist/{serve-BECdtivY.cjs → serve-BtdAhkIE.cjs} +35 -58
  74. package/dist/{serve-BNdoEDVx.mjs → serve-CwibwU6N.mjs} +12 -35
  75. package/dist/session-2sU4Bu1R.mjs +108 -0
  76. package/dist/session-D--F87zL.cjs +110 -0
  77. package/dist/sessionKeyCache-4Tp8pJ3N.cjs +36 -0
  78. package/dist/sessionKeyCache-D2BwO91C.mjs +33 -0
  79. package/dist/sessionList-BCSw3qYt.mjs +51 -0
  80. package/dist/sessionList-ICJsjNtJ.cjs +54 -0
  81. package/dist/sessionSummary-Bkz52BPh.cjs +393 -0
  82. package/dist/sessionSummary-lHGBWQSb.mjs +387 -0
  83. package/dist/spawnHappyCLI-CoORX0YW.mjs +66 -0
  84. package/dist/spawnHappyCLI-D_V-AAoP.cjs +70 -0
  85. package/dist/ssh-BTOlhhxi.cjs +333 -0
  86. package/dist/ssh-DL_AkRAD.mjs +328 -0
  87. package/dist/systemdScope-BeOtAkhk.mjs +25 -0
  88. package/dist/systemdScope-DW3-UoKB.cjs +29 -0
  89. package/dist/task-CQzrHRZe.mjs +165 -0
  90. package/dist/task-CSpaC9HX.cjs +167 -0
  91. package/dist/time-Bj2HRkKH.cjs +33 -0
  92. package/dist/time-DgsBLJA_.mjs +29 -0
  93. package/dist/trimIdent-4gvk9Wk6.cjs +22 -0
  94. package/dist/trimIdent-BAjmv_TG.mjs +20 -0
  95. package/dist/types-BoJh4Qfv.mjs +57 -0
  96. package/dist/types-DWeB7aqh.cjs +59 -0
  97. package/dist/uninstall-BiKyRAb1.cjs +77 -0
  98. package/dist/uninstall-nFMFGUVM.mjs +75 -0
  99. package/dist/update-BcqRHXf2.mjs +109 -0
  100. package/dist/update-Dw8pgsAT.cjs +111 -0
  101. package/dist/workflowCommon-B0d01Y_a.mjs +56 -0
  102. package/dist/workflowCommon-Bl568-rt.cjs +62 -0
  103. package/dist/workflowReports-BEWuQesA.cjs +113 -0
  104. package/dist/workflowReports-Dd8-4-8f.mjs +108 -0
  105. package/package.json +1 -1
  106. package/dist/index-BbCDDvoB.cjs +0 -11376
  107. package/dist/index-vvZXdswy.mjs +0 -11336
  108. package/dist/mcpToolSchemas-CdPXJQmb.mjs +0 -175
  109. package/dist/mcpToolSchemas-f-kttjFw.cjs +0 -186
@@ -0,0 +1,3763 @@
1
+ import os, { homedir } from 'node:os';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { existsSync, mkdirSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs';
4
+ import { readFile, mkdir, writeFile, stat, copyFile } from 'node:fs/promises';
5
+ import { I as InvalidateSync, k as AsyncLock, d as connectionState, A as ApiClient, r as resolveToolsDir, s as startOfflineReconnection, l as findClaudeProjectId } from './api-CIZKyfSn.mjs';
6
+ import { l as logger, i as isDebug, c as configuration, a as readSettings, p as packageJson } from './persistence-BFjvtUad.mjs';
7
+ import { g as getProjectPath, a as claudeLocal, E as ExitCodeError, b as claudeCheckSession, s as systemPrompt } from './claudeLocal-CPT3zcIU.mjs';
8
+ import { R as RawJSONLinesSchema } from './types-BoJh4Qfv.mjs';
9
+ import { join, resolve } from 'node:path';
10
+ import { d as delay } from './time-DgsBLJA_.mjs';
11
+ import { watch, access } from 'fs/promises';
12
+ import { useStdout, useInput, Box, Text, render } from 'ink';
13
+ import { e as parseSpecialCommand, b as MessageBuffer, c as createMcpContext, M as MessageQueue2, h as hashObject, r as registerKillSessionHandler, a as detectGitWorktree } from './downloadImage-DfSFLb-h.mjs';
14
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
15
+ import { execSync, spawn } from 'node:child_process';
16
+ import { createInterface } from 'node:readline';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { i as isBun } from './spawnHappyCLI-CoORX0YW.mjs';
19
+ import { p as projectPath } from './projectPath-BTBIMB57.mjs';
20
+ import 'axios';
21
+ import 'node:events';
22
+ import 'socket.io-client';
23
+ import 'tweetnacl';
24
+ import 'ws';
25
+ import 'crypto';
26
+ import '@stablelib/base64';
27
+ import 'expo-server-sdk';
28
+ import 'chalk';
29
+ import { a as formatMessageForClaude } from './formatImageMessage-B4lSxlk2.mjs';
30
+ import { isDeepStrictEqual } from 'node:util';
31
+ import { i as initialMachineMetadata, s as stopCaffeinate, a as startCaffeinate } from './run-DBZqN6Kn.mjs';
32
+ import { getEnvironmentInfo } from './doctor-DZYB6IVc.mjs';
33
+ import { notifyDaemonSessionStarted } from './controlClient-CM_QcJnB.mjs';
34
+ import { createServer } from 'node:http';
35
+ import 'zod';
36
+ import './index-CPKdvi7J.mjs';
37
+ import '@paralleldrive/cuid2';
38
+ import 'child_process';
39
+ import 'util';
40
+ import 'path';
41
+ import 'node:module';
42
+ import './sessionKeyCache-D2BwO91C.mjs';
43
+ import 'fs';
44
+ import './prompt-BsAZsPbo.mjs';
45
+ import './trimIdent-BAjmv_TG.mjs';
46
+ import './common-BdNgghUJ.mjs';
47
+ import '@modelcontextprotocol/sdk/server/mcp.js';
48
+ import '@modelcontextprotocol/sdk/server/streamableHttp.js';
49
+ import '@modelcontextprotocol/sdk/types.js';
50
+ import './deviceExec-D93AOVV4.mjs';
51
+ import './auth-BE8dD20T.mjs';
52
+ import 'qrcode-terminal';
53
+ import './browser-Dll5hxOV.mjs';
54
+ import 'open';
55
+ import './mcpToolSchemas-CjeTfrAs.mjs';
56
+ import 'url';
57
+ import 'os';
58
+ import 'tmp';
59
+ import './install-oRAjVLjP.mjs';
60
+ import 'fastify';
61
+ import 'fastify-type-provider-zod';
62
+ import './doctor-ON7pN5G5.mjs';
63
+ import 'ps-list';
64
+ import 'cross-spawn';
65
+
66
+ class Session {
67
+ path;
68
+ logPath;
69
+ api;
70
+ client;
71
+ queue;
72
+ claudeEnvVars;
73
+ claudeArgs;
74
+ // Made mutable to allow filtering
75
+ mcpServers;
76
+ allowedTools;
77
+ _onModeChange;
78
+ /** Path to temporary settings file with SessionStart hook (required for session tracking) */
79
+ hookSettingsPath;
80
+ /** JavaScript runtime to use for spawning Claude Code (default: 'node') */
81
+ jsRuntime;
82
+ sessionId;
83
+ mode = "local";
84
+ thinking = false;
85
+ /** Callbacks to be notified when session ID is found/changed */
86
+ sessionFoundCallbacks = [];
87
+ /** Keep alive interval reference for cleanup */
88
+ keepAliveInterval;
89
+ constructor(opts) {
90
+ this.path = opts.path;
91
+ this.api = opts.api;
92
+ this.client = opts.client;
93
+ this.logPath = opts.logPath;
94
+ this.sessionId = opts.sessionId;
95
+ this.queue = opts.messageQueue;
96
+ this.claudeEnvVars = opts.claudeEnvVars;
97
+ this.claudeArgs = opts.claudeArgs;
98
+ this.mcpServers = opts.mcpServers;
99
+ this.allowedTools = opts.allowedTools;
100
+ this._onModeChange = opts.onModeChange;
101
+ this.hookSettingsPath = opts.hookSettingsPath;
102
+ this.jsRuntime = opts.jsRuntime ?? "node";
103
+ this.client.keepAlive(this.thinking, this.mode);
104
+ this.keepAliveInterval = setInterval(() => {
105
+ this.client.keepAlive(this.thinking, this.mode);
106
+ }, 2e3);
107
+ }
108
+ /**
109
+ * Cleanup resources (call when session is no longer needed)
110
+ */
111
+ cleanup = () => {
112
+ clearInterval(this.keepAliveInterval);
113
+ this.sessionFoundCallbacks = [];
114
+ logger.debug("[Session] Cleaned up resources");
115
+ };
116
+ onThinkingChange = (thinking) => {
117
+ this.thinking = thinking;
118
+ this.client.keepAlive(thinking, this.mode);
119
+ };
120
+ onModeChange = (mode) => {
121
+ this.mode = mode;
122
+ this.client.keepAlive(this.thinking, mode);
123
+ this._onModeChange(mode);
124
+ };
125
+ /**
126
+ * Called when Claude session ID is discovered or changed.
127
+ *
128
+ * This is triggered by the SessionStart hook when:
129
+ * - Claude starts a new session (fresh start)
130
+ * - Claude resumes a session (--continue, --resume flags)
131
+ * - Claude forks a session (/compact, double-escape fork)
132
+ *
133
+ * Updates internal state, syncs to API metadata, and notifies
134
+ * all registered callbacks (e.g., SessionScanner) about the change.
135
+ */
136
+ onSessionFound = (sessionId) => {
137
+ this.sessionId = sessionId;
138
+ this.client.updateMetadata((metadata) => ({
139
+ ...metadata,
140
+ claudeSessionId: sessionId
141
+ }));
142
+ logger.debug(`[Session] Claude Code session ID ${sessionId} added to metadata`);
143
+ for (const callback of this.sessionFoundCallbacks) {
144
+ callback(sessionId);
145
+ }
146
+ };
147
+ /**
148
+ * Register a callback to be notified when session ID is found/changed
149
+ */
150
+ addSessionFoundCallback = (callback) => {
151
+ this.sessionFoundCallbacks.push(callback);
152
+ };
153
+ /**
154
+ * Remove a session found callback
155
+ */
156
+ removeSessionFoundCallback = (callback) => {
157
+ const index = this.sessionFoundCallbacks.indexOf(callback);
158
+ if (index !== -1) {
159
+ this.sessionFoundCallbacks.splice(index, 1);
160
+ }
161
+ };
162
+ /**
163
+ * Clear the current session ID (used by /clear command)
164
+ */
165
+ clearSessionId = () => {
166
+ this.sessionId = null;
167
+ logger.debug("[Session] Session ID cleared");
168
+ };
169
+ /**
170
+ * Consume one-time Claude flags from claudeArgs after Claude spawn
171
+ * Handles: --resume (with or without session ID), --continue
172
+ */
173
+ consumeOneTimeFlags = () => {
174
+ if (!this.claudeArgs) return;
175
+ const filteredArgs = [];
176
+ for (let i = 0; i < this.claudeArgs.length; i++) {
177
+ const arg = this.claudeArgs[i];
178
+ if (arg === "--continue") {
179
+ logger.debug("[Session] Consumed --continue flag");
180
+ continue;
181
+ }
182
+ if (arg === "--resume") {
183
+ if (i + 1 < this.claudeArgs.length) {
184
+ const nextArg = this.claudeArgs[i + 1];
185
+ if (!nextArg.startsWith("-") && nextArg.includes("-")) {
186
+ i++;
187
+ logger.debug(`[Session] Consumed --resume flag with session ID: ${nextArg}`);
188
+ } else {
189
+ logger.debug("[Session] Consumed --resume flag (no session ID)");
190
+ }
191
+ } else {
192
+ logger.debug("[Session] Consumed --resume flag (no session ID)");
193
+ }
194
+ continue;
195
+ }
196
+ filteredArgs.push(arg);
197
+ }
198
+ this.claudeArgs = filteredArgs.length > 0 ? filteredArgs : void 0;
199
+ logger.debug(`[Session] Consumed one-time flags, remaining args:`, this.claudeArgs);
200
+ };
201
+ }
202
+
203
+ class Future {
204
+ _resolve;
205
+ _reject;
206
+ _promise;
207
+ constructor() {
208
+ this._promise = new Promise((resolve, reject) => {
209
+ this._resolve = resolve;
210
+ this._reject = reject;
211
+ });
212
+ }
213
+ resolve(value) {
214
+ this._resolve(value);
215
+ }
216
+ reject(reason) {
217
+ this._reject(reason);
218
+ }
219
+ get promise() {
220
+ return this._promise;
221
+ }
222
+ }
223
+
224
+ function startFileWatcher(file, onFileChange) {
225
+ const abortController = new AbortController();
226
+ void (async () => {
227
+ while (true) {
228
+ try {
229
+ logger.debug(`[FILE_WATCHER] Starting watcher for ${file}`);
230
+ const watcher = watch(file, { persistent: true, signal: abortController.signal });
231
+ for await (const event of watcher) {
232
+ if (abortController.signal.aborted) {
233
+ return;
234
+ }
235
+ logger.debug(`[FILE_WATCHER] File changed: ${file}`);
236
+ onFileChange(file);
237
+ }
238
+ } catch (e) {
239
+ if (abortController.signal.aborted) {
240
+ return;
241
+ }
242
+ logger.debug(`[FILE_WATCHER] Watch error: ${e.message}, restarting watcher in a second`);
243
+ await delay(1e3);
244
+ }
245
+ }
246
+ })();
247
+ return () => {
248
+ abortController.abort();
249
+ };
250
+ }
251
+
252
+ const INTERNAL_CLAUDE_EVENT_TYPES$1 = /* @__PURE__ */ new Set([
253
+ "file-history-snapshot",
254
+ "change",
255
+ "queue-operation"
256
+ ]);
257
+ async function createSessionScanner(opts) {
258
+ const projectDir = getProjectPath(opts.workingDirectory);
259
+ let finishedSessions = /* @__PURE__ */ new Set();
260
+ let pendingSessions = /* @__PURE__ */ new Set();
261
+ let currentSessionId = null;
262
+ let watchers = /* @__PURE__ */ new Map();
263
+ let processedMessageKeys = /* @__PURE__ */ new Set();
264
+ if (opts.sessionId) {
265
+ let messages = await readSessionLog$1(projectDir, opts.sessionId);
266
+ logger.debug(`[SESSION_SCANNER] Marking ${messages.length} existing messages as processed from session ${opts.sessionId}`);
267
+ for (let m of messages) {
268
+ processedMessageKeys.add(messageKey$1(m));
269
+ }
270
+ currentSessionId = opts.sessionId;
271
+ }
272
+ const sync = new InvalidateSync(async () => {
273
+ let sessions = [];
274
+ for (let p of pendingSessions) {
275
+ sessions.push(p);
276
+ }
277
+ if (currentSessionId && !pendingSessions.has(currentSessionId)) {
278
+ sessions.push(currentSessionId);
279
+ }
280
+ for (let [sessionId] of watchers) {
281
+ if (!sessions.includes(sessionId)) {
282
+ sessions.push(sessionId);
283
+ }
284
+ }
285
+ for (let session of sessions) {
286
+ const sessionMessages = await readSessionLog$1(projectDir, session);
287
+ let skipped = 0;
288
+ let sent = 0;
289
+ for (let file of sessionMessages) {
290
+ let key = messageKey$1(file);
291
+ if (processedMessageKeys.has(key)) {
292
+ skipped++;
293
+ continue;
294
+ }
295
+ processedMessageKeys.add(key);
296
+ logger.debug(`[SESSION_SCANNER] Sending new message: type=${file.type}, uuid=${file.type === "summary" ? file.leafUuid : file.uuid}`);
297
+ opts.onMessage(file);
298
+ sent++;
299
+ }
300
+ if (sessionMessages.length > 0) {
301
+ logger.debug(`[SESSION_SCANNER] Session ${session}: found=${sessionMessages.length}, skipped=${skipped}, sent=${sent}`);
302
+ }
303
+ }
304
+ for (let p of sessions) {
305
+ if (pendingSessions.has(p)) {
306
+ pendingSessions.delete(p);
307
+ finishedSessions.add(p);
308
+ }
309
+ }
310
+ for (let p of sessions) {
311
+ if (!watchers.has(p)) {
312
+ logger.debug(`[SESSION_SCANNER] Starting watcher for session: ${p}`);
313
+ watchers.set(p, startFileWatcher(join(projectDir, `${p}.jsonl`), () => {
314
+ sync.invalidate();
315
+ }));
316
+ }
317
+ }
318
+ });
319
+ await sync.invalidateAndAwait();
320
+ const intervalId = setInterval(() => {
321
+ sync.invalidate();
322
+ }, 3e3);
323
+ return {
324
+ cleanup: async () => {
325
+ clearInterval(intervalId);
326
+ for (let w of watchers.values()) {
327
+ w();
328
+ }
329
+ watchers.clear();
330
+ await sync.invalidateAndAwait();
331
+ sync.stop();
332
+ },
333
+ onNewSession: (sessionId) => {
334
+ if (currentSessionId === sessionId) {
335
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
336
+ return;
337
+ }
338
+ if (finishedSessions.has(sessionId)) {
339
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
340
+ return;
341
+ }
342
+ if (pendingSessions.has(sessionId)) {
343
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
344
+ return;
345
+ }
346
+ if (currentSessionId) {
347
+ pendingSessions.add(currentSessionId);
348
+ }
349
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId}`);
350
+ currentSessionId = sessionId;
351
+ sync.invalidate();
352
+ }
353
+ };
354
+ }
355
+ function messageKey$1(message) {
356
+ if (message.type === "user") {
357
+ return message.uuid;
358
+ } else if (message.type === "assistant") {
359
+ return message.uuid;
360
+ } else if (message.type === "summary") {
361
+ return "summary: " + message.leafUuid + ": " + message.summary;
362
+ } else if (message.type === "system") {
363
+ return message.uuid;
364
+ } else if (message.type === "progress") {
365
+ return message.uuid;
366
+ } else {
367
+ throw Error();
368
+ }
369
+ }
370
+ async function readSessionLog$1(projectDir, sessionId) {
371
+ const expectedSessionFile = join(projectDir, `${sessionId}.jsonl`);
372
+ logger.debug(`[SESSION_SCANNER] Reading session file: ${expectedSessionFile}`);
373
+ let file;
374
+ try {
375
+ file = await readFile(expectedSessionFile, "utf-8");
376
+ } catch (error) {
377
+ logger.debug(`[SESSION_SCANNER] Session file not found: ${expectedSessionFile}`);
378
+ return [];
379
+ }
380
+ let lines = file.split("\n");
381
+ let messages = [];
382
+ for (let l of lines) {
383
+ try {
384
+ if (l.trim() === "") {
385
+ continue;
386
+ }
387
+ let message = JSON.parse(l);
388
+ if (message.type && INTERNAL_CLAUDE_EVENT_TYPES$1.has(message.type)) {
389
+ continue;
390
+ }
391
+ let parsed = RawJSONLinesSchema.safeParse(message);
392
+ if (!parsed.success) {
393
+ continue;
394
+ }
395
+ messages.push(parsed.data);
396
+ } catch (e) {
397
+ logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
398
+ continue;
399
+ }
400
+ }
401
+ return messages;
402
+ }
403
+
404
+ async function claudeLocalLauncher(session) {
405
+ const scanner = await createSessionScanner({
406
+ sessionId: session.sessionId,
407
+ workingDirectory: session.path,
408
+ onMessage: (message) => {
409
+ if (message.type !== "summary") {
410
+ session.client.sendClaudeSessionMessage(message);
411
+ }
412
+ }
413
+ });
414
+ const scannerSessionCallback = (sessionId) => {
415
+ scanner.onNewSession(sessionId);
416
+ };
417
+ session.addSessionFoundCallback(scannerSessionCallback);
418
+ let exitReason = null;
419
+ const processAbortController = new AbortController();
420
+ let exutFuture = new Future();
421
+ try {
422
+ async function abort() {
423
+ if (!processAbortController.signal.aborted) {
424
+ processAbortController.abort();
425
+ }
426
+ await exutFuture.promise;
427
+ }
428
+ async function doAbort() {
429
+ logger.debug("[local]: doAbort");
430
+ if (!exitReason) {
431
+ exitReason = { type: "switch" };
432
+ }
433
+ session.queue.reset();
434
+ await abort();
435
+ }
436
+ async function doSwitch() {
437
+ logger.debug("[local]: doSwitch");
438
+ if (!exitReason) {
439
+ exitReason = { type: "switch" };
440
+ }
441
+ await abort();
442
+ }
443
+ session.client.rpcHandlerManager.registerHandler("abort", doAbort);
444
+ session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
445
+ session.queue.setOnMessage((message, mode) => {
446
+ doSwitch();
447
+ });
448
+ if (session.queue.size() > 0) {
449
+ return { type: "switch" };
450
+ }
451
+ const handleSessionStart = (sessionId) => {
452
+ session.onSessionFound(sessionId);
453
+ scanner.onNewSession(sessionId);
454
+ };
455
+ while (true) {
456
+ if (exitReason) {
457
+ return exitReason;
458
+ }
459
+ logger.debug("[local]: launch");
460
+ try {
461
+ await claudeLocal({
462
+ path: session.path,
463
+ sessionId: session.sessionId,
464
+ onSessionFound: handleSessionStart,
465
+ onThinkingChange: session.onThinkingChange,
466
+ abort: processAbortController.signal,
467
+ claudeEnvVars: session.claudeEnvVars,
468
+ claudeArgs: session.claudeArgs,
469
+ mcpServers: session.mcpServers,
470
+ allowedTools: session.allowedTools,
471
+ hookSettingsPath: session.hookSettingsPath
472
+ });
473
+ session.consumeOneTimeFlags();
474
+ if (!exitReason) {
475
+ exitReason = { type: "exit", code: 0 };
476
+ break;
477
+ }
478
+ } catch (e) {
479
+ const errorMessage = e instanceof Error ? e.message : String(e);
480
+ logger.debug("[local]: launch error", e);
481
+ if (e instanceof ExitCodeError) {
482
+ if (!exitReason) {
483
+ exitReason = { type: "exit", code: e.exitCode };
484
+ }
485
+ break;
486
+ }
487
+ if (!exitReason) {
488
+ session.client.sendSessionEvent({ type: "message", message: `Process exited unexpectedly: ${errorMessage}` });
489
+ continue;
490
+ } else {
491
+ break;
492
+ }
493
+ }
494
+ logger.debug("[local]: launch done");
495
+ }
496
+ } finally {
497
+ exutFuture.resolve(void 0);
498
+ session.client.rpcHandlerManager.registerHandler("abort", async () => {
499
+ });
500
+ session.client.rpcHandlerManager.registerHandler("switch", async () => {
501
+ });
502
+ session.queue.setOnMessage(null);
503
+ session.removeSessionFoundCallback(scannerSessionCallback);
504
+ await scanner.cleanup();
505
+ }
506
+ return exitReason || { type: "exit", code: 0 };
507
+ }
508
+
509
+ const RemoteModeDisplay = ({ messageBuffer, logPath, onExit, onSwitchToLocal }) => {
510
+ const [messages, setMessages] = useState([]);
511
+ const [confirmationMode, setConfirmationMode] = useState(null);
512
+ const [actionInProgress, setActionInProgress] = useState(null);
513
+ const confirmationTimeoutRef = useRef(null);
514
+ const { stdout } = useStdout();
515
+ const terminalWidth = stdout.columns || 80;
516
+ const terminalHeight = stdout.rows || 24;
517
+ useEffect(() => {
518
+ setMessages(messageBuffer.getMessages());
519
+ const unsubscribe = messageBuffer.onUpdate((newMessages) => {
520
+ setMessages(newMessages);
521
+ });
522
+ return () => {
523
+ unsubscribe();
524
+ if (confirmationTimeoutRef.current) {
525
+ clearTimeout(confirmationTimeoutRef.current);
526
+ }
527
+ };
528
+ }, [messageBuffer]);
529
+ const resetConfirmation = useCallback(() => {
530
+ setConfirmationMode(null);
531
+ if (confirmationTimeoutRef.current) {
532
+ clearTimeout(confirmationTimeoutRef.current);
533
+ confirmationTimeoutRef.current = null;
534
+ }
535
+ }, []);
536
+ const setConfirmationWithTimeout = useCallback((mode) => {
537
+ setConfirmationMode(mode);
538
+ if (confirmationTimeoutRef.current) {
539
+ clearTimeout(confirmationTimeoutRef.current);
540
+ }
541
+ confirmationTimeoutRef.current = setTimeout(() => {
542
+ resetConfirmation();
543
+ }, 15e3);
544
+ }, [resetConfirmation]);
545
+ useInput(useCallback(async (input, key) => {
546
+ if (actionInProgress) return;
547
+ if (key.ctrl && input === "c") {
548
+ if (confirmationMode === "exit") {
549
+ resetConfirmation();
550
+ setActionInProgress("exiting");
551
+ await new Promise((resolve) => setTimeout(resolve, 100));
552
+ onExit?.();
553
+ } else {
554
+ setConfirmationWithTimeout("exit");
555
+ }
556
+ return;
557
+ }
558
+ if (input === " ") {
559
+ if (confirmationMode === "switch") {
560
+ resetConfirmation();
561
+ setActionInProgress("switching");
562
+ await new Promise((resolve) => setTimeout(resolve, 100));
563
+ onSwitchToLocal?.();
564
+ } else {
565
+ setConfirmationWithTimeout("switch");
566
+ }
567
+ return;
568
+ }
569
+ if (confirmationMode) {
570
+ resetConfirmation();
571
+ }
572
+ }, [confirmationMode, actionInProgress, onExit, onSwitchToLocal, setConfirmationWithTimeout, resetConfirmation]));
573
+ const getMessageColor = (type) => {
574
+ switch (type) {
575
+ case "user":
576
+ return "magenta";
577
+ case "assistant":
578
+ return "cyan";
579
+ case "system":
580
+ return "blue";
581
+ case "tool":
582
+ return "yellow";
583
+ case "result":
584
+ return "green";
585
+ case "status":
586
+ return "gray";
587
+ default:
588
+ return "white";
589
+ }
590
+ };
591
+ const formatMessage = (msg) => {
592
+ const lines = msg.content.split("\n");
593
+ const maxLineLength = terminalWidth - 10;
594
+ return lines.map((line) => {
595
+ if (line.length <= maxLineLength) return line;
596
+ const chunks = [];
597
+ for (let i = 0; i < line.length; i += maxLineLength) {
598
+ chunks.push(line.slice(i, i + maxLineLength));
599
+ }
600
+ return chunks.join("\n");
601
+ }).join("\n");
602
+ };
603
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
604
+ Box,
605
+ {
606
+ flexDirection: "column",
607
+ width: terminalWidth,
608
+ height: terminalHeight - 4,
609
+ borderStyle: "round",
610
+ borderColor: "gray",
611
+ paddingX: 1,
612
+ overflow: "hidden"
613
+ },
614
+ /* @__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)))),
615
+ /* @__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...") : (
616
+ // Show only the last messages that fit in the available space
617
+ 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))))
618
+ ))
619
+ ), /* @__PURE__ */ React.createElement(
620
+ Box,
621
+ {
622
+ width: terminalWidth,
623
+ borderStyle: "round",
624
+ borderColor: actionInProgress ? "gray" : confirmationMode === "exit" ? "red" : confirmationMode === "switch" ? "yellow" : "green",
625
+ paddingX: 2,
626
+ justifyContent: "center",
627
+ alignItems: "center",
628
+ flexDirection: "column"
629
+ },
630
+ /* @__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")), isDebug() && logPath && /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
631
+ ));
632
+ };
633
+
634
+ class Stream {
635
+ constructor(returned) {
636
+ this.returned = returned;
637
+ }
638
+ queue = [];
639
+ readResolve;
640
+ readReject;
641
+ isDone = false;
642
+ hasError;
643
+ started = false;
644
+ /**
645
+ * Implements async iterable protocol
646
+ */
647
+ [Symbol.asyncIterator]() {
648
+ if (this.started) {
649
+ throw new Error("Stream can only be iterated once");
650
+ }
651
+ this.started = true;
652
+ return this;
653
+ }
654
+ /**
655
+ * Gets the next value from the stream
656
+ */
657
+ async next() {
658
+ if (this.queue.length > 0) {
659
+ return Promise.resolve({
660
+ done: false,
661
+ value: this.queue.shift()
662
+ });
663
+ }
664
+ if (this.isDone) {
665
+ return Promise.resolve({ done: true, value: void 0 });
666
+ }
667
+ if (this.hasError) {
668
+ return Promise.reject(this.hasError);
669
+ }
670
+ return new Promise((resolve, reject) => {
671
+ this.readResolve = resolve;
672
+ this.readReject = reject;
673
+ });
674
+ }
675
+ /**
676
+ * Adds a value to the stream
677
+ */
678
+ enqueue(value) {
679
+ if (this.readResolve) {
680
+ const resolve = this.readResolve;
681
+ this.readResolve = void 0;
682
+ this.readReject = void 0;
683
+ resolve({ done: false, value });
684
+ } else {
685
+ this.queue.push(value);
686
+ }
687
+ }
688
+ /**
689
+ * Marks the stream as complete
690
+ */
691
+ done() {
692
+ this.isDone = true;
693
+ if (this.readResolve) {
694
+ const resolve = this.readResolve;
695
+ this.readResolve = void 0;
696
+ this.readReject = void 0;
697
+ resolve({ done: true, value: void 0 });
698
+ }
699
+ }
700
+ /**
701
+ * Propagates an error through the stream
702
+ */
703
+ error(error) {
704
+ this.hasError = error;
705
+ if (this.readReject) {
706
+ const reject = this.readReject;
707
+ this.readResolve = void 0;
708
+ this.readReject = void 0;
709
+ reject(error);
710
+ }
711
+ }
712
+ /**
713
+ * Implements async iterator cleanup
714
+ */
715
+ async return() {
716
+ this.isDone = true;
717
+ if (this.returned) {
718
+ this.returned();
719
+ }
720
+ return Promise.resolve({ done: true, value: void 0 });
721
+ }
722
+ }
723
+
724
+ class AbortError extends Error {
725
+ constructor(message) {
726
+ super(message);
727
+ this.name = "AbortError";
728
+ }
729
+ }
730
+
731
+ const __filename$1 = fileURLToPath(import.meta.url);
732
+ const __dirname$1 = join(__filename$1, "..");
733
+ function getGlobalClaudeVersion() {
734
+ try {
735
+ const cleanEnv = getCleanEnv();
736
+ const output = execSync("claude --version", {
737
+ encoding: "utf8",
738
+ stdio: ["pipe", "pipe", "pipe"],
739
+ cwd: homedir(),
740
+ env: cleanEnv
741
+ }).trim();
742
+ const match = output.match(/(\d+\.\d+\.\d+)/);
743
+ logger.debug(`[Claude SDK] Global claude --version output: ${output}`);
744
+ return match ? match[1] : null;
745
+ } catch {
746
+ return null;
747
+ }
748
+ }
749
+ function getCleanEnv() {
750
+ const env = { ...process.env };
751
+ const cwd = process.cwd();
752
+ const pathSep = process.platform === "win32" ? ";" : ":";
753
+ const pathKey = process.platform === "win32" ? "Path" : "PATH";
754
+ const actualPathKey = Object.keys(env).find((k) => k.toLowerCase() === "path") || pathKey;
755
+ if (env[actualPathKey]) {
756
+ const cleanPath = env[actualPathKey].split(pathSep).filter((p) => {
757
+ const normalizedP = p.replace(/\\/g, "/").toLowerCase();
758
+ const normalizedCwd = cwd.replace(/\\/g, "/").toLowerCase();
759
+ return !normalizedP.startsWith(normalizedCwd);
760
+ }).join(pathSep);
761
+ env[actualPathKey] = cleanPath;
762
+ logger.debug(`[Claude SDK] Cleaned PATH, removed local paths from: ${cwd}`);
763
+ }
764
+ if (isBun()) {
765
+ Object.keys(env).forEach((key) => {
766
+ if (key.startsWith("BUN_")) {
767
+ delete env[key];
768
+ }
769
+ });
770
+ logger.debug("[Claude SDK] Removed Bun-specific environment variables for Node.js compatibility");
771
+ }
772
+ return env;
773
+ }
774
+ function findGlobalClaudePath() {
775
+ const homeDir = homedir();
776
+ const cleanEnv = getCleanEnv();
777
+ try {
778
+ execSync("claude --version", {
779
+ encoding: "utf8",
780
+ stdio: ["pipe", "pipe", "pipe"],
781
+ cwd: homeDir,
782
+ env: cleanEnv
783
+ });
784
+ logger.debug("[Claude SDK] Global claude command available (checked with clean PATH)");
785
+ return "claude";
786
+ } catch {
787
+ }
788
+ if (process.platform !== "win32") {
789
+ try {
790
+ const result = execSync("which claude", {
791
+ encoding: "utf8",
792
+ stdio: ["pipe", "pipe", "pipe"],
793
+ cwd: homeDir,
794
+ env: cleanEnv
795
+ }).trim();
796
+ if (result && existsSync(result)) {
797
+ logger.debug(`[Claude SDK] Found global claude path via which: ${result}`);
798
+ return result;
799
+ }
800
+ } catch {
801
+ }
802
+ }
803
+ return null;
804
+ }
805
+ function getDefaultClaudeCodePath() {
806
+ const nodeModulesPath = join(__dirname$1, "..", "..", "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
807
+ if (process.env.HAPPY_CLAUDE_PATH) {
808
+ logger.debug(`[Claude SDK] Using HAPPY_CLAUDE_PATH: ${process.env.HAPPY_CLAUDE_PATH}`);
809
+ return process.env.HAPPY_CLAUDE_PATH;
810
+ }
811
+ if (process.env.HAPPY_USE_BUNDLED_CLAUDE === "1") {
812
+ logger.debug(`[Claude SDK] Forced bundled version: ${nodeModulesPath}`);
813
+ return nodeModulesPath;
814
+ }
815
+ const globalPath = findGlobalClaudePath();
816
+ if (!globalPath) {
817
+ logger.debug(`[Claude SDK] No global claude found, using bundled: ${nodeModulesPath}`);
818
+ return nodeModulesPath;
819
+ }
820
+ const globalVersion = getGlobalClaudeVersion();
821
+ logger.debug(`[Claude SDK] Global version: ${globalVersion || "unknown"}`);
822
+ if (!globalVersion) {
823
+ logger.debug(`[Claude SDK] Cannot compare versions, using global: ${globalPath}`);
824
+ return globalPath;
825
+ }
826
+ return globalPath;
827
+ }
828
+ function logDebug(message) {
829
+ if (isDebug()) {
830
+ logger.debug(message);
831
+ console.log(message);
832
+ }
833
+ }
834
+ async function streamToStdin(stream, stdin, abort) {
835
+ for await (const message of stream) {
836
+ if (abort?.aborted) break;
837
+ stdin.write(JSON.stringify(message) + "\n");
838
+ }
839
+ stdin.end();
840
+ }
841
+
842
+ class Query {
843
+ constructor(childStdin, childStdout, processExitPromise, canCallTool) {
844
+ this.childStdin = childStdin;
845
+ this.childStdout = childStdout;
846
+ this.processExitPromise = processExitPromise;
847
+ this.canCallTool = canCallTool;
848
+ this.readMessages();
849
+ this.sdkMessages = this.readSdkMessages();
850
+ }
851
+ pendingControlResponses = /* @__PURE__ */ new Map();
852
+ cancelControllers = /* @__PURE__ */ new Map();
853
+ sdkMessages;
854
+ inputStream = new Stream();
855
+ canCallTool;
856
+ /**
857
+ * Set an error on the stream
858
+ */
859
+ setError(error) {
860
+ this.inputStream.error(error);
861
+ }
862
+ /**
863
+ * AsyncIterableIterator implementation
864
+ */
865
+ next(...args) {
866
+ return this.sdkMessages.next(...args);
867
+ }
868
+ return(value) {
869
+ if (this.sdkMessages.return) {
870
+ return this.sdkMessages.return(value);
871
+ }
872
+ return Promise.resolve({ done: true, value: void 0 });
873
+ }
874
+ throw(e) {
875
+ if (this.sdkMessages.throw) {
876
+ return this.sdkMessages.throw(e);
877
+ }
878
+ return Promise.reject(e);
879
+ }
880
+ [Symbol.asyncIterator]() {
881
+ return this.sdkMessages;
882
+ }
883
+ /**
884
+ * Read messages from Claude process stdout
885
+ */
886
+ async readMessages() {
887
+ const rl = createInterface({ input: this.childStdout });
888
+ try {
889
+ for await (const line of rl) {
890
+ if (line.trim()) {
891
+ try {
892
+ const message = JSON.parse(line);
893
+ if (message.type === "control_response") {
894
+ const controlResponse = message;
895
+ const requestId = controlResponse.response.request_id;
896
+ const handler = this.pendingControlResponses.get(requestId);
897
+ if (handler) {
898
+ this.pendingControlResponses.delete(requestId);
899
+ handler(controlResponse.response);
900
+ }
901
+ continue;
902
+ } else if (message.type === "control_request") {
903
+ await this.handleControlRequest(message);
904
+ continue;
905
+ } else if (message.type === "control_cancel_request") {
906
+ this.handleControlCancelRequest(message);
907
+ continue;
908
+ }
909
+ this.inputStream.enqueue(message);
910
+ } catch (e) {
911
+ logger.debug(line);
912
+ }
913
+ }
914
+ }
915
+ await this.processExitPromise;
916
+ } catch (error) {
917
+ this.inputStream.error(error);
918
+ } finally {
919
+ this.inputStream.done();
920
+ this.cleanupControllers();
921
+ rl.close();
922
+ }
923
+ }
924
+ /**
925
+ * Async generator for SDK messages
926
+ */
927
+ async *readSdkMessages() {
928
+ for await (const message of this.inputStream) {
929
+ yield message;
930
+ }
931
+ }
932
+ /**
933
+ * Send interrupt request to Claude
934
+ */
935
+ async interrupt() {
936
+ if (!this.childStdin) {
937
+ throw new Error("Interrupt requires --input-format stream-json");
938
+ }
939
+ await this.request({
940
+ subtype: "interrupt"
941
+ }, this.childStdin);
942
+ }
943
+ /**
944
+ * Send control request to Claude process
945
+ */
946
+ request(request, childStdin) {
947
+ const requestId = Math.random().toString(36).substring(2, 15);
948
+ const sdkRequest = {
949
+ request_id: requestId,
950
+ type: "control_request",
951
+ request
952
+ };
953
+ return new Promise((resolve, reject) => {
954
+ this.pendingControlResponses.set(requestId, (response) => {
955
+ if (response.subtype === "success") {
956
+ resolve(response);
957
+ } else {
958
+ reject(new Error(response.error));
959
+ }
960
+ });
961
+ childStdin.write(JSON.stringify(sdkRequest) + "\n");
962
+ });
963
+ }
964
+ /**
965
+ * Handle incoming control requests for tool permissions
966
+ * Replicates the exact logic from the SDK's handleControlRequest method
967
+ */
968
+ async handleControlRequest(request) {
969
+ if (!this.childStdin) {
970
+ logDebug("Cannot handle control request - no stdin available");
971
+ return;
972
+ }
973
+ const controller = new AbortController();
974
+ this.cancelControllers.set(request.request_id, controller);
975
+ try {
976
+ const response = await this.processControlRequest(request, controller.signal);
977
+ const controlResponse = {
978
+ type: "control_response",
979
+ response: {
980
+ subtype: "success",
981
+ request_id: request.request_id,
982
+ response
983
+ }
984
+ };
985
+ this.childStdin.write(JSON.stringify(controlResponse) + "\n");
986
+ } catch (error) {
987
+ const controlErrorResponse = {
988
+ type: "control_response",
989
+ response: {
990
+ subtype: "error",
991
+ request_id: request.request_id,
992
+ error: error instanceof Error ? error.message : String(error)
993
+ }
994
+ };
995
+ this.childStdin.write(JSON.stringify(controlErrorResponse) + "\n");
996
+ } finally {
997
+ this.cancelControllers.delete(request.request_id);
998
+ }
999
+ }
1000
+ /**
1001
+ * Handle control cancel requests
1002
+ * Replicates the exact logic from the SDK's handleControlCancelRequest method
1003
+ */
1004
+ handleControlCancelRequest(request) {
1005
+ const controller = this.cancelControllers.get(request.request_id);
1006
+ if (controller) {
1007
+ controller.abort();
1008
+ this.cancelControllers.delete(request.request_id);
1009
+ }
1010
+ }
1011
+ /**
1012
+ * Process control requests based on subtype
1013
+ * Replicates the exact logic from the SDK's processControlRequest method
1014
+ */
1015
+ async processControlRequest(request, signal) {
1016
+ if (request.request.subtype === "can_use_tool") {
1017
+ if (!this.canCallTool) {
1018
+ throw new Error("canCallTool callback is not provided.");
1019
+ }
1020
+ return this.canCallTool(request.request.tool_name, request.request.input, {
1021
+ signal
1022
+ });
1023
+ }
1024
+ throw new Error("Unsupported control request subtype: " + request.request.subtype);
1025
+ }
1026
+ /**
1027
+ * Cleanup method to abort all pending control requests
1028
+ */
1029
+ cleanupControllers() {
1030
+ for (const [requestId, controller] of this.cancelControllers.entries()) {
1031
+ controller.abort();
1032
+ this.cancelControllers.delete(requestId);
1033
+ }
1034
+ }
1035
+ }
1036
+ function query(config) {
1037
+ const {
1038
+ prompt,
1039
+ options: {
1040
+ allowedTools = [],
1041
+ appendSystemPrompt,
1042
+ customSystemPrompt,
1043
+ cwd,
1044
+ disallowedTools = [],
1045
+ executable = "node",
1046
+ executableArgs = [],
1047
+ maxTurns,
1048
+ mcpServers,
1049
+ pathToClaudeCodeExecutable = getDefaultClaudeCodePath(),
1050
+ permissionMode = "default",
1051
+ continue: continueConversation,
1052
+ resume,
1053
+ forkSession,
1054
+ model,
1055
+ effort,
1056
+ fallbackModel,
1057
+ strictMcpConfig,
1058
+ canCallTool,
1059
+ settingsPath
1060
+ } = {}
1061
+ } = config;
1062
+ if (!process.env.CLAUDE_CODE_ENTRYPOINT) {
1063
+ process.env.CLAUDE_CODE_ENTRYPOINT = "sdk-ts";
1064
+ }
1065
+ const args = ["--output-format", "stream-json", "--verbose"];
1066
+ if (customSystemPrompt) args.push("--system-prompt", customSystemPrompt);
1067
+ if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
1068
+ if (maxTurns) args.push("--max-turns", maxTurns.toString());
1069
+ if (model) args.push("--model", model);
1070
+ if (effort) args.push("--effort", effort);
1071
+ if (canCallTool) {
1072
+ if (typeof prompt === "string") {
1073
+ throw new Error("canCallTool callback requires --input-format stream-json. Please set prompt as an AsyncIterable.");
1074
+ }
1075
+ args.push("--permission-prompt-tool", "stdio");
1076
+ }
1077
+ if (continueConversation) args.push("--continue");
1078
+ if (resume) args.push("--resume", resume);
1079
+ if (forkSession) args.push("--fork-session");
1080
+ if (allowedTools.length > 0) args.push("--allowedTools", allowedTools.join(","));
1081
+ if (disallowedTools.length > 0) args.push("--disallowedTools", disallowedTools.join(","));
1082
+ if (mcpServers && Object.keys(mcpServers).length > 0) {
1083
+ args.push("--mcp-config", JSON.stringify({ mcpServers }));
1084
+ }
1085
+ if (strictMcpConfig) args.push("--strict-mcp-config");
1086
+ if (permissionMode) args.push("--permission-mode", permissionMode);
1087
+ if (settingsPath) args.push("--settings", settingsPath);
1088
+ if (fallbackModel) {
1089
+ if (model && fallbackModel === model) {
1090
+ throw new Error("Fallback model cannot be the same as the main model. Please specify a different model for fallbackModel option.");
1091
+ }
1092
+ args.push("--fallback-model", fallbackModel);
1093
+ }
1094
+ if (typeof prompt === "string") {
1095
+ args.push("--print", prompt.trim());
1096
+ } else {
1097
+ args.push("--input-format", "stream-json");
1098
+ }
1099
+ const isJsFile = pathToClaudeCodeExecutable.endsWith(".js") || pathToClaudeCodeExecutable.endsWith(".cjs");
1100
+ const isCommandOnly = pathToClaudeCodeExecutable === "claude";
1101
+ if (!isCommandOnly && !existsSync(pathToClaudeCodeExecutable)) {
1102
+ throw new ReferenceError(`Claude Code executable not found at ${pathToClaudeCodeExecutable}. Is options.pathToClaudeCodeExecutable set?`);
1103
+ }
1104
+ const spawnCommand = isJsFile ? executable : pathToClaudeCodeExecutable;
1105
+ const spawnArgs = isJsFile ? [...executableArgs, pathToClaudeCodeExecutable, ...args] : args;
1106
+ const spawnEnv = isCommandOnly ? getCleanEnv() : process.env;
1107
+ logDebug(`Spawning Claude Code process: ${spawnCommand} ${spawnArgs.join(" ")} (using ${isCommandOnly ? "clean" : "normal"} env)`);
1108
+ const child = spawn(spawnCommand, spawnArgs, {
1109
+ cwd,
1110
+ stdio: ["pipe", "pipe", "pipe"],
1111
+ // Note: `signal` intentionally omitted – the abort signal is handled
1112
+ // by the explicit listener below so that callers can send an SDK
1113
+ // interrupt() control-request *before* the process is killed.
1114
+ env: spawnEnv,
1115
+ // Use shell on Windows for global binaries and command-only mode
1116
+ shell: !isJsFile && process.platform === "win32"
1117
+ });
1118
+ let childStdin = null;
1119
+ if (typeof prompt === "string") {
1120
+ child.stdin.end();
1121
+ } else {
1122
+ streamToStdin(prompt, child.stdin, config.options?.abort);
1123
+ childStdin = child.stdin;
1124
+ }
1125
+ const stderrChunks = [];
1126
+ child.stderr.on("data", (data) => {
1127
+ const text = data.toString().trim();
1128
+ if (text) {
1129
+ logger.debug(`[Claude SDK] stderr: ${text}`);
1130
+ stderrChunks.push(text);
1131
+ }
1132
+ });
1133
+ let exited = false;
1134
+ child.on("close", () => {
1135
+ exited = true;
1136
+ });
1137
+ const cleanup = () => {
1138
+ if (exited) return;
1139
+ child.kill("SIGTERM");
1140
+ const killTimer = setTimeout(() => {
1141
+ if (!exited) {
1142
+ logDebug("Process did not exit after SIGTERM, sending SIGKILL");
1143
+ child.kill("SIGKILL");
1144
+ }
1145
+ }, 3e3);
1146
+ killTimer.unref();
1147
+ };
1148
+ if (config.options?.abort?.aborted) {
1149
+ cleanup();
1150
+ } else {
1151
+ config.options?.abort?.addEventListener("abort", cleanup);
1152
+ }
1153
+ process.on("exit", cleanup);
1154
+ const processExitPromise = new Promise((resolve) => {
1155
+ child.on("close", (code) => {
1156
+ if (config.options?.abort?.aborted) {
1157
+ query2.setError(new AbortError("Claude Code process aborted by user"));
1158
+ }
1159
+ if (code !== 0) {
1160
+ const stderrOutput = stderrChunks.join("\n");
1161
+ const errorMsg = stderrOutput ? `Claude Code process exited with code ${code}: ${stderrOutput}` : `Claude Code process exited with code ${code}`;
1162
+ query2.setError(new Error(errorMsg));
1163
+ } else {
1164
+ resolve();
1165
+ }
1166
+ });
1167
+ });
1168
+ const query2 = new Query(childStdin, child.stdout, processExitPromise, canCallTool);
1169
+ child.on("error", (error) => {
1170
+ if (config.options?.abort?.aborted) {
1171
+ query2.setError(new AbortError("Claude Code process aborted by user"));
1172
+ } else {
1173
+ query2.setError(new Error(`Failed to spawn Claude Code process: ${error.message}`));
1174
+ }
1175
+ });
1176
+ processExitPromise.finally(() => {
1177
+ cleanup();
1178
+ config.options?.abort?.removeEventListener("abort", cleanup);
1179
+ if (process.env.CLAUDE_SDK_MCP_SERVERS) {
1180
+ delete process.env.CLAUDE_SDK_MCP_SERVERS;
1181
+ }
1182
+ });
1183
+ return query2;
1184
+ }
1185
+
1186
+ function mapToClaudeMode(mode) {
1187
+ const codexToClaudeMap = {
1188
+ "yolo": "bypassPermissions",
1189
+ "safe-yolo": "default",
1190
+ "read-only": "default"
1191
+ };
1192
+ return codexToClaudeMap[mode] ?? mode;
1193
+ }
1194
+
1195
+ class PushableAsyncIterable {
1196
+ queue = [];
1197
+ waiters = [];
1198
+ isDone = false;
1199
+ error = null;
1200
+ started = false;
1201
+ constructor() {
1202
+ }
1203
+ /**
1204
+ * Push a value to the iterable
1205
+ */
1206
+ push(value) {
1207
+ if (this.isDone) {
1208
+ throw new Error("Cannot push to completed iterable");
1209
+ }
1210
+ if (this.error) {
1211
+ throw this.error;
1212
+ }
1213
+ const waiter = this.waiters.shift();
1214
+ if (waiter) {
1215
+ waiter.resolve({ done: false, value });
1216
+ } else {
1217
+ this.queue.push(value);
1218
+ }
1219
+ }
1220
+ /**
1221
+ * Mark the iterable as complete
1222
+ */
1223
+ end() {
1224
+ if (this.isDone) {
1225
+ return;
1226
+ }
1227
+ this.isDone = true;
1228
+ this.cleanup();
1229
+ }
1230
+ /**
1231
+ * Set an error on the iterable
1232
+ */
1233
+ setError(err) {
1234
+ if (this.isDone) {
1235
+ return;
1236
+ }
1237
+ this.error = err;
1238
+ this.isDone = true;
1239
+ this.cleanup();
1240
+ }
1241
+ /**
1242
+ * Cleanup waiting consumers
1243
+ */
1244
+ cleanup() {
1245
+ while (this.waiters.length > 0) {
1246
+ const waiter = this.waiters.shift();
1247
+ if (this.error) {
1248
+ waiter.reject(this.error);
1249
+ } else {
1250
+ waiter.resolve({ done: true, value: void 0 });
1251
+ }
1252
+ }
1253
+ }
1254
+ /**
1255
+ * AsyncIterableIterator implementation
1256
+ */
1257
+ async next() {
1258
+ if (this.queue.length > 0) {
1259
+ return { done: false, value: this.queue.shift() };
1260
+ }
1261
+ if (this.isDone) {
1262
+ if (this.error) {
1263
+ throw this.error;
1264
+ }
1265
+ return { done: true, value: void 0 };
1266
+ }
1267
+ return new Promise((resolve, reject) => {
1268
+ this.waiters.push({ resolve, reject });
1269
+ });
1270
+ }
1271
+ /**
1272
+ * AsyncIterableIterator return implementation
1273
+ */
1274
+ async return(_value) {
1275
+ this.end();
1276
+ return { done: true, value: void 0 };
1277
+ }
1278
+ /**
1279
+ * AsyncIterableIterator throw implementation
1280
+ */
1281
+ async throw(e) {
1282
+ this.setError(e instanceof Error ? e : new Error(String(e)));
1283
+ throw this.error;
1284
+ }
1285
+ /**
1286
+ * Make this iterable
1287
+ */
1288
+ [Symbol.asyncIterator]() {
1289
+ if (this.started) {
1290
+ throw new Error("PushableAsyncIterable can only be iterated once");
1291
+ }
1292
+ this.started = true;
1293
+ return this;
1294
+ }
1295
+ /**
1296
+ * Check if the iterable is done
1297
+ */
1298
+ get done() {
1299
+ return this.isDone;
1300
+ }
1301
+ /**
1302
+ * Check if the iterable has an error
1303
+ */
1304
+ get hasError() {
1305
+ return this.error !== null;
1306
+ }
1307
+ /**
1308
+ * Get the current queue size
1309
+ */
1310
+ get queueSize() {
1311
+ return this.queue.length;
1312
+ }
1313
+ /**
1314
+ * Get the number of waiting consumers
1315
+ */
1316
+ get waiterCount() {
1317
+ return this.waiters.length;
1318
+ }
1319
+ }
1320
+
1321
+ async function awaitFileExist(file, timeout = 1e4) {
1322
+ const startTime = Date.now();
1323
+ while (Date.now() - startTime < timeout) {
1324
+ try {
1325
+ await access(file);
1326
+ return true;
1327
+ } catch (e) {
1328
+ await delay(1e3);
1329
+ }
1330
+ }
1331
+ return false;
1332
+ }
1333
+
1334
+ async function claudeRemote(opts) {
1335
+ let startFrom = opts.sessionId;
1336
+ if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
1337
+ startFrom = null;
1338
+ }
1339
+ let forkSession = false;
1340
+ if (opts.claudeArgs) {
1341
+ if (opts.claudeArgs.includes("--fork-session")) {
1342
+ forkSession = true;
1343
+ logger.debug("[claudeRemote] Found --fork-session flag");
1344
+ }
1345
+ if (!startFrom) {
1346
+ for (let i = 0; i < opts.claudeArgs.length; i++) {
1347
+ if (opts.claudeArgs[i] === "--resume") {
1348
+ if (i + 1 < opts.claudeArgs.length) {
1349
+ const nextArg = opts.claudeArgs[i + 1];
1350
+ if (!nextArg.startsWith("-") && nextArg.includes("-")) {
1351
+ startFrom = nextArg;
1352
+ logger.debug(`[claudeRemote] Found --resume with session ID: ${startFrom}`);
1353
+ break;
1354
+ } else {
1355
+ logger.debug("[claudeRemote] Found --resume without session ID - not supported in remote mode");
1356
+ break;
1357
+ }
1358
+ } else {
1359
+ logger.debug("[claudeRemote] Found --resume without session ID - not supported in remote mode");
1360
+ break;
1361
+ }
1362
+ }
1363
+ }
1364
+ }
1365
+ }
1366
+ if (opts.claudeEnvVars) {
1367
+ Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
1368
+ process.env[key] = value;
1369
+ });
1370
+ }
1371
+ const initial = await opts.nextMessage();
1372
+ if (!initial) {
1373
+ return;
1374
+ }
1375
+ const initialText = typeof initial.message === "string" ? initial.message : initial.message.text;
1376
+ const specialCommand = parseSpecialCommand(initialText);
1377
+ if (specialCommand.type === "clear") {
1378
+ if (opts.onCompletionEvent) {
1379
+ opts.onCompletionEvent("Context was reset");
1380
+ }
1381
+ if (opts.onSessionReset) {
1382
+ opts.onSessionReset();
1383
+ }
1384
+ if (opts.onThinkingChange) {
1385
+ opts.onThinkingChange(false);
1386
+ }
1387
+ return;
1388
+ }
1389
+ let isCompactCommand = false;
1390
+ if (specialCommand.type === "compact") {
1391
+ logger.debug("[claudeRemote] /compact command detected - will process as normal but with compaction behavior");
1392
+ isCompactCommand = true;
1393
+ if (opts.onCompletionEvent) {
1394
+ opts.onCompletionEvent("Compaction started");
1395
+ }
1396
+ }
1397
+ let mode = initial.mode;
1398
+ const sdkOptions = {
1399
+ cwd: opts.path,
1400
+ resume: startFrom ?? void 0,
1401
+ forkSession: forkSession || void 0,
1402
+ mcpServers: opts.mcpServers,
1403
+ permissionMode: mapToClaudeMode(initial.mode.permissionMode),
1404
+ model: initial.mode.model,
1405
+ effort: initial.mode.reasoningEffort,
1406
+ fallbackModel: initial.mode.fallbackModel,
1407
+ customSystemPrompt: initial.mode.customSystemPrompt ? initial.mode.customSystemPrompt + "\n\n" + systemPrompt : void 0,
1408
+ appendSystemPrompt: initial.mode.appendSystemPrompt ? initial.mode.appendSystemPrompt + "\n\n" + systemPrompt : systemPrompt,
1409
+ allowedTools: initial.mode.allowedTools ? initial.mode.allowedTools.concat(opts.allowedTools) : opts.allowedTools,
1410
+ disallowedTools: initial.mode.disallowedTools,
1411
+ canCallTool: (toolName, input, options) => opts.canCallTool(toolName, input, mode, options),
1412
+ executable: opts.jsRuntime ?? "node",
1413
+ abort: opts.signal,
1414
+ pathToClaudeCodeExecutable: (() => {
1415
+ return resolve(join(projectPath(), "scripts", "claude_remote_launcher.cjs"));
1416
+ })(),
1417
+ settingsPath: opts.hookSettingsPath
1418
+ };
1419
+ let thinking = false;
1420
+ const updateThinking = (newThinking) => {
1421
+ if (thinking !== newThinking) {
1422
+ thinking = newThinking;
1423
+ logger.debug(`[claudeRemote] Thinking state changed to: ${thinking}`);
1424
+ if (opts.onThinkingChange) {
1425
+ opts.onThinkingChange(thinking);
1426
+ }
1427
+ }
1428
+ };
1429
+ let messages = new PushableAsyncIterable();
1430
+ const toClaudeMessageContent = async (message) => {
1431
+ if (typeof message === "object" && "type" in message) {
1432
+ if (message.type === "mixed") {
1433
+ return formatMessageForClaude(message.text, message.images);
1434
+ }
1435
+ return message.text;
1436
+ }
1437
+ return message;
1438
+ };
1439
+ const initialMessageContent = await toClaudeMessageContent(initial.message);
1440
+ messages.push({
1441
+ type: "user",
1442
+ message: {
1443
+ role: "user",
1444
+ content: initialMessageContent
1445
+ }
1446
+ });
1447
+ let awaitingNextUserMessage = false;
1448
+ let streamEnded = false;
1449
+ const pumpNextUserMessage = async () => {
1450
+ if (awaitingNextUserMessage || streamEnded) {
1451
+ return;
1452
+ }
1453
+ awaitingNextUserMessage = true;
1454
+ try {
1455
+ const next = await opts.nextMessage();
1456
+ if (!next) {
1457
+ streamEnded = true;
1458
+ messages.end();
1459
+ return;
1460
+ }
1461
+ mode = next.mode;
1462
+ const messageContent = await toClaudeMessageContent(next.message);
1463
+ messages.push({
1464
+ type: "user",
1465
+ message: {
1466
+ role: "user",
1467
+ content: messageContent
1468
+ }
1469
+ });
1470
+ } catch (error) {
1471
+ logger.debug("[claudeRemote] Failed to fetch next user message", error);
1472
+ streamEnded = true;
1473
+ messages.end();
1474
+ } finally {
1475
+ awaitingNextUserMessage = false;
1476
+ }
1477
+ };
1478
+ const response = query({
1479
+ prompt: messages,
1480
+ options: sdkOptions
1481
+ });
1482
+ opts.onQueryCreated?.(response);
1483
+ updateThinking(true);
1484
+ try {
1485
+ logger.debug(`[claudeRemote] Starting to iterate over response`);
1486
+ for await (const message of response) {
1487
+ logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
1488
+ opts.onMessage(message);
1489
+ if (message.type === "system" && message.subtype === "init") {
1490
+ updateThinking(true);
1491
+ const systemInit = message;
1492
+ if (systemInit.session_id) {
1493
+ logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${systemInit.session_id}`);
1494
+ const projectDir = getProjectPath(opts.path);
1495
+ const found = await awaitFileExist(join(projectDir, `${systemInit.session_id}.jsonl`));
1496
+ logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
1497
+ opts.onSessionFound(systemInit.session_id);
1498
+ }
1499
+ }
1500
+ if (message.type === "result") {
1501
+ updateThinking(false);
1502
+ logger.debug("[claudeRemote] Result received, waiting for next user message");
1503
+ if (isCompactCommand) {
1504
+ logger.debug("[claudeRemote] Compaction completed");
1505
+ if (opts.onCompletionEvent) {
1506
+ opts.onCompletionEvent("Compaction completed");
1507
+ }
1508
+ isCompactCommand = false;
1509
+ }
1510
+ opts.onReady();
1511
+ void pumpNextUserMessage();
1512
+ }
1513
+ if (message.type === "user") {
1514
+ const msg = message;
1515
+ if (msg.message.role === "user" && Array.isArray(msg.message.content)) {
1516
+ for (let c of msg.message.content) {
1517
+ if (c.type === "tool_result" && c.tool_use_id && opts.isAborted(c.tool_use_id)) {
1518
+ logger.debug("[claudeRemote] Tool aborted, exiting claudeRemote");
1519
+ return;
1520
+ }
1521
+ }
1522
+ }
1523
+ }
1524
+ }
1525
+ } catch (e) {
1526
+ if (e instanceof AbortError) {
1527
+ logger.debug(`[claudeRemote] Aborted`);
1528
+ } else {
1529
+ throw e;
1530
+ }
1531
+ } finally {
1532
+ updateThinking(false);
1533
+ }
1534
+ }
1535
+
1536
+ 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.`;
1537
+ const PLAN_FAKE_RESTART = `PlEaZe Continue with plan.`;
1538
+
1539
+ const STANDARD_TOOLS = {
1540
+ // File operations
1541
+ "Read": "Read File",
1542
+ "Write": "Write File",
1543
+ "Edit": "Edit File",
1544
+ "MultiEdit": "Edit File",
1545
+ "NotebookEdit": "Edit Notebook",
1546
+ // Search and navigation
1547
+ "Glob": "Find Files",
1548
+ "Grep": "Search in Files",
1549
+ "LS": "List Directory",
1550
+ // Command execution
1551
+ "Bash": "Run Command",
1552
+ "BashOutput": "Check Command Output",
1553
+ "KillBash": "Stop Command",
1554
+ // Task management
1555
+ "TodoWrite": "Update Tasks",
1556
+ "TodoRead": "Read Tasks",
1557
+ "Task": "Launch Agent",
1558
+ // Web tools
1559
+ "WebFetch": "Fetch Web Page",
1560
+ "WebSearch": "Search Web",
1561
+ // Special cases
1562
+ "exit_plan_mode": "Execute Plan",
1563
+ "ExitPlanMode": "Execute Plan"
1564
+ };
1565
+ function toTitleCase(str) {
1566
+ return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
1567
+ }
1568
+ function getToolName(toolName) {
1569
+ if (STANDARD_TOOLS[toolName]) {
1570
+ return STANDARD_TOOLS[toolName];
1571
+ }
1572
+ if (toolName.startsWith("mcp__") || toolName.startsWith("mcp:")) {
1573
+ const parts = toolName.replace(/:/g, "__").split("__");
1574
+ if (parts.length >= 3) {
1575
+ const server = toTitleCase(parts[1]);
1576
+ const action = toTitleCase(parts.slice(2).join("_"));
1577
+ return `${server}: ${action}`;
1578
+ }
1579
+ }
1580
+ return toTitleCase(toolName);
1581
+ }
1582
+
1583
+ function getToolDescriptor(toolName) {
1584
+ if (toolName === "exit_plan_mode" || toolName === "ExitPlanMode") {
1585
+ return { edit: false, exitPlan: true };
1586
+ }
1587
+ if (toolName === "Edit" || toolName === "MultiEdit" || toolName === "Write" || toolName === "NotebookEdit") {
1588
+ return { edit: true, exitPlan: false };
1589
+ }
1590
+ return { edit: false, exitPlan: false };
1591
+ }
1592
+
1593
+ class PermissionHandler {
1594
+ toolCalls = [];
1595
+ responses = /* @__PURE__ */ new Map();
1596
+ pendingRequests = /* @__PURE__ */ new Map();
1597
+ session;
1598
+ allowedTools = /* @__PURE__ */ new Set();
1599
+ allowedBashLiterals = /* @__PURE__ */ new Set();
1600
+ allowedBashPrefixes = /* @__PURE__ */ new Set();
1601
+ permissionMode = "default";
1602
+ lastNonPlanPermissionMode = "default";
1603
+ onPermissionRequestCallback;
1604
+ isUserInteractionTool(toolName) {
1605
+ return ["AskUserQuestion", "ExitPlanMode", "exit_plan_mode"].includes(toolName);
1606
+ }
1607
+ constructor(session) {
1608
+ this.session = session;
1609
+ this.setupClientHandler();
1610
+ }
1611
+ /**
1612
+ * Set callback to trigger when permission request is made
1613
+ */
1614
+ setOnPermissionRequest(callback) {
1615
+ this.onPermissionRequestCallback = callback;
1616
+ }
1617
+ handleModeChange(mode) {
1618
+ this.permissionMode = mode;
1619
+ if (mode !== "plan") {
1620
+ this.lastNonPlanPermissionMode = mode;
1621
+ }
1622
+ }
1623
+ getResumeModeAfterPlan(responseMode) {
1624
+ if (responseMode && responseMode !== "plan") {
1625
+ return responseMode;
1626
+ }
1627
+ if (this.permissionMode !== "plan") {
1628
+ return this.permissionMode;
1629
+ }
1630
+ return this.lastNonPlanPermissionMode;
1631
+ }
1632
+ /**
1633
+ * Handler response
1634
+ */
1635
+ handlePermissionResponse(response, pending) {
1636
+ if (response.allowTools && response.allowTools.length > 0) {
1637
+ response.allowTools.forEach((tool) => {
1638
+ if (tool.startsWith("Bash(") || tool === "Bash") {
1639
+ this.parseBashPermission(tool);
1640
+ } else {
1641
+ this.allowedTools.add(tool);
1642
+ }
1643
+ });
1644
+ this.persistAllowedTools(response.allowTools);
1645
+ }
1646
+ if (response.mode) {
1647
+ this.permissionMode = response.mode;
1648
+ if (response.mode !== "plan") {
1649
+ this.lastNonPlanPermissionMode = response.mode;
1650
+ }
1651
+ }
1652
+ if (response.approved && response.mode === "bypassPermissions") {
1653
+ this.approvePendingNonInteractiveRequests(response);
1654
+ }
1655
+ if (pending.toolName === "exit_plan_mode" || pending.toolName === "ExitPlanMode") {
1656
+ logger.debug("Plan mode result received", response);
1657
+ if (response.approved) {
1658
+ logger.debug("Plan approved - injecting PLAN_FAKE_RESTART");
1659
+ const resumeMode = this.getResumeModeAfterPlan(response.mode);
1660
+ this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: resumeMode });
1661
+ pending.resolve({ behavior: "deny", message: PLAN_FAKE_REJECT });
1662
+ } else {
1663
+ pending.resolve({ behavior: "deny", message: response.reason || "Plan rejected" });
1664
+ }
1665
+ } else if (pending.toolName === "AskUserQuestion" && response.approved && response.answers) {
1666
+ pending.resolve({ behavior: "deny", message: formatAskUserQuestionAnswers(response.answers) });
1667
+ } else {
1668
+ const updatedInput = pending.input || {};
1669
+ const result = response.approved ? { behavior: "allow", updatedInput } : { 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.` };
1670
+ pending.resolve(result);
1671
+ }
1672
+ }
1673
+ /**
1674
+ * Creates the canCallTool callback for the SDK
1675
+ */
1676
+ handleToolCall = async (toolName, input, mode, options) => {
1677
+ if (toolName === "Bash") {
1678
+ const inputObj = input;
1679
+ if (inputObj?.command) {
1680
+ if (this.allowedBashLiterals.has(inputObj.command)) {
1681
+ return { behavior: "allow", updatedInput: input };
1682
+ }
1683
+ for (const prefix of this.allowedBashPrefixes) {
1684
+ if (inputObj.command.startsWith(prefix)) {
1685
+ return { behavior: "allow", updatedInput: input };
1686
+ }
1687
+ }
1688
+ }
1689
+ } else if (this.allowedTools.has(toolName)) {
1690
+ return { behavior: "allow", updatedInput: input };
1691
+ }
1692
+ const descriptor = getToolDescriptor(toolName);
1693
+ const isUserInteractionTool = this.isUserInteractionTool(toolName);
1694
+ if (this.permissionMode === "yolo" && !isUserInteractionTool) {
1695
+ return { behavior: "allow", updatedInput: input };
1696
+ }
1697
+ if (this.permissionMode === "bypassPermissions" && !isUserInteractionTool) {
1698
+ return { behavior: "allow", updatedInput: input };
1699
+ }
1700
+ if (this.permissionMode === "acceptEdits" && descriptor.edit) {
1701
+ return { behavior: "allow", updatedInput: input };
1702
+ }
1703
+ let toolCallId = this.resolveToolCallId(toolName, input);
1704
+ if (!toolCallId) {
1705
+ await delay(1e3);
1706
+ toolCallId = this.resolveToolCallId(toolName, input);
1707
+ if (!toolCallId) {
1708
+ throw new Error(`Could not resolve tool call ID for ${toolName}`);
1709
+ }
1710
+ }
1711
+ return this.handlePermissionRequest(toolCallId, toolName, input, options.signal);
1712
+ };
1713
+ /**
1714
+ * Handles individual permission requests
1715
+ */
1716
+ async handlePermissionRequest(id, toolName, input, signal) {
1717
+ return new Promise((resolve, reject) => {
1718
+ const abortHandler = () => {
1719
+ this.pendingRequests.delete(id);
1720
+ reject(new Error("Permission request aborted"));
1721
+ };
1722
+ signal.addEventListener("abort", abortHandler, { once: true });
1723
+ this.pendingRequests.set(id, {
1724
+ resolve: (result) => {
1725
+ signal.removeEventListener("abort", abortHandler);
1726
+ resolve(result);
1727
+ },
1728
+ reject: (error) => {
1729
+ signal.removeEventListener("abort", abortHandler);
1730
+ reject(error);
1731
+ },
1732
+ toolName,
1733
+ input
1734
+ });
1735
+ if (this.onPermissionRequestCallback) {
1736
+ this.onPermissionRequestCallback(id);
1737
+ }
1738
+ this.session.api.push().sendToAllDevices(
1739
+ "Permission Request",
1740
+ `Claude wants to ${getToolName(toolName)}`,
1741
+ {
1742
+ sessionId: this.session.client.sessionId,
1743
+ requestId: id,
1744
+ tool: toolName,
1745
+ type: "permission_request"
1746
+ }
1747
+ );
1748
+ this.session.client.updateAgentState((currentState) => ({
1749
+ ...currentState,
1750
+ requests: {
1751
+ ...currentState.requests,
1752
+ [id]: {
1753
+ tool: toolName,
1754
+ arguments: input,
1755
+ createdAt: Date.now()
1756
+ }
1757
+ }
1758
+ }));
1759
+ logger.debug(`Permission request sent for tool call ${id}: ${toolName}`);
1760
+ });
1761
+ }
1762
+ approvePendingNonInteractiveRequests(response) {
1763
+ const pendingSnapshot = Array.from(this.pendingRequests.entries());
1764
+ const completedAt = Date.now();
1765
+ for (const [id, pending] of pendingSnapshot) {
1766
+ if (this.isUserInteractionTool(pending.toolName)) {
1767
+ continue;
1768
+ }
1769
+ this.pendingRequests.delete(id);
1770
+ const updatedInput = pending.input || {};
1771
+ pending.resolve({ behavior: "allow", updatedInput });
1772
+ }
1773
+ if (pendingSnapshot.length === 0) {
1774
+ return;
1775
+ }
1776
+ this.session.client.updateAgentState((currentState) => {
1777
+ const requests = { ...currentState.requests || {} };
1778
+ const completedRequests = { ...currentState.completedRequests };
1779
+ let changed = false;
1780
+ for (const [id, pending] of pendingSnapshot) {
1781
+ if (this.isUserInteractionTool(pending.toolName)) {
1782
+ continue;
1783
+ }
1784
+ const request = requests[id];
1785
+ if (!request) {
1786
+ continue;
1787
+ }
1788
+ delete requests[id];
1789
+ completedRequests[id] = {
1790
+ ...request,
1791
+ completedAt,
1792
+ status: "approved",
1793
+ reason: response.reason,
1794
+ mode: response.mode,
1795
+ allowTools: response.allowTools
1796
+ };
1797
+ changed = true;
1798
+ }
1799
+ if (!changed) {
1800
+ return currentState;
1801
+ }
1802
+ return {
1803
+ ...currentState,
1804
+ requests,
1805
+ completedRequests
1806
+ };
1807
+ });
1808
+ }
1809
+ /**
1810
+ * Persists allowed tools to .claude/settings.local.json so they survive across sessions.
1811
+ * This matches Claude Code CLI's native behavior for "Allow always".
1812
+ */
1813
+ async persistAllowedTools(tools) {
1814
+ try {
1815
+ const settingsDir = join(this.session.path, ".claude");
1816
+ const settingsPath = join(settingsDir, "settings.local.json");
1817
+ let settings = {};
1818
+ try {
1819
+ const content = await readFile(settingsPath, "utf-8");
1820
+ settings = JSON.parse(content);
1821
+ } catch {
1822
+ }
1823
+ if (!settings.permissions) {
1824
+ settings.permissions = {};
1825
+ }
1826
+ if (!Array.isArray(settings.permissions.allow)) {
1827
+ settings.permissions.allow = [];
1828
+ }
1829
+ let changed = false;
1830
+ for (const tool of tools) {
1831
+ if (!settings.permissions.allow.includes(tool)) {
1832
+ settings.permissions.allow.push(tool);
1833
+ changed = true;
1834
+ }
1835
+ }
1836
+ if (changed) {
1837
+ await mkdir(settingsDir, { recursive: true });
1838
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1839
+ logger.debug(`Persisted allowed tools to ${settingsPath}: ${tools.join(", ")}`);
1840
+ }
1841
+ } catch (error) {
1842
+ logger.debug(`Failed to persist allowed tools: ${error}`);
1843
+ }
1844
+ }
1845
+ /**
1846
+ * Parses Bash permission strings into literal and prefix sets
1847
+ */
1848
+ parseBashPermission(permission) {
1849
+ if (permission === "Bash") {
1850
+ return;
1851
+ }
1852
+ const bashPattern = /^Bash\((.+?)\)$/;
1853
+ const match = permission.match(bashPattern);
1854
+ if (!match) {
1855
+ return;
1856
+ }
1857
+ const command = match[1];
1858
+ if (command.endsWith(":*")) {
1859
+ const prefix = command.slice(0, -2);
1860
+ this.allowedBashPrefixes.add(prefix);
1861
+ } else {
1862
+ this.allowedBashLiterals.add(command);
1863
+ }
1864
+ }
1865
+ /**
1866
+ * Resolves tool call ID based on tool name and input
1867
+ */
1868
+ resolveToolCallId(name, args) {
1869
+ for (let i = this.toolCalls.length - 1; i >= 0; i--) {
1870
+ const call = this.toolCalls[i];
1871
+ if (call.name === name && isDeepStrictEqual(call.input, args)) {
1872
+ if (call.used) {
1873
+ return null;
1874
+ }
1875
+ call.used = true;
1876
+ return call.id;
1877
+ }
1878
+ }
1879
+ return null;
1880
+ }
1881
+ /**
1882
+ * Handles messages to track tool calls
1883
+ */
1884
+ onMessage(message) {
1885
+ if (message.type === "assistant") {
1886
+ const assistantMsg = message;
1887
+ if (assistantMsg.message && assistantMsg.message.content) {
1888
+ for (const block of assistantMsg.message.content) {
1889
+ if (block.type === "tool_use") {
1890
+ this.toolCalls.push({
1891
+ id: block.id,
1892
+ name: block.name,
1893
+ input: block.input,
1894
+ used: false
1895
+ });
1896
+ }
1897
+ }
1898
+ }
1899
+ }
1900
+ if (message.type === "user") {
1901
+ const userMsg = message;
1902
+ if (userMsg.message && userMsg.message.content && Array.isArray(userMsg.message.content)) {
1903
+ for (const block of userMsg.message.content) {
1904
+ if (block.type === "tool_result" && block.tool_use_id) {
1905
+ const toolCall = this.toolCalls.find((tc) => tc.id === block.tool_use_id);
1906
+ if (toolCall && !toolCall.used) {
1907
+ toolCall.used = true;
1908
+ }
1909
+ }
1910
+ }
1911
+ }
1912
+ }
1913
+ }
1914
+ /**
1915
+ * Checks if a tool call is rejected
1916
+ */
1917
+ isAborted(toolCallId) {
1918
+ if (this.responses.get(toolCallId)?.approved === false) {
1919
+ return true;
1920
+ }
1921
+ const toolCall = this.toolCalls.find((tc) => tc.id === toolCallId);
1922
+ if (toolCall && (toolCall.name === "exit_plan_mode" || toolCall.name === "ExitPlanMode")) {
1923
+ return true;
1924
+ }
1925
+ return false;
1926
+ }
1927
+ /**
1928
+ * Resets all state for new sessions
1929
+ */
1930
+ reset() {
1931
+ this.toolCalls = [];
1932
+ this.responses.clear();
1933
+ this.allowedTools.clear();
1934
+ this.allowedBashLiterals.clear();
1935
+ this.allowedBashPrefixes.clear();
1936
+ this.permissionMode = "default";
1937
+ this.lastNonPlanPermissionMode = "default";
1938
+ for (const [, pending] of this.pendingRequests.entries()) {
1939
+ pending.reject(new Error("Session reset"));
1940
+ }
1941
+ this.pendingRequests.clear();
1942
+ this.session.client.updateAgentState((currentState) => {
1943
+ const pendingRequests = currentState.requests || {};
1944
+ const completedRequests = { ...currentState.completedRequests };
1945
+ for (const [id, request] of Object.entries(pendingRequests)) {
1946
+ completedRequests[id] = {
1947
+ ...request,
1948
+ completedAt: Date.now(),
1949
+ status: "canceled",
1950
+ reason: "Session switched to local mode"
1951
+ };
1952
+ }
1953
+ return {
1954
+ ...currentState,
1955
+ requests: {},
1956
+ // Clear all pending requests
1957
+ completedRequests
1958
+ };
1959
+ });
1960
+ }
1961
+ /**
1962
+ * Sets up the client handler for permission responses
1963
+ */
1964
+ setupClientHandler() {
1965
+ this.session.client.rpcHandlerManager.registerHandler("permission", async (message) => {
1966
+ logger.debug(`Permission response: ${JSON.stringify(message)}`);
1967
+ const id = message.id;
1968
+ const pending = this.pendingRequests.get(id);
1969
+ if (!pending) {
1970
+ logger.debug("Permission request not found or already resolved");
1971
+ return;
1972
+ }
1973
+ this.responses.set(id, { ...message, receivedAt: Date.now() });
1974
+ this.pendingRequests.delete(id);
1975
+ this.handlePermissionResponse(message, pending);
1976
+ this.session.client.updateAgentState((currentState) => {
1977
+ const request = currentState.requests?.[id];
1978
+ if (!request) return currentState;
1979
+ let r = { ...currentState.requests };
1980
+ delete r[id];
1981
+ return {
1982
+ ...currentState,
1983
+ requests: r,
1984
+ completedRequests: {
1985
+ ...currentState.completedRequests,
1986
+ [id]: {
1987
+ ...request,
1988
+ completedAt: Date.now(),
1989
+ status: message.approved ? "approved" : "denied",
1990
+ reason: message.reason,
1991
+ mode: message.mode,
1992
+ allowTools: message.allowTools,
1993
+ answers: message.answers
1994
+ }
1995
+ }
1996
+ };
1997
+ });
1998
+ });
1999
+ }
2000
+ /**
2001
+ * Gets the responses map (for compatibility with existing code)
2002
+ */
2003
+ getResponses() {
2004
+ return this.responses;
2005
+ }
2006
+ }
2007
+ function formatAskUserQuestionAnswers(answers) {
2008
+ const lines = Object.entries(answers).map(([header, value]) => `- ${header}\uFF1A${value}`).join("\n");
2009
+ return `\u6211\u7684\u9009\u62E9\u662F\uFF1A
2010
+ ${lines}
2011
+
2012
+ \u8BF7\u6839\u636E\u4EE5\u4E0A\u9009\u62E9\u7EE7\u7EED\u3002`;
2013
+ }
2014
+
2015
+ function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
2016
+ logger.debugLargeJson("[CLAUDE INK] Message from remote mode:", message);
2017
+ switch (message.type) {
2018
+ case "system": {
2019
+ const sysMsg = message;
2020
+ if (sysMsg.subtype === "init") {
2021
+ messageBuffer.addMessage("\u2500".repeat(40), "status");
2022
+ messageBuffer.addMessage(`\u{1F680} Session initialized: ${sysMsg.session_id}`, "system");
2023
+ messageBuffer.addMessage(` Model: ${sysMsg.model}`, "status");
2024
+ messageBuffer.addMessage(` CWD: ${sysMsg.cwd}`, "status");
2025
+ if (sysMsg.tools && sysMsg.tools.length > 0) {
2026
+ messageBuffer.addMessage(` Tools: ${sysMsg.tools.join(", ")}`, "status");
2027
+ }
2028
+ messageBuffer.addMessage("\u2500".repeat(40), "status");
2029
+ }
2030
+ break;
2031
+ }
2032
+ case "user": {
2033
+ const userMsg = message;
2034
+ if (userMsg.message && typeof userMsg.message === "object" && "content" in userMsg.message) {
2035
+ const content = userMsg.message.content;
2036
+ if (typeof content === "string") {
2037
+ messageBuffer.addMessage(`\u{1F464} User: ${content}`, "user");
2038
+ } else if (Array.isArray(content)) {
2039
+ for (const block of content) {
2040
+ if (block.type === "text") {
2041
+ messageBuffer.addMessage(`\u{1F464} User: ${block.text}`, "user");
2042
+ } else if (block.type === "tool_result") {
2043
+ messageBuffer.addMessage(`\u2705 Tool Result (ID: ${block.tool_use_id})`, "result");
2044
+ if (block.content) {
2045
+ const outputStr = typeof block.content === "string" ? block.content : JSON.stringify(block.content, null, 2);
2046
+ const maxLength = 200;
2047
+ if (outputStr.length > maxLength) {
2048
+ messageBuffer.addMessage(outputStr.substring(0, maxLength) + "... (truncated)", "result");
2049
+ } else {
2050
+ messageBuffer.addMessage(outputStr, "result");
2051
+ }
2052
+ }
2053
+ }
2054
+ }
2055
+ } else {
2056
+ messageBuffer.addMessage(`\u{1F464} User: ${JSON.stringify(content, null, 2)}`, "user");
2057
+ }
2058
+ }
2059
+ break;
2060
+ }
2061
+ case "assistant": {
2062
+ const assistantMsg = message;
2063
+ if (assistantMsg.message && assistantMsg.message.content) {
2064
+ messageBuffer.addMessage("\u{1F916} Assistant:", "assistant");
2065
+ for (const block of assistantMsg.message.content) {
2066
+ if (block.type === "text") {
2067
+ messageBuffer.addMessage(block.text || "", "assistant");
2068
+ } else if (block.type === "tool_use") {
2069
+ messageBuffer.addMessage(`\u{1F527} Tool: ${block.name}`, "tool");
2070
+ if (block.input) {
2071
+ const inputStr = JSON.stringify(block.input, null, 2);
2072
+ const maxLength = 500;
2073
+ if (inputStr.length > maxLength) {
2074
+ messageBuffer.addMessage(`Input: ${inputStr.substring(0, maxLength)}... (truncated)`, "tool");
2075
+ } else {
2076
+ messageBuffer.addMessage(`Input: ${inputStr}`, "tool");
2077
+ }
2078
+ }
2079
+ }
2080
+ }
2081
+ }
2082
+ break;
2083
+ }
2084
+ case "result": {
2085
+ const resultMsg = message;
2086
+ if (resultMsg.subtype === "success") {
2087
+ if ("result" in resultMsg && resultMsg.result) {
2088
+ messageBuffer.addMessage("\u2728 Summary:", "result");
2089
+ messageBuffer.addMessage(resultMsg.result || "", "result");
2090
+ }
2091
+ if (resultMsg.usage) {
2092
+ messageBuffer.addMessage("\u{1F4CA} Session Stats:", "status");
2093
+ messageBuffer.addMessage(` \u2022 Turns: ${resultMsg.num_turns}`, "status");
2094
+ messageBuffer.addMessage(` \u2022 Input tokens: ${resultMsg.usage.input_tokens}`, "status");
2095
+ messageBuffer.addMessage(` \u2022 Output tokens: ${resultMsg.usage.output_tokens}`, "status");
2096
+ if (resultMsg.usage.cache_read_input_tokens) {
2097
+ messageBuffer.addMessage(` \u2022 Cache read tokens: ${resultMsg.usage.cache_read_input_tokens}`, "status");
2098
+ }
2099
+ if (resultMsg.usage.cache_creation_input_tokens) {
2100
+ messageBuffer.addMessage(` \u2022 Cache creation tokens: ${resultMsg.usage.cache_creation_input_tokens}`, "status");
2101
+ }
2102
+ messageBuffer.addMessage(` \u2022 Cost: $${resultMsg.total_cost_usd.toFixed(4)}`, "status");
2103
+ messageBuffer.addMessage(` \u2022 Duration: ${resultMsg.duration_ms}ms`, "status");
2104
+ }
2105
+ } else if (resultMsg.subtype === "error_max_turns") {
2106
+ messageBuffer.addMessage("\u274C Error: Maximum turns reached", "result");
2107
+ messageBuffer.addMessage(`Completed ${resultMsg.num_turns} turns`, "status");
2108
+ } else if (resultMsg.subtype === "error_during_execution") {
2109
+ messageBuffer.addMessage("\u274C Error during execution", "result");
2110
+ messageBuffer.addMessage(`Completed ${resultMsg.num_turns} turns before error`, "status");
2111
+ logger.debugLargeJson("[RESULT] Error during execution", resultMsg);
2112
+ }
2113
+ break;
2114
+ }
2115
+ default: {
2116
+ if (isDebug()) {
2117
+ messageBuffer.addMessage(`[Unknown message type: ${message.type}]`, "status");
2118
+ }
2119
+ }
2120
+ }
2121
+ }
2122
+
2123
+ function getGitBranch(cwd) {
2124
+ try {
2125
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
2126
+ cwd,
2127
+ encoding: "utf8",
2128
+ stdio: ["ignore", "pipe", "ignore"]
2129
+ }).trim();
2130
+ return branch || void 0;
2131
+ } catch {
2132
+ return void 0;
2133
+ }
2134
+ }
2135
+ class SDKToLogConverter {
2136
+ lastUuid = null;
2137
+ context;
2138
+ responses;
2139
+ sidechainLastUUID = /* @__PURE__ */ new Map();
2140
+ constructor(context, responses) {
2141
+ this.context = {
2142
+ ...context,
2143
+ gitBranch: context.gitBranch ?? getGitBranch(context.cwd),
2144
+ version: context.version ?? process.env.npm_package_version ?? "0.0.0",
2145
+ parentUuid: null
2146
+ };
2147
+ this.responses = responses;
2148
+ }
2149
+ /**
2150
+ * Update session ID (for when session changes during resume)
2151
+ */
2152
+ updateSessionId(sessionId) {
2153
+ this.context.sessionId = sessionId;
2154
+ }
2155
+ /**
2156
+ * Reset parent chain (useful when starting new conversation)
2157
+ */
2158
+ resetParentChain() {
2159
+ this.lastUuid = null;
2160
+ this.context.parentUuid = null;
2161
+ }
2162
+ /**
2163
+ * Convert SDK message to log format
2164
+ */
2165
+ convert(sdkMessage) {
2166
+ const uuid = randomUUID();
2167
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2168
+ let parentUuid = this.lastUuid;
2169
+ let isSidechain = false;
2170
+ if (sdkMessage.parent_tool_use_id) {
2171
+ isSidechain = true;
2172
+ parentUuid = this.sidechainLastUUID.get(sdkMessage.parent_tool_use_id) ?? null;
2173
+ this.sidechainLastUUID.set(sdkMessage.parent_tool_use_id, uuid);
2174
+ }
2175
+ const baseFields = {
2176
+ parentUuid,
2177
+ isSidechain,
2178
+ userType: "external",
2179
+ cwd: this.context.cwd,
2180
+ sessionId: this.context.sessionId,
2181
+ version: this.context.version,
2182
+ gitBranch: this.context.gitBranch,
2183
+ uuid,
2184
+ timestamp
2185
+ };
2186
+ let logMessage = null;
2187
+ switch (sdkMessage.type) {
2188
+ case "user": {
2189
+ const userMsg = sdkMessage;
2190
+ logMessage = {
2191
+ ...baseFields,
2192
+ type: "user",
2193
+ message: userMsg.message
2194
+ };
2195
+ if (Array.isArray(userMsg.message.content)) {
2196
+ const toolUseResult = sdkMessage.toolUseResult ?? sdkMessage.tool_use_result;
2197
+ if (toolUseResult !== void 0) {
2198
+ logMessage.toolUseResult = toolUseResult;
2199
+ }
2200
+ for (const content of userMsg.message.content) {
2201
+ if (content.type === "tool_result" && content.tool_use_id && this.responses?.has(content.tool_use_id)) {
2202
+ const response = this.responses.get(content.tool_use_id);
2203
+ if (response?.mode) {
2204
+ logMessage.mode = response.mode;
2205
+ }
2206
+ }
2207
+ }
2208
+ } else if (typeof userMsg.message.content === "string") ;
2209
+ break;
2210
+ }
2211
+ case "assistant": {
2212
+ const assistantMsg = sdkMessage;
2213
+ logMessage = {
2214
+ ...baseFields,
2215
+ type: "assistant",
2216
+ message: assistantMsg.message,
2217
+ // Assistant messages often have additional fields
2218
+ requestId: assistantMsg.requestId
2219
+ };
2220
+ break;
2221
+ }
2222
+ case "system": {
2223
+ const systemMsg = sdkMessage;
2224
+ if (systemMsg.subtype === "init" && systemMsg.session_id) {
2225
+ this.updateSessionId(systemMsg.session_id);
2226
+ }
2227
+ logMessage = {
2228
+ ...baseFields,
2229
+ type: "system",
2230
+ subtype: systemMsg.subtype,
2231
+ model: systemMsg.model,
2232
+ tools: systemMsg.tools,
2233
+ // Include all other fields
2234
+ ...systemMsg
2235
+ };
2236
+ break;
2237
+ }
2238
+ case "result": {
2239
+ break;
2240
+ }
2241
+ // Handle tool use results (often comes as user messages)
2242
+ case "tool_result": {
2243
+ const toolMsg = sdkMessage;
2244
+ const baseLogMessage = {
2245
+ ...baseFields,
2246
+ type: "user",
2247
+ message: {
2248
+ role: "user",
2249
+ content: [{
2250
+ type: "tool_result",
2251
+ tool_use_id: toolMsg.tool_use_id,
2252
+ content: toolMsg.content
2253
+ }]
2254
+ },
2255
+ toolUseResult: toolMsg.content
2256
+ };
2257
+ if (toolMsg.tool_use_id && this.responses?.has(toolMsg.tool_use_id)) {
2258
+ const response = this.responses.get(toolMsg.tool_use_id);
2259
+ if (response?.mode) {
2260
+ baseLogMessage.mode = response.mode;
2261
+ }
2262
+ }
2263
+ logMessage = baseLogMessage;
2264
+ break;
2265
+ }
2266
+ default:
2267
+ logMessage = {
2268
+ ...baseFields,
2269
+ ...sdkMessage,
2270
+ type: sdkMessage.type
2271
+ // Override type last to ensure it's set
2272
+ };
2273
+ }
2274
+ if (logMessage && logMessage.type !== "summary") {
2275
+ this.lastUuid = uuid;
2276
+ }
2277
+ return logMessage;
2278
+ }
2279
+ /**
2280
+ * Convert multiple SDK messages to log format
2281
+ */
2282
+ convertMany(sdkMessages) {
2283
+ return sdkMessages.map((msg) => this.convert(msg)).filter((msg) => msg !== null);
2284
+ }
2285
+ /**
2286
+ * Convert a simple string content to a sidechain user message
2287
+ * Used for Task tool sub-agent prompts
2288
+ */
2289
+ convertSidechainUserMessage(toolUseId, content) {
2290
+ const uuid = randomUUID();
2291
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2292
+ this.sidechainLastUUID.set(toolUseId, uuid);
2293
+ return {
2294
+ parentUuid: null,
2295
+ isSidechain: true,
2296
+ userType: "external",
2297
+ cwd: this.context.cwd,
2298
+ sessionId: this.context.sessionId,
2299
+ version: this.context.version,
2300
+ gitBranch: this.context.gitBranch,
2301
+ type: "user",
2302
+ message: {
2303
+ role: "user",
2304
+ content
2305
+ },
2306
+ uuid,
2307
+ timestamp
2308
+ };
2309
+ }
2310
+ /**
2311
+ * Generate an interrupted tool result message
2312
+ * Used when a tool call is interrupted by the user
2313
+ * @param toolUseId - The ID of the tool that was interrupted
2314
+ * @param parentToolUseId - Optional parent tool ID if this is a sidechain tool
2315
+ */
2316
+ generateInterruptedToolResult(toolUseId, parentToolUseId) {
2317
+ const uuid = randomUUID();
2318
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2319
+ const errorMessage = "[Request interrupted by user for tool use]";
2320
+ let isSidechain = false;
2321
+ let parentUuid = this.lastUuid;
2322
+ if (parentToolUseId) {
2323
+ isSidechain = true;
2324
+ parentUuid = this.sidechainLastUUID.get(parentToolUseId) ?? null;
2325
+ this.sidechainLastUUID.set(parentToolUseId, uuid);
2326
+ }
2327
+ const logMessage = {
2328
+ type: "user",
2329
+ isSidechain,
2330
+ uuid,
2331
+ message: {
2332
+ role: "user",
2333
+ content: [
2334
+ {
2335
+ type: "tool_result",
2336
+ content: errorMessage,
2337
+ is_error: true,
2338
+ tool_use_id: toolUseId
2339
+ }
2340
+ ]
2341
+ },
2342
+ parentUuid,
2343
+ userType: "external",
2344
+ cwd: this.context.cwd,
2345
+ sessionId: this.context.sessionId,
2346
+ version: this.context.version,
2347
+ gitBranch: this.context.gitBranch,
2348
+ timestamp,
2349
+ toolUseResult: `Error: ${errorMessage}`
2350
+ };
2351
+ this.lastUuid = uuid;
2352
+ return logMessage;
2353
+ }
2354
+ }
2355
+
2356
+ class OutgoingMessageQueue {
2357
+ constructor(sendFunction) {
2358
+ this.sendFunction = sendFunction;
2359
+ }
2360
+ queue = [];
2361
+ nextId = 1;
2362
+ lock = new AsyncLock();
2363
+ processTimer;
2364
+ delayTimers = /* @__PURE__ */ new Map();
2365
+ /**
2366
+ * Add message to queue
2367
+ */
2368
+ enqueue(logMessage, options) {
2369
+ this.lock.inLock(async () => {
2370
+ const item = {
2371
+ id: this.nextId++,
2372
+ logMessage,
2373
+ delayed: !!options?.delay,
2374
+ delayMs: options?.delay || 0,
2375
+ toolCallIds: options?.toolCallIds,
2376
+ released: !options?.delay,
2377
+ // Not delayed = already released
2378
+ sent: false
2379
+ };
2380
+ this.queue.push(item);
2381
+ if (item.delayed) {
2382
+ const timer = setTimeout(() => {
2383
+ this.releaseItem(item.id);
2384
+ }, item.delayMs);
2385
+ this.delayTimers.set(item.id, timer);
2386
+ }
2387
+ });
2388
+ this.scheduleProcessing();
2389
+ }
2390
+ /**
2391
+ * Release specific item by ID
2392
+ */
2393
+ async releaseItem(itemId) {
2394
+ await this.lock.inLock(async () => {
2395
+ const item = this.queue.find((i) => i.id === itemId);
2396
+ if (item && !item.released) {
2397
+ item.released = true;
2398
+ const timer = this.delayTimers.get(itemId);
2399
+ if (timer) {
2400
+ clearTimeout(timer);
2401
+ this.delayTimers.delete(itemId);
2402
+ }
2403
+ }
2404
+ });
2405
+ this.scheduleProcessing();
2406
+ }
2407
+ /**
2408
+ * Release all messages with specific tool call ID
2409
+ */
2410
+ async releaseToolCall(toolCallId) {
2411
+ await this.lock.inLock(async () => {
2412
+ for (const item of this.queue) {
2413
+ if (item.toolCallIds?.includes(toolCallId) && !item.released) {
2414
+ item.released = true;
2415
+ const timer = this.delayTimers.get(item.id);
2416
+ if (timer) {
2417
+ clearTimeout(timer);
2418
+ this.delayTimers.delete(item.id);
2419
+ }
2420
+ }
2421
+ }
2422
+ });
2423
+ this.scheduleProcessing();
2424
+ }
2425
+ /**
2426
+ * Process queue - send messages in ID order that are released
2427
+ * (Internal implementation without lock)
2428
+ */
2429
+ processQueueInternal() {
2430
+ this.queue.sort((a, b) => a.id - b.id);
2431
+ while (this.queue.length > 0) {
2432
+ const item = this.queue[0];
2433
+ if (!item.released) {
2434
+ break;
2435
+ }
2436
+ if (!item.sent) {
2437
+ if (item.logMessage.type !== "system") {
2438
+ this.sendFunction(item.logMessage);
2439
+ }
2440
+ item.sent = true;
2441
+ }
2442
+ this.queue.shift();
2443
+ }
2444
+ }
2445
+ /**
2446
+ * Process queue - send messages in ID order that are released
2447
+ */
2448
+ async processQueue() {
2449
+ await this.lock.inLock(async () => {
2450
+ this.processQueueInternal();
2451
+ });
2452
+ }
2453
+ /**
2454
+ * Flush all messages immediately (for cleanup)
2455
+ */
2456
+ async flush() {
2457
+ await this.lock.inLock(async () => {
2458
+ for (const timer of this.delayTimers.values()) {
2459
+ clearTimeout(timer);
2460
+ }
2461
+ this.delayTimers.clear();
2462
+ for (const item of this.queue) {
2463
+ item.released = true;
2464
+ }
2465
+ this.processQueueInternal();
2466
+ });
2467
+ }
2468
+ /**
2469
+ * Schedule processing on next tick
2470
+ */
2471
+ scheduleProcessing() {
2472
+ if (this.processTimer) {
2473
+ clearTimeout(this.processTimer);
2474
+ }
2475
+ this.processTimer = setTimeout(() => {
2476
+ this.processQueue();
2477
+ }, 0);
2478
+ }
2479
+ /**
2480
+ * Cleanup timers and resources
2481
+ */
2482
+ destroy() {
2483
+ if (this.processTimer) {
2484
+ clearTimeout(this.processTimer);
2485
+ }
2486
+ for (const timer of this.delayTimers.values()) {
2487
+ clearTimeout(timer);
2488
+ }
2489
+ this.delayTimers.clear();
2490
+ }
2491
+ }
2492
+
2493
+ async function claudeRemoteLauncher(session) {
2494
+ logger.debug("[claudeRemoteLauncher] Starting remote launcher");
2495
+ const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
2496
+ logger.debug(`[claudeRemoteLauncher] TTY available: ${hasTTY}`);
2497
+ let messageBuffer = new MessageBuffer();
2498
+ let inkInstance = null;
2499
+ if (hasTTY) {
2500
+ console.clear();
2501
+ inkInstance = render(React.createElement(RemoteModeDisplay, {
2502
+ messageBuffer,
2503
+ logPath: isDebug() ? session.logPath : void 0,
2504
+ onExit: async () => {
2505
+ logger.debug("[remote]: Exiting client via Ctrl-C");
2506
+ if (!exitReason) {
2507
+ exitReason = "exit";
2508
+ }
2509
+ await abort();
2510
+ },
2511
+ onSwitchToLocal: () => {
2512
+ logger.debug("[remote]: Switching to local mode via double space");
2513
+ doSwitch();
2514
+ }
2515
+ }), {
2516
+ exitOnCtrlC: false,
2517
+ patchConsole: false
2518
+ });
2519
+ }
2520
+ if (hasTTY) {
2521
+ process.stdin.resume();
2522
+ if (process.stdin.isTTY) {
2523
+ process.stdin.setRawMode(true);
2524
+ }
2525
+ process.stdin.setEncoding("utf8");
2526
+ }
2527
+ let exitReason = null;
2528
+ let abortController = null;
2529
+ let abortFuture = null;
2530
+ let currentQuery = null;
2531
+ async function abort() {
2532
+ const query = currentQuery;
2533
+ const controller = abortController;
2534
+ const future = abortFuture;
2535
+ if (controller?.signal.aborted) {
2536
+ await future?.promise;
2537
+ return;
2538
+ }
2539
+ if (query) {
2540
+ try {
2541
+ await Promise.race([
2542
+ query.interrupt(),
2543
+ new Promise(
2544
+ (_, reject) => setTimeout(() => reject(new Error("interrupt timeout")), 3e3)
2545
+ )
2546
+ ]);
2547
+ logger.debug("[remote]: interrupt() succeeded");
2548
+ } catch (e) {
2549
+ logger.debug("[remote]: interrupt() failed or timed out, falling back to SIGTERM", e);
2550
+ }
2551
+ }
2552
+ if (controller && !controller.signal.aborted) {
2553
+ controller.abort();
2554
+ }
2555
+ await future?.promise;
2556
+ }
2557
+ async function doAbort() {
2558
+ logger.debug("[remote]: doAbort");
2559
+ await abort();
2560
+ }
2561
+ async function doSwitch() {
2562
+ logger.debug("[remote]: doSwitch");
2563
+ if (!exitReason) {
2564
+ exitReason = "switch";
2565
+ }
2566
+ await abort();
2567
+ }
2568
+ session.client.rpcHandlerManager.registerHandler("abort", doAbort);
2569
+ session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
2570
+ const permissionHandler = new PermissionHandler(session);
2571
+ const validPermissionModes = ["default", "acceptEdits", "bypassPermissions", "plan"];
2572
+ session.client.rpcHandlerManager.registerHandler(
2573
+ "permission-mode-changed",
2574
+ async (payload) => {
2575
+ const mode = payload?.mode;
2576
+ if (!mode || !validPermissionModes.includes(mode)) {
2577
+ logger.debug("[remote]: invalid permission mode via rpc", { mode });
2578
+ return false;
2579
+ }
2580
+ permissionHandler.handleModeChange(mode);
2581
+ logger.debug(`[remote]: permission mode updated via rpc to ${mode}`);
2582
+ return true;
2583
+ }
2584
+ );
2585
+ const messageQueue = new OutgoingMessageQueue(
2586
+ (logMessage) => session.client.sendClaudeSessionMessage(logMessage)
2587
+ );
2588
+ permissionHandler.setOnPermissionRequest((toolCallId) => {
2589
+ messageQueue.releaseToolCall(toolCallId);
2590
+ });
2591
+ const sdkToLogConverter = new SDKToLogConverter({
2592
+ sessionId: session.sessionId || "unknown",
2593
+ cwd: session.path,
2594
+ version: process.env.npm_package_version
2595
+ }, permissionHandler.getResponses());
2596
+ let currentSyncedModel;
2597
+ let currentSyncedEffort;
2598
+ function syncModelMetadata(mode) {
2599
+ const model = mode.model;
2600
+ const effort = mode.reasoningEffort;
2601
+ if (model === currentSyncedModel && effort === currentSyncedEffort) return;
2602
+ currentSyncedModel = model;
2603
+ currentSyncedEffort = effort;
2604
+ session.client.updateMetadata((m) => ({
2605
+ ...m,
2606
+ ...model ? { model } : {},
2607
+ ...effort ? { reasoningEffort: effort } : {}
2608
+ }));
2609
+ }
2610
+ let planModeToolCalls = /* @__PURE__ */ new Set();
2611
+ let ongoingToolCalls = /* @__PURE__ */ new Map();
2612
+ let hadAssistantMessage = false;
2613
+ function onMessage(message) {
2614
+ formatClaudeMessageForInk(message, messageBuffer);
2615
+ if (message.type === "system" && message.subtype === "init") {
2616
+ const initModel = message.model;
2617
+ if (initModel && initModel !== currentSyncedModel) {
2618
+ currentSyncedModel = initModel;
2619
+ session.client.updateMetadata((m) => ({ ...m, model: initModel }));
2620
+ }
2621
+ }
2622
+ if (message.type === "result") {
2623
+ const resultMsg = message;
2624
+ if (resultMsg.subtype === "error_during_execution") {
2625
+ const errors = resultMsg.errors;
2626
+ if (abortController?.signal.aborted) {
2627
+ logger.debug("[remote]: suppressing error_during_execution caused by user abort", { errors });
2628
+ } else if (errors && errors.length > 0) {
2629
+ const errorText = errors.join("\n");
2630
+ session.client.sendSessionEvent({ type: "message", message: `Error: ${errorText}` });
2631
+ logger.debug("[remote]: sent error_during_execution as session event", { errorCount: errors.length });
2632
+ } else {
2633
+ session.client.sendSessionEvent({ type: "message", message: "An error occurred during execution" });
2634
+ }
2635
+ } else if (!hadAssistantMessage && typeof resultMsg.result === "string" && resultMsg.result.length > 0) {
2636
+ session.client.sendSessionEvent({ type: "message", message: resultMsg.result });
2637
+ logger.debug("[remote]: forwarded result text as session event (no assistant output this turn)");
2638
+ }
2639
+ return;
2640
+ }
2641
+ permissionHandler.onMessage(message);
2642
+ if (message.type === "assistant") {
2643
+ hadAssistantMessage = true;
2644
+ }
2645
+ if (message.type === "assistant") {
2646
+ let umessage = message;
2647
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2648
+ for (let c of umessage.message.content) {
2649
+ if (c.type === "tool_use" && (c.name === "exit_plan_mode" || c.name === "ExitPlanMode")) {
2650
+ logger.debug("[remote]: detected plan mode tool call " + c.id);
2651
+ planModeToolCalls.add(c.id);
2652
+ }
2653
+ }
2654
+ }
2655
+ }
2656
+ if (message.type === "assistant") {
2657
+ let umessage = message;
2658
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2659
+ for (let c of umessage.message.content) {
2660
+ if (c.type === "tool_use") {
2661
+ logger.debug("[remote]: detected tool use " + c.id + " parent: " + umessage.parent_tool_use_id);
2662
+ ongoingToolCalls.set(c.id, { parentToolCallId: umessage.parent_tool_use_id ?? null });
2663
+ }
2664
+ }
2665
+ }
2666
+ }
2667
+ if (message.type === "user") {
2668
+ let umessage = message;
2669
+ const content = umessage.message.content;
2670
+ if (typeof content === "string") {
2671
+ hadAssistantMessage = false;
2672
+ } else if (Array.isArray(content)) {
2673
+ let hasToolResult = false;
2674
+ let hasNonToolResult = false;
2675
+ for (let c of content) {
2676
+ if (c.type === "tool_result" && c.tool_use_id) {
2677
+ hasToolResult = true;
2678
+ ongoingToolCalls.delete(c.tool_use_id);
2679
+ messageQueue.releaseToolCall(c.tool_use_id);
2680
+ } else {
2681
+ hasNonToolResult = true;
2682
+ }
2683
+ }
2684
+ if (hasNonToolResult && !hasToolResult) {
2685
+ hadAssistantMessage = false;
2686
+ }
2687
+ }
2688
+ }
2689
+ let msg = message;
2690
+ if (message.type === "user") {
2691
+ let umessage = message;
2692
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2693
+ msg = {
2694
+ ...umessage,
2695
+ message: {
2696
+ ...umessage.message,
2697
+ content: umessage.message.content.map((c) => {
2698
+ if (c.type === "tool_result" && c.tool_use_id && planModeToolCalls.has(c.tool_use_id)) {
2699
+ if (c.content === PLAN_FAKE_REJECT) {
2700
+ logger.debug("[remote]: hack plan mode exit");
2701
+ logger.debugLargeJson("[remote]: hack plan mode exit", c);
2702
+ return {
2703
+ ...c,
2704
+ is_error: false,
2705
+ content: "Plan approved",
2706
+ mode: c.mode
2707
+ };
2708
+ } else {
2709
+ return c;
2710
+ }
2711
+ }
2712
+ return c;
2713
+ })
2714
+ }
2715
+ };
2716
+ }
2717
+ }
2718
+ const logMessage = sdkToLogConverter.convert(msg);
2719
+ if (logMessage) {
2720
+ if (logMessage.type === "user" && logMessage.message?.content) {
2721
+ const content = Array.isArray(logMessage.message.content) ? logMessage.message.content : [];
2722
+ for (let i = 0; i < content.length; i++) {
2723
+ const c = content[i];
2724
+ if (c.type === "tool_result" && c.tool_use_id) {
2725
+ const responses = permissionHandler.getResponses();
2726
+ const response = responses.get(c.tool_use_id);
2727
+ if (response) {
2728
+ const permissions = {
2729
+ date: response.receivedAt || Date.now(),
2730
+ result: response.approved ? "approved" : "denied"
2731
+ };
2732
+ if (response.mode) {
2733
+ permissions.mode = response.mode;
2734
+ }
2735
+ if (response.allowTools && response.allowTools.length > 0) {
2736
+ permissions.allowedTools = response.allowTools;
2737
+ }
2738
+ content[i] = {
2739
+ ...c,
2740
+ permissions
2741
+ };
2742
+ }
2743
+ }
2744
+ }
2745
+ }
2746
+ if (logMessage.type === "assistant" && message.type === "assistant") {
2747
+ const assistantMsg = message;
2748
+ const toolCallIds = [];
2749
+ if (assistantMsg.message.content && Array.isArray(assistantMsg.message.content)) {
2750
+ for (const block of assistantMsg.message.content) {
2751
+ if (block.type === "tool_use" && block.id) {
2752
+ toolCallIds.push(block.id);
2753
+ }
2754
+ }
2755
+ }
2756
+ if (toolCallIds.length > 0) {
2757
+ const isSidechain = assistantMsg.parent_tool_use_id !== void 0;
2758
+ if (!isSidechain) {
2759
+ messageQueue.enqueue(logMessage, {
2760
+ delay: 250,
2761
+ toolCallIds
2762
+ });
2763
+ return;
2764
+ }
2765
+ }
2766
+ }
2767
+ messageQueue.enqueue(logMessage);
2768
+ }
2769
+ if (message.type === "assistant") {
2770
+ let umessage = message;
2771
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2772
+ for (let c of umessage.message.content) {
2773
+ if (c.type === "tool_use" && c.name === "Task" && c.input && typeof c.input.prompt === "string") {
2774
+ const logMessage2 = sdkToLogConverter.convertSidechainUserMessage(c.id, c.input.prompt);
2775
+ if (logMessage2) {
2776
+ messageQueue.enqueue(logMessage2);
2777
+ }
2778
+ }
2779
+ }
2780
+ }
2781
+ }
2782
+ }
2783
+ try {
2784
+ let pending = null;
2785
+ let previousSessionId = null;
2786
+ while (!exitReason) {
2787
+ logger.debug("[remote]: launch");
2788
+ messageBuffer.addMessage("\u2550".repeat(40), "status");
2789
+ const isNewSession = session.sessionId !== previousSessionId;
2790
+ if (isNewSession) {
2791
+ messageBuffer.addMessage("Starting new Claude session...", "status");
2792
+ permissionHandler.reset();
2793
+ sdkToLogConverter.resetParentChain();
2794
+ logger.debug(`[remote]: New session detected (previous: ${previousSessionId}, current: ${session.sessionId})`);
2795
+ } else {
2796
+ messageBuffer.addMessage("Continuing Claude session...", "status");
2797
+ logger.debug(`[remote]: Continuing existing session: ${session.sessionId}`);
2798
+ }
2799
+ previousSessionId = session.sessionId;
2800
+ const controller = new AbortController();
2801
+ abortController = controller;
2802
+ abortFuture = new Future();
2803
+ let modeHash = null;
2804
+ let mode = null;
2805
+ try {
2806
+ const remoteResult = await claudeRemote({
2807
+ sessionId: session.sessionId,
2808
+ path: session.path,
2809
+ allowedTools: session.allowedTools ?? [],
2810
+ mcpServers: session.mcpServers,
2811
+ hookSettingsPath: session.hookSettingsPath,
2812
+ jsRuntime: session.jsRuntime,
2813
+ canCallTool: permissionHandler.handleToolCall,
2814
+ isAborted: (toolCallId) => {
2815
+ return permissionHandler.isAborted(toolCallId);
2816
+ },
2817
+ nextMessage: async () => {
2818
+ if (pending) {
2819
+ let p = pending;
2820
+ pending = null;
2821
+ permissionHandler.handleModeChange(p.mode.permissionMode);
2822
+ syncModelMetadata(p.mode);
2823
+ return p;
2824
+ }
2825
+ let msg = await session.queue.waitForMessagesAndGetAsString(controller.signal);
2826
+ if (msg) {
2827
+ if (modeHash && msg.hash !== modeHash || msg.isolate) {
2828
+ logger.debug("[remote]: mode has changed, pending message");
2829
+ pending = msg;
2830
+ return null;
2831
+ }
2832
+ modeHash = msg.hash;
2833
+ mode = msg.mode;
2834
+ permissionHandler.handleModeChange(mode.permissionMode);
2835
+ syncModelMetadata(mode);
2836
+ return {
2837
+ message: msg.message,
2838
+ mode: msg.mode
2839
+ };
2840
+ }
2841
+ return null;
2842
+ },
2843
+ onSessionFound: (sessionId) => {
2844
+ sdkToLogConverter.updateSessionId(sessionId);
2845
+ session.onSessionFound(sessionId);
2846
+ },
2847
+ onThinkingChange: session.onThinkingChange,
2848
+ claudeEnvVars: session.claudeEnvVars,
2849
+ claudeArgs: session.claudeArgs,
2850
+ onMessage,
2851
+ onCompletionEvent: (message) => {
2852
+ logger.debug(`[remote]: Completion event: ${message}`);
2853
+ session.client.sendSessionEvent({ type: "message", message });
2854
+ },
2855
+ onSessionReset: () => {
2856
+ logger.debug("[remote]: Session reset");
2857
+ session.clearSessionId();
2858
+ },
2859
+ onQueryCreated: (q) => {
2860
+ currentQuery = q;
2861
+ },
2862
+ onReady: () => {
2863
+ if (!pending && session.queue.size() === 0) {
2864
+ session.client.sendSessionEvent({ type: "ready" });
2865
+ session.api.push().sendToAllDevices(
2866
+ "It's ready!",
2867
+ `Claude is waiting for your command`,
2868
+ { sessionId: session.client.sessionId }
2869
+ );
2870
+ session.client.flush().finally(() => {
2871
+ session.client.updateAgentState((state) => ({
2872
+ ...state,
2873
+ taskCompleted: Date.now()
2874
+ }));
2875
+ });
2876
+ }
2877
+ },
2878
+ signal: abortController.signal
2879
+ });
2880
+ session.consumeOneTimeFlags();
2881
+ if (!exitReason && abortController.signal.aborted) {
2882
+ session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
2883
+ }
2884
+ } catch (e) {
2885
+ const errorMessage = e instanceof Error ? e.message : String(e);
2886
+ logger.debug("[remote]: launch error", e);
2887
+ if (!exitReason) {
2888
+ session.client.sendSessionEvent({ type: "message", message: `Process exited unexpectedly: ${errorMessage}` });
2889
+ continue;
2890
+ }
2891
+ } finally {
2892
+ logger.debug("[remote]: launch finally");
2893
+ currentQuery = null;
2894
+ for (let [toolCallId, { parentToolCallId }] of ongoingToolCalls) {
2895
+ const converted = sdkToLogConverter.generateInterruptedToolResult(toolCallId, parentToolCallId);
2896
+ if (converted) {
2897
+ logger.debug("[remote]: terminating tool call " + toolCallId + " parent: " + parentToolCallId);
2898
+ session.client.sendClaudeSessionMessage(converted);
2899
+ }
2900
+ }
2901
+ ongoingToolCalls.clear();
2902
+ logger.debug("[remote]: flushing message queue");
2903
+ await messageQueue.flush();
2904
+ messageQueue.destroy();
2905
+ logger.debug("[remote]: message queue flushed");
2906
+ abortController = null;
2907
+ abortFuture?.resolve(void 0);
2908
+ abortFuture = null;
2909
+ logger.debug("[remote]: launch done");
2910
+ permissionHandler.reset();
2911
+ modeHash = null;
2912
+ mode = null;
2913
+ }
2914
+ }
2915
+ } finally {
2916
+ permissionHandler.reset();
2917
+ process.stdin.off("data", abort);
2918
+ if (process.stdin.isTTY) {
2919
+ process.stdin.setRawMode(false);
2920
+ }
2921
+ if (inkInstance) {
2922
+ inkInstance.unmount();
2923
+ }
2924
+ messageBuffer.clear();
2925
+ session.queue.setOnMessage(null);
2926
+ if (abortFuture) {
2927
+ abortFuture.resolve(void 0);
2928
+ }
2929
+ }
2930
+ return exitReason || "exit";
2931
+ }
2932
+
2933
+ async function loop(opts) {
2934
+ const logPath = logger.logFilePath;
2935
+ let session = new Session({
2936
+ api: opts.api,
2937
+ client: opts.session,
2938
+ path: opts.path,
2939
+ sessionId: null,
2940
+ claudeEnvVars: opts.claudeEnvVars,
2941
+ claudeArgs: opts.claudeArgs,
2942
+ mcpServers: opts.mcpServers,
2943
+ logPath,
2944
+ messageQueue: opts.messageQueue,
2945
+ allowedTools: opts.allowedTools,
2946
+ onModeChange: opts.onModeChange,
2947
+ hookSettingsPath: opts.hookSettingsPath,
2948
+ jsRuntime: opts.jsRuntime
2949
+ });
2950
+ opts.onSessionReady?.(session);
2951
+ let mode = opts.startingMode ?? "local";
2952
+ while (true) {
2953
+ logger.debug(`[loop] Iteration with mode: ${mode}`);
2954
+ switch (mode) {
2955
+ case "local": {
2956
+ const result = await claudeLocalLauncher(session);
2957
+ switch (result.type) {
2958
+ case "switch":
2959
+ mode = "remote";
2960
+ opts.onModeChange?.(mode);
2961
+ break;
2962
+ case "exit":
2963
+ return result.code;
2964
+ }
2965
+ break;
2966
+ }
2967
+ case "remote": {
2968
+ const reason = await claudeRemoteLauncher(session);
2969
+ switch (reason) {
2970
+ case "exit":
2971
+ return 0;
2972
+ case "switch":
2973
+ mode = "local";
2974
+ opts.onModeChange?.(mode);
2975
+ break;
2976
+ }
2977
+ break;
2978
+ }
2979
+ }
2980
+ }
2981
+ }
2982
+
2983
+ async function extractSDKMetadata() {
2984
+ const abortController = new AbortController();
2985
+ try {
2986
+ logger.debug("[metadataExtractor] Starting SDK metadata extraction");
2987
+ const sdkQuery = query({
2988
+ prompt: "hello",
2989
+ options: {
2990
+ allowedTools: ["Bash(echo)"],
2991
+ maxTurns: 1,
2992
+ abort: abortController.signal
2993
+ }
2994
+ });
2995
+ for await (const message of sdkQuery) {
2996
+ if (message.type === "system" && message.subtype === "init") {
2997
+ const systemMessage = message;
2998
+ const metadata = {
2999
+ tools: systemMessage.tools,
3000
+ slashCommands: systemMessage.slash_commands
3001
+ };
3002
+ logger.debug("[metadataExtractor] Captured SDK metadata:", metadata);
3003
+ abortController.abort();
3004
+ return metadata;
3005
+ }
3006
+ }
3007
+ logger.debug("[metadataExtractor] No init message received from SDK");
3008
+ return {};
3009
+ } catch (error) {
3010
+ if (error instanceof Error && error.name === "AbortError") {
3011
+ logger.debug("[metadataExtractor] SDK query aborted after capturing metadata");
3012
+ return {};
3013
+ }
3014
+ logger.debug("[metadataExtractor] Error extracting SDK metadata:", error);
3015
+ return {};
3016
+ }
3017
+ }
3018
+ function extractSDKMetadataAsync(onComplete) {
3019
+ extractSDKMetadata().then((metadata) => {
3020
+ if (metadata.tools || metadata.slashCommands) {
3021
+ onComplete(metadata);
3022
+ }
3023
+ }).catch((error) => {
3024
+ logger.debug("[metadataExtractor] Async extraction failed:", error);
3025
+ });
3026
+ }
3027
+
3028
+ async function startHookServer(options) {
3029
+ const { onSessionHook } = options;
3030
+ return new Promise((resolve, reject) => {
3031
+ const server = createServer(async (req, res) => {
3032
+ if (req.method === "POST" && req.url === "/hook/session-start") {
3033
+ const timeout = setTimeout(() => {
3034
+ if (!res.headersSent) {
3035
+ logger.debug("[hookServer] Request timeout");
3036
+ res.writeHead(408).end("timeout");
3037
+ }
3038
+ }, 5e3);
3039
+ try {
3040
+ const chunks = [];
3041
+ for await (const chunk of req) {
3042
+ chunks.push(chunk);
3043
+ }
3044
+ clearTimeout(timeout);
3045
+ const body = Buffer.concat(chunks).toString("utf-8");
3046
+ logger.debug("[hookServer] Received session hook:", body);
3047
+ let data = {};
3048
+ try {
3049
+ data = JSON.parse(body);
3050
+ } catch (parseError) {
3051
+ logger.debug("[hookServer] Failed to parse hook data as JSON:", parseError);
3052
+ }
3053
+ const sessionId = data.session_id || data.sessionId;
3054
+ if (sessionId) {
3055
+ logger.debug(`[hookServer] Session hook received session ID: ${sessionId}`);
3056
+ onSessionHook(sessionId, data);
3057
+ } else {
3058
+ logger.debug("[hookServer] Session hook received but no session_id found in data");
3059
+ }
3060
+ res.writeHead(200, { "Content-Type": "text/plain" }).end("ok");
3061
+ } catch (error) {
3062
+ clearTimeout(timeout);
3063
+ logger.debug("[hookServer] Error handling session hook:", error);
3064
+ if (!res.headersSent) {
3065
+ res.writeHead(500).end("error");
3066
+ }
3067
+ }
3068
+ return;
3069
+ }
3070
+ res.writeHead(404).end("not found");
3071
+ });
3072
+ server.listen(0, "127.0.0.1", () => {
3073
+ const address = server.address();
3074
+ if (!address || typeof address === "string") {
3075
+ reject(new Error("Failed to get server address"));
3076
+ return;
3077
+ }
3078
+ const port = address.port;
3079
+ logger.debug(`[hookServer] Started on port ${port}`);
3080
+ resolve({
3081
+ port,
3082
+ stop: () => {
3083
+ server.close();
3084
+ logger.debug("[hookServer] Stopped");
3085
+ }
3086
+ });
3087
+ });
3088
+ server.on("error", (err) => {
3089
+ logger.debug("[hookServer] Server error:", err);
3090
+ reject(err);
3091
+ });
3092
+ });
3093
+ }
3094
+
3095
+ const INTERNAL_CLAUDE_EVENT_TYPES = /* @__PURE__ */ new Set([
3096
+ "file-history-snapshot",
3097
+ "change",
3098
+ "queue-operation"
3099
+ ]);
3100
+ function messageKey(message) {
3101
+ if (message.type === "user") {
3102
+ return message.uuid;
3103
+ }
3104
+ if (message.type === "assistant") {
3105
+ return message.uuid;
3106
+ }
3107
+ if (message.type === "summary") {
3108
+ return `summary:${message.leafUuid}`;
3109
+ }
3110
+ if (message.type === "system") {
3111
+ return message.uuid;
3112
+ }
3113
+ if (message.type === "progress") {
3114
+ return message.uuid;
3115
+ }
3116
+ return `unknown:${Math.random()}`;
3117
+ }
3118
+ function isPrimaryUserMessage(message) {
3119
+ if (message.type !== "user") return false;
3120
+ if (message.isSidechain) return false;
3121
+ if (message.isMeta) return false;
3122
+ const content = message.message?.content;
3123
+ if (Array.isArray(content)) return false;
3124
+ if (typeof content === "string") {
3125
+ const text = content.trim();
3126
+ if (text.startsWith("<task-notification>") && text.includes("</task-notification>")) {
3127
+ return false;
3128
+ }
3129
+ }
3130
+ return true;
3131
+ }
3132
+ async function readSessionLog(projectDir, sessionId) {
3133
+ const expectedSessionFile = join(projectDir, `${sessionId}.jsonl`);
3134
+ logger.debug(`[BACKFILL] Reading session file: ${expectedSessionFile}`);
3135
+ let file;
3136
+ try {
3137
+ file = await readFile(expectedSessionFile, "utf-8");
3138
+ } catch (error) {
3139
+ logger.debug(`[BACKFILL] Session file not found: ${expectedSessionFile}`);
3140
+ return [];
3141
+ }
3142
+ const lines = file.split("\n");
3143
+ const messages = [];
3144
+ for (const line of lines) {
3145
+ if (!line.trim()) continue;
3146
+ try {
3147
+ const parsedLine = JSON.parse(line);
3148
+ if (parsedLine?.type && INTERNAL_CLAUDE_EVENT_TYPES.has(parsedLine.type)) {
3149
+ continue;
3150
+ }
3151
+ const parsed = RawJSONLinesSchema.safeParse(parsedLine);
3152
+ if (!parsed.success) {
3153
+ continue;
3154
+ }
3155
+ messages.push(parsed.data);
3156
+ } catch (error) {
3157
+ logger.debug("[BACKFILL] Failed to parse session line", { error });
3158
+ }
3159
+ }
3160
+ return messages;
3161
+ }
3162
+ async function backfillClaudeSessionHistory(opts) {
3163
+ const maxMessages = opts.maxMessages ?? 200;
3164
+ const maxUserMessages = opts.maxUserMessages ?? 20;
3165
+ const maxBytes = opts.maxBytes ?? Number.POSITIVE_INFINITY;
3166
+ const projectDir = getProjectPath(opts.workingDirectory);
3167
+ let messages = await readSessionLog(projectDir, opts.sessionId);
3168
+ if (!messages.length) {
3169
+ const rawProjectId = opts.workingDirectory.replace(/[\\\/\.: _]/g, "-");
3170
+ const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
3171
+ const rawProjectDir = join(claudeConfigDir, "projects", rawProjectId);
3172
+ if (rawProjectDir !== projectDir) {
3173
+ messages = await readSessionLog(rawProjectDir, opts.sessionId);
3174
+ }
3175
+ }
3176
+ if (!messages.length) {
3177
+ logger.debug("[BACKFILL] No messages to backfill");
3178
+ return;
3179
+ }
3180
+ const selected = [];
3181
+ let totalCount = 0;
3182
+ let userCount = 0;
3183
+ let totalBytes = 0;
3184
+ for (let i = messages.length - 1; i >= 0; i--) {
3185
+ const message = messages[i];
3186
+ if (message.type === "summary") {
3187
+ continue;
3188
+ }
3189
+ const estimatedBytes = Buffer.byteLength(JSON.stringify(message), "utf8");
3190
+ if (totalBytes + estimatedBytes > maxBytes) {
3191
+ if (selected.length === 0) {
3192
+ selected.push(message);
3193
+ totalBytes += estimatedBytes;
3194
+ }
3195
+ break;
3196
+ }
3197
+ selected.push(message);
3198
+ totalBytes += estimatedBytes;
3199
+ if (message.type === "progress") {
3200
+ continue;
3201
+ }
3202
+ totalCount += 1;
3203
+ const isPrimaryUser = isPrimaryUserMessage(message);
3204
+ if (isPrimaryUser) {
3205
+ userCount += 1;
3206
+ }
3207
+ if (userCount >= maxUserMessages) {
3208
+ break;
3209
+ }
3210
+ if (totalCount >= maxMessages && isPrimaryUser) {
3211
+ break;
3212
+ }
3213
+ }
3214
+ selected.reverse();
3215
+ logger.debug(`[BACKFILL] Backfilling ${selected.length} messages (users: ${userCount}, total: ${totalCount}, bytes: ${totalBytes})`);
3216
+ const batch = selected.map((message) => ({
3217
+ message,
3218
+ localId: `claude-log:${opts.sessionId}:${messageKey(message)}`
3219
+ }));
3220
+ if (opts.sendBatch) {
3221
+ await opts.sendBatch(batch);
3222
+ return;
3223
+ }
3224
+ if (opts.send) {
3225
+ for (const item of batch) {
3226
+ opts.send(item.message, item.localId);
3227
+ }
3228
+ }
3229
+ }
3230
+
3231
+ function generateHookSettingsFile(port) {
3232
+ const hooksDir = join(configuration.happyHomeDir, "tmp", "hooks");
3233
+ mkdirSync(hooksDir, { recursive: true });
3234
+ const filename = `session-hook-${process.pid}.json`;
3235
+ const filepath = join(hooksDir, filename);
3236
+ const forwarderScript = resolve(projectPath(), "scripts", "session_hook_forwarder.cjs");
3237
+ const hookCommand = `node "${forwarderScript}" ${port}`;
3238
+ const settings = {
3239
+ hooks: {
3240
+ SessionStart: [
3241
+ {
3242
+ matcher: "*",
3243
+ hooks: [
3244
+ {
3245
+ type: "command",
3246
+ command: hookCommand
3247
+ }
3248
+ ]
3249
+ }
3250
+ ]
3251
+ }
3252
+ };
3253
+ writeFileSync(filepath, JSON.stringify(settings, null, 2));
3254
+ logger.debug(`[generateHookSettings] Created hook settings file: ${filepath}`);
3255
+ return filepath;
3256
+ }
3257
+ function cleanupHookSettingsFile(filepath) {
3258
+ try {
3259
+ if (existsSync(filepath)) {
3260
+ unlinkSync(filepath);
3261
+ logger.debug(`[generateHookSettings] Cleaned up hook settings file: ${filepath}`);
3262
+ }
3263
+ } catch (error) {
3264
+ logger.debug(`[generateHookSettings] Failed to cleanup hook settings file: ${error}`);
3265
+ }
3266
+ }
3267
+
3268
+ function detectWorktreeMetadata(cwd) {
3269
+ if (process.env.HAPPY_WORKSPACE_REPOS) {
3270
+ try {
3271
+ const repos = JSON.parse(process.env.HAPPY_WORKSPACE_REPOS);
3272
+ return {
3273
+ isWorktree: true,
3274
+ workspaceRepos: repos,
3275
+ workspacePath: process.env.HAPPY_WORKSPACE_PATH
3276
+ };
3277
+ } catch {
3278
+ }
3279
+ }
3280
+ if (process.env.HAPPY_WORKTREE_BASE_PATH) {
3281
+ return {
3282
+ isWorktree: true,
3283
+ worktreeBasePath: process.env.HAPPY_WORKTREE_BASE_PATH,
3284
+ worktreeBranchName: process.env.HAPPY_WORKTREE_BRANCH_NAME
3285
+ };
3286
+ }
3287
+ const info = detectGitWorktree(cwd);
3288
+ if (info.isWorktree) {
3289
+ return {
3290
+ isWorktree: true,
3291
+ worktreeBasePath: info.worktreeBasePath,
3292
+ worktreeBranchName: info.worktreeBranchName
3293
+ };
3294
+ }
3295
+ try {
3296
+ const entries = readdirSync(cwd, { withFileTypes: true });
3297
+ const repos = [];
3298
+ for (const entry of entries) {
3299
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
3300
+ const subDir = join(cwd, entry.name);
3301
+ const sub = detectGitWorktree(subDir);
3302
+ if (sub.isWorktree && sub.worktreeBasePath && sub.worktreeBranchName) {
3303
+ repos.push({
3304
+ path: subDir,
3305
+ basePath: sub.worktreeBasePath,
3306
+ branchName: sub.worktreeBranchName,
3307
+ displayName: entry.name
3308
+ });
3309
+ }
3310
+ }
3311
+ if (repos.length > 0) {
3312
+ return {
3313
+ isWorktree: true,
3314
+ workspaceRepos: repos,
3315
+ workspacePath: cwd
3316
+ };
3317
+ }
3318
+ } catch {
3319
+ }
3320
+ return {};
3321
+ }
3322
+ async function runClaude(credentials, options = {}) {
3323
+ logger.debug(`[CLAUDE] ===== CLAUDE MODE STARTING =====`);
3324
+ logger.debug(`[CLAUDE] This is the Claude agent, NOT Gemini`);
3325
+ const workingDirectory = process.cwd();
3326
+ const sessionTag = randomUUID();
3327
+ logger.debugLargeJson("[START] Happy process started", getEnvironmentInfo());
3328
+ logger.debug(`[START] Options: startedBy=${options.startedBy}, startingMode=${options.startingMode}`);
3329
+ if (options.startedBy === "daemon" && options.startingMode === "local") {
3330
+ throw new Error("Daemon-spawned sessions cannot use local/interactive mode. Use --happy-starting-mode remote or spawn sessions directly from terminal.");
3331
+ }
3332
+ connectionState.setBackend("Claude");
3333
+ const api = await ApiClient.create(credentials);
3334
+ let state = {};
3335
+ const settings = await readSettings();
3336
+ let machineId = settings?.machineId;
3337
+ if (!machineId) {
3338
+ console.error(`[START] No machine ID found in settings, which is unexpected since authAndSetupMachineIfNeeded should have created it. Please report this issue on https://github.com/ZonEaseTech/happy-wfs/issues`);
3339
+ process.exit(1);
3340
+ }
3341
+ logger.debug(`Using machineId: ${machineId}`);
3342
+ await api.getOrCreateMachine({
3343
+ machineId,
3344
+ metadata: initialMachineMetadata
3345
+ });
3346
+ let metadata = {
3347
+ path: workingDirectory,
3348
+ host: os.hostname(),
3349
+ version: packageJson.version,
3350
+ os: os.platform(),
3351
+ machineId,
3352
+ homeDir: os.homedir(),
3353
+ happyHomeDir: configuration.happyHomeDir,
3354
+ happyLibDir: projectPath(),
3355
+ happyToolsDir: resolveToolsDir(),
3356
+ startedFromDaemon: options.startedBy === "daemon",
3357
+ hostPid: process.pid,
3358
+ startedBy: options.startedBy || "terminal",
3359
+ // Initialize lifecycle state
3360
+ lifecycleState: "running",
3361
+ lifecycleStateSince: Date.now(),
3362
+ flavor: "claude",
3363
+ // Worktree metadata: env vars from daemon take priority, otherwise detect via git
3364
+ ...detectWorktreeMetadata(workingDirectory)
3365
+ };
3366
+ const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
3367
+ if (!response) {
3368
+ let offlineSessionId = null;
3369
+ const reconnection = startOfflineReconnection({
3370
+ serverUrl: configuration.serverUrl,
3371
+ onReconnected: async () => {
3372
+ const resp = await api.getOrCreateSession({ tag: randomUUID(), metadata, state });
3373
+ if (!resp) throw new Error("Server unavailable");
3374
+ const session2 = api.sessionSyncClient(resp);
3375
+ const scanner = await createSessionScanner({
3376
+ sessionId: null,
3377
+ workingDirectory,
3378
+ onMessage: (msg) => session2.sendClaudeSessionMessage(msg)
3379
+ });
3380
+ if (offlineSessionId) scanner.onNewSession(offlineSessionId);
3381
+ return { session: session2, scanner };
3382
+ },
3383
+ onNotify: console.log,
3384
+ onCleanup: () => {
3385
+ }
3386
+ });
3387
+ try {
3388
+ await claudeLocal({
3389
+ path: workingDirectory,
3390
+ sessionId: null,
3391
+ onSessionFound: (id) => {
3392
+ offlineSessionId = id;
3393
+ },
3394
+ onThinkingChange: () => {
3395
+ },
3396
+ abort: new AbortController().signal,
3397
+ claudeEnvVars: options.claudeEnvVars,
3398
+ claudeArgs: options.claudeArgs,
3399
+ mcpServers: {},
3400
+ allowedTools: []
3401
+ });
3402
+ } finally {
3403
+ reconnection.cancel();
3404
+ stopCaffeinate();
3405
+ }
3406
+ process.exit(0);
3407
+ }
3408
+ logger.debug(`Session created: ${response.id}`);
3409
+ try {
3410
+ logger.debug(`[START] Reporting session ${response.id} to daemon`);
3411
+ const result = await notifyDaemonSessionStarted(response.id, metadata);
3412
+ if (result.error) {
3413
+ logger.debug(`[START] Failed to report to daemon (may not be running):`, result.error);
3414
+ } else {
3415
+ logger.debug(`[START] Reported session ${response.id} to daemon`);
3416
+ }
3417
+ } catch (error) {
3418
+ logger.debug("[START] Failed to report to daemon (may not be running):", error);
3419
+ }
3420
+ extractSDKMetadataAsync(async (sdkMetadata) => {
3421
+ logger.debug("[start] SDK metadata extracted, updating session:", sdkMetadata);
3422
+ try {
3423
+ api.sessionSyncClient(response).updateMetadata((currentMetadata) => ({
3424
+ ...currentMetadata,
3425
+ tools: sdkMetadata.tools,
3426
+ slashCommands: sdkMetadata.slashCommands
3427
+ }));
3428
+ logger.debug("[start] Session metadata updated with SDK capabilities");
3429
+ } catch (error) {
3430
+ logger.debug("[start] Failed to update session metadata:", error);
3431
+ }
3432
+ });
3433
+ const session = api.sessionSyncClient(response);
3434
+ const sessionTitle = process.env.HAPPY_SESSION_TITLE?.trim();
3435
+ const initialClaudeSessionId = process.env.HAPPY_CLAUDE_RESUME_SESSION_ID?.trim();
3436
+ const skipForkSession = process.env.HAPPY_CLAUDE_SKIP_FORK_SESSION === "1";
3437
+ if (sessionTitle || initialClaudeSessionId && skipForkSession) {
3438
+ session.updateMetadata((currentMetadata) => ({
3439
+ ...currentMetadata,
3440
+ ...sessionTitle && {
3441
+ summary: {
3442
+ text: sessionTitle,
3443
+ updatedAt: Date.now()
3444
+ }
3445
+ },
3446
+ ...initialClaudeSessionId && skipForkSession && {
3447
+ claudeSessionId: initialClaudeSessionId
3448
+ }
3449
+ }));
3450
+ }
3451
+ const injectedMemories = await api.listMemories();
3452
+ let initialMemoryAppendPrompt;
3453
+ if (injectedMemories.length > 0) {
3454
+ const memoryBlock = "<user_memory>\n" + injectedMemories.map((m) => m.content).join("\n\n") + "\n</user_memory>";
3455
+ initialMemoryAppendPrompt = memoryBlock;
3456
+ const injectedMemoryIds = injectedMemories.map((m) => m.id);
3457
+ session.updateMetadata((currentMetadata) => ({
3458
+ ...currentMetadata,
3459
+ injectedMemoryIds
3460
+ }));
3461
+ logger.debug(`[START] Injected ${injectedMemoryIds.length} memories into session`);
3462
+ }
3463
+ const mcp = await createMcpContext(session);
3464
+ logger.debug(`[START] MCP context created`);
3465
+ let currentSession = null;
3466
+ const shouldBackfill = ["1", "true", "yes"].includes(String(process.env.HAPPY_CLAUDE_BACKFILL).toLowerCase());
3467
+ const backfillMaxMessages = Number(process.env.HAPPY_CLAUDE_BACKFILL_MAX_MESSAGES) || 200;
3468
+ const backfillMaxUserMessages = Number(process.env.HAPPY_CLAUDE_BACKFILL_MAX_USER_MESSAGES) || 20;
3469
+ const backfillMaxBytes = Number(process.env.HAPPY_CLAUDE_BACKFILL_MAX_BYTES) || 3 * 1024 * 1024;
3470
+ const resumeSessionId = process.env.HAPPY_CLAUDE_RESUME_SESSION_ID || void 0;
3471
+ const backfilledSessions = /* @__PURE__ */ new Set();
3472
+ const backfillInFlight = /* @__PURE__ */ new Set();
3473
+ const ensureResumeSessionFileAvailable = async (sessionId) => {
3474
+ const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join(os.homedir(), ".claude");
3475
+ const resolvedProjectDir = getProjectPath(workingDirectory);
3476
+ const rawProjectId = workingDirectory.replace(/[\\\/\.: _]/g, "-");
3477
+ const rawProjectDir = join(claudeConfigDir, "projects", rawProjectId);
3478
+ const targetDirs = /* @__PURE__ */ new Set([resolvedProjectDir, rawProjectDir]);
3479
+ const sourceProjectId = await findClaudeProjectId(sessionId);
3480
+ if (!sourceProjectId) {
3481
+ logger.debug(`[START] Resume session file not found in any Claude project: ${sessionId}`);
3482
+ return;
3483
+ }
3484
+ const sourcePath = join(claudeConfigDir, "projects", sourceProjectId, `${sessionId}.jsonl`);
3485
+ logger.debug(`[START] Resume session file source: ${sourcePath}`);
3486
+ for (const targetDir of targetDirs) {
3487
+ const targetPath = join(targetDir, `${sessionId}.jsonl`);
3488
+ if (targetPath === sourcePath) {
3489
+ logger.debug(`[START] Resume session file already present (source): ${targetPath}`);
3490
+ continue;
3491
+ }
3492
+ try {
3493
+ await stat(targetPath);
3494
+ logger.debug(`[START] Resume session file already present: ${targetPath}`);
3495
+ continue;
3496
+ } catch {
3497
+ }
3498
+ try {
3499
+ await mkdir(targetDir, { recursive: true });
3500
+ await copyFile(sourcePath, targetPath);
3501
+ logger.debug(`[START] Copied resume session file into project dir: ${targetPath}`);
3502
+ } catch (error) {
3503
+ logger.debug("[START] Resume session copy error details", { targetPath, error });
3504
+ }
3505
+ }
3506
+ };
3507
+ const runBackfill = async (sessionId) => {
3508
+ if (!shouldBackfill) return;
3509
+ if (backfilledSessions.has(sessionId)) return;
3510
+ if (backfillInFlight.has(sessionId)) return;
3511
+ backfillInFlight.add(sessionId);
3512
+ for (let i = 0; i < 15 && !session.isConnected(); i++) {
3513
+ await new Promise((resolve2) => setTimeout(resolve2, 200));
3514
+ }
3515
+ if (!session.isConnected()) {
3516
+ logger.debug("[START] Backfill skipped: session socket not connected");
3517
+ backfillInFlight.delete(sessionId);
3518
+ return;
3519
+ }
3520
+ try {
3521
+ await backfillClaudeSessionHistory({
3522
+ workingDirectory,
3523
+ sessionId,
3524
+ sendBatch: async (messages) => {
3525
+ await session.sendClaudeSessionMessageBatch(messages, "replace");
3526
+ },
3527
+ maxMessages: backfillMaxMessages,
3528
+ maxUserMessages: backfillMaxUserMessages,
3529
+ maxBytes: backfillMaxBytes
3530
+ });
3531
+ backfilledSessions.add(sessionId);
3532
+ } finally {
3533
+ backfillInFlight.delete(sessionId);
3534
+ }
3535
+ };
3536
+ if (resumeSessionId) {
3537
+ await ensureResumeSessionFileAvailable(resumeSessionId);
3538
+ runBackfill(resumeSessionId).catch((error) => {
3539
+ logger.debug("[START] Backfill failed:", error);
3540
+ });
3541
+ }
3542
+ const hookServer = await startHookServer({
3543
+ onSessionHook: (sessionId, data) => {
3544
+ logger.debug(`[START] Session hook received: ${sessionId}`, data);
3545
+ if (currentSession) {
3546
+ const previousSessionId = currentSession.sessionId;
3547
+ if (previousSessionId !== sessionId) {
3548
+ logger.debug(`[START] Claude session ID changed: ${previousSessionId} -> ${sessionId}`);
3549
+ currentSession.onSessionFound(sessionId);
3550
+ }
3551
+ }
3552
+ runBackfill(sessionId).catch((error) => {
3553
+ logger.debug("[START] Backfill failed:", error);
3554
+ });
3555
+ }
3556
+ });
3557
+ logger.debug(`[START] Hook server started on port ${hookServer.port}`);
3558
+ const hookSettingsPath = generateHookSettingsFile(hookServer.port);
3559
+ logger.debug(`[START] Generated hook settings file: ${hookSettingsPath}`);
3560
+ const logPath = logger.logFilePath;
3561
+ logger.infoDeveloper(`Session: ${response.id}`);
3562
+ logger.infoDeveloper(`Logs: ${logPath}`);
3563
+ session.updateAgentState((currentState) => ({
3564
+ ...currentState,
3565
+ controlledByUser: options.startingMode !== "remote"
3566
+ }));
3567
+ const caffeinateStarted = startCaffeinate();
3568
+ if (caffeinateStarted) {
3569
+ logger.infoDeveloper("Sleep prevention enabled (macOS)");
3570
+ }
3571
+ const messageQueue = new MessageQueue2((mode) => hashObject({
3572
+ isPlan: mode.permissionMode === "plan",
3573
+ model: mode.model,
3574
+ reasoningEffort: mode.reasoningEffort,
3575
+ fallbackModel: mode.fallbackModel,
3576
+ customSystemPrompt: mode.customSystemPrompt,
3577
+ appendSystemPrompt: mode.appendSystemPrompt,
3578
+ allowedTools: mode.allowedTools,
3579
+ disallowedTools: mode.disallowedTools
3580
+ }));
3581
+ let currentPermissionMode = options.permissionMode;
3582
+ let currentModel = options.model;
3583
+ let currentReasoningEffort = void 0;
3584
+ let currentFallbackModel = void 0;
3585
+ let currentCustomSystemPrompt = void 0;
3586
+ let currentAppendSystemPrompt = initialMemoryAppendPrompt;
3587
+ let currentAllowedTools = void 0;
3588
+ let currentDisallowedTools = void 0;
3589
+ session.onUserMessage((message) => {
3590
+ let messagePermissionMode = currentPermissionMode;
3591
+ if (message.meta?.permissionMode) {
3592
+ messagePermissionMode = message.meta.permissionMode;
3593
+ currentPermissionMode = messagePermissionMode;
3594
+ logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
3595
+ } else {
3596
+ logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
3597
+ }
3598
+ let messageModel = currentModel;
3599
+ if (message.meta?.hasOwnProperty("model")) {
3600
+ messageModel = message.meta.model || void 0;
3601
+ currentModel = messageModel;
3602
+ logger.debug(`[loop] Model updated from user message: ${messageModel || "reset to default"}`);
3603
+ } else {
3604
+ logger.debug(`[loop] User message received with no model override, using current: ${currentModel || "default"}`);
3605
+ }
3606
+ let messageReasoningEffort = currentReasoningEffort;
3607
+ if (message.meta?.hasOwnProperty("reasoningEffort")) {
3608
+ messageReasoningEffort = message.meta.reasoningEffort || void 0;
3609
+ currentReasoningEffort = messageReasoningEffort;
3610
+ logger.debug(`[loop] Reasoning effort updated from user message: ${messageReasoningEffort || "reset to default"}`);
3611
+ } else {
3612
+ logger.debug(`[loop] User message received with no reasoning effort override, using current: ${currentReasoningEffort || "default"}`);
3613
+ }
3614
+ let messageCustomSystemPrompt = currentCustomSystemPrompt;
3615
+ if (message.meta?.hasOwnProperty("customSystemPrompt")) {
3616
+ messageCustomSystemPrompt = message.meta.customSystemPrompt || void 0;
3617
+ currentCustomSystemPrompt = messageCustomSystemPrompt;
3618
+ logger.debug(`[loop] Custom system prompt updated from user message: ${messageCustomSystemPrompt ? "set" : "reset to none"}`);
3619
+ } else {
3620
+ logger.debug(`[loop] User message received with no custom system prompt override, using current: ${currentCustomSystemPrompt ? "set" : "none"}`);
3621
+ }
3622
+ let messageFallbackModel = currentFallbackModel;
3623
+ if (message.meta?.hasOwnProperty("fallbackModel")) {
3624
+ messageFallbackModel = message.meta.fallbackModel || void 0;
3625
+ currentFallbackModel = messageFallbackModel;
3626
+ logger.debug(`[loop] Fallback model updated from user message: ${messageFallbackModel || "reset to none"}`);
3627
+ } else {
3628
+ logger.debug(`[loop] User message received with no fallback model override, using current: ${currentFallbackModel || "none"}`);
3629
+ }
3630
+ let messageAppendSystemPrompt = currentAppendSystemPrompt;
3631
+ if (message.meta?.hasOwnProperty("appendSystemPrompt")) {
3632
+ messageAppendSystemPrompt = message.meta.appendSystemPrompt || void 0;
3633
+ currentAppendSystemPrompt = messageAppendSystemPrompt;
3634
+ logger.debug(`[loop] Append system prompt updated from user message: ${messageAppendSystemPrompt ? "set" : "reset to none"}`);
3635
+ } else {
3636
+ logger.debug(`[loop] User message received with no append system prompt override, using current: ${currentAppendSystemPrompt ? "set" : "none"}`);
3637
+ }
3638
+ let messageAllowedTools = currentAllowedTools;
3639
+ if (message.meta?.hasOwnProperty("allowedTools")) {
3640
+ messageAllowedTools = message.meta.allowedTools || void 0;
3641
+ currentAllowedTools = messageAllowedTools;
3642
+ logger.debug(`[loop] Allowed tools updated from user message: ${messageAllowedTools ? messageAllowedTools.join(", ") : "reset to none"}`);
3643
+ } else {
3644
+ logger.debug(`[loop] User message received with no allowed tools override, using current: ${currentAllowedTools ? currentAllowedTools.join(", ") : "none"}`);
3645
+ }
3646
+ let messageDisallowedTools = currentDisallowedTools;
3647
+ if (message.meta?.hasOwnProperty("disallowedTools")) {
3648
+ messageDisallowedTools = message.meta.disallowedTools || void 0;
3649
+ currentDisallowedTools = messageDisallowedTools;
3650
+ logger.debug(`[loop] Disallowed tools updated from user message: ${messageDisallowedTools ? messageDisallowedTools.join(", ") : "reset to none"}`);
3651
+ } else {
3652
+ logger.debug(`[loop] User message received with no disallowed tools override, using current: ${currentDisallowedTools ? currentDisallowedTools.join(", ") : "none"}`);
3653
+ }
3654
+ const specialCommand = parseSpecialCommand(message.content.text);
3655
+ const enhancedMode = {
3656
+ permissionMode: messagePermissionMode || "default",
3657
+ model: messageModel,
3658
+ reasoningEffort: messageReasoningEffort,
3659
+ fallbackModel: messageFallbackModel,
3660
+ customSystemPrompt: messageCustomSystemPrompt,
3661
+ appendSystemPrompt: messageAppendSystemPrompt,
3662
+ allowedTools: messageAllowedTools,
3663
+ disallowedTools: messageDisallowedTools
3664
+ };
3665
+ if (specialCommand.type === "compact") {
3666
+ logger.debug("[start] Detected /compact command");
3667
+ messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode);
3668
+ logger.debugLargeJson("[start] /compact command pushed to queue:", message);
3669
+ return;
3670
+ }
3671
+ if (specialCommand.type === "clear") {
3672
+ logger.debug("[start] Detected /clear command");
3673
+ messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode);
3674
+ logger.debugLargeJson("[start] /clear command pushed to queue:", message);
3675
+ return;
3676
+ }
3677
+ if (message.content.type === "mixed") {
3678
+ messageQueue.push(message.content, enhancedMode);
3679
+ } else {
3680
+ messageQueue.push(message.content.text, enhancedMode);
3681
+ }
3682
+ logger.debugLargeJson("User message pushed to queue:", message);
3683
+ });
3684
+ const cleanup = async () => {
3685
+ logger.debug("[START] Received termination signal, cleaning up...");
3686
+ try {
3687
+ if (session) {
3688
+ session.updateMetadata((currentMetadata) => ({
3689
+ ...currentMetadata,
3690
+ lifecycleState: "archived",
3691
+ lifecycleStateSince: Date.now(),
3692
+ archivedBy: "cli",
3693
+ archiveReason: "User terminated"
3694
+ }));
3695
+ currentSession?.cleanup();
3696
+ session.sendSessionDeath();
3697
+ await session.flush();
3698
+ await session.close();
3699
+ }
3700
+ stopCaffeinate();
3701
+ mcp.stop();
3702
+ hookServer.stop();
3703
+ cleanupHookSettingsFile(hookSettingsPath);
3704
+ logger.debug("[START] Cleanup complete, exiting");
3705
+ process.exit(0);
3706
+ } catch (error) {
3707
+ logger.debug("[START] Error during cleanup:", error);
3708
+ process.exit(1);
3709
+ }
3710
+ };
3711
+ process.on("SIGTERM", cleanup);
3712
+ process.on("SIGINT", cleanup);
3713
+ process.on("uncaughtException", (error) => {
3714
+ logger.debug("[START] Uncaught exception:", error);
3715
+ cleanup();
3716
+ });
3717
+ process.on("unhandledRejection", (reason) => {
3718
+ logger.debug("[START] Unhandled rejection:", reason);
3719
+ cleanup();
3720
+ });
3721
+ registerKillSessionHandler(session.rpcHandlerManager, cleanup);
3722
+ const exitCode = await loop({
3723
+ path: workingDirectory,
3724
+ model: options.model,
3725
+ permissionMode: options.permissionMode,
3726
+ startingMode: options.startingMode,
3727
+ messageQueue,
3728
+ api,
3729
+ allowedTools: mcp.allowedToolNames(),
3730
+ onModeChange: (newMode) => {
3731
+ session.sendSessionEvent({ type: "switch", mode: newMode });
3732
+ session.updateAgentState((currentState) => ({
3733
+ ...currentState,
3734
+ controlledByUser: newMode === "local"
3735
+ }));
3736
+ },
3737
+ onSessionReady: (sessionInstance) => {
3738
+ currentSession = sessionInstance;
3739
+ },
3740
+ mcpServers: mcp.configForClaude(),
3741
+ session,
3742
+ claudeEnvVars: options.claudeEnvVars,
3743
+ claudeArgs: options.claudeArgs,
3744
+ hookSettingsPath,
3745
+ jsRuntime: options.jsRuntime
3746
+ });
3747
+ currentSession?.cleanup();
3748
+ session.sendSessionDeath();
3749
+ logger.debug("Waiting for socket to flush...");
3750
+ await session.flush();
3751
+ logger.debug("Closing session...");
3752
+ await session.close();
3753
+ stopCaffeinate();
3754
+ logger.debug("Stopped sleep prevention");
3755
+ mcp.stop();
3756
+ logger.debug("Stopped Happy MCP server");
3757
+ hookServer.stop();
3758
+ cleanupHookSettingsFile(hookSettingsPath);
3759
+ logger.debug("Stopped Hook server and cleaned up settings file");
3760
+ process.exit(exitCode);
3761
+ }
3762
+
3763
+ export { runClaude };