@hermespilot/link 0.3.7 → 0.3.9

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.
@@ -1099,58 +1099,200 @@ function readProfileAvatarType(row) {
1099
1099
  }
1100
1100
 
1101
1101
  // src/hermes/cron-link-delivery.ts
1102
- import { mkdir as mkdir4, readdir as readdir2, readFile as readFile3, stat } from "fs/promises";
1102
+ import { mkdir as mkdir3, readdir as readdir3, readFile as readFile3, stat as stat2 } from "fs/promises";
1103
1103
  import path4 from "path";
1104
1104
 
1105
1105
  // src/storage/atomic-json.ts
1106
- import { mkdir as mkdir2, open, readFile, rename, rm } from "fs/promises";
1106
+ import { readFile } from "fs/promises";
1107
+
1108
+ // src/storage/atomic-file.ts
1107
1109
  import { randomUUID as randomUUID2 } from "crypto";
1110
+ import {
1111
+ chmod,
1112
+ chown,
1113
+ lstat,
1114
+ mkdir as mkdir2,
1115
+ open,
1116
+ readdir,
1117
+ rename,
1118
+ rm,
1119
+ stat
1120
+ } from "fs/promises";
1108
1121
  import path2 from "path";
1109
- async function readJsonFile(filePath) {
1122
+ async function atomicWriteFilePreservingMetadata(filePath, value, options = {}) {
1123
+ const resolvedPath = path2.resolve(filePath);
1124
+ const directory = path2.dirname(resolvedPath);
1125
+ await ensureDirectoryWithInheritedMetadata(
1126
+ directory,
1127
+ options.directoryMode ?? 448
1128
+ );
1129
+ const existingMetadata = await readExistingFileMetadata(resolvedPath) ?? (options.metadataSourcePath ? await readExistingFileMetadata(path2.resolve(options.metadataSourcePath)) : null);
1130
+ const directoryMetadata = await readPathMetadata(directory);
1131
+ const metadata = {
1132
+ uid: existingMetadata?.uid ?? directoryMetadata.uid,
1133
+ gid: existingMetadata?.gid ?? directoryMetadata.gid,
1134
+ mode: existingMetadata?.mode ?? options.mode ?? 384
1135
+ };
1136
+ const tempPath = path2.join(
1137
+ directory,
1138
+ `.${path2.basename(resolvedPath)}.${process.pid}.${Date.now()}.${randomUUID2()}.tmp`
1139
+ );
1110
1140
  try {
1111
- const raw = await readFile(filePath, "utf8");
1112
- return JSON.parse(raw);
1141
+ const handle = await open(tempPath, "wx", metadata.mode);
1142
+ try {
1143
+ if (typeof value === "string") {
1144
+ await handle.writeFile(value, options.encoding ?? "utf8");
1145
+ } else {
1146
+ await handle.writeFile(value);
1147
+ }
1148
+ await handle.sync();
1149
+ } finally {
1150
+ await handle.close();
1151
+ }
1152
+ await applyMetadata(tempPath, metadata);
1153
+ await rename(tempPath, resolvedPath);
1113
1154
  } catch (error) {
1155
+ await rm(tempPath, { force: true });
1156
+ throw error;
1157
+ }
1158
+ }
1159
+ async function ensureDirectoryWithInheritedMetadata(directory, mode) {
1160
+ const { source, missing } = await findExistingAncestor(directory);
1161
+ await mkdir2(directory, { recursive: true, mode });
1162
+ for (const missingDirectory of missing) {
1163
+ await applyMetadata(missingDirectory, {
1164
+ uid: source.uid,
1165
+ gid: source.gid,
1166
+ mode
1167
+ });
1168
+ }
1169
+ }
1170
+ async function inheritOwnerRecursively(targetPath, sourcePath) {
1171
+ if (process.platform === "win32") {
1172
+ return;
1173
+ }
1174
+ const source = await readPathMetadata(sourcePath);
1175
+ await applyOwnerRecursively(targetPath, source);
1176
+ }
1177
+ async function findExistingAncestor(directory) {
1178
+ const missing = [];
1179
+ let current = path2.resolve(directory);
1180
+ while (true) {
1181
+ const currentStat = await stat(current).catch((error) => {
1182
+ if (isNodeError(error, "ENOENT")) {
1183
+ return null;
1184
+ }
1185
+ throw error;
1186
+ });
1187
+ if (currentStat) {
1188
+ if (!currentStat.isDirectory()) {
1189
+ throw new Error(`${current} is not a directory`);
1190
+ }
1191
+ return {
1192
+ source: metadataFromStats(currentStat),
1193
+ missing: missing.reverse()
1194
+ };
1195
+ }
1196
+ missing.push(current);
1197
+ const parent = path2.dirname(current);
1198
+ if (parent === current) {
1199
+ throw new Error(`No existing parent directory for ${directory}`);
1200
+ }
1201
+ current = parent;
1202
+ }
1203
+ }
1204
+ async function readExistingFileMetadata(filePath) {
1205
+ const fileStat = await stat(filePath).catch((error) => {
1114
1206
  if (isNodeError(error, "ENOENT")) {
1115
1207
  return null;
1116
1208
  }
1117
1209
  throw error;
1210
+ });
1211
+ if (!fileStat) {
1212
+ return null;
1213
+ }
1214
+ if (!fileStat.isFile()) {
1215
+ throw new Error(`${filePath} is not a file`);
1118
1216
  }
1217
+ return metadataFromStats(fileStat);
1119
1218
  }
1120
- async function writeJsonFile(filePath, value, mode = 384) {
1121
- await mkdir2(path2.dirname(filePath), { recursive: true, mode: 448 });
1122
- const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${randomUUID2()}.tmp`;
1123
- const payload = `${JSON.stringify(value, null, 2)}
1124
- `;
1125
- const handle = await open(tmpPath, "w", mode);
1219
+ async function readPathMetadata(filePath) {
1220
+ return metadataFromStats(await stat(filePath));
1221
+ }
1222
+ async function applyMetadata(filePath, metadata) {
1223
+ await applyOwner(filePath, metadata);
1224
+ await chmod(filePath, metadata.mode);
1225
+ }
1226
+ async function applyOwnerRecursively(filePath, metadata) {
1227
+ const current = await lstat(filePath);
1228
+ if (current.isSymbolicLink()) {
1229
+ return;
1230
+ }
1231
+ await applyOwner(filePath, metadata);
1232
+ if (!current.isDirectory()) {
1233
+ return;
1234
+ }
1235
+ const entries = await readdir(filePath, { withFileTypes: true });
1236
+ await Promise.all(
1237
+ entries.map(
1238
+ (entry) => applyOwnerRecursively(path2.join(filePath, entry.name), metadata)
1239
+ )
1240
+ );
1241
+ }
1242
+ async function applyOwner(filePath, metadata) {
1243
+ if (process.platform === "win32") {
1244
+ return;
1245
+ }
1246
+ const currentUid = typeof process.getuid === "function" ? process.getuid() : void 0;
1247
+ const currentGid = typeof process.getgid === "function" ? process.getgid() : void 0;
1248
+ if (metadata.uid === currentUid && metadata.gid === currentGid) {
1249
+ return;
1250
+ }
1126
1251
  try {
1127
- await handle.writeFile(payload, "utf8");
1128
- await handle.sync();
1129
- } finally {
1130
- await handle.close();
1252
+ await chown(filePath, metadata.uid, metadata.gid);
1253
+ } catch (error) {
1254
+ const current = await stat(filePath);
1255
+ if (current.uid !== metadata.uid || current.gid !== metadata.gid) {
1256
+ throw error;
1257
+ }
1131
1258
  }
1259
+ }
1260
+ function metadataFromStats(statsValue) {
1261
+ return {
1262
+ uid: statsValue.uid,
1263
+ gid: statsValue.gid,
1264
+ mode: statsValue.mode & 511
1265
+ };
1266
+ }
1267
+ function isNodeError(error, code) {
1268
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
1269
+ }
1270
+
1271
+ // src/storage/atomic-json.ts
1272
+ async function readJsonFile(filePath) {
1132
1273
  try {
1133
- await rename(tmpPath, filePath);
1274
+ const raw = await readFile(filePath, "utf8");
1275
+ return JSON.parse(raw);
1134
1276
  } catch (error) {
1135
- await rm(tmpPath, { force: true });
1277
+ if (isNodeError2(error, "ENOENT")) {
1278
+ return null;
1279
+ }
1136
1280
  throw error;
1137
1281
  }
1138
1282
  }
1139
- function isNodeError(error, code) {
1283
+ async function writeJsonFile(filePath, value, mode = 384) {
1284
+ const payload = `${JSON.stringify(value, null, 2)}
1285
+ `;
1286
+ await atomicWriteFilePreservingMetadata(filePath, payload, { mode });
1287
+ }
1288
+ function isNodeError2(error, code) {
1140
1289
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
1141
1290
  }
1142
1291
 
1143
1292
  // src/hermes/config.ts
1144
1293
  import { randomBytes } from "crypto";
1145
1294
  import net from "net";
1146
- import {
1147
- copyFile,
1148
- mkdir as mkdir3,
1149
- readFile as readFile2,
1150
- readdir,
1151
- rename as rename2,
1152
- writeFile
1153
- } from "fs/promises";
1295
+ import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
1154
1296
  import os from "os";
1155
1297
  import path3 from "path";
1156
1298
  import YAML from "yaml";
@@ -1374,7 +1516,7 @@ async function readHermesSessionsDir(profileName = "default", configPath = resol
1374
1516
  async function readHermesApiServerConfig(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
1375
1517
  const existingRaw = await readFile2(configPath, "utf8").catch(
1376
1518
  (error) => {
1377
- if (isNodeError2(error, "ENOENT")) {
1519
+ if (isNodeError3(error, "ENOENT")) {
1378
1520
  return null;
1379
1521
  }
1380
1522
  throw error;
@@ -1399,7 +1541,7 @@ async function readHermesApiServerConfig(profileName = "default", configPath = r
1399
1541
  async function readHermesModelConfig(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
1400
1542
  const existingRaw = await readFile2(configPath, "utf8").catch(
1401
1543
  (error) => {
1402
- if (isNodeError2(error, "ENOENT")) {
1544
+ if (isNodeError3(error, "ENOENT")) {
1403
1545
  return null;
1404
1546
  }
1405
1547
  throw error;
@@ -1920,7 +2062,7 @@ async function ensureHermesApiServerConfig(profileName = "default", configPath =
1920
2062
  async function ensureHermesApiServerConfigUnlocked(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
1921
2063
  const existingRaw = await readFile2(configPath, "utf8").catch(
1922
2064
  (error) => {
1923
- if (isNodeError2(error, "ENOENT")) {
2065
+ if (isNodeError3(error, "ENOENT")) {
1924
2066
  return null;
1925
2067
  }
1926
2068
  throw error;
@@ -1986,12 +2128,13 @@ async function ensureHermesApiServerConfigUnlocked(profileName = "default", conf
1986
2128
  };
1987
2129
  }
1988
2130
  const backupPath = existingRaw ? `${configPath}.bak.${Date.now()}` : null;
1989
- await mkdir3(path3.dirname(configPath), { recursive: true, mode: 448 });
1990
- if (backupPath) {
1991
- await copyFile(configPath, backupPath);
2131
+ if (backupPath && existingRaw !== null) {
2132
+ await atomicWriteFilePreservingMetadata(backupPath, existingRaw, {
2133
+ metadataSourcePath: configPath
2134
+ });
1992
2135
  }
1993
2136
  document.contents = document.createNode(config);
1994
- await writeFile(configPath, document.toString(), { mode: 384 });
2137
+ await atomicWriteFilePreservingMetadata(configPath, document.toString());
1995
2138
  return {
1996
2139
  configPath,
1997
2140
  apiServer: applyEnvOverrides(
@@ -2017,7 +2160,7 @@ async function ensureHermesApiServerConfigUnlocked(profileName = "default", conf
2017
2160
  async function readHermesConfigDocument(configPath) {
2018
2161
  const existingRaw = await readFile2(configPath, "utf8").catch(
2019
2162
  (error) => {
2020
- if (isNodeError2(error, "ENOENT")) {
2163
+ if (isNodeError3(error, "ENOENT")) {
2021
2164
  return null;
2022
2165
  }
2023
2166
  throw error;
@@ -2032,14 +2175,16 @@ async function readHermesConfigDocument(configPath) {
2032
2175
  }
2033
2176
  async function writeHermesConfigDocument(input) {
2034
2177
  const backupPath = input.existingRaw ? `${input.configPath}.bak.${Date.now()}` : null;
2035
- await mkdir3(path3.dirname(input.configPath), { recursive: true, mode: 448 });
2036
2178
  if (backupPath) {
2037
- await copyFile(input.configPath, backupPath);
2179
+ await atomicWriteFilePreservingMetadata(backupPath, input.existingRaw, {
2180
+ metadataSourcePath: input.configPath
2181
+ });
2038
2182
  }
2039
2183
  input.document.contents = input.document.createNode(input.config);
2040
- const tempPath = `${input.configPath}.tmp.${process.pid}.${Date.now()}`;
2041
- await writeFile(tempPath, input.document.toString(), { mode: 384 });
2042
- await rename2(tempPath, input.configPath);
2184
+ await atomicWriteFilePreservingMetadata(
2185
+ input.configPath,
2186
+ input.document.toString()
2187
+ );
2043
2188
  return backupPath;
2044
2189
  }
2045
2190
  function readManagedModelConfigs(config, env, defaultModel, defaultReasoningEffort) {
@@ -3361,9 +3506,9 @@ async function readConfiguredApiServerPorts(excludedProfileName) {
3361
3506
  resolveDefaultHermesRoot(resolveHermesProfileDir("default")),
3362
3507
  "profiles"
3363
3508
  );
3364
- const entries = await readdir(profilesRoot, { withFileTypes: true }).catch(
3509
+ const entries = await readdir2(profilesRoot, { withFileTypes: true }).catch(
3365
3510
  (error) => {
3366
- if (isNodeError2(error, "ENOENT")) {
3511
+ if (isNodeError3(error, "ENOENT")) {
3367
3512
  return [];
3368
3513
  }
3369
3514
  throw error;
@@ -3385,7 +3530,7 @@ async function addConfiguredApiServerPort(ports, profileName, excludedProfileNam
3385
3530
  resolveHermesConfigPath(profileName),
3386
3531
  "utf8"
3387
3532
  ).catch((error) => {
3388
- if (isNodeError2(error, "ENOENT")) {
3533
+ if (isNodeError3(error, "ENOENT")) {
3389
3534
  return "";
3390
3535
  }
3391
3536
  throw error;
@@ -3458,7 +3603,7 @@ async function readHermesApiServerEnvOverrides(profileName) {
3458
3603
  async function readHermesEnvFile(profileName) {
3459
3604
  const envPath = path3.join(resolveHermesProfileDir(profileName), ".env");
3460
3605
  const raw = await readFile2(envPath, "utf8").catch((error) => {
3461
- if (isNodeError2(error, "ENOENT")) {
3606
+ if (isNodeError3(error, "ENOENT")) {
3462
3607
  return "";
3463
3608
  }
3464
3609
  throw error;
@@ -3483,7 +3628,7 @@ async function writeHermesEnvValue(profileName, key, value) {
3483
3628
  const envPath = path3.join(resolveHermesProfileDir(profileName), ".env");
3484
3629
  const existingRaw = await readFile2(envPath, "utf8").catch(
3485
3630
  (error) => {
3486
- if (isNodeError2(error, "ENOENT")) {
3631
+ if (isNodeError3(error, "ENOENT")) {
3487
3632
  return "";
3488
3633
  }
3489
3634
  throw error;
@@ -3506,13 +3651,14 @@ async function writeHermesEnvValue(profileName, key, value) {
3506
3651
  nextLines.push(`${key}=${formatEnvValue(value)}`);
3507
3652
  }
3508
3653
  const nextRaw = nextLines.join("\n").replace(/\n*$/u, "\n");
3509
- await mkdir3(path3.dirname(envPath), { recursive: true, mode: 448 });
3510
3654
  if (existingRaw) {
3511
- await copyFile(envPath, `${envPath}.bak.${Date.now()}`);
3655
+ await atomicWriteFilePreservingMetadata(
3656
+ `${envPath}.bak.${Date.now()}`,
3657
+ existingRaw,
3658
+ { metadataSourcePath: envPath }
3659
+ );
3512
3660
  }
3513
- const tempPath = `${envPath}.tmp.${process.pid}.${Date.now()}`;
3514
- await writeFile(tempPath, nextRaw, { mode: 384 });
3515
- await rename2(tempPath, envPath);
3661
+ await atomicWriteFilePreservingMetadata(envPath, nextRaw);
3516
3662
  }
3517
3663
  function applyEnvOverrides(config, env, withDefaults) {
3518
3664
  const host = config.host ?? env.host;
@@ -3591,7 +3737,7 @@ function ensureRecord(parent, key) {
3591
3737
  function resolveHermesConfiguredPath(value, baseDir) {
3592
3738
  return path3.isAbsolute(value) ? path3.normalize(value) : path3.resolve(baseDir, value);
3593
3739
  }
3594
- function isNodeError2(error, code) {
3740
+ function isNodeError3(error, code) {
3595
3741
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
3596
3742
  }
3597
3743
  function resolveDefaultHermesRoot(hermesHome) {
@@ -3716,9 +3862,9 @@ async function listCronOutputFiles(profileName, jobId) {
3716
3862
  "output",
3717
3863
  jobId
3718
3864
  );
3719
- const entries = await readdir2(outputDir, { withFileTypes: true }).catch(
3865
+ const entries = await readdir3(outputDir, { withFileTypes: true }).catch(
3720
3866
  (error) => {
3721
- if (isNodeError3(error, "ENOENT")) {
3867
+ if (isNodeError4(error, "ENOENT")) {
3722
3868
  return [];
3723
3869
  }
3724
3870
  throw error;
@@ -3730,7 +3876,7 @@ async function listCronOutputFiles(profileName, jobId) {
3730
3876
  continue;
3731
3877
  }
3732
3878
  const outputPath = path4.join(outputDir, entry.name);
3733
- const fileStat = await stat(outputPath);
3879
+ const fileStat = await stat2(outputPath);
3734
3880
  files.push({
3735
3881
  path: outputPath,
3736
3882
  mtime: fileStat.mtime.toISOString(),
@@ -3763,7 +3909,7 @@ async function readRegistry(paths) {
3763
3909
  return { version: REGISTRY_VERSION, bindings: [] };
3764
3910
  }
3765
3911
  async function writeRegistry(paths, registry) {
3766
- await mkdir4(paths.indexesDir, { recursive: true, mode: 448 });
3912
+ await mkdir3(paths.indexesDir, { recursive: true, mode: 448 });
3767
3913
  await writeJsonFile(registryPath(paths), registry);
3768
3914
  }
3769
3915
  function registryPath(paths) {
@@ -3789,13 +3935,13 @@ function readString3(record, ...keys) {
3789
3935
  }
3790
3936
  return void 0;
3791
3937
  }
3792
- function isNodeError3(error, code) {
3938
+ function isNodeError4(error, code) {
3793
3939
  return error instanceof Error && error.code === code;
3794
3940
  }
3795
3941
 
3796
3942
  // src/hermes/gateway.ts
3797
3943
  import { execFile as execFile2, spawn } from "child_process";
3798
- import { access, readFile as readFile5, stat as stat3 } from "fs/promises";
3944
+ import { access, readFile as readFile5, stat as stat4 } from "fs/promises";
3799
3945
  import path7 from "path";
3800
3946
  import { setTimeout as delay } from "timers/promises";
3801
3947
  import { promisify as promisify2 } from "util";
@@ -3815,7 +3961,7 @@ function isLinkHttpError(error) {
3815
3961
  }
3816
3962
 
3817
3963
  // src/runtime/logger.ts
3818
- import { appendFile, mkdir as mkdir5, open as open2, readFile as readFile4, rename as rename3, rm as rm2, stat as stat2 } from "fs/promises";
3964
+ import { appendFile, mkdir as mkdir4, open as open2, readFile as readFile4, rename as rename2, rm as rm2, stat as stat3 } from "fs/promises";
3819
3965
  import os3 from "os";
3820
3966
  import path6 from "path";
3821
3967
 
@@ -3824,7 +3970,7 @@ import os2 from "os";
3824
3970
  import path5 from "path";
3825
3971
 
3826
3972
  // src/constants.ts
3827
- var LINK_VERSION = "0.3.7";
3973
+ var LINK_VERSION = "0.3.9";
3828
3974
  var LINK_COMMAND = "hermeslink";
3829
3975
  var LINK_DEFAULT_PORT = 52379;
3830
3976
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -3900,7 +4046,7 @@ var FileLogger = class {
3900
4046
  return this.queue;
3901
4047
  }
3902
4048
  async appendEntry(entry) {
3903
- await mkdir5(this.paths.logsDir, { recursive: true, mode: 448 });
4049
+ await mkdir4(this.paths.logsDir, { recursive: true, mode: 448 });
3904
4050
  const line = `${JSON.stringify(entry)}
3905
4051
  `;
3906
4052
  await this.rotateIfNeeded(Buffer.byteLength(line, "utf8"));
@@ -3936,7 +4082,7 @@ function createRotatingTextLogWriter(options) {
3936
4082
  return queue;
3937
4083
  }
3938
4084
  const next = queue.then(async () => {
3939
- await mkdir5(paths.logsDir, { recursive: true, mode: 448 });
4085
+ await mkdir4(paths.logsDir, { recursive: true, mode: 448 });
3940
4086
  await rotateLogFileIfNeeded(filePath, buffer.length, maxFileBytes, maxFiles);
3941
4087
  await appendFile(filePath, buffer, { mode: 384 });
3942
4088
  }).catch(() => void 0);
@@ -4037,7 +4183,7 @@ function clampLimit(value) {
4037
4183
  return Math.min(MAX_READ_LIMIT, Math.max(1, Math.floor(value)));
4038
4184
  }
4039
4185
  async function rotateLogFileIfNeeded(filePath, nextBytes, maxFileBytes, maxFiles) {
4040
- const current = await stat2(filePath).catch(() => null);
4186
+ const current = await stat3(filePath).catch(() => null);
4041
4187
  if (!current || current.size === 0 || current.size + nextBytes <= maxFileBytes) {
4042
4188
  return;
4043
4189
  }
@@ -4152,7 +4298,7 @@ async function readTail(filePath, maxBytes) {
4152
4298
  return tail?.content ?? null;
4153
4299
  }
4154
4300
  async function readTailWithMetadata(filePath, maxBytes) {
4155
- const info = await stat2(filePath).catch(() => null);
4301
+ const info = await stat3(filePath).catch(() => null);
4156
4302
  if (!info || info.size <= 0) {
4157
4303
  return null;
4158
4304
  }
@@ -4176,7 +4322,7 @@ async function readTailWithMetadata(filePath, maxBytes) {
4176
4322
  }
4177
4323
  async function moveIfExists(from, to) {
4178
4324
  await rm2(to, { force: true }).catch(() => void 0);
4179
- await rename3(from, to).catch((error) => {
4325
+ await rename2(from, to).catch((error) => {
4180
4326
  if (error.code !== "ENOENT") {
4181
4327
  throw error;
4182
4328
  }
@@ -4309,6 +4455,9 @@ var DEFAULT_START_TIMEOUT_MS = 12e3;
4309
4455
  var HEALTH_TIMEOUT_MS = 1500;
4310
4456
  var MIN_API_SERVER_VERSION = "0.4.0";
4311
4457
  var PROFILE_NAME_PATTERN = /^[a-zA-Z0-9._-]{1,64}$/u;
4458
+ var DASHBOARD_STATUS_URL = "http://127.0.0.1:9119/api/status";
4459
+ var DASHBOARD_STATUS_TIMEOUT_MS = 1500;
4460
+ var MAX_VERSION_LOG_OUTPUT_LENGTH = 1200;
4312
4461
  var gatewayStartInFlightByProfile = /* @__PURE__ */ new Map();
4313
4462
  async function ensureHermesApiServerAvailable(options = {}) {
4314
4463
  const profileName = normalizeProfileName(options.profileName);
@@ -4425,28 +4574,56 @@ async function reloadHermesGateway(options = {}) {
4425
4574
  }
4426
4575
  return ensureHermesApiServerAvailable({ ...options, forceRestart: true });
4427
4576
  }
4428
- async function readHermesVersion() {
4429
- const { stdout } = await execHermesVersion();
4430
- const raw = stdout.trim();
4431
- const version = parseHermesVersion(raw);
4432
- return {
4433
- raw,
4434
- version,
4435
- supportsApiServer: version ? compareSemver(version, MIN_API_SERVER_VERSION) >= 0 : null
4436
- };
4437
- }
4438
- async function execHermesVersion() {
4577
+ async function readHermesVersion(options = {}) {
4439
4578
  try {
4440
- return await execFileAsync2(resolveHermesBin(), ["version"], {
4441
- timeout: 5e3,
4442
- windowsHide: true
4443
- });
4444
- } catch {
4445
- return await execFileAsync2(resolveHermesBin(), ["--version"], {
4446
- timeout: 5e3,
4447
- windowsHide: true
4579
+ const { stdout } = await execHermesVersion(options.logger);
4580
+ const raw = stdout.trim();
4581
+ const version = parseHermesVersion(raw);
4582
+ return buildHermesVersionInfo(raw, version);
4583
+ } catch (cliError) {
4584
+ const dashboardStatusUrl = options.dashboardStatusUrl ?? DASHBOARD_STATUS_URL;
4585
+ void options.logger?.warn("hermes_version_dashboard_fallback_requested", {
4586
+ dashboard_status_url: dashboardStatusUrl,
4587
+ reason: cliError instanceof Error ? cliError.message : String(cliError)
4448
4588
  });
4589
+ try {
4590
+ const fallback = await readHermesDashboardVersion({
4591
+ fetchImpl: options.fetchImpl,
4592
+ statusUrl: dashboardStatusUrl,
4593
+ timeoutMs: options.dashboardTimeoutMs
4594
+ });
4595
+ void options.logger?.info("hermes_version_dashboard_fallback_succeeded", {
4596
+ dashboard_status_url: dashboardStatusUrl,
4597
+ hermes_version: fallback.version
4598
+ });
4599
+ return fallback;
4600
+ } catch (dashboardError) {
4601
+ void options.logger?.warn("hermes_version_dashboard_fallback_failed", {
4602
+ dashboard_status_url: dashboardStatusUrl,
4603
+ error: dashboardError instanceof Error ? dashboardError.message : String(dashboardError)
4604
+ });
4605
+ throw new Error(
4606
+ `Hermes version detection failed. CLI: ${cliError instanceof Error ? cliError.message : String(cliError)}; dashboard fallback: ${dashboardError instanceof Error ? dashboardError.message : String(dashboardError)}`
4607
+ );
4608
+ }
4609
+ }
4610
+ }
4611
+ async function execHermesVersion(logger) {
4612
+ const hermesBin = resolveHermesBin();
4613
+ const failures = [];
4614
+ for (const args of [["version"], ["--version"]]) {
4615
+ try {
4616
+ return await execFileAsync2(hermesBin, args, {
4617
+ timeout: 5e3,
4618
+ windowsHide: true
4619
+ });
4620
+ } catch (error) {
4621
+ const failure = describeVersionCommandFailure(hermesBin, args, error);
4622
+ failures.push(failure.summary);
4623
+ void logger?.warn("hermes_version_cli_command_failed", failure.fields);
4624
+ }
4449
4625
  }
4626
+ throw new Error(failures.join("; "));
4450
4627
  }
4451
4628
  function assertHermesRunsApiSupported(version, status) {
4452
4629
  if (status !== 404) {
@@ -4471,7 +4648,7 @@ async function startHermesGatewayOnce(paths, profileName, logger) {
4471
4648
  return await gatewayStartInFlightByProfile.get(profileName);
4472
4649
  }
4473
4650
  async function startHermesGateway(paths, profileName, logger) {
4474
- const version = await readHermesVersion().catch((error) => {
4651
+ const version = await readHermesVersion({ logger }).catch((error) => {
4475
4652
  void logger?.error("gateway_hermes_cli_unavailable", {
4476
4653
  error: error instanceof Error ? error.message : String(error)
4477
4654
  });
@@ -4583,7 +4760,7 @@ async function restartHermesGatewayServiceIfAvailable(options) {
4583
4760
  return {
4584
4761
  pid: null,
4585
4762
  logPath,
4586
- version: await readHermesVersion().catch(() => null),
4763
+ version: await readHermesVersion({ logger: options.logger }).catch(() => null),
4587
4764
  ...logHint ? { logHint } : {}
4588
4765
  };
4589
4766
  }
@@ -4599,8 +4776,8 @@ async function assertProfileExists(profileName) {
4599
4776
  if (profileName === "default") {
4600
4777
  return;
4601
4778
  }
4602
- const exists = await stat3(resolveHermesProfileDir(profileName)).then((value) => value.isDirectory()).catch((error) => {
4603
- if (isNodeError4(error, "ENOENT")) {
4779
+ const exists = await stat4(resolveHermesProfileDir(profileName)).then((value) => value.isDirectory()).catch((error) => {
4780
+ if (isNodeError5(error, "ENOENT")) {
4604
4781
  return false;
4605
4782
  }
4606
4783
  throw error;
@@ -4800,7 +4977,7 @@ function isHealthyPlatformState(value) {
4800
4977
  function toRecord2(value) {
4801
4978
  return typeof value === "object" && value !== null ? value : {};
4802
4979
  }
4803
- function isNodeError4(error, code) {
4980
+ function isNodeError5(error, code) {
4804
4981
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
4805
4982
  }
4806
4983
  function readString4(payload, key) {
@@ -4826,6 +5003,72 @@ function parseHermesVersion(value) {
4826
5003
  const match = /\bv?(\d+\.\d+\.\d+)\b/u.exec(value);
4827
5004
  return match?.[1] ?? null;
4828
5005
  }
5006
+ function buildHermesVersionInfo(raw, version) {
5007
+ return {
5008
+ raw,
5009
+ version,
5010
+ supportsApiServer: version ? compareSemver(version, MIN_API_SERVER_VERSION) >= 0 : null
5011
+ };
5012
+ }
5013
+ async function readHermesDashboardVersion(options = {}) {
5014
+ const fetcher = options.fetchImpl ?? fetch;
5015
+ const controller = new AbortController();
5016
+ const timer = setTimeout(
5017
+ () => controller.abort(),
5018
+ options.timeoutMs ?? DASHBOARD_STATUS_TIMEOUT_MS
5019
+ );
5020
+ try {
5021
+ const response = await fetcher(options.statusUrl ?? DASHBOARD_STATUS_URL, {
5022
+ method: "GET",
5023
+ headers: { accept: "application/json" },
5024
+ signal: controller.signal
5025
+ });
5026
+ if (!response.ok) {
5027
+ throw new Error(`Hermes dashboard returned HTTP ${response.status}`);
5028
+ }
5029
+ const payload = await response.json().catch(() => null);
5030
+ const record = toRecord2(payload);
5031
+ const versionText = readString4(record, "version");
5032
+ if (!versionText) {
5033
+ throw new Error("Hermes dashboard status did not include a version");
5034
+ }
5035
+ const raw = truncateVersionLogOutput(JSON.stringify(record));
5036
+ const version = parseHermesVersion(versionText) ?? parseHermesVersion(raw);
5037
+ return buildHermesVersionInfo(raw, version);
5038
+ } catch (error) {
5039
+ if (error instanceof Error && error.name === "AbortError") {
5040
+ throw new Error("Hermes dashboard version probe timed out");
5041
+ }
5042
+ throw error;
5043
+ } finally {
5044
+ clearTimeout(timer);
5045
+ }
5046
+ }
5047
+ function describeVersionCommandFailure(hermesBin, args, error) {
5048
+ const message = error instanceof Error ? error.message : String(error);
5049
+ const output = truncateVersionLogOutput(readExecErrorOutput2(error));
5050
+ return {
5051
+ summary: `${hermesBin} ${args.join(" ")} failed: ${message}`,
5052
+ fields: {
5053
+ hermes_bin: hermesBin,
5054
+ command: args.join(" "),
5055
+ error: message,
5056
+ ...output ? { output } : {}
5057
+ }
5058
+ };
5059
+ }
5060
+ function readExecErrorOutput2(error) {
5061
+ if (typeof error !== "object" || error === null) {
5062
+ return "";
5063
+ }
5064
+ const stdout = "stdout" in error && error.stdout != null ? String(error.stdout) : "";
5065
+ const stderr = "stderr" in error && error.stderr != null ? String(error.stderr) : "";
5066
+ return `${stdout}
5067
+ ${stderr}`.trim();
5068
+ }
5069
+ function truncateVersionLogOutput(value) {
5070
+ return value.length > MAX_VERSION_LOG_OUTPUT_LENGTH ? `${value.slice(0, MAX_VERSION_LOG_OUTPUT_LENGTH)}...` : value;
5071
+ }
4829
5072
  function compareSemver(left, right) {
4830
5073
  const leftParts = left.split(".").map((part) => Number.parseInt(part, 10));
4831
5074
  const rightParts = right.split(".").map((part) => Number.parseInt(part, 10));
@@ -5168,7 +5411,7 @@ function firstRecord(...values) {
5168
5411
 
5169
5412
  // src/conversations/blob-store.ts
5170
5413
  import { randomUUID as randomUUID3 } from "crypto";
5171
- import { mkdir as mkdir6, readFile as readFile6, readdir as readdir3, rm as rm3, stat as stat4, writeFile as writeFile2 } from "fs/promises";
5414
+ import { mkdir as mkdir5, readFile as readFile6, readdir as readdir4, rm as rm3, stat as stat5, writeFile } from "fs/promises";
5172
5415
  import path9 from "path";
5173
5416
 
5174
5417
  // src/conversations/media.ts
@@ -5623,8 +5866,8 @@ async function writeConversationBlob(paths, conversationId, input, options) {
5623
5866
  }
5624
5867
  const id = `blob_${randomUUID3().replaceAll("-", "")}`;
5625
5868
  const filePath = blobPath(paths, id);
5626
- await mkdir6(path9.dirname(filePath), { recursive: true, mode: 448 });
5627
- await writeFile2(filePath, input.bytes, { mode: 384 });
5869
+ await mkdir5(path9.dirname(filePath), { recursive: true, mode: 448 });
5870
+ await writeFile(filePath, input.bytes, { mode: 384 });
5628
5871
  const blob = {
5629
5872
  id,
5630
5873
  size: input.bytes.byteLength,
@@ -5651,7 +5894,7 @@ async function readConversationBlob(paths, conversationId, blobId) {
5651
5894
  const filePath = blobPath(paths, blobId);
5652
5895
  const manifest = await readConversationBlobManifest(paths, conversationId, blobId);
5653
5896
  const bytes = await readFile6(filePath).catch((error) => {
5654
- if (isNodeError5(error, "ENOENT")) {
5897
+ if (isNodeError6(error, "ENOENT")) {
5655
5898
  throw new LinkHttpError(404, "blob_not_found", "Blob was not found");
5656
5899
  }
5657
5900
  throw error;
@@ -5693,7 +5936,7 @@ async function deleteConversationBlobIfUnreferenced(paths, conversationId, blobI
5693
5936
  async function materializeConversationBlob(paths, conversationId, blobId, manifest) {
5694
5937
  const existingPath = manifest.materialized_path;
5695
5938
  if (existingPath) {
5696
- const exists = await stat4(existingPath).then((value) => value.isFile()).catch(() => false);
5939
+ const exists = await stat5(existingPath).then((value) => value.isFile()).catch(() => false);
5697
5940
  if (exists) {
5698
5941
  return existingPath;
5699
5942
  }
@@ -5703,8 +5946,8 @@ async function materializeConversationBlob(paths, conversationId, blobId, manife
5703
5946
  targetDir,
5704
5947
  materializedAttachmentFilename(blobId, manifest.filename ?? blobId)
5705
5948
  );
5706
- await mkdir6(targetDir, { recursive: true, mode: 448 });
5707
- await writeFile2(targetPath, await readFile6(blobPath(paths, blobId)), {
5949
+ await mkdir5(targetDir, { recursive: true, mode: 448 });
5950
+ await writeFile(targetPath, await readFile6(blobPath(paths, blobId)), {
5708
5951
  mode: 384
5709
5952
  });
5710
5953
  await writeJsonFile(`${blobPath(paths, blobId)}.json`, {
@@ -5733,11 +5976,11 @@ async function pruneConversationBlobReference(paths, conversationId, blobId) {
5733
5976
  }
5734
5977
  async function listConversationBlobIds(paths, conversationId) {
5735
5978
  assertValidConversationId(conversationId);
5736
- await mkdir6(paths.blobsDir, { recursive: true, mode: 448 });
5737
- const entries = await readdir3(paths.blobsDir, {
5979
+ await mkdir5(paths.blobsDir, { recursive: true, mode: 448 });
5980
+ const entries = await readdir4(paths.blobsDir, {
5738
5981
  withFileTypes: true
5739
5982
  }).catch((error) => {
5740
- if (isNodeError5(error, "ENOENT")) {
5983
+ if (isNodeError6(error, "ENOENT")) {
5741
5984
  return [];
5742
5985
  }
5743
5986
  throw error;
@@ -5771,12 +6014,12 @@ function conversationAttachmentsDir(paths, conversationId) {
5771
6014
  assertValidConversationId(conversationId);
5772
6015
  return path9.join(paths.conversationsDir, conversationId, "attachments");
5773
6016
  }
5774
- function isNodeError5(error, code) {
6017
+ function isNodeError6(error, code) {
5775
6018
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
5776
6019
  }
5777
6020
 
5778
6021
  // src/conversations/profile-runtime.ts
5779
- import { stat as stat5 } from "fs/promises";
6022
+ import { stat as stat6 } from "fs/promises";
5780
6023
  var PROFILE_NAME_PATTERN2 = /^[a-zA-Z0-9._-]{1,64}$/u;
5781
6024
  function normalizeProfileName2(profileName) {
5782
6025
  const value = profileName?.trim() || "default";
@@ -5917,8 +6160,8 @@ async function buildConversationRuntimeMetadata(paths, manifest, snapshot) {
5917
6160
  async function ensureProfileIdentityForRuntime(paths, profileName) {
5918
6161
  const profilePath = resolveHermesProfileDir(profileName);
5919
6162
  if (profileName !== "default") {
5920
- const exists = await stat5(profilePath).then((value) => value.isDirectory()).catch((error) => {
5921
- if (isNodeError6(error, "ENOENT")) {
6163
+ const exists = await stat6(profilePath).then((value) => value.isDirectory()).catch((error) => {
6164
+ if (isNodeError7(error, "ENOENT")) {
5922
6165
  return false;
5923
6166
  }
5924
6167
  throw error;
@@ -5944,7 +6187,7 @@ function displayNameForProfile(profile) {
5944
6187
  const custom = profile.displayName?.trim();
5945
6188
  return custom || fallbackProfileDisplayName(profile.profileName);
5946
6189
  }
5947
- function isNodeError6(error, code) {
6190
+ function isNodeError7(error, code) {
5948
6191
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
5949
6192
  }
5950
6193
 
@@ -6489,11 +6732,11 @@ function formatContextUsageLines(runtime) {
6489
6732
  }
6490
6733
 
6491
6734
  // src/conversations/delivery-staging.ts
6492
- import { mkdir as mkdir7, rm as rm4 } from "fs/promises";
6735
+ import { mkdir as mkdir6, rm as rm4 } from "fs/promises";
6493
6736
  import path10 from "path";
6494
6737
  async function prepareDeliveryStagingRunDir(paths, conversationId, runId) {
6495
6738
  const directory = deliveryStagingRunDir(paths, conversationId, runId);
6496
- await mkdir7(directory, { recursive: true, mode: 448 });
6739
+ await mkdir6(directory, { recursive: true, mode: 448 });
6497
6740
  return directory;
6498
6741
  }
6499
6742
  async function removeConversationDeliveryStaging(paths, conversationId) {
@@ -6852,7 +7095,7 @@ function isUsableLanIpv4(value) {
6852
7095
  }
6853
7096
 
6854
7097
  // src/hermes/session-title.ts
6855
- import { stat as stat6 } from "fs/promises";
7098
+ import { stat as stat7 } from "fs/promises";
6856
7099
  import path11 from "path";
6857
7100
  async function readHermesSessionTitle(sessionId, paths, profileName) {
6858
7101
  const trimmedSessionId = sessionId.trim();
@@ -6864,8 +7107,8 @@ async function readHermesSessionTitle(sessionId, paths, profileName) {
6864
7107
  resolveHermesProfileDir(resolvedProfileName),
6865
7108
  "state.db"
6866
7109
  );
6867
- const exists = await stat6(dbPath).then((value) => value.isFile()).catch((error) => {
6868
- if (isNodeError7(error, "ENOENT")) {
7110
+ const exists = await stat7(dbPath).then((value) => value.isFile()).catch((error) => {
7111
+ if (isNodeError8(error, "ENOENT")) {
6869
7112
  return false;
6870
7113
  }
6871
7114
  throw error;
@@ -6885,8 +7128,8 @@ async function readHermesCompressionTip(sessionId, paths, profileName) {
6885
7128
  resolveHermesProfileDir(resolvedProfileName),
6886
7129
  "state.db"
6887
7130
  );
6888
- const exists = await stat6(dbPath).then((value) => value.isFile()).catch((error) => {
6889
- if (isNodeError7(error, "ENOENT")) {
7131
+ const exists = await stat7(dbPath).then((value) => value.isFile()).catch((error) => {
7132
+ if (isNodeError8(error, "ENOENT")) {
6890
7133
  return false;
6891
7134
  }
6892
7135
  throw error;
@@ -6949,7 +7192,7 @@ function readCompressionTipFromStateDb(dbPath, sessionId) {
6949
7192
  db?.close();
6950
7193
  }
6951
7194
  }
6952
- function isNodeError7(error, code) {
7195
+ function isNodeError8(error, code) {
6953
7196
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
6954
7197
  }
6955
7198
  function isValidProfileName(value) {
@@ -8648,11 +8891,11 @@ function hydrateAgentEventBlocks(blocks, agentEvents) {
8648
8891
  // src/conversations/conversation-store.ts
8649
8892
  import {
8650
8893
  appendFile as appendFile2,
8651
- mkdir as mkdir8,
8652
- readdir as readdir4,
8894
+ mkdir as mkdir7,
8895
+ readdir as readdir5,
8653
8896
  readFile as readFile7,
8654
8897
  rm as rm5,
8655
- writeFile as writeFile3
8898
+ writeFile as writeFile2
8656
8899
  } from "fs/promises";
8657
8900
  import path12 from "path";
8658
8901
  var ConversationStore = class {
@@ -8661,14 +8904,14 @@ var ConversationStore = class {
8661
8904
  }
8662
8905
  paths;
8663
8906
  async ensureConversationsDir() {
8664
- await mkdir8(this.paths.conversationsDir, { recursive: true, mode: 448 });
8907
+ await mkdir7(this.paths.conversationsDir, { recursive: true, mode: 448 });
8665
8908
  }
8666
8909
  async listConversationIds() {
8667
8910
  await this.ensureConversationsDir();
8668
- const entries = await readdir4(this.paths.conversationsDir, {
8911
+ const entries = await readdir5(this.paths.conversationsDir, {
8669
8912
  withFileTypes: true
8670
8913
  }).catch((error) => {
8671
- if (isNodeError8(error, "ENOENT")) {
8914
+ if (isNodeError9(error, "ENOENT")) {
8672
8915
  return [];
8673
8916
  }
8674
8917
  throw error;
@@ -8676,7 +8919,7 @@ var ConversationStore = class {
8676
8919
  return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
8677
8920
  }
8678
8921
  async createConversation(manifest, snapshot = createEmptySnapshot2()) {
8679
- await mkdir8(this.conversationDir(manifest.id), {
8922
+ await mkdir7(this.conversationDir(manifest.id), {
8680
8923
  recursive: true,
8681
8924
  mode: 448
8682
8925
  });
@@ -8716,7 +8959,7 @@ var ConversationStore = class {
8716
8959
  conversation_id: conversationId,
8717
8960
  created_at: now
8718
8961
  };
8719
- await mkdir8(this.conversationDir(conversationId), {
8962
+ await mkdir7(this.conversationDir(conversationId), {
8720
8963
  recursive: true,
8721
8964
  mode: 448
8722
8965
  });
@@ -8737,7 +8980,7 @@ var ConversationStore = class {
8737
8980
  await this.readManifest(conversationId);
8738
8981
  const raw = await readFile7(this.eventsPath(conversationId), "utf8").catch(
8739
8982
  (error) => {
8740
- if (isNodeError8(error, "ENOENT")) {
8983
+ if (isNodeError9(error, "ENOENT")) {
8741
8984
  return "";
8742
8985
  }
8743
8986
  throw error;
@@ -8747,7 +8990,7 @@ var ConversationStore = class {
8747
8990
  }
8748
8991
  overwriteEvents(conversationId, events) {
8749
8992
  const content = events.map((event) => JSON.stringify(event)).join("\n");
8750
- return writeFile3(
8993
+ return writeFile2(
8751
8994
  this.eventsPath(conversationId),
8752
8995
  content ? `${content}
8753
8996
  ` : "",
@@ -8792,18 +9035,18 @@ var ConversationStore = class {
8792
9035
  function createEmptySnapshot2() {
8793
9036
  return { schema_version: 1, messages: [], runs: [] };
8794
9037
  }
8795
- function isNodeError8(error, code) {
9038
+ function isNodeError9(error, code) {
8796
9039
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
8797
9040
  }
8798
9041
 
8799
9042
  // src/conversations/hermes-session-sync.ts
8800
9043
  import { randomUUID as randomUUID6 } from "crypto";
8801
- import { readdir as readdir6, readFile as readFile9, stat as stat8 } from "fs/promises";
9044
+ import { readdir as readdir7, readFile as readFile9, stat as stat9 } from "fs/promises";
8802
9045
  import os4 from "os";
8803
9046
  import path14 from "path";
8804
9047
 
8805
9048
  // src/conversations/delivery-import.ts
8806
- import { lstat, readFile as readFile8, readdir as readdir5, stat as stat7 } from "fs/promises";
9049
+ import { lstat as lstat2, readFile as readFile8, readdir as readdir6, stat as stat8 } from "fs/promises";
8807
9050
  import path13 from "path";
8808
9051
  var MAX_IMPORTED_BLOB_BYTES = 100 * 1024 * 1024;
8809
9052
  var MAX_MEDIA_IMPORT_FAILURES = 20;
@@ -8899,8 +9142,8 @@ function resolveDeliveryStagingTarget(paths, stagingDir) {
8899
9142
  };
8900
9143
  }
8901
9144
  async function collectStagedDeliveryReferences(stagingDir) {
8902
- const directoryStat = await lstat(stagingDir).catch((error) => {
8903
- if (isNodeError9(error, "ENOENT")) {
9145
+ const directoryStat = await lstat2(stagingDir).catch((error) => {
9146
+ if (isNodeError10(error, "ENOENT")) {
8904
9147
  throw new LinkHttpError(
8905
9148
  404,
8906
9149
  "delivery_staging_not_found",
@@ -8916,7 +9159,7 @@ async function collectStagedDeliveryReferences(stagingDir) {
8916
9159
  "delivery staging path is not a directory"
8917
9160
  );
8918
9161
  }
8919
- const entries = await readdir5(stagingDir, { withFileTypes: true });
9162
+ const entries = await readdir6(stagingDir, { withFileTypes: true });
8920
9163
  return entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).filter((entry) => isSupportedDeliveryFilename(entry.name)).sort(
8921
9164
  (left, right) => left.name.localeCompare(right.name, "en", { numeric: true })
8922
9165
  ).slice(0, MAX_DELIVERY_FILES).map((entry) => {
@@ -9080,8 +9323,8 @@ function emptyImportResult(input) {
9080
9323
  }
9081
9324
  async function writeBlobFromFile(deps, conversationId, source) {
9082
9325
  const sourcePath = resolveMediaSourcePath(source.path);
9083
- const fileStat = await stat7(sourcePath).catch((error) => {
9084
- if (isNodeError9(error, "ENOENT")) {
9326
+ const fileStat = await stat8(sourcePath).catch((error) => {
9327
+ if (isNodeError10(error, "ENOENT")) {
9085
9328
  throw new LinkHttpError(
9086
9329
  404,
9087
9330
  "media_source_not_found",
@@ -9115,7 +9358,7 @@ function describeMediaImportFailure(reference, sourceKey, error) {
9115
9358
  key: sourceKey,
9116
9359
  filename: sanitizeFilename(reference.path, "attachment"),
9117
9360
  reason: error instanceof Error ? error.message : String(error),
9118
- ...isNodeError9(error) && error.code ? { code: error.code } : {}
9361
+ ...isNodeError10(error) && error.code ? { code: error.code } : {}
9119
9362
  };
9120
9363
  }
9121
9364
  function isSupportedDeliveryFilename(filename) {
@@ -9128,7 +9371,7 @@ function readString8(payload, key) {
9128
9371
  function toRecord7(value) {
9129
9372
  return typeof value === "object" && value !== null ? value : {};
9130
9373
  }
9131
- function isNodeError9(error, code) {
9374
+ function isNodeError10(error, code) {
9132
9375
  return typeof error === "object" && error !== null && "code" in error && (code === void 0 || error.code === code);
9133
9376
  }
9134
9377
 
@@ -9895,9 +10138,9 @@ function rememberKnownHermesConversation(map, sessionId, conversationId) {
9895
10138
  async function discoverHermesProfileNames() {
9896
10139
  const names = /* @__PURE__ */ new Set([DEFAULT_PROFILE_NAME]);
9897
10140
  const profilesDir = path14.join(os4.homedir(), ".hermes", "profiles");
9898
- const entries = await readdir6(profilesDir, { withFileTypes: true }).catch(
10141
+ const entries = await readdir7(profilesDir, { withFileTypes: true }).catch(
9899
10142
  (error) => {
9900
- if (isNodeError10(error, "ENOENT")) {
10143
+ if (isNodeError11(error, "ENOENT")) {
9901
10144
  return [];
9902
10145
  }
9903
10146
  throw error;
@@ -10098,7 +10341,7 @@ async function readJsonlMessages(profileName, sessionId) {
10098
10341
  const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path14.join(profileDir, "sessions"));
10099
10342
  const transcriptPath = path14.join(sessionsDir, `${sessionId}.jsonl`);
10100
10343
  const raw = await readFile9(transcriptPath, "utf8").catch((error) => {
10101
- if (isNodeError10(error, "ENOENT")) {
10344
+ if (isNodeError11(error, "ENOENT")) {
10102
10345
  return "";
10103
10346
  }
10104
10347
  throw error;
@@ -10373,8 +10616,8 @@ function quoteIdentifier(value) {
10373
10616
  return `"${value.replaceAll('"', '""')}"`;
10374
10617
  }
10375
10618
  async function isFile(filePath) {
10376
- return stat8(filePath).then((value) => value.isFile()).catch((error) => {
10377
- if (isNodeError10(error, "ENOENT")) {
10619
+ return stat9(filePath).then((value) => value.isFile()).catch((error) => {
10620
+ if (isNodeError11(error, "ENOENT")) {
10378
10621
  return false;
10379
10622
  }
10380
10623
  throw error;
@@ -10407,12 +10650,12 @@ function readBoolean(value) {
10407
10650
  }
10408
10651
  return false;
10409
10652
  }
10410
- function isNodeError10(error, code) {
10653
+ function isNodeError11(error, code) {
10411
10654
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
10412
10655
  }
10413
10656
 
10414
10657
  // src/conversations/run-lifecycle.ts
10415
- import { readdir as readdir7 } from "fs/promises";
10658
+ import { readdir as readdir8 } from "fs/promises";
10416
10659
 
10417
10660
  // src/hermes/api-server.ts
10418
10661
  async function listHermesModels(options = {}) {
@@ -10525,7 +10768,7 @@ async function createHermesRun(input, options = {}) {
10525
10768
  );
10526
10769
  if (response.status === 404 || response.status === 503) {
10527
10770
  assertHermesRunsApiSupported(
10528
- await readHermesVersion().catch(() => null),
10771
+ await readHermesVersion({ logger: options.logger }).catch(() => null),
10529
10772
  response.status
10530
10773
  );
10531
10774
  throw new LinkHttpError(
@@ -10552,7 +10795,7 @@ async function streamHermesRunEvents(runId, options = {}) {
10552
10795
  options
10553
10796
  );
10554
10797
  assertHermesRunsApiSupported(
10555
- await readHermesVersion().catch(() => null),
10798
+ await readHermesVersion({ logger: options.logger }).catch(() => null),
10556
10799
  response.status
10557
10800
  );
10558
10801
  if (!response.ok || !response.body) {
@@ -10595,7 +10838,7 @@ async function streamHermesResponses(input, options = {}) {
10595
10838
  );
10596
10839
  if (response.status === 404 || response.status === 503) {
10597
10840
  assertHermesRunsApiSupported(
10598
- await readHermesVersion().catch(() => null),
10841
+ await readHermesVersion({ logger: options.logger }).catch(() => null),
10599
10842
  response.status
10600
10843
  );
10601
10844
  throw new LinkHttpError(
@@ -10806,7 +11049,7 @@ function readString10(payload, key) {
10806
11049
  }
10807
11050
 
10808
11051
  // src/conversations/history-builder.ts
10809
- import { readFile as readFile10, stat as stat9 } from "fs/promises";
11052
+ import { readFile as readFile10, stat as stat10 } from "fs/promises";
10810
11053
  import path15 from "path";
10811
11054
  var HISTORY_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
10812
11055
  var HERMES_HISTORY_COLUMNS = [
@@ -10914,8 +11157,8 @@ async function readHermesTranscriptHistory(sessionId, profileName) {
10914
11157
  };
10915
11158
  }
10916
11159
  async function readHermesStateDbHistory(dbPath, sessionId) {
10917
- const exists = await stat9(dbPath).then((value) => value.isFile()).catch((error) => {
10918
- if (isNodeError11(error, "ENOENT")) {
11160
+ const exists = await stat10(dbPath).then((value) => value.isFile()).catch((error) => {
11161
+ if (isNodeError12(error, "ENOENT")) {
10919
11162
  return false;
10920
11163
  }
10921
11164
  throw error;
@@ -10934,7 +11177,7 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
10934
11177
  }
10935
11178
  const transcriptPath = path15.join(sessionsDir, `${sessionId}.jsonl`);
10936
11179
  const raw = await readFile10(transcriptPath, "utf8").catch((error) => {
10937
- if (isNodeError11(error, "ENOENT")) {
11180
+ if (isNodeError12(error, "ENOENT")) {
10938
11181
  return "";
10939
11182
  }
10940
11183
  throw error;
@@ -11156,7 +11399,7 @@ function readTableColumns2(db, table) {
11156
11399
  db.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name).filter((name) => typeof name === "string")
11157
11400
  );
11158
11401
  }
11159
- function isNodeError11(error, code) {
11402
+ function isNodeError12(error, code) {
11160
11403
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
11161
11404
  }
11162
11405
  function isValidProfileName2(value) {
@@ -11174,7 +11417,7 @@ function normalizeProfileForCompare(value) {
11174
11417
 
11175
11418
  // src/hermes/stt.ts
11176
11419
  import { execFile as execFile3 } from "child_process";
11177
- import { access as access2, readFile as readFile11, stat as stat10 } from "fs/promises";
11420
+ import { access as access2, readFile as readFile11, stat as stat11 } from "fs/promises";
11178
11421
  import path16 from "path";
11179
11422
  import { promisify as promisify3 } from "util";
11180
11423
  var execFileAsync3 = promisify3(execFile3);
@@ -11334,7 +11577,7 @@ async function resolveExecutablePath(command) {
11334
11577
  }
11335
11578
  async function isExecutableFile(filePath) {
11336
11579
  try {
11337
- const info = await stat10(filePath);
11580
+ const info = await stat11(filePath);
11338
11581
  if (!info.isFile()) {
11339
11582
  return false;
11340
11583
  }
@@ -11375,7 +11618,7 @@ async function findDevHermesAgentSource() {
11375
11618
  return null;
11376
11619
  }
11377
11620
  async function isDirectory(candidate) {
11378
- return stat10(candidate).then((info) => info.isDirectory()).catch(() => false);
11621
+ return stat11(candidate).then((info) => info.isDirectory()).catch(() => false);
11379
11622
  }
11380
11623
  function compactProcessOutput(value) {
11381
11624
  const compact = value.trim().replace(/\s+/gu, " ").slice(0, 500);
@@ -12619,8 +12862,8 @@ function formatFilenameList(filenames) {
12619
12862
  return remaining > 0 ? `${preview.join("\u3001")} \u7B49 ${filenames.length} \u4E2A` : preview.join("\u3001");
12620
12863
  }
12621
12864
  async function readdirWithDirs(directory) {
12622
- return readdir7(directory, { withFileTypes: true }).catch((error) => {
12623
- if (isNodeError12(error, "ENOENT")) {
12865
+ return readdir8(directory, { withFileTypes: true }).catch((error) => {
12866
+ if (isNodeError13(error, "ENOENT")) {
12624
12867
  return [];
12625
12868
  }
12626
12869
  throw error;
@@ -12850,7 +13093,7 @@ function readResponseId(payload) {
12850
13093
  const response = toRecord11(payload.response);
12851
13094
  return readString13(payload, "response_id") ?? readString13(response, "id");
12852
13095
  }
12853
- function isNodeError12(error, code) {
13096
+ function isNodeError13(error, code) {
12854
13097
  if (typeof error !== "object" || error === null || !("code" in error)) {
12855
13098
  return false;
12856
13099
  }
@@ -13686,7 +13929,7 @@ function findApproval(snapshot, approvalId) {
13686
13929
 
13687
13930
  // src/identity/identity.ts
13688
13931
  import { generateKeyPairSync, randomUUID as randomUUID8, sign } from "crypto";
13689
- import { mkdir as mkdir9, chmod } from "fs/promises";
13932
+ import { mkdir as mkdir8, chmod as chmod2 } from "fs/promises";
13690
13933
  import { z } from "zod";
13691
13934
  var linkIdentitySchema = z.object({
13692
13935
  install_id: z.string().min(1),
@@ -13708,8 +13951,8 @@ async function ensureIdentity(paths = resolveRuntimePaths()) {
13708
13951
  if (existing) {
13709
13952
  return existing;
13710
13953
  }
13711
- await mkdir9(paths.homeDir, { recursive: true, mode: 448 });
13712
- await chmod(paths.homeDir, 448).catch(() => void 0);
13954
+ await mkdir8(paths.homeDir, { recursive: true, mode: 448 });
13955
+ await chmod2(paths.homeDir, 448).catch(() => void 0);
13713
13956
  const { publicKey, privateKey } = generateKeyPairSync("ed25519");
13714
13957
  const now = (/* @__PURE__ */ new Date()).toISOString();
13715
13958
  const identity = {
@@ -14903,7 +15146,7 @@ function createHttpErrorMiddleware(logger) {
14903
15146
  }
14904
15147
 
14905
15148
  // src/hermes/profiles.ts
14906
- import { mkdir as mkdir10, readdir as readdir8, readFile as readFile12, rename as rename4, rm as rm6, stat as stat11 } from "fs/promises";
15149
+ import { readdir as readdir9, readFile as readFile12, rename as rename3, rm as rm6, stat as stat12 } from "fs/promises";
14907
15150
  import os5 from "os";
14908
15151
  import path17 from "path";
14909
15152
  import YAML2 from "yaml";
@@ -14913,9 +15156,9 @@ async function listHermesProfiles(paths = resolveRuntimePaths()) {
14913
15156
  const profiles = /* @__PURE__ */ new Map();
14914
15157
  profiles.set(DEFAULT_PROFILE, await profileInfo(DEFAULT_PROFILE, paths));
14915
15158
  const profilesDir = path17.join(os5.homedir(), ".hermes", "profiles");
14916
- const entries = await readdir8(profilesDir, { withFileTypes: true }).catch(
15159
+ const entries = await readdir9(profilesDir, { withFileTypes: true }).catch(
14917
15160
  (error) => {
14918
- if (isNodeError13(error, "ENOENT")) {
15161
+ if (isNodeError14(error, "ENOENT")) {
14919
15162
  return [];
14920
15163
  }
14921
15164
  throw error;
@@ -14939,8 +15182,8 @@ async function listHermesProfiles(paths = resolveRuntimePaths()) {
14939
15182
  async function getHermesProfileStatus(name, paths = resolveRuntimePaths()) {
14940
15183
  assertProfileName(name);
14941
15184
  const profile = await profileInfo(name, paths);
14942
- const exists = await stat11(profile.path).then((value) => value.isDirectory()).catch((error) => {
14943
- if (isNodeError13(error, "ENOENT")) {
15185
+ const exists = await stat12(profile.path).then((value) => value.isDirectory()).catch((error) => {
15186
+ if (isNodeError14(error, "ENOENT")) {
14944
15187
  return false;
14945
15188
  }
14946
15189
  throw error;
@@ -14957,7 +15200,7 @@ async function renameHermesProfile(oldName, newName, paths = resolveRuntimePaths
14957
15200
  assertMutableProfile(newName);
14958
15201
  const oldProfile = await profileInfo(oldName, paths);
14959
15202
  const newProfilePath = resolveHermesProfileDir(newName);
14960
- await rename4(oldProfile.path, newProfilePath);
15203
+ await rename3(oldProfile.path, newProfilePath);
14961
15204
  const identity = await renameProfileIdentity(paths, {
14962
15205
  oldProfileName: oldName,
14963
15206
  newProfileName: newName,
@@ -14980,8 +15223,8 @@ async function updateHermesProfileMetadata(name, metadata, paths = resolveRuntim
14980
15223
  async function deleteHermesProfile(name, paths = resolveRuntimePaths()) {
14981
15224
  assertMutableProfile(name);
14982
15225
  const profile = await profileInfo(name, paths);
14983
- const exists = await stat11(profile.path).then((value) => value.isDirectory()).catch((error) => {
14984
- if (isNodeError13(error, "ENOENT")) {
15226
+ const exists = await stat12(profile.path).then((value) => value.isDirectory()).catch((error) => {
15227
+ if (isNodeError14(error, "ENOENT")) {
14985
15228
  return false;
14986
15229
  }
14987
15230
  throw error;
@@ -15049,13 +15292,13 @@ function assertProfileName(name) {
15049
15292
  throw new LinkHttpError(400, "invalid_profile_name", "invalid profile name");
15050
15293
  }
15051
15294
  }
15052
- function isNodeError13(error, code) {
15295
+ function isNodeError14(error, code) {
15053
15296
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
15054
15297
  }
15055
15298
  async function countSkills(root) {
15056
- const entries = await readdir8(root, { withFileTypes: true }).catch(
15299
+ const entries = await readdir9(root, { withFileTypes: true }).catch(
15057
15300
  (error) => {
15058
- if (isNodeError13(error, "ENOENT")) {
15301
+ if (isNodeError14(error, "ENOENT")) {
15059
15302
  return [];
15060
15303
  }
15061
15304
  throw error;
@@ -15082,7 +15325,7 @@ async function countConfiguredTools(profileName) {
15082
15325
  resolveHermesConfigPath(profileName),
15083
15326
  "utf8"
15084
15327
  ).catch((error) => {
15085
- if (isNodeError13(error, "ENOENT")) {
15328
+ if (isNodeError14(error, "ENOENT")) {
15086
15329
  return "";
15087
15330
  }
15088
15331
  throw error;
@@ -15759,14 +16002,11 @@ function errorMessage(error) {
15759
16002
  import { spawn as spawn2 } from "child_process";
15760
16003
  import { EventEmitter as EventEmitter2 } from "events";
15761
16004
  import {
15762
- copyFile as copyFile2,
15763
16005
  cp,
15764
- mkdir as mkdir11,
16006
+ mkdir as mkdir9,
15765
16007
  readFile as readFile13,
15766
16008
  rm as rm7,
15767
- stat as stat12,
15768
- writeFile as writeFile4,
15769
- rename as rename5
16009
+ stat as stat13
15770
16010
  } from "fs/promises";
15771
16011
  import path18 from "path";
15772
16012
  import YAML3 from "yaml";
@@ -15816,7 +16056,7 @@ async function startHermesProfileCreation(input, options) {
15816
16056
  signal: null,
15817
16057
  error: null
15818
16058
  };
15819
- await mkdir11(options.paths.runDir, { recursive: true, mode: 448 });
16059
+ await mkdir9(options.paths.runDir, { recursive: true, mode: 448 });
15820
16060
  await writeProfileCreationState(options.paths, started);
15821
16061
  await writer.write(`
15822
16062
  === profile creation started ${startedAt} ===
@@ -16093,7 +16333,7 @@ function randomSuffix(attempt) {
16093
16333
  async function applyProfileCreationPostSteps(input) {
16094
16334
  const profilePath = resolveHermesProfileDir(input.profileName);
16095
16335
  if (!await pathExists(profilePath)) {
16096
- await mkdir11(profilePath, { recursive: true, mode: 448 });
16336
+ await ensureDirectoryWithInheritedMetadata(profilePath, 448);
16097
16337
  }
16098
16338
  if (input.sourceProfile && input.copyScopes.length > 0) {
16099
16339
  await copyProfileScopes({
@@ -16221,7 +16461,7 @@ function collectEnvKeys(value, keys = /* @__PURE__ */ new Set()) {
16221
16461
  async function writeEnvValues(profileName, values) {
16222
16462
  const envPath = path18.join(resolveHermesProfileDir(profileName), ".env");
16223
16463
  const existingRaw = await readFile13(envPath, "utf8").catch((error) => {
16224
- if (isNodeError14(error, "ENOENT")) {
16464
+ if (isNodeError15(error, "ENOENT")) {
16225
16465
  return "";
16226
16466
  }
16227
16467
  throw error;
@@ -16246,13 +16486,14 @@ async function writeEnvValues(profileName, values) {
16246
16486
  nextLines.push(`${key}=${formatEnvValue2(value)}`);
16247
16487
  }
16248
16488
  const nextRaw = nextLines.join("\n").replace(/\n*$/u, "\n");
16249
- await mkdir11(path18.dirname(envPath), { recursive: true, mode: 448 });
16250
16489
  if (existingRaw) {
16251
- await copyFile2(envPath, `${envPath}.bak.${Date.now()}`);
16490
+ await atomicWriteFilePreservingMetadata(
16491
+ `${envPath}.bak.${Date.now()}`,
16492
+ existingRaw,
16493
+ { metadataSourcePath: envPath }
16494
+ );
16252
16495
  }
16253
- const tempPath = `${envPath}.tmp.${process.pid}.${Date.now()}`;
16254
- await writeFile4(tempPath, nextRaw, { mode: 384 });
16255
- await rename5(tempPath, envPath);
16496
+ await atomicWriteFilePreservingMetadata(envPath, nextRaw);
16256
16497
  }
16257
16498
  async function copySkills(sourceProfile, targetProfile) {
16258
16499
  const sourceSkills = path18.join(resolveHermesProfileDir(sourceProfile), "skills");
@@ -16266,6 +16507,10 @@ async function copySkills(sourceProfile, targetProfile) {
16266
16507
  force: true,
16267
16508
  errorOnExist: false
16268
16509
  });
16510
+ await inheritOwnerRecursively(
16511
+ targetSkills,
16512
+ resolveHermesProfileDir(targetProfile)
16513
+ );
16269
16514
  }
16270
16515
  function copyProperty(source, target, key) {
16271
16516
  if (Object.prototype.hasOwnProperty.call(source, key)) {
@@ -16277,7 +16522,7 @@ function copyProperty(source, target, key) {
16277
16522
  async function readYamlConfig(configPath) {
16278
16523
  const existingRaw = await readFile13(configPath, "utf8").catch(
16279
16524
  (error) => {
16280
- if (isNodeError14(error, "ENOENT")) {
16525
+ if (isNodeError15(error, "ENOENT")) {
16281
16526
  return null;
16282
16527
  }
16283
16528
  throw error;
@@ -16289,14 +16534,15 @@ async function readYamlConfig(configPath) {
16289
16534
  };
16290
16535
  }
16291
16536
  async function writeYamlConfig(configPath, input) {
16292
- await mkdir11(path18.dirname(configPath), { recursive: true, mode: 448 });
16293
16537
  if (input.existingRaw) {
16294
- await copyFile2(configPath, `${configPath}.bak.${Date.now()}`);
16538
+ await atomicWriteFilePreservingMetadata(
16539
+ `${configPath}.bak.${Date.now()}`,
16540
+ input.existingRaw,
16541
+ { metadataSourcePath: configPath }
16542
+ );
16295
16543
  }
16296
16544
  const document = new YAML3.Document(input.config);
16297
- const tempPath = `${configPath}.tmp.${process.pid}.${Date.now()}`;
16298
- await writeFile4(tempPath, document.toString(), { mode: 384 });
16299
- await rename5(tempPath, configPath);
16545
+ await atomicWriteFilePreservingMetadata(configPath, document.toString());
16300
16546
  }
16301
16547
  async function failProfileCreation(input) {
16302
16548
  if (input.rollbackProfileName) {
@@ -16362,8 +16608,8 @@ async function clearProfileCreationLogFiles(paths) {
16362
16608
  ]);
16363
16609
  }
16364
16610
  async function pathExists(targetPath) {
16365
- return await stat12(targetPath).then(() => true).catch((error) => {
16366
- if (isNodeError14(error, "ENOENT")) {
16611
+ return await stat13(targetPath).then(() => true).catch((error) => {
16612
+ if (isNodeError15(error, "ENOENT")) {
16367
16613
  return false;
16368
16614
  }
16369
16615
  throw error;
@@ -16410,7 +16656,7 @@ function formatEnvValue2(value) {
16410
16656
  function escapeRegExp2(value) {
16411
16657
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
16412
16658
  }
16413
- function isNodeError14(error, code) {
16659
+ function isNodeError15(error, code) {
16414
16660
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
16415
16661
  }
16416
16662
 
@@ -16619,13 +16865,9 @@ function toProfileToolConfigHttpError(error) {
16619
16865
  // src/hermes/memory.ts
16620
16866
  import {
16621
16867
  access as access3,
16622
- copyFile as copyFile3,
16623
- mkdir as mkdir12,
16624
- readdir as readdir9,
16868
+ readdir as readdir10,
16625
16869
  readFile as readFile14,
16626
- rename as rename6,
16627
- stat as stat13,
16628
- writeFile as writeFile5
16870
+ stat as stat14
16629
16871
  } from "fs/promises";
16630
16872
  import path19 from "path";
16631
16873
  import YAML4 from "yaml";
@@ -16938,12 +17180,11 @@ function isSensitiveConfigKey(key) {
16938
17180
  }
16939
17181
  async function writeCustomProviderConfig(profileName, provider, config) {
16940
17182
  const configPath = customProviderConfigPath(profileName, provider);
16941
- await mkdir12(path19.dirname(configPath), { recursive: true, mode: 448 });
16942
- await writeFile5(configPath, `${JSON.stringify(config, null, 2)}
16943
- `, {
16944
- encoding: "utf8",
16945
- mode: 384
16946
- });
17183
+ await atomicWriteFilePreservingMetadata(
17184
+ configPath,
17185
+ `${JSON.stringify(config, null, 2)}
17186
+ `
17187
+ );
16947
17188
  }
16948
17189
  function normalizeConfigurableProvider(provider, patch) {
16949
17190
  if (provider === CUSTOM_PROVIDER_CARD_ID) {
@@ -16991,7 +17232,7 @@ async function patchHermesMemoryProvider(profileName, provider) {
16991
17232
  const configPath = resolveHermesConfigPath(profileName);
16992
17233
  const existingRaw = await readFile14(configPath, "utf8").catch(
16993
17234
  (error) => {
16994
- if (isNodeError15(error, "ENOENT")) {
17235
+ if (isNodeError16(error, "ENOENT")) {
16995
17236
  return null;
16996
17237
  }
16997
17238
  throw error;
@@ -17003,17 +17244,13 @@ async function patchHermesMemoryProvider(profileName, provider) {
17003
17244
  memory.provider = provider === "built-in" ? "" : provider;
17004
17245
  config.memory = memory;
17005
17246
  const backupPath = existingRaw ? `${configPath}.bak.${Date.now()}` : null;
17006
- await mkdir12(path19.dirname(configPath), { recursive: true, mode: 448 });
17007
- if (backupPath) {
17008
- await copyFile3(configPath, backupPath);
17247
+ if (backupPath && existingRaw !== null) {
17248
+ await atomicWriteFilePreservingMetadata(backupPath, existingRaw, {
17249
+ metadataSourcePath: configPath
17250
+ });
17009
17251
  }
17010
17252
  document.contents = document.createNode(config);
17011
- const tempPath = `${configPath}.tmp.${process.pid}.${Date.now()}`;
17012
- await writeFile5(tempPath, document.toString(), {
17013
- encoding: "utf8",
17014
- mode: 384
17015
- });
17016
- await rename6(tempPath, configPath);
17253
+ await atomicWriteFilePreservingMetadata(configPath, document.toString());
17017
17254
  }
17018
17255
  function resolveMemoryDir(profileName) {
17019
17256
  return path19.join(resolveHermesProfileDir(profileName), "memories");
@@ -17021,8 +17258,8 @@ function resolveMemoryDir(profileName) {
17021
17258
  async function readMemoryStore(profileName, target, limits) {
17022
17259
  const filePath = memoryFilePath(profileName, target);
17023
17260
  const entries = await readMemoryEntries(filePath);
17024
- const fileStat = await stat13(filePath).catch((error) => {
17025
- if (isNodeError15(error, "ENOENT")) {
17261
+ const fileStat = await stat14(filePath).catch((error) => {
17262
+ if (isNodeError16(error, "ENOENT")) {
17026
17263
  return null;
17027
17264
  }
17028
17265
  throw error;
@@ -17051,7 +17288,7 @@ async function readMemoryStore(profileName, target, limits) {
17051
17288
  }
17052
17289
  async function readMemoryEntries(filePath) {
17053
17290
  const raw = await readFile14(filePath, "utf8").catch((error) => {
17054
- if (isNodeError15(error, "ENOENT")) {
17291
+ if (isNodeError16(error, "ENOENT")) {
17055
17292
  return "";
17056
17293
  }
17057
17294
  throw error;
@@ -17071,17 +17308,10 @@ async function mutateMemoryEntries(profileName, target, mutate) {
17071
17308
  async function writeMemoryEntries(profileName, target, entries) {
17072
17309
  assertWithinLimit(target, entries, await readMemoryLimits(profileName));
17073
17310
  const filePath = memoryFilePath(profileName, target);
17074
- const dir = path19.dirname(filePath);
17075
- await mkdir12(dir, { recursive: true, mode: 448 });
17076
- const tempPath = path19.join(
17077
- dir,
17078
- `.mem_${process.pid}_${Date.now()}_${target}.tmp`
17311
+ await atomicWriteFilePreservingMetadata(
17312
+ filePath,
17313
+ entries.join(ENTRY_DELIMITER)
17079
17314
  );
17080
- await writeFile5(tempPath, entries.join(ENTRY_DELIMITER), {
17081
- encoding: "utf8",
17082
- mode: 384
17083
- });
17084
- await rename6(tempPath, filePath);
17085
17315
  }
17086
17316
  function memoryFilePath(profileName, target) {
17087
17317
  return path19.join(
@@ -17562,7 +17792,7 @@ function customProviderRegistryPath(profileName) {
17562
17792
  async function readCustomProviderRegistry(profileName) {
17563
17793
  const raw = await readFile14(customProviderRegistryPath(profileName), "utf8").catch(
17564
17794
  (error) => {
17565
- if (isNodeError15(error, "ENOENT")) {
17795
+ if (isNodeError16(error, "ENOENT")) {
17566
17796
  return "";
17567
17797
  }
17568
17798
  throw error;
@@ -17599,19 +17829,17 @@ async function saveCustomProviderRegistryEntry(profileName, provider) {
17599
17829
  { id: providerId, label: providerId, description: "\u81EA\u5B9A\u4E49 memory provider\u3002" }
17600
17830
  ].sort((left, right) => left.id.localeCompare(right.id));
17601
17831
  const registryPath2 = customProviderRegistryPath(profileName);
17602
- await mkdir12(path19.dirname(registryPath2), { recursive: true, mode: 448 });
17603
- await writeFile5(
17832
+ await atomicWriteFilePreservingMetadata(
17604
17833
  registryPath2,
17605
17834
  `${JSON.stringify({ providers }, null, 2)}
17606
- `,
17607
- { encoding: "utf8", mode: 384 }
17835
+ `
17608
17836
  );
17609
17837
  }
17610
17838
  async function discoverUserMemoryProviderDescriptors(profileName) {
17611
17839
  const pluginsDir = path19.join(resolveHermesProfileDir(profileName), "plugins");
17612
- const entries = await readdir9(pluginsDir, { withFileTypes: true }).catch(
17840
+ const entries = await readdir10(pluginsDir, { withFileTypes: true }).catch(
17613
17841
  (error) => {
17614
- if (isNodeError15(error, "ENOENT")) {
17842
+ if (isNodeError16(error, "ENOENT")) {
17615
17843
  return [];
17616
17844
  }
17617
17845
  throw error;
@@ -17652,7 +17880,7 @@ async function isUserMemoryProviderInstalled(profileName, provider) {
17652
17880
  async function isMemoryProviderPluginDir(providerDir) {
17653
17881
  const source = await readFile14(path19.join(providerDir, "__init__.py"), "utf8").catch(
17654
17882
  (error) => {
17655
- if (isNodeError15(error, "ENOENT")) {
17883
+ if (isNodeError16(error, "ENOENT")) {
17656
17884
  return "";
17657
17885
  }
17658
17886
  throw error;
@@ -17664,7 +17892,7 @@ async function isMemoryProviderPluginDir(providerDir) {
17664
17892
  async function readPluginMetadata(providerDir) {
17665
17893
  const raw = await readFile14(path19.join(providerDir, "plugin.yaml"), "utf8").catch(
17666
17894
  (error) => {
17667
- if (isNodeError15(error, "ENOENT")) {
17895
+ if (isNodeError16(error, "ENOENT")) {
17668
17896
  return "";
17669
17897
  }
17670
17898
  throw error;
@@ -17690,7 +17918,7 @@ async function resolveByteRoverCli() {
17690
17918
  async function readHolographicProviderConfig(profileName) {
17691
17919
  const raw = await readFile14(resolveHermesConfigPath(profileName), "utf8").catch(
17692
17920
  (error) => {
17693
- if (isNodeError15(error, "ENOENT")) {
17921
+ if (isNodeError16(error, "ENOENT")) {
17694
17922
  return "";
17695
17923
  }
17696
17924
  throw error;
@@ -17704,7 +17932,7 @@ async function patchHolographicProviderConfig(profileName, patch) {
17704
17932
  const configPath = resolveHermesConfigPath(profileName);
17705
17933
  const existingRaw = await readFile14(configPath, "utf8").catch(
17706
17934
  (error) => {
17707
- if (isNodeError15(error, "ENOENT")) {
17935
+ if (isNodeError16(error, "ENOENT")) {
17708
17936
  return null;
17709
17937
  }
17710
17938
  throw error;
@@ -17721,17 +17949,15 @@ async function patchHolographicProviderConfig(profileName, patch) {
17721
17949
  }
17722
17950
  plugins["hermes-memory-store"] = memoryStore;
17723
17951
  config.plugins = plugins;
17724
- await mkdir12(path19.dirname(configPath), { recursive: true, mode: 448 });
17725
17952
  if (existingRaw) {
17726
- await copyFile3(configPath, `${configPath}.bak.${Date.now()}`);
17953
+ await atomicWriteFilePreservingMetadata(
17954
+ `${configPath}.bak.${Date.now()}`,
17955
+ existingRaw,
17956
+ { metadataSourcePath: configPath }
17957
+ );
17727
17958
  }
17728
17959
  document.contents = document.createNode(config);
17729
- const tempPath = `${configPath}.tmp.${process.pid}.${Date.now()}`;
17730
- await writeFile5(tempPath, document.toString(), {
17731
- encoding: "utf8",
17732
- mode: 384
17733
- });
17734
- await rename6(tempPath, configPath);
17960
+ await atomicWriteFilePreservingMetadata(configPath, document.toString());
17735
17961
  }
17736
17962
  async function patchHermesMemoryEnv(profileName, patch) {
17737
17963
  const entries = Object.entries(patch).filter(
@@ -17742,7 +17968,7 @@ async function patchHermesMemoryEnv(profileName, patch) {
17742
17968
  }
17743
17969
  const envPath = path19.join(resolveHermesProfileDir(profileName), ".env");
17744
17970
  const existingRaw = await readFile14(envPath, "utf8").catch((error) => {
17745
- if (isNodeError15(error, "ENOENT")) {
17971
+ if (isNodeError16(error, "ENOENT")) {
17746
17972
  return "";
17747
17973
  }
17748
17974
  throw error;
@@ -17772,13 +17998,14 @@ async function patchHermesMemoryEnv(profileName, patch) {
17772
17998
  }
17773
17999
  }
17774
18000
  const nextRaw = nextLines.join("\n").replace(/\n*$/u, "\n");
17775
- await mkdir12(path19.dirname(envPath), { recursive: true, mode: 448 });
17776
18001
  if (existingRaw) {
17777
- await copyFile3(envPath, `${envPath}.bak.${Date.now()}`);
18002
+ await atomicWriteFilePreservingMetadata(
18003
+ `${envPath}.bak.${Date.now()}`,
18004
+ existingRaw,
18005
+ { metadataSourcePath: envPath }
18006
+ );
17778
18007
  }
17779
- const tempPath = `${envPath}.tmp.${process.pid}.${Date.now()}`;
17780
- await writeFile5(tempPath, nextRaw, { encoding: "utf8", mode: 384 });
17781
- await rename6(tempPath, envPath);
18008
+ await atomicWriteFilePreservingMetadata(envPath, nextRaw);
17782
18009
  }
17783
18010
  function isMemoryEnvKeyWritable(key) {
17784
18011
  return [
@@ -17799,7 +18026,7 @@ async function readActiveMemoryProvider(profileName) {
17799
18026
  resolveHermesConfigPath(profileName),
17800
18027
  "utf8"
17801
18028
  ).catch((error) => {
17802
- if (isNodeError15(error, "ENOENT")) {
18029
+ if (isNodeError16(error, "ENOENT")) {
17803
18030
  return "";
17804
18031
  }
17805
18032
  throw error;
@@ -17824,16 +18051,15 @@ async function patchJsonProviderConfig(profileName, relativePath, patch) {
17824
18051
  next[key] = value;
17825
18052
  }
17826
18053
  }
17827
- await mkdir12(path19.dirname(configPath), { recursive: true, mode: 448 });
17828
- await writeFile5(configPath, `${JSON.stringify(next, null, 2)}
17829
- `, {
17830
- encoding: "utf8",
17831
- mode: 384
17832
- });
18054
+ await atomicWriteFilePreservingMetadata(
18055
+ configPath,
18056
+ `${JSON.stringify(next, null, 2)}
18057
+ `
18058
+ );
17833
18059
  }
17834
18060
  async function readJsonObject(filePath) {
17835
18061
  const raw = await readFile14(filePath, "utf8").catch((error) => {
17836
- if (isNodeError15(error, "ENOENT")) {
18062
+ if (isNodeError16(error, "ENOENT")) {
17837
18063
  return "{}";
17838
18064
  }
17839
18065
  throw error;
@@ -17887,7 +18113,7 @@ async function readMemoryLimits(profileName) {
17887
18113
  resolveHermesConfigPath(profileName),
17888
18114
  "utf8"
17889
18115
  ).catch((error) => {
17890
- if (isNodeError15(error, "ENOENT")) {
18116
+ if (isNodeError16(error, "ENOENT")) {
17891
18117
  return "";
17892
18118
  }
17893
18119
  throw error;
@@ -17980,7 +18206,7 @@ function formatEnvValue3(value) {
17980
18206
  function escapeRegExp3(value) {
17981
18207
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
17982
18208
  }
17983
- function isNodeError15(error, code) {
18209
+ function isNodeError16(error, code) {
17984
18210
  return error instanceof Error && "code" in error && error.code === code;
17985
18211
  }
17986
18212
 
@@ -18388,14 +18614,7 @@ function toMemoryHttpError(error) {
18388
18614
  }
18389
18615
 
18390
18616
  // src/hermes/skills.ts
18391
- import {
18392
- copyFile as copyFile4,
18393
- mkdir as mkdir13,
18394
- readFile as readFile15,
18395
- readdir as readdir10,
18396
- rename as rename7,
18397
- writeFile as writeFile6
18398
- } from "fs/promises";
18617
+ import { readFile as readFile15, readdir as readdir11 } from "fs/promises";
18399
18618
  import path20 from "path";
18400
18619
  import YAML5 from "yaml";
18401
18620
  var HermesSkillNotFoundError = class extends Error {
@@ -18487,9 +18706,9 @@ async function findSkillFiles(root) {
18487
18706
  return results.sort((left, right) => left.localeCompare(right));
18488
18707
  }
18489
18708
  async function collectSkillFiles(directory, results) {
18490
- const entries = await readdir10(directory, { withFileTypes: true }).catch(
18709
+ const entries = await readdir11(directory, { withFileTypes: true }).catch(
18491
18710
  (error) => {
18492
- if (isNodeError16(error, "ENOENT")) {
18711
+ if (isNodeError17(error, "ENOENT")) {
18493
18712
  return [];
18494
18713
  }
18495
18714
  throw error;
@@ -18514,7 +18733,7 @@ async function collectSkillFiles(directory, results) {
18514
18733
  async function readSkillMetadata(input) {
18515
18734
  const raw = await readFile15(input.skillFile, "utf8").catch(
18516
18735
  (error) => {
18517
- if (isNodeError16(error, "ENOENT") || isNodeError16(error, "EACCES")) {
18736
+ if (isNodeError17(error, "ENOENT") || isNodeError17(error, "EACCES")) {
18518
18737
  return null;
18519
18738
  }
18520
18739
  throw error;
@@ -18591,7 +18810,7 @@ function normalizeDescription(value) {
18591
18810
  }
18592
18811
  async function readDisabledSkillNames(configPath) {
18593
18812
  const raw = await readFile15(configPath, "utf8").catch((error) => {
18594
- if (isNodeError16(error, "ENOENT")) {
18813
+ if (isNodeError17(error, "ENOENT")) {
18595
18814
  return "";
18596
18815
  }
18597
18816
  throw error;
@@ -18616,7 +18835,7 @@ async function readSkillProvenance(root) {
18616
18835
  async function readBundledSkillNames(root) {
18617
18836
  const raw = await readFile15(path20.join(root, ".bundled_manifest"), "utf8").catch(
18618
18837
  (error) => {
18619
- if (isNodeError16(error, "ENOENT")) {
18838
+ if (isNodeError17(error, "ENOENT")) {
18620
18839
  return "";
18621
18840
  }
18622
18841
  throw error;
@@ -18639,7 +18858,7 @@ async function readBundledSkillNames(root) {
18639
18858
  async function readHubInstalledSkills(root) {
18640
18859
  const raw = await readFile15(path20.join(root, ".hub", "lock.json"), "utf8").catch(
18641
18860
  (error) => {
18642
- if (isNodeError16(error, "ENOENT")) {
18861
+ if (isNodeError17(error, "ENOENT")) {
18643
18862
  return "";
18644
18863
  }
18645
18864
  throw error;
@@ -18711,7 +18930,7 @@ function compareCategoryNames(left, right) {
18711
18930
  async function readHermesConfigDocument2(configPath) {
18712
18931
  const existingRaw = await readFile15(configPath, "utf8").catch(
18713
18932
  (error) => {
18714
- if (isNodeError16(error, "ENOENT")) {
18933
+ if (isNodeError17(error, "ENOENT")) {
18715
18934
  return null;
18716
18935
  }
18717
18936
  throw error;
@@ -18726,14 +18945,16 @@ async function readHermesConfigDocument2(configPath) {
18726
18945
  }
18727
18946
  async function writeHermesConfigDocument2(input) {
18728
18947
  const backupPath = input.existingRaw ? `${input.configPath}.bak.${Date.now()}` : null;
18729
- await mkdir13(path20.dirname(input.configPath), { recursive: true, mode: 448 });
18730
18948
  if (backupPath) {
18731
- await copyFile4(input.configPath, backupPath);
18949
+ await atomicWriteFilePreservingMetadata(backupPath, input.existingRaw, {
18950
+ metadataSourcePath: input.configPath
18951
+ });
18732
18952
  }
18733
18953
  input.document.contents = input.document.createNode(input.config);
18734
- const tempPath = `${input.configPath}.tmp.${process.pid}.${Date.now()}`;
18735
- await writeFile6(tempPath, input.document.toString(), { mode: 384 });
18736
- await rename7(tempPath, input.configPath);
18954
+ await atomicWriteFilePreservingMetadata(
18955
+ input.configPath,
18956
+ input.document.toString()
18957
+ );
18737
18958
  return backupPath;
18738
18959
  }
18739
18960
  function readStringList3(value) {
@@ -18756,7 +18977,7 @@ function ensureRecord3(target, key) {
18756
18977
  target[key] = current;
18757
18978
  return current;
18758
18979
  }
18759
- function isNodeError16(error, code) {
18980
+ function isNodeError17(error, code) {
18760
18981
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
18761
18982
  }
18762
18983
 
@@ -19238,7 +19459,7 @@ function readModelList(payload) {
19238
19459
  // src/hermes/updates.ts
19239
19460
  import { EventEmitter as EventEmitter3 } from "events";
19240
19461
  import { spawn as spawn3 } from "child_process";
19241
- import { mkdir as mkdir14, readFile as readFile16, rm as rm8 } from "fs/promises";
19462
+ import { mkdir as mkdir10, readFile as readFile16, rm as rm8 } from "fs/promises";
19242
19463
  import path21 from "path";
19243
19464
  var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
19244
19465
  var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
@@ -19252,7 +19473,10 @@ var runningUpdate = null;
19252
19473
  async function readHermesUpdateCheck(options) {
19253
19474
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
19254
19475
  const [local, remoteResult] = await Promise.all([
19255
- readHermesVersion().catch((error) => ({
19476
+ readHermesVersion({
19477
+ fetchImpl: options.fetchImpl,
19478
+ logger: options.logger
19479
+ }).catch((error) => ({
19256
19480
  raw: error instanceof Error ? error.message : String(error),
19257
19481
  version: null
19258
19482
  })),
@@ -19299,7 +19523,7 @@ async function startHermesUpdate(options) {
19299
19523
  signal: null,
19300
19524
  error: null
19301
19525
  };
19302
- await mkdir14(options.paths.runDir, { recursive: true, mode: 448 });
19526
+ await mkdir10(options.paths.runDir, { recursive: true, mode: 448 });
19303
19527
  await writer.write(`
19304
19528
  === hermes update started ${startedAt} ===
19305
19529
  `);
@@ -19603,17 +19827,17 @@ function readString17(payload, key) {
19603
19827
  // src/link/updates.ts
19604
19828
  import { spawn as spawn5 } from "child_process";
19605
19829
  import { EventEmitter as EventEmitter4 } from "events";
19606
- import { mkdir as mkdir17, readFile as readFile18, rm as rm11 } from "fs/promises";
19830
+ import { mkdir as mkdir13, readFile as readFile18, rm as rm11 } from "fs/promises";
19607
19831
  import path23 from "path";
19608
19832
 
19609
19833
  // src/daemon/process.ts
19610
19834
  import { spawn as spawn4 } from "child_process";
19611
- import { mkdir as mkdir16, readFile as readFile17, rm as rm10 } from "fs/promises";
19835
+ import { mkdir as mkdir12, readFile as readFile17, rm as rm10 } from "fs/promises";
19612
19836
  import path22 from "path";
19613
19837
 
19614
19838
  // src/daemon/service.ts
19615
19839
  import { createServer } from "http";
19616
- import { mkdir as mkdir15, rm as rm9, writeFile as writeFile7 } from "fs/promises";
19840
+ import { mkdir as mkdir11, rm as rm9, writeFile as writeFile3 } from "fs/promises";
19617
19841
 
19618
19842
  // src/relay/control-client.ts
19619
19843
  import WebSocket from "ws";
@@ -20606,8 +20830,8 @@ function pidFilePath(paths = resolveRuntimePaths()) {
20606
20830
  return `${paths.runDir}/hermeslink.pid`;
20607
20831
  }
20608
20832
  async function writePidFile(paths) {
20609
- await mkdir15(paths.runDir, { recursive: true, mode: 448 });
20610
- await writeFile7(pidFilePath(paths), `${process.pid}
20833
+ await mkdir11(paths.runDir, { recursive: true, mode: 448 });
20834
+ await writeFile3(pidFilePath(paths), `${process.pid}
20611
20835
  `, { mode: 384 });
20612
20836
  }
20613
20837
  async function closeServer(server) {
@@ -20681,8 +20905,8 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
20681
20905
  return status;
20682
20906
  }
20683
20907
  }
20684
- await mkdir16(paths.logsDir, { recursive: true, mode: 448 });
20685
- await mkdir16(paths.runDir, { recursive: true, mode: 448 });
20908
+ await mkdir12(paths.logsDir, { recursive: true, mode: 448 });
20909
+ await mkdir12(paths.runDir, { recursive: true, mode: 448 });
20686
20910
  const scriptPath = currentCliScriptPath();
20687
20911
  const child = spawn4(process.execPath, [scriptPath, "daemon-supervisor"], {
20688
20912
  detached: true,
@@ -20700,7 +20924,7 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
20700
20924
  return await getDaemonStatus(paths);
20701
20925
  }
20702
20926
  async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
20703
- await mkdir16(paths.logsDir, { recursive: true, mode: 448 });
20927
+ await mkdir12(paths.logsDir, { recursive: true, mode: 448 });
20704
20928
  const log = createRotatingTextLogWriter({
20705
20929
  paths,
20706
20930
  fileName: path22.basename(daemonLogFile(paths))
@@ -20944,7 +21168,7 @@ async function startLinkUpdate(options) {
20944
21168
  error: null,
20945
21169
  manual_command: manualCommand
20946
21170
  };
20947
- await mkdir17(options.paths.runDir, { recursive: true, mode: 448 });
21171
+ await mkdir13(options.paths.runDir, { recursive: true, mode: 448 });
20948
21172
  await writer.write(
20949
21173
  `
20950
21174
  === link update started ${startedAt} target=${targetVersion} ===
@@ -21313,6 +21537,17 @@ import path24 from "path";
21313
21537
  import { rm as rm12 } from "fs/promises";
21314
21538
 
21315
21539
  // src/relay/bootstrap.ts
21540
+ var RelayNetworkError = class extends Error {
21541
+ constructor(relayBaseUrl, causeMessage) {
21542
+ super(
21543
+ `Cannot reach Hermes Relay at ${relayBaseUrl}. Please check your network connection, VPN, or proxy settings, then try again.`
21544
+ );
21545
+ this.relayBaseUrl = relayBaseUrl;
21546
+ this.causeMessage = causeMessage;
21547
+ }
21548
+ relayBaseUrl;
21549
+ causeMessage;
21550
+ };
21316
21551
  async function bootstrapRelayLink(options) {
21317
21552
  const fetcher = options.fetchImpl ?? fetch;
21318
21553
  const baseUrl = options.relayBaseUrl.replace(/\/+$/u, "");
@@ -21353,14 +21588,23 @@ async function bootstrapRelayLink(options) {
21353
21588
  };
21354
21589
  }
21355
21590
  async function postJson(fetcher, url, token, body) {
21356
- const response = await fetcher(url, {
21357
- method: "POST",
21358
- headers: {
21359
- authorization: `Bearer ${token}`,
21360
- "content-type": "application/json"
21361
- },
21362
- body: JSON.stringify(body)
21363
- });
21591
+ let response;
21592
+ try {
21593
+ response = await fetcher(url, {
21594
+ method: "POST",
21595
+ headers: {
21596
+ authorization: `Bearer ${token}`,
21597
+ "content-type": "application/json"
21598
+ },
21599
+ body: JSON.stringify(body)
21600
+ });
21601
+ } catch (error) {
21602
+ const baseUrl = new URL(url).origin;
21603
+ throw new RelayNetworkError(
21604
+ baseUrl,
21605
+ error instanceof Error ? error.message : String(error)
21606
+ );
21607
+ }
21364
21608
  const payload = await response.json().catch(() => null);
21365
21609
  if (!response.ok) {
21366
21610
  const message = readErrorMessage4(payload) ?? `Relay request failed with HTTP ${response.status}`;
@@ -21388,14 +21632,22 @@ async function preparePairing(paths = resolveRuntimePaths()) {
21388
21632
  const config = await loadConfig(paths);
21389
21633
  const identity = await ensureIdentity(paths);
21390
21634
  const systemInfo = readLinkSystemInfo();
21391
- const created = await postServerJson(config.serverBaseUrl, "/api/v1/link-pairings", {
21392
- install_id: identity.install_id,
21393
- link_id: identity.link_id ?? void 0,
21394
- display_name: systemInfo.defaultDisplayName,
21395
- platform: systemInfo.platform,
21396
- hostname: systemInfo.hostname ?? void 0,
21397
- public_key_pem: identity.public_key_pem
21398
- });
21635
+ const created = await postServerJson(
21636
+ config.serverBaseUrl,
21637
+ "/api/v1/link-pairings",
21638
+ {
21639
+ install_id: identity.install_id,
21640
+ link_id: identity.link_id ?? void 0,
21641
+ display_name: systemInfo.defaultDisplayName,
21642
+ platform: systemInfo.platform,
21643
+ hostname: systemInfo.hostname ?? void 0,
21644
+ public_key_pem: identity.public_key_pem
21645
+ },
21646
+ {
21647
+ target: "server",
21648
+ action: "create pairing session"
21649
+ }
21650
+ );
21399
21651
  const relayBaseUrl = created.relayBaseUrl || config.relayBaseUrl;
21400
21652
  let assigned;
21401
21653
  let updatedIdentity;
@@ -21417,28 +21669,43 @@ async function preparePairing(paths = resolveRuntimePaths()) {
21417
21669
  installId: updatedIdentity.install_id,
21418
21670
  publicKeyPem: updatedIdentity.public_key_pem
21419
21671
  });
21420
- await patchServerJson(config.serverBaseUrl, `/api/v1/link-pairings/${created.sessionId}/link`, created.pairingToken, {
21421
- install_id: updatedIdentity.install_id,
21422
- link_id: assigned.linkId,
21423
- link_version: LINK_VERSION,
21424
- display_name: systemInfo.defaultDisplayName,
21425
- platform: systemInfo.platform,
21426
- hostname: systemInfo.hostname ?? void 0,
21427
- lan_ips: routes.lanIps,
21428
- public_ipv4s: routes.publicIpv4s,
21429
- public_ipv6s: routes.publicIpv6s,
21430
- preferred_urls: routes.preferredUrls,
21431
- environment: routes.environment
21432
- });
21672
+ await patchServerJson(
21673
+ config.serverBaseUrl,
21674
+ `/api/v1/link-pairings/${created.sessionId}/link`,
21675
+ created.pairingToken,
21676
+ {
21677
+ install_id: updatedIdentity.install_id,
21678
+ link_id: assigned.linkId,
21679
+ link_version: LINK_VERSION,
21680
+ display_name: systemInfo.defaultDisplayName,
21681
+ platform: systemInfo.platform,
21682
+ hostname: systemInfo.hostname ?? void 0,
21683
+ lan_ips: routes.lanIps,
21684
+ public_ipv4s: routes.publicIpv4s,
21685
+ public_ipv6s: routes.publicIpv6s,
21686
+ preferred_urls: routes.preferredUrls,
21687
+ environment: routes.environment
21688
+ },
21689
+ {
21690
+ target: "server",
21691
+ action: "finalize pairing"
21692
+ }
21693
+ );
21433
21694
  } catch (error) {
21695
+ const reportedError = error instanceof RelayNetworkError ? createPairingNetworkError({
21696
+ target: "relay",
21697
+ action: "connect to Relay",
21698
+ baseUrl: error.relayBaseUrl,
21699
+ detail: error.causeMessage
21700
+ }) : error;
21434
21701
  await reportPairingErrorToServer({
21435
21702
  serverBaseUrl: config.serverBaseUrl,
21436
21703
  sessionId: created.sessionId,
21437
21704
  source: "link",
21438
21705
  pairingToken: created.pairingToken,
21439
- error: pairingErrorSnapshot("prepare_pairing", error)
21706
+ error: pairingErrorSnapshot("prepare_pairing", reportedError)
21440
21707
  });
21441
- throw error;
21708
+ throw reportedError;
21442
21709
  }
21443
21710
  const qrPayload = {
21444
21711
  kind: "hermes_link_pairing",
@@ -21538,6 +21805,10 @@ async function claimPairing(input) {
21538
21805
  {
21539
21806
  claim_token: input.claimToken,
21540
21807
  app_instance_id: input.appInstanceId ?? void 0
21808
+ },
21809
+ {
21810
+ target: "server",
21811
+ action: "verify pairing claim"
21541
21812
  }
21542
21813
  );
21543
21814
  } catch (error) {
@@ -21585,15 +21856,25 @@ async function loadRequiredIdentity2(paths) {
21585
21856
  }
21586
21857
  return identity;
21587
21858
  }
21588
- async function postServerJson(serverBaseUrl, path25, body) {
21589
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path25}`, {
21590
- method: "POST",
21591
- headers: {
21592
- accept: "application/json",
21593
- "content-type": "application/json"
21594
- },
21595
- body: JSON.stringify(body)
21596
- });
21859
+ async function postServerJson(serverBaseUrl, path25, body, options) {
21860
+ let response;
21861
+ try {
21862
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path25}`, {
21863
+ method: "POST",
21864
+ headers: {
21865
+ accept: "application/json",
21866
+ "content-type": "application/json"
21867
+ },
21868
+ body: JSON.stringify(body)
21869
+ });
21870
+ } catch (error) {
21871
+ throw createPairingNetworkError({
21872
+ target: options.target,
21873
+ action: options.action,
21874
+ baseUrl: serverBaseUrl,
21875
+ detail: error instanceof Error ? error.message : String(error)
21876
+ });
21877
+ }
21597
21878
  return readJsonResponse2(response);
21598
21879
  }
21599
21880
  async function reportPairingErrorToServer(input) {
@@ -21626,16 +21907,26 @@ function pairingErrorSnapshot(stage, error) {
21626
21907
  occurred_at: (/* @__PURE__ */ new Date()).toISOString()
21627
21908
  };
21628
21909
  }
21629
- async function patchServerJson(serverBaseUrl, path25, token, body) {
21630
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path25}`, {
21631
- method: "PATCH",
21632
- headers: {
21633
- accept: "application/json",
21634
- authorization: `Bearer ${token}`,
21635
- "content-type": "application/json"
21636
- },
21637
- body: JSON.stringify(body)
21638
- });
21910
+ async function patchServerJson(serverBaseUrl, path25, token, body, options) {
21911
+ let response;
21912
+ try {
21913
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path25}`, {
21914
+ method: "PATCH",
21915
+ headers: {
21916
+ accept: "application/json",
21917
+ authorization: `Bearer ${token}`,
21918
+ "content-type": "application/json"
21919
+ },
21920
+ body: JSON.stringify(body)
21921
+ });
21922
+ } catch (error) {
21923
+ throw createPairingNetworkError({
21924
+ target: options.target,
21925
+ action: options.action,
21926
+ baseUrl: serverBaseUrl,
21927
+ detail: error instanceof Error ? error.message : String(error)
21928
+ });
21929
+ }
21639
21930
  return readJsonResponse2(response);
21640
21931
  }
21641
21932
  async function readJsonResponse2(response) {
@@ -21657,6 +21948,15 @@ function readErrorMessage5(payload) {
21657
21948
  const message = error.message;
21658
21949
  return typeof message === "string" ? message : null;
21659
21950
  }
21951
+ function createPairingNetworkError(input) {
21952
+ const baseMessage = input.target === "server" ? `HermesPilot Server is unreachable while trying to ${input.action}.` : `Hermes Relay is unreachable while trying to ${input.action}.`;
21953
+ const hint = "If you are using a VPN, proxy, or corporate network, try turning it off and retrying.";
21954
+ return new LinkHttpError(
21955
+ 503,
21956
+ input.target === "server" ? "pairing_server_unreachable" : "pairing_relay_unreachable",
21957
+ `${baseMessage} Please check whether ${input.baseUrl} is reachable. ${hint} Detail: ${input.detail}`
21958
+ );
21959
+ }
21660
21960
  function pairingClaimPath(sessionId, paths) {
21661
21961
  return path24.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
21662
21962
  }
@@ -22792,6 +23092,7 @@ export {
22792
23092
  createFileLogger,
22793
23093
  getLinkLogFile,
22794
23094
  ensureHermesApiServerAvailable,
23095
+ readHermesVersion,
22795
23096
  loadConfig,
22796
23097
  saveConfig,
22797
23098
  normalizeLanHost,