happy-coder 0.7.2 → 0.9.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -20,18 +20,18 @@ require('node:events');
20
20
  require('socket.io-client');
21
21
  var tweetnacl = require('tweetnacl');
22
22
  require('expo-server-sdk');
23
- var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
24
- var node_http = require('node:http');
25
- var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
26
- var z = require('zod');
27
23
  var child_process = require('child_process');
28
24
  var util = require('util');
29
25
  var crypto = require('crypto');
26
+ var z = require('zod');
30
27
  var fastify = require('fastify');
31
28
  var fastifyTypeProviderZod = require('fastify-type-provider-zod');
32
29
  var os$1 = require('os');
33
30
  var qrcode = require('qrcode-terminal');
34
31
  var open = require('open');
32
+ var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
33
+ var node_http = require('node:http');
34
+ var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
35
35
  var fs = require('fs');
36
36
 
37
37
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
@@ -63,6 +63,7 @@ class Session {
63
63
  claudeEnvVars;
64
64
  claudeArgs;
65
65
  mcpServers;
66
+ allowedTools;
66
67
  _onModeChange;
67
68
  sessionId;
68
69
  mode = "local";
@@ -77,6 +78,7 @@ class Session {
77
78
  this.claudeEnvVars = opts.claudeEnvVars;
78
79
  this.claudeArgs = opts.claudeArgs;
79
80
  this.mcpServers = opts.mcpServers;
81
+ this.allowedTools = opts.allowedTools;
80
82
  this._onModeChange = opts.onModeChange;
81
83
  this.client.keepAlive(this.thinking, this.mode);
82
84
  setInterval(() => {
@@ -135,6 +137,29 @@ function projectPath() {
135
137
  return path$1;
136
138
  }
137
139
 
140
+ function trimIdent(text) {
141
+ const lines = text.split("\n");
142
+ while (lines.length > 0 && lines[0].trim() === "") {
143
+ lines.shift();
144
+ }
145
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
146
+ lines.pop();
147
+ }
148
+ const minSpaces = lines.reduce((min, line) => {
149
+ if (line.trim() === "") {
150
+ return min;
151
+ }
152
+ const leadingSpaces = line.match(/^\s*/)[0].length;
153
+ return Math.min(min, leadingSpaces);
154
+ }, Infinity);
155
+ const trimmedLines = lines.map((line) => line.slice(minSpaces));
156
+ return trimmedLines.join("\n");
157
+ }
158
+
159
+ const systemPrompt = trimIdent(`
160
+ ALWAYS when you start a new chat - you must call a tool "mcp__happy__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
161
+ `);
162
+
138
163
  node_path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
139
164
  async function claudeLocal(opts) {
140
165
  const projectDir = getProjectPath(opts.path);
@@ -182,6 +207,13 @@ async function claudeLocal(opts) {
182
207
  if (startFrom) {
183
208
  args.push("--resume", startFrom);
184
209
  }
210
+ args.push("--append-system-prompt", systemPrompt);
211
+ if (opts.mcpServers && Object.keys(opts.mcpServers).length > 0) {
212
+ args.push("--mcp-config", JSON.stringify({ mcpServers: opts.mcpServers }));
213
+ }
214
+ if (opts.allowedTools && opts.allowedTools.length > 0) {
215
+ args.push("--allowedTools", opts.allowedTools.join(","));
216
+ }
185
217
  if (opts.claudeArgs) {
186
218
  args.push(...opts.claudeArgs);
187
219
  }
@@ -526,7 +558,9 @@ async function claudeLocalLauncher(session) {
526
558
  sessionId: session.sessionId,
527
559
  workingDirectory: session.path,
528
560
  onMessage: (message) => {
529
- session.client.sendClaudeSessionMessage(message);
561
+ if (message.type !== "summary") {
562
+ session.client.sendClaudeSessionMessage(message);
563
+ }
530
564
  }
531
565
  });
532
566
  let exitReason = null;
@@ -556,7 +590,9 @@ async function claudeLocalLauncher(session) {
556
590
  }
557
591
  session.client.setHandler("abort", doAbort);
558
592
  session.client.setHandler("switch", doSwitch);
559
- session.queue.setOnMessage(doSwitch);
593
+ session.queue.setOnMessage((message, mode) => {
594
+ doSwitch();
595
+ });
560
596
  if (session.queue.size() > 0) {
561
597
  return "switch";
562
598
  }
@@ -577,7 +613,9 @@ async function claudeLocalLauncher(session) {
577
613
  onThinkingChange: session.onThinkingChange,
578
614
  abort: processAbortController.signal,
579
615
  claudeEnvVars: session.claudeEnvVars,
580
- claudeArgs: session.claudeArgs
616
+ claudeArgs: session.claudeArgs,
617
+ mcpServers: session.mcpServers,
618
+ allowedTools: session.allowedTools
581
619
  });
582
620
  if (!exitReason) {
583
621
  exitReason = "exit";
@@ -885,16 +923,19 @@ async function streamToStdin(stream, stdin, abort) {
885
923
  }
886
924
 
887
925
  class Query {
888
- constructor(childStdin, childStdout, processExitPromise) {
926
+ constructor(childStdin, childStdout, processExitPromise, canCallTool) {
889
927
  this.childStdin = childStdin;
890
928
  this.childStdout = childStdout;
891
929
  this.processExitPromise = processExitPromise;
930
+ this.canCallTool = canCallTool;
892
931
  this.readMessages();
893
932
  this.sdkMessages = this.readSdkMessages();
894
933
  }
895
934
  pendingControlResponses = /* @__PURE__ */ new Map();
935
+ cancelControllers = /* @__PURE__ */ new Map();
896
936
  sdkMessages;
897
937
  inputStream = new Stream();
938
+ canCallTool;
898
939
  /**
899
940
  * Set an error on the stream
900
941
  */
@@ -939,6 +980,12 @@ class Query {
939
980
  handler(controlResponse.response);
940
981
  }
941
982
  continue;
983
+ } else if (message.type === "control_request") {
984
+ await this.handleControlRequest(message);
985
+ continue;
986
+ } else if (message.type === "control_cancel_request") {
987
+ this.handleControlCancelRequest(message);
988
+ continue;
942
989
  }
943
990
  this.inputStream.enqueue(message);
944
991
  } catch (e) {
@@ -951,6 +998,7 @@ class Query {
951
998
  this.inputStream.error(error);
952
999
  } finally {
953
1000
  this.inputStream.done();
1001
+ this.cleanupControllers();
954
1002
  rl.close();
955
1003
  }
956
1004
  }
@@ -994,6 +1042,77 @@ class Query {
994
1042
  childStdin.write(JSON.stringify(sdkRequest) + "\n");
995
1043
  });
996
1044
  }
1045
+ /**
1046
+ * Handle incoming control requests for tool permissions
1047
+ * Replicates the exact logic from the SDK's handleControlRequest method
1048
+ */
1049
+ async handleControlRequest(request) {
1050
+ if (!this.childStdin) {
1051
+ logDebug("Cannot handle control request - no stdin available");
1052
+ return;
1053
+ }
1054
+ const controller = new AbortController();
1055
+ this.cancelControllers.set(request.request_id, controller);
1056
+ try {
1057
+ const response = await this.processControlRequest(request, controller.signal);
1058
+ const controlResponse = {
1059
+ type: "control_response",
1060
+ response: {
1061
+ subtype: "success",
1062
+ request_id: request.request_id,
1063
+ response
1064
+ }
1065
+ };
1066
+ this.childStdin.write(JSON.stringify(controlResponse) + "\n");
1067
+ } catch (error) {
1068
+ const controlErrorResponse = {
1069
+ type: "control_response",
1070
+ response: {
1071
+ subtype: "error",
1072
+ request_id: request.request_id,
1073
+ error: error instanceof Error ? error.message : String(error)
1074
+ }
1075
+ };
1076
+ this.childStdin.write(JSON.stringify(controlErrorResponse) + "\n");
1077
+ } finally {
1078
+ this.cancelControllers.delete(request.request_id);
1079
+ }
1080
+ }
1081
+ /**
1082
+ * Handle control cancel requests
1083
+ * Replicates the exact logic from the SDK's handleControlCancelRequest method
1084
+ */
1085
+ handleControlCancelRequest(request) {
1086
+ const controller = this.cancelControllers.get(request.request_id);
1087
+ if (controller) {
1088
+ controller.abort();
1089
+ this.cancelControllers.delete(request.request_id);
1090
+ }
1091
+ }
1092
+ /**
1093
+ * Process control requests based on subtype
1094
+ * Replicates the exact logic from the SDK's processControlRequest method
1095
+ */
1096
+ async processControlRequest(request, signal) {
1097
+ if (request.request.subtype === "can_use_tool") {
1098
+ if (!this.canCallTool) {
1099
+ throw new Error("canCallTool callback is not provided.");
1100
+ }
1101
+ return this.canCallTool(request.request.tool_name, request.request.input, {
1102
+ signal
1103
+ });
1104
+ }
1105
+ throw new Error("Unsupported control request subtype: " + request.request.subtype);
1106
+ }
1107
+ /**
1108
+ * Cleanup method to abort all pending control requests
1109
+ */
1110
+ cleanupControllers() {
1111
+ for (const [requestId, controller] of this.cancelControllers.entries()) {
1112
+ controller.abort();
1113
+ this.cancelControllers.delete(requestId);
1114
+ }
1115
+ }
997
1116
  }
998
1117
  function query(config) {
999
1118
  const {
@@ -1010,12 +1129,12 @@ function query(config) {
1010
1129
  mcpServers,
1011
1130
  pathToClaudeCodeExecutable = getDefaultClaudeCodePath(),
1012
1131
  permissionMode = "default",
1013
- permissionPromptToolName,
1014
1132
  continue: continueConversation,
1015
1133
  resume,
1016
1134
  model,
1017
1135
  fallbackModel,
1018
- strictMcpConfig
1136
+ strictMcpConfig,
1137
+ canCallTool
1019
1138
  } = {}
1020
1139
  } = config;
1021
1140
  if (!process.env.CLAUDE_CODE_ENTRYPOINT) {
@@ -1026,7 +1145,12 @@ function query(config) {
1026
1145
  if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
1027
1146
  if (maxTurns) args.push("--max-turns", maxTurns.toString());
1028
1147
  if (model) args.push("--model", model);
1029
- if (permissionPromptToolName) args.push("--permission-prompt-tool", permissionPromptToolName);
1148
+ if (canCallTool) {
1149
+ if (typeof prompt === "string") {
1150
+ throw new Error("canCallTool callback requires --input-format stream-json. Please set prompt as an AsyncIterable.");
1151
+ }
1152
+ args.push("--permission-prompt-tool", "stdio");
1153
+ }
1030
1154
  if (continueConversation) args.push("--continue");
1031
1155
  if (resume) args.push("--resume", resume);
1032
1156
  if (allowedTools.length > 0) args.push("--allowedTools", allowedTools.join(","));
@@ -1090,7 +1214,7 @@ function query(config) {
1090
1214
  }
1091
1215
  });
1092
1216
  });
