happy-coder 0.1.3 → 0.1.6
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 +1458 -977
- package/dist/index.mjs +1455 -974
- package/package.json +9 -6
- package/scripts/claudeInteractiveLaunch.cjs +36 -0
package/dist/index.cjs
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var chalk = require('chalk');
|
|
3
4
|
var axios = require('axios');
|
|
4
|
-
var fs = require('
|
|
5
|
-
var promises = require('node:fs/promises');
|
|
6
|
-
var node_crypto = require('node:crypto');
|
|
7
|
-
var tweetnacl = require('tweetnacl');
|
|
5
|
+
var fs = require('fs');
|
|
8
6
|
var os = require('node:os');
|
|
9
7
|
var node_path = require('node:path');
|
|
10
|
-
var
|
|
11
|
-
var
|
|
8
|
+
var promises = require('node:fs/promises');
|
|
9
|
+
var node_fs = require('node:fs');
|
|
12
10
|
var node_events = require('node:events');
|
|
13
11
|
var socket_ioClient = require('socket.io-client');
|
|
14
|
-
var
|
|
15
|
-
var
|
|
12
|
+
var z = require('zod');
|
|
13
|
+
var node_crypto = require('node:crypto');
|
|
14
|
+
var tweetnacl = require('tweetnacl');
|
|
15
|
+
var expoServerSdk = require('expo-server-sdk');
|
|
16
16
|
var claudeCode = require('@anthropic-ai/claude-code');
|
|
17
|
-
var
|
|
17
|
+
var node_child_process = require('node:child_process');
|
|
18
18
|
var node_readline = require('node:readline');
|
|
19
|
+
var node_url = require('node:url');
|
|
20
|
+
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
19
21
|
var node_http = require('node:http');
|
|
22
|
+
var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
23
|
+
var qrcode = require('qrcode-terminal');
|
|
20
24
|
|
|
25
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
21
26
|
function _interopNamespaceDefault(e) {
|
|
22
27
|
var n = Object.create(null);
|
|
23
28
|
if (e) {
|
|
@@ -35,111 +40,35 @@ function _interopNamespaceDefault(e) {
|
|
|
35
40
|
return Object.freeze(n);
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
var
|
|
39
|
-
var pty__namespace = /*#__PURE__*/_interopNamespaceDefault(pty);
|
|
40
|
-
|
|
41
|
-
function encodeBase64(buffer) {
|
|
42
|
-
return Buffer.from(buffer).toString("base64");
|
|
43
|
-
}
|
|
44
|
-
function encodeBase64Url(buffer) {
|
|
45
|
-
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
46
|
-
}
|
|
47
|
-
function decodeBase64(base64) {
|
|
48
|
-
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
49
|
-
}
|
|
50
|
-
function getRandomBytes(size) {
|
|
51
|
-
return new Uint8Array(node_crypto.randomBytes(size));
|
|
52
|
-
}
|
|
53
|
-
function encrypt(data, secret) {
|
|
54
|
-
const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
|
|
55
|
-
const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
|
|
56
|
-
const result = new Uint8Array(nonce.length + encrypted.length);
|
|
57
|
-
result.set(nonce);
|
|
58
|
-
result.set(encrypted, nonce.length);
|
|
59
|
-
return result;
|
|
60
|
-
}
|
|
61
|
-
function decrypt(data, secret) {
|
|
62
|
-
const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
|
|
63
|
-
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
|
|
64
|
-
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
|
|
65
|
-
if (!decrypted) {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
69
|
-
}
|
|
70
|
-
function authChallenge(secret) {
|
|
71
|
-
const keypair = tweetnacl.sign.keyPair.fromSeed(secret);
|
|
72
|
-
const challenge = getRandomBytes(32);
|
|
73
|
-
const signature = tweetnacl.sign.detached(challenge, keypair.secretKey);
|
|
74
|
-
return {
|
|
75
|
-
challenge,
|
|
76
|
-
publicKey: keypair.publicKey,
|
|
77
|
-
signature
|
|
78
|
-
};
|
|
79
|
-
}
|
|
43
|
+
var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
|
|
80
44
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
45
|
+
class Configuration {
|
|
46
|
+
serverUrl;
|
|
47
|
+
// Directories and paths (from persistence)
|
|
48
|
+
happyDir;
|
|
49
|
+
logsDir;
|
|
50
|
+
settingsFile;
|
|
51
|
+
privateKeyFile;
|
|
52
|
+
constructor(location) {
|
|
53
|
+
this.serverUrl = process.env.HANDY_SERVER_URL || "https://handy-api.korshakov.org";
|
|
54
|
+
if (location === "local") {
|
|
55
|
+
this.happyDir = node_path.join(process.cwd(), ".happy");
|
|
56
|
+
} else {
|
|
57
|
+
this.happyDir = node_path.join(os.homedir(), ".happy");
|
|
58
|
+
}
|
|
59
|
+
this.logsDir = node_path.join(this.happyDir, "logs");
|
|
60
|
+
this.settingsFile = node_path.join(this.happyDir, "settings.json");
|
|
61
|
+
this.privateKeyFile = node_path.join(this.happyDir, "access.key");
|
|
90
62
|
}
|
|
91
|
-
return response.data.token;
|
|
92
63
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
64
|
+
let configuration = void 0;
|
|
65
|
+
function initializeConfiguration(location) {
|
|
66
|
+
configuration = new Configuration(location);
|
|
96
67
|
}
|
|
97
68
|
|
|
98
|
-
const handyDir = node_path.join(os.homedir(), ".handy");
|
|
99
|
-
const logsDir = node_path.join(handyDir, "logs");
|
|
100
|
-
const settingsFile = node_path.join(handyDir, "settings.json");
|
|
101
|
-
const privateKeyFile = node_path.join(handyDir, "access.key");
|
|
102
|
-
const defaultSettings = {
|
|
103
|
-
onboardingCompleted: false
|
|
104
|
-
};
|
|
105
|
-
async function readSettings() {
|
|
106
|
-
if (!fs.existsSync(settingsFile)) {
|
|
107
|
-
return { ...defaultSettings };
|
|
108
|
-
}
|
|
109
|
-
try {
|
|
110
|
-
const content = await promises.readFile(settingsFile, "utf8");
|
|
111
|
-
return JSON.parse(content);
|
|
112
|
-
} catch {
|
|
113
|
-
return { ...defaultSettings };
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
async function writeSettings(settings) {
|
|
117
|
-
if (!fs.existsSync(handyDir)) {
|
|
118
|
-
await promises.mkdir(handyDir, { recursive: true });
|
|
119
|
-
}
|
|
120
|
-
await promises.writeFile(settingsFile, JSON.stringify(settings, null, 2));
|
|
121
|
-
}
|
|
122
|
-
async function readPrivateKey() {
|
|
123
|
-
if (!fs.existsSync(privateKeyFile)) {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
try {
|
|
127
|
-
const keyBase64 = (await promises.readFile(privateKeyFile, "utf8")).trim();
|
|
128
|
-
return new Uint8Array(Buffer.from(keyBase64, "base64"));
|
|
129
|
-
} catch {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
async function writePrivateKey(key) {
|
|
134
|
-
if (!fs.existsSync(handyDir)) {
|
|
135
|
-
await promises.mkdir(handyDir, { recursive: true });
|
|
136
|
-
}
|
|
137
|
-
const keyBase64 = Buffer.from(key).toString("base64");
|
|
138
|
-
await promises.writeFile(privateKeyFile, keyBase64, "utf8");
|
|
139
|
-
}
|
|
140
69
|
async function getSessionLogPath() {
|
|
141
|
-
if (!
|
|
142
|
-
await promises.mkdir(logsDir, { recursive: true });
|
|
70
|
+
if (!node_fs.existsSync(configuration.logsDir)) {
|
|
71
|
+
await promises.mkdir(configuration.logsDir, { recursive: true });
|
|
143
72
|
}
|
|
144
73
|
const now = /* @__PURE__ */ new Date();
|
|
145
74
|
const timestamp = now.toLocaleString("sv-SE", {
|
|
@@ -151,15 +80,26 @@ async function getSessionLogPath() {
|
|
|
151
80
|
minute: "2-digit",
|
|
152
81
|
second: "2-digit"
|
|
153
82
|
}).replace(/[: ]/g, "-").replace(/,/g, "");
|
|
154
|
-
return node_path.join(logsDir, `${timestamp}.log`);
|
|
83
|
+
return node_path.join(configuration.logsDir, `${timestamp}.log`);
|
|
155
84
|
}
|
|
156
|
-
|
|
157
85
|
class Logger {
|
|
158
86
|
constructor(logFilePathPromise = getSessionLogPath()) {
|
|
159
87
|
this.logFilePathPromise = logFilePathPromise;
|
|
160
88
|
}
|
|
89
|
+
// Use local timezone for simplicity of locating the logs,
|
|
90
|
+
// in practice you will not need absolute timestamps
|
|
91
|
+
localTimezoneTimestamp() {
|
|
92
|
+
return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", {
|
|
93
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
94
|
+
hour12: false,
|
|
95
|
+
hour: "2-digit",
|
|
96
|
+
minute: "2-digit",
|
|
97
|
+
second: "2-digit",
|
|
98
|
+
fractionalSecondDigits: 3
|
|
99
|
+
});
|
|
100
|
+
}
|
|
161
101
|
debug(message, ...args) {
|
|
162
|
-
this.logToFile(`[${
|
|
102
|
+
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, ...args);
|
|
163
103
|
}
|
|
164
104
|
debugLargeJson(message, object, maxStringLength = 100, maxArrayLength = 10) {
|
|
165
105
|
if (!process.env.DEBUG) {
|
|
@@ -179,6 +119,9 @@ class Logger {
|
|
|
179
119
|
if (obj && typeof obj === "object") {
|
|
180
120
|
const result = {};
|
|
181
121
|
for (const [key, value] of Object.entries(obj)) {
|
|
122
|
+
if (key === "usage") {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
182
125
|
result[key] = truncateStrings(value);
|
|
183
126
|
}
|
|
184
127
|
return result;
|
|
@@ -187,10 +130,11 @@ class Logger {
|
|
|
187
130
|
};
|
|
188
131
|
const truncatedObject = truncateStrings(object);
|
|
189
132
|
const json = JSON.stringify(truncatedObject, null, 2);
|
|
190
|
-
this.logToFile(`[${
|
|
133
|
+
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, "\n", json);
|
|
191
134
|
}
|
|
192
135
|
info(message, ...args) {
|
|
193
136
|
this.logToConsole("info", "", message, ...args);
|
|
137
|
+
this.debug(message, args);
|
|
194
138
|
}
|
|
195
139
|
logToConsole(level, prefix, message, ...args) {
|
|
196
140
|
switch (level) {
|
|
@@ -223,7 +167,14 @@ class Logger {
|
|
|
223
167
|
).join(" ")}
|
|
224
168
|
`;
|
|
225
169
|
this.logFilePathPromise.then((logFilePath) => {
|
|
226
|
-
|
|
170
|
+
try {
|
|
171
|
+
fs.appendFileSync(logFilePath, logLine);
|
|
172
|
+
} catch (appendError) {
|
|
173
|
+
if (process.env.DEBUG) {
|
|
174
|
+
console.error("Failed to append to log file:", appendError);
|
|
175
|
+
throw appendError;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
227
178
|
}).catch((error) => {
|
|
228
179
|
if (process.env.DEBUG) {
|
|
229
180
|
console.log("This message only visible in DEBUG mode, not in production");
|
|
@@ -233,87 +184,123 @@ class Logger {
|
|
|
233
184
|
});
|
|
234
185
|
}
|
|
235
186
|
}
|
|
236
|
-
|
|
187
|
+
let logger;
|
|
188
|
+
function initLoggerWithGlobalConfiguration() {
|
|
189
|
+
logger = new Logger();
|
|
190
|
+
}
|
|
237
191
|
|
|
238
|
-
const SessionMessageContentSchema =
|
|
239
|
-
c:
|
|
192
|
+
const SessionMessageContentSchema = z.z.object({
|
|
193
|
+
c: z.z.string(),
|
|
240
194
|
// Base64 encoded encrypted content
|
|
241
|
-
t:
|
|
195
|
+
t: z.z.literal("encrypted")
|
|
242
196
|
});
|
|
243
|
-
const UpdateBodySchema =
|
|
244
|
-
message:
|
|
245
|
-
id:
|
|
246
|
-
seq:
|
|
197
|
+
const UpdateBodySchema = z.z.object({
|
|
198
|
+
message: z.z.object({
|
|
199
|
+
id: z.z.string(),
|
|
200
|
+
seq: z.z.number(),
|
|
247
201
|
content: SessionMessageContentSchema
|
|
248
202
|
}),
|
|
249
|
-
sid:
|
|
203
|
+
sid: z.z.string(),
|
|
250
204
|
// Session ID
|
|
251
|
-
t:
|
|
205
|
+
t: z.z.literal("new-message")
|
|
252
206
|
});
|
|
253
|
-
const UpdateSessionBodySchema =
|
|
254
|
-
t:
|
|
255
|
-
sid:
|
|
256
|
-
metadata:
|
|
257
|
-
version:
|
|
258
|
-
metadata:
|
|
207
|
+
const UpdateSessionBodySchema = z.z.object({
|
|
208
|
+
t: z.z.literal("update-session"),
|
|
209
|
+
sid: z.z.string(),
|
|
210
|
+
metadata: z.z.object({
|
|
211
|
+
version: z.z.number(),
|
|
212
|
+
metadata: z.z.string()
|
|
259
213
|
}).nullish(),
|
|
260
|
-
agentState:
|
|
261
|
-
version:
|
|
262
|
-
agentState:
|
|
214
|
+
agentState: z.z.object({
|
|
215
|
+
version: z.z.number(),
|
|
216
|
+
agentState: z.z.string()
|
|
263
217
|
}).nullish()
|
|
264
218
|
});
|
|
265
|
-
|
|
266
|
-
id:
|
|
267
|
-
seq:
|
|
268
|
-
body:
|
|
269
|
-
createdAt:
|
|
219
|
+
z.z.object({
|
|
220
|
+
id: z.z.string(),
|
|
221
|
+
seq: z.z.number(),
|
|
222
|
+
body: z.z.union([UpdateBodySchema, UpdateSessionBodySchema]),
|
|
223
|
+
createdAt: z.z.number()
|
|
270
224
|
});
|
|
271
|
-
|
|
272
|
-
createdAt:
|
|
273
|
-
id:
|
|
274
|
-
seq:
|
|
275
|
-
updatedAt:
|
|
276
|
-
metadata:
|
|
277
|
-
metadataVersion:
|
|
278
|
-
agentState:
|
|
279
|
-
agentStateVersion:
|
|
225
|
+
z.z.object({
|
|
226
|
+
createdAt: z.z.number(),
|
|
227
|
+
id: z.z.string(),
|
|
228
|
+
seq: z.z.number(),
|
|
229
|
+
updatedAt: z.z.number(),
|
|
230
|
+
metadata: z.z.any(),
|
|
231
|
+
metadataVersion: z.z.number(),
|
|
232
|
+
agentState: z.z.any().nullable(),
|
|
233
|
+
agentStateVersion: z.z.number()
|
|
280
234
|
});
|
|
281
|
-
|
|
235
|
+
z.z.object({
|
|
282
236
|
content: SessionMessageContentSchema,
|
|
283
|
-
createdAt:
|
|
284
|
-
id:
|
|
285
|
-
seq:
|
|
286
|
-
updatedAt:
|
|
237
|
+
createdAt: z.z.number(),
|
|
238
|
+
id: z.z.string(),
|
|
239
|
+
seq: z.z.number(),
|
|
240
|
+
updatedAt: z.z.number()
|
|
287
241
|
});
|
|
288
|
-
|
|
289
|
-
session:
|
|
290
|
-
id:
|
|
291
|
-
tag:
|
|
292
|
-
seq:
|
|
293
|
-
createdAt:
|
|
294
|
-
updatedAt:
|
|
295
|
-
metadata:
|
|
296
|
-
metadataVersion:
|
|
297
|
-
agentState:
|
|
298
|
-
agentStateVersion:
|
|
242
|
+
z.z.object({
|
|
243
|
+
session: z.z.object({
|
|
244
|
+
id: z.z.string(),
|
|
245
|
+
tag: z.z.string(),
|
|
246
|
+
seq: z.z.number(),
|
|
247
|
+
createdAt: z.z.number(),
|
|
248
|
+
updatedAt: z.z.number(),
|
|
249
|
+
metadata: z.z.string(),
|
|
250
|
+
metadataVersion: z.z.number(),
|
|
251
|
+
agentState: z.z.string().nullable(),
|
|
252
|
+
agentStateVersion: z.z.number()
|
|
299
253
|
})
|
|
300
254
|
});
|
|
301
|
-
const UserMessageSchema =
|
|
302
|
-
role:
|
|
303
|
-
content:
|
|
304
|
-
type:
|
|
305
|
-
text:
|
|
255
|
+
const UserMessageSchema$1 = z.z.object({
|
|
256
|
+
role: z.z.literal("user"),
|
|
257
|
+
content: z.z.object({
|
|
258
|
+
type: z.z.literal("text"),
|
|
259
|
+
text: z.z.string()
|
|
306
260
|
}),
|
|
307
|
-
localKey:
|
|
261
|
+
localKey: z.z.string().optional(),
|
|
308
262
|
// Mobile messages include this
|
|
309
|
-
sentFrom:
|
|
263
|
+
sentFrom: z.z.enum(["mobile", "cli"]).optional()
|
|
310
264
|
// Source identifier
|
|
311
265
|
});
|
|
312
|
-
const AgentMessageSchema =
|
|
313
|
-
role:
|
|
314
|
-
content:
|
|
266
|
+
const AgentMessageSchema = z.z.object({
|
|
267
|
+
role: z.z.literal("agent"),
|
|
268
|
+
content: z.z.object({
|
|
269
|
+
type: z.z.literal("output"),
|
|
270
|
+
data: z.z.any()
|
|
271
|
+
})
|
|
315
272
|
});
|
|
316
|
-
|
|
273
|
+
z.z.union([UserMessageSchema$1, AgentMessageSchema]);
|
|
274
|
+
|
|
275
|
+
function encodeBase64(buffer) {
|
|
276
|
+
return Buffer.from(buffer).toString("base64");
|
|
277
|
+
}
|
|
278
|
+
function encodeBase64Url(buffer) {
|
|
279
|
+
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
280
|
+
}
|
|
281
|
+
function decodeBase64(base64) {
|
|
282
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
283
|
+
}
|
|
284
|
+
function getRandomBytes(size) {
|
|
285
|
+
return new Uint8Array(node_crypto.randomBytes(size));
|
|
286
|
+
}
|
|
287
|
+
function encrypt(data, secret) {
|
|
288
|
+
const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
|
|
289
|
+
const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
|
|
290
|
+
const result = new Uint8Array(nonce.length + encrypted.length);
|
|
291
|
+
result.set(nonce);
|
|
292
|
+
result.set(encrypted, nonce.length);
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
function decrypt(data, secret) {
|
|
296
|
+
const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
|
|
297
|
+
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
|
|
298
|
+
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
|
|
299
|
+
if (!decrypted) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
303
|
+
}
|
|
317
304
|
|
|
318
305
|
async function delay(ms) {
|
|
319
306
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -352,8 +339,6 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
352
339
|
agentState;
|
|
353
340
|
agentStateVersion;
|
|
354
341
|
socket;
|
|
355
|
-
receivedMessages = /* @__PURE__ */ new Set();
|
|
356
|
-
sentLocalKeys = /* @__PURE__ */ new Set();
|
|
357
342
|
pendingMessages = [];
|
|
358
343
|
pendingMessageCallback = null;
|
|
359
344
|
rpcHandlers = /* @__PURE__ */ new Map();
|
|
@@ -366,9 +351,11 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
366
351
|
this.metadataVersion = session.metadataVersion;
|
|
367
352
|
this.agentState = session.agentState;
|
|
368
353
|
this.agentStateVersion = session.agentStateVersion;
|
|
369
|
-
this.socket = socket_ioClient.io(
|
|
354
|
+
this.socket = socket_ioClient.io(configuration.serverUrl, {
|
|
370
355
|
auth: {
|
|
371
|
-
token: this.token
|
|
356
|
+
token: this.token,
|
|
357
|
+
clientType: "session-scoped",
|
|
358
|
+
sessionId: this.sessionId
|
|
372
359
|
},
|
|
373
360
|
path: "/v1/updates",
|
|
374
361
|
reconnection: true,
|
|
@@ -380,7 +367,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
380
367
|
autoConnect: false
|
|
381
368
|
});
|
|
382
369
|
this.socket.on("connect", () => {
|
|
383
|
-
logger.
|
|
370
|
+
logger.debug("Socket connected successfully");
|
|
384
371
|
this.reregisterHandlers();
|
|
385
372
|
});
|
|
386
373
|
this.socket.on("rpc-request", async (data, callback) => {
|
|
@@ -409,24 +396,18 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
409
396
|
logger.debug("[API] Socket disconnected:", reason);
|
|
410
397
|
});
|
|
411
398
|
this.socket.on("connect_error", (error) => {
|
|
412
|
-
logger.debug("[API] Socket connection error:", error
|
|
399
|
+
logger.debug("[API] Socket connection error:", error);
|
|
413
400
|
});
|
|
414
401
|
this.socket.on("update", (data) => {
|
|
415
402
|
if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
|
|
416
403
|
const body = decrypt(decodeBase64(data.body.message.content.c), this.secret);
|
|
417
404
|
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
|
|
418
|
-
const userResult = UserMessageSchema.safeParse(body);
|
|
405
|
+
const userResult = UserMessageSchema$1.safeParse(body);
|
|
419
406
|
if (userResult.success) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
this.receivedMessages.add(data.body.message.id);
|
|
425
|
-
if (this.pendingMessageCallback) {
|
|
426
|
-
this.pendingMessageCallback(userResult.data);
|
|
427
|
-
} else {
|
|
428
|
-
this.pendingMessages.push(userResult.data);
|
|
429
|
-
}
|
|
407
|
+
if (this.pendingMessageCallback) {
|
|
408
|
+
this.pendingMessageCallback(userResult.data);
|
|
409
|
+
} else {
|
|
410
|
+
this.pendingMessages.push(userResult.data);
|
|
430
411
|
}
|
|
431
412
|
} else {
|
|
432
413
|
this.emit("message", body);
|
|
@@ -442,6 +423,9 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
442
423
|
}
|
|
443
424
|
}
|
|
444
425
|
});
|
|
426
|
+
this.socket.on("error", (error) => {
|
|
427
|
+
logger.debug("[API] Socket error:", error);
|
|
428
|
+
});
|
|
445
429
|
this.socket.connect();
|
|
446
430
|
}
|
|
447
431
|
onUserMessage(callback) {
|
|
@@ -454,21 +438,27 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
454
438
|
* Send message to session
|
|
455
439
|
* @param body - Message body (can be MessageContent or raw content for agent messages)
|
|
456
440
|
*/
|
|
457
|
-
|
|
458
|
-
logger.debugLargeJson("[SOCKET] Sending message through socket:", body);
|
|
441
|
+
sendClaudeSessionMessage(body) {
|
|
459
442
|
let content;
|
|
460
|
-
if (body.
|
|
461
|
-
content =
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
443
|
+
if (body.type === "user" && typeof body.message.content === "string") {
|
|
444
|
+
content = {
|
|
445
|
+
role: "user",
|
|
446
|
+
content: {
|
|
447
|
+
type: "text",
|
|
448
|
+
text: body.message.content
|
|
449
|
+
}
|
|
450
|
+
};
|
|
466
451
|
} else {
|
|
467
452
|
content = {
|
|
468
453
|
role: "agent",
|
|
469
|
-
content:
|
|
454
|
+
content: {
|
|
455
|
+
type: "output",
|
|
456
|
+
data: body
|
|
457
|
+
// This wraps the entire Claude message
|
|
458
|
+
}
|
|
470
459
|
};
|
|
471
460
|
}
|
|
461
|
+
logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
|
|
472
462
|
const encrypted = encodeBase64(encrypt(content, this.secret));
|
|
473
463
|
this.socket.emit("message", {
|
|
474
464
|
sid: this.sessionId,
|
|
@@ -512,27 +502,31 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
512
502
|
* @param handler - Handler function that returns the updated agent state
|
|
513
503
|
*/
|
|
514
504
|
updateAgentState(handler) {
|
|
505
|
+
console.log("Updating agent state", this.agentState);
|
|
515
506
|
backoff(async () => {
|
|
516
507
|
let updated = handler(this.agentState || {});
|
|
517
|
-
const answer = await this.socket.emitWithAck("update-
|
|
508
|
+
const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(updated, this.secret)) : null });
|
|
518
509
|
if (answer.result === "success") {
|
|
519
510
|
this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
|
|
520
511
|
this.agentStateVersion = answer.version;
|
|
512
|
+
console.log("Agent state updated", this.agentState);
|
|
521
513
|
} else if (answer.result === "version-mismatch") {
|
|
522
514
|
if (answer.version > this.agentStateVersion) {
|
|
523
515
|
this.agentStateVersion = answer.version;
|
|
524
516
|
this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
|
|
525
517
|
}
|
|
526
518
|
throw new Error("Agent state version mismatch");
|
|
527
|
-
} else if (answer.result === "error")
|
|
519
|
+
} else if (answer.result === "error") {
|
|
520
|
+
console.error("Agent state update error", answer);
|
|
521
|
+
}
|
|
528
522
|
});
|
|
529
523
|
}
|
|
530
524
|
/**
|
|
531
|
-
*
|
|
525
|
+
* Set a custom RPC handler for a specific method with encrypted arguments and responses
|
|
532
526
|
* @param method - The method name to handle
|
|
533
527
|
* @param handler - The handler function to call when the method is invoked
|
|
534
528
|
*/
|
|
535
|
-
|
|
529
|
+
setHandler(method, handler) {
|
|
536
530
|
const prefixedMethod = `${this.sessionId}:${method}`;
|
|
537
531
|
this.rpcHandlers.set(prefixedMethod, handler);
|
|
538
532
|
this.socket.emit("rpc-register", { method: prefixedMethod });
|
|
@@ -571,12 +565,119 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
571
565
|
}
|
|
572
566
|
}
|
|
573
567
|
|
|
568
|
+
class PushNotificationClient {
|
|
569
|
+
token;
|
|
570
|
+
baseUrl;
|
|
571
|
+
expo;
|
|
572
|
+
constructor(token, baseUrl = "https://handy-api.korshakov.org") {
|
|
573
|
+
this.token = token;
|
|
574
|
+
this.baseUrl = baseUrl;
|
|
575
|
+
this.expo = new expoServerSdk.Expo();
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Fetch all push tokens for the authenticated user
|
|
579
|
+
*/
|
|
580
|
+
async fetchPushTokens() {
|
|
581
|
+
try {
|
|
582
|
+
const response = await axios.get(
|
|
583
|
+
`${this.baseUrl}/v1/push-tokens`,
|
|
584
|
+
{
|
|
585
|
+
headers: {
|
|
586
|
+
"Authorization": `Bearer ${this.token}`,
|
|
587
|
+
"Content-Type": "application/json"
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
);
|
|
591
|
+
logger.info(`Fetched ${response.data.tokens.length} push tokens`);
|
|
592
|
+
return response.data.tokens;
|
|
593
|
+
} catch (error) {
|
|
594
|
+
logger.debug("[PUSH] [ERROR] Failed to fetch push tokens:", error);
|
|
595
|
+
throw new Error(`Failed to fetch push tokens: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Send push notification via Expo Push API with retry
|
|
600
|
+
* @param messages - Array of push messages to send
|
|
601
|
+
*/
|
|
602
|
+
async sendPushNotifications(messages) {
|
|
603
|
+
logger.info(`Sending ${messages.length} push notifications`);
|
|
604
|
+
const validMessages = messages.filter((message) => {
|
|
605
|
+
if (Array.isArray(message.to)) {
|
|
606
|
+
return message.to.every((token) => expoServerSdk.Expo.isExpoPushToken(token));
|
|
607
|
+
}
|
|
608
|
+
return expoServerSdk.Expo.isExpoPushToken(message.to);
|
|
609
|
+
});
|
|
610
|
+
if (validMessages.length === 0) {
|
|
611
|
+
logger.info("No valid Expo push tokens found");
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const chunks = this.expo.chunkPushNotifications(validMessages);
|
|
615
|
+
for (const chunk of chunks) {
|
|
616
|
+
const startTime = Date.now();
|
|
617
|
+
const timeout = 3e5;
|
|
618
|
+
let attempt = 0;
|
|
619
|
+
while (true) {
|
|
620
|
+
try {
|
|
621
|
+
const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
|
|
622
|
+
const errors = ticketChunk.filter((ticket) => ticket.status === "error");
|
|
623
|
+
if (errors.length > 0) {
|
|
624
|
+
logger.debug("[PUSH] Some notifications failed:", errors);
|
|
625
|
+
}
|
|
626
|
+
if (errors.length === ticketChunk.length) {
|
|
627
|
+
throw new Error("All push notifications in chunk failed");
|
|
628
|
+
}
|
|
629
|
+
break;
|
|
630
|
+
} catch (error) {
|
|
631
|
+
const elapsed = Date.now() - startTime;
|
|
632
|
+
if (elapsed >= timeout) {
|
|
633
|
+
logger.debug("[PUSH] Timeout reached after 5 minutes, giving up on chunk");
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
attempt++;
|
|
637
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt), 3e4);
|
|
638
|
+
const remainingTime = timeout - elapsed;
|
|
639
|
+
const waitTime = Math.min(delay, remainingTime);
|
|
640
|
+
if (waitTime > 0) {
|
|
641
|
+
logger.debug(`[PUSH] Retrying in ${waitTime}ms (attempt ${attempt})`);
|
|
642
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
logger.info(`Push notifications sent successfully`);
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Send a push notification to all registered devices for the user
|
|
651
|
+
* @param title - Notification title
|
|
652
|
+
* @param body - Notification body
|
|
653
|
+
* @param data - Additional data to send with the notification
|
|
654
|
+
*/
|
|
655
|
+
async sendToAllDevices(title, body, data) {
|
|
656
|
+
const tokens = await this.fetchPushTokens();
|
|
657
|
+
if (tokens.length === 0) {
|
|
658
|
+
logger.info("No push tokens found for user");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const messages = tokens.map((token) => ({
|
|
662
|
+
to: token.token,
|
|
663
|
+
title,
|
|
664
|
+
body,
|
|
665
|
+
data,
|
|
666
|
+
sound: "default",
|
|
667
|
+
priority: "high"
|
|
668
|
+
}));
|
|
669
|
+
await this.sendPushNotifications(messages);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
574
673
|
class ApiClient {
|
|
575
674
|
token;
|
|
576
675
|
secret;
|
|
676
|
+
pushClient;
|
|
577
677
|
constructor(token, secret) {
|
|
578
678
|
this.token = token;
|
|
579
679
|
this.secret = secret;
|
|
680
|
+
this.pushClient = new PushNotificationClient(token);
|
|
580
681
|
}
|
|
581
682
|
/**
|
|
582
683
|
* Create a new session or load existing one with the given tag
|
|
@@ -584,7 +685,7 @@ class ApiClient {
|
|
|
584
685
|
async getOrCreateSession(opts) {
|
|
585
686
|
try {
|
|
586
687
|
const response = await axios.post(
|
|
587
|
-
|
|
688
|
+
`${configuration.serverUrl}/v1/sessions`,
|
|
588
689
|
{
|
|
589
690
|
tag: opts.tag,
|
|
590
691
|
metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
|
|
@@ -597,7 +698,7 @@ class ApiClient {
|
|
|
597
698
|
}
|
|
598
699
|
}
|
|
599
700
|
);
|
|
600
|
-
logger.
|
|
701
|
+
logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
|
|
601
702
|
let raw = response.data.session;
|
|
602
703
|
let session = {
|
|
603
704
|
id: raw.id,
|
|
@@ -623,857 +724,1220 @@ class ApiClient {
|
|
|
623
724
|
session(session) {
|
|
624
725
|
return new ApiSessionClient(this.token, this.secret, session);
|
|
625
726
|
}
|
|
727
|
+
/**
|
|
728
|
+
* Get push notification client
|
|
729
|
+
* @returns Push notification client
|
|
730
|
+
*/
|
|
731
|
+
push() {
|
|
732
|
+
return this.pushClient;
|
|
733
|
+
}
|
|
626
734
|
}
|
|
627
735
|
|
|
628
|
-
function
|
|
629
|
-
logger.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
736
|
+
function formatClaudeMessage(message, onAssistantResult) {
|
|
737
|
+
logger.debugLargeJson("[CLAUDE] Message from non interactive & remote mode:", message);
|
|
738
|
+
switch (message.type) {
|
|
739
|
+
case "system": {
|
|
740
|
+
const sysMsg = message;
|
|
741
|
+
if (sysMsg.subtype === "init") {
|
|
742
|
+
console.log(chalk.gray("\u2500".repeat(60)));
|
|
743
|
+
console.log(chalk.blue.bold("\u{1F680} Session initialized:"), chalk.cyan(sysMsg.session_id));
|
|
744
|
+
console.log(chalk.gray(` Model: ${sysMsg.model}`));
|
|
745
|
+
console.log(chalk.gray(` CWD: ${sysMsg.cwd}`));
|
|
746
|
+
if (sysMsg.tools && sysMsg.tools.length > 0) {
|
|
747
|
+
console.log(chalk.gray(` Tools: ${sysMsg.tools.join(", ")}`));
|
|
748
|
+
}
|
|
749
|
+
console.log(chalk.gray("\u2500".repeat(60)));
|
|
750
|
+
}
|
|
751
|
+
break;
|
|
635
752
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
for await (const message of response) {
|
|
659
|
-
logger.debugLargeJson("[CLAUDE SDK] Message:", message);
|
|
660
|
-
switch (message.type) {
|
|
661
|
-
case "system":
|
|
662
|
-
if (message.subtype === "init") {
|
|
663
|
-
yield { type: "json", data: message };
|
|
753
|
+
case "user": {
|
|
754
|
+
const userMsg = message;
|
|
755
|
+
if (userMsg.message && typeof userMsg.message === "object" && "content" in userMsg.message) {
|
|
756
|
+
const content = userMsg.message.content;
|
|
757
|
+
if (typeof content === "string") {
|
|
758
|
+
console.log(chalk.magenta.bold("\n\u{1F464} User:"), content);
|
|
759
|
+
} else if (Array.isArray(content)) {
|
|
760
|
+
for (const block of content) {
|
|
761
|
+
if (block.type === "text") {
|
|
762
|
+
console.log(chalk.magenta.bold("\n\u{1F464} User:"), block.text);
|
|
763
|
+
} else if (block.type === "tool_result") {
|
|
764
|
+
console.log(chalk.green.bold("\n\u2705 Tool Result:"), chalk.gray(`(Tool ID: ${block.tool_use_id})`));
|
|
765
|
+
if (block.content) {
|
|
766
|
+
const outputStr = typeof block.content === "string" ? block.content : JSON.stringify(block.content, null, 2);
|
|
767
|
+
const maxLength = 200;
|
|
768
|
+
if (outputStr.length > maxLength) {
|
|
769
|
+
console.log(outputStr.substring(0, maxLength) + chalk.gray("\n... (truncated)"));
|
|
770
|
+
} else {
|
|
771
|
+
console.log(outputStr);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
664
775
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
776
|
+
} else {
|
|
777
|
+
console.log(chalk.magenta.bold("\n\u{1F464} User:"), JSON.stringify(content, null, 2));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
case "assistant": {
|
|
783
|
+
const assistantMsg = message;
|
|
784
|
+
if (assistantMsg.message && assistantMsg.message.content) {
|
|
785
|
+
console.log(chalk.cyan.bold("\n\u{1F916} Assistant:"));
|
|
786
|
+
for (const block of assistantMsg.message.content) {
|
|
787
|
+
if (block.type === "text") {
|
|
788
|
+
console.log(block.text);
|
|
789
|
+
} else if (block.type === "tool_use") {
|
|
790
|
+
console.log(chalk.yellow.bold(`
|
|
791
|
+
\u{1F527} Tool: ${block.name}`));
|
|
792
|
+
if (block.input) {
|
|
793
|
+
const inputStr = JSON.stringify(block.input, null, 2);
|
|
794
|
+
const maxLength = 500;
|
|
795
|
+
if (inputStr.length > maxLength) {
|
|
796
|
+
console.log(chalk.gray("Input:"), inputStr.substring(0, maxLength) + chalk.gray("\n... (truncated)"));
|
|
797
|
+
} else {
|
|
798
|
+
console.log(chalk.gray("Input:"), inputStr);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
678
801
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
case "result": {
|
|
807
|
+
const resultMsg = message;
|
|
808
|
+
if (resultMsg.subtype === "success") {
|
|
809
|
+
if ("result" in resultMsg && resultMsg.result) {
|
|
810
|
+
console.log(chalk.green.bold("\n\u2728 Summary:"));
|
|
811
|
+
console.log(resultMsg.result);
|
|
812
|
+
}
|
|
813
|
+
if (resultMsg.usage) {
|
|
814
|
+
console.log(chalk.gray("\n\u{1F4CA} Session Stats:"));
|
|
815
|
+
console.log(chalk.gray(` \u2022 Turns: ${resultMsg.num_turns}`));
|
|
816
|
+
console.log(chalk.gray(` \u2022 Input tokens: ${resultMsg.usage.input_tokens}`));
|
|
817
|
+
console.log(chalk.gray(` \u2022 Output tokens: ${resultMsg.usage.output_tokens}`));
|
|
818
|
+
if (resultMsg.usage.cache_read_input_tokens) {
|
|
819
|
+
console.log(chalk.gray(` \u2022 Cache read tokens: ${resultMsg.usage.cache_read_input_tokens}`));
|
|
820
|
+
}
|
|
821
|
+
if (resultMsg.usage.cache_creation_input_tokens) {
|
|
822
|
+
console.log(chalk.gray(` \u2022 Cache creation tokens: ${resultMsg.usage.cache_creation_input_tokens}`));
|
|
823
|
+
}
|
|
824
|
+
console.log(chalk.gray(` \u2022 Cost: $${resultMsg.total_cost_usd.toFixed(4)}`));
|
|
825
|
+
console.log(chalk.gray(` \u2022 Duration: ${resultMsg.duration_ms}ms`));
|
|
826
|
+
console.log(chalk.gray("\n\u{1F440} Back already?"));
|
|
827
|
+
console.log(chalk.green("\u{1F449} Press any key to continue your session in `claude`"));
|
|
828
|
+
if (onAssistantResult) {
|
|
829
|
+
Promise.resolve(onAssistantResult(resultMsg)).catch((err) => {
|
|
830
|
+
logger.debug("Error in onAssistantResult callback:", err);
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
} else if (resultMsg.subtype === "error_max_turns") {
|
|
835
|
+
console.log(chalk.red.bold("\n\u274C Error: Maximum turns reached"));
|
|
836
|
+
console.log(chalk.gray(`Completed ${resultMsg.num_turns} turns`));
|
|
837
|
+
} else if (resultMsg.subtype === "error_during_execution") {
|
|
838
|
+
console.log(chalk.red.bold("\n\u274C Error during execution"));
|
|
839
|
+
console.log(chalk.gray(`Completed ${resultMsg.num_turns} turns before error`));
|
|
840
|
+
logger.debugLargeJson("[RESULT] Error during execution", resultMsg);
|
|
841
|
+
}
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
default: {
|
|
845
|
+
const exhaustiveCheck = message;
|
|
846
|
+
if (process.env.DEBUG) {
|
|
847
|
+
console.log(chalk.gray(`[Unknown message type]`), exhaustiveCheck);
|
|
682
848
|
}
|
|
683
849
|
}
|
|
684
|
-
} catch (error) {
|
|
685
|
-
logger.debug("[CLAUDE SDK] [ERROR] SDK error:", error);
|
|
686
|
-
yield { type: "error", error: error instanceof Error ? error.message : String(error) };
|
|
687
|
-
yield { type: "exit", code: 1, signal: null };
|
|
688
850
|
}
|
|
689
851
|
}
|
|
690
|
-
function
|
|
691
|
-
|
|
692
|
-
const modeMap = {
|
|
693
|
-
"auto": "acceptEdits",
|
|
694
|
-
"default": "default",
|
|
695
|
-
"plan": "bypassPermissions"
|
|
696
|
-
};
|
|
697
|
-
return modeMap[mode];
|
|
852
|
+
function printDivider() {
|
|
853
|
+
console.log(chalk.gray("\u2550".repeat(60)));
|
|
698
854
|
}
|
|
699
855
|
|
|
700
|
-
function
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
856
|
+
function claudeCheckSession(sessionId, path) {
|
|
857
|
+
const projectName = node_path.resolve(path).replace(/\//g, "-");
|
|
858
|
+
const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
|
|
859
|
+
const sessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
|
|
860
|
+
const sessionExists = node_fs.existsSync(sessionFile);
|
|
861
|
+
if (!sessionExists) {
|
|
862
|
+
logger.debug(`[claudeCheckSession] Path ${sessionFile} does not exist`);
|
|
863
|
+
return false;
|
|
705
864
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
if (options.model) {
|
|
714
|
-
args.push("-m", options.model);
|
|
715
|
-
}
|
|
716
|
-
if (options.permissionMode) {
|
|
717
|
-
args.push("-p", options.permissionMode);
|
|
718
|
-
}
|
|
719
|
-
logger.debug("[PTY] Creating PTY process with args:", args);
|
|
720
|
-
const ptyProcess = pty__namespace.spawn(claudePath(), args, {
|
|
721
|
-
name: "xterm-256color",
|
|
722
|
-
cols: process.stdout.columns,
|
|
723
|
-
rows: process.stdout.rows,
|
|
724
|
-
cwd: options.workingDirectory,
|
|
725
|
-
env: process.env
|
|
726
|
-
});
|
|
727
|
-
logger.debug("[PTY] PTY process created, pid:", ptyProcess.pid);
|
|
728
|
-
ptyProcess.onData((data) => {
|
|
729
|
-
process.stdout.write(data);
|
|
730
|
-
});
|
|
731
|
-
const resizeHandler = () => {
|
|
732
|
-
logger.debug("[PTY] SIGWINCH received, resizing to:", { cols: process.stdout.columns, rows: process.stdout.rows });
|
|
733
|
-
ptyProcess.resize(process.stdout.columns, process.stdout.rows);
|
|
734
|
-
};
|
|
735
|
-
process.on("SIGWINCH", resizeHandler);
|
|
736
|
-
const exitPromise = new Promise((resolve) => {
|
|
737
|
-
ptyProcess.onExit((exitCode) => {
|
|
738
|
-
logger.debug("[PTY] PTY process exited with code:", exitCode.exitCode);
|
|
739
|
-
logger.debug("[PTY] Removing SIGWINCH handler");
|
|
740
|
-
process.removeListener("SIGWINCH", resizeHandler);
|
|
741
|
-
resolve(exitCode.exitCode);
|
|
742
|
-
});
|
|
743
|
-
});
|
|
744
|
-
return {
|
|
745
|
-
write: (data) => {
|
|
746
|
-
ptyProcess.write(data);
|
|
747
|
-
},
|
|
748
|
-
kill: () => {
|
|
749
|
-
logger.debug("[PTY] Kill called");
|
|
750
|
-
process.removeListener("SIGWINCH", resizeHandler);
|
|
751
|
-
ptyProcess.kill();
|
|
752
|
-
},
|
|
753
|
-
waitForExit: () => exitPromise,
|
|
754
|
-
resize: (cols, rows) => {
|
|
755
|
-
logger.debug("[PTY] Manual resize called:", { cols, rows });
|
|
756
|
-
ptyProcess.resize(cols, rows);
|
|
865
|
+
const sessionData = node_fs.readFileSync(sessionFile, "utf-8").split("\n");
|
|
866
|
+
const hasGoodMessage = !!sessionData.find((v) => {
|
|
867
|
+
try {
|
|
868
|
+
return typeof JSON.parse(v).uuid === "string";
|
|
869
|
+
} catch (e) {
|
|
870
|
+
return false;
|
|
757
871
|
}
|
|
758
|
-
};
|
|
872
|
+
});
|
|
873
|
+
return hasGoodMessage;
|
|
759
874
|
}
|
|
760
875
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
}).loose();
|
|
766
|
-
const SDKMessageSchema = zod.z.object({
|
|
767
|
-
session_id: zod.z.string().optional(),
|
|
768
|
-
type: zod.z.string(),
|
|
769
|
-
subtype: zod.z.string().optional()
|
|
770
|
-
}).loose();
|
|
771
|
-
function parseClaudePersistedMessage(message) {
|
|
772
|
-
const result = PersisstedMessageSchema.safeParse(message);
|
|
773
|
-
if (!result.success) {
|
|
774
|
-
logger.debug("[ERROR] Failed to parse interactive message:", result.error);
|
|
775
|
-
logger.debugLargeJson("[ERROR] Message:", message);
|
|
776
|
-
return void 0;
|
|
876
|
+
async function claudeRemote(opts) {
|
|
877
|
+
let startFrom = opts.sessionId;
|
|
878
|
+
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
879
|
+
startFrom = null;
|
|
777
880
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
function parseClaudeSdkMessage(message) {
|
|
789
|
-
const result = SDKMessageSchema.safeParse(message);
|
|
790
|
-
if (!result.success) {
|
|
791
|
-
logger.debug("[ERROR] Failed to parse SDK message:", result.error);
|
|
792
|
-
return void 0;
|
|
793
|
-
}
|
|
794
|
-
return {
|
|
795
|
-
sessionId: result.data.session_id,
|
|
796
|
-
type: result.data.type,
|
|
797
|
-
rawMessage: {
|
|
798
|
-
...message,
|
|
799
|
-
// Lets patch the message with another type of id just in case
|
|
800
|
-
session_id: result.data.session_id
|
|
801
|
-
}
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
async function* watchMostRecentSession(workingDirectory, abortController) {
|
|
806
|
-
const projectName = node_path.resolve(workingDirectory).replace(/\//g, "-");
|
|
807
|
-
const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
|
|
808
|
-
logger.debug(`Starting session watcher for project: ${projectName}`);
|
|
809
|
-
logger.debug(`Watching directory: ${projectDir}`);
|
|
810
|
-
if (!fs.existsSync(projectDir)) {
|
|
811
|
-
logger.debug("Project directory does not exist, creating it");
|
|
812
|
-
await promises.mkdir(projectDir, { recursive: true });
|
|
813
|
-
}
|
|
814
|
-
const getSessionFiles = async () => {
|
|
815
|
-
const files = await promises.readdir(projectDir);
|
|
816
|
-
return files.filter((f) => f.endsWith(".jsonl")).map((f) => ({
|
|
817
|
-
name: f,
|
|
818
|
-
path: node_path.join(projectDir, f),
|
|
819
|
-
sessionId: f.replace(".jsonl", "")
|
|
820
|
-
}));
|
|
881
|
+
const abortController = new AbortController();
|
|
882
|
+
const sdkOptions = {
|
|
883
|
+
cwd: opts.path,
|
|
884
|
+
resume: startFrom ?? void 0,
|
|
885
|
+
mcpServers: opts.mcpServers,
|
|
886
|
+
permissionPromptToolName: opts.permissionPromptToolName,
|
|
887
|
+
executable: "node",
|
|
888
|
+
abortController
|
|
821
889
|
};
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
if (event.filename && event.filename.endsWith(".jsonl")) {
|
|
833
|
-
const files = await getSessionFiles();
|
|
834
|
-
for (const file of files) {
|
|
835
|
-
if (!knownFiles.has(file.name)) {
|
|
836
|
-
logger.debug(`New session file detected: ${file.name}`);
|
|
837
|
-
knownFiles.add(file.name);
|
|
838
|
-
logger.debug(`Returning file path: ${file.path}`);
|
|
839
|
-
return file.path;
|
|
840
|
-
}
|
|
890
|
+
let aborted = false;
|
|
891
|
+
let response;
|
|
892
|
+
opts.abort.addEventListener("abort", () => {
|
|
893
|
+
if (!aborted) {
|
|
894
|
+
aborted = true;
|
|
895
|
+
if (response) {
|
|
896
|
+
(async () => {
|
|
897
|
+
try {
|
|
898
|
+
const r = await response.interrupt();
|
|
899
|
+
} catch (e) {
|
|
841
900
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
logger.debug("[ERROR] Directory watcher unexpected error:", err);
|
|
901
|
+
abortController.abort();
|
|
902
|
+
})();
|
|
903
|
+
} else {
|
|
904
|
+
abortController.abort();
|
|
847
905
|
}
|
|
848
|
-
logger.debug("Directory watcher aborted");
|
|
849
906
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
907
|
+
});
|
|
908
|
+
logger.debug(`[claudeRemote] Starting query with messages`);
|
|
909
|
+
response = claudeCode.query({
|
|
910
|
+
prompt: opts.messages,
|
|
911
|
+
abortController,
|
|
912
|
+
options: sdkOptions
|
|
913
|
+
});
|
|
914
|
+
if (opts.interruptController) {
|
|
915
|
+
opts.interruptController.register(async () => {
|
|
916
|
+
logger.debug("[claudeRemote] Interrupting Claude via SDK");
|
|
917
|
+
await response.interrupt();
|
|
918
|
+
});
|
|
855
919
|
}
|
|
856
|
-
|
|
857
|
-
yield* watchSessionFile(newSessionFilePath, abortController);
|
|
858
|
-
}
|
|
859
|
-
async function* watchSessionFile(filePath, abortController) {
|
|
860
|
-
logger.debug(`Watching session file: ${filePath}`);
|
|
861
|
-
let position = 0;
|
|
862
|
-
const handle = await promises.open(filePath, "r");
|
|
863
|
-
const stats = await handle.stat();
|
|
864
|
-
position = stats.size;
|
|
865
|
-
await handle.close();
|
|
866
|
-
logger.debug(`Starting file watch from position: ${position}`);
|
|
867
|
-
const fileWatcher = promises.watch(filePath, { signal: abortController.signal });
|
|
920
|
+
printDivider();
|
|
868
921
|
try {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
const
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
logger.debugLargeJson("[WATCHER] Message:", message);
|
|
882
|
-
yield message;
|
|
883
|
-
} else {
|
|
884
|
-
logger.debug("[ERROR] Skipping invalid JSON line");
|
|
885
|
-
}
|
|
886
|
-
} catch {
|
|
887
|
-
logger.debug("Skipping invalid JSON line");
|
|
922
|
+
logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
923
|
+
for await (const message of response) {
|
|
924
|
+
logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
|
|
925
|
+
formatClaudeMessage(message, opts.onAssistantResult);
|
|
926
|
+
if (message.type === "system" && message.subtype === "init") {
|
|
927
|
+
const projectName = node_path.resolve(opts.path).replace(/\//g, "-");
|
|
928
|
+
const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
|
|
929
|
+
node_fs.mkdirSync(projectDir, { recursive: true });
|
|
930
|
+
const watcher = node_fs.watch(projectDir).on("change", (_, filename) => {
|
|
931
|
+
if (filename === `${message.session_id}.jsonl`) {
|
|
932
|
+
opts.onSessionFound(message.session_id);
|
|
933
|
+
watcher.close();
|
|
888
934
|
}
|
|
889
|
-
}
|
|
890
|
-
rl.close();
|
|
891
|
-
await handle2.close();
|
|
892
|
-
const newHandle = await promises.open(filePath, "r");
|
|
893
|
-
const stats2 = await newHandle.stat();
|
|
894
|
-
const oldPosition = position;
|
|
895
|
-
position = stats2.size;
|
|
896
|
-
logger.debug(`Updated file position: ${oldPosition} -> ${position}`);
|
|
897
|
-
await newHandle.close();
|
|
935
|
+
});
|
|
898
936
|
}
|
|
899
937
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
938
|
+
logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
939
|
+
} catch (e) {
|
|
940
|
+
if (abortController.signal.aborted) {
|
|
941
|
+
logger.debug(`[claudeRemote] Aborted`);
|
|
942
|
+
}
|
|
943
|
+
if (e instanceof claudeCode.AbortError) {
|
|
944
|
+
logger.debug(`[claudeRemote] Aborted`);
|
|
945
|
+
} else {
|
|
946
|
+
throw e;
|
|
947
|
+
}
|
|
948
|
+
} finally {
|
|
949
|
+
if (opts.interruptController) {
|
|
950
|
+
opts.interruptController.unregister();
|
|
904
951
|
}
|
|
905
|
-
logger.debug("File watcher aborted");
|
|
906
952
|
}
|
|
953
|
+
printDivider();
|
|
954
|
+
logger.debug(`[claudeRemote] Function completed`);
|
|
907
955
|
}
|
|
908
956
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
logger.debug(`[LOOP] New session detected from watcher: ${event.sessionId}`);
|
|
933
|
-
currentClaudeSessionId = event.sessionId;
|
|
934
|
-
logger.debug("[LOOP] Updated currentSessionId to:", currentClaudeSessionId);
|
|
935
|
-
}
|
|
936
|
-
if (event.rawMessage) {
|
|
937
|
-
if (event.type === "user" && event.rawMessage.message) {
|
|
938
|
-
const userMessage = {
|
|
939
|
-
role: "user",
|
|
940
|
-
localKey: event.rawMessage.uuid,
|
|
941
|
-
// Use Claude's UUID as localKey
|
|
942
|
-
sentFrom: "cli",
|
|
943
|
-
// Identify this as coming from CLI
|
|
944
|
-
content: {
|
|
945
|
-
type: "text",
|
|
946
|
-
text: event.rawMessage.message.content
|
|
947
|
-
}
|
|
948
|
-
};
|
|
949
|
-
session.sendMessage(userMessage);
|
|
950
|
-
} else if (event.type === "assistant") {
|
|
951
|
-
session.sendMessage({
|
|
952
|
-
data: event.rawMessage,
|
|
953
|
-
type: "output"
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
+
const __dirname$1 = 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))));
|
|
958
|
+
async function claudeLocal(opts) {
|
|
959
|
+
const projectName = node_path.resolve(opts.path).replace(/\//g, "-");
|
|
960
|
+
const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
|
|
961
|
+
node_fs.mkdirSync(projectDir, { recursive: true });
|
|
962
|
+
const watcher = node_fs.watch(projectDir);
|
|
963
|
+
let resolvedSessionId = null;
|
|
964
|
+
const detectedIdsRandomUUID = /* @__PURE__ */ new Set();
|
|
965
|
+
const detectedIdsFileSystem = /* @__PURE__ */ new Set();
|
|
966
|
+
watcher.on("change", (event, filename) => {
|
|
967
|
+
if (typeof filename === "string" && filename.toLowerCase().endsWith(".jsonl")) {
|
|
968
|
+
logger.debug("change", event, filename);
|
|
969
|
+
const sessionId = filename.replace(".jsonl", "");
|
|
970
|
+
if (detectedIdsFileSystem.has(sessionId)) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
detectedIdsFileSystem.add(sessionId);
|
|
974
|
+
if (resolvedSessionId) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (detectedIdsRandomUUID.has(sessionId)) {
|
|
978
|
+
resolvedSessionId = sessionId;
|
|
979
|
+
opts.onSessionFound(sessionId);
|
|
957
980
|
}
|
|
958
|
-
};
|
|
959
|
-
void startWatcher();
|
|
960
|
-
logger.info(chalk.bold.blue("\u{1F4F1} Happy CLI - Interactive Mode"));
|
|
961
|
-
if (process.env.DEBUG) {
|
|
962
|
-
logger.logFilePathPromise.then((path) => {
|
|
963
|
-
logger.info(`Debug file for this session: ${path}`);
|
|
964
|
-
});
|
|
965
981
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
const cols = process.stdout.columns;
|
|
978
|
-
const rows = process.stdout.rows;
|
|
979
|
-
logger.debug("[LOOP] Force resize timeout fire.d");
|
|
980
|
-
logger.debug("[LOOP] Terminal size:", { cols, rows });
|
|
981
|
-
logger.debug("[LOOP] Resizing to cols-1, rows-1");
|
|
982
|
-
interactiveProcess.resize(cols - 1, rows - 1);
|
|
983
|
-
setTimeout(() => {
|
|
984
|
-
logger.debug("[LOOP] Second resize timeout fired");
|
|
985
|
-
logger.debug("[LOOP] Resizing back to normal size");
|
|
986
|
-
interactiveProcess?.resize(cols, rows);
|
|
987
|
-
}, 10);
|
|
988
|
-
} else {
|
|
989
|
-
logger.debug("[LOOP] Force resize skipped - no process or invalid terminal size");
|
|
982
|
+
});
|
|
983
|
+
let startFrom = opts.sessionId;
|
|
984
|
+
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
985
|
+
startFrom = null;
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
process.stdin.pause();
|
|
989
|
+
await new Promise((r, reject) => {
|
|
990
|
+
const args = [];
|
|
991
|
+
if (startFrom) {
|
|
992
|
+
args.push("--resume", startFrom);
|
|
990
993
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
994
|
+
const claudeCliPath = process.env.HAPPY_CLAUDE_CLI_PATH || node_path.resolve(node_path.join(__dirname$1, "..", "scripts", "claudeInteractiveLaunch.cjs"));
|
|
995
|
+
const child = node_child_process.spawn("node", [claudeCliPath, ...args], {
|
|
996
|
+
stdio: ["inherit", "inherit", "inherit", "pipe"],
|
|
997
|
+
signal: opts.abort,
|
|
998
|
+
cwd: opts.path
|
|
999
|
+
});
|
|
1000
|
+
if (child.stdio[3]) {
|
|
1001
|
+
const rl = node_readline.createInterface({
|
|
1002
|
+
input: child.stdio[3],
|
|
1003
|
+
crlfDelay: Infinity
|
|
1004
|
+
});
|
|
1005
|
+
rl.on("line", (line) => {
|
|
1006
|
+
const sessionMatch = line.match(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i);
|
|
1007
|
+
if (sessionMatch) {
|
|
1008
|
+
detectedIdsRandomUUID.add(sessionMatch[0]);
|
|
1009
|
+
if (resolvedSessionId) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
if (detectedIdsFileSystem.has(sessionMatch[0])) {
|
|
1013
|
+
resolvedSessionId = sessionMatch[0];
|
|
1014
|
+
opts.onSessionFound(sessionMatch[0]);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
rl.on("error", (err) => {
|
|
1019
|
+
console.error("Error reading from fd 3:", err);
|
|
1020
|
+
});
|
|
1002
1021
|
}
|
|
1022
|
+
child.on("error", (error) => {
|
|
1023
|
+
});
|
|
1024
|
+
child.on("exit", (code, signal) => {
|
|
1025
|
+
if (signal === "SIGTERM" && opts.abort.aborted) {
|
|
1026
|
+
r();
|
|
1027
|
+
} else if (signal) {
|
|
1028
|
+
reject(new Error(`Process terminated with signal: ${signal}`));
|
|
1029
|
+
} else {
|
|
1030
|
+
r();
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1003
1033
|
});
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1034
|
+
} finally {
|
|
1035
|
+
watcher.close();
|
|
1036
|
+
process.stdin.resume();
|
|
1037
|
+
}
|
|
1038
|
+
return resolvedSessionId;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
class MessageQueue {
|
|
1042
|
+
queue = [];
|
|
1043
|
+
waiters = [];
|
|
1044
|
+
closed = false;
|
|
1045
|
+
closePromise;
|
|
1046
|
+
closeResolve;
|
|
1047
|
+
constructor() {
|
|
1048
|
+
this.closePromise = new Promise((resolve) => {
|
|
1049
|
+
this.closeResolve = resolve;
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Push a message to the queue
|
|
1054
|
+
*/
|
|
1055
|
+
push(message) {
|
|
1056
|
+
if (this.closed) {
|
|
1057
|
+
throw new Error("Cannot push to closed queue");
|
|
1058
|
+
}
|
|
1059
|
+
logger.debug(`[MessageQueue] push() called. Waiters: ${this.waiters.length}, Queue size before: ${this.queue.length}`);
|
|
1060
|
+
const waiter = this.waiters.shift();
|
|
1061
|
+
if (waiter) {
|
|
1062
|
+
logger.debug(`[MessageQueue] Found waiter! Delivering message directly: "${message}"`);
|
|
1063
|
+
waiter({
|
|
1064
|
+
type: "user",
|
|
1065
|
+
message: {
|
|
1066
|
+
role: "user",
|
|
1067
|
+
content: message
|
|
1068
|
+
},
|
|
1069
|
+
parent_tool_use_id: null,
|
|
1070
|
+
session_id: ""
|
|
1071
|
+
});
|
|
1020
1072
|
} else {
|
|
1021
|
-
logger.debug(
|
|
1073
|
+
logger.debug(`[MessageQueue] No waiter found. Adding to queue: "${message}"`);
|
|
1074
|
+
this.queue.push({
|
|
1075
|
+
type: "user",
|
|
1076
|
+
message: {
|
|
1077
|
+
role: "user",
|
|
1078
|
+
content: message
|
|
1079
|
+
},
|
|
1080
|
+
parent_tool_use_id: null,
|
|
1081
|
+
session_id: ""
|
|
1082
|
+
});
|
|
1022
1083
|
}
|
|
1023
|
-
logger.
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
logger.debug(
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
break;
|
|
1084
|
+
logger.debug(`[MessageQueue] push() completed. Waiters: ${this.waiters.length}, Queue size after: ${this.queue.length}`);
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Close the queue - no more messages can be pushed
|
|
1088
|
+
*/
|
|
1089
|
+
close() {
|
|
1090
|
+
logger.debug(`[MessageQueue] close() called. Waiters: ${this.waiters.length}`);
|
|
1091
|
+
this.closed = true;
|
|
1092
|
+
this.closeResolve?.();
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Check if the queue is closed
|
|
1096
|
+
*/
|
|
1097
|
+
isClosed() {
|
|
1098
|
+
return this.closed;
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Get the current queue size
|
|
1102
|
+
*/
|
|
1103
|
+
size() {
|
|
1104
|
+
return this.queue.length;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Async iterator implementation
|
|
1108
|
+
*/
|
|
1109
|
+
async *[Symbol.asyncIterator]() {
|
|
1110
|
+
logger.debug(`[MessageQueue] Iterator started`);
|
|
1111
|
+
while (true) {
|
|
1112
|
+
const message = this.queue.shift();
|
|
1113
|
+
if (message !== void 0) {
|
|
1114
|
+
logger.debug(`[MessageQueue] Iterator yielding queued message`);
|
|
1115
|
+
yield message;
|
|
1116
|
+
continue;
|
|
1057
1117
|
}
|
|
1058
|
-
if (
|
|
1059
|
-
logger.
|
|
1060
|
-
|
|
1061
|
-
data: output.data,
|
|
1062
|
-
type: "output"
|
|
1063
|
-
});
|
|
1064
|
-
const claudeSdkMessage = parseClaudeSdkMessage(output.data);
|
|
1065
|
-
if (claudeSdkMessage) {
|
|
1066
|
-
currentClaudeSessionId = claudeSdkMessage.sessionId;
|
|
1067
|
-
logger.debug(`[LOOP] Updated session ID from SDK: ${currentClaudeSessionId}`);
|
|
1068
|
-
logger.debugLargeJson("[LOOP] Full init data:", output.data);
|
|
1069
|
-
}
|
|
1118
|
+
if (this.closed) {
|
|
1119
|
+
logger.debug(`[MessageQueue] Iterator ending - queue closed`);
|
|
1120
|
+
return;
|
|
1070
1121
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
logger.debug("Received remote message, adding to queue");
|
|
1077
|
-
messageQueue.push(message);
|
|
1078
|
-
if (mode === "interactive") {
|
|
1079
|
-
requestSwitchToRemote();
|
|
1122
|
+
logger.debug(`[MessageQueue] Iterator waiting for next message...`);
|
|
1123
|
+
const nextMessage = await this.waitForNext();
|
|
1124
|
+
if (nextMessage === void 0) {
|
|
1125
|
+
logger.debug(`[MessageQueue] Iterator ending - no more messages`);
|
|
1126
|
+
return;
|
|
1080
1127
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1128
|
+
logger.debug(`[MessageQueue] Iterator yielding waited message`);
|
|
1129
|
+
yield nextMessage;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Wait for the next message or queue closure
|
|
1134
|
+
*/
|
|
1135
|
+
waitForNext() {
|
|
1136
|
+
return new Promise((resolve) => {
|
|
1137
|
+
if (this.closed) {
|
|
1138
|
+
logger.debug(`[MessageQueue] waitForNext() called but queue is closed`);
|
|
1139
|
+
resolve(void 0);
|
|
1140
|
+
return;
|
|
1085
1141
|
}
|
|
1142
|
+
const waiter = (value) => resolve(value);
|
|
1143
|
+
this.waiters.push(waiter);
|
|
1144
|
+
logger.debug(`[MessageQueue] waitForNext() adding waiter. Total waiters: ${this.waiters.length}`);
|
|
1145
|
+
this.closePromise?.then(() => {
|
|
1146
|
+
const index = this.waiters.indexOf(waiter);
|
|
1147
|
+
if (index !== -1) {
|
|
1148
|
+
this.waiters.splice(index, 1);
|
|
1149
|
+
logger.debug(`[MessageQueue] waitForNext() waiter removed due to close. Remaining waiters: ${this.waiters.length}`);
|
|
1150
|
+
resolve(void 0);
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1086
1153
|
});
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
class InvalidateSync {
|
|
1158
|
+
_invalidated = false;
|
|
1159
|
+
_invalidatedDouble = false;
|
|
1160
|
+
_stopped = false;
|
|
1161
|
+
_command;
|
|
1162
|
+
_pendings = [];
|
|
1163
|
+
constructor(command) {
|
|
1164
|
+
this._command = command;
|
|
1165
|
+
}
|
|
1166
|
+
invalidate() {
|
|
1167
|
+
if (this._stopped) {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
if (!this._invalidated) {
|
|
1171
|
+
this._invalidated = true;
|
|
1172
|
+
this._invalidatedDouble = false;
|
|
1173
|
+
this._doSync();
|
|
1174
|
+
} else {
|
|
1175
|
+
if (!this._invalidatedDouble) {
|
|
1176
|
+
this._invalidatedDouble = true;
|
|
1096
1177
|
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
async invalidateAndAwait() {
|
|
1181
|
+
if (this._stopped) {
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
await new Promise((resolve) => {
|
|
1185
|
+
this._pendings.push(resolve);
|
|
1186
|
+
this.invalidate();
|
|
1097
1187
|
});
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1188
|
+
}
|
|
1189
|
+
stop() {
|
|
1190
|
+
if (this._stopped) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
this._notifyPendings();
|
|
1194
|
+
this._stopped = true;
|
|
1195
|
+
}
|
|
1196
|
+
_notifyPendings = () => {
|
|
1197
|
+
for (let pending of this._pendings) {
|
|
1198
|
+
pending();
|
|
1199
|
+
}
|
|
1200
|
+
this._pendings = [];
|
|
1201
|
+
};
|
|
1202
|
+
_doSync = async () => {
|
|
1203
|
+
await backoff(async () => {
|
|
1204
|
+
if (this._stopped) {
|
|
1103
1205
|
return;
|
|
1104
1206
|
}
|
|
1105
|
-
|
|
1106
|
-
interactiveProcess.write(data);
|
|
1107
|
-
} else if (mode === "remote") {
|
|
1108
|
-
logger.debug("[LOOP] Key pressed in remote mode, switching back to interactive");
|
|
1109
|
-
startInteractive();
|
|
1110
|
-
} else {
|
|
1111
|
-
logger.debug("[LOOP] [ERROR] Data received but no action taken");
|
|
1112
|
-
}
|
|
1207
|
+
await this._command();
|
|
1113
1208
|
});
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
process.exit(0);
|
|
1118
|
-
});
|
|
1119
|
-
process.on("SIGTERM", () => {
|
|
1120
|
-
logger.debug("[LOOP] SIGTERM received");
|
|
1121
|
-
cleanup();
|
|
1122
|
-
process.exit(0);
|
|
1123
|
-
});
|
|
1124
|
-
logger.debug("[LOOP] Initial startup - launching interactive mode");
|
|
1125
|
-
startInteractive();
|
|
1126
|
-
while (!exiting) {
|
|
1127
|
-
if (mode === "remote" && messageQueue.length > 0) {
|
|
1128
|
-
const message = messageQueue.shift();
|
|
1129
|
-
if (message) {
|
|
1130
|
-
await processRemoteMessage(message);
|
|
1131
|
-
}
|
|
1132
|
-
} else {
|
|
1133
|
-
logger.debug("Waiting for next message or event");
|
|
1134
|
-
await new Promise((resolve) => {
|
|
1135
|
-
messageResolve = resolve;
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1209
|
+
if (this._stopped) {
|
|
1210
|
+
this._notifyPendings();
|
|
1211
|
+
return;
|
|
1138
1212
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
exiting = true;
|
|
1143
|
-
if (interactiveProcess) {
|
|
1144
|
-
logger.debug("[LOOP] Killing interactive process in cleanup");
|
|
1145
|
-
interactiveProcess.kill();
|
|
1213
|
+
if (this._invalidatedDouble) {
|
|
1214
|
+
this._invalidatedDouble = false;
|
|
1215
|
+
this._doSync();
|
|
1146
1216
|
} else {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
if (watcherAbortController) {
|
|
1150
|
-
logger.debug("[LOOP] Aborting watcher");
|
|
1151
|
-
watcherAbortController.abort();
|
|
1217
|
+
this._invalidated = false;
|
|
1218
|
+
this._notifyPendings();
|
|
1152
1219
|
}
|
|
1153
|
-
if (messageResolve) {
|
|
1154
|
-
logger.debug("[LOOP] Waking up message loop");
|
|
1155
|
-
messageResolve();
|
|
1156
|
-
}
|
|
1157
|
-
logger.debug("[LOOP] Setting stdin raw mode to false");
|
|
1158
|
-
process.stdin.setRawMode(false);
|
|
1159
|
-
process.stdin.pause();
|
|
1160
|
-
process.stdout.write("\x1B[?25h");
|
|
1161
|
-
};
|
|
1162
|
-
const promise = run();
|
|
1163
|
-
return async () => {
|
|
1164
|
-
cleanup();
|
|
1165
|
-
await promise;
|
|
1166
1220
|
};
|
|
1167
1221
|
}
|
|
1168
1222
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1223
|
+
const UsageSchema = z.z.object({
|
|
1224
|
+
input_tokens: z.z.number().int().nonnegative(),
|
|
1225
|
+
cache_creation_input_tokens: z.z.number().int().nonnegative().optional(),
|
|
1226
|
+
cache_read_input_tokens: z.z.number().int().nonnegative().optional(),
|
|
1227
|
+
output_tokens: z.z.number().int().nonnegative(),
|
|
1228
|
+
service_tier: z.z.string().optional()
|
|
1229
|
+
});
|
|
1230
|
+
const TextContentSchema = z.z.object({
|
|
1231
|
+
type: z.z.literal("text"),
|
|
1232
|
+
text: z.z.string()
|
|
1233
|
+
});
|
|
1234
|
+
const ThinkingContentSchema = z.z.object({
|
|
1235
|
+
type: z.z.literal("thinking"),
|
|
1236
|
+
thinking: z.z.string(),
|
|
1237
|
+
signature: z.z.string()
|
|
1238
|
+
});
|
|
1239
|
+
const ToolUseContentSchema = z.z.object({
|
|
1240
|
+
type: z.z.literal("tool_use"),
|
|
1241
|
+
id: z.z.string(),
|
|
1242
|
+
name: z.z.string(),
|
|
1243
|
+
input: z.z.unknown()
|
|
1244
|
+
// Tool-specific input parameters
|
|
1245
|
+
});
|
|
1246
|
+
const ToolResultContentSchema = z.z.object({
|
|
1247
|
+
tool_use_id: z.z.string(),
|
|
1248
|
+
type: z.z.literal("tool_result"),
|
|
1249
|
+
content: z.z.union([
|
|
1250
|
+
z.z.string(),
|
|
1251
|
+
// For simple string responses
|
|
1252
|
+
z.z.array(TextContentSchema)
|
|
1253
|
+
// For structured content blocks (typically text)
|
|
1254
|
+
]),
|
|
1255
|
+
is_error: z.z.boolean().optional()
|
|
1256
|
+
});
|
|
1257
|
+
const ContentSchema = z.z.union([
|
|
1258
|
+
TextContentSchema,
|
|
1259
|
+
ThinkingContentSchema,
|
|
1260
|
+
ToolUseContentSchema,
|
|
1261
|
+
ToolResultContentSchema
|
|
1262
|
+
]);
|
|
1263
|
+
const UserMessageSchema = z.z.object({
|
|
1264
|
+
role: z.z.literal("user"),
|
|
1265
|
+
content: z.z.union([
|
|
1266
|
+
z.z.string(),
|
|
1267
|
+
// Simple string content
|
|
1268
|
+
z.z.array(z.z.union([ToolResultContentSchema, TextContentSchema]))
|
|
1269
|
+
])
|
|
1270
|
+
});
|
|
1271
|
+
const AssistantMessageSchema = z.z.object({
|
|
1272
|
+
id: z.z.string(),
|
|
1273
|
+
type: z.z.literal("message"),
|
|
1274
|
+
role: z.z.literal("assistant"),
|
|
1275
|
+
model: z.z.string(),
|
|
1276
|
+
content: z.z.array(ContentSchema),
|
|
1277
|
+
stop_reason: z.z.string().nullable(),
|
|
1278
|
+
stop_sequence: z.z.string().nullable(),
|
|
1279
|
+
usage: UsageSchema
|
|
1280
|
+
});
|
|
1281
|
+
const BaseEntrySchema = z.z.object({
|
|
1282
|
+
cwd: z.z.string(),
|
|
1283
|
+
sessionId: z.z.string(),
|
|
1284
|
+
version: z.z.string(),
|
|
1285
|
+
uuid: z.z.string(),
|
|
1286
|
+
timestamp: z.z.string().datetime(),
|
|
1287
|
+
parent_tool_use_id: z.z.string().nullable().optional()
|
|
1288
|
+
});
|
|
1289
|
+
const SummaryEntrySchema = z.z.object({
|
|
1290
|
+
type: z.z.literal("summary"),
|
|
1291
|
+
summary: z.z.string(),
|
|
1292
|
+
leafUuid: z.z.string()
|
|
1293
|
+
});
|
|
1294
|
+
const UserEntrySchema = BaseEntrySchema.extend({
|
|
1295
|
+
type: z.z.literal("user"),
|
|
1296
|
+
message: UserMessageSchema,
|
|
1297
|
+
isMeta: z.z.boolean().optional(),
|
|
1298
|
+
toolUseResult: z.z.unknown().optional()
|
|
1299
|
+
// Present when user responds to tool use
|
|
1300
|
+
});
|
|
1301
|
+
const AssistantEntrySchema = BaseEntrySchema.extend({
|
|
1302
|
+
type: z.z.literal("assistant"),
|
|
1303
|
+
message: AssistantMessageSchema,
|
|
1304
|
+
requestId: z.z.string().optional()
|
|
1305
|
+
});
|
|
1306
|
+
const SystemEntrySchema = BaseEntrySchema.extend({
|
|
1307
|
+
type: z.z.literal("system"),
|
|
1308
|
+
content: z.z.string(),
|
|
1309
|
+
isMeta: z.z.boolean().optional(),
|
|
1310
|
+
level: z.z.string().optional(),
|
|
1311
|
+
parentUuid: z.z.string().optional(),
|
|
1312
|
+
isSidechain: z.z.boolean().optional(),
|
|
1313
|
+
userType: z.z.string().optional()
|
|
1314
|
+
});
|
|
1315
|
+
const RawJSONLinesSchema = z.z.discriminatedUnion("type", [
|
|
1316
|
+
UserEntrySchema,
|
|
1317
|
+
AssistantEntrySchema,
|
|
1318
|
+
SummaryEntrySchema,
|
|
1319
|
+
SystemEntrySchema
|
|
1320
|
+
]);
|
|
1321
|
+
|
|
1322
|
+
function createSessionScanner(opts) {
|
|
1323
|
+
const projectName = node_path.resolve(opts.workingDirectory).replace(/\//g, "-");
|
|
1324
|
+
const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
|
|
1325
|
+
let finishedSessions = /* @__PURE__ */ new Set();
|
|
1326
|
+
let pendingSessions = /* @__PURE__ */ new Set();
|
|
1327
|
+
let currentSessionId = null;
|
|
1328
|
+
let currentSessionWatcherAbortController = null;
|
|
1329
|
+
let processedMessages = /* @__PURE__ */ new Set();
|
|
1330
|
+
const sync = new InvalidateSync(async () => {
|
|
1331
|
+
let sessions = [];
|
|
1332
|
+
for (let p of pendingSessions) {
|
|
1333
|
+
sessions.push(p);
|
|
1182
1334
|
}
|
|
1183
|
-
if (
|
|
1184
|
-
|
|
1185
|
-
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
1186
|
-
return;
|
|
1335
|
+
if (currentSessionId) {
|
|
1336
|
+
sessions.push(currentSessionId);
|
|
1187
1337
|
}
|
|
1188
|
-
let
|
|
1189
|
-
|
|
1190
|
-
|
|
1338
|
+
let processSessionFile = async (sessionId) => {
|
|
1339
|
+
const expectedSessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
|
|
1340
|
+
let file;
|
|
1191
1341
|
try {
|
|
1192
|
-
|
|
1193
|
-
logger.debug("[MCP] Request:", request.method, request.params?.name || "");
|
|
1194
|
-
if (request.method === "tools/list") {
|
|
1195
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1196
|
-
res.end(JSON.stringify({
|
|
1197
|
-
jsonrpc: "2.0",
|
|
1198
|
-
id: request.id,
|
|
1199
|
-
result: {
|
|
1200
|
-
tools: [
|
|
1201
|
-
{
|
|
1202
|
-
name: "request_permission",
|
|
1203
|
-
description: "Request permission to execute a tool",
|
|
1204
|
-
inputSchema: {
|
|
1205
|
-
type: "object",
|
|
1206
|
-
properties: {
|
|
1207
|
-
tool: {
|
|
1208
|
-
type: "string",
|
|
1209
|
-
description: "The tool that needs permission"
|
|
1210
|
-
},
|
|
1211
|
-
arguments: {
|
|
1212
|
-
type: "object",
|
|
1213
|
-
description: "The arguments for the tool"
|
|
1214
|
-
}
|
|
1215
|
-
},
|
|
1216
|
-
required: ["tool", "arguments"]
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
]
|
|
1220
|
-
}
|
|
1221
|
-
}));
|
|
1222
|
-
} else if (request.method === "tools/call" && request.params?.name === "request_permission") {
|
|
1223
|
-
logger.info(`[MCP] Full request params:`, JSON.stringify(request.params, null, 2));
|
|
1224
|
-
const args = request.params.arguments || {};
|
|
1225
|
-
const { tool_name, input, tool_use_id } = args;
|
|
1226
|
-
lastRequestInput = input || {};
|
|
1227
|
-
const permissionRequest = {
|
|
1228
|
-
id: Math.random().toString(36).substring(7),
|
|
1229
|
-
tool: tool_name || "unknown",
|
|
1230
|
-
arguments: input || {},
|
|
1231
|
-
timestamp: Date.now()
|
|
1232
|
-
};
|
|
1233
|
-
logger.info(`[MCP] Permission request for tool: ${tool_name}`, input);
|
|
1234
|
-
logger.info(`[MCP] Tool use ID: ${tool_use_id}`);
|
|
1235
|
-
onPermissionRequest(permissionRequest);
|
|
1236
|
-
const response = await waitForPermissionResponse(permissionRequest.id);
|
|
1237
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1238
|
-
res.end(JSON.stringify({
|
|
1239
|
-
jsonrpc: "2.0",
|
|
1240
|
-
id: request.id,
|
|
1241
|
-
result: response
|
|
1242
|
-
}));
|
|
1243
|
-
} else if (request.method === "initialize") {
|
|
1244
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1245
|
-
res.end(JSON.stringify({
|
|
1246
|
-
jsonrpc: "2.0",
|
|
1247
|
-
id: request.id,
|
|
1248
|
-
result: {
|
|
1249
|
-
protocolVersion: "2024-11-05",
|
|
1250
|
-
capabilities: {
|
|
1251
|
-
tools: {}
|
|
1252
|
-
},
|
|
1253
|
-
serverInfo: {
|
|
1254
|
-
name: "permission-server",
|
|
1255
|
-
version: "1.0.0"
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
}));
|
|
1259
|
-
} else {
|
|
1260
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1261
|
-
res.end(JSON.stringify({
|
|
1262
|
-
jsonrpc: "2.0",
|
|
1263
|
-
id: request.id,
|
|
1264
|
-
error: {
|
|
1265
|
-
code: -32601,
|
|
1266
|
-
message: "Method not found"
|
|
1267
|
-
}
|
|
1268
|
-
}));
|
|
1269
|
-
}
|
|
1342
|
+
file = await promises.readFile(expectedSessionFile, "utf-8");
|
|
1270
1343
|
} catch (error) {
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
let lines = file.split("\n");
|
|
1347
|
+
for (let l of lines) {
|
|
1348
|
+
try {
|
|
1349
|
+
let message = JSON.parse(l);
|
|
1350
|
+
let parsed = RawJSONLinesSchema.safeParse(message);
|
|
1351
|
+
if (!parsed.success) {
|
|
1352
|
+
logger.debugLargeJson(`[SESSION_SCANNER] Failed to parse message`, message);
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
let key = getMessageKey(parsed.data);
|
|
1356
|
+
if (processedMessages.has(key)) {
|
|
1357
|
+
continue;
|
|
1278
1358
|
}
|
|
1279
|
-
|
|
1359
|
+
processedMessages.add(key);
|
|
1360
|
+
logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
|
|
1361
|
+
logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
|
|
1362
|
+
opts.onMessage(parsed.data);
|
|
1363
|
+
} catch (e) {
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1280
1366
|
}
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
await new Promise((resolve, reject) => {
|
|
1290
|
-
server = node_http.createServer(handleRequest);
|
|
1291
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1292
|
-
const address = server.address();
|
|
1293
|
-
if (address && typeof address !== "string") {
|
|
1294
|
-
port = address.port;
|
|
1295
|
-
logger.info(`[MCP] HTTP server started on port ${port}`);
|
|
1296
|
-
resolve();
|
|
1297
|
-
} else {
|
|
1298
|
-
reject(new Error("Failed to get server port"));
|
|
1367
|
+
};
|
|
1368
|
+
for (let session of sessions) {
|
|
1369
|
+
await processSessionFile(session);
|
|
1370
|
+
}
|
|
1371
|
+
for (let p of sessions) {
|
|
1372
|
+
if (pendingSessions.has(p)) {
|
|
1373
|
+
pendingSessions.delete(p);
|
|
1374
|
+
finishedSessions.add(p);
|
|
1299
1375
|
}
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1376
|
+
}
|
|
1377
|
+
currentSessionWatcherAbortController?.abort();
|
|
1378
|
+
currentSessionWatcherAbortController = new AbortController();
|
|
1379
|
+
void (async () => {
|
|
1380
|
+
if (currentSessionId) {
|
|
1381
|
+
const sessionFile = node_path.join(projectDir, `${currentSessionId}.jsonl`);
|
|
1382
|
+
try {
|
|
1383
|
+
for await (const change of promises.watch(sessionFile, { persistent: true, signal: currentSessionWatcherAbortController.signal })) {
|
|
1384
|
+
await processSessionFile(currentSessionId);
|
|
1385
|
+
}
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
if (error.name !== "AbortError") {
|
|
1388
|
+
logger.debug(`[SESSION_SCANNER] Watch error: ${error.message}`);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
})();
|
|
1305
1393
|
});
|
|
1394
|
+
const intervalId = setInterval(() => {
|
|
1395
|
+
sync.invalidate();
|
|
1396
|
+
}, 3e3);
|
|
1306
1397
|
return {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
logger.debug("[MCP] Stopping HTTP server...");
|
|
1312
|
-
return new Promise((resolve) => {
|
|
1313
|
-
server.close(() => {
|
|
1314
|
-
logger.debug("[MCP] HTTP server stopped");
|
|
1315
|
-
resolve();
|
|
1316
|
-
});
|
|
1317
|
-
});
|
|
1398
|
+
refresh: () => sync.invalidate(),
|
|
1399
|
+
cleanup: () => {
|
|
1400
|
+
clearInterval(intervalId);
|
|
1401
|
+
currentSessionWatcherAbortController?.abort();
|
|
1318
1402
|
},
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1403
|
+
onNewSession: (sessionId) => {
|
|
1404
|
+
if (currentSessionId === sessionId) {
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
if (finishedSessions.has(sessionId)) {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (pendingSessions.has(sessionId)) {
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
if (currentSessionId) {
|
|
1414
|
+
pendingSessions.add(currentSessionId);
|
|
1415
|
+
}
|
|
1416
|
+
currentSessionId = sessionId;
|
|
1417
|
+
sync.invalidate();
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
function getMessageKey(message) {
|
|
1422
|
+
if (message.type === "user") {
|
|
1423
|
+
return `user:${message.uuid}`;
|
|
1424
|
+
} else if (message.type === "assistant") {
|
|
1425
|
+
const { usage, ...messageWithoutUsage } = message.message;
|
|
1426
|
+
return stableStringify(messageWithoutUsage);
|
|
1427
|
+
} else if (message.type === "summary") {
|
|
1428
|
+
return `summary:${message.leafUuid}`;
|
|
1429
|
+
} else if (message.type === "system") {
|
|
1430
|
+
return `system:${message.uuid}`;
|
|
1431
|
+
}
|
|
1432
|
+
return `unknown:<error, this should be unreachable>`;
|
|
1433
|
+
}
|
|
1434
|
+
function stableStringify(obj) {
|
|
1435
|
+
return JSON.stringify(sortKeys(obj), null, 2);
|
|
1436
|
+
}
|
|
1437
|
+
function sortKeys(value) {
|
|
1438
|
+
if (Array.isArray(value)) {
|
|
1439
|
+
return value.map(sortKeys);
|
|
1440
|
+
} else if (value && typeof value === "object" && value.constructor === Object) {
|
|
1441
|
+
return Object.keys(value).sort().reduce((result, key) => {
|
|
1442
|
+
result[key] = sortKeys(value[key]);
|
|
1443
|
+
return result;
|
|
1444
|
+
}, {});
|
|
1445
|
+
} else {
|
|
1446
|
+
return value;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
async function loop(opts) {
|
|
1451
|
+
let mode = "interactive";
|
|
1452
|
+
let currentMessageQueue = new MessageQueue();
|
|
1453
|
+
let sessionId = null;
|
|
1454
|
+
let onMessage = null;
|
|
1455
|
+
let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
|
|
1456
|
+
opts.session.onUserMessage((message) => {
|
|
1457
|
+
logger.debugLargeJson("User message pushed to queue:", message);
|
|
1458
|
+
currentMessageQueue.push(message.content.text);
|
|
1459
|
+
seenRemoteUserMessageCounters.set(message.content.text, (seenRemoteUserMessageCounters.get(message.content.text) || 0) + 1);
|
|
1460
|
+
if (onMessage) {
|
|
1461
|
+
onMessage();
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
const sessionScanner = createSessionScanner({
|
|
1465
|
+
workingDirectory: opts.path,
|
|
1466
|
+
onMessage: (message) => {
|
|
1467
|
+
if (message.type === "user" && typeof message.message.content === "string") {
|
|
1468
|
+
const currentCounter = seenRemoteUserMessageCounters.get(message.message.content);
|
|
1469
|
+
if (currentCounter && currentCounter > 0) {
|
|
1470
|
+
seenRemoteUserMessageCounters.set(message.message.content, currentCounter - 1);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
opts.session.sendClaudeSessionMessage(message);
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
let onSessionFound = (newSessionId) => {
|
|
1478
|
+
sessionId = newSessionId;
|
|
1479
|
+
sessionScanner.onNewSession(newSessionId);
|
|
1480
|
+
};
|
|
1481
|
+
while (true) {
|
|
1482
|
+
if (currentMessageQueue.size() > 0) {
|
|
1483
|
+
mode = "remote";
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
if (mode === "interactive") {
|
|
1487
|
+
let abortedOutside = false;
|
|
1488
|
+
const interactiveAbortController = new AbortController();
|
|
1489
|
+
opts.session.setHandler("switch", () => {
|
|
1490
|
+
if (!interactiveAbortController.signal.aborted) {
|
|
1491
|
+
abortedOutside = true;
|
|
1492
|
+
mode = "remote";
|
|
1493
|
+
interactiveAbortController.abort();
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
onMessage = () => {
|
|
1497
|
+
if (!interactiveAbortController.signal.aborted) {
|
|
1498
|
+
abortedOutside = true;
|
|
1499
|
+
mode = "remote";
|
|
1500
|
+
interactiveAbortController.abort();
|
|
1501
|
+
}
|
|
1502
|
+
onMessage = null;
|
|
1503
|
+
};
|
|
1504
|
+
await claudeLocal({
|
|
1505
|
+
path: opts.path,
|
|
1506
|
+
sessionId,
|
|
1507
|
+
onSessionFound,
|
|
1508
|
+
abort: interactiveAbortController.signal
|
|
1509
|
+
});
|
|
1510
|
+
onMessage = null;
|
|
1511
|
+
if (!abortedOutside) {
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (mode !== "interactive") {
|
|
1515
|
+
console.log("Switching to remote mode...");
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
if (mode === "remote") {
|
|
1519
|
+
logger.debug("Starting " + sessionId);
|
|
1520
|
+
const remoteAbortController = new AbortController();
|
|
1521
|
+
opts.session.setHandler("abort", () => {
|
|
1522
|
+
if (!remoteAbortController.signal.aborted) {
|
|
1523
|
+
remoteAbortController.abort();
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
const abortHandler = () => {
|
|
1527
|
+
if (!remoteAbortController.signal.aborted) {
|
|
1528
|
+
mode = "interactive";
|
|
1529
|
+
remoteAbortController.abort();
|
|
1530
|
+
}
|
|
1531
|
+
process.stdin.setRawMode(false);
|
|
1532
|
+
};
|
|
1533
|
+
process.stdin.resume();
|
|
1534
|
+
process.stdin.setRawMode(true);
|
|
1535
|
+
process.stdin.setEncoding("utf8");
|
|
1536
|
+
process.stdin.on("data", abortHandler);
|
|
1537
|
+
try {
|
|
1538
|
+
logger.debug(`Starting claudeRemote with messages: ${currentMessageQueue.size()}`);
|
|
1539
|
+
await claudeRemote({
|
|
1540
|
+
abort: remoteAbortController.signal,
|
|
1541
|
+
sessionId,
|
|
1542
|
+
path: opts.path,
|
|
1543
|
+
mcpServers: opts.mcpServers,
|
|
1544
|
+
permissionPromptToolName: opts.permissionPromptToolName,
|
|
1545
|
+
onSessionFound,
|
|
1546
|
+
messages: currentMessageQueue,
|
|
1547
|
+
onAssistantResult: opts.onAssistantResult,
|
|
1548
|
+
interruptController: opts.interruptController
|
|
1333
1549
|
});
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1550
|
+
} finally {
|
|
1551
|
+
process.stdin.off("data", abortHandler);
|
|
1552
|
+
process.stdin.setRawMode(false);
|
|
1553
|
+
currentMessageQueue.close();
|
|
1554
|
+
currentMessageQueue = new MessageQueue();
|
|
1555
|
+
}
|
|
1556
|
+
if (mode !== "remote") {
|
|
1557
|
+
console.log("Switching back to good old claude...");
|
|
1337
1558
|
}
|
|
1338
1559
|
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
async function startPermissionServerV2(handler) {
|
|
1564
|
+
const mcp = new mcp_js.McpServer({
|
|
1565
|
+
name: "Permission Server",
|
|
1566
|
+
version: "1.0.0",
|
|
1567
|
+
description: "A server that allows you to request permissions from the user"
|
|
1568
|
+
});
|
|
1569
|
+
mcp.registerTool("ask_permission", {
|
|
1570
|
+
description: "Request permission to execute a tool",
|
|
1571
|
+
title: "Request Permission",
|
|
1572
|
+
inputSchema: {
|
|
1573
|
+
tool_name: z.z.string().describe("The tool that needs permission"),
|
|
1574
|
+
input: z.z.any().describe("The arguments for the tool")
|
|
1575
|
+
}
|
|
1576
|
+
// outputSchema: {
|
|
1577
|
+
// approved: z.boolean().describe('Whether the tool was approved'),
|
|
1578
|
+
// reason: z.string().describe('The reason for the approval or denial'),
|
|
1579
|
+
// },
|
|
1580
|
+
}, async (args) => {
|
|
1581
|
+
const response = await handler({ name: args.tool_name, arguments: args.input });
|
|
1582
|
+
const result = response.approved ? { behavior: "allow", updatedInput: args.input || {} } : { behavior: "deny", message: response.reason || "Permission denied by user" };
|
|
1583
|
+
return {
|
|
1584
|
+
content: [
|
|
1585
|
+
{
|
|
1586
|
+
type: "text",
|
|
1587
|
+
text: JSON.stringify(result)
|
|
1588
|
+
}
|
|
1589
|
+
],
|
|
1590
|
+
isError: false
|
|
1591
|
+
};
|
|
1592
|
+
});
|
|
1593
|
+
const transport = new streamableHttp_js.StreamableHTTPServerTransport({
|
|
1594
|
+
// NOTE: Returning session id here will result in claude
|
|
1595
|
+
// sdk spawn to fail with `Invalid Request: Server already initialized`
|
|
1596
|
+
sessionIdGenerator: void 0
|
|
1597
|
+
});
|
|
1598
|
+
await mcp.connect(transport);
|
|
1599
|
+
const server = node_http.createServer(async (req, res) => {
|
|
1600
|
+
try {
|
|
1601
|
+
await transport.handleRequest(req, res);
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
logger.debug("Error handling request:", error);
|
|
1604
|
+
if (!res.headersSent) {
|
|
1605
|
+
res.writeHead(500).end();
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
const baseUrl = await new Promise((resolve) => {
|
|
1610
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1611
|
+
const addr = server.address();
|
|
1612
|
+
resolve(new URL(`http://127.0.0.1:${addr.port}`));
|
|
1613
|
+
});
|
|
1614
|
+
});
|
|
1615
|
+
return {
|
|
1616
|
+
url: baseUrl.toString(),
|
|
1617
|
+
toolName: "ask_permission"
|
|
1339
1618
|
};
|
|
1340
1619
|
}
|
|
1341
1620
|
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1621
|
+
class InterruptController {
|
|
1622
|
+
interruptFn;
|
|
1623
|
+
isInterrupting = false;
|
|
1624
|
+
/**
|
|
1625
|
+
* Register an interrupt function from claudeRemote
|
|
1626
|
+
*/
|
|
1627
|
+
register(fn) {
|
|
1628
|
+
this.interruptFn = fn;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Unregister the interrupt function (cleanup)
|
|
1632
|
+
*/
|
|
1633
|
+
unregister() {
|
|
1634
|
+
this.interruptFn = void 0;
|
|
1635
|
+
this.isInterrupting = false;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Trigger the interrupt - can be called from anywhere
|
|
1639
|
+
*/
|
|
1640
|
+
async interrupt() {
|
|
1641
|
+
if (!this.interruptFn || this.isInterrupting) {
|
|
1642
|
+
return false;
|
|
1643
|
+
}
|
|
1644
|
+
this.isInterrupting = true;
|
|
1645
|
+
try {
|
|
1646
|
+
await this.interruptFn();
|
|
1647
|
+
return true;
|
|
1648
|
+
} catch (error) {
|
|
1649
|
+
logger.debug("Failed to interrupt Claude:", error);
|
|
1650
|
+
return false;
|
|
1651
|
+
} finally {
|
|
1652
|
+
this.isInterrupting = false;
|
|
1358
1653
|
}
|
|
1359
1654
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1655
|
+
/**
|
|
1656
|
+
* Check if interrupt is available
|
|
1657
|
+
*/
|
|
1658
|
+
canInterrupt() {
|
|
1659
|
+
return !!this.interruptFn && !this.isInterrupting;
|
|
1364
1660
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
const
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
async function start(credentials, options = {}) {
|
|
1664
|
+
const workingDirectory = process.cwd();
|
|
1665
|
+
const sessionTag = node_crypto.randomUUID();
|
|
1666
|
+
const api = new ApiClient(credentials.token, credentials.secret);
|
|
1369
1667
|
let state = {};
|
|
1370
1668
|
let metadata = { path: workingDirectory, host: os.hostname() };
|
|
1371
1669
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
1372
|
-
logger.
|
|
1373
|
-
if (needsOnboarding) {
|
|
1374
|
-
const handyUrl = generateAppUrl(secret);
|
|
1375
|
-
displayQRCode(handyUrl);
|
|
1376
|
-
const secretBase64Url = encodeBase64Url(secret);
|
|
1377
|
-
logger.info(`Or manually enter this code: ${secretBase64Url}`);
|
|
1378
|
-
logger.info("\n" + chalk.bold("Press Enter to continue..."));
|
|
1379
|
-
await new Promise((resolve) => {
|
|
1380
|
-
process.stdin.once("data", () => resolve());
|
|
1381
|
-
});
|
|
1382
|
-
await writeSettings({ onboardingCompleted: true });
|
|
1383
|
-
}
|
|
1670
|
+
logger.debug(`Session created: ${response.id}`);
|
|
1384
1671
|
const session = api.session(response);
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1672
|
+
const pushClient = api.push();
|
|
1673
|
+
const interruptController = new InterruptController();
|
|
1674
|
+
let requests = /* @__PURE__ */ new Map();
|
|
1675
|
+
const permissionServer = await startPermissionServerV2(async (request) => {
|
|
1676
|
+
const id = node_crypto.randomUUID();
|
|
1677
|
+
let promise = new Promise((resolve) => {
|
|
1678
|
+
requests.set(id, resolve);
|
|
1390
1679
|
});
|
|
1680
|
+
let timeout = setTimeout(async () => {
|
|
1681
|
+
logger.info("Permission timeout - attempting to interrupt Claude");
|
|
1682
|
+
const interrupted = await interruptController.interrupt();
|
|
1683
|
+
if (interrupted) {
|
|
1684
|
+
logger.info("Claude interrupted successfully");
|
|
1685
|
+
}
|
|
1686
|
+
requests.delete(id);
|
|
1687
|
+
session.updateAgentState((currentState) => {
|
|
1688
|
+
let r = { ...currentState.requests };
|
|
1689
|
+
delete r[id];
|
|
1690
|
+
return {
|
|
1691
|
+
...currentState,
|
|
1692
|
+
requests: r
|
|
1693
|
+
};
|
|
1694
|
+
});
|
|
1695
|
+
}, 1e3 * 60 * 4.5);
|
|
1696
|
+
logger.info("Permission request" + id + " " + JSON.stringify(request));
|
|
1697
|
+
try {
|
|
1698
|
+
await pushClient.sendToAllDevices(
|
|
1699
|
+
"Permission Request",
|
|
1700
|
+
`Claude wants to use ${request.name}`,
|
|
1701
|
+
{
|
|
1702
|
+
sessionId: response.id,
|
|
1703
|
+
requestId: id,
|
|
1704
|
+
tool: request.name,
|
|
1705
|
+
type: "permission_request"
|
|
1706
|
+
}
|
|
1707
|
+
);
|
|
1708
|
+
logger.info("Push notification sent for permission request");
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
logger.debug("Failed to send push notification:", error);
|
|
1711
|
+
}
|
|
1712
|
+
session.updateAgentState((currentState) => ({
|
|
1713
|
+
...currentState,
|
|
1714
|
+
requests: {
|
|
1715
|
+
...currentState.requests,
|
|
1716
|
+
[id]: {
|
|
1717
|
+
tool: request.name,
|
|
1718
|
+
arguments: request.arguments
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}));
|
|
1722
|
+
promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
|
|
1723
|
+
return promise;
|
|
1391
1724
|
});
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1725
|
+
session.setHandler("permission", (message) => {
|
|
1726
|
+
logger.info("Permission response" + JSON.stringify(message));
|
|
1727
|
+
const id = message.id;
|
|
1728
|
+
const resolve = requests.get(id);
|
|
1729
|
+
if (resolve) {
|
|
1730
|
+
resolve({ approved: message.approved, reason: message.reason });
|
|
1731
|
+
} else {
|
|
1732
|
+
logger.info("Permission request stale, likely timed out");
|
|
1733
|
+
return;
|
|
1397
1734
|
}
|
|
1735
|
+
session.updateAgentState((currentState) => {
|
|
1736
|
+
let r = { ...currentState.requests };
|
|
1737
|
+
delete r[id];
|
|
1738
|
+
return {
|
|
1739
|
+
...currentState,
|
|
1740
|
+
requests: r
|
|
1741
|
+
};
|
|
1742
|
+
});
|
|
1398
1743
|
});
|
|
1399
|
-
|
|
1400
|
-
"
|
|
1401
|
-
|
|
1402
|
-
|
|
1744
|
+
session.setHandler("abort", async () => {
|
|
1745
|
+
logger.info("Abort request - interrupting Claude");
|
|
1746
|
+
await interruptController.interrupt();
|
|
1747
|
+
});
|
|
1748
|
+
let thinking = false;
|
|
1749
|
+
const pingInterval = setInterval(() => {
|
|
1750
|
+
session.keepAlive(thinking);
|
|
1751
|
+
}, 15e3);
|
|
1752
|
+
const onAssistantResult = async (result) => {
|
|
1753
|
+
try {
|
|
1754
|
+
const summary = "result" in result && result.result ? result.result.substring(0, 100) + (result.result.length > 100 ? "..." : "") : "";
|
|
1755
|
+
await pushClient.sendToAllDevices(
|
|
1756
|
+
"Your move :D",
|
|
1757
|
+
summary,
|
|
1758
|
+
{
|
|
1759
|
+
sessionId: response.id,
|
|
1760
|
+
type: "assistant_result",
|
|
1761
|
+
turns: result.num_turns,
|
|
1762
|
+
duration_ms: result.duration_ms,
|
|
1763
|
+
cost_usd: result.total_cost_usd
|
|
1764
|
+
}
|
|
1765
|
+
);
|
|
1766
|
+
logger.debug("Push notification sent: Assistant result");
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
logger.debug("Failed to send assistant result push notification:", error);
|
|
1403
1769
|
}
|
|
1404
1770
|
};
|
|
1405
|
-
|
|
1406
|
-
const loopDestroy = startClaudeLoop({
|
|
1771
|
+
await loop({
|
|
1407
1772
|
path: workingDirectory,
|
|
1408
1773
|
model: options.model,
|
|
1409
1774
|
permissionMode: options.permissionMode,
|
|
1410
|
-
mcpServers
|
|
1411
|
-
|
|
1775
|
+
mcpServers: {
|
|
1776
|
+
"permission": {
|
|
1777
|
+
type: "http",
|
|
1778
|
+
url: permissionServer.url
|
|
1779
|
+
}
|
|
1780
|
+
},
|
|
1781
|
+
permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
|
|
1412
1782
|
onThinking: (t) => {
|
|
1413
1783
|
thinking = t;
|
|
1414
1784
|
session.keepAlive(t);
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
}
|
|
1420
|
-
}, session);
|
|
1421
|
-
const pingInterval = setInterval(() => {
|
|
1422
|
-
session.keepAlive(thinking);
|
|
1423
|
-
}, 15e3);
|
|
1424
|
-
const shutdown = async () => {
|
|
1425
|
-
logger.info("Shutting down...");
|
|
1426
|
-
clearInterval(pingInterval);
|
|
1427
|
-
await loopDestroy();
|
|
1428
|
-
await permissionServer.stop();
|
|
1429
|
-
session.sendSessionDeath();
|
|
1430
|
-
await session.flush();
|
|
1431
|
-
await session.close();
|
|
1432
|
-
process.exit(0);
|
|
1433
|
-
};
|
|
1434
|
-
process.on("SIGINT", shutdown);
|
|
1435
|
-
process.on("SIGTERM", shutdown);
|
|
1436
|
-
logger.info("Happy CLI is starting...");
|
|
1437
|
-
await new Promise(() => {
|
|
1785
|
+
},
|
|
1786
|
+
session,
|
|
1787
|
+
onAssistantResult,
|
|
1788
|
+
interruptController
|
|
1438
1789
|
});
|
|
1790
|
+
clearInterval(pingInterval);
|
|
1791
|
+
process.exit(0);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
const credentialsSchema = z__namespace.object({
|
|
1795
|
+
secret: z__namespace.string().base64(),
|
|
1796
|
+
token: z__namespace.string()
|
|
1797
|
+
});
|
|
1798
|
+
async function readCredentials() {
|
|
1799
|
+
if (!node_fs.existsSync(configuration.privateKeyFile)) {
|
|
1800
|
+
return null;
|
|
1801
|
+
}
|
|
1802
|
+
try {
|
|
1803
|
+
const keyBase64 = await promises.readFile(configuration.privateKeyFile, "utf8");
|
|
1804
|
+
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
1805
|
+
return {
|
|
1806
|
+
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
1807
|
+
token: credentials.token
|
|
1808
|
+
};
|
|
1809
|
+
} catch {
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
async function writeCredentials(credentials) {
|
|
1814
|
+
if (!node_fs.existsSync(configuration.happyDir)) {
|
|
1815
|
+
await promises.mkdir(configuration.happyDir, { recursive: true });
|
|
1816
|
+
}
|
|
1817
|
+
await promises.writeFile(configuration.privateKeyFile, JSON.stringify({
|
|
1818
|
+
secret: encodeBase64(credentials.secret),
|
|
1819
|
+
token: credentials.token
|
|
1820
|
+
}, null, 2));
|
|
1439
1821
|
}
|
|
1440
1822
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
console.
|
|
1823
|
+
function displayQRCode(url) {
|
|
1824
|
+
console.log("=".repeat(80));
|
|
1825
|
+
console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
|
|
1826
|
+
console.log("=".repeat(80));
|
|
1827
|
+
qrcode.generate(url, { small: true }, (qr) => {
|
|
1828
|
+
for (let l of qr.split("\n")) {
|
|
1829
|
+
console.log(" ".repeat(10) + l);
|
|
1448
1830
|
}
|
|
1449
|
-
process.exit(1);
|
|
1450
1831
|
});
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1832
|
+
console.log("=".repeat(80));
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
async function doAuth() {
|
|
1836
|
+
console.log("Starting authentication...");
|
|
1837
|
+
const secret = new Uint8Array(node_crypto.randomBytes(32));
|
|
1838
|
+
const keypair = tweetnacl.box.keyPair.fromSecretKey(secret);
|
|
1839
|
+
try {
|
|
1840
|
+
await axios.post(`${configuration.serverUrl}/v1/auth/request`, {
|
|
1841
|
+
publicKey: encodeBase64(keypair.publicKey)
|
|
1842
|
+
});
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
console.log("Failed to create authentication request, please try again later.");
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
console.log("Please, authenticate using mobile app");
|
|
1848
|
+
displayQRCode("happy://terminal?" + encodeBase64Url(keypair.publicKey));
|
|
1849
|
+
let credentials = null;
|
|
1850
|
+
while (true) {
|
|
1851
|
+
try {
|
|
1852
|
+
const response = await axios.post(`${configuration.serverUrl}/v1/auth/request`, {
|
|
1853
|
+
publicKey: encodeBase64(keypair.publicKey)
|
|
1854
|
+
});
|
|
1855
|
+
if (response.data.state === "authorized") {
|
|
1856
|
+
let token = response.data.token;
|
|
1857
|
+
let r = decodeBase64(response.data.response);
|
|
1858
|
+
let decrypted = decryptWithEphemeralKey(r, keypair.secretKey);
|
|
1859
|
+
if (decrypted) {
|
|
1860
|
+
credentials = {
|
|
1861
|
+
secret: decrypted,
|
|
1862
|
+
token
|
|
1863
|
+
};
|
|
1864
|
+
await writeCredentials(credentials);
|
|
1865
|
+
return credentials;
|
|
1866
|
+
} else {
|
|
1867
|
+
console.log("Failed to decrypt response, please try again later.");
|
|
1868
|
+
return null;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
} catch (error) {
|
|
1872
|
+
console.log("Failed to create authentication request, please try again later.");
|
|
1873
|
+
return null;
|
|
1468
1874
|
}
|
|
1875
|
+
await delay(1e3);
|
|
1876
|
+
}
|
|
1877
|
+
return null;
|
|
1878
|
+
}
|
|
1879
|
+
function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
1880
|
+
const ephemeralPublicKey = encryptedBundle.slice(0, 32);
|
|
1881
|
+
const nonce = encryptedBundle.slice(32, 32 + tweetnacl.box.nonceLength);
|
|
1882
|
+
const encrypted = encryptedBundle.slice(32 + tweetnacl.box.nonceLength);
|
|
1883
|
+
const decrypted = tweetnacl.box.open(encrypted, nonce, ephemeralPublicKey, recipientSecretKey);
|
|
1884
|
+
if (!decrypted) {
|
|
1885
|
+
return null;
|
|
1469
1886
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1887
|
+
return decrypted;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
(async () => {
|
|
1891
|
+
const args = process.argv.slice(2);
|
|
1892
|
+
let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
|
|
1893
|
+
initializeConfiguration(installationLocation);
|
|
1894
|
+
initLoggerWithGlobalConfiguration();
|
|
1895
|
+
logger.debug("Starting happy CLI with args: ", process.argv);
|
|
1896
|
+
const subcommand = args[0];
|
|
1897
|
+
if (subcommand === "logout") {
|
|
1898
|
+
try {
|
|
1899
|
+
await cleanKey();
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1902
|
+
if (process.env.DEBUG) {
|
|
1903
|
+
console.error(error);
|
|
1904
|
+
}
|
|
1905
|
+
process.exit(1);
|
|
1906
|
+
}
|
|
1907
|
+
return;
|
|
1908
|
+
} else if (subcommand === "login" || subcommand === "auth") {
|
|
1909
|
+
await doAuth();
|
|
1910
|
+
return;
|
|
1911
|
+
} else {
|
|
1912
|
+
const options = {};
|
|
1913
|
+
let showHelp = false;
|
|
1914
|
+
let showVersion = false;
|
|
1915
|
+
for (let i = 0; i < args.length; i++) {
|
|
1916
|
+
const arg = args[i];
|
|
1917
|
+
if (arg === "-h" || arg === "--help") {
|
|
1918
|
+
showHelp = true;
|
|
1919
|
+
} else if (arg === "-v" || arg === "--version") {
|
|
1920
|
+
showVersion = true;
|
|
1921
|
+
} else if (arg === "-m" || arg === "--model") {
|
|
1922
|
+
options.model = args[++i];
|
|
1923
|
+
} else if (arg === "-p" || arg === "--permission-mode") {
|
|
1924
|
+
options.permissionMode = args[++i];
|
|
1925
|
+
} else if (arg === "--local") {
|
|
1926
|
+
i++;
|
|
1927
|
+
} else {
|
|
1928
|
+
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
1929
|
+
process.exit(1);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
if (showHelp) {
|
|
1933
|
+
console.log(`
|
|
1472
1934
|
${chalk.bold("happy")} - Claude Code session sharing
|
|
1473
1935
|
|
|
1474
1936
|
${chalk.bold("Usage:")}
|
|
1475
1937
|
happy [options]
|
|
1476
|
-
happy
|
|
1938
|
+
happy logout Logs out of your account and removes data directory
|
|
1939
|
+
happy login Show your secret QR code
|
|
1940
|
+
happy auth Same as login
|
|
1477
1941
|
|
|
1478
1942
|
${chalk.bold("Options:")}
|
|
1479
1943
|
-h, --help Show this help message
|
|
@@ -1481,33 +1945,50 @@ ${chalk.bold("Options:")}
|
|
|
1481
1945
|
-m, --model <model> Claude model to use (default: sonnet)
|
|
1482
1946
|
-p, --permission-mode Permission mode: auto, default, or plan
|
|
1483
1947
|
|
|
1948
|
+
[Advanced]
|
|
1949
|
+
--local < global | local >
|
|
1950
|
+
Will use .happy folder in the current directory for storing your private key and debug logs.
|
|
1951
|
+
You will require re-login each time you run this in a new directory.
|
|
1952
|
+
Use with login to show either global or local QR code.
|
|
1953
|
+
|
|
1484
1954
|
${chalk.bold("Examples:")}
|
|
1485
1955
|
happy Start a session with default settings
|
|
1486
1956
|
happy -m opus Use Claude Opus model
|
|
1487
1957
|
happy -p plan Use plan permission mode
|
|
1488
|
-
happy
|
|
1958
|
+
happy logout Logs out of your account and removes data directory
|
|
1489
1959
|
`);
|
|
1490
|
-
|
|
1491
|
-
}
|
|
1492
|
-
if (showVersion) {
|
|
1493
|
-
console.log("0.1.0");
|
|
1494
|
-
process.exit(0);
|
|
1495
|
-
}
|
|
1496
|
-
start(options).catch((error) => {
|
|
1497
|
-
console.error(chalk.red("Error:"), error.message);
|
|
1498
|
-
if (process.env.DEBUG) {
|
|
1499
|
-
console.error(error);
|
|
1960
|
+
process.exit(0);
|
|
1500
1961
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1962
|
+
if (showVersion) {
|
|
1963
|
+
console.log("0.1.3");
|
|
1964
|
+
process.exit(0);
|
|
1965
|
+
}
|
|
1966
|
+
let credentials = await readCredentials();
|
|
1967
|
+
if (!credentials) {
|
|
1968
|
+
let res = await doAuth();
|
|
1969
|
+
if (!res) {
|
|
1970
|
+
process.exit(1);
|
|
1971
|
+
}
|
|
1972
|
+
credentials = res;
|
|
1973
|
+
}
|
|
1974
|
+
try {
|
|
1975
|
+
await start(credentials, options);
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1978
|
+
if (process.env.DEBUG) {
|
|
1979
|
+
console.error(error);
|
|
1980
|
+
}
|
|
1981
|
+
process.exit(1);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
})();
|
|
1504
1985
|
async function cleanKey() {
|
|
1505
|
-
const
|
|
1506
|
-
if (!
|
|
1507
|
-
console.log(chalk.yellow("No happy data directory found at:"),
|
|
1986
|
+
const happyDir = configuration.happyDir;
|
|
1987
|
+
if (!node_fs.existsSync(happyDir)) {
|
|
1988
|
+
console.log(chalk.yellow("No happy data directory found at:"), happyDir);
|
|
1508
1989
|
return;
|
|
1509
1990
|
}
|
|
1510
|
-
console.log(chalk.blue("Found happy data directory at:"),
|
|
1991
|
+
console.log(chalk.blue("Found happy data directory at:"), happyDir);
|
|
1511
1992
|
console.log(chalk.yellow("\u26A0\uFE0F This will remove all authentication data and require reconnecting your phone."));
|
|
1512
1993
|
const rl = node_readline.createInterface({
|
|
1513
1994
|
input: process.stdin,
|
|
@@ -1519,7 +2000,7 @@ async function cleanKey() {
|
|
|
1519
2000
|
rl.close();
|
|
1520
2001
|
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
1521
2002
|
try {
|
|
1522
|
-
|
|
2003
|
+
node_fs.rmSync(happyDir, { recursive: true, force: true });
|
|
1523
2004
|
console.log(chalk.green("\u2713 Happy data directory removed successfully"));
|
|
1524
2005
|
console.log(chalk.blue("\u2139\uFE0F You will need to reconnect your phone on the next session"));
|
|
1525
2006
|
} catch (error) {
|