@kmmao/happy-agent 0.3.7 → 0.3.8

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.8";
22
22
 
23
23
  function loadConfig() {
24
24
  const serverUrl = (process.env.HAPPY_SERVER_URL ?? "https://happyserve.xycloud.info").replace(/\/+$/, "");
@@ -1460,6 +1460,7 @@ class MachineClient {
1460
1460
  keepAliveInterval = null;
1461
1461
  tailscaleRefreshInterval = null;
1462
1462
  lastTailscaleInfo = null;
1463
+ tunnelManager = null;
1463
1464
  token;
1464
1465
  serverUrl;
1465
1466
  onEphemeral;
@@ -1629,6 +1630,10 @@ class MachineClient {
1629
1630
  setTailscaleInfo(info) {
1630
1631
  this.lastTailscaleInfo = info;
1631
1632
  }
1633
+ /** Attach a TunnelManager for periodic tunnel state refresh. */
1634
+ setTunnelManager(manager) {
1635
+ this.tunnelManager = manager;
1636
+ }
1632
1637
  shutdown() {
1633
1638
  logger.debug("[MACHINE] Shutting down");
1634
1639
  this.stopKeepAlive();
@@ -1660,14 +1665,22 @@ class MachineClient {
1660
1665
  const info = await detectTailscale();
1661
1666
  const serves = info.status === "connected" ? await detectTailscaleServe() : [];
1662
1667
  const fullInfo = { ...info, serves };
1663
- if (tailscaleChanged(this.lastTailscaleInfo, fullInfo)) {
1668
+ const tsChanged = tailscaleChanged(this.lastTailscaleInfo, fullInfo);
1669
+ if (tsChanged) {
1664
1670
  logger.debug(
1665
1671
  `[MACHINE] Tailscale changed: ${this.lastTailscaleInfo?.status} \u2192 ${fullInfo.status}, serves: ${serves.length}`
1666
1672
  );
1667
1673
  this.lastTailscaleInfo = fullInfo;
1674
+ }
1675
+ const tunnels = this.tunnelManager ? await this.tunnelManager.detectAll() : void 0;
1676
+ if (tsChanged || tunnels) {
1668
1677
  this.updateDaemonState((state) => {
1669
- if (!state) return { status: "running", tailscale: fullInfo };
1670
- return { ...state, tailscale: fullInfo };
1678
+ if (!state) return { status: "running", tailscale: fullInfo, tunnels };
1679
+ return {
1680
+ ...state,
1681
+ ...tsChanged ? { tailscale: fullInfo } : {},
1682
+ ...tunnels ? { tunnels } : {}
1683
+ };
1671
1684
  });
1672
1685
  }
1673
1686
  }, 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
 
@@ -278,6 +278,66 @@ type TailscaleInfo = {
278
278
  serves?: TailscaleServeEntry[];
279
279
  };
280
280
 
281
+ /**
282
+ * Tunnel Provider abstraction — unified interface for all tunnel backends.
283
+ *
284
+ * Each provider (Tailscale, UPnP, Cloudflare, FRP, etc.) implements this
285
+ * interface. TunnelManager aggregates all providers and exposes a single API.
286
+ */
287
+
288
+ interface TunnelProvider {
289
+ /** Unique provider name: "tailscale" | "upnp" | "cloudflare" | "frp" */
290
+ readonly name: string;
291
+ /** Detect provider availability and list current tunnel entries. Never throws. */
292
+ detect(): Promise<TunnelProviderInfo>;
293
+ /** Add a tunnel mapping */
294
+ add(params: TunnelAddParams): Promise<TunnelOpResult>;
295
+ /** Remove a tunnel mapping */
296
+ remove(params: TunnelRemoveParams): Promise<TunnelOpResult>;
297
+ /** Toggle public/private access (not all providers support this) */
298
+ toggleAccess?(entry: TunnelEntry, publicAccess: boolean): Promise<TunnelOpResult>;
299
+ }
300
+ interface TunnelAddParams {
301
+ localPort: number;
302
+ remotePort?: number;
303
+ protocol?: string;
304
+ path?: string;
305
+ publicAccess?: boolean;
306
+ hostname?: string;
307
+ }
308
+ interface TunnelRemoveParams {
309
+ localPort?: number;
310
+ remotePort?: number;
311
+ path?: string;
312
+ hostname?: string;
313
+ /** Remove entire site (all routes for this hostname) */
314
+ removeEntireSite?: boolean;
315
+ }
316
+ interface TunnelOpResult {
317
+ success: boolean;
318
+ error?: string;
319
+ }
320
+
321
+ /**
322
+ * TunnelManager for happy-agent — aggregates all tunnel providers.
323
+ */
324
+
325
+ declare class TunnelManager {
326
+ private readonly providers;
327
+ private refreshTimer;
328
+ private leaseRenewalTimer;
329
+ private lastState;
330
+ constructor(providers: TunnelProvider[]);
331
+ detectAll(): Promise<TunnelState>;
332
+ getLastState(): TunnelState;
333
+ getProvider(name: string): TunnelProvider | undefined;
334
+ add(providerName: string, params: TunnelAddParams): Promise<TunnelOpResult>;
335
+ remove(providerName: string, params: TunnelRemoveParams): Promise<TunnelOpResult>;
336
+ toggleAccess(providerName: string, entry: TunnelEntry, publicAccess: boolean): Promise<TunnelOpResult>;
337
+ startRefresh(onChange: (state: TunnelState) => void, intervalMs?: number): void;
338
+ stopRefresh(): void;
339
+ }
340
+
281
341
  /**
282
342
  * Machine WebSocket client — trimmed from CLI's ApiMachineClient.
283
343
  *
@@ -313,6 +373,7 @@ declare class MachineClient {
313
373
  private keepAliveInterval;
314
374
  private tailscaleRefreshInterval;
315
375
  private lastTailscaleInfo;
376
+ private tunnelManager;
316
377
  private readonly token;
317
378
  private readonly serverUrl;
318
379
  private readonly onEphemeral?;
@@ -322,6 +383,8 @@ declare class MachineClient {
322
383
  updateDaemonState(handler: (state: DaemonState | null) => DaemonState): Promise<void>;
323
384
  /** Seed initial Tailscale info detected before connect. */
324
385
  setTailscaleInfo(info: TailscaleInfo): void;
386
+ /** Attach a TunnelManager for periodic tunnel state refresh. */
387
+ setTunnelManager(manager: TunnelManager): void;
325
388
  shutdown(): void;
326
389
  private startKeepAlive;
327
390
  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
 
@@ -278,6 +278,66 @@ type TailscaleInfo = {
278
278
  serves?: TailscaleServeEntry[];
279
279
  };
280
280
 
281
+ /**
282
+ * Tunnel Provider abstraction — unified interface for all tunnel backends.
283
+ *
284
+ * Each provider (Tailscale, UPnP, Cloudflare, FRP, etc.) implements this
285
+ * interface. TunnelManager aggregates all providers and exposes a single API.
286
+ */
287
+
288
+ interface TunnelProvider {
289
+ /** Unique provider name: "tailscale" | "upnp" | "cloudflare" | "frp" */
290
+ readonly name: string;
291
+ /** Detect provider availability and list current tunnel entries. Never throws. */
292
+ detect(): Promise<TunnelProviderInfo>;
293
+ /** Add a tunnel mapping */
294
+ add(params: TunnelAddParams): Promise<TunnelOpResult>;
295
+ /** Remove a tunnel mapping */
296
+ remove(params: TunnelRemoveParams): Promise<TunnelOpResult>;
297
+ /** Toggle public/private access (not all providers support this) */
298
+ toggleAccess?(entry: TunnelEntry, publicAccess: boolean): Promise<TunnelOpResult>;
299
+ }
300
+ interface TunnelAddParams {
301
+ localPort: number;
302
+ remotePort?: number;
303
+ protocol?: string;
304
+ path?: string;
305
+ publicAccess?: boolean;
306
+ hostname?: string;
307
+ }
308
+ interface TunnelRemoveParams {
309
+ localPort?: number;
310
+ remotePort?: number;
311
+ path?: string;
312
+ hostname?: string;
313
+ /** Remove entire site (all routes for this hostname) */
314
+ removeEntireSite?: boolean;
315
+ }
316
+ interface TunnelOpResult {
317
+ success: boolean;
318
+ error?: string;
319
+ }
320
+
321
+ /**
322
+ * TunnelManager for happy-agent — aggregates all tunnel providers.
323
+ */
324
+
325
+ declare class TunnelManager {
326
+ private readonly providers;
327
+ private refreshTimer;
328
+ private leaseRenewalTimer;
329
+ private lastState;
330
+ constructor(providers: TunnelProvider[]);
331
+ detectAll(): Promise<TunnelState>;
332
+ getLastState(): TunnelState;
333
+ getProvider(name: string): TunnelProvider | undefined;
334
+ add(providerName: string, params: TunnelAddParams): Promise<TunnelOpResult>;
335
+ remove(providerName: string, params: TunnelRemoveParams): Promise<TunnelOpResult>;
336
+ toggleAccess(providerName: string, entry: TunnelEntry, publicAccess: boolean): Promise<TunnelOpResult>;
337
+ startRefresh(onChange: (state: TunnelState) => void, intervalMs?: number): void;
338
+ stopRefresh(): void;
339
+ }
340
+
281
341
  /**
282
342
  * Machine WebSocket client — trimmed from CLI's ApiMachineClient.
283
343
  *
@@ -313,6 +373,7 @@ declare class MachineClient {
313
373
  private keepAliveInterval;
314
374
  private tailscaleRefreshInterval;
315
375
  private lastTailscaleInfo;
376
+ private tunnelManager;
316
377
  private readonly token;
317
378
  private readonly serverUrl;
318
379
  private readonly onEphemeral?;
@@ -322,6 +383,8 @@ declare class MachineClient {
322
383
  updateDaemonState(handler: (state: DaemonState | null) => DaemonState): Promise<void>;
323
384
  /** Seed initial Tailscale info detected before connect. */
324
385
  setTailscaleInfo(info: TailscaleInfo): void;
386
+ /** Attach a TunnelManager for periodic tunnel state refresh. */
387
+ setTunnelManager(manager: TunnelManager): void;
325
388
  shutdown(): void;
326
389
  private startKeepAlive;
327
390
  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.8";
20
20
 
21
21
  function loadConfig() {
22
22
  const serverUrl = (process.env.HAPPY_SERVER_URL ?? "https://happyserve.xycloud.info").replace(/\/+$/, "");
@@ -1458,6 +1458,7 @@ class MachineClient {
1458
1458
  keepAliveInterval = null;
1459
1459
  tailscaleRefreshInterval = null;
1460
1460
  lastTailscaleInfo = null;
1461
+ tunnelManager = null;
1461
1462
  token;
1462
1463
  serverUrl;
1463
1464
  onEphemeral;
@@ -1627,6 +1628,10 @@ class MachineClient {
1627
1628
  setTailscaleInfo(info) {
1628
1629
  this.lastTailscaleInfo = info;
1629
1630
  }
1631
+ /** Attach a TunnelManager for periodic tunnel state refresh. */
1632
+ setTunnelManager(manager) {
1633
+ this.tunnelManager = manager;
1634
+ }
1630
1635
  shutdown() {
1631
1636
  logger.debug("[MACHINE] Shutting down");
1632
1637
  this.stopKeepAlive();
@@ -1658,14 +1663,22 @@ class MachineClient {
1658
1663
  const info = await detectTailscale();
1659
1664
  const serves = info.status === "connected" ? await detectTailscaleServe() : [];
1660
1665
  const fullInfo = { ...info, serves };
1661
- if (tailscaleChanged(this.lastTailscaleInfo, fullInfo)) {
1666
+ const tsChanged = tailscaleChanged(this.lastTailscaleInfo, fullInfo);
1667
+ if (tsChanged) {
1662
1668
  logger.debug(
1663
1669
  `[MACHINE] Tailscale changed: ${this.lastTailscaleInfo?.status} \u2192 ${fullInfo.status}, serves: ${serves.length}`
1664
1670
  );
1665
1671
  this.lastTailscaleInfo = fullInfo;
1672
+ }
1673
+ const tunnels = this.tunnelManager ? await this.tunnelManager.detectAll() : void 0;
1674
+ if (tsChanged || tunnels) {
1666
1675
  this.updateDaemonState((state) => {
1667
- if (!state) return { status: "running", tailscale: fullInfo };
1668
- return { ...state, tailscale: fullInfo };
1676
+ if (!state) return { status: "running", tailscale: fullInfo, tunnels };
1677
+ return {
1678
+ ...state,
1679
+ ...tsChanged ? { tailscale: fullInfo } : {},
1680
+ ...tunnels ? { tunnels } : {}
1681
+ };
1669
1682
  });
1670
1683
  }
1671
1684
  }, 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.8",
4
4
  "description": "CLI client for controlling Happy Coder agents remotely",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "license": "MIT",