@kdonev/termscape 0.1.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.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +288 -0
  3. package/dist/agents/env.d.ts +36 -0
  4. package/dist/agents/env.d.ts.map +1 -0
  5. package/dist/agents/env.js +61 -0
  6. package/dist/agents/env.js.map +1 -0
  7. package/dist/agents/profiles.d.ts +45 -0
  8. package/dist/agents/profiles.d.ts.map +1 -0
  9. package/dist/agents/profiles.js +123 -0
  10. package/dist/agents/profiles.js.map +1 -0
  11. package/dist/agents/resolve.d.ts +16 -0
  12. package/dist/agents/resolve.d.ts.map +1 -0
  13. package/dist/agents/resolve.js +71 -0
  14. package/dist/agents/resolve.js.map +1 -0
  15. package/dist/agents/router.d.ts +29 -0
  16. package/dist/agents/router.d.ts.map +1 -0
  17. package/dist/agents/router.js +83 -0
  18. package/dist/agents/router.js.map +1 -0
  19. package/dist/agents/tokens.d.ts +21 -0
  20. package/dist/agents/tokens.d.ts.map +1 -0
  21. package/dist/agents/tokens.js +39 -0
  22. package/dist/agents/tokens.js.map +1 -0
  23. package/dist/agents/wiring.d.ts +30 -0
  24. package/dist/agents/wiring.d.ts.map +1 -0
  25. package/dist/agents/wiring.js +110 -0
  26. package/dist/agents/wiring.js.map +1 -0
  27. package/dist/browser.d.ts +13 -0
  28. package/dist/browser.d.ts.map +1 -0
  29. package/dist/browser.js +34 -0
  30. package/dist/browser.js.map +1 -0
  31. package/dist/cli.d.ts +3 -0
  32. package/dist/cli.d.ts.map +1 -0
  33. package/dist/cli.js +200 -0
  34. package/dist/cli.js.map +1 -0
  35. package/dist/db/index.d.ts +14 -0
  36. package/dist/db/index.d.ts.map +1 -0
  37. package/dist/db/index.js +24 -0
  38. package/dist/db/index.js.map +1 -0
  39. package/dist/db/migrations.d.ts +15 -0
  40. package/dist/db/migrations.d.ts.map +1 -0
  41. package/dist/db/migrations.js +176 -0
  42. package/dist/db/migrations.js.map +1 -0
  43. package/dist/db/store.d.ts +71 -0
  44. package/dist/db/store.d.ts.map +1 -0
  45. package/dist/db/store.js +341 -0
  46. package/dist/db/store.js.map +1 -0
  47. package/dist/hub.d.ts +188 -0
  48. package/dist/hub.d.ts.map +1 -0
  49. package/dist/hub.js +636 -0
  50. package/dist/hub.js.map +1 -0
  51. package/dist/hub.tgz +0 -0
  52. package/dist/mcp/server.d.ts +31 -0
  53. package/dist/mcp/server.d.ts.map +1 -0
  54. package/dist/mcp/server.js +66 -0
  55. package/dist/mcp/server.js.map +1 -0
  56. package/dist/paths.d.ts +27 -0
  57. package/dist/paths.d.ts.map +1 -0
  58. package/dist/paths.js +31 -0
  59. package/dist/paths.js.map +1 -0
  60. package/dist/protocol/address.d.ts +15 -0
  61. package/dist/protocol/address.d.ts.map +1 -0
  62. package/dist/protocol/address.js +39 -0
  63. package/dist/protocol/address.js.map +1 -0
  64. package/dist/protocol/domain.d.ts +151 -0
  65. package/dist/protocol/domain.d.ts.map +1 -0
  66. package/dist/protocol/domain.js +104 -0
  67. package/dist/protocol/domain.js.map +1 -0
  68. package/dist/protocol/index.d.ts +7 -0
  69. package/dist/protocol/index.d.ts.map +1 -0
  70. package/dist/protocol/index.js +7 -0
  71. package/dist/protocol/index.js.map +1 -0
  72. package/dist/protocol/inject.d.ts +29 -0
  73. package/dist/protocol/inject.d.ts.map +1 -0
  74. package/dist/protocol/inject.js +40 -0
  75. package/dist/protocol/inject.js.map +1 -0
  76. package/dist/protocol/mcp-tools.d.ts +45 -0
  77. package/dist/protocol/mcp-tools.d.ts.map +1 -0
  78. package/dist/protocol/mcp-tools.js +46 -0
  79. package/dist/protocol/mcp-tools.js.map +1 -0
  80. package/dist/protocol/peer.d.ts +304 -0
  81. package/dist/protocol/peer.d.ts.map +1 -0
  82. package/dist/protocol/peer.js +183 -0
  83. package/dist/protocol/peer.js.map +1 -0
  84. package/dist/protocol/ws.d.ts +416 -0
  85. package/dist/protocol/ws.d.ts.map +1 -0
  86. package/dist/protocol/ws.js +119 -0
  87. package/dist/protocol/ws.js.map +1 -0
  88. package/dist/remote/deployer.d.ts +78 -0
  89. package/dist/remote/deployer.d.ts.map +1 -0
  90. package/dist/remote/deployer.js +216 -0
  91. package/dist/remote/deployer.js.map +1 -0
  92. package/dist/remote/enroll.d.ts +14 -0
  93. package/dist/remote/enroll.d.ts.map +1 -0
  94. package/dist/remote/enroll.js +244 -0
  95. package/dist/remote/enroll.js.map +1 -0
  96. package/dist/remote/join-script.d.ts +29 -0
  97. package/dist/remote/join-script.d.ts.map +1 -0
  98. package/dist/remote/join-script.js +709 -0
  99. package/dist/remote/join-script.js.map +1 -0
  100. package/dist/remote/join.d.ts +19 -0
  101. package/dist/remote/join.d.ts.map +1 -0
  102. package/dist/remote/join.js +195 -0
  103. package/dist/remote/join.js.map +1 -0
  104. package/dist/remote/lan.d.ts +45 -0
  105. package/dist/remote/lan.d.ts.map +1 -0
  106. package/dist/remote/lan.js +105 -0
  107. package/dist/remote/lan.js.map +1 -0
  108. package/dist/remote/peer-serve.d.ts +27 -0
  109. package/dist/remote/peer-serve.d.ts.map +1 -0
  110. package/dist/remote/peer-serve.js +274 -0
  111. package/dist/remote/peer-serve.js.map +1 -0
  112. package/dist/remote/peer.d.ts +62 -0
  113. package/dist/remote/peer.d.ts.map +1 -0
  114. package/dist/remote/peer.js +250 -0
  115. package/dist/remote/peer.js.map +1 -0
  116. package/dist/remote/registry.d.ts +104 -0
  117. package/dist/remote/registry.d.ts.map +1 -0
  118. package/dist/remote/registry.js +359 -0
  119. package/dist/remote/registry.js.map +1 -0
  120. package/dist/remote/tarball.d.ts +9 -0
  121. package/dist/remote/tarball.d.ts.map +1 -0
  122. package/dist/remote/tarball.js +22 -0
  123. package/dist/remote/tarball.js.map +1 -0
  124. package/dist/server.d.ts +54 -0
  125. package/dist/server.d.ts.map +1 -0
  126. package/dist/server.js +438 -0
  127. package/dist/server.js.map +1 -0
  128. package/dist/session/manager.d.ts +104 -0
  129. package/dist/session/manager.d.ts.map +1 -0
  130. package/dist/session/manager.js +421 -0
  131. package/dist/session/manager.js.map +1 -0
  132. package/dist/session/pty.d.ts +114 -0
  133. package/dist/session/pty.d.ts.map +1 -0
  134. package/dist/session/pty.js +300 -0
  135. package/dist/session/pty.js.map +1 -0
  136. package/package.json +49 -0
  137. package/web/assets/index-B2JtCtHa.css +1 -0
  138. package/web/assets/index-BOqZOoep.js +107 -0
  139. package/web/index.html +13 -0
