happy-imou-cloud 2.0.10 → 2.0.11

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.
Files changed (34) hide show
  1. package/dist/BaseReasoningProcessor-5ACv9gKu.mjs +320 -0
  2. package/dist/BaseReasoningProcessor-COrRWyn0.cjs +323 -0
  3. package/dist/ProviderSelectionHandler-BljOLMQn.mjs +261 -0
  4. package/dist/ProviderSelectionHandler-DBGobhGZ.cjs +265 -0
  5. package/dist/{api-BjxmW-0W.mjs → api-BcWf5v4i.mjs} +174 -46
  6. package/dist/{api-DUE5TJBE.cjs → api-Ye-rPX6s.cjs} +174 -46
  7. package/dist/{command-Df7u5eAT.cjs → command-BK93nizl.cjs} +6 -6
  8. package/dist/{command-ComOeFLY.mjs → command-nOI80Mnm.mjs} +6 -6
  9. package/dist/{index-CzvgPwr1.mjs → index-DnsqY6I_.mjs} +189 -60
  10. package/dist/{index-Cuvfa15L.cjs → index-J7QKJ8lc.cjs} +194 -61
  11. package/dist/index.cjs +8 -8
  12. package/dist/index.mjs +8 -8
  13. package/dist/lib.cjs +2 -3
  14. package/dist/lib.d.cts +2 -0
  15. package/dist/lib.d.mts +2 -0
  16. package/dist/lib.mjs +2 -3
  17. package/dist/{persistence-BxP6Jw1f.mjs → persistence-BMa6cyw9.mjs} +2 -3
  18. package/dist/{persistence-D7JtnrYA.cjs → persistence-xK5CKhbn.cjs} +2 -3
  19. package/dist/registerKillSessionHandler-CH6yN0eG.cjs +1198 -0
  20. package/dist/registerKillSessionHandler-Dxwg4L4J.mjs +1179 -0
  21. package/dist/{runClaude-C1W_Nw0C.cjs → runClaude-BMHlBny_.cjs} +884 -398
  22. package/dist/{runClaude-B_fTMxc4.mjs → runClaude-cQ-UT0Ke.mjs} +882 -396
  23. package/dist/{runCodex-CUgOiIEz.mjs → runCodex-AnJUPIhX.mjs} +155 -578
  24. package/dist/{runCodex-D2VCWVEK.cjs → runCodex-roOSOWWL.cjs} +158 -583
  25. package/dist/{runGemini-2_FEtJYa.mjs → runGemini-BGo_0mzA.mjs} +142 -387
  26. package/dist/{runGemini-CiGnjflq.cjs → runGemini-Cg6Zbqlz.cjs} +145 -390
  27. package/package.json +9 -8
  28. package/scripts/e2e/fake-codex-acp-agent.mjs +139 -0
  29. package/scripts/e2e/local-server-session-roundtrip.mjs +1063 -0
  30. package/scripts/release-smoke.mjs +23 -6
  31. package/dist/names-CicSgRNg.mjs +0 -625
  32. package/dist/names-yJNZoTv_.cjs +0 -636
  33. package/dist/registerKillSessionHandler-ARQrPvnT.mjs +0 -454
  34. package/dist/registerKillSessionHandler-DCMFiXyA.cjs +0 -459
@@ -1,45 +1,46 @@
1
- import os, { homedir } from 'node:os';
2
1
  import { randomUUID } from 'node:crypto';
3
- import { l as logger, d as backoff, f as delay, g as AsyncLock, c as configuration, b as connectionState, A as ApiClient, p as packageJson, i as isAuthenticationRequiredError, s as startOfflineReconnection } from './api-BjxmW-0W.mjs';
4
- import { h as getProjectPath, j as claudeLocal, E as ExitCodeError, k as isBun, l as trimIdent, m as claudeCheckSession, d as projectPath, n as getEnvironmentInfo, i as initialMachineMetadata, b as stopCaffeinate, p as publishSessionRegistration, o as startCaffeinate } from './index-CzvgPwr1.mjs';
5
- import { R as RawJSONLinesSchema } from './types-CiliQpqS.mjs';
2
+ import { l as logger, d as backoff, f as delay, g as AsyncLock, c as configuration, s as startOfflineReconnection, b as connectionState, A as ApiClient, i as isAuthenticationRequiredError } from './api-BcWf5v4i.mjs';
3
+ import 'cross-spawn';
4
+ import '@agentclientprotocol/sdk';
5
+ import { j as getProjectPath, k as claudeLocal, E as ExitCodeError, l as trimIdent, m as createClaudeBackend, f as formatDisplayMessage, t as truncateDisplayMessage, n as isBun, o as claudeCheckSession, e as projectPath, q as mapToClaudeMode, b as stopCaffeinate, p as publishSessionRegistration, u as getEnvironmentInfo, w as startCaffeinate } from './index-DnsqY6I_.mjs';
6
+ import 'ps-list';
7
+ import 'fs';
8
+ import 'path';
9
+ import 'os';
10
+ import 'child_process';
11
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
6
12
  import { dirname, basename, join, resolve } from 'node:path';
13
+ import { homedir } from 'node:os';
14
+ import './persistence-BMa6cyw9.mjs';
7
15
  import { readFile } from 'node:fs/promises';
8
16
  import { stat, watch, access } from 'fs/promises';
9
- import { useStdout, useInput, Box, Text, render } from 'ink';
10
- import { a as MessageBuffer, M as MessageQueue2, h as hashObject, r as registerKillSessionHandler } from './registerKillSessionHandler-ARQrPvnT.mjs';
11
- import React, { useState, useRef, useEffect, useCallback } from 'react';
17
+ import 'crypto';
12
18
  import { execSync, spawn } from 'node:child_process';
19
+ import 'chalk';
20
+ import 'node:events';
21
+ import 'axios';
22
+ import 'tweetnacl';
23
+ import 'open';
24
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
25
+ import { useStdout, useInput, Box, Text, render } from 'ink';
26
+ import { c as createKeepAliveController, P as ProviderSelectionHandler, r as runModeLoop } from './ProviderSelectionHandler-BljOLMQn.mjs';
27
+ import { R as RawJSONLinesSchema } from './types-CiliQpqS.mjs';
28
+ import { B as BasePermissionHandler, d as MessageBuffer, C as ConversationHistory$1, w as waitForResponseCompleteWithAbort, l as launchRuntimeHandleWithFactoryResult, j as forwardAgentMessageToProviderSession, s as syncControlledByUserState, e as ensureManagedProviderMachine, M as MissingMachineIdError, c as createSessionMetadata, b as MessageQueue2, h as hashObject, r as registerKillSessionHandler, f as closeProviderSession } from './registerKillSessionHandler-Dxwg4L4J.mjs';
13
29
  import { createInterface } from 'node:readline';
14
- import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
15
30
  import { fileURLToPath } from 'node:url';
16
- import 'axios';
17
- import 'node:events';
18
31
  import 'socket.io-client';
19
- import 'tweetnacl';
20
32
  import 'expo-server-sdk';
21
- import 'chalk';
22
33
  import { isDeepStrictEqual } from 'node:util';
23
- import { readSettings } from './persistence-BxP6Jw1f.mjs';
24
34
  import { createServer } from 'node:http';
25
- import 'fs';
26
35
  import 'zod';
27
- import 'child_process';
28
- import 'util';
29
- import 'crypto';
30
- import 'path';
31
- import 'os';
32
36
  import 'tmp';
33
37
  import 'qrcode-terminal';
34
38
  import 'node:module';
35
- import 'open';
36
39
  import 'url';
37
- import 'ps-list';
38
- import 'cross-spawn';
39
40
  import 'fastify';
40
41
  import 'fastify-type-provider-zod';
41
42
  import 'http';
42
- import '@agentclientprotocol/sdk';
43
+ import 'util';
43
44
 
