@hermespilot/link 0.1.6 → 0.1.7

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.7";
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");
@@ -131,9 +151,12 @@ async function readHermesApiServerConfig(profileName = "default", configPath = r
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 readApiServerConfig(apiServer, true);
135
155
  }
136
156
  async function ensureHermesApiServerKey(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
157
+ return ensureHermesApiServerConfig(profileName, configPath);
158
+ }
159
+ async function ensureHermesApiServerConfig(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
137
160
  const existingRaw = await readFile2(configPath, "utf8").catch((error) => {
138
161
  if (isNodeError2(error, "ENOENT")) {
139
162
  return null;
@@ -146,7 +169,28 @@ async function ensureHermesApiServerKey(profileName = "default", configPath = re
146
169
  const apiServer = ensureRecord(platforms, "api_server");
147
170
  const extra = ensureRecord(apiServer, "extra");
148
171
  const beforeKey = typeof extra.key === "string" && extra.key.length > 0 ? extra.key : null;
172
+ const beforeEnabled = apiServer.enabled === true;
173
+ const beforeHost = typeof extra.host === "string" && extra.host.trim() ? extra.host : null;
174
+ const beforePort = typeof extra.port === "number" && Number.isFinite(extra.port) ? extra.port : null;
149
175
  let changed = false;
176
+ let enabledAdded = false;
177
+ let hostAdded = false;
178
+ let portAdded = false;
179
+ if (!beforeEnabled) {
180
+ apiServer.enabled = true;
181
+ enabledAdded = true;
182
+ changed = true;
183
+ }
184
+ if (!beforeHost) {
185
+ extra.host = DEFAULT_HERMES_API_SERVER_HOST;
186
+ hostAdded = true;
187
+ changed = true;
188
+ }
189
+ if (!beforePort) {
190
+ extra.port = DEFAULT_HERMES_API_SERVER_PORT;
191
+ portAdded = true;
192
+ changed = true;
193
+ }
150
194
  if (!beforeKey) {
151
195
  extra.key = randomBytes(32).toString("base64url");
152
196
  changed = true;
@@ -154,9 +198,12 @@ async function ensureHermesApiServerKey(profileName = "default", configPath = re
154
198
  if (!changed) {
155
199
  return {
156
200
  configPath,
157
- apiServer: readApiServerConfig(extra),
201
+ apiServer: readApiServerConfig(apiServer, true),
158
202
  changed: false,
159
203
  keyAdded: false,
204
+ enabledAdded: false,
205
+ hostAdded: false,
206
+ portAdded: false,
160
207
  backupPath: null,
161
208
  notice: null
162
209
  };
@@ -170,20 +217,44 @@ async function ensureHermesApiServerKey(profileName = "default", configPath = re
170
217
  await writeFile(configPath, document.toString(), { mode: 384 });
171
218
  return {
172
219
  configPath,
173
- apiServer: readApiServerConfig(extra),
220
+ apiServer: readApiServerConfig(apiServer, true),
174
221
  changed: true,
175
- keyAdded: true,
222
+ keyAdded: !beforeKey,
223
+ enabledAdded,
224
+ hostAdded,
225
+ portAdded,
176
226
  backupPath,
177
- notice: "\u5DF2\u4E3A Hermes API Server \u81EA\u52A8\u8865\u5145 key\uFF1B\u672A\u8986\u76D6\u5DF2\u6709 port/host/key\u3002"
227
+ notice: buildNotice({ keyAdded: !beforeKey, enabledAdded, hostAdded, portAdded })
178
228
  };
179
229
  }
180
- function readApiServerConfig(extra) {
230
+ function readApiServerConfig(apiServerOrExtra, withDefaults = false) {
231
+ const apiServer = "extra" in apiServerOrExtra ? apiServerOrExtra : {};
232
+ const extra = toRecord("extra" in apiServerOrExtra ? apiServerOrExtra.extra : apiServerOrExtra);
233
+ const port = typeof extra.port === "number" ? extra.port : void 0;
234
+ const host = typeof extra.host === "string" ? extra.host : void 0;
181
235
  return {
182
- host: typeof extra.host === "string" ? extra.host : void 0,
183
- port: typeof extra.port === "number" ? extra.port : void 0,
236
+ enabled: apiServer.enabled === true,
237
+ host: withDefaults ? host ?? DEFAULT_HERMES_API_SERVER_HOST : host,
238
+ port: withDefaults ? port ?? DEFAULT_HERMES_API_SERVER_PORT : port,
184
239
  key: typeof extra.key === "string" ? extra.key : void 0
185
240
  };
186
241
  }
242
+ function buildNotice(flags) {
243
+ const fields = [];
244
+ if (flags.enabledAdded) {
245
+ fields.push("enabled");
246
+ }
247
+ if (flags.hostAdded) {
248
+ fields.push("host=127.0.0.1");
249
+ }
250
+ if (flags.portAdded) {
251
+ fields.push("port=8642");
252
+ }
253
+ if (flags.keyAdded) {
254
+ fields.push("key");
255
+ }
256
+ return `\u5DF2\u4E3A Hermes API Server \u81EA\u52A8\u8865\u5145 ${fields.join("\u3001")}\uFF1B\u672A\u8986\u76D6\u5DF2\u6709 port/host/key\u3002`;
257
+ }
187
258
  function toRecord(value) {
188
259
  return typeof value === "object" && value !== null ? value : {};
189
260
  }
@@ -200,22 +271,199 @@ function isNodeError2(error, code) {
200
271
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
201
272
  }
202
273
 
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;
274
+ // src/hermes/cli.ts
275
+ import { execFile } from "child_process";
276
+ import { promisify } from "util";
277
+ var execFileAsync = promisify(execFile);
278
+ async function deleteHermesSession(sessionId) {
279
+ if (!sessionId.trim()) {
280
+ throw new LinkHttpError(400, "hermes_session_id_required", "Hermes session id is required");
281
+ }
282
+ try {
283
+ await execFileAsync(resolveHermesBin(), ["sessions", "delete", sessionId, "--yes"], {
284
+ timeout: 1e4,
285
+ windowsHide: true
286
+ });
287
+ } catch (error) {
288
+ throw new LinkHttpError(
289
+ 502,
290
+ "hermes_session_delete_failed",
291
+ error instanceof Error ? error.message : "Hermes session delete failed"
292
+ );
209
293
  }
210
- status;
211
- code;
212
- };
213
- function isLinkHttpError(error) {
214
- return error instanceof LinkHttpError;
294
+ }
295
+ function resolveHermesBin() {
296
+ return process.env.HERMES_BIN?.trim() || "hermes";
297
+ }
298
+
299
+ // src/hermes/gateway.ts
300
+ var execFileAsync2 = promisify2(execFile2);
301
+ var DEFAULT_START_TIMEOUT_MS = 12e3;
302
+ var HEALTH_TIMEOUT_MS = 1500;
303
+ var MIN_API_SERVER_VERSION = "0.4.0";
304
+ var gatewayStartInFlight = null;
305
+ async function ensureHermesApiServerAvailable(options = {}) {
306
+ const configResult = await ensureHermesApiServerConfig();
307
+ const fetcher = options.fetchImpl ?? fetch;
308
+ if (await isHermesApiHealthy(configResult.apiServer, fetcher)) {
309
+ return { available: true, configResult, started: false };
310
+ }
311
+ if (!shouldAutoStart(options.autoStart)) {
312
+ throw new LinkHttpError(503, "hermes_api_server_unavailable", unavailableMessage());
313
+ }
314
+ const start = await startHermesGatewayOnce(options.paths ?? resolveRuntimePaths());
315
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_START_TIMEOUT_MS);
316
+ while (Date.now() < deadline) {
317
+ if (await isHermesApiHealthy(configResult.apiServer, fetcher)) {
318
+ return { available: true, configResult, started: true, start };
319
+ }
320
+ await delay(400);
321
+ }
322
+ throw new LinkHttpError(
323
+ 503,
324
+ "hermes_api_server_unavailable",
325
+ `${unavailableMessage()} Link tried to start it with \`${resolveHermesBin()} gateway run --replace\`, but it did not become ready. Gateway log: ${start.logPath}`
326
+ );
327
+ }
328
+ async function readHermesVersion() {
329
+ const { stdout } = await execHermesVersion();
330
+ const raw = stdout.trim();
331
+ const version = parseHermesVersion(raw);
332
+ return {
333
+ raw,
334
+ version,
335
+ supportsApiServer: version ? compareSemver(version, MIN_API_SERVER_VERSION) >= 0 : null
336
+ };
337
+ }
338
+ async function execHermesVersion() {
339
+ try {
340
+ return await execFileAsync2(resolveHermesBin(), ["version"], {
341
+ timeout: 5e3,
342
+ windowsHide: true
343
+ });
344
+ } catch {
345
+ return await execFileAsync2(resolveHermesBin(), ["--version"], {
346
+ timeout: 5e3,
347
+ windowsHide: true
348
+ });
349
+ }
350
+ }
351
+ function assertHermesRunsApiSupported(version, status) {
352
+ if (status !== 404) {
353
+ return;
354
+ }
355
+ const versionText = version?.version ? `\u5F53\u524D\u68C0\u6D4B\u5230 Hermes Agent ${version.version}\u3002` : "";
356
+ throw new LinkHttpError(
357
+ 502,
358
+ "hermes_runs_api_unsupported",
359
+ `${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`
360
+ );
361
+ }
362
+ async function startHermesGatewayOnce(paths) {
363
+ if (!gatewayStartInFlight) {
364
+ gatewayStartInFlight = startHermesGateway(paths).finally(() => {
365
+ gatewayStartInFlight = null;
366
+ });
367
+ }
368
+ return await gatewayStartInFlight;
369
+ }
370
+ async function startHermesGateway(paths) {
371
+ const version = await readHermesVersion().catch((error) => {
372
+ throw new LinkHttpError(
373
+ 503,
374
+ "hermes_agent_not_available",
375
+ `\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)}`
376
+ );
377
+ });
378
+ if (version.supportsApiServer === false) {
379
+ throw new LinkHttpError(
380
+ 503,
381
+ "hermes_agent_too_old",
382
+ `\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`
383
+ );
384
+ }
385
+ await mkdir3(paths.logsDir, { recursive: true, mode: 448 });
386
+ const logPath = path4.join(paths.logsDir, "hermes-gateway.log");
387
+ const logFile = await open2(logPath, "a", 384);
388
+ try {
389
+ const child = spawn(resolveHermesBin(), ["gateway", "run", "--replace"], {
390
+ detached: true,
391
+ env: process.env,
392
+ stdio: ["ignore", logFile.fd, logFile.fd],
393
+ windowsHide: true
394
+ });
395
+ await new Promise((resolve, reject) => {
396
+ let settled = false;
397
+ const finish = (callback) => {
398
+ if (settled) {
399
+ return;
400
+ }
401
+ settled = true;
402
+ callback();
403
+ };
404
+ child.once("spawn", () => finish(resolve));
405
+ child.once("error", (error) => finish(() => reject(error)));
406
+ });
407
+ child.unref();
408
+ return { pid: child.pid ?? null, logPath, version };
409
+ } finally {
410
+ await logFile.close().catch(() => void 0);
411
+ }
412
+ }
413
+ async function isHermesApiHealthy(config, fetcher) {
414
+ const response = await fetchWithTimeout(`http://127.0.0.1:${config.port}/health`, {
415
+ method: "GET",
416
+ headers: authHeaders(config)
417
+ }, fetcher);
418
+ return Boolean(response?.ok);
419
+ }
420
+ function fetchWithTimeout(input, init, fetcher) {
421
+ const controller = new AbortController();
422
+ const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
423
+ return fetcher(input, { ...init, signal: controller.signal }).catch(() => null).finally(() => clearTimeout(timer));
424
+ }
425
+ function authHeaders(config) {
426
+ const headers = new Headers();
427
+ headers.set("accept", "application/json");
428
+ if (config.key) {
429
+ headers.set("x-api-key", config.key);
430
+ headers.set("authorization", `Bearer ${config.key}`);
431
+ }
432
+ return headers;
433
+ }
434
+ function shouldAutoStart(value) {
435
+ if (value !== void 0) {
436
+ return value;
437
+ }
438
+ const raw = process.env.HERMESLINK_GATEWAY_AUTOSTART?.trim().toLowerCase();
439
+ return raw !== "0" && raw !== "false" && raw !== "off";
440
+ }
441
+ function unavailableMessage() {
442
+ 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";
443
+ }
444
+ function parseHermesVersion(value) {
445
+ const match = /\bv?(\d+\.\d+\.\d+)\b/u.exec(value);
446
+ return match?.[1] ?? null;
447
+ }
448
+ function compareSemver(left, right) {
449
+ const leftParts = left.split(".").map((part) => Number.parseInt(part, 10));
450
+ const rightParts = right.split(".").map((part) => Number.parseInt(part, 10));
451
+ for (let index = 0; index < 3; index += 1) {
452
+ const diff = (leftParts[index] || 0) - (rightParts[index] || 0);
453
+ if (diff !== 0) {
454
+ return diff;
455
+ }
456
+ }
457
+ return 0;
458
+ }
459
+ function formatErrorSuffix(error) {
460
+ if (!(error instanceof Error) || !error.message) {
461
+ return "";
462
+ }
463
+ return ` \u539F\u59CB\u9519\u8BEF\uFF1A${error.message}`;
215
464
  }
216
465
 
217
466
  // src/hermes/api-server.ts
218
- var fallbackRuns = /* @__PURE__ */ new Map();
219
467
  async function listHermesModels(options = {}) {
220
468
  const response = await callHermesApi("/v1/models", { method: "GET" }, options);
221
469
  if (response.status === 404) {
@@ -234,9 +482,8 @@ async function createHermesRun(input, options = {}) {
234
482
  options
235
483
  );
236
484
  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 };
485
+ assertHermesRunsApiSupported(await readHermesVersion().catch(() => null), response.status);
486
+ throw new LinkHttpError(503, "hermes_api_server_unavailable", "Hermes API Server is unavailable");
240
487
  }
241
488
  const payload = await readJsonResponse(response);
242
489
  const runId = readString(payload, "run_id") ?? readString(payload, "runId") ?? readString(payload, "id");
@@ -246,17 +493,8 @@ async function createHermesRun(input, options = {}) {
246
493
  return { run_id: runId, fallback: false };
247
494
  }
248
495
  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
496
  const response = await callHermesApi(`/v1/runs/${encodeURIComponent(runId)}/events`, { method: "GET" }, options);
497
+ assertHermesRunsApiSupported(await readHermesVersion().catch(() => null), response.status);
260
498
  if (!response.ok || !response.body) {
261
499
  throw new LinkHttpError(502, "hermes_events_unavailable", "Hermes run event stream is unavailable");
262
500
  }
@@ -277,22 +515,23 @@ async function cancelHermesRun(runId, options = {}) {
277
515
  if (!response.ok && response.status !== 404) {
278
516
  throw new LinkHttpError(502, "hermes_cancel_failed", "Hermes run cancel failed");
279
517
  }
280
- fallbackRuns.delete(runId);
281
518
  }
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
- }
519
+ async function callHermesApi(path9, init, options) {
520
+ const availability = await ensureHermesApiServerAvailable({ fetchImpl: options.fetchImpl });
521
+ const config = availability.configResult.apiServer;
287
522
  const fetcher = options.fetchImpl ?? fetch;
288
523
  const headers = new Headers(init.headers);
289
524
  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}`, {
525
+ if (config.key) {
526
+ headers.set("x-api-key", config.key);
527
+ headers.set("authorization", `Bearer ${config.key}`);
528
+ }
529
+ return await fetcher(`http://127.0.0.1:${config.port}${path9}`, {
293
530
  ...init,
294
531
  headers
295
- }).catch(() => new Response(null, { status: 503 }));
532
+ }).catch(() => {
533
+ throw new LinkHttpError(503, "hermes_api_server_unavailable", "Hermes API Server is unavailable");
534
+ });
296
535
  }
297
536
  async function readJsonResponse(response) {
298
537
  const payload = await response.json().catch(() => null);
@@ -301,53 +540,11 @@ async function readJsonResponse(response) {
301
540
  }
302
541
  return payload;
303
542
  }
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
543
  function readString(payload, key) {
322
544
  const value = payload[key];
323
545
  return typeof value === "string" && value.trim() ? value.trim() : null;
324
546
  }
325
547
 
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");
333
- }
334
- 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
- );
345
- }
346
- }
347
- function hermesBin() {
348
- return process.env.HERMES_BIN?.trim() || "hermes";
349
- }
350
-
351
548
  // src/conversations/conversation-service.ts