@@ -0,0 +1,104 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { Host, PeerAgent, Session, WindowRect } from '../protocol/index.js';
3
+ import type { Store } from '../db/store.js';
4
+ import type { WebSocket as WsSocket } from 'ws';
5
+ import { PeerConnection } from './peer.js';
6
+ /**
7
+ * Tracks connected peer hubs and the sessions they own.
8
+ *
9
+ * Remote sessions are held in memory only: the peer is the source of truth for
10
+ * its own sessions and snapshots, so a stale local copy would be worse than
11
+ * none. What *is* persisted locally is the window layout, because that is the
12
+ * user's view of their canvas rather than the peer's state.
13
+ */
14
+ export declare class PeerRegistry extends EventEmitter {
15
+ private readonly store;
16
+ private readonly hubVersion;
17
+ private readonly peers;
18
+ /** hostId -> address -> session, as last reported by that peer. */
19
+ private readonly remote;
20
+ constructor(store: Store, hubVersion: string);
21
+ add(host: Host, url: string, token: string): PeerConnection;
22
+ /**
23
+ * Adopt a socket an enrolled host opened towards us. Same peer, same events,
24
+ * same routing — only the direction of the dial differs, and nothing
25
+ * downstream of here can tell.
26
+ */
27
+ addInbound(host: Host, socket: WsSocket, remoteVersion: string): PeerConnection;
28
+ private create;
29
+ remove(hostId: string): void;
30
+ closeAll(): void;
31
+ private updateHost;
32
+ /**
33
+ * Overlay the locally stored window rect. The peer has no idea where the
34
+ * user put its window, and should not: layout is a local concern.
35
+ */
36
+ private withLocalLayout;
37
+ /** Remote hosts get their own band of canvas, well clear of local windows. */
38
+ private placeRemote;
39
+ peer(hostId: string): PeerConnection | null;
40
+ /** Whether there is any attached machine to tell things to. */
41
+ get any(): boolean;
42
+ /**
43
+ * Tell every attached host who is on the canvas.
44
+ *
45
+ * `forHost` receives the whole directory minus that host's own agents: it
46
+ * already has those, and listing them twice is worse than not sending them.
47
+ */
48
+ announce(forHost: (hostId: string) => PeerAgent[]): void;
49
+ /** Every remote session across every peer. */
50
+ sessions(): Session[];
51
+ find(address: string): {
52
+ peer: PeerConnection;
53
+ session: Session;
54
+ } | null;
55
+ /**
56
+ * Which host owns an address. The peer's workspace rows do not exist in the
57
+ * local database (remote state is not cached), so the registry is the only
58
+ * thing that knows this mapping.
59
+ */
60
+ hostIdFor(address: string): string | null;
61
+ /**
62
+ * Close a remote window for good.
63
+ *
64
+ * Returns false for an address this registry does not own, so the caller can
65
+ * fall through to its local path — the same shape as saveLayout.
66
+ *
67
+ * The window goes either way: the user closed it, and making that conditional
68
+ * on a host being up would be its own kind of broken. What varies is only
69
+ * when the owning hub hears about it.
70
+ */
71
+ removeSession(address: string): Promise<boolean>;
72
+ /**
73
+ * Replay removals queued while a host was unreachable. Each one is
74
+ * idempotent over there, so a row whose removal already landed costs one
75
+ * round trip and nothing else.
76
+ */
77
+ private flushPendingRemovals;
78
+ /** Drop every local trace of a remote session: cache, layout, and queue. */
79
+ private forget;
80
+ private isPendingRemoval;
81
+ saveLayout(address: string, rect: WindowRect): boolean;
82
+ deliver(from: string, to: string, body: string): Promise<void>;
83
+ /**
84
+ * Start an agent on a peer. The receiving hub creates the workspace if it
85
+ * does not have one by that name, so a host needs no setup before its first
86
+ * session — which is what makes picking a host at workspace-creation time
87
+ * enough on its own.
88
+ */
89
+ startSession(hostId: string, req: {
90
+ workspaceName: string;
91
+ rootPath: string;
92
+ profile: string;
93
+ name?: string;
94
+ spawnedByAddress: string | null;
95
+ }): Promise<Session>;
96
+ /**
97
+ * Ask a peer to stop its whole hub. Best effort by nature: it may already be
98
+ * gone, and it stops answering the moment it obeys.
99
+ */
100
+ requestShutdown(hostId: string): Promise<boolean>;
101
+ stopSession(address: string): Promise<void>;
102
+ readScreen(address: string, lines?: number): Promise<unknown>;
103
+ }
104
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/remote/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,KAAK,EACV,IAAI,EACJ,SAAS,EAET,OAAO,EACP,UAAU,EACX,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,KAAK,EAAE,SAAS,IAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE3C;;;;;;;GAOG;AACH,qBAAa,YAAa,SAAQ,YAAY;IAM1C,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAN7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAC3D,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2C;gBAG/C,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,MAAM;IAOrC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc;IAM3D;;;;OAIG;IACH,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,GAAG,cAAc;IAM/E,OAAO,CAAC,MAAM;IA6Fd,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAO5B,QAAQ,IAAI,IAAI;IAQhB,OAAO,CAAC,UAAU;IAYlB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAavB,8EAA8E;IAC9E,OAAO,CAAC,WAAW;IAcnB,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAI3C,+DAA+D;IAC/D,IAAI,GAAG,IAAI,OAAO,CAEjB;IAED;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,SAAS,EAAE,GAAG,IAAI;IAYxD,8CAA8C;IAC9C,QAAQ,IAAI,OAAO,EAAE;IAarB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;IASxE;;;;OAIG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IASzC;;;;;;;;;OASG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBtD;;;;OAIG;YACW,oBAAoB;IAWlC,4EAA4E;IAC5E,OAAO,CAAC,MAAM;IAOd,OAAO,CAAC,gBAAgB;IAMxB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO;IAchD,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASpE;;;;;OAKG;IACG,YAAY,CAChB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE;QACH,aAAa,EAAE,MAAM,CAAC;QACtB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;KACjC,GACA,OAAO,CAAC,OAAO,CAAC;IAOnB;;;OAGG;IACG,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYjD,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM3C,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAKpE"}
@@ -0,0 +1,359 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { DEFAULT_WINDOW } from '../db/store.js';
4
+ import { PeerConnection } from './peer.js';
5
+ /**
6
+ * Tracks connected peer hubs and the sessions they own.
7
+ *
8
+ * Remote sessions are held in memory only: the peer is the source of truth for
9
+ * its own sessions and snapshots, so a stale local copy would be worse than
10
+ * none. What *is* persisted locally is the window layout, because that is the
11
+ * user's view of their canvas rather than the peer's state.
12
+ */
13
+ export class PeerRegistry extends EventEmitter {
14
+ store;
15
+ hubVersion;
16
+ peers = new Map();
17
+ /** hostId -> address -> session, as last reported by that peer. */
18
+ remote = new Map();
19
+ constructor(store, hubVersion) {
20
+ super();
21
+ this.store = store;
22
+ this.hubVersion = hubVersion;
23
+ }
24
+ /* ---------------------------------------------------------- connection */
25
+ add(host, url, token) {
26
+ const peer = this.create(host, { url, token });
27
+ peer.connect();
28
+ return peer;
29
+ }
30
+ /**
31
+ * Adopt a socket an enrolled host opened towards us. Same peer, same events,
32
+ * same routing — only the direction of the dial differs, and nothing
33
+ * downstream of here can tell.
34
+ */
35
+ addInbound(host, socket, remoteVersion) {
36
+ const peer = this.create(host, {});
37
+ peer.adopt(socket, remoteVersion);
38
+ return peer;
39
+ }
40
+ create(host, opts) {
41
+ const existing = this.peers.get(host.id);
42
+ if (existing?.connected) {
43
+ // Two live processes are claiming one host - a duplicate left behind by
44
+ // an interrupted install, say. Taking the newcomer and merely dropping
45
+ // the old one is not enough: it reconnects, displaces the newcomer, and
46
+ // the two evict each other forever. Tell it to stop instead.
47
+ void existing
48
+ .request({ t: 'shutdown', id: randomUUID() })
49
+ .catch(() => {
50
+ // It may go away before answering, which is the point.
51
+ });
52
+ }
53
+ this.remove(host.id);
54
+ const peer = new PeerConnection({
55
+ hostId: host.id,
56
+ ...opts,
57
+ hubVersion: this.hubVersion,
58
+ });
59
+ peer.on('connected', (version) => {
60
+ this.updateHost(host.id, { state: 'connected', hubVersion: version, error: null });
61
+ // A host that was unreachable when the user closed one of its windows
62
+ // has an instruction waiting for it.
63
+ void this.flushPendingRemovals(host.id, peer);
64
+ // It knows nothing about the rest of the canvas until it is told, and
65
+ // its agents are running the moment it reconnects.
66
+ this.emit('directoryStale');
67
+ });
68
+ peer.on('disconnected', () => {
69
+ this.updateHost(host.id, { state: 'disconnected' });
70
+ // Windows stay on the canvas (layout is local) but go offline, rather
71
+ // than vanishing and losing the user's arrangement.
72
+ this.emit('peerSessionsChanged', host.id);
73
+ });
74
+ peer.on('error', (err) => {
75
+ this.updateHost(host.id, { state: 'error', error: err.message });
76
+ });
77
+ peer.on('sessions', (sessions) => {
78
+ // A removal still queued for this host must not put its window back on
79
+ // the canvas in the meantime — and withLocalLayout would mint it a fresh
80
+ // layout row on the way past, undoing the close a second time.
81
+ const pending = new Set(this.store.pendingRemovals(host.id).map((p) => p.address));
82
+ const map = new Map();
83
+ for (const s of sessions) {
84
+ if (pending.has(s.address))
85
+ continue;
86
+ map.set(s.address, this.withLocalLayout(host.id, s));
87
+ }
88
+ this.remote.set(host.id, map);
89
+ this.emit('peerSessionsChanged', host.id);
90
+ });
91
+ peer.on('sessionUpserted', (s) => {
92
+ if (this.isPendingRemoval(host.id, s.address))
93
+ return;
94
+ const map = this.remote.get(host.id) ?? new Map();
95
+ map.set(s.address, this.withLocalLayout(host.id, s));
96
+ this.remote.set(host.id, map);
97
+ this.emit('peerSession', this.withLocalLayout(host.id, s));
98
+ });
99
+ peer.on('sessionRemoved', (address) => {
100
+ this.forget(address);
101
+ });
102
+ peer.on('output', (address, data) => {
103
+ this.emit('output', address, data);
104
+ });
105
+ // An agent over there acting on an address its own hub cannot resolve.
106
+ // We do it — we are the only hub that knows where every address on this
107
+ // canvas lives — and the answer goes back over the same link.
108
+ peer.on('relay', (relayId, ask) => {
109
+ this.emit('relay', ask, (ok, result, error) => {
110
+ void peer
111
+ .request({ t: 'relayResult', id: randomUUID(), relayId, ok, result, error })
112
+ .catch(() => {
113
+ // The host went away between asking and being answered. Its own
114
+ // relay timeout is what tells its agent, and there is nothing
115
+ // useful to do about it here.
116
+ });
117
+ });
118
+ });
119
+ this.peers.set(host.id, peer);
120
+ this.remote.set(host.id, new Map());
121
+ this.updateHost(host.id, { state: 'connecting' });
122
+ return peer;
123
+ }
124
+ remove(hostId) {
125
+ this.peers.get(hostId)?.close();
126
+ this.peers.delete(hostId);
127
+ this.remote.delete(hostId);
128
+ this.emit('peerSessionsChanged', hostId);
129
+ }
130
+ closeAll() {
131
+ for (const p of this.peers.values())
132
+ p.close();
133
+ this.peers.clear();
134
+ this.remote.clear();
135
+ }
136
+ /* --------------------------------------------------------------- state */
137
+ updateHost(hostId, patch) {
138
+ const host = this.store.listHosts().find((h) => h.id === hostId);
139
+ if (!host)
140
+ return;
141
+ const next = {
142
+ ...host,
143
+ ...patch,
144
+ lastSeenAt: patch.state === 'connected' ? Date.now() : host.lastSeenAt,
145
+ };
146
+ this.store.upsertHost(next);
147
+ this.emit('host', next);
148
+ }
149
+ /**
150
+ * Overlay the locally stored window rect. The peer has no idea where the
151
+ * user put its window, and should not: layout is a local concern.
152
+ */
153
+ withLocalLayout(hostId, s) {
154
+ // Re-key remote sessions by address. The peer's internal session id means
155
+ // nothing here and could collide with a local one, whereas the address is
156
+ // already globally unique and is what every cross-hub call uses. This
157
+ // makes attach/input/resize routing fall out of a single lookup.
158
+ const base = { ...s, id: s.address };
159
+ const saved = this.store.getRemoteWindows().get(s.address);
160
+ if (saved)
161
+ return { ...base, window: saved };
162
+ const placed = this.placeRemote(hostId);
163
+ this.store.saveRemoteWindow(s.address, hostId, placed);
164
+ return { ...base, window: placed };
165
+ }
166
+ /** Remote hosts get their own band of canvas, well clear of local windows. */
167
+ placeRemote(hostId) {
168
+ const hostIndex = Math.max(0, this.store.listHosts().findIndex((h) => h.id === hostId));
169
+ const existing = this.remote.get(hostId)?.size ?? 0;
170
+ return {
171
+ ...DEFAULT_WINDOW,
172
+ x: existing % 3 * (DEFAULT_WINDOW.w + 40),
173
+ y: 900 + hostIndex * 1200 + Math.floor(existing / 3) * (DEFAULT_WINDOW.h + 40),
174
+ z: existing,
175
+ };
176
+ }
177
+ peer(hostId) {
178
+ return this.peers.get(hostId) ?? null;
179
+ }
180
+ /** Whether there is any attached machine to tell things to. */
181
+ get any() {
182
+ return this.peers.size > 0;
183
+ }
184
+ /**
185
+ * Tell every attached host who is on the canvas.
186
+ *
187
+ * `forHost` receives the whole directory minus that host's own agents: it
188
+ * already has those, and listing them twice is worse than not sending them.
189
+ */
190
+ announce(forHost) {
191
+ for (const [hostId, peer] of this.peers) {
192
+ if (!peer.connected)
193
+ continue;
194
+ void peer
195
+ .request({ t: 'directory', id: randomUUID(), agents: forHost(hostId) })
196
+ .catch(() => {
197
+ // A host that dropped mid-announce gets the current directory when
198
+ // it reconnects, which is the same thing a moment later.
199
+ });
200
+ }
201
+ }
202
+ /** Every remote session across every peer. */
203
+ sessions() {
204
+ const out = [];
205
+ for (const [hostId, map] of this.remote) {
206
+ const online = this.peers.get(hostId)?.connected ?? false;
207
+ for (const s of map.values()) {
208
+ // A peer we cannot reach cannot have running sessions from our point
209
+ // of view; showing them as running would be a lie.
210
+ out.push(online ? s : { ...s, state: 'stopped', status: 'unknown' });
211
+ }
212
+ }
213
+ return out;
214
+ }
215
+ find(address) {
216
+ for (const [hostId, map] of this.remote) {
217
+ const s = map.get(address);
218
+ const peer = this.peers.get(hostId);
219
+ if (s && peer)
220
+ return { peer, session: s };
221
+ }
222
+ return null;
223
+ }
224
+ /**
225
+ * Which host owns an address. The peer's workspace rows do not exist in the
226
+ * local database (remote state is not cached), so the registry is the only
227
+ * thing that knows this mapping.
228
+ */
229
+ hostIdFor(address) {
230
+ for (const [hostId, map] of this.remote) {
231
+ if (map.has(address))
232
+ return hostId;
233
+ }
234
+ return null;
235
+ }
236
+ /* ------------------------------------------------------------ removal */
237
+ /**
238
+ * Close a remote window for good.
239
+ *
240
+ * Returns false for an address this registry does not own, so the caller can
241
+ * fall through to its local path — the same shape as saveLayout.
242
+ *
243
+ * The window goes either way: the user closed it, and making that conditional
244
+ * on a host being up would be its own kind of broken. What varies is only
245
+ * when the owning hub hears about it.
246
+ */
247
+ async removeSession(address) {
248
+ const hostId = this.hostIdFor(address);
249
+ if (!hostId)
250
+ return false;
251
+ const peer = this.peers.get(hostId);
252
+ this.forget(address);
253
+ try {
254
+ if (!peer?.connected)
255
+ throw new Error(`host ${hostId} is not connected`);
256
+ await peer.request({ t: 'removeSession', id: randomUUID(), address });
257
+ }
258
+ catch {
259
+ // Unreachable, or it went away mid-request. The instruction waits in the
260
+ // database rather than dying with this process, which is precisely what
261
+ // used to let a closed terminal come back after a restart.
262
+ this.store.addPendingRemoval(address, hostId);
263
+ }
264
+ return true;
265
+ }
266
+ /**
267
+ * Replay removals queued while a host was unreachable. Each one is
268
+ * idempotent over there, so a row whose removal already landed costs one
269
+ * round trip and nothing else.
270
+ */
271
+ async flushPendingRemovals(hostId, peer) {
272
+ for (const { address } of this.store.pendingRemovals(hostId)) {
273
+ try {
274
+ await peer.request({ t: 'removeSession', id: randomUUID(), address });
275
+ this.forget(address);
276
+ }
277
+ catch {
278
+ // Still out of reach. The row survives for the next connection.
279
+ }
280
+ }
281
+ }
282
+ /** Drop every local trace of a remote session: cache, layout, and queue. */
283
+ forget(address) {
284
+ for (const map of this.remote.values())
285
+ map.delete(address);
286
+ this.store.removeRemoteWindow(address);
287
+ this.store.clearPendingRemoval(address);
288
+ this.emit('peerSessionRemoved', address);
289
+ }
290
+ isPendingRemoval(hostId, address) {
291
+ return this.store.pendingRemovals(hostId).some((p) => p.address === address);
292
+ }
293
+ /* ------------------------------------------------------------- layout */
294
+ saveLayout(address, rect) {
295
+ for (const [hostId, map] of this.remote) {
296
+ if (map.has(address)) {
297
+ this.store.saveRemoteWindow(address, hostId, rect);
298
+ const s = map.get(address);
299
+ map.set(address, { ...s, window: rect });
300
+ return true;
301
+ }
302
+ }
303
+ return false;
304
+ }
305
+ /* ------------------------------------------------------------ requests */
306
+ async deliver(from, to, body) {
307
+ const found = this.find(to);
308
+ if (!found)
309
+ throw new Error(`no agent at address "${to}"`);
310
+ if (!found.peer.connected) {
311
+ throw new Error(`host for "${to}" is not connected`);
312
+ }
313
+ await found.peer.request({ t: 'deliver', id: randomUUID(), from, to, body });
314
+ }
315
+ /**
316
+ * Start an agent on a peer. The receiving hub creates the workspace if it
317
+ * does not have one by that name, so a host needs no setup before its first
318
+ * session — which is what makes picking a host at workspace-creation time
319
+ * enough on its own.
320
+ */
321
+ async startSession(hostId, req) {
322
+ const peer = this.peers.get(hostId);
323
+ if (!peer)
324
+ throw new Error(`host ${hostId} is not connected`);
325
+ if (!peer.connected)
326
+ throw new Error(`host ${hostId} is not connected`);
327
+ return peer.request({ t: 'startSession', id: randomUUID(), ...req });
328
+ }
329
+ /**
330
+ * Ask a peer to stop its whole hub. Best effort by nature: it may already be
331
+ * gone, and it stops answering the moment it obeys.
332
+ */
333
+ async requestShutdown(hostId) {
334
+ const peer = this.peers.get(hostId);
335
+ if (!peer?.connected)
336
+ return false;
337
+ try {
338
+ await peer.request({ t: 'shutdown', id: randomUUID() });
339
+ return true;
340
+ }
341
+ catch {
342
+ // It went away before replying, which is the outcome we wanted.
343
+ return false;
344
+ }
345
+ }
346
+ async stopSession(address) {
347
+ const found = this.find(address);
348
+ if (!found)
349
+ throw new Error(`no agent at address "${address}"`);
350
+ await found.peer.request({ t: 'stopSession', id: randomUUID(), address });
351
+ }
352
+ async readScreen(address, lines) {
353
+ const found = this.find(address);
354
+ if (!found)
355
+ throw new Error(`no agent at address "${address}"`);
356
+ return found.peer.request({ t: 'readScreen', id: randomUUID(), address, lines });
357
+ }
358
+ }
359
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/remote/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The packed hub, so a machine being attached has something to install.
3
+ *
4
+ * Built by scripts/pack-hub.mjs into dist/, which is why neither attach path
5
+ * needs the operator to run `npm pack` by hand any more. The env var stays as
6
+ * an override for anyone testing an unreleased build.
7
+ */
8
+ export declare function hubTarballPath(): string | null;
9
+ //# sourceMappingURL=tarball.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tarball.d.ts","sourceRoot":"","sources":["../../src/remote/tarball.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAS9C"}
@@ -0,0 +1,22 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /**
5
+ * The packed hub, so a machine being attached has something to install.
6
+ *
7
+ * Built by scripts/pack-hub.mjs into dist/, which is why neither attach path
8
+ * needs the operator to run `npm pack` by hand any more. The env var stays as
9
+ * an override for anyone testing an unreleased build.
10
+ */
11
+ export function hubTarballPath() {
12
+ const override = process.env.TERMSCAPE_HUB_TARBALL;
13
+ if (override && existsSync(override))
14
+ return override;
15
+ const here = dirname(fileURLToPath(import.meta.url));
16
+ const candidates = [
17
+ join(here, '..', 'hub.tgz'), // dist/hub.tgz, beside the compiled cli
18
+ join(here, '..', '..', 'dist', 'hub.tgz'),
19
+ ];
20
+ return candidates.find((p) => existsSync(p)) ?? null;
21
+ }
22
+ //# sourceMappingURL=tarball.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tarball.js","sourceRoot":"","sources":["../../src/remote/tarball.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;;;;GAMG;AACH,MAAM,UAAU,cAAc;IAC5B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;IACnD,IAAI,QAAQ,IAAI,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IACtD,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,MAAM,UAAU,GAAG;QACjB,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,wCAAwC;QACrE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC;KAC1C,CAAC;IACF,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AACvD,CAAC"}
@@ -0,0 +1,54 @@
1
+ import { type FastifyInstance } from 'fastify';
2
+ import { Hub } from './hub.js';
3
+ import { type PeerServer } from './remote/peer-serve.js';
4
+ import { type Enrollment } from './remote/enroll.js';
5
+ export interface ServeOptions {
6
+ hub: Hub;
7
+ /**
8
+ * Bind address. Defaults to loopback; anything else is a deliberate choice
9
+ * by the operator to let other machines reach the join page.
10
+ */
11
+ host?: string;
12
+ /**
13
+ * A port to bind exactly (0 lets the OS pick), or a list of candidates to
14
+ * try in order until one is free.
15
+ */
16
+ port?: number | number[];
17
+ clientToken: string;
18
+ /** Remote hubs serve no UI. */
19
+ headless?: boolean;
20
+ }
21
+ export interface ServeResult {
22
+ app: FastifyInstance;
23
+ /**
24
+ * How to reach this hub *from this machine*. Agents get it in their
25
+ * generated MCP config, so it stays 127.0.0.1 whenever the bind answers
26
+ * there — an agent's tool endpoint has no business being advertised on the
27
+ * network. It only becomes a real address if the operator bound one
28
+ * specific non-loopback interface, where loopback genuinely will not answer.
29
+ */
30
+ origin: string;
31
+ /** Where another machine can reach the join page, or null when loopback-bound. */
32
+ enrollOrigin: string | null;
33
+ /** The same origin by IP, when `enrollOrigin` uses this machine's name. */
34
+ enrollAltOrigin: string | null;
35
+ port: number;
36
+ /** So a joining hub can answer the canvas hub over a socket it dialled. */
37
+ peerServer: PeerServer;
38
+ /** Mints the single-use tokens the join installers carry. */
39
+ enrollment: Enrollment;
40
+ }
41
+ /**
42
+ * Ports to try before falling back to whatever the OS hands out.
43
+ *
44
+ * The join page is the one URL someone has to type by hand, on a different
45
+ * machine, from memory — it carries no token precisely so it can be typed. A
46
+ * five-digit ephemeral port makes that miserable, so prefer something a person
47
+ * can hold in their head.
48
+ *
49
+ * Ordered by memorability, skipping ports that common dev tooling squats on:
50
+ * 3000/8000/8080 (everything), 5555 (adb), 8888 (Jupyter), 2222 (alt ssh).
51
+ */
52
+ export declare const MEMORABLE_PORTS: number[];
53
+ export declare function serve(opts: ServeOptions): Promise<ServeResult>;
54
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA,OAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AAYxD,OAAO,EAAE,GAAG,EAAe,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAC3E,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AASzE,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,GAAG,CAAC;IACT;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,eAAe,CAAC;IACrB;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,2EAA2E;IAC3E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,UAAU,EAAE,UAAU,CAAC;IACvB,6DAA6D;IAC7D,UAAU,EAAE,UAAU,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,UAAmD,CAAC;AAsBhF,wBAAsB,KAAK,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA2apE"}