@termlink/cli 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 (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/dist/auth/login.d.ts +55 -0
  4. package/dist/auth/login.d.ts.map +1 -0
  5. package/dist/auth/login.js +238 -0
  6. package/dist/auth/login.js.map +1 -0
  7. package/dist/auth/oauth-defaults.d.ts +9 -0
  8. package/dist/auth/oauth-defaults.d.ts.map +1 -0
  9. package/dist/auth/oauth-defaults.js +9 -0
  10. package/dist/auth/oauth-defaults.js.map +1 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +319 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/errors.d.ts +8 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +13 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/index.d.ts +20 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +19 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/providers/claude/history.d.ts +15 -0
  24. package/dist/providers/claude/history.d.ts.map +1 -0
  25. package/dist/providers/claude/history.js +71 -0
  26. package/dist/providers/claude/history.js.map +1 -0
  27. package/dist/providers/claude/input-queue.d.ts +8 -0
  28. package/dist/providers/claude/input-queue.d.ts.map +1 -0
  29. package/dist/providers/claude/input-queue.js +34 -0
  30. package/dist/providers/claude/input-queue.js.map +1 -0
  31. package/dist/providers/claude/mapper.d.ts +21 -0
  32. package/dist/providers/claude/mapper.d.ts.map +1 -0
  33. package/dist/providers/claude/mapper.js +136 -0
  34. package/dist/providers/claude/mapper.js.map +1 -0
  35. package/dist/providers/claude/provider.d.ts +46 -0
  36. package/dist/providers/claude/provider.d.ts.map +1 -0
  37. package/dist/providers/claude/provider.js +148 -0
  38. package/dist/providers/claude/provider.js.map +1 -0
  39. package/dist/providers/claude/session.d.ts +27 -0
  40. package/dist/providers/claude/session.d.ts.map +1 -0
  41. package/dist/providers/claude/session.js +353 -0
  42. package/dist/providers/claude/session.js.map +1 -0
  43. package/dist/providers/codex/app-server.d.ts +24 -0
  44. package/dist/providers/codex/app-server.d.ts.map +1 -0
  45. package/dist/providers/codex/app-server.js +79 -0
  46. package/dist/providers/codex/app-server.js.map +1 -0
  47. package/dist/providers/codex/command.d.ts +12 -0
  48. package/dist/providers/codex/command.d.ts.map +1 -0
  49. package/dist/providers/codex/command.js +60 -0
  50. package/dist/providers/codex/command.js.map +1 -0
  51. package/dist/providers/codex/history.d.ts +5 -0
  52. package/dist/providers/codex/history.d.ts.map +1 -0
  53. package/dist/providers/codex/history.js +33 -0
  54. package/dist/providers/codex/history.js.map +1 -0
  55. package/dist/providers/codex/mapper.d.ts +13 -0
  56. package/dist/providers/codex/mapper.d.ts.map +1 -0
  57. package/dist/providers/codex/mapper.js +83 -0
  58. package/dist/providers/codex/mapper.js.map +1 -0
  59. package/dist/providers/codex/protocol.d.ts +223 -0
  60. package/dist/providers/codex/protocol.d.ts.map +1 -0
  61. package/dist/providers/codex/protocol.js +11 -0
  62. package/dist/providers/codex/protocol.js.map +1 -0
  63. package/dist/providers/codex/provider.d.ts +28 -0
  64. package/dist/providers/codex/provider.d.ts.map +1 -0
  65. package/dist/providers/codex/provider.js +116 -0
  66. package/dist/providers/codex/provider.js.map +1 -0
  67. package/dist/providers/codex/rpc.d.ts +23 -0
  68. package/dist/providers/codex/rpc.d.ts.map +1 -0
  69. package/dist/providers/codex/rpc.js +104 -0
  70. package/dist/providers/codex/rpc.js.map +1 -0
  71. package/dist/providers/codex/session.d.ts +16 -0
  72. package/dist/providers/codex/session.d.ts.map +1 -0
  73. package/dist/providers/codex/session.js +262 -0
  74. package/dist/providers/codex/session.js.map +1 -0
  75. package/dist/providers/display.d.ts +9 -0
  76. package/dist/providers/display.d.ts.map +1 -0
  77. package/dist/providers/display.js +21 -0
  78. package/dist/providers/display.js.map +1 -0
  79. package/dist/providers/terminal.d.ts +24 -0
  80. package/dist/providers/terminal.d.ts.map +1 -0
  81. package/dist/providers/terminal.js +33 -0
  82. package/dist/providers/terminal.js.map +1 -0
  83. package/dist/providers/types.d.ts +72 -0
  84. package/dist/providers/types.d.ts.map +1 -0
  85. package/dist/providers/types.js +2 -0
  86. package/dist/providers/types.js.map +1 -0
  87. package/dist/relay/api.d.ts +13 -0
  88. package/dist/relay/api.d.ts.map +1 -0
  89. package/dist/relay/api.js +75 -0
  90. package/dist/relay/api.js.map +1 -0
  91. package/dist/relay/device.d.ts +26 -0
  92. package/dist/relay/device.d.ts.map +1 -0
  93. package/dist/relay/device.js +83 -0
  94. package/dist/relay/device.js.map +1 -0
  95. package/dist/relay/relay-host.d.ts +41 -0
  96. package/dist/relay/relay-host.d.ts.map +1 -0
  97. package/dist/relay/relay-host.js +238 -0
  98. package/dist/relay/relay-host.js.map +1 -0
  99. package/dist/server/coalesce.d.ts +15 -0
  100. package/dist/server/coalesce.d.ts.map +1 -0
  101. package/dist/server/coalesce.js +50 -0
  102. package/dist/server/coalesce.js.map +1 -0
  103. package/dist/server/connection.d.ts +30 -0
  104. package/dist/server/connection.d.ts.map +1 -0
  105. package/dist/server/connection.js +182 -0
  106. package/dist/server/connection.js.map +1 -0
  107. package/dist/server/local-server.d.ts +27 -0
  108. package/dist/server/local-server.d.ts.map +1 -0
  109. package/dist/server/local-server.js +108 -0
  110. package/dist/server/local-server.js.map +1 -0
  111. package/dist/session/event-log.d.ts +17 -0
  112. package/dist/session/event-log.d.ts.map +1 -0
  113. package/dist/session/event-log.js +21 -0
  114. package/dist/session/event-log.js.map +1 -0
  115. package/dist/session/manager.d.ts +62 -0
  116. package/dist/session/manager.d.ts.map +1 -0
  117. package/dist/session/manager.js +282 -0
  118. package/dist/session/manager.js.map +1 -0
  119. package/dist/session/session.d.ts +43 -0
  120. package/dist/session/session.d.ts.map +1 -0
  121. package/dist/session/session.js +310 -0
  122. package/dist/session/session.js.map +1 -0
  123. package/dist/session/store.d.ts +22 -0
  124. package/dist/session/store.d.ts.map +1 -0
  125. package/dist/session/store.js +44 -0
  126. package/dist/session/store.js.map +1 -0
  127. package/dist/session/terminal-session.d.ts +52 -0
  128. package/dist/session/terminal-session.d.ts.map +1 -0
  129. package/dist/session/terminal-session.js +210 -0
  130. package/dist/session/terminal-session.js.map +1 -0
  131. package/dist/util/id.d.ts +2 -0
  132. package/dist/util/id.d.ts.map +1 -0
  133. package/dist/util/id.js +5 -0
  134. package/dist/util/id.js.map +1 -0
  135. package/dist/util/lock.d.ts +12 -0
  136. package/dist/util/lock.d.ts.map +1 -0
  137. package/dist/util/lock.js +52 -0
  138. package/dist/util/lock.js.map +1 -0
  139. package/dist/util/which.d.ts +3 -0
  140. package/dist/util/which.d.ts.map +1 -0
  141. package/dist/util/which.js +12 -0
  142. package/dist/util/which.js.map +1 -0
  143. package/dist/version.d.ts +2 -0
  144. package/dist/version.d.ts.map +1 -0
  145. package/dist/version.js +5 -0
  146. package/dist/version.js.map +1 -0
  147. package/package.json +56 -0
@@ -0,0 +1,83 @@
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { httpBase } from "./api.js";
5
+ /** A problem only the user can fix, by signing in again. */
6
+ export class DeviceError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "DeviceError";
10
+ }
11
+ }
12
+ const RENEW_WHEN_LEFT_MS = 30 * 24 * 60 * 60 * 1000;
13
+ export function defaultCredentialPath() {
14
+ return path.join(os.homedir(), ".termlink", "device.json");
15
+ }
16
+ const normalize = (url) => url.trim().replace(/\/+$/, "").toLowerCase();
17
+ export async function loadDevice(file, server, now = Date.now()) {
18
+ let raw;
19
+ try {
20
+ raw = await readFile(file, "utf8");
21
+ }
22
+ catch {
23
+ throw new DeviceError(`no TermLink sign-in found at ${file}; run \`termlink login\` first`);
24
+ }
25
+ let credential;
26
+ try {
27
+ credential = JSON.parse(raw);
28
+ }
29
+ catch {
30
+ throw new DeviceError(`${file} is not valid JSON; run \`termlink login\` again`);
31
+ }
32
+ if (typeof credential.token !== "string" || !credential.token) {
33
+ throw new DeviceError(`${file} has no device token; run \`termlink login\` again`);
34
+ }
35
+ if (normalize(String(credential.master ?? "")) !== normalize(server)) {
36
+ throw new DeviceError(`the sign-in in ${file} is for ${credential.master}, not ${server}; run \`termlink login\``);
37
+ }
38
+ if (!(Date.parse(credential.expires_at) > now)) {
39
+ throw new DeviceError("the TermLink sign-in on this machine has expired; run `termlink login`");
40
+ }
41
+ return credential;
42
+ }
43
+ /** Renews the device token when less than 30 days are left, like the Go CLI does at startup. */
44
+ export async function renewIfDue(file, server, credential, fetchFn, now = Date.now()) {
45
+ if (Date.parse(credential.expires_at) - now > RENEW_WHEN_LEFT_MS)
46
+ return credential;
47
+ let res;
48
+ try {
49
+ res = await fetchFn(`${httpBase(server)}/devices/renew`, {
50
+ method: "POST",
51
+ headers: { authorization: `Bearer ${credential.token}` },
52
+ signal: AbortSignal.timeout(15_000),
53
+ });
54
+ }
55
+ catch {
56
+ return credential; // Offline: the current token still works; try again next start.
57
+ }
58
+ if (res.status === 401 || res.status === 403) {
59
+ throw new DeviceError("this machine was signed out of TermLink; run `termlink login`");
60
+ }
61
+ if (!res.ok)
62
+ return credential;
63
+ const body = (await res.json());
64
+ if (!body.device_token || !body.expires_at)
65
+ return credential;
66
+ const fresh = {
67
+ ...credential,
68
+ token: body.device_token,
69
+ expires_at: body.expires_at,
70
+ ...(body.device_id ? { id: body.device_id } : {}),
71
+ ...(body.email ? { email: body.email } : {}),
72
+ ...(body.name ? { name: body.name } : {}),
73
+ };
74
+ await saveDevice(file, fresh);
75
+ return fresh;
76
+ }
77
+ export async function saveDevice(file, credential) {
78
+ await mkdir(path.dirname(file), { recursive: true });
79
+ const temp = `${file}.${process.pid}.tmp`;
80
+ await writeFile(temp, `${JSON.stringify(credential, null, 2)}\n`, { mode: 0o600 });
81
+ await rename(temp, file);
82
+ }
83
+ //# sourceMappingURL=device.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.js","sourceRoot":"","sources":["../../src/relay/device.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import { type HostInfo } from "@termlink/protocol";
2
+ import type { SessionManager } from "../session/manager.js";
3
+ import { type Fetch } from "./api.js";
4
+ export declare const DEFAULT_SERVER = "wss://connect.getterm.link:9000";
5
+ export interface RelayHostOptions {
6
+ manager: SessionManager;
7
+ hostInfo: () => Promise<HostInfo>;
8
+ /** Master relay. */
9
+ server: string;
10
+ /** Relay session name. One per host process; every agent session travels inside it. */
11
+ session: string;
12
+ name?: string;
13
+ /** Shown in the host list; the first allowed root is a good value. */
14
+ cwd?: string;
15
+ credentialPath?: string;
16
+ fetch?: Fetch;
17
+ minBackoffMs?: number;
18
+ maxBackoffMs?: number;
19
+ pingIntervalMs?: number;
20
+ log?: (line: string) => void;
21
+ /** The relay connection ended for good: the session was ended from the account, or sign-in is needed. */
22
+ onStopped?: (reason: string) => void;
23
+ }
24
+ /**
25
+ * Connects the host to the TermLink relay as `role=host&caps=agent`. The relay
26
+ * merges every client into this one socket and broadcasts what the host sends,
27
+ * so the whole relay link is served by a single ClientConnection.
28
+ */
29
+ export declare class RelayHost {
30
+ #private;
31
+ readonly relayUrl: string;
32
+ /** Resolves on the first successful connection. */
33
+ readonly ready: Promise<void>;
34
+ private constructor();
35
+ /** Signs in with the device credential, registers the session and starts connecting. */
36
+ static start(options: RelayHostOptions): Promise<RelayHost>;
37
+ close(): Promise<void>;
38
+ }
39
+ /** Relay session name for this machine: stable, so a restarted host takes its slot back. */
40
+ export declare function defaultRelaySession(): string;
41
+ //# sourceMappingURL=relay-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay-host.d.ts","sourceRoot":"","sources":["../../src/relay/relay-host.ts"],"names":[],"mappings":"AACA,OAAO,EAAmD,KAAK,QAAQ,EAAoB,MAAM,oBAAoB,CAAC;AAMtH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAA2D,KAAK,KAAK,EAAE,MAAM,UAAU,CAAC;AAG/F,eAAO,MAAM,cAAc,oCAAoC,CAAC;AAEhE,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,cAAc,CAAC;IACxB,QAAQ,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClC,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,yGAAyG;IACzG,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAWD;;;;GAIG;AACH,qBAAa,SAAS;;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mDAAmD;IACnD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAa9B,OAAO,eAON;IAED,wFAAwF;IACxF,OAAa,KAAK,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,CAUhE;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAU3B;CA6JF;AAYD,4FAA4F;AAC5F,wBAAgB,mBAAmB,IAAI,MAAM,CAG5C"}
@@ -0,0 +1,238 @@
1
+ import os from "node:os";
2
+ import { FrameAssembler, RELAY_FRAME_BYTES, encodeFrames } from "@termlink/protocol";
3
+ import { WebSocket } from "ws";
4
+ import { errorMessage } from "../errors.js";
5
+ import { DeltaCoalescer } from "../server/coalesce.js";
6
+ import { ClientConnection } from "../server/connection.js";
7
+ import { toBytes } from "../server/local-server.js";
8
+ import { newId } from "../util/id.js";
9
+ import { RelayHttpError, pickRelay, registerHost, unregisterHost } from "./api.js";
10
+ import { DeviceError, defaultCredentialPath, loadDevice, renewIfDue } from "./device.js";
11
+ export const DEFAULT_SERVER = "wss://connect.getterm.link:9000";
12
+ const PONG_TIMEOUT_MS = 60_000;
13
+ /**
14
+ * Connects the host to the TermLink relay as `role=host&caps=agent`. The relay
15
+ * merges every client into this one socket and broadcasts what the host sends,
16
+ * so the whole relay link is served by a single ClientConnection.
17
+ */
18
+ export class RelayHost {
19
+ relayUrl;
20
+ /** Resolves on the first successful connection. */
21
+ ready;
22
+ #options;
23
+ #fetch;
24
+ #deviceToken;
25
+ #hostToken = null;
26
+ #live = null;
27
+ #socket = null;
28
+ #backoffMs;
29
+ #reconnectTimer = null;
30
+ #stopped = false;
31
+ #lastLogged = "";
32
+ #markReady = () => { };
33
+ constructor(options, relayUrl, deviceToken, fetchFn) {
34
+ this.#options = options;
35
+ this.relayUrl = relayUrl;
36
+ this.#deviceToken = deviceToken;
37
+ this.#fetch = fetchFn;
38
+ this.#backoffMs = options.minBackoffMs ?? 1_000;
39
+ this.ready = new Promise((resolve) => (this.#markReady = resolve));
40
+ }
41
+ /** Signs in with the device credential, registers the session and starts connecting. */
42
+ static async start(options) {
43
+ const fetchFn = options.fetch ?? fetch;
44
+ const credentialPath = options.credentialPath ?? defaultCredentialPath();
45
+ let device = await loadDevice(credentialPath, options.server);
46
+ device = await renewIfDue(credentialPath, options.server, device, fetchFn);
47
+ const relayUrl = await pickRelay(options.server, fetchFn);
48
+ const host = new RelayHost(options, relayUrl, device.token, fetchFn);
49
+ await host.#register();
50
+ host.#connect();
51
+ return host;
52
+ }
53
+ async close() {
54
+ this.#stopped = true;
55
+ if (this.#reconnectTimer)
56
+ clearTimeout(this.#reconnectTimer);
57
+ this.#teardown();
58
+ const socket = this.#socket;
59
+ this.#socket = null;
60
+ socket?.close(1000, "host shutting down");
61
+ if (this.#hostToken) {
62
+ await unregisterHost(this.#options.server, this.#hostToken, this.#options.session, this.#fetch).catch(() => { });
63
+ }
64
+ }
65
+ async #register() {
66
+ try {
67
+ this.#hostToken = await registerHost(this.#options.server, this.#deviceToken, this.#options.session, this.#fetch);
68
+ }
69
+ catch (err) {
70
+ // 401/403 is a sign-in problem or the plan's session limit; retrying will not help.
71
+ if (err instanceof RelayHttpError && (err.status === 401 || err.status === 403)) {
72
+ throw new DeviceError(`the relay refused this host: ${err.message}`);
73
+ }
74
+ throw err;
75
+ }
76
+ }
77
+ #connect() {
78
+ this.#reconnectTimer = null;
79
+ if (this.#stopped)
80
+ return;
81
+ this.#dial().catch((err) => {
82
+ if (err instanceof DeviceError)
83
+ this.#stop(err.message);
84
+ else {
85
+ this.#log(`relay: ${errorMessage(err)}`);
86
+ this.#scheduleReconnect();
87
+ }
88
+ });
89
+ }
90
+ async #dial() {
91
+ if (!this.#hostToken)
92
+ await this.#register();
93
+ const query = new URLSearchParams({
94
+ session: this.#options.session,
95
+ role: "host",
96
+ token: this.#hostToken ?? "",
97
+ name: this.#options.name ?? os.hostname(),
98
+ term: "termlink-agent",
99
+ cwd: this.#options.cwd ?? process.cwd(),
100
+ os: process.platform,
101
+ arch: process.arch,
102
+ relay: this.relayUrl,
103
+ caps: "agent",
104
+ });
105
+ const ws = new WebSocket(`${this.relayUrl}/ws?${query}`, { handshakeTimeout: 15_000 });
106
+ this.#socket = ws;
107
+ ws.on("error", (err) => {
108
+ // A rejected host token (expired, or signed with an old relay secret): register again.
109
+ const status = /Unexpected server response: (\d+)/.exec(err.message)?.[1];
110
+ if (status === "401" || status === "403")
111
+ this.#hostToken = null;
112
+ this.#log(`relay: ${err.message}`);
113
+ });
114
+ ws.on("open", () => {
115
+ this.#backoffMs = this.#options.minBackoffMs ?? 1_000;
116
+ this.#lastLogged = "";
117
+ this.#log(`relay: connected to ${this.relayUrl} as ${this.#options.session}`);
118
+ this.#serve(ws);
119
+ this.#markReady();
120
+ });
121
+ ws.on("close", (code, reason) => {
122
+ if (this.#socket === ws) {
123
+ this.#teardown();
124
+ this.#socket = null;
125
+ }
126
+ if (this.#stopped)
127
+ return;
128
+ this.#log(`relay: disconnected (${code}${reason.length > 0 ? ` ${reason.toString()}` : ""}), reconnecting`);
129
+ this.#scheduleReconnect();
130
+ });
131
+ }
132
+ #serve(ws) {
133
+ const coalescer = new DeltaCoalescer((message) => this.#write(ws, message));
134
+ const connection = new ClientConnection(this.#options.manager, {
135
+ send: (message) => coalescer.push(message),
136
+ sendBinary: (frame) => {
137
+ // Keep terminal bytes in order with the JSON events around them.
138
+ coalescer.flush();
139
+ if (ws.readyState === WebSocket.OPEN)
140
+ ws.send(frame, { binary: true });
141
+ },
142
+ }, this.#options.hostInfo);
143
+ const assembler = new FrameAssembler();
144
+ let clients = -1;
145
+ let lastPong = Date.now();
146
+ ws.on("pong", () => (lastPong = Date.now()));
147
+ ws.on("message", (data, isBinary) => {
148
+ if (isBinary) {
149
+ connection.handleBinary(toBytes(data));
150
+ return;
151
+ }
152
+ let message;
153
+ try {
154
+ message = assembler.push(data.toString());
155
+ }
156
+ catch {
157
+ return;
158
+ }
159
+ if (message === undefined)
160
+ return;
161
+ if (!isRelayControl(message)) {
162
+ void connection.handleMessage(message);
163
+ return;
164
+ }
165
+ if (message.type === "clients") {
166
+ // Greet newly joined clients; everyone else just sees host.ready again.
167
+ const count = Array.isArray(message.clients) ? message.clients.length : 0;
168
+ if (count > clients && clients >= 0)
169
+ void connection.announce();
170
+ clients = count;
171
+ }
172
+ else if (message.type === "terminated") {
173
+ this.#stop(typeof message.reason === "string" ? message.reason : "the session was ended");
174
+ }
175
+ });
176
+ const heartbeat = setInterval(() => {
177
+ if (Date.now() - lastPong > PONG_TIMEOUT_MS)
178
+ ws.terminate();
179
+ else
180
+ ws.ping();
181
+ }, this.#options.pingIntervalMs ?? 25_000);
182
+ this.#live = { ws, connection, coalescer, heartbeat };
183
+ void connection.open();
184
+ }
185
+ #write(ws, message) {
186
+ if (ws.readyState !== WebSocket.OPEN)
187
+ return;
188
+ for (const frame of encodeFrames(message, RELAY_FRAME_BYTES, () => newId("ck")))
189
+ ws.send(frame);
190
+ }
191
+ #teardown() {
192
+ const live = this.#live;
193
+ this.#live = null;
194
+ if (!live)
195
+ return;
196
+ clearInterval(live.heartbeat);
197
+ live.coalescer.dispose();
198
+ live.connection.dispose();
199
+ }
200
+ #scheduleReconnect() {
201
+ if (this.#stopped || this.#reconnectTimer)
202
+ return;
203
+ const delay = this.#backoffMs;
204
+ this.#backoffMs = Math.min(this.#backoffMs * 2, this.#options.maxBackoffMs ?? 30_000);
205
+ this.#reconnectTimer = setTimeout(() => this.#connect(), delay);
206
+ }
207
+ #stop(reason) {
208
+ if (this.#stopped)
209
+ return;
210
+ this.#stopped = true;
211
+ if (this.#reconnectTimer)
212
+ clearTimeout(this.#reconnectTimer);
213
+ this.#log(`relay: stopped: ${reason}`);
214
+ this.#teardown();
215
+ this.#socket?.close(1000, "host stopped");
216
+ this.#options.onStopped?.(reason);
217
+ }
218
+ // Reconnect loops repeat the same failure; log each distinct message once.
219
+ #log(line) {
220
+ if (line === this.#lastLogged)
221
+ return;
222
+ this.#lastLogged = line;
223
+ this.#options.log?.(line);
224
+ }
225
+ }
226
+ function isRelayControl(message) {
227
+ return (typeof message === "object" &&
228
+ message !== null &&
229
+ !("kind" in message) &&
230
+ "type" in message &&
231
+ typeof message.type === "string");
232
+ }
233
+ /** Relay session name for this machine: stable, so a restarted host takes its slot back. */
234
+ export function defaultRelaySession() {
235
+ const host = os.hostname().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "host";
236
+ return `${host}-agent`;
237
+ }
238
+ //# sourceMappingURL=relay-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay-host.js","sourceRoot":"","sources":["../../src/relay/relay-host.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { HostMessage } from "@termlink/protocol";
2
+ /**
3
+ * Merges consecutive item.delta events for the same item and field that arrive
4
+ * within a short window, so a streaming reply is not one relay frame per token.
5
+ * Any other message flushes first, so ordering is preserved.
6
+ */
7
+ export declare class DeltaCoalescer {
8
+ #private;
9
+ constructor(send: (message: HostMessage) => void, windowMs?: number);
10
+ push(message: HostMessage): void;
11
+ flush(): void;
12
+ /** Drops anything pending without sending it. */
13
+ dispose(): void;
14
+ }
15
+ //# sourceMappingURL=coalesce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coalesce.d.ts","sourceRoot":"","sources":["../../src/server/coalesce.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAItD;;;;GAIG;AACH,qBAAa,cAAc;;IAMzB,YAAY,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,EAAE,QAAQ,SAAK,EAG9D;IAED,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAmB/B;IAED,KAAK,IAAI,IAAI,CAMZ;IAED,iDAAiD;IACjD,OAAO,IAAI,IAAI,CAId;CACF"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Merges consecutive item.delta events for the same item and field that arrive
3
+ * within a short window, so a streaming reply is not one relay frame per token.
4
+ * Any other message flushes first, so ordering is preserved.
5
+ */
6
+ export class DeltaCoalescer {
7
+ #send;
8
+ #windowMs;
9
+ #pending = null;
10
+ #timer = null;
11
+ constructor(send, windowMs = 40) {
12
+ this.#send = send;
13
+ this.#windowMs = windowMs;
14
+ }
15
+ push(message) {
16
+ if (message.kind === "evt" && message.type === "item.delta") {
17
+ const pending = this.#pending;
18
+ if (pending &&
19
+ pending.sessionId === message.sessionId &&
20
+ pending.payload.itemId === message.payload.itemId &&
21
+ pending.payload.field === message.payload.field) {
22
+ pending.payload = { ...pending.payload, delta: pending.payload.delta + message.payload.delta };
23
+ return;
24
+ }
25
+ this.flush();
26
+ this.#pending = { ...message, payload: { ...message.payload } };
27
+ this.#timer = setTimeout(() => this.flush(), this.#windowMs);
28
+ return;
29
+ }
30
+ this.flush();
31
+ this.#send(message);
32
+ }
33
+ flush() {
34
+ if (this.#timer)
35
+ clearTimeout(this.#timer);
36
+ this.#timer = null;
37
+ const pending = this.#pending;
38
+ this.#pending = null;
39
+ if (pending)
40
+ this.#send(pending);
41
+ }
42
+ /** Drops anything pending without sending it. */
43
+ dispose() {
44
+ if (this.#timer)
45
+ clearTimeout(this.#timer);
46
+ this.#timer = null;
47
+ this.#pending = null;
48
+ }
49
+ }
50
+ //# sourceMappingURL=coalesce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coalesce.js","sourceRoot":"","sources":["../../src/server/coalesce.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,OAAO,cAAc;IAChB,KAAK,CAAiC;IACtC,SAAS,CAAS;IAC3B,QAAQ,GAAsB,IAAI,CAAC;IACnC,MAAM,GAA0B,IAAI,CAAC;IAErC,YAAY,IAAoC,EAAE,QAAQ,GAAG,EAAE;QAC7D,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;IAC5B,CAAC;IAED,IAAI,CAAC,OAAoB;QACvB,IAAI,OAAO,CAAC,IAAI,KAAK,KAAK,IAAI,OAAO,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC;YAC9B,IACE,OAAO;gBACP,OAAO,CAAC,SAAS,KAAK,OAAO,CAAC,SAAS;gBACvC,OAAO,CAAC,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,MAAM;gBACjD,OAAO,CAAC,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,EAC/C,CAAC;gBACD,OAAO,CAAC,OAAO,GAAG,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;gBAC/F,OAAO;YACT,CAAC;YACD,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,IAAI,CAAC,QAAQ,GAAG,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;YAChE,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YAC7D,OAAO;QACT,CAAC;QACD,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACtB,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,MAAM;YAAE,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC9B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,OAAO;YAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAED,iDAAiD;IACjD,OAAO;QACL,IAAI,IAAI,CAAC,MAAM;YAAE,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;IACvB,CAAC;CACF"}
@@ -0,0 +1,30 @@
1
+ import { type HostInfo, type HostMessage } from "@termlink/protocol";
2
+ import type { SessionManager } from "../session/manager.js";
3
+ type Send = (message: HostMessage) => void;
4
+ /** How messages reach the client. Binary frames carry terminal bytes; a transport without them has no terminals. */
5
+ export interface ConnectionTransport {
6
+ send: Send;
7
+ sendBinary?: ((frame: Uint8Array) => void) | undefined;
8
+ }
9
+ /**
10
+ * One client's view of the host, independent of transport. The local WebSocket
11
+ * server and the relay link both use it.
12
+ */
13
+ export declare class ClientConnection {
14
+ #private;
15
+ constructor(manager: SessionManager, transport: Send | ConnectionTransport, hostInfo: () => Promise<HostInfo>);
16
+ open(): Promise<void>;
17
+ /** Sends host.ready. The relay transport repeats it when a new client joins. */
18
+ announce(): Promise<void>;
19
+ handle(raw: string): Promise<void>;
20
+ /** Handles an already parsed message, such as one reassembled from chunks. */
21
+ handleMessage(data: unknown): Promise<void>;
22
+ /**
23
+ * Terminal input from a binary frame. Only for sessions this connection is attached
24
+ * to; anything else is dropped without a reply, since frames carry no reqId.
25
+ */
26
+ handleBinary(data: Uint8Array): void;
27
+ dispose(): void;
28
+ }
29
+ export {};
30
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/server/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,QAAQ,EACb,KAAK,WAAW,EACjB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAI5D,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;AAE3C,oHAAoH;AACpH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,IAAI,CAAC;IACX,UAAU,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CACxD;AAED;;;GAGG;AACH,qBAAa,gBAAgB;;IAS3B,YAAY,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,IAAI,GAAG,mBAAmB,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,EAM5G;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1B;IAED,gFAAgF;IAC1E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAI9B;IAEK,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASvC;IAED,8EAA8E;IACxE,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAahD;IAED;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAKnC;IAED,OAAO,IAAI,IAAI,CAKd;CAgGF"}