@kmmao/happy-agent 0.3.7 → 0.3.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.
package/dist/index.cjs CHANGED
@@ -18,7 +18,7 @@ var path = require('path');
18
18
  var fs = require('fs');
19
19
  var os = require('os');
20
20
 
21
- var version = "0.3.7";
21
+ var version = "0.3.9";
22
22
 
23
23
  function loadConfig() {
24
24
  const serverUrl = (process.env.HAPPY_SERVER_URL ?? "https://happyserve.xycloud.info").replace(/\/+$/, "");
@@ -622,6 +622,7 @@ class RpcHandlerManager {
622
622
  logger;
623
623
  socket = null;
624
624
  reregisterInterval = null;
625
+ fastRetryTimer = null;
625
626
  constructor(config) {
626
627
  this.scopePrefix = config.scopePrefix;
627
628
  this.encryptionKey = config.encryptionKey;
@@ -681,11 +682,13 @@ class RpcHandlerManager {
681
682
  onSocketConnect(socket) {
682
683
  this.socket = socket;
683
684
  this.registerAllHandlers(socket);
685
+ this.scheduleFastRetry(socket);
684
686
  this.startReregisterInterval();
685
687
  }
686
688
  onSocketDisconnect() {
687
689
  this.socket = null;
688
690
  this.stopReregisterInterval();
691
+ this.cancelFastRetry();
689
692
  }
690
693
  getHandlerCount() {
691
694
  return this.handlers.size;
@@ -741,7 +744,27 @@ class RpcHandlerManager {
741
744
  }
742
745
  }
743
746
  /**
744
- * Periodic re-registration every 60s as a safety net.
747
+ * Fast retry: 5 seconds after initial connect, re-register all handlers once.
748
+ * Covers the case where the first batch of registrations failed silently.
749
+ */
750
+ scheduleFastRetry(socket) {
751
+ this.cancelFastRetry();
752
+ this.fastRetryTimer = setTimeout(() => {
753
+ this.fastRetryTimer = null;
754
+ if (this.socket === socket && this.handlers.size > 0) {
755
+ this.logger("[RPC] Fast retry: re-registering all handlers");
756
+ this.registerAllHandlers(socket);
757
+ }
758
+ }, 5e3);
759
+ }
760
+ cancelFastRetry() {
761
+ if (this.fastRetryTimer) {
762
+ clearTimeout(this.fastRetryTimer);
763
+ this.fastRetryTimer = null;
764
+ }
765
+ }
766
+ /**
767
+ * Periodic re-registration every 30s as a safety net.
745
768
  */
746
769
  startReregisterInterval() {
747
770
  this.stopReregisterInterval();
@@ -749,7 +772,7 @@ class RpcHandlerManager {
749
772
  if (this.socket && this.handlers.size > 0) {
750
773
  this.registerAllHandlers(this.socket);
751
774
  }
752
- }, 6e4);
775
+ }, 3e4);
753
776
  }
754
777
  stopReregisterInterval() {
755
778
  if (this.reregisterInterval) {
@@ -1460,6 +1483,7 @@ class MachineClient {
1460
1483
  keepAliveInterval = null;
1461
1484
  tailscaleRefreshInterval = null;
1462
1485
  lastTailscaleInfo = null;
1486
+ tunnelManager = null;
1463
1487
  token;
1464
1488
  serverUrl;
1465
1489
  onEphemeral;
@@ -1629,6 +1653,10 @@ class MachineClient {
1629
1653
  setTailscaleInfo(info) {
1630
1654
  this.lastTailscaleInfo = info;
1631
1655
  }
1656
+ /** Attach a TunnelManager for periodic tunnel state refresh. */
1657
+ setTunnelManager(manager) {
1658
+ this.tunnelManager = manager;
1659
+ }
1632
1660
  shutdown() {
1633
1661
  logger.debug("[MACHINE] Shutting down");
1634
1662
  this.stopKeepAlive();
@@ -1660,14 +1688,22 @@ class MachineClient {
1660
1688
  const info = await detectTailscale();
1661
1689
  const serves = info.status === "connected" ? await detectTailscaleServe() : [];
1662
1690
  const fullInfo = { ...info, serves };
1663
- if (tailscaleChanged(this.lastTailscaleInfo, fullInfo)) {
1691
+ const tsChanged = tailscaleChanged(this.lastTailscaleInfo, fullInfo);
1692
+ if (tsChanged) {
1664
1693
  logger.debug(
1665
1694
  `[MACHINE] Tailscale changed: ${this.lastTailscaleInfo?.status} \u2192 ${fullInfo.status}, serves: ${serves.length}`
1666
1695
  );
1667
1696
  this.lastTailscaleInfo = fullInfo;
1697
+ }
1698
+ const tunnels = this.tunnelManager ? await this.tunnelManager.detectAll() : void 0;
1699
+ if (tsChanged || tunnels) {
1668
1700
  this.updateDaemonState((state) => {
1669
- if (!state) return { status: "running", tailscale: fullInfo };
1670
- return { ...state, tailscale: fullInfo };
1701
+ if (!state) return { status: "running", tailscale: fullInfo, tunnels };
1702
+ return {
1703
+ ...state,
1704
+ ...tsChanged ? { tailscale: fullInfo } : {},
1705
+ ...tunnels ? { tunnels } : {}
1706
+ };
1671
1707
  });
1672
1708
  }
1673
1709
  }, TAILSCALE_REFRESH_MS);
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { MachineMetadata, DaemonState } from '@kmmao/happy-wire';
2
+ import { MachineMetadata, DaemonState, TunnelProviderInfo, TunnelEntry, TunnelState } from '@kmmao/happy-wire';
3
3
  import { EventEmitter } from 'node:events';
4
4
  import { Socket } from 'socket.io-client';
5
5
 
@@ -175,6 +175,7 @@ declare class RpcHandlerManager {
175
175
  private readonly logger;
176
176
  private socket;
177
177
  private reregisterInterval;
178
+ private fastRetryTimer;
178
179
  constructor(config: RpcHandlerConfig);
179
180
  /**
180
181
  * Register an RPC handler for a specific method
@@ -196,7 +197,13 @@ declare class RpcHandlerManager {
196
197
  private emitRegisterWithRetry;
197
198
  private registerAllHandlers;
198
199
  /**
199
- * Periodic re-registration every 60s as a safety net.
200
+ * Fast retry: 5 seconds after initial connect, re-register all handlers once.
201
+ * Covers the case where the first batch of registrations failed silently.
202
+ */
203
+ private scheduleFastRetry;
204
+ private cancelFastRetry;
205
+ /**
206
+ * Periodic re-registration every 30s as a safety net.
200
207
  */
201
208
  private startReregisterInterval;
202
209
  private stopReregisterInterval;
@@ -278,6 +285,66 @@ type TailscaleInfo = {
278
285
  serves?: TailscaleServeEntry[];
279
286
  };
280
287
 
288
+ /**
289
+ * Tunnel Provider abstraction — unified interface for all tunnel backends.
290
+ *
291
+ * Each provider (Tailscale, UPnP, Cloudflare, FRP, etc.) implements this
292
+ * interface. TunnelManager aggregates all providers and exposes a single API.
293
+ */
294
+
295
+ interface TunnelProvider {
296
+ /** Unique provider name: "tailscale" | "upnp" | "cloudflare" | "frp" */
297
+ readonly name: string;
298
+ /** Detect provider availability and list current tunnel entries. Never throws. */
299
+ detect(): Promise<TunnelProviderInfo>;
300
+ /** Add a tunnel mapping */
301
+ add(params: TunnelAddParams): Promise<TunnelOpResult>;
302
+ /** Remove a tunnel mapping */
303
+ remove(params: TunnelRemoveParams): Promise<TunnelOpResult>;
304
+ /** Toggle public/private access (not all providers support this) */
305
+ toggleAccess?(entry: TunnelEntry, publicAccess: boolean): Promise<TunnelOpResult>;
306
+ }
307
+ interface TunnelAddParams {
308
+ localPort: number;
309
+ remotePort?: number;
310
+ protocol?: string;
311
+ path?: string;
312
+ publicAccess?: boolean;
313
+ hostname?: string;
314
+ }
315
+ interface TunnelRemoveParams {
316
+ localPort?: number;
317
+ remotePort?: number;
318
+ path?: string;
319
+ hostname?: string;
320
+ /** Remove entire site (all routes for this hostname) */
321
+ removeEntireSite?: boolean;
322
+ }
323
+ interface TunnelOpResult {
324
+ success: boolean;
325
+ error?: string;
326
+ }
327
+
328
+ /**
329
+ * TunnelManager for happy-agent — aggregates all tunnel providers.
330
+ */
331
+
332
+ declare class TunnelManager {
333
+ private readonly providers;
334
+ private refreshTimer;
335
+ private leaseRenewalTimer;
336
+ private lastState;
337
+ constructor(providers: TunnelProvider[]);
338
+ detectAll(): Promise<TunnelState>;
339
+ getLastState(): TunnelState;
340
+ getProvider(name: string): TunnelProvider | undefined;
341
+ add(providerName: string, params: TunnelAddParams): Promise<TunnelOpResult>;
342
+ remove(providerName: string, params: TunnelRemoveParams): Promise<TunnelOpResult>;
343
+ toggleAccess(providerName: string, entry: TunnelEntry, publicAccess: boolean): Promise<TunnelOpResult>;
344
+ startRefresh(onChange: (state: TunnelState) => void, intervalMs?: number): void;
345
+ stopRefresh(): void;
346
+ }
347
+
281
348
  /**
282
349
  * Machine WebSocket client — trimmed from CLI's ApiMachineClient.
283
350
  *
@@ -313,6 +380,7 @@ declare class MachineClient {
313
380
  private keepAliveInterval;
314
381
  private tailscaleRefreshInterval;
315
382
  private lastTailscaleInfo;
383
+ private tunnelManager;
316
384
  private readonly token;
317
385
  private readonly serverUrl;
318
386
  private readonly onEphemeral?;
@@ -322,6 +390,8 @@ declare class MachineClient {
322
390
  updateDaemonState(handler: (state: DaemonState | null) => DaemonState): Promise<void>;
323
391
  /** Seed initial Tailscale info detected before connect. */
324
392
  setTailscaleInfo(info: TailscaleInfo): void;
393
+ /** Attach a TunnelManager for periodic tunnel state refresh. */
394
+ setTunnelManager(manager: TunnelManager): void;
325
395
  shutdown(): void;
326
396
  private startKeepAlive;
327
397
  private stopKeepAlive;
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { MachineMetadata, DaemonState } from '@kmmao/happy-wire';
2
+ import { MachineMetadata, DaemonState, TunnelProviderInfo, TunnelEntry, TunnelState } from '@kmmao/happy-wire';
3
3
  import { EventEmitter } from 'node:events';
4
4
  import { Socket } from 'socket.io-client';
5
5
 
@@ -175,6 +175,7 @@ declare class RpcHandlerManager {
175
175
  private readonly logger;
176
176
  private socket;
177
177
  private reregisterInterval;
178
+ private fastRetryTimer;
178
179
  constructor(config: RpcHandlerConfig);
179
180
  /**
180
181
  * Register an RPC handler for a specific method
@@ -196,7 +197,13 @@ declare class RpcHandlerManager {
196
197
  private emitRegisterWithRetry;
197
198
  private registerAllHandlers;
198
199
  /**
199
- * Periodic re-registration every 60s as a safety net.
200
+ * Fast retry: 5 seconds after initial connect, re-register all handlers once.
201
+ * Covers the case where the first batch of registrations failed silently.
202
+ */
203
+ private scheduleFastRetry;
204
+ private cancelFastRetry;
205
+ /**
206
+ * Periodic re-registration every 30s as a safety net.
200
207
  */
201
208
  private startReregisterInterval;
202
209
  private stopReregisterInterval;
@@ -278,6 +285,66 @@ type TailscaleInfo = {
278
285
  serves?: TailscaleServeEntry[];
279
286
  };
280
287
 
288
+ /**
289
+ * Tunnel Provider abstraction — unified interface for all tunnel backends.
290
+ *
291
+ * Each provider (Tailscale, UPnP, Cloudflare, FRP, etc.) implements this
292
+ * interface. TunnelManager aggregates all providers and exposes a single API.
293
+ */
294
+
295
+ interface TunnelProvider {
296
+ /** Unique provider name: "tailscale" | "upnp" | "cloudflare" | "frp" */
297
+ readonly name: string;
298
+ /** Detect provider availability and list current tunnel entries. Never throws. */
299
+ detect(): Promise<TunnelProviderInfo>;
300
+ /** Add a tunnel mapping */
301
+ add(params: TunnelAddParams): Promise<TunnelOpResult>;
302
+ /** Remove a tunnel mapping */
303
+ remove(params: TunnelRemoveParams): Promise<TunnelOpResult>;
304
+ /** Toggle public/private access (not all providers support this) */
305
+ toggleAccess?(entry: TunnelEntry, publicAccess: boolean): Promise<TunnelOpResult>;
306
+ }
307
+ interface TunnelAddParams {
308
+ localPort: number;
309
+ remotePort?: number;
310
+ protocol?: string;
311
+ path?: string;
312
+ publicAccess?: boolean;
313
+ hostname?: string;
314
+ }
315
+ interface TunnelRemoveParams {
316
+ localPort?: number;
317
+ remotePort?: number;
318
+ path?: string;
319
+ hostname?: string;
320
+ /** Remove entire site (all routes for this hostname) */
321
+ removeEntireSite?: boolean;
322
+ }
323
+ interface TunnelOpResult {
324
+ success: boolean;
325
+ error?: string;
326
+ }
327
+
328
+ /**
329
+ * TunnelManager for happy-agent — aggregates all tunnel providers.
330
+ */
331
+
332
+ declare class TunnelManager {
333
+ private readonly providers;
334
+ private refreshTimer;
335
+ private leaseRenewalTimer;
336
+ private lastState;
337
+ constructor(providers: TunnelProvider[]);
338
+ detectAll(): Promise<TunnelState>;
339
+ getLastState(): TunnelState;
340
+ getProvider(name: string): TunnelProvider | undefined;
341
+ add(providerName: string, params: TunnelAddParams): Promise<TunnelOpResult>;
342
+ remove(providerName: string, params: TunnelRemoveParams): Promise<TunnelOpResult>;
343
+ toggleAccess(providerName: string, entry: TunnelEntry, publicAccess: boolean): Promise<TunnelOpResult>;
344
+ startRefresh(onChange: (state: TunnelState) => void, intervalMs?: number): void;
345
+ stopRefresh(): void;
346
+ }
347
+
281
348
  /**
282
349
  * Machine WebSocket client — trimmed from CLI's ApiMachineClient.
283
350
  *
@@ -313,6 +380,7 @@ declare class MachineClient {
313
380
  private keepAliveInterval;
314
381
  private tailscaleRefreshInterval;
315
382
  private lastTailscaleInfo;
383
+ private tunnelManager;
316
384
  private readonly token;
317
385
  private readonly serverUrl;
318
386
  private readonly onEphemeral?;
@@ -322,6 +390,8 @@ declare class MachineClient {
322
390
  updateDaemonState(handler: (state: DaemonState | null) => DaemonState): Promise<void>;
323
391
  /** Seed initial Tailscale info detected before connect. */
324
392
  setTailscaleInfo(info: TailscaleInfo): void;
393
+ /** Attach a TunnelManager for periodic tunnel state refresh. */
394
+ setTunnelManager(manager: TunnelManager): void;
325
395
  shutdown(): void;
326
396
  private startKeepAlive;
327
397
  private stopKeepAlive;
package/dist/index.mjs CHANGED
@@ -16,7 +16,7 @@ import { join as join$1, resolve } from 'path';
16
16
  import { realpathSync } from 'fs';
17
17
  import { tmpdir } from 'os';
18
18
 
19
- var version = "0.3.7";
19
+ var version = "0.3.9";
20
20
 
21
21
  function loadConfig() {
22
22
  const serverUrl = (process.env.HAPPY_SERVER_URL ?? "https://happyserve.xycloud.info").replace(/\/+$/, "");
@@ -620,6 +620,7 @@ class RpcHandlerManager {
620
620
  logger;
621
621
  socket = null;
622
622
  reregisterInterval = null;
623
+ fastRetryTimer = null;
623
624
  constructor(config) {
624
625
  this.scopePrefix = config.scopePrefix;
625
626
  this.encryptionKey = config.encryptionKey;
@@ -679,11 +680,13 @@ class RpcHandlerManager {
679
680
  onSocketConnect(socket) {
680
681
  this.socket = socket;
681
682
  this.registerAllHandlers(socket);
683
+ this.scheduleFastRetry(socket);
682
684
  this.startReregisterInterval();
683
685
  }
684
686
  onSocketDisconnect() {
685
687
  this.socket = null;
686
688
  this.stopReregisterInterval();
689
+ this.cancelFastRetry();
687
690
  }
688
691
  getHandlerCount() {
689
692
  return this.handlers.size;
@@ -739,7 +742,27 @@ class RpcHandlerManager {
739
742
  }
740
743
  }
741
744
  /**
742
- * Periodic re-registration every 60s as a safety net.
745
+ * Fast retry: 5 seconds after initial connect, re-register all handlers once.
746
+ * Covers the case where the first batch of registrations failed silently.
747
+ */
748
+ scheduleFastRetry(socket) {
749
+ this.cancelFastRetry();
750
+ this.fastRetryTimer = setTimeout(() => {
751
+ this.fastRetryTimer = null;
752
+ if (this.socket === socket && this.handlers.size > 0) {
753
+ this.logger("[RPC] Fast retry: re-registering all handlers");
754
+ this.registerAllHandlers(socket);
755
+ }
756
+ }, 5e3);
757
+ }
758
+ cancelFastRetry() {
759
+ if (this.fastRetryTimer) {
760
+ clearTimeout(this.fastRetryTimer);
761
+ this.fastRetryTimer = null;
762
+ }
763
+ }
764
+ /**
765
+ * Periodic re-registration every 30s as a safety net.
743
766
  */
744
767
  startReregisterInterval() {
745
768
  this.stopReregisterInterval();
@@ -747,7 +770,7 @@ class RpcHandlerManager {
747
770
  if (this.socket && this.handlers.size > 0) {
748
771
  this.registerAllHandlers(this.socket);
749
772
  }
750
- }, 6e4);
773
+ }, 3e4);
751
774
  }
752
775
  stopReregisterInterval() {
753
776
  if (this.reregisterInterval) {
@@ -1458,6 +1481,7 @@ class MachineClient {
1458
1481
  keepAliveInterval = null;
1459
1482
  tailscaleRefreshInterval = null;
1460
1483
  lastTailscaleInfo = null;
1484
+ tunnelManager = null;
1461
1485
  token;
1462
1486
  serverUrl;
1463
1487
  onEphemeral;
@@ -1627,6 +1651,10 @@ class MachineClient {
1627
1651
  setTailscaleInfo(info) {
1628
1652
  this.lastTailscaleInfo = info;
1629
1653
  }
1654
+ /** Attach a TunnelManager for periodic tunnel state refresh. */
1655
+ setTunnelManager(manager) {
1656
+ this.tunnelManager = manager;
1657
+ }
1630
1658
  shutdown() {
1631
1659
  logger.debug("[MACHINE] Shutting down");
1632
1660
  this.stopKeepAlive();
@@ -1658,14 +1686,22 @@ class MachineClient {
1658
1686
  const info = await detectTailscale();
1659
1687
  const serves = info.status === "connected" ? await detectTailscaleServe() : [];
1660
1688
  const fullInfo = { ...info, serves };
1661
- if (tailscaleChanged(this.lastTailscaleInfo, fullInfo)) {
1689
+ const tsChanged = tailscaleChanged(this.lastTailscaleInfo, fullInfo);
1690
+ if (tsChanged) {
1662
1691
  logger.debug(
1663
1692
  `[MACHINE] Tailscale changed: ${this.lastTailscaleInfo?.status} \u2192 ${fullInfo.status}, serves: ${serves.length}`
1664
1693
  );
1665
1694
  this.lastTailscaleInfo = fullInfo;
1695
+ }
1696
+ const tunnels = this.tunnelManager ? await this.tunnelManager.detectAll() : void 0;
1697
+ if (tsChanged || tunnels) {
1666
1698
  this.updateDaemonState((state) => {
1667
- if (!state) return { status: "running", tailscale: fullInfo };
1668
- return { ...state, tailscale: fullInfo };
1699
+ if (!state) return { status: "running", tailscale: fullInfo, tunnels };
1700
+ return {
1701
+ ...state,
1702
+ ...tsChanged ? { tailscale: fullInfo } : {},
1703
+ ...tunnels ? { tunnels } : {}
1704
+ };
1669
1705
  });
1670
1706
  }
1671
1707
  }, TAILSCALE_REFRESH_MS);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kmmao/happy-agent",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "CLI client for controlling Happy Coder agents remotely",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "license": "MIT",