@kmmao/happy-agent 0.2.2 → 0.3.0

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 CHANGED
@@ -10,14 +10,22 @@ var axios = require('axios');
10
10
  var qrcode = require('qrcode-terminal');
11
11
  var node_events = require('node:events');
12
12
  var socket_ioClient = require('socket.io-client');
13
+ var child_process = require('child_process');
14
+ var util = require('util');
15
+ var promises = require('fs/promises');
16
+ var crypto = require('crypto');
17
+ var path = require('path');
18
+ var fs = require('fs');
19
+ var os = require('os');
13
20
 
14
- var version = "0.2.2";
21
+ var version = "0.3.0";
15
22
 
16
23
  function loadConfig() {
17
- const serverUrl = (process.env.HAPPY_SERVER_URL ?? "https://api.cluster-fluster.com").replace(/\/+$/, "");
24
+ const serverUrl = (process.env.HAPPY_SERVER_URL ?? "https://happyserve.xycloud.info").replace(/\/+$/, "");
25
+ const webappUrl = (process.env.HAPPY_WEBAPP_URL ?? "https://happyapp.xycloud.info").replace(/\/+$/, "");
18
26
  const homeDir = process.env.HAPPY_HOME_DIR ?? node_path.join(node_os.homedir(), ".happy");
19
27
  const credentialPath = node_path.join(homeDir, "agent.key");
20
- return { serverUrl, homeDir, credentialPath };
28
+ return { serverUrl, webappUrl, homeDir, credentialPath };
21
29
  }
22
30
 