44
45
  class Session {
45
46
  path;
@@ -63,8 +64,7 @@ class Session {
63
64
  thinking = false;
64
65
  /** Callbacks to be notified when session ID is found/changed */
65
66
  sessionFoundCallbacks = [];
66
- /** Keep alive interval reference for cleanup */
67
- keepAliveInterval;
67
+ keepAliveController;
68
68
  constructor(opts) {
69
69
  this.path = opts.path;
70
70
  this.api = opts.api;
@@ -79,26 +79,29 @@ class Session {
79
79
  this._onModeChange = opts.onModeChange;
80
80
  this.hookSettingsPath = opts.hookSettingsPath;
81
81
  this.jsRuntime = opts.jsRuntime ?? "node";
82
- this.client.keepAlive(this.thinking, this.mode);
83
- this.keepAliveInterval = setInterval(() => {
84
- this.client.keepAlive(this.thinking, this.mode);
85
- }, 2e3);
82
+ this.keepAliveController = createKeepAliveController({
83
+ initialMode: this.mode,
84
+ initialThinking: this.thinking,
85
+ send: (thinking, mode) => {
86
+ this.client.keepAlive(thinking, mode);
87
+ }
88
+ });
86
89
  }
87
90
  /**
88
91
  * Cleanup resources (call when session is no longer needed)
89
92
  */
90
93
  cleanup = () => {
91
- clearInterval(this.keepAliveInterval);
94
+ this.keepAliveController.dispose();
92
95
  this.sessionFoundCallbacks = [];
93
96
  logger.debug("[Session] Cleaned up resources");
94
97
  };
95
98
  onThinkingChange = (thinking) => {
96
99
  this.thinking = thinking;
97
- this.client.keepAlive(thinking, this.mode);
100
+ this.keepAliveController.setThinking(thinking);
98
101
  };
99
102
  onModeChange = (mode) => {
100
103
  this.mode = mode;
101
- this.client.keepAlive(this.thinking, mode);
104
+ this.keepAliveController.setMode(mode);
102
105
  this._onModeChange(mode);
103
106
  };
104
107
  /**
@@ -712,6 +715,50 @@ async function claudeLocalLauncher(session) {
712
715
  return exitReason || { type: "exit", code: 0 };
713
716
  }
714
717
 
718
+ function parseCompact(message) {
719
+ const trimmed = message.trim();
720
+ if (trimmed === "/compact") {
721
+ return {
722
+ isCompact: true,
723
+ originalMessage: trimmed
724
+ };
725
+ }
726
+ if (trimmed.startsWith("/compact ")) {
727
+ return {
728
+ isCompact: true,
729
+ originalMessage: trimmed
730
+ };
731
+ }
732
+ return {
733
+ isCompact: false,
734
+ originalMessage: message
735
+ };
736
+ }
737
+ function parseClear(message) {
738
+ const trimmed = message.trim();
739
+ return {
740
+ isClear: trimmed === "/clear"
741
+ };
742
+ }
743
+ function parseSpecialCommand(message) {
744
+ const compactResult = parseCompact(message);
745
+ if (compactResult.isCompact) {
746
+ return {
747
+ type: "compact",
748
+ originalMessage: compactResult.originalMessage
749
+ };
750
+ }
751
+ const clearResult = parseClear(message);
752
+ if (clearResult.isClear) {
753
+ return {
754
+ type: "clear"
755
+ };
756
+ }
757
+ return {
758
+ type: null
759
+ };
760
+ }
761
+
715
762
  const RemoteModeDisplay = ({ messageBuffer, logPath, onExit, onSwitchToLocal }) => {
716
763
  const [messages, setMessages] = useState([]);
717
764
  const [confirmationMode, setConfirmationMode] = useState(null);
@@ -803,39 +850,615 @@ const RemoteModeDisplay = ({ messageBuffer, logPath, onExit, onSwitchToLocal })
803
850
  for (let i = 0; i < line.length; i += maxLineLength) {
804
851
  chunks.push(line.slice(i, i + maxLineLength));
805
852
  }
806
- return chunks.join("\n");
807
- }).join("\n");
808
- };
809
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
810
- Box,
811
- {
812
- flexDirection: "column",
813
- width: terminalWidth,
814
- height: terminalHeight - 4,
815
- borderStyle: "round",
816
- borderColor: "gray",
817
- paddingX: 1,
818
- overflow: "hidden"
819
- },
820
- /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "gray", bold: true }, "\u{1F4E1} Remote Mode - Claude Messages"), /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "\u2500".repeat(Math.min(terminalWidth - 4, 60)))),
821
- /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", height: terminalHeight - 10, overflow: "hidden" }, messages.length === 0 ? /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "Waiting for messages...") : (
822
- // Show only the last messages that fit in the available space
823
- messages.slice(-Math.max(1, terminalHeight - 10)).map((msg) => /* @__PURE__ */ React.createElement(Box, { key: msg.id, flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: getMessageColor(msg.type), dimColor: true }, formatMessage(msg))))
824
- ))
825
- ), /* @__PURE__ */ React.createElement(
826
- Box,
827
- {
828
- width: terminalWidth,
829
- borderStyle: "round",
830
- borderColor: actionInProgress ? "gray" : confirmationMode === "exit" ? "red" : confirmationMode === "switch" ? "yellow" : "green",
831
- paddingX: 2,
832
- justifyContent: "center",
833
- alignItems: "center",
834
- flexDirection: "column"
835
- },
836
- /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", alignItems: "center" }, actionInProgress === "exiting" ? /* @__PURE__ */ React.createElement(Text, { color: "gray", bold: true }, "Exiting...") : actionInProgress === "switching" ? /* @__PURE__ */ React.createElement(Text, { color: "gray", bold: true }, "Switching to local mode...") : confirmationMode === "exit" ? /* @__PURE__ */ React.createElement(Text, { color: "red", bold: true }, "\u26A0\uFE0F Press Ctrl-C again to exit completely") : confirmationMode === "switch" ? /* @__PURE__ */ React.createElement(Text, { color: "yellow", bold: true }, "\u23F8\uFE0F Press space again to switch to local mode") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { color: "green", bold: true }, "\u{1F4F1} Press space to switch to local mode \u2022 Ctrl-C to exit")), process.env.DEBUG && logPath && /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
837
- ));
838
- };
853
+ return chunks.join("\n");
854
+ }).join("\n");
855
+ };
856
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
857
+ Box,
858
+ {
859
+ flexDirection: "column",
860
+ width: terminalWidth,
861
+ height: terminalHeight - 4,
862
+ borderStyle: "round",
863
+ borderColor: "gray",
864
+ paddingX: 1,
865
+ overflow: "hidden"
866
+ },
867
+ /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "gray", bold: true }, "\u{1F4E1} Remote Mode - Claude Messages"), /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "\u2500".repeat(Math.min(terminalWidth - 4, 60)))),
868
+ /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", height: terminalHeight - 10, overflow: "hidden" }, messages.length === 0 ? /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "Waiting for messages...") : (
869
+ // Show only the last messages that fit in the available space
870
+ messages.slice(-Math.max(1, terminalHeight - 10)).map((msg) => /* @__PURE__ */ React.createElement(Box, { key: msg.id, flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: getMessageColor(msg.type), dimColor: true }, formatMessage(msg))))
871
+ ))
872
+ ), /* @__PURE__ */ React.createElement(
873
+ Box,
874
+ {
875
+ width: terminalWidth,
876
+ borderStyle: "round",
877
+ borderColor: actionInProgress ? "gray" : confirmationMode === "exit" ? "red" : confirmationMode === "switch" ? "yellow" : "green",
878
+ paddingX: 2,
879
+ justifyContent: "center",
880
+ alignItems: "center",
881
+ flexDirection: "column"
882
+ },
883
+ /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", alignItems: "center" }, actionInProgress === "exiting" ? /* @__PURE__ */ React.createElement(Text, { color: "gray", bold: true }, "Exiting...") : actionInProgress === "switching" ? /* @__PURE__ */ React.createElement(Text, { color: "gray", bold: true }, "Switching to local mode...") : confirmationMode === "exit" ? /* @__PURE__ */ React.createElement(Text, { color: "red", bold: true }, "\u26A0\uFE0F Press Ctrl-C again to exit completely") : confirmationMode === "switch" ? /* @__PURE__ */ React.createElement(Text, { color: "yellow", bold: true }, "\u23F8\uFE0F Press space again to switch to local mode") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { color: "green", bold: true }, "\u{1F4F1} Press space to switch to local mode \u2022 Ctrl-C to exit")), process.env.DEBUG && logPath && /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
884
+ ));
885
+ };
886
+
887
+ function getClaudeSettingsPath() {
888
+ const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
889
+ return join(claudeConfigDir, "settings.json");
890
+ }
891
+ function readClaudeSettings() {
892
+ try {
893
+ const settingsPath = getClaudeSettingsPath();
894
+ if (!existsSync(settingsPath)) {
895
+ logger.debug(`[ClaudeSettings] No Claude settings file found at ${settingsPath}`);
896
+ return null;
897
+ }
898
+ const settingsContent = readFileSync(settingsPath, "utf-8");
899
+ const settings = JSON.parse(settingsContent);
900
+ logger.debug(`[ClaudeSettings] Successfully read Claude settings from ${settingsPath}`);
901
+ logger.debug(`[ClaudeSettings] includeCoAuthoredBy: ${settings.includeCoAuthoredBy}`);
902
+ return settings;
903
+ } catch (error) {
904
+ logger.debug(`[ClaudeSettings] Error reading Claude settings: ${error}`);
905
+ return null;
906
+ }
907
+ }
908
+ function shouldIncludeCoAuthoredBy() {
909
+ const settings = readClaudeSettings();
910
+ if (!settings || settings.includeCoAuthoredBy === void 0) {
911
+ return true;
912
+ }
913
+ return settings.includeCoAuthoredBy;
914
+ }
915
+
916
+ const BASE_SYSTEM_PROMPT = (() => trimIdent(`
917
+ Help human.
918
+ `))();
919
+ const CO_AUTHORED_CREDITS = (() => trimIdent(`
920
+ When making commit messages, instead of just giving co-credit to Claude, also give credit to Happy like so:
921
+
922
+ <main commit message>
923
+
924
+ Generated with [Claude Code](https://claude.ai/code)
925
+ via [Happy](https://happy.engineering)
926
+
927
+ Co-Authored-By: Claude <noreply@anthropic.com>
928
+ Co-Authored-By: Happy <yesreply@happy.engineering>
929
+ `))();
930
+ const systemPrompt = (() => {
931
+ const includeCoAuthored = shouldIncludeCoAuthoredBy();
932
+ if (includeCoAuthored) {
933
+ return BASE_SYSTEM_PROMPT + "\n\n" + CO_AUTHORED_CREDITS;
934
+ } else {
935
+ return BASE_SYSTEM_PROMPT;
936
+ }
937
+ })();
938
+
939
+ function getToolDescriptor(toolName) {
940
+ if (toolName === "exit_plan_mode" || toolName === "ExitPlanMode") {
941
+ return { edit: false, exitPlan: true };
942
+ }
943
+ if (toolName === "Edit" || toolName === "MultiEdit" || toolName === "Write" || toolName === "NotebookEdit") {
944
+ return { edit: true, exitPlan: false };
945
+ }
946
+ return { edit: false, exitPlan: false };
947
+ }
948
+
949
+ class ClaudeAcpPermissionHandler extends BasePermissionHandler {
950
+ currentPermissionMode = "default";
951
+ constructor(session) {
952
+ super(session);
953
+ }
954
+ getLogPrefix() {
955
+ return "[ClaudeACP]";
956
+ }
957
+ setPermissionMode(mode) {
958
+ this.currentPermissionMode = mode;
959
+ logger.debug(`${this.getLogPrefix()} Permission mode set to: ${mode}`);
960
+ }
961
+ shouldAutoApprove(toolName) {
962
+ const descriptor = getToolDescriptor(toolName);
963
+ switch (this.currentPermissionMode) {
964
+ case "bypassPermissions":
965
+ case "yolo":
966
+ return true;
967
+ case "acceptEdits":
968
+ return descriptor.edit;
969
+ default:
970
+ return false;
971
+ }
972
+ }
973
+ async handleToolCall(toolCallId, toolName, input) {
974
+ if (this.shouldAutoApprove(toolName)) {
975
+ const decision = this.currentPermissionMode === "bypassPermissions" || this.currentPermissionMode === "yolo" ? "approved_for_session" : "approved";
976
+ this.session.updateAgentState((currentState) => ({
977
+ ...currentState,
978
+ completedRequests: {
979
+ ...currentState.completedRequests,
980
+ [toolCallId]: {
981
+ tool: toolName,
982
+ arguments: input,
983
+ createdAt: Date.now(),
984
+ completedAt: Date.now(),
985
+ status: "approved",
986
+ decision
987
+ }
988
+ }
989
+ }));
990
+ logger.debug(`${this.getLogPrefix()} Auto-approved tool ${toolName} (${toolCallId}) in ${this.currentPermissionMode} mode`);
991
+ return { decision };
992
+ }
993
+ return this.registerPendingRequest(
994
+ toolCallId,
995
+ toolName,
996
+ input,
997
+ ` in ${this.currentPermissionMode} mode`
998
+ );
999
+ }
1000
+ }
1001
+
1002
+ function normalizeClaudeBackendError(error) {
1003
+ const record = typeof error === "object" && error !== null ? error : null;
1004
+ const text = formatDisplayMessage(error).trim();
1005
+ const stderrText = record ? formatDisplayMessage(record.stderr).trim() : "";
1006
+ const detailText = record ? formatDisplayMessage(record.detail).trim() : "";
1007
+ const searchable = [text, stderrText, detailText].filter(Boolean).join("\n").trim();
1008
+ return searchable || "Claude ACP backend exited unexpectedly";
1009
+ }
1010
+ async function claudeAcpRemoteLauncher(session) {
1011
+ const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
1012
+ const messageBuffer = new MessageBuffer({ enabled: hasTTY });
1013
+ let inkInstance = null;
1014
+ let shouldExit = false;
1015
+ let abortController = new AbortController();
1016
+ let runtimeHandle = null;
1017
+ let unsubscribeRuntimeMessages = null;
1018
+ let currentModeHash = null;
1019
+ let pending = null;
1020
+ let accumulatedResponse = "";
1021
+ let isResponseInProgress = false;
1022
+ let taskStartedSent = false;
1023
+ let currentAssistantMessageId = null;
1024
+ let currentThinkingMessageId = null;
1025
+ let shouldInjectHistoryOnNextSession = false;
1026
+ let readyAlreadySent = false;
1027
+ const permissionHandler = new ClaudeAcpPermissionHandler(session.client);
1028
+ const selectionHandler = new ProviderSelectionHandler(session.client, "Claude");
1029
+ const conversationHistory = new ConversationHistory$1({
1030
+ maxMessages: 20,
1031
+ maxCharacters: 5e4
1032
+ });
1033
+ const rotateAbortController = () => {
1034
+ const activeController = abortController;
1035
+ abortController = new AbortController();
1036
+ return activeController;
1037
+ };
1038
+ const sendReady = () => {
1039
+ session.client.sendSessionEvent({ type: "ready" });
1040
+ try {
1041
+ session.api.push().sendToAllDevices(
1042
+ "It's ready!",
1043
+ "Claude is waiting for your command",
1044
+ { sessionId: session.client.sessionId }
1045
+ );
1046
+ } catch (pushError) {
1047
+ logger.debug("[ClaudeACP] Failed to send ready push", pushError);
1048
+ }
1049
+ };
1050
+ const emitStatusMessage = (message) => {
1051
+ messageBuffer.addMessage(message, "status");
1052
+ session.client.sendSessionEvent({ type: "message", message });
1053
+ };
1054
+ const emitUserVisibleErrorMessage = (message) => {
1055
+ emitStatusMessage(message);
1056
+ session.client.sendAgentMessage("claude", {
1057
+ type: "message",
1058
+ message
1059
+ });
1060
+ };
1061
+ const resetTurnState = () => {
1062
+ accumulatedResponse = "";
1063
+ isResponseInProgress = false;
1064
+ taskStartedSent = false;
1065
+ currentAssistantMessageId = null;
1066
+ currentThinkingMessageId = null;
1067
+ session.onThinkingChange(false);
1068
+ };
1069
+ const emitFinalAssistantMessage = () => {
1070
+ const finalMessage = accumulatedResponse.trim();
1071
+ if (!finalMessage) {
1072
+ accumulatedResponse = "";
1073
+ isResponseInProgress = false;
1074
+ return;
1075
+ }
1076
+ conversationHistory.addAssistantMessage(finalMessage);
1077
+ session.client.sendAgentMessage("claude", {
1078
+ type: "message",
1079
+ message: finalMessage
1080
+ });
1081
+ accumulatedResponse = "";
1082
+ isResponseInProgress = false;
1083
+ };
1084
+ const disposeRuntimeHandle = async () => {
1085
+ if (!runtimeHandle) {
1086
+ return;
1087
+ }
1088
+ const activeHandle = runtimeHandle;
1089
+ runtimeHandle = null;
1090
+ unsubscribeRuntimeMessages?.();
1091
+ unsubscribeRuntimeMessages = null;
1092
+ try {
1093
+ await activeHandle.dispose();
1094
+ } catch (error) {
1095
+ logger.debug("[ClaudeACP] Error disposing runtime handle:", error);
1096
+ }
1097
+ };
1098
+ const queueHistoryInjectionForRestart = (reason) => {
1099
+ messageBuffer.addMessage("\u2550".repeat(40), "status");
1100
+ if (conversationHistory.hasHistory()) {
1101
+ shouldInjectHistoryOnNextSession = true;
1102
+ const message = `${reason} Preserving ${conversationHistory.size()} earlier messages of context.`;
1103
+ emitStatusMessage(message);
1104
+ logger.debug(`[ClaudeACP] Will inject conversation history after restart: ${conversationHistory.getSummary()}`);
1105
+ return;
1106
+ }
1107
+ emitStatusMessage(reason);
1108
+ };
1109
+ const setupRuntimeMessageHandler = (activeRuntimeHandle) => {
1110
+ const forwardAgentMessage = (agentMessage) => {
1111
+ forwardAgentMessageToProviderSession(agentMessage, {
1112
+ provider: "claude",
1113
+ send: (body) => session.client.sendAgentMessage("claude", body)
1114
+ });
1115
+ };
1116
+ unsubscribeRuntimeMessages?.();
1117
+ unsubscribeRuntimeMessages = activeRuntimeHandle.onMessage((msg) => {
1118
+ switch (msg.type) {
1119
+ case "model-output": {
1120
+ const text = msg.textDelta ?? msg.fullText ?? "";
1121
+ if (!text) {
1122
+ return;
1123
+ }
1124
+ if (!isResponseInProgress) {
1125
+ if (currentThinkingMessageId) {
1126
+ messageBuffer.removeMessage(currentThinkingMessageId);
1127
+ currentThinkingMessageId = null;
1128
+ }
1129
+ currentAssistantMessageId = messageBuffer.addMessage(text, "assistant");
1130
+ isResponseInProgress = true;
1131
+ } else if (currentAssistantMessageId) {
1132
+ const updated = messageBuffer.updateMessage(currentAssistantMessageId, text);
1133
+ if (!updated) {
1134
+ currentAssistantMessageId = messageBuffer.addMessage(text, "assistant");
1135
+ }
1136
+ }
1137
+ accumulatedResponse += text;
1138
+ return;
1139
+ }
1140
+ case "status": {
1141
+ if (msg.status === "running") {
1142
+ session.onThinkingChange(true);
1143
+ if (!taskStartedSent) {
1144
+ session.client.sendAgentMessage("claude", {
1145
+ type: "task_started",
1146
+ id: randomUUID()
1147
+ });
1148
+ taskStartedSent = true;
1149
+ }
1150
+ if (!isResponseInProgress && !currentThinkingMessageId) {
1151
+ currentThinkingMessageId = messageBuffer.addMessage("Thinking...", "system");
1152
+ }
1153
+ return;
1154
+ }
1155
+ if (msg.status === "idle" || msg.status === "stopped") {
1156
+ session.onThinkingChange(false);
1157
+ return;
1158
+ }
1159
+ if (msg.status === "error") {
1160
+ messageBuffer.addMessage(`Error: ${normalizeClaudeBackendError(msg.detail)}`, "status");
1161
+ }
1162
+ return;
1163
+ }
1164
+ case "tool-call": {
1165
+ const toolArgs = truncateDisplayMessage(msg.args, 100);
1166
+ messageBuffer.addMessage(
1167
+ `Executing: ${msg.toolName}${toolArgs ? ` ${toolArgs}` : ""}`,
1168
+ "tool"
1169
+ );
1170
+ forwardAgentMessage(msg);
1171
+ return;
1172
+ }
1173
+ case "tool-result": {
1174
+ const resultText = truncateDisplayMessage(msg.result, 200);
1175
+ messageBuffer.addMessage(resultText ? `Result: ${resultText}` : "Tool completed", "result");
1176
+ forwardAgentMessage(msg);
1177
+ return;
1178
+ }
1179
+ case "fs-edit": {
1180
+ messageBuffer.addMessage(`File edit: ${msg.description}`, "tool");
1181
+ forwardAgentMessage(msg);
1182
+ return;
1183
+ }
1184
+ case "terminal-output": {
1185
+ const output = formatDisplayMessage(msg.data);
1186
+ messageBuffer.addMessage(output, "result");
1187
+ forwardAgentMessage({
1188
+ ...msg,
1189
+ data: output
1190
+ });
1191
+ return;
1192
+ }
1193
+ case "permission-request": {
1194
+ forwardAgentMessage(msg);
1195
+ return;
1196
+ }
1197
+ case "token-count": {
1198
+ forwardAgentMessage(msg);
1199
+ return;
1200
+ }
1201
+ case "exec-approval-request":
1202
+ case "patch-apply-begin":
1203
+ case "patch-apply-end": {
1204
+ forwardAgentMessage(msg);
1205
+ return;
1206
+ }
1207
+ case "event": {
1208
+ if (msg.name === "thinking") {
1209
+ const payload = msg.payload;
1210
+ const thinkingText = typeof payload?.text === "string" ? payload.text : "";
1211
+ if (thinkingText) {
1212
+ session.client.sendAgentMessage("claude", {
1213
+ type: "thinking",
1214
+ text: thinkingText
1215
+ });
1216
+ if (!isResponseInProgress) {
1217
+ const preview = `[Thinking] ${thinkingText.substring(0, 100)}...`;
1218
+ if (currentThinkingMessageId) {
1219
+ messageBuffer.updateMessage(currentThinkingMessageId, preview, { mode: "replace" });
1220
+ } else {
1221
+ currentThinkingMessageId = messageBuffer.addMessage(preview, "system");
1222
+ }
1223
+ }
1224
+ }
1225
+ }
1226
+ return;
1227
+ }
1228
+ default:
1229
+ return;
1230
+ }
1231
+ });
1232
+ };
1233
+ const createRuntimeHandle = async (mode) => {
1234
+ const { session: nextRuntimeHandle, factoryResult } = await launchRuntimeHandleWithFactoryResult({
1235
+ provider: "claude",
1236
+ cwd: session.path,
1237
+ createBackendResult: (opts) => createClaudeBackend({
1238
+ ...opts,
1239
+ baseArgs: session.claudeArgs,
1240
+ mcpServers: session.mcpServers,
1241
+ permissionHandler,
1242
+ selectionHandler: {
1243
+ handleSelection: (request) => selectionHandler.requestSelection(request)
1244
+ },
1245
+ permissionMode: mode.permissionMode,
1246
+ model: mode.model,
1247
+ fallbackModel: mode.fallbackModel,
1248
+ customSystemPrompt: mode.customSystemPrompt ? `${mode.customSystemPrompt}
1249
+
1250
+ ${systemPrompt}` : void 0,
1251
+ appendSystemPrompt: mode.appendSystemPrompt ? `${mode.appendSystemPrompt}
1252
+
1253
+ ${systemPrompt}` : systemPrompt,
1254
+ allowedTools: mode.allowedTools ? mode.allowedTools.concat(session.allowedTools ?? []) : session.allowedTools,
1255
+ disallowedTools: mode.disallowedTools,
1256
+ settingsPath: session.hookSettingsPath
1257
+ })
1258
+ });
1259
+ logger.debug("[ClaudeACP] Started Claude ACP backend", {
1260
+ command: factoryResult.command,
1261
+ args: factoryResult.args
1262
+ });
1263
+ runtimeHandle = nextRuntimeHandle;
1264
+ setupRuntimeMessageHandler(nextRuntimeHandle);
1265
+ session.consumeOneTimeFlags();
1266
+ return nextRuntimeHandle;
1267
+ };
1268
+ const abortActiveTurn = async () => {
1269
+ const activeController = rotateAbortController();
1270
+ activeController.abort();
1271
+ session.onThinkingChange(false);
1272
+ if (runtimeHandle) {
1273
+ await runtimeHandle.cancel().catch((error) => {
1274
+ logger.debug("[ClaudeACP] Error cancelling runtime handle:", error);
1275
+ });
1276
+ }
1277
+ };
1278
+ const completeSyntheticTurn = () => {
1279
+ session.client.sendAgentMessage("claude", {
1280
+ type: "task_complete",
1281
+ id: randomUUID()
1282
+ });
1283
+ };
1284
+ const handleClearCommand = async () => {
1285
+ logger.debug("[ClaudeACP] /clear command received - resetting runtime and local history");
1286
+ conversationHistory.clear();
1287
+ shouldInjectHistoryOnNextSession = false;
1288
+ await disposeRuntimeHandle();
1289
+ session.clearSessionId();
1290
+ currentModeHash = null;
1291
+ permissionHandler.reset();
1292
+ selectionHandler.reset();
1293
+ resetTurnState();
1294
+ emitStatusMessage("Context was reset");
1295
+ completeSyntheticTurn();
1296
+ };
1297
+ const handleSwitchToLocal = async () => {
1298
+ const message = "Daemon-spawned Claude ACP sessions stay in remote mode.";
1299
+ logger.debug("[ClaudeACP] Ignoring switch request because daemon sessions are remote-only");
1300
+ emitStatusMessage(message);
1301
+ };
1302
+ const handleAbort = async () => {
1303
+ logger.debug("[ClaudeACP] Abort requested - stopping current task");
1304
+ await abortActiveTurn();
1305
+ };
1306
+ if (hasTTY) {
1307
+ console.clear();
1308
+ inkInstance = render(React.createElement(RemoteModeDisplay, {
1309
+ messageBuffer,
1310
+ logPath: process.env.DEBUG ? session.logPath : void 0,
1311
+ onExit: async () => {
1312
+ shouldExit = true;
1313
+ await handleAbort();
1314
+ },
1315
+ onSwitchToLocal: () => {
1316
+ void handleSwitchToLocal();
1317
+ }
1318
+ }), {
1319
+ exitOnCtrlC: false,
1320
+ patchConsole: false
1321
+ });
1322
+ }
1323
+ if (hasTTY) {
1324
+ process.stdin.resume();
1325
+ if (process.stdin.isTTY) {
1326
+ process.stdin.setRawMode(true);
1327
+ }
1328
+ process.stdin.setEncoding("utf8");
1329
+ }
1330
+ session.client.rpcHandlerManager.registerHandler("abort", handleAbort);
1331
+ session.client.rpcHandlerManager.registerHandler("switch", handleSwitchToLocal);
1332
+ try {
1333
+ while (!shouldExit) {
1334
+ let message = pending;
1335
+ pending = null;
1336
+ if (!message) {
1337
+ const waitSignal = abortController.signal;
1338
+ const batch = await session.queue.waitForMessagesAndGetAsString(waitSignal);
1339
+ if (!batch) {
1340
+ if (waitSignal.aborted && !shouldExit) {
1341
+ continue;
1342
+ }
1343
+ break;
1344
+ }
1345
+ message = batch;
1346
+ }
1347
+ if (!message) {
1348
+ break;
1349
+ }
1350
+ if (runtimeHandle && currentModeHash && message.hash !== currentModeHash) {
1351
+ queueHistoryInjectionForRestart("Starting new Claude ACP session (execution settings changed)...");
1352
+ await disposeRuntimeHandle();
1353
+ session.clearSessionId();
1354
+ currentModeHash = null;
1355
+ pending = message;
1356
+ permissionHandler.reset();
1357
+ selectionHandler.reset();
1358
+ resetTurnState();
1359
+ continue;
1360
+ }
1361
+ currentModeHash = message.hash;
1362
+ readyAlreadySent = false;
1363
+ const specialCommand = parseSpecialCommand(message.message);
1364
+ if (specialCommand.type === "clear") {
1365
+ await handleClearCommand();
1366
+ if (!shouldExit && !pending && session.queue.size() === 0 && !readyAlreadySent) {
1367
+ sendReady();
1368
+ readyAlreadySent = true;
1369
+ }
1370
+ continue;
1371
+ }
1372
+ permissionHandler.setPermissionMode(message.mode.permissionMode);
1373
+ messageBuffer.addMessage(message.message, "user");
1374
+ let shouldClearHistoryAfterTurn = false;
1375
+ try {
1376
+ const activeRuntimeHandle = runtimeHandle ?? await createRuntimeHandle(message.mode);
1377
+ let promptToSend = message.message;
1378
+ if (shouldInjectHistoryOnNextSession && conversationHistory.hasHistory()) {
1379
+ const historyContext = conversationHistory.getContextForNewSession(
1380
+ "Continue from the prior Claude ACP session using the conversation below as context."
1381
+ );
1382
+ promptToSend = historyContext + promptToSend;
1383
+ logger.debug(`[ClaudeACP] Injected conversation history context (${historyContext.length} chars)`);
1384
+ }
1385
+ if (specialCommand.type === "compact") {
1386
+ emitStatusMessage("Compaction started");
1387
+ }
1388
+ conversationHistory.addUserMessage(message.message);
1389
+ await activeRuntimeHandle.sendPrompt(promptToSend);
1390
+ await waitForResponseCompleteWithAbort(activeRuntimeHandle.backend, abortController.signal);
1391
+ shouldInjectHistoryOnNextSession = false;
1392
+ shouldClearHistoryAfterTurn = specialCommand.type === "compact";
1393
+ } catch (error) {
1394
+ logger.warn("[ClaudeACP] Error in Claude ACP session:", error);
1395
+ const isAbortError = error instanceof Error && error.name === "AbortError";
1396
+ const isExpectedInterruption = isAbortError || abortController.signal.aborted || shouldExit;
1397
+ if (isExpectedInterruption) {
1398
+ session.client.sendAgentMessage("claude", {
1399
+ type: "turn_aborted",
1400
+ id: randomUUID()
1401
+ });
1402
+ emitStatusMessage("Aborted by user");
1403
+ } else {
1404
+ const errorMessage = normalizeClaudeBackendError(error);
1405
+ emitUserVisibleErrorMessage(errorMessage);
1406
+ if (conversationHistory.hasHistory()) {
1407
+ shouldInjectHistoryOnNextSession = true;
1408
+ }
1409
+ await disposeRuntimeHandle();
1410
+ session.clearSessionId();
1411
+ currentModeHash = null;
1412
+ }
1413
+ } finally {
1414
+ emitFinalAssistantMessage();
1415
+ if (shouldClearHistoryAfterTurn) {
1416
+ conversationHistory.clear();
1417
+ emitStatusMessage("Compaction completed");
1418
+ }
1419
+ if (!shouldExit) {
1420
+ session.client.sendAgentMessage("claude", {
1421
+ type: "task_complete",
1422
+ id: randomUUID()
1423
+ });
1424
+ }
1425
+ permissionHandler.reset();
1426
+ selectionHandler.reset();
1427
+ resetTurnState();
1428
+ if (!shouldExit && !pending && session.queue.size() === 0 && !readyAlreadySent) {
1429
+ sendReady();
1430
+ readyAlreadySent = true;
1431
+ }
1432
+ }
1433
+ }
1434
+ } finally {
1435
+ await disposeRuntimeHandle();
1436
+ permissionHandler.reset();
1437
+ selectionHandler.reset();
1438
+ if (process.stdin.isTTY) {
1439
+ try {
1440
+ process.stdin.setRawMode(false);
1441
+ } catch {
1442
+ }
1443
+ }
1444
+ if (hasTTY) {
1445
+ try {
1446
+ process.stdin.pause();
1447
+ } catch {
1448
+ }
1449
+ }
1450
+ session.client.rpcHandlerManager.registerHandler("abort", async () => {
1451
+ });
1452
+ session.client.rpcHandlerManager.registerHandler("switch", async () => {
1453
+ });
1454
+ if (inkInstance) {
1455
+ inkInstance.unmount();
1456
+ }
1457
+ messageBuffer.clear();
1458
+ }
1459
+ logger.debug("[ClaudeACP] Remote launcher returning: exit");
1460
+ return "exit";
1461
+ }
839
1462
 
