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