happy-coder 0.1.13 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1114 -406
- package/dist/index.mjs +1115 -407
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +28 -5
- package/dist/lib.d.mts +28 -5
- package/dist/lib.mjs +1 -1
- package/dist/types-Cg4664gs.cjs +879 -0
- package/dist/types-CkPUFpfr.cjs +885 -0
- package/dist/types-DD9P_5rj.mjs +868 -0
- package/dist/types-DNu8okOb.mjs +874 -0
- package/package.json +3 -3
- package/scripts/claudeInteractiveLaunch.cjs +72 -13
package/dist/index.cjs
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chalk = require('chalk');
|
|
4
|
-
var types = require('./types-
|
|
4
|
+
var types = require('./types-CkPUFpfr.cjs');
|
|
5
5
|
var node_crypto = require('node:crypto');
|
|
6
|
-
var
|
|
6
|
+
var node_child_process = require('node:child_process');
|
|
7
|
+
var node_readline = require('node:readline');
|
|
7
8
|
var node_fs = require('node:fs');
|
|
8
9
|
var node_path = require('node:path');
|
|
10
|
+
var node_url = require('node:url');
|
|
9
11
|
var os = require('node:os');
|
|
10
12
|
var promises = require('fs/promises');
|
|
11
|
-
var node_child_process = require('node:child_process');
|
|
12
|
-
var node_readline = require('node:readline');
|
|
13
|
-
var node_url = require('node:url');
|
|
14
13
|
var promises$1 = require('node:fs/promises');
|
|
15
14
|
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
16
15
|
var node_http = require('node:http');
|
|
@@ -21,7 +20,6 @@ var util = require('util');
|
|
|
21
20
|
var crypto = require('crypto');
|
|
22
21
|
var path = require('path');
|
|
23
22
|
var url = require('url');
|
|
24
|
-
var httpProxy = require('http-proxy');
|
|
25
23
|
var tweetnacl = require('tweetnacl');
|
|
26
24
|
var axios = require('axios');
|
|
27
25
|
var qrcode = require('qrcode-terminal');
|
|
@@ -51,6 +49,343 @@ function _interopNamespaceDefault(e) {
|
|
|
51
49
|
|
|
52
50
|
var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
|
|
53
51
|
|
|
52
|
+
class Stream {
|
|
53
|
+
constructor(returned) {
|
|
54
|
+
this.returned = returned;
|
|
55
|
+
}
|
|
56
|
+
queue = [];
|
|
57
|
+
readResolve;
|
|
58
|
+
readReject;
|
|
59
|
+
isDone = false;
|
|
60
|
+
hasError;
|
|
61
|
+
started = false;
|
|
62
|
+
/**
|
|
63
|
+
* Implements async iterable protocol
|
|
64
|
+
*/
|
|
65
|
+
[Symbol.asyncIterator]() {
|
|
66
|
+
if (this.started) {
|
|
67
|
+
throw new Error("Stream can only be iterated once");
|
|
68
|
+
}
|
|
69
|
+
this.started = true;
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Gets the next value from the stream
|
|
74
|
+
*/
|
|
75
|
+
async next() {
|
|
76
|
+
if (this.queue.length > 0) {
|
|
77
|
+
return Promise.resolve({
|
|
78
|
+
done: false,
|
|
79
|
+
value: this.queue.shift()
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (this.isDone) {
|
|
83
|
+
return Promise.resolve({ done: true, value: void 0 });
|
|
84
|
+
}
|
|
85
|
+
if (this.hasError) {
|
|
86
|
+
return Promise.reject(this.hasError);
|
|
87
|
+
}
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
this.readResolve = resolve;
|
|
90
|
+
this.readReject = reject;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Adds a value to the stream
|
|
95
|
+
*/
|
|
96
|
+
enqueue(value) {
|
|
97
|
+
if (this.readResolve) {
|
|
98
|
+
const resolve = this.readResolve;
|
|
99
|
+
this.readResolve = void 0;
|
|
100
|
+
this.readReject = void 0;
|
|
101
|
+
resolve({ done: false, value });
|
|
102
|
+
} else {
|
|
103
|
+
this.queue.push(value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Marks the stream as complete
|
|
108
|
+
*/
|
|
109
|
+
done() {
|
|
110
|
+
this.isDone = true;
|
|
111
|
+
if (this.readResolve) {
|
|
112
|
+
const resolve = this.readResolve;
|
|
113
|
+
this.readResolve = void 0;
|
|
114
|
+
this.readReject = void 0;
|
|
115
|
+
resolve({ done: true, value: void 0 });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Propagates an error through the stream
|
|
120
|
+
*/
|
|
121
|
+
error(error) {
|
|
122
|
+
this.hasError = error;
|
|
123
|
+
if (this.readReject) {
|
|
124
|
+
const reject = this.readReject;
|
|
125
|
+
this.readResolve = void 0;
|
|
126
|
+
this.readReject = void 0;
|
|
127
|
+
reject(error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Implements async iterator cleanup
|
|
132
|
+
*/
|
|
133
|
+
async return() {
|
|
134
|
+
this.isDone = true;
|
|
135
|
+
if (this.returned) {
|
|
136
|
+
this.returned();
|
|
137
|
+
}
|
|
138
|
+
return Promise.resolve({ done: true, value: void 0 });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
class AbortError extends Error {
|
|
143
|
+
constructor(message) {
|
|
144
|
+
super(message);
|
|
145
|
+
this.name = "AbortError";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
150
|
+
const __dirname$3 = node_path.join(__filename$1, "..");
|
|
151
|
+
function getDefaultClaudeCodePath() {
|
|
152
|
+
return node_path.join(__dirname$3, "..", "..", "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
|
|
153
|
+
}
|
|
154
|
+
function logDebug(message) {
|
|
155
|
+
if (process.env.DEBUG) {
|
|
156
|
+
types.logger.debug(message);
|
|
157
|
+
console.log(message);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function streamToStdin(stream, stdin, abortController) {
|
|
161
|
+
for await (const message of stream) {
|
|
162
|
+
if (abortController.signal.aborted) break;
|
|
163
|
+
stdin.write(JSON.stringify(message) + "\n");
|
|
164
|
+
}
|
|
165
|
+
stdin.end();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
class Query {
|
|
169
|
+
constructor(childStdin, childStdout, processExitPromise) {
|
|
170
|
+
this.childStdin = childStdin;
|
|
171
|
+
this.childStdout = childStdout;
|
|
172
|
+
this.processExitPromise = processExitPromise;
|
|
173
|
+
this.readMessages();
|
|
174
|
+
this.sdkMessages = this.readSdkMessages();
|
|
175
|
+
}
|
|
176
|
+
pendingControlResponses = /* @__PURE__ */ new Map();
|
|
177
|
+
sdkMessages;
|
|
178
|
+
inputStream = new Stream();
|
|
179
|
+
/**
|
|
180
|
+
* Set an error on the stream
|
|
181
|
+
*/
|
|
182
|
+
setError(error) {
|
|
183
|
+
this.inputStream.error(error);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* AsyncIterableIterator implementation
|
|
187
|
+
*/
|
|
188
|
+
next(...args) {
|
|
189
|
+
return this.sdkMessages.next(...args);
|
|
190
|
+
}
|
|
191
|
+
return(value) {
|
|
192
|
+
if (this.sdkMessages.return) {
|
|
193
|
+
return this.sdkMessages.return(value);
|
|
194
|
+
}
|
|
195
|
+
return Promise.resolve({ done: true, value: void 0 });
|
|
196
|
+
}
|
|
197
|
+
throw(e) {
|
|
198
|
+
if (this.sdkMessages.throw) {
|
|
199
|
+
return this.sdkMessages.throw(e);
|
|
200
|
+
}
|
|
201
|
+
return Promise.reject(e);
|
|
202
|
+
}
|
|
203
|
+
[Symbol.asyncIterator]() {
|
|
204
|
+
return this.sdkMessages;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Read messages from Claude process stdout
|
|
208
|
+
*/
|
|
209
|
+
async readMessages() {
|
|
210
|
+
const rl = node_readline.createInterface({ input: this.childStdout });
|
|
211
|
+
try {
|
|
212
|
+
for await (const line of rl) {
|
|
213
|
+
if (line.trim()) {
|
|
214
|
+
const message = JSON.parse(line);
|
|
215
|
+
if (message.type === "control_response") {
|
|
216
|
+
const controlResponse = message;
|
|
217
|
+
const handler = this.pendingControlResponses.get(controlResponse.response.request_id);
|
|
218
|
+
if (handler) {
|
|
219
|
+
handler(controlResponse.response);
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
this.inputStream.enqueue(message);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
await this.processExitPromise;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
this.inputStream.error(error);
|
|
229
|
+
} finally {
|
|
230
|
+
this.inputStream.done();
|
|
231
|
+
rl.close();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Async generator for SDK messages
|
|
236
|
+
*/
|
|
237
|
+
async *readSdkMessages() {
|
|
238
|
+
for await (const message of this.inputStream) {
|
|
239
|
+
yield message;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Send interrupt request to Claude
|
|
244
|
+
*/
|
|
245
|
+
async interrupt() {
|
|
246
|
+
if (!this.childStdin) {
|
|
247
|
+
throw new Error("Interrupt requires --input-format stream-json");
|
|
248
|
+
}
|
|
249
|
+
await this.request({
|
|
250
|
+
subtype: "interrupt"
|
|
251
|
+
}, this.childStdin);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Send control request to Claude process
|
|
255
|
+
*/
|
|
256
|
+
request(request, childStdin) {
|
|
257
|
+
const requestId = Math.random().toString(36).substring(2, 15);
|
|
258
|
+
const sdkRequest = {
|
|
259
|
+
request_id: requestId,
|
|
260
|
+
type: "control_request",
|
|
261
|
+
request
|
|
262
|
+
};
|
|
263
|
+
return new Promise((resolve, reject) => {
|
|
264
|
+
this.pendingControlResponses.set(requestId, (response) => {
|
|
265
|
+
if (response.subtype === "success") {
|
|
266
|
+
resolve(response);
|
|
267
|
+
} else {
|
|
268
|
+
reject(new Error(response.error));
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
childStdin.write(JSON.stringify(sdkRequest) + "\n");
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function query(config) {
|
|
276
|
+
const {
|
|
277
|
+
prompt,
|
|
278
|
+
abortController = config.abortController || new AbortController(),
|
|
279
|
+
options: {
|
|
280
|
+
allowedTools = [],
|
|
281
|
+
appendSystemPrompt,
|
|
282
|
+
customSystemPrompt,
|
|
283
|
+
cwd,
|
|
284
|
+
disallowedTools = [],
|
|
285
|
+
executable = "node",
|
|
286
|
+
executableArgs = [],
|
|
287
|
+
maxTurns,
|
|
288
|
+
mcpServers,
|
|
289
|
+
pathToClaudeCodeExecutable = getDefaultClaudeCodePath(),
|
|
290
|
+
permissionMode = "default",
|
|
291
|
+
permissionPromptToolName,
|
|
292
|
+
continue: continueConversation,
|
|
293
|
+
resume,
|
|
294
|
+
model,
|
|
295
|
+
fallbackModel,
|
|
296
|
+
strictMcpConfig
|
|
297
|
+
} = {}
|
|
298
|
+
} = config;
|
|
299
|
+
if (!process.env.CLAUDE_CODE_ENTRYPOINT) {
|
|
300
|
+
process.env.CLAUDE_CODE_ENTRYPOINT = "sdk-ts";
|
|
301
|
+
}
|
|
302
|
+
const args = ["--output-format", "stream-json", "--verbose"];
|
|
303
|
+
if (customSystemPrompt) args.push("--system-prompt", customSystemPrompt);
|
|
304
|
+
if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
|
|
305
|
+
if (maxTurns) args.push("--max-turns", maxTurns.toString());
|
|
306
|
+
if (model) args.push("--model", model);
|
|
307
|
+
if (permissionPromptToolName) args.push("--permission-prompt-tool", permissionPromptToolName);
|
|
308
|
+
if (continueConversation) args.push("--continue");
|
|
309
|
+
if (resume) args.push("--resume", resume);
|
|
310
|
+
if (allowedTools.length > 0) args.push("--allowedTools", allowedTools.join(","));
|
|
311
|
+
if (disallowedTools.length > 0) args.push("--disallowedTools", disallowedTools.join(","));
|
|
312
|
+
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
313
|
+
args.push("--mcp-config", JSON.stringify({ mcpServers }));
|
|
314
|
+
}
|
|
315
|
+
if (strictMcpConfig) args.push("--strict-mcp-config");
|
|
316
|
+
if (permissionMode) args.push("--permission-mode", permissionMode);
|
|
317
|
+
if (fallbackModel) {
|
|
318
|
+
if (model && fallbackModel === model) {
|
|
319
|
+
throw new Error("Fallback model cannot be the same as the main model. Please specify a different model for fallbackModel option.");
|
|
320
|
+
}
|
|
321
|
+
args.push("--fallback-model", fallbackModel);
|
|
322
|
+
}
|
|
323
|
+
if (typeof prompt === "string") {
|
|
324
|
+
args.push("--print", prompt.trim());
|
|
325
|
+
} else {
|
|
326
|
+
args.push("--input-format", "stream-json");
|
|
327
|
+
}
|
|
328
|
+
if (!node_fs.existsSync(pathToClaudeCodeExecutable)) {
|
|
329
|
+
throw new ReferenceError(`Claude Code executable not found at ${pathToClaudeCodeExecutable}. Is options.pathToClaudeCodeExecutable set?`);
|
|
330
|
+
}
|
|
331
|
+
logDebug(`Spawning Claude Code process: ${executable} ${[...executableArgs, pathToClaudeCodeExecutable, ...args].join(" ")}`);
|
|
332
|
+
const child = node_child_process.spawn(executable, [...executableArgs, pathToClaudeCodeExecutable, ...args], {
|
|
333
|
+
cwd,
|
|
334
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
335
|
+
signal: abortController.signal,
|
|
336
|
+
env: {
|
|
337
|
+
...process.env
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
let childStdin = null;
|
|
341
|
+
if (typeof prompt === "string") {
|
|
342
|
+
child.stdin.end();
|
|
343
|
+
} else {
|
|
344
|
+
streamToStdin(prompt, child.stdin, abortController);
|
|
345
|
+
childStdin = child.stdin;
|
|
346
|
+
}
|
|
347
|
+
if (process.env.DEBUG) {
|
|
348
|
+
child.stderr.on("data", (data) => {
|
|
349
|
+
console.error("Claude Code stderr:", data.toString());
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
const cleanup = () => {
|
|
353
|
+
if (!child.killed) {
|
|
354
|
+
child.kill("SIGTERM");
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
abortController.signal.addEventListener("abort", cleanup);
|
|
358
|
+
process.on("exit", cleanup);
|
|
359
|
+
const processExitPromise = new Promise((resolve) => {
|
|
360
|
+
child.on("close", (code) => {
|
|
361
|
+
if (abortController.signal.aborted) {
|
|
362
|
+
query2.setError(new AbortError("Claude Code process aborted by user"));
|
|
363
|
+
}
|
|
364
|
+
if (code !== 0) {
|
|
365
|
+
query2.setError(new Error(`Claude Code process exited with code ${code}`));
|
|
366
|
+
} else {
|
|
367
|
+
resolve();
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
const query2 = new Query(childStdin, child.stdout, processExitPromise);
|
|
372
|
+
child.on("error", (error) => {
|
|
373
|
+
if (abortController.signal.aborted) {
|
|
374
|
+
query2.setError(new AbortError("Claude Code process aborted by user"));
|
|
375
|
+
} else {
|
|
376
|
+
query2.setError(new Error(`Failed to spawn Claude Code process: ${error.message}`));
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
processExitPromise.finally(() => {
|
|
380
|
+
cleanup();
|
|
381
|
+
abortController.signal.removeEventListener("abort", cleanup);
|
|
382
|
+
if (process.env.CLAUDE_SDK_MCP_SERVERS) {
|
|
383
|
+
delete process.env.CLAUDE_SDK_MCP_SERVERS;
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
return query2;
|
|
387
|
+
}
|
|
388
|
+
|
|
54
389
|
function formatClaudeMessage(message, onAssistantResult) {
|
|
55
390
|
types.logger.debugLargeJson("[CLAUDE] Message from non interactive & remote mode:", message);
|
|
56
391
|
switch (message.type) {
|
|
@@ -160,9 +495,8 @@ function formatClaudeMessage(message, onAssistantResult) {
|
|
|
160
495
|
break;
|
|
161
496
|
}
|
|
162
497
|
default: {
|
|
163
|
-
const exhaustiveCheck = message;
|
|
164
498
|
if (process.env.DEBUG) {
|
|
165
|
-
console.log(chalk.gray(`[Unknown message type]`)
|
|
499
|
+
console.log(chalk.gray(`[Unknown message type: ${message.type}]`));
|
|
166
500
|
}
|
|
167
501
|
}
|
|
168
502
|
}
|
|
@@ -208,6 +542,19 @@ async function awaitFileExist(file, timeout = 1e4) {
|
|
|
208
542
|
return false;
|
|
209
543
|
}
|
|
210
544
|
|
|
545
|
+
function deepEqual(a, b) {
|
|
546
|
+
if (a === b) return true;
|
|
547
|
+
if (a == null || b == null) return false;
|
|
548
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
549
|
+
const keysA = Object.keys(a);
|
|
550
|
+
const keysB = Object.keys(b);
|
|
551
|
+
if (keysA.length !== keysB.length) return false;
|
|
552
|
+
for (const key of keysA) {
|
|
553
|
+
if (!keysB.includes(key)) return false;
|
|
554
|
+
if (!deepEqual(a[key], b[key])) return false;
|
|
555
|
+
}
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
211
558
|
async function claudeRemote(opts) {
|
|
212
559
|
let startFrom = opts.sessionId;
|
|
213
560
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
@@ -224,6 +571,7 @@ async function claudeRemote(opts) {
|
|
|
224
571
|
resume: startFrom ?? void 0,
|
|
225
572
|
mcpServers: opts.mcpServers,
|
|
226
573
|
permissionPromptToolName: opts.permissionPromptToolName,
|
|
574
|
+
permissionMode: opts.permissionMode,
|
|
227
575
|
executable: "node",
|
|
228
576
|
abortController
|
|
229
577
|
};
|
|
@@ -238,7 +586,7 @@ async function claudeRemote(opts) {
|
|
|
238
586
|
if (response) {
|
|
239
587
|
(async () => {
|
|
240
588
|
try {
|
|
241
|
-
|
|
589
|
+
await response.interrupt();
|
|
242
590
|
} catch (e) {
|
|
243
591
|
}
|
|
244
592
|
abortController.abort();
|
|
@@ -248,10 +596,9 @@ async function claudeRemote(opts) {
|
|
|
248
596
|
}
|
|
249
597
|
}
|
|
250
598
|
});
|
|
251
|
-
types.logger.debug(`[claudeRemote] Starting query with
|
|
252
|
-
response =
|
|
253
|
-
prompt: opts.
|
|
254
|
-
abortController,
|
|
599
|
+
types.logger.debug(`[claudeRemote] Starting query with permission mode: ${opts.permissionMode}`);
|
|
600
|
+
response = query({
|
|
601
|
+
prompt: opts.message,
|
|
255
602
|
options: sdkOptions
|
|
256
603
|
});
|
|
257
604
|
if (opts.interruptController) {
|
|
@@ -261,17 +608,86 @@ async function claudeRemote(opts) {
|
|
|
261
608
|
});
|
|
262
609
|
}
|
|
263
610
|
printDivider();
|
|
611
|
+
let thinking = false;
|
|
612
|
+
const updateThinking = (newThinking) => {
|
|
613
|
+
if (thinking !== newThinking) {
|
|
614
|
+
thinking = newThinking;
|
|
615
|
+
types.logger.debug(`[claudeRemote] Thinking state changed to: ${thinking}`);
|
|
616
|
+
if (opts.onThinkingChange) {
|
|
617
|
+
opts.onThinkingChange(thinking);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
const toolCalls = [];
|
|
622
|
+
const resolveToolCallId = (name, args) => {
|
|
623
|
+
for (let i = toolCalls.length - 1; i >= 0; i--) {
|
|
624
|
+
const call = toolCalls[i];
|
|
625
|
+
if (call.name === name && deepEqual(call.input, args)) {
|
|
626
|
+
if (call.used) {
|
|
627
|
+
types.logger.debug("[claudeRemote] Warning: Permission request matched an already-used tool call");
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
call.used = true;
|
|
631
|
+
types.logger.debug(`[claudeRemote] Resolved tool call ID: ${call.id} for ${name}`);
|
|
632
|
+
return call.id;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
types.logger.debug(`[claudeRemote] No matching tool call found for permission request: ${name}`);
|
|
636
|
+
return null;
|
|
637
|
+
};
|
|
638
|
+
if (opts.onToolCallResolver) {
|
|
639
|
+
opts.onToolCallResolver(resolveToolCallId);
|
|
640
|
+
}
|
|
264
641
|
try {
|
|
265
642
|
types.logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
266
643
|
for await (const message of response) {
|
|
267
644
|
types.logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
|
|
268
645
|
formatClaudeMessage(message, opts.onAssistantResult);
|
|
646
|
+
if (message.type === "assistant") {
|
|
647
|
+
const assistantMsg = message;
|
|
648
|
+
if (assistantMsg.message && assistantMsg.message.content) {
|
|
649
|
+
for (const block of assistantMsg.message.content) {
|
|
650
|
+
if (block.type === "tool_use") {
|
|
651
|
+
toolCalls.push({
|
|
652
|
+
id: block.id,
|
|
653
|
+
name: block.name,
|
|
654
|
+
input: block.input,
|
|
655
|
+
used: false
|
|
656
|
+
});
|
|
657
|
+
types.logger.debug(`[claudeRemote] Tracked tool call: ${block.id} - ${block.name}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (message.type === "user") {
|
|
663
|
+
const userMsg = message;
|
|
664
|
+
if (userMsg.message && userMsg.message.content && Array.isArray(userMsg.message.content)) {
|
|
665
|
+
for (const block of userMsg.message.content) {
|
|
666
|
+
if (block.type === "tool_result" && block.tool_use_id) {
|
|
667
|
+
const toolCall = toolCalls.find((tc) => tc.id === block.tool_use_id);
|
|
668
|
+
if (toolCall && !toolCall.used) {
|
|
669
|
+
toolCall.used = true;
|
|
670
|
+
types.logger.debug(`[claudeRemote] Tool completed execution, marked as used: ${block.tool_use_id}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
269
676
|
if (message.type === "system" && message.subtype === "init") {
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
677
|
+
updateThinking(true);
|
|
678
|
+
const systemInit = message;
|
|
679
|
+
if (systemInit.session_id) {
|
|
680
|
+
types.logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${systemInit.session_id}`);
|
|
681
|
+
const projectDir = getProjectPath(opts.path);
|
|
682
|
+
const found = await awaitFileExist(node_path.join(projectDir, `${systemInit.session_id}.jsonl`));
|
|
683
|
+
types.logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
|
|
684
|
+
opts.onSessionFound(systemInit.session_id);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (message.type === "result") {
|
|
688
|
+
updateThinking(false);
|
|
689
|
+
types.logger.debug("[claudeRemote] Result received, exiting claudeRemote");
|
|
690
|
+
break;
|
|
275
691
|
}
|
|
276
692
|
}
|
|
277
693
|
types.logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
@@ -279,12 +695,17 @@ async function claudeRemote(opts) {
|
|
|
279
695
|
if (abortController.signal.aborted) {
|
|
280
696
|
types.logger.debug(`[claudeRemote] Aborted`);
|
|
281
697
|
}
|
|
282
|
-
if (e instanceof
|
|
698
|
+
if (e instanceof AbortError) {
|
|
283
699
|
types.logger.debug(`[claudeRemote] Aborted`);
|
|
284
700
|
} else {
|
|
285
701
|
throw e;
|
|
286
702
|
}
|
|
287
703
|
} finally {
|
|
704
|
+
updateThinking(false);
|
|
705
|
+
toolCalls.length = 0;
|
|
706
|
+
if (opts.onToolCallResolver) {
|
|
707
|
+
opts.onToolCallResolver(null);
|
|
708
|
+
}
|
|
288
709
|
if (opts.interruptController) {
|
|
289
710
|
opts.interruptController.unregister();
|
|
290
711
|
}
|
|
@@ -348,22 +769,70 @@ async function claudeLocal(opts) {
|
|
|
348
769
|
input: child.stdio[3],
|
|
349
770
|
crlfDelay: Infinity
|
|
350
771
|
});
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
772
|
+
const activeFetches = /* @__PURE__ */ new Map();
|
|
773
|
+
let thinking = false;
|
|
774
|
+
let stopThinkingTimeout = null;
|
|
775
|
+
const updateThinking = (newThinking) => {
|
|
776
|
+
if (thinking !== newThinking) {
|
|
777
|
+
thinking = newThinking;
|
|
778
|
+
types.logger.debug(`[ClaudeLocal] Thinking state changed to: ${thinking}`);
|
|
779
|
+
if (opts.onThinkingChange) {
|
|
780
|
+
opts.onThinkingChange(thinking);
|
|
357
781
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
rl.on("line", (line) => {
|
|
785
|
+
try {
|
|
786
|
+
const message = JSON.parse(line);
|
|
787
|
+
switch (message.type) {
|
|
788
|
+
case "uuid":
|
|
789
|
+
detectedIdsRandomUUID.add(message.value);
|
|
790
|
+
if (!resolvedSessionId && detectedIdsFileSystem.has(message.value)) {
|
|
791
|
+
resolvedSessionId = message.value;
|
|
792
|
+
opts.onSessionFound(message.value);
|
|
793
|
+
}
|
|
794
|
+
break;
|
|
795
|
+
case "fetch-start":
|
|
796
|
+
types.logger.debug(`[ClaudeLocal] Fetch start: ${message.method} ${message.hostname}${message.path} (id: ${message.id})`);
|
|
797
|
+
activeFetches.set(message.id, {
|
|
798
|
+
hostname: message.hostname,
|
|
799
|
+
path: message.path,
|
|
800
|
+
startTime: message.timestamp
|
|
801
|
+
});
|
|
802
|
+
if (stopThinkingTimeout) {
|
|
803
|
+
clearTimeout(stopThinkingTimeout);
|
|
804
|
+
stopThinkingTimeout = null;
|
|
805
|
+
}
|
|
806
|
+
updateThinking(true);
|
|
807
|
+
break;
|
|
808
|
+
case "fetch-end":
|
|
809
|
+
types.logger.debug(`[ClaudeLocal] Fetch end: id ${message.id}`);
|
|
810
|
+
activeFetches.delete(message.id);
|
|
811
|
+
if (activeFetches.size === 0 && thinking && !stopThinkingTimeout) {
|
|
812
|
+
stopThinkingTimeout = setTimeout(() => {
|
|
813
|
+
if (activeFetches.size === 0) {
|
|
814
|
+
updateThinking(false);
|
|
815
|
+
}
|
|
816
|
+
stopThinkingTimeout = null;
|
|
817
|
+
}, 500);
|
|
818
|
+
}
|
|
819
|
+
break;
|
|
820
|
+
default:
|
|
821
|
+
types.logger.debug(`[ClaudeLocal] Unknown message type: ${message.type}`);
|
|
361
822
|
}
|
|
823
|
+
} catch (e) {
|
|
824
|
+
types.logger.debug(`[ClaudeLocal] Non-JSON line from fd3: ${line}`);
|
|
362
825
|
}
|
|
363
826
|
});
|
|
364
827
|
rl.on("error", (err) => {
|
|
365
828
|
console.error("Error reading from fd 3:", err);
|
|
366
829
|
});
|
|
830
|
+
child.on("exit", () => {
|
|
831
|
+
if (stopThinkingTimeout) {
|
|
832
|
+
clearTimeout(stopThinkingTimeout);
|
|
833
|
+
}
|
|
834
|
+
updateThinking(false);
|
|
835
|
+
});
|
|
367
836
|
}
|
|
368
837
|
child.on("error", (error) => {
|
|
369
838
|
});
|
|
@@ -384,58 +853,69 @@ async function claudeLocal(opts) {
|
|
|
384
853
|
return resolvedSessionId;
|
|
385
854
|
}
|
|
386
855
|
|
|
387
|
-
class
|
|
856
|
+
class MessageQueue2 {
|
|
857
|
+
constructor(modeHasher) {
|
|
858
|
+
this.modeHasher = modeHasher;
|
|
859
|
+
types.logger.debug(`[MessageQueue2] Initialized`);
|
|
860
|
+
}
|
|
388
861
|
queue = [];
|
|
389
|
-
|
|
862
|
+
waiter = null;
|
|
390
863
|
closed = false;
|
|
391
|
-
closePromise;
|
|
392
|
-
closeResolve;
|
|
393
|
-
constructor() {
|
|
394
|
-
this.closePromise = new Promise((resolve) => {
|
|
395
|
-
this.closeResolve = resolve;
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
864
|
/**
|
|
399
|
-
* Push a message to the queue
|
|
865
|
+
* Push a message to the queue with a mode.
|
|
400
866
|
*/
|
|
401
|
-
push(message) {
|
|
867
|
+
push(message, mode) {
|
|
402
868
|
if (this.closed) {
|
|
403
869
|
throw new Error("Cannot push to closed queue");
|
|
404
870
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
});
|
|
418
|
-
} else {
|
|
419
|
-
types.logger.debug(`[MessageQueue] No waiter found. Adding to queue: "${message}"`);
|
|
420
|
-
this.queue.push({
|
|
421
|
-
type: "user",
|
|
422
|
-
message: {
|
|
423
|
-
role: "user",
|
|
424
|
-
content: message
|
|
425
|
-
},
|
|
426
|
-
parent_tool_use_id: null,
|
|
427
|
-
session_id: ""
|
|
428
|
-
});
|
|
871
|
+
const modeHash = this.modeHasher(mode);
|
|
872
|
+
types.logger.debug(`[MessageQueue2] push() called with mode hash: ${modeHash}`);
|
|
873
|
+
this.queue.push({
|
|
874
|
+
message,
|
|
875
|
+
mode,
|
|
876
|
+
modeHash
|
|
877
|
+
});
|
|
878
|
+
if (this.waiter) {
|
|
879
|
+
types.logger.debug(`[MessageQueue2] Notifying waiter`);
|
|
880
|
+
const waiter = this.waiter;
|
|
881
|
+
this.waiter = null;
|
|
882
|
+
waiter(true);
|
|
429
883
|
}
|
|
430
|
-
types.logger.debug(`[
|
|
884
|
+
types.logger.debug(`[MessageQueue2] push() completed. Queue size: ${this.queue.length}`);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Push a message to the beginning of the queue with a mode.
|
|
888
|
+
*/
|
|
889
|
+
unshift(message, mode) {
|
|
890
|
+
if (this.closed) {
|
|
891
|
+
throw new Error("Cannot unshift to closed queue");
|
|
892
|
+
}
|
|
893
|
+
const modeHash = this.modeHasher(mode);
|
|
894
|
+
types.logger.debug(`[MessageQueue2] unshift() called with mode hash: ${modeHash}`);
|
|
895
|
+
this.queue.unshift({
|
|
896
|
+
message,
|
|
897
|
+
mode,
|
|
898
|
+
modeHash
|
|
899
|
+
});
|
|
900
|
+
if (this.waiter) {
|
|
901
|
+
types.logger.debug(`[MessageQueue2] Notifying waiter`);
|
|
902
|
+
const waiter = this.waiter;
|
|
903
|
+
this.waiter = null;
|
|
904
|
+
waiter(true);
|
|
905
|
+
}
|
|
906
|
+
types.logger.debug(`[MessageQueue2] unshift() completed. Queue size: ${this.queue.length}`);
|
|
431
907
|
}
|
|
432
908
|
/**
|
|
433
909
|
* Close the queue - no more messages can be pushed
|
|
434
910
|
*/
|
|
435
911
|
close() {
|
|
436
|
-
types.logger.debug(`[
|
|
912
|
+
types.logger.debug(`[MessageQueue2] close() called`);
|
|
437
913
|
this.closed = true;
|
|
438
|
-
this.
|
|
914
|
+
if (this.waiter) {
|
|
915
|
+
const waiter = this.waiter;
|
|
916
|
+
this.waiter = null;
|
|
917
|
+
waiter(false);
|
|
918
|
+
}
|
|
439
919
|
}
|
|
440
920
|
/**
|
|
441
921
|
* Check if the queue is closed
|
|
@@ -450,56 +930,91 @@ class MessageQueue {
|
|
|
450
930
|
return this.queue.length;
|
|
451
931
|
}
|
|
452
932
|
/**
|
|
453
|
-
*
|
|
933
|
+
* Wait for messages and return all messages with the same mode as a single string
|
|
934
|
+
* Returns { message: string, mode: T } or null if aborted/closed
|
|
454
935
|
*/
|
|
455
|
-
async
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
types.logger.debug(`[MessageQueue] Iterator yielding waited message`);
|
|
475
|
-
yield nextMessage;
|
|
936
|
+
async waitForMessagesAndGetAsString(abortSignal) {
|
|
937
|
+
if (this.queue.length > 0) {
|
|
938
|
+
return this.collectBatch();
|
|
939
|
+
}
|
|
940
|
+
if (this.closed || abortSignal?.aborted) {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
const hasMessages = await this.waitForMessages(abortSignal);
|
|
944
|
+
if (!hasMessages) {
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
return this.collectBatch();
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Collect a batch of messages with the same mode
|
|
951
|
+
*/
|
|
952
|
+
collectBatch() {
|
|
953
|
+
if (this.queue.length === 0) {
|
|
954
|
+
return null;
|
|
476
955
|
}
|
|
956
|
+
const firstItem = this.queue[0];
|
|
957
|
+
const sameModeMessages = [];
|
|
958
|
+
let mode = firstItem.mode;
|
|
959
|
+
const targetModeHash = firstItem.modeHash;
|
|
960
|
+
while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash) {
|
|
961
|
+
const item = this.queue.shift();
|
|
962
|
+
sameModeMessages.push(item.message);
|
|
963
|
+
}
|
|
964
|
+
const combinedMessage = sameModeMessages.join("\n");
|
|
965
|
+
types.logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
|
|
966
|
+
return {
|
|
967
|
+
message: combinedMessage,
|
|
968
|
+
mode
|
|
969
|
+
};
|
|
477
970
|
}
|
|
478
971
|
/**
|
|
479
|
-
* Wait for
|
|
972
|
+
* Wait for messages to arrive
|
|
480
973
|
*/
|
|
481
|
-
|
|
974
|
+
waitForMessages(abortSignal) {
|
|
482
975
|
return new Promise((resolve) => {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
976
|
+
let abortHandler = null;
|
|
977
|
+
if (abortSignal) {
|
|
978
|
+
abortHandler = () => {
|
|
979
|
+
types.logger.debug("[MessageQueue2] Wait aborted");
|
|
980
|
+
if (this.waiter === waiterFunc) {
|
|
981
|
+
this.waiter = null;
|
|
982
|
+
}
|
|
983
|
+
resolve(false);
|
|
984
|
+
};
|
|
985
|
+
abortSignal.addEventListener("abort", abortHandler);
|
|
986
|
+
}
|
|
987
|
+
const waiterFunc = (hasMessages) => {
|
|
988
|
+
if (abortHandler && abortSignal) {
|
|
989
|
+
abortSignal.removeEventListener("abort", abortHandler);
|
|
990
|
+
}
|
|
991
|
+
resolve(hasMessages);
|
|
992
|
+
};
|
|
993
|
+
if (this.queue.length > 0) {
|
|
994
|
+
if (abortHandler && abortSignal) {
|
|
995
|
+
abortSignal.removeEventListener("abort", abortHandler);
|
|
996
|
+
}
|
|
997
|
+
resolve(true);
|
|
486
998
|
return;
|
|
487
999
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
this.closePromise?.then(() => {
|
|
492
|
-
const index = this.waiters.indexOf(waiter);
|
|
493
|
-
if (index !== -1) {
|
|
494
|
-
this.waiters.splice(index, 1);
|
|
495
|
-
types.logger.debug(`[MessageQueue] waitForNext() waiter removed due to close. Remaining waiters: ${this.waiters.length}`);
|
|
496
|
-
resolve(void 0);
|
|
1000
|
+
if (this.closed || abortSignal?.aborted) {
|
|
1001
|
+
if (abortHandler && abortSignal) {
|
|
1002
|
+
abortSignal.removeEventListener("abort", abortHandler);
|
|
497
1003
|
}
|
|
498
|
-
|
|
1004
|
+
resolve(false);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
this.waiter = waiterFunc;
|
|
1008
|
+
types.logger.debug("[MessageQueue2] Waiting for messages...");
|
|
499
1009
|
});
|
|
500
1010
|
}
|
|
501
1011
|
}
|
|
502
1012
|
|
|
1013
|
+
var MessageQueue2$1 = /*#__PURE__*/Object.freeze({
|
|
1014
|
+
__proto__: null,
|
|
1015
|
+
MessageQueue2: MessageQueue2
|
|
1016
|
+
});
|
|
1017
|
+
|
|
503
1018
|
class InvalidateSync {
|
|
504
1019
|
_invalidated = false;
|
|
505
1020
|
_invalidatedDouble = false;
|
|
@@ -594,6 +1109,39 @@ function startFileWatcher(file, onFileChange) {
|
|
|
594
1109
|
};
|
|
595
1110
|
}
|
|
596
1111
|
|
|
1112
|
+
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.`;
|
|
1113
|
+
const PLAN_FAKE_RESTART = `PlEaZe Continue with plan.`;
|
|
1114
|
+
|
|
1115
|
+
function hackToolResponse(message) {
|
|
1116
|
+
console.log("hackToolResponse", JSON.stringify(message, null, 2));
|
|
1117
|
+
if (message.type === "user" && message.message?.role === "user" && message.message?.content && Array.isArray(message.message.content)) {
|
|
1118
|
+
let modified = false;
|
|
1119
|
+
const hackedContent = message.message.content.map((item) => {
|
|
1120
|
+
if (item.type === "tool_result" && item.is_error === true) {
|
|
1121
|
+
if (item.content === PLAN_FAKE_REJECT) {
|
|
1122
|
+
types.logger.debug(`[SESSION_SCANNER] Hacking exit_plan_mode tool_result: flipping is_error from true to false`);
|
|
1123
|
+
modified = true;
|
|
1124
|
+
return {
|
|
1125
|
+
...item,
|
|
1126
|
+
is_error: false,
|
|
1127
|
+
content: "Plan approved"
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return item;
|
|
1132
|
+
});
|
|
1133
|
+
if (modified) {
|
|
1134
|
+
return {
|
|
1135
|
+
...message,
|
|
1136
|
+
message: {
|
|
1137
|
+
...message.message,
|
|
1138
|
+
content: hackedContent
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return message;
|
|
1144
|
+
}
|
|
597
1145
|
function createSessionScanner(opts) {
|
|
598
1146
|
const projectDir = getProjectPath(opts.workingDirectory);
|
|
599
1147
|
let finishedSessions = /* @__PURE__ */ new Set();
|
|
@@ -646,7 +1194,8 @@ function createSessionScanner(opts) {
|
|
|
646
1194
|
continue;
|
|
647
1195
|
}
|
|
648
1196
|
}
|
|
649
|
-
|
|
1197
|
+
const hackedMessage = hackToolResponse(message);
|
|
1198
|
+
opts.onMessage(hackedMessage);
|
|
650
1199
|
} catch (e) {
|
|
651
1200
|
types.logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
|
|
652
1201
|
continue;
|
|
@@ -746,10 +1295,15 @@ function sortKeys(value) {
|
|
|
746
1295
|
|
|
747
1296
|
async function loop(opts) {
|
|
748
1297
|
let mode = opts.startingMode ?? "local";
|
|
749
|
-
let
|
|
1298
|
+
let currentPermissionMode = opts.permissionMode ?? "default";
|
|
1299
|
+
types.logger.debug(`[loop] Starting with permission mode: ${currentPermissionMode}`);
|
|
1300
|
+
let currentMessageQueue = opts.messageQueue || new MessageQueue2(
|
|
1301
|
+
(mode2) => mode2
|
|
1302
|
+
// Simple string hasher since modes are already strings
|
|
1303
|
+
);
|
|
750
1304
|
let sessionId = null;
|
|
751
1305
|
let onMessage = null;
|
|
752
|
-
const sessionScanner = createSessionScanner({
|
|
1306
|
+
const sessionScanner = opts.sessionScanner || createSessionScanner({
|
|
753
1307
|
workingDirectory: opts.path,
|
|
754
1308
|
onMessage: (message) => {
|
|
755
1309
|
opts.session.sendClaudeSessionMessage(message);
|
|
@@ -757,7 +1311,20 @@ async function loop(opts) {
|
|
|
757
1311
|
});
|
|
758
1312
|
opts.session.onUserMessage((message) => {
|
|
759
1313
|
sessionScanner.onRemoteUserMessageForDeduplication(message.content.text);
|
|
760
|
-
|
|
1314
|
+
let messagePermissionMode = currentPermissionMode;
|
|
1315
|
+
if (message.meta?.permissionMode) {
|
|
1316
|
+
const validModes = ["default", "acceptEdits", "bypassPermissions", "plan"];
|
|
1317
|
+
if (validModes.includes(message.meta.permissionMode)) {
|
|
1318
|
+
messagePermissionMode = message.meta.permissionMode;
|
|
1319
|
+
currentPermissionMode = messagePermissionMode;
|
|
1320
|
+
types.logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
|
|
1321
|
+
} else {
|
|
1322
|
+
types.logger.info(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
|
|
1323
|
+
}
|
|
1324
|
+
} else {
|
|
1325
|
+
types.logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
|
|
1326
|
+
}
|
|
1327
|
+
currentMessageQueue.push(message.content.text, messagePermissionMode);
|
|
761
1328
|
types.logger.debugLargeJson("User message pushed to queue:", message);
|
|
762
1329
|
if (onMessage) {
|
|
763
1330
|
onMessage();
|
|
@@ -768,6 +1335,7 @@ async function loop(opts) {
|
|
|
768
1335
|
sessionScanner.onNewSession(newSessionId);
|
|
769
1336
|
};
|
|
770
1337
|
while (true) {
|
|
1338
|
+
types.logger.debug(`[loop] Starting loop iteration, queue size: ${currentMessageQueue.size()}, mode: ${mode}`);
|
|
771
1339
|
if (currentMessageQueue.size() > 0) {
|
|
772
1340
|
if (mode !== "remote") {
|
|
773
1341
|
mode = "remote";
|
|
@@ -775,7 +1343,6 @@ async function loop(opts) {
|
|
|
775
1343
|
opts.onModeChange(mode);
|
|
776
1344
|
}
|
|
777
1345
|
}
|
|
778
|
-
continue;
|
|
779
1346
|
}
|
|
780
1347
|
if (mode === "local") {
|
|
781
1348
|
let abortedOutside = false;
|
|
@@ -819,6 +1386,7 @@ async function loop(opts) {
|
|
|
819
1386
|
path: opts.path,
|
|
820
1387
|
sessionId,
|
|
821
1388
|
onSessionFound,
|
|
1389
|
+
onThinkingChange: opts.onThinkingChange,
|
|
822
1390
|
abort: interactiveAbortController.signal,
|
|
823
1391
|
claudeEnvVars: opts.claudeEnvVars,
|
|
824
1392
|
claudeArgs: opts.claudeArgs
|
|
@@ -841,15 +1409,16 @@ async function loop(opts) {
|
|
|
841
1409
|
}
|
|
842
1410
|
}
|
|
843
1411
|
if (mode === "remote") {
|
|
1412
|
+
console.log("Starting remote mode...");
|
|
844
1413
|
types.logger.debug("Starting " + sessionId);
|
|
845
1414
|
const remoteAbortController = new AbortController();
|
|
846
1415
|
opts.session.setHandler("abort", () => {
|
|
847
|
-
if (!remoteAbortController.signal.aborted) {
|
|
1416
|
+
if (remoteAbortController && !remoteAbortController.signal.aborted) {
|
|
848
1417
|
remoteAbortController.abort();
|
|
849
1418
|
}
|
|
850
1419
|
});
|
|
851
1420
|
const abortHandler = () => {
|
|
852
|
-
if (!remoteAbortController.signal.aborted) {
|
|
1421
|
+
if (remoteAbortController && !remoteAbortController.signal.aborted) {
|
|
853
1422
|
if (mode !== "local") {
|
|
854
1423
|
mode = "local";
|
|
855
1424
|
if (opts.onModeChange) {
|
|
@@ -871,21 +1440,35 @@ async function loop(opts) {
|
|
|
871
1440
|
process.stdin.on("data", abortHandler);
|
|
872
1441
|
try {
|
|
873
1442
|
types.logger.debug(`Starting claudeRemote with messages: ${currentMessageQueue.size()}`);
|
|
1443
|
+
types.logger.debug("[loop] Waiting for messages before starting claudeRemote...");
|
|
1444
|
+
const messageData = await currentMessageQueue.waitForMessagesAndGetAsString(remoteAbortController.signal);
|
|
1445
|
+
if (!messageData) {
|
|
1446
|
+
console.log("[LOOP] No message received (queue closed or aborted), continuing loop");
|
|
1447
|
+
types.logger.debug("[loop] No message received (queue closed or aborted), skipping remote mode");
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
currentPermissionMode = messageData.mode;
|
|
1451
|
+
types.logger.debug(`[loop] Using permission mode from queue: ${currentPermissionMode}`);
|
|
874
1452
|
if (opts.onProcessStart) {
|
|
875
1453
|
opts.onProcessStart("remote");
|
|
876
1454
|
}
|
|
1455
|
+
opts.session.sendSessionEvent({ type: "permission-mode-changed", mode: currentPermissionMode });
|
|
1456
|
+
types.logger.debug(`[loop] Sent permission-mode-changed event to app: ${currentPermissionMode}`);
|
|
877
1457
|
await claudeRemote({
|
|
878
1458
|
abort: remoteAbortController.signal,
|
|
879
1459
|
sessionId,
|
|
880
1460
|
path: opts.path,
|
|
881
1461
|
mcpServers: opts.mcpServers,
|
|
882
1462
|
permissionPromptToolName: opts.permissionPromptToolName,
|
|
1463
|
+
permissionMode: currentPermissionMode,
|
|
883
1464
|
onSessionFound,
|
|
884
|
-
|
|
1465
|
+
onThinkingChange: opts.onThinkingChange,
|
|
1466
|
+
message: messageData.message,
|
|
885
1467
|
onAssistantResult: opts.onAssistantResult,
|
|
886
1468
|
interruptController: opts.interruptController,
|
|
887
1469
|
claudeEnvVars: opts.claudeEnvVars,
|
|
888
|
-
claudeArgs: opts.claudeArgs
|
|
1470
|
+
claudeArgs: opts.claudeArgs,
|
|
1471
|
+
onToolCallResolver: opts.onToolCallResolver
|
|
889
1472
|
});
|
|
890
1473
|
} catch (e) {
|
|
891
1474
|
if (!remoteAbortController.signal.aborted) {
|
|
@@ -899,8 +1482,6 @@ async function loop(opts) {
|
|
|
899
1482
|
if (process.stdin.isTTY) {
|
|
900
1483
|
process.stdin.setRawMode(false);
|
|
901
1484
|
}
|
|
902
|
-
currentMessageQueue.close();
|
|
903
|
-
currentMessageQueue = new MessageQueue();
|
|
904
1485
|
}
|
|
905
1486
|
if (mode !== "remote") {
|
|
906
1487
|
console.log("Switching back to good old claude...");
|
|
@@ -1009,7 +1590,7 @@ class InterruptController {
|
|
|
1009
1590
|
}
|
|
1010
1591
|
}
|
|
1011
1592
|
|
|
1012
|
-
var version = "0.
|
|
1593
|
+
var version = "0.2.0";
|
|
1013
1594
|
var packageJson = {
|
|
1014
1595
|
version: version};
|
|
1015
1596
|
|
|
@@ -1069,6 +1650,7 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
|
|
|
1069
1650
|
if (!request) return currentState;
|
|
1070
1651
|
let r = { ...currentState.requests };
|
|
1071
1652
|
delete r[id];
|
|
1653
|
+
const isExitPlanModeSuccess = request.tool === "exit_plan_mode" && !message.approved && message.reason === PLAN_FAKE_REJECT;
|
|
1072
1654
|
return {
|
|
1073
1655
|
...currentState,
|
|
1074
1656
|
requests: r,
|
|
@@ -1077,8 +1659,8 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
|
|
|
1077
1659
|
[id]: {
|
|
1078
1660
|
...request,
|
|
1079
1661
|
completedAt: Date.now(),
|
|
1080
|
-
status: message.approved ? "approved" : "denied",
|
|
1081
|
-
reason: message.reason
|
|
1662
|
+
status: isExitPlanModeSuccess ? "approved" : message.approved ? "approved" : "denied",
|
|
1663
|
+
reason: isExitPlanModeSuccess ? "Plan approved" : message.reason
|
|
1082
1664
|
}
|
|
1083
1665
|
}
|
|
1084
1666
|
};
|
|
@@ -1294,148 +1876,77 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
|
|
|
1294
1876
|
});
|
|
1295
1877
|
}
|
|
1296
1878
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
proxy.on("proxyReq", (proxyReq, req, res) => {
|
|
1311
|
-
if (options.onRequest) {
|
|
1312
|
-
options.onRequest(req, proxyReq);
|
|
1313
|
-
}
|
|
1314
|
-
});
|
|
1315
|
-
proxy.on("proxyRes", (proxyRes, req, res) => {
|
|
1316
|
-
if (options.onResponse) {
|
|
1317
|
-
options.onResponse(req, proxyRes);
|
|
1318
|
-
}
|
|
1319
|
-
});
|
|
1320
|
-
const server = node_http.createServer((req, res) => {
|
|
1321
|
-
proxy.web(req, res);
|
|
1322
|
-
});
|
|
1323
|
-
const url = await new Promise((resolve, reject) => {
|
|
1324
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1325
|
-
const addr = server.address();
|
|
1326
|
-
if (addr && typeof addr === "object") {
|
|
1327
|
-
const proxyUrl = `http://127.0.0.1:${addr.port}`;
|
|
1328
|
-
types.logger.debug(`[HTTPProxy] Started on ${proxyUrl} --> ${options.target}`);
|
|
1329
|
-
resolve(proxyUrl);
|
|
1330
|
-
} else {
|
|
1331
|
-
reject(new Error("Failed to get server address"));
|
|
1332
|
-
}
|
|
1333
|
-
});
|
|
1334
|
-
});
|
|
1335
|
-
return url;
|
|
1879
|
+
const defaultSettings = {
|
|
1880
|
+
onboardingCompleted: false
|
|
1881
|
+
};
|
|
1882
|
+
async function readSettings() {
|
|
1883
|
+
if (!node_fs.existsSync(types.configuration.settingsFile)) {
|
|
1884
|
+
return { ...defaultSettings };
|
|
1885
|
+
}
|
|
1886
|
+
try {
|
|
1887
|
+
const content = await promises$1.readFile(types.configuration.settingsFile, "utf8");
|
|
1888
|
+
return JSON.parse(content);
|
|
1889
|
+
} catch {
|
|
1890
|
+
return { ...defaultSettings };
|
|
1891
|
+
}
|
|
1336
1892
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
activeRequests.set(requestId, timeout);
|
|
1371
|
-
if (!isThinking) {
|
|
1372
|
-
isThinking = true;
|
|
1373
|
-
onThinking(true);
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
},
|
|
1377
|
-
onResponse: (req, proxyRes) => {
|
|
1378
|
-
proxyRes.headers["connection"] = "close";
|
|
1379
|
-
if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
|
|
1380
|
-
const requestId = req._requestId;
|
|
1381
|
-
const timeout = activeRequests.get(requestId);
|
|
1382
|
-
if (timeout) {
|
|
1383
|
-
clearTimeout(timeout);
|
|
1384
|
-
}
|
|
1385
|
-
let cleaned = false;
|
|
1386
|
-
const cleanupRequest = () => {
|
|
1387
|
-
if (!cleaned) {
|
|
1388
|
-
cleaned = true;
|
|
1389
|
-
activeRequests.delete(requestId);
|
|
1390
|
-
checkAndStopThinking();
|
|
1391
|
-
}
|
|
1392
|
-
};
|
|
1393
|
-
proxyRes.on("end", () => {
|
|
1394
|
-
cleanupRequest();
|
|
1395
|
-
});
|
|
1396
|
-
proxyRes.on("error", (err) => {
|
|
1397
|
-
cleanupRequest();
|
|
1398
|
-
});
|
|
1399
|
-
proxyRes.on("aborted", () => {
|
|
1400
|
-
cleanupRequest();
|
|
1401
|
-
});
|
|
1402
|
-
proxyRes.on("close", () => {
|
|
1403
|
-
cleanupRequest();
|
|
1404
|
-
});
|
|
1405
|
-
req.on("close", () => {
|
|
1406
|
-
cleanupRequest();
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
});
|
|
1411
|
-
const reset = () => {
|
|
1412
|
-
for (const [requestId, timeout] of activeRequests) {
|
|
1413
|
-
clearTimeout(timeout);
|
|
1414
|
-
}
|
|
1415
|
-
activeRequests.clear();
|
|
1416
|
-
if (stopThinkingTimeout) {
|
|
1417
|
-
clearTimeout(stopThinkingTimeout);
|
|
1418
|
-
stopThinkingTimeout = null;
|
|
1419
|
-
}
|
|
1420
|
-
if (isThinking) {
|
|
1421
|
-
isThinking = false;
|
|
1422
|
-
onThinking(false);
|
|
1423
|
-
}
|
|
1424
|
-
};
|
|
1425
|
-
return {
|
|
1426
|
-
proxyUrl,
|
|
1427
|
-
reset
|
|
1428
|
-
};
|
|
1893
|
+
async function writeSettings(settings) {
|
|
1894
|
+
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1895
|
+
await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1896
|
+
}
|
|
1897
|
+
await promises$1.writeFile(types.configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1898
|
+
}
|
|
1899
|
+
const credentialsSchema = z__namespace.object({
|
|
1900
|
+
secret: z__namespace.string().base64(),
|
|
1901
|
+
token: z__namespace.string()
|
|
1902
|
+
});
|
|
1903
|
+
async function readCredentials() {
|
|
1904
|
+
if (!node_fs.existsSync(types.configuration.privateKeyFile)) {
|
|
1905
|
+
return null;
|
|
1906
|
+
}
|
|
1907
|
+
try {
|
|
1908
|
+
const keyBase64 = await promises$1.readFile(types.configuration.privateKeyFile, "utf8");
|
|
1909
|
+
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
1910
|
+
return {
|
|
1911
|
+
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
1912
|
+
token: credentials.token
|
|
1913
|
+
};
|
|
1914
|
+
} catch {
|
|
1915
|
+
return null;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
async function writeCredentials(credentials) {
|
|
1919
|
+
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1920
|
+
await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1921
|
+
}
|
|
1922
|
+
await promises$1.writeFile(types.configuration.privateKeyFile, JSON.stringify({
|
|
1923
|
+
secret: types.encodeBase64(credentials.secret),
|
|
1924
|
+
token: credentials.token
|
|
1925
|
+
}, null, 2));
|
|
1429
1926
|
}
|
|
1430
1927
|
|
|
1431
1928
|
async function start(credentials, options = {}) {
|
|
1432
1929
|
const workingDirectory = process.cwd();
|
|
1433
1930
|
const sessionTag = node_crypto.randomUUID();
|
|
1931
|
+
if (options.daemonSpawn && options.startingMode === "local") {
|
|
1932
|
+
types.logger.debug("Daemon spawn requested with local mode - forcing remote mode");
|
|
1933
|
+
options.startingMode = "remote";
|
|
1934
|
+
}
|
|
1434
1935
|
const api = new types.ApiClient(credentials.token, credentials.secret);
|
|
1435
1936
|
let state = {};
|
|
1436
|
-
|
|
1937
|
+
const settings = await readSettings() || { };
|
|
1938
|
+
let metadata = {
|
|
1939
|
+
path: workingDirectory,
|
|
1940
|
+
host: os.hostname(),
|
|
1941
|
+
version: packageJson.version,
|
|
1942
|
+
os: os.platform(),
|
|
1943
|
+
machineId: settings.machineId
|
|
1944
|
+
};
|
|
1437
1945
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
1438
1946
|
types.logger.debug(`Session created: ${response.id}`);
|
|
1947
|
+
if (options.daemonSpawn) {
|
|
1948
|
+
console.log(`daemon:sessionIdCreated:${response.id}`);
|
|
1949
|
+
}
|
|
1439
1950
|
const session = api.session(response);
|
|
1440
1951
|
const pushClient = api.push();
|
|
1441
1952
|
let thinking = false;
|
|
@@ -1443,20 +1954,57 @@ async function start(credentials, options = {}) {
|
|
|
1443
1954
|
let pingInterval = setInterval(() => {
|
|
1444
1955
|
session.keepAlive(thinking, mode);
|
|
1445
1956
|
}, 2e3);
|
|
1446
|
-
const activityTracker = await startClaudeActivityTracker((newThinking) => {
|
|
1447
|
-
thinking = newThinking;
|
|
1448
|
-
session.keepAlive(thinking, mode);
|
|
1449
|
-
});
|
|
1450
|
-
process.env.ANTHROPIC_BASE_URL = activityTracker.proxyUrl;
|
|
1451
1957
|
const logPath = await types.logger.logFilePathPromise;
|
|
1452
1958
|
types.logger.infoDeveloper(`Session: ${response.id}`);
|
|
1453
1959
|
types.logger.infoDeveloper(`Logs: ${logPath}`);
|
|
1454
1960
|
const interruptController = new InterruptController();
|
|
1961
|
+
const { MessageQueue2 } = await Promise.resolve().then(function () { return MessageQueue2$1; });
|
|
1962
|
+
const messageQueue = new MessageQueue2(
|
|
1963
|
+
(mode2) => mode2
|
|
1964
|
+
// Simple string hasher since modes are already strings
|
|
1965
|
+
);
|
|
1455
1966
|
let requests = /* @__PURE__ */ new Map();
|
|
1967
|
+
let toolCallResolver = null;
|
|
1968
|
+
const sessionScanner = createSessionScanner({
|
|
1969
|
+
workingDirectory,
|
|
1970
|
+
onMessage: (message) => {
|
|
1971
|
+
session.sendClaudeSessionMessage(message);
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1456
1974
|
const permissionServer = await startPermissionServerV2(async (request) => {
|
|
1457
|
-
|
|
1975
|
+
if (!toolCallResolver) {
|
|
1976
|
+
const error = `Tool call resolver not available for permission request: ${request.name}`;
|
|
1977
|
+
types.logger.info(`ERROR: ${error}`);
|
|
1978
|
+
throw new Error(error);
|
|
1979
|
+
}
|
|
1980
|
+
const toolCallId = toolCallResolver(request.name, request.arguments);
|
|
1981
|
+
if (!toolCallId) {
|
|
1982
|
+
const error = `Could not resolve tool call ID for permission request: ${request.name}`;
|
|
1983
|
+
types.logger.info(`ERROR: ${error}`);
|
|
1984
|
+
throw new Error(error);
|
|
1985
|
+
}
|
|
1986
|
+
const id = toolCallId;
|
|
1987
|
+
types.logger.debug(`Using tool call ID as permission request ID: ${id} for ${request.name}`);
|
|
1458
1988
|
let promise = new Promise((resolve) => {
|
|
1459
|
-
|
|
1989
|
+
if (request.name === "exit_plan_mode") {
|
|
1990
|
+
const wrappedResolve = (response2) => {
|
|
1991
|
+
if (response2.approved) {
|
|
1992
|
+
types.logger.debug("[HACK] exit_plan_mode approved - injecting approval message and denying");
|
|
1993
|
+
sessionScanner.onRemoteUserMessageForDeduplication(PLAN_FAKE_RESTART);
|
|
1994
|
+
messageQueue.unshift(PLAN_FAKE_RESTART, "default");
|
|
1995
|
+
types.logger.debug(`[HACK] Message queue size after unshift: ${messageQueue.size()}`);
|
|
1996
|
+
resolve({
|
|
1997
|
+
approved: false,
|
|
1998
|
+
reason: PLAN_FAKE_REJECT
|
|
1999
|
+
});
|
|
2000
|
+
} else {
|
|
2001
|
+
resolve(response2);
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
2004
|
+
requests.set(id, wrappedResolve);
|
|
2005
|
+
} else {
|
|
2006
|
+
requests.set(id, resolve);
|
|
2007
|
+
}
|
|
1460
2008
|
});
|
|
1461
2009
|
let timeout = setTimeout(async () => {
|
|
1462
2010
|
types.logger.debug("Permission timeout - attempting to interrupt Claude");
|
|
@@ -1540,12 +2088,15 @@ async function start(credentials, options = {}) {
|
|
|
1540
2088
|
model: options.model,
|
|
1541
2089
|
permissionMode: options.permissionMode,
|
|
1542
2090
|
startingMode: options.startingMode,
|
|
2091
|
+
messageQueue,
|
|
2092
|
+
sessionScanner,
|
|
1543
2093
|
onModeChange: (newMode) => {
|
|
1544
2094
|
mode = newMode;
|
|
1545
2095
|
session.sendSessionEvent({ type: "switch", mode: newMode });
|
|
1546
2096
|
session.keepAlive(thinking, mode);
|
|
1547
2097
|
if (newMode === "local") {
|
|
1548
2098
|
types.logger.debug("Switching to local mode - clearing pending permission requests");
|
|
2099
|
+
toolCallResolver = null;
|
|
1549
2100
|
for (const [id, resolve] of requests) {
|
|
1550
2101
|
types.logger.debug(`Rejecting pending permission request: ${id}`);
|
|
1551
2102
|
resolve({ approved: false, reason: "Session switched to local mode" });
|
|
@@ -1579,7 +2130,6 @@ async function start(credentials, options = {}) {
|
|
|
1579
2130
|
},
|
|
1580
2131
|
onProcessStart: (processMode) => {
|
|
1581
2132
|
types.logger.debug(`[Process Lifecycle] Starting ${processMode} mode`);
|
|
1582
|
-
activityTracker.reset();
|
|
1583
2133
|
types.logger.debug("Starting process - clearing any stale permission requests");
|
|
1584
2134
|
for (const [id, resolve] of requests) {
|
|
1585
2135
|
types.logger.debug(`Rejecting stale permission request: ${id}`);
|
|
@@ -1589,13 +2139,14 @@ async function start(credentials, options = {}) {
|
|
|
1589
2139
|
},
|
|
1590
2140
|
onProcessStop: (processMode) => {
|
|
1591
2141
|
types.logger.debug(`[Process Lifecycle] Stopped ${processMode} mode`);
|
|
1592
|
-
activityTracker.reset();
|
|
1593
2142
|
types.logger.debug("Stopping process - clearing any stale permission requests");
|
|
1594
2143
|
for (const [id, resolve] of requests) {
|
|
1595
2144
|
types.logger.debug(`Rejecting stale permission request: ${id}`);
|
|
1596
2145
|
resolve({ approved: false, reason: "Process restarted" });
|
|
1597
2146
|
}
|
|
1598
2147
|
requests.clear();
|
|
2148
|
+
thinking = false;
|
|
2149
|
+
session.keepAlive(thinking, mode);
|
|
1599
2150
|
},
|
|
1600
2151
|
mcpServers: {
|
|
1601
2152
|
"permission": {
|
|
@@ -1608,61 +2159,19 @@ async function start(credentials, options = {}) {
|
|
|
1608
2159
|
onAssistantResult,
|
|
1609
2160
|
interruptController,
|
|
1610
2161
|
claudeEnvVars: options.claudeEnvVars,
|
|
1611
|
-
claudeArgs: options.claudeArgs
|
|
2162
|
+
claudeArgs: options.claudeArgs,
|
|
2163
|
+
onThinkingChange: (newThinking) => {
|
|
2164
|
+
thinking = newThinking;
|
|
2165
|
+
session.keepAlive(thinking, mode);
|
|
2166
|
+
},
|
|
2167
|
+
onToolCallResolver: (resolver) => {
|
|
2168
|
+
toolCallResolver = resolver;
|
|
2169
|
+
}
|
|
1612
2170
|
});
|
|
1613
2171
|
clearInterval(pingInterval);
|
|
1614
2172
|
process.exit(0);
|
|
1615
2173
|
}
|
|
1616
2174
|
|
|
1617
|
-
const defaultSettings = {
|
|
1618
|
-
onboardingCompleted: false
|
|
1619
|
-
};
|
|
1620
|
-
async function readSettings() {
|
|
1621
|
-
if (!node_fs.existsSync(types.configuration.settingsFile)) {
|
|
1622
|
-
return { ...defaultSettings };
|
|
1623
|
-
}
|
|
1624
|
-
try {
|
|
1625
|
-
const content = await promises$1.readFile(types.configuration.settingsFile, "utf8");
|
|
1626
|
-
return JSON.parse(content);
|
|
1627
|
-
} catch {
|
|
1628
|
-
return { ...defaultSettings };
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
async function writeSettings(settings) {
|
|
1632
|
-
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1633
|
-
await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1634
|
-
}
|
|
1635
|
-
await promises$1.writeFile(types.configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1636
|
-
}
|
|
1637
|
-
const credentialsSchema = z__namespace.object({
|
|
1638
|
-
secret: z__namespace.string().base64(),
|
|
1639
|
-
token: z__namespace.string()
|
|
1640
|
-
});
|
|
1641
|
-
async function readCredentials() {
|
|
1642
|
-
if (!node_fs.existsSync(types.configuration.privateKeyFile)) {
|
|
1643
|
-
return null;
|
|
1644
|
-
}
|
|
1645
|
-
try {
|
|
1646
|
-
const keyBase64 = await promises$1.readFile(types.configuration.privateKeyFile, "utf8");
|
|
1647
|
-
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
1648
|
-
return {
|
|
1649
|
-
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
1650
|
-
token: credentials.token
|
|
1651
|
-
};
|
|
1652
|
-
} catch {
|
|
1653
|
-
return null;
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
async function writeCredentials(credentials) {
|
|
1657
|
-
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1658
|
-
await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1659
|
-
}
|
|
1660
|
-
await promises$1.writeFile(types.configuration.privateKeyFile, JSON.stringify({
|
|
1661
|
-
secret: types.encodeBase64(credentials.secret),
|
|
1662
|
-
token: credentials.token
|
|
1663
|
-
}, null, 2));
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
2175
|
function displayQRCode(url) {
|
|
1667
2176
|
console.log("=".repeat(80));
|
|
1668
2177
|
console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
|
|
@@ -1690,10 +2199,8 @@ async function doAuth() {
|
|
|
1690
2199
|
console.log("Please, authenticate using mobile app");
|
|
1691
2200
|
const authUrl = "happy://terminal?" + types.encodeBase64Url(keypair.publicKey);
|
|
1692
2201
|
displayQRCode(authUrl);
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
console.log(authUrl);
|
|
1696
|
-
}
|
|
2202
|
+
console.log("\n\u{1F4CB} For manual entry, copy this URL:");
|
|
2203
|
+
console.log(authUrl);
|
|
1697
2204
|
let credentials = null;
|
|
1698
2205
|
while (true) {
|
|
1699
2206
|
try {
|
|
@@ -1741,18 +2248,20 @@ class ApiDaemonSession extends node_events.EventEmitter {
|
|
|
1741
2248
|
keepAliveInterval = null;
|
|
1742
2249
|
token;
|
|
1743
2250
|
secret;
|
|
2251
|
+
spawnedProcesses = /* @__PURE__ */ new Set();
|
|
1744
2252
|
constructor(token, secret, machineIdentity) {
|
|
1745
2253
|
super();
|
|
1746
2254
|
this.token = token;
|
|
1747
2255
|
this.secret = secret;
|
|
1748
2256
|
this.machineIdentity = machineIdentity;
|
|
2257
|
+
types.logger.daemonDebug(`Connecting to server: ${types.configuration.serverUrl}`);
|
|
1749
2258
|
const socket = socket_ioClient.io(types.configuration.serverUrl, {
|
|
1750
2259
|
auth: {
|
|
1751
2260
|
token: this.token,
|
|
1752
2261
|
clientType: "machine-scoped",
|
|
1753
2262
|
machineId: this.machineIdentity.machineId
|
|
1754
2263
|
},
|
|
1755
|
-
path: "/v1/
|
|
2264
|
+
path: "/v1/updates",
|
|
1756
2265
|
reconnection: true,
|
|
1757
2266
|
reconnectionAttempts: Infinity,
|
|
1758
2267
|
reconnectionDelay: 1e3,
|
|
@@ -1762,68 +2271,146 @@ class ApiDaemonSession extends node_events.EventEmitter {
|
|
|
1762
2271
|
autoConnect: false
|
|
1763
2272
|
});
|
|
1764
2273
|
socket.on("connect", () => {
|
|
1765
|
-
types.logger.
|
|
2274
|
+
types.logger.daemonDebug("Socket connected");
|
|
2275
|
+
types.logger.daemonDebug(`Connected with auth - token: ${this.token.substring(0, 10)}..., machineId: ${this.machineIdentity.machineId}`);
|
|
2276
|
+
const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
2277
|
+
socket.emit("rpc-register", { method: rpcMethod });
|
|
2278
|
+
types.logger.daemonDebug(`Emitted RPC registration: ${rpcMethod}`);
|
|
1766
2279
|
this.emit("connected");
|
|
1767
|
-
socket.emit("machine-connect", {
|
|
1768
|
-
token: this.token,
|
|
1769
|
-
machineIdentity: types.encodeBase64(types.encrypt(this.machineIdentity, this.secret))
|
|
1770
|
-
});
|
|
1771
2280
|
this.startKeepAlive();
|
|
1772
2281
|
});
|
|
1773
|
-
socket.on("
|
|
1774
|
-
types.logger.
|
|
1775
|
-
this.
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
const child = child_process.spawn("happy", args, {
|
|
2282
|
+
socket.on("rpc-request", async (data, callback) => {
|
|
2283
|
+
types.logger.daemonDebug(`Received RPC request: ${JSON.stringify(data)}`);
|
|
2284
|
+
const expectedMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
2285
|
+
if (data.method === expectedMethod) {
|
|
2286
|
+
types.logger.daemonDebug("Processing spawn-happy-session RPC");
|
|
2287
|
+
try {
|
|
2288
|
+
const { directory } = data.params || {};
|
|
2289
|
+
if (!directory) {
|
|
2290
|
+
throw new Error("Directory is required");
|
|
2291
|
+
}
|
|
2292
|
+
const args = [
|
|
2293
|
+
"--daemon-spawn",
|
|
2294
|
+
"--happy-starting-mode",
|
|
2295
|
+
"remote"
|
|
2296
|
+
// ALWAYS force remote mode for daemon spawns
|
|
2297
|
+
];
|
|
2298
|
+
if (types.configuration.installationLocation === "local") {
|
|
2299
|
+
args.push("--local");
|
|
2300
|
+
}
|
|
2301
|
+
if (types.configuration.serverUrl !== "https://handy-api.korshakov.org") {
|
|
2302
|
+
args.push("--happy-server-url", types.configuration.serverUrl);
|
|
2303
|
+
}
|
|
2304
|
+
types.logger.daemonDebug(`Spawning happy in directory: ${directory} with args: ${args.join(" ")}`);
|
|
2305
|
+
const happyPath = process.argv[1];
|
|
2306
|
+
const isTypeScript = happyPath.endsWith(".ts");
|
|
2307
|
+
const happyProcess = isTypeScript ? child_process.spawn("npx", ["tsx", happyPath, ...args], {
|
|
2308
|
+
cwd: directory,
|
|
2309
|
+
env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
|
|
1802
2310
|
detached: true,
|
|
1803
|
-
stdio: "ignore",
|
|
1804
|
-
|
|
2311
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2312
|
+
// We need stdout
|
|
2313
|
+
}) : child_process.spawn(process.argv[0], [happyPath, ...args], {
|
|
2314
|
+
cwd: directory,
|
|
2315
|
+
env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
|
|
2316
|
+
detached: true,
|
|
2317
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2318
|
+
// We need stdout
|
|
1805
2319
|
});
|
|
1806
|
-
|
|
2320
|
+
this.spawnedProcesses.add(happyProcess);
|
|
2321
|
+
let sessionId = null;
|
|
2322
|
+
let output = "";
|
|
2323
|
+
let timeoutId = null;
|
|
2324
|
+
const cleanup = () => {
|
|
2325
|
+
happyProcess.stdout.removeAllListeners("data");
|
|
2326
|
+
happyProcess.stderr.removeAllListeners("data");
|
|
2327
|
+
happyProcess.removeAllListeners("error");
|
|
2328
|
+
happyProcess.removeAllListeners("exit");
|
|
2329
|
+
if (timeoutId) {
|
|
2330
|
+
clearTimeout(timeoutId);
|
|
2331
|
+
timeoutId = null;
|
|
2332
|
+
}
|
|
2333
|
+
};
|
|
2334
|
+
happyProcess.stdout.on("data", (data2) => {
|
|
2335
|
+
output += data2.toString();
|
|
2336
|
+
const match = output.match(/daemon:sessionIdCreated:(.+?)[\n\r]/);
|
|
2337
|
+
if (match && !sessionId) {
|
|
2338
|
+
sessionId = match[1];
|
|
2339
|
+
types.logger.daemonDebug(`Session spawned successfully: ${sessionId}`);
|
|
2340
|
+
callback({ sessionId });
|
|
2341
|
+
cleanup();
|
|
2342
|
+
happyProcess.unref();
|
|
2343
|
+
}
|
|
2344
|
+
});
|
|
2345
|
+
happyProcess.stderr.on("data", (data2) => {
|
|
2346
|
+
types.logger.daemonDebug(`Spawned process stderr: ${data2.toString()}`);
|
|
2347
|
+
});
|
|
2348
|
+
happyProcess.on("error", (error) => {
|
|
2349
|
+
types.logger.daemonDebug("Error spawning session:", error);
|
|
2350
|
+
if (!sessionId) {
|
|
2351
|
+
callback({ error: `Failed to spawn: ${error.message}` });
|
|
2352
|
+
cleanup();
|
|
2353
|
+
this.spawnedProcesses.delete(happyProcess);
|
|
2354
|
+
}
|
|
2355
|
+
});
|
|
2356
|
+
happyProcess.on("exit", (code, signal) => {
|
|
2357
|
+
types.logger.daemonDebug(`Spawned process exited with code ${code}, signal ${signal}`);
|
|
2358
|
+
this.spawnedProcesses.delete(happyProcess);
|
|
2359
|
+
if (!sessionId) {
|
|
2360
|
+
callback({ error: `Process exited before session ID received` });
|
|
2361
|
+
cleanup();
|
|
2362
|
+
}
|
|
2363
|
+
});
|
|
2364
|
+
timeoutId = setTimeout(() => {
|
|
2365
|
+
if (!sessionId) {
|
|
2366
|
+
types.logger.daemonDebug("Timeout waiting for session ID");
|
|
2367
|
+
callback({ error: "Timeout waiting for session" });
|
|
2368
|
+
cleanup();
|
|
2369
|
+
happyProcess.kill();
|
|
2370
|
+
this.spawnedProcesses.delete(happyProcess);
|
|
2371
|
+
}
|
|
2372
|
+
}, 1e4);
|
|
2373
|
+
} catch (error) {
|
|
2374
|
+
types.logger.daemonDebug("Error spawning session:", error);
|
|
2375
|
+
callback({ error: error instanceof Error ? error.message : "Unknown error" });
|
|
1807
2376
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
2377
|
+
} else {
|
|
2378
|
+
types.logger.daemonDebug(`Unknown RPC method: ${data.method}`);
|
|
2379
|
+
callback({ error: `Unknown method: ${data.method}` });
|
|
2380
|
+
}
|
|
2381
|
+
});
|
|
2382
|
+
socket.on("disconnect", (reason) => {
|
|
2383
|
+
types.logger.daemonDebug(`Disconnected from server. Reason: ${reason}`);
|
|
2384
|
+
this.emit("disconnected");
|
|
2385
|
+
this.stopKeepAlive();
|
|
2386
|
+
});
|
|
2387
|
+
socket.on("reconnect", () => {
|
|
2388
|
+
types.logger.daemonDebug("Reconnected to server");
|
|
2389
|
+
const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
2390
|
+
socket.emit("rpc-register", { method: rpcMethod });
|
|
2391
|
+
types.logger.daemonDebug(`Re-registered RPC method: ${rpcMethod}`);
|
|
2392
|
+
});
|
|
2393
|
+
socket.on("rpc-registered", (data) => {
|
|
2394
|
+
types.logger.daemonDebug(`RPC registration confirmed: ${data.method}`);
|
|
2395
|
+
});
|
|
2396
|
+
socket.on("rpc-unregistered", (data) => {
|
|
2397
|
+
types.logger.daemonDebug(`RPC unregistered: ${data.method}`);
|
|
2398
|
+
});
|
|
2399
|
+
socket.on("rpc-error", (data) => {
|
|
2400
|
+
types.logger.daemonDebug(`RPC error: ${JSON.stringify(data)}`);
|
|
2401
|
+
});
|
|
2402
|
+
socket.onAny((event, ...args) => {
|
|
2403
|
+
if (!event.startsWith("machine-alive")) {
|
|
2404
|
+
types.logger.daemonDebug(`Socket event: ${event}, args: ${JSON.stringify(args)}`);
|
|
1825
2405
|
}
|
|
1826
2406
|
});
|
|
2407
|
+
socket.on("connect_error", (error) => {
|
|
2408
|
+
types.logger.daemonDebug(`Connection error: ${error.message}`);
|
|
2409
|
+
types.logger.daemonDebug(`Error: ${JSON.stringify(error, null, 2)}`);
|
|
2410
|
+
});
|
|
2411
|
+
socket.on("error", (error) => {
|
|
2412
|
+
types.logger.daemonDebug(`Socket error: ${error}`);
|
|
2413
|
+
});
|
|
1827
2414
|
socket.on("daemon-command", (data) => {
|
|
1828
2415
|
switch (data.command) {
|
|
1829
2416
|
case "shutdown":
|
|
@@ -1854,22 +2441,42 @@ class ApiDaemonSession extends node_events.EventEmitter {
|
|
|
1854
2441
|
this.socket.connect();
|
|
1855
2442
|
}
|
|
1856
2443
|
shutdown() {
|
|
2444
|
+
types.logger.daemonDebug(`Shutting down daemon, killing ${this.spawnedProcesses.size} spawned processes`);
|
|
2445
|
+
for (const process2 of this.spawnedProcesses) {
|
|
2446
|
+
try {
|
|
2447
|
+
types.logger.daemonDebug(`Killing spawned process with PID: ${process2.pid}`);
|
|
2448
|
+
process2.kill("SIGTERM");
|
|
2449
|
+
setTimeout(() => {
|
|
2450
|
+
try {
|
|
2451
|
+
process2.kill("SIGKILL");
|
|
2452
|
+
} catch (e) {
|
|
2453
|
+
}
|
|
2454
|
+
}, 1e3);
|
|
2455
|
+
} catch (error) {
|
|
2456
|
+
types.logger.daemonDebug(`Error killing process: ${error}`);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
this.spawnedProcesses.clear();
|
|
1857
2460
|
this.stopKeepAlive();
|
|
1858
2461
|
this.socket.close();
|
|
1859
2462
|
this.emit("shutdown");
|
|
1860
2463
|
}
|
|
1861
2464
|
}
|
|
1862
2465
|
|
|
2466
|
+
let pidFileFd = null;
|
|
1863
2467
|
async function startDaemon() {
|
|
1864
|
-
|
|
2468
|
+
if (process.platform !== "darwin") {
|
|
2469
|
+
console.error("ERROR: Daemon is only supported on macOS");
|
|
2470
|
+
process.exit(1);
|
|
2471
|
+
}
|
|
2472
|
+
types.logger.daemonDebug("Starting daemon process...");
|
|
2473
|
+
types.logger.daemonDebug(`Server URL: ${types.configuration.serverUrl}`);
|
|
1865
2474
|
if (await isDaemonRunning()) {
|
|
1866
|
-
|
|
2475
|
+
types.logger.daemonDebug("Happy daemon is already running");
|
|
1867
2476
|
process.exit(0);
|
|
1868
2477
|
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
console.log("[DAEMON] PID file written successfully");
|
|
1872
|
-
types.logger.info("Happy CLI daemon started successfully");
|
|
2478
|
+
pidFileFd = writePidFile();
|
|
2479
|
+
types.logger.daemonDebug("PID file written");
|
|
1873
2480
|
process.on("SIGINT", () => {
|
|
1874
2481
|
stopDaemon().catch(console.error);
|
|
1875
2482
|
});
|
|
@@ -1894,7 +2501,7 @@ async function startDaemon() {
|
|
|
1894
2501
|
};
|
|
1895
2502
|
let credentials = await readCredentials();
|
|
1896
2503
|
if (!credentials) {
|
|
1897
|
-
types.logger.
|
|
2504
|
+
types.logger.daemonDebug("No credentials found, running auth");
|
|
1898
2505
|
await doAuth();
|
|
1899
2506
|
credentials = await readCredentials();
|
|
1900
2507
|
if (!credentials) {
|
|
@@ -1902,64 +2509,64 @@ async function startDaemon() {
|
|
|
1902
2509
|
}
|
|
1903
2510
|
}
|
|
1904
2511
|
const { token, secret } = credentials;
|
|
1905
|
-
const daemon = new ApiDaemonSession(
|
|
2512
|
+
const daemon = new ApiDaemonSession(
|
|
2513
|
+
token,
|
|
2514
|
+
secret,
|
|
2515
|
+
machineIdentity
|
|
2516
|
+
);
|
|
1906
2517
|
daemon.on("connected", () => {
|
|
1907
|
-
types.logger.
|
|
2518
|
+
types.logger.daemonDebug("Connected to server event received");
|
|
1908
2519
|
});
|
|
1909
2520
|
daemon.on("disconnected", () => {
|
|
1910
|
-
types.logger.
|
|
2521
|
+
types.logger.daemonDebug("Disconnected from server event received");
|
|
1911
2522
|
});
|
|
1912
2523
|
daemon.on("shutdown", () => {
|
|
1913
|
-
types.logger.
|
|
2524
|
+
types.logger.daemonDebug("Shutdown requested");
|
|
1914
2525
|
stopDaemon();
|
|
1915
2526
|
process.exit(0);
|
|
1916
2527
|
});
|
|
1917
2528
|
daemon.connect();
|
|
1918
|
-
|
|
1919
|
-
}, 1e3);
|
|
2529
|
+
types.logger.daemonDebug("Daemon started successfully");
|
|
1920
2530
|
} catch (error) {
|
|
1921
|
-
types.logger.
|
|
2531
|
+
types.logger.daemonDebug("Failed to start daemon", error);
|
|
1922
2532
|
stopDaemon();
|
|
1923
2533
|
process.exit(1);
|
|
1924
2534
|
}
|
|
1925
|
-
process.on("SIGINT", () => process.exit(0));
|
|
1926
|
-
process.on("SIGTERM", () => process.exit(0));
|
|
1927
|
-
process.on("exit", () => process.exit(0));
|
|
1928
2535
|
while (true) {
|
|
1929
2536
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1930
2537
|
}
|
|
1931
2538
|
}
|
|
1932
2539
|
async function isDaemonRunning() {
|
|
1933
2540
|
try {
|
|
1934
|
-
|
|
2541
|
+
types.logger.daemonDebug("[isDaemonRunning] Checking if daemon is running...");
|
|
1935
2542
|
if (fs.existsSync(types.configuration.daemonPidFile)) {
|
|
1936
|
-
|
|
2543
|
+
types.logger.daemonDebug("[isDaemonRunning] PID file exists");
|
|
1937
2544
|
const pid = parseInt(fs.readFileSync(types.configuration.daemonPidFile, "utf-8"));
|
|
1938
|
-
|
|
2545
|
+
types.logger.daemonDebug("[isDaemonRunning] PID from file:", pid);
|
|
1939
2546
|
try {
|
|
1940
2547
|
process.kill(pid, 0);
|
|
1941
|
-
|
|
2548
|
+
types.logger.daemonDebug("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
|
|
1942
2549
|
const isHappyDaemon = await isProcessHappyDaemon(pid);
|
|
1943
|
-
|
|
2550
|
+
types.logger.daemonDebug("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
|
|
1944
2551
|
if (isHappyDaemon) {
|
|
1945
2552
|
return true;
|
|
1946
2553
|
} else {
|
|
1947
|
-
|
|
1948
|
-
types.logger.debug(`
|
|
2554
|
+
types.logger.daemonDebug("[isDaemonRunning] PID is not a happy daemon, cleaning up");
|
|
2555
|
+
types.logger.debug(`PID ${pid} is not a happy daemon, cleaning up`);
|
|
1949
2556
|
fs.unlinkSync(types.configuration.daemonPidFile);
|
|
1950
2557
|
}
|
|
1951
2558
|
} catch (error) {
|
|
1952
|
-
|
|
1953
|
-
types.logger.debug("
|
|
2559
|
+
types.logger.daemonDebug("[isDaemonRunning] Process not running, cleaning up stale PID file");
|
|
2560
|
+
types.logger.debug("Process not running, cleaning up stale PID file");
|
|
1954
2561
|
fs.unlinkSync(types.configuration.daemonPidFile);
|
|
1955
2562
|
}
|
|
1956
2563
|
} else {
|
|
1957
|
-
|
|
2564
|
+
types.logger.daemonDebug("[isDaemonRunning] No PID file found");
|
|
1958
2565
|
}
|
|
1959
2566
|
return false;
|
|
1960
2567
|
} catch (error) {
|
|
1961
|
-
|
|
1962
|
-
types.logger.debug("
|
|
2568
|
+
types.logger.daemonDebug("[isDaemonRunning] Error:", error);
|
|
2569
|
+
types.logger.debug("Error checking daemon status", error);
|
|
1963
2570
|
return false;
|
|
1964
2571
|
}
|
|
1965
2572
|
}
|
|
@@ -1969,20 +2576,46 @@ function writePidFile() {
|
|
|
1969
2576
|
fs.mkdirSync(happyDir, { recursive: true });
|
|
1970
2577
|
}
|
|
1971
2578
|
try {
|
|
1972
|
-
fs.
|
|
2579
|
+
const fd = fs.openSync(types.configuration.daemonPidFile, "wx");
|
|
2580
|
+
fs.writeSync(fd, process.pid.toString());
|
|
2581
|
+
return fd;
|
|
1973
2582
|
} catch (error) {
|
|
1974
2583
|
if (error.code === "EEXIST") {
|
|
1975
|
-
|
|
1976
|
-
|
|
2584
|
+
try {
|
|
2585
|
+
const fd = fs.openSync(types.configuration.daemonPidFile, "r+");
|
|
2586
|
+
const existingPid = fs.readFileSync(types.configuration.daemonPidFile, "utf-8").trim();
|
|
2587
|
+
fs.closeSync(fd);
|
|
2588
|
+
try {
|
|
2589
|
+
process.kill(parseInt(existingPid), 0);
|
|
2590
|
+
types.logger.daemonDebug("PID file exists and process is running");
|
|
2591
|
+
types.logger.daemonDebug("Happy daemon is already running");
|
|
2592
|
+
process.exit(0);
|
|
2593
|
+
} catch {
|
|
2594
|
+
types.logger.daemonDebug("PID file exists but process is dead, cleaning up");
|
|
2595
|
+
fs.unlinkSync(types.configuration.daemonPidFile);
|
|
2596
|
+
return writePidFile();
|
|
2597
|
+
}
|
|
2598
|
+
} catch (lockError) {
|
|
2599
|
+
types.logger.daemonDebug("Cannot acquire write lock on PID file, daemon is running");
|
|
2600
|
+
types.logger.daemonDebug("Happy daemon is already running");
|
|
2601
|
+
process.exit(0);
|
|
2602
|
+
}
|
|
1977
2603
|
}
|
|
1978
2604
|
throw error;
|
|
1979
2605
|
}
|
|
1980
2606
|
}
|
|
1981
2607
|
async function stopDaemon() {
|
|
1982
2608
|
try {
|
|
2609
|
+
if (pidFileFd !== null) {
|
|
2610
|
+
try {
|
|
2611
|
+
fs.closeSync(pidFileFd);
|
|
2612
|
+
} catch {
|
|
2613
|
+
}
|
|
2614
|
+
pidFileFd = null;
|
|
2615
|
+
}
|
|
1983
2616
|
if (fs.existsSync(types.configuration.daemonPidFile)) {
|
|
1984
2617
|
const pid = parseInt(fs.readFileSync(types.configuration.daemonPidFile, "utf-8"));
|
|
1985
|
-
types.logger.debug(`
|
|
2618
|
+
types.logger.debug(`Stopping daemon with PID ${pid}`);
|
|
1986
2619
|
try {
|
|
1987
2620
|
process.kill(pid, "SIGTERM");
|
|
1988
2621
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
@@ -1992,12 +2625,12 @@ async function stopDaemon() {
|
|
|
1992
2625
|
} catch {
|
|
1993
2626
|
}
|
|
1994
2627
|
} catch (error) {
|
|
1995
|
-
types.logger.debug("
|
|
2628
|
+
types.logger.debug("Process already dead or inaccessible", error);
|
|
1996
2629
|
}
|
|
1997
2630
|
fs.unlinkSync(types.configuration.daemonPidFile);
|
|
1998
2631
|
}
|
|
1999
2632
|
} catch (error) {
|
|
2000
|
-
types.logger.debug("
|
|
2633
|
+
types.logger.debug("Error stopping daemon", error);
|
|
2001
2634
|
}
|
|
2002
2635
|
}
|
|
2003
2636
|
async function isProcessHappyDaemon(pid) {
|
|
@@ -2145,7 +2778,12 @@ async function uninstall() {
|
|
|
2145
2778
|
(async () => {
|
|
2146
2779
|
const args = process.argv.slice(2);
|
|
2147
2780
|
let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
|
|
2148
|
-
|
|
2781
|
+
let serverUrl;
|
|
2782
|
+
const serverUrlIndex = args.indexOf("--happy-server-url");
|
|
2783
|
+
if (serverUrlIndex !== -1 && serverUrlIndex + 1 < args.length) {
|
|
2784
|
+
serverUrl = args[serverUrlIndex + 1];
|
|
2785
|
+
}
|
|
2786
|
+
types.initializeConfiguration(installationLocation, serverUrl);
|
|
2149
2787
|
types.initLoggerWithGlobalConfiguration();
|
|
2150
2788
|
types.logger.debug("Starting happy CLI with args: ", process.argv);
|
|
2151
2789
|
const subcommand = args[0];
|
|
@@ -2213,7 +2851,7 @@ Currently only supported on macOS.
|
|
|
2213
2851
|
} else if (arg === "-m" || arg === "--model") {
|
|
2214
2852
|
options.model = args[++i];
|
|
2215
2853
|
} else if (arg === "-p" || arg === "--permission-mode") {
|
|
2216
|
-
options.permissionMode = z.z.enum(["
|
|
2854
|
+
options.permissionMode = z.z.enum(["default", "acceptEdits", "bypassPermissions", "plan"]).parse(args[++i]);
|
|
2217
2855
|
} else if (arg === "--local") ; else if (arg === "--happy-starting-mode") {
|
|
2218
2856
|
options.startingMode = z.z.enum(["local", "remote"]).parse(args[++i]);
|
|
2219
2857
|
} else if (arg === "--claude-env") {
|
|
@@ -2227,6 +2865,10 @@ Currently only supported on macOS.
|
|
|
2227
2865
|
} else if (arg === "--claude-arg") {
|
|
2228
2866
|
const claudeArg = args[++i];
|
|
2229
2867
|
options.claudeArgs = [...options.claudeArgs || [], claudeArg];
|
|
2868
|
+
} else if (arg === "--daemon-spawn") {
|
|
2869
|
+
options.daemonSpawn = true;
|
|
2870
|
+
} else if (arg === "--happy-server-url") {
|
|
2871
|
+
i++;
|
|
2230
2872
|
} else {
|
|
2231
2873
|
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
2232
2874
|
process.exit(1);
|
|
@@ -2245,7 +2887,7 @@ ${chalk.bold("Options:")}
|
|
|
2245
2887
|
-h, --help Show this help message
|
|
2246
2888
|
-v, --version Show version
|
|
2247
2889
|
-m, --model <model> Claude model to use (default: sonnet)
|
|
2248
|
-
-p, --permission-mode Permission mode:
|
|
2890
|
+
-p, --permission-mode Permission mode: default, acceptEdits, bypassPermissions, or plan
|
|
2249
2891
|
--auth, --login Force re-authentication
|
|
2250
2892
|
--claude-env KEY=VALUE Set environment variable for Claude Code
|
|
2251
2893
|
--claude-arg ARG Pass additional argument to Claude CLI
|
|
@@ -2262,6 +2904,8 @@ ${chalk.bold("Options:")}
|
|
|
2262
2904
|
You will require re-login each time you run this in a new directory.
|
|
2263
2905
|
--happy-starting-mode <interactive|remote>
|
|
2264
2906
|
Set the starting mode for new sessions (default: remote)
|
|
2907
|
+
--happy-server-url <url>
|
|
2908
|
+
Set the server URL (overrides HANDY_SERVER_URL environment variable)
|
|
2265
2909
|
|
|
2266
2910
|
${chalk.bold("Examples:")}
|
|
2267
2911
|
happy Start a session with default settings
|
|
@@ -2288,7 +2932,71 @@ ${chalk.bold("Examples:")}
|
|
|
2288
2932
|
}
|
|
2289
2933
|
credentials = res;
|
|
2290
2934
|
}
|
|
2291
|
-
await readSettings() || { };
|
|
2935
|
+
const settings = await readSettings() || { onboardingCompleted: false };
|
|
2936
|
+
process.env.EXPERIMENTAL_FEATURES !== void 0;
|
|
2937
|
+
if (settings.daemonAutoStartWhenRunningHappy === void 0) {
|
|
2938
|
+
console.log(chalk.cyan("\n\u{1F680} Happy Daemon Setup\n"));
|
|
2939
|
+
const rl = node_readline.createInterface({
|
|
2940
|
+
input: process.stdin,
|
|
2941
|
+
output: process.stdout
|
|
2942
|
+
});
|
|
2943
|
+
console.log(chalk.cyan("\n\u{1F4F1} Happy can run a background service that allows you to:"));
|
|
2944
|
+
console.log(chalk.cyan(" \u2022 Spawn new conversations from your phone"));
|
|
2945
|
+
console.log(chalk.cyan(" \u2022 Continue closed conversations remotely"));
|
|
2946
|
+
console.log(chalk.cyan(" \u2022 Work with Claude while your computer has internet\n"));
|
|
2947
|
+
const answer = await new Promise((resolve) => {
|
|
2948
|
+
rl.question(chalk.green("Would you like Happy to start this service automatically? (recommended) [Y/n]: "), resolve);
|
|
2949
|
+
});
|
|
2950
|
+
rl.close();
|
|
2951
|
+
const shouldAutoStart = answer.toLowerCase() !== "n";
|
|
2952
|
+
settings.daemonAutoStartWhenRunningHappy = shouldAutoStart;
|
|
2953
|
+
if (shouldAutoStart) {
|
|
2954
|
+
console.log(chalk.green("\u2713 Happy will start the background service automatically"));
|
|
2955
|
+
console.log(chalk.gray(" The service will run whenever you use the happy command"));
|
|
2956
|
+
} else {
|
|
2957
|
+
console.log(chalk.yellow(" You can enable this later by running: happy daemon install"));
|
|
2958
|
+
}
|
|
2959
|
+
await writeSettings(settings);
|
|
2960
|
+
}
|
|
2961
|
+
if (settings.daemonAutoStartWhenRunningHappy) {
|
|
2962
|
+
console.debug("Starting Happy background service...");
|
|
2963
|
+
if (!await isDaemonRunning()) {
|
|
2964
|
+
const happyPath = process.argv[1];
|
|
2965
|
+
const isBuiltBinary = happyPath.endsWith("/bin/happy") || happyPath.endsWith("\\bin\\happy");
|
|
2966
|
+
const daemonArgs = ["daemon", "start"];
|
|
2967
|
+
if (serverUrl) {
|
|
2968
|
+
daemonArgs.push("--happy-server-url", serverUrl);
|
|
2969
|
+
}
|
|
2970
|
+
if (installationLocation === "local") {
|
|
2971
|
+
daemonArgs.push("--local");
|
|
2972
|
+
}
|
|
2973
|
+
const daemonProcess = isBuiltBinary ? child_process.spawn(happyPath, daemonArgs, {
|
|
2974
|
+
detached: true,
|
|
2975
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
2976
|
+
// Show stdout/stderr for debugging
|
|
2977
|
+
env: {
|
|
2978
|
+
...process.env,
|
|
2979
|
+
HANDY_SERVER_URL: serverUrl || process.env.HANDY_SERVER_URL,
|
|
2980
|
+
// Pass through server URL
|
|
2981
|
+
HANDY_LOCAL: process.env.HANDY_LOCAL
|
|
2982
|
+
// Pass through local flag
|
|
2983
|
+
}
|
|
2984
|
+
}) : child_process.spawn("npx", ["tsx", happyPath, ...daemonArgs], {
|
|
2985
|
+
detached: true,
|
|
2986
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
2987
|
+
// Show stdout/stderr for debugging
|
|
2988
|
+
env: {
|
|
2989
|
+
...process.env,
|
|
2990
|
+
HANDY_SERVER_URL: serverUrl || process.env.HANDY_SERVER_URL,
|
|
2991
|
+
// Pass through server URL
|
|
2992
|
+
HANDY_LOCAL: process.env.HANDY_LOCAL
|
|
2993
|
+
// Pass through local flag
|
|
2994
|
+
}
|
|
2995
|
+
});
|
|
2996
|
+
daemonProcess.unref();
|
|
2997
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
2292
3000
|
try {
|
|
2293
3001
|
await start(credentials, options);
|
|
2294
3002
|
} catch (error) {
|