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