1093
- const query2 = new Query(childStdin, child.stdout, processExitPromise);
1217
+ const query2 = new Query(childStdin, child.stdout, processExitPromise, canCallTool);
1094
1218
  child.on("error", (error) => {
1095
1219
  if (config.options?.abort?.aborted) {
1096
1220
  query2.setError(new AbortError("Claude Code process aborted by user"));
@@ -1108,17 +1232,48 @@ function query(config) {
1108
1232
  return query2;
1109
1233
  }
1110
1234
 
1111
- async function awaitFileExist(file, timeout = 1e4) {
1112
- const startTime = Date.now();
1113
- while (Date.now() - startTime < timeout) {
1114
- try {
1115
- await promises.access(file);
1116
- return true;
1117
- } catch (e) {
1118
- await types$1.delay(1e3);
1119
- }
1235
+ function parseCompact(message) {
1236
+ const trimmed = message.trim();
1237
+ if (trimmed === "/compact") {
1238
+ return {
1239
+ isCompact: true,
1240
+ originalMessage: trimmed
1241
+ };
1120
1242
  }
1121
- return false;
1243
+ if (trimmed.startsWith("/compact ")) {
1244
+ return {
1245
+ isCompact: true,
1246
+ originalMessage: trimmed
1247
+ };
1248
+ }
1249
+ return {
1250
+ isCompact: false,
1251
+ originalMessage: message
1252
+ };
1253
+ }
1254
+ function parseClear(message) {
1255
+ const trimmed = message.trim();
1256
+ return {
1257
+ isClear: trimmed === "/clear"
1258
+ };
1259
+ }
1260
+ function parseSpecialCommand(message) {
1261
+ const compactResult = parseCompact(message);
1262
+ if (compactResult.isCompact) {
1263
+ return {
1264
+ type: "compact",
1265
+ originalMessage: compactResult.originalMessage
1266
+ };
1267
+ }
1268
+ const clearResult = parseClear(message);
1269
+ if (clearResult.isClear) {
1270
+ return {
1271
+ type: "clear"
1272
+ };
1273
+ }
1274
+ return {
1275
+ type: null
1276
+ };
1122
1277
  }
1123
1278
 
1124
1279
  class PushableAsyncIterable {
@@ -1247,48 +1402,17 @@ class PushableAsyncIterable {
1247
1402
  }
1248
1403
  }
1249
1404
 
1250
- function parseCompact(message) {
1251
- const trimmed = message.trim();
1252
- if (trimmed === "/compact") {
1253
- return {
1254
- isCompact: true,
1255
- originalMessage: trimmed
1256
- };
1257
- }
1258
- if (trimmed.startsWith("/compact ")) {
1259
- return {
1260
- isCompact: true,
1261
- originalMessage: trimmed
1262
- };
1263
- }
1264
- return {
1265
- isCompact: false,
1266
- originalMessage: message
1267
- };
1268
- }
1269
- function parseClear(message) {
1270
- const trimmed = message.trim();
1271
- return {
1272
- isClear: trimmed === "/clear"
1273
- };
1274
- }
1275
- function parseSpecialCommand(message) {
1276
- const compactResult = parseCompact(message);
1277
- if (compactResult.isCompact) {
1278
- return {
1279
- type: "compact",
1280
- originalMessage: compactResult.originalMessage
1281
- };
1282
- }
1283
- const clearResult = parseClear(message);
1284
- if (clearResult.isClear) {
1285
- return {
1286
- type: "clear"
1287
- };
1405
+ async function awaitFileExist(file, timeout = 1e4) {
1406
+ const startTime = Date.now();
1407
+ while (Date.now() - startTime < timeout) {
1408
+ try {
1409
+ await promises.access(file);
1410
+ return true;
1411
+ } catch (e) {
1412
+ await types$1.delay(1e3);
1413
+ }
1288
1414
  }
1289
- return {
1290
- type: null
1291
- };
1415
+ return false;
1292
1416
  }
1293
1417
 
1294
1418
  async function claudeRemote(opts) {
@@ -1301,32 +1425,12 @@ async function claudeRemote(opts) {
1301
1425
  process.env[key] = value;
1302
1426
  });
1303
1427
  }
1304
- let response;
1305
- const sdkOptions = {
1306
- cwd: opts.path,
1307
- resume: startFrom ?? void 0,
1308
- mcpServers: opts.mcpServers,
1309
- permissionPromptToolName: opts.permissionPromptToolName,
1310
- permissionMode: opts.permissionMode,
1311
- model: opts.model,
1312
- fallbackModel: opts.fallbackModel,
1313
- customSystemPrompt: opts.customSystemPrompt,
1314
- appendSystemPrompt: opts.appendSystemPrompt,
1315
- allowedTools: opts.allowedTools,
1316
- disallowedTools: opts.disallowedTools,
1317
- executable: "node",
1318
- abort: opts.signal,
1319
- pathToClaudeCodeExecutable: (() => {
1320
- return node_path.resolve(node_path.join(projectPath(), "scripts", "claude_remote_launcher.cjs"));
1321
- })()
1322
- };
1323
- if (opts.claudeArgs && opts.claudeArgs.length > 0) {
1324
- sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
1428
+ const initial = await opts.nextMessage();
1429
+ if (!initial) {
1430
+ return;
1325
1431
  }
1326
- types$1.logger.debug(`[claudeRemote] Starting query with permission mode: ${opts.permissionMode}, model: ${opts.model || "default"}, fallbackModel: ${opts.fallbackModel || "none"}, customSystemPrompt: ${opts.customSystemPrompt ? "set" : "none"}, appendSystemPrompt: ${opts.appendSystemPrompt ? "set" : "none"}, allowedTools: ${opts.allowedTools ? opts.allowedTools.join(",") : "none"}, disallowedTools: ${opts.disallowedTools ? opts.disallowedTools.join(",") : "none"}`);
1327
- const specialCommand = parseSpecialCommand(opts.message);
1432
+ const specialCommand = parseSpecialCommand(initial.message);
1328
1433
  if (specialCommand.type === "clear") {
1329
- types$1.logger.debug("[claudeRemote] /clear command detected - should not reach here, handled in start.ts");
1330
1434
  if (opts.onCompletionEvent) {
1331
1435
  opts.onCompletionEvent("Context was reset");
1332
1436
  }
@@ -1335,23 +1439,33 @@ async function claudeRemote(opts) {
1335
1439
  }
1336
1440
  return;
1337
1441
  }
1442
+ let isCompactCommand = false;
1338
1443
  if (specialCommand.type === "compact") {
1339
1444
  types$1.logger.debug("[claudeRemote] /compact command detected - will process as normal but with compaction behavior");
1340
- }
1341
- const isCompactCommand = specialCommand.type === "compact";
1342
- let message = new PushableAsyncIterable();
1343
- message.push({
1344
- type: "user",
1345
- message: {
1346
- role: "user",
1347
- content: opts.message
1445
+ isCompactCommand = true;
1446
+ if (opts.onCompletionEvent) {
1447
+ opts.onCompletionEvent("Compaction started");
1348
1448
  }
1349
- });
1350
- message.end();
1351
- response = query({
1352
- prompt: message,
1353
- options: sdkOptions
1354
- });
1449
+ }
1450
+ let mode = initial.mode;
1451
+ const sdkOptions = {
1452
+ cwd: opts.path,
1453
+ resume: startFrom ?? void 0,
1454
+ mcpServers: opts.mcpServers,
1455
+ permissionMode: initial.mode.permissionMode === "plan" ? "plan" : "default",
1456
+ model: initial.mode.model,
1457
+ fallbackModel: initial.mode.fallbackModel,
1458
+ customSystemPrompt: initial.mode.customSystemPrompt ? initial.mode.customSystemPrompt + "\n\n" + systemPrompt : void 0,
1459
+ appendSystemPrompt: initial.mode.appendSystemPrompt ? initial.mode.appendSystemPrompt + "\n\n" + systemPrompt : systemPrompt,
1460
+ allowedTools: initial.mode.allowedTools ? initial.mode.allowedTools.concat(opts.allowedTools) : opts.allowedTools,
1461
+ disallowedTools: initial.mode.disallowedTools,
1462
+ canCallTool: (toolName, input, options) => opts.canCallTool(toolName, input, mode, options),
1463
+ executable: "node",
1464
+ abort: opts.signal,
1465
+ pathToClaudeCodeExecutable: (() => {
1466
+ return node_path.resolve(node_path.join(projectPath(), "scripts", "claude_remote_launcher.cjs"));
1467
+ })()
1468
+ };
1355
1469
  let thinking = false;
1356
1470
  const updateThinking = (newThinking) => {
1357
1471
  if (thinking !== newThinking) {
@@ -1362,15 +1476,27 @@ async function claudeRemote(opts) {
1362
1476
  }
1363
1477
  }
1364
1478
  };
1479
+ let messages = new PushableAsyncIterable();
1480
+ messages.push({
1481
+ type: "user",
1482
+ message: {
1483
+ role: "user",
1484
+ content: initial.message
1485
+ }
1486
+ });
1487
+ const response = query({
1488
+ prompt: messages,
1489
+ options: sdkOptions
1490
+ });
1365
1491
  updateThinking(true);
1366
1492
  try {
1367
1493
  types$1.logger.debug(`[claudeRemote] Starting to iterate over response`);
1368
- for await (const message2 of response) {
1369
- types$1.logger.debugLargeJson(`[claudeRemote] Message ${message2.type}`, message2);
1370
- opts.onMessage(message2);
1371
- if (message2.type === "system" && message2.subtype === "init") {
1494
+ for await (const message of response) {
1495
+ types$1.logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
1496
+ opts.onMessage(message);
1497
+ if (message.type === "system" && message.subtype === "init") {
1372
1498
  updateThinking(true);
1373
- const systemInit = message2;
1499
+ const systemInit = message;
1374
1500
  if (systemInit.session_id) {
1375
1501
  types$1.logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${systemInit.session_id}`);
1376
1502
  const projectDir = getProjectPath(opts.path);
@@ -1378,14 +1504,8 @@ async function claudeRemote(opts) {
1378
1504
  types$1.logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
1379
1505
  opts.onSessionFound(systemInit.session_id);
1380
1506
  }
1381
- if (isCompactCommand) {
1382
- types$1.logger.debug("[claudeRemote] Compaction started");
1383
- if (opts.onCompletionEvent) {
1384
- opts.onCompletionEvent("Compaction started");
1385
- }
1386
- }
1387
1507
  }
1388
- if (message2.type === "result") {
1508
+ if (message.type === "result") {
1389
1509
  updateThinking(false);
1390
1510
  types$1.logger.debug("[claudeRemote] Result received, exiting claudeRemote");
1391
1511
  if (isCompactCommand) {
@@ -1393,26 +1513,28 @@ async function claudeRemote(opts) {
1393
1513
  if (opts.onCompletionEvent) {
1394
1514
  opts.onCompletionEvent("Compaction completed");
1395
1515
  }
1516
+ isCompactCommand = false;
1396
1517
  }
1397
- return;
1518
+ const next = await opts.nextMessage();
1519
+ if (!next) {
1520
+ messages.end();
1521
+ return;
1522
+ }
1523
+ mode = next.mode;
1524
+ messages.push({ type: "user", message: { role: "user", content: next.message } });
1398
1525
  }
1399
- if (message2.type === "user") {
1400
- const msg = message2;
1526
+ if (message.type === "user") {
1527
+ const msg = message;
1401
1528
  if (msg.message.role === "user" && Array.isArray(msg.message.content)) {
1402
1529
  for (let c of msg.message.content) {
1403
- if (c.type === "tool_result" && (c.name === "exit_plan_mode" || c.name === "ExitPlanMode")) {
1404
- types$1.logger.debug("[claudeRemote] Plan result received, exiting claudeRemote");
1405
- return;
1406
- }
1407
- if (c.type === "tool_result" && c.tool_use_id && opts.responses.has(c.tool_use_id) && !opts.responses.get(c.tool_use_id).approved) {
1408
- types$1.logger.debug("[claudeRemote] Tool rejected, exiting claudeRemote");
1530
+ if (c.type === "tool_result" && c.tool_use_id && opts.isAborted(c.tool_use_id)) {
1531
+ types$1.logger.debug("[claudeRemote] Tool aborted, exiting claudeRemote");
1409
1532
  return;
1410
1533
  }
1411
1534
  }
1412
1535
  }
1413
1536
  }
1414
1537
  }
1415
- types$1.logger.debug(`[claudeRemote] Finished iterating over response`);
1416
1538
  } catch (e) {
1417
1539
  if (e instanceof AbortError) {
1418
1540
  types$1.logger.debug(`[claudeRemote] Aborted`);
@@ -1422,71 +1544,11 @@ async function claudeRemote(opts) {
1422
1544
  } finally {
1423
1545
  updateThinking(false);
1424
1546
  }
1425
- types$1.logger.debug(`[claudeRemote] Function completed`);
1426
1547
  }
1427
1548
 
1428
1549
  const PLAN_FAKE_REJECT = `User approved plan, but you need to be restarted. STOP IMMEDIATELY TO SWITCH FROM PLAN MODE. DO NOT REPLY TO THIS MESSAGE.`;
1429
1550
  const PLAN_FAKE_RESTART = `PlEaZe Continue with plan.`;
1430
1551
 
1431
- async function startPermissionServerV2(handler) {
1432
- const mcp = new mcp_js.McpServer({
1433
- name: "Permission Server",
1434
- version: "1.0.0",
1435
- description: "A server that allows you to request permissions from the user"
1436
- });
1437
- mcp.registerTool("ask_permission", {
1438
- description: "Request permission to execute a tool",
1439
- title: "Request Permission",
1440
- inputSchema: {
1441
- tool_name: z.z.string().describe("The tool that needs permission"),
1442
- input: z.z.any().describe("The arguments for the tool")
1443
- }
1444
- }, async (args) => {
1445
- const response = await handler({ name: args.tool_name, arguments: args.input });
1446
- types$1.logger.debugLargeJson("[permissionServerV2] Response", response);
1447
- 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.` };
1448
- return {
1449
- content: [
1450
- {
1451
- type: "text",
1452
- text: JSON.stringify(result)
1453
- }
1454
- ],
1455
- isError: false
1456
- };
1457
- });
1458
- const transport = new streamableHttp_js.StreamableHTTPServerTransport({
1459
- // NOTE: Returning session id here will result in claude
1460
- // sdk spawn to fail with `Invalid Request: Server already initialized`
1461
- sessionIdGenerator: void 0
1462
- });
1463
- await mcp.connect(transport);
1464
- const server = node_http.createServer(async (req, res) => {
1465
- try {
1466
- await transport.handleRequest(req, res);
1467
- } catch (error) {
1468
- types$1.logger.debug("Error handling request:", error);
1469
- if (!res.headersSent) {
1470
- res.writeHead(500).end();
1471
- }
1472
- }
1473
- });
1474
- const baseUrl = await new Promise((resolve) => {
1475
- server.listen(0, "127.0.0.1", () => {
1476
- const addr = server.address();
1477
- resolve(new URL(`http://127.0.0.1:${addr.port}`));
1478
- });
1479
- });
1480
- return {
1481
- url: baseUrl.toString(),
1482
- toolName: "ask_permission",
1483
- stop: () => {
1484
- mcp.close();
1485
- server.close();
1486
- }
1487
- };
1488
- }
1489
-
1490
1552
  function deepEqual(a, b) {
1491
1553
  if (a === b) return true;
1492
1554
  if (a == null || b == null) return false;
@@ -1498,136 +1560,181 @@ function deepEqual(a, b) {
1498
1560
  if (!keysB.includes(key)) return false;
1499
1561
  if (!deepEqual(a[key], b[key])) return false;
1500
1562
  }
1501
- return true;
1563
+ return true;
1564
+ }
1565
+
1566
+ const STANDARD_TOOLS = {
1567
+ // File operations
1568
+ "Read": "Read File",
1569
+ "Write": "Write File",
1570
+ "Edit": "Edit File",
1571
+ "MultiEdit": "Edit File",
1572
+ "NotebookEdit": "Edit Notebook",
1573
+ // Search and navigation
1574
+ "Glob": "Find Files",
1575
+ "Grep": "Search in Files",
1576
+ "LS": "List Directory",
1577
+ // Command execution
1578
+ "Bash": "Run Command",
1579
+ "BashOutput": "Check Command Output",
1580
+ "KillBash": "Stop Command",
1581
+ // Task management
1582
+ "TodoWrite": "Update Tasks",
1583
+ "TodoRead": "Read Tasks",
1584
+ "Task": "Launch Agent",
1585
+ // Web tools
1586
+ "WebFetch": "Fetch Web Page",
1587
+ "WebSearch": "Search Web",
1588
+ // Special cases
1589
+ "exit_plan_mode": "Execute Plan",
1590
+ "ExitPlanMode": "Execute Plan"
1591
+ };
1592
+ function toTitleCase(str) {
1593
+ return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
1594
+ }
1595
+ function getToolName(toolName) {
1596
+ if (STANDARD_TOOLS[toolName]) {
1597
+ return STANDARD_TOOLS[toolName];
1598
+ }
1599
+ if (toolName.startsWith("mcp__")) {
1600
+ const parts = toolName.split("__");
1601
+ if (parts.length >= 3) {
1602
+ const server = toTitleCase(parts[1]);
1603
+ const action = toTitleCase(parts.slice(2).join("_"));
1604
+ return `${server}: ${action}`;
1605
+ }
1606
+ }
1607
+ return toTitleCase(toolName);
1502
1608
  }
1503
1609
 
1504
- async function startPermissionResolver(session) {
1505
- let toolCalls = [];
1506
- let responses = /* @__PURE__ */ new Map();
1507
- let requests = /* @__PURE__ */ new Map();
1508
- let pendingPermissionRequests = [];
1509
- const server = await startPermissionServerV2(async (request) => {
1510
- const id = resolveToolCallId(request.name, request.arguments);
1511
- if (!id) {
1512
- types$1.logger.debug(`Tool call ID not yet available for ${request.name}, queueing request`);
1513
- return new Promise((resolve, reject) => {
1514
- const timeout = setTimeout(() => {
1515
- const idx = pendingPermissionRequests.findIndex((p) => p.request === request);
1516
- if (idx !== -1) {
1517
- pendingPermissionRequests.splice(idx, 1);
1518
- reject(new Error(`Timeout: Tool call ID never arrived for ${request.name}`));
1519
- }
1520
- }, 3e4);
1521
- pendingPermissionRequests.push({ request, resolve, reject, timeout });
1522
- });
1523
- }
1524
- return handlePermissionRequest(id, request);
1525
- });
1526
- function handlePermissionRequest(id, request) {
1527
- let promise = new Promise((resolve) => {
1528
- if (request.name === "exit_plan_mode" || request.name === "ExitPlanMode") {
1529
- const wrappedResolve = (response) => {
1530
- if (response.approved) {
1531
- types$1.logger.debug("Plan approved - injecting PLAN_FAKE_RESTART");
1532
- if (response.mode && ["default", "acceptEdits", "bypassPermissions"].includes(response.mode)) {
1533
- session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: response.mode });
1534
- } else {
1535
- session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: "default" });
1536
- }
1537
- resolve({ approved: false, reason: PLAN_FAKE_REJECT });
1538
- } else {
1539
- resolve(response);
1540
- }
1541
- };
1542
- requests.set(id, wrappedResolve);
1543
- } else {
1544
- requests.set(id, resolve);
1545
- }
1546
- });
1547
- let timeout = setTimeout(async () => {
1548
- types$1.logger.debug("Permission timeout - attempting to interrupt Claude");
1549
- requests.delete(id);
1550
- session.client.updateAgentState((currentState) => {
1551
- const request2 = currentState.requests?.[id];
1552
- if (!request2) return currentState;
1553
- let r = { ...currentState.requests };
1554
- delete r[id];
1555
- return {
1556
- ...currentState,
1557
- requests: r,
1558
- completedRequests: {
1559
- ...currentState.completedRequests,
1560
- [id]: {
1561
- ...request2,
1562
- completedAt: Date.now(),
1563
- status: "canceled",
1564
- reason: "Timeout"
1565
- }
1566
- }
1567
- };
1568
- });
1569
- }, 1e3 * 60 * 4.5);
1570
- types$1.logger.debug("Permission request" + id + " " + JSON.stringify(request));
1571
- session.api.push().sendToAllDevices(
1572
- "Permission Request",
1573
- `Claude wants to use ${request.name}`,
1574
- {
1575
- sessionId: session.client.sessionId,
1576
- requestId: id,
1577
- tool: request.name,
1578
- type: "permission_request"
1579
- }
1580
- );
1581
- session.client.updateAgentState((currentState) => ({
1582
- ...currentState,
1583
- requests: {
1584
- ...currentState.requests,
1585
- [id]: {
1586
- tool: request.name,
1587
- arguments: request.arguments,
1588
- createdAt: Date.now()
1610
+ function getToolDescriptor(toolName) {
1611
+ if (toolName === "exit_plan_mode" || toolName === "ExitPlanMode") {
1612
+ return { edit: false, exitPlan: true };
1613
+ }
1614
+ if (toolName === "Edit" || toolName === "MultiEdit" || toolName === "Write" || toolName === "NotebookEdit") {
1615
+ return { edit: true, exitPlan: false };
1616
+ }
1617
+ return { edit: false, exitPlan: false };
1618
+ }
1619
+
1620
+ class PermissionHandler {
1621
+ toolCalls = [];
1622
+ responses = /* @__PURE__ */ new Map();
1623
+ pendingRequests = /* @__PURE__ */ new Map();
1624
+ session;
1625
+ allowedTools = /* @__PURE__ */ new Set();
1626
+ permissionMode = "default";
1627
+ constructor(session) {
1628
+ this.session = session;
1629
+ this.setupClientHandler();
1630
+ }
1631
+ handleModeChange(mode) {
1632
+ this.permissionMode = mode;
1633
+ }
1634
+ /**
1635
+ * Handler response
1636
+ */
1637
+ handlePermissionResponse(response, pending) {
1638
+ if (response.allowTools && response.allowTools.length > 0) {
1639
+ response.allowTools.forEach((tool) => this.allowedTools.add(tool));
1640
+ }
1641
+ if (response.mode) {
1642
+ this.permissionMode = response.mode;
1643
+ }
1644
+ if (pending.toolName === "exit_plan_mode" || pending.toolName === "ExitPlanMode") {
1645
+ types$1.logger.debug("Plan mode result received", response);
1646
+ if (response.approved) {
1647
+ types$1.logger.debug("Plan approved - injecting PLAN_FAKE_RESTART");
1648
+ if (response.mode && ["default", "acceptEdits", "bypassPermissions"].includes(response.mode)) {
1649
+ this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: response.mode });
1650
+ } else {
1651
+ this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: "default" });
1589
1652
  }
1653
+ pending.resolve({ behavior: "deny", message: PLAN_FAKE_REJECT });
1654
+ } else {
1655
+ pending.resolve({ behavior: "deny", message: response.reason || "Plan rejected" });
1590
1656
  }
1591
- }));
1592
- promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
1593
- return promise;
1594
- }
1595
- session.client.setHandler("permission", async (message) => {
1596
- types$1.logger.debug("Permission response" + JSON.stringify(message));
1597
- const id = message.id;
1598
- const resolve = requests.get(id);
1599
- if (resolve) {
1600
- responses.set(id, message);
1601
- resolve({ approved: message.approved, reason: message.reason, mode: message.mode });
1602
- requests.delete(id);
1603
1657
  } else {
1604
- types$1.logger.debug("Permission request stale, likely timed out");
1605
- return;
1658
+ const result = response.approved ? { behavior: "allow", updatedInput: pending.input || {} } : { behavior: "deny", message: response.reason || `The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.` };
1659
+ pending.resolve(result);
1606
1660
  }
1607
- session.client.updateAgentState((currentState) => {
1608
- const request = currentState.requests?.[id];
1609
- if (!request) return currentState;
1610
- let r = { ...currentState.requests };
1611
- delete r[id];
1612
- const isExitPlanModeSuccess = request.tool === "exit_plan_mode" && !message.approved && message.reason === PLAN_FAKE_REJECT;
1613
- return {
1661
+ }
1662
+ /**
1663
+ * Creates the canCallTool callback for the SDK
1664
+ */
1665
+ handleToolCall = async (toolName, input, mode, options) => {
1666
+ if (this.allowedTools.has(toolName)) {
1667
+ return { behavior: "allow", updatedInput: input };
1668
+ }
1669
+ const descriptor = getToolDescriptor(toolName);
1670
+ if (this.permissionMode === "bypassPermissions") {
1671
+ return { behavior: "allow", updatedInput: input };
1672
+ }
1673
+ if (this.permissionMode === "acceptEdits" && descriptor.edit) {
1674
+ return { behavior: "allow", updatedInput: input };
1675
+ }
1676
+ let toolCallId = this.resolveToolCallId(toolName, input);
1677
+ if (!toolCallId) {
1678
+ await types$1.delay(1e3);
1679
+ toolCallId = this.resolveToolCallId(toolName, input);
1680
+ if (!toolCallId) {
1681
+ throw new Error(`Could not resolve tool call ID for ${toolName}`);
1682
+ }
1683
+ }
1684
+ return this.handlePermissionRequest(toolCallId, toolName, input, options.signal);
1685
+ };
1686
+ /**
1687
+ * Handles individual permission requests
1688
+ */
1689
+ async handlePermissionRequest(id, toolName, input, signal) {
1690
+ return new Promise((resolve, reject) => {
1691
+ const abortHandler = () => {
1692
+ this.pendingRequests.delete(id);
1693
+ reject(new Error("Permission request aborted"));
1694
+ };
1695
+ signal.addEventListener("abort", abortHandler, { once: true });
1696
+ this.pendingRequests.set(id, {
1697
+ resolve: (result) => {
1698
+ signal.removeEventListener("abort", abortHandler);
1699
+ resolve(result);
1700
+ },
1701
+ reject: (error) => {
1702
+ signal.removeEventListener("abort", abortHandler);
1703
+ reject(error);
1704
+ },
1705
+ toolName,
1706
+ input
1707
+ });
1708
+ this.session.api.push().sendToAllDevices(
1709
+ "Permission Request",
1710
+ `Claude wants to ${getToolName(toolName)}`,
1711
+ {
1712
+ sessionId: this.session.client.sessionId,
1713
+ requestId: id,
1714
+ tool: toolName,
1715
+ type: "permission_request"
1716
+ }
1717
+ );
1718
+ this.session.client.updateAgentState((currentState) => ({
1614
1719
  ...currentState,
1615
- requests: r,
1616
- completedRequests: {
1617
- ...currentState.completedRequests,
1720
+ requests: {
1721
+ ...currentState.requests,
1618
1722
  [id]: {
1619
- ...request,
1620
- completedAt: Date.now(),
1621
- status: isExitPlanModeSuccess ? "approved" : message.approved ? "approved" : "denied",
1622
- reason: isExitPlanModeSuccess ? "Plan approved" : message.reason
1723
+ tool: toolName,
1724
+ arguments: input,
1725
+ createdAt: Date.now()
1623
1726
  }
1624
1727
  }
1625
- };
1728
+ }));
1729
+ types$1.logger.debug(`Permission request sent for tool call ${id}: ${toolName}`);
1626
1730
  });
1627
- });
1628
- const resolveToolCallId = (name, args) => {
1629
- for (let i = toolCalls.length - 1; i >= 0; i--) {
1630
- const call = toolCalls[i];
1731
+ }
1732
+ /**
1733
+ * Resolves tool call ID based on tool name and input
1734
+ */
1735
+ resolveToolCallId(name, args) {
1736
+ for (let i = this.toolCalls.length - 1; i >= 0; i--) {
1737
+ const call = this.toolCalls[i];
1631
1738
  if (call.name === name && deepEqual(call.input, args)) {
1632
1739
  if (call.used) {
1633
1740
  return null;
@@ -1637,59 +1744,22 @@ async function startPermissionResolver(session) {
1637
1744
  }
1638
1745
  }
1639
1746
  return null;
1640
- };
1641
- function reset() {
1642
- toolCalls = [];
1643
- requests.clear();
1644
- responses.clear();
1645
- for (const pending of pendingPermissionRequests) {
1646
- clearTimeout(pending.timeout);
1647
- }
1648
- pendingPermissionRequests = [];
1649
- session.client.updateAgentState((currentState) => {
1650
- const pendingRequests = currentState.requests || {};
1651
- const completedRequests = { ...currentState.completedRequests };
1652
- for (const [id, request] of Object.entries(pendingRequests)) {
1653
- completedRequests[id] = {
1654
- ...request,
1655
- completedAt: Date.now(),
1656
- status: "canceled",
1657
- reason: "Session switched to local mode"
1658
- };
1659
- }
1660
- return {
1661
- ...currentState,
1662
- requests: {},
1663
- // Clear all pending requests
1664
- completedRequests
1665
- };
1666
- });
1667
1747
  }
1668
- function onMessage(message) {
1748
+ /**
1749
+ * Handles messages to track tool calls
1750
+ */
1751
+ onMessage(message) {
1669
1752
  if (message.type === "assistant") {
1670
1753
  const assistantMsg = message;
1671
1754
  if (assistantMsg.message && assistantMsg.message.content) {
1672
1755
  for (const block of assistantMsg.message.content) {
1673
1756
  if (block.type === "tool_use") {
1674
- toolCalls.push({
1757
+ this.toolCalls.push({
1675
1758
  id: block.id,
1676
1759
  name: block.name,
1677
1760
  input: block.input,
1678
1761
  used: false
1679
1762
  });
1680
- for (let i = pendingPermissionRequests.length - 1; i >= 0; i--) {
1681
- const pending = pendingPermissionRequests[i];
1682
- if (pending.request.name === block.name && deepEqual(pending.request.arguments, block.input)) {
1683
- types$1.logger.debug(`Resolving pending permission request for ${block.name} with ID ${block.id}`);
1684
- clearTimeout(pending.timeout);
1685
- pendingPermissionRequests.splice(i, 1);
1686
- handlePermissionRequest(block.id, pending.request).then(
1687
- pending.resolve,
1688
- pending.reject
1689
- );
1690
- break;
1691
- }
1692
- }
1693
1763
  }
1694
1764
  }
1695
1765
  }
@@ -1699,7 +1769,7 @@ async function startPermissionResolver(session) {
1699
1769
  if (userMsg.message && userMsg.message.content && Array.isArray(userMsg.message.content)) {
1700
1770
  for (const block of userMsg.message.content) {
1701
1771
  if (block.type === "tool_result" && block.tool_use_id) {
1702
- const toolCall = toolCalls.find((tc) => tc.id === block.tool_use_id);
1772
+ const toolCall = this.toolCalls.find((tc) => tc.id === block.tool_use_id);
1703
1773
  if (toolCall && !toolCall.used) {
1704
1774
  toolCall.used = true;
1705
1775
  }
@@ -1708,12 +1778,92 @@ async function startPermissionResolver(session) {
1708
1778
  }
1709
1779
  }
1710
1780
  }
1711
- return {
1712
- server,
1713
- reset,
1714
- onMessage,
1715
- responses
1716
- };
1781
+ /**
1782
+ * Checks if a tool call is rejected
1783
+ */
1784
+ isAborted(toolCallId) {
1785
+ if (this.responses.get(toolCallId)?.approved === false) {
1786
+ return true;
1787
+ }
1788
+ const toolCall = this.toolCalls.find((tc) => tc.id === toolCallId);
1789
+ if (toolCall && (toolCall.name === "exit_plan_mode" || toolCall.name === "ExitPlanMode")) {
1790
+ return true;
1791
+ }
1792
+ return false;
1793
+ }
1794
+ /**
1795
+ * Resets all state for new sessions
1796
+ */
1797
+ reset() {
1798
+ this.toolCalls = [];
1799
+ this.responses.clear();
1800
+ for (const [, pending] of this.pendingRequests.entries()) {
1801
+ pending.reject(new Error("Session reset"));
1802
+ }
1803
+ this.pendingRequests.clear();
1804
+ this.session.client.updateAgentState((currentState) => {
1805
+ const pendingRequests = currentState.requests || {};
1806
+ const completedRequests = { ...currentState.completedRequests };
1807
+ for (const [id, request] of Object.entries(pendingRequests)) {
1808
+ completedRequests[id] = {
1809
+ ...request,
1810
+ completedAt: Date.now(),
1811
+ status: "canceled",
1812
+ reason: "Session switched to local mode"
1813
+ };
1814
+ }
1815
+ return {
1816
+ ...currentState,
1817
+ requests: {},
1818
+ // Clear all pending requests
1819
+ completedRequests
1820
+ };
1821
+ });
1822
+ }
1823
+ /**
1824
+ * Sets up the client handler for permission responses
1825
+ */
1826
+ setupClientHandler() {
1827
+ this.session.client.setHandler("permission", async (message) => {
1828
+ types$1.logger.debug(`Permission response: ${JSON.stringify(message)}`);
1829
+ const id = message.id;
1830
+ const pending = this.pendingRequests.get(id);
1831
+ if (!pending) {
1832
+ types$1.logger.debug("Permission request not found or already resolved");
1833
+ return;
1834
+ }
1835
+ this.responses.set(id, { ...message, receivedAt: Date.now() });
1836
+ this.pendingRequests.delete(id);
1837
+ this.handlePermissionResponse(message, pending);
1838
+ this.session.client.updateAgentState((currentState) => {
1839
+ const request = currentState.requests?.[id];
1840
+ if (!request) return currentState;
1841
+ let r = { ...currentState.requests };
1842
+ delete r[id];
1843
+ return {
1844
+ ...currentState,
1845
+ requests: r,
1846
+ completedRequests: {
1847
+ ...currentState.completedRequests,
1848
+ [id]: {
1849
+ ...request,
1850
+ completedAt: Date.now(),
1851
+ status: message.approved ? "approved" : "denied",
1852
+ reason: message.reason,
1853
+ mode: message.mode,
1854
+ allowTools: message.allowTools
1855
+ }
1856
+ }
1857
+ };
1858
+ });
1859
+ });
1860
+ }
1861
+ /**
1862
+ * Gets the responses map (for compatibility with existing code)
1863
+ */
1864
+ getResponses() {
1865
+ return this.responses;
1866
+ }
1717
1867
  }
1718
1868
 
1719
1869
  function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
@@ -2087,15 +2237,6 @@ async function claudeRemoteLauncher(session) {
2087
2237
  }
2088
2238
  process.stdin.setEncoding("utf8");
2089
2239
  }
2090
- const scanner = await createSessionScanner({
2091
- sessionId: session.sessionId,
2092
- workingDirectory: session.path,
2093
- onMessage: (message) => {
2094
- if (message.type === "summary") {
2095
- session.client.sendClaudeSessionMessage(message);
2096
- }
2097
- }
2098
- });
2099
2240
  let exitReason = null;
2100
2241
  let abortController = null;
2101
2242
  let abortFuture = null;
@@ -2118,17 +2259,17 @@ async function claudeRemoteLauncher(session) {
2118
2259
  }
2119
2260
  session.client.setHandler("abort", doAbort);
2120
2261
  session.client.setHandler("switch", doSwitch);
2121
- const permissions = await startPermissionResolver(session);
2262
+ const permissionHandler = new PermissionHandler(session);
2122
2263
  const sdkToLogConverter = new SDKToLogConverter({
2123
2264
  sessionId: session.sessionId || "unknown",
2124
2265
  cwd: session.path,
2125
2266
  version: process.env.npm_package_version
2126
- }, permissions.responses);
2267
+ }, permissionHandler.getResponses());
2127
2268
  let planModeToolCalls = /* @__PURE__ */ new Set();
2128
2269
  let ongoingToolCalls = /* @__PURE__ */ new Map();
2129
2270
  function onMessage(message) {
2130
2271
  formatClaudeMessageForInk(message, messageBuffer);
2131
- permissions.onMessage(message);
2272
+ permissionHandler.onMessage(message);
2132
2273
  if (message.type === "assistant") {
2133
2274
  let umessage = message;
2134
2275
  if (umessage.message.content && Array.isArray(umessage.message.content)) {
@@ -2192,6 +2333,32 @@ async function claudeRemoteLauncher(session) {
2192
2333
  }
2193
2334
  const logMessage = sdkToLogConverter.convert(msg);
2194
2335
  if (logMessage) {
2336
+ if (logMessage.type === "user" && logMessage.message?.content) {
2337
+ const content = Array.isArray(logMessage.message.content) ? logMessage.message.content : [];
2338
+ for (let i = 0; i < content.length; i++) {
2339
+ const c = content[i];
2340
+ if (c.type === "tool_result" && c.tool_use_id) {
2341
+ const responses = permissionHandler.getResponses();
2342
+ const response = responses.get(c.tool_use_id);
2343
+ if (response) {
2344
+ const permissions = {
2345
+ date: response.receivedAt || Date.now(),
2346
+ result: response.approved ? "approved" : "denied"
2347
+ };
2348
+ if (response.mode) {
2349
+ permissions.mode = response.mode;
2350
+ }
2351
+ if (response.allowTools && response.allowTools.length > 0) {
2352
+ permissions.allowedTools = response.allowTools;
2353
+ }
2354
+ content[i] = {
2355
+ ...c,
2356
+ permissions
2357
+ };
2358
+ }
2359
+ }
2360
+ }
2361
+ }
2195
2362
  if (logMessage.type !== "system") {
2196
2363
  session.client.sendClaudeSessionMessage(logMessage);
2197
2364
  }
@@ -2211,58 +2378,57 @@ async function claudeRemoteLauncher(session) {
2211
2378
  }
2212
2379
  }
2213
2380
  try {
2381
+ let pending = null;
2214
2382
  while (!exitReason) {
2215
- types$1.logger.debug("[remote]: fetch next message");
2216
- abortController = new AbortController();
2217
- abortFuture = new Future();
2218
- const messageData = await session.queue.waitForMessagesAndGetAsString(abortController.signal);
2219
- if (!messageData || abortController.signal.aborted) {
2220
- types$1.logger.debug("[remote]: fetch next message done: no message or aborted");
2221
- abortFuture?.resolve(void 0);
2222
- if (exitReason) {
2223
- return exitReason;
2224
- } else {
2225
- continue;
2226
- }
2227
- }
2228
- types$1.logger.debug("[remote]: fetch next message done: message received");
2229
- abortFuture?.resolve(void 0);
2230
- abortFuture = null;
2231
- abortController = null;
2232
2383
  types$1.logger.debug("[remote]: launch");
2233
2384
  messageBuffer.addMessage("\u2550".repeat(40), "status");
2234
2385
  messageBuffer.addMessage("Starting new Claude session...", "status");
2235
- abortController = new AbortController();
2386
+ const controller = new AbortController();
2387
+ abortController = controller;
2236
2388
  abortFuture = new Future();
2237
- permissions.reset();
2389
+ permissionHandler.reset();
2238
2390
  sdkToLogConverter.resetParentChain();
2391
+ let modeHash = null;
2392
+ let mode = null;
2239
2393
  try {
2240
2394
  await claudeRemote({
2241
2395
  sessionId: session.sessionId,
2242
2396
  path: session.path,
2243
- responses: permissions.responses,
2244
- mcpServers: {
2245
- ...session.mcpServers,
2246
- permission: {
2247
- type: "http",
2248
- url: permissions.server.url
2397
+ allowedTools: session.allowedTools ?? [],
2398
+ mcpServers: session.mcpServers,
2399
+ canCallTool: permissionHandler.handleToolCall,
2400
+ isAborted: (toolCallId) => {
2401
+ return permissionHandler.isAborted(toolCallId);
2402
+ },
2403
+ nextMessage: async () => {
2404
+ if (pending) {
2405
+ let p = pending;
2406
+ pending = null;
2407
+ permissionHandler.handleModeChange(p.mode.permissionMode);
2408
+ return p;
2409
+ }
2410
+ let msg = await session.queue.waitForMessagesAndGetAsString(controller.signal);
2411
+ if (msg) {
2412
+ if (modeHash && msg.hash !== modeHash || msg.isolate) {
2413
+ types$1.logger.debug("[remote]: mode has changed, pending message");
2414
+ pending = msg;
2415
+ return null;
2416
+ }
2417
+ modeHash = msg.hash;
2418
+ mode = msg.mode;
2419
+ permissionHandler.handleModeChange(mode.permissionMode);
2420
+ return {
2421
+ message: msg.message,
2422
+ mode: msg.mode
2423
+ };
2249
2424
  }
2425
+ return null;
2250
2426
  },
2251
- permissionPromptToolName: "mcp__permission__" + permissions.server.toolName,
2252
- permissionMode: messageData.mode.permissionMode,
2253
- model: messageData.mode.model,
2254
- fallbackModel: messageData.mode.fallbackModel,
2255
- customSystemPrompt: messageData.mode.customSystemPrompt,
2256
- appendSystemPrompt: messageData.mode.appendSystemPrompt,
2257
- allowedTools: messageData.mode.allowedTools,
2258
- disallowedTools: messageData.mode.disallowedTools,
2259
2427
  onSessionFound: (sessionId) => {
2260
2428
  sdkToLogConverter.updateSessionId(sessionId);
2261
2429
  session.onSessionFound(sessionId);
2262
- scanner.onNewSession(sessionId);
2263
2430
  },
2264
2431
  onThinkingChange: session.onThinkingChange,
2265
- message: messageData.message,
2266
2432
  claudeEnvVars: session.claudeEnvVars,
2267
2433
  claudeArgs: session.claudeArgs,
2268
2434
  onMessage,
@@ -2280,11 +2446,13 @@ async function claudeRemoteLauncher(session) {
2280
2446
  session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
2281
2447
  }
2282
2448
  } catch (e) {
2449
+ types$1.logger.debug("[remote]: launch error", e);
2283
2450
  if (!exitReason) {
2284
2451
  session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
2285
2452
  continue;
2286
2453
  }
2287
2454
  } finally {
2455
+ types$1.logger.debug("[remote]: launch finally");
2288
2456
  for (let [toolCallId, { parentToolCallId }] of ongoingToolCalls) {
2289
2457
  const converted = sdkToLogConverter.generateInterruptedToolResult(toolCallId, parentToolCallId);
2290
2458
  if (converted) {
@@ -2297,11 +2465,13 @@ async function claudeRemoteLauncher(session) {
2297
2465
  abortFuture?.resolve(void 0);
2298
2466
  abortFuture = null;
2299
2467
  types$1.logger.debug("[remote]: launch done");
2300
- permissions.reset();
2468
+ permissionHandler.reset();
2469
+ modeHash = null;
2470
+ mode = null;
2301
2471
  }
2302
2472
  }
2303
2473
  } finally {
2304
- permissions.server.stop();
2474
+ permissionHandler.reset();
2305
2475
  process.stdin.off("data", abort);
2306
2476
  if (process.stdin.isTTY) {
2307
2477
  process.stdin.setRawMode(false);
@@ -2313,7 +2483,6 @@ async function claudeRemoteLauncher(session) {
2313
2483
  if (abortFuture) {
2314
2484
  abortFuture.resolve(void 0);
2315
2485
  }
2316
- await scanner.cleanup();
2317
2486
  }
2318
2487
  return exitReason || "exit";
2319
2488
  }
@@ -2330,6 +2499,7 @@ async function loop(opts) {
2330
2499
  mcpServers: opts.mcpServers,
2331
2500
  logPath,
2332
2501
  messageQueue: opts.messageQueue,
2502
+ allowedTools: opts.allowedTools,
2333
2503
  onModeChange: opts.onModeChange
2334
2504
  });
2335
2505
  if (opts.onSessionReady) {
@@ -2364,7 +2534,7 @@ async function loop(opts) {
2364
2534
  }
2365
2535
 
2366
2536
  var name = "happy-coder";
2367
- var version = "0.7.2";
2537
+ var version = "0.9.0-0";
2368
2538
  var description = "Claude Code session sharing CLI";
2369
2539
  var author = "Kirill Dubovitskiy";
2370
2540
  var license = "MIT";
@@ -2414,18 +2584,14 @@ var scripts = {
2414
2584
  test: "yarn build && vitest run",
2415
2585
  "test:watch": "vitest",
2416
2586
  "test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
2417
- dev: "yarn build && npx tsx src/index.ts",
2587
+ dev: "yarn build && DEBUG=1 npx tsx src/index.ts",
2418
2588
  "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
2419
2589
  "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
2420
2590
  prepublishOnly: "yarn build && yarn test",
2421
- "minor:publish": "yarn build && npm version minor && npm publish",
2422
- "patch:publish": "yarn build && npm version patch && npm publish",
2423
- "version:prerelease": "yarn build && npm version prerelease --preid=beta",
2424
- "publish:prerelease": "npm publish --tag beta",
2425
- "beta:publish": "yarn version:prerelease && yarn publish:prerelease"
2591
+ release: "release-it"
2426
2592
  };
2427
2593
  var dependencies = {
2428
- "@anthropic-ai/claude-code": "^1.0.73",
2594
+ "@anthropic-ai/claude-code": "^1.0.89",
2429
2595
  "@anthropic-ai/sdk": "^0.56.0",
2430
2596
  "@modelcontextprotocol/sdk": "^1.15.1",
2431
2597
  "@stablelib/base64": "^2.0.1",
@@ -2454,6 +2620,7 @@ var devDependencies = {
2454
2620
  eslint: "^9",
2455
2621
  "eslint-config-prettier": "^10",
2456
2622
  pkgroll: "^2.14.2",
2623
+ "release-it": "^19.0.4",
2457
2624
  shx: "^0.3.3",
2458
2625
  "ts-node": "^10",
2459
2626
  tsx: "^4.20.3",
@@ -2461,7 +2628,12 @@ var devDependencies = {
2461
2628
  vitest: "^3.2.4"
2462
2629
  };
2463
2630
  var resolutions = {
2464
- "whatwg-url": "14.2.0"
2631
+ "whatwg-url": "14.2.0",
2632
+ "parse-path": "7.0.3",
2633
+ "@types/parse-path": "7.0.3"
2634
+ };
2635
+ var publishConfig = {
2636
+ registry: "https://registry.npmjs.org"
2465
2637
  };
2466
2638
  var packageManager = "yarn@1.22.22";
2467
2639
  var packageJson = {
@@ -2484,6 +2656,7 @@ var packageJson = {
2484
2656
  dependencies: dependencies,
2485
2657
  devDependencies: devDependencies,
2486
2658
  resolutions: resolutions,
2659
+ publishConfig: publishConfig,
2487
2660
  packageManager: packageManager
2488
2661
  };
2489
2662
 
@@ -2870,15 +3043,17 @@ async function clearDaemonState() {
2870
3043
  }
2871
3044
 
2872
3045
  class MessageQueue2 {
2873
- constructor(modeHasher) {
2874
- this.modeHasher = modeHasher;
2875
- types$1.logger.debug(`[MessageQueue2] Initialized`);
2876
- }
2877
3046
  queue = [];
2878
3047
  // Made public for testing
2879
3048
  waiter = null;
2880
3049
  closed = false;
2881
3050
  onMessageHandler = null;
3051
+ modeHasher;
3052
+ constructor(modeHasher, onMessageHandler = null) {
3053
+ this.modeHasher = modeHasher;
3054
+ this.onMessageHandler = onMessageHandler;
3055
+ types$1.logger.debug(`[MessageQueue2] Initialized`);
3056
+ }
2882
3057
  /**
2883
3058
  * Set a handler that will be called when a message arrives
2884
3059
  */
@@ -3053,6 +3228,7 @@ class MessageQueue2 {
3053
3228
  const firstItem = this.queue[0];
3054
3229
  const sameModeMessages = [];
3055
3230
  let mode = firstItem.mode;
3231
+ let isolate = firstItem.isolate ?? false;
3056
3232
  const targetModeHash = firstItem.modeHash;
3057
3233
  if (firstItem.isolate) {
3058
3234
  const item = this.queue.shift();
@@ -3068,7 +3244,9 @@ class MessageQueue2 {
3068
3244
  const combinedMessage = sameModeMessages.join("\n");
3069
3245
  return {
3070
3246
  message: combinedMessage,
3071
- mode
3247
+ mode,
3248
+ hash: targetModeHash,
3249
+ isolate
3072
3250
  };
3073
3251
  }
3074
3252
  /**
@@ -3963,10 +4141,10 @@ async function doWebAuth(keypair) {
3963
4141
  console.log("\u2713 Browser opened\n");
3964
4142
  console.log("Complete authentication in your browser window.");
3965
4143
  } else {
3966
- console.log("Could not open browser automatically.\n");
3967
- console.log("Please open this URL manually:");
3968
- console.log(webUrl);
4144
+ console.log("Could not open browser automatically.");
3969
4145
  }
4146
+ console.log("\nIf the browser did not open, please copy and paste this URL:");
4147
+ console.log(webUrl);
3970
4148
  console.log("");
3971
4149
  return await waitForAuthentication(keypair);
3972
4150
  }
@@ -4323,6 +4501,88 @@ async function startDaemon() {
4323
4501
  }
4324
4502
  }
4325
4503
 
4504
+ async function startHappyServer(client) {
4505
+ const handler = async (title) => {
4506
+ types$1.logger.debug("[happyMCP] Changing title to:", title);
4507
+ try {
4508
+ client.sendClaudeSessionMessage({
4509
+ type: "summary",
4510
+ summary: title,
4511
+ leafUuid: node_crypto.randomUUID()
4512
+ });
4513
+ return { success: true };
4514
+ } catch (error) {
4515
+ return { success: false, error: String(error) };
4516
+ }
4517
+ };
4518
+ const mcp = new mcp_js.McpServer({
4519
+ name: "Happy MCP",
4520
+ version: "1.0.0",
4521
+ description: "Happy CLI MCP server with chat session management tools"
4522
+ });
4523
+ mcp.registerTool("change_title", {
4524
+ description: "Change the title of the current chat session",
4525
+ title: "Change Chat Title",
4526
+ inputSchema: {
4527
+ title: z.z.string().describe("The new title for the chat session")
4528
+ }
4529
+ }, async (args) => {
4530
+ const response = await handler(args.title);
4531
+ types$1.logger.debug("[happyMCP] Response:", response);
4532
+ if (response.success) {
4533
+ return {
4534
+ content: [
4535
+ {
4536
+ type: "text",
4537
+ text: `Successfully changed chat title to: "${args.title}"`
4538
+ }
4539
+ ],
4540
+ isError: false
4541
+ };
4542
+ } else {
4543
+ return {
4544
+ content: [
4545
+ {
4546
+ type: "text",
4547
+ text: `Failed to change chat title: ${response.error || "Unknown error"}`
4548
+ }
4549
+ ],
4550
+ isError: true
4551
+ };
4552
+ }
4553
+ });
4554
+ const transport = new streamableHttp_js.StreamableHTTPServerTransport({
4555
+ // NOTE: Returning session id here will result in claude
4556
+ // sdk spawn to fail with `Invalid Request: Server already initialized`
4557
+ sessionIdGenerator: void 0
4558
+ });
4559
+ await mcp.connect(transport);
4560
+ const server = node_http.createServer(async (req, res) => {
4561
+ try {
4562
+ await transport.handleRequest(req, res);
4563
+ } catch (error) {
4564
+ types$1.logger.debug("Error handling request:", error);
4565
+ if (!res.headersSent) {
4566
+ res.writeHead(500).end();
4567
+ }
4568
+ }
4569
+ });
4570
+ const baseUrl = await new Promise((resolve) => {
4571
+ server.listen(0, "127.0.0.1", () => {
4572
+ const addr = server.address();
4573
+ resolve(new URL(`http://127.0.0.1:${addr.port}`));
4574
+ });
4575
+ });
4576
+ return {
4577
+ url: baseUrl.toString(),
4578
+ toolNames: ["change_title"],
4579
+ stop: () => {
4580
+ mcp.close();
4581
+ server.close();
4582
+ }
4583
+ };
4584
+ }
4585
+
4326
4586
  async function start(credentials, options = {}) {
4327
4587
  const workingDirectory = process.cwd();
4328
4588
  const sessionTag = node_crypto.randomUUID();
@@ -4382,6 +4642,8 @@ async function start(credentials, options = {}) {
4382
4642
  }
4383
4643
  });
4384
4644
  const session = api.sessionSyncClient(response);
4645
+ const happyServer = await startHappyServer(session);
4646
+ types$1.logger.debug(`[START] Happy MCP server started at ${happyServer.url}`);
4385
4647
  const logPath = await types$1.logger.logFilePathPromise;
4386
4648
  types$1.logger.infoDeveloper(`Session: ${response.id}`);
4387
4649
  types$1.logger.infoDeveloper(`Logs: ${logPath}`);
@@ -4393,7 +4655,15 @@ async function start(credentials, options = {}) {
4393
4655
  if (caffeinateStarted) {
4394
4656
  types$1.logger.infoDeveloper("Sleep prevention enabled (macOS)");
4395
4657
  }
4396
- const messageQueue = new MessageQueue2((mode) => hashObject(mode));
4658
+ const messageQueue = new MessageQueue2((mode) => hashObject({
4659
+ isPlan: mode.permissionMode === "plan",
4660
+ model: mode.model,
4661
+ fallbackModel: mode.fallbackModel,
4662
+ customSystemPrompt: mode.customSystemPrompt,
4663
+ appendSystemPrompt: mode.appendSystemPrompt,
4664
+ allowedTools: mode.allowedTools,
4665
+ disallowedTools: mode.disallowedTools
4666
+ }));
4397
4667
  registerHandlers(session);
4398
4668
  let currentPermissionMode = options.permissionMode;
4399
4669
  let currentModel = options.model;
@@ -4516,6 +4786,7 @@ async function start(credentials, options = {}) {
4516
4786
  await session.close();
4517
4787
  }
4518
4788
  stopCaffeinate();
4789
+ happyServer.stop();
4519
4790
  types$1.logger.debug("[START] Cleanup complete, exiting");
4520
4791
  process.exit(0);
4521
4792
  } catch (error) {
@@ -4540,6 +4811,7 @@ async function start(credentials, options = {}) {
4540
4811
  startingMode: options.startingMode,
4541
4812
  messageQueue,
4542
4813
  api,
4814
+ allowedTools: happyServer.toolNames.map((toolName) => `mcp__happy__${toolName}`),
4543
4815
  onModeChange: (newMode) => {
4544
4816
  session.sendSessionEvent({ type: "switch", mode: newMode });
4545
4817
  session.updateAgentState((currentState) => ({
@@ -4549,7 +4821,12 @@ async function start(credentials, options = {}) {
4549
4821
  },
4550
4822
  onSessionReady: (sessionInstance) => {
4551
4823
  },
4552
- mcpServers: {},
4824
+ mcpServers: {
4825
+ "happy": {
4826
+ type: "http",
4827
+ url: happyServer.url
4828
+ }
4829
+ },
4553
4830
  session,
4554
4831
  claudeEnvVars: options.claudeEnvVars,
4555
4832
  claudeArgs: options.claudeArgs
@@ -4561,28 +4838,11 @@ async function start(credentials, options = {}) {
4561
4838
  await session.close();
4562
4839
  stopCaffeinate();
4563
4840
  types$1.logger.debug("Stopped sleep prevention");
4841
+ happyServer.stop();
4842
+ types$1.logger.debug("Stopped Happy MCP server");
4564
4843
  process.exit(0);
4565
4844
  }
4566
4845
 
4567
- function trimIdent(text) {
4568
- const lines = text.split("\n");
4569
- while (lines.length > 0 && lines[0].trim() === "") {
4570
- lines.shift();
4571
- }
4572
- while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
4573
- lines.pop();
4574
- }
4575
- const minSpaces = lines.reduce((min, line) => {
4576
- if (line.trim() === "") {
4577
- return min;
4578
- }
4579
- const leadingSpaces = line.match(/^\s*/)[0].length;
4580
- return Math.min(min, leadingSpaces);
4581
- }, Infinity);
4582
- const trimmedLines = lines.map((line) => line.slice(minSpaces));
4583
- return trimmedLines.join("\n");
4584
- }
4585
-
4586
4846
  const PLIST_LABEL$1 = "com.happy-cli.daemon";
4587
4847
  const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
4588
4848
  async function install$1() {