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.mjs
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { l as logger, d as delay, e as backoff, R as RawJSONLinesSchema,
|
|
2
|
+
import { l as logger, d as delay, e as backoff, 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-DNu8okOb.mjs';
|
|
3
3
|
import { randomUUID, randomBytes } from 'node:crypto';
|
|
4
|
-
import { query, AbortError } from '@anthropic-ai/claude-code';
|
|
5
|
-
import { existsSync, readFileSync, mkdirSync, watch, rmSync } from 'node:fs';
|
|
6
|
-
import { resolve, join, dirname } from 'node:path';
|
|
7
|
-
import os, { homedir } from 'node:os';
|
|
8
|
-
import { access, watch as watch$1, readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
|
|
9
4
|
import { spawn } from 'node:child_process';
|
|
10
5
|
import { createInterface } from 'node:readline';
|
|
6
|
+
import { existsSync, readFileSync, mkdirSync, watch, rmSync } from 'node:fs';
|
|
7
|
+
import { join, resolve, dirname } from 'node:path';
|
|
11
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import os, { homedir } from 'node:os';
|
|
10
|
+
import { access, watch as watch$1, readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
|
|
12
11
|
import { readFile, mkdir, writeFile as writeFile$1 } from 'node:fs/promises';
|
|
13
12
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
14
|
-
import { createServer
|
|
13
|
+
import { createServer } from 'node:http';
|
|
15
14
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
16
15
|
import * as z from 'zod';
|
|
17
16
|
import { z as z$1 } from 'zod';
|
|
@@ -20,16 +19,352 @@ import { promisify } from 'util';
|
|
|
20
19
|
import crypto, { createHash } from 'crypto';
|
|
21
20
|
import { dirname as dirname$1, join as join$1 } from 'path';
|
|
22
21
|
import { fileURLToPath as fileURLToPath$1 } from 'url';
|
|
23
|
-
import httpProxy from 'http-proxy';
|
|
24
22
|
import tweetnacl from 'tweetnacl';
|
|
25
23
|
import axios from 'axios';
|
|
26
24
|
import qrcode from 'qrcode-terminal';
|
|
27
25
|
import { EventEmitter } from 'node:events';
|
|
28
26
|
import { io } from 'socket.io-client';
|
|
29
27
|
import { hostname, homedir as homedir$1 } from 'os';
|
|
30
|
-
import { existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync, mkdirSync as mkdirSync$1, writeFileSync, chmodSync } from 'fs';
|
|
28
|
+
import { closeSync, existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync, mkdirSync as mkdirSync$1, openSync, writeSync, writeFileSync, chmodSync } from 'fs';
|
|
31
29
|
import 'expo-server-sdk';
|
|
32
30
|
|
|
31
|
+
class Stream {
|
|
32
|
+
constructor(returned) {
|
|
33
|
+
this.returned = returned;
|
|
34
|
+
}
|
|
35
|
+
queue = [];
|
|
36
|
+
readResolve;
|
|
37
|
+
readReject;
|
|
38
|
+
isDone = false;
|
|
39
|
+
hasError;
|
|
40
|
+
started = false;
|
|
41
|
+
/**
|
|
42
|
+
* Implements async iterable protocol
|
|
43
|
+
*/
|
|
44
|
+
[Symbol.asyncIterator]() {
|
|
45
|
+
if (this.started) {
|
|
46
|
+
throw new Error("Stream can only be iterated once");
|
|
47
|
+
}
|
|
48
|
+
this.started = true;
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Gets the next value from the stream
|
|
53
|
+
*/
|
|
54
|
+
async next() {
|
|
55
|
+
if (this.queue.length > 0) {
|
|
56
|
+
return Promise.resolve({
|
|
57
|
+
done: false,
|
|
58
|
+
value: this.queue.shift()
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (this.isDone) {
|
|
62
|
+
return Promise.resolve({ done: true, value: void 0 });
|
|
63
|
+
}
|
|
64
|
+
if (this.hasError) {
|
|
65
|
+
return Promise.reject(this.hasError);
|
|
66
|
+
}
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
this.readResolve = resolve;
|
|
69
|
+
this.readReject = reject;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Adds a value to the stream
|
|
74
|
+
*/
|
|
75
|
+
enqueue(value) {
|
|
76
|
+
if (this.readResolve) {
|
|
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);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Marks the stream as complete
|
|
87
|
+
*/
|
|
88
|
+
done() {
|
|
89
|
+
this.isDone = true;
|
|
90
|
+
if (this.readResolve) {
|
|
91
|
+
const resolve = this.readResolve;
|
|
92
|
+
this.readResolve = void 0;
|
|
93
|
+
this.readReject = void 0;
|
|
94
|
+
resolve({ done: true, value: void 0 });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Propagates an error through the stream
|
|
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
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Implements async iterator cleanup
|
|
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 });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
class AbortError extends Error {
|
|
122
|
+
constructor(message) {
|
|
123
|
+
super(message);
|
|
124
|
+
this.name = "AbortError";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
129
|
+
const __dirname$2 = join(__filename, "..");
|
|
130
|
+
function getDefaultClaudeCodePath() {
|
|
131
|
+
return join(__dirname$2, "..", "..", "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
|
|
132
|
+
}
|
|
133
|
+
function logDebug(message) {
|
|
134
|
+
if (process.env.DEBUG) {
|
|
135
|
+
logger.debug(message);
|
|
136
|
+
console.log(message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function streamToStdin(stream, stdin, abortController) {
|
|
140
|
+
for await (const message of stream) {
|
|
141
|
+
if (abortController.signal.aborted) break;
|
|
142
|
+
stdin.write(JSON.stringify(message) + "\n");
|
|
143
|
+
}
|
|
144
|
+
stdin.end();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
class Query {
|
|
148
|
+
constructor(childStdin, childStdout, processExitPromise) {
|
|
149
|
+
this.childStdin = childStdin;
|
|
150
|
+
this.childStdout = childStdout;
|
|
151
|
+
this.processExitPromise = processExitPromise;
|
|
152
|
+
this.readMessages();
|
|
153
|
+
this.sdkMessages = this.readSdkMessages();
|
|
154
|
+
}
|
|
155
|
+
pendingControlResponses = /* @__PURE__ */ new Map();
|
|
156
|
+
sdkMessages;
|
|
157
|
+
inputStream = new Stream();
|
|
158
|
+
/**
|
|
159
|
+
* Set an error on the stream
|
|
160
|
+
*/
|
|
161
|
+
setError(error) {
|
|
162
|
+
this.inputStream.error(error);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* AsyncIterableIterator implementation
|
|
166
|
+
*/
|
|
167
|
+
next(...args) {
|
|
168
|
+
return this.sdkMessages.next(...args);
|
|
169
|
+
}
|
|
170
|
+
return(value) {
|
|
171
|
+
if (this.sdkMessages.return) {
|
|
172
|
+
return this.sdkMessages.return(value);
|
|
173
|
+
}
|
|
174
|
+
return Promise.resolve({ done: true, value: void 0 });
|
|
175
|
+
}
|
|
176
|
+
throw(e) {
|
|
177
|
+
if (this.sdkMessages.throw) {
|
|
178
|
+
return this.sdkMessages.throw(e);
|
|
179
|
+
}
|
|
180
|
+
return Promise.reject(e);
|
|
181
|
+
}
|
|
182
|
+
[Symbol.asyncIterator]() {
|
|
183
|
+
return this.sdkMessages;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Read messages from Claude process stdout
|
|
187
|
+
*/
|
|
188
|
+
async readMessages() {
|
|
189
|
+
const rl = createInterface({ input: this.childStdout });
|
|
190
|
+
try {
|
|
191
|
+
for await (const line of rl) {
|
|
192
|
+
if (line.trim()) {
|
|
193
|
+
const message = JSON.parse(line);
|
|
194
|
+
if (message.type === "control_response") {
|
|
195
|
+
const controlResponse = message;
|
|
196
|
+
const handler = this.pendingControlResponses.get(controlResponse.response.request_id);
|
|
197
|
+
if (handler) {
|
|
198
|
+
handler(controlResponse.response);
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
this.inputStream.enqueue(message);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
await this.processExitPromise;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
this.inputStream.error(error);
|
|
208
|
+
} finally {
|
|
209
|
+
this.inputStream.done();
|
|
210
|
+
rl.close();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Async generator for SDK messages
|
|
215
|
+
*/
|
|
216
|
+
async *readSdkMessages() {
|
|
217
|
+
for await (const message of this.inputStream) {
|
|
218
|
+
yield message;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Send interrupt request to Claude
|
|
223
|
+
*/
|
|
224
|
+
async interrupt() {
|
|
225
|
+
if (!this.childStdin) {
|
|
226
|
+
throw new Error("Interrupt requires --input-format stream-json");
|
|
227
|
+
}
|
|
228
|
+
await this.request({
|
|
229
|
+
subtype: "interrupt"
|
|
230
|
+
}, this.childStdin);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Send control request to Claude process
|
|
234
|
+
*/
|
|
235
|
+
request(request, childStdin) {
|
|
236
|
+
const requestId = Math.random().toString(36).substring(2, 15);
|
|
237
|
+
const sdkRequest = {
|
|
238
|
+
request_id: requestId,
|
|
239
|
+
type: "control_request",
|
|
240
|
+
request
|
|
241
|
+
};
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
this.pendingControlResponses.set(requestId, (response) => {
|
|
244
|
+
if (response.subtype === "success") {
|
|
245
|
+
resolve(response);
|
|
246
|
+
} else {
|
|
247
|
+
reject(new Error(response.error));
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
childStdin.write(JSON.stringify(sdkRequest) + "\n");
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function query(config) {
|
|
255
|
+
const {
|
|
256
|
+
prompt,
|
|
257
|
+
abortController = config.abortController || new AbortController(),
|
|
258
|
+
options: {
|
|
259
|
+
allowedTools = [],
|
|
260
|
+
appendSystemPrompt,
|
|
261
|
+
customSystemPrompt,
|
|
262
|
+
cwd,
|
|
263
|
+
disallowedTools = [],
|
|
264
|
+
executable = "node",
|
|
265
|
+
executableArgs = [],
|
|
266
|
+
maxTurns,
|
|
267
|
+
mcpServers,
|
|
268
|
+
pathToClaudeCodeExecutable = getDefaultClaudeCodePath(),
|
|
269
|
+
permissionMode = "default",
|
|
270
|
+
permissionPromptToolName,
|
|
271
|
+
continue: continueConversation,
|
|
272
|
+
resume,
|
|
273
|
+
model,
|
|
274
|
+
fallbackModel,
|
|
275
|
+
strictMcpConfig
|
|
276
|
+
} = {}
|
|
277
|
+
} = config;
|
|
278
|
+
if (!process.env.CLAUDE_CODE_ENTRYPOINT) {
|
|
279
|
+
process.env.CLAUDE_CODE_ENTRYPOINT = "sdk-ts";
|
|
280
|
+
}
|
|
281
|
+
const args = ["--output-format", "stream-json", "--verbose"];
|
|
282
|
+
if (customSystemPrompt) args.push("--system-prompt", customSystemPrompt);
|
|
283
|
+
if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
|
|
284
|
+
if (maxTurns) args.push("--max-turns", maxTurns.toString());
|
|
285
|
+
if (model) args.push("--model", model);
|
|
286
|
+
if (permissionPromptToolName) args.push("--permission-prompt-tool", permissionPromptToolName);
|
|
287
|
+
if (continueConversation) args.push("--continue");
|
|
288
|
+
if (resume) args.push("--resume", resume);
|
|
289
|
+
if (allowedTools.length > 0) args.push("--allowedTools", allowedTools.join(","));
|
|
290
|
+
if (disallowedTools.length > 0) args.push("--disallowedTools", disallowedTools.join(","));
|
|
291
|
+
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
292
|
+
args.push("--mcp-config", JSON.stringify({ mcpServers }));
|
|
293
|
+
}
|
|
294
|
+
if (strictMcpConfig) args.push("--strict-mcp-config");
|
|
295
|
+
if (permissionMode) args.push("--permission-mode", permissionMode);
|
|
296
|
+
if (fallbackModel) {
|
|
297
|
+
if (model && fallbackModel === model) {
|
|
298
|
+
throw new Error("Fallback model cannot be the same as the main model. Please specify a different model for fallbackModel option.");
|
|
299
|
+
}
|
|
300
|
+
args.push("--fallback-model", fallbackModel);
|
|
301
|
+
}
|
|
302
|
+
if (typeof prompt === "string") {
|
|
303
|
+
args.push("--print", prompt.trim());
|
|
304
|
+
} else {
|
|
305
|
+
args.push("--input-format", "stream-json");
|
|
306
|
+
}
|
|
307
|
+
if (!existsSync(pathToClaudeCodeExecutable)) {
|
|
308
|
+
throw new ReferenceError(`Claude Code executable not found at ${pathToClaudeCodeExecutable}. Is options.pathToClaudeCodeExecutable set?`);
|
|
309
|
+
}
|
|
310
|
+
logDebug(`Spawning Claude Code process: ${executable} ${[...executableArgs, pathToClaudeCodeExecutable, ...args].join(" ")}`);
|
|
311
|
+
const child = spawn(executable, [...executableArgs, pathToClaudeCodeExecutable, ...args], {
|
|
312
|
+
cwd,
|
|
313
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
314
|
+
signal: abortController.signal,
|
|
315
|
+
env: {
|
|
316
|
+
...process.env
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
let childStdin = null;
|
|
320
|
+
if (typeof prompt === "string") {
|
|
321
|
+
child.stdin.end();
|
|
322
|
+
} else {
|
|
323
|
+
streamToStdin(prompt, child.stdin, abortController);
|
|
324
|
+
childStdin = child.stdin;
|
|
325
|
+
}
|
|
326
|
+
if (process.env.DEBUG) {
|
|
327
|
+
child.stderr.on("data", (data) => {
|
|
328
|
+
console.error("Claude Code stderr:", data.toString());
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
const cleanup = () => {
|
|
332
|
+
if (!child.killed) {
|
|
333
|
+
child.kill("SIGTERM");
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
abortController.signal.addEventListener("abort", cleanup);
|
|
337
|
+
process.on("exit", cleanup);
|
|
338
|
+
const processExitPromise = new Promise((resolve) => {
|
|
339
|
+
child.on("close", (code) => {
|
|
340
|
+
if (abortController.signal.aborted) {
|
|
341
|
+
query2.setError(new AbortError("Claude Code process aborted by user"));
|
|
342
|
+
}
|
|
343
|
+
if (code !== 0) {
|
|
344
|
+
query2.setError(new Error(`Claude Code process exited with code ${code}`));
|
|
345
|
+
} else {
|
|
346
|
+
resolve();
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
const query2 = new Query(childStdin, child.stdout, processExitPromise);
|
|
351
|
+
child.on("error", (error) => {
|
|
352
|
+
if (abortController.signal.aborted) {
|
|
353
|
+
query2.setError(new AbortError("Claude Code process aborted by user"));
|
|
354
|
+
} else {
|
|
355
|
+
query2.setError(new Error(`Failed to spawn Claude Code process: ${error.message}`));
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
processExitPromise.finally(() => {
|
|
359
|
+
cleanup();
|
|
360
|
+
abortController.signal.removeEventListener("abort", cleanup);
|
|
361
|
+
if (process.env.CLAUDE_SDK_MCP_SERVERS) {
|
|
362
|
+
delete process.env.CLAUDE_SDK_MCP_SERVERS;
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
return query2;
|
|
366
|
+
}
|
|
367
|
+
|
|
33
368
|
function formatClaudeMessage(message, onAssistantResult) {
|
|
34
369
|
logger.debugLargeJson("[CLAUDE] Message from non interactive & remote mode:", message);
|
|
35
370
|
switch (message.type) {
|
|
@@ -139,9 +474,8 @@ function formatClaudeMessage(message, onAssistantResult) {
|
|
|
139
474
|
break;
|
|
140
475
|
}
|
|
141
476
|
default: {
|
|
142
|
-
const exhaustiveCheck = message;
|
|
143
477
|
if (process.env.DEBUG) {
|
|
144
|
-
console.log(chalk.gray(`[Unknown message type]`)
|
|
478
|
+
console.log(chalk.gray(`[Unknown message type: ${message.type}]`));
|
|
145
479
|
}
|
|
146
480
|
}
|
|
147
481
|
}
|
|
@@ -187,6 +521,19 @@ async function awaitFileExist(file, timeout = 1e4) {
|
|
|
187
521
|
return false;
|
|
188
522
|
}
|
|
189
523
|
|
|
524
|
+
function deepEqual(a, b) {
|
|
525
|
+
if (a === b) return true;
|
|
526
|
+
if (a == null || b == null) return false;
|
|
527
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
528
|
+
const keysA = Object.keys(a);
|
|
529
|
+
const keysB = Object.keys(b);
|
|
530
|
+
if (keysA.length !== keysB.length) return false;
|
|
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
|
+
}
|
|
190
537
|
async function claudeRemote(opts) {
|
|
191
538
|
let startFrom = opts.sessionId;
|
|
192
539
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
@@ -203,6 +550,7 @@ async function claudeRemote(opts) {
|
|
|
203
550
|
resume: startFrom ?? void 0,
|
|
204
551
|
mcpServers: opts.mcpServers,
|
|
205
552
|
permissionPromptToolName: opts.permissionPromptToolName,
|
|
553
|
+
permissionMode: opts.permissionMode,
|
|
206
554
|
executable: "node",
|
|
207
555
|
abortController
|
|
208
556
|
};
|
|
@@ -217,7 +565,7 @@ async function claudeRemote(opts) {
|
|
|
217
565
|
if (response) {
|
|
218
566
|
(async () => {
|
|
219
567
|
try {
|
|
220
|
-
|
|
568
|
+
await response.interrupt();
|
|
221
569
|
} catch (e) {
|
|
222
570
|
}
|
|
223
571
|
abortController.abort();
|
|
@@ -227,10 +575,9 @@ async function claudeRemote(opts) {
|
|
|
227
575
|
}
|
|
228
576
|
}
|
|
229
577
|
});
|
|
230
|
-
logger.debug(`[claudeRemote] Starting query with
|
|
578
|
+
logger.debug(`[claudeRemote] Starting query with permission mode: ${opts.permissionMode}`);
|
|
231
579
|
response = query({
|
|
232
|
-
prompt: opts.
|
|
233
|
-
abortController,
|
|
580
|
+
prompt: opts.message,
|
|
234
581
|
options: sdkOptions
|
|
235
582
|
});
|
|
236
583
|
if (opts.interruptController) {
|
|
@@ -240,17 +587,86 @@ async function claudeRemote(opts) {
|
|
|
240
587
|
});
|
|
241
588
|
}
|
|
242
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);
|
|
619
|
+
}
|
|
243
620
|
try {
|
|
244
621
|
logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
245
622
|
for await (const message of response) {
|
|
246
623
|
logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
|
|
247
624
|
formatClaudeMessage(message, opts.onAssistantResult);
|
|
625
|
+
if (message.type === "assistant") {
|
|
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
|
+
}
|
|
248
655
|
if (message.type === "system" && message.subtype === "init") {
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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;
|
|
254
670
|
}
|
|
255
671
|
}
|
|
256
672
|
logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
@@ -264,6 +680,11 @@ async function claudeRemote(opts) {
|
|
|
264
680
|
throw e;
|
|
265
681
|
}
|
|
266
682
|
} finally {
|
|
683
|
+
updateThinking(false);
|
|
684
|
+
toolCalls.length = 0;
|
|
685
|
+
if (opts.onToolCallResolver) {
|
|
686
|
+
opts.onToolCallResolver(null);
|
|
687
|
+
}
|
|
267
688
|
if (opts.interruptController) {
|
|
268
689
|
opts.interruptController.unregister();
|
|
269
690
|
}
|
|
@@ -327,22 +748,70 @@ async function claudeLocal(opts) {
|
|
|
327
748
|
input: child.stdio[3],
|
|
328
749
|
crlfDelay: Infinity
|
|
329
750
|
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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);
|
|
336
760
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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}`);
|
|
340
801
|
}
|
|
802
|
+
} catch (e) {
|
|
803
|
+
logger.debug(`[ClaudeLocal] Non-JSON line from fd3: ${line}`);
|
|
341
804
|
}
|
|
342
805
|
});
|
|
343
806
|
rl.on("error", (err) => {
|
|
344
807
|
console.error("Error reading from fd 3:", err);
|
|
345
808
|
});
|
|
809
|
+
child.on("exit", () => {
|
|
810
|
+
if (stopThinkingTimeout) {
|
|
811
|
+
clearTimeout(stopThinkingTimeout);
|
|
812
|
+
}
|
|
813
|
+
updateThinking(false);
|
|
814
|
+
});
|
|
346
815
|
}
|
|
347
816
|
child.on("error", (error) => {
|
|
348
817
|
});
|
|
@@ -363,58 +832,69 @@ async function claudeLocal(opts) {
|
|
|
363
832
|
return resolvedSessionId;
|
|
364
833
|
}
|
|
365
834
|
|
|
366
|
-
class
|
|
835
|
+
class MessageQueue2 {
|
|
836
|
+
constructor(modeHasher) {
|
|
837
|
+
this.modeHasher = modeHasher;
|
|
838
|
+
logger.debug(`[MessageQueue2] Initialized`);
|
|
839
|
+
}
|
|
367
840
|
queue = [];
|
|
368
|
-
|
|
841
|
+
waiter = null;
|
|
369
842
|
closed = false;
|
|
370
|
-
closePromise;
|
|
371
|
-
closeResolve;
|
|
372
|
-
constructor() {
|
|
373
|
-
this.closePromise = new Promise((resolve) => {
|
|
374
|
-
this.closeResolve = resolve;
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
843
|
/**
|
|
378
|
-
* Push a message to the queue
|
|
844
|
+
* Push a message to the queue with a mode.
|
|
379
845
|
*/
|
|
380
|
-
push(message) {
|
|
846
|
+
push(message, mode) {
|
|
381
847
|
if (this.closed) {
|
|
382
848
|
throw new Error("Cannot push to closed queue");
|
|
383
849
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
});
|
|
397
|
-
} else {
|
|
398
|
-
logger.debug(`[MessageQueue] No waiter found. Adding to queue: "${message}"`);
|
|
399
|
-
this.queue.push({
|
|
400
|
-
type: "user",
|
|
401
|
-
message: {
|
|
402
|
-
role: "user",
|
|
403
|
-
content: message
|
|
404
|
-
},
|
|
405
|
-
parent_tool_use_id: null,
|
|
406
|
-
session_id: ""
|
|
407
|
-
});
|
|
850
|
+
const modeHash = this.modeHasher(mode);
|
|
851
|
+
logger.debug(`[MessageQueue2] push() called with mode hash: ${modeHash}`);
|
|
852
|
+
this.queue.push({
|
|
853
|
+
message,
|
|
854
|
+
mode,
|
|
855
|
+
modeHash
|
|
856
|
+
});
|
|
857
|
+
if (this.waiter) {
|
|
858
|
+
logger.debug(`[MessageQueue2] Notifying waiter`);
|
|
859
|
+
const waiter = this.waiter;
|
|
860
|
+
this.waiter = null;
|
|
861
|
+
waiter(true);
|
|
408
862
|
}
|
|
409
|
-
logger.debug(`[
|
|
863
|
+
logger.debug(`[MessageQueue2] push() completed. Queue size: ${this.queue.length}`);
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Push a message to the beginning of the queue with a mode.
|
|
867
|
+
*/
|
|
868
|
+
unshift(message, mode) {
|
|
869
|
+
if (this.closed) {
|
|
870
|
+
throw new Error("Cannot unshift to closed queue");
|
|
871
|
+
}
|
|
872
|
+
const modeHash = this.modeHasher(mode);
|
|
873
|
+
logger.debug(`[MessageQueue2] unshift() called with mode hash: ${modeHash}`);
|
|
874
|
+
this.queue.unshift({
|
|
875
|
+
message,
|
|
876
|
+
mode,
|
|
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);
|
|
884
|
+
}
|
|
885
|
+
logger.debug(`[MessageQueue2] unshift() completed. Queue size: ${this.queue.length}`);
|
|
410
886
|
}
|
|
411
887
|
/**
|
|
412
888
|
* Close the queue - no more messages can be pushed
|
|
413
889
|
*/
|
|
414
890
|
close() {
|
|
415
|
-
logger.debug(`[
|
|
891
|
+
logger.debug(`[MessageQueue2] close() called`);
|
|
416
892
|
this.closed = true;
|
|
417
|
-
this.
|
|
893
|
+
if (this.waiter) {
|
|
894
|
+
const waiter = this.waiter;
|
|
895
|
+
this.waiter = null;
|
|
896
|
+
waiter(false);
|
|
897
|
+
}
|
|
418
898
|
}
|
|
419
899
|
/**
|
|
420
900
|
* Check if the queue is closed
|
|
@@ -429,56 +909,91 @@ class MessageQueue {
|
|
|
429
909
|
return this.queue.length;
|
|
430
910
|
}
|
|
431
911
|
/**
|
|
432
|
-
*
|
|
912
|
+
* Wait for messages and return all messages with the same mode as a single string
|
|
913
|
+
* Returns { message: string, mode: T } or null if aborted/closed
|
|
433
914
|
*/
|
|
434
|
-
async
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
logger.debug(`[MessageQueue] Iterator yielding waited message`);
|
|
454
|
-
yield nextMessage;
|
|
915
|
+
async waitForMessagesAndGetAsString(abortSignal) {
|
|
916
|
+
if (this.queue.length > 0) {
|
|
917
|
+
return this.collectBatch();
|
|
918
|
+
}
|
|
919
|
+
if (this.closed || abortSignal?.aborted) {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
const hasMessages = await this.waitForMessages(abortSignal);
|
|
923
|
+
if (!hasMessages) {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
return this.collectBatch();
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Collect a batch of messages with the same mode
|
|
930
|
+
*/
|
|
931
|
+
collectBatch() {
|
|
932
|
+
if (this.queue.length === 0) {
|
|
933
|
+
return null;
|
|
455
934
|
}
|
|
935
|
+
const firstItem = this.queue[0];
|
|
936
|
+
const sameModeMessages = [];
|
|
937
|
+
let mode = firstItem.mode;
|
|
938
|
+
const targetModeHash = firstItem.modeHash;
|
|
939
|
+
while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash) {
|
|
940
|
+
const item = this.queue.shift();
|
|
941
|
+
sameModeMessages.push(item.message);
|
|
942
|
+
}
|
|
943
|
+
const combinedMessage = sameModeMessages.join("\n");
|
|
944
|
+
logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
|
|
945
|
+
return {
|
|
946
|
+
message: combinedMessage,
|
|
947
|
+
mode
|
|
948
|
+
};
|
|
456
949
|
}
|
|
457
950
|
/**
|
|
458
|
-
* Wait for
|
|
951
|
+
* Wait for messages to arrive
|
|
459
952
|
*/
|
|
460
|
-
|
|
953
|
+
waitForMessages(abortSignal) {
|
|
461
954
|
return new Promise((resolve) => {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
955
|
+
let abortHandler = null;
|
|
956
|
+
if (abortSignal) {
|
|
957
|
+
abortHandler = () => {
|
|
958
|
+
logger.debug("[MessageQueue2] Wait aborted");
|
|
959
|
+
if (this.waiter === waiterFunc) {
|
|
960
|
+
this.waiter = null;
|
|
961
|
+
}
|
|
962
|
+
resolve(false);
|
|
963
|
+
};
|
|
964
|
+
abortSignal.addEventListener("abort", abortHandler);
|
|
965
|
+
}
|
|
966
|
+
const waiterFunc = (hasMessages) => {
|
|
967
|
+
if (abortHandler && abortSignal) {
|
|
968
|
+
abortSignal.removeEventListener("abort", abortHandler);
|
|
969
|
+
}
|
|
970
|
+
resolve(hasMessages);
|
|
971
|
+
};
|
|
972
|
+
if (this.queue.length > 0) {
|
|
973
|
+
if (abortHandler && abortSignal) {
|
|
974
|
+
abortSignal.removeEventListener("abort", abortHandler);
|
|
975
|
+
}
|
|
976
|
+
resolve(true);
|
|
465
977
|
return;
|
|
466
978
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
this.closePromise?.then(() => {
|
|
471
|
-
const index = this.waiters.indexOf(waiter);
|
|
472
|
-
if (index !== -1) {
|
|
473
|
-
this.waiters.splice(index, 1);
|
|
474
|
-
logger.debug(`[MessageQueue] waitForNext() waiter removed due to close. Remaining waiters: ${this.waiters.length}`);
|
|
475
|
-
resolve(void 0);
|
|
979
|
+
if (this.closed || abortSignal?.aborted) {
|
|
980
|
+
if (abortHandler && abortSignal) {
|
|
981
|
+
abortSignal.removeEventListener("abort", abortHandler);
|
|
476
982
|
}
|
|
477
|
-
|
|
983
|
+
resolve(false);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
this.waiter = waiterFunc;
|
|
987
|
+
logger.debug("[MessageQueue2] Waiting for messages...");
|
|
478
988
|
});
|
|
479
989
|
}
|
|
480
990
|
}
|
|
481
991
|
|
|
992
|
+
var MessageQueue2$1 = /*#__PURE__*/Object.freeze({
|
|
993
|
+
__proto__: null,
|
|
994
|
+
MessageQueue2: MessageQueue2
|
|
995
|
+
});
|
|
996
|
+
|
|
482
997
|
class InvalidateSync {
|
|
483
998
|
_invalidated = false;
|
|
484
999
|
_invalidatedDouble = false;
|
|
@@ -573,6 +1088,39 @@ function startFileWatcher(file, onFileChange) {
|
|
|
573
1088
|
};
|
|
574
1089
|
}
|
|
575
1090
|
|
|
1091
|
+
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.`;
|
|
1092
|
+
const PLAN_FAKE_RESTART = `PlEaZe Continue with plan.`;
|
|
1093
|
+
|
|
1094
|
+
function hackToolResponse(message) {
|
|
1095
|
+
console.log("hackToolResponse", JSON.stringify(message, null, 2));
|
|
1096
|
+
if (message.type === "user" && message.message?.role === "user" && message.message?.content && Array.isArray(message.message.content)) {
|
|
1097
|
+
let modified = false;
|
|
1098
|
+
const hackedContent = message.message.content.map((item) => {
|
|
1099
|
+
if (item.type === "tool_result" && item.is_error === true) {
|
|
1100
|
+
if (item.content === PLAN_FAKE_REJECT) {
|
|
1101
|
+
logger.debug(`[SESSION_SCANNER] Hacking exit_plan_mode tool_result: flipping is_error from true to false`);
|
|
1102
|
+
modified = true;
|
|
1103
|
+
return {
|
|
1104
|
+
...item,
|
|
1105
|
+
is_error: false,
|
|
1106
|
+
content: "Plan approved"
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return item;
|
|
1111
|
+
});
|
|
1112
|
+
if (modified) {
|
|
1113
|
+
return {
|
|
1114
|
+
...message,
|
|
1115
|
+
message: {
|
|
1116
|
+
...message.message,
|
|
1117
|
+
content: hackedContent
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return message;
|
|
1123
|
+
}
|
|
576
1124
|
function createSessionScanner(opts) {
|
|
577
1125
|
const projectDir = getProjectPath(opts.workingDirectory);
|
|
578
1126
|
let finishedSessions = /* @__PURE__ */ new Set();
|
|
@@ -625,7 +1173,8 @@ function createSessionScanner(opts) {
|
|
|
625
1173
|
continue;
|
|
626
1174
|
}
|
|
627
1175
|
}
|
|
628
|
-
|
|
1176
|
+
const hackedMessage = hackToolResponse(message);
|
|
1177
|
+
opts.onMessage(hackedMessage);
|
|
629
1178
|
} catch (e) {
|
|
630
1179
|
logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
|
|
631
1180
|
continue;
|
|
@@ -725,10 +1274,15 @@ function sortKeys(value) {
|
|
|
725
1274
|
|
|
726
1275
|
async function loop(opts) {
|
|
727
1276
|
let mode = opts.startingMode ?? "local";
|
|
728
|
-
let
|
|
1277
|
+
let currentPermissionMode = opts.permissionMode ?? "default";
|
|
1278
|
+
logger.debug(`[loop] Starting with permission mode: ${currentPermissionMode}`);
|
|
1279
|
+
let currentMessageQueue = opts.messageQueue || new MessageQueue2(
|
|
1280
|
+
(mode2) => mode2
|
|
1281
|
+
// Simple string hasher since modes are already strings
|
|
1282
|
+
);
|
|
729
1283
|
let sessionId = null;
|
|
730
1284
|
let onMessage = null;
|
|
731
|
-
const sessionScanner = createSessionScanner({
|
|
1285
|
+
const sessionScanner = opts.sessionScanner || createSessionScanner({
|
|
732
1286
|
workingDirectory: opts.path,
|
|
733
1287
|
onMessage: (message) => {
|
|
734
1288
|
opts.session.sendClaudeSessionMessage(message);
|
|
@@ -736,7 +1290,20 @@ async function loop(opts) {
|
|
|
736
1290
|
});
|
|
737
1291
|
opts.session.onUserMessage((message) => {
|
|
738
1292
|
sessionScanner.onRemoteUserMessageForDeduplication(message.content.text);
|
|
739
|
-
|
|
1293
|
+
let messagePermissionMode = currentPermissionMode;
|
|
1294
|
+
if (message.meta?.permissionMode) {
|
|
1295
|
+
const validModes = ["default", "acceptEdits", "bypassPermissions", "plan"];
|
|
1296
|
+
if (validModes.includes(message.meta.permissionMode)) {
|
|
1297
|
+
messagePermissionMode = message.meta.permissionMode;
|
|
1298
|
+
currentPermissionMode = messagePermissionMode;
|
|
1299
|
+
logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
|
|
1300
|
+
} else {
|
|
1301
|
+
logger.info(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
|
|
1302
|
+
}
|
|
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);
|
|
740
1307
|
logger.debugLargeJson("User message pushed to queue:", message);
|
|
741
1308
|
if (onMessage) {
|
|
742
1309
|
onMessage();
|
|
@@ -747,6 +1314,7 @@ async function loop(opts) {
|
|
|
747
1314
|
sessionScanner.onNewSession(newSessionId);
|
|
748
1315
|
};
|
|
749
1316
|
while (true) {
|
|
1317
|
+
logger.debug(`[loop] Starting loop iteration, queue size: ${currentMessageQueue.size()}, mode: ${mode}`);
|
|
750
1318
|
if (currentMessageQueue.size() > 0) {
|
|
751
1319
|
if (mode !== "remote") {
|
|
752
1320
|
mode = "remote";
|
|
@@ -754,7 +1322,6 @@ async function loop(opts) {
|
|
|
754
1322
|
opts.onModeChange(mode);
|
|
755
1323
|
}
|
|
756
1324
|
}
|
|
757
|
-
continue;
|
|
758
1325
|
}
|
|
759
1326
|
if (mode === "local") {
|
|
760
1327
|
let abortedOutside = false;
|
|
@@ -798,6 +1365,7 @@ async function loop(opts) {
|
|
|
798
1365
|
path: opts.path,
|
|
799
1366
|
sessionId,
|
|
800
1367
|
onSessionFound,
|
|
1368
|
+
onThinkingChange: opts.onThinkingChange,
|
|
801
1369
|
abort: interactiveAbortController.signal,
|
|
802
1370
|
claudeEnvVars: opts.claudeEnvVars,
|
|
803
1371
|
claudeArgs: opts.claudeArgs
|
|
@@ -820,15 +1388,16 @@ async function loop(opts) {
|
|
|
820
1388
|
}
|
|
821
1389
|
}
|
|
822
1390
|
if (mode === "remote") {
|
|
1391
|
+
console.log("Starting remote mode...");
|
|
823
1392
|
logger.debug("Starting " + sessionId);
|
|
824
1393
|
const remoteAbortController = new AbortController();
|
|
825
1394
|
opts.session.setHandler("abort", () => {
|
|
826
|
-
if (!remoteAbortController.signal.aborted) {
|
|
1395
|
+
if (remoteAbortController && !remoteAbortController.signal.aborted) {
|
|
827
1396
|
remoteAbortController.abort();
|
|
828
1397
|
}
|
|
829
1398
|
});
|
|
830
1399
|
const abortHandler = () => {
|
|
831
|
-
if (!remoteAbortController.signal.aborted) {
|
|
1400
|
+
if (remoteAbortController && !remoteAbortController.signal.aborted) {
|
|
832
1401
|
if (mode !== "local") {
|
|
833
1402
|
mode = "local";
|
|
834
1403
|
if (opts.onModeChange) {
|
|
@@ -850,21 +1419,35 @@ async function loop(opts) {
|
|
|
850
1419
|
process.stdin.on("data", abortHandler);
|
|
851
1420
|
try {
|
|
852
1421
|
logger.debug(`Starting claudeRemote with messages: ${currentMessageQueue.size()}`);
|
|
1422
|
+
logger.debug("[loop] Waiting for messages before starting claudeRemote...");
|
|
1423
|
+
const messageData = await currentMessageQueue.waitForMessagesAndGetAsString(remoteAbortController.signal);
|
|
1424
|
+
if (!messageData) {
|
|
1425
|
+
console.log("[LOOP] No message received (queue closed or aborted), continuing loop");
|
|
1426
|
+
logger.debug("[loop] No message received (queue closed or aborted), skipping remote mode");
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
currentPermissionMode = messageData.mode;
|
|
1430
|
+
logger.debug(`[loop] Using permission mode from queue: ${currentPermissionMode}`);
|
|
853
1431
|
if (opts.onProcessStart) {
|
|
854
1432
|
opts.onProcessStart("remote");
|
|
855
1433
|
}
|
|
1434
|
+
opts.session.sendSessionEvent({ type: "permission-mode-changed", mode: currentPermissionMode });
|
|
1435
|
+
logger.debug(`[loop] Sent permission-mode-changed event to app: ${currentPermissionMode}`);
|
|
856
1436
|
await claudeRemote({
|
|
857
1437
|
abort: remoteAbortController.signal,
|
|
858
1438
|
sessionId,
|
|
859
1439
|
path: opts.path,
|
|
860
1440
|
mcpServers: opts.mcpServers,
|
|
861
1441
|
permissionPromptToolName: opts.permissionPromptToolName,
|
|
1442
|
+
permissionMode: currentPermissionMode,
|
|
862
1443
|
onSessionFound,
|
|
863
|
-
|
|
1444
|
+
onThinkingChange: opts.onThinkingChange,
|
|
1445
|
+
message: messageData.message,
|
|
864
1446
|
onAssistantResult: opts.onAssistantResult,
|
|
865
1447
|
interruptController: opts.interruptController,
|
|
866
1448
|
claudeEnvVars: opts.claudeEnvVars,
|
|
867
|
-
claudeArgs: opts.claudeArgs
|
|
1449
|
+
claudeArgs: opts.claudeArgs,
|
|
1450
|
+
onToolCallResolver: opts.onToolCallResolver
|
|
868
1451
|
});
|
|
869
1452
|
} catch (e) {
|
|
870
1453
|
if (!remoteAbortController.signal.aborted) {
|
|
@@ -878,8 +1461,6 @@ async function loop(opts) {
|
|
|
878
1461
|
if (process.stdin.isTTY) {
|
|
879
1462
|
process.stdin.setRawMode(false);
|
|
880
1463
|
}
|
|
881
|
-
currentMessageQueue.close();
|
|
882
|
-
currentMessageQueue = new MessageQueue();
|
|
883
1464
|
}
|
|
884
1465
|
if (mode !== "remote") {
|
|
885
1466
|
console.log("Switching back to good old claude...");
|
|
@@ -988,7 +1569,7 @@ class InterruptController {
|
|
|
988
1569
|
}
|
|
989
1570
|
}
|
|
990
1571
|
|
|
991
|
-
var version = "0.
|
|
1572
|
+
var version = "0.2.0";
|
|
992
1573
|
var packageJson = {
|
|
993
1574
|
version: version};
|
|
994
1575
|
|
|
@@ -1048,6 +1629,7 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
|
|
|
1048
1629
|
if (!request) return currentState;
|
|
1049
1630
|
let r = { ...currentState.requests };
|
|
1050
1631
|
delete r[id];
|
|
1632
|
+
const isExitPlanModeSuccess = request.tool === "exit_plan_mode" && !message.approved && message.reason === PLAN_FAKE_REJECT;
|
|
1051
1633
|
return {
|
|
1052
1634
|
...currentState,
|
|
1053
1635
|
requests: r,
|
|
@@ -1056,8 +1638,8 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
|
|
|
1056
1638
|
[id]: {
|
|
1057
1639
|
...request,
|
|
1058
1640
|
completedAt: Date.now(),
|
|
1059
|
-
status: message.approved ? "approved" : "denied",
|
|
1060
|
-
reason: message.reason
|
|
1641
|
+
status: isExitPlanModeSuccess ? "approved" : message.approved ? "approved" : "denied",
|
|
1642
|
+
reason: isExitPlanModeSuccess ? "Plan approved" : message.reason
|
|
1061
1643
|
}
|
|
1062
1644
|
}
|
|
1063
1645
|
};
|
|
@@ -1273,148 +1855,77 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
|
|
|
1273
1855
|
});
|
|
1274
1856
|
}
|
|
1275
1857
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
proxy.on("proxyReq", (proxyReq, req, res) => {
|
|
1290
|
-
if (options.onRequest) {
|
|
1291
|
-
options.onRequest(req, proxyReq);
|
|
1292
|
-
}
|
|
1293
|
-
});
|
|
1294
|
-
proxy.on("proxyRes", (proxyRes, req, res) => {
|
|
1295
|
-
if (options.onResponse) {
|
|
1296
|
-
options.onResponse(req, proxyRes);
|
|
1297
|
-
}
|
|
1298
|
-
});
|
|
1299
|
-
const server = createServer((req, res) => {
|
|
1300
|
-
proxy.web(req, res);
|
|
1301
|
-
});
|
|
1302
|
-
const url = await new Promise((resolve, reject) => {
|
|
1303
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1304
|
-
const addr = server.address();
|
|
1305
|
-
if (addr && typeof addr === "object") {
|
|
1306
|
-
const proxyUrl = `http://127.0.0.1:${addr.port}`;
|
|
1307
|
-
logger.debug(`[HTTPProxy] Started on ${proxyUrl} --> ${options.target}`);
|
|
1308
|
-
resolve(proxyUrl);
|
|
1309
|
-
} else {
|
|
1310
|
-
reject(new Error("Failed to get server address"));
|
|
1311
|
-
}
|
|
1312
|
-
});
|
|
1313
|
-
});
|
|
1314
|
-
return url;
|
|
1858
|
+
const defaultSettings = {
|
|
1859
|
+
onboardingCompleted: false
|
|
1860
|
+
};
|
|
1861
|
+
async function readSettings() {
|
|
1862
|
+
if (!existsSync(configuration.settingsFile)) {
|
|
1863
|
+
return { ...defaultSettings };
|
|
1864
|
+
}
|
|
1865
|
+
try {
|
|
1866
|
+
const content = await readFile(configuration.settingsFile, "utf8");
|
|
1867
|
+
return JSON.parse(content);
|
|
1868
|
+
} catch {
|
|
1869
|
+
return { ...defaultSettings };
|
|
1870
|
+
}
|
|
1315
1871
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
activeRequests.set(requestId, timeout);
|
|
1350
|
-
if (!isThinking) {
|
|
1351
|
-
isThinking = true;
|
|
1352
|
-
onThinking(true);
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
},
|
|
1356
|
-
onResponse: (req, proxyRes) => {
|
|
1357
|
-
proxyRes.headers["connection"] = "close";
|
|
1358
|
-
if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
|
|
1359
|
-
const requestId = req._requestId;
|
|
1360
|
-
const timeout = activeRequests.get(requestId);
|
|
1361
|
-
if (timeout) {
|
|
1362
|
-
clearTimeout(timeout);
|
|
1363
|
-
}
|
|
1364
|
-
let cleaned = false;
|
|
1365
|
-
const cleanupRequest = () => {
|
|
1366
|
-
if (!cleaned) {
|
|
1367
|
-
cleaned = true;
|
|
1368
|
-
activeRequests.delete(requestId);
|
|
1369
|
-
checkAndStopThinking();
|
|
1370
|
-
}
|
|
1371
|
-
};
|
|
1372
|
-
proxyRes.on("end", () => {
|
|
1373
|
-
cleanupRequest();
|
|
1374
|
-
});
|
|
1375
|
-
proxyRes.on("error", (err) => {
|
|
1376
|
-
cleanupRequest();
|
|
1377
|
-
});
|
|
1378
|
-
proxyRes.on("aborted", () => {
|
|
1379
|
-
cleanupRequest();
|
|
1380
|
-
});
|
|
1381
|
-
proxyRes.on("close", () => {
|
|
1382
|
-
cleanupRequest();
|
|
1383
|
-
});
|
|
1384
|
-
req.on("close", () => {
|
|
1385
|
-
cleanupRequest();
|
|
1386
|
-
});
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
});
|
|
1390
|
-
const reset = () => {
|
|
1391
|
-
for (const [requestId, timeout] of activeRequests) {
|
|
1392
|
-
clearTimeout(timeout);
|
|
1393
|
-
}
|
|
1394
|
-
activeRequests.clear();
|
|
1395
|
-
if (stopThinkingTimeout) {
|
|
1396
|
-
clearTimeout(stopThinkingTimeout);
|
|
1397
|
-
stopThinkingTimeout = null;
|
|
1398
|
-
}
|
|
1399
|
-
if (isThinking) {
|
|
1400
|
-
isThinking = false;
|
|
1401
|
-
onThinking(false);
|
|
1402
|
-
}
|
|
1403
|
-
};
|
|
1404
|
-
return {
|
|
1405
|
-
proxyUrl,
|
|
1406
|
-
reset
|
|
1407
|
-
};
|
|
1872
|
+
async function writeSettings(settings) {
|
|
1873
|
+
if (!existsSync(configuration.happyDir)) {
|
|
1874
|
+
await mkdir(configuration.happyDir, { recursive: true });
|
|
1875
|
+
}
|
|
1876
|
+
await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1877
|
+
}
|
|
1878
|
+
const credentialsSchema = z.object({
|
|
1879
|
+
secret: z.string().base64(),
|
|
1880
|
+
token: z.string()
|
|
1881
|
+
});
|
|
1882
|
+
async function readCredentials() {
|
|
1883
|
+
if (!existsSync(configuration.privateKeyFile)) {
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
try {
|
|
1887
|
+
const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
|
|
1888
|
+
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
1889
|
+
return {
|
|
1890
|
+
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
1891
|
+
token: credentials.token
|
|
1892
|
+
};
|
|
1893
|
+
} catch {
|
|
1894
|
+
return null;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
async function writeCredentials(credentials) {
|
|
1898
|
+
if (!existsSync(configuration.happyDir)) {
|
|
1899
|
+
await mkdir(configuration.happyDir, { recursive: true });
|
|
1900
|
+
}
|
|
1901
|
+
await writeFile$1(configuration.privateKeyFile, JSON.stringify({
|
|
1902
|
+
secret: encodeBase64(credentials.secret),
|
|
1903
|
+
token: credentials.token
|
|
1904
|
+
}, null, 2));
|
|
1408
1905
|
}
|
|
1409
1906
|
|
|
1410
1907
|
async function start(credentials, options = {}) {
|
|
1411
1908
|
const workingDirectory = process.cwd();
|
|
1412
1909
|
const sessionTag = randomUUID();
|
|
1910
|
+
if (options.daemonSpawn && options.startingMode === "local") {
|
|
1911
|
+
logger.debug("Daemon spawn requested with local mode - forcing remote mode");
|
|
1912
|
+
options.startingMode = "remote";
|
|
1913
|
+
}
|
|
1413
1914
|
const api = new ApiClient(credentials.token, credentials.secret);
|
|
1414
1915
|
let state = {};
|
|
1415
|
-
|
|
1916
|
+
const settings = await readSettings() || { };
|
|
1917
|
+
let metadata = {
|
|
1918
|
+
path: workingDirectory,
|
|
1919
|
+
host: os.hostname(),
|
|
1920
|
+
version: packageJson.version,
|
|
1921
|
+
os: os.platform(),
|
|
1922
|
+
machineId: settings.machineId
|
|
1923
|
+
};
|
|
1416
1924
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
1417
1925
|
logger.debug(`Session created: ${response.id}`);
|
|
1926
|
+
if (options.daemonSpawn) {
|
|
1927
|
+
console.log(`daemon:sessionIdCreated:${response.id}`);
|
|
1928
|
+
}
|
|
1418
1929
|
const session = api.session(response);
|
|
1419
1930
|
const pushClient = api.push();
|
|
1420
1931
|
let thinking = false;
|
|
@@ -1422,20 +1933,57 @@ async function start(credentials, options = {}) {
|
|
|
1422
1933
|
let pingInterval = setInterval(() => {
|
|
1423
1934
|
session.keepAlive(thinking, mode);
|
|
1424
1935
|
}, 2e3);
|
|
1425
|
-
const activityTracker = await startClaudeActivityTracker((newThinking) => {
|
|
1426
|
-
thinking = newThinking;
|
|
1427
|
-
session.keepAlive(thinking, mode);
|
|
1428
|
-
});
|
|
1429
|
-
process.env.ANTHROPIC_BASE_URL = activityTracker.proxyUrl;
|
|
1430
1936
|
const logPath = await logger.logFilePathPromise;
|
|
1431
1937
|
logger.infoDeveloper(`Session: ${response.id}`);
|
|
1432
1938
|
logger.infoDeveloper(`Logs: ${logPath}`);
|
|
1433
1939
|
const interruptController = new InterruptController();
|
|
1940
|
+
const { MessageQueue2 } = await Promise.resolve().then(function () { return MessageQueue2$1; });
|
|
1941
|
+
const messageQueue = new MessageQueue2(
|
|
1942
|
+
(mode2) => mode2
|
|
1943
|
+
// Simple string hasher since modes are already strings
|
|
1944
|
+
);
|
|
1434
1945
|
let requests = /* @__PURE__ */ new Map();
|
|
1946
|
+
let toolCallResolver = null;
|
|
1947
|
+
const sessionScanner = createSessionScanner({
|
|
1948
|
+
workingDirectory,
|
|
1949
|
+
onMessage: (message) => {
|
|
1950
|
+
session.sendClaudeSessionMessage(message);
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1435
1953
|
const permissionServer = await startPermissionServerV2(async (request) => {
|
|
1436
|
-
|
|
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}`);
|
|
1437
1967
|
let promise = new Promise((resolve) => {
|
|
1438
|
-
|
|
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);
|
|
1984
|
+
} else {
|
|
1985
|
+
requests.set(id, resolve);
|
|
1986
|
+
}
|
|
1439
1987
|
});
|
|
1440
1988
|
let timeout = setTimeout(async () => {
|
|
1441
1989
|
logger.debug("Permission timeout - attempting to interrupt Claude");
|
|
@@ -1519,12 +2067,15 @@ async function start(credentials, options = {}) {
|
|
|
1519
2067
|
model: options.model,
|
|
1520
2068
|
permissionMode: options.permissionMode,
|
|
1521
2069
|
startingMode: options.startingMode,
|
|
2070
|
+
messageQueue,
|
|
2071
|
+
sessionScanner,
|
|
1522
2072
|
onModeChange: (newMode) => {
|
|
1523
2073
|
mode = newMode;
|
|
1524
2074
|
session.sendSessionEvent({ type: "switch", mode: newMode });
|
|
1525
2075
|
session.keepAlive(thinking, mode);
|
|
1526
2076
|
if (newMode === "local") {
|
|
1527
2077
|
logger.debug("Switching to local mode - clearing pending permission requests");
|
|
2078
|
+
toolCallResolver = null;
|
|
1528
2079
|
for (const [id, resolve] of requests) {
|
|
1529
2080
|
logger.debug(`Rejecting pending permission request: ${id}`);
|
|
1530
2081
|
resolve({ approved: false, reason: "Session switched to local mode" });
|
|
@@ -1558,7 +2109,6 @@ async function start(credentials, options = {}) {
|
|
|
1558
2109
|
},
|
|
1559
2110
|
onProcessStart: (processMode) => {
|
|
1560
2111
|
logger.debug(`[Process Lifecycle] Starting ${processMode} mode`);
|
|
1561
|
-
activityTracker.reset();
|
|
1562
2112
|
logger.debug("Starting process - clearing any stale permission requests");
|
|
1563
2113
|
for (const [id, resolve] of requests) {
|
|
1564
2114
|
logger.debug(`Rejecting stale permission request: ${id}`);
|
|
@@ -1568,13 +2118,14 @@ async function start(credentials, options = {}) {
|
|
|
1568
2118
|
},
|
|
1569
2119
|
onProcessStop: (processMode) => {
|
|
1570
2120
|
logger.debug(`[Process Lifecycle] Stopped ${processMode} mode`);
|
|
1571
|
-
activityTracker.reset();
|
|
1572
2121
|
logger.debug("Stopping process - clearing any stale permission requests");
|
|
1573
2122
|
for (const [id, resolve] of requests) {
|
|
1574
2123
|
logger.debug(`Rejecting stale permission request: ${id}`);
|
|
1575
2124
|
resolve({ approved: false, reason: "Process restarted" });
|
|
1576
2125
|
}
|
|
1577
2126
|
requests.clear();
|
|
2127
|
+
thinking = false;
|
|
2128
|
+
session.keepAlive(thinking, mode);
|
|
1578
2129
|
},
|
|
1579
2130
|
mcpServers: {
|
|
1580
2131
|
"permission": {
|
|
@@ -1587,61 +2138,19 @@ async function start(credentials, options = {}) {
|
|
|
1587
2138
|
onAssistantResult,
|
|
1588
2139
|
interruptController,
|
|
1589
2140
|
claudeEnvVars: options.claudeEnvVars,
|
|
1590
|
-
claudeArgs: options.claudeArgs
|
|
2141
|
+
claudeArgs: options.claudeArgs,
|
|
2142
|
+
onThinkingChange: (newThinking) => {
|
|
2143
|
+
thinking = newThinking;
|
|
2144
|
+
session.keepAlive(thinking, mode);
|
|
2145
|
+
},
|
|
2146
|
+
onToolCallResolver: (resolver) => {
|
|
2147
|
+
toolCallResolver = resolver;
|
|
2148
|
+
}
|
|
1591
2149
|
});
|
|
1592
2150
|
clearInterval(pingInterval);
|
|
1593
2151
|
process.exit(0);
|
|
1594
2152
|
}
|
|
1595
2153
|
|
|
1596
|
-
const defaultSettings = {
|
|
1597
|
-
onboardingCompleted: false
|
|
1598
|
-
};
|
|
1599
|
-
async function readSettings() {
|
|
1600
|
-
if (!existsSync(configuration.settingsFile)) {
|
|
1601
|
-
return { ...defaultSettings };
|
|
1602
|
-
}
|
|
1603
|
-
try {
|
|
1604
|
-
const content = await readFile(configuration.settingsFile, "utf8");
|
|
1605
|
-
return JSON.parse(content);
|
|
1606
|
-
} catch {
|
|
1607
|
-
return { ...defaultSettings };
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
async function writeSettings(settings) {
|
|
1611
|
-
if (!existsSync(configuration.happyDir)) {
|
|
1612
|
-
await mkdir(configuration.happyDir, { recursive: true });
|
|
1613
|
-
}
|
|
1614
|
-
await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1615
|
-
}
|
|
1616
|
-
const credentialsSchema = z.object({
|
|
1617
|
-
secret: z.string().base64(),
|
|
1618
|
-
token: z.string()
|
|
1619
|
-
});
|
|
1620
|
-
async function readCredentials() {
|
|
1621
|
-
if (!existsSync(configuration.privateKeyFile)) {
|
|
1622
|
-
return null;
|
|
1623
|
-
}
|
|
1624
|
-
try {
|
|
1625
|
-
const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
|
|
1626
|
-
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
1627
|
-
return {
|
|
1628
|
-
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
1629
|
-
token: credentials.token
|
|
1630
|
-
};
|
|
1631
|
-
} catch {
|
|
1632
|
-
return null;
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
async function writeCredentials(credentials) {
|
|
1636
|
-
if (!existsSync(configuration.happyDir)) {
|
|
1637
|
-
await mkdir(configuration.happyDir, { recursive: true });
|
|
1638
|
-
}
|
|
1639
|
-
await writeFile$1(configuration.privateKeyFile, JSON.stringify({
|
|
1640
|
-
secret: encodeBase64(credentials.secret),
|
|
1641
|
-
token: credentials.token
|
|
1642
|
-
}, null, 2));
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
2154
|
function displayQRCode(url) {
|
|
1646
2155
|
console.log("=".repeat(80));
|
|
1647
2156
|
console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
|
|
@@ -1669,10 +2178,8 @@ async function doAuth() {
|
|
|
1669
2178
|
console.log("Please, authenticate using mobile app");
|
|
1670
2179
|
const authUrl = "happy://terminal?" + encodeBase64Url(keypair.publicKey);
|
|
1671
2180
|
displayQRCode(authUrl);
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
console.log(authUrl);
|
|
1675
|
-
}
|
|
2181
|
+
console.log("\n\u{1F4CB} For manual entry, copy this URL:");
|
|
2182
|
+
console.log(authUrl);
|
|
1676
2183
|
let credentials = null;
|
|
1677
2184
|
while (true) {
|
|
1678
2185
|
try {
|
|
@@ -1720,18 +2227,20 @@ class ApiDaemonSession extends EventEmitter {
|
|
|
1720
2227
|
keepAliveInterval = null;
|
|
1721
2228
|
token;
|
|
1722
2229
|
secret;
|
|
2230
|
+
spawnedProcesses = /* @__PURE__ */ new Set();
|
|
1723
2231
|
constructor(token, secret, machineIdentity) {
|
|
1724
2232
|
super();
|
|
1725
2233
|
this.token = token;
|
|
1726
2234
|
this.secret = secret;
|
|
1727
2235
|
this.machineIdentity = machineIdentity;
|
|
2236
|
+
logger.daemonDebug(`Connecting to server: ${configuration.serverUrl}`);
|
|
1728
2237
|
const socket = io(configuration.serverUrl, {
|
|
1729
2238
|
auth: {
|
|
1730
2239
|
token: this.token,
|
|
1731
2240
|
clientType: "machine-scoped",
|
|
1732
2241
|
machineId: this.machineIdentity.machineId
|
|
1733
2242
|
},
|
|
1734
|
-
path: "/v1/
|
|
2243
|
+
path: "/v1/updates",
|
|
1735
2244
|
reconnection: true,
|
|
1736
2245
|
reconnectionAttempts: Infinity,
|
|
1737
2246
|
reconnectionDelay: 1e3,
|
|
@@ -1741,68 +2250,146 @@ class ApiDaemonSession extends EventEmitter {
|
|
|
1741
2250
|
autoConnect: false
|
|
1742
2251
|
});
|
|
1743
2252
|
socket.on("connect", () => {
|
|
1744
|
-
logger.
|
|
2253
|
+
logger.daemonDebug("Socket connected");
|
|
2254
|
+
logger.daemonDebug(`Connected with auth - token: ${this.token.substring(0, 10)}..., machineId: ${this.machineIdentity.machineId}`);
|
|
2255
|
+
const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
2256
|
+
socket.emit("rpc-register", { method: rpcMethod });
|
|
2257
|
+
logger.daemonDebug(`Emitted RPC registration: ${rpcMethod}`);
|
|
1745
2258
|
this.emit("connected");
|
|
1746
|
-
socket.emit("machine-connect", {
|
|
1747
|
-
token: this.token,
|
|
1748
|
-
machineIdentity: encodeBase64(encrypt(this.machineIdentity, this.secret))
|
|
1749
|
-
});
|
|
1750
2259
|
this.startKeepAlive();
|
|
1751
2260
|
});
|
|
1752
|
-
socket.on("
|
|
1753
|
-
logger.
|
|
1754
|
-
this.
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
const child = spawn$1("happy", args, {
|
|
2261
|
+
socket.on("rpc-request", async (data, callback) => {
|
|
2262
|
+
logger.daemonDebug(`Received RPC request: ${JSON.stringify(data)}`);
|
|
2263
|
+
const expectedMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
2264
|
+
if (data.method === expectedMethod) {
|
|
2265
|
+
logger.daemonDebug("Processing spawn-happy-session RPC");
|
|
2266
|
+
try {
|
|
2267
|
+
const { directory } = data.params || {};
|
|
2268
|
+
if (!directory) {
|
|
2269
|
+
throw new Error("Directory is required");
|
|
2270
|
+
}
|
|
2271
|
+
const args = [
|
|
2272
|
+
"--daemon-spawn",
|
|
2273
|
+
"--happy-starting-mode",
|
|
2274
|
+
"remote"
|
|
2275
|
+
// ALWAYS force remote mode for daemon spawns
|
|
2276
|
+
];
|
|
2277
|
+
if (configuration.installationLocation === "local") {
|
|
2278
|
+
args.push("--local");
|
|
2279
|
+
}
|
|
2280
|
+
if (configuration.serverUrl !== "https://handy-api.korshakov.org") {
|
|
2281
|
+
args.push("--happy-server-url", configuration.serverUrl);
|
|
2282
|
+
}
|
|
2283
|
+
logger.daemonDebug(`Spawning happy in directory: ${directory} with args: ${args.join(" ")}`);
|
|
2284
|
+
const happyPath = process.argv[1];
|
|
2285
|
+
const isTypeScript = happyPath.endsWith(".ts");
|
|
2286
|
+
const happyProcess = isTypeScript ? spawn$1("npx", ["tsx", happyPath, ...args], {
|
|
2287
|
+
cwd: directory,
|
|
2288
|
+
env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
|
|
1781
2289
|
detached: true,
|
|
1782
|
-
stdio: "ignore",
|
|
1783
|
-
|
|
2290
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2291
|
+
// We need stdout
|
|
2292
|
+
}) : spawn$1(process.argv[0], [happyPath, ...args], {
|
|
2293
|
+
cwd: directory,
|
|
2294
|
+
env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
|
|
2295
|
+
detached: true,
|
|
2296
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2297
|
+
// We need stdout
|
|
1784
2298
|
});
|
|
1785
|
-
|
|
2299
|
+
this.spawnedProcesses.add(happyProcess);
|
|
2300
|
+
let sessionId = null;
|
|
2301
|
+
let output = "";
|
|
2302
|
+
let timeoutId = null;
|
|
2303
|
+
const cleanup = () => {
|
|
2304
|
+
happyProcess.stdout.removeAllListeners("data");
|
|
2305
|
+
happyProcess.stderr.removeAllListeners("data");
|
|
2306
|
+
happyProcess.removeAllListeners("error");
|
|
2307
|
+
happyProcess.removeAllListeners("exit");
|
|
2308
|
+
if (timeoutId) {
|
|
2309
|
+
clearTimeout(timeoutId);
|
|
2310
|
+
timeoutId = null;
|
|
2311
|
+
}
|
|
2312
|
+
};
|
|
2313
|
+
happyProcess.stdout.on("data", (data2) => {
|
|
2314
|
+
output += data2.toString();
|
|
2315
|
+
const match = output.match(/daemon:sessionIdCreated:(.+?)[\n\r]/);
|
|
2316
|
+
if (match && !sessionId) {
|
|
2317
|
+
sessionId = match[1];
|
|
2318
|
+
logger.daemonDebug(`Session spawned successfully: ${sessionId}`);
|
|
2319
|
+
callback({ sessionId });
|
|
2320
|
+
cleanup();
|
|
2321
|
+
happyProcess.unref();
|
|
2322
|
+
}
|
|
2323
|
+
});
|
|
2324
|
+
happyProcess.stderr.on("data", (data2) => {
|
|
2325
|
+
logger.daemonDebug(`Spawned process stderr: ${data2.toString()}`);
|
|
2326
|
+
});
|
|
2327
|
+
happyProcess.on("error", (error) => {
|
|
2328
|
+
logger.daemonDebug("Error spawning session:", error);
|
|
2329
|
+
if (!sessionId) {
|
|
2330
|
+
callback({ error: `Failed to spawn: ${error.message}` });
|
|
2331
|
+
cleanup();
|
|
2332
|
+
this.spawnedProcesses.delete(happyProcess);
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
happyProcess.on("exit", (code, signal) => {
|
|
2336
|
+
logger.daemonDebug(`Spawned process exited with code ${code}, signal ${signal}`);
|
|
2337
|
+
this.spawnedProcesses.delete(happyProcess);
|
|
2338
|
+
if (!sessionId) {
|
|
2339
|
+
callback({ error: `Process exited before session ID received` });
|
|
2340
|
+
cleanup();
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2343
|
+
timeoutId = setTimeout(() => {
|
|
2344
|
+
if (!sessionId) {
|
|
2345
|
+
logger.daemonDebug("Timeout waiting for session ID");
|
|
2346
|
+
callback({ error: "Timeout waiting for session" });
|
|
2347
|
+
cleanup();
|
|
2348
|
+
happyProcess.kill();
|
|
2349
|
+
this.spawnedProcesses.delete(happyProcess);
|
|
2350
|
+
}
|
|
2351
|
+
}, 1e4);
|
|
2352
|
+
} catch (error) {
|
|
2353
|
+
logger.daemonDebug("Error spawning session:", error);
|
|
2354
|
+
callback({ error: error instanceof Error ? error.message : "Unknown error" });
|
|
1786
2355
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
2356
|
+
} else {
|
|
2357
|
+
logger.daemonDebug(`Unknown RPC method: ${data.method}`);
|
|
2358
|
+
callback({ error: `Unknown method: ${data.method}` });
|
|
2359
|
+
}
|
|
2360
|
+
});
|
|
2361
|
+
socket.on("disconnect", (reason) => {
|
|
2362
|
+
logger.daemonDebug(`Disconnected from server. Reason: ${reason}`);
|
|
2363
|
+
this.emit("disconnected");
|
|
2364
|
+
this.stopKeepAlive();
|
|
2365
|
+
});
|
|
2366
|
+
socket.on("reconnect", () => {
|
|
2367
|
+
logger.daemonDebug("Reconnected to server");
|
|
2368
|
+
const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
2369
|
+
socket.emit("rpc-register", { method: rpcMethod });
|
|
2370
|
+
logger.daemonDebug(`Re-registered RPC method: ${rpcMethod}`);
|
|
2371
|
+
});
|
|
2372
|
+
socket.on("rpc-registered", (data) => {
|
|
2373
|
+
logger.daemonDebug(`RPC registration confirmed: ${data.method}`);
|
|
2374
|
+
});
|
|
2375
|
+
socket.on("rpc-unregistered", (data) => {
|
|
2376
|
+
logger.daemonDebug(`RPC unregistered: ${data.method}`);
|
|
2377
|
+
});
|
|
2378
|
+
socket.on("rpc-error", (data) => {
|
|
2379
|
+
logger.daemonDebug(`RPC error: ${JSON.stringify(data)}`);
|
|
2380
|
+
});
|
|
2381
|
+
socket.onAny((event, ...args) => {
|
|
2382
|
+
if (!event.startsWith("machine-alive")) {
|
|
2383
|
+
logger.daemonDebug(`Socket event: ${event}, args: ${JSON.stringify(args)}`);
|
|
1804
2384
|
}
|
|
1805
2385
|
});
|
|
2386
|
+
socket.on("connect_error", (error) => {
|
|
2387
|
+
logger.daemonDebug(`Connection error: ${error.message}`);
|
|
2388
|
+
logger.daemonDebug(`Error: ${JSON.stringify(error, null, 2)}`);
|
|
2389
|
+
});
|
|
2390
|
+
socket.on("error", (error) => {
|
|
2391
|
+
logger.daemonDebug(`Socket error: ${error}`);
|
|
2392
|
+
});
|
|
1806
2393
|
socket.on("daemon-command", (data) => {
|
|
1807
2394
|
switch (data.command) {
|
|
1808
2395
|
case "shutdown":
|
|
@@ -1833,22 +2420,42 @@ class ApiDaemonSession extends EventEmitter {
|
|
|
1833
2420
|
this.socket.connect();
|
|
1834
2421
|
}
|
|
1835
2422
|
shutdown() {
|
|
2423
|
+
logger.daemonDebug(`Shutting down daemon, killing ${this.spawnedProcesses.size} spawned processes`);
|
|
2424
|
+
for (const process2 of this.spawnedProcesses) {
|
|
2425
|
+
try {
|
|
2426
|
+
logger.daemonDebug(`Killing spawned process with PID: ${process2.pid}`);
|
|
2427
|
+
process2.kill("SIGTERM");
|
|
2428
|
+
setTimeout(() => {
|
|
2429
|
+
try {
|
|
2430
|
+
process2.kill("SIGKILL");
|
|
2431
|
+
} catch (e) {
|
|
2432
|
+
}
|
|
2433
|
+
}, 1e3);
|
|
2434
|
+
} catch (error) {
|
|
2435
|
+
logger.daemonDebug(`Error killing process: ${error}`);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
this.spawnedProcesses.clear();
|
|
1836
2439
|
this.stopKeepAlive();
|
|
1837
2440
|
this.socket.close();
|
|
1838
2441
|
this.emit("shutdown");
|
|
1839
2442
|
}
|
|
1840
2443
|
}
|
|
1841
2444
|
|
|
2445
|
+
let pidFileFd = null;
|
|
1842
2446
|
async function startDaemon() {
|
|
1843
|
-
|
|
2447
|
+
if (process.platform !== "darwin") {
|
|
2448
|
+
console.error("ERROR: Daemon is only supported on macOS");
|
|
2449
|
+
process.exit(1);
|
|
2450
|
+
}
|
|
2451
|
+
logger.daemonDebug("Starting daemon process...");
|
|
2452
|
+
logger.daemonDebug(`Server URL: ${configuration.serverUrl}`);
|
|
1844
2453
|
if (await isDaemonRunning()) {
|
|
1845
|
-
|
|
2454
|
+
logger.daemonDebug("Happy daemon is already running");
|
|
1846
2455
|
process.exit(0);
|
|
1847
2456
|
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
console.log("[DAEMON] PID file written successfully");
|
|
1851
|
-
logger.info("Happy CLI daemon started successfully");
|
|
2457
|
+
pidFileFd = writePidFile();
|
|
2458
|
+
logger.daemonDebug("PID file written");
|
|
1852
2459
|
process.on("SIGINT", () => {
|
|
1853
2460
|
stopDaemon().catch(console.error);
|
|
1854
2461
|
});
|
|
@@ -1873,7 +2480,7 @@ async function startDaemon() {
|
|
|
1873
2480
|
};
|
|
1874
2481
|
let credentials = await readCredentials();
|
|
1875
2482
|
if (!credentials) {
|
|
1876
|
-
logger.
|
|
2483
|
+
logger.daemonDebug("No credentials found, running auth");
|
|
1877
2484
|
await doAuth();
|
|
1878
2485
|
credentials = await readCredentials();
|
|
1879
2486
|
if (!credentials) {
|
|
@@ -1881,64 +2488,64 @@ async function startDaemon() {
|
|
|
1881
2488
|
}
|
|
1882
2489
|
}
|
|
1883
2490
|
const { token, secret } = credentials;
|
|
1884
|
-
const daemon = new ApiDaemonSession(
|
|
2491
|
+
const daemon = new ApiDaemonSession(
|
|
2492
|
+
token,
|
|
2493
|
+
secret,
|
|
2494
|
+
machineIdentity
|
|
2495
|
+
);
|
|
1885
2496
|
daemon.on("connected", () => {
|
|
1886
|
-
logger.
|
|
2497
|
+
logger.daemonDebug("Connected to server event received");
|
|
1887
2498
|
});
|
|
1888
2499
|
daemon.on("disconnected", () => {
|
|
1889
|
-
logger.
|
|
2500
|
+
logger.daemonDebug("Disconnected from server event received");
|
|
1890
2501
|
});
|
|
1891
2502
|
daemon.on("shutdown", () => {
|
|
1892
|
-
logger.
|
|
2503
|
+
logger.daemonDebug("Shutdown requested");
|
|
1893
2504
|
stopDaemon();
|
|
1894
2505
|
process.exit(0);
|
|
1895
2506
|
});
|
|
1896
2507
|
daemon.connect();
|
|
1897
|
-
|
|
1898
|
-
}, 1e3);
|
|
2508
|
+
logger.daemonDebug("Daemon started successfully");
|
|
1899
2509
|
} catch (error) {
|
|
1900
|
-
logger.
|
|
2510
|
+
logger.daemonDebug("Failed to start daemon", error);
|
|
1901
2511
|
stopDaemon();
|
|
1902
2512
|
process.exit(1);
|
|
1903
2513
|
}
|
|
1904
|
-
process.on("SIGINT", () => process.exit(0));
|
|
1905
|
-
process.on("SIGTERM", () => process.exit(0));
|
|
1906
|
-
process.on("exit", () => process.exit(0));
|
|
1907
2514
|
while (true) {
|
|
1908
2515
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1909
2516
|
}
|
|
1910
2517
|
}
|
|
1911
2518
|
async function isDaemonRunning() {
|
|
1912
2519
|
try {
|
|
1913
|
-
|
|
2520
|
+
logger.daemonDebug("[isDaemonRunning] Checking if daemon is running...");
|
|
1914
2521
|
if (existsSync$1(configuration.daemonPidFile)) {
|
|
1915
|
-
|
|
2522
|
+
logger.daemonDebug("[isDaemonRunning] PID file exists");
|
|
1916
2523
|
const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
|
|
1917
|
-
|
|
2524
|
+
logger.daemonDebug("[isDaemonRunning] PID from file:", pid);
|
|
1918
2525
|
try {
|
|
1919
2526
|
process.kill(pid, 0);
|
|
1920
|
-
|
|
2527
|
+
logger.daemonDebug("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
|
|
1921
2528
|
const isHappyDaemon = await isProcessHappyDaemon(pid);
|
|
1922
|
-
|
|
2529
|
+
logger.daemonDebug("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
|
|
1923
2530
|
if (isHappyDaemon) {
|
|
1924
2531
|
return true;
|
|
1925
2532
|
} else {
|
|
1926
|
-
|
|
1927
|
-
logger.debug(`
|
|
2533
|
+
logger.daemonDebug("[isDaemonRunning] PID is not a happy daemon, cleaning up");
|
|
2534
|
+
logger.debug(`PID ${pid} is not a happy daemon, cleaning up`);
|
|
1928
2535
|
unlinkSync(configuration.daemonPidFile);
|
|
1929
2536
|
}
|
|
1930
2537
|
} catch (error) {
|
|
1931
|
-
|
|
1932
|
-
logger.debug("
|
|
2538
|
+
logger.daemonDebug("[isDaemonRunning] Process not running, cleaning up stale PID file");
|
|
2539
|
+
logger.debug("Process not running, cleaning up stale PID file");
|
|
1933
2540
|
unlinkSync(configuration.daemonPidFile);
|
|
1934
2541
|
}
|
|
1935
2542
|
} else {
|
|
1936
|
-
|
|
2543
|
+
logger.daemonDebug("[isDaemonRunning] No PID file found");
|
|
1937
2544
|
}
|
|
1938
2545
|
return false;
|
|
1939
2546
|
} catch (error) {
|
|
1940
|
-
|
|
1941
|
-
logger.debug("
|
|
2547
|
+
logger.daemonDebug("[isDaemonRunning] Error:", error);
|
|
2548
|
+
logger.debug("Error checking daemon status", error);
|
|
1942
2549
|
return false;
|
|
1943
2550
|
}
|
|
1944
2551
|
}
|
|
@@ -1948,20 +2555,46 @@ function writePidFile() {
|
|
|
1948
2555
|
mkdirSync$1(happyDir, { recursive: true });
|
|
1949
2556
|
}
|
|
1950
2557
|
try {
|
|
1951
|
-
|
|
2558
|
+
const fd = openSync(configuration.daemonPidFile, "wx");
|
|
2559
|
+
writeSync(fd, process.pid.toString());
|
|
2560
|
+
return fd;
|
|
1952
2561
|
} catch (error) {
|
|
1953
2562
|
if (error.code === "EEXIST") {
|
|
1954
|
-
|
|
1955
|
-
|
|
2563
|
+
try {
|
|
2564
|
+
const fd = openSync(configuration.daemonPidFile, "r+");
|
|
2565
|
+
const existingPid = readFileSync$1(configuration.daemonPidFile, "utf-8").trim();
|
|
2566
|
+
closeSync(fd);
|
|
2567
|
+
try {
|
|
2568
|
+
process.kill(parseInt(existingPid), 0);
|
|
2569
|
+
logger.daemonDebug("PID file exists and process is running");
|
|
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
|
+
}
|
|
1956
2582
|
}
|
|
1957
2583
|
throw error;
|
|
1958
2584
|
}
|
|
1959
2585
|
}
|
|
1960
2586
|
async function stopDaemon() {
|
|
1961
2587
|
try {
|
|
2588
|
+
if (pidFileFd !== null) {
|
|
2589
|
+
try {
|
|
2590
|
+
closeSync(pidFileFd);
|
|
2591
|
+
} catch {
|
|
2592
|
+
}
|
|
2593
|
+
pidFileFd = null;
|
|
2594
|
+
}
|
|
1962
2595
|
if (existsSync$1(configuration.daemonPidFile)) {
|
|
1963
2596
|
const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
|
|
1964
|
-
logger.debug(`
|
|
2597
|
+
logger.debug(`Stopping daemon with PID ${pid}`);
|
|
1965
2598
|
try {
|
|
1966
2599
|
process.kill(pid, "SIGTERM");
|
|
1967
2600
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
@@ -1971,12 +2604,12 @@ async function stopDaemon() {
|
|
|
1971
2604
|
} catch {
|
|
1972
2605
|
}
|
|
1973
2606
|
} catch (error) {
|
|
1974
|
-
logger.debug("
|
|
2607
|
+
logger.debug("Process already dead or inaccessible", error);
|
|
1975
2608
|
}
|
|
1976
2609
|
unlinkSync(configuration.daemonPidFile);
|
|
1977
2610
|
}
|
|
1978
2611
|
} catch (error) {
|
|
1979
|
-
logger.debug("
|
|
2612
|
+
logger.debug("Error stopping daemon", error);
|
|
1980
2613
|
}
|
|
1981
2614
|
}
|
|
1982
2615
|
async function isProcessHappyDaemon(pid) {
|
|
@@ -2124,7 +2757,12 @@ async function uninstall() {
|
|
|
2124
2757
|
(async () => {
|
|
2125
2758
|
const args = process.argv.slice(2);
|
|
2126
2759
|
let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
|
|
2127
|
-
|
|
2760
|
+
let serverUrl;
|
|
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);
|
|
2128
2766
|
initLoggerWithGlobalConfiguration();
|
|
2129
2767
|
logger.debug("Starting happy CLI with args: ", process.argv);
|
|
2130
2768
|
const subcommand = args[0];
|
|
@@ -2192,7 +2830,7 @@ Currently only supported on macOS.
|
|
|
2192
2830
|
} else if (arg === "-m" || arg === "--model") {
|
|
2193
2831
|
options.model = args[++i];
|
|
2194
2832
|
} else if (arg === "-p" || arg === "--permission-mode") {
|
|
2195
|
-
options.permissionMode = z$1.enum(["
|
|
2833
|
+
options.permissionMode = z$1.enum(["default", "acceptEdits", "bypassPermissions", "plan"]).parse(args[++i]);
|
|
2196
2834
|
} else if (arg === "--local") ; else if (arg === "--happy-starting-mode") {
|
|
2197
2835
|
options.startingMode = z$1.enum(["local", "remote"]).parse(args[++i]);
|
|
2198
2836
|
} else if (arg === "--claude-env") {
|
|
@@ -2206,6 +2844,10 @@ Currently only supported on macOS.
|
|
|
2206
2844
|
} else if (arg === "--claude-arg") {
|
|
2207
2845
|
const claudeArg = args[++i];
|
|
2208
2846
|
options.claudeArgs = [...options.claudeArgs || [], claudeArg];
|
|
2847
|
+
} else if (arg === "--daemon-spawn") {
|
|
2848
|
+
options.daemonSpawn = true;
|
|
2849
|
+
} else if (arg === "--happy-server-url") {
|
|
2850
|
+
i++;
|
|
2209
2851
|
} else {
|
|
2210
2852
|
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
2211
2853
|
process.exit(1);
|
|
@@ -2224,7 +2866,7 @@ ${chalk.bold("Options:")}
|
|
|
2224
2866
|
-h, --help Show this help message
|
|
2225
2867
|
-v, --version Show version
|
|
2226
2868
|
-m, --model <model> Claude model to use (default: sonnet)
|
|
2227
|
-
-p, --permission-mode Permission mode:
|
|
2869
|
+
-p, --permission-mode Permission mode: default, acceptEdits, bypassPermissions, or plan
|
|
2228
2870
|
--auth, --login Force re-authentication
|
|
2229
2871
|
--claude-env KEY=VALUE Set environment variable for Claude Code
|
|
2230
2872
|
--claude-arg ARG Pass additional argument to Claude CLI
|
|
@@ -2241,6 +2883,8 @@ ${chalk.bold("Options:")}
|
|
|
2241
2883
|
You will require re-login each time you run this in a new directory.
|
|
2242
2884
|
--happy-starting-mode <interactive|remote>
|
|
2243
2885
|
Set the starting mode for new sessions (default: remote)
|
|
2886
|
+
--happy-server-url <url>
|
|
2887
|
+
Set the server URL (overrides HANDY_SERVER_URL environment variable)
|
|
2244
2888
|
|
|
2245
2889
|
${chalk.bold("Examples:")}
|
|
2246
2890
|
happy Start a session with default settings
|
|
@@ -2267,7 +2911,71 @@ ${chalk.bold("Examples:")}
|
|
|
2267
2911
|
}
|
|
2268
2912
|
credentials = res;
|
|
2269
2913
|
}
|
|
2270
|
-
await readSettings() || { };
|
|
2914
|
+
const settings = await readSettings() || { onboardingCompleted: false };
|
|
2915
|
+
process.env.EXPERIMENTAL_FEATURES !== void 0;
|
|
2916
|
+
if (settings.daemonAutoStartWhenRunningHappy === void 0) {
|
|
2917
|
+
console.log(chalk.cyan("\n\u{1F680} Happy Daemon Setup\n"));
|
|
2918
|
+
const rl = createInterface({
|
|
2919
|
+
input: process.stdin,
|
|
2920
|
+
output: process.stdout
|
|
2921
|
+
});
|
|
2922
|
+
console.log(chalk.cyan("\n\u{1F4F1} Happy can run a background service that allows you to:"));
|
|
2923
|
+
console.log(chalk.cyan(" \u2022 Spawn new conversations from your phone"));
|
|
2924
|
+
console.log(chalk.cyan(" \u2022 Continue closed conversations remotely"));
|
|
2925
|
+
console.log(chalk.cyan(" \u2022 Work with Claude while your computer has internet\n"));
|
|
2926
|
+
const answer = await new Promise((resolve) => {
|
|
2927
|
+
rl.question(chalk.green("Would you like Happy to start this service automatically? (recommended) [Y/n]: "), resolve);
|
|
2928
|
+
});
|
|
2929
|
+
rl.close();
|
|
2930
|
+
const shouldAutoStart = answer.toLowerCase() !== "n";
|
|
2931
|
+
settings.daemonAutoStartWhenRunningHappy = shouldAutoStart;
|
|
2932
|
+
if (shouldAutoStart) {
|
|
2933
|
+
console.log(chalk.green("\u2713 Happy will start the background service automatically"));
|
|
2934
|
+
console.log(chalk.gray(" The service will run whenever you use the happy command"));
|
|
2935
|
+
} else {
|
|
2936
|
+
console.log(chalk.yellow(" You can enable this later by running: happy daemon install"));
|
|
2937
|
+
}
|
|
2938
|
+
await writeSettings(settings);
|
|
2939
|
+
}
|
|
2940
|
+
if (settings.daemonAutoStartWhenRunningHappy) {
|
|
2941
|
+
console.debug("Starting Happy background service...");
|
|
2942
|
+
if (!await isDaemonRunning()) {
|
|
2943
|
+
const happyPath = process.argv[1];
|
|
2944
|
+
const isBuiltBinary = happyPath.endsWith("/bin/happy") || happyPath.endsWith("\\bin\\happy");
|
|
2945
|
+
const daemonArgs = ["daemon", "start"];
|
|
2946
|
+
if (serverUrl) {
|
|
2947
|
+
daemonArgs.push("--happy-server-url", serverUrl);
|
|
2948
|
+
}
|
|
2949
|
+
if (installationLocation === "local") {
|
|
2950
|
+
daemonArgs.push("--local");
|
|
2951
|
+
}
|
|
2952
|
+
const daemonProcess = isBuiltBinary ? spawn$1(happyPath, daemonArgs, {
|
|
2953
|
+
detached: true,
|
|
2954
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
2955
|
+
// Show stdout/stderr for debugging
|
|
2956
|
+
env: {
|
|
2957
|
+
...process.env,
|
|
2958
|
+
HANDY_SERVER_URL: serverUrl || process.env.HANDY_SERVER_URL,
|
|
2959
|
+
// Pass through server URL
|
|
2960
|
+
HANDY_LOCAL: process.env.HANDY_LOCAL
|
|
2961
|
+
// Pass through local flag
|
|
2962
|
+
}
|
|
2963
|
+
}) : spawn$1("npx", ["tsx", happyPath, ...daemonArgs], {
|
|
2964
|
+
detached: true,
|
|
2965
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
2966
|
+
// 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
|
+
});
|
|
2975
|
+
daemonProcess.unref();
|
|
2976
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2271
2979
|
try {
|
|
2272
2980
|
await start(credentials, options);
|
|
2273
2981
|
} catch (error) {
|