happy-coder 0.1.2 → 0.1.5
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 +1792 -388
- package/dist/index.mjs +1792 -389
- package/package.json +9 -5
package/dist/index.cjs
CHANGED
|
@@ -1,19 +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
|
|
8
|
+
var promises = require('node:fs/promises');
|
|
9
|
+
var node_fs = require('node:fs');
|
|
11
10
|
var node_events = require('node:events');
|
|
12
11
|
var socket_ioClient = require('socket.io-client');
|
|
13
|
-
var
|
|
14
|
-
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
|
+
var claudeCode = require('@anthropic-ai/claude-code');
|
|
15
17
|
var node_child_process = require('node:child_process');
|
|
18
|
+
var node_readline = require('node:readline');
|
|
19
|
+
var node_url = require('node:url');
|
|
20
|
+
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
21
|
+
var node_http = require('node:http');
|
|
22
|
+
var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
23
|
+
var qrcode = require('qrcode-terminal');
|
|
16
24
|
|
|
25
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
17
26
|
function _interopNamespaceDefault(e) {
|
|
18
27
|
var n = Object.create(null);
|
|
19
28
|
if (e) {
|
|
@@ -31,193 +40,322 @@ function _interopNamespaceDefault(e) {
|
|
|
31
40
|
return Object.freeze(n);
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
var
|
|
43
|
+
var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
result.set(encrypted, nonce.length);
|
|
54
|
-
return result;
|
|
55
|
-
}
|
|
56
|
-
function decrypt(data, secret) {
|
|
57
|
-
const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
|
|
58
|
-
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
|
|
59
|
-
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
|
|
60
|
-
if (!decrypted) {
|
|
61
|
-
return null;
|
|
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");
|
|
62
62
|
}
|
|
63
|
-
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
64
63
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const signature = tweetnacl.sign.detached(challenge, keypair.secretKey);
|
|
69
|
-
return {
|
|
70
|
-
challenge,
|
|
71
|
-
publicKey: keypair.publicKey,
|
|
72
|
-
signature
|
|
73
|
-
};
|
|
64
|
+
let configuration = void 0;
|
|
65
|
+
function initializeConfiguration(location) {
|
|
66
|
+
configuration = new Configuration(location);
|
|
74
67
|
}
|
|
75
68
|
|
|
76
|
-
async function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const keyBase642 = fs.readFileSync(keyPath, "utf8").trim();
|
|
80
|
-
return new Uint8Array(Buffer.from(keyBase642, "base64"));
|
|
81
|
-
}
|
|
82
|
-
const secret = getRandomBytes(32);
|
|
83
|
-
const keyBase64 = encodeBase64(secret);
|
|
84
|
-
fs.mkdirSync(node_path.join(os.homedir(), ".handy"), { recursive: true });
|
|
85
|
-
fs.writeFileSync(keyPath, keyBase64);
|
|
86
|
-
await promises.chmod(keyPath, 384);
|
|
87
|
-
return secret;
|
|
88
|
-
}
|
|
89
|
-
async function authGetToken(secret) {
|
|
90
|
-
const { challenge, publicKey, signature } = authChallenge(secret);
|
|
91
|
-
const response = await axios.post(`https://handy-api.korshakov.org/v1/auth`, {
|
|
92
|
-
challenge: encodeBase64(challenge),
|
|
93
|
-
publicKey: encodeBase64(publicKey),
|
|
94
|
-
signature: encodeBase64(signature)
|
|
95
|
-
});
|
|
96
|
-
if (!response.data.success || !response.data.token) {
|
|
97
|
-
throw new Error("Authentication failed");
|
|
69
|
+
async function getSessionLogPath() {
|
|
70
|
+
if (!node_fs.existsSync(configuration.logsDir)) {
|
|
71
|
+
await promises.mkdir(configuration.logsDir, { recursive: true });
|
|
98
72
|
}
|
|
99
|
-
|
|
73
|
+
const now = /* @__PURE__ */ new Date();
|
|
74
|
+
const timestamp = now.toLocaleString("sv-SE", {
|
|
75
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
76
|
+
year: "numeric",
|
|
77
|
+
month: "2-digit",
|
|
78
|
+
day: "2-digit",
|
|
79
|
+
hour: "2-digit",
|
|
80
|
+
minute: "2-digit",
|
|
81
|
+
second: "2-digit"
|
|
82
|
+
}).replace(/[: ]/g, "-").replace(/,/g, "");
|
|
83
|
+
return node_path.join(configuration.logsDir, `${timestamp}.log`);
|
|
100
84
|
}
|
|
101
|
-
function generateAppUrl(secret) {
|
|
102
|
-
const secretBase64Url = encodeBase64Url(secret);
|
|
103
|
-
return `handy://${secretBase64Url}`;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
85
|
class Logger {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
this.log("DEBUG" /* DEBUG */, message, ...args);
|
|
110
|
-
}
|
|
86
|
+
constructor(logFilePathPromise = getSessionLogPath()) {
|
|
87
|
+
this.logFilePathPromise = logFilePathPromise;
|
|
111
88
|
}
|
|
112
|
-
|
|
113
|
-
|
|
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
|
+
});
|
|
114
100
|
}
|
|
115
|
-
|
|
116
|
-
this.
|
|
101
|
+
debug(message, ...args) {
|
|
102
|
+
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, ...args);
|
|
117
103
|
}
|
|
118
|
-
|
|
119
|
-
|
|
104
|
+
debugLargeJson(message, object, maxStringLength = 100, maxArrayLength = 10) {
|
|
105
|
+
if (!process.env.DEBUG) {
|
|
106
|
+
this.debug(`In production, skipping message inspection`);
|
|
107
|
+
}
|
|
108
|
+
const truncateStrings = (obj) => {
|
|
109
|
+
if (typeof obj === "string") {
|
|
110
|
+
return obj.length > maxStringLength ? obj.substring(0, maxStringLength) + "... [truncated for logs]" : obj;
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(obj)) {
|
|
113
|
+
const truncatedArray = obj.map((item) => truncateStrings(item)).slice(0, maxArrayLength);
|
|
114
|
+
if (obj.length > maxArrayLength) {
|
|
115
|
+
truncatedArray.push(`... [truncated array for logs up to ${maxArrayLength} items]`);
|
|
116
|
+
}
|
|
117
|
+
return truncatedArray;
|
|
118
|
+
}
|
|
119
|
+
if (obj && typeof obj === "object") {
|
|
120
|
+
const result = {};
|
|
121
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
122
|
+
if (key === "usage") {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
result[key] = truncateStrings(value);
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
return obj;
|
|
130
|
+
};
|
|
131
|
+
const truncatedObject = truncateStrings(object);
|
|
132
|
+
const json = JSON.stringify(truncatedObject, null, 2);
|
|
133
|
+
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, "\n", json);
|
|
120
134
|
}
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
info(message, ...args) {
|
|
136
|
+
this.logToConsole("info", "", message, ...args);
|
|
137
|
+
this.debug(message, args);
|
|
123
138
|
}
|
|
124
|
-
|
|
125
|
-
const timestamp = this.getTimestamp();
|
|
126
|
-
const prefix = `[${timestamp}] [${level}]`;
|
|
139
|
+
logToConsole(level, prefix, message, ...args) {
|
|
127
140
|
switch (level) {
|
|
128
|
-
case "
|
|
141
|
+
case "debug": {
|
|
129
142
|
console.log(chalk.gray(prefix), message, ...args);
|
|
130
143
|
break;
|
|
131
144
|
}
|
|
132
|
-
case "
|
|
145
|
+
case "error": {
|
|
133
146
|
console.error(chalk.red(prefix), message, ...args);
|
|
134
147
|
break;
|
|
135
148
|
}
|
|
136
|
-
case "
|
|
149
|
+
case "info": {
|
|
137
150
|
console.log(chalk.blue(prefix), message, ...args);
|
|
138
151
|
break;
|
|
139
152
|
}
|
|
140
|
-
case "
|
|
153
|
+
case "warn": {
|
|
141
154
|
console.log(chalk.yellow(prefix), message, ...args);
|
|
142
155
|
break;
|
|
143
156
|
}
|
|
157
|
+
default: {
|
|
158
|
+
this.debug("Unknown log level:", level);
|
|
159
|
+
console.log(chalk.blue(prefix), message, ...args);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
144
162
|
}
|
|
145
163
|
}
|
|
164
|
+
logToFile(prefix, message, ...args) {
|
|
165
|
+
const logLine = `${prefix} ${message} ${args.map(
|
|
166
|
+
(arg) => typeof arg === "string" ? arg : JSON.stringify(arg)
|
|
167
|
+
).join(" ")}
|
|
168
|
+
`;
|
|
169
|
+
this.logFilePathPromise.then((logFilePath) => {
|
|
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
|
+
}
|
|
178
|
+
}).catch((error) => {
|
|
179
|
+
if (process.env.DEBUG) {
|
|
180
|
+
console.log("This message only visible in DEBUG mode, not in production");
|
|
181
|
+
console.error("Failed to resolve log file path:", error);
|
|
182
|
+
console.log(prefix, message, ...args);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
let logger;
|
|
188
|
+
function initLoggerWithGlobalConfiguration() {
|
|
189
|
+
logger = new Logger();
|
|
146
190
|
}
|
|
147
|
-
const logger = new Logger();
|
|
148
191
|
|
|
149
|
-
const SessionMessageContentSchema =
|
|
150
|
-
c:
|
|
192
|
+
const SessionMessageContentSchema = z.z.object({
|
|
193
|
+
c: z.z.string(),
|
|
151
194
|
// Base64 encoded encrypted content
|
|
152
|
-
t:
|
|
195
|
+
t: z.z.literal("encrypted")
|
|
153
196
|
});
|
|
154
|
-
const UpdateBodySchema =
|
|
155
|
-
message:
|
|
156
|
-
id:
|
|
157
|
-
seq:
|
|
197
|
+
const UpdateBodySchema = z.z.object({
|
|
198
|
+
message: z.z.object({
|
|
199
|
+
id: z.z.string(),
|
|
200
|
+
seq: z.z.number(),
|
|
158
201
|
content: SessionMessageContentSchema
|
|
159
202
|
}),
|
|
160
|
-
sid:
|
|
203
|
+
sid: z.z.string(),
|
|
161
204
|
// Session ID
|
|
162
|
-
t:
|
|
205
|
+
t: z.z.literal("new-message")
|
|
163
206
|
});
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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()
|
|
213
|
+
}).nullish(),
|
|
214
|
+
agentState: z.z.object({
|
|
215
|
+
version: z.z.number(),
|
|
216
|
+
agentState: z.z.string()
|
|
217
|
+
}).nullish()
|
|
169
218
|
});
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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()
|
|
175
224
|
});
|
|
176
|
-
|
|
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()
|
|
234
|
+
});
|
|
235
|
+
z.z.object({
|
|
177
236
|
content: SessionMessageContentSchema,
|
|
178
|
-
createdAt:
|
|
179
|
-
id:
|
|
180
|
-
seq:
|
|
181
|
-
updatedAt:
|
|
237
|
+
createdAt: z.z.number(),
|
|
238
|
+
id: z.z.string(),
|
|
239
|
+
seq: z.z.number(),
|
|
240
|
+
updatedAt: z.z.number()
|
|
182
241
|
});
|
|
183
|
-
|
|
184
|
-
session:
|
|
185
|
-
id:
|
|
186
|
-
tag:
|
|
187
|
-
seq:
|
|
188
|
-
createdAt:
|
|
189
|
-
updatedAt:
|
|
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()
|
|
190
253
|
})
|
|
191
254
|
});
|
|
192
|
-
const UserMessageSchema =
|
|
193
|
-
role:
|
|
194
|
-
content:
|
|
195
|
-
type:
|
|
196
|
-
text:
|
|
197
|
-
})
|
|
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()
|
|
260
|
+
}),
|
|
261
|
+
localKey: z.z.string().optional(),
|
|
262
|
+
// Mobile messages include this
|
|
263
|
+
sentFrom: z.z.enum(["mobile", "cli"]).optional()
|
|
264
|
+
// Source identifier
|
|
198
265
|
});
|
|
199
|
-
const AgentMessageSchema =
|
|
200
|
-
role:
|
|
201
|
-
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
|
+
})
|
|
202
272
|
});
|
|
203
|
-
|
|
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
|
+
}
|
|
304
|
+
|
|
305
|
+
async function delay(ms) {
|
|
306
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
307
|
+
}
|
|
308
|
+
function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
|
|
309
|
+
let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.max(currentFailureCount, maxFailureCount);
|
|
310
|
+
return Math.round(Math.random() * maxDelayRet);
|
|
311
|
+
}
|
|
312
|
+
function createBackoff(opts) {
|
|
313
|
+
return async (callback) => {
|
|
314
|
+
let currentFailureCount = 0;
|
|
315
|
+
const minDelay = 250;
|
|
316
|
+
const maxDelay = 1e3;
|
|
317
|
+
const maxFailureCount = 50;
|
|
318
|
+
while (true) {
|
|
319
|
+
try {
|
|
320
|
+
return await callback();
|
|
321
|
+
} catch (e) {
|
|
322
|
+
if (currentFailureCount < maxFailureCount) {
|
|
323
|
+
currentFailureCount++;
|
|
324
|
+
}
|
|
325
|
+
let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
|
|
326
|
+
await delay(waitForRequest);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
let backoff = createBackoff();
|
|
204
332
|
|
|
205
333
|
class ApiSessionClient extends node_events.EventEmitter {
|
|
206
334
|
token;
|
|
207
335
|
secret;
|
|
208
336
|
sessionId;
|
|
337
|
+
metadata;
|
|
338
|
+
metadataVersion;
|
|
339
|
+
agentState;
|
|
340
|
+
agentStateVersion;
|
|
209
341
|
socket;
|
|
210
|
-
receivedMessages = /* @__PURE__ */ new Set();
|
|
211
342
|
pendingMessages = [];
|
|
212
343
|
pendingMessageCallback = null;
|
|
213
|
-
|
|
344
|
+
rpcHandlers = /* @__PURE__ */ new Map();
|
|
345
|
+
constructor(token, secret, session) {
|
|
214
346
|
super();
|
|
215
347
|
this.token = token;
|
|
216
348
|
this.secret = secret;
|
|
217
|
-
this.sessionId =
|
|
218
|
-
this.
|
|
349
|
+
this.sessionId = session.id;
|
|
350
|
+
this.metadata = session.metadata;
|
|
351
|
+
this.metadataVersion = session.metadataVersion;
|
|
352
|
+
this.agentState = session.agentState;
|
|
353
|
+
this.agentStateVersion = session.agentStateVersion;
|
|
354
|
+
this.socket = socket_ioClient.io(configuration.serverUrl, {
|
|
219
355
|
auth: {
|
|
220
|
-
token: this.token
|
|
356
|
+
token: this.token,
|
|
357
|
+
clientType: "session-scoped",
|
|
358
|
+
sessionId: this.sessionId
|
|
221
359
|
},
|
|
222
360
|
path: "/v1/updates",
|
|
223
361
|
reconnection: true,
|
|
@@ -229,30 +367,65 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
229
367
|
autoConnect: false
|
|
230
368
|
});
|
|
231
369
|
this.socket.on("connect", () => {
|
|
232
|
-
logger.
|
|
370
|
+
logger.debug("Socket connected successfully");
|
|
371
|
+
this.reregisterHandlers();
|
|
372
|
+
});
|
|
373
|
+
this.socket.on("rpc-request", async (data, callback) => {
|
|
374
|
+
try {
|
|
375
|
+
const method = data.method;
|
|
376
|
+
const handler = this.rpcHandlers.get(method);
|
|
377
|
+
if (!handler) {
|
|
378
|
+
logger.debug("[SOCKET] [RPC] [ERROR] method not found", { method });
|
|
379
|
+
const errorResponse = { error: "Method not found" };
|
|
380
|
+
const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
|
|
381
|
+
callback(encryptedError);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const decryptedParams = decrypt(decodeBase64(data.params), this.secret);
|
|
385
|
+
const result = await handler(decryptedParams);
|
|
386
|
+
const encryptedResponse = encodeBase64(encrypt(result, this.secret));
|
|
387
|
+
callback(encryptedResponse);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
logger.debug("[SOCKET] [RPC] [ERROR] Error handling RPC request", { error });
|
|
390
|
+
const errorResponse = { error: error instanceof Error ? error.message : "Unknown error" };
|
|
391
|
+
const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
|
|
392
|
+
callback(encryptedError);
|
|
393
|
+
}
|
|
233
394
|
});
|
|
234
395
|
this.socket.on("disconnect", (reason) => {
|
|
235
|
-
logger.
|
|
396
|
+
logger.debug("[API] Socket disconnected:", reason);
|
|
236
397
|
});
|
|
237
398
|
this.socket.on("connect_error", (error) => {
|
|
238
|
-
logger.
|
|
399
|
+
logger.debug("[API] Socket connection error:", error);
|
|
239
400
|
});
|
|
240
401
|
this.socket.on("update", (data) => {
|
|
241
402
|
if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
|
|
242
403
|
const body = decrypt(decodeBase64(data.body.message.content.c), this.secret);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
this.pendingMessages.push(result.data);
|
|
251
|
-
}
|
|
404
|
+
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
|
|
405
|
+
const userResult = UserMessageSchema$1.safeParse(body);
|
|
406
|
+
if (userResult.success) {
|
|
407
|
+
if (this.pendingMessageCallback) {
|
|
408
|
+
this.pendingMessageCallback(userResult.data);
|
|
409
|
+
} else {
|
|
410
|
+
this.pendingMessages.push(userResult.data);
|
|
252
411
|
}
|
|
412
|
+
} else {
|
|
413
|
+
this.emit("message", body);
|
|
414
|
+
}
|
|
415
|
+
} else if (data.body.t === "update-session") {
|
|
416
|
+
if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
|
|
417
|
+
this.metadata = decrypt(decodeBase64(data.body.metadata.metadata), this.secret);
|
|
418
|
+
this.metadataVersion = data.body.metadata.version;
|
|
419
|
+
}
|
|
420
|
+
if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
|
|
421
|
+
this.agentState = data.body.agentState.agentState ? decrypt(decodeBase64(data.body.agentState.agentState), this.secret) : null;
|
|
422
|
+
this.agentStateVersion = data.body.agentState.version;
|
|
253
423
|
}
|
|
254
424
|
}
|
|
255
425
|
});
|
|
426
|
+
this.socket.on("error", (error) => {
|
|
427
|
+
logger.debug("[API] Socket error:", error);
|
|
428
|
+
});
|
|
256
429
|
this.socket.connect();
|
|
257
430
|
}
|
|
258
431
|
onUserMessage(callback) {
|
|
@@ -263,30 +436,248 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
263
436
|
}
|
|
264
437
|
/**
|
|
265
438
|
* Send message to session
|
|
266
|
-
* @param body - Message body
|
|
439
|
+
* @param body - Message body (can be MessageContent or raw content for agent messages)
|
|
267
440
|
*/
|
|
268
|
-
|
|
269
|
-
let content
|
|
270
|
-
|
|
271
|
-
content
|
|
272
|
-
|
|
441
|
+
sendClaudeSessionMessage(body) {
|
|
442
|
+
let content;
|
|
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
|
+
};
|
|
451
|
+
} else {
|
|
452
|
+
content = {
|
|
453
|
+
role: "agent",
|
|
454
|
+
content: {
|
|
455
|
+
type: "output",
|
|
456
|
+
data: body
|
|
457
|
+
// This wraps the entire Claude message
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
|
|
273
462
|
const encrypted = encodeBase64(encrypt(content, this.secret));
|
|
274
463
|
this.socket.emit("message", {
|
|
275
464
|
sid: this.sessionId,
|
|
276
465
|
message: encrypted
|
|
277
466
|
});
|
|
278
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* Send a ping message to keep the connection alive
|
|
470
|
+
*/
|
|
471
|
+
keepAlive(thinking) {
|
|
472
|
+
this.socket.volatile.emit("session-alive", { sid: this.sessionId, time: Date.now(), thinking });
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Send session death message
|
|
476
|
+
*/
|
|
477
|
+
sendSessionDeath() {
|
|
478
|
+
this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Update session metadata
|
|
482
|
+
* @param handler - Handler function that returns the updated metadata
|
|
483
|
+
*/
|
|
484
|
+
updateMetadata(handler) {
|
|
485
|
+
backoff(async () => {
|
|
486
|
+
let updated = handler(this.metadata);
|
|
487
|
+
const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(updated, this.secret)) });
|
|
488
|
+
if (answer.result === "success") {
|
|
489
|
+
this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
|
|
490
|
+
this.metadataVersion = answer.version;
|
|
491
|
+
} else if (answer.result === "version-mismatch") {
|
|
492
|
+
if (answer.version > this.metadataVersion) {
|
|
493
|
+
this.metadataVersion = answer.version;
|
|
494
|
+
this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
|
|
495
|
+
}
|
|
496
|
+
throw new Error("Metadata version mismatch");
|
|
497
|
+
} else if (answer.result === "error") ;
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Update session agent state
|
|
502
|
+
* @param handler - Handler function that returns the updated agent state
|
|
503
|
+
*/
|
|
504
|
+
updateAgentState(handler) {
|
|
505
|
+
console.log("Updating agent state", this.agentState);
|
|
506
|
+
backoff(async () => {
|
|
507
|
+
let updated = handler(this.agentState || {});
|
|
508
|
+
const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(updated, this.secret)) : null });
|
|
509
|
+
if (answer.result === "success") {
|
|
510
|
+
this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
|
|
511
|
+
this.agentStateVersion = answer.version;
|
|
512
|
+
console.log("Agent state updated", this.agentState);
|
|
513
|
+
} else if (answer.result === "version-mismatch") {
|
|
514
|
+
if (answer.version > this.agentStateVersion) {
|
|
515
|
+
this.agentStateVersion = answer.version;
|
|
516
|
+
this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
|
|
517
|
+
}
|
|
518
|
+
throw new Error("Agent state version mismatch");
|
|
519
|
+
} else if (answer.result === "error") {
|
|
520
|
+
console.error("Agent state update error", answer);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Set a custom RPC handler for a specific method with encrypted arguments and responses
|
|
526
|
+
* @param method - The method name to handle
|
|
527
|
+
* @param handler - The handler function to call when the method is invoked
|
|
528
|
+
*/
|
|
529
|
+
setHandler(method, handler) {
|
|
530
|
+
const prefixedMethod = `${this.sessionId}:${method}`;
|
|
531
|
+
this.rpcHandlers.set(prefixedMethod, handler);
|
|
532
|
+
this.socket.emit("rpc-register", { method: prefixedMethod });
|
|
533
|
+
logger.debug("Registered RPC handler", { method, prefixedMethod });
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Re-register all RPC handlers after reconnection
|
|
537
|
+
*/
|
|
538
|
+
reregisterHandlers() {
|
|
539
|
+
logger.debug("Re-registering RPC handlers after reconnection", {
|
|
540
|
+
totalMethods: this.rpcHandlers.size
|
|
541
|
+
});
|
|
542
|
+
for (const [prefixedMethod] of this.rpcHandlers) {
|
|
543
|
+
this.socket.emit("rpc-register", { method: prefixedMethod });
|
|
544
|
+
logger.debug("Re-registered method", { prefixedMethod });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Wait for socket buffer to flush
|
|
549
|
+
*/
|
|
550
|
+
async flush() {
|
|
551
|
+
if (!this.socket.connected) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
return new Promise((resolve) => {
|
|
555
|
+
this.socket.emit("ping", () => {
|
|
556
|
+
resolve();
|
|
557
|
+
});
|
|
558
|
+
setTimeout(() => {
|
|
559
|
+
resolve();
|
|
560
|
+
}, 1e4);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
279
563
|
async close() {
|
|
280
564
|
this.socket.close();
|
|
281
565
|
}
|
|
282
566
|
}
|
|
283
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
|
+
|
|
284
673
|
class ApiClient {
|
|
285
674
|
token;
|
|
286
675
|
secret;
|
|
676
|
+
pushClient;
|
|
287
677
|
constructor(token, secret) {
|
|
288
678
|
this.token = token;
|
|
289
679
|
this.secret = secret;
|
|
680
|
+
this.pushClient = new PushNotificationClient(token);
|
|
290
681
|
}
|
|
291
682
|
/**
|
|
292
683
|
* Create a new session or load existing one with the given tag
|
|
@@ -294,8 +685,12 @@ class ApiClient {
|
|
|
294
685
|
async getOrCreateSession(opts) {
|
|
295
686
|
try {
|
|
296
687
|
const response = await axios.post(
|
|
297
|
-
|
|
298
|
-
{
|
|
688
|
+
`${configuration.serverUrl}/v1/sessions`,
|
|
689
|
+
{
|
|
690
|
+
tag: opts.tag,
|
|
691
|
+
metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
|
|
692
|
+
agentState: opts.state ? encodeBase64(encrypt(opts.state, this.secret)) : null
|
|
693
|
+
},
|
|
299
694
|
{
|
|
300
695
|
headers: {
|
|
301
696
|
"Authorization": `Bearer ${this.token}`,
|
|
@@ -303,10 +698,21 @@ class ApiClient {
|
|
|
303
698
|
}
|
|
304
699
|
}
|
|
305
700
|
);
|
|
306
|
-
logger.
|
|
307
|
-
|
|
701
|
+
logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
|
|
702
|
+
let raw = response.data.session;
|
|
703
|
+
let session = {
|
|
704
|
+
id: raw.id,
|
|
705
|
+
createdAt: raw.createdAt,
|
|
706
|
+
updatedAt: raw.updatedAt,
|
|
707
|
+
seq: raw.seq,
|
|
708
|
+
metadata: decrypt(decodeBase64(raw.metadata), this.secret),
|
|
709
|
+
metadataVersion: raw.metadataVersion,
|
|
710
|
+
agentState: raw.agentState ? decrypt(decodeBase64(raw.agentState), this.secret) : null,
|
|
711
|
+
agentStateVersion: raw.agentStateVersion
|
|
712
|
+
};
|
|
713
|
+
return session;
|
|
308
714
|
} catch (error) {
|
|
309
|
-
logger.
|
|
715
|
+
logger.debug("[API] [ERROR] Failed to get or create session:", error);
|
|
310
716
|
throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
311
717
|
}
|
|
312
718
|
}
|
|
@@ -315,272 +721,1223 @@ class ApiClient {
|
|
|
315
721
|
* @param id - Session ID
|
|
316
722
|
* @returns Session client
|
|
317
723
|
*/
|
|
318
|
-
session(
|
|
319
|
-
return new ApiSessionClient(this.token, this.secret,
|
|
724
|
+
session(session) {
|
|
725
|
+
return new ApiSessionClient(this.token, this.secret, session);
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Get push notification client
|
|
729
|
+
* @returns Push notification client
|
|
730
|
+
*/
|
|
731
|
+
push() {
|
|
732
|
+
return this.pushClient;
|
|
320
733
|
}
|
|
321
734
|
}
|
|
322
735
|
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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)));
|
|
331
750
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
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
|
+
}
|
|
775
|
+
}
|
|
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
|
+
}
|
|
801
|
+
}
|
|
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);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
337
850
|
}
|
|
338
851
|
}
|
|
852
|
+
function printDivider() {
|
|
853
|
+
console.log(chalk.gray("\u2550".repeat(60)));
|
|
854
|
+
}
|
|
339
855
|
|
|
340
|
-
function
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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;
|
|
345
864
|
}
|
|
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;
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
return hasGoodMessage;
|
|
346
874
|
}
|
|
347
875
|
|
|
348
|
-
async function
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
let
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
outputResolve = null;
|
|
378
|
-
}
|
|
876
|
+
async function claudeRemote(opts) {
|
|
877
|
+
let startFrom = opts.sessionId;
|
|
878
|
+
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
879
|
+
startFrom = null;
|
|
880
|
+
}
|
|
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
|
|
889
|
+
};
|
|
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) {
|
|
900
|
+
}
|
|
901
|
+
abortController.abort();
|
|
902
|
+
})();
|
|
903
|
+
} else {
|
|
904
|
+
abortController.abort();
|
|
379
905
|
}
|
|
380
906
|
}
|
|
381
907
|
});
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
+
});
|
|
919
|
+
}
|
|
920
|
+
printDivider();
|
|
921
|
+
try {
|
|
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();
|
|
934
|
+
}
|
|
935
|
+
});
|
|
393
936
|
}
|
|
394
937
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (outputResolve) {
|
|
400
|
-
outputResolve();
|
|
401
|
-
outputResolve = null;
|
|
938
|
+
logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
939
|
+
} catch (e) {
|
|
940
|
+
if (abortController.signal.aborted) {
|
|
941
|
+
logger.debug(`[claudeRemote] Aborted`);
|
|
402
942
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
printDivider();
|
|
954
|
+
logger.debug(`[claudeRemote] Function completed`);
|
|
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);
|
|
980
|
+
}
|
|
410
981
|
}
|
|
411
982
|
});
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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);
|
|
993
|
+
}
|
|
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
|
+
});
|
|
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();
|
|
419
1031
|
}
|
|
420
1032
|
});
|
|
1033
|
+
});
|
|
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");
|
|
421
1058
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
+
});
|
|
1072
|
+
} else {
|
|
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
|
+
});
|
|
425
1083
|
}
|
|
1084
|
+
logger.debug(`[MessageQueue] push() completed. Waiters: ${this.waiters.length}, Queue size after: ${this.queue.length}`);
|
|
426
1085
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
if (stderrBuffer.trim()) {
|
|
436
|
-
yield { type: "error", error: stderrBuffer };
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
function buildArgs(options) {
|
|
440
|
-
const args = [
|
|
441
|
-
"--print",
|
|
442
|
-
options.command,
|
|
443
|
-
"--output-format",
|
|
444
|
-
"stream-json",
|
|
445
|
-
"--verbose"
|
|
446
|
-
];
|
|
447
|
-
if (options.model) {
|
|
448
|
-
args.push("--model", options.model);
|
|
449
|
-
}
|
|
450
|
-
if (options.permissionMode) {
|
|
451
|
-
const modeMap = {
|
|
452
|
-
"auto": "acceptEdits",
|
|
453
|
-
"default": "default",
|
|
454
|
-
"plan": "bypassPermissions"
|
|
455
|
-
};
|
|
456
|
-
args.push("--permission-mode", modeMap[options.permissionMode]);
|
|
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?.();
|
|
457
1093
|
}
|
|
458
|
-
|
|
459
|
-
|
|
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;
|
|
1117
|
+
}
|
|
1118
|
+
if (this.closed) {
|
|
1119
|
+
logger.debug(`[MessageQueue] Iterator ending - queue closed`);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
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;
|
|
1127
|
+
}
|
|
1128
|
+
logger.debug(`[MessageQueue] Iterator yielding waited message`);
|
|
1129
|
+
yield nextMessage;
|
|
1130
|
+
}
|
|
460
1131
|
}
|
|
461
|
-
|
|
462
|
-
|
|
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;
|
|
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
|
+
});
|
|
1153
|
+
});
|
|
463
1154
|
}
|
|
464
|
-
return args;
|
|
465
1155
|
}
|
|
466
1156
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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;
|
|
478
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();
|
|
479
1187
|
});
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
await this._command();
|
|
1208
|
+
});
|
|
1209
|
+
if (this._stopped) {
|
|
1210
|
+
this._notifyPendings();
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (this._invalidatedDouble) {
|
|
1214
|
+
this._invalidatedDouble = false;
|
|
1215
|
+
this._doSync();
|
|
1216
|
+
} else {
|
|
1217
|
+
this._invalidated = false;
|
|
1218
|
+
this._notifyPendings();
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
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);
|
|
1334
|
+
}
|
|
1335
|
+
if (currentSessionId) {
|
|
1336
|
+
sessions.push(currentSessionId);
|
|
1337
|
+
}
|
|
1338
|
+
let processSessionFile = async (sessionId) => {
|
|
1339
|
+
const expectedSessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
|
|
1340
|
+
let file;
|
|
1341
|
+
try {
|
|
1342
|
+
file = await promises.readFile(expectedSessionFile, "utf-8");
|
|
1343
|
+
} catch (error) {
|
|
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;
|
|
513
1354
|
}
|
|
1355
|
+
let key = getMessageKey(parsed.data);
|
|
1356
|
+
if (processedMessages.has(key)) {
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
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
|
+
}
|
|
1366
|
+
}
|
|
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);
|
|
1375
|
+
}
|
|
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
|
+
})();
|
|
1393
|
+
});
|
|
1394
|
+
const intervalId = setInterval(() => {
|
|
1395
|
+
sync.invalidate();
|
|
1396
|
+
}, 3e3);
|
|
1397
|
+
return {
|
|
1398
|
+
refresh: () => sync.invalidate(),
|
|
1399
|
+
cleanup: () => {
|
|
1400
|
+
clearInterval(intervalId);
|
|
1401
|
+
currentSessionWatcherAbortController?.abort();
|
|
1402
|
+
},
|
|
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;
|
|
514
1472
|
}
|
|
515
1473
|
}
|
|
516
|
-
|
|
517
|
-
|
|
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
|
|
518
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
|
|
1549
|
+
});
|
|
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...");
|
|
1558
|
+
}
|
|
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")
|
|
519
1575
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
+
}
|
|
525
1607
|
}
|
|
526
|
-
|
|
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"
|
|
527
1618
|
};
|
|
528
1619
|
}
|
|
529
1620
|
|
|
530
|
-
|
|
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;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Check if interrupt is available
|
|
1657
|
+
*/
|
|
1658
|
+
canInterrupt() {
|
|
1659
|
+
return !!this.interruptFn && !this.isInterrupting;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
async function start(credentials, options = {}) {
|
|
531
1664
|
const workingDirectory = process.cwd();
|
|
532
|
-
const projectName = node_path.basename(workingDirectory);
|
|
533
1665
|
const sessionTag = node_crypto.randomUUID();
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
logger.
|
|
539
|
-
const
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1666
|
+
const api = new ApiClient(credentials.token, credentials.secret);
|
|
1667
|
+
let state = {};
|
|
1668
|
+
let metadata = { path: workingDirectory, host: os.hostname() };
|
|
1669
|
+
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
1670
|
+
logger.debug(`Session created: ${response.id}`);
|
|
1671
|
+
const session = api.session(response);
|
|
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);
|
|
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;
|
|
1724
|
+
});
|
|
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;
|
|
1734
|
+
}
|
|
1735
|
+
session.updateAgentState((currentState) => {
|
|
1736
|
+
let r = { ...currentState.requests };
|
|
1737
|
+
delete r[id];
|
|
1738
|
+
return {
|
|
1739
|
+
...currentState,
|
|
1740
|
+
requests: r
|
|
1741
|
+
};
|
|
1742
|
+
});
|
|
1743
|
+
});
|
|
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);
|
|
1769
|
+
}
|
|
551
1770
|
};
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
1771
|
+
await loop({
|
|
1772
|
+
path: workingDirectory,
|
|
1773
|
+
model: options.model,
|
|
1774
|
+
permissionMode: options.permissionMode,
|
|
1775
|
+
mcpServers: {
|
|
1776
|
+
"permission": {
|
|
1777
|
+
type: "http",
|
|
1778
|
+
url: permissionServer.url
|
|
1779
|
+
}
|
|
1780
|
+
},
|
|
1781
|
+
permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
|
|
1782
|
+
onThinking: (t) => {
|
|
1783
|
+
thinking = t;
|
|
1784
|
+
session.keepAlive(t);
|
|
1785
|
+
},
|
|
1786
|
+
session,
|
|
1787
|
+
onAssistantResult,
|
|
1788
|
+
interruptController
|
|
556
1789
|
});
|
|
1790
|
+
clearInterval(pingInterval);
|
|
1791
|
+
process.exit(0);
|
|
557
1792
|
}
|
|
558
1793
|
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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));
|
|
1821
|
+
}
|
|
1822
|
+
|
|
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);
|
|
1830
|
+
}
|
|
1831
|
+
});
|
|
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;
|
|
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;
|
|
576
1886
|
}
|
|
1887
|
+
return decrypted;
|
|
577
1888
|
}
|
|
578
|
-
|
|
579
|
-
|
|
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(`
|
|
580
1934
|
${chalk.bold("happy")} - Claude Code session sharing
|
|
581
1935
|
|
|
582
1936
|
${chalk.bold("Usage:")}
|
|
583
1937
|
happy [options]
|
|
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
|
|
584
1941
|
|
|
585
1942
|
${chalk.bold("Options:")}
|
|
586
1943
|
-h, --help Show this help message
|
|
@@ -588,21 +1945,68 @@ ${chalk.bold("Options:")}
|
|
|
588
1945
|
-m, --model <model> Claude model to use (default: sonnet)
|
|
589
1946
|
-p, --permission-mode Permission mode: auto, default, or plan
|
|
590
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
|
+
|
|
591
1954
|
${chalk.bold("Examples:")}
|
|
592
1955
|
happy Start a session with default settings
|
|
593
1956
|
happy -m opus Use Claude Opus model
|
|
594
1957
|
happy -p plan Use plan permission mode
|
|
1958
|
+
happy logout Logs out of your account and removes data directory
|
|
595
1959
|
`);
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
if (showVersion) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
1960
|
+
process.exit(0);
|
|
1961
|
+
}
|
|
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
|
+
}
|
|
606
1983
|
}
|
|
607
|
-
|
|
608
|
-
|
|
1984
|
+
})();
|
|
1985
|
+
async function cleanKey() {
|
|
1986
|
+
const happyDir = configuration.happyDir;
|
|
1987
|
+
if (!node_fs.existsSync(happyDir)) {
|
|
1988
|
+
console.log(chalk.yellow("No happy data directory found at:"), happyDir);
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
console.log(chalk.blue("Found happy data directory at:"), happyDir);
|
|
1992
|
+
console.log(chalk.yellow("\u26A0\uFE0F This will remove all authentication data and require reconnecting your phone."));
|
|
1993
|
+
const rl = node_readline.createInterface({
|
|
1994
|
+
input: process.stdin,
|
|
1995
|
+
output: process.stdout
|
|
1996
|
+
});
|
|
1997
|
+
const answer = await new Promise((resolve) => {
|
|
1998
|
+
rl.question(chalk.yellow("Are you sure you want to remove the happy data directory? (y/N): "), resolve);
|
|
1999
|
+
});
|
|
2000
|
+
rl.close();
|
|
2001
|
+
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
2002
|
+
try {
|
|
2003
|
+
node_fs.rmSync(happyDir, { recursive: true, force: true });
|
|
2004
|
+
console.log(chalk.green("\u2713 Happy data directory removed successfully"));
|
|
2005
|
+
console.log(chalk.blue("\u2139\uFE0F You will need to reconnect your phone on the next session"));
|
|
2006
|
+
} catch (error) {
|
|
2007
|
+
throw new Error(`Failed to remove data directory: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2008
|
+
}
|
|
2009
|
+
} else {
|
|
2010
|
+
console.log(chalk.blue("Operation cancelled"));
|
|
2011
|
+
}
|
|
2012
|
+
}
|