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