happy-coder 0.8.0 → 0.9.0-1

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 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;
@@ -923,16 +923,19 @@ async function streamToStdin(stream, stdin, abort) {
923
923
  }
924
924
 
925
925
  class Query {
926
- constructor(childStdin, childStdout, processExitPromise) {
926
+ constructor(childStdin, childStdout, processExitPromise, canCallTool) {
927
927
  this.childStdin = childStdin;
928
928
  this.childStdout = childStdout;
929
929
  this.processExitPromise = processExitPromise;
930
+ this.canCallTool = canCallTool;
930
931
  this.readMessages();
931
932
  this.sdkMessages = this.readSdkMessages();
932
933
  }
933
934
  pendingControlResponses = /* @__PURE__ */ new Map();
935
+ cancelControllers = /* @__PURE__ */ new Map();
934
936
  sdkMessages;
935
937
  inputStream = new Stream();
938
+ canCallTool;
936
939
  /**
937
940
  * Set an error on the stream
938
941
  */
@@ -977,6 +980,12 @@ class Query {
977
980
  handler(controlResponse.response);
978
981
  }
979
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;
980
989
  }
981
990
  this.inputStream.enqueue(message);
982
991
  } catch (e) {
@@ -989,6 +998,7 @@ class Query {
989
998
  this.inputStream.error(error);
990
999
  } finally {
991
1000
  this.inputStream.done();
1001
+ this.cleanupControllers();
992
1002
  rl.close();
993
1003
  }
994
1004
  }
@@ -1032,6 +1042,77 @@ class Query {
1032
1042
  childStdin.write(JSON.stringify(sdkRequest) + "\n");
1033
1043
  });
1034
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
+ }
1035
1116
  }
1036
1117
  function query(config) {
1037
1118
  const {
@@ -1048,12 +1129,12 @@ function query(config) {
1048
1129
  mcpServers,
1049
1130
  pathToClaudeCodeExecutable = getDefaultClaudeCodePath(),
1050
1131
  permissionMode = "default",
1051
- permissionPromptToolName,
1052
1132
  continue: continueConversation,
1053
1133
  resume,
1054
1134
  model,
1055
1135
  fallbackModel,
1056
- strictMcpConfig
1136
+ strictMcpConfig,
1137
+ canCallTool
1057
1138
  } = {}
1058
1139
  } = config;
1059
1140
  if (!process.env.CLAUDE_CODE_ENTRYPOINT) {
@@ -1064,7 +1145,12 @@ function query(config) {
1064
1145
  if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
1065
1146
  if (maxTurns) args.push("--max-turns", maxTurns.toString());
1066
1147
  if (model) args.push("--model", model);
1067
- if (permissionPromptToolName) args.push("--permission-prompt-tool", permissionPromptToolName);
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
+ }
1068
1154
  if (continueConversation) args.push("--continue");
1069
1155
  if (resume) args.push("--resume", resume);
1070
1156
  if (allowedTools.length > 0) args.push("--allowedTools", allowedTools.join(","));
@@ -1128,7 +1214,7 @@ function query(config) {
1128
1214
  }
1129
1215
  });
1130
1216
  });
