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