840
1463
  class Stream {
841
1464
  constructor(returned) {
@@ -1362,59 +1985,6 @@ function query(config) {
1362
1985
  return query2;
1363
1986
  }
1364
1987
 
1365
- function mapToClaudeMode(mode) {
1366
- const codexToClaudeMap = {
1367
- "yolo": "bypassPermissions",
1368
- "safe-yolo": "default",
1369
- "read-only": "default"
1370
- };
1371
- return codexToClaudeMap[mode] ?? mode;
1372
- }
1373
-
1374
- function parseCompact(message) {
1375
- const trimmed = message.trim();
1376
- if (trimmed === "/compact") {
1377
- return {
1378
- isCompact: true,
1379
- originalMessage: trimmed
1380
- };
1381
- }
1382
- if (trimmed.startsWith("/compact ")) {
1383
- return {
1384
- isCompact: true,
1385
- originalMessage: trimmed
1386
- };
1387
- }
1388
- return {
1389
- isCompact: false,
1390
- originalMessage: message
1391
- };
1392
- }
1393
- function parseClear(message) {
1394
- const trimmed = message.trim();
1395
- return {
1396
- isClear: trimmed === "/clear"
1397
- };
1398
- }
1399
- function parseSpecialCommand(message) {
1400
- const compactResult = parseCompact(message);
1401
- if (compactResult.isCompact) {
1402
- return {
1403
- type: "compact",
1404
- originalMessage: compactResult.originalMessage
1405
- };
1406
- }
1407
- const clearResult = parseClear(message);
1408
- if (clearResult.isClear) {
1409
- return {
1410
- type: "clear"
1411
- };
1412
- }
1413
- return {
1414
- type: null
1415
- };
1416
- }
1417
-
1418
1988
  class PushableAsyncIterable {
1419
1989
  queue = [];
1420
1990
  waiters = [];
@@ -1554,58 +2124,6 @@ async function awaitFileExist(file, timeout = 1e4) {
1554
2124
  return false;
1555
2125
  }
1556
2126
 
1557
- function getClaudeSettingsPath() {
1558
- const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
1559
- return join(claudeConfigDir, "settings.json");
1560
- }
1561
- function readClaudeSettings() {
1562
- try {
1563
- const settingsPath = getClaudeSettingsPath();
1564
- if (!existsSync(settingsPath)) {
1565
- logger.debug(`[ClaudeSettings] No Claude settings file found at ${settingsPath}`);
1566
- return null;
1567
- }
1568
- const settingsContent = readFileSync(settingsPath, "utf-8");
1569
- const settings = JSON.parse(settingsContent);
1570
- logger.debug(`[ClaudeSettings] Successfully read Claude settings from ${settingsPath}`);
1571
- logger.debug(`[ClaudeSettings] includeCoAuthoredBy: ${settings.includeCoAuthoredBy}`);
1572
- return settings;
1573
- } catch (error) {
1574
- logger.debug(`[ClaudeSettings] Error reading Claude settings: ${error}`);
1575
- return null;
1576
- }
1577
- }
1578
- function shouldIncludeCoAuthoredBy() {
1579
- const settings = readClaudeSettings();
1580
- if (!settings || settings.includeCoAuthoredBy === void 0) {
1581
- return true;
1582
- }
1583
- return settings.includeCoAuthoredBy;
1584
- }
1585
-
1586
- const BASE_SYSTEM_PROMPT = (() => trimIdent(`
1587
- Help human.
1588
- `))();
1589
- const CO_AUTHORED_CREDITS = (() => trimIdent(`
1590
- When making commit messages, instead of just giving co-credit to Claude, also give credit to Happy like so:
1591
-
1592
- <main commit message>
1593
-
1594
- Generated with [Claude Code](https://claude.ai/code)
1595
- via [Happy](https://happy.engineering)
1596
-
1597
- Co-Authored-By: Claude <noreply@anthropic.com>
1598
- Co-Authored-By: Happy <yesreply@happy.engineering>
1599
- `))();
1600
- const systemPrompt = (() => {
1601
- const includeCoAuthored = shouldIncludeCoAuthoredBy();
1602
- if (includeCoAuthored) {
1603
- return BASE_SYSTEM_PROMPT + "\n\n" + CO_AUTHORED_CREDITS;
1604
- } else {
1605
- return BASE_SYSTEM_PROMPT;
1606
- }
1607
- })();
1608
-
1609
2127
  async function claudeRemote(opts) {
1610
2128
  let startFrom = opts.sessionId;
1611
2129
  if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path, opts.transcriptPath)) {
@@ -1809,16 +2327,6 @@ function getToolName(toolName) {
1809
2327
  return toTitleCase(toolName);
1810
2328
  }
1811
2329
 
1812
- function getToolDescriptor(toolName) {
1813
- if (toolName === "exit_plan_mode" || toolName === "ExitPlanMode") {
1814
- return { edit: false, exitPlan: true };
1815
- }
1816
- if (toolName === "Edit" || toolName === "MultiEdit" || toolName === "Write" || toolName === "NotebookEdit") {
1817
- return { edit: true, exitPlan: false };
1818
- }
1819
- return { edit: false, exitPlan: false };
1820
- }
1821
-
1822
2330
  class PermissionHandler {
1823
2331
  toolCalls = [];
1824
2332
  responses = /* @__PURE__ */ new Map();
@@ -2960,36 +3468,31 @@ async function loop(opts) {
2960
3468
  jsRuntime: opts.jsRuntime
2961
3469
  });
2962
3470
  opts.onSessionReady?.(session);
2963
- let mode = opts.startingMode ?? "local";
2964
- while (true) {
2965
- logger.debug(`[loop] Iteration with mode: ${mode}`);
2966
- switch (mode) {
2967
- case "local": {
3471
+ return await runModeLoop({
3472
+ startingMode: opts.startingMode ?? "local",
3473
+ onIteration: (mode) => {
3474
+ logger.debug(`[loop] Iteration with mode: ${mode}`);
3475
+ },
3476
+ onModeChange: async (mode) => {
3477
+ await opts.onModeChange(mode);
3478
+ },
3479
+ launchers: {
3480
+ local: async () => {
2968
3481
  const result = await claudeLocalLauncher(session);
2969
- switch (result.type) {
2970
- case "switch":
2971
- mode = "remote";
2972
- opts.onModeChange?.(mode);
2973
- break;
2974
- case "exit":
2975
- return result.code;
3482
+ if (result.type === "switch") {
3483
+ return { type: "switch", mode: "remote" };
2976
3484
  }
2977
- break;
2978
- }
2979
- case "remote": {
2980
- const reason = await claudeRemoteLauncher(session);
2981
- switch (reason) {
2982
- case "exit":
2983
- return 0;
2984
- case "switch":
2985
- mode = "local";
2986
- opts.onModeChange?.(mode);
2987
- break;
3485
+ return { type: "exit", value: result.code };
3486
+ },
3487
+ remote: async () => {
3488
+ const reason = opts.startedBy === "daemon" ? await claudeAcpRemoteLauncher(session) : await claudeRemoteLauncher(session);
3489
+ if (reason === "switch") {
3490
+ return { type: "switch", mode: "local" };
2988
3491
  }
2989
- break;
3492
+ return { type: "exit", value: 0 };
2990
3493
  }
2991
3494
  }
2992
- }
3495
+ });
2993
3496
  }
2994
3497
 
2995
3498
  async function extractSDKMetadata() {
@@ -3148,6 +3651,143 @@ function cleanupHookSettingsFile(filepath) {
3148
3651
  }
3149
3652
  }
3150
3653
 
3654
+ async function runOfflineClaudeLocalFallback(opts) {
3655
+ let offlineSessionId = null;
3656
+ const reconnection = startOfflineReconnection({
3657
+ serverUrl: configuration.serverUrl,
3658
+ onReconnected: async () => {
3659
+ const response = await opts.api.getOrCreateSession({
3660
+ tag: randomUUID(),
3661
+ metadata: opts.metadata,
3662
+ state: opts.state
3663
+ });
3664
+ if (!response) {
3665
+ throw new Error("Server unavailable");
3666
+ }
3667
+ await publishSessionRegistration(response.id, opts.metadata);
3668
+ const session = opts.api.sessionSyncClient(response);
3669
+ await syncControlledByUserState(session, true);
3670
+ const scanner = await createSessionScanner({
3671
+ sessionId: null,
3672
+ workingDirectory: opts.workingDirectory,
3673
+ onMessage: (message) => session.sendClaudeSessionMessage(message)
3674
+ });
3675
+ if (offlineSessionId) {
3676
+ scanner.onNewSession(offlineSessionId);
3677
+ }
3678
+ return { session, scanner };
3679
+ },
3680
+ onNotify: console.log,
3681
+ onCleanup: () => {
3682
+ logger.debug("[claude] Offline reconnection cleanup triggered");
3683
+ }
3684
+ });
3685
+ try {
3686
+ await claudeLocal({
3687
+ path: opts.workingDirectory,
3688
+ sessionId: null,
3689
+ onSessionFound: (sessionId) => {
3690
+ offlineSessionId = sessionId;
3691
+ },
3692
+ onThinkingChange: () => {
3693
+ },
3694
+ abort: new AbortController().signal,
3695
+ claudeEnvVars: opts.claudeEnvVars,
3696
+ claudeArgs: opts.claudeArgs,
3697
+ allowedTools: []
3698
+ });
3699
+ } finally {
3700
+ reconnection.cancel();
3701
+ stopCaffeinate();
3702
+ }
3703
+ }
3704
+
3705
+ function hasMetaOverride(message, key) {
3706
+ return Object.prototype.hasOwnProperty.call(message.meta ?? {}, key);
3707
+ }
3708
+ function resolveNullableStringOverride(message, key, currentValue) {
3709
+ if (!hasMetaOverride(message, key)) {
3710
+ return currentValue;
3711
+ }
3712
+ return message.meta?.[key] || void 0;
3713
+ }
3714
+ function resolveNullableArrayOverride(message, key, currentValue) {
3715
+ if (!hasMetaOverride(message, key)) {
3716
+ return currentValue;
3717
+ }
3718
+ return message.meta?.[key] || void 0;
3719
+ }
3720
+ function resolveClaudeQueuedMessage(message, currentState) {
3721
+ const permissionMode = message.meta?.permissionMode ?? currentState.permissionMode;
3722
+ const model = resolveNullableStringOverride(message, "model", currentState.model);
3723
+ const fallbackModel = resolveNullableStringOverride(message, "fallbackModel", currentState.fallbackModel);
3724
+ const customSystemPrompt = resolveNullableStringOverride(
3725
+ message,
3726
+ "customSystemPrompt",
3727
+ currentState.customSystemPrompt
3728
+ );
3729
+ const appendSystemPrompt = resolveNullableStringOverride(
3730
+ message,
3731
+ "appendSystemPrompt",
3732
+ currentState.appendSystemPrompt
3733
+ );
3734
+ const allowedTools = resolveNullableArrayOverride(message, "allowedTools", currentState.allowedTools);
3735
+ const disallowedTools = resolveNullableArrayOverride(message, "disallowedTools", currentState.disallowedTools);
3736
+ const mode = {
3737
+ permissionMode: permissionMode || "default",
3738
+ model,
3739
+ fallbackModel,
3740
+ customSystemPrompt,
3741
+ appendSystemPrompt,
3742
+ allowedTools,
3743
+ disallowedTools
3744
+ };
3745
+ const specialCommand = parseSpecialCommand(message.content.text);
3746
+ const nextState = {
3747
+ permissionMode,
3748
+ model,
3749
+ fallbackModel,
3750
+ customSystemPrompt,
3751
+ appendSystemPrompt,
3752
+ allowedTools,
3753
+ disallowedTools
3754
+ };
3755
+ if (specialCommand.type === "compact" || specialCommand.type === "clear") {
3756
+ return {
3757
+ nextState,
3758
+ queuedMessage: {
3759
+ text: specialCommand.originalMessage || message.content.text,
3760
+ mode,
3761
+ isolate: true,
3762
+ specialCommand: specialCommand.type
3763
+ }
3764
+ };
3765
+ }
3766
+ return {
3767
+ nextState,
3768
+ queuedMessage: {
3769
+ text: message.content.text,
3770
+ mode,
3771
+ isolate: false,
3772
+ specialCommand: null
3773
+ }
3774
+ };
3775
+ }
3776
+ function bindClaudeUserMessageQueue(opts) {
3777
+ let currentState = { ...opts.initialState };
3778
+ opts.session.onUserMessage((message) => {
3779
+ const { nextState, queuedMessage } = resolveClaudeQueuedMessage(message, currentState);
3780
+ currentState = nextState;
3781
+ if (queuedMessage.isolate) {
3782
+ logger.debug(`[claude] Queuing isolated special command: ${queuedMessage.specialCommand}`);
3783
+ opts.messageQueue.pushIsolateAndClear(queuedMessage.text, queuedMessage.mode);
3784
+ } else {
3785
+ opts.messageQueue.push(queuedMessage.text, queuedMessage.mode);
3786
+ }
3787
+ logger.debugLargeJson("[claude] User message queued:", message);
3788
+ });
3789
+ }
3790
+
3151
3791
  async function runClaude(credentials, options = {}) {
3152
3792
  logger.debug(`[CLAUDE] ===== CLAUDE MODE STARTING =====`);
3153
3793
  logger.debug(`[CLAUDE] This is the Claude agent, NOT Gemini`);
@@ -3160,36 +3800,25 @@ async function runClaude(credentials, options = {}) {
3160
3800
  }
3161
3801
  connectionState.setBackend("Claude");
3162
3802
  const api = await ApiClient.create(credentials);
3163
- let state = {};
3164
- const settings = await readSettings();
3165
- let machineId = settings?.machineId;
3166
- if (!machineId) {
3167
- console.error(`[START] No machine ID found in settings, which is unexpected since authAndSetupMachineIfNeeded should have created it. Please report this issue on https://github.com/slopus/happy-cli/issues`);
3168
- process.exit(1);
3803
+ let machineId;
3804
+ try {
3805
+ machineId = await ensureManagedProviderMachine({
3806
+ api,
3807
+ missingMachineIdMessage: "[START] No machine ID found in settings, which is unexpected since authAndSetupMachineIfNeeded should have created it. Please report this issue on https://github.com/slopus/happy-cli/issues"
3808
+ });
3809
+ } catch (error) {
3810
+ if (error instanceof MissingMachineIdError) {
3811
+ console.error(error.message);
3812
+ process.exit(1);
3813
+ }
3814
+ throw error;
3169
3815
  }
3170
3816
  logger.debug(`Using machineId: ${machineId}`);
3171
- await api.getOrCreateMachine({
3817
+ const { state, metadata } = createSessionMetadata({
3818
+ flavor: "claude",
3172
3819
  machineId,
3173
- metadata: initialMachineMetadata
3820
+ startedBy: options.startedBy
3174
3821
  });
3175
- let metadata = {
3176
- path: workingDirectory,
3177
- host: os.hostname(),
3178
- version: packageJson.version,
3179
- os: os.platform(),
3180
- machineId,
3181
- homeDir: os.homedir(),
3182
- happyHomeDir: configuration.happyCloudHomeDir,
3183
- happyLibDir: projectPath(),
3184
- happyToolsDir: resolve(projectPath(), "tools", "unpacked"),
3185
- startedFromDaemon: options.startedBy === "daemon",
3186
- hostPid: process.pid,
3187
- startedBy: options.startedBy || "terminal",
3188
- // Initialize lifecycle state
3189
- lifecycleState: "running",
3190
- lifecycleStateSince: Date.now(),
3191
- flavor: "claude"
3192
- };
3193
3822
  let response = null;
3194
3823
  try {
3195
3824
  response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
@@ -3200,44 +3829,14 @@ async function runClaude(credentials, options = {}) {
3200
3829
  logger.debug("[START] Falling back to local/offline mode after authentication failure");
3201
3830
  }
3202
3831
  if (!response) {
3203
- let offlineSessionId = null;
3204
- const reconnection = startOfflineReconnection({
3205
- serverUrl: configuration.serverUrl,
3206
- onReconnected: async () => {
3207
- const resp = await api.getOrCreateSession({ tag: randomUUID(), metadata, state });
3208
- if (!resp) throw new Error("Server unavailable");
3209
- await publishSessionRegistration(resp.id, metadata);
3210
- const session2 = api.sessionSyncClient(resp);
3211
- const scanner = await createSessionScanner({
3212
- sessionId: null,
3213
- workingDirectory,
3214
- onMessage: (msg) => session2.sendClaudeSessionMessage(msg)
3215
- });
3216
- if (offlineSessionId) scanner.onNewSession(offlineSessionId);
3217
- return { session: session2, scanner };
3218
- },
3219
- onNotify: console.log,
3220
- onCleanup: () => {
3221
- }
3832
+ await runOfflineClaudeLocalFallback({
3833
+ api,
3834
+ metadata,
3835
+ state,
3836
+ workingDirectory,
3837
+ claudeEnvVars: options.claudeEnvVars,
3838
+ claudeArgs: options.claudeArgs
3222
3839
  });
3223
- try {
3224
- await claudeLocal({
3225
- path: workingDirectory,
3226
- sessionId: null,
3227
- onSessionFound: (id) => {
3228
- offlineSessionId = id;
3229
- },
3230
- onThinkingChange: () => {
3231
- },
3232
- abort: new AbortController().signal,
3233
- claudeEnvVars: options.claudeEnvVars,
3234
- claudeArgs: options.claudeArgs,
3235
- allowedTools: []
3236
- });
3237
- } finally {
3238
- reconnection.cancel();
3239
- stopCaffeinate();
3240
- }
3241
3840
  process.exit(0);
3242
3841
  }
3243
3842
  logger.debug(`Session created: ${response.id}`);
@@ -3271,10 +3870,7 @@ async function runClaude(credentials, options = {}) {
3271
3870
  const logPath = logger.logFilePath;
3272
3871
  logger.infoDeveloper(`Session: ${response.id}`);
3273
3872
  logger.infoDeveloper(`Logs: ${logPath}`);
3274
- session.updateAgentState((currentState) => ({
3275
- ...currentState,
3276
- controlledByUser: options.startingMode !== "remote"
3277
- }));
3873
+ await syncControlledByUserState(session, options.startingMode !== "remote");
3278
3874
  const caffeinateStarted = startCaffeinate();
3279
3875
  if (caffeinateStarted) {
3280
3876
  logger.infoDeveloper("Sleep prevention enabled (macOS)");
@@ -3288,128 +3884,22 @@ async function runClaude(credentials, options = {}) {
3288
3884
  allowedTools: mode.allowedTools,
3289
3885
  disallowedTools: mode.disallowedTools
3290
3886
  }));
3291
- let currentPermissionMode = options.permissionMode;
3292
- let currentModel = options.model;
3293
- let currentFallbackModel = void 0;
3294
- let currentCustomSystemPrompt = void 0;
3295
- let currentAppendSystemPrompt = void 0;
3296
- let currentAllowedTools = void 0;
3297
- let currentDisallowedTools = void 0;
3298
- session.onUserMessage((message) => {
3299
- let messagePermissionMode = currentPermissionMode;
3300
- if (message.meta?.permissionMode) {
3301
- messagePermissionMode = message.meta.permissionMode;
3302
- currentPermissionMode = messagePermissionMode;
3303
- logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
3304
- } else {
3305
- logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
3306
- }
3307
- let messageModel = currentModel;
3308
- if (message.meta?.hasOwnProperty("model")) {
3309
- messageModel = message.meta.model || void 0;
3310
- currentModel = messageModel;
3311
- logger.debug(`[loop] Model updated from user message: ${messageModel || "reset to default"}`);
3312
- } else {
3313
- logger.debug(`[loop] User message received with no model override, using current: ${currentModel || "default"}`);
3314
- }
3315
- let messageCustomSystemPrompt = currentCustomSystemPrompt;
3316
- if (message.meta?.hasOwnProperty("customSystemPrompt")) {
3317
- messageCustomSystemPrompt = message.meta.customSystemPrompt || void 0;
3318
- currentCustomSystemPrompt = messageCustomSystemPrompt;
3319
- logger.debug(`[loop] Custom system prompt updated from user message: ${messageCustomSystemPrompt ? "set" : "reset to none"}`);
3320
- } else {
3321
- logger.debug(`[loop] User message received with no custom system prompt override, using current: ${currentCustomSystemPrompt ? "set" : "none"}`);
3322
- }
3323
- let messageFallbackModel = currentFallbackModel;
3324
- if (message.meta?.hasOwnProperty("fallbackModel")) {
3325
- messageFallbackModel = message.meta.fallbackModel || void 0;
3326
- currentFallbackModel = messageFallbackModel;
3327
- logger.debug(`[loop] Fallback model updated from user message: ${messageFallbackModel || "reset to none"}`);
3328
- } else {
3329
- logger.debug(`[loop] User message received with no fallback model override, using current: ${currentFallbackModel || "none"}`);
3330
- }
3331
- let messageAppendSystemPrompt = currentAppendSystemPrompt;
3332
- if (message.meta?.hasOwnProperty("appendSystemPrompt")) {
3333
- messageAppendSystemPrompt = message.meta.appendSystemPrompt || void 0;
3334
- currentAppendSystemPrompt = messageAppendSystemPrompt;
3335
- logger.debug(`[loop] Append system prompt updated from user message: ${messageAppendSystemPrompt ? "set" : "reset to none"}`);
3336
- } else {
3337
- logger.debug(`[loop] User message received with no append system prompt override, using current: ${currentAppendSystemPrompt ? "set" : "none"}`);
3338
- }
3339
- let messageAllowedTools = currentAllowedTools;
3340
- if (message.meta?.hasOwnProperty("allowedTools")) {
3341
- messageAllowedTools = message.meta.allowedTools || void 0;
3342
- currentAllowedTools = messageAllowedTools;
3343
- logger.debug(`[loop] Allowed tools updated from user message: ${messageAllowedTools ? messageAllowedTools.join(", ") : "reset to none"}`);
3344
- } else {
3345
- logger.debug(`[loop] User message received with no allowed tools override, using current: ${currentAllowedTools ? currentAllowedTools.join(", ") : "none"}`);
3346
- }
3347
- let messageDisallowedTools = currentDisallowedTools;
3348
- if (message.meta?.hasOwnProperty("disallowedTools")) {
3349
- messageDisallowedTools = message.meta.disallowedTools || void 0;
3350
- currentDisallowedTools = messageDisallowedTools;
3351
- logger.debug(`[loop] Disallowed tools updated from user message: ${messageDisallowedTools ? messageDisallowedTools.join(", ") : "reset to none"}`);
3352
- } else {
3353
- logger.debug(`[loop] User message received with no disallowed tools override, using current: ${currentDisallowedTools ? currentDisallowedTools.join(", ") : "none"}`);
3354
- }
3355
- const specialCommand = parseSpecialCommand(message.content.text);
3356
- if (specialCommand.type === "compact") {
3357
- logger.debug("[start] Detected /compact command");
3358
- const enhancedMode2 = {
3359
- permissionMode: messagePermissionMode || "default",
3360
- model: messageModel,
3361
- fallbackModel: messageFallbackModel,
3362
- customSystemPrompt: messageCustomSystemPrompt,
3363
- appendSystemPrompt: messageAppendSystemPrompt,
3364
- allowedTools: messageAllowedTools,
3365
- disallowedTools: messageDisallowedTools
3366
- };
3367
- messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
3368
- logger.debugLargeJson("[start] /compact command pushed to queue:", message);
3369
- return;
3370
- }
3371
- if (specialCommand.type === "clear") {
3372
- logger.debug("[start] Detected /clear command");
3373
- const enhancedMode2 = {
3374
- permissionMode: messagePermissionMode || "default",
3375
- model: messageModel,
3376
- fallbackModel: messageFallbackModel,
3377
- customSystemPrompt: messageCustomSystemPrompt,
3378
- appendSystemPrompt: messageAppendSystemPrompt,
3379
- allowedTools: messageAllowedTools,
3380
- disallowedTools: messageDisallowedTools
3381
- };
3382
- messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
3383
- logger.debugLargeJson("[start] /compact command pushed to queue:", message);
3384
- return;
3887
+ bindClaudeUserMessageQueue({
3888
+ session,
3889
+ messageQueue,
3890
+ initialState: {
3891
+ permissionMode: options.permissionMode,
3892
+ model: options.model
3385
3893
  }
3386
- const enhancedMode = {
3387
- permissionMode: messagePermissionMode || "default",
3388
- model: messageModel,
3389
- fallbackModel: messageFallbackModel,
3390
- customSystemPrompt: messageCustomSystemPrompt,
3391
- appendSystemPrompt: messageAppendSystemPrompt,
3392
- allowedTools: messageAllowedTools,
3393
- disallowedTools: messageDisallowedTools
3394
- };
3395
- messageQueue.push(message.content.text, enhancedMode);
3396
- logger.debugLargeJson("User message pushed to queue:", message);
3397
3894
  });
3398
3895
  const cleanup = async () => {
3399
3896
  logger.debug("[START] Received termination signal, cleaning up...");
3400
3897
  try {
3401
3898
  if (session) {
3402
- session.updateMetadata((currentMetadata) => ({
3403
- ...currentMetadata,
3404
- lifecycleState: "archived",
3405
- lifecycleStateSince: Date.now(),
3406
- archivedBy: "cli",
3407
- archiveReason: "User terminated"
3408
- }));
3409
3899
  currentSession?.cleanup();
3410
- session.sendSessionDeath();
3411
- await session.flush();
3412
- await session.close();
3900
+ await closeProviderSession(session, {
3901
+ archiveReason: "User terminated"
3902
+ });
3413
3903
  }
3414
3904
  stopCaffeinate();
3415
3905
  hookServer.stop();
@@ -3441,12 +3931,9 @@ async function runClaude(credentials, options = {}) {
3441
3931
  api,
3442
3932
  allowedTools: [],
3443
3933
  mcpServers: {},
3444
- onModeChange: (newMode) => {
3934
+ onModeChange: async (newMode) => {
3445
3935
  session.sendSessionEvent({ type: "switch", mode: newMode });
3446
- session.updateAgentState((currentState) => ({
3447
- ...currentState,
3448
- controlledByUser: newMode === "local"
3449
- }));
3936
+ await syncControlledByUserState(session, newMode === "local");
3450
3937
  },
3451
3938
  onSessionReady: (sessionInstance) => {
3452
3939
  currentSession = sessionInstance;
@@ -3455,14 +3942,13 @@ async function runClaude(credentials, options = {}) {
3455
3942
  claudeEnvVars: options.claudeEnvVars,
3456
3943
  claudeArgs: options.claudeArgs,
3457
3944
  hookSettingsPath,
3458
- jsRuntime: options.jsRuntime
3945
+ jsRuntime: options.jsRuntime,
3946
+ startedBy: options.startedBy
3459
3947
  });
3460
3948
  currentSession?.cleanup();
3461
- session.sendSessionDeath();
3462
3949
  logger.debug("Waiting for socket to flush...");
3463
- await session.flush();
3464
3950
  logger.debug("Closing session...");
3465
- await session.close();
3951
+ await closeProviderSession(session);
3466
3952
  stopCaffeinate();
3467
3953
  logger.debug("Stopped sleep prevention");
3468
3954
  hookServer.stop();