@hermespilot/link 0.2.0 → 0.2.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.
@@ -3587,7 +3587,7 @@ async function listCronOutputFiles(profileName, jobId) {
3587
3587
  mtimeMs: fileStat.mtimeMs
3588
3588
  });
3589
3589
  }
3590
- return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path22, mtime }) => ({ path: path22, mtime }));
3590
+ return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path23, mtime }) => ({ path: path23, mtime }));
3591
3591
  }
3592
3592
  async function readCronOutput(outputPath) {
3593
3593
  const content = await readFile3(outputPath, "utf8");
@@ -4677,7 +4677,7 @@ import os2 from "os";
4677
4677
  import path7 from "path";
4678
4678
 
4679
4679
  // src/constants.ts
4680
- var LINK_VERSION = "0.2.0";
4680
+ var LINK_VERSION = "0.2.2";
4681
4681
  var LINK_COMMAND = "hermeslink";
4682
4682
  var LINK_DEFAULT_PORT = 52379;
4683
4683
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -7963,10 +7963,10 @@ async function cancelHermesRun(runId, options = {}) {
7963
7963
  );
7964
7964
  }
7965
7965
  }
7966
- async function callHermesApi(path22, init, options) {
7966
+ async function callHermesApi(path23, init, options) {
7967
7967
  const method = init.method ?? "GET";
7968
7968
  const startedAt = Date.now();
7969
- void options.logger?.debug("hermes_api_request_started", { method, path: path22 });
7969
+ void options.logger?.debug("hermes_api_request_started", { method, path: path23 });
7970
7970
  const availability = await ensureHermesApiServerAvailable({
7971
7971
  fetchImpl: options.fetchImpl,
7972
7972
  logger: options.logger,
@@ -7974,21 +7974,21 @@ async function callHermesApi(path22, init, options) {
7974
7974
  });
7975
7975
  let config = availability.configResult.apiServer;
7976
7976
  const fetcher = options.fetchImpl ?? fetch;
7977
- const request = () => fetchHermesApi(fetcher, config, path22, init, options);
7977
+ const request = () => fetchHermesApi(fetcher, config, path23, init, options);
7978
7978
  let response;
7979
7979
  try {
7980
7980
  response = await request();
7981
7981
  } catch (error) {
7982
- logHermesApiError(options.logger, method, path22, startedAt, error);
7982
+ logHermesApiError(options.logger, method, path23, startedAt, error);
7983
7983
  throw error;
7984
7984
  }
7985
7985
  if (response.status !== 401) {
7986
- logHermesApiResponse(options.logger, method, path22, startedAt, response);
7986
+ logHermesApiResponse(options.logger, method, path23, startedAt, response);
7987
7987
  return response;
7988
7988
  }
7989
7989
  void options.logger?.warn("hermes_api_request_retrying_after_401", {
7990
7990
  method,
7991
- path: path22,
7991
+ path: path23,
7992
7992
  duration_ms: Date.now() - startedAt
7993
7993
  });
7994
7994
  const refreshedAvailability = await ensureHermesApiServerAvailable({
@@ -8001,20 +8001,20 @@ async function callHermesApi(path22, init, options) {
8001
8001
  try {
8002
8002
  response = await request();
8003
8003
  } catch (error) {
8004
- logHermesApiError(options.logger, method, path22, startedAt, error);
8004
+ logHermesApiError(options.logger, method, path23, startedAt, error);
8005
8005
  throw error;
8006
8006
  }
8007
- logHermesApiResponse(options.logger, method, path22, startedAt, response);
8007
+ logHermesApiResponse(options.logger, method, path23, startedAt, response);
8008
8008
  return response;
8009
8009
  }
8010
- async function fetchHermesApi(fetcher, config, path22, init, options) {
8010
+ async function fetchHermesApi(fetcher, config, path23, init, options) {
8011
8011
  const headers = new Headers(init.headers);
8012
8012
  headers.set("accept", headers.get("accept") ?? "application/json");
8013
8013
  if (config.key) {
8014
8014
  headers.set("x-api-key", config.key);
8015
8015
  headers.set("authorization", `Bearer ${config.key}`);
8016
8016
  }
8017
- return await fetcher(`http://127.0.0.1:${config.port}${path22}`, {
8017
+ return await fetcher(`http://127.0.0.1:${config.port}${path23}`, {
8018
8018
  ...init,
8019
8019
  headers
8020
8020
  }).catch((error) => {
@@ -8022,7 +8022,7 @@ async function fetchHermesApi(fetcher, config, path22, init, options) {
8022
8022
  throw error;
8023
8023
  }
8024
8024
  void options.logger?.warn("hermes_api_server_connect_failed", {
8025
- path: path22,
8025
+ path: path23,
8026
8026
  port: config.port ?? null,
8027
8027
  error: error instanceof Error ? error.message : String(error)
8028
8028
  });
@@ -8033,10 +8033,10 @@ async function fetchHermesApi(fetcher, config, path22, init, options) {
8033
8033
  );
8034
8034
  });
8035
8035
  }
8036
- function logHermesApiResponse(logger, method, path22, startedAt, response) {
8036
+ function logHermesApiResponse(logger, method, path23, startedAt, response) {
8037
8037
  const fields = {
8038
8038
  method,
8039
- path: path22,
8039
+ path: path23,
8040
8040
  status: response.status,
8041
8041
  duration_ms: Date.now() - startedAt
8042
8042
  };
@@ -8056,10 +8056,10 @@ async function logHermesApiFailureResponse(logger, fields, response) {
8056
8056
  ...upstreamError ? { upstream_error: upstreamError } : {}
8057
8057
  });
8058
8058
  }
8059
- function logHermesApiError(logger, method, path22, startedAt, error) {
8059
+ function logHermesApiError(logger, method, path23, startedAt, error) {
8060
8060
  void logger?.warn("hermes_api_request_failed", {
8061
8061
  method,
8062
- path: path22,
8062
+ path: path23,
8063
8063
  duration_ms: Date.now() - startedAt,
8064
8064
  ...error instanceof LinkHttpError ? { status: error.status, code: error.code } : {},
8065
8065
  error: error instanceof Error ? error.message : String(error)
@@ -15882,10 +15882,535 @@ function readString14(payload, key) {
15882
15882
  }
15883
15883
 
15884
15884
  // src/link/updates.ts
15885
- import { spawn as spawn4 } from "child_process";
15885
+ import { spawn as spawn5 } from "child_process";
15886
15886
  import { EventEmitter as EventEmitter4 } from "events";
15887
- import { mkdir as mkdir14, readFile as readFile15, rm as rm8 } from "fs/promises";
15887
+ import { mkdir as mkdir16, readFile as readFile16, rm as rm10 } from "fs/promises";
15888
+ import path21 from "path";
15889
+
15890
+ // src/daemon/process.ts
15891
+ import { spawn as spawn4 } from "child_process";
15892
+ import { mkdir as mkdir15, readFile as readFile15, rm as rm9 } from "fs/promises";
15888
15893
  import path20 from "path";
15894
+
15895
+ // src/daemon/service.ts
15896
+ import { createServer } from "http";
15897
+ import { mkdir as mkdir14, rm as rm8, writeFile as writeFile7 } from "fs/promises";
15898
+
15899
+ // src/relay/control-client.ts
15900
+ import WebSocket from "ws";
15901
+ function connectRelayControl(options) {
15902
+ const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
15903
+ wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
15904
+ wsUrl.searchParams.set("link_id", options.linkId);
15905
+ const maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
15906
+ const backoffBaseMs = options.backoffBaseMs ?? 1e3;
15907
+ const backoffMaxMs = options.backoffMaxMs ?? 3e4;
15908
+ let reconnectAttempts = 0;
15909
+ let closedByUser = false;
15910
+ let socket = null;
15911
+ let retryTimer = null;
15912
+ let abortControllers = /* @__PURE__ */ new Map();
15913
+ let fatalRelayRejection = null;
15914
+ const connect = () => {
15915
+ options.onStatus?.({ state: "connecting", attempt: reconnectAttempts });
15916
+ fatalRelayRejection = null;
15917
+ socket = new WebSocket(wsUrl, {
15918
+ headers: {
15919
+ "x-hermes-link-version": LINK_VERSION
15920
+ }
15921
+ });
15922
+ socket.on("open", () => {
15923
+ reconnectAttempts = 0;
15924
+ options.onStatus?.({ state: "connected", attempt: reconnectAttempts });
15925
+ });
15926
+ socket.on("message", (raw) => {
15927
+ if (!socket || typeof raw !== "string" && !Buffer.isBuffer(raw)) {
15928
+ return;
15929
+ }
15930
+ void handleFrame(socket, String(raw), options.localPort, abortControllers).catch((error) => {
15931
+ const message = error instanceof Error ? error.message : "Relay request failed";
15932
+ socket?.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
15933
+ });
15934
+ });
15935
+ socket.on("error", (error) => {
15936
+ const message = error instanceof Error ? error.message : "Relay websocket error";
15937
+ fatalRelayRejection = resolveFatalRelayRejection(message);
15938
+ options.onStatus?.({
15939
+ state: "disconnected",
15940
+ attempt: reconnectAttempts,
15941
+ message: fatalRelayRejection ?? message
15942
+ });
15943
+ });
15944
+ socket.on("close", () => {
15945
+ abortAll(abortControllers);
15946
+ abortControllers = /* @__PURE__ */ new Map();
15947
+ if (fatalRelayRejection) {
15948
+ options.onStatus?.({
15949
+ state: "failed",
15950
+ attempt: reconnectAttempts,
15951
+ message: fatalRelayRejection
15952
+ });
15953
+ return;
15954
+ }
15955
+ if (closedByUser) {
15956
+ options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts });
15957
+ return;
15958
+ }
15959
+ if (reconnectAttempts >= maxReconnectAttempts) {
15960
+ options.onStatus?.({ state: "failed", attempt: reconnectAttempts, message: "Relay reconnect attempts exhausted" });
15961
+ return;
15962
+ }
15963
+ reconnectAttempts += 1;
15964
+ const delay2 = computeBackoffMs(reconnectAttempts, backoffBaseMs, backoffMaxMs);
15965
+ options.onStatus?.({ state: "retrying", attempt: reconnectAttempts, message: `Retrying in ${delay2}ms` });
15966
+ retryTimer = setTimeout(connect, delay2);
15967
+ retryTimer.unref?.();
15968
+ });
15969
+ };
15970
+ connect();
15971
+ return {
15972
+ close() {
15973
+ closedByUser = true;
15974
+ if (retryTimer) {
15975
+ clearTimeout(retryTimer);
15976
+ retryTimer = null;
15977
+ }
15978
+ abortAll(abortControllers);
15979
+ socket?.terminate();
15980
+ }
15981
+ };
15982
+ }
15983
+ function resolveFatalRelayRejection(message) {
15984
+ if (!/Unexpected server response:\s*(400|401|403|426)\b/u.test(message)) {
15985
+ return null;
15986
+ }
15987
+ return "Relay refused the Hermes Link connection. Check Link version and pairing state before retrying.";
15988
+ }
15989
+ function abortAll(abortControllers) {
15990
+ for (const controller of abortControllers.values()) {
15991
+ controller.abort();
15992
+ }
15993
+ abortControllers.clear();
15994
+ }
15995
+ function computeBackoffMs(attempt, baseMs, maxMs) {
15996
+ const exponential = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt - 1));
15997
+ const jitter = Math.floor(Math.random() * Math.min(1e3, exponential * 0.2));
15998
+ return exponential + jitter;
15999
+ }
16000
+ async function handleFrame(socket, raw, localPort, abortControllers) {
16001
+ const frame = JSON.parse(raw);
16002
+ if (frame.type === "http.cancel") {
16003
+ abortControllers.get(frame.id)?.abort();
16004
+ abortControllers.delete(frame.id);
16005
+ return;
16006
+ }
16007
+ if (frame.type !== "http.request") {
16008
+ return;
16009
+ }
16010
+ const abortController = new AbortController();
16011
+ abortControllers.set(frame.id, abortController);
16012
+ try {
16013
+ const response = await fetch(`http://127.0.0.1:${localPort}${frame.path}`, {
16014
+ method: frame.method,
16015
+ headers: frame.headers ?? {},
16016
+ body: frame.bodyBase64 ? Buffer.from(frame.bodyBase64, "base64") : void 0,
16017
+ signal: abortController.signal
16018
+ });
16019
+ const headers = Object.fromEntries(response.headers.entries());
16020
+ const contentType = response.headers.get("content-type") ?? "";
16021
+ if (response.body && contentType.includes("text/event-stream")) {
16022
+ socket.send(JSON.stringify({ type: "http.stream.start", id: frame.id, status: response.status, headers }));
16023
+ const reader = response.body.getReader();
16024
+ while (true) {
16025
+ const next = await reader.read();
16026
+ if (next.done) {
16027
+ break;
16028
+ }
16029
+ socket.send(JSON.stringify({ type: "http.stream.chunk", id: frame.id, bodyBase64: Buffer.from(next.value).toString("base64") }));
16030
+ }
16031
+ socket.send(JSON.stringify({ type: "http.stream.end", id: frame.id }));
16032
+ return;
16033
+ }
16034
+ const body = Buffer.from(await response.arrayBuffer()).toString("base64");
16035
+ socket.send(JSON.stringify({ type: "http.response", id: frame.id, status: response.status, headers, bodyBase64: body }));
16036
+ } catch (error) {
16037
+ const message = error instanceof Error ? error.message : "Relay request failed";
16038
+ socket.send(JSON.stringify({ type: "http.error", id: frame.id, status: 502, message }));
16039
+ } finally {
16040
+ abortControllers.delete(frame.id);
16041
+ }
16042
+ }
16043
+
16044
+ // src/daemon/scheduler.ts
16045
+ function startCronDeliveryScheduler(options) {
16046
+ let running = false;
16047
+ const syncCronDeliveries = async () => {
16048
+ if (running) {
16049
+ return;
16050
+ }
16051
+ running = true;
16052
+ try {
16053
+ await syncHermesLinkCronDeliveries(
16054
+ options.paths,
16055
+ options.conversations,
16056
+ options.logger
16057
+ );
16058
+ } catch (error) {
16059
+ void options.logger.warn("cron_link_delivery_sync_failed", {
16060
+ error: error instanceof Error ? error.message : String(error)
16061
+ });
16062
+ } finally {
16063
+ running = false;
16064
+ }
16065
+ };
16066
+ const timer = setInterval(() => {
16067
+ void syncCronDeliveries();
16068
+ }, options.intervalMs ?? 3e4);
16069
+ timer.unref?.();
16070
+ return {
16071
+ close() {
16072
+ clearInterval(timer);
16073
+ }
16074
+ };
16075
+ }
16076
+
16077
+ // src/daemon/service.ts
16078
+ async function startLinkService(options = {}) {
16079
+ const paths = options.paths ?? resolveRuntimePaths();
16080
+ const logger = createFileLogger({ paths });
16081
+ const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
16082
+ await logger.info("service_starting", {
16083
+ port: config.port,
16084
+ mode: identity?.link_id ? "paired" : "local-only"
16085
+ });
16086
+ const migration = await migrateLinkDatabase(paths);
16087
+ if (migration.appliedVersions.length > 0) {
16088
+ await logger.info("database_migrated", {
16089
+ database_file: migration.databaseFile,
16090
+ applied_versions: migration.appliedVersions,
16091
+ current_version: migration.currentVersion
16092
+ });
16093
+ }
16094
+ const conversations = new ConversationService(paths, logger);
16095
+ await conversations.rebuildStatisticsIndex();
16096
+ const app = await createApp({
16097
+ paths,
16098
+ logger,
16099
+ conversations,
16100
+ onPairingClaimed: options.onPairingClaimed
16101
+ });
16102
+ const server = createServer(app.callback());
16103
+ try {
16104
+ await listenServer(server, config.port);
16105
+ } catch (error) {
16106
+ await logger.error("service_start_failed", {
16107
+ port: config.port,
16108
+ error: error instanceof Error ? error.message : String(error)
16109
+ });
16110
+ await logger.flush();
16111
+ throw error;
16112
+ }
16113
+ server.on("error", (error) => {
16114
+ void logger.error("service_error", { error: error.message });
16115
+ });
16116
+ void logger.info("service_started", {
16117
+ port: config.port,
16118
+ link_id: identity?.link_id ?? null
16119
+ });
16120
+ const scheduler = startCronDeliveryScheduler({
16121
+ paths,
16122
+ conversations,
16123
+ logger
16124
+ });
16125
+ let relay = null;
16126
+ if (identity?.link_id) {
16127
+ relay = connectRelayControl({
16128
+ relayBaseUrl: config.relayBaseUrl,
16129
+ linkId: identity.link_id,
16130
+ localPort: config.port,
16131
+ maxReconnectAttempts: options.relayMaxReconnectAttempts ?? 5,
16132
+ backoffBaseMs: 1e3,
16133
+ backoffMaxMs: 3e4,
16134
+ onStatus: (status) => {
16135
+ void logger.info("relay_status", status);
16136
+ }
16137
+ });
16138
+ } else {
16139
+ void logger.info("relay_skipped", { reason: "link_not_paired" });
16140
+ }
16141
+ if (options.writePidFile) {
16142
+ await writePidFile(paths);
16143
+ }
16144
+ return {
16145
+ async close() {
16146
+ scheduler.close();
16147
+ relay?.close();
16148
+ await closeServer(server);
16149
+ await logger.info("service_stopped");
16150
+ await logger.flush();
16151
+ if (options.writePidFile) {
16152
+ await rm8(pidFilePath(paths), { force: true }).catch(() => void 0);
16153
+ }
16154
+ }
16155
+ };
16156
+ }
16157
+ function pidFilePath(paths = resolveRuntimePaths()) {
16158
+ return `${paths.runDir}/hermeslink.pid`;
16159
+ }
16160
+ async function writePidFile(paths) {
16161
+ await mkdir14(paths.runDir, { recursive: true, mode: 448 });
16162
+ await writeFile7(pidFilePath(paths), `${process.pid}
16163
+ `, { mode: 384 });
16164
+ }
16165
+ async function closeServer(server) {
16166
+ await new Promise((resolve, reject) => {
16167
+ let settled = false;
16168
+ let forceCloseTimer;
16169
+ let timeoutTimer;
16170
+ const settle = (error) => {
16171
+ if (settled) {
16172
+ return;
16173
+ }
16174
+ settled = true;
16175
+ clearTimeout(forceCloseTimer);
16176
+ clearTimeout(timeoutTimer);
16177
+ if (error) {
16178
+ reject(error);
16179
+ return;
16180
+ }
16181
+ resolve();
16182
+ };
16183
+ forceCloseTimer = setTimeout(() => {
16184
+ server.closeIdleConnections?.();
16185
+ server.closeAllConnections?.();
16186
+ }, 250);
16187
+ timeoutTimer = setTimeout(() => {
16188
+ server.closeAllConnections?.();
16189
+ settle();
16190
+ }, 5e3);
16191
+ server.close((error) => {
16192
+ if (error) {
16193
+ settle(error);
16194
+ return;
16195
+ }
16196
+ settle();
16197
+ });
16198
+ server.closeIdleConnections?.();
16199
+ });
16200
+ }
16201
+ async function listenServer(server, port) {
16202
+ await new Promise((resolve, reject) => {
16203
+ const cleanup = () => {
16204
+ server.off("error", onError);
16205
+ server.off("listening", onListening);
16206
+ };
16207
+ const onError = (error) => {
16208
+ cleanup();
16209
+ reject(error);
16210
+ };
16211
+ const onListening = () => {
16212
+ cleanup();
16213
+ resolve();
16214
+ };
16215
+ server.once("error", onError);
16216
+ server.once("listening", onListening);
16217
+ server.listen(port);
16218
+ });
16219
+ }
16220
+
16221
+ // src/daemon/process.ts
16222
+ async function startDaemonProcess(paths = resolveRuntimePaths()) {
16223
+ const config = await loadConfig(paths);
16224
+ let status = await getDaemonStatus(paths);
16225
+ if (status.running) {
16226
+ const probe = await probeLocalLinkService({ port: config.port, timeoutMs: 500 });
16227
+ if (probe.reachable) {
16228
+ return status;
16229
+ }
16230
+ await stopDaemonProcess(paths);
16231
+ status = await getDaemonStatus(paths);
16232
+ if (status.running) {
16233
+ return status;
16234
+ }
16235
+ }
16236
+ await mkdir15(paths.logsDir, { recursive: true, mode: 448 });
16237
+ await mkdir15(paths.runDir, { recursive: true, mode: 448 });
16238
+ const scriptPath = currentCliScriptPath();
16239
+ const child = spawn4(process.execPath, [scriptPath, "daemon-supervisor"], {
16240
+ detached: true,
16241
+ stdio: "ignore",
16242
+ env: process.env
16243
+ });
16244
+ child.unref();
16245
+ for (let index = 0; index < 12; index += 1) {
16246
+ await wait(250);
16247
+ const next = await getDaemonStatus(paths);
16248
+ if (next.running && (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable) {
16249
+ return next;
16250
+ }
16251
+ }
16252
+ return await getDaemonStatus(paths);
16253
+ }
16254
+ async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
16255
+ await mkdir15(paths.logsDir, { recursive: true, mode: 448 });
16256
+ const log = createRotatingTextLogWriter({
16257
+ paths,
16258
+ fileName: path20.basename(daemonLogFile(paths))
16259
+ });
16260
+ const scriptPath = currentCliScriptPath();
16261
+ const child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
16262
+ stdio: ["ignore", "pipe", "pipe"],
16263
+ env: process.env
16264
+ });
16265
+ const write = (chunk) => {
16266
+ void log.write(chunk);
16267
+ };
16268
+ write(`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor started
16269
+ `);
16270
+ child.stdout?.on("data", write);
16271
+ child.stderr?.on("data", write);
16272
+ const forwardStop = () => {
16273
+ if (child.pid && isProcessAlive3(child.pid)) {
16274
+ child.kill("SIGTERM");
16275
+ }
16276
+ };
16277
+ process.once("SIGINT", forwardStop);
16278
+ process.once("SIGTERM", forwardStop);
16279
+ const result = await new Promise((resolve, reject) => {
16280
+ child.once("error", reject);
16281
+ child.once("exit", (code, signal) => resolve({ code, signal }));
16282
+ }).catch((error) => {
16283
+ write(`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor failed: ${error instanceof Error ? error.message : String(error)}
16284
+ `);
16285
+ return { code: 1, signal: null };
16286
+ });
16287
+ process.off("SIGINT", forwardStop);
16288
+ process.off("SIGTERM", forwardStop);
16289
+ write(
16290
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor stopped code=${result.code ?? "null"} signal=${result.signal ?? "null"}
16291
+ `
16292
+ );
16293
+ await log.flush();
16294
+ return result.code ?? (result.signal ? 0 : 1);
16295
+ }
16296
+ async function probeLocalLinkService(options) {
16297
+ const unreachable = {
16298
+ reachable: false,
16299
+ reusable: false,
16300
+ linkId: null,
16301
+ version: null
16302
+ };
16303
+ let response;
16304
+ try {
16305
+ response = await fetch(`http://127.0.0.1:${options.port}/api/v1/bootstrap`, {
16306
+ headers: { accept: "application/json" },
16307
+ signal: AbortSignal.timeout(options.timeoutMs ?? 1e3)
16308
+ });
16309
+ } catch {
16310
+ return unreachable;
16311
+ }
16312
+ if (!response.ok) {
16313
+ return unreachable;
16314
+ }
16315
+ const payload = await response.json().catch(() => null);
16316
+ if (!payload || payload.api_version !== 1) {
16317
+ return unreachable;
16318
+ }
16319
+ const linkId = typeof payload.link_id === "string" ? payload.link_id : null;
16320
+ return {
16321
+ reachable: true,
16322
+ reusable: options.linkId ? linkId === options.linkId : true,
16323
+ linkId,
16324
+ version: typeof payload.version === "string" ? payload.version : null
16325
+ };
16326
+ }
16327
+ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
16328
+ const status = await getDaemonStatus(paths);
16329
+ if (!status.running || !status.pid) {
16330
+ return status;
16331
+ }
16332
+ try {
16333
+ process.kill(status.pid, "SIGTERM");
16334
+ } catch {
16335
+ await rm9(pidFilePath(paths), { force: true }).catch(() => void 0);
16336
+ return await getDaemonStatus(paths);
16337
+ }
16338
+ for (let index = 0; index < 20; index += 1) {
16339
+ await wait(250);
16340
+ if (!isProcessAlive3(status.pid)) {
16341
+ break;
16342
+ }
16343
+ }
16344
+ if (isProcessAlive3(status.pid)) {
16345
+ try {
16346
+ process.kill(status.pid, "SIGKILL");
16347
+ } catch {
16348
+ }
16349
+ for (let index = 0; index < 10; index += 1) {
16350
+ await wait(250);
16351
+ if (!isProcessAlive3(status.pid)) {
16352
+ break;
16353
+ }
16354
+ }
16355
+ }
16356
+ if (!isProcessAlive3(status.pid) || !await pidBackedServiceIsReachable(paths)) {
16357
+ await rm9(pidFilePath(paths), { force: true }).catch(() => void 0);
16358
+ }
16359
+ return await getDaemonStatus(paths);
16360
+ }
16361
+ async function getDaemonStatus(paths = resolveRuntimePaths()) {
16362
+ const pidFile = pidFilePath(paths);
16363
+ const pid = await readPid(pidFile);
16364
+ if (pid && !isProcessAlive3(pid)) {
16365
+ await rm9(pidFile, { force: true }).catch(() => void 0);
16366
+ return {
16367
+ running: false,
16368
+ pid: null,
16369
+ pidFile,
16370
+ logFile: daemonLogFile(paths)
16371
+ };
16372
+ }
16373
+ return {
16374
+ running: Boolean(pid),
16375
+ pid,
16376
+ pidFile,
16377
+ logFile: daemonLogFile(paths)
16378
+ };
16379
+ }
16380
+ function daemonLogFile(paths = resolveRuntimePaths()) {
16381
+ return getDaemonLogFile(paths);
16382
+ }
16383
+ function currentCliScriptPath() {
16384
+ return process.argv[1];
16385
+ }
16386
+ async function readPid(filePath) {
16387
+ const raw = await readFile15(filePath, "utf8").catch(() => null);
16388
+ if (!raw) {
16389
+ return null;
16390
+ }
16391
+ const pid = Number.parseInt(raw.trim(), 10);
16392
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
16393
+ }
16394
+ function isProcessAlive3(pid) {
16395
+ try {
16396
+ process.kill(pid, 0);
16397
+ return true;
16398
+ } catch {
16399
+ return false;
16400
+ }
16401
+ }
16402
+ async function pidBackedServiceIsReachable(paths) {
16403
+ const config = await loadConfig(paths).catch(() => null);
16404
+ if (!config) {
16405
+ return false;
16406
+ }
16407
+ return (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable;
16408
+ }
16409
+ function wait(ms) {
16410
+ return new Promise((resolve) => setTimeout(resolve, ms));
16411
+ }
16412
+
16413
+ // src/link/updates.ts
15889
16414
  var SERVER_LINK_CURRENT_RELEASE_PATH = "/api/v1/link/releases/current";
15890
16415
  var LINK_NPM_PACKAGE = "@hermespilot/link";
15891
16416
  var UPDATE_LOG_FILE2 = "link-update.log";
@@ -15893,6 +16418,7 @@ var UPDATE_LOG_MAX_FILES2 = 3;
15893
16418
  var UPDATE_FETCH_TIMEOUT_MS = 5e3;
15894
16419
  var MAX_UPDATE_LOG_LINES2 = 240;
15895
16420
  var MAX_OUTPUT_LINE_LENGTH3 = 1200;
16421
+ var AUTO_RESTART_DELAY_MS = 1500;
15896
16422
  var updateEvents2 = new EventEmitter4();
15897
16423
  var runningUpdate2 = null;
15898
16424
  async function readLinkUpdateCheck(options) {
@@ -15970,7 +16496,7 @@ async function startLinkUpdate(options) {
15970
16496
  error: null,
15971
16497
  manual_command: manualCommand
15972
16498
  };
15973
- await mkdir14(options.paths.runDir, { recursive: true, mode: 448 });
16499
+ await mkdir16(options.paths.runDir, { recursive: true, mode: 448 });
15974
16500
  await writer.write(
15975
16501
  `
15976
16502
  === link update started ${startedAt} target=${targetVersion} ===
@@ -15979,7 +16505,7 @@ async function startLinkUpdate(options) {
15979
16505
  await writer.write(`$ ${manualCommand}
15980
16506
  `);
15981
16507
  await writeUpdateState2(options.paths, started);
15982
- const child = spawn4(
16508
+ const child = spawn5(
15983
16509
  resolveNpmBin(),
15984
16510
  ["install", "-g", `${LINK_NPM_PACKAGE}@${targetVersion}`],
15985
16511
  {
@@ -16044,12 +16570,16 @@ async function startLinkUpdate(options) {
16044
16570
  if (succeeded) {
16045
16571
  await writer.write(
16046
16572
  `
16047
- [restart-required] Run \`${LINK_COMMAND} restart\` on this computer if Link does not reconnect automatically.
16573
+ [restart-scheduled] Hermes Link will restart automatically. If it does not reconnect, run \`${LINK_COMMAND} restart\` on this computer.
16048
16574
  `
16049
16575
  );
16050
16576
  }
16051
16577
  await writeUpdateState2(options.paths, state);
16052
16578
  await emitUpdateStatus2(options.paths);
16579
+ if (succeeded) {
16580
+ await writer.flush();
16581
+ scheduleAutomaticRestart(options);
16582
+ }
16053
16583
  void options.logger?.info(
16054
16584
  succeeded ? "link_update_restart_required" : "link_update_failed",
16055
16585
  {
@@ -16074,6 +16604,22 @@ async function startLinkUpdate(options) {
16074
16604
  });
16075
16605
  return readLinkUpdateStatus(options.paths);
16076
16606
  }
16607
+ function scheduleAutomaticRestart(options) {
16608
+ const scriptPath = currentCliScriptPath();
16609
+ setTimeout(() => {
16610
+ const child = spawn5(process.execPath, [scriptPath, "restart"], {
16611
+ detached: true,
16612
+ stdio: "ignore",
16613
+ env: process.env,
16614
+ windowsHide: true
16615
+ });
16616
+ child.unref();
16617
+ void options.logger?.info("link_update_restart_scheduled", {
16618
+ delay_ms: AUTO_RESTART_DELAY_MS,
16619
+ command: `${LINK_COMMAND} restart`
16620
+ });
16621
+ }, AUTO_RESTART_DELAY_MS).unref();
16622
+ }
16077
16623
  async function readLinkUpdateStatus(paths) {
16078
16624
  let state = await readJsonFile(updateStatePath2(paths));
16079
16625
  if (state?.state === "restart_required" && state.target_version) {
@@ -16086,7 +16632,7 @@ async function readLinkUpdateStatus(paths) {
16086
16632
  await writeUpdateState2(paths, state);
16087
16633
  }
16088
16634
  }
16089
- if (state?.state === "running" && !runningUpdate2 && !isRecentRunningState3(state) && !isProcessAlive3(state.pid)) {
16635
+ if (state?.state === "running" && !runningUpdate2 && !isRecentRunningState3(state) && !isProcessAlive4(state.pid)) {
16090
16636
  state = {
16091
16637
  ...state,
16092
16638
  state: "failed",
@@ -16239,7 +16785,7 @@ async function writeUpdateState2(paths, state) {
16239
16785
  await writeJsonFile(updateStatePath2(paths), state);
16240
16786
  }
16241
16787
  async function readUpdateLogLines2(paths) {
16242
- const raw = await readFile15(updateLogPath2(paths), "utf8").catch(() => "");
16788
+ const raw = await readFile16(updateLogPath2(paths), "utf8").catch(() => "");
16243
16789
  if (!raw.trim()) {
16244
16790
  return [];
16245
16791
  }
@@ -16248,18 +16794,18 @@ async function readUpdateLogLines2(paths) {
16248
16794
  );
16249
16795
  }
16250
16796
  function updateStatePath2(paths) {
16251
- return path20.join(paths.runDir, "link-update-state.json");
16797
+ return path21.join(paths.runDir, "link-update-state.json");
16252
16798
  }
16253
16799
  function updateLogPath2(paths) {
16254
- return path20.join(paths.logsDir, UPDATE_LOG_FILE2);
16800
+ return path21.join(paths.logsDir, UPDATE_LOG_FILE2);
16255
16801
  }
16256
16802
  async function clearUpdateLogFiles2(paths) {
16257
16803
  const primary = updateLogPath2(paths);
16258
16804
  await Promise.all([
16259
- rm8(primary, { force: true }).catch(() => void 0),
16805
+ rm10(primary, { force: true }).catch(() => void 0),
16260
16806
  ...Array.from(
16261
16807
  { length: UPDATE_LOG_MAX_FILES2 },
16262
- (_, index) => rm8(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
16808
+ (_, index) => rm10(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
16263
16809
  )
16264
16810
  ]);
16265
16811
  }
@@ -16292,7 +16838,7 @@ function isRecentRunningState3(state, now = Date.now()) {
16292
16838
  const startedAt = state.started_at ? Date.parse(state.started_at) : Number.NaN;
16293
16839
  return Number.isFinite(startedAt) && now - startedAt < 1e4;
16294
16840
  }
16295
- function isProcessAlive3(pid) {
16841
+ function isProcessAlive4(pid) {
16296
16842
  if (!pid || pid <= 0) {
16297
16843
  return false;
16298
16844
  }
@@ -16315,8 +16861,8 @@ function readString15(payload, key) {
16315
16861
  }
16316
16862
 
16317
16863
  // src/pairing/pairing.ts
16318
- import path21 from "path";
16319
- import { rm as rm9 } from "fs/promises";
16864
+ import path22 from "path";
16865
+ import { rm as rm11 } from "fs/promises";
16320
16866
 
16321
16867
  // src/relay/bootstrap.ts
16322
16868
  async function bootstrapRelayLink(options) {
@@ -16632,7 +17178,7 @@ async function readPairingClaim(sessionId, paths = resolveRuntimePaths()) {
16632
17178
  };
16633
17179
  }
16634
17180
  async function clearPairingClaim(sessionId, paths = resolveRuntimePaths()) {
16635
- await rm9(pairingClaimPath(sessionId, paths), { force: true }).catch(() => void 0);
17181
+ await rm11(pairingClaimPath(sessionId, paths), { force: true }).catch(() => void 0);
16636
17182
  }
16637
17183
  async function claimPairing(input) {
16638
17184
  const paths = input.paths ?? resolveRuntimePaths();
@@ -16680,8 +17226,8 @@ async function loadRequiredIdentity2(paths) {
16680
17226
  }
16681
17227
  return identity;
16682
17228
  }
16683
- async function postServerJson(serverBaseUrl, path22, body) {
16684
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path22}`, {
17229
+ async function postServerJson(serverBaseUrl, path23, body) {
17230
+ const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path23}`, {
16685
17231
  method: "POST",
16686
17232
  headers: {
16687
17233
  accept: "application/json",
@@ -16691,8 +17237,8 @@ async function postServerJson(serverBaseUrl, path22, body) {
16691
17237
  });
16692
17238
  return readJsonResponse2(response);
16693
17239
  }
16694
- async function patchServerJson(serverBaseUrl, path22, token, body) {
16695
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path22}`, {
17240
+ async function patchServerJson(serverBaseUrl, path23, token, body) {
17241
+ const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path23}`, {
16696
17242
  method: "PATCH",
16697
17243
  headers: {
16698
17244
  accept: "application/json",
@@ -16726,7 +17272,7 @@ function defaultDisplayName() {
16726
17272
  return `Hermes Link ${process.platform}`;
16727
17273
  }
16728
17274
  function pairingClaimPath(sessionId, paths) {
16729
- return path21.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
17275
+ return path22.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
16730
17276
  }
16731
17277
  function qrPreferredUrls(routes) {
16732
17278
  return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
@@ -17392,20 +17938,14 @@ async function createApp(options = {}) {
17392
17938
  export {
17393
17939
  LINK_VERSION,
17394
17940
  LINK_COMMAND,
17395
- migrateLinkDatabase,
17396
17941
  resolveHermesProfileDir,
17397
17942
  resolveHermesConfigPath,
17398
17943
  readHermesApiServerConfig,
17399
17944
  ensureHermesApiServerConfig,
17400
- syncHermesLinkCronDeliveries,
17401
17945
  LinkHttpError,
17402
17946
  resolveRuntimePaths,
17403
- createFileLogger,
17404
17947
  getLinkLogFile,
17405
- getDaemonLogFile,
17406
- createRotatingTextLogWriter,
17407
17948
  ensureHermesApiServerAvailable,
17408
- ConversationService,
17409
17949
  loadConfig,
17410
17950
  loadIdentity,
17411
17951
  ensureIdentity,
@@ -17414,6 +17954,14 @@ export {
17414
17954
  preparePairing,
17415
17955
  readPairingClaim,
17416
17956
  clearPairingClaim,
17417
- createApp
17957
+ createApp,
17958
+ startLinkService,
17959
+ startDaemonProcess,
17960
+ runDaemonSupervisor,
17961
+ probeLocalLinkService,
17962
+ stopDaemonProcess,
17963
+ getDaemonStatus,
17964
+ daemonLogFile,
17965
+ currentCliScriptPath
17418
17966
  };
17419
- //# sourceMappingURL=chunk-TMCXOV6J.js.map
17967
+ //# sourceMappingURL=chunk-4YF43CT4.js.map