happy-coder 0.1.6 → 0.1.9

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.
Files changed (57) hide show
  1. package/dist/index.cjs +354 -917
  2. package/dist/index.mjs +280 -843
  3. package/dist/install-B2r_gX72.cjs +109 -0
  4. package/dist/install-HKe7dyS4.mjs +107 -0
  5. package/dist/lib.cjs +32 -0
  6. package/dist/lib.d.cts +727 -0
  7. package/dist/lib.d.mts +727 -0
  8. package/dist/lib.mjs +14 -0
  9. package/dist/run-FBXkmmN7.mjs +32 -0
  10. package/dist/run-q2To6b-c.cjs +34 -0
  11. package/dist/types-fXgEaaqP.mjs +861 -0
  12. package/dist/types-mykDX2xe.cjs +872 -0
  13. package/dist/uninstall-C42CoSCI.cjs +53 -0
  14. package/dist/uninstall-CLkTtlMv.mjs +51 -0
  15. package/package.json +28 -13
  16. package/dist/auth/auth.d.ts +0 -38
  17. package/dist/auth/auth.js +0 -76
  18. package/dist/auth/auth.test.d.ts +0 -7
  19. package/dist/auth/auth.test.js +0 -96
  20. package/dist/auth/crypto.d.ts +0 -25
  21. package/dist/auth/crypto.js +0 -36
  22. package/dist/claude/claude.d.ts +0 -54
  23. package/dist/claude/claude.js +0 -170
  24. package/dist/claude/claude.test.d.ts +0 -7
  25. package/dist/claude/claude.test.js +0 -130
  26. package/dist/claude/types.d.ts +0 -37
  27. package/dist/claude/types.js +0 -7
  28. package/dist/commands/start.d.ts +0 -38
  29. package/dist/commands/start.js +0 -161
  30. package/dist/commands/start.test.d.ts +0 -7
  31. package/dist/commands/start.test.js +0 -307
  32. package/dist/handlers/message-handler.d.ts +0 -65
  33. package/dist/handlers/message-handler.js +0 -187
  34. package/dist/index.d.ts +0 -1
  35. package/dist/index.js +0 -1
  36. package/dist/session/service.d.ts +0 -27
  37. package/dist/session/service.js +0 -93
  38. package/dist/session/service.test.d.ts +0 -7
  39. package/dist/session/service.test.js +0 -71
  40. package/dist/session/types.d.ts +0 -44
  41. package/dist/session/types.js +0 -4
  42. package/dist/socket/client.d.ts +0 -50
  43. package/dist/socket/client.js +0 -136
  44. package/dist/socket/client.test.d.ts +0 -7
  45. package/dist/socket/client.test.js +0 -74
  46. package/dist/socket/types.d.ts +0 -80
  47. package/dist/socket/types.js +0 -12
  48. package/dist/utils/config.d.ts +0 -22
  49. package/dist/utils/config.js +0 -23
  50. package/dist/utils/logger.d.ts +0 -26
  51. package/dist/utils/logger.js +0 -60
  52. package/dist/utils/paths.d.ts +0 -18
  53. package/dist/utils/paths.js +0 -24
  54. package/dist/utils/qrcode.d.ts +0 -19
  55. package/dist/utils/qrcode.js +0 -37
  56. package/dist/utils/qrcode.test.d.ts +0 -7
  57. package/dist/utils/qrcode.test.js +0 -14