352
549
  var MAX_IMPORTED_BLOB_BYTES = 100 * 1024 * 1024;
353
550
  var MAX_UPLOADED_BLOB_BYTES = 50 * 1024 * 1024;
@@ -364,7 +561,7 @@ var ConversationService = class {
364
561
  logger;
365
562
  emitter = new EventEmitter();
366
563
  async listConversations() {
367
- await mkdir3(this.paths.conversationsDir, { recursive: true, mode: 448 });
564
+ await mkdir4(this.paths.conversationsDir, { recursive: true, mode: 448 });
368
565
  const entries = await readdir(this.paths.conversationsDir, { withFileTypes: true }).catch((error) => {
369
566
  if (isNodeError3(error, "ENOENT")) {
370
567
  return [];
@@ -386,9 +583,9 @@ var ConversationService = class {
386
583
  return summaries.sort((left, right) => Date.parse(right.updated_at) - Date.parse(left.updated_at));
387
584
  }
388
585
  async createConversation(input = {}) {
389
- await mkdir3(this.paths.conversationsDir, { recursive: true, mode: 448 });
586
+ await mkdir4(this.paths.conversationsDir, { recursive: true, mode: 448 });
390
587
  const now = (/* @__PURE__ */ new Date()).toISOString();
391
- const id = `conv_${randomUUID2().replaceAll("-", "")}`;
588
+ const id = `conv_${randomUUID().replaceAll("-", "")}`;
392
589
  const title = input.title?.trim() || "Untitled";
393
590
  const manifest = {
394
591
  id,
@@ -401,7 +598,7 @@ var ConversationService = class {
401
598
  updated_at: now,
402
599
  last_event_seq: 0
403
600
  };
404
- await mkdir3(this.conversationDir(id), { recursive: true, mode: 448 });
601
+ await mkdir4(this.conversationDir(id), { recursive: true, mode: 448 });
405
602
  await this.writeManifest(manifest);
406
603
  await this.writeSnapshot(id, emptySnapshot());
407
604
  const event = await this.appendEvent(manifest.id, {
@@ -462,7 +659,7 @@ var ConversationService = class {
462
659
  }
463
660
  const now = (/* @__PURE__ */ new Date()).toISOString();
464
661
  const userMessage = {
465
- id: `msg_${randomUUID2().replaceAll("-", "")}`,
662
+ id: `msg_${randomUUID().replaceAll("-", "")}`,
466
663
  schema_version: 1,
467
664
  conversation_id: manifest.id,
468
665
  role: "user",
@@ -481,7 +678,7 @@ var ConversationService = class {
481
678
  raw: { format: "hermes-link-user-message", payload: { content, attachments: input.attachments ?? [] } }
482
679
  };
483
680
  const assistantMessage = {
484
- id: `msg_${randomUUID2().replaceAll("-", "")}`,
681
+ id: `msg_${randomUUID().replaceAll("-", "")}`,
485
682
  schema_version: 1,
486
683
  conversation_id: manifest.id,
487
684
  role: "assistant",
@@ -493,7 +690,7 @@ var ConversationService = class {
493
690
  attachments: []
494
691
  };
495
692
  const run = {
496
- id: `run_${randomUUID2().replaceAll("-", "")}`,
693
+ id: `run_${randomUUID().replaceAll("-", "")}`,
497
694
  conversation_id: manifest.id,
498
695
  trigger_message_id: userMessage.id,
499
696
  assistant_message_id: assistantMessage.id,
@@ -563,9 +760,9 @@ var ConversationService = class {
563
760
  if (input.bytes.byteLength > MAX_UPLOADED_BLOB_BYTES) {
564
761
  throw new LinkHttpError(413, "blob_too_large", "Blob is too large");
565
762
  }
566
- const id = `blob_${randomUUID2().replaceAll("-", "")}`;
763
+ const id = `blob_${randomUUID().replaceAll("-", "")}`;
567
764
  const filePath = this.blobPath(id);
568
- await mkdir3(path4.dirname(filePath), { recursive: true, mode: 448 });
765
+ await mkdir4(path5.dirname(filePath), { recursive: true, mode: 448 });
569
766
  await writeFile2(filePath, input.bytes, { mode: 384 });
570
767
  const manifestPath = `${filePath}.json`;
571
768
  const blob = {
@@ -665,7 +862,7 @@ ${attachmentLines.join("\n")}`;
665
862
  }
666
863
  return this.writeBlob(conversationId, {
667
864
  bytes: await readFile3(sourcePath),
668
- filename: path4.basename(sourcePath),
865
+ filename: path5.basename(sourcePath),
669
866
  mime: source.mime ?? inferMimeType(sourcePath)
670
867
  });
671
868
  }
@@ -916,7 +1113,7 @@ ${attachmentLines.join("\n")}`;
916
1113
  conversation_id: conversationId,
917
1114
  created_at: now
918
1115
  };
919
- await mkdir3(this.conversationDir(conversationId), { recursive: true, mode: 448 });
1116
+ await mkdir4(this.conversationDir(conversationId), { recursive: true, mode: 448 });
920
1117
  await appendFile(this.eventsPath(conversationId), `${JSON.stringify(event)}
921
1118
  `, { mode: 384 });
922
1119
  await this.writeManifest({
@@ -956,20 +1153,20 @@ ${attachmentLines.join("\n")}`;
956
1153
  }
957
1154
  conversationDir(conversationId) {
958
1155
  assertValidConversationId(conversationId);
959
- return path4.join(this.paths.conversationsDir, conversationId);
1156
+ return path5.join(this.paths.conversationsDir, conversationId);
960
1157
  }
961
1158
  manifestPath(conversationId) {
962
- return path4.join(this.conversationDir(conversationId), "manifest.json");
1159
+ return path5.join(this.conversationDir(conversationId), "manifest.json");
963
1160
  }
964
1161
  snapshotPath(conversationId) {
965
- return path4.join(this.conversationDir(conversationId), "snapshot.json");
1162
+ return path5.join(this.conversationDir(conversationId), "snapshot.json");
966
1163
  }
967
1164
  eventsPath(conversationId) {
968
- return path4.join(this.conversationDir(conversationId), "events.ndjson");
1165
+ return path5.join(this.conversationDir(conversationId), "events.ndjson");
969
1166
  }
970
1167
  blobPath(blobId) {
971
1168
  assertValidBlobId(blobId);
972
- return path4.join(this.paths.blobsDir, `${blobId}.bin`);
1169
+ return path5.join(this.paths.blobsDir, `${blobId}.bin`);
973
1170
  }
974
1171
  liveEventName(conversationId) {
975
1172
  return `conversation:${conversationId}`;
@@ -996,7 +1193,7 @@ ${attachmentLines.join("\n")}`;
996
1193
  }
997
1194
  }
998
1195
  async listConversationBlobIds(conversationId) {
999
- await mkdir3(this.paths.blobsDir, { recursive: true, mode: 448 });
1196
+ await mkdir4(this.paths.blobsDir, { recursive: true, mode: 448 });
1000
1197
  const entries = await readdir(this.paths.blobsDir, { withFileTypes: true }).catch((error) => {
1001
1198
  if (isNodeError3(error, "ENOENT")) {
1002
1199
  return [];
@@ -1013,7 +1210,7 @@ ${attachmentLines.join("\n")}`;
1013
1210
  continue;
1014
1211
  }
1015
1212
  const manifest = await readJsonFile(
1016
- path4.join(this.paths.blobsDir, entry.name)
1213
+ path5.join(this.paths.blobsDir, entry.name)
1017
1214
  ).catch(() => null);
1018
1215
  if (manifest?.conversation_ids?.includes(conversationId)) {
1019
1216
  blobIds.push(blobId);
@@ -1199,7 +1396,7 @@ function isExplicitMediaPathKey(key) {
1199
1396
  ].includes(key);
1200
1397
  }
1201
1398
  function inferMimeType(filePath) {
1202
- const extension = path4.extname(filePath).toLowerCase();
1399
+ const extension = path5.extname(filePath).toLowerCase();
1203
1400
  return {
1204
1401
  ".png": "image/png",
1205
1402
  ".jpg": "image/jpeg",
@@ -1261,15 +1458,15 @@ function mediaSourceKey(sourcePath) {
1261
1458
  return createHash("sha256").update(resolveMediaSourcePath(sourcePath)).digest("hex").slice(0, 32);
1262
1459
  }
1263
1460
  function sanitizeFilename(value, fallback) {
1264
- const base = path4.basename((value ?? "").replace(/[\r\n\t]/gu, " ").trim());
1461
+ const base = path5.basename((value ?? "").replace(/[\r\n\t]/gu, " ").trim());
1265
1462
  const safe = base.replace(/[/:\\]/gu, "_").slice(0, 200).trim();
1266
1463
  return safe || fallback;
1267
1464
  }
1268
1465
  function resolveMediaSourcePath(sourcePath) {
1269
1466
  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)) {
1467
+ const expanded = trimmed.startsWith("~/") ? path5.join(process.env.HOME ?? "", trimmed.slice(2)) : trimmed;
1468
+ const resolved = path5.resolve(expanded);
1469
+ if (!path5.isAbsolute(expanded)) {
1273
1470
  throw new LinkHttpError(400, "media_source_path_not_absolute", "Hermes output media source must be an absolute path");
1274
1471
  }
1275
1472
  return resolved;
@@ -1296,16 +1493,16 @@ function isNodeError3(error, code) {
1296
1493
  }
1297
1494
 
1298
1495
  // src/hermes/profiles.ts
1299
- import { mkdir as mkdir4, readdir as readdir2, rename as rename2, rm as rm3, stat as stat2 } from "fs/promises";
1496
+ import { mkdir as mkdir5, readdir as readdir2, rename as rename2, rm as rm3, stat as stat2 } from "fs/promises";
1300
1497
  import os3 from "os";
1301
- import path5 from "path";
1498
+ import path6 from "path";
1302
1499
  var DEFAULT_PROFILE = "default";
1303
1500
  var PROFILE_NAME_PATTERN = /^[a-zA-Z0-9._-]{1,64}$/;
1304
1501
  async function listHermesProfiles(paths = resolveRuntimePaths()) {
1305
1502
  const activeProfile = await getActiveProfile(paths);
1306
1503
  const profiles = /* @__PURE__ */ new Map();
1307
1504
  profiles.set(DEFAULT_PROFILE, profileInfo(DEFAULT_PROFILE, activeProfile));
1308
- const profilesDir = path5.join(os3.homedir(), ".hermes", "profiles");
1505
+ const profilesDir = path6.join(os3.homedir(), ".hermes", "profiles");
1309
1506
  const entries = await readdir2(profilesDir, { withFileTypes: true }).catch((error) => {
1310
1507
  if (isNodeError4(error, "ENOENT")) {
1311
1508
  return [];
@@ -1352,14 +1549,14 @@ async function createHermesProfile(name) {
1352
1549
  if (await pathExists(profile.path)) {
1353
1550
  throw new Error("profile already exists");
1354
1551
  }
1355
- await mkdir4(profile.path, { recursive: true, mode: 448 });
1552
+ await mkdir5(profile.path, { recursive: true, mode: 448 });
1356
1553
  await ensureHermesApiServerKey(name, profile.configPath);
1357
1554
  return profile;
1358
1555
  }
1359
1556
  async function useHermesProfile(name, paths = resolveRuntimePaths()) {
1360
1557
  assertProfileName(name);
1361
1558
  const profile = profileInfo(name, name);
1362
- await mkdir4(profile.path, { recursive: true, mode: 448 });
1559
+ await mkdir5(profile.path, { recursive: true, mode: 448 });
1363
1560
  const current = await readJsonFile(paths.stateFile) ?? {};
1364
1561
  await writeJsonFile(paths.stateFile, { ...current, activeProfile: name });
1365
1562
  return profile;
@@ -1412,8 +1609,8 @@ function isNodeError4(error, code) {
1412
1609
  }
1413
1610
 
1414
1611
  // src/identity/identity.ts
1415
- import { generateKeyPairSync, randomUUID as randomUUID3, sign } from "crypto";
1416
- import { mkdir as mkdir5, chmod } from "fs/promises";
1612
+ import { generateKeyPairSync, randomUUID as randomUUID2, sign } from "crypto";
1613
+ import { mkdir as mkdir6, chmod } from "fs/promises";
1417
1614
  import { z } from "zod";
1418
1615
  var linkIdentitySchema = z.object({
1419
1616
  install_id: z.string().min(1),
@@ -1435,12 +1632,12 @@ async function ensureIdentity(paths = resolveRuntimePaths()) {
1435
1632
  if (existing) {
1436
1633
  return existing;
1437
1634
  }
1438
- await mkdir5(paths.homeDir, { recursive: true, mode: 448 });
1635
+ await mkdir6(paths.homeDir, { recursive: true, mode: 448 });
1439
1636
  await chmod(paths.homeDir, 448).catch(() => void 0);
1440
1637
  const { publicKey, privateKey } = generateKeyPairSync("ed25519");
1441
1638
  const now = (/* @__PURE__ */ new Date()).toISOString();
1442
1639
  const identity = {
1443
- install_id: `install_${randomUUID3().replaceAll("-", "")}`,
1640
+ install_id: `install_${randomUUID2().replaceAll("-", "")}`,
1444
1641
  link_id: null,
1445
1642
  public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
1446
1643
  private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
@@ -1474,11 +1671,11 @@ function getIdentityStatus(identity) {
1474
1671
  }
1475
1672
 
1476
1673
  // src/pairing/pairing.ts
1477
- import path6 from "path";
1674
+ import path7 from "path";
1478
1675
  import { rm as rm4 } from "fs/promises";
1479
1676
 
1480
1677
  // src/security/devices.ts
1481
- import { randomBytes as randomBytes2, randomUUID as randomUUID4, timingSafeEqual, createHash as createHash2 } from "crypto";
1678
+ import { randomBytes as randomBytes2, randomUUID as randomUUID3, timingSafeEqual, createHash as createHash2 } from "crypto";
1482
1679
  var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
1483
1680
  var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
1484
1681
  async function createDeviceSession(input, paths = resolveRuntimePaths()) {
@@ -1487,7 +1684,7 @@ async function createDeviceSession(input, paths = resolveRuntimePaths()) {
1487
1684
  const accessToken = randomToken("hpat_");
1488
1685
  const refreshToken = randomToken("hprt_");
1489
1686
  const device = {
1490
- id: `dev_${randomUUID4().replaceAll("-", "")}`,
1687
+ id: `dev_${randomUUID3().replaceAll("-", "")}`,
1491
1688
  label: input.label,
1492
1689
  platform: input.platform,
1493
1690
  scope: "admin",
@@ -1942,8 +2139,8 @@ async function loadRequiredIdentity(paths) {
1942
2139
  }
1943
2140
  return identity;
1944
2141
  }
1945
- async function postServerJson(serverBaseUrl, path8, body) {
1946
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path8}`, {
2142
+ async function postServerJson(serverBaseUrl, path9, body) {
2143
+ const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path9}`, {
1947
2144
  method: "POST",
1948
2145
  headers: {
1949
2146
  accept: "application/json",
@@ -1953,8 +2150,8 @@ async function postServerJson(serverBaseUrl, path8, body) {
1953
2150
  });
1954
2151
  return readJsonResponse2(response);
1955
2152
  }
1956
- async function patchServerJson(serverBaseUrl, path8, token, body) {
1957
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path8}`, {
2153
+ async function patchServerJson(serverBaseUrl, path9, token, body) {
2154
+ const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path9}`, {
1958
2155
  method: "PATCH",
1959
2156
  headers: {
1960
2157
  accept: "application/json",
@@ -1988,7 +2185,7 @@ function defaultDisplayName() {
1988
2185
  return `Hermes Link ${process.platform}`;
1989
2186
  }
1990
2187
  function pairingClaimPath(sessionId, paths) {
1991
- return path6.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
2188
+ return path7.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
1992
2189
  }
1993
2190
  function qrPreferredUrls(routes) {
1994
2191
  return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
@@ -2064,8 +2261,8 @@ function base64UrlToBase64(value) {
2064
2261
  }
2065
2262
 
2066
2263
  // 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";
2264
+ 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";
2265
+ import path8 from "path";
2069
2266
  var DEFAULT_LOG_FILE = "hermeslink.log";
2070
2267
  var DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
2071
2268
  var DEFAULT_MAX_FILES = 5;
@@ -2113,7 +2310,7 @@ var FileLogger = class {
2113
2310
  return this.queue;
2114
2311
  }
2115
2312
  async appendEntry(entry) {
2116
- await mkdir6(this.paths.logsDir, { recursive: true, mode: 448 });
2313
+ await mkdir7(this.paths.logsDir, { recursive: true, mode: 448 });
2117
2314
  const line = `${JSON.stringify(entry)}
2118
2315
  `;
2119
2316
  await this.rotateIfNeeded(Buffer.byteLength(line, "utf8"));
@@ -2139,7 +2336,7 @@ function createFileLogger(options = {}) {
2139
2336
  return new FileLogger(options);
2140
2337
  }
2141
2338
  function getLinkLogFile(paths = resolveRuntimePaths(), fileName = DEFAULT_LOG_FILE) {
2142
- return path7.join(paths.logsDir, fileName);
2339
+ return path8.join(paths.logsDir, fileName);
2143
2340
  }
2144
2341
  async function readRecentLogEntries(options = {}) {
2145
2342
  const paths = options.paths ?? resolveRuntimePaths();
@@ -2241,7 +2438,7 @@ async function readTail(filePath, maxBytes) {
2241
2438
  if (info.size <= maxBytes) {
2242
2439
  return await readFile4(filePath, "utf8").catch(() => null);
2243
2440
  }
2244
- const handle = await open2(filePath, "r").catch(() => null);
2441
+ const handle = await open3(filePath, "r").catch(() => null);
2245
2442
  if (!handle) {
2246
2443
  return null;
2247
2444
  }
@@ -2780,7 +2977,8 @@ export {
2780
2977
  LINK_COMMAND,
2781
2978
  resolveRuntimePaths,
2782
2979
  loadConfig,
2783
- ensureHermesApiServerKey,
2980
+ ensureHermesApiServerConfig,
2981
+ ensureHermesApiServerAvailable,
2784
2982
  loadIdentity,
2785
2983
  ensureIdentity,
2786
2984
  getIdentityStatus,
@@ -2791,4 +2989,4 @@ export {
2791
2989
  getLinkLogFile,
2792
2990
  createApp
2793
2991
  };
2794
- //# sourceMappingURL=chunk-VCQJ5DSN.js.map
2992
+ //# sourceMappingURL=chunk-SCIAZZ4C.js.map