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.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-DD9P_5rj.mjs';
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]`), exhaustiveCheck);
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
- const r = await response.interrupt();
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 messages`);
578
+ logger.debug(`[claudeRemote] Starting query with permission mode: ${opts.permissionMode}`);
230
579
  response = query({
231
- prompt: opts.messages,
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
- logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${message.session_id}`);
260
- const projectDir = getProjectPath(opts.path);
261
- const found = await awaitFileExist(join(projectDir, `${message.session_id}.jsonl`));
262
- logger.debug(`[claudeRemote] Session file found: ${message.session_id} ${found}`);
263
- opts.onSessionFound(message.session_id);
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 MessageQueue {
835
+ class MessageQueue2 {
836
+ constructor(modeHasher) {
837
+ this.modeHasher = modeHasher;
838
+ logger.debug(`[MessageQueue2] Initialized`);
839
+ }
429
840
  queue = [];
430
- waiters = [];
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
- logger.debug(`[MessageQueue] push() called. Waiters: ${this.waiters.length}, Queue size before: ${this.queue.length}`);
447
- const waiter = this.waiters.shift();
448
- if (waiter) {
449
- logger.debug(`[MessageQueue] Found waiter! Delivering message directly: "${message}"`);
450
- waiter({
451
- type: "user",
452
- message: {
453
- role: "user",
454
- content: message
455
- },
456
- parent_tool_use_id: null,
457
- session_id: ""
458
- });
459
- } else {
460
- logger.debug(`[MessageQueue] No waiter found. Adding to queue: "${message}"`);
461
- this.queue.push({
462
- type: "user",
463
- message: {
464
- role: "user",
465
- content: message
466
- },
467
- parent_tool_use_id: null,
468
- session_id: ""
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(`[MessageQueue] push() completed. Waiters: ${this.waiters.length}, Queue size after: ${this.queue.length}`);
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(`[MessageQueue] close() called. Waiters: ${this.waiters.length}`);
891
+ logger.debug(`[MessageQueue2] close() called`);
478
892
  this.closed = true;
479
- this.closeResolve?.();
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
- * Async iterator implementation
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 *[Symbol.asyncIterator]() {
497
- logger.debug(`[MessageQueue] Iterator started`);
498
- while (true) {
499
- const message = this.queue.shift();
500
- if (message !== void 0) {
501
- logger.debug(`[MessageQueue] Iterator yielding queued message`);
502
- yield message;
503
- continue;
504
- }
505
- if (this.closed) {
506
- logger.debug(`[MessageQueue] Iterator ending - queue closed`);
507
- return;
508
- }
509
- logger.debug(`[MessageQueue] Iterator waiting for next message...`);
510
- const nextMessage = await this.waitForNext();
511
- if (nextMessage === void 0) {
512
- logger.debug(`[MessageQueue] Iterator ending - no more messages`);
513
- return;
514
- }
515
- logger.debug(`[MessageQueue] Iterator yielding waited message`);
516
- 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;
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 the next message or queue closure
951
+ * Wait for messages to arrive
521
952
  */
522
- waitForNext() {
953
+ waitForMessages(abortSignal) {
523
954
  return new Promise((resolve) => {
524
- if (this.closed) {
525
- logger.debug(`[MessageQueue] waitForNext() called but queue is closed`);
526
- resolve(void 0);
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
- const waiter = (value) => resolve(value);
530
- this.waiters.push(waiter);
531
- logger.debug(`[MessageQueue] waitForNext() adding waiter. Total waiters: ${this.waiters.length}`);
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
- opts.onMessage(message);
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 currentMessageQueue = new MessageQueue();
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
- currentMessageQueue.push(message.content.text);
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
- messages: currentMessageQueue,
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.14";
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
- const id = randomUUID();
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
- requests.set(id, resolve);
1968
+ if (request.name === "exit_plan_mode") {
1969
+ const wrappedResolve = (response2) => {
1970
+ if (response2.approved) {
1971
+ logger.debug("[HACK] exit_plan_mode approved - injecting approval message and denying");
1972
+ sessionScanner.onRemoteUserMessageForDeduplication(PLAN_FAKE_RESTART);
1973
+ messageQueue.unshift(PLAN_FAKE_RESTART, "default");
1974
+ logger.debug(`[HACK] Message queue size after unshift: ${messageQueue.size()}`);
1975
+ resolve({
1976
+ approved: false,
1977
+ reason: PLAN_FAKE_REJECT
1978
+ });
1979
+ } else {
1980
+ resolve(response2);
1981
+ }
1982
+ };
1983
+ requests.set(id, wrappedResolve);
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(["auto", "default", "plan"]).parse(args[++i]);
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: auto, default, or plan
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.log("Starting Happy background service...");
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");