happy-coder 0.1.14 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +678 -113
- package/dist/index.mjs +677 -112
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +21 -3
- package/dist/lib.d.mts +21 -3
- package/dist/lib.mjs +1 -1
- package/dist/types-CkPUFpfr.cjs +885 -0
- package/dist/types-DNu8okOb.mjs +874 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
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-
|
|
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
13
|
import { createServer } from 'node:http';
|
|
@@ -29,6 +28,343 @@ import { hostname, homedir as homedir$1 } from 'os';
|
|
|
29
28
|
import { closeSync, existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync, mkdirSync as mkdirSync$1, openSync, writeSync, writeFileSync, chmodSync } from 'fs';
|
|
30
29
|
import 'expo-server-sdk';
|
|
31
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
|
+
|
|
32
368
|
function formatClaudeMessage(message, onAssistantResult) {
|
|
33
369
|
logger.debugLargeJson("[CLAUDE] Message from non interactive & remote mode:", message);
|
|
34
370
|
switch (message.type) {
|
|
@@ -138,9 +474,8 @@ function formatClaudeMessage(message, onAssistantResult) {
|
|
|
138
474
|
break;
|
|
139
475
|
}
|
|
140
476
|
default: {
|
|
141
|
-
const exhaustiveCheck = message;
|
|
142
477
|
if (process.env.DEBUG) {
|
|
143
|
-
console.log(chalk.gray(`[Unknown message type]`)
|
|
478
|
+
console.log(chalk.gray(`[Unknown message type: ${message.type}]`));
|
|
144
479
|
}
|
|
145
480
|
}
|
|
146
481
|
}
|
|
@@ -186,6 +521,19 @@ async function awaitFileExist(file, timeout = 1e4) {
|
|
|
186
521
|
return false;
|
|
187
522
|
}
|
|
188
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
|
+
}
|
|
189
537
|
async function claudeRemote(opts) {
|
|
190
538
|
let startFrom = opts.sessionId;
|
|
191
539
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
@@ -202,6 +550,7 @@ async function claudeRemote(opts) {
|
|
|
202
550
|
resume: startFrom ?? void 0,
|
|
203
551
|
mcpServers: opts.mcpServers,
|
|
204
552
|
permissionPromptToolName: opts.permissionPromptToolName,
|
|
553
|
+
permissionMode: opts.permissionMode,
|
|
205
554
|
executable: "node",
|
|
206
555
|
abortController
|
|
207
556
|
};
|
|
@@ -216,7 +565,7 @@ async function claudeRemote(opts) {
|
|
|
216
565
|
if (response) {
|
|
217
566
|
(async () => {
|
|
218
567
|
try {
|
|
219
|
-
|
|
568
|
+
await response.interrupt();
|
|
220
569
|
} catch (e) {
|
|
221
570
|
}
|
|
222
571
|
abortController.abort();
|
|
@@ -226,10 +575,9 @@ async function claudeRemote(opts) {
|
|
|
226
575
|
}
|
|
227
576
|
}
|
|
228
577
|
});
|
|
229
|
-
logger.debug(`[claudeRemote] Starting query with
|
|
578
|
+
logger.debug(`[claudeRemote] Starting query with permission mode: ${opts.permissionMode}`);
|
|
230
579
|
response = query({
|
|
231
|
-
prompt: opts.
|
|
232
|
-
abortController,
|
|
580
|
+
prompt: opts.message,
|
|
233
581
|
options: sdkOptions
|
|
234
582
|
});
|
|
235
583
|
if (opts.interruptController) {
|
|
@@ -249,21 +597,76 @@ async function claudeRemote(opts) {
|
|
|
249
597
|
}
|
|
250
598
|
}
|
|
251
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
|
+
}
|
|
252
620
|
try {
|
|
253
621
|
logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
254
622
|
for await (const message of response) {
|
|
255
623
|
logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
|
|
256
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
|
+
}
|
|
257
655
|
if (message.type === "system" && message.subtype === "init") {
|
|
258
656
|
updateThinking(true);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
+
}
|
|
264
665
|
}
|
|
265
666
|
if (message.type === "result") {
|
|
266
667
|
updateThinking(false);
|
|
668
|
+
logger.debug("[claudeRemote] Result received, exiting claudeRemote");
|
|
669
|
+
break;
|
|
267
670
|
}
|
|
268
671
|
}
|
|
269
672
|
logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
@@ -278,6 +681,10 @@ async function claudeRemote(opts) {
|
|
|
278
681
|
}
|
|
279
682
|
} finally {
|
|
280
683
|
updateThinking(false);
|
|
684
|
+
toolCalls.length = 0;
|
|
685
|
+
if (opts.onToolCallResolver) {
|
|
686
|
+
opts.onToolCallResolver(null);
|
|
687
|
+
}
|
|
281
688
|
if (opts.interruptController) {
|
|
282
689
|
opts.interruptController.unregister();
|
|
283
690
|
}
|
|
@@ -425,58 +832,69 @@ async function claudeLocal(opts) {
|
|
|
425
832
|
return resolvedSessionId;
|
|
426
833
|
}
|
|
427
834
|
|
|
428
|
-
class
|
|
835
|
+
class MessageQueue2 {
|
|
836
|
+
constructor(modeHasher) {
|
|
837
|
+
this.modeHasher = modeHasher;
|
|
838
|
+
logger.debug(`[MessageQueue2] Initialized`);
|
|
839
|
+
}
|
|
429
840
|
queue = [];
|
|
430
|
-
|
|
841
|
+
waiter = null;
|
|
431
842
|
closed = false;
|
|
432
|
-
closePromise;
|
|
433
|
-
closeResolve;
|
|
434
|
-
constructor() {
|
|
435
|
-
this.closePromise = new Promise((resolve) => {
|
|
436
|
-
this.closeResolve = resolve;
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
843
|
/**
|
|
440
|
-
* Push a message to the queue
|
|
844
|
+
* Push a message to the queue with a mode.
|
|
441
845
|
*/
|
|
442
|
-
push(message) {
|
|
846
|
+
push(message, mode) {
|
|
443
847
|
if (this.closed) {
|
|
444
848
|
throw new Error("Cannot push to closed queue");
|
|
445
849
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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);
|
|
862
|
+
}
|
|
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);
|
|
470
884
|
}
|
|
471
|
-
logger.debug(`[
|
|
885
|
+
logger.debug(`[MessageQueue2] unshift() completed. Queue size: ${this.queue.length}`);
|
|
472
886
|
}
|
|
473
887
|
/**
|
|
474
888
|
* Close the queue - no more messages can be pushed
|
|
475
889
|
*/
|
|
476
890
|
close() {
|
|
477
|
-
logger.debug(`[
|
|
891
|
+
logger.debug(`[MessageQueue2] close() called`);
|
|
478
892
|
this.closed = true;
|
|
479
|
-
this.
|
|
893
|
+
if (this.waiter) {
|
|
894
|
+
const waiter = this.waiter;
|
|
895
|
+
this.waiter = null;
|
|
896
|
+
waiter(false);
|
|
897
|
+
}
|
|
480
898
|
}
|
|
481
899
|
/**
|
|
482
900
|
* Check if the queue is closed
|
|
@@ -491,56 +909,91 @@ class MessageQueue {
|
|
|
491
909
|
return this.queue.length;
|
|
492
910
|
}
|
|
493
911
|
/**
|
|
494
|
-
*
|
|
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
|
|
495
914
|
*/
|
|
496
|
-
async
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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;
|
|
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);
|
|
517
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
|
+
};
|
|
518
949
|
}
|
|
519
950
|
/**
|
|
520
|
-
* Wait for
|
|
951
|
+
* Wait for messages to arrive
|
|
521
952
|
*/
|
|
522
|
-
|
|
953
|
+
waitForMessages(abortSignal) {
|
|
523
954
|
return new Promise((resolve) => {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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);
|
|
527
977
|
return;
|
|
528
978
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
this.closePromise?.then(() => {
|
|
533
|
-
const index = this.waiters.indexOf(waiter);
|
|
534
|
-
if (index !== -1) {
|
|
535
|
-
this.waiters.splice(index, 1);
|
|
536
|
-
logger.debug(`[MessageQueue] waitForNext() waiter removed due to close. Remaining waiters: ${this.waiters.length}`);
|
|
537
|
-
resolve(void 0);
|
|
979
|
+
if (this.closed || abortSignal?.aborted) {
|
|
980
|
+
if (abortHandler && abortSignal) {
|
|
981
|
+
abortSignal.removeEventListener("abort", abortHandler);
|
|
538
982
|
}
|
|
539
|
-
|
|
983
|
+
resolve(false);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
this.waiter = waiterFunc;
|
|
987
|
+
logger.debug("[MessageQueue2] Waiting for messages...");
|
|
540
988
|
});
|
|
541
989
|
}
|
|
542
990
|
}
|
|
543
991
|
|
|
992
|
+
var MessageQueue2$1 = /*#__PURE__*/Object.freeze({
|
|
993
|
+
__proto__: null,
|
|
994
|
+
MessageQueue2: MessageQueue2
|
|
995
|
+
});
|
|
996
|
+
|
|
544
997
|
class InvalidateSync {
|
|
545
998
|
_invalidated = false;
|
|
546
999
|
_invalidatedDouble = false;
|
|
@@ -635,6 +1088,39 @@ function startFileWatcher(file, onFileChange) {
|
|
|
635
1088
|
};
|
|
636
1089
|
}
|
|
637
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.debug("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
|
+
}
|
|
638
1124
|
function createSessionScanner(opts) {
|
|
639
1125
|
const projectDir = getProjectPath(opts.workingDirectory);
|
|
640
1126
|
let finishedSessions = /* @__PURE__ */ new Set();
|
|
@@ -687,7 +1173,8 @@ function createSessionScanner(opts) {
|
|
|
687
1173
|
continue;
|
|
688
1174
|
}
|
|
689
1175
|
}
|
|
690
|
-
|
|
1176
|
+
const hackedMessage = hackToolResponse(message);
|
|
1177
|
+
opts.onMessage(hackedMessage);
|
|
691
1178
|
} catch (e) {
|
|
692
1179
|
logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
|
|
693
1180
|
continue;
|
|
@@ -787,10 +1274,15 @@ function sortKeys(value) {
|
|
|
787
1274
|
|
|
788
1275
|
async function loop(opts) {
|
|
789
1276
|
let mode = opts.startingMode ?? "local";
|
|
790
|
-
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
|
+
);
|
|
791
1283
|
let sessionId = null;
|
|
792
1284
|
let onMessage = null;
|
|
793
|
-
const sessionScanner = createSessionScanner({
|
|
1285
|
+
const sessionScanner = opts.sessionScanner || createSessionScanner({
|
|
794
1286
|
workingDirectory: opts.path,
|
|
795
1287
|
onMessage: (message) => {
|
|
796
1288
|
opts.session.sendClaudeSessionMessage(message);
|
|
@@ -798,7 +1290,20 @@ async function loop(opts) {
|
|
|
798
1290
|
});
|
|
799
1291
|
opts.session.onUserMessage((message) => {
|
|
800
1292
|
sessionScanner.onRemoteUserMessageForDeduplication(message.content.text);
|
|
801
|
-
|
|
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);
|
|
802
1307
|
logger.debugLargeJson("User message pushed to queue:", message);
|
|
803
1308
|
if (onMessage) {
|
|
804
1309
|
onMessage();
|
|
@@ -809,6 +1314,7 @@ async function loop(opts) {
|
|
|
809
1314
|
sessionScanner.onNewSession(newSessionId);
|
|
810
1315
|
};
|
|
811
1316
|
while (true) {
|
|
1317
|
+
logger.debug(`[loop] Starting loop iteration, queue size: ${currentMessageQueue.size()}, mode: ${mode}`);
|
|
812
1318
|
if (currentMessageQueue.size() > 0) {
|
|
813
1319
|
if (mode !== "remote") {
|
|
814
1320
|
mode = "remote";
|
|
@@ -816,7 +1322,6 @@ async function loop(opts) {
|
|
|
816
1322
|
opts.onModeChange(mode);
|
|
817
1323
|
}
|
|
818
1324
|
}
|
|
819
|
-
continue;
|
|
820
1325
|
}
|
|
821
1326
|
if (mode === "local") {
|
|
822
1327
|
let abortedOutside = false;
|
|
@@ -883,15 +1388,16 @@ async function loop(opts) {
|
|
|
883
1388
|
}
|
|
884
1389
|
}
|
|
885
1390
|
if (mode === "remote") {
|
|
1391
|
+
console.log("Starting remote mode...");
|
|
886
1392
|
logger.debug("Starting " + sessionId);
|
|
887
1393
|
const remoteAbortController = new AbortController();
|
|
888
1394
|
opts.session.setHandler("abort", () => {
|
|
889
|
-
if (!remoteAbortController.signal.aborted) {
|
|
1395
|
+
if (remoteAbortController && !remoteAbortController.signal.aborted) {
|
|
890
1396
|
remoteAbortController.abort();
|
|
891
1397
|
}
|
|
892
1398
|
});
|
|
893
1399
|
const abortHandler = () => {
|
|
894
|
-
if (!remoteAbortController.signal.aborted) {
|
|
1400
|
+
if (remoteAbortController && !remoteAbortController.signal.aborted) {
|
|
895
1401
|
if (mode !== "local") {
|
|
896
1402
|
mode = "local";
|
|
897
1403
|
if (opts.onModeChange) {
|
|
@@ -913,22 +1419,35 @@ async function loop(opts) {
|
|
|
913
1419
|
process.stdin.on("data", abortHandler);
|
|
914
1420
|
try {
|
|
915
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}`);
|
|
916
1431
|
if (opts.onProcessStart) {
|
|
917
1432
|
opts.onProcessStart("remote");
|
|
918
1433
|
}
|
|
1434
|
+
opts.session.sendSessionEvent({ type: "permission-mode-changed", mode: currentPermissionMode });
|
|
1435
|
+
logger.debug(`[loop] Sent permission-mode-changed event to app: ${currentPermissionMode}`);
|
|
919
1436
|
await claudeRemote({
|
|
920
1437
|
abort: remoteAbortController.signal,
|
|
921
1438
|
sessionId,
|
|
922
1439
|
path: opts.path,
|
|
923
1440
|
mcpServers: opts.mcpServers,
|
|
924
1441
|
permissionPromptToolName: opts.permissionPromptToolName,
|
|
1442
|
+
permissionMode: currentPermissionMode,
|
|
925
1443
|
onSessionFound,
|
|
926
1444
|
onThinkingChange: opts.onThinkingChange,
|
|
927
|
-
|
|
1445
|
+
message: messageData.message,
|
|
928
1446
|
onAssistantResult: opts.onAssistantResult,
|
|
929
1447
|
interruptController: opts.interruptController,
|
|
930
1448
|
claudeEnvVars: opts.claudeEnvVars,
|
|
931
|
-
claudeArgs: opts.claudeArgs
|
|
1449
|
+
claudeArgs: opts.claudeArgs,
|
|
1450
|
+
onToolCallResolver: opts.onToolCallResolver
|
|
932
1451
|
});
|
|
933
1452
|
} catch (e) {
|
|
934
1453
|
if (!remoteAbortController.signal.aborted) {
|
|
@@ -942,8 +1461,6 @@ async function loop(opts) {
|
|
|
942
1461
|
if (process.stdin.isTTY) {
|
|
943
1462
|
process.stdin.setRawMode(false);
|
|
944
1463
|
}
|
|
945
|
-
currentMessageQueue.close();
|
|
946
|
-
currentMessageQueue = new MessageQueue();
|
|
947
1464
|
}
|
|
948
1465
|
if (mode !== "remote") {
|
|
949
1466
|
console.log("Switching back to good old claude...");
|
|
@@ -1052,7 +1569,7 @@ class InterruptController {
|
|
|
1052
1569
|
}
|
|
1053
1570
|
}
|
|
1054
1571
|
|
|
1055
|
-
var version = "0.1
|
|
1572
|
+
var version = "0.2.1";
|
|
1056
1573
|
var packageJson = {
|
|
1057
1574
|
version: version};
|
|
1058
1575
|
|
|
@@ -1112,6 +1629,7 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
|
|
|
1112
1629
|
if (!request) return currentState;
|
|
1113
1630
|
let r = { ...currentState.requests };
|
|
1114
1631
|
delete r[id];
|
|
1632
|
+
const isExitPlanModeSuccess = request.tool === "exit_plan_mode" && !message.approved && message.reason === PLAN_FAKE_REJECT;
|
|
1115
1633
|
return {
|
|
1116
1634
|
...currentState,
|
|
1117
1635
|
requests: r,
|
|
@@ -1120,8 +1638,8 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
|
|
|
1120
1638
|
[id]: {
|
|
1121
1639
|
...request,
|
|
1122
1640
|
completedAt: Date.now(),
|
|
1123
|
-
status: message.approved ? "approved" : "denied",
|
|
1124
|
-
reason: message.reason
|
|
1641
|
+
status: isExitPlanModeSuccess ? "approved" : message.approved ? "approved" : "denied",
|
|
1642
|
+
reason: isExitPlanModeSuccess ? "Plan approved" : message.reason
|
|
1125
1643
|
}
|
|
1126
1644
|
}
|
|
1127
1645
|
};
|
|
@@ -1419,11 +1937,53 @@ async function start(credentials, options = {}) {
|
|
|
1419
1937
|
logger.infoDeveloper(`Session: ${response.id}`);
|
|
1420
1938
|
logger.infoDeveloper(`Logs: ${logPath}`);
|
|
1421
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
|
+
);
|
|
1422
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
|
+
});
|
|
1423
1953
|
const permissionServer = await startPermissionServerV2(async (request) => {
|
|
1424
|
-
|
|
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}`);
|
|
1425
1967
|
let promise = new Promise((resolve) => {
|
|
1426
|
-
|
|
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
|
+
}
|
|
1427
1987
|
});
|
|
1428
1988
|
let timeout = setTimeout(async () => {
|
|
1429
1989
|
logger.debug("Permission timeout - attempting to interrupt Claude");
|
|
@@ -1507,12 +2067,15 @@ async function start(credentials, options = {}) {
|
|
|
1507
2067
|
model: options.model,
|
|
1508
2068
|
permissionMode: options.permissionMode,
|
|
1509
2069
|
startingMode: options.startingMode,
|
|
2070
|
+
messageQueue,
|
|
2071
|
+
sessionScanner,
|
|
1510
2072
|
onModeChange: (newMode) => {
|
|
1511
2073
|
mode = newMode;
|
|
1512
2074
|
session.sendSessionEvent({ type: "switch", mode: newMode });
|
|
1513
2075
|
session.keepAlive(thinking, mode);
|
|
1514
2076
|
if (newMode === "local") {
|
|
1515
2077
|
logger.debug("Switching to local mode - clearing pending permission requests");
|
|
2078
|
+
toolCallResolver = null;
|
|
1516
2079
|
for (const [id, resolve] of requests) {
|
|
1517
2080
|
logger.debug(`Rejecting pending permission request: ${id}`);
|
|
1518
2081
|
resolve({ approved: false, reason: "Session switched to local mode" });
|
|
@@ -1579,6 +2142,9 @@ async function start(credentials, options = {}) {
|
|
|
1579
2142
|
onThinkingChange: (newThinking) => {
|
|
1580
2143
|
thinking = newThinking;
|
|
1581
2144
|
session.keepAlive(thinking, mode);
|
|
2145
|
+
},
|
|
2146
|
+
onToolCallResolver: (resolver) => {
|
|
2147
|
+
toolCallResolver = resolver;
|
|
1582
2148
|
}
|
|
1583
2149
|
});
|
|
1584
2150
|
clearInterval(pingInterval);
|
|
@@ -1839,7 +2405,6 @@ class ApiDaemonSession extends EventEmitter {
|
|
|
1839
2405
|
startKeepAlive() {
|
|
1840
2406
|
this.stopKeepAlive();
|
|
1841
2407
|
this.keepAliveInterval = setInterval(() => {
|
|
1842
|
-
logger.daemonDebug("Sending keep-alive ping");
|
|
1843
2408
|
this.socket.volatile.emit("machine-alive", {
|
|
1844
2409
|
time: Date.now()
|
|
1845
2410
|
});
|
|
@@ -2265,7 +2830,7 @@ Currently only supported on macOS.
|
|
|
2265
2830
|
} else if (arg === "-m" || arg === "--model") {
|
|
2266
2831
|
options.model = args[++i];
|
|
2267
2832
|
} else if (arg === "-p" || arg === "--permission-mode") {
|
|
2268
|
-
options.permissionMode = z$1.enum(["
|
|
2833
|
+
options.permissionMode = z$1.enum(["default", "acceptEdits", "bypassPermissions", "plan"]).parse(args[++i]);
|
|
2269
2834
|
} else if (arg === "--local") ; else if (arg === "--happy-starting-mode") {
|
|
2270
2835
|
options.startingMode = z$1.enum(["local", "remote"]).parse(args[++i]);
|
|
2271
2836
|
} else if (arg === "--claude-env") {
|
|
@@ -2301,7 +2866,7 @@ ${chalk.bold("Options:")}
|
|
|
2301
2866
|
-h, --help Show this help message
|
|
2302
2867
|
-v, --version Show version
|
|
2303
2868
|
-m, --model <model> Claude model to use (default: sonnet)
|
|
2304
|
-
-p, --permission-mode Permission mode:
|
|
2869
|
+
-p, --permission-mode Permission mode: default, acceptEdits, bypassPermissions, or plan
|
|
2305
2870
|
--auth, --login Force re-authentication
|
|
2306
2871
|
--claude-env KEY=VALUE Set environment variable for Claude Code
|
|
2307
2872
|
--claude-arg ARG Pass additional argument to Claude CLI
|
|
@@ -2373,7 +2938,7 @@ ${chalk.bold("Examples:")}
|
|
|
2373
2938
|
await writeSettings(settings);
|
|
2374
2939
|
}
|
|
2375
2940
|
if (settings.daemonAutoStartWhenRunningHappy) {
|
|
2376
|
-
console.
|
|
2941
|
+
console.debug("Starting Happy background service...");
|
|
2377
2942
|
if (!await isDaemonRunning()) {
|
|
2378
2943
|
const happyPath = process.argv[1];
|
|
2379
2944
|
const isBuiltBinary = happyPath.endsWith("/bin/happy") || happyPath.endsWith("\\bin\\happy");
|