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 +699 -439
- package/dist/index.mjs +700 -440
- package/dist/lib.d.cts +75 -71
- package/dist/lib.d.mts +75 -71
- package/package.json +11 -9
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 {
|
|
@@ -42,6 +42,7 @@ class Session {
|
|
|
42
42
|
claudeEnvVars;
|
|
43
43
|
claudeArgs;
|
|
44
44
|
mcpServers;
|
|
45
|
+
allowedTools;
|
|
45
46
|
_onModeChange;
|
|
46
47
|
sessionId;
|
|
47
48
|
mode = "local";
|
|
@@ -56,6 +57,7 @@ class Session {
|
|
|
56
57
|
this.claudeEnvVars = opts.claudeEnvVars;
|
|
57
58
|
this.claudeArgs = opts.claudeArgs;
|
|
58
59
|
this.mcpServers = opts.mcpServers;
|
|
60
|
+
this.allowedTools = opts.allowedTools;
|
|
59
61
|
this._onModeChange = opts.onModeChange;
|
|
60
62
|
this.client.keepAlive(this.thinking, this.mode);
|
|
61
63
|
setInterval(() => {
|
|
@@ -114,6 +116,29 @@ function projectPath() {
|
|
|
114
116
|
return path;
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
function trimIdent(text) {
|
|
120
|
+
const lines = text.split("\n");
|
|
121
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
122
|
+
lines.shift();
|
|
123
|
+
}
|
|
124
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
125
|
+
lines.pop();
|
|
126
|
+
}
|
|
127
|
+
const minSpaces = lines.reduce((min, line) => {
|
|
128
|
+
if (line.trim() === "") {
|
|
129
|
+
return min;
|
|
130
|
+
}
|
|
131
|
+
const leadingSpaces = line.match(/^\s*/)[0].length;
|
|
132
|
+
return Math.min(min, leadingSpaces);
|
|
133
|
+
}, Infinity);
|
|
134
|
+
const trimmedLines = lines.map((line) => line.slice(minSpaces));
|
|
135
|
+
return trimmedLines.join("\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const systemPrompt = trimIdent(`
|
|
139
|
+
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.
|
|
140
|
+
`);
|
|
141
|
+
|
|
117
142
|
dirname$1(fileURLToPath$1(import.meta.url));
|
|
118
143
|
async function claudeLocal(opts) {
|
|
119
144
|
const projectDir = getProjectPath(opts.path);
|
|
@@ -161,6 +186,13 @@ async function claudeLocal(opts) {
|
|
|
161
186
|
if (startFrom) {
|
|
162
187
|
args.push("--resume", startFrom);
|
|
163
188
|
}
|
|
189
|
+
args.push("--append-system-prompt", systemPrompt);
|
|
190
|
+
if (opts.mcpServers && Object.keys(opts.mcpServers).length > 0) {
|
|
191
|
+
args.push("--mcp-config", JSON.stringify({ mcpServers: opts.mcpServers }));
|
|
192
|
+
}
|
|
193
|
+
if (opts.allowedTools && opts.allowedTools.length > 0) {
|
|
194
|
+
args.push("--allowedTools", opts.allowedTools.join(","));
|
|
195
|
+
}
|
|
164
196
|
if (opts.claudeArgs) {
|
|
165
197
|
args.push(...opts.claudeArgs);
|
|
166
198
|
}
|
|
@@ -505,7 +537,9 @@ async function claudeLocalLauncher(session) {
|
|
|
505
537
|
sessionId: session.sessionId,
|
|
506
538
|
workingDirectory: session.path,
|
|
507
539
|
onMessage: (message) => {
|
|
508
|
-
|
|
540
|
+
if (message.type !== "summary") {
|
|
541
|
+
session.client.sendClaudeSessionMessage(message);
|
|
542
|
+
}
|
|
509
543
|
}
|
|
510
544
|
});
|
|
511
545
|
let exitReason = null;
|
|
@@ -535,7 +569,9 @@ async function claudeLocalLauncher(session) {
|
|
|
535
569
|
}
|
|
536
570
|
session.client.setHandler("abort", doAbort);
|
|
537
571
|
session.client.setHandler("switch", doSwitch);
|
|
538
|
-
session.queue.setOnMessage(
|
|
572
|
+
session.queue.setOnMessage((message, mode) => {
|
|
573
|
+
doSwitch();
|
|
574
|
+
});
|
|
539
575
|
if (session.queue.size() > 0) {
|
|
540
576
|
return "switch";
|
|
541
577
|
}
|
|
@@ -556,7 +592,9 @@ async function claudeLocalLauncher(session) {
|
|
|
556
592
|
onThinkingChange: session.onThinkingChange,
|
|
557
593
|
abort: processAbortController.signal,
|
|
558
594
|
claudeEnvVars: session.claudeEnvVars,
|
|
559
|
-
claudeArgs: session.claudeArgs
|
|
595
|
+
claudeArgs: session.claudeArgs,
|
|
596
|
+
mcpServers: session.mcpServers,
|
|
597
|
+
allowedTools: session.allowedTools
|
|
560
598
|
});
|
|
561
599
|
if (!exitReason) {
|
|
562
600
|
exitReason = "exit";
|
|
@@ -864,16 +902,19 @@ async function streamToStdin(stream, stdin, abort) {
|
|
|
864
902
|
}
|
|
865
903
|
|
|
866
904
|
class Query {
|
|
867
|
-
constructor(childStdin, childStdout, processExitPromise) {
|
|
905
|
+
constructor(childStdin, childStdout, processExitPromise, canCallTool) {
|
|
868
906
|
this.childStdin = childStdin;
|
|
869
907
|
this.childStdout = childStdout;
|
|
870
908
|
this.processExitPromise = processExitPromise;
|
|
909
|
+
this.canCallTool = canCallTool;
|
|
871
910
|
this.readMessages();
|
|
872
911
|
this.sdkMessages = this.readSdkMessages();
|
|
873
912
|
}
|
|
874
913
|
pendingControlResponses = /* @__PURE__ */ new Map();
|
|
914
|
+
cancelControllers = /* @__PURE__ */ new Map();
|
|
875
915
|
sdkMessages;
|
|
876
916
|
inputStream = new Stream();
|
|
917
|
+
canCallTool;
|
|
877
918
|
/**
|
|
878
919
|
* Set an error on the stream
|
|
879
920
|
*/
|
|
@@ -918,6 +959,12 @@ class Query {
|
|
|
918
959
|
handler(controlResponse.response);
|
|
919
960
|
}
|
|
920
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;
|
|
921
968
|
}
|
|
922
969
|
this.inputStream.enqueue(message);
|
|
923
970
|
} catch (e) {
|
|
@@ -930,6 +977,7 @@ class Query {
|
|
|
930
977
|
this.inputStream.error(error);
|
|
931
978
|
} finally {
|
|
932
979
|
this.inputStream.done();
|
|
980
|
+
this.cleanupControllers();
|
|
933
981
|
rl.close();
|
|
934
982
|
}
|
|
935
983
|
}
|
|
@@ -973,6 +1021,77 @@ class Query {
|
|
|
973
1021
|
childStdin.write(JSON.stringify(sdkRequest) + "\n");
|
|
974
1022
|
});
|
|
975
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
|
+
}
|
|
976
1095
|
}
|
|
977
1096
|
function query(config) {
|
|
978
1097
|
const {
|
|
@@ -989,12 +1108,12 @@ function query(config) {
|
|
|
989
1108
|
mcpServers,
|
|
990
1109
|
pathToClaudeCodeExecutable = getDefaultClaudeCodePath(),
|
|
991
1110
|
permissionMode = "default",
|
|
992
|
-
permissionPromptToolName,
|
|
993
1111
|
continue: continueConversation,
|
|
994
1112
|
resume,
|
|
995
1113
|
model,
|
|
996
1114
|
fallbackModel,
|
|
997
|
-
strictMcpConfig
|
|
1115
|
+
strictMcpConfig,
|
|
1116
|
+
canCallTool
|
|
998
1117
|
} = {}
|
|
999
1118
|
} = config;
|
|
1000
1119
|
if (!process.env.CLAUDE_CODE_ENTRYPOINT) {
|
|
@@ -1005,7 +1124,12 @@ function query(config) {
|
|
|
1005
1124
|
if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
|
|
1006
1125
|
if (maxTurns) args.push("--max-turns", maxTurns.toString());
|
|
1007
1126
|
if (model) args.push("--model", model);
|
|
1008
|
-
if (
|
|
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
|
+
}
|
|
1009
1133
|
if (continueConversation) args.push("--continue");
|
|
1010
1134
|
if (resume) args.push("--resume", resume);
|
|
1011
1135
|
if (allowedTools.length > 0) args.push("--allowedTools", allowedTools.join(","));
|
|
@@ -1069,7 +1193,7 @@ function query(config) {
|
|
|
1069
1193
|
}
|
|
1070
1194
|
});
|
|
1071
1195
|
});
|
|
1072
|
-
const query2 = new Query(childStdin, child.stdout, processExitPromise);
|
|
1196
|
+
const query2 = new Query(childStdin, child.stdout, processExitPromise, canCallTool);
|
|
1073
1197
|
child.on("error", (error) => {
|
|
1074
1198
|
if (config.options?.abort?.aborted) {
|
|
1075
1199
|
query2.setError(new AbortError("Claude Code process aborted by user"));
|
|
@@ -1087,17 +1211,48 @@ function query(config) {
|
|
|
1087
1211
|
return query2;
|
|
1088
1212
|
}
|
|
1089
1213
|
|
|
1090
|
-
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
}
|
|
1097
|
-
await delay(1e3);
|
|
1098
|
-
}
|
|
1214
|
+
function parseCompact(message) {
|
|
1215
|
+
const trimmed = message.trim();
|
|
1216
|
+
if (trimmed === "/compact") {
|
|
1217
|
+
return {
|
|
1218
|
+
isCompact: true,
|
|
1219
|
+
originalMessage: trimmed
|
|
1220
|
+
};
|
|
1099
1221
|
}
|
|
1100
|
-
|
|
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
|
+
};
|
|
1101
1256
|
}
|
|
1102
1257
|
|
|
1103
1258
|
class PushableAsyncIterable {
|
|
@@ -1226,48 +1381,17 @@ class PushableAsyncIterable {
|
|
|
1226
1381
|
}
|
|
1227
1382
|
}
|
|
1228
1383
|
|
|
1229
|
-
function
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
return {
|
|
1239
|
-
isCompact: true,
|
|
1240
|
-
originalMessage: trimmed
|
|
1241
|
-
};
|
|
1242
|
-
}
|
|
1243
|
-
return {
|
|
1244
|
-
isCompact: false,
|
|
1245
|
-
originalMessage: message
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
function parseClear(message) {
|
|
1249
|
-
const trimmed = message.trim();
|
|
1250
|
-
return {
|
|
1251
|
-
isClear: trimmed === "/clear"
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
function parseSpecialCommand(message) {
|
|
1255
|
-
const compactResult = parseCompact(message);
|
|
1256
|
-
if (compactResult.isCompact) {
|
|
1257
|
-
return {
|
|
1258
|
-
type: "compact",
|
|
1259
|
-
originalMessage: compactResult.originalMessage
|
|
1260
|
-
};
|
|
1261
|
-
}
|
|
1262
|
-
const clearResult = parseClear(message);
|
|
1263
|
-
if (clearResult.isClear) {
|
|
1264
|
-
return {
|
|
1265
|
-
type: "clear"
|
|
1266
|
-
};
|
|
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
|
+
}
|
|
1267
1393
|
}
|
|
1268
|
-
return
|
|
1269
|
-
type: null
|
|
1270
|
-
};
|
|
1394
|
+
return false;
|
|
1271
1395
|
}
|
|
1272
1396
|
|
|
1273
1397
|
async function claudeRemote(opts) {
|
|
@@ -1280,32 +1404,12 @@ async function claudeRemote(opts) {
|
|
|
1280
1404
|
process.env[key] = value;
|
|
1281
1405
|
});
|
|
1282
1406
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
resume: startFrom ?? void 0,
|
|
1287
|
-
mcpServers: opts.mcpServers,
|
|
1288
|
-
permissionPromptToolName: opts.permissionPromptToolName,
|
|
1289
|
-
permissionMode: opts.permissionMode,
|
|
1290
|
-
model: opts.model,
|
|
1291
|
-
fallbackModel: opts.fallbackModel,
|
|
1292
|
-
customSystemPrompt: opts.customSystemPrompt,
|
|
1293
|
-
appendSystemPrompt: opts.appendSystemPrompt,
|
|
1294
|
-
allowedTools: opts.allowedTools,
|
|
1295
|
-
disallowedTools: opts.disallowedTools,
|
|
1296
|
-
executable: "node",
|
|
1297
|
-
abort: opts.signal,
|
|
1298
|
-
pathToClaudeCodeExecutable: (() => {
|
|
1299
|
-
return resolve(join(projectPath(), "scripts", "claude_remote_launcher.cjs"));
|
|
1300
|
-
})()
|
|
1301
|
-
};
|
|
1302
|
-
if (opts.claudeArgs && opts.claudeArgs.length > 0) {
|
|
1303
|
-
sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
|
|
1407
|
+
const initial = await opts.nextMessage();
|
|
1408
|
+
if (!initial) {
|
|
1409
|
+
return;
|
|
1304
1410
|
}
|
|
1305
|
-
|
|
1306
|
-
const specialCommand = parseSpecialCommand(opts.message);
|
|
1411
|
+
const specialCommand = parseSpecialCommand(initial.message);
|
|
1307
1412
|
if (specialCommand.type === "clear") {
|
|
1308
|
-
logger.debug("[claudeRemote] /clear command detected - should not reach here, handled in start.ts");
|
|
1309
1413
|
if (opts.onCompletionEvent) {
|
|
1310
1414
|
opts.onCompletionEvent("Context was reset");
|
|
1311
1415
|
}
|
|
@@ -1314,23 +1418,33 @@ async function claudeRemote(opts) {
|
|
|
1314
1418
|
}
|
|
1315
1419
|
return;
|
|
1316
1420
|
}
|
|
1421
|
+
let isCompactCommand = false;
|
|
1317
1422
|
if (specialCommand.type === "compact") {
|
|
1318
1423
|
logger.debug("[claudeRemote] /compact command detected - will process as normal but with compaction behavior");
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
message.push({
|
|
1323
|
-
type: "user",
|
|
1324
|
-
message: {
|
|
1325
|
-
role: "user",
|
|
1326
|
-
content: opts.message
|
|
1424
|
+
isCompactCommand = true;
|
|
1425
|
+
if (opts.onCompletionEvent) {
|
|
1426
|
+
opts.onCompletionEvent("Compaction started");
|
|
1327
1427
|
}
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1428
|
+
}
|
|
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
|
+
};
|
|
1334
1448
|
let thinking = false;
|
|
1335
1449
|
const updateThinking = (newThinking) => {
|
|
1336
1450
|
if (thinking !== newThinking) {
|
|
@@ -1341,15 +1455,27 @@ async function claudeRemote(opts) {
|
|
|
1341
1455
|
}
|
|
1342
1456
|
}
|
|
1343
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
|
+
});
|
|
1344
1470
|
updateThinking(true);
|
|
1345
1471
|
try {
|
|
1346
1472
|
logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
1347
|
-
for await (const
|
|
1348
|
-
logger.debugLargeJson(`[claudeRemote] Message ${
|
|
1349
|
-
opts.onMessage(
|
|
1350
|
-
if (
|
|
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") {
|
|
1351
1477
|
updateThinking(true);
|
|
1352
|
-
const systemInit =
|
|
1478
|
+
const systemInit = message;
|
|
1353
1479
|
if (systemInit.session_id) {
|
|
1354
1480
|
logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${systemInit.session_id}`);
|
|
1355
1481
|
const projectDir = getProjectPath(opts.path);
|
|
@@ -1357,14 +1483,8 @@ async function claudeRemote(opts) {
|
|
|
1357
1483
|
logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
|
|
1358
1484
|
opts.onSessionFound(systemInit.session_id);
|
|
1359
1485
|
}
|
|
1360
|
-
if (isCompactCommand) {
|
|
1361
|
-
logger.debug("[claudeRemote] Compaction started");
|
|
1362
|
-
if (opts.onCompletionEvent) {
|
|
1363
|
-
opts.onCompletionEvent("Compaction started");
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
1486
|
}
|
|
1367
|
-
if (
|
|
1487
|
+
if (message.type === "result") {
|
|
1368
1488
|
updateThinking(false);
|
|
1369
1489
|
logger.debug("[claudeRemote] Result received, exiting claudeRemote");
|
|
1370
1490
|
if (isCompactCommand) {
|
|
@@ -1372,26 +1492,28 @@ async function claudeRemote(opts) {
|
|
|
1372
1492
|
if (opts.onCompletionEvent) {
|
|
1373
1493
|
opts.onCompletionEvent("Compaction completed");
|
|
1374
1494
|
}
|
|
1495
|
+
isCompactCommand = false;
|
|
1375
1496
|
}
|
|
1376
|
-
|
|
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 } });
|
|
1377
1504
|
}
|
|
1378
|
-
if (
|
|
1379
|
-
const msg =
|
|
1505
|
+
if (message.type === "user") {
|
|
1506
|
+
const msg = message;
|
|
1380
1507
|
if (msg.message.role === "user" && Array.isArray(msg.message.content)) {
|
|
1381
1508
|
for (let c of msg.message.content) {
|
|
1382
|
-
if (c.type === "tool_result" &&
|
|
1383
|
-
logger.debug("[claudeRemote]
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
if (c.type === "tool_result" && c.tool_use_id && opts.responses.has(c.tool_use_id) && !opts.responses.get(c.tool_use_id).approved) {
|
|
1387
|
-
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");
|
|
1388
1511
|
return;
|
|
1389
1512
|
}
|
|
1390
1513
|
}
|
|
1391
1514
|
}
|
|
1392
1515
|
}
|
|
1393
1516
|
}
|
|
1394
|
-
logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
1395
1517
|
} catch (e) {
|
|
1396
1518
|
if (e instanceof AbortError) {
|
|
1397
1519
|
logger.debug(`[claudeRemote] Aborted`);
|
|
@@ -1401,71 +1523,11 @@ async function claudeRemote(opts) {
|
|
|
1401
1523
|
} finally {
|
|
1402
1524
|
updateThinking(false);
|
|
1403
1525
|
}
|
|
1404
|
-
logger.debug(`[claudeRemote] Function completed`);
|
|
1405
1526
|
}
|
|
1406
1527
|
|
|
1407
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.`;
|
|
1408
1529
|
const PLAN_FAKE_RESTART = `PlEaZe Continue with plan.`;
|
|
1409
1530
|
|
|
1410
|
-
async function startPermissionServerV2(handler) {
|
|
1411
|
-
const mcp = new McpServer({
|
|
1412
|
-
name: "Permission Server",
|
|
1413
|
-
version: "1.0.0",
|
|
1414
|
-
description: "A server that allows you to request permissions from the user"
|
|
1415
|
-
});
|
|
1416
|
-
mcp.registerTool("ask_permission", {
|
|
1417
|
-
description: "Request permission to execute a tool",
|
|
1418
|
-
title: "Request Permission",
|
|
1419
|
-
inputSchema: {
|
|
1420
|
-
tool_name: z$1.string().describe("The tool that needs permission"),
|
|
1421
|
-
input: z$1.any().describe("The arguments for the tool")
|
|
1422
|
-
}
|
|
1423
|
-
}, async (args) => {
|
|
1424
|
-
const response = await handler({ name: args.tool_name, arguments: args.input });
|
|
1425
|
-
logger.debugLargeJson("[permissionServerV2] Response", response);
|
|
1426
|
-
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.` };
|
|
1427
|
-
return {
|
|
1428
|
-
content: [
|
|
1429
|
-
{
|
|
1430
|
-
type: "text",
|
|
1431
|
-
text: JSON.stringify(result)
|
|
1432
|
-
}
|
|
1433
|
-
],
|
|
1434
|
-
isError: false
|
|
1435
|
-
};
|
|
1436
|
-
});
|
|
1437
|
-
const transport = new StreamableHTTPServerTransport({
|
|
1438
|
-
// NOTE: Returning session id here will result in claude
|
|
1439
|
-
// sdk spawn to fail with `Invalid Request: Server already initialized`
|
|
1440
|
-
sessionIdGenerator: void 0
|
|
1441
|
-
});
|
|
1442
|
-
await mcp.connect(transport);
|
|
1443
|
-
const server = createServer(async (req, res) => {
|
|
1444
|
-
try {
|
|
1445
|
-
await transport.handleRequest(req, res);
|
|
1446
|
-
} catch (error) {
|
|
1447
|
-
logger.debug("Error handling request:", error);
|
|
1448
|
-
if (!res.headersSent) {
|
|
1449
|
-
res.writeHead(500).end();
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
});
|
|
1453
|
-
const baseUrl = await new Promise((resolve) => {
|
|
1454
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1455
|
-
const addr = server.address();
|
|
1456
|
-
resolve(new URL(`http://127.0.0.1:${addr.port}`));
|
|
1457
|
-
});
|
|
1458
|
-
});
|
|
1459
|
-
return {
|
|
1460
|
-
url: baseUrl.toString(),
|
|
1461
|
-
toolName: "ask_permission",
|
|
1462
|
-
stop: () => {
|
|
1463
|
-
mcp.close();
|
|
1464
|
-
server.close();
|
|
1465
|
-
}
|
|
1466
|
-
};
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
1531
|
function deepEqual(a, b) {
|
|
1470
1532
|
if (a === b) return true;
|
|
1471
1533
|
if (a == null || b == null) return false;
|
|
@@ -1477,136 +1539,181 @@ function deepEqual(a, b) {
|
|
|
1477
1539
|
if (!keysB.includes(key)) return false;
|
|
1478
1540
|
if (!deepEqual(a[key], b[key])) return false;
|
|
1479
1541
|
}
|
|
1480
|
-
return true;
|
|
1542
|
+
return true;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
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);
|
|
1481
1587
|
}
|
|
1482
1588
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
});
|
|
1526
|
-
let timeout = setTimeout(async () => {
|
|
1527
|
-
logger.debug("Permission timeout - attempting to interrupt Claude");
|
|
1528
|
-
requests.delete(id);
|
|
1529
|
-
session.client.updateAgentState((currentState) => {
|
|
1530
|
-
const request2 = currentState.requests?.[id];
|
|
1531
|
-
if (!request2) return currentState;
|
|
1532
|
-
let r = { ...currentState.requests };
|
|
1533
|
-
delete r[id];
|
|
1534
|
-
return {
|
|
1535
|
-
...currentState,
|
|
1536
|
-
requests: r,
|
|
1537
|
-
completedRequests: {
|
|
1538
|
-
...currentState.completedRequests,
|
|
1539
|
-
[id]: {
|
|
1540
|
-
...request2,
|
|
1541
|
-
completedAt: Date.now(),
|
|
1542
|
-
status: "canceled",
|
|
1543
|
-
reason: "Timeout"
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
};
|
|
1547
|
-
});
|
|
1548
|
-
}, 1e3 * 60 * 4.5);
|
|
1549
|
-
logger.debug("Permission request" + id + " " + JSON.stringify(request));
|
|
1550
|
-
session.api.push().sendToAllDevices(
|
|
1551
|
-
"Permission Request",
|
|
1552
|
-
`Claude wants to use ${request.name}`,
|
|
1553
|
-
{
|
|
1554
|
-
sessionId: session.client.sessionId,
|
|
1555
|
-
requestId: id,
|
|
1556
|
-
tool: request.name,
|
|
1557
|
-
type: "permission_request"
|
|
1558
|
-
}
|
|
1559
|
-
);
|
|
1560
|
-
session.client.updateAgentState((currentState) => ({
|
|
1561
|
-
...currentState,
|
|
1562
|
-
requests: {
|
|
1563
|
-
...currentState.requests,
|
|
1564
|
-
[id]: {
|
|
1565
|
-
tool: request.name,
|
|
1566
|
-
arguments: request.arguments,
|
|
1567
|
-
createdAt: Date.now()
|
|
1589
|
+
function getToolDescriptor(toolName) {
|
|
1590
|
+
if (toolName === "exit_plan_mode" || toolName === "ExitPlanMode") {
|
|
1591
|
+
return { edit: false, exitPlan: true };
|
|
1592
|
+
}
|
|
1593
|
+
if (toolName === "Edit" || toolName === "MultiEdit" || toolName === "Write" || toolName === "NotebookEdit") {
|
|
1594
|
+
return { edit: true, exitPlan: false };
|
|
1595
|
+
}
|
|
1596
|
+
return { edit: false, exitPlan: false };
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
class PermissionHandler {
|
|
1600
|
+
toolCalls = [];
|
|
1601
|
+
responses = /* @__PURE__ */ new Map();
|
|
1602
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
1603
|
+
session;
|
|
1604
|
+
allowedTools = /* @__PURE__ */ new Set();
|
|
1605
|
+
permissionMode = "default";
|
|
1606
|
+
constructor(session) {
|
|
1607
|
+
this.session = session;
|
|
1608
|
+
this.setupClientHandler();
|
|
1609
|
+
}
|
|
1610
|
+
handleModeChange(mode) {
|
|
1611
|
+
this.permissionMode = mode;
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Handler response
|
|
1615
|
+
*/
|
|
1616
|
+
handlePermissionResponse(response, pending) {
|
|
1617
|
+
if (response.allowTools && response.allowTools.length > 0) {
|
|
1618
|
+
response.allowTools.forEach((tool) => this.allowedTools.add(tool));
|
|
1619
|
+
}
|
|
1620
|
+
if (response.mode) {
|
|
1621
|
+
this.permissionMode = response.mode;
|
|
1622
|
+
}
|
|
1623
|
+
if (pending.toolName === "exit_plan_mode" || pending.toolName === "ExitPlanMode") {
|
|
1624
|
+
logger.debug("Plan mode result received", response);
|
|
1625
|
+
if (response.approved) {
|
|
1626
|
+
logger.debug("Plan approved - injecting PLAN_FAKE_RESTART");
|
|
1627
|
+
if (response.mode && ["default", "acceptEdits", "bypassPermissions"].includes(response.mode)) {
|
|
1628
|
+
this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: response.mode });
|
|
1629
|
+
} else {
|
|
1630
|
+
this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: "default" });
|
|
1568
1631
|
}
|
|
1632
|
+
pending.resolve({ behavior: "deny", message: PLAN_FAKE_REJECT });
|
|
1633
|
+
} else {
|
|
1634
|
+
pending.resolve({ behavior: "deny", message: response.reason || "Plan rejected" });
|
|
1569
1635
|
}
|
|
1570
|
-
}));
|
|
1571
|
-
promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
|
|
1572
|
-
return promise;
|
|
1573
|
-
}
|
|
1574
|
-
session.client.setHandler("permission", async (message) => {
|
|
1575
|
-
logger.debug("Permission response" + JSON.stringify(message));
|
|
1576
|
-
const id = message.id;
|
|
1577
|
-
const resolve = requests.get(id);
|
|
1578
|
-
if (resolve) {
|
|
1579
|
-
responses.set(id, message);
|
|
1580
|
-
resolve({ approved: message.approved, reason: message.reason, mode: message.mode });
|
|
1581
|
-
requests.delete(id);
|
|
1582
1636
|
} else {
|
|
1583
|
-
|
|
1584
|
-
|
|
1637
|
+
const result = response.approved ? { behavior: "allow", updatedInput: pending.input || {} } : { behavior: "deny", message: response.reason || `The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.` };
|
|
1638
|
+
pending.resolve(result);
|
|
1585
1639
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
return {
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Creates the canCallTool callback for the SDK
|
|
1643
|
+
*/
|
|
1644
|
+
handleToolCall = async (toolName, input, mode, options) => {
|
|
1645
|
+
if (this.allowedTools.has(toolName)) {
|
|
1646
|
+
return { behavior: "allow", updatedInput: input };
|
|
1647
|
+
}
|
|
1648
|
+
const descriptor = getToolDescriptor(toolName);
|
|
1649
|
+
if (this.permissionMode === "bypassPermissions") {
|
|
1650
|
+
return { behavior: "allow", updatedInput: input };
|
|
1651
|
+
}
|
|
1652
|
+
if (this.permissionMode === "acceptEdits" && descriptor.edit) {
|
|
1653
|
+
return { behavior: "allow", updatedInput: input };
|
|
1654
|
+
}
|
|
1655
|
+
let toolCallId = this.resolveToolCallId(toolName, input);
|
|
1656
|
+
if (!toolCallId) {
|
|
1657
|
+
await delay(1e3);
|
|
1658
|
+
toolCallId = this.resolveToolCallId(toolName, input);
|
|
1659
|
+
if (!toolCallId) {
|
|
1660
|
+
throw new Error(`Could not resolve tool call ID for ${toolName}`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return this.handlePermissionRequest(toolCallId, toolName, input, options.signal);
|
|
1664
|
+
};
|
|
1665
|
+
/**
|
|
1666
|
+
* Handles individual permission requests
|
|
1667
|
+
*/
|
|
1668
|
+
async handlePermissionRequest(id, toolName, input, signal) {
|
|
1669
|
+
return new Promise((resolve, reject) => {
|
|
1670
|
+
const abortHandler = () => {
|
|
1671
|
+
this.pendingRequests.delete(id);
|
|
1672
|
+
reject(new Error("Permission request aborted"));
|
|
1673
|
+
};
|
|
1674
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1675
|
+
this.pendingRequests.set(id, {
|
|
1676
|
+
resolve: (result) => {
|
|
1677
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1678
|
+
resolve(result);
|
|
1679
|
+
},
|
|
1680
|
+
reject: (error) => {
|
|
1681
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1682
|
+
reject(error);
|
|
1683
|
+
},
|
|
1684
|
+
toolName,
|
|
1685
|
+
input
|
|
1686
|
+
});
|
|
1687
|
+
this.session.api.push().sendToAllDevices(
|
|
1688
|
+
"Permission Request",
|
|
1689
|
+
`Claude wants to ${getToolName(toolName)}`,
|
|
1690
|
+
{
|
|
1691
|
+
sessionId: this.session.client.sessionId,
|
|
1692
|
+
requestId: id,
|
|
1693
|
+
tool: toolName,
|
|
1694
|
+
type: "permission_request"
|
|
1695
|
+
}
|
|
1696
|
+
);
|
|
1697
|
+
this.session.client.updateAgentState((currentState) => ({
|
|
1593
1698
|
...currentState,
|
|
1594
|
-
requests:
|
|
1595
|
-
|
|
1596
|
-
...currentState.completedRequests,
|
|
1699
|
+
requests: {
|
|
1700
|
+
...currentState.requests,
|
|
1597
1701
|
[id]: {
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
reason: isExitPlanModeSuccess ? "Plan approved" : message.reason
|
|
1702
|
+
tool: toolName,
|
|
1703
|
+
arguments: input,
|
|
1704
|
+
createdAt: Date.now()
|
|
1602
1705
|
}
|
|
1603
1706
|
}
|
|
1604
|
-
};
|
|
1707
|
+
}));
|
|
1708
|
+
logger.debug(`Permission request sent for tool call ${id}: ${toolName}`);
|
|
1605
1709
|
});
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Resolves tool call ID based on tool name and input
|
|
1713
|
+
*/
|
|
1714
|
+
resolveToolCallId(name, args) {
|
|
1715
|
+
for (let i = this.toolCalls.length - 1; i >= 0; i--) {
|
|
1716
|
+
const call = this.toolCalls[i];
|
|
1610
1717
|
if (call.name === name && deepEqual(call.input, args)) {
|
|
1611
1718
|
if (call.used) {
|
|
1612
1719
|
return null;
|
|
@@ -1616,59 +1723,22 @@ async function startPermissionResolver(session) {
|
|
|
1616
1723
|
}
|
|
1617
1724
|
}
|
|
1618
1725
|
return null;
|
|
1619
|
-
};
|
|
1620
|
-
function reset() {
|
|
1621
|
-
toolCalls = [];
|
|
1622
|
-
requests.clear();
|
|
1623
|
-
responses.clear();
|
|
1624
|
-
for (const pending of pendingPermissionRequests) {
|
|
1625
|
-
clearTimeout(pending.timeout);
|
|
1626
|
-
}
|
|
1627
|
-
pendingPermissionRequests = [];
|
|
1628
|
-
session.client.updateAgentState((currentState) => {
|
|
1629
|
-
const pendingRequests = currentState.requests || {};
|
|
1630
|
-
const completedRequests = { ...currentState.completedRequests };
|
|
1631
|
-
for (const [id, request] of Object.entries(pendingRequests)) {
|
|
1632
|
-
completedRequests[id] = {
|
|
1633
|
-
...request,
|
|
1634
|
-
completedAt: Date.now(),
|
|
1635
|
-
status: "canceled",
|
|
1636
|
-
reason: "Session switched to local mode"
|
|
1637
|
-
};
|
|
1638
|
-
}
|
|
1639
|
-
return {
|
|
1640
|
-
...currentState,
|
|
1641
|
-
requests: {},
|
|
1642
|
-
// Clear all pending requests
|
|
1643
|
-
completedRequests
|
|
1644
|
-
};
|
|
1645
|
-
});
|
|
1646
1726
|
}
|
|
1647
|
-
|
|
1727
|
+
/**
|
|
1728
|
+
* Handles messages to track tool calls
|
|
1729
|
+
*/
|
|
1730
|
+
onMessage(message) {
|
|
1648
1731
|
if (message.type === "assistant") {
|
|
1649
1732
|
const assistantMsg = message;
|
|
1650
1733
|
if (assistantMsg.message && assistantMsg.message.content) {
|
|
1651
1734
|
for (const block of assistantMsg.message.content) {
|
|
1652
1735
|
if (block.type === "tool_use") {
|
|
1653
|
-
toolCalls.push({
|
|
1736
|
+
this.toolCalls.push({
|
|
1654
1737
|
id: block.id,
|
|
1655
1738
|
name: block.name,
|
|
1656
1739
|
input: block.input,
|
|
1657
1740
|
used: false
|
|
1658
1741
|
});
|
|
1659
|
-
for (let i = pendingPermissionRequests.length - 1; i >= 0; i--) {
|
|
1660
|
-
const pending = pendingPermissionRequests[i];
|
|
1661
|
-
if (pending.request.name === block.name && deepEqual(pending.request.arguments, block.input)) {
|
|
1662
|
-
logger.debug(`Resolving pending permission request for ${block.name} with ID ${block.id}`);
|
|
1663
|
-
clearTimeout(pending.timeout);
|
|
1664
|
-
pendingPermissionRequests.splice(i, 1);
|
|
1665
|
-
handlePermissionRequest(block.id, pending.request).then(
|
|
1666
|
-
pending.resolve,
|
|
1667
|
-
pending.reject
|
|
1668
|
-
);
|
|
1669
|
-
break;
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
1742
|
}
|
|
1673
1743
|
}
|
|
1674
1744
|
}
|
|
@@ -1678,7 +1748,7 @@ async function startPermissionResolver(session) {
|
|
|
1678
1748
|
if (userMsg.message && userMsg.message.content && Array.isArray(userMsg.message.content)) {
|
|
1679
1749
|
for (const block of userMsg.message.content) {
|
|
1680
1750
|
if (block.type === "tool_result" && block.tool_use_id) {
|
|
1681
|
-
const toolCall = toolCalls.find((tc) => tc.id === block.tool_use_id);
|
|
1751
|
+
const toolCall = this.toolCalls.find((tc) => tc.id === block.tool_use_id);
|
|
1682
1752
|
if (toolCall && !toolCall.used) {
|
|
1683
1753
|
toolCall.used = true;
|
|
1684
1754
|
}
|
|
@@ -1687,12 +1757,92 @@ async function startPermissionResolver(session) {
|
|
|
1687
1757
|
}
|
|
1688
1758
|
}
|
|
1689
1759
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
responses
|
|
1695
|
-
|
|
1760
|
+
/**
|
|
1761
|
+
* Checks if a tool call is rejected
|
|
1762
|
+
*/
|
|
1763
|
+
isAborted(toolCallId) {
|
|
1764
|
+
if (this.responses.get(toolCallId)?.approved === false) {
|
|
1765
|
+
return true;
|
|
1766
|
+
}
|
|
1767
|
+
const toolCall = this.toolCalls.find((tc) => tc.id === toolCallId);
|
|
1768
|
+
if (toolCall && (toolCall.name === "exit_plan_mode" || toolCall.name === "ExitPlanMode")) {
|
|
1769
|
+
return true;
|
|
1770
|
+
}
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Resets all state for new sessions
|
|
1775
|
+
*/
|
|
1776
|
+
reset() {
|
|
1777
|
+
this.toolCalls = [];
|
|
1778
|
+
this.responses.clear();
|
|
1779
|
+
for (const [, pending] of this.pendingRequests.entries()) {
|
|
1780
|
+
pending.reject(new Error("Session reset"));
|
|
1781
|
+
}
|
|
1782
|
+
this.pendingRequests.clear();
|
|
1783
|
+
this.session.client.updateAgentState((currentState) => {
|
|
1784
|
+
const pendingRequests = currentState.requests || {};
|
|
1785
|
+
const completedRequests = { ...currentState.completedRequests };
|
|
1786
|
+
for (const [id, request] of Object.entries(pendingRequests)) {
|
|
1787
|
+
completedRequests[id] = {
|
|
1788
|
+
...request,
|
|
1789
|
+
completedAt: Date.now(),
|
|
1790
|
+
status: "canceled",
|
|
1791
|
+
reason: "Session switched to local mode"
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
return {
|
|
1795
|
+
...currentState,
|
|
1796
|
+
requests: {},
|
|
1797
|
+
// Clear all pending requests
|
|
1798
|
+
completedRequests
|
|
1799
|
+
};
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Sets up the client handler for permission responses
|
|
1804
|
+
*/
|
|
1805
|
+
setupClientHandler() {
|
|
1806
|
+
this.session.client.setHandler("permission", async (message) => {
|
|
1807
|
+
logger.debug(`Permission response: ${JSON.stringify(message)}`);
|
|
1808
|
+
const id = message.id;
|
|
1809
|
+
const pending = this.pendingRequests.get(id);
|
|
1810
|
+
if (!pending) {
|
|
1811
|
+
logger.debug("Permission request not found or already resolved");
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
this.responses.set(id, { ...message, receivedAt: Date.now() });
|
|
1815
|
+
this.pendingRequests.delete(id);
|
|
1816
|
+
this.handlePermissionResponse(message, pending);
|
|
1817
|
+
this.session.client.updateAgentState((currentState) => {
|
|
1818
|
+
const request = currentState.requests?.[id];
|
|
1819
|
+
if (!request) return currentState;
|
|
1820
|
+
let r = { ...currentState.requests };
|
|
1821
|
+
delete r[id];
|
|
1822
|
+
return {
|
|
1823
|
+
...currentState,
|
|
1824
|
+
requests: r,
|
|
1825
|
+
completedRequests: {
|
|
1826
|
+
...currentState.completedRequests,
|
|
1827
|
+
[id]: {
|
|
1828
|
+
...request,
|
|
1829
|
+
completedAt: Date.now(),
|
|
1830
|
+
status: message.approved ? "approved" : "denied",
|
|
1831
|
+
reason: message.reason,
|
|
1832
|
+
mode: message.mode,
|
|
1833
|
+
allowTools: message.allowTools
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
});
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Gets the responses map (for compatibility with existing code)
|
|
1842
|
+
*/
|
|
1843
|
+
getResponses() {
|
|
1844
|
+
return this.responses;
|
|
1845
|
+
}
|
|
1696
1846
|
}
|
|
1697
1847
|
|
|
1698
1848
|
function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
|
|
@@ -2066,15 +2216,6 @@ async function claudeRemoteLauncher(session) {
|
|
|
2066
2216
|
}
|
|
2067
2217
|
process.stdin.setEncoding("utf8");
|
|
2068
2218
|
}
|
|
2069
|
-
const scanner = await createSessionScanner({
|
|
2070
|
-
sessionId: session.sessionId,
|
|
2071
|
-
workingDirectory: session.path,
|
|
2072
|
-
onMessage: (message) => {
|
|
2073
|
-
if (message.type === "summary") {
|
|
2074
|
-
session.client.sendClaudeSessionMessage(message);
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
});
|
|
2078
2219
|
let exitReason = null;
|
|
2079
2220
|
let abortController = null;
|
|
2080
2221
|
let abortFuture = null;
|
|
@@ -2097,17 +2238,17 @@ async function claudeRemoteLauncher(session) {
|
|
|
2097
2238
|
}
|
|
2098
2239
|
session.client.setHandler("abort", doAbort);
|
|
2099
2240
|
session.client.setHandler("switch", doSwitch);
|
|
2100
|
-
const
|
|
2241
|
+
const permissionHandler = new PermissionHandler(session);
|
|
2101
2242
|
const sdkToLogConverter = new SDKToLogConverter({
|
|
2102
2243
|
sessionId: session.sessionId || "unknown",
|
|
2103
2244
|
cwd: session.path,
|
|
2104
2245
|
version: process.env.npm_package_version
|
|
2105
|
-
},
|
|
2246
|
+
}, permissionHandler.getResponses());
|
|
2106
2247
|
let planModeToolCalls = /* @__PURE__ */ new Set();
|
|
2107
2248
|
let ongoingToolCalls = /* @__PURE__ */ new Map();
|
|
2108
2249
|
function onMessage(message) {
|
|
2109
2250
|
formatClaudeMessageForInk(message, messageBuffer);
|
|
2110
|
-
|
|
2251
|
+
permissionHandler.onMessage(message);
|
|
2111
2252
|
if (message.type === "assistant") {
|
|
2112
2253
|
let umessage = message;
|
|
2113
2254
|
if (umessage.message.content && Array.isArray(umessage.message.content)) {
|
|
@@ -2171,6 +2312,32 @@ async function claudeRemoteLauncher(session) {
|
|
|
2171
2312
|
}
|
|
2172
2313
|
const logMessage = sdkToLogConverter.convert(msg);
|
|
2173
2314
|
if (logMessage) {
|
|
2315
|
+
if (logMessage.type === "user" && logMessage.message?.content) {
|
|
2316
|
+
const content = Array.isArray(logMessage.message.content) ? logMessage.message.content : [];
|
|
2317
|
+
for (let i = 0; i < content.length; i++) {
|
|
2318
|
+
const c = content[i];
|
|
2319
|
+
if (c.type === "tool_result" && c.tool_use_id) {
|
|
2320
|
+
const responses = permissionHandler.getResponses();
|
|
2321
|
+
const response = responses.get(c.tool_use_id);
|
|
2322
|
+
if (response) {
|
|
2323
|
+
const permissions = {
|
|
2324
|
+
date: response.receivedAt || Date.now(),
|
|
2325
|
+
result: response.approved ? "approved" : "denied"
|
|
2326
|
+
};
|
|
2327
|
+
if (response.mode) {
|
|
2328
|
+
permissions.mode = response.mode;
|
|
2329
|
+
}
|
|
2330
|
+
if (response.allowTools && response.allowTools.length > 0) {
|
|
2331
|
+
permissions.allowedTools = response.allowTools;
|
|
2332
|
+
}
|
|
2333
|
+
content[i] = {
|
|
2334
|
+
...c,
|
|
2335
|
+
permissions
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2174
2341
|
if (logMessage.type !== "system") {
|
|
2175
2342
|
session.client.sendClaudeSessionMessage(logMessage);
|
|
2176
2343
|
}
|
|
@@ -2190,58 +2357,57 @@ async function claudeRemoteLauncher(session) {
|
|
|
2190
2357
|
}
|
|
2191
2358
|
}
|
|
2192
2359
|
try {
|
|
2360
|
+
let pending = null;
|
|
2193
2361
|
while (!exitReason) {
|
|
2194
|
-
logger.debug("[remote]: fetch next message");
|
|
2195
|
-
abortController = new AbortController();
|
|
2196
|
-
abortFuture = new Future();
|
|
2197
|
-
const messageData = await session.queue.waitForMessagesAndGetAsString(abortController.signal);
|
|
2198
|
-
if (!messageData || abortController.signal.aborted) {
|
|
2199
|
-
logger.debug("[remote]: fetch next message done: no message or aborted");
|
|
2200
|
-
abortFuture?.resolve(void 0);
|
|
2201
|
-
if (exitReason) {
|
|
2202
|
-
return exitReason;
|
|
2203
|
-
} else {
|
|
2204
|
-
continue;
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
logger.debug("[remote]: fetch next message done: message received");
|
|
2208
|
-
abortFuture?.resolve(void 0);
|
|
2209
|
-
abortFuture = null;
|
|
2210
|
-
abortController = null;
|
|
2211
2362
|
logger.debug("[remote]: launch");
|
|
2212
2363
|
messageBuffer.addMessage("\u2550".repeat(40), "status");
|
|
2213
2364
|
messageBuffer.addMessage("Starting new Claude session...", "status");
|
|
2214
|
-
|
|
2365
|
+
const controller = new AbortController();
|
|
2366
|
+
abortController = controller;
|
|
2215
2367
|
abortFuture = new Future();
|
|
2216
|
-
|
|
2368
|
+
permissionHandler.reset();
|
|
2217
2369
|
sdkToLogConverter.resetParentChain();
|
|
2370
|
+
let modeHash = null;
|
|
2371
|
+
let mode = null;
|
|
2218
2372
|
try {
|
|
2219
2373
|
await claudeRemote({
|
|
2220
2374
|
sessionId: session.sessionId,
|
|
2221
2375
|
path: session.path,
|
|
2222
|
-
|
|
2223
|
-
mcpServers:
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2376
|
+
allowedTools: session.allowedTools ?? [],
|
|
2377
|
+
mcpServers: session.mcpServers,
|
|
2378
|
+
canCallTool: permissionHandler.handleToolCall,
|
|
2379
|
+
isAborted: (toolCallId) => {
|
|
2380
|
+
return permissionHandler.isAborted(toolCallId);
|
|
2381
|
+
},
|
|
2382
|
+
nextMessage: async () => {
|
|
2383
|
+
if (pending) {
|
|
2384
|
+
let p = pending;
|
|
2385
|
+
pending = null;
|
|
2386
|
+
permissionHandler.handleModeChange(p.mode.permissionMode);
|
|
2387
|
+
return p;
|
|
2388
|
+
}
|
|
2389
|
+
let msg = await session.queue.waitForMessagesAndGetAsString(controller.signal);
|
|
2390
|
+
if (msg) {
|
|
2391
|
+
if (modeHash && msg.hash !== modeHash || msg.isolate) {
|
|
2392
|
+
logger.debug("[remote]: mode has changed, pending message");
|
|
2393
|
+
pending = msg;
|
|
2394
|
+
return null;
|
|
2395
|
+
}
|
|
2396
|
+
modeHash = msg.hash;
|
|
2397
|
+
mode = msg.mode;
|
|
2398
|
+
permissionHandler.handleModeChange(mode.permissionMode);
|
|
2399
|
+
return {
|
|
2400
|
+
message: msg.message,
|
|
2401
|
+
mode: msg.mode
|
|
2402
|
+
};
|
|
2228
2403
|
}
|
|
2404
|
+
return null;
|
|
2229
2405
|
},
|
|
2230
|
-
permissionPromptToolName: "mcp__permission__" + permissions.server.toolName,
|
|
2231
|
-
permissionMode: messageData.mode.permissionMode,
|
|
2232
|
-
model: messageData.mode.model,
|
|
2233
|
-
fallbackModel: messageData.mode.fallbackModel,
|
|
2234
|
-
customSystemPrompt: messageData.mode.customSystemPrompt,
|
|
2235
|
-
appendSystemPrompt: messageData.mode.appendSystemPrompt,
|
|
2236
|
-
allowedTools: messageData.mode.allowedTools,
|
|
2237
|
-
disallowedTools: messageData.mode.disallowedTools,
|
|
2238
2406
|
onSessionFound: (sessionId) => {
|
|
2239
2407
|
sdkToLogConverter.updateSessionId(sessionId);
|
|
2240
2408
|
session.onSessionFound(sessionId);
|
|
2241
|
-
scanner.onNewSession(sessionId);
|
|
2242
2409
|
},
|
|
2243
2410
|
onThinkingChange: session.onThinkingChange,
|
|
2244
|
-
message: messageData.message,
|
|
2245
2411
|
claudeEnvVars: session.claudeEnvVars,
|
|
2246
2412
|
claudeArgs: session.claudeArgs,
|
|
2247
2413
|
onMessage,
|
|
@@ -2259,11 +2425,13 @@ async function claudeRemoteLauncher(session) {
|
|
|
2259
2425
|
session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
|
|
2260
2426
|
}
|
|
2261
2427
|
} catch (e) {
|
|
2428
|
+
logger.debug("[remote]: launch error", e);
|
|
2262
2429
|
if (!exitReason) {
|
|
2263
2430
|
session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
|
|
2264
2431
|
continue;
|
|
2265
2432
|
}
|
|
2266
2433
|
} finally {
|
|
2434
|
+
logger.debug("[remote]: launch finally");
|
|
2267
2435
|
for (let [toolCallId, { parentToolCallId }] of ongoingToolCalls) {
|
|
2268
2436
|
const converted = sdkToLogConverter.generateInterruptedToolResult(toolCallId, parentToolCallId);
|
|
2269
2437
|
if (converted) {
|
|
@@ -2276,11 +2444,13 @@ async function claudeRemoteLauncher(session) {
|
|
|
2276
2444
|
abortFuture?.resolve(void 0);
|
|
2277
2445
|
abortFuture = null;
|
|
2278
2446
|
logger.debug("[remote]: launch done");
|
|
2279
|
-
|
|
2447
|
+
permissionHandler.reset();
|
|
2448
|
+
modeHash = null;
|
|
2449
|
+
mode = null;
|
|
2280
2450
|
}
|
|
2281
2451
|
}
|
|
2282
2452
|
} finally {
|
|
2283
|
-
|
|
2453
|
+
permissionHandler.reset();
|
|
2284
2454
|
process.stdin.off("data", abort);
|
|
2285
2455
|
if (process.stdin.isTTY) {
|
|
2286
2456
|
process.stdin.setRawMode(false);
|
|
@@ -2292,7 +2462,6 @@ async function claudeRemoteLauncher(session) {
|
|
|
2292
2462
|
if (abortFuture) {
|
|
2293
2463
|
abortFuture.resolve(void 0);
|
|
2294
2464
|
}
|
|
2295
|
-
await scanner.cleanup();
|
|
2296
2465
|
}
|
|
2297
2466
|
return exitReason || "exit";
|
|
2298
2467
|
}
|
|
@@ -2309,6 +2478,7 @@ async function loop(opts) {
|
|
|
2309
2478
|
mcpServers: opts.mcpServers,
|
|
2310
2479
|
logPath,
|
|
2311
2480
|
messageQueue: opts.messageQueue,
|
|
2481
|
+
allowedTools: opts.allowedTools,
|
|
2312
2482
|
onModeChange: opts.onModeChange
|
|
2313
2483
|
});
|
|
2314
2484
|
if (opts.onSessionReady) {
|
|
@@ -2343,7 +2513,7 @@ async function loop(opts) {
|
|
|
2343
2513
|
}
|
|
2344
2514
|
|
|
2345
2515
|
var name = "happy-coder";
|
|
2346
|
-
var version = "0.
|
|
2516
|
+
var version = "0.9.0-0";
|
|
2347
2517
|
var description = "Claude Code session sharing CLI";
|
|
2348
2518
|
var author = "Kirill Dubovitskiy";
|
|
2349
2519
|
var license = "MIT";
|
|
@@ -2393,18 +2563,14 @@ var scripts = {
|
|
|
2393
2563
|
test: "yarn build && vitest run",
|
|
2394
2564
|
"test:watch": "vitest",
|
|
2395
2565
|
"test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
|
|
2396
|
-
dev: "yarn build && npx tsx src/index.ts",
|
|
2566
|
+
dev: "yarn build && DEBUG=1 npx tsx src/index.ts",
|
|
2397
2567
|
"dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
|
|
2398
2568
|
"dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
|
|
2399
2569
|
prepublishOnly: "yarn build && yarn test",
|
|
2400
|
-
|
|
2401
|
-
"patch:publish": "yarn build && npm version patch && npm publish",
|
|
2402
|
-
"version:prerelease": "yarn build && npm version prerelease --preid=beta",
|
|
2403
|
-
"publish:prerelease": "npm publish --tag beta",
|
|
2404
|
-
"beta:publish": "yarn version:prerelease && yarn publish:prerelease"
|
|
2570
|
+
release: "release-it"
|
|
2405
2571
|
};
|
|
2406
2572
|
var dependencies = {
|
|
2407
|
-
"@anthropic-ai/claude-code": "^1.0.
|
|
2573
|
+
"@anthropic-ai/claude-code": "^1.0.89",
|
|
2408
2574
|
"@anthropic-ai/sdk": "^0.56.0",
|
|
2409
2575
|
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
2410
2576
|
"@stablelib/base64": "^2.0.1",
|
|
@@ -2433,6 +2599,7 @@ var devDependencies = {
|
|
|
2433
2599
|
eslint: "^9",
|
|
2434
2600
|
"eslint-config-prettier": "^10",
|
|
2435
2601
|
pkgroll: "^2.14.2",
|
|
2602
|
+
"release-it": "^19.0.4",
|
|
2436
2603
|
shx: "^0.3.3",
|
|
2437
2604
|
"ts-node": "^10",
|
|
2438
2605
|
tsx: "^4.20.3",
|
|
@@ -2440,7 +2607,12 @@ var devDependencies = {
|
|
|
2440
2607
|
vitest: "^3.2.4"
|
|
2441
2608
|
};
|
|
2442
2609
|
var resolutions = {
|
|
2443
|
-
"whatwg-url": "14.2.0"
|
|
2610
|
+
"whatwg-url": "14.2.0",
|
|
2611
|
+
"parse-path": "7.0.3",
|
|
2612
|
+
"@types/parse-path": "7.0.3"
|
|
2613
|
+
};
|
|
2614
|
+
var publishConfig = {
|
|
2615
|
+
registry: "https://registry.npmjs.org"
|
|
2444
2616
|
};
|
|
2445
2617
|
var packageManager = "yarn@1.22.22";
|
|
2446
2618
|
var packageJson = {
|
|
@@ -2463,6 +2635,7 @@ var packageJson = {
|
|
|
2463
2635
|
dependencies: dependencies,
|
|
2464
2636
|
devDependencies: devDependencies,
|
|
2465
2637
|
resolutions: resolutions,
|
|
2638
|
+
publishConfig: publishConfig,
|
|
2466
2639
|
packageManager: packageManager
|
|
2467
2640
|
};
|
|
2468
2641
|
|
|
@@ -2849,15 +3022,17 @@ async function clearDaemonState() {
|
|
|
2849
3022
|
}
|
|
2850
3023
|
|
|
2851
3024
|
class MessageQueue2 {
|
|
2852
|
-
constructor(modeHasher) {
|
|
2853
|
-
this.modeHasher = modeHasher;
|
|
2854
|
-
logger.debug(`[MessageQueue2] Initialized`);
|
|
2855
|
-
}
|
|
2856
3025
|
queue = [];
|
|
2857
3026
|
// Made public for testing
|
|
2858
3027
|
waiter = null;
|
|
2859
3028
|
closed = false;
|
|
2860
3029
|
onMessageHandler = null;
|
|
3030
|
+
modeHasher;
|
|
3031
|
+
constructor(modeHasher, onMessageHandler = null) {
|
|
3032
|
+
this.modeHasher = modeHasher;
|
|
3033
|
+
this.onMessageHandler = onMessageHandler;
|
|
3034
|
+
logger.debug(`[MessageQueue2] Initialized`);
|
|
3035
|
+
}
|
|
2861
3036
|
/**
|
|
2862
3037
|
* Set a handler that will be called when a message arrives
|
|
2863
3038
|
*/
|
|
@@ -3032,6 +3207,7 @@ class MessageQueue2 {
|
|
|
3032
3207
|
const firstItem = this.queue[0];
|
|
3033
3208
|
const sameModeMessages = [];
|
|
3034
3209
|
let mode = firstItem.mode;
|
|
3210
|
+
let isolate = firstItem.isolate ?? false;
|
|
3035
3211
|
const targetModeHash = firstItem.modeHash;
|
|
3036
3212
|
if (firstItem.isolate) {
|
|
3037
3213
|
const item = this.queue.shift();
|
|
@@ -3047,7 +3223,9 @@ class MessageQueue2 {
|
|
|
3047
3223
|
const combinedMessage = sameModeMessages.join("\n");
|
|
3048
3224
|
return {
|
|
3049
3225
|
message: combinedMessage,
|
|
3050
|
-
mode
|
|
3226
|
+
mode,
|
|
3227
|
+
hash: targetModeHash,
|
|
3228
|
+
isolate
|
|
3051
3229
|
};
|
|
3052
3230
|
}
|
|
3053
3231
|
/**
|
|
@@ -3942,10 +4120,10 @@ async function doWebAuth(keypair) {
|
|
|
3942
4120
|
console.log("\u2713 Browser opened\n");
|
|
3943
4121
|
console.log("Complete authentication in your browser window.");
|
|
3944
4122
|
} else {
|
|
3945
|
-
console.log("Could not open browser automatically
|
|
3946
|
-
console.log("Please open this URL manually:");
|
|
3947
|
-
console.log(webUrl);
|
|
4123
|
+
console.log("Could not open browser automatically.");
|
|
3948
4124
|
}
|
|
4125
|
+
console.log("\nIf the browser did not open, please copy and paste this URL:");
|
|
4126
|
+
console.log(webUrl);
|
|
3949
4127
|
console.log("");
|
|
3950
4128
|
return await waitForAuthentication(keypair);
|
|
3951
4129
|
}
|
|
@@ -4302,6 +4480,88 @@ async function startDaemon() {
|
|
|
4302
4480
|
}
|
|
4303
4481
|
}
|
|
4304
4482
|
|
|
4483
|
+
async function startHappyServer(client) {
|
|
4484
|
+
const handler = async (title) => {
|
|
4485
|
+
logger.debug("[happyMCP] Changing title to:", title);
|
|
4486
|
+
try {
|
|
4487
|
+
client.sendClaudeSessionMessage({
|
|
4488
|
+
type: "summary",
|
|
4489
|
+
summary: title,
|
|
4490
|
+
leafUuid: randomUUID()
|
|
4491
|
+
});
|
|
4492
|
+
return { success: true };
|
|
4493
|
+
} catch (error) {
|
|
4494
|
+
return { success: false, error: String(error) };
|
|
4495
|
+
}
|
|
4496
|
+
};
|
|
4497
|
+
const mcp = new McpServer({
|
|
4498
|
+
name: "Happy MCP",
|
|
4499
|
+
version: "1.0.0",
|
|
4500
|
+
description: "Happy CLI MCP server with chat session management tools"
|
|
4501
|
+
});
|
|
4502
|
+
mcp.registerTool("change_title", {
|
|
4503
|
+
description: "Change the title of the current chat session",
|
|
4504
|
+
title: "Change Chat Title",
|
|
4505
|
+
inputSchema: {
|
|
4506
|
+
title: z$1.string().describe("The new title for the chat session")
|
|
4507
|
+
}
|
|
4508
|
+
}, async (args) => {
|
|
4509
|
+
const response = await handler(args.title);
|
|
4510
|
+
logger.debug("[happyMCP] Response:", response);
|
|
4511
|
+
if (response.success) {
|
|
4512
|
+
return {
|
|
4513
|
+
content: [
|
|
4514
|
+
{
|
|
4515
|
+
type: "text",
|
|
4516
|
+
text: `Successfully changed chat title to: "${args.title}"`
|
|
4517
|
+
}
|
|
4518
|
+
],
|
|
4519
|
+
isError: false
|
|
4520
|
+
};
|
|
4521
|
+
} else {
|
|
4522
|
+
return {
|
|
4523
|
+
content: [
|
|
4524
|
+
{
|
|
4525
|
+
type: "text",
|
|
4526
|
+
text: `Failed to change chat title: ${response.error || "Unknown error"}`
|
|
4527
|
+
}
|
|
4528
|
+
],
|
|
4529
|
+
isError: true
|
|
4530
|
+
};
|
|
4531
|
+
}
|
|
4532
|
+
});
|
|
4533
|
+
const transport = new StreamableHTTPServerTransport({
|
|
4534
|
+
// NOTE: Returning session id here will result in claude
|
|
4535
|
+
// sdk spawn to fail with `Invalid Request: Server already initialized`
|
|
4536
|
+
sessionIdGenerator: void 0
|
|
4537
|
+
});
|
|
4538
|
+
await mcp.connect(transport);
|
|
4539
|
+
const server = createServer(async (req, res) => {
|
|
4540
|
+
try {
|
|
4541
|
+
await transport.handleRequest(req, res);
|
|
4542
|
+
} catch (error) {
|
|
4543
|
+
logger.debug("Error handling request:", error);
|
|
4544
|
+
if (!res.headersSent) {
|
|
4545
|
+
res.writeHead(500).end();
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
});
|
|
4549
|
+
const baseUrl = await new Promise((resolve) => {
|
|
4550
|
+
server.listen(0, "127.0.0.1", () => {
|
|
4551
|
+
const addr = server.address();
|
|
4552
|
+
resolve(new URL(`http://127.0.0.1:${addr.port}`));
|
|
4553
|
+
});
|
|
4554
|
+
});
|
|
4555
|
+
return {
|
|
4556
|
+
url: baseUrl.toString(),
|
|
4557
|
+
toolNames: ["change_title"],
|
|
4558
|
+
stop: () => {
|
|
4559
|
+
mcp.close();
|
|
4560
|
+
server.close();
|
|
4561
|
+
}
|
|
4562
|
+
};
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4305
4565
|
async function start(credentials, options = {}) {
|
|
4306
4566
|
const workingDirectory = process.cwd();
|
|
4307
4567
|
const sessionTag = randomUUID();
|
|
@@ -4361,6 +4621,8 @@ async function start(credentials, options = {}) {
|
|
|
4361
4621
|
}
|
|
4362
4622
|
});
|
|
4363
4623
|
const session = api.sessionSyncClient(response);
|
|
4624
|
+
const happyServer = await startHappyServer(session);
|
|
4625
|
+
logger.debug(`[START] Happy MCP server started at ${happyServer.url}`);
|
|
4364
4626
|
const logPath = await logger.logFilePathPromise;
|
|
4365
4627
|
logger.infoDeveloper(`Session: ${response.id}`);
|
|
4366
4628
|
logger.infoDeveloper(`Logs: ${logPath}`);
|
|
@@ -4372,7 +4634,15 @@ async function start(credentials, options = {}) {
|
|
|
4372
4634
|
if (caffeinateStarted) {
|
|
4373
4635
|
logger.infoDeveloper("Sleep prevention enabled (macOS)");
|
|
4374
4636
|
}
|
|
4375
|
-
const messageQueue = new MessageQueue2((mode) => hashObject(
|
|
4637
|
+
const messageQueue = new MessageQueue2((mode) => hashObject({
|
|
4638
|
+
isPlan: mode.permissionMode === "plan",
|
|
4639
|
+
model: mode.model,
|
|
4640
|
+
fallbackModel: mode.fallbackModel,
|
|
4641
|
+
customSystemPrompt: mode.customSystemPrompt,
|
|
4642
|
+
appendSystemPrompt: mode.appendSystemPrompt,
|
|
4643
|
+
allowedTools: mode.allowedTools,
|
|
4644
|
+
disallowedTools: mode.disallowedTools
|
|
4645
|
+
}));
|
|
4376
4646
|
registerHandlers(session);
|
|
4377
4647
|
let currentPermissionMode = options.permissionMode;
|
|
4378
4648
|
let currentModel = options.model;
|
|
@@ -4495,6 +4765,7 @@ async function start(credentials, options = {}) {
|
|
|
4495
4765
|
await session.close();
|
|
4496
4766
|
}
|
|
4497
4767
|
stopCaffeinate();
|
|
4768
|
+
happyServer.stop();
|
|
4498
4769
|
logger.debug("[START] Cleanup complete, exiting");
|
|
4499
4770
|
process.exit(0);
|
|
4500
4771
|
} catch (error) {
|
|
@@ -4519,6 +4790,7 @@ async function start(credentials, options = {}) {
|
|
|
4519
4790
|
startingMode: options.startingMode,
|
|
4520
4791
|
messageQueue,
|
|
4521
4792
|
api,
|
|
4793
|
+
allowedTools: happyServer.toolNames.map((toolName) => `mcp__happy__${toolName}`),
|
|
4522
4794
|
onModeChange: (newMode) => {
|
|
4523
4795
|
session.sendSessionEvent({ type: "switch", mode: newMode });
|
|
4524
4796
|
session.updateAgentState((currentState) => ({
|
|
@@ -4528,7 +4800,12 @@ async function start(credentials, options = {}) {
|
|
|
4528
4800
|
},
|
|
4529
4801
|
onSessionReady: (sessionInstance) => {
|
|
4530
4802
|
},
|
|
4531
|
-
mcpServers: {
|
|
4803
|
+
mcpServers: {
|
|
4804
|
+
"happy": {
|
|
4805
|
+
type: "http",
|
|
4806
|
+
url: happyServer.url
|
|
4807
|
+
}
|
|
4808
|
+
},
|
|
4532
4809
|
session,
|
|
4533
4810
|
claudeEnvVars: options.claudeEnvVars,
|
|
4534
4811
|
claudeArgs: options.claudeArgs
|
|
@@ -4540,28 +4817,11 @@ async function start(credentials, options = {}) {
|
|
|
4540
4817
|
await session.close();
|
|
4541
4818
|
stopCaffeinate();
|
|
4542
4819
|
logger.debug("Stopped sleep prevention");
|
|
4820
|
+
happyServer.stop();
|
|
4821
|
+
logger.debug("Stopped Happy MCP server");
|
|
4543
4822
|
process.exit(0);
|
|
4544
4823
|
}
|
|
4545
4824
|
|
|
4546
|
-
function trimIdent(text) {
|
|
4547
|
-
const lines = text.split("\n");
|
|
4548
|
-
while (lines.length > 0 && lines[0].trim() === "") {
|
|
4549
|
-
lines.shift();
|
|
4550
|
-
}
|
|
4551
|
-
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
4552
|
-
lines.pop();
|
|
4553
|
-
}
|
|
4554
|
-
const minSpaces = lines.reduce((min, line) => {
|
|
4555
|
-
if (line.trim() === "") {
|
|
4556
|
-
return min;
|
|
4557
|
-
}
|
|
4558
|
-
const leadingSpaces = line.match(/^\s*/)[0].length;
|
|
4559
|
-
return Math.min(min, leadingSpaces);
|
|
4560
|
-
}, Infinity);
|
|
4561
|
-
const trimmedLines = lines.map((line) => line.slice(minSpaces));
|
|
4562
|
-
return trimmedLines.join("\n");
|
|
4563
|
-
}
|
|
4564
|
-
|
|
4565
4825
|
const PLIST_LABEL$1 = "com.happy-cli.daemon";
|
|
4566
4826
|
const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
|
|
4567
4827
|
async function install$1() {
|