happy-coder 0.1.13 → 0.2.0

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