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