@hermespilot/link 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import Router from "@koa/router";
4
4
  import { Readable } from "stream";
5
5
 
6
6
  // src/constants.ts
7
- var LINK_VERSION = "0.1.6";
7
+ var LINK_VERSION = "0.1.8";
8
8
  var LINK_COMMAND = "hermeslink";
9
9
  var LINK_DEFAULT_PORT = 52379;
10
10
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -96,12 +96,30 @@ function normalizeConfiguredLanguage(language) {
96
96
 
97
97
  // src/conversations/conversation-service.ts
98
98
  import { EventEmitter } from "events";
99
- import { appendFile, mkdir as mkdir3, readdir, readFile as readFile3, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
100
- import path4 from "path";
101
- import { createHash, randomUUID as randomUUID2 } from "crypto";
99
+ import { appendFile, mkdir as mkdir4, readdir, readFile as readFile3, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
100
+ import path5 from "path";
101
+ import { createHash, randomUUID } from "crypto";
102
102
 
103
- // src/hermes/api-server.ts
104
- import { randomUUID } from "crypto";
103
+ // src/http/errors.ts
104
+ var LinkHttpError = class extends Error {
105
+ constructor(status, code, message) {
106
+ super(message);
107
+ this.status = status;
108
+ this.code = code;
109
+ }
110
+ status;
111
+ code;
112
+ };
113
+ function isLinkHttpError(error) {
114
+ return error instanceof LinkHttpError;
115
+ }
116
+
117
+ // src/hermes/gateway.ts
118
+ import { execFile as execFile2, spawn } from "child_process";
119
+ import { mkdir as mkdir3, open as open2 } from "fs/promises";
120
+ import path4 from "path";
121
+ import { setTimeout as delay } from "timers/promises";
122
+ import { promisify as promisify2 } from "util";
105
123
 
106
124
  // src/hermes/config.ts
107
125
  import { randomBytes } from "crypto";
@@ -109,6 +127,8 @@ import { copyFile, mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/
109
127
  import os2 from "os";
110
128
  import path3 from "path";
111
129
  import YAML from "yaml";
130
+ var DEFAULT_HERMES_API_SERVER_HOST = "127.0.0.1";
131
+ var DEFAULT_HERMES_API_SERVER_PORT = 8642;
112
132
  function resolveHermesProfileDir(profileName = "default") {
113
133
  if (profileName === "default") {
114
134
  return path3.join(os2.homedir(), ".hermes");
@@ -126,14 +146,21 @@ async function readHermesApiServerConfig(profileName = "default", configPath = r
126
146
  throw error;
127
147
  });
128
148
  if (!existingRaw) {
129
- return {};
149
+ return applyEnvOverrides({}, await readHermesApiServerEnvOverrides(profileName), false);
130
150
  }
131
151
  const config = toRecord(YAML.parse(existingRaw));
132
152
  const platforms = toRecord(config.platforms);
133
153
  const apiServer = toRecord(platforms.api_server);
134
- return readApiServerConfig(toRecord(apiServer.extra));
154
+ return applyEnvOverrides(
155
+ readApiServerConfig(apiServer, true),
156
+ await readHermesApiServerEnvOverrides(profileName),
157
+ true
158
+ );
135
159
  }
136
160
  async function ensureHermesApiServerKey(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
161
+ return ensureHermesApiServerConfig(profileName, configPath);
162
+ }
163
+ async function ensureHermesApiServerConfig(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
137
164
  const existingRaw = await readFile2(configPath, "utf8").catch((error) => {
138
165
  if (isNodeError2(error, "ENOENT")) {
139
166
  return null;
@@ -145,8 +172,31 @@ async function ensureHermesApiServerKey(profileName = "default", configPath = re
145
172
  const platforms = ensureRecord(config, "platforms");
146
173
  const apiServer = ensureRecord(platforms, "api_server");
147
174
  const extra = ensureRecord(apiServer, "extra");
148
- const beforeKey = typeof extra.key === "string" && extra.key.length > 0 ? extra.key : null;
175
+ const envOverrides = await readHermesApiServerEnvOverrides(profileName);
176
+ const before = applyEnvOverrides(readApiServerConfig(apiServer), envOverrides, false);
177
+ const beforeKey = before.key?.trim() ? before.key : null;
178
+ const beforeEnabled = before.enabled === true;
179
+ const beforeHost = before.host?.trim() ? before.host : null;
180
+ const beforePort = typeof before.port === "number" && Number.isFinite(before.port) ? before.port : null;
149
181
  let changed = false;
182
+ let enabledAdded = false;
183
+ let hostAdded = false;
184
+ let portAdded = false;
185
+ if (!beforeEnabled) {
186
+ apiServer.enabled = true;
187
+ enabledAdded = true;
188
+ changed = true;
189
+ }
190
+ if (!beforeHost) {
191
+ extra.host = DEFAULT_HERMES_API_SERVER_HOST;
192
+ hostAdded = true;
193
+ changed = true;
194
+ }
195
+ if (!beforePort) {
196
+ extra.port = DEFAULT_HERMES_API_SERVER_PORT;
197
+ portAdded = true;
198
+ changed = true;
199
+ }
150
200
  if (!beforeKey) {
151
201
  extra.key = randomBytes(32).toString("base64url");
152
202
  changed = true;
@@ -154,9 +204,12 @@ async function ensureHermesApiServerKey(profileName = "default", configPath = re
154
204
  if (!changed) {
155
205
  return {
156
206
  configPath,
157
- apiServer: readApiServerConfig(extra),
207
+ apiServer: applyEnvOverrides(readApiServerConfig(apiServer, true), envOverrides, true),
158
208
  changed: false,
159
209
  keyAdded: false,
210
+ enabledAdded: false,
211
+ hostAdded: false,
212
+ portAdded: false,
160
213
  backupPath: null,
161
214
  notice: null
162
215
  };
@@ -170,20 +223,111 @@ async function ensureHermesApiServerKey(profileName = "default", configPath = re
170
223
  await writeFile(configPath, document.toString(), { mode: 384 });
171
224
  return {
172
225
  configPath,
173
- apiServer: readApiServerConfig(extra),
226
+ apiServer: applyEnvOverrides(readApiServerConfig(apiServer, true), envOverrides, true),
174
227
  changed: true,
175
- keyAdded: true,
228
+ keyAdded: !beforeKey,
229
+ enabledAdded,
230
+ hostAdded,
231
+ portAdded,
176
232
  backupPath,
177
- notice: "\u5DF2\u4E3A Hermes API Server \u81EA\u52A8\u8865\u5145 key\uFF1B\u672A\u8986\u76D6\u5DF2\u6709 port/host/key\u3002"
233
+ notice: buildNotice({ keyAdded: !beforeKey, enabledAdded, hostAdded, portAdded })
178
234
  };
179
235
  }
180
- function readApiServerConfig(extra) {
236
+ function readApiServerConfig(apiServerOrExtra, withDefaults = false) {
237
+ const apiServer = "extra" in apiServerOrExtra ? apiServerOrExtra : {};
238
+ const extra = toRecord("extra" in apiServerOrExtra ? apiServerOrExtra.extra : apiServerOrExtra);
239
+ const port = typeof extra.port === "number" ? extra.port : void 0;
240
+ const host = typeof extra.host === "string" ? extra.host : void 0;
181
241
  return {
182
- host: typeof extra.host === "string" ? extra.host : void 0,
183
- port: typeof extra.port === "number" ? extra.port : void 0,
242
+ enabled: apiServer.enabled === true,
243
+ host: withDefaults ? host ?? DEFAULT_HERMES_API_SERVER_HOST : host,
244
+ port: withDefaults ? port ?? DEFAULT_HERMES_API_SERVER_PORT : port,
184
245
  key: typeof extra.key === "string" ? extra.key : void 0
185
246
  };
186
247
  }
248
+ async function readHermesApiServerEnvOverrides(profileName) {
249
+ const values = await readHermesEnvFile(profileName);
250
+ for (const key of ["API_SERVER_ENABLED", "API_SERVER_HOST", "API_SERVER_PORT", "API_SERVER_KEY"]) {
251
+ const value = process.env[key];
252
+ if (typeof value === "string" && value.trim()) {
253
+ values[key] = value;
254
+ }
255
+ }
256
+ const port = Number.parseInt(values.API_SERVER_PORT ?? "", 10);
257
+ return {
258
+ enabled: parseEnvBoolean(values.API_SERVER_ENABLED),
259
+ host: values.API_SERVER_HOST?.trim() || void 0,
260
+ port: Number.isFinite(port) ? port : void 0,
261
+ key: values.API_SERVER_KEY?.trim() || void 0
262
+ };
263
+ }
264
+ async function readHermesEnvFile(profileName) {
265
+ const envPath = path3.join(resolveHermesProfileDir(profileName), ".env");
266
+ const raw = await readFile2(envPath, "utf8").catch((error) => {
267
+ if (isNodeError2(error, "ENOENT")) {
268
+ return "";
269
+ }
270
+ throw error;
271
+ });
272
+ const values = {};
273
+ for (const line of raw.split(/\r?\n/u)) {
274
+ const trimmed = line.trim();
275
+ if (!trimmed || trimmed.startsWith("#")) {
276
+ continue;
277
+ }
278
+ const match = /^(?:export\s+)?(?<key>[A-Za-z_][A-Za-z0-9_]*)=(?<value>.*)$/u.exec(trimmed);
279
+ if (!match?.groups) {
280
+ continue;
281
+ }
282
+ values[match.groups.key] = unquoteEnvValue(match.groups.value.trim());
283
+ }
284
+ return values;
285
+ }
286
+ function applyEnvOverrides(config, env, withDefaults) {
287
+ const host = env.host ?? config.host;
288
+ const port = env.port ?? config.port;
289
+ return {
290
+ enabled: env.enabled ?? config.enabled,
291
+ host: withDefaults ? host ?? DEFAULT_HERMES_API_SERVER_HOST : host,
292
+ port: withDefaults ? port ?? DEFAULT_HERMES_API_SERVER_PORT : port,
293
+ key: env.key ?? config.key
294
+ };
295
+ }
296
+ function parseEnvBoolean(value) {
297
+ if (!value) {
298
+ return void 0;
299
+ }
300
+ const normalized = value.trim().toLowerCase();
301
+ if (["1", "true", "yes", "on"].includes(normalized)) {
302
+ return true;
303
+ }
304
+ if (["0", "false", "no", "off"].includes(normalized)) {
305
+ return false;
306
+ }
307
+ return void 0;
308
+ }
309
+ function unquoteEnvValue(value) {
310
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
311
+ return value.slice(1, -1);
312
+ }
313
+ return value;
314
+ }
315
+ function buildNotice(flags) {
316
+ const fields = [];
317
+ if (flags.enabledAdded) {
318
+ fields.push("enabled");
319
+ }
320
+ if (flags.hostAdded) {
321
+ fields.push("host=127.0.0.1");
322
+ }
323
+ if (flags.portAdded) {
324
+ fields.push("port=8642");
325
+ }
326
+ if (flags.keyAdded) {
327
+ fields.push("key");
328
+ }
329
+ return `\u5DF2\u4E3A Hermes API Server \u81EA\u52A8\u8865\u5145 ${fields.join("\u3001")}\uFF1B\u672A\u8986\u76D6\u5DF2\u6709 port/host/key\u3002`;
330
+ }
187
331
  function toRecord(value) {
188
332
  return typeof value === "object" && value !== null ? value : {};
189
333
  }
@@ -200,22 +344,199 @@ function isNodeError2(error, code) {
200
344
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
201
345
  }
202
346
 
203
- // src/http/errors.ts
204
- var LinkHttpError = class extends Error {
205
- constructor(status, code, message) {
206
- super(message);
207
- this.status = status;
208
- this.code = code;
347
+ // src/hermes/cli.ts
348
+ import { execFile } from "child_process";
349
+ import { promisify } from "util";
350
+ var execFileAsync = promisify(execFile);
351
+ async function deleteHermesSession(sessionId) {
352
+ if (!sessionId.trim()) {
353
+ throw new LinkHttpError(400, "hermes_session_id_required", "Hermes session id is required");
209
354
  }
210
- status;
211
- code;
212
- };
213
- function isLinkHttpError(error) {
214
- return error instanceof LinkHttpError;
355
+ try {
356
+ await execFileAsync(resolveHermesBin(), ["sessions", "delete", sessionId, "--yes"], {
357
+ timeout: 1e4,
358
+ windowsHide: true
359
+ });
360
+ } catch (error) {
361
+ throw new LinkHttpError(
362
+ 502,
363
+ "hermes_session_delete_failed",
364
+ error instanceof Error ? error.message : "Hermes session delete failed"
365
+ );
366
+ }
367
+ }
368
+ function resolveHermesBin() {
369
+ return process.env.HERMES_BIN?.trim() || "hermes";
370
+ }
371
+
372
+ // src/hermes/gateway.ts
373
+ var execFileAsync2 = promisify2(execFile2);
374
+ var DEFAULT_START_TIMEOUT_MS = 12e3;
375
+ var HEALTH_TIMEOUT_MS = 1500;
376
+ var MIN_API_SERVER_VERSION = "0.4.0";
377
+ var gatewayStartInFlight = null;
378
+ async function ensureHermesApiServerAvailable(options = {}) {
379
+ const configResult = await ensureHermesApiServerConfig();
380
+ const fetcher = options.fetchImpl ?? fetch;
381
+ if (!options.forceRestart && await isHermesApiHealthy(configResult.apiServer, fetcher)) {
382
+ return { available: true, configResult, started: false };
383
+ }
384
+ if (!shouldAutoStart(options.autoStart)) {
385
+ throw new LinkHttpError(503, "hermes_api_server_unavailable", unavailableMessage());
386
+ }
387
+ const start = await startHermesGatewayOnce(options.paths ?? resolveRuntimePaths());
388
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_START_TIMEOUT_MS);
389
+ while (Date.now() < deadline) {
390
+ if (await isHermesApiHealthy(configResult.apiServer, fetcher)) {
391
+ return { available: true, configResult, started: true, start };
392
+ }
393
+ await delay(400);
394
+ }
395
+ throw new LinkHttpError(
396
+ 503,
397
+ "hermes_api_server_unavailable",
398
+ `${unavailableMessage()} Link tried to start it with \`${resolveHermesBin()} gateway run --replace\`, but it did not become ready. Gateway log: ${start.logPath}`
399
+ );
400
+ }
401
+ async function readHermesVersion() {
402
+ const { stdout } = await execHermesVersion();
403
+ const raw = stdout.trim();
404
+ const version = parseHermesVersion(raw);
405
+ return {
406
+ raw,
407
+ version,
408
+ supportsApiServer: version ? compareSemver(version, MIN_API_SERVER_VERSION) >= 0 : null
409
+ };
410
+ }
411
+ async function execHermesVersion() {
412
+ try {
413
+ return await execFileAsync2(resolveHermesBin(), ["version"], {
414
+ timeout: 5e3,
415
+ windowsHide: true
416
+ });
417
+ } catch {
418
+ return await execFileAsync2(resolveHermesBin(), ["--version"], {
419
+ timeout: 5e3,
420
+ windowsHide: true
421
+ });
422
+ }
423
+ }
424
+ function assertHermesRunsApiSupported(version, status) {
425
+ if (status !== 404) {
426
+ return;
427
+ }
428
+ const versionText = version?.version ? `\u5F53\u524D\u68C0\u6D4B\u5230 Hermes Agent ${version.version}\u3002` : "";
429
+ throw new LinkHttpError(
430
+ 502,
431
+ "hermes_runs_api_unsupported",
432
+ `${versionText}\u5F53\u524D Hermes Agent API Server \u4E0D\u652F\u6301 HermesPilot \u9700\u8981\u7684 /v1/runs \u63A5\u53E3\uFF0C\u8BF7\u5148\u8FD0\u884C \`hermes update\` \u5347\u7EA7 Hermes Agent\u3002`
433
+ );
434
+ }
435
+ async function startHermesGatewayOnce(paths) {
436
+ if (!gatewayStartInFlight) {
437
+ gatewayStartInFlight = startHermesGateway(paths).finally(() => {
438
+ gatewayStartInFlight = null;
439
+ });
440
+ }
441
+ return await gatewayStartInFlight;
442
+ }
443
+ async function startHermesGateway(paths) {
444
+ const version = await readHermesVersion().catch((error) => {
445
+ throw new LinkHttpError(
446
+ 503,
447
+ "hermes_agent_not_available",
448
+ `\u6CA1\u6709\u627E\u5230\u53EF\u7528\u7684 Hermes Agent CLI\u3002\u8BF7\u5148\u5B89\u88C5 Hermes Agent\uFF0C\u6216\u8BBE\u7F6E HERMES_BIN\u3002${formatErrorSuffix(error)}`
449
+ );
450
+ });
451
+ if (version.supportsApiServer === false) {
452
+ throw new LinkHttpError(
453
+ 503,
454
+ "hermes_agent_too_old",
455
+ `\u5F53\u524D Hermes Agent ${version.version} \u592A\u65E7\uFF0C\u53EF\u80FD\u4E0D\u652F\u6301 Hermes API Server\u3002\u8BF7\u5148\u8FD0\u884C \`hermes update\` \u5347\u7EA7\u3002`
456
+ );
457
+ }
458
+ await mkdir3(paths.logsDir, { recursive: true, mode: 448 });
459
+ const logPath = path4.join(paths.logsDir, "hermes-gateway.log");
460
+ const logFile = await open2(logPath, "a", 384);
461
+ try {
462
+ const child = spawn(resolveHermesBin(), ["gateway", "run", "--replace"], {
463
+ detached: true,
464
+ env: process.env,
465
+ stdio: ["ignore", logFile.fd, logFile.fd],
466
+ windowsHide: true
467
+ });
468
+ await new Promise((resolve, reject) => {
469
+ let settled = false;
470
+ const finish = (callback) => {
471
+ if (settled) {
472
+ return;
473
+ }
474
+ settled = true;
475
+ callback();
476
+ };
477
+ child.once("spawn", () => finish(resolve));
478
+ child.once("error", (error) => finish(() => reject(error)));
479
+ });
480
+ child.unref();
481
+ return { pid: child.pid ?? null, logPath, version };
482
+ } finally {
483
+ await logFile.close().catch(() => void 0);
484
+ }
485
+ }
486
+ async function isHermesApiHealthy(config, fetcher) {
487
+ const response = await fetchWithTimeout(`http://127.0.0.1:${config.port}/health`, {
488
+ method: "GET",
489
+ headers: authHeaders(config)
490
+ }, fetcher);
491
+ return Boolean(response?.ok);
492
+ }
493
+ function fetchWithTimeout(input, init, fetcher) {
494
+ const controller = new AbortController();
495
+ const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
496
+ return fetcher(input, { ...init, signal: controller.signal }).catch(() => null).finally(() => clearTimeout(timer));
497
+ }
498
+ function authHeaders(config) {
499
+ const headers = new Headers();
500
+ headers.set("accept", "application/json");
501
+ if (config.key) {
502
+ headers.set("x-api-key", config.key);
503
+ headers.set("authorization", `Bearer ${config.key}`);
504
+ }
505
+ return headers;
506
+ }
507
+ function shouldAutoStart(value) {
508
+ if (value !== void 0) {
509
+ return value;
510
+ }
511
+ const raw = process.env.HERMESLINK_GATEWAY_AUTOSTART?.trim().toLowerCase();
512
+ return raw !== "0" && raw !== "false" && raw !== "off";
513
+ }
514
+ function unavailableMessage() {
515
+ return "Hermes API Server \u5F53\u524D\u4E0D\u53EF\u7528\u3002\u8BF7\u786E\u8BA4 Hermes Agent \u5DF2\u5B89\u88C5\uFF0C\u5E76\u53EF\u901A\u8FC7 `hermes gateway run` \u542F\u52A8\uFF1BHermesPilot Link \u9700\u8981 Hermes Agent \u7684\u672C\u673A API Server\u3002";
516
+ }
517
+ function parseHermesVersion(value) {
518
+ const match = /\bv?(\d+\.\d+\.\d+)\b/u.exec(value);
519
+ return match?.[1] ?? null;
520
+ }
521
+ function compareSemver(left, right) {
522
+ const leftParts = left.split(".").map((part) => Number.parseInt(part, 10));
523
+ const rightParts = right.split(".").map((part) => Number.parseInt(part, 10));
524
+ for (let index = 0; index < 3; index += 1) {
525
+ const diff = (leftParts[index] || 0) - (rightParts[index] || 0);
526
+ if (diff !== 0) {
527
+ return diff;
528
+ }
529
+ }
530
+ return 0;
531
+ }
532
+ function formatErrorSuffix(error) {
533
+ if (!(error instanceof Error) || !error.message) {
534
+ return "";
535
+ }
536
+ return ` \u539F\u59CB\u9519\u8BEF\uFF1A${error.message}`;
215
537
  }
216
538
 
217
539
  // src/hermes/api-server.ts
218
- var fallbackRuns = /* @__PURE__ */ new Map();
219
540
  async function listHermesModels(options = {}) {
220
541
  const response = await callHermesApi("/v1/models", { method: "GET" }, options);
221
542
  if (response.status === 404) {
@@ -234,9 +555,8 @@ async function createHermesRun(input, options = {}) {
234
555
  options
235
556
  );
236
557
  if (response.status === 404 || response.status === 503) {
237
- const runId2 = `run_${randomUUID().replaceAll("-", "")}`;
238
- fallbackRuns.set(runId2, { input: input.input, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
239
- return { run_id: runId2, fallback: true };
558
+ assertHermesRunsApiSupported(await readHermesVersion().catch(() => null), response.status);
559
+ throw new LinkHttpError(503, "hermes_api_server_unavailable", "Hermes API Server is unavailable");
240
560
  }
241
561
  const payload = await readJsonResponse(response);
242
562
  const runId = readString(payload, "run_id") ?? readString(payload, "runId") ?? readString(payload, "id");
@@ -246,17 +566,8 @@ async function createHermesRun(input, options = {}) {
246
566
  return { run_id: runId, fallback: false };
247
567
  }
248
568
  async function streamHermesRunEvents(runId, options = {}) {
249
- const fallback = fallbackRuns.get(runId);
250
- if (fallback) {
251
- fallbackRuns.delete(runId);
252
- return new Response(createFallbackSseStream(fallback.input), {
253
- headers: {
254
- "content-type": "text/event-stream; charset=utf-8",
255
- "cache-control": "no-store"
256
- }
257
- });
258
- }
259
569
  const response = await callHermesApi(`/v1/runs/${encodeURIComponent(runId)}/events`, { method: "GET" }, options);
570
+ assertHermesRunsApiSupported(await readHermesVersion().catch(() => null), response.status);
260
571
  if (!response.ok || !response.body) {
261
572
  throw new LinkHttpError(502, "hermes_events_unavailable", "Hermes run event stream is unavailable");
262
573
  }
@@ -277,75 +588,68 @@ async function cancelHermesRun(runId, options = {}) {
277
588
  if (!response.ok && response.status !== 404) {
278
589
  throw new LinkHttpError(502, "hermes_cancel_failed", "Hermes run cancel failed");
279
590
  }
280
- fallbackRuns.delete(runId);
281
591
  }
282
- async function callHermesApi(path8, init, options) {
283
- const config = await readHermesApiServerConfig();
284
- if (!config.port || !config.key) {
285
- return new Response(null, { status: 503 });
286
- }
592
+ async function callHermesApi(path9, init, options) {
593
+ const availability = await ensureHermesApiServerAvailable({ fetchImpl: options.fetchImpl });
594
+ const config = availability.configResult.apiServer;
287
595
  const fetcher = options.fetchImpl ?? fetch;
596
+ const request = () => fetchHermesApi(fetcher, config, path9, init);
597
+ const response = await request();
598
+ if (response.status !== 401) {
599
+ return response;
600
+ }
601
+ await ensureHermesApiServerAvailable({ fetchImpl: options.fetchImpl, forceRestart: true });
602
+ return await request();
603
+ }
604
+ async function fetchHermesApi(fetcher, config, path9, init) {
288
605
  const headers = new Headers(init.headers);
289
606
  headers.set("accept", headers.get("accept") ?? "application/json");
290
- headers.set("x-api-key", config.key);
291
- headers.set("authorization", `Bearer ${config.key}`);
292
- return await fetcher(`http://127.0.0.1:${config.port}${path8}`, {
607
+ if (config.key) {
608
+ headers.set("x-api-key", config.key);
609
+ headers.set("authorization", `Bearer ${config.key}`);
610
+ }
611
+ return await fetcher(`http://127.0.0.1:${config.port}${path9}`, {
293
612
  ...init,
294
613
  headers
295
- }).catch(() => new Response(null, { status: 503 }));
614
+ }).catch(() => {
615
+ throw new LinkHttpError(503, "hermes_api_server_unavailable", "Hermes API Server is unavailable");
616
+ });
296
617
  }
297
618
  async function readJsonResponse(response) {
298
- const payload = await response.json().catch(() => null);
619
+ const raw = await response.text().catch(() => "");
620
+ const payload = parseJsonObject(raw);
299
621
  if (!response.ok || typeof payload !== "object" || payload === null) {
300
- throw new LinkHttpError(502, "hermes_response_invalid", "Hermes API Server returned an invalid response");
622
+ throw new LinkHttpError(
623
+ 502,
624
+ "hermes_response_invalid",
625
+ `Hermes API Server returned HTTP ${response.status}: ${readUpstreamMessage(payload, raw)}`
626
+ );
301
627
  }
302
628
  return payload;
303
629
  }
304
- function createFallbackSseStream(input) {
305
- const encoder = new TextEncoder();
306
- return new ReadableStream({
307
- start(controller) {
308
- const message = `Hermes API Server is not running yet. Link received: ${input}`;
309
- controller.enqueue(encoder.encode(`event: message.delta
310
- data: ${JSON.stringify({ type: "message.delta", delta: message })}
311
-
312
- `));
313
- controller.enqueue(encoder.encode(`event: run.completed
314
- data: ${JSON.stringify({ type: "run.completed" })}
315
-
316
- `));
317
- controller.close();
318
- }
319
- });
320
- }
321
- function readString(payload, key) {
322
- const value = payload[key];
323
- return typeof value === "string" && value.trim() ? value.trim() : null;
324
- }
325
-
326
- // src/hermes/cli.ts
327
- import { execFile } from "child_process";
328
- import { promisify } from "util";
329
- var execFileAsync = promisify(execFile);
330
- async function deleteHermesSession(sessionId) {
331
- if (!sessionId.trim()) {
332
- throw new LinkHttpError(400, "hermes_session_id_required", "Hermes session id is required");
630
+ function parseJsonObject(raw) {
631
+ if (!raw.trim()) {
632
+ return null;
333
633
  }
334
634
  try {
335
- await execFileAsync(hermesBin(), ["sessions", "delete", sessionId, "--yes"], {
336
- timeout: 1e4,
337
- windowsHide: true
338
- });
339
- } catch (error) {
340
- throw new LinkHttpError(
341
- 502,
342
- "hermes_session_delete_failed",
343
- error instanceof Error ? error.message : "Hermes session delete failed"
344
- );
635
+ const parsed = JSON.parse(raw);
636
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
637
+ } catch {
638
+ return null;
345
639
  }
346
640
  }
347
- function hermesBin() {
348
- return process.env.HERMES_BIN?.trim() || "hermes";
641
+ function readUpstreamMessage(payload, raw) {
642
+ const error = typeof payload?.error === "object" && payload.error !== null ? payload.error : null;
643
+ const message = readString(error ?? {}, "message") ?? readString(payload ?? {}, "message");
644
+ if (message) {
645
+ return message;
646
+ }
647
+ const body = raw.trim().replace(/\s+/gu, " ").slice(0, 500);
648
+ return body || "empty response body";
649
+ }
650
+ function readString(payload, key) {
651
+ const value = payload[key];
652
+ return typeof value === "string" && value.trim() ? value.trim() : null;
349
653
  }
350
654
 
351
655
  // src/conversations/conversation-service.ts
@@ -364,7 +668,7 @@ var ConversationService = class {
364
668
  logger;
365
669
  emitter = new EventEmitter();
366
670
  async listConversations() {
367
- await mkdir3(this.paths.conversationsDir, { recursive: true, mode: 448 });
671
+ await mkdir4(this.paths.conversationsDir, { recursive: true, mode: 448 });
368
672
  const entries = await readdir(this.paths.conversationsDir, { withFileTypes: true }).catch((error) => {
369
673
  if (isNodeError3(error, "ENOENT")) {
370
674
  return [];
@@ -386,9 +690,9 @@ var ConversationService = class {
386
690
  return summaries.sort((left, right) => Date.parse(right.updated_at) - Date.parse(left.updated_at));
387
691
  }
388
692
  async createConversation(input = {}) {
389
- await mkdir3(this.paths.conversationsDir, { recursive: true, mode: 448 });
693
+ await mkdir4(this.paths.conversationsDir, { recursive: true, mode: 448 });
390
694
  const now = (/* @__PURE__ */ new Date()).toISOString();
391
- const id = `conv_${randomUUID2().replaceAll("-", "")}`;
695
+ const id = `conv_${randomUUID().replaceAll("-", "")}`;
392
696
  const title = input.title?.trim() || "Untitled";
393
697
  const manifest = {
394
698
  id,
@@ -401,7 +705,7 @@ var ConversationService = class {
401
705
  updated_at: now,
402
706
  last_event_seq: 0
403
707
  };
404
- await mkdir3(this.conversationDir(id), { recursive: true, mode: 448 });
708
+ await mkdir4(this.conversationDir(id), { recursive: true, mode: 448 });
405
709
  await this.writeManifest(manifest);
406
710
  await this.writeSnapshot(id, emptySnapshot());
407
711
  const event = await this.appendEvent(manifest.id, {
@@ -462,7 +766,7 @@ var ConversationService = class {
462
766
  }
463
767
  const now = (/* @__PURE__ */ new Date()).toISOString();
464
768
  const userMessage = {
465
- id: `msg_${randomUUID2().replaceAll("-", "")}`,
769
+ id: `msg_${randomUUID().replaceAll("-", "")}`,
466
770
  schema_version: 1,
467
771
  conversation_id: manifest.id,
468
772
  role: "user",
@@ -481,7 +785,7 @@ var ConversationService = class {
481
785
  raw: { format: "hermes-link-user-message", payload: { content, attachments: input.attachments ?? [] } }
482
786
  };
483
787
  const assistantMessage = {
484
- id: `msg_${randomUUID2().replaceAll("-", "")}`,
788
+ id: `msg_${randomUUID().replaceAll("-", "")}`,
485
789
  schema_version: 1,
486
790
  conversation_id: manifest.id,
487
791
  role: "assistant",
@@ -493,7 +797,7 @@ var ConversationService = class {
493
797
  attachments: []
494
798
  };
495
799
  const run = {
496
- id: `run_${randomUUID2().replaceAll("-", "")}`,
800
+ id: `run_${randomUUID().replaceAll("-", "")}`,
497
801
  conversation_id: manifest.id,
498
802
  trigger_message_id: userMessage.id,
499
803
  assistant_message_id: assistantMessage.id,
@@ -563,9 +867,9 @@ var ConversationService = class {
563
867
  if (input.bytes.byteLength > MAX_UPLOADED_BLOB_BYTES) {
564
868
  throw new LinkHttpError(413, "blob_too_large", "Blob is too large");
565
869
  }
566
- const id = `blob_${randomUUID2().replaceAll("-", "")}`;
870
+ const id = `blob_${randomUUID().replaceAll("-", "")}`;
567
871
  const filePath = this.blobPath(id);
568
- await mkdir3(path4.dirname(filePath), { recursive: true, mode: 448 });
872
+ await mkdir4(path5.dirname(filePath), { recursive: true, mode: 448 });
569
873
  await writeFile2(filePath, input.bytes, { mode: 384 });
570
874
  const manifestPath = `${filePath}.json`;
571
875
  const blob = {
@@ -665,7 +969,7 @@ ${attachmentLines.join("\n")}`;
665
969
  }
666
970
  return this.writeBlob(conversationId, {
667
971
  bytes: await readFile3(sourcePath),
668
- filename: path4.basename(sourcePath),
972
+ filename: path5.basename(sourcePath),
669
973
  mime: source.mime ?? inferMimeType(sourcePath)
670
974
  });
671
975
  }
@@ -916,7 +1220,7 @@ ${attachmentLines.join("\n")}`;
916
1220
  conversation_id: conversationId,
917
1221
  created_at: now
918
1222
  };
919
- await mkdir3(this.conversationDir(conversationId), { recursive: true, mode: 448 });
1223
+ await mkdir4(this.conversationDir(conversationId), { recursive: true, mode: 448 });
920
1224
  await appendFile(this.eventsPath(conversationId), `${JSON.stringify(event)}
921
1225
  `, { mode: 384 });
922
1226
  await this.writeManifest({
@@ -956,20 +1260,20 @@ ${attachmentLines.join("\n")}`;
956
1260
  }
957
1261
  conversationDir(conversationId) {
958
1262
  assertValidConversationId(conversationId);
959
- return path4.join(this.paths.conversationsDir, conversationId);
1263
+ return path5.join(this.paths.conversationsDir, conversationId);
960
1264
  }
961
1265
  manifestPath(conversationId) {
962
- return path4.join(this.conversationDir(conversationId), "manifest.json");
1266
+ return path5.join(this.conversationDir(conversationId), "manifest.json");
963
1267
  }
964
1268
  snapshotPath(conversationId) {
965
- return path4.join(this.conversationDir(conversationId), "snapshot.json");
1269
+ return path5.join(this.conversationDir(conversationId), "snapshot.json");
966
1270
  }
967
1271
  eventsPath(conversationId) {
968
- return path4.join(this.conversationDir(conversationId), "events.ndjson");
1272
+ return path5.join(this.conversationDir(conversationId), "events.ndjson");
969
1273
  }
970
1274
  blobPath(blobId) {
971
1275
  assertValidBlobId(blobId);
972
- return path4.join(this.paths.blobsDir, `${blobId}.bin`);
1276
+ return path5.join(this.paths.blobsDir, `${blobId}.bin`);
973
1277
  }
974
1278
  liveEventName(conversationId) {
975
1279
  return `conversation:${conversationId}`;
@@ -996,7 +1300,7 @@ ${attachmentLines.join("\n")}`;
996
1300
  }
997
1301
  }
998
1302
  async listConversationBlobIds(conversationId) {
999
- await mkdir3(this.paths.blobsDir, { recursive: true, mode: 448 });
1303
+ await mkdir4(this.paths.blobsDir, { recursive: true, mode: 448 });
1000
1304
  const entries = await readdir(this.paths.blobsDir, { withFileTypes: true }).catch((error) => {
1001
1305
  if (isNodeError3(error, "ENOENT")) {
1002
1306
  return [];
@@ -1013,7 +1317,7 @@ ${attachmentLines.join("\n")}`;
1013
1317
  continue;
1014
1318
  }
1015
1319
  const manifest = await readJsonFile(
1016
- path4.join(this.paths.blobsDir, entry.name)
1320
+ path5.join(this.paths.blobsDir, entry.name)
1017
1321
  ).catch(() => null);
1018
1322
  if (manifest?.conversation_ids?.includes(conversationId)) {
1019
1323
  blobIds.push(blobId);
@@ -1199,7 +1503,7 @@ function isExplicitMediaPathKey(key) {
1199
1503
  ].includes(key);
1200
1504
  }
1201
1505
  function inferMimeType(filePath) {
1202
- const extension = path4.extname(filePath).toLowerCase();
1506
+ const extension = path5.extname(filePath).toLowerCase();
1203
1507
  return {
1204
1508
  ".png": "image/png",
1205
1509
  ".jpg": "image/jpeg",
@@ -1261,15 +1565,15 @@ function mediaSourceKey(sourcePath) {
1261
1565
  return createHash("sha256").update(resolveMediaSourcePath(sourcePath)).digest("hex").slice(0, 32);
1262
1566
  }
1263
1567
  function sanitizeFilename(value, fallback) {
1264
- const base = path4.basename((value ?? "").replace(/[\r\n\t]/gu, " ").trim());
1568
+ const base = path5.basename((value ?? "").replace(/[\r\n\t]/gu, " ").trim());
1265
1569
  const safe = base.replace(/[/:\\]/gu, "_").slice(0, 200).trim();
1266
1570
  return safe || fallback;
1267
1571
  }
1268
1572
  function resolveMediaSourcePath(sourcePath) {
1269
1573
  const trimmed = sourcePath.trim();
1270
- const expanded = trimmed.startsWith("~/") ? path4.join(process.env.HOME ?? "", trimmed.slice(2)) : trimmed;
1271
- const resolved = path4.resolve(expanded);
1272
- if (!path4.isAbsolute(expanded)) {
1574
+ const expanded = trimmed.startsWith("~/") ? path5.join(process.env.HOME ?? "", trimmed.slice(2)) : trimmed;
1575
+ const resolved = path5.resolve(expanded);
1576
+ if (!path5.isAbsolute(expanded)) {
1273
1577
  throw new LinkHttpError(400, "media_source_path_not_absolute", "Hermes output media source must be an absolute path");
1274
1578
  }
1275
1579
  return resolved;
@@ -1296,16 +1600,16 @@ function isNodeError3(error, code) {
1296
1600
  }
1297
1601
 
1298
1602
  // src/hermes/profiles.ts
1299
- import { mkdir as mkdir4, readdir as readdir2, rename as rename2, rm as rm3, stat as stat2 } from "fs/promises";
1603
+ import { mkdir as mkdir5, readdir as readdir2, rename as rename2, rm as rm3, stat as stat2 } from "fs/promises";
1300
1604
  import os3 from "os";
1301
- import path5 from "path";
1605
+ import path6 from "path";
1302
1606
  var DEFAULT_PROFILE = "default";
1303
1607
  var PROFILE_NAME_PATTERN = /^[a-zA-Z0-9._-]{1,64}$/;
1304
1608
  async function listHermesProfiles(paths = resolveRuntimePaths()) {
1305
1609
  const activeProfile = await getActiveProfile(paths);
1306
1610
  const profiles = /* @__PURE__ */ new Map();
1307
1611
  profiles.set(DEFAULT_PROFILE, profileInfo(DEFAULT_PROFILE, activeProfile));
1308
- const profilesDir = path5.join(os3.homedir(), ".hermes", "profiles");
1612
+ const profilesDir = path6.join(os3.homedir(), ".hermes", "profiles");
1309
1613
  const entries = await readdir2(profilesDir, { withFileTypes: true }).catch((error) => {
1310
1614
  if (isNodeError4(error, "ENOENT")) {
1311
1615
  return [];
@@ -1352,14 +1656,14 @@ async function createHermesProfile(name) {
1352
1656
  if (await pathExists(profile.path)) {
1353
1657
  throw new Error("profile already exists");
1354
1658
  }
1355
- await mkdir4(profile.path, { recursive: true, mode: 448 });
1659
+ await mkdir5(profile.path, { recursive: true, mode: 448 });
1356
1660
  await ensureHermesApiServerKey(name, profile.configPath);
1357
1661
  return profile;
1358
1662
  }
1359
1663
  async function useHermesProfile(name, paths = resolveRuntimePaths()) {
1360
1664
  assertProfileName(name);
1361
1665
  const profile = profileInfo(name, name);
1362
- await mkdir4(profile.path, { recursive: true, mode: 448 });
1666
+ await mkdir5(profile.path, { recursive: true, mode: 448 });
1363
1667
  const current = await readJsonFile(paths.stateFile) ?? {};
1364
1668
  await writeJsonFile(paths.stateFile, { ...current, activeProfile: name });
1365
1669
  return profile;
@@ -1412,8 +1716,8 @@ function isNodeError4(error, code) {
1412
1716
  }
1413
1717
 
1414
1718
  // src/identity/identity.ts
1415
- import { generateKeyPairSync, randomUUID as randomUUID3, sign } from "crypto";
1416
- import { mkdir as mkdir5, chmod } from "fs/promises";
1719
+ import { generateKeyPairSync, randomUUID as randomUUID2, sign } from "crypto";
1720
+ import { mkdir as mkdir6, chmod } from "fs/promises";
1417
1721
  import { z } from "zod";
1418
1722
  var linkIdentitySchema = z.object({
1419
1723
  install_id: z.string().min(1),
@@ -1435,12 +1739,12 @@ async function ensureIdentity(paths = resolveRuntimePaths()) {
1435
1739
  if (existing) {
1436
1740
  return existing;
1437
1741
  }
1438
- await mkdir5(paths.homeDir, { recursive: true, mode: 448 });
1742
+ await mkdir6(paths.homeDir, { recursive: true, mode: 448 });
1439
1743
  await chmod(paths.homeDir, 448).catch(() => void 0);
1440
1744
  const { publicKey, privateKey } = generateKeyPairSync("ed25519");
1441
1745
  const now = (/* @__PURE__ */ new Date()).toISOString();
1442
1746
  const identity = {
1443
- install_id: `install_${randomUUID3().replaceAll("-", "")}`,
1747
+ install_id: `install_${randomUUID2().replaceAll("-", "")}`,
1444
1748
  link_id: null,
1445
1749
  public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
1446
1750
  private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
@@ -1474,11 +1778,11 @@ function getIdentityStatus(identity) {
1474
1778
  }
1475
1779
 
1476
1780
  // src/pairing/pairing.ts
1477
- import path6 from "path";
1781
+ import path7 from "path";
1478
1782
  import { rm as rm4 } from "fs/promises";
1479
1783
 
1480
1784
  // src/security/devices.ts
1481
- import { randomBytes as randomBytes2, randomUUID as randomUUID4, timingSafeEqual, createHash as createHash2 } from "crypto";
1785
+ import { randomBytes as randomBytes2, randomUUID as randomUUID3, timingSafeEqual, createHash as createHash2 } from "crypto";
1482
1786
  var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
1483
1787
  var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
1484
1788
  async function createDeviceSession(input, paths = resolveRuntimePaths()) {
@@ -1487,7 +1791,7 @@ async function createDeviceSession(input, paths = resolveRuntimePaths()) {
1487
1791
  const accessToken = randomToken("hpat_");
1488
1792
  const refreshToken = randomToken("hprt_");
1489
1793
  const device = {
1490
- id: `dev_${randomUUID4().replaceAll("-", "")}`,
1794
+ id: `dev_${randomUUID3().replaceAll("-", "")}`,
1491
1795
  label: input.label,
1492
1796
  platform: input.platform,
1493
1797
  scope: "admin",
@@ -1942,8 +2246,8 @@ async function loadRequiredIdentity(paths) {
1942
2246
  }
1943
2247
  return identity;
1944
2248
  }
1945
- async function postServerJson(serverBaseUrl, path8, body) {
1946
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path8}`, {
2249
+ async function postServerJson(serverBaseUrl, path9, body) {
2250
+ const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path9}`, {
1947
2251
  method: "POST",
1948
2252
  headers: {
1949
2253
  accept: "application/json",
@@ -1953,8 +2257,8 @@ async function postServerJson(serverBaseUrl, path8, body) {
1953
2257
  });
1954
2258
  return readJsonResponse2(response);
1955
2259
  }
1956
- async function patchServerJson(serverBaseUrl, path8, token, body) {
1957
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path8}`, {
2260
+ async function patchServerJson(serverBaseUrl, path9, token, body) {
2261
+ const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path9}`, {
1958
2262
  method: "PATCH",
1959
2263
  headers: {
1960
2264
  accept: "application/json",
@@ -1988,7 +2292,7 @@ function defaultDisplayName() {
1988
2292
  return `Hermes Link ${process.platform}`;
1989
2293
  }
1990
2294
  function pairingClaimPath(sessionId, paths) {
1991
- return path6.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
2295
+ return path7.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
1992
2296
  }
1993
2297
  function qrPreferredUrls(routes) {
1994
2298
  return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
@@ -2064,8 +2368,8 @@ function base64UrlToBase64(value) {
2064
2368
  }
2065
2369
 
2066
2370
  // src/runtime/logger.ts
2067
- import { appendFile as appendFile2, mkdir as mkdir6, open as open2, readFile as readFile4, rename as rename3, rm as rm5, stat as stat3 } from "fs/promises";
2068
- import path7 from "path";
2371
+ import { appendFile as appendFile2, mkdir as mkdir7, open as open3, readFile as readFile4, rename as rename3, rm as rm5, stat as stat3 } from "fs/promises";
2372
+ import path8 from "path";
2069
2373
  var DEFAULT_LOG_FILE = "hermeslink.log";
2070
2374
  var DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
2071
2375
  var DEFAULT_MAX_FILES = 5;
@@ -2113,7 +2417,7 @@ var FileLogger = class {
2113
2417
  return this.queue;
2114
2418
  }
2115
2419
  async appendEntry(entry) {
2116
- await mkdir6(this.paths.logsDir, { recursive: true, mode: 448 });
2420
+ await mkdir7(this.paths.logsDir, { recursive: true, mode: 448 });
2117
2421
  const line = `${JSON.stringify(entry)}
2118
2422
  `;
2119
2423
  await this.rotateIfNeeded(Buffer.byteLength(line, "utf8"));
@@ -2139,7 +2443,7 @@ function createFileLogger(options = {}) {
2139
2443
  return new FileLogger(options);
2140
2444
  }
2141
2445
  function getLinkLogFile(paths = resolveRuntimePaths(), fileName = DEFAULT_LOG_FILE) {
2142
- return path7.join(paths.logsDir, fileName);
2446
+ return path8.join(paths.logsDir, fileName);
2143
2447
  }
2144
2448
  async function readRecentLogEntries(options = {}) {
2145
2449
  const paths = options.paths ?? resolveRuntimePaths();
@@ -2241,7 +2545,7 @@ async function readTail(filePath, maxBytes) {
2241
2545
  if (info.size <= maxBytes) {
2242
2546
  return await readFile4(filePath, "utf8").catch(() => null);
2243
2547
  }
2244
- const handle = await open2(filePath, "r").catch(() => null);
2548
+ const handle = await open3(filePath, "r").catch(() => null);
2245
2549
  if (!handle) {
2246
2550
  return null;
2247
2551
  }
@@ -2780,7 +3084,8 @@ export {
2780
3084
  LINK_COMMAND,
2781
3085
  resolveRuntimePaths,
2782
3086
  loadConfig,
2783
- ensureHermesApiServerKey,
3087
+ ensureHermesApiServerConfig,
3088
+ ensureHermesApiServerAvailable,
2784
3089
  loadIdentity,
2785
3090
  ensureIdentity,
2786
3091
  getIdentityStatus,
@@ -2791,4 +3096,4 @@ export {
2791
3096
  getLinkLogFile,
2792
3097
  createApp
2793
3098
  };
2794
- //# sourceMappingURL=chunk-VCQJ5DSN.js.map
3099
+ //# sourceMappingURL=chunk-L2NM2XMX.js.map