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 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,178 @@ 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 });
1560
- });
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}`;
1561
1605
  }
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);
1581
- } else {
1582
- requests.set(id, resolve);
1583
- }
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
- }
1604
- }
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()
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
+ permissionMode = "default";
1627
+ constructor(session) {
1628
+ this.session = session;
1629
+ this.setupClientHandler();
1630
+ }
1631
+ handleModeChange(mode) {
1632
+ this.permissionMode = mode;
1633
+ }
1634
+ /**
1635
+ * Handler response
1636
+ */
1637
+ handlePermissionResponse(response, pending) {
1638
+ if (response.allowTools && response.allowTools.length > 0) {
1639
+ response.allowTools.forEach((tool) => this.allowedTools.add(tool));
1640
+ }
1641
+ if (response.mode) {
1642
+ this.permissionMode = response.mode;
1643
+ }
1644
+ if (pending.toolName === "exit_plan_mode" || pending.toolName === "ExitPlanMode") {
1645
+ types$1.logger.debug("Plan mode result received", response);
1646
+ if (response.approved) {
1647
+ types$1.logger.debug("Plan approved - injecting PLAN_FAKE_RESTART");
1648
+ if (response.mode && ["default", "acceptEdits", "bypassPermissions"].includes(response.mode)) {
1649
+ this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: response.mode });
1650
+ } else {
1651
+ this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: "default" });
1627
1652
  }
1653
+ pending.resolve({ behavior: "deny", message: PLAN_FAKE_REJECT });
1654
+ } else {
1655
+ pending.resolve({ behavior: "deny", message: response.reason || "Plan rejected" });
1628
1656
  }
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
1657
  } else {
1642
- types$1.logger.debug("Permission request stale, likely timed out");
1643
- return;
1658
+ const result = response.approved ? { behavior: "allow", updatedInput: pending.input || {} } : { behavior: "deny", message: response.reason || `The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.` };
1659
+ pending.resolve(result);
1644
1660
  }
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 {
1661
+ }
1662
+ /**
1663
+ * Creates the canCallTool callback for the SDK
1664
+ */
1665
+ handleToolCall = async (toolName, input, mode, options) => {
1666
+ if (this.allowedTools.has(toolName)) {
1667
+ return { behavior: "allow", updatedInput: input };
1668
+ }
1669
+ const descriptor = getToolDescriptor(toolName);
1670
+ if (this.permissionMode === "bypassPermissions") {
1671
+ return { behavior: "allow", updatedInput: input };
1672
+ }
1673
+ if (this.permissionMode === "acceptEdits" && descriptor.edit) {
1674
+ return { behavior: "allow", updatedInput: input };
1675
+ }
1676
+ let toolCallId = this.resolveToolCallId(toolName, input);
1677
+ if (!toolCallId) {
1678
+ await types$1.delay(1e3);
1679
+ toolCallId = this.resolveToolCallId(toolName, input);
1680
+ if (!toolCallId) {
1681
+ throw new Error(`Could not resolve tool call ID for ${toolName}`);
1682
+ }
1683
+ }
1684
+ return this.handlePermissionRequest(toolCallId, toolName, input, options.signal);
1685
+ };
1686
+ /**
1687
+ * Handles individual permission requests
1688
+ */
1689
+ async handlePermissionRequest(id, toolName, input, signal) {
1690
+ return new Promise((resolve, reject) => {
1691
+ const abortHandler = () => {
1692
+ this.pendingRequests.delete(id);
1693
+ reject(new Error("Permission request aborted"));
1694
+ };
1695
+ signal.addEventListener("abort", abortHandler, { once: true });
1696
+ this.pendingRequests.set(id, {
1697
+ resolve: (result) => {
1698
+ signal.removeEventListener("abort", abortHandler);
1699
+ resolve(result);
1700
+ },
1701
+ reject: (error) => {
1702
+ signal.removeEventListener("abort", abortHandler);
1703
+ reject(error);
1704
+ },
1705
+ toolName,
1706
+ input
1707
+ });
1708
+ this.session.api.push().sendToAllDevices(
1709
+ "Permission Request",
1710
+ `Claude wants to ${getToolName(toolName)}`,
1711
+ {
1712
+ sessionId: this.session.client.sessionId,
1713
+ requestId: id,
1714
+ tool: toolName,
1715
+ type: "permission_request"
1716
+ }
1717
+ );
1718
+ this.session.client.updateAgentState((currentState) => ({
1652
1719
  ...currentState,
1653
- requests: r,
1654
- completedRequests: {
1655
- ...currentState.completedRequests,
1720
+ requests: {
1721
+ ...currentState.requests,
1656
1722
  [id]: {
1657
- ...request,
1658
- completedAt: Date.now(),
1659
- status: isExitPlanModeSuccess ? "approved" : message.approved ? "approved" : "denied",
1660
- reason: isExitPlanModeSuccess ? "Plan approved" : message.reason
1723
+ tool: toolName,
1724
+ arguments: input,
1725
+ createdAt: Date.now()
1661
1726
  }
1662
1727
  }
1663
- };
1728
+ }));
1729
+ types$1.logger.debug(`Permission request sent for tool call ${id}: ${toolName}`);
1664
1730
  });
1665
- });
1666
- const resolveToolCallId = (name, args) => {
1667
- for (let i = toolCalls.length - 1; i >= 0; i--) {
1668
- const call = toolCalls[i];
1731
+ }
1732
+ /**
1733
+ * Resolves tool call ID based on tool name and input
1734
+ */
1735
+ resolveToolCallId(name, args) {
1736
+ for (let i = this.toolCalls.length - 1; i >= 0; i--) {
1737
+ const call = this.toolCalls[i];
1669
1738
  if (call.name === name && deepEqual(call.input, args)) {
1670
1739
  if (call.used) {
1671
1740
  return null;
@@ -1675,59 +1744,22 @@ async function startPermissionResolver(session) {
1675
1744
  }
1676
1745
  }
1677
1746
  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
1747
  }
1706
- function onMessage(message) {
1748
+ /**
1749
+ * Handles messages to track tool calls
1750
+ */
1751
+ onMessage(message) {
1707
1752
  if (message.type === "assistant") {
1708
1753
  const assistantMsg = message;
1709
1754
  if (assistantMsg.message && assistantMsg.message.content) {
1710
1755
  for (const block of assistantMsg.message.content) {
1711
1756
  if (block.type === "tool_use") {
1712
- toolCalls.push({
1757
+ this.toolCalls.push({
1713
1758
  id: block.id,
1714
1759
  name: block.name,
1715
1760
  input: block.input,
1716
1761
  used: false
1717
1762
  });
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
1763
  }
1732
1764
  }
1733
1765
  }
@@ -1737,7 +1769,7 @@ async function startPermissionResolver(session) {
1737
1769
  if (userMsg.message && userMsg.message.content && Array.isArray(userMsg.message.content)) {
1738
1770
  for (const block of userMsg.message.content) {
1739
1771
  if (block.type === "tool_result" && block.tool_use_id) {
1740
- const toolCall = toolCalls.find((tc) => tc.id === block.tool_use_id);
1772
+ const toolCall = this.toolCalls.find((tc) => tc.id === block.tool_use_id);
1741
1773
  if (toolCall && !toolCall.used) {
1742
1774
  toolCall.used = true;
1743
1775
  }
@@ -1746,12 +1778,92 @@ async function startPermissionResolver(session) {
1746
1778
  }
1747
1779
  }
1748
1780
  }
1749
- return {
1750
- server,
1751
- reset,
1752
- onMessage,
1753
- responses
1754
- };
1781
+ /**
1782
+ * Checks if a tool call is rejected
1783
+ */
1784
+ isAborted(toolCallId) {
1785
+ if (this.responses.get(toolCallId)?.approved === false) {
1786
+ return true;
1787
+ }
1788
+ const toolCall = this.toolCalls.find((tc) => tc.id === toolCallId);
1789
+ if (toolCall && (toolCall.name === "exit_plan_mode" || toolCall.name === "ExitPlanMode")) {
1790
+ return true;
1791
+ }
1792
+ return false;
1793
+ }
1794
+ /**
1795
+ * Resets all state for new sessions
1796
+ */
1797
+ reset() {
1798
+ this.toolCalls = [];
1799
+ this.responses.clear();
1800
+ for (const [, pending] of this.pendingRequests.entries()) {
1801
+ pending.reject(new Error("Session reset"));
1802
+ }
1803
+ this.pendingRequests.clear();
1804
+ this.session.client.updateAgentState((currentState) => {
1805
+ const pendingRequests = currentState.requests || {};
1806
+ const completedRequests = { ...currentState.completedRequests };
1807
+ for (const [id, request] of Object.entries(pendingRequests)) {
1808
+ completedRequests[id] = {
1809
+ ...request,
1810
+ completedAt: Date.now(),
1811
+ status: "canceled",
1812
+ reason: "Session switched to local mode"
1813
+ };
1814
+ }
1815
+ return {
1816
+ ...currentState,
1817
+ requests: {},
1818
+ // Clear all pending requests
1819
+ completedRequests
1820
+ };
1821
+ });
1822
+ }
1823
+ /**
1824
+ * Sets up the client handler for permission responses
1825
+ */
1826
+ setupClientHandler() {
1827
+ this.session.client.setHandler("permission", async (message) => {
1828
+ types$1.logger.debug(`Permission response: ${JSON.stringify(message)}`);
1829
+ const id = message.id;
1830
+ const pending = this.pendingRequests.get(id);
1831
+ if (!pending) {
1832
+ types$1.logger.debug("Permission request not found or already resolved");
1833
+ return;
1834
+ }
1835
+ this.responses.set(id, { ...message, receivedAt: Date.now() });
1836
+ this.pendingRequests.delete(id);
1837
+ this.handlePermissionResponse(message, pending);
1838
+ this.session.client.updateAgentState((currentState) => {
1839
+ const request = currentState.requests?.[id];
1840
+ if (!request) return currentState;
1841
+ let r = { ...currentState.requests };
1842
+ delete r[id];
1843
+ return {
1844
+ ...currentState,
1845
+ requests: r,
1846
+ completedRequests: {
1847
+ ...currentState.completedRequests,
1848
+ [id]: {
1849
+ ...request,
1850
+ completedAt: Date.now(),
1851
+ status: message.approved ? "approved" : "denied",
1852
+ reason: message.reason,
1853
+ mode: message.mode,
1854
+ allowTools: message.allowTools
1855
+ }
1856
+ }
1857
+ };
1858
+ });
1859
+ });
1860
+ }
1861
+ /**
1862
+ * Gets the responses map (for compatibility with existing code)
1863
+ */
1864
+ getResponses() {
1865
+ return this.responses;
1866
+ }
1755
1867
  }
1756
1868
 
1757
1869
  function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
@@ -2125,12 +2237,6 @@ async function claudeRemoteLauncher(session) {
2125
2237
  }
2126
2238
  process.stdin.setEncoding("utf8");
2127
2239
  }
2128
- const scanner = await createSessionScanner({
2129
- sessionId: session.sessionId,
2130
- workingDirectory: session.path,
2131
- onMessage: (message) => {
2132
- }
2133
- });
2134
2240
  let exitReason = null;
2135
2241
  let abortController = null;
2136
2242
  let abortFuture = null;
@@ -2153,17 +2259,17 @@ async function claudeRemoteLauncher(session) {
2153
2259
  }
2154
2260
  session.client.setHandler("abort", doAbort);
2155
2261
  session.client.setHandler("switch", doSwitch);
2156
- const permissions = await startPermissionResolver(session);
2262
+ const permissionHandler = new PermissionHandler(session);
2157
2263
  const sdkToLogConverter = new SDKToLogConverter({
2158
2264
  sessionId: session.sessionId || "unknown",
2159
2265
  cwd: session.path,
2160
2266
  version: process.env.npm_package_version
2161
- }, permissions.responses);
2267
+ }, permissionHandler.getResponses());
2162
2268
  let planModeToolCalls = /* @__PURE__ */ new Set();
2163
2269
  let ongoingToolCalls = /* @__PURE__ */ new Map();
2164
2270
  function onMessage(message) {
2165
2271
  formatClaudeMessageForInk(message, messageBuffer);
2166
- permissions.onMessage(message);
2272
+ permissionHandler.onMessage(message);
2167
2273
  if (message.type === "assistant") {
2168
2274
  let umessage = message;
2169
2275
  if (umessage.message.content && Array.isArray(umessage.message.content)) {
@@ -2227,6 +2333,32 @@ async function claudeRemoteLauncher(session) {
2227
2333
  }
2228
2334
  const logMessage = sdkToLogConverter.convert(msg);
2229
2335
  if (logMessage) {
2336
+ if (logMessage.type === "user" && logMessage.message?.content) {
2337
+ const content = Array.isArray(logMessage.message.content) ? logMessage.message.content : [];
2338
+ for (let i = 0; i < content.length; i++) {
2339
+ const c = content[i];
2340
+ if (c.type === "tool_result" && c.tool_use_id) {
2341
+ const responses = permissionHandler.getResponses();
2342
+ const response = responses.get(c.tool_use_id);
2343
+ if (response) {
2344
+ const permissions = {
2345
+ date: response.receivedAt || Date.now(),
2346
+ result: response.approved ? "approved" : "denied"
2347
+ };
2348
+ if (response.mode) {
2349
+ permissions.mode = response.mode;
2350
+ }
2351
+ if (response.allowTools && response.allowTools.length > 0) {
2352
+ permissions.allowedTools = response.allowTools;
2353
+ }
2354
+ content[i] = {
2355
+ ...c,
2356
+ permissions
2357
+ };
2358
+ }
2359
+ }
2360
+ }
2361
+ }
2230
2362
  if (logMessage.type !== "system") {
2231
2363
  session.client.sendClaudeSessionMessage(logMessage);
2232
2364
  }
@@ -2246,58 +2378,57 @@ async function claudeRemoteLauncher(session) {
2246
2378
  }
2247
2379
  }
2248
2380
  try {
2381
+ let pending = null;
2249
2382
  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
2383
  types$1.logger.debug("[remote]: launch");
2268
2384
  messageBuffer.addMessage("\u2550".repeat(40), "status");
2269
2385
  messageBuffer.addMessage("Starting new Claude session...", "status");
2270
- abortController = new AbortController();
2386
+ const controller = new AbortController();
2387
+ abortController = controller;
2271
2388
  abortFuture = new Future();
2272
- permissions.reset();
2389
+ permissionHandler.reset();
2273
2390
  sdkToLogConverter.resetParentChain();
2391
+ let modeHash = null;
2392
+ let mode = null;
2274
2393
  try {
2275
2394
  await claudeRemote({
2276
2395
  sessionId: session.sessionId,
2277
2396
  path: session.path,
2278
- responses: permissions.responses,
2279
- mcpServers: {
2280
- ...session.mcpServers,
2281
- permission: {
2282
- type: "http",
2283
- url: permissions.server.url
2397
+ allowedTools: session.allowedTools ?? [],
2398
+ mcpServers: session.mcpServers,
2399
+ canCallTool: permissionHandler.handleToolCall,
2400
+ isAborted: (toolCallId) => {
2401
+ return permissionHandler.isAborted(toolCallId);
2402
+ },
2403
+ nextMessage: async () => {
2404
+ if (pending) {
2405
+ let p = pending;
2406
+ pending = null;
2407
+ permissionHandler.handleModeChange(p.mode.permissionMode);
2408
+ return p;
2284
2409
  }
2410
+ let msg = await session.queue.waitForMessagesAndGetAsString(controller.signal);
2411
+ if (msg) {
2412
+ if (modeHash && msg.hash !== modeHash || msg.isolate) {
2413
+ types$1.logger.debug("[remote]: mode has changed, pending message");
2414
+ pending = msg;
2415
+ return null;
2416
+ }
2417
+ modeHash = msg.hash;
2418
+ mode = msg.mode;
2419
+ permissionHandler.handleModeChange(mode.permissionMode);
2420
+ return {
2421
+ message: msg.message,
2422
+ mode: msg.mode
2423
+ };
2424
+ }
2425
+ return null;
2285
2426
  },
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
2427
  onSessionFound: (sessionId) => {
2295
2428
  sdkToLogConverter.updateSessionId(sessionId);
2296
2429
  session.onSessionFound(sessionId);
2297
- scanner.onNewSession(sessionId);
2298
2430
  },
2299
2431
  onThinkingChange: session.onThinkingChange,
2300
- message: messageData.message,
2301
2432
  claudeEnvVars: session.claudeEnvVars,
2302
2433
  claudeArgs: session.claudeArgs,
2303
2434
  onMessage,
@@ -2315,11 +2446,13 @@ async function claudeRemoteLauncher(session) {
2315
2446
  session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
2316
2447
  }
2317
2448
  } catch (e) {
2449
+ types$1.logger.debug("[remote]: launch error", e);
2318
2450
  if (!exitReason) {
2319
2451
  session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
2320
2452
  continue;
2321
2453
  }
2322
2454
  } finally {
2455
+ types$1.logger.debug("[remote]: launch finally");
2323
2456
  for (let [toolCallId, { parentToolCallId }] of ongoingToolCalls) {
2324
2457
  const converted = sdkToLogConverter.generateInterruptedToolResult(toolCallId, parentToolCallId);
2325
2458
  if (converted) {
@@ -2332,11 +2465,13 @@ async function claudeRemoteLauncher(session) {
2332
2465
  abortFuture?.resolve(void 0);
2333
2466
  abortFuture = null;
2334
2467
  types$1.logger.debug("[remote]: launch done");
2335
- permissions.reset();
2468
+ permissionHandler.reset();
2469
+ modeHash = null;
2470
+ mode = null;
2336
2471
  }
2337
2472
  }
2338
2473
  } finally {
2339
- permissions.server.stop();
2474
+ permissionHandler.reset();
2340
2475
  process.stdin.off("data", abort);
2341
2476
  if (process.stdin.isTTY) {
2342
2477
  process.stdin.setRawMode(false);
@@ -2348,7 +2483,6 @@ async function claudeRemoteLauncher(session) {
2348
2483
  if (abortFuture) {
2349
2484
  abortFuture.resolve(void 0);
2350
2485
  }
2351
- await scanner.cleanup();
2352
2486
  }
2353
2487
  return exitReason || "exit";
2354
2488
  }
@@ -2400,7 +2534,7 @@ async function loop(opts) {
2400
2534
  }
2401
2535
 
2402
2536
  var name = "happy-coder";
2403
- var version = "0.8.0";
2537
+ var version = "0.9.0-0";
2404
2538
  var description = "Claude Code session sharing CLI";
2405
2539
  var author = "Kirill Dubovitskiy";
2406
2540
  var license = "MIT";
@@ -2450,18 +2584,14 @@ var scripts = {
2450
2584
  test: "yarn build && vitest run",
2451
2585
  "test:watch": "vitest",
2452
2586
  "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",
2587
+ dev: "yarn build && DEBUG=1 npx tsx src/index.ts",
2454
2588
  "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
2455
2589
  "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
2456
2590
  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"
2591
+ release: "release-it"
2462
2592
  };
2463
2593
  var dependencies = {
2464
- "@anthropic-ai/claude-code": "^1.0.73",
2594
+ "@anthropic-ai/claude-code": "^1.0.89",
2465
2595
  "@anthropic-ai/sdk": "^0.56.0",
2466
2596
  "@modelcontextprotocol/sdk": "^1.15.1",
2467
2597
  "@stablelib/base64": "^2.0.1",
@@ -2490,6 +2620,7 @@ var devDependencies = {
2490
2620
  eslint: "^9",
2491
2621
  "eslint-config-prettier": "^10",
2492
2622
  pkgroll: "^2.14.2",
2623
+ "release-it": "^19.0.4",
2493
2624
  shx: "^0.3.3",
2494
2625
  "ts-node": "^10",
2495
2626
  tsx: "^4.20.3",
@@ -2497,7 +2628,12 @@ var devDependencies = {
2497
2628
  vitest: "^3.2.4"
2498
2629
  };
2499
2630
  var resolutions = {
2500
- "whatwg-url": "14.2.0"
2631
+ "whatwg-url": "14.2.0",
2632
+ "parse-path": "7.0.3",
2633
+ "@types/parse-path": "7.0.3"
2634
+ };
2635
+ var publishConfig = {
2636
+ registry: "https://registry.npmjs.org"
2501
2637
  };
2502
2638
  var packageManager = "yarn@1.22.22";
2503
2639
  var packageJson = {
@@ -2520,6 +2656,7 @@ var packageJson = {
2520
2656
  dependencies: dependencies,
2521
2657
  devDependencies: devDependencies,
2522
2658
  resolutions: resolutions,
2659
+ publishConfig: publishConfig,
2523
2660
  packageManager: packageManager
2524
2661
  };
2525
2662
 
@@ -2906,15 +3043,17 @@ async function clearDaemonState() {
2906
3043
  }
2907
3044
 
2908
3045
  class MessageQueue2 {
2909
- constructor(modeHasher) {
2910
- this.modeHasher = modeHasher;
2911
- types$1.logger.debug(`[MessageQueue2] Initialized`);
2912
- }
2913
3046
  queue = [];
2914
3047
  // Made public for testing
2915
3048
  waiter = null;
2916
3049
  closed = false;
2917
3050
  onMessageHandler = null;
3051
+ modeHasher;
3052
+ constructor(modeHasher, onMessageHandler = null) {
3053
+ this.modeHasher = modeHasher;
3054
+ this.onMessageHandler = onMessageHandler;
3055
+ types$1.logger.debug(`[MessageQueue2] Initialized`);
3056
+ }
2918
3057
  /**
2919
3058
  * Set a handler that will be called when a message arrives
2920
3059
  */
@@ -3089,6 +3228,7 @@ class MessageQueue2 {
3089
3228
  const firstItem = this.queue[0];
3090
3229
  const sameModeMessages = [];
3091
3230
  let mode = firstItem.mode;
3231
+ let isolate = firstItem.isolate ?? false;
3092
3232
  const targetModeHash = firstItem.modeHash;
3093
3233
  if (firstItem.isolate) {
3094
3234
  const item = this.queue.shift();
@@ -3104,7 +3244,9 @@ class MessageQueue2 {
3104
3244
  const combinedMessage = sameModeMessages.join("\n");
3105
3245
  return {
3106
3246
  message: combinedMessage,
3107
- mode
3247
+ mode,
3248
+ hash: targetModeHash,
3249
+ isolate
3108
3250
  };
3109
3251
  }
3110
3252
  /**
@@ -3999,10 +4141,10 @@ async function doWebAuth(keypair) {
3999
4141
  console.log("\u2713 Browser opened\n");
4000
4142
  console.log("Complete authentication in your browser window.");
4001
4143
  } else {
4002
- console.log("Could not open browser automatically.\n");
4003
- console.log("Please open this URL manually:");
4004
- console.log(webUrl);
4144
+ console.log("Could not open browser automatically.");
4005
4145
  }
4146
+ console.log("\nIf the browser did not open, please copy and paste this URL:");
4147
+ console.log(webUrl);
4006
4148
  console.log("");
4007
4149
  return await waitForAuthentication(keypair);
4008
4150
  }
@@ -4513,7 +4655,15 @@ async function start(credentials, options = {}) {
4513
4655
  if (caffeinateStarted) {
4514
4656
  types$1.logger.infoDeveloper("Sleep prevention enabled (macOS)");
4515
4657
  }
4516
- const messageQueue = new MessageQueue2((mode) => hashObject(mode));
4658
+ const messageQueue = new MessageQueue2((mode) => hashObject({
4659
+ isPlan: mode.permissionMode === "plan",
4660
+ model: mode.model,
4661
+ fallbackModel: mode.fallbackModel,
4662
+ customSystemPrompt: mode.customSystemPrompt,
4663
+ appendSystemPrompt: mode.appendSystemPrompt,
4664
+ allowedTools: mode.allowedTools,
4665
+ disallowedTools: mode.disallowedTools
4666
+ }));
4517
4667
  registerHandlers(session);
4518
4668
  let currentPermissionMode = options.permissionMode;
4519
4669
  let currentModel = options.model;