@hermespilot/link 0.1.0 → 0.1.2

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/cli/index.js CHANGED
@@ -2,96 +2,257 @@
2
2
  import {
3
3
  LINK_COMMAND,
4
4
  LINK_VERSION,
5
+ createApp,
5
6
  ensureHermesApiServerKey,
6
7
  ensureIdentity,
7
8
  getIdentityStatus,
8
9
  loadConfig,
9
10
  loadIdentity,
10
- resolveRuntimePaths,
11
- saveAssignedLinkId,
12
- signRelayNonce
13
- } from "../chunk-YTX3DQGX.js";
11
+ preparePairing,
12
+ resolveRuntimePaths
13
+ } from "../chunk-E2BRK5JT.js";
14
14
 
15
15
  // src/cli/index.ts
16
16
  import { Command } from "commander";
17
+ import qrcode from "qrcode-terminal";
17
18
 
18
- // src/relay/bootstrap.ts
19
- async function bootstrapRelayLink(options) {
20
- const fetcher = options.fetchImpl ?? fetch;
21
- const baseUrl = options.relayBaseUrl.replace(/\/+$/u, "");
22
- const commonPayload = {
23
- install_id: options.identity.install_id,
24
- link_id: options.identity.link_id ?? void 0,
25
- public_key_pem: options.identity.public_key_pem
26
- };
27
- const challenge = await postJson(
28
- fetcher,
29
- `${baseUrl}/api/v1/relay/link/challenge`,
30
- options.relayBootstrapToken,
31
- commonPayload
32
- );
33
- if (challenge.ok !== true || typeof challenge.nonce !== "string") {
34
- throw new Error("Relay did not return a valid install challenge");
35
- }
36
- const proof = {
37
- nonce: challenge.nonce,
38
- signature: signRelayNonce(options.identity, challenge.nonce)
39
- };
40
- const assigned = await postJson(
41
- fetcher,
42
- `${baseUrl}/api/v1/relay/link/bootstrap`,
43
- options.relayBootstrapToken,
44
- {
45
- ...commonPayload,
46
- proof
19
+ // src/i18n.ts
20
+ var messages = {
21
+ en: {
22
+ "program.description": "Hermes Link companion service",
23
+ "program.version": "print Hermes Link version",
24
+ "status.description": "Show local Hermes Link status",
25
+ "status.json": "print machine-readable status",
26
+ "status.runtime": "Runtime: {value}",
27
+ "status.mode": "Mode: {value}",
28
+ "status.port": "Local port: {value}",
29
+ "status.linkId": "Link ID: {value}",
30
+ "status.notPaired": "not paired",
31
+ "start.description": "Start Hermes Link daemon",
32
+ "start.notPaired": "Hermes Link is not paired yet. Starting in local-only maintenance mode.",
33
+ "start.notPaired.detail": "Relay, Server polling, and LAN entrypoints stay disabled until you run `hermeslink pair`.",
34
+ "start.listening": "Hermes Link API listening on http://127.0.0.1:{port}",
35
+ "start.relayConnecting": "Relay control connecting for {linkId}",
36
+ "pair.description": "Create a Hermes Link pairing session",
37
+ "pair.preparing": "Preparing pairing session through HermesPilot Server and Relay...",
38
+ "pair.server": "Server: {url}",
39
+ "pair.relay": "Relay: {url}",
40
+ "pair.linkId": "Hermes Link ID: {value}",
41
+ "pair.code": "Pairing code: {value}",
42
+ "pair.localApi": "Local API: http://127.0.0.1:{port}",
43
+ "pair.scan": "Scan this QR code with the HermesPilot App:",
44
+ "pair.expires": "Pairing expires in 10 minutes. Press Ctrl+C to stop Hermes Link.",
45
+ "doctor.description": "Run local diagnostics",
46
+ "doctor.identityOk": "Runtime identity: OK",
47
+ "doctor.installId": "Install ID: {value}",
48
+ "doctor.linkId": "Link ID: {value}",
49
+ "doctor.notAssigned": "not assigned",
50
+ "error.relayPublicKeyMismatch": "Relay rejected the pairing request because the Server-issued bootstrap token does not match this Link public key. Make sure Server and Relay are deployed with the same bootstrap key configuration, then run `hermeslink pair` again.",
51
+ "error.relayChallengeInvalid": "Relay did not return a valid install challenge.",
52
+ "error.relayLinkInvalid": "Relay did not return a valid link_id.",
53
+ "error.relayEmpty": "Relay returned an empty response.",
54
+ "error.serverHttp": "HermesPilot Server request failed with HTTP {status}.",
55
+ "error.pairingRequires": "Pairing needs HermesPilot Server and Relay, but this command could not start a complete pairing session.",
56
+ "error.pairingRequires.detail": "The deployed services may be healthy, but the installed Link package must call Server for a short-lived relay bootstrap token before it can request a link_id."
57
+ },
58
+ "zh-CN": {
59
+ "program.description": "Hermes Link \u672C\u5730\u4F34\u968F\u670D\u52A1",
60
+ "program.version": "\u8F93\u51FA Hermes Link \u7248\u672C\u53F7",
61
+ "status.description": "\u67E5\u770B\u672C\u673A Hermes Link \u72B6\u6001",
62
+ "status.json": "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB\u7684\u72B6\u6001 JSON",
63
+ "status.runtime": "\u8FD0\u884C\u76EE\u5F55\uFF1A{value}",
64
+ "status.mode": "\u6A21\u5F0F\uFF1A{value}",
65
+ "status.port": "\u672C\u5730\u7AEF\u53E3\uFF1A{value}",
66
+ "status.linkId": "Link ID\uFF1A{value}",
67
+ "status.notPaired": "\u5C1A\u672A\u914D\u5BF9",
68
+ "start.description": "\u542F\u52A8 Hermes Link \u670D\u52A1",
69
+ "start.notPaired": "Hermes Link \u8FD8\u6CA1\u6709\u914D\u5BF9\uFF0C\u5C06\u4EE5\u672C\u5730\u7EF4\u62A4\u6A21\u5F0F\u542F\u52A8\u3002",
70
+ "start.notPaired.detail": "\u5728\u4F60\u8FD0\u884C `hermeslink pair` \u524D\uFF0CRelay\u3001Server \u8F6E\u8BE2\u548C\u5C40\u57DF\u7F51\u5165\u53E3\u90FD\u4F1A\u4FDD\u6301\u5173\u95ED\u3002",
71
+ "start.listening": "Hermes Link API \u6B63\u5728\u76D1\u542C http://127.0.0.1:{port}",
72
+ "start.relayConnecting": "\u6B63\u5728\u4E3A {linkId} \u8FDE\u63A5 Relay \u63A7\u5236\u901A\u9053",
73
+ "pair.description": "\u521B\u5EFA Hermes Link \u914D\u5BF9\u4F1A\u8BDD",
74
+ "pair.preparing": "\u6B63\u5728\u901A\u8FC7 HermesPilot Server \u548C Relay \u521B\u5EFA\u914D\u5BF9\u4F1A\u8BDD...",
75
+ "pair.server": "Server\uFF1A{url}",
76
+ "pair.relay": "Relay\uFF1A{url}",
77
+ "pair.linkId": "Hermes Link ID\uFF1A{value}",
78
+ "pair.code": "\u914D\u5BF9\u7801\uFF1A{value}",
79
+ "pair.localApi": "\u672C\u5730 API\uFF1Ahttp://127.0.0.1:{port}",
80
+ "pair.scan": "\u8BF7\u4F7F\u7528 HermesPilot App \u626B\u63CF\u8FD9\u4E2A\u4E8C\u7EF4\u7801\uFF1A",
81
+ "pair.expires": "\u914D\u5BF9\u4F1A\u8BDD 10 \u5206\u949F\u540E\u8FC7\u671F\u3002\u6309 Ctrl+C \u505C\u6B62 Hermes Link\u3002",
82
+ "doctor.description": "\u8FD0\u884C\u672C\u673A\u8BCA\u65AD",
83
+ "doctor.identityOk": "\u8FD0\u884C\u8EAB\u4EFD\uFF1A\u6B63\u5E38",
84
+ "doctor.installId": "Install ID\uFF1A{value}",
85
+ "doctor.linkId": "Link ID\uFF1A{value}",
86
+ "doctor.notAssigned": "\u5C1A\u672A\u5206\u914D",
87
+ "error.relayPublicKeyMismatch": "Relay \u62D2\u7EDD\u4E86\u914D\u5BF9\u8BF7\u6C42\uFF1AServer \u7B7E\u53D1\u7684 bootstrap token \u4E0E\u672C\u673A Link \u516C\u94A5\u4E0D\u5339\u914D\u3002\u8BF7\u786E\u8BA4 Server \u548C Relay \u4F7F\u7528\u540C\u4E00\u5957 bootstrap key \u914D\u7F6E\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C `hermeslink pair`\u3002",
88
+ "error.relayChallengeInvalid": "Relay \u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684\u5B89\u88C5\u6311\u6218\u3002",
89
+ "error.relayLinkInvalid": "Relay \u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684 link_id\u3002",
90
+ "error.relayEmpty": "Relay \u8FD4\u56DE\u4E86\u7A7A\u54CD\u5E94\u3002",
91
+ "error.serverHttp": "HermesPilot Server \u8BF7\u6C42\u5931\u8D25\uFF0CHTTP \u72B6\u6001\u7801\uFF1A{status}\u3002",
92
+ "error.pairingRequires": "\u914D\u5BF9\u9700\u8981 HermesPilot Server \u548C Relay\uFF0C\u4F46\u5F53\u524D\u547D\u4EE4\u6CA1\u6709\u80FD\u542F\u52A8\u5B8C\u6574\u914D\u5BF9\u4F1A\u8BDD\u3002",
93
+ "error.pairingRequires.detail": "\u4E91\u7AEF\u670D\u52A1\u53EF\u4EE5\u662F\u5DF2\u90E8\u7F72\u4E14\u5065\u5EB7\u7684\uFF1B\u672C\u673A Link \u4ECD\u5FC5\u987B\u5148\u5411 Server \u7533\u8BF7\u77ED\u671F relay bootstrap token\uFF0C\u624D\u80FD\u518D\u5411 Relay \u7533\u8BF7 link_id\u3002"
94
+ }
95
+ };
96
+ function detectSystemLanguage(env = process.env) {
97
+ const candidates = [
98
+ env.HERMESLINK_LANG,
99
+ env.HERMESLINK_LANGUAGE,
100
+ env.LC_ALL,
101
+ env.LC_MESSAGES,
102
+ env.LANG,
103
+ env.LANGUAGE?.split(":")[0],
104
+ Intl.DateTimeFormat().resolvedOptions().locale
105
+ ];
106
+ for (const candidate of candidates) {
107
+ const language = parseLanguage(candidate);
108
+ if (language) {
109
+ return language;
47
110
  }
48
- );
49
- if (assigned.ok !== true || typeof assigned.link_id !== "string") {
50
- throw new Error("Relay did not return a valid link_id");
51
111
  }
52
- await saveAssignedLinkId(assigned.link_id, options.paths ?? resolveRuntimePaths());
53
- return {
54
- linkId: assigned.link_id,
55
- reused: assigned.reused === true
56
- };
112
+ return "en";
57
113
  }
58
- async function postJson(fetcher, url, token, body) {
59
- const response = await fetcher(url, {
60
- method: "POST",
61
- headers: {
62
- authorization: `Bearer ${token}`,
63
- "content-type": "application/json"
64
- },
65
- body: JSON.stringify(body)
66
- });
67
- const payload = await response.json().catch(() => null);
68
- if (!response.ok) {
69
- const message = readErrorMessage(payload) ?? `Relay request failed with HTTP ${response.status}`;
70
- throw new Error(message);
114
+ function resolveLanguage(setting) {
115
+ const configured = parseLanguage(setting);
116
+ if (configured) {
117
+ return configured;
71
118
  }
72
- if (!payload) {
73
- throw new Error("Relay returned an empty response");
119
+ return detectSystemLanguage();
120
+ }
121
+ function translate(language, key, values = {}) {
122
+ const template = messages[language][key] ?? messages.en[key];
123
+ return template.replace(/\{(\w+)\}/gu, (_, name) => String(values[name] ?? ""));
124
+ }
125
+ function localizeErrorMessage(error, language) {
126
+ const message = error instanceof Error ? error.message : String(error);
127
+ if (language === "en") {
128
+ return message;
74
129
  }
75
- return payload;
130
+ const mapped = translateKnownError(message, language);
131
+ return mapped ?? message;
76
132
  }
77
- function readErrorMessage(payload) {
78
- if (typeof payload !== "object" || payload === null) {
79
- return null;
133
+ function translateKnownError(message, language) {
134
+ if (message === "Relay bootstrap token does not match public key") {
135
+ return translate(language, "error.relayPublicKeyMismatch");
136
+ }
137
+ if (message === "Relay did not return a valid install challenge") {
138
+ return translate(language, "error.relayChallengeInvalid");
139
+ }
140
+ if (message === "Relay did not return a valid link_id") {
141
+ return translate(language, "error.relayLinkInvalid");
142
+ }
143
+ if (message === "Relay returned an empty response") {
144
+ return translate(language, "error.relayEmpty");
80
145
  }
81
- const error = payload.error;
82
- if (typeof error !== "object" || error === null) {
146
+ const serverHttp = /^HermesPilot Server request failed with HTTP (?<status>\d+)$/u.exec(message);
147
+ if (serverHttp?.groups?.status) {
148
+ return translate(language, "error.serverHttp", { status: serverHttp.groups.status });
149
+ }
150
+ if (message.includes("Pairing requires HermesPilot Server and Relay")) {
151
+ return [translate(language, "error.pairingRequires"), translate(language, "error.pairingRequires.detail")].join("\n");
152
+ }
153
+ return null;
154
+ }
155
+ function parseLanguage(value) {
156
+ const normalized = value?.trim().replace("_", "-").toLowerCase();
157
+ if (!normalized || normalized === "auto" || normalized === "c" || normalized === "posix") {
83
158
  return null;
84
159
  }
85
- const message = error.message;
86
- return typeof message === "string" ? message : null;
160
+ if (normalized.startsWith("zh")) {
161
+ return "zh-CN";
162
+ }
163
+ if (normalized.startsWith("en")) {
164
+ return "en";
165
+ }
166
+ return null;
167
+ }
168
+
169
+ // src/relay/control-client.ts
170
+ import WebSocket from "ws";
171
+ function connectRelayControl(options) {
172
+ const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
173
+ wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
174
+ wsUrl.searchParams.set("link_id", options.linkId);
175
+ const socket = new WebSocket(wsUrl, {
176
+ headers: {
177
+ "x-hermes-link-version": LINK_VERSION
178
+ }
179
+ });
180
+ const abortControllers = /* @__PURE__ */ new Map();
181
+ socket.on("message", (raw) => {
182
+ if (typeof raw !== "string" && !Buffer.isBuffer(raw)) {
183
+ return;
184
+ }
185
+ void handleFrame(socket, String(raw), options.localPort, abortControllers).catch((error) => {
186
+ const message = error instanceof Error ? error.message : "Relay request failed";
187
+ socket.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
188
+ });
189
+ });
190
+ socket.on("close", () => {
191
+ for (const controller of abortControllers.values()) {
192
+ controller.abort();
193
+ }
194
+ abortControllers.clear();
195
+ });
196
+ return {
197
+ close() {
198
+ socket.close();
199
+ }
200
+ };
201
+ }
202
+ async function handleFrame(socket, raw, localPort, abortControllers) {
203
+ const frame = JSON.parse(raw);
204
+ if (frame.type === "http.cancel") {
205
+ abortControllers.get(frame.id)?.abort();
206
+ abortControllers.delete(frame.id);
207
+ return;
208
+ }
209
+ if (frame.type !== "http.request") {
210
+ return;
211
+ }
212
+ const abortController = new AbortController();
213
+ abortControllers.set(frame.id, abortController);
214
+ try {
215
+ const response = await fetch(`http://127.0.0.1:${localPort}${frame.path}`, {
216
+ method: frame.method,
217
+ headers: frame.headers ?? {},
218
+ body: frame.bodyBase64 ? Buffer.from(frame.bodyBase64, "base64") : void 0,
219
+ signal: abortController.signal
220
+ });
221
+ const headers = Object.fromEntries(response.headers.entries());
222
+ const contentType = response.headers.get("content-type") ?? "";
223
+ if (response.body && contentType.includes("text/event-stream")) {
224
+ socket.send(JSON.stringify({ type: "http.stream.start", id: frame.id, status: response.status, headers }));
225
+ const reader = response.body.getReader();
226
+ while (true) {
227
+ const next = await reader.read();
228
+ if (next.done) {
229
+ break;
230
+ }
231
+ socket.send(JSON.stringify({ type: "http.stream.chunk", id: frame.id, bodyBase64: Buffer.from(next.value).toString("base64") }));
232
+ }
233
+ socket.send(JSON.stringify({ type: "http.stream.end", id: frame.id }));
234
+ return;
235
+ }
236
+ const body = Buffer.from(await response.arrayBuffer()).toString("base64");
237
+ socket.send(JSON.stringify({ type: "http.response", id: frame.id, status: response.status, headers, bodyBase64: body }));
238
+ } catch (error) {
239
+ const message = error instanceof Error ? error.message : "Relay request failed";
240
+ socket.send(JSON.stringify({ type: "http.error", id: frame.id, status: 502, message }));
241
+ } finally {
242
+ abortControllers.delete(frame.id);
243
+ }
87
244
  }
88
245
 
89
246
  // src/cli/index.ts
90
247
  var program = new Command();
91
- program.name(LINK_COMMAND).description("Hermes Link companion service").version(LINK_VERSION, "-v, --version", "print Hermes Link version");
92
- program.command("status").option("--json", "print machine-readable status").description("Show local Hermes Link status").action(async (options) => {
248
+ var helpLanguage = detectSystemLanguage();
249
+ var helpText = translate.bind(null, helpLanguage);
250
+ program.name(LINK_COMMAND).description(helpText("program.description")).version(LINK_VERSION, "-v, --version", helpText("program.version"));
251
+ program.command("status").option("--json", helpText("status.json")).description(helpText("status.description")).action(async (options) => {
93
252
  const paths = resolveRuntimePaths();
94
253
  const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
254
+ const language = resolveLanguage(config.language);
255
+ const t = translate.bind(null, language);
95
256
  const payload = {
96
257
  version: LINK_VERSION,
97
258
  runtimeHome: paths.homeDir,
@@ -109,44 +270,70 @@ program.command("status").option("--json", "print machine-readable status").desc
109
270
  return;
110
271
  }
111
272
  console.log(`Hermes Link ${payload.version}`);
112
- console.log(`Runtime: ${payload.runtimeHome}`);
113
- console.log(`Mode: ${payload.mode}`);
114
- console.log(`Local port: ${payload.port}`);
115
- console.log(`Link ID: ${payload.identity?.linkId ?? "not paired"}`);
273
+ console.log(t("status.runtime", { value: payload.runtimeHome }));
274
+ console.log(t("status.mode", { value: payload.mode }));
275
+ console.log(t("status.port", { value: payload.port }));
276
+ console.log(t("status.linkId", { value: payload.identity?.linkId ?? t("status.notPaired") }));
116
277
  });
117
- program.command("start").description("Start Hermes Link daemon").action(async () => {
118
- const identity = await loadIdentity();
278
+ program.command("start").description(helpText("start.description")).action(async () => {
279
+ const [identity, config] = await Promise.all([loadIdentity(), loadConfig()]);
280
+ const language = resolveLanguage(config.language);
281
+ const t = translate.bind(null, language);
119
282
  if (!identity?.link_id) {
120
- console.log("Hermes Link is not paired yet. Starting in local-only maintenance mode.");
121
- console.log("Relay, Server polling, and LAN entrypoints stay disabled until you run `hermeslink pair`.");
122
- return;
283
+ console.log(t("start.notPaired"));
284
+ console.log(t("start.notPaired.detail"));
285
+ }
286
+ const server = await startHttpServer(config.port);
287
+ const relay = identity?.link_id ? connectRelayControl({
288
+ relayBaseUrl: config.relayBaseUrl,
289
+ linkId: identity.link_id,
290
+ localPort: config.port
291
+ }) : null;
292
+ console.log(t("start.listening", { port: config.port }));
293
+ if (identity?.link_id) {
294
+ console.log(t("start.relayConnecting", { linkId: identity.link_id }));
123
295
  }
124
- console.log("Daemon start is not wired yet in this scaffold. Protocol identity is ready.");
296
+ await waitForShutdown(async () => {
297
+ relay?.close();
298
+ await new Promise((resolve) => server.close(() => resolve()));
299
+ });
125
300
  });
126
- program.command("pair").option("--relay-bootstrap-token <token>", "short-lived relay bootstrap token from HermesPilot Server").description("Create a Hermes Link pairing session").action(async (options) => {
301
+ program.command("pair").description(helpText("pair.description")).action(async () => {
127
302
  const paths = resolveRuntimePaths();
128
- const identity = await ensureIdentity(paths);
129
303
  const config = await loadConfig(paths);
130
- const token = options.relayBootstrapToken ?? process.env.HERMESLINK_RELAY_BOOTSTRAP_TOKEN;
131
- if (!token) {
132
- console.log("Pairing requires HermesPilot Server and Relay.");
133
- console.log("Server must issue a short-lived relay_bootstrap_token before Link can request a link_id.");
134
- return;
135
- }
136
- const result = await bootstrapRelayLink({
137
- relayBaseUrl: config.relayBaseUrl,
138
- relayBootstrapToken: token,
139
- identity,
140
- paths
304
+ const language = resolveLanguage(config.language);
305
+ const t = translate.bind(null, language);
306
+ console.log(t("pair.preparing"));
307
+ console.log(t("pair.server", { url: config.serverBaseUrl }));
308
+ console.log(t("pair.relay", { url: config.relayBaseUrl }));
309
+ await ensureIdentity(paths);
310
+ const prepared = await preparePairing(paths);
311
+ const server = await startHttpServer(config.port);
312
+ const relay = connectRelayControl({
313
+ relayBaseUrl: prepared.relayBaseUrl,
314
+ linkId: prepared.linkId,
315
+ localPort: config.port
316
+ });
317
+ const qrValue = JSON.stringify(prepared.qrPayload);
318
+ console.log(t("pair.linkId", { value: prepared.linkId }));
319
+ console.log(t("pair.code", { value: prepared.code }));
320
+ console.log(t("pair.localApi", { port: config.port }));
321
+ console.log(t("pair.scan"));
322
+ qrcode.generate(qrValue, { small: true });
323
+ console.log(t("pair.expires"));
324
+ await waitForShutdown(async () => {
325
+ relay.close();
326
+ await new Promise((resolve) => server.close(() => resolve()));
141
327
  });
142
- console.log(`Relay link_id: ${result.linkId}${result.reused ? " (reused)" : ""}`);
143
328
  });
144
- program.command("doctor").description("Run local diagnostics").action(async () => {
145
- const identity = await ensureIdentity();
329
+ program.command("doctor").description(helpText("doctor.description")).action(async () => {
330
+ const [identity, config] = await Promise.all([ensureIdentity(), loadConfig()]);
331
+ const language = resolveLanguage(config.language);
332
+ const t = translate.bind(null, language);
146
333
  const hermesConfig = await ensureHermesApiServerKey();
147
- console.log("Runtime identity: OK");
148
- console.log(`Install ID: ${identity.install_id}`);
149
- console.log(`Link ID: ${identity.link_id ?? "not assigned"}`);
334
+ console.log(t("doctor.identityOk"));
335
+ console.log(t("doctor.installId", { value: identity.install_id }));
336
+ console.log(t("doctor.linkId", { value: identity.link_id ?? t("doctor.notAssigned") }));
150
337
  if (hermesConfig.notice) {
151
338
  console.log(hermesConfig.notice);
152
339
  if (hermesConfig.backupPath) {
@@ -154,8 +341,25 @@ program.command("doctor").description("Run local diagnostics").action(async () =
154
341
  }
155
342
  }
156
343
  });
157
- program.parseAsync(process.argv).catch((error) => {
158
- console.error(error instanceof Error ? error.message : String(error));
344
+ program.parseAsync(process.argv).catch(async (error) => {
345
+ const language = await loadCliLanguage().catch(() => detectSystemLanguage());
346
+ console.error(localizeErrorMessage(error, language));
159
347
  process.exitCode = 1;
160
348
  });
349
+ async function loadCliLanguage() {
350
+ const config = await loadConfig();
351
+ return resolveLanguage(config.language);
352
+ }
353
+ async function startHttpServer(port) {
354
+ const app = await createApp();
355
+ return app.listen(port);
356
+ }
357
+ async function waitForShutdown(cleanup) {
358
+ await new Promise((resolve) => {
359
+ const stop = () => resolve();
360
+ process.once("SIGINT", stop);
361
+ process.once("SIGTERM", stop);
362
+ });
363
+ await cleanup();
364
+ }
161
365
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/cli/index.ts","../../src/relay/bootstrap.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from 'commander'\nimport { LINK_COMMAND, LINK_VERSION } from '../constants.js'\nimport { loadConfig } from '../config/config.js'\nimport { ensureHermesApiServerKey } from '../hermes/config.js'\nimport { ensureIdentity, getIdentityStatus, loadIdentity } from '../identity/identity.js'\nimport { bootstrapRelayLink } from '../relay/bootstrap.js'\nimport { resolveRuntimePaths } from '../runtime/paths.js'\n\nconst program = new Command()\n\nprogram\n .name(LINK_COMMAND)\n .description('Hermes Link companion service')\n .version(LINK_VERSION, '-v, --version', 'print Hermes Link version')\n\nprogram\n .command('status')\n .option('--json', 'print machine-readable status')\n .description('Show local Hermes Link status')\n .action(async (options: { json?: boolean }) => {\n const paths = resolveRuntimePaths()\n const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)])\n const payload = {\n version: LINK_VERSION,\n runtimeHome: paths.homeDir,\n paired: Boolean(identity?.link_id),\n mode: identity?.link_id ? 'paired' : 'local-only',\n port: config.port,\n identity: identity ? getIdentityStatus(identity) : null,\n relay: {\n configured: Boolean(config.relayBaseUrl),\n connected: false,\n },\n }\n if (options.json) {\n console.log(JSON.stringify(payload, null, 2))\n return\n }\n console.log(`Hermes Link ${payload.version}`)\n console.log(`Runtime: ${payload.runtimeHome}`)\n console.log(`Mode: ${payload.mode}`)\n console.log(`Local port: ${payload.port}`)\n console.log(`Link ID: ${payload.identity?.linkId ?? 'not paired'}`)\n })\n\nprogram\n .command('start')\n .description('Start Hermes Link daemon')\n .action(async () => {\n const identity = await loadIdentity()\n if (!identity?.link_id) {\n console.log('Hermes Link is not paired yet. Starting in local-only maintenance mode.')\n console.log('Relay, Server polling, and LAN entrypoints stay disabled until you run `hermeslink pair`.')\n return\n }\n console.log('Daemon start is not wired yet in this scaffold. Protocol identity is ready.')\n })\n\nprogram\n .command('pair')\n .option('--relay-bootstrap-token <token>', 'short-lived relay bootstrap token from HermesPilot Server')\n .description('Create a Hermes Link pairing session')\n .action(async (options: { relayBootstrapToken?: string }) => {\n const paths = resolveRuntimePaths()\n const identity = await ensureIdentity(paths)\n const config = await loadConfig(paths)\n const token = options.relayBootstrapToken ?? process.env.HERMESLINK_RELAY_BOOTSTRAP_TOKEN\n if (!token) {\n console.log('Pairing requires HermesPilot Server and Relay.')\n console.log('Server must issue a short-lived relay_bootstrap_token before Link can request a link_id.')\n return\n }\n const result = await bootstrapRelayLink({\n relayBaseUrl: config.relayBaseUrl,\n relayBootstrapToken: token,\n identity,\n paths,\n })\n console.log(`Relay link_id: ${result.linkId}${result.reused ? ' (reused)' : ''}`)\n })\n\nprogram\n .command('doctor')\n .description('Run local diagnostics')\n .action(async () => {\n const identity = await ensureIdentity()\n const hermesConfig = await ensureHermesApiServerKey()\n console.log('Runtime identity: OK')\n console.log(`Install ID: ${identity.install_id}`)\n console.log(`Link ID: ${identity.link_id ?? 'not assigned'}`)\n if (hermesConfig.notice) {\n console.log(hermesConfig.notice)\n if (hermesConfig.backupPath) {\n console.log(`Hermes config backup: ${hermesConfig.backupPath}`)\n }\n }\n })\n\nprogram.parseAsync(process.argv).catch((error) => {\n console.error(error instanceof Error ? error.message : String(error))\n process.exitCode = 1\n})\n","import { saveAssignedLinkId, signRelayNonce, type LinkIdentity } from '../identity/identity.js'\nimport { resolveRuntimePaths, type RuntimePaths } from '../runtime/paths.js'\n\nexport interface RelayBootstrapOptions {\n relayBaseUrl: string\n relayBootstrapToken: string\n identity: LinkIdentity\n paths?: RuntimePaths\n fetchImpl?: typeof fetch\n}\n\nexport interface RelayBootstrapResult {\n linkId: string\n reused: boolean\n}\n\ninterface RelayChallengeResponse {\n ok?: unknown\n nonce?: unknown\n expires_at?: unknown\n}\n\ninterface RelayBootstrapResponse {\n ok?: unknown\n link_id?: unknown\n reused?: unknown\n}\n\nexport async function bootstrapRelayLink(options: RelayBootstrapOptions): Promise<RelayBootstrapResult> {\n const fetcher = options.fetchImpl ?? fetch\n const baseUrl = options.relayBaseUrl.replace(/\\/+$/u, '')\n const commonPayload = {\n install_id: options.identity.install_id,\n link_id: options.identity.link_id ?? undefined,\n public_key_pem: options.identity.public_key_pem,\n }\n const challenge = await postJson<RelayChallengeResponse>(\n fetcher,\n `${baseUrl}/api/v1/relay/link/challenge`,\n options.relayBootstrapToken,\n commonPayload,\n )\n if (challenge.ok !== true || typeof challenge.nonce !== 'string') {\n throw new Error('Relay did not return a valid install challenge')\n }\n\n const proof = {\n nonce: challenge.nonce,\n signature: signRelayNonce(options.identity, challenge.nonce),\n }\n const assigned = await postJson<RelayBootstrapResponse>(\n fetcher,\n `${baseUrl}/api/v1/relay/link/bootstrap`,\n options.relayBootstrapToken,\n {\n ...commonPayload,\n proof,\n },\n )\n if (assigned.ok !== true || typeof assigned.link_id !== 'string') {\n throw new Error('Relay did not return a valid link_id')\n }\n\n await saveAssignedLinkId(assigned.link_id, options.paths ?? resolveRuntimePaths())\n return {\n linkId: assigned.link_id,\n reused: assigned.reused === true,\n }\n}\n\nasync function postJson<T>(\n fetcher: typeof fetch,\n url: string,\n token: string,\n body: Record<string, unknown>,\n): Promise<T> {\n const response = await fetcher(url, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n const payload = (await response.json().catch(() => null)) as T | null\n if (!response.ok) {\n const message = readErrorMessage(payload) ?? `Relay request failed with HTTP ${response.status}`\n throw new Error(message)\n }\n if (!payload) {\n throw new Error('Relay returned an empty response')\n }\n return payload\n}\n\nfunction readErrorMessage(payload: unknown): string | null {\n if (typeof payload !== 'object' || payload === null) {\n return null\n }\n const error = (payload as { error?: unknown }).error\n if (typeof error !== 'object' || error === null) {\n return null\n }\n const message = (error as { message?: unknown }).message\n return typeof message === 'string' ? message : null\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/cli/index.ts","../../src/i18n.ts","../../src/relay/control-client.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from 'commander'\nimport qrcode from 'qrcode-terminal'\nimport { LINK_COMMAND, LINK_VERSION } from '../constants.js'\nimport { loadConfig } from '../config/config.js'\nimport { ensureHermesApiServerKey } from '../hermes/config.js'\nimport { createApp } from '../http/app.js'\nimport { ensureIdentity, getIdentityStatus, loadIdentity } from '../identity/identity.js'\nimport { detectSystemLanguage, localizeErrorMessage, resolveLanguage, translate } from '../i18n.js'\nimport { preparePairing } from '../pairing/pairing.js'\nimport { connectRelayControl } from '../relay/control-client.js'\nimport { resolveRuntimePaths } from '../runtime/paths.js'\n\nconst program = new Command()\nconst helpLanguage = detectSystemLanguage()\nconst helpText = translate.bind(null, helpLanguage)\n\nprogram\n .name(LINK_COMMAND)\n .description(helpText('program.description'))\n .version(LINK_VERSION, '-v, --version', helpText('program.version'))\n\nprogram\n .command('status')\n .option('--json', helpText('status.json'))\n .description(helpText('status.description'))\n .action(async (options: { json?: boolean }) => {\n const paths = resolveRuntimePaths()\n const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)])\n const language = resolveLanguage(config.language)\n const t = translate.bind(null, language)\n const payload = {\n version: LINK_VERSION,\n runtimeHome: paths.homeDir,\n paired: Boolean(identity?.link_id),\n mode: identity?.link_id ? 'paired' : 'local-only',\n port: config.port,\n identity: identity ? getIdentityStatus(identity) : null,\n relay: {\n configured: Boolean(config.relayBaseUrl),\n connected: false,\n },\n }\n if (options.json) {\n console.log(JSON.stringify(payload, null, 2))\n return\n }\n console.log(`Hermes Link ${payload.version}`)\n console.log(t('status.runtime', { value: payload.runtimeHome }))\n console.log(t('status.mode', { value: payload.mode }))\n console.log(t('status.port', { value: payload.port }))\n console.log(t('status.linkId', { value: payload.identity?.linkId ?? t('status.notPaired') }))\n })\n\nprogram\n .command('start')\n .description(helpText('start.description'))\n .action(async () => {\n const [identity, config] = await Promise.all([loadIdentity(), loadConfig()])\n const language = resolveLanguage(config.language)\n const t = translate.bind(null, language)\n if (!identity?.link_id) {\n console.log(t('start.notPaired'))\n console.log(t('start.notPaired.detail'))\n }\n const server = await startHttpServer(config.port)\n const relay = identity?.link_id\n ? connectRelayControl({\n relayBaseUrl: config.relayBaseUrl,\n linkId: identity.link_id,\n localPort: config.port,\n })\n : null\n console.log(t('start.listening', { port: config.port }))\n if (identity?.link_id) {\n console.log(t('start.relayConnecting', { linkId: identity.link_id }))\n }\n await waitForShutdown(async () => {\n relay?.close()\n await new Promise<void>((resolve) => server.close(() => resolve()))\n })\n })\n\nprogram\n .command('pair')\n .description(helpText('pair.description'))\n .action(async () => {\n const paths = resolveRuntimePaths()\n const config = await loadConfig(paths)\n const language = resolveLanguage(config.language)\n const t = translate.bind(null, language)\n console.log(t('pair.preparing'))\n console.log(t('pair.server', { url: config.serverBaseUrl }))\n console.log(t('pair.relay', { url: config.relayBaseUrl }))\n await ensureIdentity(paths)\n const prepared = await preparePairing(paths)\n const server = await startHttpServer(config.port)\n const relay = connectRelayControl({\n relayBaseUrl: prepared.relayBaseUrl,\n linkId: prepared.linkId,\n localPort: config.port,\n })\n const qrValue = JSON.stringify(prepared.qrPayload)\n console.log(t('pair.linkId', { value: prepared.linkId }))\n console.log(t('pair.code', { value: prepared.code }))\n console.log(t('pair.localApi', { port: config.port }))\n console.log(t('pair.scan'))\n qrcode.generate(qrValue, { small: true })\n console.log(t('pair.expires'))\n await waitForShutdown(async () => {\n relay.close()\n await new Promise<void>((resolve) => server.close(() => resolve()))\n })\n })\n\nprogram\n .command('doctor')\n .description(helpText('doctor.description'))\n .action(async () => {\n const [identity, config] = await Promise.all([ensureIdentity(), loadConfig()])\n const language = resolveLanguage(config.language)\n const t = translate.bind(null, language)\n const hermesConfig = await ensureHermesApiServerKey()\n console.log(t('doctor.identityOk'))\n console.log(t('doctor.installId', { value: identity.install_id }))\n console.log(t('doctor.linkId', { value: identity.link_id ?? t('doctor.notAssigned') }))\n if (hermesConfig.notice) {\n console.log(hermesConfig.notice)\n if (hermesConfig.backupPath) {\n console.log(`Hermes config backup: ${hermesConfig.backupPath}`)\n }\n }\n })\n\nprogram.parseAsync(process.argv).catch(async (error) => {\n const language = await loadCliLanguage().catch(() => detectSystemLanguage())\n console.error(localizeErrorMessage(error, language))\n process.exitCode = 1\n})\n\nasync function loadCliLanguage() {\n const config = await loadConfig()\n return resolveLanguage(config.language)\n}\n\nasync function startHttpServer(port: number) {\n const app = await createApp()\n return app.listen(port)\n}\n\nasync function waitForShutdown(cleanup: () => Promise<void>): Promise<void> {\n await new Promise<void>((resolve) => {\n const stop = () => resolve()\n process.once('SIGINT', stop)\n process.once('SIGTERM', stop)\n })\n await cleanup()\n}\n","export type SupportedLanguage = 'zh-CN' | 'en'\nexport type ConfiguredLanguage = SupportedLanguage | 'auto'\n\nconst messages = {\n en: {\n 'program.description': 'Hermes Link companion service',\n 'program.version': 'print Hermes Link version',\n 'status.description': 'Show local Hermes Link status',\n 'status.json': 'print machine-readable status',\n 'status.runtime': 'Runtime: {value}',\n 'status.mode': 'Mode: {value}',\n 'status.port': 'Local port: {value}',\n 'status.linkId': 'Link ID: {value}',\n 'status.notPaired': 'not paired',\n 'start.description': 'Start Hermes Link daemon',\n 'start.notPaired': 'Hermes Link is not paired yet. Starting in local-only maintenance mode.',\n 'start.notPaired.detail':\n 'Relay, Server polling, and LAN entrypoints stay disabled until you run `hermeslink pair`.',\n 'start.listening': 'Hermes Link API listening on http://127.0.0.1:{port}',\n 'start.relayConnecting': 'Relay control connecting for {linkId}',\n 'pair.description': 'Create a Hermes Link pairing session',\n 'pair.preparing': 'Preparing pairing session through HermesPilot Server and Relay...',\n 'pair.server': 'Server: {url}',\n 'pair.relay': 'Relay: {url}',\n 'pair.linkId': 'Hermes Link ID: {value}',\n 'pair.code': 'Pairing code: {value}',\n 'pair.localApi': 'Local API: http://127.0.0.1:{port}',\n 'pair.scan': 'Scan this QR code with the HermesPilot App:',\n 'pair.expires': 'Pairing expires in 10 minutes. Press Ctrl+C to stop Hermes Link.',\n 'doctor.description': 'Run local diagnostics',\n 'doctor.identityOk': 'Runtime identity: OK',\n 'doctor.installId': 'Install ID: {value}',\n 'doctor.linkId': 'Link ID: {value}',\n 'doctor.notAssigned': 'not assigned',\n 'error.relayPublicKeyMismatch':\n 'Relay rejected the pairing request because the Server-issued bootstrap token does not match this Link public key. Make sure Server and Relay are deployed with the same bootstrap key configuration, then run `hermeslink pair` again.',\n 'error.relayChallengeInvalid': 'Relay did not return a valid install challenge.',\n 'error.relayLinkInvalid': 'Relay did not return a valid link_id.',\n 'error.relayEmpty': 'Relay returned an empty response.',\n 'error.serverHttp': 'HermesPilot Server request failed with HTTP {status}.',\n 'error.pairingRequires':\n 'Pairing needs HermesPilot Server and Relay, but this command could not start a complete pairing session.',\n 'error.pairingRequires.detail':\n 'The deployed services may be healthy, but the installed Link package must call Server for a short-lived relay bootstrap token before it can request a link_id.',\n },\n 'zh-CN': {\n 'program.description': 'Hermes Link 本地伴随服务',\n 'program.version': '输出 Hermes Link 版本号',\n 'status.description': '查看本机 Hermes Link 状态',\n 'status.json': '输出机器可读的状态 JSON',\n 'status.runtime': '运行目录:{value}',\n 'status.mode': '模式:{value}',\n 'status.port': '本地端口:{value}',\n 'status.linkId': 'Link ID:{value}',\n 'status.notPaired': '尚未配对',\n 'start.description': '启动 Hermes Link 服务',\n 'start.notPaired': 'Hermes Link 还没有配对,将以本地维护模式启动。',\n 'start.notPaired.detail': '在你运行 `hermeslink pair` 前,Relay、Server 轮询和局域网入口都会保持关闭。',\n 'start.listening': 'Hermes Link API 正在监听 http://127.0.0.1:{port}',\n 'start.relayConnecting': '正在为 {linkId} 连接 Relay 控制通道',\n 'pair.description': '创建 Hermes Link 配对会话',\n 'pair.preparing': '正在通过 HermesPilot Server 和 Relay 创建配对会话...',\n 'pair.server': 'Server:{url}',\n 'pair.relay': 'Relay:{url}',\n 'pair.linkId': 'Hermes Link ID:{value}',\n 'pair.code': '配对码:{value}',\n 'pair.localApi': '本地 API:http://127.0.0.1:{port}',\n 'pair.scan': '请使用 HermesPilot App 扫描这个二维码:',\n 'pair.expires': '配对会话 10 分钟后过期。按 Ctrl+C 停止 Hermes Link。',\n 'doctor.description': '运行本机诊断',\n 'doctor.identityOk': '运行身份:正常',\n 'doctor.installId': 'Install ID:{value}',\n 'doctor.linkId': 'Link ID:{value}',\n 'doctor.notAssigned': '尚未分配',\n 'error.relayPublicKeyMismatch':\n 'Relay 拒绝了配对请求:Server 签发的 bootstrap token 与本机 Link 公钥不匹配。请确认 Server 和 Relay 使用同一套 bootstrap key 配置,然后重新运行 `hermeslink pair`。',\n 'error.relayChallengeInvalid': 'Relay 没有返回有效的安装挑战。',\n 'error.relayLinkInvalid': 'Relay 没有返回有效的 link_id。',\n 'error.relayEmpty': 'Relay 返回了空响应。',\n 'error.serverHttp': 'HermesPilot Server 请求失败,HTTP 状态码:{status}。',\n 'error.pairingRequires': '配对需要 HermesPilot Server 和 Relay,但当前命令没有能启动完整配对会话。',\n 'error.pairingRequires.detail':\n '云端服务可以是已部署且健康的;本机 Link 仍必须先向 Server 申请短期 relay bootstrap token,才能再向 Relay 申请 link_id。',\n },\n} satisfies Record<SupportedLanguage, Record<string, string>>\n\nexport type MessageKey = keyof (typeof messages)['en']\n\nexport function detectSystemLanguage(env: NodeJS.ProcessEnv = process.env): SupportedLanguage {\n const candidates = [\n env.HERMESLINK_LANG,\n env.HERMESLINK_LANGUAGE,\n env.LC_ALL,\n env.LC_MESSAGES,\n env.LANG,\n env.LANGUAGE?.split(':')[0],\n Intl.DateTimeFormat().resolvedOptions().locale,\n ]\n for (const candidate of candidates) {\n const language = parseLanguage(candidate)\n if (language) {\n return language\n }\n }\n return 'en'\n}\n\nexport function resolveLanguage(setting?: ConfiguredLanguage | string | null): SupportedLanguage {\n const configured = parseLanguage(setting)\n if (configured) {\n return configured\n }\n return detectSystemLanguage()\n}\n\nexport function translate(\n language: SupportedLanguage,\n key: MessageKey,\n values: Record<string, string | number> = {},\n): string {\n const template = messages[language][key] ?? messages.en[key]\n return template.replace(/\\{(\\w+)\\}/gu, (_, name: string) => String(values[name] ?? ''))\n}\n\nexport function localizeErrorMessage(error: unknown, language: SupportedLanguage): string {\n const message = error instanceof Error ? error.message : String(error)\n if (language === 'en') {\n return message\n }\n const mapped = translateKnownError(message, language)\n return mapped ?? message\n}\n\nfunction translateKnownError(message: string, language: SupportedLanguage): string | null {\n if (message === 'Relay bootstrap token does not match public key') {\n return translate(language, 'error.relayPublicKeyMismatch')\n }\n if (message === 'Relay did not return a valid install challenge') {\n return translate(language, 'error.relayChallengeInvalid')\n }\n if (message === 'Relay did not return a valid link_id') {\n return translate(language, 'error.relayLinkInvalid')\n }\n if (message === 'Relay returned an empty response') {\n return translate(language, 'error.relayEmpty')\n }\n const serverHttp = /^HermesPilot Server request failed with HTTP (?<status>\\d+)$/u.exec(message)\n if (serverHttp?.groups?.status) {\n return translate(language, 'error.serverHttp', { status: serverHttp.groups.status })\n }\n if (message.includes('Pairing requires HermesPilot Server and Relay')) {\n return [translate(language, 'error.pairingRequires'), translate(language, 'error.pairingRequires.detail')].join('\\n')\n }\n return null\n}\n\nfunction parseLanguage(value: string | null | undefined): SupportedLanguage | null {\n const normalized = value?.trim().replace('_', '-').toLowerCase()\n if (!normalized || normalized === 'auto' || normalized === 'c' || normalized === 'posix') {\n return null\n }\n if (normalized.startsWith('zh')) {\n return 'zh-CN'\n }\n if (normalized.startsWith('en')) {\n return 'en'\n }\n return null\n}\n","import WebSocket from 'ws'\nimport { LINK_VERSION } from '../constants.js'\n\ninterface RelayRequestFrame {\n type: 'http.request'\n id: string\n method: string\n path: string\n headers?: Record<string, string>\n bodyBase64?: string | null\n}\n\ninterface RelayCancelFrame {\n type: 'http.cancel'\n id: string\n}\n\ntype RelayFrame = RelayRequestFrame | RelayCancelFrame\n\nexport interface RelayControlClient {\n close(): void\n}\n\nexport function connectRelayControl(options: {\n relayBaseUrl: string\n linkId: string\n localPort: number\n}): RelayControlClient {\n const wsUrl = new URL(`${options.relayBaseUrl.replace(/\\/+$/u, '')}/api/v1/relay/link/connect`)\n wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:'\n wsUrl.searchParams.set('link_id', options.linkId)\n\n const socket = new WebSocket(wsUrl, {\n headers: {\n 'x-hermes-link-version': LINK_VERSION,\n },\n })\n const abortControllers = new Map<string, AbortController>()\n socket.on('message', (raw) => {\n if (typeof raw !== 'string' && !Buffer.isBuffer(raw)) {\n return\n }\n void handleFrame(socket, String(raw), options.localPort, abortControllers).catch((error) => {\n const message = error instanceof Error ? error.message : 'Relay request failed'\n socket.send(JSON.stringify({ type: 'http.error', id: 'unknown', status: 502, message }))\n })\n })\n socket.on('close', () => {\n for (const controller of abortControllers.values()) {\n controller.abort()\n }\n abortControllers.clear()\n })\n return {\n close() {\n socket.close()\n },\n }\n}\n\nasync function handleFrame(\n socket: WebSocket,\n raw: string,\n localPort: number,\n abortControllers: Map<string, AbortController>,\n): Promise<void> {\n const frame = JSON.parse(raw) as RelayFrame\n if (frame.type === 'http.cancel') {\n abortControllers.get(frame.id)?.abort()\n abortControllers.delete(frame.id)\n return\n }\n if (frame.type !== 'http.request') {\n return\n }\n const abortController = new AbortController()\n abortControllers.set(frame.id, abortController)\n try {\n const response = await fetch(`http://127.0.0.1:${localPort}${frame.path}`, {\n method: frame.method,\n headers: frame.headers ?? {},\n body: frame.bodyBase64 ? Buffer.from(frame.bodyBase64, 'base64') : undefined,\n signal: abortController.signal,\n })\n const headers = Object.fromEntries(response.headers.entries())\n const contentType = response.headers.get('content-type') ?? ''\n if (response.body && contentType.includes('text/event-stream')) {\n socket.send(JSON.stringify({ type: 'http.stream.start', id: frame.id, status: response.status, headers }))\n const reader = response.body.getReader()\n while (true) {\n const next = await reader.read()\n if (next.done) {\n break\n }\n socket.send(JSON.stringify({ type: 'http.stream.chunk', id: frame.id, bodyBase64: Buffer.from(next.value).toString('base64') }))\n }\n socket.send(JSON.stringify({ type: 'http.stream.end', id: frame.id }))\n return\n }\n const body = Buffer.from(await response.arrayBuffer()).toString('base64')\n socket.send(JSON.stringify({ type: 'http.response', id: frame.id, status: response.status, headers, bodyBase64: body }))\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Relay request failed'\n socket.send(JSON.stringify({ type: 'http.error', id: frame.id, status: 502, message }))\n } finally {\n abortControllers.delete(frame.id)\n }\n}\n"],"mappings":"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","names":[]}