1131
- const query2 = new Query(childStdin, child.stdout, processExitPromise);
1217
+ const query2 = new Query(childStdin, child.stdout, processExitPromise, canCallTool);
1132
1218
  child.on("error", (error) => {
1133
1219
  if (config.options?.abort?.aborted) {
1134
1220
  query2.setError(new AbortError("Claude Code process aborted by user"));
@@ -1146,17 +1232,48 @@ function query(config) {
1146
1232
  return query2;
1147
1233
  }
1148
1234
 
1149
- async function awaitFileExist(file, timeout = 1e4) {
1150
- const startTime = Date.now();
1151
- while (Date.now() - startTime < timeout) {
1152
- try {
1153
- await promises.access(file);
1154
- return true;
1155
- } catch (e) {
1156
- await types$1.delay(1e3);
1157
- }
1235
+ function parseCompact(message) {
1236
+ const trimmed = message.trim();
1237
+ if (trimmed === "/compact") {
1238
+ return {
1239
+ isCompact: true,
1240
+ originalMessage: trimmed
1241
+ };
1158
1242
  }
1159
- return false;
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
+ };
1160
1277
  }
1161
1278
 
1162
1279
  class PushableAsyncIterable {
@@ -1285,48 +1402,17 @@ class PushableAsyncIterable {
1285
1402
  }
1286
1403
  }
1287
1404
 
1288
- function parseCompact(message) {
1289
- const trimmed = message.trim();
1290
- if (trimmed === "/compact") {
1291
- return {
1292
- isCompact: true,
1293
- originalMessage: trimmed
1294
- };
1295
- }
1296
- if (trimmed.startsWith("/compact ")) {
1297
- return {
1298
- isCompact: true,
1299
- originalMessage: trimmed
1300
- };
1301
- }
1302
- return {
1303
- isCompact: false,
1304
- originalMessage: message
1305
- };
1306
- }
1307
- function parseClear(message) {
1308
- const trimmed = message.trim();
1309
- return {
1310
- isClear: trimmed === "/clear"
1311
- };
1312
- }
1313
- function parseSpecialCommand(message) {
1314
- const compactResult = parseCompact(message);
1315
- if (compactResult.isCompact) {
1316
- return {
1317
- type: "compact",
1318
- originalMessage: compactResult.originalMessage
1319
- };
1320
- }
1321
- const clearResult = parseClear(message);
1322
- if (clearResult.isClear) {
1323
- return {
1324
- type: "clear"
1325
- };
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
+ }
1326
1414
  }
1327
- return {
1328
- type: null
1329
- };
1415
+ return false;
1330
1416
  }
1331
1417
 
1332
1418
  async function claudeRemote(opts) {
@@ -1339,32 +1425,12 @@ async function claudeRemote(opts) {
1339
1425
  process.env[key] = value;
1340
1426
  });
1341
1427
  }
1342
- let response;
1343
- const sdkOptions = {
1344
- cwd: opts.path,
1345
- resume: startFrom ?? void 0,
1346
- mcpServers: opts.mcpServers,
1347
- permissionPromptToolName: opts.permissionPromptToolName,
1348
- permissionMode: opts.permissionMode,
1349
- model: opts.model,
1350
- fallbackModel: opts.fallbackModel,
1351
- customSystemPrompt: opts.customSystemPrompt,
1352
- appendSystemPrompt: opts.appendSystemPrompt,
1353
- allowedTools: opts.allowedTools,
1354
- disallowedTools: opts.disallowedTools,
1355
- executable: "node",
1356
- abort: opts.signal,
1357
- pathToClaudeCodeExecutable: (() => {
1358
- return node_path.resolve(node_path.join(projectPath(), "scripts", "claude_remote_launcher.cjs"));
1359
- })()
1360
- };
1361
- if (opts.claudeArgs && opts.claudeArgs.length > 0) {
1362
- sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
1428
+ const initial = await opts.nextMessage();
1429
+ if (!initial) {
1430
+ return;
1363
1431
  }
1364
- types$1.logger.debug(`[claudeRemote] Starting query with permission mode: ${opts.permissionMode}, model: ${opts.model || "default"}, fallbackModel: ${opts.fallbackModel || "none"}, customSystemPrompt: ${opts.customSystemPrompt ? "set" : "none"}, appendSystemPrompt: ${opts.appendSystemPrompt ? "set" : "none"}, allowedTools: ${opts.allowedTools ? opts.allowedTools.join(",") : "none"}, disallowedTools: ${opts.disallowedTools ? opts.disallowedTools.join(",") : "none"}`);
1365
- const specialCommand = parseSpecialCommand(opts.message);
1432
+ const specialCommand = parseSpecialCommand(initial.message);
1366
1433
  if (specialCommand.type === "clear") {
1367
- types$1.logger.debug("[claudeRemote] /clear command detected - should not reach here, handled in start.ts");
1368
1434
  if (opts.onCompletionEvent) {
1369
1435
  opts.onCompletionEvent("Context was reset");
1370
1436
  }
@@ -1373,29 +1439,33 @@ async function claudeRemote(opts) {
1373
1439
  }
1374
1440
  return;
1375
1441
  }
1442
+ let isCompactCommand = false;
1376
1443
  if (specialCommand.type === "compact") {
1377
1444
  types$1.logger.debug("[claudeRemote] /compact command detected - will process as normal but with compaction behavior");
1378
- }
1379
- const isCompactCommand = specialCommand.type === "compact";
1380
- if (isCompactCommand) {
1381
- types$1.logger.debug("[claudeRemote] Compaction started");
1445
+ isCompactCommand = true;
1382
1446
  if (opts.onCompletionEvent) {
1383
1447
  opts.onCompletionEvent("Compaction started");
1384
1448
  }
1385
1449
  }
1386
- let message = new PushableAsyncIterable();
1387
- message.push({
1388
- type: "user",
1389
- message: {
1390
- role: "user",
1391
- content: opts.message
1392
- }
1393
- });
1394
- message.end();
1395
- response = query({
1396
- prompt: message,
1397
- options: sdkOptions
1398
- });
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
+ };
1399
1469
  let thinking = false;
1400
1470
  const updateThinking = (newThinking) => {
1401
1471
  if (thinking !== newThinking) {
@@ -1406,15 +1476,27 @@ async function claudeRemote(opts) {
1406
1476
  }
1407
1477
  }
1408
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
+ });
1409
1491
  updateThinking(true);
1410
1492
  try {
1411
1493
  types$1.logger.debug(`[claudeRemote] Starting to iterate over response`);
1412
- for await (const message2 of response) {
1413
- types$1.logger.debugLargeJson(`[claudeRemote] Message ${message2.type}`, message2);
1414
- opts.onMessage(message2);
1415
- if (message2.type === "system" && message2.subtype === "init") {
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") {
1416
1498
  updateThinking(true);
1417
- const systemInit = message2;
1499
+ const systemInit = message;
1418
1500
  if (systemInit.session_id) {
1419
1501
  types$1.logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${systemInit.session_id}`);
1420
1502
  const projectDir = getProjectPath(opts.path);
@@ -1423,7 +1505,7 @@ async function claudeRemote(opts) {
1423
1505
  opts.onSessionFound(systemInit.session_id);
1424
1506
  }
1425
1507
  }
1426
- if (message2.type === "result") {
1508
+ if (message.type === "result") {
1427
1509
  updateThinking(false);
1428
1510
  types$1.logger.debug("[claudeRemote] Result received, exiting claudeRemote");
1429
1511
  if (isCompactCommand) {
@@ -1431,26 +1513,28 @@ async function claudeRemote(opts) {
1431
1513
  if (opts.onCompletionEvent) {
1432
1514
  opts.onCompletionEvent("Compaction completed");
1433
1515
  }
1516
+ isCompactCommand = false;
1434
1517
  }
1435
- return;
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 } });
1436
1525
  }
1437
- if (message2.type === "user") {
1438
- const msg = message2;
1526
+ if (message.type === "user") {
1527
+ const msg = message;
1439
1528
  if (msg.message.role === "user" && Array.isArray(msg.message.content)) {
1440
1529
  for (let c of msg.message.content) {
1441
- if (c.type === "tool_result" && (c.name === "exit_plan_mode" || c.name === "ExitPlanMode")) {
1442
- types$1.logger.debug("[claudeRemote] Plan result received, exiting claudeRemote");
1443
- return;
1444
- }
1445
- if (c.type === "tool_result" && c.tool_use_id && opts.responses.has(c.tool_use_id) && !opts.responses.get(c.tool_use_id).approved) {
1446
- 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");
1447
1532
  return;
1448
1533
  }
1449
1534
  }
1450
1535
  }
1451
1536
  }
1452
1537
  }
1453
- types$1.logger.debug(`[claudeRemote] Finished iterating over response`);
1454
1538
  } catch (e) {
1455
1539
  if (e instanceof AbortError) {
1456
1540
  types$1.logger.debug(`[claudeRemote] Aborted`);
@@ -1460,71 +1544,11 @@ async function claudeRemote(opts) {
1460
1544
  } finally {
1461
1545
  updateThinking(false);
1462
1546
  }
1463
- types$1.logger.debug(`[claudeRemote] Function completed`);
1464
1547
  }
1465
1548
 
1466
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.`;
1467
1550
  const PLAN_FAKE_RESTART = `PlEaZe Continue with plan.`;
1468
1551
 
1469
- async function startPermissionServerV2(handler) {
1470
- const mcp = new mcp_js.McpServer({
1471
- name: "Permission Server",
1472
- version: "1.0.0",
1473
- description: "A server that allows you to request permissions from the user"
1474
- });
1475
- mcp.registerTool("ask_permission", {
1476
- description: "Request permission to execute a tool",
1477
- title: "Request Permission",
1478
- inputSchema: {
1479
- tool_name: z.z.string().describe("The tool that needs permission"),
1480
- input: z.z.any().describe("The arguments for the tool")
1481
- }
1482
- }, async (args) => {
1483
- const response = await handler({ name: args.tool_name, arguments: args.input });
1484
- types$1.logger.debugLargeJson("[permissionServerV2] Response", response);
1485
- 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.` };
1486
- return {
1487
- content: [
1488
- {
1489
- type: "text",
1490
- text: JSON.stringify(result)
1491
- }
1492
- ],
1493
- isError: false
1494
- };
1495
- });
1496
- const transport = new streamableHttp_js.StreamableHTTPServerTransport({
1497
- // NOTE: Returning session id here will result in claude
1498
- // sdk spawn to fail with `Invalid Request: Server already initialized`
1499
- sessionIdGenerator: void 0
1500
- });
1501
- await mcp.connect(transport);
1502
- const server = node_http.createServer(async (req, res) => {
1503
- try {
1504
- await transport.handleRequest(req, res);
1505
- } catch (error) {
1506
- types$1.logger.debug("Error handling request:", error);
1507
- if (!res.headersSent) {
1508
- res.writeHead(500).end();
1509
- }
1510
- }
1511
- });
1512
- const baseUrl = await new Promise((resolve) => {
1513
- server.listen(0, "127.0.0.1", () => {
1514
- const addr = server.address();
1515
- resolve(new URL(`http://127.0.0.1:${addr.port}`));
1516
- });
1517
- });
1518
- return {
1519
- url: baseUrl.toString(),
1520
- toolName: "ask_permission",
1521
- stop: () => {
1522
- mcp.close();
1523
- server.close();
1524
- }
1525
- };
1526
- }
1527
-
1528
1552
  function deepEqual(a, b) {
1529
1553
  if (a === b) return true;
1530
1554
  if (a == null || b == null) return false;
@@ -1539,133 +1563,218 @@ function deepEqual(a, b) {
1539
1563
  return true;
1540
1564
  }
1541
1565
 
1542
- async function startPermissionResolver(session) {
1543
- let toolCalls = [];
1544
- let responses = /* @__PURE__ */ new Map();
1545
- let requests = /* @__PURE__ */ new Map();
1546
- let pendingPermissionRequests = [];
1547
- const server = await startPermissionServerV2(async (request) => {
1548
- const id = resolveToolCallId(request.name, request.arguments);
1549
- if (!id) {
1550
- types$1.logger.debug(`Tool call ID not yet available for ${request.name}, queueing request`);
1551
- return new Promise((resolve, reject) => {
1552
- const timeout = setTimeout(() => {
1553
- const idx = pendingPermissionRequests.findIndex((p) => p.request === request);
1554
- if (idx !== -1) {
1555
- pendingPermissionRequests.splice(idx, 1);
1556
- reject(new Error(`Timeout: Tool call ID never arrived for ${request.name}`));
1557
- }
1558
- }, 3e4);
1559
- pendingPermissionRequests.push({ request, resolve, reject, timeout });
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);
1608
+ }
1609
+
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
+ allowedBashLiterals = /* @__PURE__ */ new Set();
1627
+ allowedBashPrefixes = /* @__PURE__ */ new Set();
1628
+ permissionMode = "default";
1629
+ constructor(session) {
1630
+ this.session = session;
1631
+ this.setupClientHandler();
1632
+ }
1633
+ handleModeChange(mode) {
1634
+ this.permissionMode = mode;
1635
+ }
1636
+ /**
1637
+ * Handler response
1638
+ */
1639
+ handlePermissionResponse(response, pending) {
1640
+ if (response.allowTools && response.allowTools.length > 0) {
1641
+ response.allowTools.forEach((tool) => {
1642
+ if (tool.startsWith("Bash(") || tool === "Bash") {
1643
+ this.parseBashPermission(tool);
1644
+ } else {
1645
+ this.allowedTools.add(tool);
1646
+ }
1560
1647
  });
1561
1648
  }
1562
- return handlePermissionRequest(id, request);
1563
- });
1564
- function handlePermissionRequest(id, request) {
1565
- let promise = new Promise((resolve) => {
1566
- if (request.name === "exit_plan_mode" || request.name === "ExitPlanMode") {
1567
- const wrappedResolve = (response) => {
1568
- if (response.approved) {
1569
- types$1.logger.debug("Plan approved - injecting PLAN_FAKE_RESTART");
1570
- if (response.mode && ["default", "acceptEdits", "bypassPermissions"].includes(response.mode)) {
1571
- session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: response.mode });
1572
- } else {
1573
- session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: "default" });
1574
- }
1575
- resolve({ approved: false, reason: PLAN_FAKE_REJECT });
1576
- } else {
1577
- resolve(response);
1578
- }
1579
- };
1580
- requests.set(id, wrappedResolve);
1649
+ if (response.mode) {
1650
+ this.permissionMode = response.mode;
1651
+ }
1652
+ if (pending.toolName === "exit_plan_mode" || pending.toolName === "ExitPlanMode") {
1653
+ types$1.logger.debug("Plan mode result received", response);
1654
+ if (response.approved) {
1655
+ types$1.logger.debug("Plan approved - injecting PLAN_FAKE_RESTART");
1656
+ if (response.mode && ["default", "acceptEdits", "bypassPermissions"].includes(response.mode)) {
1657
+ this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: response.mode });
1658
+ } else {
1659
+ this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: "default" });
1660
+ }
1661
+ pending.resolve({ behavior: "deny", message: PLAN_FAKE_REJECT });
1581
1662
  } else {
1582
- requests.set(id, resolve);
1663
+ pending.resolve({ behavior: "deny", message: response.reason || "Plan rejected" });
1583
1664
  }
1584
- });
1585
- let timeout = setTimeout(async () => {
1586
- types$1.logger.debug("Permission timeout - attempting to interrupt Claude");
1587
- requests.delete(id);
1588
- session.client.updateAgentState((currentState) => {
1589
- const request2 = currentState.requests?.[id];
1590
- if (!request2) return currentState;
1591
- let r = { ...currentState.requests };
1592
- delete r[id];
1593
- return {
1594
- ...currentState,
1595
- requests: r,
1596
- completedRequests: {
1597
- ...currentState.completedRequests,
1598
- [id]: {
1599
- ...request2,
1600
- completedAt: Date.now(),
1601
- status: "canceled",
1602
- reason: "Timeout"
1603
- }
1665
+ } else {
1666
+ 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.` };
1667
+ pending.resolve(result);
1668
+ }
1669
+ }
1670
+ /**
1671
+ * Creates the canCallTool callback for the SDK
1672
+ */
1673
+ handleToolCall = async (toolName, input, mode, options) => {
1674
+ if (toolName === "Bash") {
1675
+ const inputObj = input;
1676
+ if (inputObj?.command) {
1677
+ if (this.allowedBashLiterals.has(inputObj.command)) {
1678
+ return { behavior: "allow", updatedInput: input };
1679
+ }
1680
+ for (const prefix of this.allowedBashPrefixes) {
1681
+ if (inputObj.command.startsWith(prefix)) {
1682
+ return { behavior: "allow", updatedInput: input };
1604
1683
  }
1605
- };
1606
- });
1607
- }, 1e3 * 60 * 4.5);
1608
- types$1.logger.debug("Permission request" + id + " " + JSON.stringify(request));
1609
- session.api.push().sendToAllDevices(
1610
- "Permission Request",
1611
- `Claude wants to use ${request.name}`,
1612
- {
1613
- sessionId: session.client.sessionId,
1614
- requestId: id,
1615
- tool: request.name,
1616
- type: "permission_request"
1617
- }
1618
- );
1619
- session.client.updateAgentState((currentState) => ({
1620
- ...currentState,
1621
- requests: {
1622
- ...currentState.requests,
1623
- [id]: {
1624
- tool: request.name,
1625
- arguments: request.arguments,
1626
- createdAt: Date.now()
1627
1684
  }
1628
1685
  }
1629
- }));
1630
- promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
1631
- return promise;
1632
- }
1633
- session.client.setHandler("permission", async (message) => {
1634
- types$1.logger.debug("Permission response" + JSON.stringify(message));
1635
- const id = message.id;
1636
- const resolve = requests.get(id);
1637
- if (resolve) {
1638
- responses.set(id, message);
1639
- resolve({ approved: message.approved, reason: message.reason, mode: message.mode });
1640
- requests.delete(id);
1641
- } else {
1642
- types$1.logger.debug("Permission request stale, likely timed out");
1643
- return;
1686
+ } else if (this.allowedTools.has(toolName)) {
1687
+ return { behavior: "allow", updatedInput: input };
1644
1688
  }
1645
- session.client.updateAgentState((currentState) => {
1646
- const request = currentState.requests?.[id];
1647
- if (!request) return currentState;
1648
- let r = { ...currentState.requests };
1649
- delete r[id];
1650
- const isExitPlanModeSuccess = request.tool === "exit_plan_mode" && !message.approved && message.reason === PLAN_FAKE_REJECT;
1651
- return {
1689
+ const descriptor = getToolDescriptor(toolName);
1690
+ if (this.permissionMode === "bypassPermissions") {
1691
+ return { behavior: "allow", updatedInput: input };
1692
+ }
1693
+ if (this.permissionMode === "acceptEdits" && descriptor.edit) {
1694
+ return { behavior: "allow", updatedInput: input };
1695
+ }
1696
+ let toolCallId = this.resolveToolCallId(toolName, input);
1697
+ if (!toolCallId) {
1698
+ await types$1.delay(1e3);
1699
+ toolCallId = this.resolveToolCallId(toolName, input);
1700
+ if (!toolCallId) {
1701
+ throw new Error(`Could not resolve tool call ID for ${toolName}`);
1702
+ }
1703
+ }
1704
+ return this.handlePermissionRequest(toolCallId, toolName, input, options.signal);
1705
+ };
1706
+ /**
1707
+ * Handles individual permission requests
1708
+ */
1709
+ async handlePermissionRequest(id, toolName, input, signal) {
1710
+ return new Promise((resolve, reject) => {
1711
+ const abortHandler = () => {
1712
+ this.pendingRequests.delete(id);
1713
+ reject(new Error("Permission request aborted"));
1714
+ };
1715
+ signal.addEventListener("abort", abortHandler, { once: true });
1716
+ this.pendingRequests.set(id, {
1717
+ resolve: (result) => {
1718
+ signal.removeEventListener("abort", abortHandler);
1719
+ resolve(result);
1720
+ },
1721
+ reject: (error) => {
1722
+ signal.removeEventListener("abort", abortHandler);
1723
+ reject(error);
1724
+ },
1725
+ toolName,
1726
+ input
1727
+ });
1728
+ this.session.api.push().sendToAllDevices(
1729
+ "Permission Request",
1730
+ `Claude wants to ${getToolName(toolName)}`,
1731
+ {
1732
+ sessionId: this.session.client.sessionId,
1733
+ requestId: id,
1734
+ tool: toolName,
1735
+ type: "permission_request"
1736
+ }
1737
+ );
1738
+ this.session.client.updateAgentState((currentState) => ({
1652
1739
  ...currentState,
1653
- requests: r,
1654
- completedRequests: {
1655
- ...currentState.completedRequests,
1740
+ requests: {
1741
+ ...currentState.requests,
1656
1742
  [id]: {
1657
- ...request,
1658
- completedAt: Date.now(),
1659
- status: isExitPlanModeSuccess ? "approved" : message.approved ? "approved" : "denied",
1660
- reason: isExitPlanModeSuccess ? "Plan approved" : message.reason
1743
+ tool: toolName,
1744
+ arguments: input,
1745
+ createdAt: Date.now()
1661
1746
  }
1662
1747
  }
1663
- };
1748
+ }));
1749
+ types$1.logger.debug(`Permission request sent for tool call ${id}: ${toolName}`);
1664
1750
  });
1665
- });
1666
- const resolveToolCallId = (name, args) => {
1667
- for (let i = toolCalls.length - 1; i >= 0; i--) {
1668
- const call = toolCalls[i];
1751
+ }
1752
+ /**
1753
+ * Parses Bash permission strings into literal and prefix sets
1754
+ */
1755
+ parseBashPermission(permission) {
1756
+ if (permission === "Bash") {
1757
+ return;
1758
+ }
1759
+ const bashPattern = /^Bash\((.+?)\)$/;
1760
+ const match = permission.match(bashPattern);
1761
+ if (!match) {
1762
+ return;
1763
+ }
1764
+ const command = match[1];
1765
+ if (command.endsWith(":*")) {
1766
+ const prefix = command.slice(0, -2);
1767
+ this.allowedBashPrefixes.add(prefix);
1768
+ } else {
1769
+ this.allowedBashLiterals.add(command);
1770
+ }
1771
+ }
1772
+ /**
1773
+ * Resolves tool call ID based on tool name and input
1774
+ */
1775
+ resolveToolCallId(name, args) {
1776
+ for (let i = this.toolCalls.length - 1; i >= 0; i--) {
1777
+ const call = this.toolCalls[i];
1669
1778
  if (call.name === name && deepEqual(call.input, args)) {
1670
1779
  if (call.used) {
1671
1780
  return null;
@@ -1675,59 +1784,22 @@ async function startPermissionResolver(session) {
1675
1784
  }
1676
1785
  }
1677
1786
  return null;
1678
- };
1679
- function reset() {
1680
- toolCalls = [];
1681
- requests.clear();
1682
- responses.clear();
1683
- for (const pending of pendingPermissionRequests) {
1684
- clearTimeout(pending.timeout);
1685
- }
1686
- pendingPermissionRequests = [];
1687
- session.client.updateAgentState((currentState) => {
1688
- const pendingRequests = currentState.requests || {};
1689
- const completedRequests = { ...currentState.completedRequests };
1690
- for (const [id, request] of Object.entries(pendingRequests)) {
1691
- completedRequests[id] = {
1692
- ...request,
1693
- completedAt: Date.now(),
1694
- status: "canceled",
1695
- reason: "Session switched to local mode"
1696
- };
1697
- }
1698
- return {
1699
- ...currentState,
1700
- requests: {},
1701
- // Clear all pending requests
1702
- completedRequests
1703
- };
1704
- });
1705
1787
  }
1706
- function onMessage(message) {
1788
+ /**
1789
+ * Handles messages to track tool calls
1790
+ */
1791
+ onMessage(message) {
1707
1792
  if (message.type === "assistant") {
1708
1793
  const assistantMsg = message;
1709
1794
  if (assistantMsg.message && assistantMsg.message.content) {
1710
1795
  for (const block of assistantMsg.message.content) {
1711
1796
  if (block.type === "tool_use") {
1712
- toolCalls.push({
1797
+ this.toolCalls.push({
1713
1798
  id: block.id,
1714
1799
  name: block.name,
1715
1800
  input: block.input,
1716
1801
  used: false
1717
1802
  });
1718
- for (let i = pendingPermissionRequests.length - 1; i >= 0; i--) {
1719
- const pending = pendingPermissionRequests[i];
1720
- if (pending.request.name === block.name && deepEqual(pending.request.arguments, block.input)) {
1721
- types$1.logger.debug(`Resolving pending permission request for ${block.name} with ID ${block.id}`);
1722
- clearTimeout(pending.timeout);
1723
- pendingPermissionRequests.splice(i, 1);
1724
- handlePermissionRequest(block.id, pending.request).then(
1725
- pending.resolve,
1726
- pending.reject
1727
- );
1728
- break;
1729
- }
1730
- }
1731
1803
  }
1732
1804
  }
1733
1805
  }
@@ -1737,7 +1809,7 @@ async function startPermissionResolver(session) {
1737
1809
  if (userMsg.message && userMsg.message.content && Array.isArray(userMsg.message.content)) {
1738
1810
  for (const block of userMsg.message.content) {
1739
1811
  if (block.type === "tool_result" && block.tool_use_id) {
1740
- const toolCall = toolCalls.find((tc) => tc.id === block.tool_use_id);
1812
+ const toolCall = this.toolCalls.find((tc) => tc.id === block.tool_use_id);
1741
1813
  if (toolCall && !toolCall.used) {
1742
1814
  toolCall.used = true;
1743
1815
  }
@@ -1746,12 +1818,95 @@ async function startPermissionResolver(session) {
1746
1818
  }
1747
1819
  }
1748
1820
  }
1749
- return {
1750
- server,
1751
- reset,
1752
- onMessage,
1753
- responses
1754
- };
1821
+ /**
1822
+ * Checks if a tool call is rejected
1823
+ */
1824
+ isAborted(toolCallId) {
1825
+ if (this.responses.get(toolCallId)?.approved === false) {
1826
+ return true;
1827
+ }
1828
+ const toolCall = this.toolCalls.find((tc) => tc.id === toolCallId);
1829
+ if (toolCall && (toolCall.name === "exit_plan_mode" || toolCall.name === "ExitPlanMode")) {
1830
+ return true;
1831
+ }
1832
+ return false;
1833
+ }
1834
+ /**
1835
+ * Resets all state for new sessions
1836
+ */
1837
+ reset() {
1838
+ this.toolCalls = [];
1839
+ this.responses.clear();
1840
+ this.allowedTools.clear();
1841
+ this.allowedBashLiterals.clear();
1842
+ this.allowedBashPrefixes.clear();
1843
+ for (const [, pending] of this.pendingRequests.entries()) {
1844
+ pending.reject(new Error("Session reset"));
1845
+ }
1846
+ this.pendingRequests.clear();
1847
+ this.session.client.updateAgentState((currentState) => {
1848
+ const pendingRequests = currentState.requests || {};
1849
+ const completedRequests = { ...currentState.completedRequests };
1850
+ for (const [id, request] of Object.entries(pendingRequests)) {
1851
+ completedRequests[id] = {
1852
+ ...request,
1853
+ completedAt: Date.now(),
1854
+ status: "canceled",
1855
+ reason: "Session switched to local mode"
1856
+ };
1857
+ }
1858
+ return {
1859
+ ...currentState,
1860
+ requests: {},
1861
+ // Clear all pending requests
1862
+ completedRequests
1863
+ };
1864
+ });
1865
+ }
1866
+ /**
1867
+ * Sets up the client handler for permission responses
1868
+ */
1869
+ setupClientHandler() {
1870
+ this.session.client.setHandler("permission", async (message) => {
1871
+ types$1.logger.debug(`Permission response: ${JSON.stringify(message)}`);
1872
+ const id = message.id;
1873
+ const pending = this.pendingRequests.get(id);
1874
+ if (!pending) {
1875
+ types$1.logger.debug("Permission request not found or already resolved");
1876
+ return;
1877
+ }
1878
+ this.responses.set(id, { ...message, receivedAt: Date.now() });
1879
+ this.pendingRequests.delete(id);
1880
+ this.handlePermissionResponse(message, pending);
1881
+ this.session.client.updateAgentState((currentState) => {
1882
+ const request = currentState.requests?.[id];
1883
+ if (!request) return currentState;
1884
+ let r = { ...currentState.requests };
1885
+ delete r[id];
1886
+ return {
1887
+ ...currentState,
1888
+ requests: r,
1889
+ completedRequests: {
1890
+ ...currentState.completedRequests,
1891
+ [id]: {
1892
+ ...request,
1893
+ completedAt: Date.now(),
1894
+ status: message.approved ? "approved" : "denied",
1895
+ reason: message.reason,
1896
+ mode: message.mode,
1897
+ allowTools: message.allowTools
1898
+ }
1899
+ }
1900
+ };
1901
+ });
1902
+ });
1903
+ }
1904
+ /**
1905
+ * Gets the responses map (for compatibility with existing code)
1906
+ */
1907
+ getResponses() {
1908
+ return this.responses;
1909
+ }
1755
1910
  }
1756
1911
 
1757
1912
  function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
@@ -2125,12 +2280,6 @@ async function claudeRemoteLauncher(session) {
2125
2280
  }
2126
2281
  process.stdin.setEncoding("utf8");
2127
2282
  }
2128
- const scanner = await createSessionScanner({
2129
- sessionId: session.sessionId,
2130
- workingDirectory: session.path,
2131
- onMessage: (message) => {
2132
- }
2133
- });
2134
2283
  let exitReason = null;
2135
2284
  let abortController = null;
2136
2285
  let abortFuture = null;
@@ -2153,17 +2302,17 @@ async function claudeRemoteLauncher(session) {
2153
2302
  }
2154
2303
  session.client.setHandler("abort", doAbort);
2155
2304
  session.client.setHandler("switch", doSwitch);
2156
- const permissions = await startPermissionResolver(session);
2305
+ const permissionHandler = new PermissionHandler(session);
2157
2306
  const sdkToLogConverter = new SDKToLogConverter({
2158
2307
  sessionId: session.sessionId || "unknown",
2159
2308
  cwd: session.path,
2160
2309
  version: process.env.npm_package_version
2161
- }, permissions.responses);
2310
+ }, permissionHandler.getResponses());
2162
2311
  let planModeToolCalls = /* @__PURE__ */ new Set();
2163
2312
  let ongoingToolCalls = /* @__PURE__ */ new Map();
2164
2313
  function onMessage(message) {
2165
2314
  formatClaudeMessageForInk(message, messageBuffer);
2166
- permissions.onMessage(message);
2315
+ permissionHandler.onMessage(message);
2167
2316
  if (message.type === "assistant") {
2168
2317
  let umessage = message;
2169
2318
  if (umessage.message.content && Array.isArray(umessage.message.content)) {
@@ -2227,6 +2376,32 @@ async function claudeRemoteLauncher(session) {
2227
2376
  }
2228
2377
  const logMessage = sdkToLogConverter.convert(msg);
2229
2378
  if (logMessage) {
2379
+ if (logMessage.type === "user" && logMessage.message?.content) {
2380
+ const content = Array.isArray(logMessage.message.content) ? logMessage.message.content : [];
2381
+ for (let i = 0; i < content.length; i++) {
2382
+ const c = content[i];
2383
+ if (c.type === "tool_result" && c.tool_use_id) {
2384
+ const responses = permissionHandler.getResponses();
2385
+ const response = responses.get(c.tool_use_id);
2386
+ if (response) {
2387
+ const permissions = {
2388
+ date: response.receivedAt || Date.now(),
2389
+ result: response.approved ? "approved" : "denied"
2390
+ };
2391
+ if (response.mode) {
2392
+ permissions.mode = response.mode;
2393
+ }
2394
+ if (response.allowTools && response.allowTools.length > 0) {
2395
+ permissions.allowedTools = response.allowTools;
2396
+ }
2397
+ content[i] = {
2398
+ ...c,
2399
+ permissions
2400
+ };
2401
+ }
2402
+ }
2403
+ }
2404
+ }
2230
2405
  if (logMessage.type !== "system") {
2231
2406
  session.client.sendClaudeSessionMessage(logMessage);
2232
2407
  }
@@ -2246,58 +2421,57 @@ async function claudeRemoteLauncher(session) {
2246
2421
  }
2247
2422
  }
2248
2423
  try {
2424
+ let pending = null;
2249
2425
  while (!exitReason) {
2250
- types$1.logger.debug("[remote]: fetch next message");
2251
- abortController = new AbortController();
2252
- abortFuture = new Future();
2253
- const messageData = await session.queue.waitForMessagesAndGetAsString(abortController.signal);
2254
- if (!messageData || abortController.signal.aborted) {
2255
- types$1.logger.debug("[remote]: fetch next message done: no message or aborted");
2256
- abortFuture?.resolve(void 0);
2257
- if (exitReason) {
2258
- return exitReason;
2259
- } else {
2260
- continue;
2261
- }
2262
- }
2263
- types$1.logger.debug("[remote]: fetch next message done: message received");
2264
- abortFuture?.resolve(void 0);
2265
- abortFuture = null;
2266
- abortController = null;
2267
2426
  types$1.logger.debug("[remote]: launch");
2268
2427
  messageBuffer.addMessage("\u2550".repeat(40), "status");
2269
2428
  messageBuffer.addMessage("Starting new Claude session...", "status");
2270
- abortController = new AbortController();
2429
+ const controller = new AbortController();
2430
+ abortController = controller;
2271
2431
  abortFuture = new Future();
2272
- permissions.reset();
2432
+ permissionHandler.reset();
2273
2433
  sdkToLogConverter.resetParentChain();
2434
+ let modeHash = null;
2435
+ let mode = null;
2274
2436
  try {
2275
2437
  await claudeRemote({
2276
2438
  sessionId: session.sessionId,
2277
2439
  path: session.path,
2278
- responses: permissions.responses,
2279
- mcpServers: {
2280
- ...session.mcpServers,
2281
- permission: {
2282
- type: "http",
2283
- url: permissions.server.url
2440
+ allowedTools: session.allowedTools ?? [],
2441
+ mcpServers: session.mcpServers,
2442
+ canCallTool: permissionHandler.handleToolCall,
2443
+ isAborted: (toolCallId) => {
2444
+ return permissionHandler.isAborted(toolCallId);
2445
+ },
2446
+ nextMessage: async () => {
2447
+ if (pending) {
2448
+ let p = pending;
2449
+ pending = null;
2450
+ permissionHandler.handleModeChange(p.mode.permissionMode);
2451
+ return p;
2284
2452
  }
2453
+ let msg = await session.queue.waitForMessagesAndGetAsString(controller.signal);
2454
+ if (msg) {
2455
+ if (modeHash && msg.hash !== modeHash || msg.isolate) {
2456
+ types$1.logger.debug("[remote]: mode has changed, pending message");
2457
+ pending = msg;
2458
+ return null;
2459
+ }
2460
+ modeHash = msg.hash;
2461
+ mode = msg.mode;
2462
+ permissionHandler.handleModeChange(mode.permissionMode);
2463
+ return {
2464
+ message: msg.message,
2465
+ mode: msg.mode
2466
+ };
2467
+ }
2468
+ return null;
2285
2469
  },
2286
- permissionPromptToolName: "mcp__permission__" + permissions.server.toolName,
2287
- permissionMode: messageData.mode.permissionMode,
2288
- model: messageData.mode.model,
2289
- fallbackModel: messageData.mode.fallbackModel,
2290
- customSystemPrompt: messageData.mode.customSystemPrompt,
2291
- appendSystemPrompt: messageData.mode.appendSystemPrompt ? messageData.mode.appendSystemPrompt + "\n" + systemPrompt : systemPrompt,
2292
- allowedTools: messageData.mode.allowedTools ? [...messageData.mode.allowedTools, ...session.allowedTools ? session.allowedTools : []] : session.allowedTools ? [...session.allowedTools] : void 0,
2293
- disallowedTools: messageData.mode.disallowedTools,
2294
2470
  onSessionFound: (sessionId) => {
2295
2471
  sdkToLogConverter.updateSessionId(sessionId);
2296
2472
  session.onSessionFound(sessionId);
2297
- scanner.onNewSession(sessionId);
2298
2473
  },
2299
2474
  onThinkingChange: session.onThinkingChange,
2300
- message: messageData.message,
2301
2475
  claudeEnvVars: session.claudeEnvVars,
2302
2476
  claudeArgs: session.claudeArgs,
2303
2477
  onMessage,
@@ -2315,11 +2489,13 @@ async function claudeRemoteLauncher(session) {
2315
2489
  session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
2316
2490
  }
2317
2491
  } catch (e) {
2492
+ types$1.logger.debug("[remote]: launch error", e);
2318
2493
  if (!exitReason) {
2319
2494
  session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
2320
2495
  continue;
2321
2496
  }
2322
2497
  } finally {
2498
+ types$1.logger.debug("[remote]: launch finally");
2323
2499
  for (let [toolCallId, { parentToolCallId }] of ongoingToolCalls) {
2324
2500
  const converted = sdkToLogConverter.generateInterruptedToolResult(toolCallId, parentToolCallId);
2325
2501
  if (converted) {
@@ -2332,11 +2508,13 @@ async function claudeRemoteLauncher(session) {
2332
2508
  abortFuture?.resolve(void 0);
2333
2509
  abortFuture = null;
2334
2510
  types$1.logger.debug("[remote]: launch done");
2335
- permissions.reset();
2511
+ permissionHandler.reset();
2512
+ modeHash = null;
2513
+ mode = null;
2336
2514
  }
2337
2515
  }
2338
2516
  } finally {
2339
- permissions.server.stop();
2517
+ permissionHandler.reset();
2340
2518
  process.stdin.off("data", abort);
2341
2519
  if (process.stdin.isTTY) {
2342
2520
  process.stdin.setRawMode(false);
@@ -2348,7 +2526,6 @@ async function claudeRemoteLauncher(session) {
2348
2526
  if (abortFuture) {
2349
2527
  abortFuture.resolve(void 0);
2350
2528
  }
2351
- await scanner.cleanup();
2352
2529
  }
2353
2530
  return exitReason || "exit";
2354
2531
  }
@@ -2400,7 +2577,7 @@ async function loop(opts) {
2400
2577
  }
2401
2578
 
2402
2579
  var name = "happy-coder";
2403
- var version = "0.8.0";
2580
+ var version = "0.9.0-1";
2404
2581
  var description = "Claude Code session sharing CLI";
2405
2582
  var author = "Kirill Dubovitskiy";
2406
2583
  var license = "MIT";
@@ -2450,18 +2627,14 @@ var scripts = {
2450
2627
  test: "yarn build && vitest run",
2451
2628
  "test:watch": "vitest",
2452
2629
  "test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
2453
- dev: "DEBUG=1 yarn build && npx tsx src/index.ts",
2630
+ dev: "yarn build && DEBUG=1 npx tsx src/index.ts",
2454
2631
  "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
2455
2632
  "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
2456
2633
  prepublishOnly: "yarn build && yarn test",
2457
- "minor:publish": "yarn build && npm version minor && npm publish",
2458
- "patch:publish": "yarn build && npm version patch && npm publish",
2459
- "version:prerelease": "yarn build && npm version prerelease --preid=beta",
2460
- "publish:prerelease": "npm publish --tag beta",
2461
- "beta:publish": "yarn version:prerelease && yarn publish:prerelease"
2634
+ release: "release-it"
2462
2635
  };
2463
2636
  var dependencies = {
2464
- "@anthropic-ai/claude-code": "^1.0.73",
2637
+ "@anthropic-ai/claude-code": "^1.0.89",
2465
2638
  "@anthropic-ai/sdk": "^0.56.0",
2466
2639
  "@modelcontextprotocol/sdk": "^1.15.1",
2467
2640
  "@stablelib/base64": "^2.0.1",
@@ -2490,6 +2663,7 @@ var devDependencies = {
2490
2663
  eslint: "^9",
2491
2664
  "eslint-config-prettier": "^10",
2492
2665
  pkgroll: "^2.14.2",
2666
+ "release-it": "^19.0.4",
2493
2667
  shx: "^0.3.3",
2494
2668
  "ts-node": "^10",
2495
2669
  tsx: "^4.20.3",
@@ -2497,7 +2671,12 @@ var devDependencies = {
2497
2671
  vitest: "^3.2.4"
2498
2672
  };
2499
2673
  var resolutions = {
2500
- "whatwg-url": "14.2.0"
2674
+ "whatwg-url": "14.2.0",
2675
+ "parse-path": "7.0.3",
2676
+ "@types/parse-path": "7.0.3"
2677
+ };
2678
+ var publishConfig = {
2679
+ registry: "https://registry.npmjs.org"
2501
2680
  };
2502
2681
  var packageManager = "yarn@1.22.22";
2503
2682
  var packageJson = {
@@ -2520,6 +2699,7 @@ var packageJson = {
2520
2699
  dependencies: dependencies,
2521
2700
  devDependencies: devDependencies,
2522
2701
  resolutions: resolutions,
2702
+ publishConfig: publishConfig,
2523
2703
  packageManager: packageManager
2524
2704
  };
2525
2705
 
@@ -2906,15 +3086,17 @@ async function clearDaemonState() {
2906
3086
  }
2907
3087
 
2908
3088
  class MessageQueue2 {
2909
- constructor(modeHasher) {
2910
- this.modeHasher = modeHasher;
2911
- types$1.logger.debug(`[MessageQueue2] Initialized`);
2912
- }
2913
3089
  queue = [];
2914
3090
  // Made public for testing
2915
3091
  waiter = null;
2916
3092
  closed = false;
2917
3093
  onMessageHandler = null;
3094
+ modeHasher;
3095
+ constructor(modeHasher, onMessageHandler = null) {
3096
+ this.modeHasher = modeHasher;
3097
+ this.onMessageHandler = onMessageHandler;
3098
+ types$1.logger.debug(`[MessageQueue2] Initialized`);
3099
+ }
2918
3100
  /**
2919
3101
  * Set a handler that will be called when a message arrives
2920
3102
  */
@@ -3089,6 +3271,7 @@ class MessageQueue2 {
3089
3271
  const firstItem = this.queue[0];
3090
3272
  const sameModeMessages = [];
3091
3273
  let mode = firstItem.mode;
3274
+ let isolate = firstItem.isolate ?? false;
3092
3275
  const targetModeHash = firstItem.modeHash;
3093
3276
  if (firstItem.isolate) {
3094
3277
  const item = this.queue.shift();
@@ -3104,7 +3287,9 @@ class MessageQueue2 {
3104
3287
  const combinedMessage = sameModeMessages.join("\n");
3105
3288
  return {
3106
3289
  message: combinedMessage,
3107
- mode
3290
+ mode,
3291
+ hash: targetModeHash,
3292
+ isolate
3108
3293
  };
3109
3294
  }
3110
3295
  /**
@@ -3999,10 +4184,10 @@ async function doWebAuth(keypair) {
3999
4184
  console.log("\u2713 Browser opened\n");
4000
4185
  console.log("Complete authentication in your browser window.");
4001
4186
  } else {
4002
- console.log("Could not open browser automatically.\n");
4003
- console.log("Please open this URL manually:");
4004
- console.log(webUrl);
4187
+ console.log("Could not open browser automatically.");
4005
4188
  }
4189
+ console.log("\nIf the browser did not open, please copy and paste this URL:");
4190
+ console.log(webUrl);
4006
4191
  console.log("");
4007
4192
  return await waitForAuthentication(keypair);
4008
4193
  }
@@ -4513,7 +4698,15 @@ async function start(credentials, options = {}) {
4513
4698
  if (caffeinateStarted) {
4514
4699
  types$1.logger.infoDeveloper("Sleep prevention enabled (macOS)");
4515
4700
  }
4516
- const messageQueue = new MessageQueue2((mode) => hashObject(mode));
4701
+ const messageQueue = new MessageQueue2((mode) => hashObject({
4702
+ isPlan: mode.permissionMode === "plan",
4703
+ model: mode.model,
4704
+ fallbackModel: mode.fallbackModel,
4705
+ customSystemPrompt: mode.customSystemPrompt,
4706
+ appendSystemPrompt: mode.appendSystemPrompt,
4707
+ allowedTools: mode.allowedTools,
4708
+ disallowedTools: mode.disallowedTools
4709
+ }));
4517
4710
  registerHandlers(session);
4518
4711
  let currentPermissionMode = options.permissionMode;
4519
4712
  let currentModel = options.model;