happy-coder 0.6.4 → 0.7.1-beta.2

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.
@@ -16,40 +16,32 @@ var expoServerSdk = require('expo-server-sdk');
16
16
 
17
17
  class Configuration {
18
18
  serverUrl;
19
- installationLocation;
20
19
  isDaemonProcess;
21
20
  // Directories and paths (from persistence)
22
- happyDir;
21
+ happyHomeDir;
23
22
  logsDir;
24
23
  daemonLogsDir;
25
24
  settingsFile;
26
25
  privateKeyFile;
27
- daemonMetadataFile;
28
- constructor(location) {
29
- this.serverUrl = process.env.HANDY_SERVER_URL || "https://handy-api.korshakov.org";
26
+ daemonStateFile;
27
+ constructor() {
28
+ this.serverUrl = process.env.HAPPY_SERVER_URL || "https://handy-api.korshakov.org";
30
29
  const args = process.argv.slice(2);
31
- this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && (args[1] === "start" || args[1] === "stop");
32
- if (location === "local") {
33
- this.happyDir = node_path.join(process.cwd(), ".happy");
34
- this.installationLocation = "local";
35
- } else if (location === "global") {
36
- this.happyDir = node_path.join(os.homedir(), ".happy");
37
- this.installationLocation = "global";
30
+ this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
31
+ if (process.env.HAPPY_HOME_DIR) {
32
+ const expandedPath = process.env.HAPPY_HOME_DIR.replace(/^~/, os.homedir());
33
+ this.happyHomeDir = expandedPath;
38
34
  } else {
39
- this.happyDir = node_path.join(location, ".happy");
40
- this.installationLocation = "global";
35
+ this.happyHomeDir = node_path.join(os.homedir(), ".happy");
41
36
  }
42
- this.logsDir = node_path.join(this.happyDir, "logs");
43
- this.daemonLogsDir = node_path.join(this.happyDir, "logs-daemon");
44
- this.settingsFile = node_path.join(this.happyDir, "settings.json");
45
- this.privateKeyFile = node_path.join(this.happyDir, "access.key");
46
- this.daemonMetadataFile = node_path.join(this.happyDir, "daemon-metadata.json");
37
+ this.logsDir = node_path.join(this.happyHomeDir, "logs");
38
+ this.daemonLogsDir = node_path.join(this.happyHomeDir, "logs-daemon");
39
+ this.settingsFile = node_path.join(this.happyHomeDir, "settings.json");
40
+ this.privateKeyFile = node_path.join(this.happyHomeDir, "access.key");
41
+ this.daemonStateFile = node_path.join(this.happyHomeDir, "daemon.state.json");
47
42
  }
48
43
  }
49
- exports.configuration = void 0;
50
- function initializeConfiguration(location) {
51
- exports.configuration = new Configuration(location);
52
- }
44
+ const configuration = new Configuration();
53
45
 
54
46
  function createTimestampForFilename(date = /* @__PURE__ */ new Date()) {
55
47
  return date.toLocaleString("sv-SE", {
@@ -73,17 +65,22 @@ function createTimestampForLogEntry(date = /* @__PURE__ */ new Date()) {
73
65
  });
74
66
  }
75
67
  async function getSessionLogPath() {
76
- if (!node_fs.existsSync(exports.configuration.logsDir)) {
77
- await promises.mkdir(exports.configuration.logsDir, { recursive: true });
68
+ if (!node_fs.existsSync(configuration.logsDir)) {
69
+ await promises.mkdir(configuration.logsDir, { recursive: true });
78
70
  }
79
71
  const timestamp = createTimestampForFilename();
80
- const filename = exports.configuration.isDaemonProcess ? `${timestamp}-daemon.log` : `${timestamp}.log`;
81
- return node_path.join(exports.configuration.logsDir, filename);
72
+ const filename = configuration.isDaemonProcess ? `${timestamp}-daemon.log` : `${timestamp}.log`;
73
+ return node_path.join(configuration.logsDir, filename);
82
74
  }
83
75
  class Logger {
84
76
  constructor(logFilePathPromise = getSessionLogPath()) {
85
77
  this.logFilePathPromise = logFilePathPromise;
78
+ if (process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING && process.env.HAPPY_SERVER_URL) {
79
+ this.dangerouslyUnencryptedServerLoggingUrl = process.env.HAPPY_SERVER_URL;
80
+ console.log(chalk.yellow("[REMOTE LOGGING] Sending logs to server for AI debugging"));
81
+ }
86
82
  }
83
+ dangerouslyUnencryptedServerLoggingUrl;
87
84
  // Use local timezone for simplicity of locating the logs,
88
85
  // in practice you will not need absolute timestamps
89
86
  localTimezoneTimestamp() {
@@ -158,11 +155,38 @@ class Logger {
158
155
  }
159
156
  }
160
157
  }
158
+ async sendToRemoteServer(level, message, ...args) {
159
+ if (!this.dangerouslyUnencryptedServerLoggingUrl) return;
160
+ try {
161
+ await fetch(this.dangerouslyUnencryptedServerLoggingUrl + "/logs-combined-from-cli-and-mobile-for-simple-ai-debugging", {
162
+ method: "POST",
163
+ headers: { "Content-Type": "application/json" },
164
+ body: JSON.stringify({
165
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
166
+ level,
167
+ message: `${message} ${args.map(
168
+ (a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)
169
+ ).join(" ")}`,
170
+ source: "cli",
171
+ platform: process.platform
172
+ })
173
+ });
174
+ } catch (error) {
175
+ }
176
+ }
161
177
  logToFile(prefix, message, ...args) {
162
178
  const logLine = `${prefix} ${message} ${args.map(
163
179
  (arg) => typeof arg === "string" ? arg : JSON.stringify(arg)
164
180
  ).join(" ")}
165
181
  `;
182
+ if (this.dangerouslyUnencryptedServerLoggingUrl) {
183
+ let level = "info";
184
+ if (prefix.includes(this.localTimezoneTimestamp())) {
185
+ level = "debug";
186
+ }
187
+ this.sendToRemoteServer(level, message, ...args).catch(() => {
188
+ });
189
+ }
166
190
  this.logFilePathPromise.then((logFilePath) => {
167
191
  try {
168
192
  fs.appendFileSync(logFilePath, logLine);
@@ -181,16 +205,7 @@ class Logger {
181
205
  });
182
206
  }
183
207
  }
184
- exports.logger = void 0;
185
- function initLoggerWithGlobalConfiguration() {
186
- exports.logger = new Logger();
187
- if (process.env.DEBUG) {
188
- exports.logger.logFilePathPromise.then((logPath) => {
189
- exports.logger.info(chalk.yellow("[DEBUG MODE] Debug logging enabled"));
190
- exports.logger.info(chalk.gray(`Log file: ${logPath}`));
191
- });
192
- }
193
- }
208
+ let logger = new Logger();
194
209
 
195
210
  const SessionMessageContentSchema = z.z.object({
196
211
  c: z.z.string(),
@@ -219,10 +234,26 @@ const UpdateSessionBodySchema = z.z.object({
219
234
  value: z.z.string()
220
235
  }).nullish()
221
236
  });
237
+ const UpdateMachineBodySchema = z.z.object({
238
+ t: z.z.literal("update-machine"),
239
+ machineId: z.z.string(),
240
+ metadata: z.z.object({
241
+ version: z.z.number(),
242
+ value: z.z.string()
243
+ }).nullish(),
244
+ daemonState: z.z.object({
245
+ version: z.z.number(),
246
+ value: z.z.string()
247
+ }).nullish()
248
+ });
222
249
  z.z.object({
223
250
  id: z.z.string(),
224
251
  seq: z.z.number(),
225
- body: z.z.union([UpdateBodySchema, UpdateSessionBodySchema]),
252
+ body: z.z.union([
253
+ UpdateBodySchema,
254
+ UpdateSessionBodySchema,
255
+ UpdateMachineBodySchema
256
+ ]),
226
257
  createdAt: z.z.number()
227
258
  });
228
259
  z.z.object({
@@ -235,6 +266,44 @@ z.z.object({
235
266
  agentState: z.z.any().nullable(),
236
267
  agentStateVersion: z.z.number()
237
268
  });
269
+ z.z.object({
270
+ host: z.z.string(),
271
+ platform: z.z.string(),
272
+ happyCliVersion: z.z.string(),
273
+ homeDir: z.z.string(),
274
+ happyHomeDir: z.z.string()
275
+ });
276
+ z.z.object({
277
+ status: z.z.union([
278
+ z.z.enum(["running", "shutting-down"]),
279
+ z.z.string()
280
+ // Forward compatibility
281
+ ]),
282
+ pid: z.z.number().optional(),
283
+ httpPort: z.z.number().optional(),
284
+ startedAt: z.z.number().optional(),
285
+ shutdownRequestedAt: z.z.number().optional(),
286
+ shutdownSource: z.z.union([
287
+ z.z.enum(["mobile-app", "cli", "os-signal", "unknown"]),
288
+ z.z.string()
289
+ // Forward compatibility
290
+ ]).optional()
291
+ });
292
+ z.z.object({
293
+ id: z.z.string(),
294
+ metadata: z.z.any(),
295
+ // Decrypted MachineMetadata
296
+ metadataVersion: z.z.number(),
297
+ daemonState: z.z.any().nullable(),
298
+ // Decrypted DaemonState
299
+ daemonStateVersion: z.z.number(),
300
+ // We don't really care about these on the CLI for now
301
+ // ApiMachineClient will not sync these
302
+ active: z.z.boolean(),
303
+ activeAt: z.z.number(),
304
+ createdAt: z.z.number(),
305
+ updatedAt: z.z.number()
306
+ });
238
307
  z.z.object({
239
308
  content: SessionMessageContentSchema,
240
309
  createdAt: z.z.number(),
@@ -325,6 +394,7 @@ function decrypt(data, secret) {
325
394
  const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
326
395
  const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
327
396
  if (!decrypted) {
397
+ logger.debug("[ERROR] Decryption failed");
328
398
  return null;
329
399
  }
330
400
  return JSON.parse(new TextDecoder().decode(decrypted));
@@ -415,7 +485,7 @@ class ApiSessionClient extends node_events.EventEmitter {
415
485
  this.metadataVersion = session.metadataVersion;
416
486
  this.agentState = session.agentState;
417
487
  this.agentStateVersion = session.agentStateVersion;
418
- this.socket = socket_ioClient.io(exports.configuration.serverUrl, {
488
+ this.socket = socket_ioClient.io(configuration.serverUrl, {
419
489
  auth: {
420
490
  token: this.token,
421
491
  clientType: "session-scoped",
@@ -431,7 +501,7 @@ class ApiSessionClient extends node_events.EventEmitter {
431
501
  autoConnect: false
432
502
  });
433
503
  this.socket.on("connect", () => {
434
- exports.logger.debug("Socket connected successfully");
504
+ logger.debug("Socket connected successfully");
435
505
  this.reregisterHandlers();
436
506
  });
437
507
  this.socket.on("rpc-request", async (data, callback) => {
@@ -439,7 +509,7 @@ class ApiSessionClient extends node_events.EventEmitter {
439
509
  const method = data.method;
440
510
  const handler = this.rpcHandlers.get(method);
441
511
  if (!handler) {
442
- exports.logger.debug("[SOCKET] [RPC] [ERROR] method not found", { method });
512
+ logger.debug("[SOCKET] [RPC] [ERROR] method not found", { method });
443
513
  const errorResponse = { error: "Method not found" };
444
514
  const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
445
515
  callback(encryptedError);
@@ -450,28 +520,28 @@ class ApiSessionClient extends node_events.EventEmitter {
450
520
  const encryptedResponse = encodeBase64(encrypt(result, this.secret));
451
521
  callback(encryptedResponse);
452
522
  } catch (error) {
453
- exports.logger.debug("[SOCKET] [RPC] [ERROR] Error handling RPC request", { error });
523
+ logger.debug("[SOCKET] [RPC] [ERROR] Error handling RPC request", { error });
454
524
  const errorResponse = { error: error instanceof Error ? error.message : "Unknown error" };
455
525
  const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
456
526
  callback(encryptedError);
457
527
  }
458
528
  });
459
529
  this.socket.on("disconnect", (reason) => {
460
- exports.logger.debug("[API] Socket disconnected:", reason);
530
+ logger.debug("[API] Socket disconnected:", reason);
461
531
  });
462
532
  this.socket.on("connect_error", (error) => {
463
- exports.logger.debug("[API] Socket connection error:", error);
533
+ logger.debug("[API] Socket connection error:", error);
464
534
  });
465
535
  this.socket.on("update", (data) => {
466
536
  try {
467
- exports.logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", data);
537
+ logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", data);
468
538
  if (!data.body) {
469
- exports.logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
539
+ logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
470
540
  return;
471
541
  }
472
542
  if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
473
543
  const body = decrypt(decodeBase64(data.body.message.content.c), this.secret);
474
- exports.logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
544
+ logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
475
545
  const userResult = UserMessageSchema.safeParse(body);
476
546
  if (userResult.success) {
477
547
  if (this.pendingMessageCallback) {
@@ -491,15 +561,17 @@ class ApiSessionClient extends node_events.EventEmitter {
491
561
  this.agentState = data.body.agentState.value ? decrypt(decodeBase64(data.body.agentState.value), this.secret) : null;
492
562
  this.agentStateVersion = data.body.agentState.version;
493
563
  }
564
+ } else if (data.body.t === "update-machine") {
565
+ logger.debug(`[SOCKET] WARNING: Session client received unexpected machine update - ignoring`);
494
566
  } else {
495
567
  this.emit("message", data.body);
496
568
  }
497
569
  } catch (error) {
498
- exports.logger.debug("[SOCKET] [UPDATE] [ERROR] Error handling update", { error });
570
+ logger.debug("[SOCKET] [UPDATE] [ERROR] Error handling update", { error });
499
571
  }
500
572
  });
501
573
  this.socket.on("error", (error) => {
502
- exports.logger.debug("[API] Socket error:", error);
574
+ logger.debug("[API] Socket error:", error);
503
575
  });
504
576
  this.socket.connect();
505
577
  }
@@ -539,7 +611,7 @@ class ApiSessionClient extends node_events.EventEmitter {
539
611
  }
540
612
  };
541
613
  }
542
- exports.logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
614
+ logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
543
615
  const encrypted = encodeBase64(encrypt(content, this.secret));
544
616
  this.socket.emit("message", {
545
617
  sid: this.sessionId,
@@ -549,7 +621,7 @@ class ApiSessionClient extends node_events.EventEmitter {
549
621
  try {
550
622
  this.sendUsageData(body.message.usage);
551
623
  } catch (error) {
552
- exports.logger.debug("[SOCKET] Failed to send usage data:", error);
624
+ logger.debug("[SOCKET] Failed to send usage data:", error);
553
625
  }
554
626
  }
555
627
  if (body.type === "summary" && "summary" in body && "leafUuid" in body) {
@@ -617,7 +689,7 @@ class ApiSessionClient extends node_events.EventEmitter {
617
689
  output: 0
618
690
  }
619
691
  };
620
- exports.logger.debugLargeJson("[SOCKET] Sending usage data:", usageReport);
692
+ logger.debugLargeJson("[SOCKET] Sending usage data:", usageReport);
621
693
  this.socket.emit("usage-report", usageReport);
622
694
  }
623
695
  /**
@@ -647,7 +719,7 @@ class ApiSessionClient extends node_events.EventEmitter {
647
719
  * @param handler - Handler function that returns the updated agent state
648
720
  */
649
721
  updateAgentState(handler) {
650
- exports.logger.debugLargeJson("Updating agent state", this.agentState);
722
+ logger.debugLargeJson("Updating agent state", this.agentState);
651
723
  this.agentStateLock.inLock(async () => {
652
724
  await backoff(async () => {
653
725
  let updated = handler(this.agentState || {});
@@ -655,7 +727,7 @@ class ApiSessionClient extends node_events.EventEmitter {
655
727
  if (answer.result === "success") {
656
728
  this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
657
729
  this.agentStateVersion = answer.version;
658
- exports.logger.debug("Agent state updated", this.agentState);
730
+ logger.debug("Agent state updated", this.agentState);
659
731
  } else if (answer.result === "version-mismatch") {
660
732
  if (answer.version > this.agentStateVersion) {
661
733
  this.agentStateVersion = answer.version;
@@ -675,18 +747,18 @@ class ApiSessionClient extends node_events.EventEmitter {
675
747
  const prefixedMethod = `${this.sessionId}:${method}`;
676
748
  this.rpcHandlers.set(prefixedMethod, handler);
677
749
  this.socket.emit("rpc-register", { method: prefixedMethod });
678
- exports.logger.debug("Registered RPC handler", { method, prefixedMethod });
750
+ logger.debug("Registered RPC handler", { method, prefixedMethod });
679
751
  }
680
752
  /**
681
753
  * Re-register all RPC handlers after reconnection
682
754
  */
683
755
  reregisterHandlers() {
684
- exports.logger.debug("Re-registering RPC handlers after reconnection", {
756
+ logger.debug("Re-registering RPC handlers after reconnection", {
685
757
  totalMethods: this.rpcHandlers.size
686
758
  });
687
759
  for (const [prefixedMethod] of this.rpcHandlers) {
688
760
  this.socket.emit("rpc-register", { method: prefixedMethod });
689
- exports.logger.debug("Re-registered method", { prefixedMethod });
761
+ logger.debug("Re-registered method", { prefixedMethod });
690
762
  }
691
763
  }
692
764
  /**
@@ -710,6 +782,243 @@ class ApiSessionClient extends node_events.EventEmitter {
710
782
  }
711
783
  }
712
784
 
785
+ class ApiMachineClient {
786
+ constructor(token, secret, machine) {
787
+ this.token = token;
788
+ this.secret = secret;
789
+ this.machine = machine;
790
+ }
791
+ socket;
792
+ keepAliveInterval = null;
793
+ // RPC handlers
794
+ spawnSession;
795
+ stopSession;
796
+ requestShutdown;
797
+ setRPCHandlers({
798
+ spawnSession,
799
+ stopSession,
800
+ requestShutdown
801
+ }) {
802
+ this.spawnSession = spawnSession;
803
+ this.stopSession = stopSession;
804
+ this.requestShutdown = requestShutdown;
805
+ }
806
+ /**
807
+ * Update machine metadata
808
+ * Currently unused, changes from the mobile client are more likely
809
+ * for example to set a custom name.
810
+ */
811
+ async updateMachineMetadata(handler) {
812
+ await backoff(async () => {
813
+ const updated = handler(this.machine.metadata);
814
+ const answer = await this.socket.emitWithAck("machine-update-metadata", {
815
+ machineId: this.machine.id,
816
+ metadata: encodeBase64(encrypt(updated, this.secret)),
817
+ expectedVersion: this.machine.metadataVersion
818
+ });
819
+ if (answer.result === "success") {
820
+ this.machine.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
821
+ this.machine.metadataVersion = answer.version;
822
+ logger.debug("[API MACHINE] Metadata updated successfully");
823
+ } else if (answer.result === "version-mismatch") {
824
+ if (answer.version > this.machine.metadataVersion) {
825
+ this.machine.metadataVersion = answer.version;
826
+ this.machine.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
827
+ }
828
+ throw new Error("Metadata version mismatch");
829
+ }
830
+ });
831
+ }
832
+ /**
833
+ * Update daemon state (runtime info) - similar to session updateAgentState
834
+ * Simplified without lock - relies on backoff for retry
835
+ */
836
+ async updateDaemonState(handler) {
837
+ await backoff(async () => {
838
+ const updated = handler(this.machine.daemonState);
839
+ const answer = await this.socket.emitWithAck("machine-update-state", {
840
+ machineId: this.machine.id,
841
+ daemonState: encodeBase64(encrypt(updated, this.secret)),
842
+ expectedVersion: this.machine.daemonStateVersion
843
+ });
844
+ if (answer.result === "success") {
845
+ this.machine.daemonState = decrypt(decodeBase64(answer.daemonState), this.secret);
846
+ this.machine.daemonStateVersion = answer.version;
847
+ logger.debug("[API MACHINE] Daemon state updated successfully");
848
+ } else if (answer.result === "version-mismatch") {
849
+ if (answer.version > this.machine.daemonStateVersion) {
850
+ this.machine.daemonStateVersion = answer.version;
851
+ this.machine.daemonState = decrypt(decodeBase64(answer.daemonState), this.secret);
852
+ }
853
+ throw new Error("Daemon state version mismatch");
854
+ }
855
+ });
856
+ }
857
+ connect() {
858
+ const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
859
+ logger.debug(`[API MACHINE] Connecting to ${serverUrl}`);
860
+ this.socket = socket_ioClient.io(serverUrl, {
861
+ transports: ["websocket"],
862
+ auth: {
863
+ token: this.token,
864
+ clientType: "machine-scoped",
865
+ machineId: this.machine.id
866
+ },
867
+ path: "/v1/updates",
868
+ reconnection: true,
869
+ reconnectionDelay: 1e3,
870
+ reconnectionDelayMax: 5e3
871
+ });
872
+ const spawnMethod = `${this.machine.id}:spawn-happy-session`;
873
+ const stopMethod = `${this.machine.id}:stop-session`;
874
+ const stopDaemonMethod = `${this.machine.id}:stop-daemon`;
875
+ this.socket.on("connect", () => {
876
+ logger.debug("[API MACHINE] Connected to server");
877
+ this.updateDaemonState((state) => ({
878
+ ...state,
879
+ status: "running",
880
+ pid: process.pid,
881
+ httpPort: this.machine.daemonState?.httpPort,
882
+ startedAt: Date.now()
883
+ }));
884
+ this.socket.emit("rpc-register", { method: spawnMethod });
885
+ this.socket.emit("rpc-register", { method: stopMethod });
886
+ this.socket.emit("rpc-register", { method: stopDaemonMethod });
887
+ logger.debug(`[API MACHINE] Registered RPC methods: ${spawnMethod}, ${stopMethod}, ${stopDaemonMethod}`);
888
+ this.startKeepAlive();
889
+ });
890
+ this.socket.on("rpc-request", async (data, callback) => {
891
+ logger.debugLargeJson(`[API MACHINE] Received RPC request:`, data);
892
+ try {
893
+ const spawnMethod2 = `${this.machine.id}:spawn-happy-session`;
894
+ const stopMethod2 = `${this.machine.id}:stop-session`;
895
+ const stopDaemonMethod2 = `${this.machine.id}:stop-daemon`;
896
+ if (data.method === spawnMethod2) {
897
+ if (!this.spawnSession) {
898
+ throw new Error("Spawn session handler not set");
899
+ }
900
+ const { directory, sessionId } = decrypt(decodeBase64(data.params), this.secret) || {};
901
+ if (!directory) {
902
+ throw new Error("Directory is required");
903
+ }
904
+ const session = await this.spawnSession(directory, sessionId);
905
+ if (!session) {
906
+ throw new Error("Failed to spawn session");
907
+ }
908
+ logger.debug(`[API MACHINE] Spawned session ${session.happySessionId || "pending"} with PID ${session.pid}`);
909
+ if (!session.happySessionId) {
910
+ throw new Error(`Session spawned (PID ${session.pid}) but no sessionId received from webhook. The session process may still be initializing.`);
911
+ }
912
+ const response = { sessionId: session.happySessionId };
913
+ logger.debug(`[API MACHINE] Sending RPC response:`, response);
914
+ callback(encodeBase64(encrypt(response, this.secret)));
915
+ return;
916
+ }
917
+ if (data.method === stopMethod2) {
918
+ logger.debug("[API MACHINE] Received stop-session RPC request");
919
+ const decryptedParams = decrypt(decodeBase64(data.params), this.secret);
920
+ const { sessionId } = decryptedParams || {};
921
+ if (!this.stopSession) {
922
+ throw new Error("Stop session handler not set");
923
+ }
924
+ if (!sessionId) {
925
+ throw new Error("Session ID is required");
926
+ }
927
+ const success = this.stopSession(sessionId);
928
+ if (!success) {
929
+ throw new Error("Session not found or failed to stop");
930
+ }
931
+ logger.debug(`[API MACHINE] Stopped session ${sessionId}`);
932
+ const response = { message: "Session stopped" };
933
+ const encryptedResponse = encodeBase64(encrypt(response, this.secret));
934
+ callback(encryptedResponse);
935
+ return;
936
+ }
937
+ if (data.method === stopDaemonMethod2) {
938
+ logger.debug("[API MACHINE] Received stop-daemon RPC request");
939
+ callback(encodeBase64(encrypt({
940
+ message: "Daemon stop request acknowledged, starting shutdown sequence..."
941
+ }, this.secret)));
942
+ setTimeout(() => {
943
+ logger.debug("[API MACHINE] Initiating daemon shutdown from RPC");
944
+ if (this.requestShutdown) {
945
+ this.requestShutdown();
946
+ }
947
+ }, 100);
948
+ return;
949
+ }
950
+ throw new Error(`Unknown RPC method: ${data.method}`);
951
+ } catch (error) {
952
+ logger.debug(`[API MACHINE] RPC handler failed:`, error.message || error);
953
+ logger.debug(`[API MACHINE] Error stack:`, error.stack);
954
+ callback(encodeBase64(encrypt({ error: error.message || String(error) }, this.secret)));
955
+ }
956
+ });
957
+ this.socket.on("update", (data) => {
958
+ if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
959
+ const update = data.body;
960
+ if (update.metadata) {
961
+ logger.debug("[API MACHINE] Received external metadata update");
962
+ this.machine.metadata = decrypt(decodeBase64(update.metadata.value), this.secret);
963
+ this.machine.metadataVersion = update.metadata.version;
964
+ }
965
+ if (update.daemonState) {
966
+ logger.debug("[API MACHINE] Received external daemon state update");
967
+ this.machine.daemonState = decrypt(decodeBase64(update.daemonState.value), this.secret);
968
+ this.machine.daemonStateVersion = update.daemonState.version;
969
+ }
970
+ } else {
971
+ logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
972
+ }
973
+ });
974
+ this.socket.on("disconnect", () => {
975
+ logger.debug("[API MACHINE] Disconnected from server");
976
+ this.stopKeepAlive();
977
+ });
978
+ this.socket.io.on("reconnect", () => {
979
+ logger.debug("[API MACHINE] Reconnected to server");
980
+ this.socket.emit("rpc-register", { method: spawnMethod });
981
+ this.socket.emit("rpc-register", { method: stopMethod });
982
+ this.socket.emit("rpc-register", { method: stopDaemonMethod });
983
+ });
984
+ this.socket.on("connect_error", (error) => {
985
+ logger.debug(`[API MACHINE] Connection error: ${error.message}`);
986
+ });
987
+ this.socket.io.on("error", (error) => {
988
+ logger.debug("[API MACHINE] Socket error:", error);
989
+ });
990
+ }
991
+ startKeepAlive() {
992
+ this.stopKeepAlive();
993
+ this.keepAliveInterval = setInterval(() => {
994
+ const payload = {
995
+ machineId: this.machine.id,
996
+ time: Date.now()
997
+ };
998
+ if (process.env.VERBOSE) {
999
+ logger.debugLargeJson(`[API MACHINE] Emitting machine-alive`, payload);
1000
+ }
1001
+ this.socket.emit("machine-alive", payload);
1002
+ }, 2e4);
1003
+ logger.debug("[API MACHINE] Keep-alive started (20s interval)");
1004
+ }
1005
+ stopKeepAlive() {
1006
+ if (this.keepAliveInterval) {
1007
+ clearInterval(this.keepAliveInterval);
1008
+ this.keepAliveInterval = null;
1009
+ logger.debug("[API MACHINE] Keep-alive stopped");
1010
+ }
1011
+ }
1012
+ shutdown() {
1013
+ logger.debug("[API MACHINE] Shutting down");
1014
+ this.stopKeepAlive();
1015
+ if (this.socket) {
1016
+ this.socket.close();
1017
+ logger.debug("[API MACHINE] Socket closed");
1018
+ }
1019
+ }
1020
+ }
1021
+
713
1022
  class PushNotificationClient {
714
1023
  token;
715
1024
  baseUrl;
@@ -733,13 +1042,13 @@ class PushNotificationClient {
733
1042
  }
734
1043
  }
735
1044
  );
736
- exports.logger.debug(`Fetched ${response.data.tokens.length} push tokens`);
1045
+ logger.debug(`Fetched ${response.data.tokens.length} push tokens`);
737
1046
  response.data.tokens.forEach((token, index) => {
738
- exports.logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, token=${token.token}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
1047
+ logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, token=${token.token}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
739
1048
  });
740
1049
  return response.data.tokens;
741
1050
  } catch (error) {
742
- exports.logger.debug("[PUSH] [ERROR] Failed to fetch push tokens:", error);
1051
+ logger.debug("[PUSH] [ERROR] Failed to fetch push tokens:", error);
743
1052
  throw new Error(`Failed to fetch push tokens: ${error instanceof Error ? error.message : "Unknown error"}`);
744
1053
  }
745
1054
  }
@@ -748,7 +1057,7 @@ class PushNotificationClient {
748
1057
  * @param messages - Array of push messages to send
749
1058
  */
750
1059
  async sendPushNotifications(messages) {
751
- exports.logger.debug(`Sending ${messages.length} push notifications`);
1060
+ logger.debug(`Sending ${messages.length} push notifications`);
752
1061
  const validMessages = messages.filter((message) => {
753
1062
  if (Array.isArray(message.to)) {
754
1063
  return message.to.every((token) => expoServerSdk.Expo.isExpoPushToken(token));
@@ -756,7 +1065,7 @@ class PushNotificationClient {
756
1065
  return expoServerSdk.Expo.isExpoPushToken(message.to);
757
1066
  });
758
1067
  if (validMessages.length === 0) {
759
- exports.logger.debug("No valid Expo push tokens found");
1068
+ logger.debug("No valid Expo push tokens found");
760
1069
  return;
761
1070
  }
762
1071
  const chunks = this.expo.chunkPushNotifications(validMessages);
@@ -770,7 +1079,7 @@ class PushNotificationClient {
770
1079
  const errors = ticketChunk.filter((ticket) => ticket.status === "error");
771
1080
  if (errors.length > 0) {
772
1081
  const errorDetails = errors.map((e) => ({ message: e.message, details: e.details }));
773
- exports.logger.debug("[PUSH] Some notifications failed:", errorDetails);
1082
+ logger.debug("[PUSH] Some notifications failed:", errorDetails);
774
1083
  }
775
1084
  if (errors.length === ticketChunk.length) {
776
1085
  throw new Error("All push notifications in chunk failed");
@@ -779,7 +1088,7 @@ class PushNotificationClient {
779
1088
  } catch (error) {
780
1089
  const elapsed = Date.now() - startTime;
781
1090
  if (elapsed >= timeout) {
782
- exports.logger.debug("[PUSH] Timeout reached after 5 minutes, giving up on chunk");
1091
+ logger.debug("[PUSH] Timeout reached after 5 minutes, giving up on chunk");
783
1092
  break;
784
1093
  }
785
1094
  attempt++;
@@ -787,13 +1096,13 @@ class PushNotificationClient {
787
1096
  const remainingTime = timeout - elapsed;
788
1097
  const waitTime = Math.min(delay, remainingTime);
789
1098
  if (waitTime > 0) {
790
- exports.logger.debug(`[PUSH] Retrying in ${waitTime}ms (attempt ${attempt})`);
1099
+ logger.debug(`[PUSH] Retrying in ${waitTime}ms (attempt ${attempt})`);
791
1100
  await new Promise((resolve) => setTimeout(resolve, waitTime));
792
1101
  }
793
1102
  }
794
1103
  }
795
1104
  }
796
- exports.logger.debug(`Push notifications sent successfully`);
1105
+ logger.debug(`Push notifications sent successfully`);
797
1106
  }
798
1107
  /**
799
1108
  * Send a push notification to all registered devices for the user
@@ -802,21 +1111,21 @@ class PushNotificationClient {
802
1111
  * @param data - Additional data to send with the notification
803
1112
  */
804
1113
  sendToAllDevices(title, body, data) {
805
- exports.logger.debug(`[PUSH] sendToAllDevices called with title: "${title}", body: "${body}"`);
1114
+ logger.debug(`[PUSH] sendToAllDevices called with title: "${title}", body: "${body}"`);
806
1115
  (async () => {
807
1116
  try {
808
- exports.logger.debug("[PUSH] Fetching push tokens...");
1117
+ logger.debug("[PUSH] Fetching push tokens...");
809
1118
  const tokens = await this.fetchPushTokens();
810
- exports.logger.debug(`[PUSH] Fetched ${tokens.length} push tokens`);
1119
+ logger.debug(`[PUSH] Fetched ${tokens.length} push tokens`);
811
1120
  tokens.forEach((token, index) => {
812
- exports.logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}, token=${token.token}`);
1121
+ logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}, token=${token.token}`);
813
1122
  });
814
1123
  if (tokens.length === 0) {
815
- exports.logger.debug("No push tokens found for user");
1124
+ logger.debug("No push tokens found for user");
816
1125
  return;
817
1126
  }
818
1127
  const messages = tokens.map((token, index) => {
819
- exports.logger.debug(`[PUSH] Creating message ${index + 1} for token: ${token.token}`);
1128
+ logger.debug(`[PUSH] Creating message ${index + 1} for token: ${token.token}`);
820
1129
  return {
821
1130
  to: token.token,
822
1131
  title,
@@ -826,11 +1135,11 @@ class PushNotificationClient {
826
1135
  priority: "high"
827
1136
  };
828
1137
  });
829
- exports.logger.debug(`[PUSH] Sending ${messages.length} push notifications...`);
1138
+ logger.debug(`[PUSH] Sending ${messages.length} push notifications...`);
830
1139
  await this.sendPushNotifications(messages);
831
- exports.logger.debug("[PUSH] Push notifications sent successfully");
1140
+ logger.debug("[PUSH] Push notifications sent successfully");
832
1141
  } catch (error) {
833
- exports.logger.debug("[PUSH] Error sending to all devices:", error);
1142
+ logger.debug("[PUSH] Error sending to all devices:", error);
834
1143
  }
835
1144
  })();
836
1145
  }
@@ -851,7 +1160,7 @@ class ApiClient {
851
1160
  async getOrCreateSession(opts) {
852
1161
  try {
853
1162
  const response = await axios.post(
854
- `${exports.configuration.serverUrl}/v1/sessions`,
1163
+ `${configuration.serverUrl}/v1/sessions`,
855
1164
  {
856
1165
  tag: opts.tag,
857
1166
  metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
@@ -861,10 +1170,12 @@ class ApiClient {
861
1170
  headers: {
862
1171
  "Authorization": `Bearer ${this.token}`,
863
1172
  "Content-Type": "application/json"
864
- }
1173
+ },
1174
+ timeout: 5e3
1175
+ // 5 second timeout
865
1176
  }
866
1177
  );
867
- exports.logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
1178
+ logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
868
1179
  let raw = response.data.session;
869
1180
  let session = {
870
1181
  id: raw.id,
@@ -878,22 +1189,81 @@ class ApiClient {
878
1189
  };
879
1190
  return session;
880
1191
  } catch (error) {
881
- exports.logger.debug("[API] [ERROR] Failed to get or create session:", error);
1192
+ logger.debug("[API] [ERROR] Failed to get or create session:", error);
882
1193
  throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
883
1194
  }
884
1195
  }
885
1196
  /**
886
- * Start realtime session client
887
- * @param id - Session ID
888
- * @returns Session client
1197
+ * Get machine by ID from the server
1198
+ * Returns the current machine state from the server with decrypted metadata and daemonState
889
1199
  */
890
- session(session) {
891
- return new ApiSessionClient(this.token, this.secret, session);
1200
+ async getMachine(machineId) {
1201
+ const response = await axios.get(`${configuration.serverUrl}/v1/machines/${machineId}`, {
1202
+ headers: {
1203
+ "Authorization": `Bearer ${this.token}`,
1204
+ "Content-Type": "application/json"
1205
+ },
1206
+ timeout: 2e3
1207
+ });
1208
+ const raw = response.data.machine;
1209
+ if (!raw) {
1210
+ return null;
1211
+ }
1212
+ logger.debug(`[API] Machine ${machineId} fetched from server`);
1213
+ const machine = {
1214
+ id: raw.id,
1215
+ metadata: raw.metadata ? decrypt(decodeBase64(raw.metadata), this.secret) : null,
1216
+ metadataVersion: raw.metadataVersion || 0,
1217
+ daemonState: raw.daemonState ? decrypt(decodeBase64(raw.daemonState), this.secret) : null,
1218
+ daemonStateVersion: raw.daemonStateVersion || 0,
1219
+ active: raw.active,
1220
+ activeAt: raw.activeAt,
1221
+ createdAt: raw.createdAt,
1222
+ updatedAt: raw.updatedAt
1223
+ };
1224
+ return machine;
892
1225
  }
893
1226
  /**
894
- * Get push notification client
895
- * @returns Push notification client
1227
+ * Register or update machine with the server
1228
+ * Returns the current machine state from the server with decrypted metadata and daemonState
896
1229
  */
1230
+ async createOrReturnExistingAsIs(opts) {
1231
+ const response = await axios.post(
1232
+ `${configuration.serverUrl}/v1/machines`,
1233
+ {
1234
+ id: opts.machineId,
1235
+ metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
1236
+ daemonState: opts.daemonState ? encodeBase64(encrypt(opts.daemonState, this.secret)) : void 0
1237
+ },
1238
+ {
1239
+ headers: {
1240
+ "Authorization": `Bearer ${this.token}`,
1241
+ "Content-Type": "application/json"
1242
+ },
1243
+ timeout: 5e3
1244
+ }
1245
+ );
1246
+ const raw = response.data.machine;
1247
+ logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
1248
+ const machine = {
1249
+ id: raw.id,
1250
+ metadata: raw.metadata ? decrypt(decodeBase64(raw.metadata), this.secret) : null,
1251
+ metadataVersion: raw.metadataVersion || 0,
1252
+ daemonState: raw.daemonState ? decrypt(decodeBase64(raw.daemonState), this.secret) : null,
1253
+ daemonStateVersion: raw.daemonStateVersion || 0,
1254
+ active: raw.active,
1255
+ activeAt: raw.activeAt,
1256
+ createdAt: raw.createdAt,
1257
+ updatedAt: raw.updatedAt
1258
+ };
1259
+ return machine;
1260
+ }
1261
+ sessionSyncClient(session) {
1262
+ return new ApiSessionClient(this.token, this.secret, session);
1263
+ }
1264
+ machineSyncClient(machine) {
1265
+ return new ApiMachineClient(this.token, this.secret, machine);
1266
+ }
897
1267
  push() {
898
1268
  return this.pushClient;
899
1269
  }
@@ -951,10 +1321,9 @@ exports.ApiClient = ApiClient;
951
1321
  exports.ApiSessionClient = ApiSessionClient;
952
1322
  exports.RawJSONLinesSchema = RawJSONLinesSchema;
953
1323
  exports.backoff = backoff;
1324
+ exports.configuration = configuration;
954
1325
  exports.decodeBase64 = decodeBase64;
955
1326
  exports.delay = delay;
956
1327
  exports.encodeBase64 = encodeBase64;
957
1328
  exports.encodeBase64Url = encodeBase64Url;
958
- exports.encrypt = encrypt;
959
- exports.initLoggerWithGlobalConfiguration = initLoggerWithGlobalConfiguration;
960
- exports.initializeConfiguration = initializeConfiguration;
1329
+ exports.logger = logger;