happy-coder 0.1.7 → 0.1.10
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 +950 -938
- package/dist/index.mjs +880 -868
- package/dist/lib.cjs +32 -0
- package/dist/lib.d.cts +527 -0
- package/dist/lib.d.mts +527 -0
- package/dist/lib.mjs +14 -0
- package/dist/types-B2JzqUiU.cjs +831 -0
- package/dist/types-DnQGY77F.mjs +818 -0
- package/package.json +25 -10
- package/dist/auth/auth.d.ts +0 -38
- package/dist/auth/auth.js +0 -76
- package/dist/auth/auth.test.d.ts +0 -7
- package/dist/auth/auth.test.js +0 -96
- package/dist/auth/crypto.d.ts +0 -25
- package/dist/auth/crypto.js +0 -36
- package/dist/claude/claude.d.ts +0 -54
- package/dist/claude/claude.js +0 -170
- package/dist/claude/claude.test.d.ts +0 -7
- package/dist/claude/claude.test.js +0 -130
- package/dist/claude/types.d.ts +0 -37
- package/dist/claude/types.js +0 -7
- package/dist/commands/start.d.ts +0 -38
- package/dist/commands/start.js +0 -161
- package/dist/commands/start.test.d.ts +0 -7
- package/dist/commands/start.test.js +0 -307
- package/dist/handlers/message-handler.d.ts +0 -65
- package/dist/handlers/message-handler.js +0 -187
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/session/service.d.ts +0 -27
- package/dist/session/service.js +0 -93
- package/dist/session/service.test.d.ts +0 -7
- package/dist/session/service.test.js +0 -71
- package/dist/session/types.d.ts +0 -44
- package/dist/session/types.js +0 -4
- package/dist/socket/client.d.ts +0 -50
- package/dist/socket/client.js +0 -136
- package/dist/socket/client.test.d.ts +0 -7
- package/dist/socket/client.test.js +0 -74
- package/dist/socket/types.d.ts +0 -80
- package/dist/socket/types.js +0 -12
- package/dist/utils/config.d.ts +0 -22
- package/dist/utils/config.js +0 -23
- package/dist/utils/logger.d.ts +0 -26
- package/dist/utils/logger.js +0 -60
- package/dist/utils/paths.d.ts +0 -18
- package/dist/utils/paths.js +0 -24
- package/dist/utils/qrcode.d.ts +0 -19
- package/dist/utils/qrcode.js +0 -37
- package/dist/utils/qrcode.test.d.ts +0 -7
- package/dist/utils/qrcode.test.js +0 -14
package/dist/index.cjs
CHANGED
|
@@ -1,740 +1,58 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chalk = require('chalk');
|
|
4
|
-
var
|
|
5
|
-
var fs = require('fs');
|
|
6
|
-
var os = require('node:os');
|
|
7
|
-
var node_path = require('node:path');
|
|
8
|
-
var promises = require('node:fs/promises');
|
|
9
|
-
var node_fs = require('node:fs');
|
|
10
|
-
var node_events = require('node:events');
|
|
11
|
-
var socket_ioClient = require('socket.io-client');
|
|
12
|
-
var z = require('zod');
|
|
4
|
+
var types = require('./types-B2JzqUiU.cjs');
|
|
13
5
|
var node_crypto = require('node:crypto');
|
|
14
|
-
var tweetnacl = require('tweetnacl');
|
|
15
|
-
var expoServerSdk = require('expo-server-sdk');
|
|
16
6
|
var claudeCode = require('@anthropic-ai/claude-code');
|
|
7
|
+
var node_fs = require('node:fs');
|
|
8
|
+
var os = require('node:os');
|
|
9
|
+
var node_path = require('node:path');
|
|
17
10
|
var node_child_process = require('node:child_process');
|
|
18
11
|
var node_readline = require('node:readline');
|
|
19
12
|
var node_url = require('node:url');
|
|
13
|
+
var promises = require('node:fs/promises');
|
|
20
14
|
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
21
15
|
var node_http = require('node:http');
|
|
22
16
|
var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
17
|
+
var z = require('zod');
|
|
18
|
+
var node_https = require('node:https');
|
|
19
|
+
var net = require('node:net');
|
|
20
|
+
var child_process = require('child_process');
|
|
21
|
+
var util = require('util');
|
|
22
|
+
var promises$1 = require('fs/promises');
|
|
23
|
+
var crypto = require('crypto');
|
|
24
|
+
var path = require('path');
|
|
25
|
+
var tweetnacl = require('tweetnacl');
|
|
26
|
+
var axios = require('axios');
|
|
23
27
|
var qrcode = require('qrcode-terminal');
|
|
28
|
+
var node_events = require('node:events');
|
|
29
|
+
var socket_ioClient = require('socket.io-client');
|
|
30
|
+
var os$1 = require('os');
|
|
31
|
+
var fs = require('fs');
|
|
32
|
+
require('expo-server-sdk');
|
|
24
33
|
|
|
25
34
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
26
35
|
function _interopNamespaceDefault(e) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
var n = Object.create(null);
|
|
37
|
+
if (e) {
|
|
38
|
+
Object.keys(e).forEach(function (k) {
|
|
39
|
+
if (k !== 'default') {
|
|
40
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
41
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
get: function () { return e[k]; }
|
|
44
|
+
});
|
|
45
|
+
}
|
|
35
46
|
});
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
n.default = e;
|
|
40
|
-
return Object.freeze(n);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
|
|
44
|
-
|
|
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
|
-
}
|
|
63
|
-
}
|
|
64
|
-
let configuration = void 0;
|
|
65
|
-
function initializeConfiguration(location) {
|
|
66
|
-
configuration = new Configuration(location);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function getSessionLogPath() {
|
|
70
|
-
if (!node_fs.existsSync(configuration.logsDir)) {
|
|
71
|
-
await promises.mkdir(configuration.logsDir, { recursive: true });
|
|
72
|
-
}
|
|
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`);
|
|
84
|
-
}
|
|
85
|
-
class Logger {
|
|
86
|
-
constructor(logFilePathPromise = getSessionLogPath()) {
|
|
87
|
-
this.logFilePathPromise = logFilePathPromise;
|
|
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
|
-
}
|
|
101
|
-
debug(message, ...args) {
|
|
102
|
-
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, ...args);
|
|
103
|
-
}
|
|
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);
|
|
134
|
-
}
|
|
135
|
-
info(message, ...args) {
|
|
136
|
-
this.logToConsole("info", "", message, ...args);
|
|
137
|
-
this.debug(message, args);
|
|
138
|
-
}
|
|
139
|
-
logToConsole(level, prefix, message, ...args) {
|
|
140
|
-
switch (level) {
|
|
141
|
-
case "debug": {
|
|
142
|
-
console.log(chalk.gray(prefix), message, ...args);
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
case "error": {
|
|
146
|
-
console.error(chalk.red(prefix), message, ...args);
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
case "info": {
|
|
150
|
-
console.log(chalk.blue(prefix), message, ...args);
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
case "warn": {
|
|
154
|
-
console.log(chalk.yellow(prefix), message, ...args);
|
|
155
|
-
break;
|
|
156
|
-
}
|
|
157
|
-
default: {
|
|
158
|
-
this.debug("Unknown log level:", level);
|
|
159
|
-
console.log(chalk.blue(prefix), message, ...args);
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
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();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const SessionMessageContentSchema = z.z.object({
|
|
193
|
-
c: z.z.string(),
|
|
194
|
-
// Base64 encoded encrypted content
|
|
195
|
-
t: z.z.literal("encrypted")
|
|
196
|
-
});
|
|
197
|
-
const UpdateBodySchema = z.z.object({
|
|
198
|
-
message: z.z.object({
|
|
199
|
-
id: z.z.string(),
|
|
200
|
-
seq: z.z.number(),
|
|
201
|
-
content: SessionMessageContentSchema
|
|
202
|
-
}),
|
|
203
|
-
sid: z.z.string(),
|
|
204
|
-
// Session ID
|
|
205
|
-
t: z.z.literal("new-message")
|
|
206
|
-
});
|
|
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()
|
|
218
|
-
});
|
|
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()
|
|
224
|
-
});
|
|
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({
|
|
236
|
-
content: SessionMessageContentSchema,
|
|
237
|
-
createdAt: z.z.number(),
|
|
238
|
-
id: z.z.string(),
|
|
239
|
-
seq: z.z.number(),
|
|
240
|
-
updatedAt: z.z.number()
|
|
241
|
-
});
|
|
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()
|
|
253
|
-
})
|
|
254
|
-
});
|
|
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
|
|
265
|
-
});
|
|
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
|
-
})
|
|
272
|
-
});
|
|
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();
|
|
332
|
-
|
|
333
|
-
class ApiSessionClient extends node_events.EventEmitter {
|
|
334
|
-
token;
|
|
335
|
-
secret;
|
|
336
|
-
sessionId;
|
|
337
|
-
metadata;
|
|
338
|
-
metadataVersion;
|
|
339
|
-
agentState;
|
|
340
|
-
agentStateVersion;
|
|
341
|
-
socket;
|
|
342
|
-
pendingMessages = [];
|
|
343
|
-
pendingMessageCallback = null;
|
|
344
|
-
rpcHandlers = /* @__PURE__ */ new Map();
|
|
345
|
-
constructor(token, secret, session) {
|
|
346
|
-
super();
|
|
347
|
-
this.token = token;
|
|
348
|
-
this.secret = secret;
|
|
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, {
|
|
355
|
-
auth: {
|
|
356
|
-
token: this.token,
|
|
357
|
-
clientType: "session-scoped",
|
|
358
|
-
sessionId: this.sessionId
|
|
359
|
-
},
|
|
360
|
-
path: "/v1/updates",
|
|
361
|
-
reconnection: true,
|
|
362
|
-
reconnectionAttempts: Infinity,
|
|
363
|
-
reconnectionDelay: 1e3,
|
|
364
|
-
reconnectionDelayMax: 5e3,
|
|
365
|
-
transports: ["websocket"],
|
|
366
|
-
withCredentials: true,
|
|
367
|
-
autoConnect: false
|
|
368
|
-
});
|
|
369
|
-
this.socket.on("connect", () => {
|
|
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
|
-
}
|
|
394
|
-
});
|
|
395
|
-
this.socket.on("disconnect", (reason) => {
|
|
396
|
-
logger.debug("[API] Socket disconnected:", reason);
|
|
397
|
-
});
|
|
398
|
-
this.socket.on("connect_error", (error) => {
|
|
399
|
-
logger.debug("[API] Socket connection error:", error);
|
|
400
|
-
});
|
|
401
|
-
this.socket.on("update", (data) => {
|
|
402
|
-
if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
|
|
403
|
-
const body = decrypt(decodeBase64(data.body.message.content.c), this.secret);
|
|
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);
|
|
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;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
this.socket.on("error", (error) => {
|
|
427
|
-
logger.debug("[API] Socket error:", error);
|
|
428
|
-
});
|
|
429
|
-
this.socket.connect();
|
|
430
|
-
}
|
|
431
|
-
onUserMessage(callback) {
|
|
432
|
-
this.pendingMessageCallback = callback;
|
|
433
|
-
while (this.pendingMessages.length > 0) {
|
|
434
|
-
callback(this.pendingMessages.shift());
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* Send message to session
|
|
439
|
-
* @param body - Message body (can be MessageContent or raw content for agent messages)
|
|
440
|
-
*/
|
|
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);
|
|
462
|
-
const encrypted = encodeBase64(encrypt(content, this.secret));
|
|
463
|
-
this.socket.emit("message", {
|
|
464
|
-
sid: this.sessionId,
|
|
465
|
-
message: encrypted
|
|
466
|
-
});
|
|
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
|
-
}
|
|
563
|
-
async close() {
|
|
564
|
-
this.socket.close();
|
|
565
|
-
}
|
|
566
|
-
}
|
|
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
47
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
title,
|
|
664
|
-
body,
|
|
665
|
-
data,
|
|
666
|
-
sound: "default",
|
|
667
|
-
priority: "high"
|
|
668
|
-
}));
|
|
669
|
-
await this.sendPushNotifications(messages);
|
|
670
|
-
}
|
|
48
|
+
n.default = e;
|
|
49
|
+
return Object.freeze(n);
|
|
671
50
|
}
|
|
672
51
|
|
|
673
|
-
|
|
674
|
-
token;
|
|
675
|
-
secret;
|
|
676
|
-
pushClient;
|
|
677
|
-
constructor(token, secret) {
|
|
678
|
-
this.token = token;
|
|
679
|
-
this.secret = secret;
|
|
680
|
-
this.pushClient = new PushNotificationClient(token);
|
|
681
|
-
}
|
|
682
|
-
/**
|
|
683
|
-
* Create a new session or load existing one with the given tag
|
|
684
|
-
*/
|
|
685
|
-
async getOrCreateSession(opts) {
|
|
686
|
-
try {
|
|
687
|
-
const response = await axios.post(
|
|
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
|
-
},
|
|
694
|
-
{
|
|
695
|
-
headers: {
|
|
696
|
-
"Authorization": `Bearer ${this.token}`,
|
|
697
|
-
"Content-Type": "application/json"
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
);
|
|
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;
|
|
714
|
-
} catch (error) {
|
|
715
|
-
logger.debug("[API] [ERROR] Failed to get or create session:", error);
|
|
716
|
-
throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* Start realtime session client
|
|
721
|
-
* @param id - Session ID
|
|
722
|
-
* @returns Session client
|
|
723
|
-
*/
|
|
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;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
52
|
+
var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
|
|
735
53
|
|
|
736
54
|
function formatClaudeMessage(message, onAssistantResult) {
|
|
737
|
-
logger.debugLargeJson("[CLAUDE] Message from non interactive & remote mode:", message);
|
|
55
|
+
types.logger.debugLargeJson("[CLAUDE] Message from non interactive & remote mode:", message);
|
|
738
56
|
switch (message.type) {
|
|
739
57
|
case "system": {
|
|
740
58
|
const sysMsg = message;
|
|
@@ -827,7 +145,7 @@ function formatClaudeMessage(message, onAssistantResult) {
|
|
|
827
145
|
console.log(chalk.green("\u{1F449} Press any key to continue your session in `claude`"));
|
|
828
146
|
if (onAssistantResult) {
|
|
829
147
|
Promise.resolve(onAssistantResult(resultMsg)).catch((err) => {
|
|
830
|
-
logger.debug("Error in onAssistantResult callback:", err);
|
|
148
|
+
types.logger.debug("Error in onAssistantResult callback:", err);
|
|
831
149
|
});
|
|
832
150
|
}
|
|
833
151
|
}
|
|
@@ -837,7 +155,7 @@ function formatClaudeMessage(message, onAssistantResult) {
|
|
|
837
155
|
} else if (resultMsg.subtype === "error_during_execution") {
|
|
838
156
|
console.log(chalk.red.bold("\n\u274C Error during execution"));
|
|
839
157
|
console.log(chalk.gray(`Completed ${resultMsg.num_turns} turns before error`));
|
|
840
|
-
logger.debugLargeJson("[RESULT] Error during execution", resultMsg);
|
|
158
|
+
types.logger.debugLargeJson("[RESULT] Error during execution", resultMsg);
|
|
841
159
|
}
|
|
842
160
|
break;
|
|
843
161
|
}
|
|
@@ -859,7 +177,7 @@ function claudeCheckSession(sessionId, path) {
|
|
|
859
177
|
const sessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
|
|
860
178
|
const sessionExists = node_fs.existsSync(sessionFile);
|
|
861
179
|
if (!sessionExists) {
|
|
862
|
-
logger.debug(`[claudeCheckSession] Path ${sessionFile} does not exist`);
|
|
180
|
+
types.logger.debug(`[claudeCheckSession] Path ${sessionFile} does not exist`);
|
|
863
181
|
return false;
|
|
864
182
|
}
|
|
865
183
|
const sessionData = node_fs.readFileSync(sessionFile, "utf-8").split("\n");
|
|
@@ -905,7 +223,7 @@ async function claudeRemote(opts) {
|
|
|
905
223
|
}
|
|
906
224
|
}
|
|
907
225
|
});
|
|
908
|
-
logger.debug(`[claudeRemote] Starting query with messages`);
|
|
226
|
+
types.logger.debug(`[claudeRemote] Starting query with messages`);
|
|
909
227
|
response = claudeCode.query({
|
|
910
228
|
prompt: opts.messages,
|
|
911
229
|
abortController,
|
|
@@ -913,15 +231,15 @@ async function claudeRemote(opts) {
|
|
|
913
231
|
});
|
|
914
232
|
if (opts.interruptController) {
|
|
915
233
|
opts.interruptController.register(async () => {
|
|
916
|
-
logger.debug("[claudeRemote] Interrupting Claude via SDK");
|
|
234
|
+
types.logger.debug("[claudeRemote] Interrupting Claude via SDK");
|
|
917
235
|
await response.interrupt();
|
|
918
236
|
});
|
|
919
237
|
}
|
|
920
238
|
printDivider();
|
|
921
239
|
try {
|
|
922
|
-
logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
240
|
+
types.logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
923
241
|
for await (const message of response) {
|
|
924
|
-
logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
|
|
242
|
+
types.logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
|
|
925
243
|
formatClaudeMessage(message, opts.onAssistantResult);
|
|
926
244
|
if (message.type === "system" && message.subtype === "init") {
|
|
927
245
|
const projectName = node_path.resolve(opts.path).replace(/\//g, "-");
|
|
@@ -935,13 +253,13 @@ async function claudeRemote(opts) {
|
|
|
935
253
|
});
|
|
936
254
|
}
|
|
937
255
|
}
|
|
938
|
-
logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
256
|
+
types.logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
939
257
|
} catch (e) {
|
|
940
258
|
if (abortController.signal.aborted) {
|
|
941
|
-
logger.debug(`[claudeRemote] Aborted`);
|
|
259
|
+
types.logger.debug(`[claudeRemote] Aborted`);
|
|
942
260
|
}
|
|
943
261
|
if (e instanceof claudeCode.AbortError) {
|
|
944
|
-
logger.debug(`[claudeRemote] Aborted`);
|
|
262
|
+
types.logger.debug(`[claudeRemote] Aborted`);
|
|
945
263
|
} else {
|
|
946
264
|
throw e;
|
|
947
265
|
}
|
|
@@ -951,7 +269,7 @@ async function claudeRemote(opts) {
|
|
|
951
269
|
}
|
|
952
270
|
}
|
|
953
271
|
printDivider();
|
|
954
|
-
logger.debug(`[claudeRemote] Function completed`);
|
|
272
|
+
types.logger.debug(`[claudeRemote] Function completed`);
|
|
955
273
|
}
|
|
956
274
|
|
|
957
275
|
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))));
|
|
@@ -965,7 +283,7 @@ async function claudeLocal(opts) {
|
|
|
965
283
|
const detectedIdsFileSystem = /* @__PURE__ */ new Set();
|
|
966
284
|
watcher.on("change", (event, filename) => {
|
|
967
285
|
if (typeof filename === "string" && filename.toLowerCase().endsWith(".jsonl")) {
|
|
968
|
-
logger.debug("change", event, filename);
|
|
286
|
+
types.logger.debug("change", event, filename);
|
|
969
287
|
const sessionId = filename.replace(".jsonl", "");
|
|
970
288
|
if (detectedIdsFileSystem.has(sessionId)) {
|
|
971
289
|
return;
|
|
@@ -1056,10 +374,10 @@ class MessageQueue {
|
|
|
1056
374
|
if (this.closed) {
|
|
1057
375
|
throw new Error("Cannot push to closed queue");
|
|
1058
376
|
}
|
|
1059
|
-
logger.debug(`[MessageQueue] push() called. Waiters: ${this.waiters.length}, Queue size before: ${this.queue.length}`);
|
|
377
|
+
types.logger.debug(`[MessageQueue] push() called. Waiters: ${this.waiters.length}, Queue size before: ${this.queue.length}`);
|
|
1060
378
|
const waiter = this.waiters.shift();
|
|
1061
379
|
if (waiter) {
|
|
1062
|
-
logger.debug(`[MessageQueue] Found waiter! Delivering message directly: "${message}"`);
|
|
380
|
+
types.logger.debug(`[MessageQueue] Found waiter! Delivering message directly: "${message}"`);
|
|
1063
381
|
waiter({
|
|
1064
382
|
type: "user",
|
|
1065
383
|
message: {
|
|
@@ -1070,7 +388,7 @@ class MessageQueue {
|
|
|
1070
388
|
session_id: ""
|
|
1071
389
|
});
|
|
1072
390
|
} else {
|
|
1073
|
-
logger.debug(`[MessageQueue] No waiter found. Adding to queue: "${message}"`);
|
|
391
|
+
types.logger.debug(`[MessageQueue] No waiter found. Adding to queue: "${message}"`);
|
|
1074
392
|
this.queue.push({
|
|
1075
393
|
type: "user",
|
|
1076
394
|
message: {
|
|
@@ -1081,13 +399,13 @@ class MessageQueue {
|
|
|
1081
399
|
session_id: ""
|
|
1082
400
|
});
|
|
1083
401
|
}
|
|
1084
|
-
logger.debug(`[MessageQueue] push() completed. Waiters: ${this.waiters.length}, Queue size after: ${this.queue.length}`);
|
|
402
|
+
types.logger.debug(`[MessageQueue] push() completed. Waiters: ${this.waiters.length}, Queue size after: ${this.queue.length}`);
|
|
1085
403
|
}
|
|
1086
404
|
/**
|
|
1087
405
|
* Close the queue - no more messages can be pushed
|
|
1088
406
|
*/
|
|
1089
407
|
close() {
|
|
1090
|
-
logger.debug(`[MessageQueue] close() called. Waiters: ${this.waiters.length}`);
|
|
408
|
+
types.logger.debug(`[MessageQueue] close() called. Waiters: ${this.waiters.length}`);
|
|
1091
409
|
this.closed = true;
|
|
1092
410
|
this.closeResolve?.();
|
|
1093
411
|
}
|
|
@@ -1107,25 +425,25 @@ class MessageQueue {
|
|
|
1107
425
|
* Async iterator implementation
|
|
1108
426
|
*/
|
|
1109
427
|
async *[Symbol.asyncIterator]() {
|
|
1110
|
-
logger.debug(`[MessageQueue] Iterator started`);
|
|
428
|
+
types.logger.debug(`[MessageQueue] Iterator started`);
|
|
1111
429
|
while (true) {
|
|
1112
430
|
const message = this.queue.shift();
|
|
1113
431
|
if (message !== void 0) {
|
|
1114
|
-
logger.debug(`[MessageQueue] Iterator yielding queued message`);
|
|
432
|
+
types.logger.debug(`[MessageQueue] Iterator yielding queued message`);
|
|
1115
433
|
yield message;
|
|
1116
434
|
continue;
|
|
1117
435
|
}
|
|
1118
436
|
if (this.closed) {
|
|
1119
|
-
logger.debug(`[MessageQueue] Iterator ending - queue closed`);
|
|
437
|
+
types.logger.debug(`[MessageQueue] Iterator ending - queue closed`);
|
|
1120
438
|
return;
|
|
1121
439
|
}
|
|
1122
|
-
logger.debug(`[MessageQueue] Iterator waiting for next message...`);
|
|
440
|
+
types.logger.debug(`[MessageQueue] Iterator waiting for next message...`);
|
|
1123
441
|
const nextMessage = await this.waitForNext();
|
|
1124
442
|
if (nextMessage === void 0) {
|
|
1125
|
-
logger.debug(`[MessageQueue] Iterator ending - no more messages`);
|
|
443
|
+
types.logger.debug(`[MessageQueue] Iterator ending - no more messages`);
|
|
1126
444
|
return;
|
|
1127
445
|
}
|
|
1128
|
-
logger.debug(`[MessageQueue] Iterator yielding waited message`);
|
|
446
|
+
types.logger.debug(`[MessageQueue] Iterator yielding waited message`);
|
|
1129
447
|
yield nextMessage;
|
|
1130
448
|
}
|
|
1131
449
|
}
|
|
@@ -1135,18 +453,18 @@ class MessageQueue {
|
|
|
1135
453
|
waitForNext() {
|
|
1136
454
|
return new Promise((resolve) => {
|
|
1137
455
|
if (this.closed) {
|
|
1138
|
-
logger.debug(`[MessageQueue] waitForNext() called but queue is closed`);
|
|
456
|
+
types.logger.debug(`[MessageQueue] waitForNext() called but queue is closed`);
|
|
1139
457
|
resolve(void 0);
|
|
1140
458
|
return;
|
|
1141
459
|
}
|
|
1142
460
|
const waiter = (value) => resolve(value);
|
|
1143
461
|
this.waiters.push(waiter);
|
|
1144
|
-
logger.debug(`[MessageQueue] waitForNext() adding waiter. Total waiters: ${this.waiters.length}`);
|
|
462
|
+
types.logger.debug(`[MessageQueue] waitForNext() adding waiter. Total waiters: ${this.waiters.length}`);
|
|
1145
463
|
this.closePromise?.then(() => {
|
|
1146
464
|
const index = this.waiters.indexOf(waiter);
|
|
1147
465
|
if (index !== -1) {
|
|
1148
466
|
this.waiters.splice(index, 1);
|
|
1149
|
-
logger.debug(`[MessageQueue] waitForNext() waiter removed due to close. Remaining waiters: ${this.waiters.length}`);
|
|
467
|
+
types.logger.debug(`[MessageQueue] waitForNext() waiter removed due to close. Remaining waiters: ${this.waiters.length}`);
|
|
1150
468
|
resolve(void 0);
|
|
1151
469
|
}
|
|
1152
470
|
});
|
|
@@ -1200,7 +518,7 @@ class InvalidateSync {
|
|
|
1200
518
|
this._pendings = [];
|
|
1201
519
|
};
|
|
1202
520
|
_doSync = async () => {
|
|
1203
|
-
await backoff(async () => {
|
|
521
|
+
await types.backoff(async () => {
|
|
1204
522
|
if (this._stopped) {
|
|
1205
523
|
return;
|
|
1206
524
|
}
|
|
@@ -1220,105 +538,6 @@ class InvalidateSync {
|
|
|
1220
538
|
};
|
|
1221
539
|
}
|
|
1222
540
|
|
|
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
541
|
function createSessionScanner(opts) {
|
|
1323
542
|
const projectName = node_path.resolve(opts.workingDirectory).replace(/\//g, "-");
|
|
1324
543
|
const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
|
|
@@ -1327,6 +546,7 @@ function createSessionScanner(opts) {
|
|
|
1327
546
|
let currentSessionId = null;
|
|
1328
547
|
let currentSessionWatcherAbortController = null;
|
|
1329
548
|
let processedMessages = /* @__PURE__ */ new Set();
|
|
549
|
+
let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
|
|
1330
550
|
const sync = new InvalidateSync(async () => {
|
|
1331
551
|
let sessions = [];
|
|
1332
552
|
for (let p of pendingSessions) {
|
|
@@ -1347,9 +567,9 @@ function createSessionScanner(opts) {
|
|
|
1347
567
|
for (let l of lines) {
|
|
1348
568
|
try {
|
|
1349
569
|
let message = JSON.parse(l);
|
|
1350
|
-
let parsed = RawJSONLinesSchema.safeParse(message);
|
|
570
|
+
let parsed = types.RawJSONLinesSchema.safeParse(message);
|
|
1351
571
|
if (!parsed.success) {
|
|
1352
|
-
logger.debugLargeJson(`[SESSION_SCANNER] Failed to parse message`, message);
|
|
572
|
+
types.logger.debugLargeJson(`[SESSION_SCANNER] Failed to parse message`, message);
|
|
1353
573
|
continue;
|
|
1354
574
|
}
|
|
1355
575
|
let key = getMessageKey(parsed.data);
|
|
@@ -1357,9 +577,16 @@ function createSessionScanner(opts) {
|
|
|
1357
577
|
continue;
|
|
1358
578
|
}
|
|
1359
579
|
processedMessages.add(key);
|
|
1360
|
-
logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
|
|
1361
|
-
logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
|
|
1362
|
-
|
|
580
|
+
types.logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
|
|
581
|
+
types.logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
|
|
582
|
+
if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
|
|
583
|
+
const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
|
|
584
|
+
if (currentCounter && currentCounter > 0) {
|
|
585
|
+
seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
opts.onMessage(message);
|
|
1363
590
|
} catch (e) {
|
|
1364
591
|
continue;
|
|
1365
592
|
}
|
|
@@ -1385,7 +612,7 @@ function createSessionScanner(opts) {
|
|
|
1385
612
|
}
|
|
1386
613
|
} catch (error) {
|
|
1387
614
|
if (error.name !== "AbortError") {
|
|
1388
|
-
logger.debug(`[SESSION_SCANNER] Watch error: ${error.message}`);
|
|
615
|
+
types.logger.debug(`[SESSION_SCANNER] Watch error: ${error.message}`);
|
|
1389
616
|
}
|
|
1390
617
|
}
|
|
1391
618
|
}
|
|
@@ -1413,8 +640,12 @@ function createSessionScanner(opts) {
|
|
|
1413
640
|
if (currentSessionId) {
|
|
1414
641
|
pendingSessions.add(currentSessionId);
|
|
1415
642
|
}
|
|
643
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId}`);
|
|
1416
644
|
currentSessionId = sessionId;
|
|
1417
645
|
sync.invalidate();
|
|
646
|
+
},
|
|
647
|
+
onRemoteUserMessageForDeduplication: (messageContent) => {
|
|
648
|
+
seenRemoteUserMessageCounters.set(messageContent, (seenRemoteUserMessageCounters.get(messageContent) || 0) + 1);
|
|
1418
649
|
}
|
|
1419
650
|
};
|
|
1420
651
|
}
|
|
@@ -1448,32 +679,24 @@ function sortKeys(value) {
|
|
|
1448
679
|
}
|
|
1449
680
|
|
|
1450
681
|
async function loop(opts) {
|
|
1451
|
-
let mode = "interactive";
|
|
682
|
+
let mode = opts.startingMode ?? "interactive";
|
|
1452
683
|
let currentMessageQueue = new MessageQueue();
|
|
1453
684
|
let sessionId = null;
|
|
1454
685
|
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
686
|
const sessionScanner = createSessionScanner({
|
|
1465
687
|
workingDirectory: opts.path,
|
|
1466
688
|
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
689
|
opts.session.sendClaudeSessionMessage(message);
|
|
1475
690
|
}
|
|
1476
691
|
});
|
|
692
|
+
opts.session.onUserMessage((message) => {
|
|
693
|
+
sessionScanner.onRemoteUserMessageForDeduplication(message.content.text);
|
|
694
|
+
currentMessageQueue.push(message.content.text);
|
|
695
|
+
types.logger.debugLargeJson("User message pushed to queue:", message);
|
|
696
|
+
if (onMessage) {
|
|
697
|
+
onMessage();
|
|
698
|
+
}
|
|
699
|
+
});
|
|
1477
700
|
let onSessionFound = (newSessionId) => {
|
|
1478
701
|
sessionId = newSessionId;
|
|
1479
702
|
sessionScanner.onNewSession(newSessionId);
|
|
@@ -1516,7 +739,7 @@ async function loop(opts) {
|
|
|
1516
739
|
}
|
|
1517
740
|
}
|
|
1518
741
|
if (mode === "remote") {
|
|
1519
|
-
logger.debug("Starting " + sessionId);
|
|
742
|
+
types.logger.debug("Starting " + sessionId);
|
|
1520
743
|
const remoteAbortController = new AbortController();
|
|
1521
744
|
opts.session.setHandler("abort", () => {
|
|
1522
745
|
if (!remoteAbortController.signal.aborted) {
|
|
@@ -1528,14 +751,18 @@ async function loop(opts) {
|
|
|
1528
751
|
mode = "interactive";
|
|
1529
752
|
remoteAbortController.abort();
|
|
1530
753
|
}
|
|
1531
|
-
process.stdin.
|
|
754
|
+
if (process.stdin.isTTY) {
|
|
755
|
+
process.stdin.setRawMode(false);
|
|
756
|
+
}
|
|
1532
757
|
};
|
|
1533
758
|
process.stdin.resume();
|
|
1534
|
-
process.stdin.
|
|
759
|
+
if (process.stdin.isTTY) {
|
|
760
|
+
process.stdin.setRawMode(true);
|
|
761
|
+
}
|
|
1535
762
|
process.stdin.setEncoding("utf8");
|
|
1536
763
|
process.stdin.on("data", abortHandler);
|
|
1537
764
|
try {
|
|
1538
|
-
logger.debug(`Starting claudeRemote with messages: ${currentMessageQueue.size()}`);
|
|
765
|
+
types.logger.debug(`Starting claudeRemote with messages: ${currentMessageQueue.size()}`);
|
|
1539
766
|
await claudeRemote({
|
|
1540
767
|
abort: remoteAbortController.signal,
|
|
1541
768
|
sessionId,
|
|
@@ -1549,7 +776,9 @@ async function loop(opts) {
|
|
|
1549
776
|
});
|
|
1550
777
|
} finally {
|
|
1551
778
|
process.stdin.off("data", abortHandler);
|
|
1552
|
-
process.stdin.
|
|
779
|
+
if (process.stdin.isTTY) {
|
|
780
|
+
process.stdin.setRawMode(false);
|
|
781
|
+
}
|
|
1553
782
|
currentMessageQueue.close();
|
|
1554
783
|
currentMessageQueue = new MessageQueue();
|
|
1555
784
|
}
|
|
@@ -1600,7 +829,7 @@ async function startPermissionServerV2(handler) {
|
|
|
1600
829
|
try {
|
|
1601
830
|
await transport.handleRequest(req, res);
|
|
1602
831
|
} catch (error) {
|
|
1603
|
-
logger.debug("Error handling request:", error);
|
|
832
|
+
types.logger.debug("Error handling request:", error);
|
|
1604
833
|
if (!res.headersSent) {
|
|
1605
834
|
res.writeHead(500).end();
|
|
1606
835
|
}
|
|
@@ -1646,7 +875,7 @@ class InterruptController {
|
|
|
1646
875
|
await this.interruptFn();
|
|
1647
876
|
return true;
|
|
1648
877
|
} catch (error) {
|
|
1649
|
-
logger.debug("Failed to interrupt Claude:", error);
|
|
878
|
+
types.logger.debug("Failed to interrupt Claude:", error);
|
|
1650
879
|
return false;
|
|
1651
880
|
} finally {
|
|
1652
881
|
this.isInterrupting = false;
|
|
@@ -1660,16 +889,410 @@ class InterruptController {
|
|
|
1660
889
|
}
|
|
1661
890
|
}
|
|
1662
891
|
|
|
892
|
+
var version = "0.1.10";
|
|
893
|
+
var packageJson = {
|
|
894
|
+
version: version};
|
|
895
|
+
|
|
896
|
+
async function startAnthropicActivityProxy(onClaudeActivity) {
|
|
897
|
+
const requestTimeouts = /* @__PURE__ */ new Map();
|
|
898
|
+
let requestCounter = 0;
|
|
899
|
+
let idleTimer = null;
|
|
900
|
+
const maxTimeBeforeIdle = 50;
|
|
901
|
+
const requestTimeout = 5 * 60 * 1e3;
|
|
902
|
+
const cleanupRequest = (requestId, reason) => {
|
|
903
|
+
const timeout = requestTimeouts.get(requestId);
|
|
904
|
+
if (timeout) {
|
|
905
|
+
clearTimeout(timeout);
|
|
906
|
+
requestTimeouts.delete(requestId);
|
|
907
|
+
types.logger.debug(`[AnthropicProxy #${requestId}] Cleaned up (${reason}), active requests: ${requestTimeouts.size}`);
|
|
908
|
+
claudeDidSomeWork();
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
const claudeDidSomeWork = () => {
|
|
912
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
913
|
+
if (requestTimeouts.size === 0) {
|
|
914
|
+
idleTimer = setTimeout(() => {
|
|
915
|
+
types.logger.debug(`[AnthropicProxy] Idle for ${maxTimeBeforeIdle}ms, active requests: ${requestTimeouts.size}`);
|
|
916
|
+
onClaudeActivity("idle");
|
|
917
|
+
}, maxTimeBeforeIdle);
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
const server = node_http.createServer((req, res) => {
|
|
921
|
+
const requestId = ++requestCounter;
|
|
922
|
+
const isAnthropicRequest = req.headers.host === "api.anthropic.com" || req.url?.includes("anthropic.com");
|
|
923
|
+
if (isAnthropicRequest) {
|
|
924
|
+
const timeout = setTimeout(() => {
|
|
925
|
+
types.logger.debug(`[AnthropicProxy #${requestId}] Request timeout after ${requestTimeout}ms`);
|
|
926
|
+
cleanupRequest(requestId, "timeout");
|
|
927
|
+
}, requestTimeout);
|
|
928
|
+
requestTimeouts.set(requestId, timeout);
|
|
929
|
+
onClaudeActivity("working");
|
|
930
|
+
types.logger.debug(`[AnthropicProxy #${requestId}] Anthropic request: ${req.method} ${req.url}, active requests: ${requestTimeouts.size}`);
|
|
931
|
+
}
|
|
932
|
+
const chunks = [];
|
|
933
|
+
req.on("data", (chunk) => {
|
|
934
|
+
chunks.push(chunk);
|
|
935
|
+
if (isAnthropicRequest) {
|
|
936
|
+
claudeDidSomeWork();
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
req.on("end", () => {
|
|
940
|
+
const body = Buffer.concat(chunks);
|
|
941
|
+
let targetUrl;
|
|
942
|
+
if (isAnthropicRequest) {
|
|
943
|
+
targetUrl = new node_url.URL(req.url || "/", "https://api.anthropic.com");
|
|
944
|
+
} else {
|
|
945
|
+
const protocol = req.headers["x-forwarded-proto"] || "https";
|
|
946
|
+
const host = req.headers.host || "localhost";
|
|
947
|
+
targetUrl = new node_url.URL(req.url || "/", `${protocol}://${host}`);
|
|
948
|
+
}
|
|
949
|
+
const options = {
|
|
950
|
+
hostname: targetUrl.hostname,
|
|
951
|
+
port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
|
|
952
|
+
path: targetUrl.pathname + targetUrl.search,
|
|
953
|
+
method: req.method,
|
|
954
|
+
headers: {
|
|
955
|
+
...req.headers,
|
|
956
|
+
host: targetUrl.hostname
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
const requestMethod = targetUrl.protocol === "https:" ? node_https.request : node_http.request;
|
|
960
|
+
const proxyReq = requestMethod(options, (proxyRes) => {
|
|
961
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
962
|
+
proxyRes.pipe(res);
|
|
963
|
+
proxyRes.on("end", () => {
|
|
964
|
+
if (isAnthropicRequest) {
|
|
965
|
+
cleanupRequest(requestId, "completed");
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
proxyReq.on("error", (error) => {
|
|
970
|
+
if (isAnthropicRequest) {
|
|
971
|
+
cleanupRequest(requestId, `error: ${error.message}`);
|
|
972
|
+
} else {
|
|
973
|
+
types.logger.debug(`[AnthropicProxy #${requestId}] Error:`, error.message);
|
|
974
|
+
}
|
|
975
|
+
res.writeHead(502);
|
|
976
|
+
res.end("Bad Gateway");
|
|
977
|
+
});
|
|
978
|
+
if (body.length > 0) {
|
|
979
|
+
proxyReq.write(body);
|
|
980
|
+
}
|
|
981
|
+
proxyReq.end();
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
server.on("connect", (req, clientSocket, head) => {
|
|
985
|
+
const requestId = ++requestCounter;
|
|
986
|
+
const [hostname, port] = req.url?.split(":") || ["", "443"];
|
|
987
|
+
const isAnthropicRequest = hostname === "api.anthropic.com";
|
|
988
|
+
if (isAnthropicRequest) {
|
|
989
|
+
const timeout = setTimeout(() => {
|
|
990
|
+
types.logger.debug(`[AnthropicProxy #${requestId}] CONNECT timeout after ${requestTimeout}ms`);
|
|
991
|
+
cleanupRequest(requestId, "timeout");
|
|
992
|
+
}, requestTimeout);
|
|
993
|
+
requestTimeouts.set(requestId, timeout);
|
|
994
|
+
onClaudeActivity("working");
|
|
995
|
+
types.logger.debug(`[AnthropicProxy #${requestId}] CONNECT to api.anthropic.com, active requests: ${requestTimeouts.size}`);
|
|
996
|
+
}
|
|
997
|
+
const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
|
|
998
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
999
|
+
serverSocket.write(head);
|
|
1000
|
+
serverSocket.pipe(clientSocket);
|
|
1001
|
+
clientSocket.pipe(serverSocket);
|
|
1002
|
+
});
|
|
1003
|
+
const cleanup = () => {
|
|
1004
|
+
if (isAnthropicRequest) {
|
|
1005
|
+
cleanupRequest(requestId, "CONNECT closed");
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
serverSocket.on("error", (err) => {
|
|
1009
|
+
types.logger.debug(`[AnthropicProxy #${requestId}] CONNECT error:`, err.message);
|
|
1010
|
+
clientSocket.end();
|
|
1011
|
+
cleanup();
|
|
1012
|
+
});
|
|
1013
|
+
clientSocket.on("error", cleanup);
|
|
1014
|
+
clientSocket.on("end", cleanup);
|
|
1015
|
+
serverSocket.on("end", cleanup);
|
|
1016
|
+
});
|
|
1017
|
+
const url = await new Promise((resolve) => {
|
|
1018
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1019
|
+
const addr = server.address();
|
|
1020
|
+
if (addr && typeof addr === "object") {
|
|
1021
|
+
resolve(`http://127.0.0.1:${addr.port}`);
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
types.logger.debug(`[AnthropicProxy] Started at ${url}`);
|
|
1026
|
+
return {
|
|
1027
|
+
url,
|
|
1028
|
+
cleanup: () => {
|
|
1029
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
1030
|
+
for (const [requestId, timeout] of requestTimeouts) {
|
|
1031
|
+
clearTimeout(timeout);
|
|
1032
|
+
types.logger.debug(`[AnthropicProxy] Cleaning up timeout for request #${requestId}`);
|
|
1033
|
+
}
|
|
1034
|
+
requestTimeouts.clear();
|
|
1035
|
+
if (requestTimeouts.size > 0) {
|
|
1036
|
+
types.logger.debug(`[AnthropicProxy] Warning: ${requestTimeouts.size} active requests still pending at cleanup:`, Array.from(requestTimeouts.keys()));
|
|
1037
|
+
}
|
|
1038
|
+
server.close();
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const execAsync = util.promisify(child_process.exec);
|
|
1044
|
+
function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
1045
|
+
session.setHandler("abort", async () => {
|
|
1046
|
+
types.logger.info("Abort request - interrupting Claude");
|
|
1047
|
+
await interruptController.interrupt();
|
|
1048
|
+
});
|
|
1049
|
+
if (permissionCallbacks) {
|
|
1050
|
+
session.setHandler("permission", async (message) => {
|
|
1051
|
+
types.logger.info("Permission response" + JSON.stringify(message));
|
|
1052
|
+
const id = message.id;
|
|
1053
|
+
const resolve = permissionCallbacks.requests.get(id);
|
|
1054
|
+
if (resolve) {
|
|
1055
|
+
if (!message.approved) {
|
|
1056
|
+
types.logger.debug("Permission denied, interrupting Claude");
|
|
1057
|
+
await interruptController.interrupt();
|
|
1058
|
+
}
|
|
1059
|
+
resolve({ approved: message.approved, reason: message.reason });
|
|
1060
|
+
permissionCallbacks.requests.delete(id);
|
|
1061
|
+
} else {
|
|
1062
|
+
types.logger.info("Permission request stale, likely timed out");
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
session.updateAgentState((currentState) => {
|
|
1066
|
+
let r = { ...currentState.requests };
|
|
1067
|
+
delete r[id];
|
|
1068
|
+
return {
|
|
1069
|
+
...currentState,
|
|
1070
|
+
requests: r
|
|
1071
|
+
};
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
session.setHandler("bash", async (data) => {
|
|
1076
|
+
types.logger.info("Shell command request:", data.command);
|
|
1077
|
+
try {
|
|
1078
|
+
const options = {
|
|
1079
|
+
cwd: data.cwd,
|
|
1080
|
+
timeout: data.timeout || 3e4
|
|
1081
|
+
// Default 30 seconds timeout
|
|
1082
|
+
};
|
|
1083
|
+
const { stdout, stderr } = await execAsync(data.command, options);
|
|
1084
|
+
return {
|
|
1085
|
+
success: true,
|
|
1086
|
+
stdout: stdout || "",
|
|
1087
|
+
stderr: stderr || "",
|
|
1088
|
+
exitCode: 0
|
|
1089
|
+
};
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
const execError = error;
|
|
1092
|
+
if (execError.code === "ETIMEDOUT" || execError.killed) {
|
|
1093
|
+
return {
|
|
1094
|
+
success: false,
|
|
1095
|
+
stdout: execError.stdout || "",
|
|
1096
|
+
stderr: execError.stderr || "",
|
|
1097
|
+
exitCode: typeof execError.code === "number" ? execError.code : -1,
|
|
1098
|
+
error: "Command timed out"
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
success: false,
|
|
1103
|
+
stdout: execError.stdout || "",
|
|
1104
|
+
stderr: execError.stderr || execError.message || "Command failed",
|
|
1105
|
+
exitCode: typeof execError.code === "number" ? execError.code : 1,
|
|
1106
|
+
error: execError.message || "Command failed"
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
session.setHandler("readFile", async (data) => {
|
|
1111
|
+
types.logger.info("Read file request:", data.path);
|
|
1112
|
+
try {
|
|
1113
|
+
const buffer = await promises$1.readFile(data.path);
|
|
1114
|
+
const content = buffer.toString("base64");
|
|
1115
|
+
return { success: true, content };
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
types.logger.debug("Failed to read file:", error);
|
|
1118
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
session.setHandler("writeFile", async (data) => {
|
|
1122
|
+
types.logger.info("Write file request:", data.path);
|
|
1123
|
+
try {
|
|
1124
|
+
if (data.expectedHash !== null && data.expectedHash !== void 0) {
|
|
1125
|
+
try {
|
|
1126
|
+
const existingBuffer = await promises$1.readFile(data.path);
|
|
1127
|
+
const existingHash = crypto.createHash("sha256").update(existingBuffer).digest("hex");
|
|
1128
|
+
if (existingHash !== data.expectedHash) {
|
|
1129
|
+
return {
|
|
1130
|
+
success: false,
|
|
1131
|
+
error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
const nodeError = error;
|
|
1136
|
+
if (nodeError.code !== "ENOENT") {
|
|
1137
|
+
throw error;
|
|
1138
|
+
}
|
|
1139
|
+
return {
|
|
1140
|
+
success: false,
|
|
1141
|
+
error: "File does not exist but hash was provided"
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1145
|
+
try {
|
|
1146
|
+
await promises$1.stat(data.path);
|
|
1147
|
+
return {
|
|
1148
|
+
success: false,
|
|
1149
|
+
error: "File already exists but was expected to be new"
|
|
1150
|
+
};
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
const nodeError = error;
|
|
1153
|
+
if (nodeError.code !== "ENOENT") {
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
const buffer = Buffer.from(data.content, "base64");
|
|
1159
|
+
await promises$1.writeFile(data.path, buffer);
|
|
1160
|
+
const hash = crypto.createHash("sha256").update(buffer).digest("hex");
|
|
1161
|
+
return { success: true, hash };
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
types.logger.debug("Failed to write file:", error);
|
|
1164
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
session.setHandler("listDirectory", async (data) => {
|
|
1168
|
+
types.logger.info("List directory request:", data.path);
|
|
1169
|
+
try {
|
|
1170
|
+
const entries = await promises$1.readdir(data.path, { withFileTypes: true });
|
|
1171
|
+
const directoryEntries = await Promise.all(
|
|
1172
|
+
entries.map(async (entry) => {
|
|
1173
|
+
const fullPath = path.join(data.path, entry.name);
|
|
1174
|
+
let type = "other";
|
|
1175
|
+
let size;
|
|
1176
|
+
let modified;
|
|
1177
|
+
if (entry.isDirectory()) {
|
|
1178
|
+
type = "directory";
|
|
1179
|
+
} else if (entry.isFile()) {
|
|
1180
|
+
type = "file";
|
|
1181
|
+
}
|
|
1182
|
+
try {
|
|
1183
|
+
const stats = await promises$1.stat(fullPath);
|
|
1184
|
+
size = stats.size;
|
|
1185
|
+
modified = stats.mtime.getTime();
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
types.logger.debug(`Failed to stat ${fullPath}:`, error);
|
|
1188
|
+
}
|
|
1189
|
+
return {
|
|
1190
|
+
name: entry.name,
|
|
1191
|
+
type,
|
|
1192
|
+
size,
|
|
1193
|
+
modified
|
|
1194
|
+
};
|
|
1195
|
+
})
|
|
1196
|
+
);
|
|
1197
|
+
directoryEntries.sort((a, b) => {
|
|
1198
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
1199
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
1200
|
+
return a.name.localeCompare(b.name);
|
|
1201
|
+
});
|
|
1202
|
+
return { success: true, entries: directoryEntries };
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
types.logger.debug("Failed to list directory:", error);
|
|
1205
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
session.setHandler("getDirectoryTree", async (data) => {
|
|
1209
|
+
types.logger.info("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
|
|
1210
|
+
async function buildTree(path$1, name, currentDepth) {
|
|
1211
|
+
try {
|
|
1212
|
+
const stats = await promises$1.stat(path$1);
|
|
1213
|
+
const node = {
|
|
1214
|
+
name,
|
|
1215
|
+
path: path$1,
|
|
1216
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
1217
|
+
size: stats.size,
|
|
1218
|
+
modified: stats.mtime.getTime()
|
|
1219
|
+
};
|
|
1220
|
+
if (stats.isDirectory() && currentDepth < data.maxDepth) {
|
|
1221
|
+
const entries = await promises$1.readdir(path$1, { withFileTypes: true });
|
|
1222
|
+
const children = [];
|
|
1223
|
+
await Promise.all(
|
|
1224
|
+
entries.map(async (entry) => {
|
|
1225
|
+
if (entry.isSymbolicLink()) {
|
|
1226
|
+
types.logger.debug(`Skipping symlink: ${path.join(path$1, entry.name)}`);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
const childPath = path.join(path$1, entry.name);
|
|
1230
|
+
const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
|
|
1231
|
+
if (childNode) {
|
|
1232
|
+
children.push(childNode);
|
|
1233
|
+
}
|
|
1234
|
+
})
|
|
1235
|
+
);
|
|
1236
|
+
children.sort((a, b) => {
|
|
1237
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
1238
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
1239
|
+
return a.name.localeCompare(b.name);
|
|
1240
|
+
});
|
|
1241
|
+
node.children = children;
|
|
1242
|
+
}
|
|
1243
|
+
return node;
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
types.logger.debug(`Failed to process ${path$1}:`, error instanceof Error ? error.message : String(error));
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
try {
|
|
1250
|
+
if (data.maxDepth < 0) {
|
|
1251
|
+
return { success: false, error: "maxDepth must be non-negative" };
|
|
1252
|
+
}
|
|
1253
|
+
const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
|
|
1254
|
+
const tree = await buildTree(data.path, baseName, 0);
|
|
1255
|
+
if (!tree) {
|
|
1256
|
+
return { success: false, error: "Failed to access the specified path" };
|
|
1257
|
+
}
|
|
1258
|
+
return { success: true, tree };
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
types.logger.debug("Failed to get directory tree:", error);
|
|
1261
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1663
1266
|
async function start(credentials, options = {}) {
|
|
1664
1267
|
const workingDirectory = process.cwd();
|
|
1665
1268
|
const sessionTag = node_crypto.randomUUID();
|
|
1666
|
-
const api = new ApiClient(credentials.token, credentials.secret);
|
|
1269
|
+
const api = new types.ApiClient(credentials.token, credentials.secret);
|
|
1667
1270
|
let state = {};
|
|
1668
|
-
let metadata = { path: workingDirectory, host: os.hostname() };
|
|
1271
|
+
let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version, os: os.platform() };
|
|
1669
1272
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
1670
|
-
logger.debug(`Session created: ${response.id}`);
|
|
1273
|
+
types.logger.debug(`Session created: ${response.id}`);
|
|
1671
1274
|
const session = api.session(response);
|
|
1672
1275
|
const pushClient = api.push();
|
|
1276
|
+
let thinking = false;
|
|
1277
|
+
let pingInterval = setInterval(() => {
|
|
1278
|
+
session.keepAlive(thinking);
|
|
1279
|
+
}, 2e3);
|
|
1280
|
+
const antropicActivityProxy = await startAnthropicActivityProxy(
|
|
1281
|
+
(activity) => {
|
|
1282
|
+
const newThinking = activity === "working";
|
|
1283
|
+
if (newThinking !== thinking) {
|
|
1284
|
+
thinking = newThinking;
|
|
1285
|
+
types.logger.debug(`[PING] Thinking state changed: ${thinking}`);
|
|
1286
|
+
session.keepAlive(thinking);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
);
|
|
1290
|
+
process.env.HTTP_PROXY = antropicActivityProxy.url;
|
|
1291
|
+
process.env.HTTPS_PROXY = antropicActivityProxy.url;
|
|
1292
|
+
types.logger.debug(`[AnthropicProxy] Set HTTP_PROXY and HTTPS_PROXY to ${antropicActivityProxy.url}`);
|
|
1293
|
+
const logPath = await types.logger.logFilePathPromise;
|
|
1294
|
+
types.logger.infoDeveloper(`Session: ${response.id}`);
|
|
1295
|
+
types.logger.infoDeveloper(`Logs: ${logPath}`);
|
|
1673
1296
|
const interruptController = new InterruptController();
|
|
1674
1297
|
let requests = /* @__PURE__ */ new Map();
|
|
1675
1298
|
const permissionServer = await startPermissionServerV2(async (request) => {
|
|
@@ -1678,10 +1301,10 @@ async function start(credentials, options = {}) {
|
|
|
1678
1301
|
requests.set(id, resolve);
|
|
1679
1302
|
});
|
|
1680
1303
|
let timeout = setTimeout(async () => {
|
|
1681
|
-
logger.info("Permission timeout - attempting to interrupt Claude");
|
|
1304
|
+
types.logger.info("Permission timeout - attempting to interrupt Claude");
|
|
1682
1305
|
const interrupted = await interruptController.interrupt();
|
|
1683
1306
|
if (interrupted) {
|
|
1684
|
-
logger.info("Claude interrupted successfully");
|
|
1307
|
+
types.logger.info("Claude interrupted successfully");
|
|
1685
1308
|
}
|
|
1686
1309
|
requests.delete(id);
|
|
1687
1310
|
session.updateAgentState((currentState) => {
|
|
@@ -1693,7 +1316,7 @@ async function start(credentials, options = {}) {
|
|
|
1693
1316
|
};
|
|
1694
1317
|
});
|
|
1695
1318
|
}, 1e3 * 60 * 4.5);
|
|
1696
|
-
logger.info("Permission request" + id + " " + JSON.stringify(request));
|
|
1319
|
+
types.logger.info("Permission request" + id + " " + JSON.stringify(request));
|
|
1697
1320
|
try {
|
|
1698
1321
|
await pushClient.sendToAllDevices(
|
|
1699
1322
|
"Permission Request",
|
|
@@ -1705,9 +1328,9 @@ async function start(credentials, options = {}) {
|
|
|
1705
1328
|
type: "permission_request"
|
|
1706
1329
|
}
|
|
1707
1330
|
);
|
|
1708
|
-
logger.info("Push notification sent for permission request");
|
|
1331
|
+
types.logger.info("Push notification sent for permission request");
|
|
1709
1332
|
} catch (error) {
|
|
1710
|
-
logger.debug("Failed to send push notification:", error);
|
|
1333
|
+
types.logger.debug("Failed to send push notification:", error);
|
|
1711
1334
|
}
|
|
1712
1335
|
session.updateAgentState((currentState) => ({
|
|
1713
1336
|
...currentState,
|
|
@@ -1722,33 +1345,7 @@ async function start(credentials, options = {}) {
|
|
|
1722
1345
|
promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
|
|
1723
1346
|
return promise;
|
|
1724
1347
|
});
|
|
1725
|
-
session
|
|
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);
|
|
1348
|
+
registerHandlers(session, interruptController, { requests });
|
|
1752
1349
|
const onAssistantResult = async (result) => {
|
|
1753
1350
|
try {
|
|
1754
1351
|
const summary = "result" in result && result.result ? result.result.substring(0, 100) + (result.result.length > 100 ? "..." : "") : "";
|
|
@@ -1763,15 +1360,16 @@ async function start(credentials, options = {}) {
|
|
|
1763
1360
|
cost_usd: result.total_cost_usd
|
|
1764
1361
|
}
|
|
1765
1362
|
);
|
|
1766
|
-
logger.debug("Push notification sent: Assistant result");
|
|
1363
|
+
types.logger.debug("Push notification sent: Assistant result");
|
|
1767
1364
|
} catch (error) {
|
|
1768
|
-
logger.debug("Failed to send assistant result push notification:", error);
|
|
1365
|
+
types.logger.debug("Failed to send assistant result push notification:", error);
|
|
1769
1366
|
}
|
|
1770
1367
|
};
|
|
1771
1368
|
await loop({
|
|
1772
1369
|
path: workingDirectory,
|
|
1773
1370
|
model: options.model,
|
|
1774
1371
|
permissionMode: options.permissionMode,
|
|
1372
|
+
startingMode: options.startingMode,
|
|
1775
1373
|
mcpServers: {
|
|
1776
1374
|
"permission": {
|
|
1777
1375
|
type: "http",
|
|
@@ -1779,28 +1377,48 @@ async function start(credentials, options = {}) {
|
|
|
1779
1377
|
}
|
|
1780
1378
|
},
|
|
1781
1379
|
permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
|
|
1782
|
-
onThinking: (t) => {
|
|
1783
|
-
thinking = t;
|
|
1784
|
-
session.keepAlive(t);
|
|
1785
|
-
},
|
|
1786
1380
|
session,
|
|
1787
1381
|
onAssistantResult,
|
|
1788
1382
|
interruptController
|
|
1789
1383
|
});
|
|
1790
1384
|
clearInterval(pingInterval);
|
|
1385
|
+
if (antropicActivityProxy) {
|
|
1386
|
+
types.logger.debug("[AnthropicProxy] Shutting down thinking activity monitoring proxy");
|
|
1387
|
+
antropicActivityProxy.cleanup();
|
|
1388
|
+
}
|
|
1791
1389
|
process.exit(0);
|
|
1792
1390
|
}
|
|
1793
1391
|
|
|
1392
|
+
const defaultSettings = {
|
|
1393
|
+
onboardingCompleted: false
|
|
1394
|
+
};
|
|
1395
|
+
async function readSettings() {
|
|
1396
|
+
if (!node_fs.existsSync(types.configuration.settingsFile)) {
|
|
1397
|
+
return { ...defaultSettings };
|
|
1398
|
+
}
|
|
1399
|
+
try {
|
|
1400
|
+
const content = await promises.readFile(types.configuration.settingsFile, "utf8");
|
|
1401
|
+
return JSON.parse(content);
|
|
1402
|
+
} catch {
|
|
1403
|
+
return { ...defaultSettings };
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
async function writeSettings(settings) {
|
|
1407
|
+
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1408
|
+
await promises.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1409
|
+
}
|
|
1410
|
+
await promises.writeFile(types.configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1411
|
+
}
|
|
1794
1412
|
const credentialsSchema = z__namespace.object({
|
|
1795
1413
|
secret: z__namespace.string().base64(),
|
|
1796
1414
|
token: z__namespace.string()
|
|
1797
1415
|
});
|
|
1798
1416
|
async function readCredentials() {
|
|
1799
|
-
if (!node_fs.existsSync(configuration.privateKeyFile)) {
|
|
1417
|
+
if (!node_fs.existsSync(types.configuration.privateKeyFile)) {
|
|
1800
1418
|
return null;
|
|
1801
1419
|
}
|
|
1802
1420
|
try {
|
|
1803
|
-
const keyBase64 = await promises.readFile(configuration.privateKeyFile, "utf8");
|
|
1421
|
+
const keyBase64 = await promises.readFile(types.configuration.privateKeyFile, "utf8");
|
|
1804
1422
|
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
1805
1423
|
return {
|
|
1806
1424
|
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
@@ -1811,11 +1429,11 @@ async function readCredentials() {
|
|
|
1811
1429
|
}
|
|
1812
1430
|
}
|
|
1813
1431
|
async function writeCredentials(credentials) {
|
|
1814
|
-
if (!node_fs.existsSync(configuration.happyDir)) {
|
|
1815
|
-
await promises.mkdir(configuration.happyDir, { recursive: true });
|
|
1432
|
+
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1433
|
+
await promises.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1816
1434
|
}
|
|
1817
|
-
await promises.writeFile(configuration.privateKeyFile, JSON.stringify({
|
|
1818
|
-
secret: encodeBase64(credentials.secret),
|
|
1435
|
+
await promises.writeFile(types.configuration.privateKeyFile, JSON.stringify({
|
|
1436
|
+
secret: types.encodeBase64(credentials.secret),
|
|
1819
1437
|
token: credentials.token
|
|
1820
1438
|
}, null, 2));
|
|
1821
1439
|
}
|
|
@@ -1837,24 +1455,29 @@ async function doAuth() {
|
|
|
1837
1455
|
const secret = new Uint8Array(node_crypto.randomBytes(32));
|
|
1838
1456
|
const keypair = tweetnacl.box.keyPair.fromSecretKey(secret);
|
|
1839
1457
|
try {
|
|
1840
|
-
await axios.post(`${configuration.serverUrl}/v1/auth/request`, {
|
|
1841
|
-
publicKey: encodeBase64(keypair.publicKey)
|
|
1458
|
+
await axios.post(`${types.configuration.serverUrl}/v1/auth/request`, {
|
|
1459
|
+
publicKey: types.encodeBase64(keypair.publicKey)
|
|
1842
1460
|
});
|
|
1843
1461
|
} catch (error) {
|
|
1844
1462
|
console.log("Failed to create authentication request, please try again later.");
|
|
1845
1463
|
return null;
|
|
1846
1464
|
}
|
|
1847
1465
|
console.log("Please, authenticate using mobile app");
|
|
1848
|
-
|
|
1466
|
+
const authUrl = "happy://terminal?" + types.encodeBase64Url(keypair.publicKey);
|
|
1467
|
+
displayQRCode(authUrl);
|
|
1468
|
+
if (process.env.DEBUG === "1") {
|
|
1469
|
+
console.log("\n\u{1F4CB} For manual entry, copy this URL:");
|
|
1470
|
+
console.log(authUrl);
|
|
1471
|
+
}
|
|
1849
1472
|
let credentials = null;
|
|
1850
1473
|
while (true) {
|
|
1851
1474
|
try {
|
|
1852
|
-
const response = await axios.post(`${configuration.serverUrl}/v1/auth/request`, {
|
|
1853
|
-
publicKey: encodeBase64(keypair.publicKey)
|
|
1475
|
+
const response = await axios.post(`${types.configuration.serverUrl}/v1/auth/request`, {
|
|
1476
|
+
publicKey: types.encodeBase64(keypair.publicKey)
|
|
1854
1477
|
});
|
|
1855
1478
|
if (response.data.state === "authorized") {
|
|
1856
1479
|
let token = response.data.token;
|
|
1857
|
-
let r = decodeBase64(response.data.response);
|
|
1480
|
+
let r = types.decodeBase64(response.data.response);
|
|
1858
1481
|
let decrypted = decryptWithEphemeralKey(r, keypair.secretKey);
|
|
1859
1482
|
if (decrypted) {
|
|
1860
1483
|
credentials = {
|
|
@@ -1872,7 +1495,7 @@ async function doAuth() {
|
|
|
1872
1495
|
console.log("Failed to create authentication request, please try again later.");
|
|
1873
1496
|
return null;
|
|
1874
1497
|
}
|
|
1875
|
-
await delay(1e3);
|
|
1498
|
+
await types.delay(1e3);
|
|
1876
1499
|
}
|
|
1877
1500
|
return null;
|
|
1878
1501
|
}
|
|
@@ -1887,12 +1510,361 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
1887
1510
|
return decrypted;
|
|
1888
1511
|
}
|
|
1889
1512
|
|
|
1513
|
+
class ApiDaemonSession extends node_events.EventEmitter {
|
|
1514
|
+
socket;
|
|
1515
|
+
machineIdentity;
|
|
1516
|
+
keepAliveInterval = null;
|
|
1517
|
+
token;
|
|
1518
|
+
secret;
|
|
1519
|
+
constructor(token, secret, machineIdentity) {
|
|
1520
|
+
super();
|
|
1521
|
+
this.token = token;
|
|
1522
|
+
this.secret = secret;
|
|
1523
|
+
this.machineIdentity = machineIdentity;
|
|
1524
|
+
const socket = socket_ioClient.io(types.configuration.serverUrl, {
|
|
1525
|
+
auth: {
|
|
1526
|
+
token: this.token,
|
|
1527
|
+
clientType: "machine-scoped",
|
|
1528
|
+
machineId: this.machineIdentity.machineId
|
|
1529
|
+
},
|
|
1530
|
+
path: "/v1/user-machine-daemon",
|
|
1531
|
+
reconnection: true,
|
|
1532
|
+
reconnectionAttempts: Infinity,
|
|
1533
|
+
reconnectionDelay: 1e3,
|
|
1534
|
+
reconnectionDelayMax: 5e3,
|
|
1535
|
+
transports: ["websocket"],
|
|
1536
|
+
withCredentials: true,
|
|
1537
|
+
autoConnect: false
|
|
1538
|
+
});
|
|
1539
|
+
socket.on("connect", () => {
|
|
1540
|
+
types.logger.debug("[DAEMON] Connected to server");
|
|
1541
|
+
this.emit("connected");
|
|
1542
|
+
socket.emit("machine-connect", {
|
|
1543
|
+
token: this.token,
|
|
1544
|
+
machineIdentity: types.encodeBase64(types.encrypt(this.machineIdentity, this.secret))
|
|
1545
|
+
});
|
|
1546
|
+
this.startKeepAlive();
|
|
1547
|
+
});
|
|
1548
|
+
socket.on("disconnect", () => {
|
|
1549
|
+
types.logger.debug("[DAEMON] Disconnected from server");
|
|
1550
|
+
this.emit("disconnected");
|
|
1551
|
+
this.stopKeepAlive();
|
|
1552
|
+
});
|
|
1553
|
+
socket.on("spawn-session", async (encryptedData, callback) => {
|
|
1554
|
+
let requestData;
|
|
1555
|
+
try {
|
|
1556
|
+
requestData = types.decrypt(types.decodeBase64(encryptedData), this.secret);
|
|
1557
|
+
types.logger.debug("[DAEMON] Received spawn-session request", requestData);
|
|
1558
|
+
const args = [
|
|
1559
|
+
"--directory",
|
|
1560
|
+
requestData.directory,
|
|
1561
|
+
"--happy-starting-mode",
|
|
1562
|
+
requestData.startingMode
|
|
1563
|
+
];
|
|
1564
|
+
if (requestData.metadata) {
|
|
1565
|
+
args.push("--metadata", requestData.metadata);
|
|
1566
|
+
}
|
|
1567
|
+
if (requestData.startingMode === "interactive" && process.platform === "darwin") {
|
|
1568
|
+
const script = `
|
|
1569
|
+
tell application "Terminal"
|
|
1570
|
+
activate
|
|
1571
|
+
do script "cd ${requestData.directory} && happy ${args.join(" ")}"
|
|
1572
|
+
end tell
|
|
1573
|
+
`;
|
|
1574
|
+
child_process.spawn("osascript", ["-e", script], { detached: true });
|
|
1575
|
+
} else {
|
|
1576
|
+
const child = child_process.spawn("happy", args, {
|
|
1577
|
+
detached: true,
|
|
1578
|
+
stdio: "ignore",
|
|
1579
|
+
cwd: requestData.directory
|
|
1580
|
+
});
|
|
1581
|
+
child.unref();
|
|
1582
|
+
}
|
|
1583
|
+
const result = { success: true };
|
|
1584
|
+
socket.emit("session-spawn-result", {
|
|
1585
|
+
requestId: requestData.requestId,
|
|
1586
|
+
result: types.encodeBase64(types.encrypt(result, this.secret))
|
|
1587
|
+
});
|
|
1588
|
+
callback(types.encodeBase64(types.encrypt({ success: true }, this.secret)));
|
|
1589
|
+
} catch (error) {
|
|
1590
|
+
types.logger.debug("[DAEMON] Failed to spawn session", error);
|
|
1591
|
+
const errorResult = {
|
|
1592
|
+
success: false,
|
|
1593
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1594
|
+
};
|
|
1595
|
+
socket.emit("session-spawn-result", {
|
|
1596
|
+
requestId: requestData?.requestId || "",
|
|
1597
|
+
result: types.encodeBase64(types.encrypt(errorResult, this.secret))
|
|
1598
|
+
});
|
|
1599
|
+
callback(types.encodeBase64(types.encrypt(errorResult, this.secret)));
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
socket.on("daemon-command", (data) => {
|
|
1603
|
+
switch (data.command) {
|
|
1604
|
+
case "shutdown":
|
|
1605
|
+
this.shutdown();
|
|
1606
|
+
break;
|
|
1607
|
+
case "status":
|
|
1608
|
+
this.emit("status-request");
|
|
1609
|
+
break;
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
this.socket = socket;
|
|
1613
|
+
}
|
|
1614
|
+
startKeepAlive() {
|
|
1615
|
+
this.stopKeepAlive();
|
|
1616
|
+
this.keepAliveInterval = setInterval(() => {
|
|
1617
|
+
this.socket.volatile.emit("machine-alive", {
|
|
1618
|
+
time: Date.now()
|
|
1619
|
+
});
|
|
1620
|
+
}, 2e4);
|
|
1621
|
+
}
|
|
1622
|
+
stopKeepAlive() {
|
|
1623
|
+
if (this.keepAliveInterval) {
|
|
1624
|
+
clearInterval(this.keepAliveInterval);
|
|
1625
|
+
this.keepAliveInterval = null;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
connect() {
|
|
1629
|
+
this.socket.connect();
|
|
1630
|
+
}
|
|
1631
|
+
shutdown() {
|
|
1632
|
+
this.stopKeepAlive();
|
|
1633
|
+
this.socket.close();
|
|
1634
|
+
this.emit("shutdown");
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
const DAEMON_PID_FILE = path.join(os$1.homedir(), ".happy", "daemon-pid");
|
|
1639
|
+
async function startDaemon() {
|
|
1640
|
+
if (isDaemonRunning()) {
|
|
1641
|
+
console.log("Happy daemon is already running");
|
|
1642
|
+
process.exit(0);
|
|
1643
|
+
}
|
|
1644
|
+
types.logger.info("Happy CLI daemon started successfully");
|
|
1645
|
+
writePidFile();
|
|
1646
|
+
process.on("SIGINT", stopDaemon);
|
|
1647
|
+
process.on("SIGTERM", stopDaemon);
|
|
1648
|
+
process.on("exit", stopDaemon);
|
|
1649
|
+
try {
|
|
1650
|
+
const settings = await readSettings() || { onboardingCompleted: false };
|
|
1651
|
+
if (!settings.machineId) {
|
|
1652
|
+
settings.machineId = crypto.randomUUID();
|
|
1653
|
+
settings.machineHost = os$1.hostname();
|
|
1654
|
+
await writeSettings(settings);
|
|
1655
|
+
}
|
|
1656
|
+
const machineIdentity = {
|
|
1657
|
+
machineId: settings.machineId,
|
|
1658
|
+
machineHost: settings.machineHost || os$1.hostname(),
|
|
1659
|
+
platform: process.platform,
|
|
1660
|
+
version: process.env.npm_package_version || "unknown"
|
|
1661
|
+
};
|
|
1662
|
+
let credentials = await readCredentials();
|
|
1663
|
+
if (!credentials) {
|
|
1664
|
+
types.logger.debug("[DAEMON] No credentials found, running auth");
|
|
1665
|
+
await doAuth();
|
|
1666
|
+
credentials = await readCredentials();
|
|
1667
|
+
if (!credentials) {
|
|
1668
|
+
throw new Error("Failed to authenticate");
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
const { token, secret } = credentials;
|
|
1672
|
+
const daemon = new ApiDaemonSession(token, secret, machineIdentity);
|
|
1673
|
+
daemon.on("connected", () => {
|
|
1674
|
+
types.logger.debug("[DAEMON] Successfully connected to server");
|
|
1675
|
+
});
|
|
1676
|
+
daemon.on("disconnected", () => {
|
|
1677
|
+
types.logger.debug("[DAEMON] Disconnected from server");
|
|
1678
|
+
});
|
|
1679
|
+
daemon.on("shutdown", () => {
|
|
1680
|
+
types.logger.debug("[DAEMON] Shutdown requested");
|
|
1681
|
+
stopDaemon();
|
|
1682
|
+
process.exit(0);
|
|
1683
|
+
});
|
|
1684
|
+
daemon.connect();
|
|
1685
|
+
setInterval(() => {
|
|
1686
|
+
}, 1e3);
|
|
1687
|
+
} catch (error) {
|
|
1688
|
+
types.logger.debug("[DAEMON] Failed to start daemon", error);
|
|
1689
|
+
stopDaemon();
|
|
1690
|
+
process.exit(1);
|
|
1691
|
+
}
|
|
1692
|
+
process.on("SIGINT", () => process.exit(0));
|
|
1693
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
1694
|
+
process.on("exit", () => process.exit(0));
|
|
1695
|
+
while (true) {
|
|
1696
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function isDaemonRunning() {
|
|
1700
|
+
try {
|
|
1701
|
+
if (!fs.existsSync(DAEMON_PID_FILE)) {
|
|
1702
|
+
console.log("No PID file found");
|
|
1703
|
+
return false;
|
|
1704
|
+
}
|
|
1705
|
+
const pid = parseInt(fs.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
1706
|
+
try {
|
|
1707
|
+
process.kill(pid, 0);
|
|
1708
|
+
return true;
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
console.log("Process not running", error);
|
|
1711
|
+
fs.unlinkSync(DAEMON_PID_FILE);
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
} catch {
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
function writePidFile() {
|
|
1719
|
+
const happyDir = path.join(os$1.homedir(), ".happy");
|
|
1720
|
+
if (!fs.existsSync(happyDir)) {
|
|
1721
|
+
fs.mkdirSync(happyDir, { recursive: true });
|
|
1722
|
+
}
|
|
1723
|
+
fs.writeFileSync(DAEMON_PID_FILE, process.pid.toString());
|
|
1724
|
+
}
|
|
1725
|
+
function stopDaemon() {
|
|
1726
|
+
try {
|
|
1727
|
+
if (fs.existsSync(DAEMON_PID_FILE)) {
|
|
1728
|
+
types.logger.debug("[DAEMON] Stopping daemon");
|
|
1729
|
+
process.kill(parseInt(fs.readFileSync(DAEMON_PID_FILE, "utf-8")), "SIGTERM");
|
|
1730
|
+
fs.unlinkSync(DAEMON_PID_FILE);
|
|
1731
|
+
}
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
types.logger.debug("[DAEMON] Error cleaning up PID file", error);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function trimIdent(text) {
|
|
1738
|
+
const lines = text.split("\n");
|
|
1739
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
1740
|
+
lines.shift();
|
|
1741
|
+
}
|
|
1742
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
1743
|
+
lines.pop();
|
|
1744
|
+
}
|
|
1745
|
+
const minSpaces = lines.reduce((min, line) => {
|
|
1746
|
+
if (line.trim() === "") {
|
|
1747
|
+
return min;
|
|
1748
|
+
}
|
|
1749
|
+
const leadingSpaces = line.match(/^\s*/)[0].length;
|
|
1750
|
+
return Math.min(min, leadingSpaces);
|
|
1751
|
+
}, Infinity);
|
|
1752
|
+
const trimmedLines = lines.map((line) => line.slice(minSpaces));
|
|
1753
|
+
return trimmedLines.join("\n");
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
const PLIST_LABEL$1 = "com.happy-cli.daemon";
|
|
1757
|
+
const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
|
|
1758
|
+
const USER_HOME = process.env.HOME || process.env.USERPROFILE;
|
|
1759
|
+
async function install$1() {
|
|
1760
|
+
try {
|
|
1761
|
+
if (fs.existsSync(PLIST_FILE$1)) {
|
|
1762
|
+
types.logger.info("Daemon plist already exists. Uninstalling first...");
|
|
1763
|
+
child_process.execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
1764
|
+
}
|
|
1765
|
+
const happyPath = process.argv[0];
|
|
1766
|
+
const scriptPath = process.argv[1];
|
|
1767
|
+
const plistContent = trimIdent(`
|
|
1768
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
1769
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1770
|
+
<plist version="1.0">
|
|
1771
|
+
<dict>
|
|
1772
|
+
<key>Label</key>
|
|
1773
|
+
<string>${PLIST_LABEL$1}</string>
|
|
1774
|
+
|
|
1775
|
+
<key>ProgramArguments</key>
|
|
1776
|
+
<array>
|
|
1777
|
+
<string>${happyPath}</string>
|
|
1778
|
+
<string>${scriptPath}</string>
|
|
1779
|
+
<string>happy-daemon</string>
|
|
1780
|
+
</array>
|
|
1781
|
+
|
|
1782
|
+
<key>EnvironmentVariables</key>
|
|
1783
|
+
<dict>
|
|
1784
|
+
<key>HAPPY_DAEMON_MODE</key>
|
|
1785
|
+
<string>true</string>
|
|
1786
|
+
</dict>
|
|
1787
|
+
|
|
1788
|
+
<key>RunAtLoad</key>
|
|
1789
|
+
<true/>
|
|
1790
|
+
|
|
1791
|
+
<key>KeepAlive</key>
|
|
1792
|
+
<true/>
|
|
1793
|
+
|
|
1794
|
+
<key>StandardErrorPath</key>
|
|
1795
|
+
<string>${USER_HOME}/.happy/daemon.err</string>
|
|
1796
|
+
|
|
1797
|
+
<key>StandardOutPath</key>
|
|
1798
|
+
<string>${USER_HOME}/.happy/daemon.log</string>
|
|
1799
|
+
|
|
1800
|
+
<key>WorkingDirectory</key>
|
|
1801
|
+
<string>/tmp</string>
|
|
1802
|
+
</dict>
|
|
1803
|
+
</plist>
|
|
1804
|
+
`);
|
|
1805
|
+
fs.writeFileSync(PLIST_FILE$1, plistContent);
|
|
1806
|
+
fs.chmodSync(PLIST_FILE$1, 420);
|
|
1807
|
+
types.logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
|
|
1808
|
+
child_process.execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
1809
|
+
types.logger.info("Daemon installed and started successfully");
|
|
1810
|
+
types.logger.info("Check logs at ~/.happy/daemon.log");
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
types.logger.debug("Failed to install daemon:", error);
|
|
1813
|
+
throw error;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
async function install() {
|
|
1818
|
+
if (process.platform !== "darwin") {
|
|
1819
|
+
throw new Error("Daemon installation is currently only supported on macOS");
|
|
1820
|
+
}
|
|
1821
|
+
if (process.getuid && process.getuid() !== 0) {
|
|
1822
|
+
throw new Error("Daemon installation requires sudo privileges. Please run with sudo.");
|
|
1823
|
+
}
|
|
1824
|
+
types.logger.info("Installing Happy CLI daemon for macOS...");
|
|
1825
|
+
await install$1();
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
const PLIST_LABEL = "com.happy-cli.daemon";
|
|
1829
|
+
const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
|
|
1830
|
+
async function uninstall$1() {
|
|
1831
|
+
try {
|
|
1832
|
+
if (!fs.existsSync(PLIST_FILE)) {
|
|
1833
|
+
types.logger.info("Daemon plist not found. Nothing to uninstall.");
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
try {
|
|
1837
|
+
child_process.execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
|
|
1838
|
+
types.logger.info("Daemon stopped successfully");
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
types.logger.info("Failed to unload daemon (it might not be running)");
|
|
1841
|
+
}
|
|
1842
|
+
fs.unlinkSync(PLIST_FILE);
|
|
1843
|
+
types.logger.info(`Removed daemon plist from ${PLIST_FILE}`);
|
|
1844
|
+
types.logger.info("Daemon uninstalled successfully");
|
|
1845
|
+
} catch (error) {
|
|
1846
|
+
types.logger.debug("Failed to uninstall daemon:", error);
|
|
1847
|
+
throw error;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
async function uninstall() {
|
|
1852
|
+
if (process.platform !== "darwin") {
|
|
1853
|
+
throw new Error("Daemon uninstallation is currently only supported on macOS");
|
|
1854
|
+
}
|
|
1855
|
+
if (process.getuid && process.getuid() !== 0) {
|
|
1856
|
+
throw new Error("Daemon uninstallation requires sudo privileges. Please run with sudo.");
|
|
1857
|
+
}
|
|
1858
|
+
types.logger.info("Uninstalling Happy CLI daemon for macOS...");
|
|
1859
|
+
await uninstall$1();
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1890
1862
|
(async () => {
|
|
1891
1863
|
const args = process.argv.slice(2);
|
|
1892
1864
|
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);
|
|
1865
|
+
types.initializeConfiguration(installationLocation);
|
|
1866
|
+
types.initLoggerWithGlobalConfiguration();
|
|
1867
|
+
types.logger.debug("Starting happy CLI with args: ", process.argv);
|
|
1896
1868
|
const subcommand = args[0];
|
|
1897
1869
|
if (subcommand === "logout") {
|
|
1898
1870
|
try {
|
|
@@ -1905,25 +1877,64 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
1905
1877
|
process.exit(1);
|
|
1906
1878
|
}
|
|
1907
1879
|
return;
|
|
1908
|
-
} else if (subcommand === "
|
|
1909
|
-
|
|
1880
|
+
} else if (subcommand === "daemon") {
|
|
1881
|
+
const daemonSubcommand = args[1];
|
|
1882
|
+
if (daemonSubcommand === "start") {
|
|
1883
|
+
await startDaemon();
|
|
1884
|
+
process.exit(0);
|
|
1885
|
+
} else if (daemonSubcommand === "stop") {
|
|
1886
|
+
await stopDaemon();
|
|
1887
|
+
process.exit(0);
|
|
1888
|
+
} else if (daemonSubcommand === "install") {
|
|
1889
|
+
try {
|
|
1890
|
+
await install();
|
|
1891
|
+
} catch (error) {
|
|
1892
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1893
|
+
process.exit(1);
|
|
1894
|
+
}
|
|
1895
|
+
} else if (daemonSubcommand === "uninstall") {
|
|
1896
|
+
try {
|
|
1897
|
+
await uninstall();
|
|
1898
|
+
} catch (error) {
|
|
1899
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1900
|
+
process.exit(1);
|
|
1901
|
+
}
|
|
1902
|
+
} else {
|
|
1903
|
+
console.log(`
|
|
1904
|
+
${chalk.bold("happy daemon")} - Daemon management
|
|
1905
|
+
|
|
1906
|
+
${chalk.bold("Usage:")}
|
|
1907
|
+
happy daemon start Start the daemon
|
|
1908
|
+
happy daemon stop Stop the daemon
|
|
1909
|
+
sudo happy daemon install Install the daemon (requires sudo)
|
|
1910
|
+
sudo happy daemon uninstall Uninstall the daemon (requires sudo)
|
|
1911
|
+
|
|
1912
|
+
${chalk.bold("Note:")} The daemon runs in the background and provides persistent services.
|
|
1913
|
+
Currently only supported on macOS.
|
|
1914
|
+
`);
|
|
1915
|
+
}
|
|
1910
1916
|
return;
|
|
1911
1917
|
} else {
|
|
1912
1918
|
const options = {};
|
|
1913
1919
|
let showHelp = false;
|
|
1914
1920
|
let showVersion = false;
|
|
1921
|
+
let forceAuth = false;
|
|
1915
1922
|
for (let i = 0; i < args.length; i++) {
|
|
1916
1923
|
const arg = args[i];
|
|
1917
1924
|
if (arg === "-h" || arg === "--help") {
|
|
1918
1925
|
showHelp = true;
|
|
1919
1926
|
} else if (arg === "-v" || arg === "--version") {
|
|
1920
1927
|
showVersion = true;
|
|
1928
|
+
} else if (arg === "--auth" || arg === "--login") {
|
|
1929
|
+
forceAuth = true;
|
|
1921
1930
|
} else if (arg === "-m" || arg === "--model") {
|
|
1922
1931
|
options.model = args[++i];
|
|
1923
1932
|
} else if (arg === "-p" || arg === "--permission-mode") {
|
|
1924
|
-
options.permissionMode = args[++i];
|
|
1933
|
+
options.permissionMode = z.z.enum(["auto", "default", "plan"]).parse(args[++i]);
|
|
1925
1934
|
} else if (arg === "--local") {
|
|
1926
1935
|
i++;
|
|
1936
|
+
} else if (arg === "--happy-starting-mode") {
|
|
1937
|
+
options.startingMode = z.z.enum(["interactive", "remote"]).parse(args[++i]);
|
|
1927
1938
|
} else {
|
|
1928
1939
|
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
1929
1940
|
process.exit(1);
|
|
@@ -1936,35 +1947,36 @@ ${chalk.bold("happy")} - Claude Code session sharing
|
|
|
1936
1947
|
${chalk.bold("Usage:")}
|
|
1937
1948
|
happy [options]
|
|
1938
1949
|
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
|
|
1941
1950
|
|
|
1942
1951
|
${chalk.bold("Options:")}
|
|
1943
1952
|
-h, --help Show this help message
|
|
1944
1953
|
-v, --version Show version
|
|
1945
1954
|
-m, --model <model> Claude model to use (default: sonnet)
|
|
1946
1955
|
-p, --permission-mode Permission mode: auto, default, or plan
|
|
1956
|
+
--auth, --login Force re-authentication
|
|
1947
1957
|
|
|
1948
1958
|
[Advanced]
|
|
1949
1959
|
--local < global | local >
|
|
1950
1960
|
Will use .happy folder in the current directory for storing your private key and debug logs.
|
|
1951
1961
|
You will require re-login each time you run this in a new directory.
|
|
1952
|
-
|
|
1962
|
+
--happy-starting-mode <interactive|remote>
|
|
1963
|
+
Set the starting mode for new sessions (default: remote)
|
|
1953
1964
|
|
|
1954
1965
|
${chalk.bold("Examples:")}
|
|
1955
1966
|
happy Start a session with default settings
|
|
1956
1967
|
happy -m opus Use Claude Opus model
|
|
1957
1968
|
happy -p plan Use plan permission mode
|
|
1969
|
+
happy --auth Force re-authentication before starting session
|
|
1958
1970
|
happy logout Logs out of your account and removes data directory
|
|
1959
1971
|
`);
|
|
1960
1972
|
process.exit(0);
|
|
1961
1973
|
}
|
|
1962
1974
|
if (showVersion) {
|
|
1963
|
-
console.log(
|
|
1975
|
+
console.log(packageJson.version);
|
|
1964
1976
|
process.exit(0);
|
|
1965
1977
|
}
|
|
1966
1978
|
let credentials = await readCredentials();
|
|
1967
|
-
if (!credentials) {
|
|
1979
|
+
if (!credentials || forceAuth) {
|
|
1968
1980
|
let res = await doAuth();
|
|
1969
1981
|
if (!res) {
|
|
1970
1982
|
process.exit(1);
|
|
@@ -1983,7 +1995,7 @@ ${chalk.bold("Examples:")}
|
|
|
1983
1995
|
}
|
|
1984
1996
|
})();
|
|
1985
1997
|
async function cleanKey() {
|
|
1986
|
-
const happyDir = configuration.happyDir;
|
|
1998
|
+
const happyDir = types.configuration.happyDir;
|
|
1987
1999
|
if (!node_fs.existsSync(happyDir)) {
|
|
1988
2000
|
console.log(chalk.yellow("No happy data directory found at:"), happyDir);
|
|
1989
2001
|
return;
|