23
31
  function encodeBase64(buffer) {
@@ -184,7 +192,7 @@ function requireCredentials(config) {
184
192
 
185
193
  const POLL_INTERVAL_MS = 1e3;
186
194
  const AUTH_TIMEOUT_MS = 12e4;
187
- async function authLogin(config) {
195
+ async function authLogin(config, opts) {
188
196
  const seed = getRandomBytes(32);
189
197
  const keypair = tweetnacl.box.keyPair.fromSecretKey(seed);
190
198
  const publicKeyBase64 = encodeBase64(keypair.publicKey);
@@ -198,15 +206,25 @@ async function authLogin(config) {
198
206
  }
199
207
  throw err;
200
208
  }
201
- const qrData = `happy:///account?${encodeBase64Url(keypair.publicKey)}`;
202
- console.log("");
203
- qrcode.generate(qrData, { small: true }, (code) => {
204
- console.log(code);
205
- });
206
- console.log("## Authentication");
207
- console.log("- Action: Scan this QR code with the Happy app");
208
- console.log("- Path: Settings -> Account -> Link New Device");
209
- console.log("");
209
+ if (opts?.web) {
210
+ const publicKeyBase64Url = encodeBase64Url(keypair.publicKey);
211
+ const webUrl = `${config.webappUrl}/terminal/connect#key=${publicKeyBase64Url}`;
212
+ console.log("");
213
+ console.log("## Authentication (Web)");
214
+ console.log(`- Open this URL in your browser: ${webUrl}`);
215
+ console.log("- Then sign in with your Happy account to complete linking.");
216
+ console.log("");
217
+ } else {
218
+ const qrData = `happy:///account?${encodeBase64Url(keypair.publicKey)}`;
219
+ console.log("");
220
+ qrcode.generate(qrData, { small: true }, (code) => {
221
+ console.log(code);
222
+ });
223
+ console.log("## Authentication");
224
+ console.log("- Action: Scan this QR code with the Happy app");
225
+ console.log("- Path: Settings -> Account -> Link New Device");
226
+ console.log("");
227
+ }
210
228
  const startTime = Date.now();
211
229
  while (Date.now() - startTime < AUTH_TIMEOUT_MS) {
212
230
  await sleep(POLL_INTERVAL_MS);
@@ -390,9 +408,7 @@ async function deleteSession(config, creds, sessionId) {
390
408
  try {
391
409
  await axios.delete(
392
410
  `${config.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
393
- {
394
- headers: authHeaders(creds)
395
- }
411
+ { headers: authHeaders(creds) }
396
412
  );
397
413
  } catch (err) {
398
414
  handleApiError(err, `deleting session ${sessionId}`);
@@ -418,9 +434,602 @@ async function getSessionMessages(config, creds, sessionId, encryption) {
418
434
  updatedAt: msg.updatedAt
419
435
  }));
420
436
  }
437
+ async function fetchMessagesAfterSeq(config, creds, sessionId, encryption, afterSeq, limit = 100) {
438
+ let data;
439
+ try {
440
+ const resp = await axios.get(
441
+ `${config.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
442
+ {
443
+ params: { after_seq: afterSeq, limit },
444
+ headers: authHeaders(creds),
445
+ timeout: 6e4
446
+ }
447
+ );
448
+ data = resp.data;
449
+ } catch (err) {
450
+ handleApiError(err, `fetching v3 messages for session ${sessionId}`);
451
+ }
452
+ const messages = data.messages.map((msg) => {
453
+ const content = decryptField(msg.content.c, encryption);
454
+ if (content === null) return null;
455
+ return {
456
+ id: msg.id,
457
+ seq: msg.seq,
458
+ content,
459
+ localId: msg.localId ?? null,
460
+ createdAt: msg.createdAt,
461
+ updatedAt: msg.updatedAt
462
+ };
463
+ }).filter((m) => m !== null);
464
+ return { messages, hasMore: data.hasMore };
465
+ }
466
+ async function sendMessagesBatch(config, creds, sessionId, encryption, contents) {
467
+ const messages = contents.map((content) => ({
468
+ content: encodeBase64(
469
+ encrypt(encryption.key, encryption.variant, content)
470
+ )
471
+ }));
472
+ let data;
473
+ try {
474
+ const resp = await axios.post(
475
+ `${config.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
476
+ { messages },
477
+ { headers: authHeaders(creds), timeout: 6e4 }
478
+ );
479
+ data = resp.data;
480
+ } catch (err) {
481
+ handleApiError(err, `sending v3 messages to session ${sessionId}`);
482
+ }
483
+ return data;
484
+ }
485
+ async function getOrCreateMachine(config, creds, metadata) {
486
+ const sessionKey = getRandomBytes(32);
487
+ const encryptedKey = libsodiumEncryptForPublicKey(
488
+ sessionKey,
489
+ creds.contentKeyPair.publicKey
490
+ );
491
+ const withVersion = new Uint8Array(1 + encryptedKey.length);
492
+ withVersion[0] = 0;
493
+ withVersion.set(encryptedKey, 1);
494
+ const encryptedMetadata = encryptWithDataKey(metadata, sessionKey);
495
+ let data;
496
+ try {
497
+ const resp = await axios.post(
498
+ `${config.serverUrl}/v2/machines`,
499
+ {
500
+ metadata: encodeBase64(encryptedMetadata),
501
+ dataEncryptionKey: encodeBase64(withVersion)
502
+ },
503
+ { headers: authHeaders(creds) }
504
+ );
505
+ data = resp.data;
506
+ } catch (err) {
507
+ handleApiError(err, "registering machine");
508
+ }
509
+ const raw = data.machine;
510
+ let encKey;
511
+ let encVariant;
512
+ if (raw.dataEncryptionKey) {
513
+ const encrypted = decodeBase64(raw.dataEncryptionKey);
514
+ const bundle = encrypted.slice(1);
515
+ const key = decryptBoxBundle(bundle, creds.contentKeyPair.secretKey);
516
+ if (!key) throw new Error("Failed to decrypt machine encryption key");
517
+ encKey = key;
518
+ encVariant = "dataKey";
519
+ } else {
520
+ encKey = creds.secret;
521
+ encVariant = "legacy";
522
+ }
523
+ return {
524
+ id: raw.id,
525
+ encryptionKey: encKey,
526
+ encryptionVariant: encVariant,
527
+ metadata: decryptField(raw.metadata, { key: encKey, variant: encVariant }) ?? metadata,
528
+ metadataVersion: raw.metadataVersion,
529
+ daemonState: raw.daemonState ? decryptField(raw.daemonState, { key: encKey, variant: encVariant }) : null,
530
+ daemonStateVersion: raw.daemonStateVersion
531
+ };
532
+ }
533
+ async function listMachines(config, creds) {
534
+ let data;
535
+ try {
536
+ const resp = await axios.get(`${config.serverUrl}/v2/machines`, {
537
+ headers: authHeaders(creds)
538
+ });
539
+ data = resp.data;
540
+ } catch (err) {
541
+ handleApiError(err, "listing machines");
542
+ }
543
+ return data.machines;
544
+ }
545
+
546
+ function timestamp() {
547
+ return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", {
548
+ hour12: false,
549
+ hour: "2-digit",
550
+ minute: "2-digit",
551
+ second: "2-digit",
552
+ fractionalSecondDigits: 3
553
+ });
554
+ }
555
+ function filenameTimestamp() {
556
+ return (/* @__PURE__ */ new Date()).toLocaleString("sv-SE", {
557
+ year: "numeric",
558
+ month: "2-digit",
559
+ day: "2-digit",
560
+ hour: "2-digit",
561
+ minute: "2-digit",
562
+ second: "2-digit"
563
+ }).replace(/[: ]/g, "-").replace(/,/g, "");
564
+ }
565
+ function resolveLogPath() {
566
+ const config = loadConfig();
567
+ const logsDir = node_path.join(config.homeDir, "logs");
568
+ node_fs.mkdirSync(logsDir, { recursive: true });
569
+ return node_path.join(logsDir, `agent-${filenameTimestamp()}-pid-${process.pid}.log`);
570
+ }
571
+ class Logger {
572
+ logFilePath;
573
+ constructor() {
574
+ this.logFilePath = resolveLogPath();
575
+ }
576
+ debug(message, ...args) {
577
+ this.write("DEBUG", message, ...args);
578
+ }
579
+ warn(message, ...args) {
580
+ this.write("WARN", message, ...args);
581
+ }
582
+ error(message, ...args) {
583
+ this.write("ERROR", message, ...args);
584
+ }
585
+ write(level, message, ...args) {
586
+ const extra = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
587
+ const line = `[${timestamp()}] [${level}] ${message}${extra ? " " + extra : ""}
588
+ `;
589
+ try {
590
+ node_fs.appendFileSync(this.logFilePath, line);
591
+ } catch {
592
+ }
593
+ }
594
+ }
595
+ const logger = new Logger();
596
+
597
+ async function withBackoff(fn, options) {
598
+ const maxRetries = options?.maxRetries ?? 3;
599
+ const baseDelay = options?.baseDelayMs ?? 200;
600
+ const label = options?.label ?? "operation";
601
+ let lastError;
602
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
603
+ try {
604
+ return await fn();
605
+ } catch (error) {
606
+ lastError = error;
607
+ if (attempt < maxRetries) {
608
+ const delay = baseDelay * 2 ** attempt;
609
+ logger.debug(`[backoff] ${label} attempt ${attempt + 1} failed, retrying in ${delay}ms`);
610
+ await new Promise((resolve) => setTimeout(resolve, delay));
611
+ }
612
+ }
613
+ }
614
+ throw lastError;
615
+ }
616
+
617
+ class RpcHandlerManager {
618
+ handlers = /* @__PURE__ */ new Map();
619
+ scopePrefix;
620
+ encryptionKey;
621
+ encryptionVariant;
622
+ logger;
623
+ socket = null;
624
+ reregisterInterval = null;
625
+ constructor(config) {
626
+ this.scopePrefix = config.scopePrefix;
627
+ this.encryptionKey = config.encryptionKey;
628
+ this.encryptionVariant = config.encryptionVariant;
629
+ this.logger = config.logger ?? ((msg, data) => logger.debug(msg, data));
630
+ }
631
+ /**
632
+ * Register an RPC handler for a specific method
633
+ */
634
+ registerHandler(method, handler) {
635
+ const prefixedMethod = this.getPrefixedMethod(method);
636
+ this.handlers.set(prefixedMethod, handler);
637
+ if (this.socket) {
638
+ this.emitRegisterWithRetry(this.socket, prefixedMethod);
639
+ }
640
+ }
641
+ /**
642
+ * Handle an incoming RPC request
643
+ */
644
+ async handleRequest(request) {
645
+ try {
646
+ const handler = this.handlers.get(request.method);
647
+ if (!handler) {
648
+ this.logger("[RPC] [ERROR] Method not found", {
649
+ method: request.method
650
+ });
651
+ const errorResponse = { error: "Method not found" };
652
+ return encodeBase64(
653
+ encrypt(this.encryptionKey, this.encryptionVariant, errorResponse)
654
+ );
655
+ }
656
+ const decryptedParams = decrypt(
657
+ this.encryptionKey,
658
+ this.encryptionVariant,
659
+ decodeBase64(request.params)
660
+ );
661
+ this.logger("[RPC] Calling handler", { method: request.method });
662
+ const result = await handler(decryptedParams);
663
+ this.logger("[RPC] Handler returned", {
664
+ method: request.method,
665
+ hasResult: result !== void 0
666
+ });
667
+ const encryptedResponse = encodeBase64(
668
+ encrypt(this.encryptionKey, this.encryptionVariant, result)
669
+ );
670
+ return encryptedResponse;
671
+ } catch (error) {
672
+ this.logger("[RPC] [ERROR] Error handling request", { error });
673
+ const errorResponse = {
674
+ error: error instanceof Error ? error.message : "Unknown error"
675
+ };
676
+ return encodeBase64(
677
+ encrypt(this.encryptionKey, this.encryptionVariant, errorResponse)
678
+ );
679
+ }
680
+ }
681
+ onSocketConnect(socket) {
682
+ this.socket = socket;
683
+ this.registerAllHandlers(socket);
684
+ this.startReregisterInterval();
685
+ }
686
+ onSocketDisconnect() {
687
+ this.socket = null;
688
+ this.stopReregisterInterval();
689
+ }
690
+ getHandlerCount() {
691
+ return this.handlers.size;
692
+ }
693
+ hasHandler(method) {
694
+ return this.handlers.has(this.getPrefixedMethod(method));
695
+ }
696
+ clearHandlers() {
697
+ this.handlers.clear();
698
+ this.logger("Cleared all RPC handlers");
699
+ }
700
+ // -----------------------------------------------------------------------
701
+ // Private
702
+ // -----------------------------------------------------------------------
703
+ /**
704
+ * Register a single method with ack + retry.
705
+ * Falls back to fire-and-forget emit after all retries exhausted.
706
+ */
707
+ emitRegisterWithRetry(socket, method, maxRetries = 3) {
708
+ const attempt = (remaining) => {
709
+ if (this.socket !== socket) return;
710
+ socket.timeout(5e3).emit(
711
+ "rpc-register",
712
+ { method },
713
+ (err, ackResponse) => {
714
+ if (this.socket !== socket) return;
715
+ if (err && remaining > 0) {
716
+ this.logger("[RPC] rpc-register ack timeout, retrying", {
717
+ method,
718
+ remaining
719
+ });
720
+ setTimeout(() => attempt(remaining - 1), 1e3);
721
+ } else if (err) {
722
+ this.logger(
723
+ "[RPC] [WARN] rpc-register failed after retries, falling back to emit",
724
+ { method }
725
+ );
726
+ socket.emit("rpc-register", { method });
727
+ } else if (!ackResponse?.ok) {
728
+ this.logger("[RPC] [WARN] rpc-register rejected by server", {
729
+ method,
730
+ error: ackResponse?.error
731
+ });
732
+ }
733
+ }
734
+ );
735
+ };
736
+ attempt(maxRetries);
737
+ }
738
+ registerAllHandlers(socket) {
739
+ for (const [prefixedMethod] of this.handlers) {
740
+ this.emitRegisterWithRetry(socket, prefixedMethod);
741
+ }
742
+ }
743
+ /**
744
+ * Periodic re-registration every 60s as a safety net.
745
+ */
746
+ startReregisterInterval() {
747
+ this.stopReregisterInterval();
748
+ this.reregisterInterval = setInterval(() => {
749
+ if (this.socket && this.handlers.size > 0) {
750
+ this.registerAllHandlers(this.socket);
751
+ }
752
+ }, 6e4);
753
+ }
754
+ stopReregisterInterval() {
755
+ if (this.reregisterInterval) {
756
+ clearInterval(this.reregisterInterval);
757
+ this.reregisterInterval = null;
758
+ }
759
+ }
760
+ getPrefixedMethod(method) {
761
+ return `${this.scopePrefix}:${method}`;
762
+ }
763
+ }
764
+ function createRpcHandlerManager(config) {
765
+ return new RpcHandlerManager(config);
766
+ }
767
+
768
+ const execAsync = util.promisify(child_process.exec);
769
+ const UPLOAD_TEMP_DIR = path.join(os.tmpdir(), "happy", "uploads");
770
+ const MAX_WRITE_SIZE = 10 * 1024 * 1024;
771
+ function validatePath(targetPath, workingDirectory, additionalAllowedDirs) {
772
+ const resolvedTarget = path.resolve(workingDirectory, targetPath);
773
+ let realTarget;
774
+ try {
775
+ realTarget = fs.realpathSync(resolvedTarget);
776
+ } catch {
777
+ const parentDir = path.resolve(resolvedTarget, "..");
778
+ try {
779
+ realTarget = fs.realpathSync(parentDir) + "/" + resolvedTarget.split("/").pop();
780
+ } catch {
781
+ realTarget = resolvedTarget;
782
+ }
783
+ }
784
+ const allowedDirs = [
785
+ workingDirectory,
786
+ ...additionalAllowedDirs ?? []
787
+ ].map((d) => {
788
+ const resolved = path.resolve(d);
789
+ try {
790
+ return fs.realpathSync(resolved);
791
+ } catch {
792
+ return resolved;
793
+ }
794
+ });
795
+ for (const dir of allowedDirs) {
796
+ if (realTarget.startsWith(dir + "/") || realTarget === dir) {
797
+ return { valid: true };
798
+ }
799
+ }
800
+ return {
801
+ valid: false,
802
+ error: `Access denied: Path '${targetPath}' is outside the allowed directories`
803
+ };
804
+ }
805
+ const BLOCKED_BASH_PATTERNS = [
806
+ { pattern: /\bprintenv\b/i, reason: "printenv is blocked for security" },
807
+ {
808
+ pattern: /\benv\b(?:\s|$|;|\|)/i,
809
+ reason: "env command is blocked for security"
810
+ },
811
+ {
812
+ pattern: /\bset\b\s*(?:$|;|\|)/i,
813
+ reason: "set (list env) is blocked for security"
814
+ },
815
+ {
816
+ pattern: /\bexport\s+-p\b/i,
817
+ reason: "export -p is blocked for security"
818
+ },
819
+ {
820
+ pattern: /\bcompgen\s+-e\b/i,
821
+ reason: "compgen -e is blocked for security"
822
+ },
823
+ {
824
+ pattern: /\bdeclare\s+-x\b/i,
825
+ reason: "declare -x is blocked for security"
826
+ },
827
+ {
828
+ pattern: /\/proc\/[^/]*\/environ/i,
829
+ reason: "reading /proc/environ is blocked for security"
830
+ },
831
+ {
832
+ pattern: /\$\{?\s*(ANTHROPIC_AUTH_TOKEN|ANTHROPIC_API_KEY|ANTHROPIC_BASE_URL|OPENAI_API_KEY|OPENAI_BASE_URL|DATABASE_URL|REDIS_URL|JWT_SECRET|ENCRYPTION_KEY|AWS_SECRET_ACCESS_KEY|GOOGLE_API_KEY|GEMINI_API_KEY|TOGETHER_API_KEY|GITHUB_CLIENT_SECRET|CLAUDE_CODE_OAUTH_TOKEN)\b/i,
833
+ reason: "accessing sensitive environment variables is blocked"
834
+ },
835
+ {
836
+ pattern: /\.(env|env\.local|env\.prod|env\.production|env\.dev)\b/i,
837
+ reason: "reading .env files is blocked for security"
838
+ },
839
+ {
840
+ pattern: /\.aws\/credentials/i,
841
+ reason: "reading AWS credentials is blocked for security"
842
+ },
843
+ {
844
+ pattern: /\.netrc/i,
845
+ reason: "reading .netrc is blocked for security"
846
+ }
847
+ ];
848
+ function checkBlockedBashCommand(command) {
849
+ for (const { pattern, reason } of BLOCKED_BASH_PATTERNS) {
850
+ if (pattern.test(command)) {
851
+ return reason;
852
+ }
853
+ }
854
+ return null;
855
+ }
856
+ function registerAgentHandlers(rpcHandlerManager, workingDirectory, sessionId) {
857
+ const safeSessionId = sessionId.replace(/[^a-zA-Z0-9-]/g, "");
858
+ rpcHandlerManager.registerHandler(
859
+ "bash",
860
+ async (data) => {
861
+ logger.debug("Shell command request:", data.command);
862
+ const blockedReason = checkBlockedBashCommand(data.command);
863
+ if (blockedReason) {
864
+ logger.warn(
865
+ `[SECURITY] Blocked bash RPC command: ${blockedReason}`,
866
+ { command: data.command }
867
+ );
868
+ return { success: false, error: blockedReason };
869
+ }
870
+ if (data.cwd && data.cwd !== "/") {
871
+ const validation = validatePath(data.cwd, workingDirectory);
872
+ if (!validation.valid) {
873
+ return { success: false, error: validation.error };
874
+ }
875
+ }
876
+ try {
877
+ const options = {
878
+ cwd: data.cwd === "/" ? void 0 : data.cwd,
879
+ timeout: data.timeout ?? 3e4
880
+ };
881
+ const { stdout, stderr } = await execAsync(data.command, options);
882
+ return {
883
+ success: true,
884
+ stdout: stdout?.toString() ?? "",
885
+ stderr: stderr?.toString() ?? "",
886
+ exitCode: 0
887
+ };
888
+ } catch (error) {
889
+ const execError = error;
890
+ if (execError.code === "ETIMEDOUT" || execError.killed) {
891
+ return {
892
+ success: false,
893
+ stdout: execError.stdout ?? "",
894
+ stderr: execError.stderr ?? "",
895
+ exitCode: typeof execError.code === "number" ? execError.code : -1,
896
+ error: "Command timed out"
897
+ };
898
+ }
899
+ return {
900
+ success: false,
901
+ stdout: execError.stdout?.toString() ?? "",
902
+ stderr: execError.stderr?.toString() ?? execError.message ?? "Command failed",
903
+ exitCode: typeof execError.code === "number" ? execError.code : 1,
904
+ error: execError.message ?? "Command failed"
905
+ };
906
+ }
907
+ }
908
+ );
909
+ rpcHandlerManager.registerHandler(
910
+ "readFile",
911
+ async (data) => {
912
+ logger.debug("Read file request:", data.path);
913
+ const sessionUploadDir = path.join(UPLOAD_TEMP_DIR, safeSessionId);
914
+ const validation = validatePath(data.path, workingDirectory, [
915
+ sessionUploadDir
916
+ ]);
917
+ if (!validation.valid) {
918
+ return { success: false, error: validation.error };
919
+ }
920
+ try {
921
+ const resolvedPath = path.resolve(workingDirectory, data.path);
922
+ const buffer = await promises.readFile(resolvedPath);
923
+ return { success: true, content: buffer.toString("base64") };
924
+ } catch (error) {
925
+ logger.debug("Failed to read file:", error);
926
+ return {
927
+ success: false,
928
+ error: error instanceof Error ? error.message : "Failed to read file"
929
+ };
930
+ }
931
+ }
932
+ );
933
+ rpcHandlerManager.registerHandler(
934
+ "writeFile",
935
+ async (data) => {
936
+ logger.debug("Write file request:", data.path);
937
+ if (data.content && data.content.length > MAX_WRITE_SIZE) {
938
+ return {
939
+ success: false,
940
+ error: `File content exceeds maximum allowed size (${MAX_WRITE_SIZE} bytes)`
941
+ };
942
+ }
943
+ const sessionUploadDir = path.join(UPLOAD_TEMP_DIR, safeSessionId);
944
+ const validation = validatePath(data.path, workingDirectory, [
945
+ sessionUploadDir
946
+ ]);
947
+ if (!validation.valid) {
948
+ return { success: false, error: validation.error };
949
+ }
950
+ try {
951
+ const resolvedPath = path.resolve(workingDirectory, data.path);
952
+ if (data.expectedHash !== null && data.expectedHash !== void 0) {
953
+ try {
954
+ const existingBuffer = await promises.readFile(resolvedPath);
955
+ const existingHash = crypto.createHash("sha256").update(existingBuffer).digest("hex");
956
+ if (existingHash !== data.expectedHash) {
957
+ return {
958
+ success: false,
959
+ error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
960
+ };
961
+ }
962
+ } catch {
963
+ return {
964
+ success: false,
965
+ error: "File does not exist but expectedHash was provided"
966
+ };
967
+ }
968
+ }
969
+ const dir = path.resolve(resolvedPath, "..");
970
+ await promises.mkdir(dir, { recursive: true });
971
+ const buffer = Buffer.from(data.content, "base64");
972
+ await promises.writeFile(resolvedPath, buffer);
973
+ const hash = crypto.createHash("sha256").update(buffer).digest("hex");
974
+ return { success: true, hash };
975
+ } catch (error) {
976
+ logger.debug("Failed to write file:", error);
977
+ return {
978
+ success: false,
979
+ error: error instanceof Error ? error.message : "Failed to write file"
980
+ };
981
+ }
982
+ }
983
+ );
984
+ rpcHandlerManager.registerHandler("listDirectory", async (data) => {
985
+ logger.debug("List directory request:", data.path);
986
+ const validation = validatePath(data.path, workingDirectory);
987
+ if (!validation.valid) {
988
+ return { success: false, error: validation.error };
989
+ }
990
+ try {
991
+ const entries = await promises.readdir(data.path, { withFileTypes: true });
992
+ const directoryEntries = await Promise.all(
993
+ entries.map(async (entry) => {
994
+ const fullPath = path.join(data.path, entry.name);
995
+ let type = "other";
996
+ let size;
997
+ let modified;
998
+ if (entry.isDirectory()) type = "directory";
999
+ else if (entry.isFile()) type = "file";
1000
+ try {
1001
+ const stats = await promises.stat(fullPath);
1002
+ size = stats.size;
1003
+ modified = stats.mtime.getTime();
1004
+ } catch {
1005
+ }
1006
+ return { name: entry.name, type, size, modified };
1007
+ })
1008
+ );
1009
+ directoryEntries.sort((a, b) => {
1010
+ if (a.type === "directory" && b.type !== "directory") return -1;
1011
+ if (a.type !== "directory" && b.type === "directory") return 1;
1012
+ return a.name.localeCompare(b.name);
1013
+ });
1014
+ return { success: true, entries: directoryEntries };
1015
+ } catch (error) {
1016
+ logger.debug("Failed to list directory:", error);
1017
+ return {
1018
+ success: false,
1019
+ error: error instanceof Error ? error.message : "Failed to list directory"
1020
+ };
1021
+ }
1022
+ });
1023
+ rpcHandlerManager.registerHandler("getUploadDir", async () => {
1024
+ const uploadDir = path.join(UPLOAD_TEMP_DIR, safeSessionId);
1025
+ await promises.mkdir(uploadDir, { recursive: true });
1026
+ return { success: true, path: uploadDir };
1027
+ });
1028
+ }
421
1029
 
422
1030
  class SessionClient extends node_events.EventEmitter {
423
1031
  sessionId;
1032
+ rpcHandlerManager;
424
1033
  encryptionKey;
425
1034
  encryptionVariant;
426
1035
  socket;
@@ -429,6 +1038,7 @@ class SessionClient extends node_events.EventEmitter {
429
1038
  agentState = null;
430
1039
  agentStateVersion = 0;
431
1040
  aliveInterval = null;
1041
+ thinking = false;
432
1042
  constructor(opts) {
433
1043
  super();
434
1044
  this.sessionId = opts.sessionId;
@@ -439,6 +1049,16 @@ class SessionClient extends node_events.EventEmitter {
439
1049
  }
440
1050
  this.on("error", () => {
441
1051
  });
1052
+ this.rpcHandlerManager = createRpcHandlerManager({
1053
+ scopePrefix: `session:${opts.sessionId}`,
1054
+ encryptionKey: opts.encryptionKey,
1055
+ encryptionVariant: opts.encryptionVariant,
1056
+ logger: (msg, data) => logger.debug(msg, data)
1057
+ });
1058
+ if (opts.enableRpc !== false) {
1059
+ const workDir = opts.workingDirectory ?? process.cwd();
1060
+ registerAgentHandlers(this.rpcHandlerManager, workDir, opts.sessionId);
1061
+ }
442
1062
  this.socket = socket_ioClient.io(opts.serverUrl, {
443
1063
  auth: {
444
1064
  token: opts.token,
@@ -453,84 +1073,17 @@ class SessionClient extends node_events.EventEmitter {
453
1073
  transports: ["websocket"],
454
1074
  autoConnect: false
455
1075
  });
456
- this.socket.on("connect", () => {
457
- this.emit("connected");
458
- this.aliveInterval = setInterval(() => {
459
- this.socket.emit("session-alive", {
460
- sid: this.sessionId,
461
- time: Date.now()
462
- });
463
- }, 2e4);
464
- });
465
- this.socket.on("disconnect", (reason) => {
466
- if (this.aliveInterval !== null) {
467
- clearInterval(this.aliveInterval);
468
- this.aliveInterval = null;
469
- }
470
- this.emit("disconnected", reason);
471
- });
472
- this.socket.on("connect_error", (error) => {
473
- this.emit("connect_error", error);
474
- });
475
- this.socket.on("update", (data) => {
476
- try {
477
- const body = data?.body;
478
- if (!body) return;
479
- if (body.t === "new-message" && body.message?.content?.t === "encrypted") {
480
- const msg = body.message;
481
- const decrypted = decrypt(
482
- this.encryptionKey,
483
- this.encryptionVariant,
484
- decodeBase64(msg.content.c)
485
- );
486
- if (decrypted === null) return;
487
- this.emit("message", {
488
- id: msg.id,
489
- seq: msg.seq,
490
- content: decrypted,
491
- localId: msg.localId,
492
- createdAt: msg.createdAt,
493
- updatedAt: msg.updatedAt
494
- });
495
- } else if (body.t === "update-session") {
496
- if (body.metadata && body.metadata.version > this.metadataVersion) {
497
- this.metadata = decrypt(
498
- this.encryptionKey,
499
- this.encryptionVariant,
500
- decodeBase64(body.metadata.value)
501
- );
502
- this.metadataVersion = body.metadata.version;
503
- }
504
- if (body.agentState && body.agentState.version > this.agentStateVersion) {
505
- this.agentState = body.agentState.value ? decrypt(
506
- this.encryptionKey,
507
- this.encryptionVariant,
508
- decodeBase64(body.agentState.value)
509
- ) : null;
510
- this.agentStateVersion = body.agentState.version;
511
- }
512
- this.emit("state-change", {
513
- metadata: this.metadata,
514
- agentState: this.agentState
515
- });
516
- }
517
- } catch (err) {
518
- this.emit("error", err);
519
- }
520
- });
1076
+ this.setupSocketListeners();
521
1077
  this.socket.connect();
522
1078
  }
1079
+ // -----------------------------------------------------------------------
1080
+ // Public API (backward compatible)
1081
+ // -----------------------------------------------------------------------
523
1082
  sendMessage(text, meta) {
524
1083
  const content = {
525
1084
  role: "user",
526
- content: {
527
- type: "text",
528
- text
529
- },
530
- meta: {
531
- sentFrom: "happy-agent",
532
- ...meta
533
- }
1085
+ content: { type: "text", text },
1086
+ meta: { sentFrom: "happy-agent", ...meta }
534
1087
  };
535
1088
  const encrypted = encodeBase64(
536
1089
  encrypt(this.encryptionKey, this.encryptionVariant, content)
@@ -546,6 +1099,9 @@ class SessionClient extends node_events.EventEmitter {
546
1099
  getAgentState() {
547
1100
  return this.agentState;
548
1101
  }
1102
+ setThinking(thinking) {
1103
+ this.thinking = thinking;
1104
+ }
549
1105
  waitForConnect(timeoutMs = 1e4) {
550
1106
  return new Promise((resolve, reject) => {
551
1107
  if (this.socket.connected) {
@@ -575,13 +1131,9 @@ class SessionClient extends node_events.EventEmitter {
575
1131
  return new Promise((resolve, reject) => {
576
1132
  const checkIdle = () => {
577
1133
  const meta = this.metadata;
578
- if (meta?.lifecycleState === "archived") {
579
- return "archived";
580
- }
1134
+ if (meta?.lifecycleState === "archived") return "archived";
581
1135
  const state = this.agentState;
582
- if (!state) {
583
- return false;
584
- }
1136
+ if (!state) return false;
585
1137
  const controlledByUser = state.controlledByUser === true;
586
1138
  const requests = state.requests;
587
1139
  const hasRequests = requests != null && typeof requests === "object" && !Array.isArray(requests) && Object.keys(requests).length > 0;
@@ -638,8 +1190,352 @@ class SessionClient extends node_events.EventEmitter {
638
1190
  clearInterval(this.aliveInterval);
639
1191
  this.aliveInterval = null;
640
1192
  }
1193
+ this.rpcHandlerManager.onSocketDisconnect();
641
1194
  this.socket.close();
642
1195
  }
1196
+ // -----------------------------------------------------------------------
1197
+ // New: OCC metadata/state updates
1198
+ // -----------------------------------------------------------------------
1199
+ async updateMetadata(newMetadata) {
1200
+ const encrypted = encodeBase64(
1201
+ encrypt(this.encryptionKey, this.encryptionVariant, newMetadata)
1202
+ );
1203
+ await withBackoff(
1204
+ () => new Promise((resolve, reject) => {
1205
+ this.socket.emit(
1206
+ "update-metadata",
1207
+ {
1208
+ sid: this.sessionId,
1209
+ expectedVersion: this.metadataVersion,
1210
+ metadata: encrypted
1211
+ },
1212
+ (answer) => {
1213
+ if (answer.result === "success" && answer.version !== void 0) {
1214
+ this.metadataVersion = answer.version;
1215
+ this.metadata = newMetadata;
1216
+ resolve();
1217
+ } else if (answer.result === "version-mismatch" && answer.version !== void 0) {
1218
+ this.metadataVersion = answer.version;
1219
+ if (answer.metadata) {
1220
+ this.metadata = decrypt(
1221
+ this.encryptionKey,
1222
+ this.encryptionVariant,
1223
+ decodeBase64(answer.metadata)
1224
+ );
1225
+ }
1226
+ reject(new Error("version-mismatch"));
1227
+ } else {
1228
+ reject(new Error(`update-metadata failed: ${answer.result}`));
1229
+ }
1230
+ }
1231
+ );
1232
+ }),
1233
+ { maxRetries: 3, label: "updateMetadata" }
1234
+ );
1235
+ }
1236
+ async updateAgentState(newState) {
1237
+ const encrypted = newState !== null ? encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, newState)) : null;
1238
+ await withBackoff(
1239
+ () => new Promise((resolve, reject) => {
1240
+ this.socket.emit(
1241
+ "update-state",
1242
+ {
1243
+ sid: this.sessionId,
1244
+ expectedVersion: this.agentStateVersion,
1245
+ agentState: encrypted
1246
+ },
1247
+ (answer) => {
1248
+ if (answer.result === "success" && answer.version !== void 0) {
1249
+ this.agentStateVersion = answer.version;
1250
+ this.agentState = newState;
1251
+ resolve();
1252
+ } else if (answer.result === "version-mismatch" && answer.version !== void 0) {
1253
+ this.agentStateVersion = answer.version;
1254
+ if (answer.agentState) {
1255
+ this.agentState = decrypt(
1256
+ this.encryptionKey,
1257
+ this.encryptionVariant,
1258
+ decodeBase64(answer.agentState)
1259
+ );
1260
+ }
1261
+ reject(new Error("version-mismatch"));
1262
+ } else {
1263
+ reject(new Error(`update-state failed: ${answer.result}`));
1264
+ }
1265
+ }
1266
+ );
1267
+ }),
1268
+ { maxRetries: 3, label: "updateAgentState" }
1269
+ );
1270
+ }
1271
+ // -----------------------------------------------------------------------
1272
+ // Private: socket listeners
1273
+ // -----------------------------------------------------------------------
1274
+ setupSocketListeners() {
1275
+ this.socket.on("connect", () => {
1276
+ this.emit("connected");
1277
+ this.rpcHandlerManager.onSocketConnect(this.socket);
1278
+ this.aliveInterval = setInterval(() => {
1279
+ this.socket.emit("session-alive", {
1280
+ sid: this.sessionId,
1281
+ time: Date.now(),
1282
+ thinking: this.thinking
1283
+ });
1284
+ }, 2e4);
1285
+ });
1286
+ this.socket.on("disconnect", (reason) => {
1287
+ if (this.aliveInterval !== null) {
1288
+ clearInterval(this.aliveInterval);
1289
+ this.aliveInterval = null;
1290
+ }
1291
+ this.rpcHandlerManager.onSocketDisconnect();
1292
+ this.emit("disconnected", reason);
1293
+ });
1294
+ this.socket.on("connect_error", (error) => {
1295
+ this.emit("connect_error", error);
1296
+ });
1297
+ this.socket.on("rpc-request", async (data, callback) => {
1298
+ try {
1299
+ const response = await this.rpcHandlerManager.handleRequest(data);
1300
+ callback(response);
1301
+ } catch (err) {
1302
+ logger.error("[RPC] Unhandled error in rpc-request handler", err);
1303
+ }
1304
+ });
1305
+ this.socket.on("update", (data) => {
1306
+ try {
1307
+ const body = data?.body;
1308
+ if (!body) return;
1309
+ if (body.t === "new-message" && body.message?.content?.t === "encrypted") {
1310
+ const msg = body.message;
1311
+ const decrypted = decrypt(
1312
+ this.encryptionKey,
1313
+ this.encryptionVariant,
1314
+ decodeBase64(msg.content.c)
1315
+ );
1316
+ if (decrypted === null) return;
1317
+ this.emit("message", {
1318
+ id: msg.id,
1319
+ seq: msg.seq,
1320
+ content: decrypted,
1321
+ localId: msg.localId,
1322
+ createdAt: msg.createdAt,
1323
+ updatedAt: msg.updatedAt
1324
+ });
1325
+ } else if (body.t === "update-session") {
1326
+ if (body.metadata && body.metadata.version > this.metadataVersion) {
1327
+ this.metadata = decrypt(
1328
+ this.encryptionKey,
1329
+ this.encryptionVariant,
1330
+ decodeBase64(body.metadata.value)
1331
+ );
1332
+ this.metadataVersion = body.metadata.version;
1333
+ }
1334
+ if (body.agentState && body.agentState.version > this.agentStateVersion) {
1335
+ this.agentState = body.agentState.value ? decrypt(
1336
+ this.encryptionKey,
1337
+ this.encryptionVariant,
1338
+ decodeBase64(body.agentState.value)
1339
+ ) : null;
1340
+ this.agentStateVersion = body.agentState.version;
1341
+ }
1342
+ this.emit("state-change", {
1343
+ metadata: this.metadata,
1344
+ agentState: this.agentState
1345
+ });
1346
+ }
1347
+ } catch (err) {
1348
+ this.emit("error", err);
1349
+ }
1350
+ });
1351
+ }
1352
+ }
1353
+
1354
+ class MachineClient {
1355
+ machine;
1356
+ rpcHandlerManager;
1357
+ socket;
1358
+ keepAliveInterval = null;
1359
+ token;
1360
+ serverUrl;
1361
+ onEphemeral;
1362
+ constructor(opts) {
1363
+ this.token = opts.token;
1364
+ this.machine = opts.machine;
1365
+ this.serverUrl = opts.serverUrl;
1366
+ this.onEphemeral = opts.onEphemeral;
1367
+ this.rpcHandlerManager = new RpcHandlerManager({
1368
+ scopePrefix: opts.machine.id,
1369
+ encryptionKey: opts.machine.encryptionKey,
1370
+ encryptionVariant: opts.machine.encryptionVariant,
1371
+ logger: (msg, data) => logger.debug(msg, data)
1372
+ });
1373
+ const workDir = opts.workingDirectory ?? process.cwd();
1374
+ registerAgentHandlers(this.rpcHandlerManager, workDir, opts.machine.id);
1375
+ }
1376
+ // -----------------------------------------------------------------------
1377
+ // Connection
1378
+ // -----------------------------------------------------------------------
1379
+ connect() {
1380
+ logger.debug(`[MACHINE] Connecting to ${this.serverUrl}`);
1381
+ this.socket = socket_ioClient.io(this.serverUrl, {
1382
+ transports: ["websocket"],
1383
+ auth: {
1384
+ token: this.token,
1385
+ clientType: "machine-scoped",
1386
+ machineId: this.machine.id
1387
+ },
1388
+ path: "/v1/updates",
1389
+ reconnection: true,
1390
+ reconnectionDelay: 1e3,
1391
+ reconnectionDelayMax: 5e3
1392
+ });
1393
+ this.socket.on("connect", () => {
1394
+ logger.debug("[MACHINE] Connected to server");
1395
+ this.rpcHandlerManager.onSocketConnect(this.socket);
1396
+ this.startKeepAlive();
1397
+ });
1398
+ this.socket.on("disconnect", () => {
1399
+ logger.debug("[MACHINE] Disconnected from server");
1400
+ this.rpcHandlerManager.onSocketDisconnect();
1401
+ this.stopKeepAlive();
1402
+ });
1403
+ this.socket.on(
1404
+ "rpc-request",
1405
+ async (data, callback) => {
1406
+ logger.debug("[MACHINE] Received RPC request:", data.method);
1407
+ callback(await this.rpcHandlerManager.handleRequest(data));
1408
+ }
1409
+ );
1410
+ this.socket.on("update", (data) => {
1411
+ const body = data?.body;
1412
+ if (body?.t === "update-machine" && body.machineId === this.machine.id) {
1413
+ if (body.metadata && body.metadata.version > this.machine.metadataVersion) {
1414
+ this.machine.metadata = decrypt(
1415
+ this.machine.encryptionKey,
1416
+ this.machine.encryptionVariant,
1417
+ decodeBase64(body.metadata.value)
1418
+ );
1419
+ this.machine.metadataVersion = body.metadata.version;
1420
+ }
1421
+ if (body.daemonState && body.daemonState.version > this.machine.daemonStateVersion) {
1422
+ this.machine.daemonState = decrypt(
1423
+ this.machine.encryptionKey,
1424
+ this.machine.encryptionVariant,
1425
+ decodeBase64(body.daemonState.value)
1426
+ );
1427
+ this.machine.daemonStateVersion = body.daemonState.version;
1428
+ }
1429
+ }
1430
+ });
1431
+ if (this.onEphemeral) {
1432
+ this.socket.on("ephemeral", (data) => {
1433
+ logger.debug("[MACHINE] Received ephemeral event:", data.type);
1434
+ this.onEphemeral?.(data);
1435
+ });
1436
+ }
1437
+ this.socket.on("connect_error", (error) => {
1438
+ logger.debug(`[MACHINE] Connection error: ${error.message}`);
1439
+ });
1440
+ }
1441
+ // -----------------------------------------------------------------------
1442
+ // OCC updates
1443
+ // -----------------------------------------------------------------------
1444
+ async updateMachineMetadata(handler) {
1445
+ await withBackoff(async () => {
1446
+ const updated = handler(this.machine.metadata);
1447
+ const answer = await new Promise((resolve) => {
1448
+ this.socket.emit(
1449
+ "machine-update-metadata",
1450
+ {
1451
+ machineId: this.machine.id,
1452
+ metadata: encodeBase64(
1453
+ encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)
1454
+ ),
1455
+ expectedVersion: this.machine.metadataVersion
1456
+ },
1457
+ resolve
1458
+ );
1459
+ });
1460
+ if (answer.result === "success") {
1461
+ this.machine.metadata = decrypt(
1462
+ this.machine.encryptionKey,
1463
+ this.machine.encryptionVariant,
1464
+ decodeBase64(answer.metadata)
1465
+ );
1466
+ this.machine.metadataVersion = answer.version;
1467
+ } else if (answer.result === "version-mismatch") {
1468
+ this.machine.metadataVersion = answer.version;
1469
+ this.machine.metadata = decrypt(
1470
+ this.machine.encryptionKey,
1471
+ this.machine.encryptionVariant,
1472
+ decodeBase64(answer.metadata)
1473
+ );
1474
+ throw new Error("Metadata version mismatch");
1475
+ }
1476
+ }, { maxRetries: 3, label: "updateMachineMetadata" });
1477
+ }
1478
+ async updateDaemonState(handler) {
1479
+ await withBackoff(async () => {
1480
+ const updated = handler(this.machine.daemonState);
1481
+ const answer = await new Promise((resolve) => {
1482
+ this.socket.emit(
1483
+ "machine-update-state",
1484
+ {
1485
+ machineId: this.machine.id,
1486
+ daemonState: encodeBase64(
1487
+ encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)
1488
+ ),
1489
+ expectedVersion: this.machine.daemonStateVersion
1490
+ },
1491
+ resolve
1492
+ );
1493
+ });
1494
+ if (answer.result === "success") {
1495
+ this.machine.daemonState = decrypt(
1496
+ this.machine.encryptionKey,
1497
+ this.machine.encryptionVariant,
1498
+ decodeBase64(answer.daemonState)
1499
+ );
1500
+ this.machine.daemonStateVersion = answer.version;
1501
+ } else if (answer.result === "version-mismatch") {
1502
+ this.machine.daemonStateVersion = answer.version;
1503
+ this.machine.daemonState = decrypt(
1504
+ this.machine.encryptionKey,
1505
+ this.machine.encryptionVariant,
1506
+ decodeBase64(answer.daemonState)
1507
+ );
1508
+ throw new Error("Daemon state version mismatch");
1509
+ }
1510
+ }, { maxRetries: 3, label: "updateDaemonState" });
1511
+ }
1512
+ // -----------------------------------------------------------------------
1513
+ // Lifecycle
1514
+ // -----------------------------------------------------------------------
1515
+ shutdown() {
1516
+ logger.debug("[MACHINE] Shutting down");
1517
+ this.stopKeepAlive();
1518
+ this.rpcHandlerManager.onSocketDisconnect();
1519
+ this.socket?.close();
1520
+ }
1521
+ // -----------------------------------------------------------------------
1522
+ // Private
1523
+ // -----------------------------------------------------------------------
1524
+ startKeepAlive() {
1525
+ this.stopKeepAlive();
1526
+ this.keepAliveInterval = setInterval(() => {
1527
+ this.socket.emit("machine-alive", {
1528
+ machineId: this.machine.id,
1529
+ time: Date.now()
1530
+ });
1531
+ }, 2e4);
1532
+ }
1533
+ stopKeepAlive() {
1534
+ if (this.keepAliveInterval) {
1535
+ clearInterval(this.keepAliveInterval);
1536
+ this.keepAliveInterval = null;
1537
+ }
1538
+ }
643
1539
  }
644
1540
 
645
1541
  function formatTime(ts) {
@@ -819,9 +1715,9 @@ function createClient(session, creds, config) {
819
1715
  const program = new commander.Command();
820
1716
  program.name("happy-agent").description("CLI client for controlling Happy Coder agents remotely").version(version);
821
1717
  program.command("auth").description("Manage authentication").addCommand(
822
- new commander.Command("login").description("Authenticate via QR code").action(async () => {
1718
+ new commander.Command("login").description("Authenticate via QR code or web URL").option("--web", "Use web URL instead of QR code (for SSH/headless environments)").action(async (opts) => {
823
1719
  const config = loadConfig();
824
- await authLogin(config);
1720
+ await authLogin(config, { web: opts.web });
825
1721
  })
826
1722
  ).addCommand(
827
1723
  new commander.Command("logout").description("Clear stored credentials").action(async () => {
@@ -1031,7 +1927,74 @@ program.command("wait").description("Wait for agent to become idle").argument("<
1031
1927
  client.close();
1032
1928
  }
1033
1929
  });
1930
+ program.command("machine").description("Manage machine identity").addCommand(
1931
+ new commander.Command("register").description("Register this machine with the server").option("--json", "Output as JSON").action(async (opts) => {
1932
+ const config = loadConfig();
1933
+ const creds = requireCredentials(config);
1934
+ const metadata = {
1935
+ host: node_os.hostname(),
1936
+ platform: process.platform,
1937
+ happyCliVersion: version,
1938
+ homeDir: config.homeDir,
1939
+ happyHomeDir: config.homeDir,
1940
+ happyLibDir: config.homeDir
1941
+ };
1942
+ const machine = await getOrCreateMachine(config, creds, metadata);
1943
+ if (opts.json) {
1944
+ console.log(formatJson({ id: machine.id, metadata: machine.metadata }));
1945
+ } else {
1946
+ console.log(
1947
+ [
1948
+ "## Machine Registered",
1949
+ "",
1950
+ `- Machine ID: \`${machine.id}\``,
1951
+ `- Host: ${machine.metadata.host}`,
1952
+ `- Platform: ${machine.metadata.platform}`
1953
+ ].join("\n")
1954
+ );
1955
+ }
1956
+ })
1957
+ ).addCommand(
1958
+ new commander.Command("list").description("List all registered machines").option("--json", "Output as JSON").action(async (opts) => {
1959
+ const config = loadConfig();
1960
+ const creds = requireCredentials(config);
1961
+ const machines = await listMachines(config, creds);
1962
+ if (opts.json) {
1963
+ console.log(formatJson(machines));
1964
+ } else {
1965
+ if (machines.length === 0) {
1966
+ console.log("No machines registered.");
1967
+ } else {
1968
+ console.log(`## Machines (${machines.length})`);
1969
+ for (const m of machines) {
1970
+ console.log(`- \`${m.id}\``);
1971
+ }
1972
+ }
1973
+ }
1974
+ })
1975
+ );
1034
1976
  program.parseAsync(process.argv).catch((err) => {
1035
1977
  console.error(err instanceof Error ? err.message : String(err));
1036
1978
  process.exitCode = 1;
1037
1979
  });
1980
+
1981
+ exports.MachineClient = MachineClient;
1982
+ exports.RpcHandlerManager = RpcHandlerManager;
1983
+ exports.SessionClient = SessionClient;
1984
+ exports.authLogin = authLogin;
1985
+ exports.authLogout = authLogout;
1986
+ exports.authStatus = authStatus;
1987
+ exports.createRpcHandlerManager = createRpcHandlerManager;
1988
+ exports.createSession = createSession;
1989
+ exports.deleteSession = deleteSession;
1990
+ exports.fetchMessagesAfterSeq = fetchMessagesAfterSeq;
1991
+ exports.getOrCreateMachine = getOrCreateMachine;
1992
+ exports.getSessionMessages = getSessionMessages;
1993
+ exports.listActiveSessions = listActiveSessions;
1994
+ exports.listMachines = listMachines;
1995
+ exports.listSessions = listSessions;
1996
+ exports.loadConfig = loadConfig;
1997
+ exports.readCredentials = readCredentials;
1998
+ exports.requireCredentials = requireCredentials;
1999
+ exports.resolveSessionEncryption = resolveSessionEncryption;
2000
+ exports.sendMessagesBatch = sendMessagesBatch;