@@ -0,0 +1,872 @@
1
+ 'use strict';
2
+
3
+ var axios = require('axios');
4
+ var chalk = require('chalk');
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');
13
+ var node_crypto = require('node:crypto');
14
+ var tweetnacl = require('tweetnacl');
15
+ var expoServerSdk = require('expo-server-sdk');
16
+
17
+ class Configuration {
18
+ serverUrl;
19
+ // Directories and paths (from persistence)
20
+ happyDir;
21
+ logsDir;
22
+ settingsFile;
23
+ privateKeyFile;
24
+ constructor(location) {
25
+ this.serverUrl = process.env.HANDY_SERVER_URL || "https://handy-api.korshakov.org";
26
+ if (location === "local") {
27
+ this.happyDir = node_path.join(process.cwd(), ".happy");
28
+ } else {
29
+ this.happyDir = node_path.join(os.homedir(), ".happy");
30
+ }
31
+ this.logsDir = node_path.join(this.happyDir, "logs");
32
+ this.settingsFile = node_path.join(this.happyDir, "settings.json");
33
+ this.privateKeyFile = node_path.join(this.happyDir, "access.key");
34
+ }
35
+ }
36
+ exports.configuration = void 0;
37
+ function initializeConfiguration(location) {
38
+ exports.configuration = new Configuration(location);
39
+ }
40
+
41
+ async function getSessionLogPath() {
42
+ if (!node_fs.existsSync(exports.configuration.logsDir)) {
43
+ await promises.mkdir(exports.configuration.logsDir, { recursive: true });
44
+ }
45
+ const now = /* @__PURE__ */ new Date();
46
+ const timestamp = now.toLocaleString("sv-SE", {
47
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
48
+ year: "numeric",
49
+ month: "2-digit",
50
+ day: "2-digit",
51
+ hour: "2-digit",
52
+ minute: "2-digit",
53
+ second: "2-digit"
54
+ }).replace(/[: ]/g, "-").replace(/,/g, "");
55
+ return node_path.join(exports.configuration.logsDir, `${timestamp}.log`);
56
+ }
57
+ class Logger {
58
+ constructor(logFilePathPromise = getSessionLogPath()) {
59
+ this.logFilePathPromise = logFilePathPromise;
60
+ }
61
+ // Use local timezone for simplicity of locating the logs,
62
+ // in practice you will not need absolute timestamps
63
+ localTimezoneTimestamp() {
64
+ return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", {
65
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
66
+ hour12: false,
67
+ hour: "2-digit",
68
+ minute: "2-digit",
69
+ second: "2-digit",
70
+ fractionalSecondDigits: 3
71
+ });
72
+ }
73
+ debug(message, ...args) {
74
+ this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, ...args);
75
+ }
76
+ debugLargeJson(message, object, maxStringLength = 100, maxArrayLength = 10) {
77
+ if (!process.env.DEBUG) {
78
+ this.debug(`In production, skipping message inspection`);
79
+ }
80
+ const truncateStrings = (obj) => {
81
+ if (typeof obj === "string") {
82
+ return obj.length > maxStringLength ? obj.substring(0, maxStringLength) + "... [truncated for logs]" : obj;
83
+ }
84
+ if (Array.isArray(obj)) {
85
+ const truncatedArray = obj.map((item) => truncateStrings(item)).slice(0, maxArrayLength);
86
+ if (obj.length > maxArrayLength) {
87
+ truncatedArray.push(`... [truncated array for logs up to ${maxArrayLength} items]`);
88
+ }
89
+ return truncatedArray;
90
+ }
91
+ if (obj && typeof obj === "object") {
92
+ const result = {};
93
+ for (const [key, value] of Object.entries(obj)) {
94
+ if (key === "usage") {
95
+ continue;
96
+ }
97
+ result[key] = truncateStrings(value);
98
+ }
99
+ return result;
100
+ }
101
+ return obj;
102
+ };
103
+ const truncatedObject = truncateStrings(object);
104
+ const json = JSON.stringify(truncatedObject, null, 2);
105
+ this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, "\n", json);
106
+ }
107
+ info(message, ...args) {
108
+ this.logToConsole("info", "", message, ...args);
109
+ this.debug(message, args);
110
+ }
111
+ infoDeveloper(message, ...args) {
112
+ this.debug(message, ...args);
113
+ if (process.env.DEBUG) {
114
+ this.logToConsole("info", "[DEV]", message, ...args);
115
+ }
116
+ }
117
+ logToConsole(level, prefix, message, ...args) {
118
+ switch (level) {
119
+ case "debug": {
120
+ console.log(chalk.gray(prefix), message, ...args);
121
+ break;
122
+ }
123
+ case "error": {
124
+ console.error(chalk.red(prefix), message, ...args);
125
+ break;
126
+ }
127
+ case "info": {
128
+ console.log(chalk.blue(prefix), message, ...args);
129
+ break;
130
+ }
131
+ case "warn": {
132
+ console.log(chalk.yellow(prefix), message, ...args);
133
+ break;
134
+ }
135
+ default: {
136
+ this.debug("Unknown log level:", level);
137
+ console.log(chalk.blue(prefix), message, ...args);
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ logToFile(prefix, message, ...args) {
143
+ const logLine = `${prefix} ${message} ${args.map(
144
+ (arg) => typeof arg === "string" ? arg : JSON.stringify(arg)
145
+ ).join(" ")}
146
+ `;
147
+ this.logFilePathPromise.then((logFilePath) => {
148
+ try {
149
+ fs.appendFileSync(logFilePath, logLine);
150
+ } catch (appendError) {
151
+ if (process.env.DEBUG) {
152
+ console.error("Failed to append to log file:", appendError);
153
+ throw appendError;
154
+ }
155
+ }
156
+ }).catch((error) => {
157
+ if (process.env.DEBUG) {
158
+ console.log("This message only visible in DEBUG mode, not in production");
159
+ console.error("Failed to resolve log file path:", error);
160
+ console.log(prefix, message, ...args);
161
+ }
162
+ });
163
+ }
164
+ }
165
+ exports.logger = void 0;
166
+ function initLoggerWithGlobalConfiguration() {
167
+ exports.logger = new Logger();
168
+ if (process.env.DEBUG) {
169
+ exports.logger.logFilePathPromise.then((logPath) => {
170
+ exports.logger.info(chalk.yellow("[DEBUG MODE] Debug logging enabled"));
171
+ exports.logger.info(chalk.gray(`Log file: ${logPath}`));
172
+ });
173
+ }
174
+ }
175
+
176
+ const SessionMessageContentSchema = z.z.object({
177
+ c: z.z.string(),
178
+ // Base64 encoded encrypted content
179
+ t: z.z.literal("encrypted")
180
+ });
181
+ const UpdateBodySchema = z.z.object({
182
+ message: z.z.object({
183
+ id: z.z.string(),
184
+ seq: z.z.number(),
185
+ content: SessionMessageContentSchema
186
+ }),
187
+ sid: z.z.string(),
188
+ // Session ID
189
+ t: z.z.literal("new-message")
190
+ });
191
+ const UpdateSessionBodySchema = z.z.object({
192
+ t: z.z.literal("update-session"),
193
+ sid: z.z.string(),
194
+ metadata: z.z.object({
195
+ version: z.z.number(),
196
+ metadata: z.z.string()
197
+ }).nullish(),
198
+ agentState: z.z.object({
199
+ version: z.z.number(),
200
+ agentState: z.z.string()
201
+ }).nullish()
202
+ });
203
+ z.z.object({
204
+ id: z.z.string(),
205
+ seq: z.z.number(),
206
+ body: z.z.union([UpdateBodySchema, UpdateSessionBodySchema]),
207
+ createdAt: z.z.number()
208
+ });
209
+ z.z.object({
210
+ createdAt: z.z.number(),
211
+ id: z.z.string(),
212
+ seq: z.z.number(),
213
+ updatedAt: z.z.number(),
214
+ metadata: z.z.any(),
215
+ metadataVersion: z.z.number(),
216
+ agentState: z.z.any().nullable(),
217
+ agentStateVersion: z.z.number()
218
+ });
219
+ z.z.object({
220
+ content: SessionMessageContentSchema,
221
+ createdAt: z.z.number(),
222
+ id: z.z.string(),
223
+ seq: z.z.number(),
224
+ updatedAt: z.z.number()
225
+ });
226
+ z.z.object({
227
+ session: z.z.object({
228
+ id: z.z.string(),
229
+ tag: z.z.string(),
230
+ seq: z.z.number(),
231
+ createdAt: z.z.number(),
232
+ updatedAt: z.z.number(),
233
+ metadata: z.z.string(),
234
+ metadataVersion: z.z.number(),
235
+ agentState: z.z.string().nullable(),
236
+ agentStateVersion: z.z.number()
237
+ })
238
+ });
239
+ const UserMessageSchema$1 = z.z.object({
240
+ role: z.z.literal("user"),
241
+ content: z.z.object({
242
+ type: z.z.literal("text"),
243
+ text: z.z.string()
244
+ }),
245
+ localKey: z.z.string().optional(),
246
+ // Mobile messages include this
247
+ sentFrom: z.z.enum(["mobile", "cli"]).optional()
248
+ // Source identifier
249
+ });
250
+ const AgentMessageSchema = z.z.object({
251
+ role: z.z.literal("agent"),
252
+ content: z.z.object({
253
+ type: z.z.literal("output"),
254
+ data: z.z.any()
255
+ })
256
+ });
257
+ z.z.union([UserMessageSchema$1, AgentMessageSchema]);
258
+
259
+ function encodeBase64(buffer, variant = "base64") {
260
+ if (variant === "base64url") {
261
+ return encodeBase64Url(buffer);
262
+ }
263
+ return Buffer.from(buffer).toString("base64");
264
+ }
265
+ function encodeBase64Url(buffer) {
266
+ return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
267
+ }
268
+ function decodeBase64(base64, variant = "base64") {
269
+ if (variant === "base64url") {
270
+ const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
271
+ return new Uint8Array(Buffer.from(base64Standard, "base64"));
272
+ }
273
+ return new Uint8Array(Buffer.from(base64, "base64"));
274
+ }
275
+ function getRandomBytes(size) {
276
+ return new Uint8Array(node_crypto.randomBytes(size));
277
+ }
278
+ function encrypt(data, secret) {
279
+ const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
280
+ const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
281
+ const result = new Uint8Array(nonce.length + encrypted.length);
282
+ result.set(nonce);
283
+ result.set(encrypted, nonce.length);
284
+ return result;
285
+ }
286
+ function decrypt(data, secret) {
287
+ const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
288
+ const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
289
+ const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
290
+ if (!decrypted) {
291
+ return null;
292
+ }
293
+ return JSON.parse(new TextDecoder().decode(decrypted));
294
+ }
295
+
296
+ async function delay(ms) {
297
+ return new Promise((resolve) => setTimeout(resolve, ms));
298
+ }
299
+ function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
300
+ let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.max(currentFailureCount, maxFailureCount);
301
+ return Math.round(Math.random() * maxDelayRet);
302
+ }
303
+ function createBackoff(opts) {
304
+ return async (callback) => {
305
+ let currentFailureCount = 0;
306
+ const minDelay = 250;
307
+ const maxDelay = 1e3;
308
+ const maxFailureCount = 50;
309
+ while (true) {
310
+ try {
311
+ return await callback();
312
+ } catch (e) {
313
+ if (currentFailureCount < maxFailureCount) {
314
+ currentFailureCount++;
315
+ }
316
+ let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
317
+ await delay(waitForRequest);
318
+ }
319
+ }
320
+ };
321
+ }
322
+ let backoff = createBackoff();
323
+
324
+ class ApiSessionClient extends node_events.EventEmitter {
325
+ token;
326
+ secret;
327
+ sessionId;
328
+ metadata;
329
+ metadataVersion;
330
+ agentState;
331
+ agentStateVersion;
332
+ socket;
333
+ pendingMessages = [];
334
+ pendingMessageCallback = null;
335
+ rpcHandlers = /* @__PURE__ */ new Map();
336
+ constructor(token, secret, session) {
337
+ super();
338
+ this.token = token;
339
+ this.secret = secret;
340
+ this.sessionId = session.id;
341
+ this.metadata = session.metadata;
342
+ this.metadataVersion = session.metadataVersion;
343
+ this.agentState = session.agentState;
344
+ this.agentStateVersion = session.agentStateVersion;
345
+ this.socket = socket_ioClient.io(exports.configuration.serverUrl, {
346
+ auth: {
347
+ token: this.token,
348
+ clientType: "session-scoped",
349
+ sessionId: this.sessionId
350
+ },
351
+ path: "/v1/updates",
352
+ reconnection: true,
353
+ reconnectionAttempts: Infinity,
354
+ reconnectionDelay: 1e3,
355
+ reconnectionDelayMax: 5e3,
356
+ transports: ["websocket"],
357
+ withCredentials: true,
358
+ autoConnect: false
359
+ });
360
+ this.socket.on("connect", () => {
361
+ exports.logger.debug("Socket connected successfully");
362
+ this.reregisterHandlers();
363
+ });
364
+ this.socket.on("rpc-request", async (data, callback) => {
365
+ try {
366
+ const method = data.method;
367
+ const handler = this.rpcHandlers.get(method);
368
+ if (!handler) {
369
+ exports.logger.debug("[SOCKET] [RPC] [ERROR] method not found", { method });
370
+ const errorResponse = { error: "Method not found" };
371
+ const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
372
+ callback(encryptedError);
373
+ return;
374
+ }
375
+ const decryptedParams = decrypt(decodeBase64(data.params), this.secret);
376
+ const result = await handler(decryptedParams);
377
+ const encryptedResponse = encodeBase64(encrypt(result, this.secret));
378
+ callback(encryptedResponse);
379
+ } catch (error) {
380
+ exports.logger.debug("[SOCKET] [RPC] [ERROR] Error handling RPC request", { error });
381
+ const errorResponse = { error: error instanceof Error ? error.message : "Unknown error" };
382
+ const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
383
+ callback(encryptedError);
384
+ }
385
+ });
386
+ this.socket.on("disconnect", (reason) => {
387
+ exports.logger.debug("[API] Socket disconnected:", reason);
388
+ });
389
+ this.socket.on("connect_error", (error) => {
390
+ exports.logger.debug("[API] Socket connection error:", error);
391
+ });
392
+ this.socket.on("update", (data) => {
393
+ if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
394
+ const body = decrypt(decodeBase64(data.body.message.content.c), this.secret);
395
+ exports.logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
396
+ const userResult = UserMessageSchema$1.safeParse(body);
397
+ if (userResult.success) {
398
+ if (this.pendingMessageCallback) {
399
+ this.pendingMessageCallback(userResult.data);
400
+ } else {
401
+ this.pendingMessages.push(userResult.data);
402
+ }
403
+ } else {
404
+ this.emit("message", body);
405
+ }
406
+ } else if (data.body.t === "update-session") {
407
+ if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
408
+ this.metadata = decrypt(decodeBase64(data.body.metadata.metadata), this.secret);
409
+ this.metadataVersion = data.body.metadata.version;
410
+ }
411
+ if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
412
+ this.agentState = data.body.agentState.agentState ? decrypt(decodeBase64(data.body.agentState.agentState), this.secret) : null;
413
+ this.agentStateVersion = data.body.agentState.version;
414
+ }
415
+ }
416
+ });
417
+ this.socket.on("error", (error) => {
418
+ exports.logger.debug("[API] Socket error:", error);
419
+ });
420
+ this.socket.connect();
421
+ }
422
+ onUserMessage(callback) {
423
+ this.pendingMessageCallback = callback;
424
+ while (this.pendingMessages.length > 0) {
425
+ callback(this.pendingMessages.shift());
426
+ }
427
+ }
428
+ /**
429
+ * Send message to session
430
+ * @param body - Message body (can be MessageContent or raw content for agent messages)
431
+ */
432
+ sendClaudeSessionMessage(body) {
433
+ let content;
434
+ if (body.type === "user" && typeof body.message.content === "string") {
435
+ content = {
436
+ role: "user",
437
+ content: {
438
+ type: "text",
439
+ text: body.message.content
440
+ }
441
+ };
442
+ } else {
443
+ content = {
444
+ role: "agent",
445
+ content: {
446
+ type: "output",
447
+ data: body
448
+ // This wraps the entire Claude message
449
+ }
450
+ };
451
+ }
452
+ exports.logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
453
+ const encrypted = encodeBase64(encrypt(content, this.secret));
454
+ this.socket.emit("message", {
455
+ sid: this.sessionId,
456
+ message: encrypted
457
+ });
458
+ if (body.type === "assistant" && body.message.usage) {
459
+ try {
460
+ this.sendUsageData(body.message.usage);
461
+ } catch (error) {
462
+ exports.logger.debug("[SOCKET] Failed to send usage data:", error);
463
+ }
464
+ }
465
+ }
466
+ /**
467
+ * Send a ping message to keep the connection alive
468
+ */
469
+ keepAlive(thinking) {
470
+ this.socket.volatile.emit("session-alive", {
471
+ sid: this.sessionId,
472
+ time: Date.now(),
473
+ thinking
474
+ });
475
+ }
476
+ /**
477
+ * Send session death message
478
+ */
479
+ sendSessionDeath() {
480
+ this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
481
+ }
482
+ /**
483
+ * Send usage data to the server
484
+ */
485
+ sendUsageData(usage) {
486
+ const totalTokens = usage.input_tokens + usage.output_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
487
+ const usageReport = {
488
+ key: "claude-session",
489
+ sessionId: this.sessionId,
490
+ tokens: {
491
+ total: totalTokens,
492
+ input: usage.input_tokens,
493
+ output: usage.output_tokens,
494
+ cache_creation: usage.cache_creation_input_tokens || 0,
495
+ cache_read: usage.cache_read_input_tokens || 0
496
+ },
497
+ cost: {
498
+ // TODO: Calculate actual costs based on pricing
499
+ // For now, using placeholder values
500
+ total: 0,
501
+ input: 0,
502
+ output: 0
503
+ }
504
+ };
505
+ exports.logger.debugLargeJson("[SOCKET] Sending usage data:", usageReport);
506
+ this.socket.emit("usage-report", usageReport);
507
+ }
508
+ /**
509
+ * Update session metadata
510
+ * @param handler - Handler function that returns the updated metadata
511
+ */
512
+ updateMetadata(handler) {
513
+ backoff(async () => {
514
+ let updated = handler(this.metadata);
515
+ const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(updated, this.secret)) });
516
+ if (answer.result === "success") {
517
+ this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
518
+ this.metadataVersion = answer.version;
519
+ } else if (answer.result === "version-mismatch") {
520
+ if (answer.version > this.metadataVersion) {
521
+ this.metadataVersion = answer.version;
522
+ this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
523
+ }
524
+ throw new Error("Metadata version mismatch");
525
+ } else if (answer.result === "error") ;
526
+ });
527
+ }
528
+ /**
529
+ * Update session agent state
530
+ * @param handler - Handler function that returns the updated agent state
531
+ */
532
+ updateAgentState(handler) {
533
+ console.log("Updating agent state", this.agentState);
534
+ backoff(async () => {
535
+ let updated = handler(this.agentState || {});
536
+ const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(updated, this.secret)) : null });
537
+ if (answer.result === "success") {
538
+ this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
539
+ this.agentStateVersion = answer.version;
540
+ console.log("Agent state updated", this.agentState);
541
+ } else if (answer.result === "version-mismatch") {
542
+ if (answer.version > this.agentStateVersion) {
543
+ this.agentStateVersion = answer.version;
544
+ this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
545
+ }
546
+ throw new Error("Agent state version mismatch");
547
+ } else if (answer.result === "error") {
548
+ console.error("Agent state update error", answer);
549
+ }
550
+ });
551
+ }
552
+ /**
553
+ * Set a custom RPC handler for a specific method with encrypted arguments and responses
554
+ * @param method - The method name to handle
555
+ * @param handler - The handler function to call when the method is invoked
556
+ */
557
+ setHandler(method, handler) {
558
+ const prefixedMethod = `${this.sessionId}:${method}`;
559
+ this.rpcHandlers.set(prefixedMethod, handler);
560
+ this.socket.emit("rpc-register", { method: prefixedMethod });
561
+ exports.logger.debug("Registered RPC handler", { method, prefixedMethod });
562
+ }
563
+ /**
564
+ * Re-register all RPC handlers after reconnection
565
+ */
566
+ reregisterHandlers() {
567
+ exports.logger.debug("Re-registering RPC handlers after reconnection", {
568
+ totalMethods: this.rpcHandlers.size
569
+ });
570
+ for (const [prefixedMethod] of this.rpcHandlers) {
571
+ this.socket.emit("rpc-register", { method: prefixedMethod });
572
+ exports.logger.debug("Re-registered method", { prefixedMethod });
573
+ }
574
+ }
575
+ /**
576
+ * Wait for socket buffer to flush
577
+ */
578
+ async flush() {
579
+ if (!this.socket.connected) {
580
+ return;
581
+ }
582
+ return new Promise((resolve) => {
583
+ this.socket.emit("ping", () => {
584
+ resolve();
585
+ });
586
+ setTimeout(() => {
587
+ resolve();
588
+ }, 1e4);
589
+ });
590
+ }
591
+ async close() {
592
+ this.socket.close();
593
+ }
594
+ }
595
+
596
+ class PushNotificationClient {
597
+ token;
598
+ baseUrl;
599
+ expo;
600
+ constructor(token, baseUrl = "https://handy-api.korshakov.org") {
601
+ this.token = token;
602
+ this.baseUrl = baseUrl;
603
+ this.expo = new expoServerSdk.Expo();
604
+ }
605
+ /**
606
+ * Fetch all push tokens for the authenticated user
607
+ */
608
+ async fetchPushTokens() {
609
+ try {
610
+ const response = await axios.get(
611
+ `${this.baseUrl}/v1/push-tokens`,
612
+ {
613
+ headers: {
614
+ "Authorization": `Bearer ${this.token}`,
615
+ "Content-Type": "application/json"
616
+ }
617
+ }
618
+ );
619
+ exports.logger.info(`Fetched ${response.data.tokens.length} push tokens`);
620
+ return response.data.tokens;
621
+ } catch (error) {
622
+ exports.logger.debug("[PUSH] [ERROR] Failed to fetch push tokens:", error);
623
+ throw new Error(`Failed to fetch push tokens: ${error instanceof Error ? error.message : "Unknown error"}`);
624
+ }
625
+ }
626
+ /**
627
+ * Send push notification via Expo Push API with retry
628
+ * @param messages - Array of push messages to send
629
+ */
630
+ async sendPushNotifications(messages) {
631
+ exports.logger.info(`Sending ${messages.length} push notifications`);
632
+ const validMessages = messages.filter((message) => {
633
+ if (Array.isArray(message.to)) {
634
+ return message.to.every((token) => expoServerSdk.Expo.isExpoPushToken(token));
635
+ }
636
+ return expoServerSdk.Expo.isExpoPushToken(message.to);
637
+ });
638
+ if (validMessages.length === 0) {
639
+ exports.logger.info("No valid Expo push tokens found");
640
+ return;
641
+ }
642
+ const chunks = this.expo.chunkPushNotifications(validMessages);
643
+ for (const chunk of chunks) {
644
+ const startTime = Date.now();
645
+ const timeout = 3e5;
646
+ let attempt = 0;
647
+ while (true) {
648
+ try {
649
+ const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
650
+ const errors = ticketChunk.filter((ticket) => ticket.status === "error");
651
+ if (errors.length > 0) {
652
+ exports.logger.debug("[PUSH] Some notifications failed:", errors);
653
+ }
654
+ if (errors.length === ticketChunk.length) {
655
+ throw new Error("All push notifications in chunk failed");
656
+ }
657
+ break;
658
+ } catch (error) {
659
+ const elapsed = Date.now() - startTime;
660
+ if (elapsed >= timeout) {
661
+ exports.logger.debug("[PUSH] Timeout reached after 5 minutes, giving up on chunk");
662
+ break;
663
+ }
664
+ attempt++;
665
+ const delay = Math.min(1e3 * Math.pow(2, attempt), 3e4);
666
+ const remainingTime = timeout - elapsed;
667
+ const waitTime = Math.min(delay, remainingTime);
668
+ if (waitTime > 0) {
669
+ exports.logger.debug(`[PUSH] Retrying in ${waitTime}ms (attempt ${attempt})`);
670
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
671
+ }
672
+ }
673
+ }
674
+ }
675
+ exports.logger.info(`Push notifications sent successfully`);
676
+ }
677
+ /**
678
+ * Send a push notification to all registered devices for the user
679
+ * @param title - Notification title
680
+ * @param body - Notification body
681
+ * @param data - Additional data to send with the notification
682
+ */
683
+ async sendToAllDevices(title, body, data) {
684
+ const tokens = await this.fetchPushTokens();
685
+ if (tokens.length === 0) {
686
+ exports.logger.info("No push tokens found for user");
687
+ return;
688
+ }
689
+ const messages = tokens.map((token) => ({
690
+ to: token.token,
691
+ title,
692
+ body,
693
+ data,
694
+ sound: "default",
695
+ priority: "high"
696
+ }));
697
+ await this.sendPushNotifications(messages);
698
+ }
699
+ }
700
+
701
+ class ApiClient {
702
+ token;
703
+ secret;
704
+ pushClient;
705
+ constructor(token, secret) {
706
+ this.token = token;
707
+ this.secret = secret;
708
+ this.pushClient = new PushNotificationClient(token);
709
+ }
710
+ /**
711
+ * Create a new session or load existing one with the given tag
712
+ */
713
+ async getOrCreateSession(opts) {
714
+ try {
715
+ const response = await axios.post(
716
+ `${exports.configuration.serverUrl}/v1/sessions`,
717
+ {
718
+ tag: opts.tag,
719
+ metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
720
+ agentState: opts.state ? encodeBase64(encrypt(opts.state, this.secret)) : null
721
+ },
722
+ {
723
+ headers: {
724
+ "Authorization": `Bearer ${this.token}`,
725
+ "Content-Type": "application/json"
726
+ }
727
+ }
728
+ );
729
+ exports.logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
730
+ let raw = response.data.session;
731
+ let session = {
732
+ id: raw.id,
733
+ createdAt: raw.createdAt,
734
+ updatedAt: raw.updatedAt,
735
+ seq: raw.seq,
736
+ metadata: decrypt(decodeBase64(raw.metadata), this.secret),
737
+ metadataVersion: raw.metadataVersion,
738
+ agentState: raw.agentState ? decrypt(decodeBase64(raw.agentState), this.secret) : null,
739
+ agentStateVersion: raw.agentStateVersion
740
+ };
741
+ return session;
742
+ } catch (error) {
743
+ exports.logger.debug("[API] [ERROR] Failed to get or create session:", error);
744
+ throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
745
+ }
746
+ }
747
+ /**
748
+ * Start realtime session client
749
+ * @param id - Session ID
750
+ * @returns Session client
751
+ */
752
+ session(session) {
753
+ return new ApiSessionClient(this.token, this.secret, session);
754
+ }
755
+ /**
756
+ * Get push notification client
757
+ * @returns Push notification client
758
+ */
759
+ push() {
760
+ return this.pushClient;
761
+ }
762
+ }
763
+
764
+ const UsageSchema = z.z.object({
765
+ input_tokens: z.z.number().int().nonnegative(),
766
+ cache_creation_input_tokens: z.z.number().int().nonnegative().optional(),
767
+ cache_read_input_tokens: z.z.number().int().nonnegative().optional(),
768
+ output_tokens: z.z.number().int().nonnegative(),
769
+ service_tier: z.z.string().optional()
770
+ });
771
+ const TextContentSchema = z.z.object({
772
+ type: z.z.literal("text"),
773
+ text: z.z.string()
774
+ });
775
+ const ThinkingContentSchema = z.z.object({
776
+ type: z.z.literal("thinking"),
777
+ thinking: z.z.string(),
778
+ signature: z.z.string()
779
+ });
780
+ const ToolUseContentSchema = z.z.object({
781
+ type: z.z.literal("tool_use"),
782
+ id: z.z.string(),
783
+ name: z.z.string(),
784
+ input: z.z.unknown()
785
+ // Tool-specific input parameters
786
+ });
787
+ const ToolResultContentSchema = z.z.object({
788
+ tool_use_id: z.z.string(),
789
+ type: z.z.literal("tool_result"),
790
+ content: z.z.union([
791
+ z.z.string(),
792
+ // For simple string responses
793
+ z.z.array(TextContentSchema)
794
+ // For structured content blocks (typically text)
795
+ ]),
796
+ is_error: z.z.boolean().optional()
797
+ });
798
+ const ContentSchema = z.z.union([
799
+ TextContentSchema,
800
+ ThinkingContentSchema,
801
+ ToolUseContentSchema,
802
+ ToolResultContentSchema
803
+ ]);
804
+ const UserMessageSchema = z.z.object({
805
+ role: z.z.literal("user"),
806
+ content: z.z.union([
807
+ z.z.string(),
808
+ // Simple string content
809
+ z.z.array(z.z.union([ToolResultContentSchema, TextContentSchema]))
810
+ ])
811
+ });
812
+ const AssistantMessageSchema = z.z.object({
813
+ id: z.z.string(),
814
+ type: z.z.literal("message"),
815
+ role: z.z.literal("assistant"),
816
+ model: z.z.string(),
817
+ content: z.z.array(ContentSchema),
818
+ stop_reason: z.z.string().nullable(),
819
+ stop_sequence: z.z.string().nullable(),
820
+ usage: UsageSchema
821
+ });
822
+ const BaseEntrySchema = z.z.object({
823
+ cwd: z.z.string(),
824
+ sessionId: z.z.string(),
825
+ version: z.z.string(),
826
+ uuid: z.z.string(),
827
+ timestamp: z.z.string().datetime(),
828
+ parent_tool_use_id: z.z.string().nullable().optional()
829
+ });
830
+ const SummaryEntrySchema = z.z.object({
831
+ type: z.z.literal("summary"),
832
+ summary: z.z.string(),
833
+ leafUuid: z.z.string()
834
+ });
835
+ const UserEntrySchema = BaseEntrySchema.extend({
836
+ type: z.z.literal("user"),
837
+ message: UserMessageSchema,
838
+ isMeta: z.z.boolean().optional(),
839
+ toolUseResult: z.z.unknown().optional()
840
+ // Present when user responds to tool use
841
+ });
842
+ const AssistantEntrySchema = BaseEntrySchema.extend({
843
+ type: z.z.literal("assistant"),
844
+ message: AssistantMessageSchema,
845
+ requestId: z.z.string().optional()
846
+ });
847
+ const SystemEntrySchema = BaseEntrySchema.extend({
848
+ type: z.z.literal("system"),
849
+ content: z.z.string(),
850
+ isMeta: z.z.boolean().optional(),
851
+ level: z.z.string().optional(),
852
+ parentUuid: z.z.string().optional(),
853
+ isSidechain: z.z.boolean().optional(),
854
+ userType: z.z.string().optional()
855
+ });
856
+ const RawJSONLinesSchema = z.z.discriminatedUnion("type", [
857
+ UserEntrySchema,
858
+ AssistantEntrySchema,
859
+ SummaryEntrySchema,
860
+ SystemEntrySchema
861
+ ]);
862
+
863
+ exports.ApiClient = ApiClient;
864
+ exports.ApiSessionClient = ApiSessionClient;
865
+ exports.RawJSONLinesSchema = RawJSONLinesSchema;
866
+ exports.backoff = backoff;
867
+ exports.decodeBase64 = decodeBase64;
868
+ exports.delay = delay;
869
+ exports.encodeBase64 = encodeBase64;
870
+ exports.encodeBase64Url = encodeBase64Url;
871
+ exports.initLoggerWithGlobalConfiguration = initLoggerWithGlobalConfiguration;
872
+ exports.initializeConfiguration = initializeConfiguration;