@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.
- package/dist/{types-BWpRU1VN.mjs → api-CIZKyfSn.mjs} +136 -1725
- package/dist/{types-DvTsFaqR.cjs → api-CwOCQeLH.cjs} +317 -1961
- package/dist/auth-0VnlXT9f.mjs +181 -0
- package/dist/auth-BE8dD20T.mjs +270 -0
- package/dist/auth-BIleVCw0.cjs +274 -0
- package/dist/auth-Cas9DR3b.cjs +183 -0
- package/dist/browser-Cc08m7of.cjs +22 -0
- package/dist/browser-Dll5hxOV.mjs +20 -0
- package/dist/claudeLocal-CPT3zcIU.mjs +344 -0
- package/dist/claudeLocal-WkgLbl1a.cjs +352 -0
- package/dist/codex/happyMcpStdioBridge.cjs +4 -1
- package/dist/codex/happyMcpStdioBridge.mjs +4 -1
- package/dist/common-BdNgghUJ.mjs +75 -0
- package/dist/common-CZvSRU4p.cjs +83 -0
- package/dist/{config-0Xnc8ARM.cjs → config-Draa-5XC.cjs} +21 -34
- package/dist/{config-_GW4YBCP.mjs → config-rTT4qTU5.mjs} +7 -20
- package/dist/connect-BfC1tYP_.mjs +679 -0
- package/dist/connect-CJZ73syK.cjs +681 -0
- package/dist/{constants-B4x1K7by.cjs → constants-E_UWYTo3.cjs} +2 -2
- package/dist/{constants-BO9OAFOT.mjs → constants-pmXZGibX.mjs} +1 -1
- package/dist/controlClient-CM_QcJnB.mjs +159 -0
- package/dist/controlClient-DZMIsKsT.cjs +168 -0
- package/dist/deviceExec-CU1R9CRb.cjs +84 -0
- package/dist/deviceExec-D93AOVV4.mjs +81 -0
- package/dist/diagnose-B3D1a_v0.cjs +60 -0
- package/dist/diagnose-CuRKXxt1.mjs +58 -0
- package/dist/doctor-C08m5Z9f.cjs +78 -0
- package/dist/doctor-Cya9YSA0.cjs +214 -0
- package/dist/doctor-DZYB6IVc.mjs +211 -0
- package/dist/doctor-ON7pN5G5.mjs +74 -0
- package/dist/downloadImage-DfSFLb-h.mjs +887 -0
- package/dist/downloadImage-MQMrUUxS.cjs +897 -0
- package/dist/enroll-BxlbVftP.cjs +115 -0
- package/dist/enroll-C52x8oly.mjs +113 -0
- package/dist/evidence-Ccd9iYcj.mjs +127 -0
- package/dist/evidence-CjWBG5rX.cjs +129 -0
- package/dist/formatImageMessage-B4lSxlk2.mjs +43 -0
- package/dist/formatImageMessage-DbyG8c2j.cjs +46 -0
- package/dist/github-DKcyF1zp.mjs +57 -0
- package/dist/github-h9GhJJ_C.cjs +59 -0
- package/dist/index-CDTNgfUf.cjs +578 -0
- package/dist/index-CPKdvi7J.mjs +554 -0
- package/dist/index.cjs +812 -40
- package/dist/index.mjs +813 -40
- package/dist/install-BokHRgFW.cjs +98 -0
- package/dist/install-C_cp8b2-.cjs +96 -0
- package/dist/install-DAeCn8ae.mjs +94 -0
- package/dist/install-oRAjVLjP.mjs +95 -0
- package/dist/lib.cjs +18 -12
- package/dist/lib.mjs +13 -7
- package/dist/{loadableToolOutput-va3RLplh.mjs → loadableToolOutput-Cuygmd4K.mjs} +4 -2
- package/dist/{loadableToolOutput-5vdQB98M.cjs → loadableToolOutput-DDCJjywt.cjs} +63 -61
- package/dist/mcpToolSchemas-CjeTfrAs.mjs +62 -0
- package/dist/mcpToolSchemas-DK42yoZ_.cjs +64 -0
- package/dist/persistence-BFjvtUad.mjs +944 -0
- package/dist/persistence-JLUn_Nou.cjs +996 -0
- package/dist/projectPath-BTBIMB57.mjs +10 -0
- package/dist/projectPath-ChLhdOyt.cjs +13 -0
- package/dist/prompt-BTWDZZO0.cjs +25 -0
- package/dist/prompt-BsAZsPbo.mjs +22 -0
- package/dist/release-BMEzNONE.mjs +168 -0
- package/dist/release-EhOJeAZe.cjs +171 -0
- package/dist/run-BDn2aE3Q.cjs +2152 -0
- package/dist/run-DBZqN6Kn.mjs +2128 -0
- package/dist/runClaude-DNHvERX0.mjs +3763 -0
- package/dist/runClaude-uaKxPZPm.cjs +3766 -0
- package/dist/{runCodex-skq0j05d.cjs → runCodex-82UrZLme.cjs} +169 -153
- package/dist/{runCodex-DiKzBf9q.mjs → runCodex-C-RTM0wA.mjs} +36 -20
- package/dist/{runGemini-BeqrVq8R.mjs → runGemini-Bm6-4Xm2.mjs} +34 -17
- package/dist/{runGemini-CVp-CEIh.cjs → runGemini-CXMr4Jwb.cjs} +212 -195
- package/dist/{runOneShot-BQNkNW61.cjs → runOneShot-1B6RMYT7.cjs} +31 -55
- package/dist/{runOneShot-ltCpHbBB.mjs → runOneShot-ioARRHrx.mjs} +14 -38
- package/dist/{serve-BECdtivY.cjs → serve-BtdAhkIE.cjs} +35 -58
- package/dist/{serve-BNdoEDVx.mjs → serve-CwibwU6N.mjs} +12 -35
- package/dist/session-2sU4Bu1R.mjs +108 -0
- package/dist/session-D--F87zL.cjs +110 -0
- package/dist/sessionKeyCache-4Tp8pJ3N.cjs +36 -0
- package/dist/sessionKeyCache-D2BwO91C.mjs +33 -0
- package/dist/sessionList-BCSw3qYt.mjs +51 -0
- package/dist/sessionList-ICJsjNtJ.cjs +54 -0
- package/dist/sessionSummary-Bkz52BPh.cjs +393 -0
- package/dist/sessionSummary-lHGBWQSb.mjs +387 -0
- package/dist/spawnHappyCLI-CoORX0YW.mjs +66 -0
- package/dist/spawnHappyCLI-D_V-AAoP.cjs +70 -0
- package/dist/ssh-BTOlhhxi.cjs +333 -0
- package/dist/ssh-DL_AkRAD.mjs +328 -0
- package/dist/systemdScope-BeOtAkhk.mjs +25 -0
- package/dist/systemdScope-DW3-UoKB.cjs +29 -0
- package/dist/task-CQzrHRZe.mjs +165 -0
- package/dist/task-CSpaC9HX.cjs +167 -0
- package/dist/time-Bj2HRkKH.cjs +33 -0
- package/dist/time-DgsBLJA_.mjs +29 -0
- package/dist/trimIdent-4gvk9Wk6.cjs +22 -0
- package/dist/trimIdent-BAjmv_TG.mjs +20 -0
- package/dist/types-BoJh4Qfv.mjs +57 -0
- package/dist/types-DWeB7aqh.cjs +59 -0
- package/dist/uninstall-BiKyRAb1.cjs +77 -0
- package/dist/uninstall-nFMFGUVM.mjs +75 -0
- package/dist/update-BcqRHXf2.mjs +109 -0
- package/dist/update-Dw8pgsAT.cjs +111 -0
- package/dist/workflowCommon-B0d01Y_a.mjs +56 -0
- package/dist/workflowCommon-Bl568-rt.cjs +62 -0
- package/dist/workflowReports-BEWuQesA.cjs +113 -0
- package/dist/workflowReports-Dd8-4-8f.mjs +108 -0
- package/package.json +1 -1
- package/dist/index-BbCDDvoB.cjs +0 -11376
- package/dist/index-vvZXdswy.mjs +0 -11336
- package/dist/mcpToolSchemas-CdPXJQmb.mjs +0 -175
- 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 };
|