happy-coder 0.8.0 → 0.9.0-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,218 @@ 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 });
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}`;
1584
+ }
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
+ allowedBashLiterals = /* @__PURE__ */ new Set();
1606
+ allowedBashPrefixes = /* @__PURE__ */ new Set();
1607
+ permissionMode = "default";
1608
+ constructor(session) {
1609
+ this.session = session;
1610
+ this.setupClientHandler();
1611
+ }
1612
+ handleModeChange(mode) {
1613
+ this.permissionMode = mode;
1614
+ }
1615
+ /**
1616
+ * Handler response
1617
+ */
1618
+ handlePermissionResponse(response, pending) {
1619
+ if (response.allowTools && response.allowTools.length > 0) {
1620
+ response.allowTools.forEach((tool) => {
1621
+ if (tool.startsWith("Bash(") || tool === "Bash") {
1622
+ this.parseBashPermission(tool);
1623
+ } else {
1624
+ this.allowedTools.add(tool);
1625
+ }
1539
1626
  });
1540
1627
  }
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);
1628
+ if (response.mode) {
1629
+ this.permissionMode = response.mode;
1630
+ }
1631
+ if (pending.toolName === "exit_plan_mode" || pending.toolName === "ExitPlanMode") {
1632
+ logger.debug("Plan mode result received", response);
1633
+ if (response.approved) {
1634
+ logger.debug("Plan approved - injecting PLAN_FAKE_RESTART");
1635
+ if (response.mode && ["default", "acceptEdits", "bypassPermissions"].includes(response.mode)) {
1636
+ this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: response.mode });
1637
+ } else {
1638
+ this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: "default" });
1639
+ }
1640
+ pending.resolve({ behavior: "deny", message: PLAN_FAKE_REJECT });
1560
1641
  } else {
1561
- requests.set(id, resolve);
1642
+ pending.resolve({ behavior: "deny", message: response.reason || "Plan rejected" });
1562
1643
  }
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
- }
1644
+ } else {
1645
+ 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.` };
1646
+ pending.resolve(result);
1647
+ }
1648
+ }
1649
+ /**
1650
+ * Creates the canCallTool callback for the SDK
1651
+ */
1652
+ handleToolCall = async (toolName, input, mode, options) => {
1653
+ if (toolName === "Bash") {
1654
+ const inputObj = input;
1655
+ if (inputObj?.command) {
1656
+ if (this.allowedBashLiterals.has(inputObj.command)) {
1657
+ return { behavior: "allow", updatedInput: input };
1658
+ }
1659
+ for (const prefix of this.allowedBashPrefixes) {
1660
+ if (inputObj.command.startsWith(prefix)) {
1661
+ return { behavior: "allow", updatedInput: input };
1583
1662
  }
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()
1606
1663
  }
1607
1664
  }
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
- } else {
1621
- logger.debug("Permission request stale, likely timed out");
1622
- return;
1665
+ } else if (this.allowedTools.has(toolName)) {
1666
+ return { behavior: "allow", updatedInput: input };
1623
1667
  }
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 {
1668
+ const descriptor = getToolDescriptor(toolName);
1669
+ if (this.permissionMode === "bypassPermissions") {
1670
+ return { behavior: "allow", updatedInput: input };
1671
+ }
1672
+ if (this.permissionMode === "acceptEdits" && descriptor.edit) {
1673
+ return { behavior: "allow", updatedInput: input };
1674
+ }
1675
+ let toolCallId = this.resolveToolCallId(toolName, input);
1676
+ if (!toolCallId) {
1677
+ await delay(1e3);
1678
+ toolCallId = this.resolveToolCallId(toolName, input);
1679
+ if (!toolCallId) {
1680
+ throw new Error(`Could not resolve tool call ID for ${toolName}`);
1681
+ }
1682
+ }
1683
+ return this.handlePermissionRequest(toolCallId, toolName, input, options.signal);
1684
+ };
1685
+ /**
1686
+ * Handles individual permission requests
1687
+ */
1688
+ async handlePermissionRequest(id, toolName, input, signal) {
1689
+ return new Promise((resolve, reject) => {
1690
+ const abortHandler = () => {
1691
+ this.pendingRequests.delete(id);
1692
+ reject(new Error("Permission request aborted"));
1693
+ };
1694
+ signal.addEventListener("abort", abortHandler, { once: true });
1695
+ this.pendingRequests.set(id, {
1696
+ resolve: (result) => {
1697
+ signal.removeEventListener("abort", abortHandler);
1698
+ resolve(result);
1699
+ },
1700
+ reject: (error) => {
1701
+ signal.removeEventListener("abort", abortHandler);
1702
+ reject(error);
1703
+ },
1704
+ toolName,
1705
+ input
1706
+ });
1707
+ this.session.api.push().sendToAllDevices(
1708
+ "Permission Request",
1709
+ `Claude wants to ${getToolName(toolName)}`,
1710
+ {
1711
+ sessionId: this.session.client.sessionId,
1712
+ requestId: id,
1713
+ tool: toolName,
1714
+ type: "permission_request"
1715
+ }
1716
+ );
1717
+ this.session.client.updateAgentState((currentState) => ({
1631
1718
  ...currentState,
1632
- requests: r,
1633
- completedRequests: {
1634
- ...currentState.completedRequests,
1719
+ requests: {
1720
+ ...currentState.requests,
1635
1721
  [id]: {
1636
- ...request,
1637
- completedAt: Date.now(),
1638
- status: isExitPlanModeSuccess ? "approved" : message.approved ? "approved" : "denied",
1639
- reason: isExitPlanModeSuccess ? "Plan approved" : message.reason
1722
+ tool: toolName,
1723
+ arguments: input,
1724
+ createdAt: Date.now()
1640
1725
  }
1641
1726
  }
1642
- };
1727
+ }));
1728
+ logger.debug(`Permission request sent for tool call ${id}: ${toolName}`);
1643
1729
  });
1644
- });
1645
- const resolveToolCallId = (name, args) => {
1646
- for (let i = toolCalls.length - 1; i >= 0; i--) {
1647
- const call = toolCalls[i];
1730
+ }
1731
+ /**
1732
+ * Parses Bash permission strings into literal and prefix sets
1733
+ */
1734
+ parseBashPermission(permission) {
1735
+ if (permission === "Bash") {
1736
+ return;
1737
+ }
1738
+ const bashPattern = /^Bash\((.+?)\)$/;
1739
+ const match = permission.match(bashPattern);
1740
+ if (!match) {
1741
+ return;
1742
+ }
1743
+ const command = match[1];
1744
+ if (command.endsWith(":*")) {
1745
+ const prefix = command.slice(0, -2);
1746
+ this.allowedBashPrefixes.add(prefix);
1747
+ } else {
1748
+ this.allowedBashLiterals.add(command);
1749
+ }
1750
+ }
1751
+ /**
1752
+ * Resolves tool call ID based on tool name and input
1753
+ */
1754
+ resolveToolCallId(name, args) {
1755
+ for (let i = this.toolCalls.length - 1; i >= 0; i--) {
1756
+ const call = this.toolCalls[i];
1648
1757
  if (call.name === name && deepEqual(call.input, args)) {
1649
1758
  if (call.used) {
1650
1759
  return null;
@@ -1654,59 +1763,22 @@ async function startPermissionResolver(session) {
1654
1763
  }
1655
1764
  }
1656
1765
  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
1766
  }
1685
- function onMessage(message) {
1767
+ /**
1768
+ * Handles messages to track tool calls
1769
+ */
1770
+ onMessage(message) {
1686
1771
  if (message.type === "assistant") {
1687
1772
  const assistantMsg = message;
1688
1773
  if (assistantMsg.message && assistantMsg.message.content) {
1689
1774
  for (const block of assistantMsg.message.content) {
1690
1775
  if (block.type === "tool_use") {
1691
- toolCalls.push({
1776
+ this.toolCalls.push({
1692
1777
  id: block.id,
1693
1778
  name: block.name,
1694
1779
  input: block.input,
1695
1780
  used: false
1696
1781
  });
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
1782
  }
1711
1783
  }
1712
1784
  }
@@ -1716,7 +1788,7 @@ async function startPermissionResolver(session) {
1716
1788
  if (userMsg.message && userMsg.message.content && Array.isArray(userMsg.message.content)) {
1717
1789
  for (const block of userMsg.message.content) {
1718
1790
  if (block.type === "tool_result" && block.tool_use_id) {
1719
- const toolCall = toolCalls.find((tc) => tc.id === block.tool_use_id);
1791
+ const toolCall = this.toolCalls.find((tc) => tc.id === block.tool_use_id);
1720
1792
  if (toolCall && !toolCall.used) {
1721
1793
  toolCall.used = true;
1722
1794
  }
@@ -1725,12 +1797,95 @@ async function startPermissionResolver(session) {
1725
1797
  }
1726
1798
  }
1727
1799
  }
1728
- return {
1729
- server,
1730
- reset,
1731
- onMessage,
1732
- responses
1733
- };
1800
+ /**
1801
+ * Checks if a tool call is rejected
1802
+ */
1803
+ isAborted(toolCallId) {
1804
+ if (this.responses.get(toolCallId)?.approved === false) {
1805
+ return true;
1806
+ }
1807
+ const toolCall = this.toolCalls.find((tc) => tc.id === toolCallId);
1808
+ if (toolCall && (toolCall.name === "exit_plan_mode" || toolCall.name === "ExitPlanMode")) {
1809
+ return true;
1810
+ }
1811
+ return false;
1812
+ }
1813
+ /**
1814
+ * Resets all state for new sessions
1815
+ */
1816
+ reset() {
1817
+ this.toolCalls = [];
1818
+ this.responses.clear();
1819
+ this.allowedTools.clear();
1820
+ this.allowedBashLiterals.clear();
1821
+ this.allowedBashPrefixes.clear();
1822
+ for (const [, pending] of this.pendingRequests.entries()) {
1823
+ pending.reject(new Error("Session reset"));
1824
+ }
1825
+ this.pendingRequests.clear();
1826
+ this.session.client.updateAgentState((currentState) => {
1827
+ const pendingRequests = currentState.requests || {};
1828
+ const completedRequests = { ...currentState.completedRequests };
1829
+ for (const [id, request] of Object.entries(pendingRequests)) {
1830
+ completedRequests[id] = {
1831
+ ...request,
1832
+ completedAt: Date.now(),
1833
+ status: "canceled",
1834
+ reason: "Session switched to local mode"
1835
+ };
1836
+ }
1837
+ return {
1838
+ ...currentState,
1839
+ requests: {},
1840
+ // Clear all pending requests
1841
+ completedRequests
1842
+ };
1843
+ });
1844
+ }
1845
+ /**
1846
+ * Sets up the client handler for permission responses
1847
+ */
1848
+ setupClientHandler() {
1849
+ this.session.client.setHandler("permission", async (message) => {
1850
+ logger.debug(`Permission response: ${JSON.stringify(message)}`);
1851
+ const id = message.id;
1852
+ const pending = this.pendingRequests.get(id);
1853
+ if (!pending) {
1854
+ logger.debug("Permission request not found or already resolved");
1855
+ return;
1856
+ }
1857
+ this.responses.set(id, { ...message, receivedAt: Date.now() });
1858
+ this.pendingRequests.delete(id);
1859
+ this.handlePermissionResponse(message, pending);
1860
+ this.session.client.updateAgentState((currentState) => {
1861
+ const request = currentState.requests?.[id];
1862
+ if (!request) return currentState;
1863
+ let r = { ...currentState.requests };
1864
+ delete r[id];
1865
+ return {
1866
+ ...currentState,
1867
+ requests: r,
1868
+ completedRequests: {
1869
+ ...currentState.completedRequests,
1870
+ [id]: {
1871
+ ...request,
1872
+ completedAt: Date.now(),
1873
+ status: message.approved ? "approved" : "denied",
1874
+ reason: message.reason,
1875
+ mode: message.mode,
1876
+ allowTools: message.allowTools
1877
+ }
1878
+ }
1879
+ };
1880
+ });
1881
+ });
1882
+ }
1883
+ /**
1884
+ * Gets the responses map (for compatibility with existing code)
1885
+ */
1886
+ getResponses() {
1887
+ return this.responses;
1888
+ }
1734
1889
  }
1735
1890
 
1736
1891
  function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
@@ -2104,12 +2259,6 @@ async function claudeRemoteLauncher(session) {
2104
2259
  }
2105
2260
  process.stdin.setEncoding("utf8");
2106
2261
  }
2107
- const scanner = await createSessionScanner({
2108
- sessionId: session.sessionId,
2109
- workingDirectory: session.path,
2110
- onMessage: (message) => {
2111
- }
2112
- });
2113
2262
  let exitReason = null;
2114
2263
  let abortController = null;
2115
2264
  let abortFuture = null;
@@ -2132,17 +2281,17 @@ async function claudeRemoteLauncher(session) {
2132
2281
  }
2133
2282
  session.client.setHandler("abort", doAbort);
2134
2283
  session.client.setHandler("switch", doSwitch);
2135
- const permissions = await startPermissionResolver(session);
2284
+ const permissionHandler = new PermissionHandler(session);
2136
2285
  const sdkToLogConverter = new SDKToLogConverter({
2137
2286
  sessionId: session.sessionId || "unknown",
2138
2287
  cwd: session.path,
2139
2288
  version: process.env.npm_package_version
2140
- }, permissions.responses);
2289
+ }, permissionHandler.getResponses());
2141
2290
  let planModeToolCalls = /* @__PURE__ */ new Set();
2142
2291
  let ongoingToolCalls = /* @__PURE__ */ new Map();
2143
2292
  function onMessage(message) {
2144
2293
  formatClaudeMessageForInk(message, messageBuffer);
2145
- permissions.onMessage(message);
2294
+ permissionHandler.onMessage(message);
2146
2295
  if (message.type === "assistant") {
2147
2296
  let umessage = message;
2148
2297
  if (umessage.message.content && Array.isArray(umessage.message.content)) {
@@ -2206,6 +2355,32 @@ async function claudeRemoteLauncher(session) {
2206
2355
  }
2207
2356
  const logMessage = sdkToLogConverter.convert(msg);
2208
2357
  if (logMessage) {
2358
+ if (logMessage.type === "user" && logMessage.message?.content) {
2359
+ const content = Array.isArray(logMessage.message.content) ? logMessage.message.content : [];
2360
+ for (let i = 0; i < content.length; i++) {
2361
+ const c = content[i];
2362
+ if (c.type === "tool_result" && c.tool_use_id) {
2363
+ const responses = permissionHandler.getResponses();
2364
+ const response = responses.get(c.tool_use_id);
2365
+ if (response) {
2366
+ const permissions = {
2367
+ date: response.receivedAt || Date.now(),
2368
+ result: response.approved ? "approved" : "denied"
2369
+ };
2370
+ if (response.mode) {
2371
+ permissions.mode = response.mode;
2372
+ }
2373
+ if (response.allowTools && response.allowTools.length > 0) {
2374
+ permissions.allowedTools = response.allowTools;
2375
+ }
2376
+ content[i] = {
2377
+ ...c,
2378
+ permissions
2379
+ };
2380
+ }
2381
+ }
2382
+ }
2383
+ }
2209
2384
  if (logMessage.type !== "system") {
2210
2385
  session.client.sendClaudeSessionMessage(logMessage);
2211
2386
  }
@@ -2225,58 +2400,57 @@ async function claudeRemoteLauncher(session) {
2225
2400
  }
2226
2401
  }
2227
2402
  try {
2403
+ let pending = null;
2228
2404
  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
2405
  logger.debug("[remote]: launch");
2247
2406
  messageBuffer.addMessage("\u2550".repeat(40), "status");
2248
2407
  messageBuffer.addMessage("Starting new Claude session...", "status");
2249
- abortController = new AbortController();
2408
+ const controller = new AbortController();
2409
+ abortController = controller;
2250
2410
  abortFuture = new Future();
2251
- permissions.reset();
2411
+ permissionHandler.reset();
2252
2412
  sdkToLogConverter.resetParentChain();
2413
+ let modeHash = null;
2414
+ let mode = null;
2253
2415
  try {
2254
2416
  await claudeRemote({
2255
2417
  sessionId: session.sessionId,
2256
2418
  path: session.path,
2257
- responses: permissions.responses,
2258
- mcpServers: {
2259
- ...session.mcpServers,
2260
- permission: {
2261
- type: "http",
2262
- url: permissions.server.url
2419
+ allowedTools: session.allowedTools ?? [],
2420
+ mcpServers: session.mcpServers,
2421
+ canCallTool: permissionHandler.handleToolCall,
2422
+ isAborted: (toolCallId) => {
2423
+ return permissionHandler.isAborted(toolCallId);
2424
+ },
2425
+ nextMessage: async () => {
2426
+ if (pending) {
2427
+ let p = pending;
2428
+ pending = null;
2429
+ permissionHandler.handleModeChange(p.mode.permissionMode);
2430
+ return p;
2263
2431
  }
2432
+ let msg = await session.queue.waitForMessagesAndGetAsString(controller.signal);
2433
+ if (msg) {
2434
+ if (modeHash && msg.hash !== modeHash || msg.isolate) {
2435
+ logger.debug("[remote]: mode has changed, pending message");
2436
+ pending = msg;
2437
+ return null;
2438
+ }
2439
+ modeHash = msg.hash;
2440
+ mode = msg.mode;
2441
+ permissionHandler.handleModeChange(mode.permissionMode);
2442
+ return {
2443
+ message: msg.message,
2444
+ mode: msg.mode
2445
+ };
2446
+ }
2447
+ return null;
2264
2448
  },
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
2449
  onSessionFound: (sessionId) => {
2274
2450
  sdkToLogConverter.updateSessionId(sessionId);
2275
2451
  session.onSessionFound(sessionId);
2276
- scanner.onNewSession(sessionId);
2277
2452
  },
2278
2453
  onThinkingChange: session.onThinkingChange,
2279
- message: messageData.message,
2280
2454
  claudeEnvVars: session.claudeEnvVars,
2281
2455
  claudeArgs: session.claudeArgs,
2282
2456
  onMessage,
@@ -2294,11 +2468,13 @@ async function claudeRemoteLauncher(session) {
2294
2468
  session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
2295
2469
  }
2296
2470
  } catch (e) {
2471
+ logger.debug("[remote]: launch error", e);
2297
2472
  if (!exitReason) {
2298
2473
  session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
2299
2474
  continue;
2300
2475
  }
2301
2476
  } finally {
2477
+ logger.debug("[remote]: launch finally");
2302
2478
  for (let [toolCallId, { parentToolCallId }] of ongoingToolCalls) {
2303
2479
  const converted = sdkToLogConverter.generateInterruptedToolResult(toolCallId, parentToolCallId);
2304
2480
  if (converted) {
@@ -2311,11 +2487,13 @@ async function claudeRemoteLauncher(session) {
2311
2487
  abortFuture?.resolve(void 0);
2312
2488
  abortFuture = null;
2313
2489
  logger.debug("[remote]: launch done");
2314
- permissions.reset();
2490
+ permissionHandler.reset();
2491
+ modeHash = null;
2492
+ mode = null;
2315
2493
  }
2316
2494
  }
2317
2495
  } finally {
2318
- permissions.server.stop();
2496
+ permissionHandler.reset();
2319
2497
  process.stdin.off("data", abort);
2320
2498
  if (process.stdin.isTTY) {
2321
2499
  process.stdin.setRawMode(false);
@@ -2327,7 +2505,6 @@ async function claudeRemoteLauncher(session) {
2327
2505
  if (abortFuture) {
2328
2506
  abortFuture.resolve(void 0);
2329
2507
  }
2330
- await scanner.cleanup();
2331
2508
  }
2332
2509
  return exitReason || "exit";
2333
2510
  }
@@ -2379,7 +2556,7 @@ async function loop(opts) {
2379
2556
  }
2380
2557
 
2381
2558
  var name = "happy-coder";
2382
- var version = "0.8.0";
2559
+ var version = "0.9.0-1";
2383
2560
  var description = "Claude Code session sharing CLI";
2384
2561
  var author = "Kirill Dubovitskiy";
2385
2562
  var license = "MIT";
@@ -2429,18 +2606,14 @@ var scripts = {
2429
2606
  test: "yarn build && vitest run",
2430
2607
  "test:watch": "vitest",
2431
2608
  "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",
2609
+ dev: "yarn build && DEBUG=1 npx tsx src/index.ts",
2433
2610
  "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
2434
2611
  "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
2435
2612
  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"
2613
+ release: "release-it"
2441
2614
  };
2442
2615
  var dependencies = {
2443
- "@anthropic-ai/claude-code": "^1.0.73",
2616
+ "@anthropic-ai/claude-code": "^1.0.89",
2444
2617
  "@anthropic-ai/sdk": "^0.56.0",
2445
2618
  "@modelcontextprotocol/sdk": "^1.15.1",
2446
2619
  "@stablelib/base64": "^2.0.1",
@@ -2469,6 +2642,7 @@ var devDependencies = {
2469
2642
  eslint: "^9",
2470
2643
  "eslint-config-prettier": "^10",
2471
2644
  pkgroll: "^2.14.2",
2645
+ "release-it": "^19.0.4",
2472
2646
  shx: "^0.3.3",
2473
2647
  "ts-node": "^10",
2474
2648
  tsx: "^4.20.3",
@@ -2476,7 +2650,12 @@ var devDependencies = {
2476
2650
  vitest: "^3.2.4"
2477
2651
  };
2478
2652
  var resolutions = {
2479
- "whatwg-url": "14.2.0"
2653
+ "whatwg-url": "14.2.0",
2654
+ "parse-path": "7.0.3",
2655
+ "@types/parse-path": "7.0.3"
2656
+ };
2657
+ var publishConfig = {
2658
+ registry: "https://registry.npmjs.org"
2480
2659
  };
2481
2660
  var packageManager = "yarn@1.22.22";
2482
2661
  var packageJson = {
@@ -2499,6 +2678,7 @@ var packageJson = {
2499
2678
  dependencies: dependencies,
2500
2679
  devDependencies: devDependencies,
2501
2680
  resolutions: resolutions,
2681
+ publishConfig: publishConfig,
2502
2682
  packageManager: packageManager
2503
2683
  };
2504
2684
 
@@ -2885,15 +3065,17 @@ async function clearDaemonState() {
2885
3065
  }
2886
3066
 
2887
3067
  class MessageQueue2 {
2888
- constructor(modeHasher) {
2889
- this.modeHasher = modeHasher;
2890
- logger.debug(`[MessageQueue2] Initialized`);
2891
- }
2892
3068
  queue = [];
2893
3069
  // Made public for testing
2894
3070
  waiter = null;
2895
3071
  closed = false;
2896
3072
  onMessageHandler = null;
3073
+ modeHasher;
3074
+ constructor(modeHasher, onMessageHandler = null) {
3075
+ this.modeHasher = modeHasher;
3076
+ this.onMessageHandler = onMessageHandler;
3077
+ logger.debug(`[MessageQueue2] Initialized`);
3078
+ }
2897
3079
  /**
2898
3080
  * Set a handler that will be called when a message arrives
2899
3081
  */
@@ -3068,6 +3250,7 @@ class MessageQueue2 {
3068
3250
  const firstItem = this.queue[0];
3069
3251
  const sameModeMessages = [];
3070
3252
  let mode = firstItem.mode;
3253
+ let isolate = firstItem.isolate ?? false;
3071
3254
  const targetModeHash = firstItem.modeHash;
3072
3255
  if (firstItem.isolate) {
3073
3256
  const item = this.queue.shift();
@@ -3083,7 +3266,9 @@ class MessageQueue2 {
3083
3266
  const combinedMessage = sameModeMessages.join("\n");
3084
3267
  return {
3085
3268
  message: combinedMessage,
3086
- mode
3269
+ mode,
3270
+ hash: targetModeHash,
3271
+ isolate
3087
3272
  };
3088
3273
  }
3089
3274
  /**
@@ -3978,10 +4163,10 @@ async function doWebAuth(keypair) {
3978
4163
  console.log("\u2713 Browser opened\n");
3979
4164
  console.log("Complete authentication in your browser window.");
3980
4165
  } else {
3981
- console.log("Could not open browser automatically.\n");
3982
- console.log("Please open this URL manually:");
3983
- console.log(webUrl);
4166
+ console.log("Could not open browser automatically.");
3984
4167
  }
4168
+ console.log("\nIf the browser did not open, please copy and paste this URL:");
4169
+ console.log(webUrl);
3985
4170
  console.log("");
3986
4171
  return await waitForAuthentication(keypair);
3987
4172
  }
@@ -4492,7 +4677,15 @@ async function start(credentials, options = {}) {
4492
4677
  if (caffeinateStarted) {
4493
4678
  logger.infoDeveloper("Sleep prevention enabled (macOS)");
4494
4679
  }
4495
- const messageQueue = new MessageQueue2((mode) => hashObject(mode));
4680
+ const messageQueue = new MessageQueue2((mode) => hashObject({
4681
+ isPlan: mode.permissionMode === "plan",
4682
+ model: mode.model,
4683
+ fallbackModel: mode.fallbackModel,
4684
+ customSystemPrompt: mode.customSystemPrompt,
4685
+ appendSystemPrompt: mode.appendSystemPrompt,
4686
+ allowedTools: mode.allowedTools,
4687
+ disallowedTools: mode.disallowedTools
4688
+ }));
4496
4689
  registerHandlers(session);
4497
4690
  let currentPermissionMode = options.permissionMode;
4498
4691
  let currentModel = options.model;