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 CHANGED
@@ -1,16 +1,15 @@
1
1
  'use strict';
2
2
 
3
3
  var chalk = require('chalk');
4
- var types = require('./types-Cg4664gs.cjs');
4
+ var types = require('./types-CkPUFpfr.cjs');
5
5
  var node_crypto = require('node:crypto');
6
- var claudeCode = require('@anthropic-ai/claude-code');
6
+ var node_child_process = require('node:child_process');
7
+ var node_readline = require('node:readline');
7
8
  var node_fs = require('node:fs');
8
9
  var node_path = require('node:path');
10
+ var node_url = require('node:url');
9
11
  var os = require('node:os');
10
12
  var promises = require('fs/promises');
11
- var node_child_process = require('node:child_process');
12
- var node_readline = require('node:readline');
13
- var node_url = require('node:url');
14
13
  var promises$1 = require('node:fs/promises');
15
14
  var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
16
15
  var node_http = require('node:http');
@@ -50,6 +49,343 @@ function _interopNamespaceDefault(e) {
50
49
 
51
50
  var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
52
51
 
52
+ class Stream {
53
+ constructor(returned) {
54
+ this.returned = returned;
55
+ }
56
+ queue = [];
57
+ readResolve;
58
+ readReject;
59
+ isDone = false;
60
+ hasError;
61
+ started = false;
62
+ /**
63
+ * Implements async iterable protocol
64
+ */
65
+ [Symbol.asyncIterator]() {
66
+ if (this.started) {
67
+ throw new Error("Stream can only be iterated once");
68
+ }
69
+ this.started = true;
70
+ return this;
71
+ }
72
+ /**
73
+ * Gets the next value from the stream
74
+ */
75
+ async next() {
76
+ if (this.queue.length > 0) {
77
+ return Promise.resolve({
78
+ done: false,
79
+ value: this.queue.shift()
80
+ });
81
+ }
82
+ if (this.isDone) {
83
+ return Promise.resolve({ done: true, value: void 0 });
84
+ }
85
+ if (this.hasError) {
86
+ return Promise.reject(this.hasError);
87
+ }
88
+ return new Promise((resolve, reject) => {
89
+ this.readResolve = resolve;
90
+ this.readReject = reject;
91
+ });
92
+ }
93
+ /**
94
+ * Adds a value to the stream
95
+ */
96
+ enqueue(value) {
97
+ if (this.readResolve) {
98
+ const resolve = this.readResolve;
99
+ this.readResolve = void 0;
100
+ this.readReject = void 0;
101
+ resolve({ done: false, value });
102
+ } else {
103
+ this.queue.push(value);
104
+ }
105
+ }
106
+ /**
107
+ * Marks the stream as complete
108
+ */
109
+ done() {
110
+ this.isDone = true;
111
+ if (this.readResolve) {
112
+ const resolve = this.readResolve;
113
+ this.readResolve = void 0;
114
+ this.readReject = void 0;
115
+ resolve({ done: true, value: void 0 });
116
+ }
117
+ }
118
+ /**
119
+ * Propagates an error through the stream
120
+ */
121
+ error(error) {
122
+ this.hasError = error;
123
+ if (this.readReject) {
124
+ const reject = this.readReject;
125
+ this.readResolve = void 0;
126
+ this.readReject = void 0;
127
+ reject(error);
128
+ }
129
+ }
130
+ /**
131
+ * Implements async iterator cleanup
132
+ */
133
+ async return() {
134
+ this.isDone = true;
135
+ if (this.returned) {
136
+ this.returned();
137
+ }
138
+ return Promise.resolve({ done: true, value: void 0 });
139
+ }
140
+ }
141
+
142
+ class AbortError extends Error {
143
+ constructor(message) {
144
+ super(message);
145
+ this.name = "AbortError";
146
+ }
147
+ }
148
+
149
+ const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
150
+ const __dirname$3 = node_path.join(__filename$1, "..");
151
+ function getDefaultClaudeCodePath() {
152
+ return node_path.join(__dirname$3, "..", "..", "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
153
+ }
154
+ function logDebug(message) {
155
+ if (process.env.DEBUG) {
156
+ types.logger.debug(message);
157
+ console.log(message);
158
+ }
159
+ }
160
+ async function streamToStdin(stream, stdin, abortController) {
161
+ for await (const message of stream) {
162
+ if (abortController.signal.aborted) break;
163
+ stdin.write(JSON.stringify(message) + "\n");
164
+ }
165
+ stdin.end();
166
+ }
167
+
168
+ class Query {
169
+ constructor(childStdin, childStdout, processExitPromise) {
170
+ this.childStdin = childStdin;
171
+ this.childStdout = childStdout;
172
+ this.processExitPromise = processExitPromise;
173
+ this.readMessages();
174
+ this.sdkMessages = this.readSdkMessages();
175
+ }
176
+ pendingControlResponses = /* @__PURE__ */ new Map();
177
+ sdkMessages;
178
+ inputStream = new Stream();
179
+ /**
180
+ * Set an error on the stream
181
+ */
182
+ setError(error) {
183
+ this.inputStream.error(error);
184
+ }
185
+ /**
186
+ * AsyncIterableIterator implementation
187
+ */
188
+ next(...args) {
189
+ return this.sdkMessages.next(...args);
190
+ }
191
+ return(value) {
192
+ if (this.sdkMessages.return) {
193
+ return this.sdkMessages.return(value);
194
+ }
195
+ return Promise.resolve({ done: true, value: void 0 });
196
+ }
197
+ throw(e) {
198
+ if (this.sdkMessages.throw) {
199
+ return this.sdkMessages.throw(e);
200
+ }
201
+ return Promise.reject(e);
202
+ }
203
+ [Symbol.asyncIterator]() {
204
+ return this.sdkMessages;
205
+ }
206
+ /**
207
+ * Read messages from Claude process stdout
208
+ */
209
+ async readMessages() {
210
+ const rl = node_readline.createInterface({ input: this.childStdout });
211
+ try {
212
+ for await (const line of rl) {
213
+ if (line.trim()) {
214
+ const message = JSON.parse(line);
215
+ if (message.type === "control_response") {
216
+ const controlResponse = message;
217
+ const handler = this.pendingControlResponses.get(controlResponse.response.request_id);
218
+ if (handler) {
219
+ handler(controlResponse.response);
220
+ }
221
+ continue;
222
+ }
223
+ this.inputStream.enqueue(message);
224
+ }
225
+ }
226
+ await this.processExitPromise;
227
+ } catch (error) {
228
+ this.inputStream.error(error);
229
+ } finally {
230
+ this.inputStream.done();
231
+ rl.close();
232
+ }
233
+ }
234
+ /**
235
+ * Async generator for SDK messages
236
+ */
237
+ async *readSdkMessages() {
238
+ for await (const message of this.inputStream) {
239
+ yield message;
240
+ }
241
+ }
242
+ /**
243
+ * Send interrupt request to Claude
244
+ */
245
+ async interrupt() {
246
+ if (!this.childStdin) {
247
+ throw new Error("Interrupt requires --input-format stream-json");
248
+ }
249
+ await this.request({
250
+ subtype: "interrupt"
251
+ }, this.childStdin);
252
+ }
253
+ /**
254
+ * Send control request to Claude process
255
+ */
256
+ request(request, childStdin) {
257
+ const requestId = Math.random().toString(36).substring(2, 15);
258
+ const sdkRequest = {
259
+ request_id: requestId,
260
+ type: "control_request",
261
+ request
262
+ };
263
+ return new Promise((resolve, reject) => {
264
+ this.pendingControlResponses.set(requestId, (response) => {
265
+ if (response.subtype === "success") {
266
+ resolve(response);
267
+ } else {
268
+ reject(new Error(response.error));
269
+ }
270
+ });
271
+ childStdin.write(JSON.stringify(sdkRequest) + "\n");
272
+ });
273
+ }
274
+ }
275
+ function query(config) {
276
+ const {
277
+ prompt,
278
+ abortController = config.abortController || new AbortController(),
279
+ options: {
280
+ allowedTools = [],
281
+ appendSystemPrompt,
282
+ customSystemPrompt,
283
+ cwd,
284
+ disallowedTools = [],
285
+ executable = "node",
286
+ executableArgs = [],
287
+ maxTurns,
288
+ mcpServers,
289
+ pathToClaudeCodeExecutable = getDefaultClaudeCodePath(),
290
+ permissionMode = "default",
291
+ permissionPromptToolName,
292
+ continue: continueConversation,
293
+ resume,
294
+ model,
295
+ fallbackModel,
296
+ strictMcpConfig
297
+ } = {}
298
+ } = config;
299
+ if (!process.env.CLAUDE_CODE_ENTRYPOINT) {
300
+ process.env.CLAUDE_CODE_ENTRYPOINT = "sdk-ts";
301
+ }
302
+ const args = ["--output-format", "stream-json", "--verbose"];
303
+ if (customSystemPrompt) args.push("--system-prompt", customSystemPrompt);
304
+ if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
305
+ if (maxTurns) args.push("--max-turns", maxTurns.toString());
306
+ if (model) args.push("--model", model);
307
+ if (permissionPromptToolName) args.push("--permission-prompt-tool", permissionPromptToolName);
308
+ if (continueConversation) args.push("--continue");
309
+ if (resume) args.push("--resume", resume);
310
+ if (allowedTools.length > 0) args.push("--allowedTools", allowedTools.join(","));
311
+ if (disallowedTools.length > 0) args.push("--disallowedTools", disallowedTools.join(","));
312
+ if (mcpServers && Object.keys(mcpServers).length > 0) {
313
+ args.push("--mcp-config", JSON.stringify({ mcpServers }));
314
+ }
315
+ if (strictMcpConfig) args.push("--strict-mcp-config");
316
+ if (permissionMode) args.push("--permission-mode", permissionMode);
317
+ if (fallbackModel) {
318
+ if (model && fallbackModel === model) {
319
+ throw new Error("Fallback model cannot be the same as the main model. Please specify a different model for fallbackModel option.");
320
+ }
321
+ args.push("--fallback-model", fallbackModel);
322
+ }
323
+ if (typeof prompt === "string") {
324
+ args.push("--print", prompt.trim());
325
+ } else {
326
+ args.push("--input-format", "stream-json");
327
+ }
328
+ if (!node_fs.existsSync(pathToClaudeCodeExecutable)) {
329
+ throw new ReferenceError(`Claude Code executable not found at ${pathToClaudeCodeExecutable}. Is options.pathToClaudeCodeExecutable set?`);
330
+ }
331
+ logDebug(`Spawning Claude Code process: ${executable} ${[...executableArgs, pathToClaudeCodeExecutable, ...args].join(" ")}`);
332
+ const child = node_child_process.spawn(executable, [...executableArgs, pathToClaudeCodeExecutable, ...args], {
333
+ cwd,
334
+ stdio: ["pipe", "pipe", "pipe"],
335
+ signal: abortController.signal,
336
+ env: {
337
+ ...process.env
338
+ }
339
+ });
340
+ let childStdin = null;
341
+ if (typeof prompt === "string") {
342
+ child.stdin.end();
343
+ } else {
344
+ streamToStdin(prompt, child.stdin, abortController);
345
+ childStdin = child.stdin;
346
+ }
347
+ if (process.env.DEBUG) {
348
+ child.stderr.on("data", (data) => {
349
+ console.error("Claude Code stderr:", data.toString());
350
+ });
351
+ }
352
+ const cleanup = () => {
353
+ if (!child.killed) {
354
+ child.kill("SIGTERM");
355
+ }
356
+ };
357
+ abortController.signal.addEventListener("abort", cleanup);
358
+ process.on("exit", cleanup);
359
+ const processExitPromise = new Promise((resolve) => {
360
+ child.on("close", (code) => {
361
+ if (abortController.signal.aborted) {
362
+ query2.setError(new AbortError("Claude Code process aborted by user"));
363
+ }
364
+ if (code !== 0) {
365
+ query2.setError(new Error(`Claude Code process exited with code ${code}`));
366
+ } else {
367
+ resolve();
368
+ }
369
+ });
370
+ });
371
+ const query2 = new Query(childStdin, child.stdout, processExitPromise);
372
+ child.on("error", (error) => {
373
+ if (abortController.signal.aborted) {
374
+ query2.setError(new AbortError("Claude Code process aborted by user"));
375
+ } else {
376
+ query2.setError(new Error(`Failed to spawn Claude Code process: ${error.message}`));
377
+ }
378
+ });
379
+ processExitPromise.finally(() => {
380
+ cleanup();
381
+ abortController.signal.removeEventListener("abort", cleanup);
382
+ if (process.env.CLAUDE_SDK_MCP_SERVERS) {
383
+ delete process.env.CLAUDE_SDK_MCP_SERVERS;
384
+ }
385
+ });
386
+ return query2;
387
+ }
388
+
53
389
  function formatClaudeMessage(message, onAssistantResult) {
54
390
  types.logger.debugLargeJson("[CLAUDE] Message from non interactive & remote mode:", message);
55
391
  switch (message.type) {
@@ -159,9 +495,8 @@ function formatClaudeMessage(message, onAssistantResult) {
159
495
  break;
160
496
  }
161
497
  default: {
162
- const exhaustiveCheck = message;
163
498
  if (process.env.DEBUG) {
164
- console.log(chalk.gray(`[Unknown message type]`), exhaustiveCheck);
499
+ console.log(chalk.gray(`[Unknown message type: ${message.type}]`));
165
500
  }
166
501
  }
167
502
  }
@@ -207,6 +542,19 @@ async function awaitFileExist(file, timeout = 1e4) {
207
542
  return false;
208
543
  }
209
544
 
545
+ function deepEqual(a, b) {
546
+ if (a === b) return true;
547
+ if (a == null || b == null) return false;
548
+ if (typeof a !== "object" || typeof b !== "object") return false;
549
+ const keysA = Object.keys(a);
550
+ const keysB = Object.keys(b);
551
+ if (keysA.length !== keysB.length) return false;
552
+ for (const key of keysA) {
553
+ if (!keysB.includes(key)) return false;
554
+ if (!deepEqual(a[key], b[key])) return false;
555
+ }
556
+ return true;
557
+ }
210
558
  async function claudeRemote(opts) {
211
559
  let startFrom = opts.sessionId;
212
560
  if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
@@ -223,6 +571,7 @@ async function claudeRemote(opts) {
223
571
  resume: startFrom ?? void 0,
224
572
  mcpServers: opts.mcpServers,
225
573
  permissionPromptToolName: opts.permissionPromptToolName,
574
+ permissionMode: opts.permissionMode,
226
575
  executable: "node",
227
576
  abortController
228
577
  };
@@ -237,7 +586,7 @@ async function claudeRemote(opts) {
237
586
  if (response) {
238
587
  (async () => {
239
588
  try {
240
- const r = await response.interrupt();
589
+ await response.interrupt();
241
590
  } catch (e) {
242
591
  }
243
592
  abortController.abort();
@@ -247,10 +596,9 @@ async function claudeRemote(opts) {
247
596
  }
248
597
  }
249
598
  });
250
- types.logger.debug(`[claudeRemote] Starting query with messages`);
251
- response = claudeCode.query({
252
- prompt: opts.messages,
253
- abortController,
599
+ types.logger.debug(`[claudeRemote] Starting query with permission mode: ${opts.permissionMode}`);
600
+ response = query({
601
+ prompt: opts.message,
254
602
  options: sdkOptions
255
603
  });
256
604
  if (opts.interruptController) {
@@ -270,21 +618,76 @@ async function claudeRemote(opts) {
270
618
  }
271
619
  }
272
620
  };
621
+ const toolCalls = [];
622
+ const resolveToolCallId = (name, args) => {
623
+ for (let i = toolCalls.length - 1; i >= 0; i--) {
624
+ const call = toolCalls[i];
625
+ if (call.name === name && deepEqual(call.input, args)) {
626
+ if (call.used) {
627
+ types.logger.debug("[claudeRemote] Warning: Permission request matched an already-used tool call");
628
+ return null;
629
+ }
630
+ call.used = true;
631
+ types.logger.debug(`[claudeRemote] Resolved tool call ID: ${call.id} for ${name}`);
632
+ return call.id;
633
+ }
634
+ }
635
+ types.logger.debug(`[claudeRemote] No matching tool call found for permission request: ${name}`);
636
+ return null;
637
+ };
638
+ if (opts.onToolCallResolver) {
639
+ opts.onToolCallResolver(resolveToolCallId);
640
+ }
273
641
  try {
274
642
  types.logger.debug(`[claudeRemote] Starting to iterate over response`);
275
643
  for await (const message of response) {
276
644
  types.logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
277
645
  formatClaudeMessage(message, opts.onAssistantResult);
646
+ if (message.type === "assistant") {
647
+ const assistantMsg = message;
648
+ if (assistantMsg.message && assistantMsg.message.content) {
649
+ for (const block of assistantMsg.message.content) {
650
+ if (block.type === "tool_use") {
651
+ toolCalls.push({
652
+ id: block.id,
653
+ name: block.name,
654
+ input: block.input,
655
+ used: false
656
+ });
657
+ types.logger.debug(`[claudeRemote] Tracked tool call: ${block.id} - ${block.name}`);
658
+ }
659
+ }
660
+ }
661
+ }
662
+ if (message.type === "user") {
663
+ const userMsg = message;
664
+ if (userMsg.message && userMsg.message.content && Array.isArray(userMsg.message.content)) {
665
+ for (const block of userMsg.message.content) {
666
+ if (block.type === "tool_result" && block.tool_use_id) {
667
+ const toolCall = toolCalls.find((tc) => tc.id === block.tool_use_id);
668
+ if (toolCall && !toolCall.used) {
669
+ toolCall.used = true;
670
+ types.logger.debug(`[claudeRemote] Tool completed execution, marked as used: ${block.tool_use_id}`);
671
+ }
672
+ }
673
+ }
674
+ }
675
+ }
278
676
  if (message.type === "system" && message.subtype === "init") {
279
677
  updateThinking(true);
280
- types.logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${message.session_id}`);
281
- const projectDir = getProjectPath(opts.path);
282
- const found = await awaitFileExist(node_path.join(projectDir, `${message.session_id}.jsonl`));
283
- types.logger.debug(`[claudeRemote] Session file found: ${message.session_id} ${found}`);
284
- opts.onSessionFound(message.session_id);
678
+ const systemInit = message;
679
+ if (systemInit.session_id) {
680
+ types.logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${systemInit.session_id}`);
681
+ const projectDir = getProjectPath(opts.path);
682
+ const found = await awaitFileExist(node_path.join(projectDir, `${systemInit.session_id}.jsonl`));
683
+ types.logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
684
+ opts.onSessionFound(systemInit.session_id);
685
+ }
285
686
  }
286
687
  if (message.type === "result") {
287
688
  updateThinking(false);
689
+ types.logger.debug("[claudeRemote] Result received, exiting claudeRemote");
690
+ break;
288
691
  }
289
692
  }
290
693
  types.logger.debug(`[claudeRemote] Finished iterating over response`);
@@ -292,13 +695,17 @@ async function claudeRemote(opts) {
292
695
  if (abortController.signal.aborted) {
293
696
  types.logger.debug(`[claudeRemote] Aborted`);
294
697
  }
295
- if (e instanceof claudeCode.AbortError) {
698
+ if (e instanceof AbortError) {
296
699
  types.logger.debug(`[claudeRemote] Aborted`);
297
700
  } else {
298
701
  throw e;
299
702
  }
300
703
  } finally {
301
704
  updateThinking(false);
705
+ toolCalls.length = 0;
706
+ if (opts.onToolCallResolver) {
707
+ opts.onToolCallResolver(null);
708
+ }
302
709
  if (opts.interruptController) {
303
710
  opts.interruptController.unregister();
304
711
  }
@@ -446,58 +853,69 @@ async function claudeLocal(opts) {
446
853
  return resolvedSessionId;
447
854
  }
448
855
 
449
- class MessageQueue {
856
+ class MessageQueue2 {
857
+ constructor(modeHasher) {
858
+ this.modeHasher = modeHasher;
859
+ types.logger.debug(`[MessageQueue2] Initialized`);
860
+ }
450
861
  queue = [];
451
- waiters = [];
862
+ waiter = null;
452
863
  closed = false;
453
- closePromise;
454
- closeResolve;
455
- constructor() {
456
- this.closePromise = new Promise((resolve) => {
457
- this.closeResolve = resolve;
458
- });
459
- }
460
864
  /**
461
- * Push a message to the queue
865
+ * Push a message to the queue with a mode.
462
866
  */
463
- push(message) {
867
+ push(message, mode) {
464
868
  if (this.closed) {
465
869
  throw new Error("Cannot push to closed queue");
466
870
  }
467
- types.logger.debug(`[MessageQueue] push() called. Waiters: ${this.waiters.length}, Queue size before: ${this.queue.length}`);
468
- const waiter = this.waiters.shift();
469
- if (waiter) {
470
- types.logger.debug(`[MessageQueue] Found waiter! Delivering message directly: "${message}"`);
471
- waiter({
472
- type: "user",
473
- message: {
474
- role: "user",
475
- content: message
476
- },
477
- parent_tool_use_id: null,
478
- session_id: ""
479
- });
480
- } else {
481
- types.logger.debug(`[MessageQueue] No waiter found. Adding to queue: "${message}"`);
482
- this.queue.push({
483
- type: "user",
484
- message: {
485
- role: "user",
486
- content: message
487
- },
488
- parent_tool_use_id: null,
489
- session_id: ""
490
- });
871
+ const modeHash = this.modeHasher(mode);
872
+ types.logger.debug(`[MessageQueue2] push() called with mode hash: ${modeHash}`);
873
+ this.queue.push({
874
+ message,
875
+ mode,
876
+ modeHash
877
+ });
878
+ if (this.waiter) {
879
+ types.logger.debug(`[MessageQueue2] Notifying waiter`);
880
+ const waiter = this.waiter;
881
+ this.waiter = null;
882
+ waiter(true);
883
+ }
884
+ types.logger.debug(`[MessageQueue2] push() completed. Queue size: ${this.queue.length}`);
885
+ }
886
+ /**
887
+ * Push a message to the beginning of the queue with a mode.
888
+ */
889
+ unshift(message, mode) {
890
+ if (this.closed) {
891
+ throw new Error("Cannot unshift to closed queue");
892
+ }
893
+ const modeHash = this.modeHasher(mode);
894
+ types.logger.debug(`[MessageQueue2] unshift() called with mode hash: ${modeHash}`);
895
+ this.queue.unshift({
896
+ message,
897
+ mode,
898
+ modeHash
899
+ });
900
+ if (this.waiter) {
901
+ types.logger.debug(`[MessageQueue2] Notifying waiter`);
902
+ const waiter = this.waiter;
903
+ this.waiter = null;
904
+ waiter(true);
491
905
  }
492
- types.logger.debug(`[MessageQueue] push() completed. Waiters: ${this.waiters.length}, Queue size after: ${this.queue.length}`);
906
+ types.logger.debug(`[MessageQueue2] unshift() completed. Queue size: ${this.queue.length}`);
493
907
  }
494
908
  /**
495
909
  * Close the queue - no more messages can be pushed
496
910
  */
497
911
  close() {
498
- types.logger.debug(`[MessageQueue] close() called. Waiters: ${this.waiters.length}`);
912
+ types.logger.debug(`[MessageQueue2] close() called`);
499
913
  this.closed = true;
500
- this.closeResolve?.();
914
+ if (this.waiter) {
915
+ const waiter = this.waiter;
916
+ this.waiter = null;
917
+ waiter(false);
918
+ }
501
919
  }
502
920
  /**
503
921
  * Check if the queue is closed
@@ -512,56 +930,91 @@ class MessageQueue {
512
930
  return this.queue.length;
513
931
  }
514
932
  /**
515
- * Async iterator implementation
933
+ * Wait for messages and return all messages with the same mode as a single string
934
+ * Returns { message: string, mode: T } or null if aborted/closed
516
935
  */
517
- async *[Symbol.asyncIterator]() {
518
- types.logger.debug(`[MessageQueue] Iterator started`);
519
- while (true) {
520
- const message = this.queue.shift();
521
- if (message !== void 0) {
522
- types.logger.debug(`[MessageQueue] Iterator yielding queued message`);
523
- yield message;
524
- continue;
525
- }
526
- if (this.closed) {
527
- types.logger.debug(`[MessageQueue] Iterator ending - queue closed`);
528
- return;
529
- }
530
- types.logger.debug(`[MessageQueue] Iterator waiting for next message...`);
531
- const nextMessage = await this.waitForNext();
532
- if (nextMessage === void 0) {
533
- types.logger.debug(`[MessageQueue] Iterator ending - no more messages`);
534
- return;
535
- }
536
- types.logger.debug(`[MessageQueue] Iterator yielding waited message`);
537
- yield nextMessage;
936
+ async waitForMessagesAndGetAsString(abortSignal) {
937
+ if (this.queue.length > 0) {
938
+ return this.collectBatch();
939
+ }
940
+ if (this.closed || abortSignal?.aborted) {
941
+ return null;
942
+ }
943
+ const hasMessages = await this.waitForMessages(abortSignal);
944
+ if (!hasMessages) {
945
+ return null;
946
+ }
947
+ return this.collectBatch();
948
+ }
949
+ /**
950
+ * Collect a batch of messages with the same mode
951
+ */
952
+ collectBatch() {
953
+ if (this.queue.length === 0) {
954
+ return null;
955
+ }
956
+ const firstItem = this.queue[0];
957
+ const sameModeMessages = [];
958
+ let mode = firstItem.mode;
959
+ const targetModeHash = firstItem.modeHash;
960
+ while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash) {
961
+ const item = this.queue.shift();
962
+ sameModeMessages.push(item.message);
538
963
  }
964
+ const combinedMessage = sameModeMessages.join("\n");
965
+ types.logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
966
+ return {
967
+ message: combinedMessage,
968
+ mode
969
+ };
539
970
  }
540
971
  /**
541
- * Wait for the next message or queue closure
972
+ * Wait for messages to arrive
542
973
  */
543
- waitForNext() {
974
+ waitForMessages(abortSignal) {
544
975
  return new Promise((resolve) => {
545
- if (this.closed) {
546
- types.logger.debug(`[MessageQueue] waitForNext() called but queue is closed`);
547
- resolve(void 0);
976
+ let abortHandler = null;
977
+ if (abortSignal) {
978
+ abortHandler = () => {
979
+ types.logger.debug("[MessageQueue2] Wait aborted");
980
+ if (this.waiter === waiterFunc) {
981
+ this.waiter = null;
982
+ }
983
+ resolve(false);
984
+ };
985
+ abortSignal.addEventListener("abort", abortHandler);
986
+ }
987
+ const waiterFunc = (hasMessages) => {
988
+ if (abortHandler && abortSignal) {
989
+ abortSignal.removeEventListener("abort", abortHandler);
990
+ }
991
+ resolve(hasMessages);
992
+ };
993
+ if (this.queue.length > 0) {
994
+ if (abortHandler && abortSignal) {
995
+ abortSignal.removeEventListener("abort", abortHandler);
996
+ }
997
+ resolve(true);
548
998
  return;
549
999
  }
550
- const waiter = (value) => resolve(value);
551
- this.waiters.push(waiter);
552
- types.logger.debug(`[MessageQueue] waitForNext() adding waiter. Total waiters: ${this.waiters.length}`);
553
- this.closePromise?.then(() => {
554
- const index = this.waiters.indexOf(waiter);
555
- if (index !== -1) {
556
- this.waiters.splice(index, 1);
557
- types.logger.debug(`[MessageQueue] waitForNext() waiter removed due to close. Remaining waiters: ${this.waiters.length}`);
558
- resolve(void 0);
1000
+ if (this.closed || abortSignal?.aborted) {
1001
+ if (abortHandler && abortSignal) {
1002
+ abortSignal.removeEventListener("abort", abortHandler);
559
1003
  }
560
- });
1004
+ resolve(false);
1005
+ return;
1006
+ }
1007
+ this.waiter = waiterFunc;
1008
+ types.logger.debug("[MessageQueue2] Waiting for messages...");
561
1009
  });
562
1010
  }
563
1011
  }
564
1012
 
1013
+ var MessageQueue2$1 = /*#__PURE__*/Object.freeze({
1014
+ __proto__: null,
1015
+ MessageQueue2: MessageQueue2
1016
+ });
1017
+
565
1018
  class InvalidateSync {
566
1019
  _invalidated = false;
567
1020
  _invalidatedDouble = false;
@@ -656,6 +1109,39 @@ function startFileWatcher(file, onFileChange) {
656
1109
  };
657
1110
  }
658
1111
 
1112
+ const PLAN_FAKE_REJECT = `User approved plan, but you need to be restarted. STOP IMMEDIATELY TO SWITCH FROM PLAN MODE. DO NOT REPLY TO THIS MESSAGE.`;
1113
+ const PLAN_FAKE_RESTART = `PlEaZe Continue with plan.`;
1114
+
1115
+ function hackToolResponse(message) {
1116
+ console.debug("hackToolResponse", JSON.stringify(message, null, 2));
1117
+ if (message.type === "user" && message.message?.role === "user" && message.message?.content && Array.isArray(message.message.content)) {
1118
+ let modified = false;
1119
+ const hackedContent = message.message.content.map((item) => {
1120
+ if (item.type === "tool_result" && item.is_error === true) {
1121
+ if (item.content === PLAN_FAKE_REJECT) {
1122
+ types.logger.debug(`[SESSION_SCANNER] Hacking exit_plan_mode tool_result: flipping is_error from true to false`);
1123
+ modified = true;
1124
+ return {
1125
+ ...item,
1126
+ is_error: false,
1127
+ content: "Plan approved"
1128
+ };
1129
+ }
1130
+ }
1131
+ return item;
1132
+ });
1133
+ if (modified) {
1134
+ return {
1135
+ ...message,
1136
+ message: {
1137
+ ...message.message,
1138
+ content: hackedContent
1139
+ }
1140
+ };
1141
+ }
1142
+ }
1143
+ return message;
1144
+ }
659
1145
  function createSessionScanner(opts) {
660
1146
  const projectDir = getProjectPath(opts.workingDirectory);
661
1147
  let finishedSessions = /* @__PURE__ */ new Set();
@@ -708,7 +1194,8 @@ function createSessionScanner(opts) {
708
1194
  continue;
709
1195
  }
710
1196
  }
711
- opts.onMessage(message);
1197
+ const hackedMessage = hackToolResponse(message);
1198
+ opts.onMessage(hackedMessage);
712
1199
  } catch (e) {
713
1200
  types.logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
714
1201
  continue;
@@ -808,10 +1295,15 @@ function sortKeys(value) {
808
1295
 
809
1296
  async function loop(opts) {
810
1297
  let mode = opts.startingMode ?? "local";
811
- let currentMessageQueue = new MessageQueue();
1298
+ let currentPermissionMode = opts.permissionMode ?? "default";
1299
+ types.logger.debug(`[loop] Starting with permission mode: ${currentPermissionMode}`);
1300
+ let currentMessageQueue = opts.messageQueue || new MessageQueue2(
1301
+ (mode2) => mode2
1302
+ // Simple string hasher since modes are already strings
1303
+ );
812
1304
  let sessionId = null;
813
1305
  let onMessage = null;
814
- const sessionScanner = createSessionScanner({
1306
+ const sessionScanner = opts.sessionScanner || createSessionScanner({
815
1307
  workingDirectory: opts.path,
816
1308
  onMessage: (message) => {
817
1309
  opts.session.sendClaudeSessionMessage(message);
@@ -819,7 +1311,20 @@ async function loop(opts) {
819
1311
  });
820
1312
  opts.session.onUserMessage((message) => {
821
1313
  sessionScanner.onRemoteUserMessageForDeduplication(message.content.text);
822
- currentMessageQueue.push(message.content.text);
1314
+ let messagePermissionMode = currentPermissionMode;
1315
+ if (message.meta?.permissionMode) {
1316
+ const validModes = ["default", "acceptEdits", "bypassPermissions", "plan"];
1317
+ if (validModes.includes(message.meta.permissionMode)) {
1318
+ messagePermissionMode = message.meta.permissionMode;
1319
+ currentPermissionMode = messagePermissionMode;
1320
+ types.logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
1321
+ } else {
1322
+ types.logger.info(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
1323
+ }
1324
+ } else {
1325
+ types.logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
1326
+ }
1327
+ currentMessageQueue.push(message.content.text, messagePermissionMode);
823
1328
  types.logger.debugLargeJson("User message pushed to queue:", message);
824
1329
  if (onMessage) {
825
1330
  onMessage();
@@ -830,6 +1335,7 @@ async function loop(opts) {
830
1335
  sessionScanner.onNewSession(newSessionId);
831
1336
  };
832
1337
  while (true) {
1338
+ types.logger.debug(`[loop] Starting loop iteration, queue size: ${currentMessageQueue.size()}, mode: ${mode}`);
833
1339
  if (currentMessageQueue.size() > 0) {
834
1340
  if (mode !== "remote") {
835
1341
  mode = "remote";
@@ -837,7 +1343,6 @@ async function loop(opts) {
837
1343
  opts.onModeChange(mode);
838
1344
  }
839
1345
  }
840
- continue;
841
1346
  }
842
1347
  if (mode === "local") {
843
1348
  let abortedOutside = false;
@@ -904,15 +1409,16 @@ async function loop(opts) {
904
1409
  }
905
1410
  }
906
1411
  if (mode === "remote") {
1412
+ console.log("Starting remote mode...");
907
1413
  types.logger.debug("Starting " + sessionId);
908
1414
  const remoteAbortController = new AbortController();
909
1415
  opts.session.setHandler("abort", () => {
910
- if (!remoteAbortController.signal.aborted) {
1416
+ if (remoteAbortController && !remoteAbortController.signal.aborted) {
911
1417
  remoteAbortController.abort();
912
1418
  }
913
1419
  });
914
1420
  const abortHandler = () => {
915
- if (!remoteAbortController.signal.aborted) {
1421
+ if (remoteAbortController && !remoteAbortController.signal.aborted) {
916
1422
  if (mode !== "local") {
917
1423
  mode = "local";
918
1424
  if (opts.onModeChange) {
@@ -934,22 +1440,35 @@ async function loop(opts) {
934
1440
  process.stdin.on("data", abortHandler);
935
1441
  try {
936
1442
  types.logger.debug(`Starting claudeRemote with messages: ${currentMessageQueue.size()}`);
1443
+ types.logger.debug("[loop] Waiting for messages before starting claudeRemote...");
1444
+ const messageData = await currentMessageQueue.waitForMessagesAndGetAsString(remoteAbortController.signal);
1445
+ if (!messageData) {
1446
+ console.log("[LOOP] No message received (queue closed or aborted), continuing loop");
1447
+ types.logger.debug("[loop] No message received (queue closed or aborted), skipping remote mode");
1448
+ continue;
1449
+ }
1450
+ currentPermissionMode = messageData.mode;
1451
+ types.logger.debug(`[loop] Using permission mode from queue: ${currentPermissionMode}`);
937
1452
  if (opts.onProcessStart) {
938
1453
  opts.onProcessStart("remote");
939
1454
  }
1455
+ opts.session.sendSessionEvent({ type: "permission-mode-changed", mode: currentPermissionMode });
1456
+ types.logger.debug(`[loop] Sent permission-mode-changed event to app: ${currentPermissionMode}`);
940
1457
  await claudeRemote({
941
1458
  abort: remoteAbortController.signal,
942
1459
  sessionId,
943
1460
  path: opts.path,
944
1461
  mcpServers: opts.mcpServers,
945
1462
  permissionPromptToolName: opts.permissionPromptToolName,
1463
+ permissionMode: currentPermissionMode,
946
1464
  onSessionFound,
947
1465
  onThinkingChange: opts.onThinkingChange,
948
- messages: currentMessageQueue,
1466
+ message: messageData.message,
949
1467
  onAssistantResult: opts.onAssistantResult,
950
1468
  interruptController: opts.interruptController,
951
1469
  claudeEnvVars: opts.claudeEnvVars,
952
- claudeArgs: opts.claudeArgs
1470
+ claudeArgs: opts.claudeArgs,
1471
+ onToolCallResolver: opts.onToolCallResolver
953
1472
  });
954
1473
  } catch (e) {
955
1474
  if (!remoteAbortController.signal.aborted) {
@@ -963,8 +1482,6 @@ async function loop(opts) {
963
1482
  if (process.stdin.isTTY) {
964
1483
  process.stdin.setRawMode(false);
965
1484
  }
966
- currentMessageQueue.close();
967
- currentMessageQueue = new MessageQueue();
968
1485
  }
969
1486
  if (mode !== "remote") {
970
1487
  console.log("Switching back to good old claude...");
@@ -1073,7 +1590,7 @@ class InterruptController {
1073
1590
  }
1074
1591
  }
1075
1592
 
1076
- var version = "0.1.14";
1593
+ var version = "0.2.1";
1077
1594
  var packageJson = {
1078
1595
  version: version};
1079
1596
 
@@ -1133,6 +1650,7 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1133
1650
  if (!request) return currentState;
1134
1651
  let r = { ...currentState.requests };
1135
1652
  delete r[id];
1653
+ const isExitPlanModeSuccess = request.tool === "exit_plan_mode" && !message.approved && message.reason === PLAN_FAKE_REJECT;
1136
1654
  return {
1137
1655
  ...currentState,
1138
1656
  requests: r,
@@ -1141,8 +1659,8 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1141
1659
  [id]: {
1142
1660
  ...request,
1143
1661
  completedAt: Date.now(),
1144
- status: message.approved ? "approved" : "denied",
1145
- reason: message.reason
1662
+ status: isExitPlanModeSuccess ? "approved" : message.approved ? "approved" : "denied",
1663
+ reason: isExitPlanModeSuccess ? "Plan approved" : message.reason
1146
1664
  }
1147
1665
  }
1148
1666
  };
@@ -1440,11 +1958,53 @@ async function start(credentials, options = {}) {
1440
1958
  types.logger.infoDeveloper(`Session: ${response.id}`);
1441
1959
  types.logger.infoDeveloper(`Logs: ${logPath}`);
1442
1960
  const interruptController = new InterruptController();
1961
+ const { MessageQueue2 } = await Promise.resolve().then(function () { return MessageQueue2$1; });
1962
+ const messageQueue = new MessageQueue2(
1963
+ (mode2) => mode2
1964
+ // Simple string hasher since modes are already strings
1965
+ );
1443
1966
  let requests = /* @__PURE__ */ new Map();
1967
+ let toolCallResolver = null;
1968
+ const sessionScanner = createSessionScanner({
1969
+ workingDirectory,
1970
+ onMessage: (message) => {
1971
+ session.sendClaudeSessionMessage(message);
1972
+ }
1973
+ });
1444
1974
  const permissionServer = await startPermissionServerV2(async (request) => {
1445
- const id = node_crypto.randomUUID();
1975
+ if (!toolCallResolver) {
1976
+ const error = `Tool call resolver not available for permission request: ${request.name}`;
1977
+ types.logger.info(`ERROR: ${error}`);
1978
+ throw new Error(error);
1979
+ }
1980
+ const toolCallId = toolCallResolver(request.name, request.arguments);
1981
+ if (!toolCallId) {
1982
+ const error = `Could not resolve tool call ID for permission request: ${request.name}`;
1983
+ types.logger.info(`ERROR: ${error}`);
1984
+ throw new Error(error);
1985
+ }
1986
+ const id = toolCallId;
1987
+ types.logger.debug(`Using tool call ID as permission request ID: ${id} for ${request.name}`);
1446
1988
  let promise = new Promise((resolve) => {
1447
- requests.set(id, resolve);
1989
+ if (request.name === "exit_plan_mode") {
1990
+ const wrappedResolve = (response2) => {
1991
+ if (response2.approved) {
1992
+ types.logger.debug("[HACK] exit_plan_mode approved - injecting approval message and denying");
1993
+ sessionScanner.onRemoteUserMessageForDeduplication(PLAN_FAKE_RESTART);
1994
+ messageQueue.unshift(PLAN_FAKE_RESTART, "default");
1995
+ types.logger.debug(`[HACK] Message queue size after unshift: ${messageQueue.size()}`);
1996
+ resolve({
1997
+ approved: false,
1998
+ reason: PLAN_FAKE_REJECT
1999
+ });
2000
+ } else {
2001
+ resolve(response2);
2002
+ }
2003
+ };
2004
+ requests.set(id, wrappedResolve);
2005
+ } else {
2006
+ requests.set(id, resolve);
2007
+ }
1448
2008
  });
1449
2009
  let timeout = setTimeout(async () => {
1450
2010
  types.logger.debug("Permission timeout - attempting to interrupt Claude");
@@ -1528,12 +2088,15 @@ async function start(credentials, options = {}) {
1528
2088
  model: options.model,
1529
2089
  permissionMode: options.permissionMode,
1530
2090
  startingMode: options.startingMode,
2091
+ messageQueue,
2092
+ sessionScanner,
1531
2093
  onModeChange: (newMode) => {
1532
2094
  mode = newMode;
1533
2095
  session.sendSessionEvent({ type: "switch", mode: newMode });
1534
2096
  session.keepAlive(thinking, mode);
1535
2097
  if (newMode === "local") {
1536
2098
  types.logger.debug("Switching to local mode - clearing pending permission requests");
2099
+ toolCallResolver = null;
1537
2100
  for (const [id, resolve] of requests) {
1538
2101
  types.logger.debug(`Rejecting pending permission request: ${id}`);
1539
2102
  resolve({ approved: false, reason: "Session switched to local mode" });
@@ -1600,6 +2163,9 @@ async function start(credentials, options = {}) {
1600
2163
  onThinkingChange: (newThinking) => {
1601
2164
  thinking = newThinking;
1602
2165
  session.keepAlive(thinking, mode);
2166
+ },
2167
+ onToolCallResolver: (resolver) => {
2168
+ toolCallResolver = resolver;
1603
2169
  }
1604
2170
  });
1605
2171
  clearInterval(pingInterval);
@@ -1860,7 +2426,6 @@ class ApiDaemonSession extends node_events.EventEmitter {
1860
2426
  startKeepAlive() {
1861
2427
  this.stopKeepAlive();
1862
2428
  this.keepAliveInterval = setInterval(() => {
1863
- types.logger.daemonDebug("Sending keep-alive ping");
1864
2429
  this.socket.volatile.emit("machine-alive", {
1865
2430
  time: Date.now()
1866
2431
  });
@@ -2286,7 +2851,7 @@ Currently only supported on macOS.
2286
2851
  } else if (arg === "-m" || arg === "--model") {
2287
2852
  options.model = args[++i];
2288
2853
  } else if (arg === "-p" || arg === "--permission-mode") {
2289
- options.permissionMode = z.z.enum(["auto", "default", "plan"]).parse(args[++i]);
2854
+ options.permissionMode = z.z.enum(["default", "acceptEdits", "bypassPermissions", "plan"]).parse(args[++i]);
2290
2855
  } else if (arg === "--local") ; else if (arg === "--happy-starting-mode") {
2291
2856
  options.startingMode = z.z.enum(["local", "remote"]).parse(args[++i]);
2292
2857
  } else if (arg === "--claude-env") {
@@ -2322,7 +2887,7 @@ ${chalk.bold("Options:")}
2322
2887
  -h, --help Show this help message
2323
2888
  -v, --version Show version
2324
2889
  -m, --model <model> Claude model to use (default: sonnet)
2325
- -p, --permission-mode Permission mode: auto, default, or plan
2890
+ -p, --permission-mode Permission mode: default, acceptEdits, bypassPermissions, or plan
2326
2891
  --auth, --login Force re-authentication
2327
2892
  --claude-env KEY=VALUE Set environment variable for Claude Code
2328
2893
  --claude-arg ARG Pass additional argument to Claude CLI
@@ -2394,7 +2959,7 @@ ${chalk.bold("Examples:")}
2394
2959
  await writeSettings(settings);
2395
2960
  }
2396
2961
  if (settings.daemonAutoStartWhenRunningHappy) {
2397
- console.log("Starting Happy background service...");
2962
+ console.debug("Starting Happy background service...");
2398
2963
  if (!await isDaemonRunning()) {
2399
2964
  const happyPath = process.argv[1];
2400
2965
  const isBuiltBinary = happyPath.endsWith("/bin/happy") || happyPath.endsWith("\\bin\\happy");