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