happy-coder 0.8.0 → 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 +558 -408
- package/dist/index.mjs +559 -409
- package/dist/lib.d.cts +77 -72
- package/dist/lib.d.mts +77 -72
- package/package.json +11 -9
package/dist/index.mjs
CHANGED
|
@@ -18,19 +18,19 @@ import 'node:events';
|
|
|
18
18
|
import 'socket.io-client';
|
|
19
19
|
import tweetnacl from 'tweetnacl';
|
|
20
20
|
import 'expo-server-sdk';
|
|
21
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
22
|
-
import { createServer } from 'node:http';
|
|
23
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
24
|
-
import * as z from 'zod';
|
|
25
|
-
import { z as z$1 } from 'zod';
|
|
26
21
|
import { spawn as spawn$1, exec, execSync as execSync$1 } from 'child_process';
|
|
27
22
|
import { promisify } from 'util';
|
|
28
23
|
import { createHash } from 'crypto';
|
|
24
|
+
import * as z from 'zod';
|
|
25
|
+
import { z as z$1 } from 'zod';
|
|
29
26
|
import fastify from 'fastify';
|
|
30
27
|
import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
|
|
31
28
|
import os from 'os';
|
|
32
29
|
import qrcode from 'qrcode-terminal';
|
|
33
30
|
import open$1 from 'open';
|
|
31
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
32
|
+
import { createServer } from 'node:http';
|
|
33
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
34
34
|
import { existsSync as existsSync$1, writeFileSync, chmodSync, unlinkSync } from 'fs';
|
|
35
35
|
|
|
36
36
|
class Session {
|
|
@@ -902,16 +902,19 @@ async function streamToStdin(stream, stdin, abort) {
|
|
|
902
902
|
}
|
|
903
903
|
|
|
904
904
|
class Query {
|
|
905
|
-
constructor(childStdin, childStdout, processExitPromise) {
|
|
905
|
+
constructor(childStdin, childStdout, processExitPromise, canCallTool) {
|
|
906
906
|
this.childStdin = childStdin;
|
|
907
907
|
this.childStdout = childStdout;
|
|
908
908
|
this.processExitPromise = processExitPromise;
|
|
909
|
+
this.canCallTool = canCallTool;
|
|
909
910
|
this.readMessages();
|
|
910
911
|
this.sdkMessages = this.readSdkMessages();
|
|
911
912
|
}
|
|
912
913
|
pendingControlResponses = /* @__PURE__ */ new Map();
|
|
914
|
+
cancelControllers = /* @__PURE__ */ new Map();
|
|
913
915
|
sdkMessages;
|
|
914
916
|
inputStream = new Stream();
|
|
917
|
+
canCallTool;
|
|
915
918
|
/**
|
|
916
919
|
* Set an error on the stream
|
|
917
920
|
*/
|
|
@@ -956,6 +959,12 @@ class Query {
|
|
|
956
959
|
handler(controlResponse.response);
|
|
957
960
|
}
|
|
958
961
|
continue;
|
|
962
|
+
} else if (message.type === "control_request") {
|
|
963
|
+
await this.handleControlRequest(message);
|
|
964
|
+
continue;
|
|
965
|
+
} else if (message.type === "control_cancel_request") {
|
|
966
|
+
this.handleControlCancelRequest(message);
|
|
967
|
+
continue;
|
|
959
968
|
}
|
|
960
969
|
this.inputStream.enqueue(message);
|
|
961
970
|
} catch (e) {
|
|
@@ -968,6 +977,7 @@ class Query {
|
|
|
968
977
|
this.inputStream.error(error);
|
|
969
978
|
} finally {
|
|
970
979
|
this.inputStream.done();
|
|
980
|
+
this.cleanupControllers();
|
|
971
981
|
rl.close();
|
|
972
982
|
}
|
|
973
983
|
}
|
|
@@ -1011,6 +1021,77 @@ class Query {
|
|
|
1011
1021
|
childStdin.write(JSON.stringify(sdkRequest) + "\n");
|
|
1012
1022
|
});
|
|
1013
1023
|
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Handle incoming control requests for tool permissions
|
|
1026
|
+
* Replicates the exact logic from the SDK's handleControlRequest method
|
|
1027
|
+
*/
|
|
1028
|
+
async handleControlRequest(request) {
|
|
1029
|
+
if (!this.childStdin) {
|
|
1030
|
+
logDebug("Cannot handle control request - no stdin available");
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const controller = new AbortController();
|
|
1034
|
+
this.cancelControllers.set(request.request_id, controller);
|
|
1035
|
+
try {
|
|
1036
|
+
const response = await this.processControlRequest(request, controller.signal);
|
|
1037
|
+
const controlResponse = {
|
|
1038
|
+
type: "control_response",
|
|
1039
|
+
response: {
|
|
1040
|
+
subtype: "success",
|
|
1041
|
+
request_id: request.request_id,
|
|
1042
|
+
response
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
this.childStdin.write(JSON.stringify(controlResponse) + "\n");
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
const controlErrorResponse = {
|
|
1048
|
+
type: "control_response",
|
|
1049
|
+
response: {
|
|
1050
|
+
subtype: "error",
|
|
1051
|
+
request_id: request.request_id,
|
|
1052
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
this.childStdin.write(JSON.stringify(controlErrorResponse) + "\n");
|
|
1056
|
+
} finally {
|
|
1057
|
+
this.cancelControllers.delete(request.request_id);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Handle control cancel requests
|
|
1062
|
+
* Replicates the exact logic from the SDK's handleControlCancelRequest method
|
|
1063
|
+
*/
|
|
1064
|
+
handleControlCancelRequest(request) {
|
|
1065
|
+
const controller = this.cancelControllers.get(request.request_id);
|
|
1066
|
+
if (controller) {
|
|
1067
|
+
controller.abort();
|
|
1068
|
+
this.cancelControllers.delete(request.request_id);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Process control requests based on subtype
|
|
1073
|
+
* Replicates the exact logic from the SDK's processControlRequest method
|
|
1074
|
+
*/
|
|
1075
|
+
async processControlRequest(request, signal) {
|
|
1076
|
+
if (request.request.subtype === "can_use_tool") {
|
|
1077
|
+
if (!this.canCallTool) {
|
|
1078
|
+
throw new Error("canCallTool callback is not provided.");
|
|
1079
|
+
}
|
|
1080
|
+
return this.canCallTool(request.request.tool_name, request.request.input, {
|
|
1081
|
+
signal
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
throw new Error("Unsupported control request subtype: " + request.request.subtype);
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Cleanup method to abort all pending control requests
|
|
1088
|
+
*/
|
|
1089
|
+
cleanupControllers() {
|
|
1090
|
+
for (const [requestId, controller] of this.cancelControllers.entries()) {
|
|
1091
|
+
controller.abort();
|
|
1092
|
+
this.cancelControllers.delete(requestId);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1014
1095
|
}
|
|
1015
1096
|
function query(config) {
|
|
1016
1097
|
const {
|
|
@@ -1027,12 +1108,12 @@ function query(config) {
|
|
|
1027
1108
|
mcpServers,
|
|
1028
1109
|
pathToClaudeCodeExecutable = getDefaultClaudeCodePath(),
|
|
1029
1110
|
permissionMode = "default",
|
|
1030
|
-
permissionPromptToolName,
|
|
1031
1111
|
continue: continueConversation,
|
|
1032
1112
|
resume,
|
|
1033
1113
|
model,
|
|
1034
1114
|
fallbackModel,
|
|
1035
|
-
strictMcpConfig
|
|
1115
|
+
strictMcpConfig,
|
|
1116
|
+
canCallTool
|
|
1036
1117
|
} = {}
|
|
1037
1118
|
} = config;
|
|
1038
1119
|
if (!process.env.CLAUDE_CODE_ENTRYPOINT) {
|
|
@@ -1043,7 +1124,12 @@ function query(config) {
|
|
|
1043
1124
|
if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
|
|
1044
1125
|
if (maxTurns) args.push("--max-turns", maxTurns.toString());
|
|
1045
1126
|
if (model) args.push("--model", model);
|
|
1046
|
-
if (
|
|
1127
|
+
if (canCallTool) {
|
|
1128
|
+
if (typeof prompt === "string") {
|
|
1129
|
+
throw new Error("canCallTool callback requires --input-format stream-json. Please set prompt as an AsyncIterable.");
|
|
1130
|
+
}
|
|
1131
|
+
args.push("--permission-prompt-tool", "stdio");
|
|
1132
|
+
}
|
|
1047
1133
|
if (continueConversation) args.push("--continue");
|
|
1048
1134
|
if (resume) args.push("--resume", resume);
|
|
1049
1135
|
if (allowedTools.length > 0) args.push("--allowedTools", allowedTools.join(","));
|
|
@@ -1107,7 +1193,7 @@ function query(config) {
|
|
|
1107
1193
|
}
|
|
1108
1194
|
});
|
|
1109
1195
|
});
|
|
1110
|
-
const query2 = new Query(childStdin, child.stdout, processExitPromise);
|
|
1196
|
+
const query2 = new Query(childStdin, child.stdout, processExitPromise, canCallTool);
|
|
1111
1197
|
child.on("error", (error) => {
|
|
1112
1198
|
if (config.options?.abort?.aborted) {
|
|
1113
1199
|
query2.setError(new AbortError("Claude Code process aborted by user"));
|
|
@@ -1125,17 +1211,48 @@ function query(config) {
|
|
|
1125
1211
|
return query2;
|
|
1126
1212
|
}
|
|
1127
1213
|
|
|
1128
|
-
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
}
|
|
1135
|
-
await delay(1e3);
|
|
1136
|
-
}
|
|
1214
|
+
function parseCompact(message) {
|
|
1215
|
+
const trimmed = message.trim();
|
|
1216
|
+
if (trimmed === "/compact") {
|
|
1217
|
+
return {
|
|
1218
|
+
isCompact: true,
|
|
1219
|
+
originalMessage: trimmed
|
|
1220
|
+
};
|
|
1137
1221
|
}
|
|
1138
|
-
|
|
1222
|
+
if (trimmed.startsWith("/compact ")) {
|
|
1223
|
+
return {
|
|
1224
|
+
isCompact: true,
|
|
1225
|
+
originalMessage: trimmed
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
return {
|
|
1229
|
+
isCompact: false,
|
|
1230
|
+
originalMessage: message
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
function parseClear(message) {
|
|
1234
|
+
const trimmed = message.trim();
|
|
1235
|
+
return {
|
|
1236
|
+
isClear: trimmed === "/clear"
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
function parseSpecialCommand(message) {
|
|
1240
|
+
const compactResult = parseCompact(message);
|
|
1241
|
+
if (compactResult.isCompact) {
|
|
1242
|
+
return {
|
|
1243
|
+
type: "compact",
|
|
1244
|
+
originalMessage: compactResult.originalMessage
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
const clearResult = parseClear(message);
|
|
1248
|
+
if (clearResult.isClear) {
|
|
1249
|
+
return {
|
|
1250
|
+
type: "clear"
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
return {
|
|
1254
|
+
type: null
|
|
1255
|
+
};
|
|
1139
1256
|
}
|
|
1140
1257
|
|
|
1141
1258
|
class PushableAsyncIterable {
|
|
@@ -1264,48 +1381,17 @@ class PushableAsyncIterable {
|
|
|
1264
1381
|
}
|
|
1265
1382
|
}
|
|
1266
1383
|
|
|
1267
|
-
function
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
return {
|
|
1277
|
-
isCompact: true,
|
|
1278
|
-
originalMessage: trimmed
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
return {
|
|
1282
|
-
isCompact: false,
|
|
1283
|
-
originalMessage: message
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
|
-
function parseClear(message) {
|
|
1287
|
-
const trimmed = message.trim();
|
|
1288
|
-
return {
|
|
1289
|
-
isClear: trimmed === "/clear"
|
|
1290
|
-
};
|
|
1291
|
-
}
|
|
1292
|
-
function parseSpecialCommand(message) {
|
|
1293
|
-
const compactResult = parseCompact(message);
|
|
1294
|
-
if (compactResult.isCompact) {
|
|
1295
|
-
return {
|
|
1296
|
-
type: "compact",
|
|
1297
|
-
originalMessage: compactResult.originalMessage
|
|
1298
|
-
};
|
|
1299
|
-
}
|
|
1300
|
-
const clearResult = parseClear(message);
|
|
1301
|
-
if (clearResult.isClear) {
|
|
1302
|
-
return {
|
|
1303
|
-
type: "clear"
|
|
1304
|
-
};
|
|
1384
|
+
async function awaitFileExist(file, timeout = 1e4) {
|
|
1385
|
+
const startTime = Date.now();
|
|
1386
|
+
while (Date.now() - startTime < timeout) {
|
|
1387
|
+
try {
|
|
1388
|
+
await access(file);
|
|
1389
|
+
return true;
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
await delay(1e3);
|
|
1392
|
+
}
|
|
1305
1393
|
}
|
|
1306
|
-
return
|
|
1307
|
-
type: null
|
|
1308
|
-
};
|
|
1394
|
+
return false;
|
|
1309
1395
|
}
|
|
1310
1396
|
|
|
1311
1397
|
async function claudeRemote(opts) {
|
|
@@ -1318,32 +1404,12 @@ async function claudeRemote(opts) {
|
|
|
1318
1404
|
process.env[key] = value;
|
|
1319
1405
|
});
|
|
1320
1406
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
resume: startFrom ?? void 0,
|
|
1325
|
-
mcpServers: opts.mcpServers,
|
|
1326
|
-
permissionPromptToolName: opts.permissionPromptToolName,
|
|
1327
|
-
permissionMode: opts.permissionMode,
|
|
1328
|
-
model: opts.model,
|
|
1329
|
-
fallbackModel: opts.fallbackModel,
|
|
1330
|
-
customSystemPrompt: opts.customSystemPrompt,
|
|
1331
|
-
appendSystemPrompt: opts.appendSystemPrompt,
|
|
1332
|
-
allowedTools: opts.allowedTools,
|
|
1333
|
-
disallowedTools: opts.disallowedTools,
|
|
1334
|
-
executable: "node",
|
|
1335
|
-
abort: opts.signal,
|
|
1336
|
-
pathToClaudeCodeExecutable: (() => {
|
|
1337
|
-
return resolve(join(projectPath(), "scripts", "claude_remote_launcher.cjs"));
|
|
1338
|
-
})()
|
|
1339
|
-
};
|
|
1340
|
-
if (opts.claudeArgs && opts.claudeArgs.length > 0) {
|
|
1341
|
-
sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
|
|
1407
|
+
const initial = await opts.nextMessage();
|
|
1408
|
+
if (!initial) {
|
|
1409
|
+
return;
|
|
1342
1410
|
}
|
|
1343
|
-
|
|
1344
|
-
const specialCommand = parseSpecialCommand(opts.message);
|
|
1411
|
+
const specialCommand = parseSpecialCommand(initial.message);
|
|
1345
1412
|
if (specialCommand.type === "clear") {
|
|
1346
|
-
logger.debug("[claudeRemote] /clear command detected - should not reach here, handled in start.ts");
|
|
1347
1413
|
if (opts.onCompletionEvent) {
|
|
1348
1414
|
opts.onCompletionEvent("Context was reset");
|
|
1349
1415
|
}
|
|
@@ -1352,29 +1418,33 @@ async function claudeRemote(opts) {
|
|
|
1352
1418
|
}
|
|
1353
1419
|
return;
|
|
1354
1420
|
}
|
|
1421
|
+
let isCompactCommand = false;
|
|
1355
1422
|
if (specialCommand.type === "compact") {
|
|
1356
1423
|
logger.debug("[claudeRemote] /compact command detected - will process as normal but with compaction behavior");
|
|
1357
|
-
|
|
1358
|
-
const isCompactCommand = specialCommand.type === "compact";
|
|
1359
|
-
if (isCompactCommand) {
|
|
1360
|
-
logger.debug("[claudeRemote] Compaction started");
|
|
1424
|
+
isCompactCommand = true;
|
|
1361
1425
|
if (opts.onCompletionEvent) {
|
|
1362
1426
|
opts.onCompletionEvent("Compaction started");
|
|
1363
1427
|
}
|
|
1364
1428
|
}
|
|
1365
|
-
let
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1429
|
+
let mode = initial.mode;
|
|
1430
|
+
const sdkOptions = {
|
|
1431
|
+
cwd: opts.path,
|
|
1432
|
+
resume: startFrom ?? void 0,
|
|
1433
|
+
mcpServers: opts.mcpServers,
|
|
1434
|
+
permissionMode: initial.mode.permissionMode === "plan" ? "plan" : "default",
|
|
1435
|
+
model: initial.mode.model,
|
|
1436
|
+
fallbackModel: initial.mode.fallbackModel,
|
|
1437
|
+
customSystemPrompt: initial.mode.customSystemPrompt ? initial.mode.customSystemPrompt + "\n\n" + systemPrompt : void 0,
|
|
1438
|
+
appendSystemPrompt: initial.mode.appendSystemPrompt ? initial.mode.appendSystemPrompt + "\n\n" + systemPrompt : systemPrompt,
|
|
1439
|
+
allowedTools: initial.mode.allowedTools ? initial.mode.allowedTools.concat(opts.allowedTools) : opts.allowedTools,
|
|
1440
|
+
disallowedTools: initial.mode.disallowedTools,
|
|
1441
|
+
canCallTool: (toolName, input, options) => opts.canCallTool(toolName, input, mode, options),
|
|
1442
|
+
executable: "node",
|
|
1443
|
+
abort: opts.signal,
|
|
1444
|
+
pathToClaudeCodeExecutable: (() => {
|
|
1445
|
+
return resolve(join(projectPath(), "scripts", "claude_remote_launcher.cjs"));
|
|
1446
|
+
})()
|
|
1447
|
+
};
|
|
1378
1448
|
let thinking = false;
|
|
1379
1449
|
const updateThinking = (newThinking) => {
|
|
1380
1450
|
if (thinking !== newThinking) {
|
|
@@ -1385,15 +1455,27 @@ async function claudeRemote(opts) {
|
|
|
1385
1455
|
}
|
|
1386
1456
|
}
|
|
1387
1457
|
};
|
|
1458
|
+
let messages = new PushableAsyncIterable();
|
|
1459
|
+
messages.push({
|
|
1460
|
+
type: "user",
|
|
1461
|
+
message: {
|
|
1462
|
+
role: "user",
|
|
1463
|
+
content: initial.message
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
const response = query({
|
|
1467
|
+
prompt: messages,
|
|
1468
|
+
options: sdkOptions
|
|
1469
|
+
});
|
|
1388
1470
|
updateThinking(true);
|
|
1389
1471
|
try {
|
|
1390
1472
|
logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
1391
|
-
for await (const
|
|
1392
|
-
logger.debugLargeJson(`[claudeRemote] Message ${
|
|
1393
|
-
opts.onMessage(
|
|
1394
|
-
if (
|
|
1473
|
+
for await (const message of response) {
|
|
1474
|
+
logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
|
|
1475
|
+
opts.onMessage(message);
|
|
1476
|
+
if (message.type === "system" && message.subtype === "init") {
|
|
1395
1477
|
updateThinking(true);
|
|
1396
|
-
const systemInit =
|
|
1478
|
+
const systemInit = message;
|
|
1397
1479
|
if (systemInit.session_id) {
|
|
1398
1480
|
logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${systemInit.session_id}`);
|
|
1399
1481
|
const projectDir = getProjectPath(opts.path);
|
|
@@ -1402,7 +1484,7 @@ async function claudeRemote(opts) {
|
|
|
1402
1484
|
opts.onSessionFound(systemInit.session_id);
|
|
1403
1485
|
}
|
|
1404
1486
|
}
|
|
1405
|
-
if (
|
|
1487
|
+
if (message.type === "result") {
|
|
1406
1488
|
updateThinking(false);
|
|
1407
1489
|
logger.debug("[claudeRemote] Result received, exiting claudeRemote");
|
|
1408
1490
|
if (isCompactCommand) {
|
|
@@ -1410,26 +1492,28 @@ async function claudeRemote(opts) {
|
|
|
1410
1492
|
if (opts.onCompletionEvent) {
|
|
1411
1493
|
opts.onCompletionEvent("Compaction completed");
|
|
1412
1494
|
}
|
|
1495
|
+
isCompactCommand = false;
|
|
1413
1496
|
}
|
|
1414
|
-
|
|
1497
|
+
const next = await opts.nextMessage();
|
|
1498
|
+
if (!next) {
|
|
1499
|
+
messages.end();
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
mode = next.mode;
|
|
1503
|
+
messages.push({ type: "user", message: { role: "user", content: next.message } });
|
|
1415
1504
|
}
|
|
1416
|
-
if (
|
|
1417
|
-
const msg =
|
|
1505
|
+
if (message.type === "user") {
|
|
1506
|
+
const msg = message;
|
|
1418
1507
|
if (msg.message.role === "user" && Array.isArray(msg.message.content)) {
|
|
1419
1508
|
for (let c of msg.message.content) {
|
|
1420
|
-
if (c.type === "tool_result" &&
|
|
1421
|
-
logger.debug("[claudeRemote]
|
|
1422
|
-
return;
|
|
1423
|
-
}
|
|
1424
|
-
if (c.type === "tool_result" && c.tool_use_id && opts.responses.has(c.tool_use_id) && !opts.responses.get(c.tool_use_id).approved) {
|
|
1425
|
-
logger.debug("[claudeRemote] Tool rejected, exiting claudeRemote");
|
|
1509
|
+
if (c.type === "tool_result" && c.tool_use_id && opts.isAborted(c.tool_use_id)) {
|
|
1510
|
+
logger.debug("[claudeRemote] Tool aborted, exiting claudeRemote");
|
|
1426
1511
|
return;
|
|
1427
1512
|
}
|
|
1428
1513
|
}
|
|
1429
1514
|
}
|
|
1430
1515
|
}
|
|
1431
1516
|
}
|
|
1432
|
-
logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
1433
1517
|
} catch (e) {
|
|
1434
1518
|
if (e instanceof AbortError) {
|
|
1435
1519
|
logger.debug(`[claudeRemote] Aborted`);
|
|
@@ -1439,71 +1523,11 @@ async function claudeRemote(opts) {
|
|
|
1439
1523
|
} finally {
|
|
1440
1524
|
updateThinking(false);
|
|
1441
1525
|
}
|
|
1442
|
-
logger.debug(`[claudeRemote] Function completed`);
|
|
1443
1526
|
}
|
|
1444
1527
|
|
|
1445
1528
|
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.`;
|
|
1446
1529
|
const PLAN_FAKE_RESTART = `PlEaZe Continue with plan.`;
|
|
1447
1530
|
|
|
1448
|
-
async function startPermissionServerV2(handler) {
|
|
1449
|
-
const mcp = new McpServer({
|
|
1450
|
-
name: "Permission Server",
|
|
1451
|
-
version: "1.0.0",
|
|
1452
|
-
description: "A server that allows you to request permissions from the user"
|
|
1453
|
-
});
|
|
1454
|
-
mcp.registerTool("ask_permission", {
|
|
1455
|
-
description: "Request permission to execute a tool",
|
|
1456
|
-
title: "Request Permission",
|
|
1457
|
-
inputSchema: {
|
|
1458
|
-
tool_name: z$1.string().describe("The tool that needs permission"),
|
|
1459
|
-
input: z$1.any().describe("The arguments for the tool")
|
|
1460
|
-
}
|
|
1461
|
-
}, async (args) => {
|
|
1462
|
-
const response = await handler({ name: args.tool_name, arguments: args.input });
|
|
1463
|
-
logger.debugLargeJson("[permissionServerV2] Response", response);
|
|
1464
|
-
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.` };
|
|
1465
|
-
return {
|
|
1466
|
-
content: [
|
|
1467
|
-
{
|
|
1468
|
-
type: "text",
|
|
1469
|
-
text: JSON.stringify(result)
|
|
1470
|
-
}
|
|
1471
|
-
],
|
|
1472
|
-
isError: false
|
|
1473
|
-
};
|
|
1474
|
-
});
|
|
1475
|
-
const transport = new StreamableHTTPServerTransport({
|
|
1476
|
-
// NOTE: Returning session id here will result in claude
|
|
1477
|
-
// sdk spawn to fail with `Invalid Request: Server already initialized`
|
|
1478
|
-
sessionIdGenerator: void 0
|
|
1479
|
-
});
|
|
1480
|
-
await mcp.connect(transport);
|
|
1481
|
-
const server = createServer(async (req, res) => {
|
|
1482
|
-
try {
|
|
1483
|
-
await transport.handleRequest(req, res);
|
|
1484
|
-
} catch (error) {
|
|
1485
|
-
logger.debug("Error handling request:", error);
|
|
1486
|
-
if (!res.headersSent) {
|
|
1487
|
-
res.writeHead(500).end();
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
});
|
|
1491
|
-
const baseUrl = await new Promise((resolve) => {
|
|
1492
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1493
|
-
const addr = server.address();
|
|
1494
|
-
resolve(new URL(`http://127.0.0.1:${addr.port}`));
|
|
1495
|
-
});
|
|
1496
|
-
});
|
|
1497
|
-
return {
|
|
1498
|
-
url: baseUrl.toString(),
|
|
1499
|
-
toolName: "ask_permission",
|
|
1500
|
-
stop: () => {
|
|
1501
|
-
mcp.close();
|
|
1502
|
-
server.close();
|
|
1503
|
-
}
|
|
1504
|
-
};
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
1531
|
function deepEqual(a, b) {
|
|
1508
1532
|
if (a === b) return true;
|
|
1509
1533
|
if (a == null || b == null) return false;
|
|
@@ -1518,133 +1542,178 @@ function deepEqual(a, b) {
|
|
|
1518
1542
|
return true;
|
|
1519
1543
|
}
|
|
1520
1544
|
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1545
|
+
const STANDARD_TOOLS = {
|
|
1546
|
+
// File operations
|
|
1547
|
+
"Read": "Read File",
|
|
1548
|
+
"Write": "Write File",
|
|
1549
|
+
"Edit": "Edit File",
|
|
1550
|
+
"MultiEdit": "Edit File",
|
|
1551
|
+
"NotebookEdit": "Edit Notebook",
|
|
1552
|
+
// Search and navigation
|
|
1553
|
+
"Glob": "Find Files",
|
|
1554
|
+
"Grep": "Search in Files",
|
|
1555
|
+
"LS": "List Directory",
|
|
1556
|
+
// Command execution
|
|
1557
|
+
"Bash": "Run Command",
|
|
1558
|
+
"BashOutput": "Check Command Output",
|
|
1559
|
+
"KillBash": "Stop Command",
|
|
1560
|
+
// Task management
|
|
1561
|
+
"TodoWrite": "Update Tasks",
|
|
1562
|
+
"TodoRead": "Read Tasks",
|
|
1563
|
+
"Task": "Launch Agent",
|
|
1564
|
+
// Web tools
|
|
1565
|
+
"WebFetch": "Fetch Web Page",
|
|
1566
|
+
"WebSearch": "Search Web",
|
|
1567
|
+
// Special cases
|
|
1568
|
+
"exit_plan_mode": "Execute Plan",
|
|
1569
|
+
"ExitPlanMode": "Execute Plan"
|
|
1570
|
+
};
|
|
1571
|
+
function toTitleCase(str) {
|
|
1572
|
+
return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
1573
|
+
}
|
|
1574
|
+
function getToolName(toolName) {
|
|
1575
|
+
if (STANDARD_TOOLS[toolName]) {
|
|
1576
|
+
return STANDARD_TOOLS[toolName];
|
|
1577
|
+
}
|
|
1578
|
+
if (toolName.startsWith("mcp__")) {
|
|
1579
|
+
const parts = toolName.split("__");
|
|
1580
|
+
if (parts.length >= 3) {
|
|
1581
|
+
const server = toTitleCase(parts[1]);
|
|
1582
|
+
const action = toTitleCase(parts.slice(2).join("_"));
|
|
1583
|
+
return `${server}: ${action}`;
|
|
1540
1584
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
logger.debug("Permission request" + id + " " + JSON.stringify(request));
|
|
1588
|
-
session.api.push().sendToAllDevices(
|
|
1589
|
-
"Permission Request",
|
|
1590
|
-
`Claude wants to use ${request.name}`,
|
|
1591
|
-
{
|
|
1592
|
-
sessionId: session.client.sessionId,
|
|
1593
|
-
requestId: id,
|
|
1594
|
-
tool: request.name,
|
|
1595
|
-
type: "permission_request"
|
|
1596
|
-
}
|
|
1597
|
-
);
|
|
1598
|
-
session.client.updateAgentState((currentState) => ({
|
|
1599
|
-
...currentState,
|
|
1600
|
-
requests: {
|
|
1601
|
-
...currentState.requests,
|
|
1602
|
-
[id]: {
|
|
1603
|
-
tool: request.name,
|
|
1604
|
-
arguments: request.arguments,
|
|
1605
|
-
createdAt: Date.now()
|
|
1585
|
+
}
|
|
1586
|
+
return toTitleCase(toolName);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function getToolDescriptor(toolName) {
|
|
1590
|
+
if (toolName === "exit_plan_mode" || toolName === "ExitPlanMode") {
|
|
1591
|
+
return { edit: false, exitPlan: true };
|
|
1592
|
+
}
|
|
1593
|
+
if (toolName === "Edit" || toolName === "MultiEdit" || toolName === "Write" || toolName === "NotebookEdit") {
|
|
1594
|
+
return { edit: true, exitPlan: false };
|
|
1595
|
+
}
|
|
1596
|
+
return { edit: false, exitPlan: false };
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
class PermissionHandler {
|
|
1600
|
+
toolCalls = [];
|
|
1601
|
+
responses = /* @__PURE__ */ new Map();
|
|
1602
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
1603
|
+
session;
|
|
1604
|
+
allowedTools = /* @__PURE__ */ new Set();
|
|
1605
|
+
permissionMode = "default";
|
|
1606
|
+
constructor(session) {
|
|
1607
|
+
this.session = session;
|
|
1608
|
+
this.setupClientHandler();
|
|
1609
|
+
}
|
|
1610
|
+
handleModeChange(mode) {
|
|
1611
|
+
this.permissionMode = mode;
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Handler response
|
|
1615
|
+
*/
|
|
1616
|
+
handlePermissionResponse(response, pending) {
|
|
1617
|
+
if (response.allowTools && response.allowTools.length > 0) {
|
|
1618
|
+
response.allowTools.forEach((tool) => this.allowedTools.add(tool));
|
|
1619
|
+
}
|
|
1620
|
+
if (response.mode) {
|
|
1621
|
+
this.permissionMode = response.mode;
|
|
1622
|
+
}
|
|
1623
|
+
if (pending.toolName === "exit_plan_mode" || pending.toolName === "ExitPlanMode") {
|
|
1624
|
+
logger.debug("Plan mode result received", response);
|
|
1625
|
+
if (response.approved) {
|
|
1626
|
+
logger.debug("Plan approved - injecting PLAN_FAKE_RESTART");
|
|
1627
|
+
if (response.mode && ["default", "acceptEdits", "bypassPermissions"].includes(response.mode)) {
|
|
1628
|
+
this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: response.mode });
|
|
1629
|
+
} else {
|
|
1630
|
+
this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: "default" });
|
|
1606
1631
|
}
|
|
1632
|
+
pending.resolve({ behavior: "deny", message: PLAN_FAKE_REJECT });
|
|
1633
|
+
} else {
|
|
1634
|
+
pending.resolve({ behavior: "deny", message: response.reason || "Plan rejected" });
|
|
1607
1635
|
}
|
|
1608
|
-
}));
|
|
1609
|
-
promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
|
|
1610
|
-
return promise;
|
|
1611
|
-
}
|
|
1612
|
-
session.client.setHandler("permission", async (message) => {
|
|
1613
|
-
logger.debug("Permission response" + JSON.stringify(message));
|
|
1614
|
-
const id = message.id;
|
|
1615
|
-
const resolve = requests.get(id);
|
|
1616
|
-
if (resolve) {
|
|
1617
|
-
responses.set(id, message);
|
|
1618
|
-
resolve({ approved: message.approved, reason: message.reason, mode: message.mode });
|
|
1619
|
-
requests.delete(id);
|
|
1620
1636
|
} else {
|
|
1621
|
-
|
|
1622
|
-
|
|
1637
|
+
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.` };
|
|
1638
|
+
pending.resolve(result);
|
|
1623
1639
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
return {
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Creates the canCallTool callback for the SDK
|
|
1643
|
+
*/
|
|
1644
|
+
handleToolCall = async (toolName, input, mode, options) => {
|
|
1645
|
+
if (this.allowedTools.has(toolName)) {
|
|
1646
|
+
return { behavior: "allow", updatedInput: input };
|
|
1647
|
+
}
|
|
1648
|
+
const descriptor = getToolDescriptor(toolName);
|
|
1649
|
+
if (this.permissionMode === "bypassPermissions") {
|
|
1650
|
+
return { behavior: "allow", updatedInput: input };
|
|
1651
|
+
}
|
|
1652
|
+
if (this.permissionMode === "acceptEdits" && descriptor.edit) {
|
|
1653
|
+
return { behavior: "allow", updatedInput: input };
|
|
1654
|
+
}
|
|
1655
|
+
let toolCallId = this.resolveToolCallId(toolName, input);
|
|
1656
|
+
if (!toolCallId) {
|
|
1657
|
+
await delay(1e3);
|
|
1658
|
+
toolCallId = this.resolveToolCallId(toolName, input);
|
|
1659
|
+
if (!toolCallId) {
|
|
1660
|
+
throw new Error(`Could not resolve tool call ID for ${toolName}`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return this.handlePermissionRequest(toolCallId, toolName, input, options.signal);
|
|
1664
|
+
};
|
|
1665
|
+
/**
|
|
1666
|
+
* Handles individual permission requests
|
|
1667
|
+
*/
|
|
1668
|
+
async handlePermissionRequest(id, toolName, input, signal) {
|
|
1669
|
+
return new Promise((resolve, reject) => {
|
|
1670
|
+
const abortHandler = () => {
|
|
1671
|
+
this.pendingRequests.delete(id);
|
|
1672
|
+
reject(new Error("Permission request aborted"));
|
|
1673
|
+
};
|
|
1674
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1675
|
+
this.pendingRequests.set(id, {
|
|
1676
|
+
resolve: (result) => {
|
|
1677
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1678
|
+
resolve(result);
|
|
1679
|
+
},
|
|
1680
|
+
reject: (error) => {
|
|
1681
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1682
|
+
reject(error);
|
|
1683
|
+
},
|
|
1684
|
+
toolName,
|
|
1685
|
+
input
|
|
1686
|
+
});
|
|
1687
|
+
this.session.api.push().sendToAllDevices(
|
|
1688
|
+
"Permission Request",
|
|
1689
|
+
`Claude wants to ${getToolName(toolName)}`,
|
|
1690
|
+
{
|
|
1691
|
+
sessionId: this.session.client.sessionId,
|
|
1692
|
+
requestId: id,
|
|
1693
|
+
tool: toolName,
|
|
1694
|
+
type: "permission_request"
|
|
1695
|
+
}
|
|
1696
|
+
);
|
|
1697
|
+
this.session.client.updateAgentState((currentState) => ({
|
|
1631
1698
|
...currentState,
|
|
1632
|
-
requests:
|
|
1633
|
-
|
|
1634
|
-
...currentState.completedRequests,
|
|
1699
|
+
requests: {
|
|
1700
|
+
...currentState.requests,
|
|
1635
1701
|
[id]: {
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
reason: isExitPlanModeSuccess ? "Plan approved" : message.reason
|
|
1702
|
+
tool: toolName,
|
|
1703
|
+
arguments: input,
|
|
1704
|
+
createdAt: Date.now()
|
|
1640
1705
|
}
|
|
1641
1706
|
}
|
|
1642
|
-
};
|
|
1707
|
+
}));
|
|
1708
|
+
logger.debug(`Permission request sent for tool call ${id}: ${toolName}`);
|
|
1643
1709
|
});
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Resolves tool call ID based on tool name and input
|
|
1713
|
+
*/
|
|
1714
|
+
resolveToolCallId(name, args) {
|
|
1715
|
+
for (let i = this.toolCalls.length - 1; i >= 0; i--) {
|
|
1716
|
+
const call = this.toolCalls[i];
|
|
1648
1717
|
if (call.name === name && deepEqual(call.input, args)) {
|
|
1649
1718
|
if (call.used) {
|
|
1650
1719
|
return null;
|
|
@@ -1654,59 +1723,22 @@ async function startPermissionResolver(session) {
|
|
|
1654
1723
|
}
|
|
1655
1724
|
}
|
|
1656
1725
|
return null;
|
|
1657
|
-
};
|
|
1658
|
-
function reset() {
|
|
1659
|
-
toolCalls = [];
|
|
1660
|
-
requests.clear();
|
|
1661
|
-
responses.clear();
|
|
1662
|
-
for (const pending of pendingPermissionRequests) {
|
|
1663
|
-
clearTimeout(pending.timeout);
|
|
1664
|
-
}
|
|
1665
|
-
pendingPermissionRequests = [];
|
|
1666
|
-
session.client.updateAgentState((currentState) => {
|
|
1667
|
-
const pendingRequests = currentState.requests || {};
|
|
1668
|
-
const completedRequests = { ...currentState.completedRequests };
|
|
1669
|
-
for (const [id, request] of Object.entries(pendingRequests)) {
|
|
1670
|
-
completedRequests[id] = {
|
|
1671
|
-
...request,
|
|
1672
|
-
completedAt: Date.now(),
|
|
1673
|
-
status: "canceled",
|
|
1674
|
-
reason: "Session switched to local mode"
|
|
1675
|
-
};
|
|
1676
|
-
}
|
|
1677
|
-
return {
|
|
1678
|
-
...currentState,
|
|
1679
|
-
requests: {},
|
|
1680
|
-
// Clear all pending requests
|
|
1681
|
-
completedRequests
|
|
1682
|
-
};
|
|
1683
|
-
});
|
|
1684
1726
|
}
|
|
1685
|
-
|
|
1727
|
+
/**
|
|
1728
|
+
* Handles messages to track tool calls
|
|
1729
|
+
*/
|
|
1730
|
+
onMessage(message) {
|
|
1686
1731
|
if (message.type === "assistant") {
|
|
1687
1732
|
const assistantMsg = message;
|
|
1688
1733
|
if (assistantMsg.message && assistantMsg.message.content) {
|
|
1689
1734
|
for (const block of assistantMsg.message.content) {
|
|
1690
1735
|
if (block.type === "tool_use") {
|
|
1691
|
-
toolCalls.push({
|
|
1736
|
+
this.toolCalls.push({
|
|
1692
1737
|
id: block.id,
|
|
1693
1738
|
name: block.name,
|
|
1694
1739
|
input: block.input,
|
|
1695
1740
|
used: false
|
|
1696
1741
|
});
|
|
1697
|
-
for (let i = pendingPermissionRequests.length - 1; i >= 0; i--) {
|
|
1698
|
-
const pending = pendingPermissionRequests[i];
|
|
1699
|
-
if (pending.request.name === block.name && deepEqual(pending.request.arguments, block.input)) {
|
|
1700
|
-
logger.debug(`Resolving pending permission request for ${block.name} with ID ${block.id}`);
|
|
1701
|
-
clearTimeout(pending.timeout);
|
|
1702
|
-
pendingPermissionRequests.splice(i, 1);
|
|
1703
|
-
handlePermissionRequest(block.id, pending.request).then(
|
|
1704
|
-
pending.resolve,
|
|
1705
|
-
pending.reject
|
|
1706
|
-
);
|
|
1707
|
-
break;
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
1742
|
}
|
|
1711
1743
|
}
|
|
1712
1744
|
}
|
|
@@ -1716,7 +1748,7 @@ async function startPermissionResolver(session) {
|
|
|
1716
1748
|
if (userMsg.message && userMsg.message.content && Array.isArray(userMsg.message.content)) {
|
|
1717
1749
|
for (const block of userMsg.message.content) {
|
|
1718
1750
|
if (block.type === "tool_result" && block.tool_use_id) {
|
|
1719
|
-
const toolCall = toolCalls.find((tc) => tc.id === block.tool_use_id);
|
|
1751
|
+
const toolCall = this.toolCalls.find((tc) => tc.id === block.tool_use_id);
|
|
1720
1752
|
if (toolCall && !toolCall.used) {
|
|
1721
1753
|
toolCall.used = true;
|
|
1722
1754
|
}
|
|
@@ -1725,12 +1757,92 @@ async function startPermissionResolver(session) {
|
|
|
1725
1757
|
}
|
|
1726
1758
|
}
|
|
1727
1759
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
responses
|
|
1733
|
-
|
|
1760
|
+
/**
|
|
1761
|
+
* Checks if a tool call is rejected
|
|
1762
|
+
*/
|
|
1763
|
+
isAborted(toolCallId) {
|
|
1764
|
+
if (this.responses.get(toolCallId)?.approved === false) {
|
|
1765
|
+
return true;
|
|
1766
|
+
}
|
|
1767
|
+
const toolCall = this.toolCalls.find((tc) => tc.id === toolCallId);
|
|
1768
|
+
if (toolCall && (toolCall.name === "exit_plan_mode" || toolCall.name === "ExitPlanMode")) {
|
|
1769
|
+
return true;
|
|
1770
|
+
}
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Resets all state for new sessions
|
|
1775
|
+
*/
|
|
1776
|
+
reset() {
|
|
1777
|
+
this.toolCalls = [];
|
|
1778
|
+
this.responses.clear();
|
|
1779
|
+
for (const [, pending] of this.pendingRequests.entries()) {
|
|
1780
|
+
pending.reject(new Error("Session reset"));
|
|
1781
|
+
}
|
|
1782
|
+
this.pendingRequests.clear();
|
|
1783
|
+
this.session.client.updateAgentState((currentState) => {
|
|
1784
|
+
const pendingRequests = currentState.requests || {};
|
|
1785
|
+
const completedRequests = { ...currentState.completedRequests };
|
|
1786
|
+
for (const [id, request] of Object.entries(pendingRequests)) {
|
|
1787
|
+
completedRequests[id] = {
|
|
1788
|
+
...request,
|
|
1789
|
+
completedAt: Date.now(),
|
|
1790
|
+
status: "canceled",
|
|
1791
|
+
reason: "Session switched to local mode"
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
return {
|
|
1795
|
+
...currentState,
|
|
1796
|
+
requests: {},
|
|
1797
|
+
// Clear all pending requests
|
|
1798
|
+
completedRequests
|
|
1799
|
+
};
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Sets up the client handler for permission responses
|
|
1804
|
+
*/
|
|
1805
|
+
setupClientHandler() {
|
|
1806
|
+
this.session.client.setHandler("permission", async (message) => {
|
|
1807
|
+
logger.debug(`Permission response: ${JSON.stringify(message)}`);
|
|
1808
|
+
const id = message.id;
|
|
1809
|
+
const pending = this.pendingRequests.get(id);
|
|
1810
|
+
if (!pending) {
|
|
1811
|
+
logger.debug("Permission request not found or already resolved");
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
this.responses.set(id, { ...message, receivedAt: Date.now() });
|
|
1815
|
+
this.pendingRequests.delete(id);
|
|
1816
|
+
this.handlePermissionResponse(message, pending);
|
|
1817
|
+
this.session.client.updateAgentState((currentState) => {
|
|
1818
|
+
const request = currentState.requests?.[id];
|
|
1819
|
+
if (!request) return currentState;
|
|
1820
|
+
let r = { ...currentState.requests };
|
|
1821
|
+
delete r[id];
|
|
1822
|
+
return {
|
|
1823
|
+
...currentState,
|
|
1824
|
+
requests: r,
|
|
1825
|
+
completedRequests: {
|
|
1826
|
+
...currentState.completedRequests,
|
|
1827
|
+
[id]: {
|
|
1828
|
+
...request,
|
|
1829
|
+
completedAt: Date.now(),
|
|
1830
|
+
status: message.approved ? "approved" : "denied",
|
|
1831
|
+
reason: message.reason,
|
|
1832
|
+
mode: message.mode,
|
|
1833
|
+
allowTools: message.allowTools
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
});
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Gets the responses map (for compatibility with existing code)
|
|
1842
|
+
*/
|
|
1843
|
+
getResponses() {
|
|
1844
|
+
return this.responses;
|
|
1845
|
+
}
|
|
1734
1846
|
}
|
|
1735
1847
|
|
|
1736
1848
|
function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
|
|
@@ -2104,12 +2216,6 @@ async function claudeRemoteLauncher(session) {
|
|
|
2104
2216
|
}
|
|
2105
2217
|
process.stdin.setEncoding("utf8");
|
|
2106
2218
|
}
|
|
2107
|
-
const scanner = await createSessionScanner({
|
|
2108
|
-
sessionId: session.sessionId,
|
|
2109
|
-
workingDirectory: session.path,
|
|
2110
|
-
onMessage: (message) => {
|
|
2111
|
-
}
|
|
2112
|
-
});
|
|
2113
2219
|
let exitReason = null;
|
|
2114
2220
|
let abortController = null;
|
|
2115
2221
|
let abortFuture = null;
|
|
@@ -2132,17 +2238,17 @@ async function claudeRemoteLauncher(session) {
|
|
|
2132
2238
|
}
|
|
2133
2239
|
session.client.setHandler("abort", doAbort);
|
|
2134
2240
|
session.client.setHandler("switch", doSwitch);
|
|
2135
|
-
const
|
|
2241
|
+
const permissionHandler = new PermissionHandler(session);
|
|
2136
2242
|
const sdkToLogConverter = new SDKToLogConverter({
|
|
2137
2243
|
sessionId: session.sessionId || "unknown",
|
|
2138
2244
|
cwd: session.path,
|
|
2139
2245
|
version: process.env.npm_package_version
|
|
2140
|
-
},
|
|
2246
|
+
}, permissionHandler.getResponses());
|
|
2141
2247
|
let planModeToolCalls = /* @__PURE__ */ new Set();
|
|
2142
2248
|
let ongoingToolCalls = /* @__PURE__ */ new Map();
|
|
2143
2249
|
function onMessage(message) {
|
|
2144
2250
|
formatClaudeMessageForInk(message, messageBuffer);
|
|
2145
|
-
|
|
2251
|
+
permissionHandler.onMessage(message);
|
|
2146
2252
|
if (message.type === "assistant") {
|
|
2147
2253
|
let umessage = message;
|
|
2148
2254
|
if (umessage.message.content && Array.isArray(umessage.message.content)) {
|
|
@@ -2206,6 +2312,32 @@ async function claudeRemoteLauncher(session) {
|
|
|
2206
2312
|
}
|
|
2207
2313
|
const logMessage = sdkToLogConverter.convert(msg);
|
|
2208
2314
|
if (logMessage) {
|
|
2315
|
+
if (logMessage.type === "user" && logMessage.message?.content) {
|
|
2316
|
+
const content = Array.isArray(logMessage.message.content) ? logMessage.message.content : [];
|
|
2317
|
+
for (let i = 0; i < content.length; i++) {
|
|
2318
|
+
const c = content[i];
|
|
2319
|
+
if (c.type === "tool_result" && c.tool_use_id) {
|
|
2320
|
+
const responses = permissionHandler.getResponses();
|
|
2321
|
+
const response = responses.get(c.tool_use_id);
|
|
2322
|
+
if (response) {
|
|
2323
|
+
const permissions = {
|
|
2324
|
+
date: response.receivedAt || Date.now(),
|
|
2325
|
+
result: response.approved ? "approved" : "denied"
|
|
2326
|
+
};
|
|
2327
|
+
if (response.mode) {
|
|
2328
|
+
permissions.mode = response.mode;
|
|
2329
|
+
}
|
|
2330
|
+
if (response.allowTools && response.allowTools.length > 0) {
|
|
2331
|
+
permissions.allowedTools = response.allowTools;
|
|
2332
|
+
}
|
|
2333
|
+
content[i] = {
|
|
2334
|
+
...c,
|
|
2335
|
+
permissions
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2209
2341
|
if (logMessage.type !== "system") {
|
|
2210
2342
|
session.client.sendClaudeSessionMessage(logMessage);
|
|
2211
2343
|
}
|
|
@@ -2225,58 +2357,57 @@ async function claudeRemoteLauncher(session) {
|
|
|
2225
2357
|
}
|
|
2226
2358
|
}
|
|
2227
2359
|
try {
|
|
2360
|
+
let pending = null;
|
|
2228
2361
|
while (!exitReason) {
|
|
2229
|
-
logger.debug("[remote]: fetch next message");
|
|
2230
|
-
abortController = new AbortController();
|
|
2231
|
-
abortFuture = new Future();
|
|
2232
|
-
const messageData = await session.queue.waitForMessagesAndGetAsString(abortController.signal);
|
|
2233
|
-
if (!messageData || abortController.signal.aborted) {
|
|
2234
|
-
logger.debug("[remote]: fetch next message done: no message or aborted");
|
|
2235
|
-
abortFuture?.resolve(void 0);
|
|
2236
|
-
if (exitReason) {
|
|
2237
|
-
return exitReason;
|
|
2238
|
-
} else {
|
|
2239
|
-
continue;
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
logger.debug("[remote]: fetch next message done: message received");
|
|
2243
|
-
abortFuture?.resolve(void 0);
|
|
2244
|
-
abortFuture = null;
|
|
2245
|
-
abortController = null;
|
|
2246
2362
|
logger.debug("[remote]: launch");
|
|
2247
2363
|
messageBuffer.addMessage("\u2550".repeat(40), "status");
|
|
2248
2364
|
messageBuffer.addMessage("Starting new Claude session...", "status");
|
|
2249
|
-
|
|
2365
|
+
const controller = new AbortController();
|
|
2366
|
+
abortController = controller;
|
|
2250
2367
|
abortFuture = new Future();
|
|
2251
|
-
|
|
2368
|
+
permissionHandler.reset();
|
|
2252
2369
|
sdkToLogConverter.resetParentChain();
|
|
2370
|
+
let modeHash = null;
|
|
2371
|
+
let mode = null;
|
|
2253
2372
|
try {
|
|
2254
2373
|
await claudeRemote({
|
|
2255
2374
|
sessionId: session.sessionId,
|
|
2256
2375
|
path: session.path,
|
|
2257
|
-
|
|
2258
|
-
mcpServers:
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2376
|
+
allowedTools: session.allowedTools ?? [],
|
|
2377
|
+
mcpServers: session.mcpServers,
|
|
2378
|
+
canCallTool: permissionHandler.handleToolCall,
|
|
2379
|
+
isAborted: (toolCallId) => {
|
|
2380
|
+
return permissionHandler.isAborted(toolCallId);
|
|
2381
|
+
},
|
|
2382
|
+
nextMessage: async () => {
|
|
2383
|
+
if (pending) {
|
|
2384
|
+
let p = pending;
|
|
2385
|
+
pending = null;
|
|
2386
|
+
permissionHandler.handleModeChange(p.mode.permissionMode);
|
|
2387
|
+
return p;
|
|
2263
2388
|
}
|
|
2389
|
+
let msg = await session.queue.waitForMessagesAndGetAsString(controller.signal);
|
|
2390
|
+
if (msg) {
|
|
2391
|
+
if (modeHash && msg.hash !== modeHash || msg.isolate) {
|
|
2392
|
+
logger.debug("[remote]: mode has changed, pending message");
|
|
2393
|
+
pending = msg;
|
|
2394
|
+
return null;
|
|
2395
|
+
}
|
|
2396
|
+
modeHash = msg.hash;
|
|
2397
|
+
mode = msg.mode;
|
|
2398
|
+
permissionHandler.handleModeChange(mode.permissionMode);
|
|
2399
|
+
return {
|
|
2400
|
+
message: msg.message,
|
|
2401
|
+
mode: msg.mode
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
return null;
|
|
2264
2405
|
},
|
|
2265
|
-
permissionPromptToolName: "mcp__permission__" + permissions.server.toolName,
|
|
2266
|
-
permissionMode: messageData.mode.permissionMode,
|
|
2267
|
-
model: messageData.mode.model,
|
|
2268
|
-
fallbackModel: messageData.mode.fallbackModel,
|
|
2269
|
-
customSystemPrompt: messageData.mode.customSystemPrompt,
|
|
2270
|
-
appendSystemPrompt: messageData.mode.appendSystemPrompt ? messageData.mode.appendSystemPrompt + "\n" + systemPrompt : systemPrompt,
|
|
2271
|
-
allowedTools: messageData.mode.allowedTools ? [...messageData.mode.allowedTools, ...session.allowedTools ? session.allowedTools : []] : session.allowedTools ? [...session.allowedTools] : void 0,
|
|
2272
|
-
disallowedTools: messageData.mode.disallowedTools,
|
|
2273
2406
|
onSessionFound: (sessionId) => {
|
|
2274
2407
|
sdkToLogConverter.updateSessionId(sessionId);
|
|
2275
2408
|
session.onSessionFound(sessionId);
|
|
2276
|
-
scanner.onNewSession(sessionId);
|
|
2277
2409
|
},
|
|
2278
2410
|
onThinkingChange: session.onThinkingChange,
|
|
2279
|
-
message: messageData.message,
|
|
2280
2411
|
claudeEnvVars: session.claudeEnvVars,
|
|
2281
2412
|
claudeArgs: session.claudeArgs,
|
|
2282
2413
|
onMessage,
|
|
@@ -2294,11 +2425,13 @@ async function claudeRemoteLauncher(session) {
|
|
|
2294
2425
|
session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
|
|
2295
2426
|
}
|
|
2296
2427
|
} catch (e) {
|
|
2428
|
+
logger.debug("[remote]: launch error", e);
|
|
2297
2429
|
if (!exitReason) {
|
|
2298
2430
|
session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
|
|
2299
2431
|
continue;
|
|
2300
2432
|
}
|
|
2301
2433
|
} finally {
|
|
2434
|
+
logger.debug("[remote]: launch finally");
|
|
2302
2435
|
for (let [toolCallId, { parentToolCallId }] of ongoingToolCalls) {
|
|
2303
2436
|
const converted = sdkToLogConverter.generateInterruptedToolResult(toolCallId, parentToolCallId);
|
|
2304
2437
|
if (converted) {
|
|
@@ -2311,11 +2444,13 @@ async function claudeRemoteLauncher(session) {
|
|
|
2311
2444
|
abortFuture?.resolve(void 0);
|
|
2312
2445
|
abortFuture = null;
|
|
2313
2446
|
logger.debug("[remote]: launch done");
|
|
2314
|
-
|
|
2447
|
+
permissionHandler.reset();
|
|
2448
|
+
modeHash = null;
|
|
2449
|
+
mode = null;
|
|
2315
2450
|
}
|
|
2316
2451
|
}
|
|
2317
2452
|
} finally {
|
|
2318
|
-
|
|
2453
|
+
permissionHandler.reset();
|
|
2319
2454
|
process.stdin.off("data", abort);
|
|
2320
2455
|
if (process.stdin.isTTY) {
|
|
2321
2456
|
process.stdin.setRawMode(false);
|
|
@@ -2327,7 +2462,6 @@ async function claudeRemoteLauncher(session) {
|
|
|
2327
2462
|
if (abortFuture) {
|
|
2328
2463
|
abortFuture.resolve(void 0);
|
|
2329
2464
|
}
|
|
2330
|
-
await scanner.cleanup();
|
|
2331
2465
|
}
|
|
2332
2466
|
return exitReason || "exit";
|
|
2333
2467
|
}
|
|
@@ -2379,7 +2513,7 @@ async function loop(opts) {
|
|
|
2379
2513
|
}
|
|
2380
2514
|
|
|
2381
2515
|
var name = "happy-coder";
|
|
2382
|
-
var version = "0.
|
|
2516
|
+
var version = "0.9.0-0";
|
|
2383
2517
|
var description = "Claude Code session sharing CLI";
|
|
2384
2518
|
var author = "Kirill Dubovitskiy";
|
|
2385
2519
|
var license = "MIT";
|
|
@@ -2429,18 +2563,14 @@ var scripts = {
|
|
|
2429
2563
|
test: "yarn build && vitest run",
|
|
2430
2564
|
"test:watch": "vitest",
|
|
2431
2565
|
"test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
|
|
2432
|
-
dev: "
|
|
2566
|
+
dev: "yarn build && DEBUG=1 npx tsx src/index.ts",
|
|
2433
2567
|
"dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
|
|
2434
2568
|
"dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
|
|
2435
2569
|
prepublishOnly: "yarn build && yarn test",
|
|
2436
|
-
|
|
2437
|
-
"patch:publish": "yarn build && npm version patch && npm publish",
|
|
2438
|
-
"version:prerelease": "yarn build && npm version prerelease --preid=beta",
|
|
2439
|
-
"publish:prerelease": "npm publish --tag beta",
|
|
2440
|
-
"beta:publish": "yarn version:prerelease && yarn publish:prerelease"
|
|
2570
|
+
release: "release-it"
|
|
2441
2571
|
};
|
|
2442
2572
|
var dependencies = {
|
|
2443
|
-
"@anthropic-ai/claude-code": "^1.0.
|
|
2573
|
+
"@anthropic-ai/claude-code": "^1.0.89",
|
|
2444
2574
|
"@anthropic-ai/sdk": "^0.56.0",
|
|
2445
2575
|
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
2446
2576
|
"@stablelib/base64": "^2.0.1",
|
|
@@ -2469,6 +2599,7 @@ var devDependencies = {
|
|
|
2469
2599
|
eslint: "^9",
|
|
2470
2600
|
"eslint-config-prettier": "^10",
|
|
2471
2601
|
pkgroll: "^2.14.2",
|
|
2602
|
+
"release-it": "^19.0.4",
|
|
2472
2603
|
shx: "^0.3.3",
|
|
2473
2604
|
"ts-node": "^10",
|
|
2474
2605
|
tsx: "^4.20.3",
|
|
@@ -2476,7 +2607,12 @@ var devDependencies = {
|
|
|
2476
2607
|
vitest: "^3.2.4"
|
|
2477
2608
|
};
|
|
2478
2609
|
var resolutions = {
|
|
2479
|
-
"whatwg-url": "14.2.0"
|
|
2610
|
+
"whatwg-url": "14.2.0",
|
|
2611
|
+
"parse-path": "7.0.3",
|
|
2612
|
+
"@types/parse-path": "7.0.3"
|
|
2613
|
+
};
|
|
2614
|
+
var publishConfig = {
|
|
2615
|
+
registry: "https://registry.npmjs.org"
|
|
2480
2616
|
};
|
|
2481
2617
|
var packageManager = "yarn@1.22.22";
|
|
2482
2618
|
var packageJson = {
|
|
@@ -2499,6 +2635,7 @@ var packageJson = {
|
|
|
2499
2635
|
dependencies: dependencies,
|
|
2500
2636
|
devDependencies: devDependencies,
|
|
2501
2637
|
resolutions: resolutions,
|
|
2638
|
+
publishConfig: publishConfig,
|
|
2502
2639
|
packageManager: packageManager
|
|
2503
2640
|
};
|
|
2504
2641
|
|
|
@@ -2885,15 +3022,17 @@ async function clearDaemonState() {
|
|
|
2885
3022
|
}
|
|
2886
3023
|
|
|
2887
3024
|
class MessageQueue2 {
|
|
2888
|
-
constructor(modeHasher) {
|
|
2889
|
-
this.modeHasher = modeHasher;
|
|
2890
|
-
logger.debug(`[MessageQueue2] Initialized`);
|
|
2891
|
-
}
|
|
2892
3025
|
queue = [];
|
|
2893
3026
|
// Made public for testing
|
|
2894
3027
|
waiter = null;
|
|
2895
3028
|
closed = false;
|
|
2896
3029
|
onMessageHandler = null;
|
|
3030
|
+
modeHasher;
|
|
3031
|
+
constructor(modeHasher, onMessageHandler = null) {
|
|
3032
|
+
this.modeHasher = modeHasher;
|
|
3033
|
+
this.onMessageHandler = onMessageHandler;
|
|
3034
|
+
logger.debug(`[MessageQueue2] Initialized`);
|
|
3035
|
+
}
|
|
2897
3036
|
/**
|
|
2898
3037
|
* Set a handler that will be called when a message arrives
|
|
2899
3038
|
*/
|
|
@@ -3068,6 +3207,7 @@ class MessageQueue2 {
|
|
|
3068
3207
|
const firstItem = this.queue[0];
|
|
3069
3208
|
const sameModeMessages = [];
|
|
3070
3209
|
let mode = firstItem.mode;
|
|
3210
|
+
let isolate = firstItem.isolate ?? false;
|
|
3071
3211
|
const targetModeHash = firstItem.modeHash;
|
|
3072
3212
|
if (firstItem.isolate) {
|
|
3073
3213
|
const item = this.queue.shift();
|
|
@@ -3083,7 +3223,9 @@ class MessageQueue2 {
|
|
|
3083
3223
|
const combinedMessage = sameModeMessages.join("\n");
|
|
3084
3224
|
return {
|
|
3085
3225
|
message: combinedMessage,
|
|
3086
|
-
mode
|
|
3226
|
+
mode,
|
|
3227
|
+
hash: targetModeHash,
|
|
3228
|
+
isolate
|
|
3087
3229
|
};
|
|
3088
3230
|
}
|
|
3089
3231
|
/**
|
|
@@ -3978,10 +4120,10 @@ async function doWebAuth(keypair) {
|
|
|
3978
4120
|
console.log("\u2713 Browser opened\n");
|
|
3979
4121
|
console.log("Complete authentication in your browser window.");
|
|
3980
4122
|
} else {
|
|
3981
|
-
console.log("Could not open browser automatically
|
|
3982
|
-
console.log("Please open this URL manually:");
|
|
3983
|
-
console.log(webUrl);
|
|
4123
|
+
console.log("Could not open browser automatically.");
|
|
3984
4124
|
}
|
|
4125
|
+
console.log("\nIf the browser did not open, please copy and paste this URL:");
|
|
4126
|
+
console.log(webUrl);
|
|
3985
4127
|
console.log("");
|
|
3986
4128
|
return await waitForAuthentication(keypair);
|
|
3987
4129
|
}
|
|
@@ -4492,7 +4634,15 @@ async function start(credentials, options = {}) {
|
|
|
4492
4634
|
if (caffeinateStarted) {
|
|
4493
4635
|
logger.infoDeveloper("Sleep prevention enabled (macOS)");
|
|
4494
4636
|
}
|
|
4495
|
-
const messageQueue = new MessageQueue2((mode) => hashObject(
|
|
4637
|
+
const messageQueue = new MessageQueue2((mode) => hashObject({
|
|
4638
|
+
isPlan: mode.permissionMode === "plan",
|
|
4639
|
+
model: mode.model,
|
|
4640
|
+
fallbackModel: mode.fallbackModel,
|
|
4641
|
+
customSystemPrompt: mode.customSystemPrompt,
|
|
4642
|
+
appendSystemPrompt: mode.appendSystemPrompt,
|
|
4643
|
+
allowedTools: mode.allowedTools,
|
|
4644
|
+
disallowedTools: mode.disallowedTools
|
|
4645
|
+
}));
|
|
4496
4646
|
registerHandlers(session);
|
|
4497
4647
|
let currentPermissionMode = options.permissionMode;
|
|
4498
4648
|
let currentModel = options.model;
|