happy-coder 0.7.2 → 0.9.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +699 -439
- package/dist/index.mjs +700 -440
- package/dist/lib.d.cts +75 -71
- package/dist/lib.d.mts +75 -71
- package/package.json +11 -9
package/dist/index.cjs
CHANGED
|
@@ -20,18 +20,18 @@ require('node:events');
|
|
|
20
20
|
require('socket.io-client');
|
|
21
21
|
var tweetnacl = require('tweetnacl');
|
|
22
22
|
require('expo-server-sdk');
|
|
23
|
-
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
24
|
-
var node_http = require('node:http');
|
|
25
|
-
var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
26
|
-
var z = require('zod');
|
|
27
23
|
var child_process = require('child_process');
|
|
28
24
|
var util = require('util');
|
|
29
25
|
var crypto = require('crypto');
|
|
26
|
+
var z = require('zod');
|
|
30
27
|
var fastify = require('fastify');
|
|
31
28
|
var fastifyTypeProviderZod = require('fastify-type-provider-zod');
|
|
32
29
|
var os$1 = require('os');
|
|
33
30
|
var qrcode = require('qrcode-terminal');
|
|
34
31
|
var open = require('open');
|
|
32
|
+
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
33
|
+
var node_http = require('node:http');
|
|
34
|
+
var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
35
35
|
var fs = require('fs');
|
|
36
36
|
|
|
37
37
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
@@ -63,6 +63,7 @@ class Session {
|
|
|
63
63
|
claudeEnvVars;
|
|
64
64
|
claudeArgs;
|
|
65
65
|
mcpServers;
|
|
66
|
+
allowedTools;
|
|
66
67
|
_onModeChange;
|
|
67
68
|
sessionId;
|
|
68
69
|
mode = "local";
|
|
@@ -77,6 +78,7 @@ class Session {
|
|
|
77
78
|
this.claudeEnvVars = opts.claudeEnvVars;
|
|
78
79
|
this.claudeArgs = opts.claudeArgs;
|
|
79
80
|
this.mcpServers = opts.mcpServers;
|
|
81
|
+
this.allowedTools = opts.allowedTools;
|
|
80
82
|
this._onModeChange = opts.onModeChange;
|
|
81
83
|
this.client.keepAlive(this.thinking, this.mode);
|
|
82
84
|
setInterval(() => {
|
|
@@ -135,6 +137,29 @@ function projectPath() {
|
|
|
135
137
|
return path$1;
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
function trimIdent(text) {
|
|
141
|
+
const lines = text.split("\n");
|
|
142
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
143
|
+
lines.shift();
|
|
144
|
+
}
|
|
145
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
146
|
+
lines.pop();
|
|
147
|
+
}
|
|
148
|
+
const minSpaces = lines.reduce((min, line) => {
|
|
149
|
+
if (line.trim() === "") {
|
|
150
|
+
return min;
|
|
151
|
+
}
|
|
152
|
+
const leadingSpaces = line.match(/^\s*/)[0].length;
|
|
153
|
+
return Math.min(min, leadingSpaces);
|
|
154
|
+
}, Infinity);
|
|
155
|
+
const trimmedLines = lines.map((line) => line.slice(minSpaces));
|
|
156
|
+
return trimmedLines.join("\n");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const systemPrompt = trimIdent(`
|
|
160
|
+
ALWAYS when you start a new chat - you must call a tool "mcp__happy__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
|
|
161
|
+
`);
|
|
162
|
+
|
|
138
163
|
node_path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
139
164
|
async function claudeLocal(opts) {
|
|
140
165
|
const projectDir = getProjectPath(opts.path);
|
|
@@ -182,6 +207,13 @@ async function claudeLocal(opts) {
|
|
|
182
207
|
if (startFrom) {
|
|
183
208
|
args.push("--resume", startFrom);
|
|
184
209
|
}
|
|
210
|
+
args.push("--append-system-prompt", systemPrompt);
|
|
211
|
+
if (opts.mcpServers && Object.keys(opts.mcpServers).length > 0) {
|
|
212
|
+
args.push("--mcp-config", JSON.stringify({ mcpServers: opts.mcpServers }));
|
|
213
|
+
}
|
|
214
|
+
if (opts.allowedTools && opts.allowedTools.length > 0) {
|
|
215
|
+
args.push("--allowedTools", opts.allowedTools.join(","));
|
|
216
|
+
}
|
|
185
217
|
if (opts.claudeArgs) {
|
|
186
218
|
args.push(...opts.claudeArgs);
|
|
187
219
|
}
|
|
@@ -526,7 +558,9 @@ async function claudeLocalLauncher(session) {
|
|
|
526
558
|
sessionId: session.sessionId,
|
|
527
559
|
workingDirectory: session.path,
|
|
528
560
|
onMessage: (message) => {
|
|
529
|
-
|
|
561
|
+
if (message.type !== "summary") {
|
|
562
|
+
session.client.sendClaudeSessionMessage(message);
|
|
563
|
+
}
|
|
530
564
|
}
|
|
531
565
|
});
|
|
532
566
|
let exitReason = null;
|
|
@@ -556,7 +590,9 @@ async function claudeLocalLauncher(session) {
|
|
|
556
590
|
}
|
|
557
591
|
session.client.setHandler("abort", doAbort);
|
|
558
592
|
session.client.setHandler("switch", doSwitch);
|
|
559
|
-
session.queue.setOnMessage(
|
|
593
|
+
session.queue.setOnMessage((message, mode) => {
|
|
594
|
+
doSwitch();
|
|
595
|
+
});
|
|
560
596
|
if (session.queue.size() > 0) {
|
|
561
597
|
return "switch";
|
|
562
598
|
}
|
|
@@ -577,7 +613,9 @@ async function claudeLocalLauncher(session) {
|
|
|
577
613
|
onThinkingChange: session.onThinkingChange,
|
|
578
614
|
abort: processAbortController.signal,
|
|
579
615
|
claudeEnvVars: session.claudeEnvVars,
|
|
580
|
-
claudeArgs: session.claudeArgs
|
|
616
|
+
claudeArgs: session.claudeArgs,
|
|
617
|
+
mcpServers: session.mcpServers,
|
|
618
|
+
allowedTools: session.allowedTools
|
|
581
619
|
});
|
|
582
620
|
if (!exitReason) {
|
|
583
621
|
exitReason = "exit";
|
|
@@ -885,16 +923,19 @@ async function streamToStdin(stream, stdin, abort) {
|
|
|
885
923
|
}
|
|
886
924
|
|
|
887
925
|
class Query {
|
|
888
|
-
constructor(childStdin, childStdout, processExitPromise) {
|
|
926
|
+
constructor(childStdin, childStdout, processExitPromise, canCallTool) {
|
|
889
927
|
this.childStdin = childStdin;
|
|
890
928
|
this.childStdout = childStdout;
|
|
891
929
|
this.processExitPromise = processExitPromise;
|
|
930
|
+
this.canCallTool = canCallTool;
|
|
892
931
|
this.readMessages();
|
|
893
932
|
this.sdkMessages = this.readSdkMessages();
|
|
894
933
|
}
|
|
895
934
|
pendingControlResponses = /* @__PURE__ */ new Map();
|
|
935
|
+
cancelControllers = /* @__PURE__ */ new Map();
|
|
896
936
|
sdkMessages;
|
|
897
937
|
inputStream = new Stream();
|
|
938
|
+
canCallTool;
|
|
898
939
|
/**
|
|
899
940
|
* Set an error on the stream
|
|
900
941
|
*/
|
|
@@ -939,6 +980,12 @@ class Query {
|
|
|
939
980
|
handler(controlResponse.response);
|
|
940
981
|
}
|
|
941
982
|
continue;
|
|
983
|
+
} else if (message.type === "control_request") {
|
|
984
|
+
await this.handleControlRequest(message);
|
|
985
|
+
continue;
|
|
986
|
+
} else if (message.type === "control_cancel_request") {
|
|
987
|
+
this.handleControlCancelRequest(message);
|
|
988
|
+
continue;
|
|
942
989
|
}
|
|
943
990
|
this.inputStream.enqueue(message);
|
|
944
991
|
} catch (e) {
|
|
@@ -951,6 +998,7 @@ class Query {
|
|
|
951
998
|
this.inputStream.error(error);
|
|
952
999
|
} finally {
|
|
953
1000
|
this.inputStream.done();
|
|
1001
|
+
this.cleanupControllers();
|
|
954
1002
|
rl.close();
|
|
955
1003
|
}
|
|
956
1004
|
}
|
|
@@ -994,6 +1042,77 @@ class Query {
|
|
|
994
1042
|
childStdin.write(JSON.stringify(sdkRequest) + "\n");
|
|
995
1043
|
});
|
|
996
1044
|
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Handle incoming control requests for tool permissions
|
|
1047
|
+
* Replicates the exact logic from the SDK's handleControlRequest method
|
|
1048
|
+
*/
|
|
1049
|
+
async handleControlRequest(request) {
|
|
1050
|
+
if (!this.childStdin) {
|
|
1051
|
+
logDebug("Cannot handle control request - no stdin available");
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
const controller = new AbortController();
|
|
1055
|
+
this.cancelControllers.set(request.request_id, controller);
|
|
1056
|
+
try {
|
|
1057
|
+
const response = await this.processControlRequest(request, controller.signal);
|
|
1058
|
+
const controlResponse = {
|
|
1059
|
+
type: "control_response",
|
|
1060
|
+
response: {
|
|
1061
|
+
subtype: "success",
|
|
1062
|
+
request_id: request.request_id,
|
|
1063
|
+
response
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
this.childStdin.write(JSON.stringify(controlResponse) + "\n");
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
const controlErrorResponse = {
|
|
1069
|
+
type: "control_response",
|
|
1070
|
+
response: {
|
|
1071
|
+
subtype: "error",
|
|
1072
|
+
request_id: request.request_id,
|
|
1073
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
this.childStdin.write(JSON.stringify(controlErrorResponse) + "\n");
|
|
1077
|
+
} finally {
|
|
1078
|
+
this.cancelControllers.delete(request.request_id);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Handle control cancel requests
|
|
1083
|
+
* Replicates the exact logic from the SDK's handleControlCancelRequest method
|
|
1084
|
+
*/
|
|
1085
|
+
handleControlCancelRequest(request) {
|
|
1086
|
+
const controller = this.cancelControllers.get(request.request_id);
|
|
1087
|
+
if (controller) {
|
|
1088
|
+
controller.abort();
|
|
1089
|
+
this.cancelControllers.delete(request.request_id);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Process control requests based on subtype
|
|
1094
|
+
* Replicates the exact logic from the SDK's processControlRequest method
|
|
1095
|
+
*/
|
|
1096
|
+
async processControlRequest(request, signal) {
|
|
1097
|
+
if (request.request.subtype === "can_use_tool") {
|
|
1098
|
+
if (!this.canCallTool) {
|
|
1099
|
+
throw new Error("canCallTool callback is not provided.");
|
|
1100
|
+
}
|
|
1101
|
+
return this.canCallTool(request.request.tool_name, request.request.input, {
|
|
1102
|
+
signal
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
throw new Error("Unsupported control request subtype: " + request.request.subtype);
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Cleanup method to abort all pending control requests
|
|
1109
|
+
*/
|
|
1110
|
+
cleanupControllers() {
|
|
1111
|
+
for (const [requestId, controller] of this.cancelControllers.entries()) {
|
|
1112
|
+
controller.abort();
|
|
1113
|
+
this.cancelControllers.delete(requestId);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
997
1116
|
}
|
|
998
1117
|
function query(config) {
|
|
999
1118
|
const {
|
|
@@ -1010,12 +1129,12 @@ function query(config) {
|
|
|
1010
1129
|
mcpServers,
|
|
1011
1130
|
pathToClaudeCodeExecutable = getDefaultClaudeCodePath(),
|
|
1012
1131
|
permissionMode = "default",
|
|
1013
|
-
permissionPromptToolName,
|
|
1014
1132
|
continue: continueConversation,
|
|
1015
1133
|
resume,
|
|
1016
1134
|
model,
|
|
1017
1135
|
fallbackModel,
|
|
1018
|
-
strictMcpConfig
|
|
1136
|
+
strictMcpConfig,
|
|
1137
|
+
canCallTool
|
|
1019
1138
|
} = {}
|
|
1020
1139
|
} = config;
|
|
1021
1140
|
if (!process.env.CLAUDE_CODE_ENTRYPOINT) {
|
|
@@ -1026,7 +1145,12 @@ function query(config) {
|
|
|
1026
1145
|
if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
|
|
1027
1146
|
if (maxTurns) args.push("--max-turns", maxTurns.toString());
|
|
1028
1147
|
if (model) args.push("--model", model);
|
|
1029
|
-
if (
|
|
1148
|
+
if (canCallTool) {
|
|
1149
|
+
if (typeof prompt === "string") {
|
|
1150
|
+
throw new Error("canCallTool callback requires --input-format stream-json. Please set prompt as an AsyncIterable.");
|
|
1151
|
+
}
|
|
1152
|
+
args.push("--permission-prompt-tool", "stdio");
|
|
1153
|
+
}
|
|
1030
1154
|
if (continueConversation) args.push("--continue");
|
|
1031
1155
|
if (resume) args.push("--resume", resume);
|
|
1032
1156
|
if (allowedTools.length > 0) args.push("--allowedTools", allowedTools.join(","));
|
|
@@ -1090,7 +1214,7 @@ function query(config) {
|
|
|
1090
1214
|
}
|
|
1091
1215
|
});
|
|
1092
1216
|
});
|
|
1093
|
-
const query2 = new Query(childStdin, child.stdout, processExitPromise);
|
|
1217
|
+
const query2 = new Query(childStdin, child.stdout, processExitPromise, canCallTool);
|
|
1094
1218
|
child.on("error", (error) => {
|
|
1095
1219
|
if (config.options?.abort?.aborted) {
|
|
1096
1220
|
query2.setError(new AbortError("Claude Code process aborted by user"));
|
|
@@ -1108,17 +1232,48 @@ function query(config) {
|
|
|
1108
1232
|
return query2;
|
|
1109
1233
|
}
|
|
1110
1234
|
|
|
1111
|
-
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1118
|
-
await types$1.delay(1e3);
|
|
1119
|
-
}
|
|
1235
|
+
function parseCompact(message) {
|
|
1236
|
+
const trimmed = message.trim();
|
|
1237
|
+
if (trimmed === "/compact") {
|
|
1238
|
+
return {
|
|
1239
|
+
isCompact: true,
|
|
1240
|
+
originalMessage: trimmed
|
|
1241
|
+
};
|
|
1120
1242
|
}
|
|
1121
|
-
|
|
1243
|
+
if (trimmed.startsWith("/compact ")) {
|
|
1244
|
+
return {
|
|
1245
|
+
isCompact: true,
|
|
1246
|
+
originalMessage: trimmed
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
return {
|
|
1250
|
+
isCompact: false,
|
|
1251
|
+
originalMessage: message
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function parseClear(message) {
|
|
1255
|
+
const trimmed = message.trim();
|
|
1256
|
+
return {
|
|
1257
|
+
isClear: trimmed === "/clear"
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
function parseSpecialCommand(message) {
|
|
1261
|
+
const compactResult = parseCompact(message);
|
|
1262
|
+
if (compactResult.isCompact) {
|
|
1263
|
+
return {
|
|
1264
|
+
type: "compact",
|
|
1265
|
+
originalMessage: compactResult.originalMessage
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
const clearResult = parseClear(message);
|
|
1269
|
+
if (clearResult.isClear) {
|
|
1270
|
+
return {
|
|
1271
|
+
type: "clear"
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
return {
|
|
1275
|
+
type: null
|
|
1276
|
+
};
|
|
1122
1277
|
}
|
|
1123
1278
|
|
|
1124
1279
|
class PushableAsyncIterable {
|
|
@@ -1247,48 +1402,17 @@ class PushableAsyncIterable {
|
|
|
1247
1402
|
}
|
|
1248
1403
|
}
|
|
1249
1404
|
|
|
1250
|
-
function
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
return {
|
|
1260
|
-
isCompact: true,
|
|
1261
|
-
originalMessage: trimmed
|
|
1262
|
-
};
|
|
1263
|
-
}
|
|
1264
|
-
return {
|
|
1265
|
-
isCompact: false,
|
|
1266
|
-
originalMessage: message
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
function parseClear(message) {
|
|
1270
|
-
const trimmed = message.trim();
|
|
1271
|
-
return {
|
|
1272
|
-
isClear: trimmed === "/clear"
|
|
1273
|
-
};
|
|
1274
|
-
}
|
|
1275
|
-
function parseSpecialCommand(message) {
|
|
1276
|
-
const compactResult = parseCompact(message);
|
|
1277
|
-
if (compactResult.isCompact) {
|
|
1278
|
-
return {
|
|
1279
|
-
type: "compact",
|
|
1280
|
-
originalMessage: compactResult.originalMessage
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
const clearResult = parseClear(message);
|
|
1284
|
-
if (clearResult.isClear) {
|
|
1285
|
-
return {
|
|
1286
|
-
type: "clear"
|
|
1287
|
-
};
|
|
1405
|
+
async function awaitFileExist(file, timeout = 1e4) {
|
|
1406
|
+
const startTime = Date.now();
|
|
1407
|
+
while (Date.now() - startTime < timeout) {
|
|
1408
|
+
try {
|
|
1409
|
+
await promises.access(file);
|
|
1410
|
+
return true;
|
|
1411
|
+
} catch (e) {
|
|
1412
|
+
await types$1.delay(1e3);
|
|
1413
|
+
}
|
|
1288
1414
|
}
|
|
1289
|
-
return
|
|
1290
|
-
type: null
|
|
1291
|
-
};
|
|
1415
|
+
return false;
|
|
1292
1416
|
}
|
|
1293
1417
|
|
|
1294
1418
|
async function claudeRemote(opts) {
|
|
@@ -1301,32 +1425,12 @@ async function claudeRemote(opts) {
|
|
|
1301
1425
|
process.env[key] = value;
|
|
1302
1426
|
});
|
|
1303
1427
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
resume: startFrom ?? void 0,
|
|
1308
|
-
mcpServers: opts.mcpServers,
|
|
1309
|
-
permissionPromptToolName: opts.permissionPromptToolName,
|
|
1310
|
-
permissionMode: opts.permissionMode,
|
|
1311
|
-
model: opts.model,
|
|
1312
|
-
fallbackModel: opts.fallbackModel,
|
|
1313
|
-
customSystemPrompt: opts.customSystemPrompt,
|
|
1314
|
-
appendSystemPrompt: opts.appendSystemPrompt,
|
|
1315
|
-
allowedTools: opts.allowedTools,
|
|
1316
|
-
disallowedTools: opts.disallowedTools,
|
|
1317
|
-
executable: "node",
|
|
1318
|
-
abort: opts.signal,
|
|
1319
|
-
pathToClaudeCodeExecutable: (() => {
|
|
1320
|
-
return node_path.resolve(node_path.join(projectPath(), "scripts", "claude_remote_launcher.cjs"));
|
|
1321
|
-
})()
|
|
1322
|
-
};
|
|
1323
|
-
if (opts.claudeArgs && opts.claudeArgs.length > 0) {
|
|
1324
|
-
sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
|
|
1428
|
+
const initial = await opts.nextMessage();
|
|
1429
|
+
if (!initial) {
|
|
1430
|
+
return;
|
|
1325
1431
|
}
|
|
1326
|
-
|
|
1327
|
-
const specialCommand = parseSpecialCommand(opts.message);
|
|
1432
|
+
const specialCommand = parseSpecialCommand(initial.message);
|
|
1328
1433
|
if (specialCommand.type === "clear") {
|
|
1329
|
-
types$1.logger.debug("[claudeRemote] /clear command detected - should not reach here, handled in start.ts");
|
|
1330
1434
|
if (opts.onCompletionEvent) {
|
|
1331
1435
|
opts.onCompletionEvent("Context was reset");
|
|
1332
1436
|
}
|
|
@@ -1335,23 +1439,33 @@ async function claudeRemote(opts) {
|
|
|
1335
1439
|
}
|
|
1336
1440
|
return;
|
|
1337
1441
|
}
|
|
1442
|
+
let isCompactCommand = false;
|
|
1338
1443
|
if (specialCommand.type === "compact") {
|
|
1339
1444
|
types$1.logger.debug("[claudeRemote] /compact command detected - will process as normal but with compaction behavior");
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
message.push({
|
|
1344
|
-
type: "user",
|
|
1345
|
-
message: {
|
|
1346
|
-
role: "user",
|
|
1347
|
-
content: opts.message
|
|
1445
|
+
isCompactCommand = true;
|
|
1446
|
+
if (opts.onCompletionEvent) {
|
|
1447
|
+
opts.onCompletionEvent("Compaction started");
|
|
1348
1448
|
}
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1449
|
+
}
|
|
1450
|
+
let mode = initial.mode;
|
|
1451
|
+
const sdkOptions = {
|
|
1452
|
+
cwd: opts.path,
|
|
1453
|
+
resume: startFrom ?? void 0,
|
|
1454
|
+
mcpServers: opts.mcpServers,
|
|
1455
|
+
permissionMode: initial.mode.permissionMode === "plan" ? "plan" : "default",
|
|
1456
|
+
model: initial.mode.model,
|
|
1457
|
+
fallbackModel: initial.mode.fallbackModel,
|
|
1458
|
+
customSystemPrompt: initial.mode.customSystemPrompt ? initial.mode.customSystemPrompt + "\n\n" + systemPrompt : void 0,
|
|
1459
|
+
appendSystemPrompt: initial.mode.appendSystemPrompt ? initial.mode.appendSystemPrompt + "\n\n" + systemPrompt : systemPrompt,
|
|
1460
|
+
allowedTools: initial.mode.allowedTools ? initial.mode.allowedTools.concat(opts.allowedTools) : opts.allowedTools,
|
|
1461
|
+
disallowedTools: initial.mode.disallowedTools,
|
|
1462
|
+
canCallTool: (toolName, input, options) => opts.canCallTool(toolName, input, mode, options),
|
|
1463
|
+
executable: "node",
|
|
1464
|
+
abort: opts.signal,
|
|
1465
|
+
pathToClaudeCodeExecutable: (() => {
|
|
1466
|
+
return node_path.resolve(node_path.join(projectPath(), "scripts", "claude_remote_launcher.cjs"));
|
|
1467
|
+
})()
|
|
1468
|
+
};
|
|
1355
1469
|
let thinking = false;
|
|
1356
1470
|
const updateThinking = (newThinking) => {
|
|
1357
1471
|
if (thinking !== newThinking) {
|
|
@@ -1362,15 +1476,27 @@ async function claudeRemote(opts) {
|
|
|
1362
1476
|
}
|
|
1363
1477
|
}
|
|
1364
1478
|
};
|
|
1479
|
+
let messages = new PushableAsyncIterable();
|
|
1480
|
+
messages.push({
|
|
1481
|
+
type: "user",
|
|
1482
|
+
message: {
|
|
1483
|
+
role: "user",
|
|
1484
|
+
content: initial.message
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
const response = query({
|
|
1488
|
+
prompt: messages,
|
|
1489
|
+
options: sdkOptions
|
|
1490
|
+
});
|
|
1365
1491
|
updateThinking(true);
|
|
1366
1492
|
try {
|
|
1367
1493
|
types$1.logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
1368
|
-
for await (const
|
|
1369
|
-
types$1.logger.debugLargeJson(`[claudeRemote] Message ${
|
|
1370
|
-
opts.onMessage(
|
|
1371
|
-
if (
|
|
1494
|
+
for await (const message of response) {
|
|
1495
|
+
types$1.logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
|
|
1496
|
+
opts.onMessage(message);
|
|
1497
|
+
if (message.type === "system" && message.subtype === "init") {
|
|
1372
1498
|
updateThinking(true);
|
|
1373
|
-
const systemInit =
|
|
1499
|
+
const systemInit = message;
|
|
1374
1500
|
if (systemInit.session_id) {
|
|
1375
1501
|
types$1.logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${systemInit.session_id}`);
|
|
1376
1502
|
const projectDir = getProjectPath(opts.path);
|
|
@@ -1378,14 +1504,8 @@ async function claudeRemote(opts) {
|
|
|
1378
1504
|
types$1.logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
|
|
1379
1505
|
opts.onSessionFound(systemInit.session_id);
|
|
1380
1506
|
}
|
|
1381
|
-
if (isCompactCommand) {
|
|
1382
|
-
types$1.logger.debug("[claudeRemote] Compaction started");
|
|
1383
|
-
if (opts.onCompletionEvent) {
|
|
1384
|
-
opts.onCompletionEvent("Compaction started");
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
1507
|
}
|
|
1388
|
-
if (
|
|
1508
|
+
if (message.type === "result") {
|
|
1389
1509
|
updateThinking(false);
|
|
1390
1510
|
types$1.logger.debug("[claudeRemote] Result received, exiting claudeRemote");
|
|
1391
1511
|
if (isCompactCommand) {
|
|
@@ -1393,26 +1513,28 @@ async function claudeRemote(opts) {
|
|
|
1393
1513
|
if (opts.onCompletionEvent) {
|
|
1394
1514
|
opts.onCompletionEvent("Compaction completed");
|
|
1395
1515
|
}
|
|
1516
|
+
isCompactCommand = false;
|
|
1396
1517
|
}
|
|
1397
|
-
|
|
1518
|
+
const next = await opts.nextMessage();
|
|
1519
|
+
if (!next) {
|
|
1520
|
+
messages.end();
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
mode = next.mode;
|
|
1524
|
+
messages.push({ type: "user", message: { role: "user", content: next.message } });
|
|
1398
1525
|
}
|
|
1399
|
-
if (
|
|
1400
|
-
const msg =
|
|
1526
|
+
if (message.type === "user") {
|
|
1527
|
+
const msg = message;
|
|
1401
1528
|
if (msg.message.role === "user" && Array.isArray(msg.message.content)) {
|
|
1402
1529
|
for (let c of msg.message.content) {
|
|
1403
|
-
if (c.type === "tool_result" &&
|
|
1404
|
-
types$1.logger.debug("[claudeRemote]
|
|
1405
|
-
return;
|
|
1406
|
-
}
|
|
1407
|
-
if (c.type === "tool_result" && c.tool_use_id && opts.responses.has(c.tool_use_id) && !opts.responses.get(c.tool_use_id).approved) {
|
|
1408
|
-
types$1.logger.debug("[claudeRemote] Tool rejected, exiting claudeRemote");
|
|
1530
|
+
if (c.type === "tool_result" && c.tool_use_id && opts.isAborted(c.tool_use_id)) {
|
|
1531
|
+
types$1.logger.debug("[claudeRemote] Tool aborted, exiting claudeRemote");
|
|
1409
1532
|
return;
|
|
1410
1533
|
}
|
|
1411
1534
|
}
|
|
1412
1535
|
}
|
|
1413
1536
|
}
|
|
1414
1537
|
}
|
|
1415
|
-
types$1.logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
1416
1538
|
} catch (e) {
|
|
1417
1539
|
if (e instanceof AbortError) {
|
|
1418
1540
|
types$1.logger.debug(`[claudeRemote] Aborted`);
|
|
@@ -1422,71 +1544,11 @@ async function claudeRemote(opts) {
|
|
|
1422
1544
|
} finally {
|
|
1423
1545
|
updateThinking(false);
|
|
1424
1546
|
}
|
|
1425
|
-
types$1.logger.debug(`[claudeRemote] Function completed`);
|
|
1426
1547
|
}
|
|
1427
1548
|
|
|
1428
1549
|
const PLAN_FAKE_REJECT = `User approved plan, but you need to be restarted. STOP IMMEDIATELY TO SWITCH FROM PLAN MODE. DO NOT REPLY TO THIS MESSAGE.`;
|
|
1429
1550
|
const PLAN_FAKE_RESTART = `PlEaZe Continue with plan.`;
|
|
1430
1551
|
|
|
1431
|
-
async function startPermissionServerV2(handler) {
|
|
1432
|
-
const mcp = new mcp_js.McpServer({
|
|
1433
|
-
name: "Permission Server",
|
|
1434
|
-
version: "1.0.0",
|
|
1435
|
-
description: "A server that allows you to request permissions from the user"
|
|
1436
|
-
});
|
|
1437
|
-
mcp.registerTool("ask_permission", {
|
|
1438
|
-
description: "Request permission to execute a tool",
|
|
1439
|
-
title: "Request Permission",
|
|
1440
|
-
inputSchema: {
|
|
1441
|
-
tool_name: z.z.string().describe("The tool that needs permission"),
|
|
1442
|
-
input: z.z.any().describe("The arguments for the tool")
|
|
1443
|
-
}
|
|
1444
|
-
}, async (args) => {
|
|
1445
|
-
const response = await handler({ name: args.tool_name, arguments: args.input });
|
|
1446
|
-
types$1.logger.debugLargeJson("[permissionServerV2] Response", response);
|
|
1447
|
-
const result = response.approved ? { behavior: "allow", updatedInput: args.input || {} } : { behavior: "deny", message: response.reason || `The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.` };
|
|
1448
|
-
return {
|
|
1449
|
-
content: [
|
|
1450
|
-
{
|
|
1451
|
-
type: "text",
|
|
1452
|
-
text: JSON.stringify(result)
|
|
1453
|
-
}
|
|
1454
|
-
],
|
|
1455
|
-
isError: false
|
|
1456
|
-
};
|
|
1457
|
-
});
|
|
1458
|
-
const transport = new streamableHttp_js.StreamableHTTPServerTransport({
|
|
1459
|
-
// NOTE: Returning session id here will result in claude
|
|
1460
|
-
// sdk spawn to fail with `Invalid Request: Server already initialized`
|
|
1461
|
-
sessionIdGenerator: void 0
|
|
1462
|
-
});
|
|
1463
|
-
await mcp.connect(transport);
|
|
1464
|
-
const server = node_http.createServer(async (req, res) => {
|
|
1465
|
-
try {
|
|
1466
|
-
await transport.handleRequest(req, res);
|
|
1467
|
-
} catch (error) {
|
|
1468
|
-
types$1.logger.debug("Error handling request:", error);
|
|
1469
|
-
if (!res.headersSent) {
|
|
1470
|
-
res.writeHead(500).end();
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
});
|
|
1474
|
-
const baseUrl = await new Promise((resolve) => {
|
|
1475
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1476
|
-
const addr = server.address();
|
|
1477
|
-
resolve(new URL(`http://127.0.0.1:${addr.port}`));
|
|
1478
|
-
});
|
|
1479
|
-
});
|
|
1480
|
-
return {
|
|
1481
|
-
url: baseUrl.toString(),
|
|
1482
|
-
toolName: "ask_permission",
|
|
1483
|
-
stop: () => {
|
|
1484
|
-
mcp.close();
|
|
1485
|
-
server.close();
|
|
1486
|
-
}
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
1552
|
function deepEqual(a, b) {
|
|
1491
1553
|
if (a === b) return true;
|
|
1492
1554
|
if (a == null || b == null) return false;
|
|
@@ -1498,136 +1560,181 @@ function deepEqual(a, b) {
|
|
|
1498
1560
|
if (!keysB.includes(key)) return false;
|
|
1499
1561
|
if (!deepEqual(a[key], b[key])) return false;
|
|
1500
1562
|
}
|
|
1501
|
-
return true;
|
|
1563
|
+
return true;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
const STANDARD_TOOLS = {
|
|
1567
|
+
// File operations
|
|
1568
|
+
"Read": "Read File",
|
|
1569
|
+
"Write": "Write File",
|
|
1570
|
+
"Edit": "Edit File",
|
|
1571
|
+
"MultiEdit": "Edit File",
|
|
1572
|
+
"NotebookEdit": "Edit Notebook",
|
|
1573
|
+
// Search and navigation
|
|
1574
|
+
"Glob": "Find Files",
|
|
1575
|
+
"Grep": "Search in Files",
|
|
1576
|
+
"LS": "List Directory",
|
|
1577
|
+
// Command execution
|
|
1578
|
+
"Bash": "Run Command",
|
|
1579
|
+
"BashOutput": "Check Command Output",
|
|
1580
|
+
"KillBash": "Stop Command",
|
|
1581
|
+
// Task management
|
|
1582
|
+
"TodoWrite": "Update Tasks",
|
|
1583
|
+
"TodoRead": "Read Tasks",
|
|
1584
|
+
"Task": "Launch Agent",
|
|
1585
|
+
// Web tools
|
|
1586
|
+
"WebFetch": "Fetch Web Page",
|
|
1587
|
+
"WebSearch": "Search Web",
|
|
1588
|
+
// Special cases
|
|
1589
|
+
"exit_plan_mode": "Execute Plan",
|
|
1590
|
+
"ExitPlanMode": "Execute Plan"
|
|
1591
|
+
};
|
|
1592
|
+
function toTitleCase(str) {
|
|
1593
|
+
return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
1594
|
+
}
|
|
1595
|
+
function getToolName(toolName) {
|
|
1596
|
+
if (STANDARD_TOOLS[toolName]) {
|
|
1597
|
+
return STANDARD_TOOLS[toolName];
|
|
1598
|
+
}
|
|
1599
|
+
if (toolName.startsWith("mcp__")) {
|
|
1600
|
+
const parts = toolName.split("__");
|
|
1601
|
+
if (parts.length >= 3) {
|
|
1602
|
+
const server = toTitleCase(parts[1]);
|
|
1603
|
+
const action = toTitleCase(parts.slice(2).join("_"));
|
|
1604
|
+
return `${server}: ${action}`;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return toTitleCase(toolName);
|
|
1502
1608
|
}
|
|
1503
1609
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
});
|
|
1547
|
-
let timeout = setTimeout(async () => {
|
|
1548
|
-
types$1.logger.debug("Permission timeout - attempting to interrupt Claude");
|
|
1549
|
-
requests.delete(id);
|
|
1550
|
-
session.client.updateAgentState((currentState) => {
|
|
1551
|
-
const request2 = currentState.requests?.[id];
|
|
1552
|
-
if (!request2) return currentState;
|
|
1553
|
-
let r = { ...currentState.requests };
|
|
1554
|
-
delete r[id];
|
|
1555
|
-
return {
|
|
1556
|
-
...currentState,
|
|
1557
|
-
requests: r,
|
|
1558
|
-
completedRequests: {
|
|
1559
|
-
...currentState.completedRequests,
|
|
1560
|
-
[id]: {
|
|
1561
|
-
...request2,
|
|
1562
|
-
completedAt: Date.now(),
|
|
1563
|
-
status: "canceled",
|
|
1564
|
-
reason: "Timeout"
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
};
|
|
1568
|
-
});
|
|
1569
|
-
}, 1e3 * 60 * 4.5);
|
|
1570
|
-
types$1.logger.debug("Permission request" + id + " " + JSON.stringify(request));
|
|
1571
|
-
session.api.push().sendToAllDevices(
|
|
1572
|
-
"Permission Request",
|
|
1573
|
-
`Claude wants to use ${request.name}`,
|
|
1574
|
-
{
|
|
1575
|
-
sessionId: session.client.sessionId,
|
|
1576
|
-
requestId: id,
|
|
1577
|
-
tool: request.name,
|
|
1578
|
-
type: "permission_request"
|
|
1579
|
-
}
|
|
1580
|
-
);
|
|
1581
|
-
session.client.updateAgentState((currentState) => ({
|
|
1582
|
-
...currentState,
|
|
1583
|
-
requests: {
|
|
1584
|
-
...currentState.requests,
|
|
1585
|
-
[id]: {
|
|
1586
|
-
tool: request.name,
|
|
1587
|
-
arguments: request.arguments,
|
|
1588
|
-
createdAt: Date.now()
|
|
1610
|
+
function getToolDescriptor(toolName) {
|
|
1611
|
+
if (toolName === "exit_plan_mode" || toolName === "ExitPlanMode") {
|
|
1612
|
+
return { edit: false, exitPlan: true };
|
|
1613
|
+
}
|
|
1614
|
+
if (toolName === "Edit" || toolName === "MultiEdit" || toolName === "Write" || toolName === "NotebookEdit") {
|
|
1615
|
+
return { edit: true, exitPlan: false };
|
|
1616
|
+
}
|
|
1617
|
+
return { edit: false, exitPlan: false };
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
class PermissionHandler {
|
|
1621
|
+
toolCalls = [];
|
|
1622
|
+
responses = /* @__PURE__ */ new Map();
|
|
1623
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
1624
|
+
session;
|
|
1625
|
+
allowedTools = /* @__PURE__ */ new Set();
|
|
1626
|
+
permissionMode = "default";
|
|
1627
|
+
constructor(session) {
|
|
1628
|
+
this.session = session;
|
|
1629
|
+
this.setupClientHandler();
|
|
1630
|
+
}
|
|
1631
|
+
handleModeChange(mode) {
|
|
1632
|
+
this.permissionMode = mode;
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Handler response
|
|
1636
|
+
*/
|
|
1637
|
+
handlePermissionResponse(response, pending) {
|
|
1638
|
+
if (response.allowTools && response.allowTools.length > 0) {
|
|
1639
|
+
response.allowTools.forEach((tool) => this.allowedTools.add(tool));
|
|
1640
|
+
}
|
|
1641
|
+
if (response.mode) {
|
|
1642
|
+
this.permissionMode = response.mode;
|
|
1643
|
+
}
|
|
1644
|
+
if (pending.toolName === "exit_plan_mode" || pending.toolName === "ExitPlanMode") {
|
|
1645
|
+
types$1.logger.debug("Plan mode result received", response);
|
|
1646
|
+
if (response.approved) {
|
|
1647
|
+
types$1.logger.debug("Plan approved - injecting PLAN_FAKE_RESTART");
|
|
1648
|
+
if (response.mode && ["default", "acceptEdits", "bypassPermissions"].includes(response.mode)) {
|
|
1649
|
+
this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: response.mode });
|
|
1650
|
+
} else {
|
|
1651
|
+
this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: "default" });
|
|
1589
1652
|
}
|
|
1653
|
+
pending.resolve({ behavior: "deny", message: PLAN_FAKE_REJECT });
|
|
1654
|
+
} else {
|
|
1655
|
+
pending.resolve({ behavior: "deny", message: response.reason || "Plan rejected" });
|
|
1590
1656
|
}
|
|
1591
|
-
}));
|
|
1592
|
-
promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
|
|
1593
|
-
return promise;
|
|
1594
|
-
}
|
|
1595
|
-
session.client.setHandler("permission", async (message) => {
|
|
1596
|
-
types$1.logger.debug("Permission response" + JSON.stringify(message));
|
|
1597
|
-
const id = message.id;
|
|
1598
|
-
const resolve = requests.get(id);
|
|
1599
|
-
if (resolve) {
|
|
1600
|
-
responses.set(id, message);
|
|
1601
|
-
resolve({ approved: message.approved, reason: message.reason, mode: message.mode });
|
|
1602
|
-
requests.delete(id);
|
|
1603
1657
|
} else {
|
|
1604
|
-
|
|
1605
|
-
|
|
1658
|
+
const result = response.approved ? { behavior: "allow", updatedInput: pending.input || {} } : { behavior: "deny", message: response.reason || `The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.` };
|
|
1659
|
+
pending.resolve(result);
|
|
1606
1660
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
return {
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Creates the canCallTool callback for the SDK
|
|
1664
|
+
*/
|
|
1665
|
+
handleToolCall = async (toolName, input, mode, options) => {
|
|
1666
|
+
if (this.allowedTools.has(toolName)) {
|
|
1667
|
+
return { behavior: "allow", updatedInput: input };
|
|
1668
|
+
}
|
|
1669
|
+
const descriptor = getToolDescriptor(toolName);
|
|
1670
|
+
if (this.permissionMode === "bypassPermissions") {
|
|
1671
|
+
return { behavior: "allow", updatedInput: input };
|
|
1672
|
+
}
|
|
1673
|
+
if (this.permissionMode === "acceptEdits" && descriptor.edit) {
|
|
1674
|
+
return { behavior: "allow", updatedInput: input };
|
|
1675
|
+
}
|
|
1676
|
+
let toolCallId = this.resolveToolCallId(toolName, input);
|
|
1677
|
+
if (!toolCallId) {
|
|
1678
|
+
await types$1.delay(1e3);
|
|
1679
|
+
toolCallId = this.resolveToolCallId(toolName, input);
|
|
1680
|
+
if (!toolCallId) {
|
|
1681
|
+
throw new Error(`Could not resolve tool call ID for ${toolName}`);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return this.handlePermissionRequest(toolCallId, toolName, input, options.signal);
|
|
1685
|
+
};
|
|
1686
|
+
/**
|
|
1687
|
+
* Handles individual permission requests
|
|
1688
|
+
*/
|
|
1689
|
+
async handlePermissionRequest(id, toolName, input, signal) {
|
|
1690
|
+
return new Promise((resolve, reject) => {
|
|
1691
|
+
const abortHandler = () => {
|
|
1692
|
+
this.pendingRequests.delete(id);
|
|
1693
|
+
reject(new Error("Permission request aborted"));
|
|
1694
|
+
};
|
|
1695
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1696
|
+
this.pendingRequests.set(id, {
|
|
1697
|
+
resolve: (result) => {
|
|
1698
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1699
|
+
resolve(result);
|
|
1700
|
+
},
|
|
1701
|
+
reject: (error) => {
|
|
1702
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1703
|
+
reject(error);
|
|
1704
|
+
},
|
|
1705
|
+
toolName,
|
|
1706
|
+
input
|
|
1707
|
+
});
|
|
1708
|
+
this.session.api.push().sendToAllDevices(
|
|
1709
|
+
"Permission Request",
|
|
1710
|
+
`Claude wants to ${getToolName(toolName)}`,
|
|
1711
|
+
{
|
|
1712
|
+
sessionId: this.session.client.sessionId,
|
|
1713
|
+
requestId: id,
|
|
1714
|
+
tool: toolName,
|
|
1715
|
+
type: "permission_request"
|
|
1716
|
+
}
|
|
1717
|
+
);
|
|
1718
|
+
this.session.client.updateAgentState((currentState) => ({
|
|
1614
1719
|
...currentState,
|
|
1615
|
-
requests:
|
|
1616
|
-
|
|
1617
|
-
...currentState.completedRequests,
|
|
1720
|
+
requests: {
|
|
1721
|
+
...currentState.requests,
|
|
1618
1722
|
[id]: {
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
reason: isExitPlanModeSuccess ? "Plan approved" : message.reason
|
|
1723
|
+
tool: toolName,
|
|
1724
|
+
arguments: input,
|
|
1725
|
+
createdAt: Date.now()
|
|
1623
1726
|
}
|
|
1624
1727
|
}
|
|
1625
|
-
};
|
|
1728
|
+
}));
|
|
1729
|
+
types$1.logger.debug(`Permission request sent for tool call ${id}: ${toolName}`);
|
|
1626
1730
|
});
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Resolves tool call ID based on tool name and input
|
|
1734
|
+
*/
|
|
1735
|
+
resolveToolCallId(name, args) {
|
|
1736
|
+
for (let i = this.toolCalls.length - 1; i >= 0; i--) {
|
|
1737
|
+
const call = this.toolCalls[i];
|
|
1631
1738
|
if (call.name === name && deepEqual(call.input, args)) {
|
|
1632
1739
|
if (call.used) {
|
|
1633
1740
|
return null;
|
|
@@ -1637,59 +1744,22 @@ async function startPermissionResolver(session) {
|
|
|
1637
1744
|
}
|
|
1638
1745
|
}
|
|
1639
1746
|
return null;
|
|
1640
|
-
};
|
|
1641
|
-
function reset() {
|
|
1642
|
-
toolCalls = [];
|
|
1643
|
-
requests.clear();
|
|
1644
|
-
responses.clear();
|
|
1645
|
-
for (const pending of pendingPermissionRequests) {
|
|
1646
|
-
clearTimeout(pending.timeout);
|
|
1647
|
-
}
|
|
1648
|
-
pendingPermissionRequests = [];
|
|
1649
|
-
session.client.updateAgentState((currentState) => {
|
|
1650
|
-
const pendingRequests = currentState.requests || {};
|
|
1651
|
-
const completedRequests = { ...currentState.completedRequests };
|
|
1652
|
-
for (const [id, request] of Object.entries(pendingRequests)) {
|
|
1653
|
-
completedRequests[id] = {
|
|
1654
|
-
...request,
|
|
1655
|
-
completedAt: Date.now(),
|
|
1656
|
-
status: "canceled",
|
|
1657
|
-
reason: "Session switched to local mode"
|
|
1658
|
-
};
|
|
1659
|
-
}
|
|
1660
|
-
return {
|
|
1661
|
-
...currentState,
|
|
1662
|
-
requests: {},
|
|
1663
|
-
// Clear all pending requests
|
|
1664
|
-
completedRequests
|
|
1665
|
-
};
|
|
1666
|
-
});
|
|
1667
1747
|
}
|
|
1668
|
-
|
|
1748
|
+
/**
|
|
1749
|
+
* Handles messages to track tool calls
|
|
1750
|
+
*/
|
|
1751
|
+
onMessage(message) {
|
|
1669
1752
|
if (message.type === "assistant") {
|
|
1670
1753
|
const assistantMsg = message;
|
|
1671
1754
|
if (assistantMsg.message && assistantMsg.message.content) {
|
|
1672
1755
|
for (const block of assistantMsg.message.content) {
|
|
1673
1756
|
if (block.type === "tool_use") {
|
|
1674
|
-
toolCalls.push({
|
|
1757
|
+
this.toolCalls.push({
|
|
1675
1758
|
id: block.id,
|
|
1676
1759
|
name: block.name,
|
|
1677
1760
|
input: block.input,
|
|
1678
1761
|
used: false
|
|
1679
1762
|
});
|
|
1680
|
-
for (let i = pendingPermissionRequests.length - 1; i >= 0; i--) {
|
|
1681
|
-
const pending = pendingPermissionRequests[i];
|
|
1682
|
-
if (pending.request.name === block.name && deepEqual(pending.request.arguments, block.input)) {
|
|
1683
|
-
types$1.logger.debug(`Resolving pending permission request for ${block.name} with ID ${block.id}`);
|
|
1684
|
-
clearTimeout(pending.timeout);
|
|
1685
|
-
pendingPermissionRequests.splice(i, 1);
|
|
1686
|
-
handlePermissionRequest(block.id, pending.request).then(
|
|
1687
|
-
pending.resolve,
|
|
1688
|
-
pending.reject
|
|
1689
|
-
);
|
|
1690
|
-
break;
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
1763
|
}
|
|
1694
1764
|
}
|
|
1695
1765
|
}
|
|
@@ -1699,7 +1769,7 @@ async function startPermissionResolver(session) {
|
|
|
1699
1769
|
if (userMsg.message && userMsg.message.content && Array.isArray(userMsg.message.content)) {
|
|
1700
1770
|
for (const block of userMsg.message.content) {
|
|
1701
1771
|
if (block.type === "tool_result" && block.tool_use_id) {
|
|
1702
|
-
const toolCall = toolCalls.find((tc) => tc.id === block.tool_use_id);
|
|
1772
|
+
const toolCall = this.toolCalls.find((tc) => tc.id === block.tool_use_id);
|
|
1703
1773
|
if (toolCall && !toolCall.used) {
|
|
1704
1774
|
toolCall.used = true;
|
|
1705
1775
|
}
|
|
@@ -1708,12 +1778,92 @@ async function startPermissionResolver(session) {
|
|
|
1708
1778
|
}
|
|
1709
1779
|
}
|
|
1710
1780
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
responses
|
|
1716
|
-
|
|
1781
|
+
/**
|
|
1782
|
+
* Checks if a tool call is rejected
|
|
1783
|
+
*/
|
|
1784
|
+
isAborted(toolCallId) {
|
|
1785
|
+
if (this.responses.get(toolCallId)?.approved === false) {
|
|
1786
|
+
return true;
|
|
1787
|
+
}
|
|
1788
|
+
const toolCall = this.toolCalls.find((tc) => tc.id === toolCallId);
|
|
1789
|
+
if (toolCall && (toolCall.name === "exit_plan_mode" || toolCall.name === "ExitPlanMode")) {
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
return false;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Resets all state for new sessions
|
|
1796
|
+
*/
|
|
1797
|
+
reset() {
|
|
1798
|
+
this.toolCalls = [];
|
|
1799
|
+
this.responses.clear();
|
|
1800
|
+
for (const [, pending] of this.pendingRequests.entries()) {
|
|
1801
|
+
pending.reject(new Error("Session reset"));
|
|
1802
|
+
}
|
|
1803
|
+
this.pendingRequests.clear();
|
|
1804
|
+
this.session.client.updateAgentState((currentState) => {
|
|
1805
|
+
const pendingRequests = currentState.requests || {};
|
|
1806
|
+
const completedRequests = { ...currentState.completedRequests };
|
|
1807
|
+
for (const [id, request] of Object.entries(pendingRequests)) {
|
|
1808
|
+
completedRequests[id] = {
|
|
1809
|
+
...request,
|
|
1810
|
+
completedAt: Date.now(),
|
|
1811
|
+
status: "canceled",
|
|
1812
|
+
reason: "Session switched to local mode"
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
return {
|
|
1816
|
+
...currentState,
|
|
1817
|
+
requests: {},
|
|
1818
|
+
// Clear all pending requests
|
|
1819
|
+
completedRequests
|
|
1820
|
+
};
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Sets up the client handler for permission responses
|
|
1825
|
+
*/
|
|
1826
|
+
setupClientHandler() {
|
|
1827
|
+
this.session.client.setHandler("permission", async (message) => {
|
|
1828
|
+
types$1.logger.debug(`Permission response: ${JSON.stringify(message)}`);
|
|
1829
|
+
const id = message.id;
|
|
1830
|
+
const pending = this.pendingRequests.get(id);
|
|
1831
|
+
if (!pending) {
|
|
1832
|
+
types$1.logger.debug("Permission request not found or already resolved");
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
this.responses.set(id, { ...message, receivedAt: Date.now() });
|
|
1836
|
+
this.pendingRequests.delete(id);
|
|
1837
|
+
this.handlePermissionResponse(message, pending);
|
|
1838
|
+
this.session.client.updateAgentState((currentState) => {
|
|
1839
|
+
const request = currentState.requests?.[id];
|
|
1840
|
+
if (!request) return currentState;
|
|
1841
|
+
let r = { ...currentState.requests };
|
|
1842
|
+
delete r[id];
|
|
1843
|
+
return {
|
|
1844
|
+
...currentState,
|
|
1845
|
+
requests: r,
|
|
1846
|
+
completedRequests: {
|
|
1847
|
+
...currentState.completedRequests,
|
|
1848
|
+
[id]: {
|
|
1849
|
+
...request,
|
|
1850
|
+
completedAt: Date.now(),
|
|
1851
|
+
status: message.approved ? "approved" : "denied",
|
|
1852
|
+
reason: message.reason,
|
|
1853
|
+
mode: message.mode,
|
|
1854
|
+
allowTools: message.allowTools
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
});
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Gets the responses map (for compatibility with existing code)
|
|
1863
|
+
*/
|
|
1864
|
+
getResponses() {
|
|
1865
|
+
return this.responses;
|
|
1866
|
+
}
|
|
1717
1867
|
}
|
|
1718
1868
|
|
|
1719
1869
|
function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
|
|
@@ -2087,15 +2237,6 @@ async function claudeRemoteLauncher(session) {
|
|
|
2087
2237
|
}
|
|
2088
2238
|
process.stdin.setEncoding("utf8");
|
|
2089
2239
|
}
|
|
2090
|
-
const scanner = await createSessionScanner({
|
|
2091
|
-
sessionId: session.sessionId,
|
|
2092
|
-
workingDirectory: session.path,
|
|
2093
|
-
onMessage: (message) => {
|
|
2094
|
-
if (message.type === "summary") {
|
|
2095
|
-
session.client.sendClaudeSessionMessage(message);
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
2098
|
-
});
|
|
2099
2240
|
let exitReason = null;
|
|
2100
2241
|
let abortController = null;
|
|
2101
2242
|
let abortFuture = null;
|
|
@@ -2118,17 +2259,17 @@ async function claudeRemoteLauncher(session) {
|
|
|
2118
2259
|
}
|
|
2119
2260
|
session.client.setHandler("abort", doAbort);
|
|
2120
2261
|
session.client.setHandler("switch", doSwitch);
|
|
2121
|
-
const
|
|
2262
|
+
const permissionHandler = new PermissionHandler(session);
|
|
2122
2263
|
const sdkToLogConverter = new SDKToLogConverter({
|
|
2123
2264
|
sessionId: session.sessionId || "unknown",
|
|
2124
2265
|
cwd: session.path,
|
|
2125
2266
|
version: process.env.npm_package_version
|
|
2126
|
-
},
|
|
2267
|
+
}, permissionHandler.getResponses());
|
|
2127
2268
|
let planModeToolCalls = /* @__PURE__ */ new Set();
|
|
2128
2269
|
let ongoingToolCalls = /* @__PURE__ */ new Map();
|
|
2129
2270
|
function onMessage(message) {
|
|
2130
2271
|
formatClaudeMessageForInk(message, messageBuffer);
|
|
2131
|
-
|
|
2272
|
+
permissionHandler.onMessage(message);
|
|
2132
2273
|
if (message.type === "assistant") {
|
|
2133
2274
|
let umessage = message;
|
|
2134
2275
|
if (umessage.message.content && Array.isArray(umessage.message.content)) {
|
|
@@ -2192,6 +2333,32 @@ async function claudeRemoteLauncher(session) {
|
|
|
2192
2333
|
}
|
|
2193
2334
|
const logMessage = sdkToLogConverter.convert(msg);
|
|
2194
2335
|
if (logMessage) {
|
|
2336
|
+
if (logMessage.type === "user" && logMessage.message?.content) {
|
|
2337
|
+
const content = Array.isArray(logMessage.message.content) ? logMessage.message.content : [];
|
|
2338
|
+
for (let i = 0; i < content.length; i++) {
|
|
2339
|
+
const c = content[i];
|
|
2340
|
+
if (c.type === "tool_result" && c.tool_use_id) {
|
|
2341
|
+
const responses = permissionHandler.getResponses();
|
|
2342
|
+
const response = responses.get(c.tool_use_id);
|
|
2343
|
+
if (response) {
|
|
2344
|
+
const permissions = {
|
|
2345
|
+
date: response.receivedAt || Date.now(),
|
|
2346
|
+
result: response.approved ? "approved" : "denied"
|
|
2347
|
+
};
|
|
2348
|
+
if (response.mode) {
|
|
2349
|
+
permissions.mode = response.mode;
|
|
2350
|
+
}
|
|
2351
|
+
if (response.allowTools && response.allowTools.length > 0) {
|
|
2352
|
+
permissions.allowedTools = response.allowTools;
|
|
2353
|
+
}
|
|
2354
|
+
content[i] = {
|
|
2355
|
+
...c,
|
|
2356
|
+
permissions
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2195
2362
|
if (logMessage.type !== "system") {
|
|
2196
2363
|
session.client.sendClaudeSessionMessage(logMessage);
|
|
2197
2364
|
}
|
|
@@ -2211,58 +2378,57 @@ async function claudeRemoteLauncher(session) {
|
|
|
2211
2378
|
}
|
|
2212
2379
|
}
|
|
2213
2380
|
try {
|
|
2381
|
+
let pending = null;
|
|
2214
2382
|
while (!exitReason) {
|
|
2215
|
-
types$1.logger.debug("[remote]: fetch next message");
|
|
2216
|
-
abortController = new AbortController();
|
|
2217
|
-
abortFuture = new Future();
|
|
2218
|
-
const messageData = await session.queue.waitForMessagesAndGetAsString(abortController.signal);
|
|
2219
|
-
if (!messageData || abortController.signal.aborted) {
|
|
2220
|
-
types$1.logger.debug("[remote]: fetch next message done: no message or aborted");
|
|
2221
|
-
abortFuture?.resolve(void 0);
|
|
2222
|
-
if (exitReason) {
|
|
2223
|
-
return exitReason;
|
|
2224
|
-
} else {
|
|
2225
|
-
continue;
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
types$1.logger.debug("[remote]: fetch next message done: message received");
|
|
2229
|
-
abortFuture?.resolve(void 0);
|
|
2230
|
-
abortFuture = null;
|
|
2231
|
-
abortController = null;
|
|
2232
2383
|
types$1.logger.debug("[remote]: launch");
|
|
2233
2384
|
messageBuffer.addMessage("\u2550".repeat(40), "status");
|
|
2234
2385
|
messageBuffer.addMessage("Starting new Claude session...", "status");
|
|
2235
|
-
|
|
2386
|
+
const controller = new AbortController();
|
|
2387
|
+
abortController = controller;
|
|
2236
2388
|
abortFuture = new Future();
|
|
2237
|
-
|
|
2389
|
+
permissionHandler.reset();
|
|
2238
2390
|
sdkToLogConverter.resetParentChain();
|
|
2391
|
+
let modeHash = null;
|
|
2392
|
+
let mode = null;
|
|
2239
2393
|
try {
|
|
2240
2394
|
await claudeRemote({
|
|
2241
2395
|
sessionId: session.sessionId,
|
|
2242
2396
|
path: session.path,
|
|
2243
|
-
|
|
2244
|
-
mcpServers:
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2397
|
+
allowedTools: session.allowedTools ?? [],
|
|
2398
|
+
mcpServers: session.mcpServers,
|
|
2399
|
+
canCallTool: permissionHandler.handleToolCall,
|
|
2400
|
+
isAborted: (toolCallId) => {
|
|
2401
|
+
return permissionHandler.isAborted(toolCallId);
|
|
2402
|
+
},
|
|
2403
|
+
nextMessage: async () => {
|
|
2404
|
+
if (pending) {
|
|
2405
|
+
let p = pending;
|
|
2406
|
+
pending = null;
|
|
2407
|
+
permissionHandler.handleModeChange(p.mode.permissionMode);
|
|
2408
|
+
return p;
|
|
2409
|
+
}
|
|
2410
|
+
let msg = await session.queue.waitForMessagesAndGetAsString(controller.signal);
|
|
2411
|
+
if (msg) {
|
|
2412
|
+
if (modeHash && msg.hash !== modeHash || msg.isolate) {
|
|
2413
|
+
types$1.logger.debug("[remote]: mode has changed, pending message");
|
|
2414
|
+
pending = msg;
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
modeHash = msg.hash;
|
|
2418
|
+
mode = msg.mode;
|
|
2419
|
+
permissionHandler.handleModeChange(mode.permissionMode);
|
|
2420
|
+
return {
|
|
2421
|
+
message: msg.message,
|
|
2422
|
+
mode: msg.mode
|
|
2423
|
+
};
|
|
2249
2424
|
}
|
|
2425
|
+
return null;
|
|
2250
2426
|
},
|
|
2251
|
-
permissionPromptToolName: "mcp__permission__" + permissions.server.toolName,
|
|
2252
|
-
permissionMode: messageData.mode.permissionMode,
|
|
2253
|
-
model: messageData.mode.model,
|
|
2254
|
-
fallbackModel: messageData.mode.fallbackModel,
|
|
2255
|
-
customSystemPrompt: messageData.mode.customSystemPrompt,
|
|
2256
|
-
appendSystemPrompt: messageData.mode.appendSystemPrompt,
|
|
2257
|
-
allowedTools: messageData.mode.allowedTools,
|
|
2258
|
-
disallowedTools: messageData.mode.disallowedTools,
|
|
2259
2427
|
onSessionFound: (sessionId) => {
|
|
2260
2428
|
sdkToLogConverter.updateSessionId(sessionId);
|
|
2261
2429
|
session.onSessionFound(sessionId);
|
|
2262
|
-
scanner.onNewSession(sessionId);
|
|
2263
2430
|
},
|
|
2264
2431
|
onThinkingChange: session.onThinkingChange,
|
|
2265
|
-
message: messageData.message,
|
|
2266
2432
|
claudeEnvVars: session.claudeEnvVars,
|
|
2267
2433
|
claudeArgs: session.claudeArgs,
|
|
2268
2434
|
onMessage,
|
|
@@ -2280,11 +2446,13 @@ async function claudeRemoteLauncher(session) {
|
|
|
2280
2446
|
session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
|
|
2281
2447
|
}
|
|
2282
2448
|
} catch (e) {
|
|
2449
|
+
types$1.logger.debug("[remote]: launch error", e);
|
|
2283
2450
|
if (!exitReason) {
|
|
2284
2451
|
session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
|
|
2285
2452
|
continue;
|
|
2286
2453
|
}
|
|
2287
2454
|
} finally {
|
|
2455
|
+
types$1.logger.debug("[remote]: launch finally");
|
|
2288
2456
|
for (let [toolCallId, { parentToolCallId }] of ongoingToolCalls) {
|
|
2289
2457
|
const converted = sdkToLogConverter.generateInterruptedToolResult(toolCallId, parentToolCallId);
|
|
2290
2458
|
if (converted) {
|
|
@@ -2297,11 +2465,13 @@ async function claudeRemoteLauncher(session) {
|
|
|
2297
2465
|
abortFuture?.resolve(void 0);
|
|
2298
2466
|
abortFuture = null;
|
|
2299
2467
|
types$1.logger.debug("[remote]: launch done");
|
|
2300
|
-
|
|
2468
|
+
permissionHandler.reset();
|
|
2469
|
+
modeHash = null;
|
|
2470
|
+
mode = null;
|
|
2301
2471
|
}
|
|
2302
2472
|
}
|
|
2303
2473
|
} finally {
|
|
2304
|
-
|
|
2474
|
+
permissionHandler.reset();
|
|
2305
2475
|
process.stdin.off("data", abort);
|
|
2306
2476
|
if (process.stdin.isTTY) {
|
|
2307
2477
|
process.stdin.setRawMode(false);
|
|
@@ -2313,7 +2483,6 @@ async function claudeRemoteLauncher(session) {
|
|
|
2313
2483
|
if (abortFuture) {
|
|
2314
2484
|
abortFuture.resolve(void 0);
|
|
2315
2485
|
}
|
|
2316
|
-
await scanner.cleanup();
|
|
2317
2486
|
}
|
|
2318
2487
|
return exitReason || "exit";
|
|
2319
2488
|
}
|
|
@@ -2330,6 +2499,7 @@ async function loop(opts) {
|
|
|
2330
2499
|
mcpServers: opts.mcpServers,
|
|
2331
2500
|
logPath,
|
|
2332
2501
|
messageQueue: opts.messageQueue,
|
|
2502
|
+
allowedTools: opts.allowedTools,
|
|
2333
2503
|
onModeChange: opts.onModeChange
|
|
2334
2504
|
});
|
|
2335
2505
|
if (opts.onSessionReady) {
|
|
@@ -2364,7 +2534,7 @@ async function loop(opts) {
|
|
|
2364
2534
|
}
|
|
2365
2535
|
|
|
2366
2536
|
var name = "happy-coder";
|
|
2367
|
-
var version = "0.
|
|
2537
|
+
var version = "0.9.0-0";
|
|
2368
2538
|
var description = "Claude Code session sharing CLI";
|
|
2369
2539
|
var author = "Kirill Dubovitskiy";
|
|
2370
2540
|
var license = "MIT";
|
|
@@ -2414,18 +2584,14 @@ var scripts = {
|
|
|
2414
2584
|
test: "yarn build && vitest run",
|
|
2415
2585
|
"test:watch": "vitest",
|
|
2416
2586
|
"test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
|
|
2417
|
-
dev: "yarn build && npx tsx src/index.ts",
|
|
2587
|
+
dev: "yarn build && DEBUG=1 npx tsx src/index.ts",
|
|
2418
2588
|
"dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
|
|
2419
2589
|
"dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
|
|
2420
2590
|
prepublishOnly: "yarn build && yarn test",
|
|
2421
|
-
|
|
2422
|
-
"patch:publish": "yarn build && npm version patch && npm publish",
|
|
2423
|
-
"version:prerelease": "yarn build && npm version prerelease --preid=beta",
|
|
2424
|
-
"publish:prerelease": "npm publish --tag beta",
|
|
2425
|
-
"beta:publish": "yarn version:prerelease && yarn publish:prerelease"
|
|
2591
|
+
release: "release-it"
|
|
2426
2592
|
};
|
|
2427
2593
|
var dependencies = {
|
|
2428
|
-
"@anthropic-ai/claude-code": "^1.0.
|
|
2594
|
+
"@anthropic-ai/claude-code": "^1.0.89",
|
|
2429
2595
|
"@anthropic-ai/sdk": "^0.56.0",
|
|
2430
2596
|
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
2431
2597
|
"@stablelib/base64": "^2.0.1",
|
|
@@ -2454,6 +2620,7 @@ var devDependencies = {
|
|
|
2454
2620
|
eslint: "^9",
|
|
2455
2621
|
"eslint-config-prettier": "^10",
|
|
2456
2622
|
pkgroll: "^2.14.2",
|
|
2623
|
+
"release-it": "^19.0.4",
|
|
2457
2624
|
shx: "^0.3.3",
|
|
2458
2625
|
"ts-node": "^10",
|
|
2459
2626
|
tsx: "^4.20.3",
|
|
@@ -2461,7 +2628,12 @@ var devDependencies = {
|
|
|
2461
2628
|
vitest: "^3.2.4"
|
|
2462
2629
|
};
|
|
2463
2630
|
var resolutions = {
|
|
2464
|
-
"whatwg-url": "14.2.0"
|
|
2631
|
+
"whatwg-url": "14.2.0",
|
|
2632
|
+
"parse-path": "7.0.3",
|
|
2633
|
+
"@types/parse-path": "7.0.3"
|
|
2634
|
+
};
|
|
2635
|
+
var publishConfig = {
|
|
2636
|
+
registry: "https://registry.npmjs.org"
|
|
2465
2637
|
};
|
|
2466
2638
|
var packageManager = "yarn@1.22.22";
|
|
2467
2639
|
var packageJson = {
|
|
@@ -2484,6 +2656,7 @@ var packageJson = {
|
|
|
2484
2656
|
dependencies: dependencies,
|
|
2485
2657
|
devDependencies: devDependencies,
|
|
2486
2658
|
resolutions: resolutions,
|
|
2659
|
+
publishConfig: publishConfig,
|
|
2487
2660
|
packageManager: packageManager
|
|
2488
2661
|
};
|
|
2489
2662
|
|
|
@@ -2870,15 +3043,17 @@ async function clearDaemonState() {
|
|
|
2870
3043
|
}
|
|
2871
3044
|
|
|
2872
3045
|
class MessageQueue2 {
|
|
2873
|
-
constructor(modeHasher) {
|
|
2874
|
-
this.modeHasher = modeHasher;
|
|
2875
|
-
types$1.logger.debug(`[MessageQueue2] Initialized`);
|
|
2876
|
-
}
|
|
2877
3046
|
queue = [];
|
|
2878
3047
|
// Made public for testing
|
|
2879
3048
|
waiter = null;
|
|
2880
3049
|
closed = false;
|
|
2881
3050
|
onMessageHandler = null;
|
|
3051
|
+
modeHasher;
|
|
3052
|
+
constructor(modeHasher, onMessageHandler = null) {
|
|
3053
|
+
this.modeHasher = modeHasher;
|
|
3054
|
+
this.onMessageHandler = onMessageHandler;
|
|
3055
|
+
types$1.logger.debug(`[MessageQueue2] Initialized`);
|
|
3056
|
+
}
|
|
2882
3057
|
/**
|
|
2883
3058
|
* Set a handler that will be called when a message arrives
|
|
2884
3059
|
*/
|
|
@@ -3053,6 +3228,7 @@ class MessageQueue2 {
|
|
|
3053
3228
|
const firstItem = this.queue[0];
|
|
3054
3229
|
const sameModeMessages = [];
|
|
3055
3230
|
let mode = firstItem.mode;
|
|
3231
|
+
let isolate = firstItem.isolate ?? false;
|
|
3056
3232
|
const targetModeHash = firstItem.modeHash;
|
|
3057
3233
|
if (firstItem.isolate) {
|
|
3058
3234
|
const item = this.queue.shift();
|
|
@@ -3068,7 +3244,9 @@ class MessageQueue2 {
|
|
|
3068
3244
|
const combinedMessage = sameModeMessages.join("\n");
|
|
3069
3245
|
return {
|
|
3070
3246
|
message: combinedMessage,
|
|
3071
|
-
mode
|
|
3247
|
+
mode,
|
|
3248
|
+
hash: targetModeHash,
|
|
3249
|
+
isolate
|
|
3072
3250
|
};
|
|
3073
3251
|
}
|
|
3074
3252
|
/**
|
|
@@ -3963,10 +4141,10 @@ async function doWebAuth(keypair) {
|
|
|
3963
4141
|
console.log("\u2713 Browser opened\n");
|
|
3964
4142
|
console.log("Complete authentication in your browser window.");
|
|
3965
4143
|
} else {
|
|
3966
|
-
console.log("Could not open browser automatically
|
|
3967
|
-
console.log("Please open this URL manually:");
|
|
3968
|
-
console.log(webUrl);
|
|
4144
|
+
console.log("Could not open browser automatically.");
|
|
3969
4145
|
}
|
|
4146
|
+
console.log("\nIf the browser did not open, please copy and paste this URL:");
|
|
4147
|
+
console.log(webUrl);
|
|
3970
4148
|
console.log("");
|
|
3971
4149
|
return await waitForAuthentication(keypair);
|
|
3972
4150
|
}
|
|
@@ -4323,6 +4501,88 @@ async function startDaemon() {
|
|
|
4323
4501
|
}
|
|
4324
4502
|
}
|
|
4325
4503
|
|
|
4504
|
+
async function startHappyServer(client) {
|
|
4505
|
+
const handler = async (title) => {
|
|
4506
|
+
types$1.logger.debug("[happyMCP] Changing title to:", title);
|
|
4507
|
+
try {
|
|
4508
|
+
client.sendClaudeSessionMessage({
|
|
4509
|
+
type: "summary",
|
|
4510
|
+
summary: title,
|
|
4511
|
+
leafUuid: node_crypto.randomUUID()
|
|
4512
|
+
});
|
|
4513
|
+
return { success: true };
|
|
4514
|
+
} catch (error) {
|
|
4515
|
+
return { success: false, error: String(error) };
|
|
4516
|
+
}
|
|
4517
|
+
};
|
|
4518
|
+
const mcp = new mcp_js.McpServer({
|
|
4519
|
+
name: "Happy MCP",
|
|
4520
|
+
version: "1.0.0",
|
|
4521
|
+
description: "Happy CLI MCP server with chat session management tools"
|
|
4522
|
+
});
|
|
4523
|
+
mcp.registerTool("change_title", {
|
|
4524
|
+
description: "Change the title of the current chat session",
|
|
4525
|
+
title: "Change Chat Title",
|
|
4526
|
+
inputSchema: {
|
|
4527
|
+
title: z.z.string().describe("The new title for the chat session")
|
|
4528
|
+
}
|
|
4529
|
+
}, async (args) => {
|
|
4530
|
+
const response = await handler(args.title);
|
|
4531
|
+
types$1.logger.debug("[happyMCP] Response:", response);
|
|
4532
|
+
if (response.success) {
|
|
4533
|
+
return {
|
|
4534
|
+
content: [
|
|
4535
|
+
{
|
|
4536
|
+
type: "text",
|
|
4537
|
+
text: `Successfully changed chat title to: "${args.title}"`
|
|
4538
|
+
}
|
|
4539
|
+
],
|
|
4540
|
+
isError: false
|
|
4541
|
+
};
|
|
4542
|
+
} else {
|
|
4543
|
+
return {
|
|
4544
|
+
content: [
|
|
4545
|
+
{
|
|
4546
|
+
type: "text",
|
|
4547
|
+
text: `Failed to change chat title: ${response.error || "Unknown error"}`
|
|
4548
|
+
}
|
|
4549
|
+
],
|
|
4550
|
+
isError: true
|
|
4551
|
+
};
|
|
4552
|
+
}
|
|
4553
|
+
});
|
|
4554
|
+
const transport = new streamableHttp_js.StreamableHTTPServerTransport({
|
|
4555
|
+
// NOTE: Returning session id here will result in claude
|
|
4556
|
+
// sdk spawn to fail with `Invalid Request: Server already initialized`
|
|
4557
|
+
sessionIdGenerator: void 0
|
|
4558
|
+
});
|
|
4559
|
+
await mcp.connect(transport);
|
|
4560
|
+
const server = node_http.createServer(async (req, res) => {
|
|
4561
|
+
try {
|
|
4562
|
+
await transport.handleRequest(req, res);
|
|
4563
|
+
} catch (error) {
|
|
4564
|
+
types$1.logger.debug("Error handling request:", error);
|
|
4565
|
+
if (!res.headersSent) {
|
|
4566
|
+
res.writeHead(500).end();
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
});
|
|
4570
|
+
const baseUrl = await new Promise((resolve) => {
|
|
4571
|
+
server.listen(0, "127.0.0.1", () => {
|
|
4572
|
+
const addr = server.address();
|
|
4573
|
+
resolve(new URL(`http://127.0.0.1:${addr.port}`));
|
|
4574
|
+
});
|
|
4575
|
+
});
|
|
4576
|
+
return {
|
|
4577
|
+
url: baseUrl.toString(),
|
|
4578
|
+
toolNames: ["change_title"],
|
|
4579
|
+
stop: () => {
|
|
4580
|
+
mcp.close();
|
|
4581
|
+
server.close();
|
|
4582
|
+
}
|
|
4583
|
+
};
|
|
4584
|
+
}
|
|
4585
|
+
|
|
4326
4586
|
async function start(credentials, options = {}) {
|
|
4327
4587
|
const workingDirectory = process.cwd();
|
|
4328
4588
|
const sessionTag = node_crypto.randomUUID();
|
|
@@ -4382,6 +4642,8 @@ async function start(credentials, options = {}) {
|
|
|
4382
4642
|
}
|
|
4383
4643
|
});
|
|
4384
4644
|
const session = api.sessionSyncClient(response);
|
|
4645
|
+
const happyServer = await startHappyServer(session);
|
|
4646
|
+
types$1.logger.debug(`[START] Happy MCP server started at ${happyServer.url}`);
|
|
4385
4647
|
const logPath = await types$1.logger.logFilePathPromise;
|
|
4386
4648
|
types$1.logger.infoDeveloper(`Session: ${response.id}`);
|
|
4387
4649
|
types$1.logger.infoDeveloper(`Logs: ${logPath}`);
|
|
@@ -4393,7 +4655,15 @@ async function start(credentials, options = {}) {
|
|
|
4393
4655
|
if (caffeinateStarted) {
|
|
4394
4656
|
types$1.logger.infoDeveloper("Sleep prevention enabled (macOS)");
|
|
4395
4657
|
}
|
|
4396
|
-
const messageQueue = new MessageQueue2((mode) => hashObject(
|
|
4658
|
+
const messageQueue = new MessageQueue2((mode) => hashObject({
|
|
4659
|
+
isPlan: mode.permissionMode === "plan",
|
|
4660
|
+
model: mode.model,
|
|
4661
|
+
fallbackModel: mode.fallbackModel,
|
|
4662
|
+
customSystemPrompt: mode.customSystemPrompt,
|
|
4663
|
+
appendSystemPrompt: mode.appendSystemPrompt,
|
|
4664
|
+
allowedTools: mode.allowedTools,
|
|
4665
|
+
disallowedTools: mode.disallowedTools
|
|
4666
|
+
}));
|
|
4397
4667
|
registerHandlers(session);
|
|
4398
4668
|
let currentPermissionMode = options.permissionMode;
|
|
4399
4669
|
let currentModel = options.model;
|
|
@@ -4516,6 +4786,7 @@ async function start(credentials, options = {}) {
|
|
|
4516
4786
|
await session.close();
|
|
4517
4787
|
}
|
|
4518
4788
|
stopCaffeinate();
|
|
4789
|
+
happyServer.stop();
|
|
4519
4790
|
types$1.logger.debug("[START] Cleanup complete, exiting");
|
|
4520
4791
|
process.exit(0);
|
|
4521
4792
|
} catch (error) {
|
|
@@ -4540,6 +4811,7 @@ async function start(credentials, options = {}) {
|
|
|
4540
4811
|
startingMode: options.startingMode,
|
|
4541
4812
|
messageQueue,
|
|
4542
4813
|
api,
|
|
4814
|
+
allowedTools: happyServer.toolNames.map((toolName) => `mcp__happy__${toolName}`),
|
|
4543
4815
|
onModeChange: (newMode) => {
|
|
4544
4816
|
session.sendSessionEvent({ type: "switch", mode: newMode });
|
|
4545
4817
|
session.updateAgentState((currentState) => ({
|
|
@@ -4549,7 +4821,12 @@ async function start(credentials, options = {}) {
|
|
|
4549
4821
|
},
|
|
4550
4822
|
onSessionReady: (sessionInstance) => {
|
|
4551
4823
|
},
|
|
4552
|
-
mcpServers: {
|
|
4824
|
+
mcpServers: {
|
|
4825
|
+
"happy": {
|
|
4826
|
+
type: "http",
|
|
4827
|
+
url: happyServer.url
|
|
4828
|
+
}
|
|
4829
|
+
},
|
|
4553
4830
|
session,
|
|
4554
4831
|
claudeEnvVars: options.claudeEnvVars,
|
|
4555
4832
|
claudeArgs: options.claudeArgs
|
|
@@ -4561,28 +4838,11 @@ async function start(credentials, options = {}) {
|
|
|
4561
4838
|
await session.close();
|
|
4562
4839
|
stopCaffeinate();
|
|
4563
4840
|
types$1.logger.debug("Stopped sleep prevention");
|
|
4841
|
+
happyServer.stop();
|
|
4842
|
+
types$1.logger.debug("Stopped Happy MCP server");
|
|
4564
4843
|
process.exit(0);
|
|
4565
4844
|
}
|
|
4566
4845
|
|
|
4567
|
-
function trimIdent(text) {
|
|
4568
|
-
const lines = text.split("\n");
|
|
4569
|
-
while (lines.length > 0 && lines[0].trim() === "") {
|
|
4570
|
-
lines.shift();
|
|
4571
|
-
}
|
|
4572
|
-
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
4573
|
-
lines.pop();
|
|
4574
|
-
}
|
|
4575
|
-
const minSpaces = lines.reduce((min, line) => {
|
|
4576
|
-
if (line.trim() === "") {
|
|
4577
|
-
return min;
|
|
4578
|
-
}
|
|
4579
|
-
const leadingSpaces = line.match(/^\s*/)[0].length;
|
|
4580
|
-
return Math.min(min, leadingSpaces);
|
|
4581
|
-
}, Infinity);
|
|
4582
|
-
const trimmedLines = lines.map((line) => line.slice(minSpaces));
|
|
4583
|
-
return trimmedLines.join("\n");
|
|
4584
|
-
}
|
|
4585
|
-
|
|
4586
4846
|
const PLIST_LABEL$1 = "com.happy-cli.daemon";
|
|
4587
4847
|
const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
|
|
4588
4848
|
async function install$1() {
|