@hermespilot/link 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,8 +9,21 @@ import { randomUUID as randomUUID7 } from "crypto";
9
9
  // src/database/link-database.ts
10
10
  import { mkdir } from "fs/promises";
11
11
  import { randomUUID } from "crypto";
12
- import { createRequire } from "module";
13
12
  import path from "path";
13
+
14
+ // src/database/sqlite.ts
15
+ import Database from "better-sqlite3";
16
+ function openSqliteDatabase(filePath, options = {}) {
17
+ return new Database(filePath, {
18
+ ...options.readonly === void 0 ? {} : {
19
+ readonly: options.readonly,
20
+ fileMustExist: options.readonly
21
+ },
22
+ ...options.timeout === void 0 ? {} : { timeout: options.timeout }
23
+ });
24
+ }
25
+
26
+ // src/database/link-database.ts
14
27
  var MIGRATIONS = [
15
28
  {
16
29
  version: 1,
@@ -136,7 +149,6 @@ var MIGRATIONS = [
136
149
  `
137
150
  }
138
151
  ];
139
- var nodeRequire = createRequire(import.meta.url);
140
152
  async function migrateLinkDatabase(paths) {
141
153
  await mkdir(path.dirname(paths.databaseFile), { recursive: true, mode: 448 });
142
154
  const db = openDatabase(paths);
@@ -210,6 +222,92 @@ async function upsertConversationStats(paths, record) {
210
222
  db.close();
211
223
  }
212
224
  }
225
+ async function listConversationStatsPage(paths, input) {
226
+ await migrateLinkDatabase(paths);
227
+ const rawLimit = Number.isFinite(input.limit) ? Math.trunc(input.limit) : 25;
228
+ const limit = Math.max(1, Math.min(100, rawLimit));
229
+ const db = openDatabase(paths);
230
+ try {
231
+ const conditions = ["status = ?"];
232
+ const params = ["active"];
233
+ if (input.cursor) {
234
+ conditions.push(`(
235
+ updated_at < ?
236
+ OR (updated_at = ? AND conversation_id < ?)
237
+ )`);
238
+ params.push(
239
+ input.cursor.updatedAt,
240
+ input.cursor.updatedAt,
241
+ input.cursor.conversationId
242
+ );
243
+ }
244
+ const rows = db.prepare(`
245
+ SELECT conversation_id, updated_at
246
+ FROM conversation_stats
247
+ WHERE ${conditions.join(" AND ")}
248
+ ORDER BY updated_at DESC, conversation_id DESC
249
+ LIMIT ?
250
+ `).all(...params, limit + 1);
251
+ const records = rows.slice(0, limit).map((row) => ({
252
+ conversationId: readString(row, "conversation_id") ?? "",
253
+ updatedAt: readString(row, "updated_at") ?? ""
254
+ })).filter((row) => row.conversationId && row.updatedAt);
255
+ return {
256
+ records,
257
+ hasMore: rows.length > limit
258
+ };
259
+ } finally {
260
+ db.close();
261
+ }
262
+ }
263
+ async function searchConversationStatsPage(paths, input) {
264
+ await migrateLinkDatabase(paths);
265
+ const rawLimit = Number.isFinite(input.limit) ? Math.trunc(input.limit) : 25;
266
+ const limit = Math.max(1, Math.min(100, rawLimit));
267
+ const query = input.query.trim();
268
+ if (!query) {
269
+ return listConversationStatsPage(paths, {
270
+ limit,
271
+ cursor: input.cursor
272
+ });
273
+ }
274
+ const db = openDatabase(paths);
275
+ try {
276
+ const conditions = ["status = ?", "LOWER(title) LIKE ? ESCAPE '\\'"];
277
+ const params = [
278
+ "active",
279
+ `%${escapeSqlLike(query.toLowerCase())}%`
280
+ ];
281
+ if (input.cursor) {
282
+ conditions.push(`(
283
+ updated_at < ?
284
+ OR (updated_at = ? AND conversation_id < ?)
285
+ )`);
286
+ params.push(
287
+ input.cursor.updatedAt,
288
+ input.cursor.updatedAt,
289
+ input.cursor.conversationId
290
+ );
291
+ }
292
+ const rows = db.prepare(`
293
+ SELECT conversation_id, updated_at
294
+ FROM conversation_stats
295
+ WHERE ${conditions.join(" AND ")}
296
+ ORDER BY updated_at DESC, conversation_id DESC
297
+ LIMIT ?
298
+ `).all(...params, limit + 1);
299
+ const records = rows.slice(0, limit).map((row) => ({
300
+ conversationId: readString(row, "conversation_id") ?? "",
301
+ updatedAt: readString(row, "updated_at") ?? ""
302
+ })).filter((row) => row.conversationId && row.updatedAt);
303
+ return {
304
+ records,
305
+ hasMore: rows.length > limit
306
+ };
307
+ } finally {
308
+ db.close();
309
+ }
310
+ }
213
311
  async function replaceRunUsageFactsForConversation(paths, conversationId, records) {
214
312
  await migrateLinkDatabase(paths);
215
313
  const db = openDatabase(paths);
@@ -618,8 +716,7 @@ async function deleteConversationStatsForProfile(paths, input) {
618
716
  }
619
717
  }
620
718
  function openDatabase(paths) {
621
- const { DatabaseSync } = nodeRequire("node:sqlite");
622
- const db = new DatabaseSync(paths.databaseFile, {
719
+ const db = openSqliteDatabase(paths.databaseFile, {
623
720
  timeout: 5e3
624
721
  });
625
722
  db.exec(`
@@ -826,6 +923,9 @@ function readString(row, key) {
826
923
  const value = row?.[key];
827
924
  return typeof value === "string" && value.trim() ? value : void 0;
828
925
  }
926
+ function escapeSqlLike(value) {
927
+ return value.replace(/[\\%_]/gu, (match) => `\\${match}`);
928
+ }
829
929
  function statisticsWhereClause(filter) {
830
930
  const profileUid = filter.profileUid?.trim();
831
931
  const profileName = filter.profileName?.trim();
@@ -999,58 +1099,200 @@ function readProfileAvatarType(row) {
999
1099
  }
1000
1100
 
1001
1101
  // src/hermes/cron-link-delivery.ts
1002
- 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";
1003
1103
  import path4 from "path";
1004
1104
 
1005
1105
  // src/storage/atomic-json.ts
1006
- import { mkdir as mkdir2, open, readFile, rename, rm } from "fs/promises";
1106
+ import { readFile } from "fs/promises";
1107
+
1108
+ // src/storage/atomic-file.ts
1007
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";
1008
1121
  import path2 from "path";
1009
- 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
+ );
1010
1140
  try {
1011
- const raw = await readFile(filePath, "utf8");
1012
- 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);
1013
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) => {
1014
1206
  if (isNodeError(error, "ENOENT")) {
1015
1207
  return null;
1016
1208
  }
1017
1209
  throw error;
1210
+ });
1211
+ if (!fileStat) {
1212
+ return null;
1213
+ }
1214
+ if (!fileStat.isFile()) {
1215
+ throw new Error(`${filePath} is not a file`);
1018
1216
  }
1217
+ return metadataFromStats(fileStat);
1019
1218
  }
1020
- async function writeJsonFile(filePath, value, mode = 384) {
1021
- await mkdir2(path2.dirname(filePath), { recursive: true, mode: 448 });
1022
- const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${randomUUID2()}.tmp`;
1023
- const payload = `${JSON.stringify(value, null, 2)}
1024
- `;
1025
- 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
+ }
1026
1251
  try {
1027
- await handle.writeFile(payload, "utf8");
1028
- await handle.sync();
1029
- } finally {
1030
- 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
+ }
1031
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) {
1032
1273
  try {
1033
- await rename(tmpPath, filePath);
1274
+ const raw = await readFile(filePath, "utf8");
1275
+ return JSON.parse(raw);
1034
1276
  } catch (error) {
1035
- await rm(tmpPath, { force: true });
1277
+ if (isNodeError2(error, "ENOENT")) {
1278
+ return null;
1279
+ }
1036
1280
  throw error;
1037
1281
  }
1038
1282
  }
1039
- 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) {
1040
1289
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
1041
1290
  }
1042
1291
 
1043
1292
  // src/hermes/config.ts
1044
1293
  import { randomBytes } from "crypto";
1045
1294
  import net from "net";
1046
- import {
1047
- copyFile,
1048
- mkdir as mkdir3,
1049
- readFile as readFile2,
1050
- readdir,
1051
- rename as rename2,
1052
- writeFile
1053
- } from "fs/promises";
1295
+ import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
1054
1296
  import os from "os";
1055
1297
  import path3 from "path";
1056
1298
  import YAML from "yaml";
@@ -1274,7 +1516,7 @@ async function readHermesSessionsDir(profileName = "default", configPath = resol
1274
1516
  async function readHermesApiServerConfig(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
1275
1517
  const existingRaw = await readFile2(configPath, "utf8").catch(
1276
1518
  (error) => {
1277
- if (isNodeError2(error, "ENOENT")) {
1519
+ if (isNodeError3(error, "ENOENT")) {
1278
1520
  return null;
1279
1521
  }
1280
1522
  throw error;
@@ -1299,7 +1541,7 @@ async function readHermesApiServerConfig(profileName = "default", configPath = r
1299
1541
  async function readHermesModelConfig(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
1300
1542
  const existingRaw = await readFile2(configPath, "utf8").catch(
1301
1543
  (error) => {
1302
- if (isNodeError2(error, "ENOENT")) {
1544
+ if (isNodeError3(error, "ENOENT")) {
1303
1545
  return null;
1304
1546
  }
1305
1547
  throw error;
@@ -1820,7 +2062,7 @@ async function ensureHermesApiServerConfig(profileName = "default", configPath =
1820
2062
  async function ensureHermesApiServerConfigUnlocked(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
1821
2063
  const existingRaw = await readFile2(configPath, "utf8").catch(
1822
2064
  (error) => {
1823
- if (isNodeError2(error, "ENOENT")) {
2065
+ if (isNodeError3(error, "ENOENT")) {
1824
2066
  return null;
1825
2067
  }
1826
2068
  throw error;
@@ -1886,12 +2128,13 @@ async function ensureHermesApiServerConfigUnlocked(profileName = "default", conf
1886
2128
  };
1887
2129
  }
1888
2130
  const backupPath = existingRaw ? `${configPath}.bak.${Date.now()}` : null;
1889
- await mkdir3(path3.dirname(configPath), { recursive: true, mode: 448 });
1890
- if (backupPath) {
1891
- await copyFile(configPath, backupPath);
2131
+ if (backupPath && existingRaw !== null) {
2132
+ await atomicWriteFilePreservingMetadata(backupPath, existingRaw, {
2133
+ metadataSourcePath: configPath
2134
+ });
1892
2135
  }
1893
2136
  document.contents = document.createNode(config);
1894
- await writeFile(configPath, document.toString(), { mode: 384 });
2137
+ await atomicWriteFilePreservingMetadata(configPath, document.toString());
1895
2138
  return {
1896
2139
  configPath,
1897
2140
  apiServer: applyEnvOverrides(
@@ -1917,7 +2160,7 @@ async function ensureHermesApiServerConfigUnlocked(profileName = "default", conf
1917
2160
  async function readHermesConfigDocument(configPath) {
1918
2161
  const existingRaw = await readFile2(configPath, "utf8").catch(
1919
2162
  (error) => {
1920
- if (isNodeError2(error, "ENOENT")) {
2163
+ if (isNodeError3(error, "ENOENT")) {
1921
2164
  return null;
1922
2165
  }
1923
2166
  throw error;
@@ -1932,14 +2175,16 @@ async function readHermesConfigDocument(configPath) {
1932
2175
  }
1933
2176
  async function writeHermesConfigDocument(input) {
1934
2177
  const backupPath = input.existingRaw ? `${input.configPath}.bak.${Date.now()}` : null;
1935
- await mkdir3(path3.dirname(input.configPath), { recursive: true, mode: 448 });
1936
2178
  if (backupPath) {
1937
- await copyFile(input.configPath, backupPath);
2179
+ await atomicWriteFilePreservingMetadata(backupPath, input.existingRaw, {
2180
+ metadataSourcePath: input.configPath
2181
+ });
1938
2182
  }
1939
2183
  input.document.contents = input.document.createNode(input.config);
1940
- const tempPath = `${input.configPath}.tmp.${process.pid}.${Date.now()}`;
1941
- await writeFile(tempPath, input.document.toString(), { mode: 384 });
1942
- await rename2(tempPath, input.configPath);
2184
+ await atomicWriteFilePreservingMetadata(
2185
+ input.configPath,
2186
+ input.document.toString()
2187
+ );
1943
2188
  return backupPath;
1944
2189
  }
1945
2190
  function readManagedModelConfigs(config, env, defaultModel, defaultReasoningEffort) {
@@ -3261,9 +3506,9 @@ async function readConfiguredApiServerPorts(excludedProfileName) {
3261
3506
  resolveDefaultHermesRoot(resolveHermesProfileDir("default")),
3262
3507
  "profiles"
3263
3508
  );
3264
- const entries = await readdir(profilesRoot, { withFileTypes: true }).catch(
3509
+ const entries = await readdir2(profilesRoot, { withFileTypes: true }).catch(
3265
3510
  (error) => {
3266
- if (isNodeError2(error, "ENOENT")) {
3511
+ if (isNodeError3(error, "ENOENT")) {
3267
3512
  return [];
3268
3513
  }
3269
3514
  throw error;
@@ -3285,7 +3530,7 @@ async function addConfiguredApiServerPort(ports, profileName, excludedProfileNam
3285
3530
  resolveHermesConfigPath(profileName),
3286
3531
  "utf8"
3287
3532
  ).catch((error) => {
3288
- if (isNodeError2(error, "ENOENT")) {
3533
+ if (isNodeError3(error, "ENOENT")) {
3289
3534
  return "";
3290
3535
  }
3291
3536
  throw error;
@@ -3358,7 +3603,7 @@ async function readHermesApiServerEnvOverrides(profileName) {
3358
3603
  async function readHermesEnvFile(profileName) {
3359
3604
  const envPath = path3.join(resolveHermesProfileDir(profileName), ".env");
3360
3605
  const raw = await readFile2(envPath, "utf8").catch((error) => {
3361
- if (isNodeError2(error, "ENOENT")) {
3606
+ if (isNodeError3(error, "ENOENT")) {
3362
3607
  return "";
3363
3608
  }
3364
3609
  throw error;
@@ -3383,7 +3628,7 @@ async function writeHermesEnvValue(profileName, key, value) {
3383
3628
  const envPath = path3.join(resolveHermesProfileDir(profileName), ".env");
3384
3629
  const existingRaw = await readFile2(envPath, "utf8").catch(
3385
3630
  (error) => {
3386
- if (isNodeError2(error, "ENOENT")) {
3631
+ if (isNodeError3(error, "ENOENT")) {
3387
3632
  return "";
3388
3633
  }
3389
3634
  throw error;
@@ -3406,13 +3651,14 @@ async function writeHermesEnvValue(profileName, key, value) {
3406
3651
  nextLines.push(`${key}=${formatEnvValue(value)}`);
3407
3652
  }
3408
3653
  const nextRaw = nextLines.join("\n").replace(/\n*$/u, "\n");
3409
- await mkdir3(path3.dirname(envPath), { recursive: true, mode: 448 });
3410
3654
  if (existingRaw) {
3411
- await copyFile(envPath, `${envPath}.bak.${Date.now()}`);
3655
+ await atomicWriteFilePreservingMetadata(
3656
+ `${envPath}.bak.${Date.now()}`,
3657
+ existingRaw,
3658
+ { metadataSourcePath: envPath }
3659
+ );
3412
3660
  }
3413
- const tempPath = `${envPath}.tmp.${process.pid}.${Date.now()}`;
3414
- await writeFile(tempPath, nextRaw, { mode: 384 });
3415
- await rename2(tempPath, envPath);
3661
+ await atomicWriteFilePreservingMetadata(envPath, nextRaw);
3416
3662
  }
3417
3663
  function applyEnvOverrides(config, env, withDefaults) {
3418
3664
  const host = config.host ?? env.host;
@@ -3491,7 +3737,7 @@ function ensureRecord(parent, key) {
3491
3737
  function resolveHermesConfiguredPath(value, baseDir) {
3492
3738
  return path3.isAbsolute(value) ? path3.normalize(value) : path3.resolve(baseDir, value);
3493
3739
  }
3494
- function isNodeError2(error, code) {
3740
+ function isNodeError3(error, code) {
3495
3741
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
3496
3742
  }
3497
3743
  function resolveDefaultHermesRoot(hermesHome) {
@@ -3616,9 +3862,9 @@ async function listCronOutputFiles(profileName, jobId) {
3616
3862
  "output",
3617
3863
  jobId
3618
3864
  );
3619
- const entries = await readdir2(outputDir, { withFileTypes: true }).catch(
3865
+ const entries = await readdir3(outputDir, { withFileTypes: true }).catch(
3620
3866
  (error) => {
3621
- if (isNodeError3(error, "ENOENT")) {
3867
+ if (isNodeError4(error, "ENOENT")) {
3622
3868
  return [];
3623
3869
  }
3624
3870
  throw error;
@@ -3630,7 +3876,7 @@ async function listCronOutputFiles(profileName, jobId) {
3630
3876
  continue;
3631
3877
  }
3632
3878
  const outputPath = path4.join(outputDir, entry.name);
3633
- const fileStat = await stat(outputPath);
3879
+ const fileStat = await stat2(outputPath);
3634
3880
  files.push({
3635
3881
  path: outputPath,
3636
3882
  mtime: fileStat.mtime.toISOString(),
@@ -3663,7 +3909,7 @@ async function readRegistry(paths) {
3663
3909
  return { version: REGISTRY_VERSION, bindings: [] };
3664
3910
  }
3665
3911
  async function writeRegistry(paths, registry) {
3666
- await mkdir4(paths.indexesDir, { recursive: true, mode: 448 });
3912
+ await mkdir3(paths.indexesDir, { recursive: true, mode: 448 });
3667
3913
  await writeJsonFile(registryPath(paths), registry);
3668
3914
  }
3669
3915
  function registryPath(paths) {
@@ -3689,13 +3935,13 @@ function readString3(record, ...keys) {
3689
3935
  }
3690
3936
  return void 0;
3691
3937
  }
3692
- function isNodeError3(error, code) {
3938
+ function isNodeError4(error, code) {
3693
3939
  return error instanceof Error && error.code === code;
3694
3940
  }
3695
3941
 
3696
3942
  // src/hermes/gateway.ts
3697
3943
  import { execFile as execFile2, spawn } from "child_process";
3698
- import { access, readFile as readFile5, stat as stat3 } from "fs/promises";
3944
+ import { access, readFile as readFile5, stat as stat4 } from "fs/promises";
3699
3945
  import path7 from "path";
3700
3946
  import { setTimeout as delay } from "timers/promises";
3701
3947
  import { promisify as promisify2 } from "util";
@@ -3715,7 +3961,7 @@ function isLinkHttpError(error) {
3715
3961
  }
3716
3962
 
3717
3963
  // src/runtime/logger.ts
3718
- 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";
3719
3965
  import os3 from "os";
3720
3966
  import path6 from "path";
3721
3967
 
@@ -3724,7 +3970,7 @@ import os2 from "os";
3724
3970
  import path5 from "path";
3725
3971
 
3726
3972
  // src/constants.ts
3727
- var LINK_VERSION = "0.3.6";
3973
+ var LINK_VERSION = "0.3.8";
3728
3974
  var LINK_COMMAND = "hermeslink";
3729
3975
  var LINK_DEFAULT_PORT = 52379;
3730
3976
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -3800,7 +4046,7 @@ var FileLogger = class {
3800
4046
  return this.queue;
3801
4047
  }
3802
4048
  async appendEntry(entry) {
3803
- await mkdir5(this.paths.logsDir, { recursive: true, mode: 448 });
4049
+ await mkdir4(this.paths.logsDir, { recursive: true, mode: 448 });
3804
4050
  const line = `${JSON.stringify(entry)}
3805
4051
  `;
3806
4052
  await this.rotateIfNeeded(Buffer.byteLength(line, "utf8"));
@@ -3836,7 +4082,7 @@ function createRotatingTextLogWriter(options) {
3836
4082
  return queue;
3837
4083
  }
3838
4084
  const next = queue.then(async () => {
3839
- await mkdir5(paths.logsDir, { recursive: true, mode: 448 });
4085
+ await mkdir4(paths.logsDir, { recursive: true, mode: 448 });
3840
4086
  await rotateLogFileIfNeeded(filePath, buffer.length, maxFileBytes, maxFiles);
3841
4087
  await appendFile(filePath, buffer, { mode: 384 });
3842
4088
  }).catch(() => void 0);
@@ -3937,7 +4183,7 @@ function clampLimit(value) {
3937
4183
  return Math.min(MAX_READ_LIMIT, Math.max(1, Math.floor(value)));
3938
4184
  }
3939
4185
  async function rotateLogFileIfNeeded(filePath, nextBytes, maxFileBytes, maxFiles) {
3940
- const current = await stat2(filePath).catch(() => null);
4186
+ const current = await stat3(filePath).catch(() => null);
3941
4187
  if (!current || current.size === 0 || current.size + nextBytes <= maxFileBytes) {
3942
4188
  return;
3943
4189
  }
@@ -4052,7 +4298,7 @@ async function readTail(filePath, maxBytes) {
4052
4298
  return tail?.content ?? null;
4053
4299
  }
4054
4300
  async function readTailWithMetadata(filePath, maxBytes) {
4055
- const info = await stat2(filePath).catch(() => null);
4301
+ const info = await stat3(filePath).catch(() => null);
4056
4302
  if (!info || info.size <= 0) {
4057
4303
  return null;
4058
4304
  }
@@ -4076,7 +4322,7 @@ async function readTailWithMetadata(filePath, maxBytes) {
4076
4322
  }
4077
4323
  async function moveIfExists(from, to) {
4078
4324
  await rm2(to, { force: true }).catch(() => void 0);
4079
- await rename3(from, to).catch((error) => {
4325
+ await rename2(from, to).catch((error) => {
4080
4326
  if (error.code !== "ENOENT") {
4081
4327
  throw error;
4082
4328
  }
@@ -4499,8 +4745,8 @@ async function assertProfileExists(profileName) {
4499
4745
  if (profileName === "default") {
4500
4746
  return;
4501
4747
  }
4502
- const exists = await stat3(resolveHermesProfileDir(profileName)).then((value) => value.isDirectory()).catch((error) => {
4503
- if (isNodeError4(error, "ENOENT")) {
4748
+ const exists = await stat4(resolveHermesProfileDir(profileName)).then((value) => value.isDirectory()).catch((error) => {
4749
+ if (isNodeError5(error, "ENOENT")) {
4504
4750
  return false;
4505
4751
  }
4506
4752
  throw error;
@@ -4700,7 +4946,7 @@ function isHealthyPlatformState(value) {
4700
4946
  function toRecord2(value) {
4701
4947
  return typeof value === "object" && value !== null ? value : {};
4702
4948
  }
4703
- function isNodeError4(error, code) {
4949
+ function isNodeError5(error, code) {
4704
4950
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
4705
4951
  }
4706
4952
  function readString4(payload, key) {
@@ -5068,7 +5314,7 @@ function firstRecord(...values) {
5068
5314
 
5069
5315
  // src/conversations/blob-store.ts
5070
5316
  import { randomUUID as randomUUID3 } from "crypto";
5071
- import { mkdir as mkdir6, readFile as readFile6, readdir as readdir3, rm as rm3, stat as stat4, writeFile as writeFile2 } from "fs/promises";
5317
+ import { mkdir as mkdir5, readFile as readFile6, readdir as readdir4, rm as rm3, stat as stat5, writeFile } from "fs/promises";
5072
5318
  import path9 from "path";
5073
5319
 
5074
5320
  // src/conversations/media.ts
@@ -5523,8 +5769,8 @@ async function writeConversationBlob(paths, conversationId, input, options) {
5523
5769
  }
5524
5770
  const id = `blob_${randomUUID3().replaceAll("-", "")}`;
5525
5771
  const filePath = blobPath(paths, id);
5526
- await mkdir6(path9.dirname(filePath), { recursive: true, mode: 448 });
5527
- await writeFile2(filePath, input.bytes, { mode: 384 });
5772
+ await mkdir5(path9.dirname(filePath), { recursive: true, mode: 448 });
5773
+ await writeFile(filePath, input.bytes, { mode: 384 });
5528
5774
  const blob = {
5529
5775
  id,
5530
5776
  size: input.bytes.byteLength,
@@ -5551,7 +5797,7 @@ async function readConversationBlob(paths, conversationId, blobId) {
5551
5797
  const filePath = blobPath(paths, blobId);
5552
5798
  const manifest = await readConversationBlobManifest(paths, conversationId, blobId);
5553
5799
  const bytes = await readFile6(filePath).catch((error) => {
5554
- if (isNodeError5(error, "ENOENT")) {
5800
+ if (isNodeError6(error, "ENOENT")) {
5555
5801
  throw new LinkHttpError(404, "blob_not_found", "Blob was not found");
5556
5802
  }
5557
5803
  throw error;
@@ -5593,7 +5839,7 @@ async function deleteConversationBlobIfUnreferenced(paths, conversationId, blobI
5593
5839
  async function materializeConversationBlob(paths, conversationId, blobId, manifest) {
5594
5840
  const existingPath = manifest.materialized_path;
5595
5841
  if (existingPath) {
5596
- const exists = await stat4(existingPath).then((value) => value.isFile()).catch(() => false);
5842
+ const exists = await stat5(existingPath).then((value) => value.isFile()).catch(() => false);
5597
5843
  if (exists) {
5598
5844
  return existingPath;
5599
5845
  }
@@ -5603,8 +5849,8 @@ async function materializeConversationBlob(paths, conversationId, blobId, manife
5603
5849
  targetDir,
5604
5850
  materializedAttachmentFilename(blobId, manifest.filename ?? blobId)
5605
5851
  );
5606
- await mkdir6(targetDir, { recursive: true, mode: 448 });
5607
- await writeFile2(targetPath, await readFile6(blobPath(paths, blobId)), {
5852
+ await mkdir5(targetDir, { recursive: true, mode: 448 });
5853
+ await writeFile(targetPath, await readFile6(blobPath(paths, blobId)), {
5608
5854
  mode: 384
5609
5855
  });
5610
5856
  await writeJsonFile(`${blobPath(paths, blobId)}.json`, {
@@ -5633,11 +5879,11 @@ async function pruneConversationBlobReference(paths, conversationId, blobId) {
5633
5879
  }
5634
5880
  async function listConversationBlobIds(paths, conversationId) {
5635
5881
  assertValidConversationId(conversationId);
5636
- await mkdir6(paths.blobsDir, { recursive: true, mode: 448 });
5637
- const entries = await readdir3(paths.blobsDir, {
5882
+ await mkdir5(paths.blobsDir, { recursive: true, mode: 448 });
5883
+ const entries = await readdir4(paths.blobsDir, {
5638
5884
  withFileTypes: true
5639
5885
  }).catch((error) => {
5640
- if (isNodeError5(error, "ENOENT")) {
5886
+ if (isNodeError6(error, "ENOENT")) {
5641
5887
  return [];
5642
5888
  }
5643
5889
  throw error;
@@ -5671,12 +5917,12 @@ function conversationAttachmentsDir(paths, conversationId) {
5671
5917
  assertValidConversationId(conversationId);
5672
5918
  return path9.join(paths.conversationsDir, conversationId, "attachments");
5673
5919
  }
5674
- function isNodeError5(error, code) {
5920
+ function isNodeError6(error, code) {
5675
5921
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
5676
5922
  }
5677
5923
 
5678
5924
  // src/conversations/profile-runtime.ts
5679
- import { stat as stat5 } from "fs/promises";
5925
+ import { stat as stat6 } from "fs/promises";
5680
5926
  var PROFILE_NAME_PATTERN2 = /^[a-zA-Z0-9._-]{1,64}$/u;
5681
5927
  function normalizeProfileName2(profileName) {
5682
5928
  const value = profileName?.trim() || "default";
@@ -5817,8 +6063,8 @@ async function buildConversationRuntimeMetadata(paths, manifest, snapshot) {
5817
6063
  async function ensureProfileIdentityForRuntime(paths, profileName) {
5818
6064
  const profilePath = resolveHermesProfileDir(profileName);
5819
6065
  if (profileName !== "default") {
5820
- const exists = await stat5(profilePath).then((value) => value.isDirectory()).catch((error) => {
5821
- if (isNodeError6(error, "ENOENT")) {
6066
+ const exists = await stat6(profilePath).then((value) => value.isDirectory()).catch((error) => {
6067
+ if (isNodeError7(error, "ENOENT")) {
5822
6068
  return false;
5823
6069
  }
5824
6070
  throw error;
@@ -5844,7 +6090,7 @@ function displayNameForProfile(profile) {
5844
6090
  const custom = profile.displayName?.trim();
5845
6091
  return custom || fallbackProfileDisplayName(profile.profileName);
5846
6092
  }
5847
- function isNodeError6(error, code) {
6093
+ function isNodeError7(error, code) {
5848
6094
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
5849
6095
  }
5850
6096
 
@@ -6389,11 +6635,11 @@ function formatContextUsageLines(runtime) {
6389
6635
  }
6390
6636
 
6391
6637
  // src/conversations/delivery-staging.ts
6392
- import { mkdir as mkdir7, rm as rm4 } from "fs/promises";
6638
+ import { mkdir as mkdir6, rm as rm4 } from "fs/promises";
6393
6639
  import path10 from "path";
6394
6640
  async function prepareDeliveryStagingRunDir(paths, conversationId, runId) {
6395
6641
  const directory = deliveryStagingRunDir(paths, conversationId, runId);
6396
- await mkdir7(directory, { recursive: true, mode: 448 });
6642
+ await mkdir6(directory, { recursive: true, mode: 448 });
6397
6643
  return directory;
6398
6644
  }
6399
6645
  async function removeConversationDeliveryStaging(paths, conversationId) {
@@ -6752,10 +6998,8 @@ function isUsableLanIpv4(value) {
6752
6998
  }
6753
6999
 
6754
7000
  // src/hermes/session-title.ts
6755
- import { stat as stat6 } from "fs/promises";
6756
- import { createRequire as createRequire2 } from "module";
7001
+ import { stat as stat7 } from "fs/promises";
6757
7002
  import path11 from "path";
6758
- var nodeRequire2 = createRequire2(import.meta.url);
6759
7003
  async function readHermesSessionTitle(sessionId, paths, profileName) {
6760
7004
  const trimmedSessionId = sessionId.trim();
6761
7005
  if (!trimmedSessionId) {
@@ -6766,8 +7010,8 @@ async function readHermesSessionTitle(sessionId, paths, profileName) {
6766
7010
  resolveHermesProfileDir(resolvedProfileName),
6767
7011
  "state.db"
6768
7012
  );
6769
- const exists = await stat6(dbPath).then((value) => value.isFile()).catch((error) => {
6770
- if (isNodeError7(error, "ENOENT")) {
7013
+ const exists = await stat7(dbPath).then((value) => value.isFile()).catch((error) => {
7014
+ if (isNodeError8(error, "ENOENT")) {
6771
7015
  return false;
6772
7016
  }
6773
7017
  throw error;
@@ -6787,8 +7031,8 @@ async function readHermesCompressionTip(sessionId, paths, profileName) {
6787
7031
  resolveHermesProfileDir(resolvedProfileName),
6788
7032
  "state.db"
6789
7033
  );
6790
- const exists = await stat6(dbPath).then((value) => value.isFile()).catch((error) => {
6791
- if (isNodeError7(error, "ENOENT")) {
7034
+ const exists = await stat7(dbPath).then((value) => value.isFile()).catch((error) => {
7035
+ if (isNodeError8(error, "ENOENT")) {
6792
7036
  return false;
6793
7037
  }
6794
7038
  throw error;
@@ -6801,11 +7045,8 @@ async function readHermesCompressionTip(sessionId, paths, profileName) {
6801
7045
  function readSessionTitleFromStateDb(dbPath, sessionId) {
6802
7046
  let db = null;
6803
7047
  try {
6804
- const { DatabaseSync } = nodeRequire2(
6805
- "node:sqlite"
6806
- );
6807
- db = new DatabaseSync(dbPath, {
6808
- readOnly: true,
7048
+ db = openSqliteDatabase(dbPath, {
7049
+ readonly: true,
6809
7050
  timeout: 1e3
6810
7051
  });
6811
7052
  const row = db.prepare("SELECT title FROM sessions WHERE id = ? LIMIT 1").get(sessionId);
@@ -6820,11 +7061,8 @@ function readSessionTitleFromStateDb(dbPath, sessionId) {
6820
7061
  function readCompressionTipFromStateDb(dbPath, sessionId) {
6821
7062
  let db = null;
6822
7063
  try {
6823
- const { DatabaseSync } = nodeRequire2(
6824
- "node:sqlite"
6825
- );
6826
- db = new DatabaseSync(dbPath, {
6827
- readOnly: true,
7064
+ db = openSqliteDatabase(dbPath, {
7065
+ readonly: true,
6828
7066
  timeout: 1e3
6829
7067
  });
6830
7068
  let current = sessionId;
@@ -6857,7 +7095,7 @@ function readCompressionTipFromStateDb(dbPath, sessionId) {
6857
7095
  db?.close();
6858
7096
  }
6859
7097
  }
6860
- function isNodeError7(error, code) {
7098
+ function isNodeError8(error, code) {
6861
7099
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
6862
7100
  }
6863
7101
  function isValidProfileName(value) {
@@ -8284,12 +8522,63 @@ function toRecord6(value) {
8284
8522
  }
8285
8523
 
8286
8524
  // src/conversations/conversation-queries.ts
8525
+ var DEFAULT_CONVERSATION_LIST_PAGE_SIZE = 25;
8526
+ var MAX_CONVERSATION_LIST_PAGE_SIZE = 100;
8287
8527
  var ConversationQueryCoordinator = class {
8288
8528
  constructor(deps) {
8289
8529
  this.deps = deps;
8290
8530
  }
8291
8531
  deps;
8292
8532
  async listConversations() {
8533
+ return this.listConversationsFromStore();
8534
+ }
8535
+ async listConversationPage(options = {}) {
8536
+ const limit = normalizeConversationListPageLimit(options.limit);
8537
+ const cursor = decodeConversationListCursor(options.cursor);
8538
+ const indexedPage = await listConversationStatsPage(this.deps.paths, {
8539
+ limit,
8540
+ cursor
8541
+ });
8542
+ if (indexedPage.records.length === 0) {
8543
+ return this.listConversationPageFromStore({ limit, cursor });
8544
+ }
8545
+ const summaries = await this.summarizeIndexedConversations(
8546
+ indexedPage.records
8547
+ );
8548
+ return {
8549
+ conversations: summaries,
8550
+ page: {
8551
+ limit,
8552
+ has_more: indexedPage.hasMore,
8553
+ next_cursor: indexedPage.hasMore && indexedPage.records.length > 0 ? encodeConversationListCursor(indexedPage.records.at(-1)) : null
8554
+ }
8555
+ };
8556
+ }
8557
+ async searchConversationPage(options = {}) {
8558
+ const query = normalizeConversationSearchQuery(options.query);
8559
+ if (!query) {
8560
+ return this.listConversationPage(options);
8561
+ }
8562
+ const limit = normalizeConversationListPageLimit(options.limit);
8563
+ const cursor = decodeConversationListCursor(options.cursor);
8564
+ const indexedPage = await searchConversationStatsPage(this.deps.paths, {
8565
+ limit,
8566
+ cursor,
8567
+ query
8568
+ });
8569
+ const summaries = await this.summarizeIndexedConversations(
8570
+ indexedPage.records
8571
+ );
8572
+ return {
8573
+ conversations: summaries,
8574
+ page: {
8575
+ limit,
8576
+ has_more: indexedPage.hasMore,
8577
+ next_cursor: indexedPage.hasMore && indexedPage.records.length > 0 ? encodeConversationListCursor(indexedPage.records.at(-1)) : null
8578
+ }
8579
+ };
8580
+ }
8581
+ async listConversationsFromStore() {
8293
8582
  const summaries = [];
8294
8583
  for (const conversationId of await this.deps.store.listConversationIds()) {
8295
8584
  const manifest = await this.deps.store.readManifest(conversationId).catch(
@@ -8305,9 +8594,41 @@ var ConversationQueryCoordinator = class {
8305
8594
  summaries.push(await this.summarizeConversation(refreshed, snapshot));
8306
8595
  }
8307
8596
  return summaries.sort(
8308
- (left, right) => Date.parse(right.updated_at) - Date.parse(left.updated_at)
8597
+ (left, right) => Date.parse(right.updated_at) - Date.parse(left.updated_at) || right.id.localeCompare(left.id)
8309
8598
  );
8310
8599
  }
8600
+ async listConversationPageFromStore(input) {
8601
+ const all = await this.listConversationsFromStore();
8602
+ const startIndex = input.cursor ? all.findIndex((summary) => isAfterConversationListCursor(summary, input.cursor)) : 0;
8603
+ const safeStartIndex = startIndex < 0 ? all.length : startIndex;
8604
+ const conversations = all.slice(safeStartIndex, safeStartIndex + input.limit);
8605
+ const hasMore = safeStartIndex + input.limit < all.length;
8606
+ return {
8607
+ conversations,
8608
+ page: {
8609
+ limit: input.limit,
8610
+ has_more: hasMore,
8611
+ next_cursor: hasMore && conversations.length > 0 ? encodeConversationListCursorFromSummary(conversations.at(-1)) : null
8612
+ }
8613
+ };
8614
+ }
8615
+ async summarizeIndexedConversations(records) {
8616
+ const summaries = [];
8617
+ for (const record of records) {
8618
+ const manifest = await this.deps.store.readManifest(record.conversationId).catch(
8619
+ () => null
8620
+ );
8621
+ if (!manifest || manifest.status !== "active") {
8622
+ continue;
8623
+ }
8624
+ const snapshot = await this.deps.store.readSnapshot(record.conversationId).catch(
8625
+ () => emptySnapshot2()
8626
+ );
8627
+ const refreshed = await this.deps.metadata.refreshTitleFromHermes(manifest, { snapshot }).catch(() => manifest);
8628
+ summaries.push(await this.summarizeConversation(refreshed, snapshot));
8629
+ }
8630
+ return summaries;
8631
+ }
8311
8632
  async getMessages(conversationId, options = {}) {
8312
8633
  const manifest = await this.deps.store.readActiveManifest(conversationId);
8313
8634
  const snapshot = await this.deps.store.readSnapshot(conversationId);
@@ -8394,6 +8715,61 @@ var ConversationQueryCoordinator = class {
8394
8715
  });
8395
8716
  }
8396
8717
  };
8718
+ function normalizeConversationListPageLimit(value) {
8719
+ if (value === void 0 || !Number.isFinite(value)) {
8720
+ return DEFAULT_CONVERSATION_LIST_PAGE_SIZE;
8721
+ }
8722
+ return Math.max(
8723
+ 1,
8724
+ Math.min(MAX_CONVERSATION_LIST_PAGE_SIZE, Math.floor(value))
8725
+ );
8726
+ }
8727
+ function encodeConversationListCursor(record) {
8728
+ return Buffer.from(
8729
+ JSON.stringify({
8730
+ updated_at: record.updatedAt,
8731
+ conversation_id: record.conversationId
8732
+ }),
8733
+ "utf8"
8734
+ ).toString("base64url");
8735
+ }
8736
+ function encodeConversationListCursorFromSummary(summary) {
8737
+ return encodeConversationListCursor({
8738
+ conversationId: summary.id,
8739
+ updatedAt: summary.updated_at
8740
+ });
8741
+ }
8742
+ function decodeConversationListCursor(value) {
8743
+ if (!value) {
8744
+ return null;
8745
+ }
8746
+ try {
8747
+ const decoded = JSON.parse(
8748
+ Buffer.from(value, "base64url").toString("utf8")
8749
+ );
8750
+ const updatedAt = readNonEmptyString(decoded.updated_at);
8751
+ const conversationId = readNonEmptyString(decoded.conversation_id);
8752
+ if (!updatedAt || !conversationId) {
8753
+ throw new Error("invalid cursor");
8754
+ }
8755
+ return { updatedAt, conversationId };
8756
+ } catch {
8757
+ throw new LinkHttpError(
8758
+ 400,
8759
+ "conversation_cursor_invalid",
8760
+ "Conversation list cursor is invalid"
8761
+ );
8762
+ }
8763
+ }
8764
+ function readNonEmptyString(value) {
8765
+ return typeof value === "string" && value.trim() ? value.trim() : null;
8766
+ }
8767
+ function normalizeConversationSearchQuery(value) {
8768
+ return typeof value === "string" ? value.trim() : "";
8769
+ }
8770
+ function isAfterConversationListCursor(summary, cursor) {
8771
+ return summary.updated_at < cursor.updatedAt || summary.updated_at === cursor.updatedAt && summary.id < cursor.conversationId;
8772
+ }
8397
8773
  function hydrateAgentEventBlocks(blocks, agentEvents) {
8398
8774
  if (!blocks?.length || agentEvents.length === 0) {
8399
8775
  return blocks;
@@ -8418,11 +8794,11 @@ function hydrateAgentEventBlocks(blocks, agentEvents) {
8418
8794
  // src/conversations/conversation-store.ts
8419
8795
  import {
8420
8796
  appendFile as appendFile2,
8421
- mkdir as mkdir8,
8422
- readdir as readdir4,
8797
+ mkdir as mkdir7,
8798
+ readdir as readdir5,
8423
8799
  readFile as readFile7,
8424
8800
  rm as rm5,
8425
- writeFile as writeFile3
8801
+ writeFile as writeFile2
8426
8802
  } from "fs/promises";
8427
8803
  import path12 from "path";
8428
8804
  var ConversationStore = class {
@@ -8431,14 +8807,14 @@ var ConversationStore = class {
8431
8807
  }
8432
8808
  paths;
8433
8809
  async ensureConversationsDir() {
8434
- await mkdir8(this.paths.conversationsDir, { recursive: true, mode: 448 });
8810
+ await mkdir7(this.paths.conversationsDir, { recursive: true, mode: 448 });
8435
8811
  }
8436
8812
  async listConversationIds() {
8437
8813
  await this.ensureConversationsDir();
8438
- const entries = await readdir4(this.paths.conversationsDir, {
8814
+ const entries = await readdir5(this.paths.conversationsDir, {
8439
8815
  withFileTypes: true
8440
8816
  }).catch((error) => {
8441
- if (isNodeError8(error, "ENOENT")) {
8817
+ if (isNodeError9(error, "ENOENT")) {
8442
8818
  return [];
8443
8819
  }
8444
8820
  throw error;
@@ -8446,7 +8822,7 @@ var ConversationStore = class {
8446
8822
  return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
8447
8823
  }
8448
8824
  async createConversation(manifest, snapshot = createEmptySnapshot2()) {
8449
- await mkdir8(this.conversationDir(manifest.id), {
8825
+ await mkdir7(this.conversationDir(manifest.id), {
8450
8826
  recursive: true,
8451
8827
  mode: 448
8452
8828
  });
@@ -8486,7 +8862,7 @@ var ConversationStore = class {
8486
8862
  conversation_id: conversationId,
8487
8863
  created_at: now
8488
8864
  };
8489
- await mkdir8(this.conversationDir(conversationId), {
8865
+ await mkdir7(this.conversationDir(conversationId), {
8490
8866
  recursive: true,
8491
8867
  mode: 448
8492
8868
  });
@@ -8507,7 +8883,7 @@ var ConversationStore = class {
8507
8883
  await this.readManifest(conversationId);
8508
8884
  const raw = await readFile7(this.eventsPath(conversationId), "utf8").catch(
8509
8885
  (error) => {
8510
- if (isNodeError8(error, "ENOENT")) {
8886
+ if (isNodeError9(error, "ENOENT")) {
8511
8887
  return "";
8512
8888
  }
8513
8889
  throw error;
@@ -8517,7 +8893,7 @@ var ConversationStore = class {
8517
8893
  }
8518
8894
  overwriteEvents(conversationId, events) {
8519
8895
  const content = events.map((event) => JSON.stringify(event)).join("\n");
8520
- return writeFile3(
8896
+ return writeFile2(
8521
8897
  this.eventsPath(conversationId),
8522
8898
  content ? `${content}
8523
8899
  ` : "",
@@ -8562,19 +8938,18 @@ var ConversationStore = class {
8562
8938
  function createEmptySnapshot2() {
8563
8939
  return { schema_version: 1, messages: [], runs: [] };
8564
8940
  }
8565
- function isNodeError8(error, code) {
8941
+ function isNodeError9(error, code) {
8566
8942
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
8567
8943
  }
8568
8944
 
8569
8945
  // src/conversations/hermes-session-sync.ts
8570
8946
  import { randomUUID as randomUUID6 } from "crypto";
8571
- import { readdir as readdir6, readFile as readFile9, stat as stat8 } from "fs/promises";
8572
- import { createRequire as createRequire3 } from "module";
8947
+ import { readdir as readdir7, readFile as readFile9, stat as stat9 } from "fs/promises";
8573
8948
  import os4 from "os";
8574
8949
  import path14 from "path";
8575
8950
 
8576
8951
  // src/conversations/delivery-import.ts
8577
- import { lstat, readFile as readFile8, readdir as readdir5, stat as stat7 } from "fs/promises";
8952
+ import { lstat as lstat2, readFile as readFile8, readdir as readdir6, stat as stat8 } from "fs/promises";
8578
8953
  import path13 from "path";
8579
8954
  var MAX_IMPORTED_BLOB_BYTES = 100 * 1024 * 1024;
8580
8955
  var MAX_MEDIA_IMPORT_FAILURES = 20;
@@ -8670,8 +9045,8 @@ function resolveDeliveryStagingTarget(paths, stagingDir) {
8670
9045
  };
8671
9046
  }
8672
9047
  async function collectStagedDeliveryReferences(stagingDir) {
8673
- const directoryStat = await lstat(stagingDir).catch((error) => {
8674
- if (isNodeError9(error, "ENOENT")) {
9048
+ const directoryStat = await lstat2(stagingDir).catch((error) => {
9049
+ if (isNodeError10(error, "ENOENT")) {
8675
9050
  throw new LinkHttpError(
8676
9051
  404,
8677
9052
  "delivery_staging_not_found",
@@ -8687,7 +9062,7 @@ async function collectStagedDeliveryReferences(stagingDir) {
8687
9062
  "delivery staging path is not a directory"
8688
9063
  );
8689
9064
  }
8690
- const entries = await readdir5(stagingDir, { withFileTypes: true });
9065
+ const entries = await readdir6(stagingDir, { withFileTypes: true });
8691
9066
  return entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).filter((entry) => isSupportedDeliveryFilename(entry.name)).sort(
8692
9067
  (left, right) => left.name.localeCompare(right.name, "en", { numeric: true })
8693
9068
  ).slice(0, MAX_DELIVERY_FILES).map((entry) => {
@@ -8851,8 +9226,8 @@ function emptyImportResult(input) {
8851
9226
  }
8852
9227
  async function writeBlobFromFile(deps, conversationId, source) {
8853
9228
  const sourcePath = resolveMediaSourcePath(source.path);
8854
- const fileStat = await stat7(sourcePath).catch((error) => {
8855
- if (isNodeError9(error, "ENOENT")) {
9229
+ const fileStat = await stat8(sourcePath).catch((error) => {
9230
+ if (isNodeError10(error, "ENOENT")) {
8856
9231
  throw new LinkHttpError(
8857
9232
  404,
8858
9233
  "media_source_not_found",
@@ -8886,7 +9261,7 @@ function describeMediaImportFailure(reference, sourceKey, error) {
8886
9261
  key: sourceKey,
8887
9262
  filename: sanitizeFilename(reference.path, "attachment"),
8888
9263
  reason: error instanceof Error ? error.message : String(error),
8889
- ...isNodeError9(error) && error.code ? { code: error.code } : {}
9264
+ ...isNodeError10(error) && error.code ? { code: error.code } : {}
8890
9265
  };
8891
9266
  }
8892
9267
  function isSupportedDeliveryFilename(filename) {
@@ -8899,12 +9274,11 @@ function readString8(payload, key) {
8899
9274
  function toRecord7(value) {
8900
9275
  return typeof value === "object" && value !== null ? value : {};
8901
9276
  }
8902
- function isNodeError9(error, code) {
9277
+ function isNodeError10(error, code) {
8903
9278
  return typeof error === "object" && error !== null && "code" in error && (code === void 0 || error.code === code);
8904
9279
  }
8905
9280
 
8906
9281
  // src/conversations/hermes-session-sync.ts
8907
- var nodeRequire3 = createRequire3(import.meta.url);
8908
9282
  var PROFILE_NAME_PATTERN3 = /^[a-zA-Z0-9._-]{1,64}$/u;
8909
9283
  var DEFAULT_PROFILE_NAME = "default";
8910
9284
  var MAX_IMPORTABLE_SESSIONS = 100;
@@ -9667,9 +10041,9 @@ function rememberKnownHermesConversation(map, sessionId, conversationId) {
9667
10041
  async function discoverHermesProfileNames() {
9668
10042
  const names = /* @__PURE__ */ new Set([DEFAULT_PROFILE_NAME]);
9669
10043
  const profilesDir = path14.join(os4.homedir(), ".hermes", "profiles");
9670
- const entries = await readdir6(profilesDir, { withFileTypes: true }).catch(
10044
+ const entries = await readdir7(profilesDir, { withFileTypes: true }).catch(
9671
10045
  (error) => {
9672
- if (isNodeError10(error, "ENOENT")) {
10046
+ if (isNodeError11(error, "ENOENT")) {
9673
10047
  return [];
9674
10048
  }
9675
10049
  throw error;
@@ -9696,11 +10070,8 @@ async function listProfileSessions(dbPath) {
9696
10070
  }
9697
10071
  let db = null;
9698
10072
  try {
9699
- const { DatabaseSync } = nodeRequire3(
9700
- "node:sqlite"
9701
- );
9702
- db = new DatabaseSync(dbPath, {
9703
- readOnly: true,
10073
+ db = openSqliteDatabase(dbPath, {
10074
+ readonly: true,
9704
10075
  timeout: 1e3
9705
10076
  });
9706
10077
  const sessionColumns = readTableColumns(db, "sessions");
@@ -9840,11 +10211,8 @@ async function readStateDbMessages(dbPath, sessionId) {
9840
10211
  }
9841
10212
  let db = null;
9842
10213
  try {
9843
- const { DatabaseSync } = nodeRequire3(
9844
- "node:sqlite"
9845
- );
9846
- db = new DatabaseSync(dbPath, {
9847
- readOnly: true,
10214
+ db = openSqliteDatabase(dbPath, {
10215
+ readonly: true,
9848
10216
  timeout: 1e3
9849
10217
  });
9850
10218
  const columns = readTableColumns(db, "messages");
@@ -9876,7 +10244,7 @@ async function readJsonlMessages(profileName, sessionId) {
9876
10244
  const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path14.join(profileDir, "sessions"));
9877
10245
  const transcriptPath = path14.join(sessionsDir, `${sessionId}.jsonl`);
9878
10246
  const raw = await readFile9(transcriptPath, "utf8").catch((error) => {
9879
- if (isNodeError10(error, "ENOENT")) {
10247
+ if (isNodeError11(error, "ENOENT")) {
9880
10248
  return "";
9881
10249
  }
9882
10250
  throw error;
@@ -10151,8 +10519,8 @@ function quoteIdentifier(value) {
10151
10519
  return `"${value.replaceAll('"', '""')}"`;
10152
10520
  }
10153
10521
  async function isFile(filePath) {
10154
- return stat8(filePath).then((value) => value.isFile()).catch((error) => {
10155
- if (isNodeError10(error, "ENOENT")) {
10522
+ return stat9(filePath).then((value) => value.isFile()).catch((error) => {
10523
+ if (isNodeError11(error, "ENOENT")) {
10156
10524
  return false;
10157
10525
  }
10158
10526
  throw error;
@@ -10185,12 +10553,12 @@ function readBoolean(value) {
10185
10553
  }
10186
10554
  return false;
10187
10555
  }
10188
- function isNodeError10(error, code) {
10556
+ function isNodeError11(error, code) {
10189
10557
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
10190
10558
  }
10191
10559
 
10192
10560
  // src/conversations/run-lifecycle.ts
10193
- import { readdir as readdir7 } from "fs/promises";
10561
+ import { readdir as readdir8 } from "fs/promises";
10194
10562
 
10195
10563
  // src/hermes/api-server.ts
10196
10564
  async function listHermesModels(options = {}) {
@@ -10584,10 +10952,8 @@ function readString10(payload, key) {
10584
10952
  }
10585
10953
 
10586
10954
  // src/conversations/history-builder.ts
10587
- import { readFile as readFile10, stat as stat9 } from "fs/promises";
10588
- import { createRequire as createRequire4 } from "module";
10955
+ import { readFile as readFile10, stat as stat10 } from "fs/promises";
10589
10956
  import path15 from "path";
10590
- var nodeRequire4 = createRequire4(import.meta.url);
10591
10957
  var HISTORY_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
10592
10958
  var HERMES_HISTORY_COLUMNS = [
10593
10959
  "role",
@@ -10694,8 +11060,8 @@ async function readHermesTranscriptHistory(sessionId, profileName) {
10694
11060
  };
10695
11061
  }
10696
11062
  async function readHermesStateDbHistory(dbPath, sessionId) {
10697
- const exists = await stat9(dbPath).then((value) => value.isFile()).catch((error) => {
10698
- if (isNodeError11(error, "ENOENT")) {
11063
+ const exists = await stat10(dbPath).then((value) => value.isFile()).catch((error) => {
11064
+ if (isNodeError12(error, "ENOENT")) {
10699
11065
  return false;
10700
11066
  }
10701
11067
  throw error;
@@ -10714,7 +11080,7 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
10714
11080
  }
10715
11081
  const transcriptPath = path15.join(sessionsDir, `${sessionId}.jsonl`);
10716
11082
  const raw = await readFile10(transcriptPath, "utf8").catch((error) => {
10717
- if (isNodeError11(error, "ENOENT")) {
11083
+ if (isNodeError12(error, "ENOENT")) {
10718
11084
  return "";
10719
11085
  }
10720
11086
  throw error;
@@ -10749,11 +11115,8 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
10749
11115
  function readHistoryRows(dbPath, sessionId) {
10750
11116
  let db = null;
10751
11117
  try {
10752
- const { DatabaseSync } = nodeRequire4(
10753
- "node:sqlite"
10754
- );
10755
- db = new DatabaseSync(dbPath, {
10756
- readOnly: true,
11118
+ db = openSqliteDatabase(dbPath, {
11119
+ readonly: true,
10757
11120
  timeout: 1e3
10758
11121
  });
10759
11122
  const columns = readTableColumns2(db, "messages");
@@ -10939,7 +11302,7 @@ function readTableColumns2(db, table) {
10939
11302
  db.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name).filter((name) => typeof name === "string")
10940
11303
  );
10941
11304
  }
10942
- function isNodeError11(error, code) {
11305
+ function isNodeError12(error, code) {
10943
11306
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
10944
11307
  }
10945
11308
  function isValidProfileName2(value) {
@@ -10957,7 +11320,7 @@ function normalizeProfileForCompare(value) {
10957
11320
 
10958
11321
  // src/hermes/stt.ts
10959
11322
  import { execFile as execFile3 } from "child_process";
10960
- import { access as access2, readFile as readFile11, stat as stat10 } from "fs/promises";
11323
+ import { access as access2, readFile as readFile11, stat as stat11 } from "fs/promises";
10961
11324
  import path16 from "path";
10962
11325
  import { promisify as promisify3 } from "util";
10963
11326
  var execFileAsync3 = promisify3(execFile3);
@@ -11117,7 +11480,7 @@ async function resolveExecutablePath(command) {
11117
11480
  }
11118
11481
  async function isExecutableFile(filePath) {
11119
11482
  try {
11120
- const info = await stat10(filePath);
11483
+ const info = await stat11(filePath);
11121
11484
  if (!info.isFile()) {
11122
11485
  return false;
11123
11486
  }
@@ -11158,7 +11521,7 @@ async function findDevHermesAgentSource() {
11158
11521
  return null;
11159
11522
  }
11160
11523
  async function isDirectory(candidate) {
11161
- return stat10(candidate).then((info) => info.isDirectory()).catch(() => false);
11524
+ return stat11(candidate).then((info) => info.isDirectory()).catch(() => false);
11162
11525
  }
11163
11526
  function compactProcessOutput(value) {
11164
11527
  const compact = value.trim().replace(/\s+/gu, " ").slice(0, 500);
@@ -12402,8 +12765,8 @@ function formatFilenameList(filenames) {
12402
12765
  return remaining > 0 ? `${preview.join("\u3001")} \u7B49 ${filenames.length} \u4E2A` : preview.join("\u3001");
12403
12766
  }
12404
12767
  async function readdirWithDirs(directory) {
12405
- return readdir7(directory, { withFileTypes: true }).catch((error) => {
12406
- if (isNodeError12(error, "ENOENT")) {
12768
+ return readdir8(directory, { withFileTypes: true }).catch((error) => {
12769
+ if (isNodeError13(error, "ENOENT")) {
12407
12770
  return [];
12408
12771
  }
12409
12772
  throw error;
@@ -12633,7 +12996,7 @@ function readResponseId(payload) {
12633
12996
  const response = toRecord11(payload.response);
12634
12997
  return readString13(payload, "response_id") ?? readString13(response, "id");
12635
12998
  }
12636
- function isNodeError12(error, code) {
12999
+ function isNodeError13(error, code) {
12637
13000
  if (typeof error !== "object" || error === null || !("code" in error)) {
12638
13001
  return false;
12639
13002
  }
@@ -12744,6 +13107,12 @@ var ConversationService = class {
12744
13107
  async listConversations() {
12745
13108
  return this.queries.listConversations();
12746
13109
  }
13110
+ listConversationPage(input = {}) {
13111
+ return this.queries.listConversationPage(input);
13112
+ }
13113
+ searchConversationPage(input = {}) {
13114
+ return this.queries.searchConversationPage(input);
13115
+ }
12747
13116
  async getStatistics(filter = {}) {
12748
13117
  return readLinkStatistics(this.paths, filter);
12749
13118
  }
@@ -13463,7 +13832,7 @@ function findApproval(snapshot, approvalId) {
13463
13832
 
13464
13833
  // src/identity/identity.ts
13465
13834
  import { generateKeyPairSync, randomUUID as randomUUID8, sign } from "crypto";
13466
- import { mkdir as mkdir9, chmod } from "fs/promises";
13835
+ import { mkdir as mkdir8, chmod as chmod2 } from "fs/promises";
13467
13836
  import { z } from "zod";
13468
13837
  var linkIdentitySchema = z.object({
13469
13838
  install_id: z.string().min(1),
@@ -13485,8 +13854,8 @@ async function ensureIdentity(paths = resolveRuntimePaths()) {
13485
13854
  if (existing) {
13486
13855
  return existing;
13487
13856
  }
13488
- await mkdir9(paths.homeDir, { recursive: true, mode: 448 });
13489
- await chmod(paths.homeDir, 448).catch(() => void 0);
13857
+ await mkdir8(paths.homeDir, { recursive: true, mode: 448 });
13858
+ await chmod2(paths.homeDir, 448).catch(() => void 0);
13490
13859
  const { publicKey, privateKey } = generateKeyPairSync("ed25519");
13491
13860
  const now = (/* @__PURE__ */ new Date()).toISOString();
13492
13861
  const identity = {
@@ -14291,9 +14660,28 @@ function registerConversationRoutes(router, options) {
14291
14660
  router.get("/api/v1/conversations", async (ctx) => {
14292
14661
  await authenticateRequest(ctx, paths);
14293
14662
  ctx.set("cache-control", "no-store");
14663
+ const result = await conversations.listConversationPage({
14664
+ limit: readLimit(ctx.query.limit),
14665
+ cursor: readQueryString(ctx.query.cursor) ?? readQueryString(ctx.query.after) ?? readQueryString(ctx.query.page_cursor)
14666
+ });
14294
14667
  ctx.body = {
14295
14668
  ok: true,
14296
- conversations: await conversations.listConversations()
14669
+ conversations: result.conversations,
14670
+ page: result.page
14671
+ };
14672
+ });
14673
+ router.get("/api/v1/conversations/search", async (ctx) => {
14674
+ await authenticateRequest(ctx, paths);
14675
+ ctx.set("cache-control", "no-store");
14676
+ const result = await conversations.searchConversationPage({
14677
+ limit: readLimit(ctx.query.limit),
14678
+ cursor: readQueryString(ctx.query.cursor) ?? readQueryString(ctx.query.after) ?? readQueryString(ctx.query.page_cursor),
14679
+ query: readQueryString(ctx.query.query) ?? readQueryString(ctx.query.q) ?? readQueryString(ctx.query.keyword) ?? ""
14680
+ });
14681
+ ctx.body = {
14682
+ ok: true,
14683
+ conversations: result.conversations,
14684
+ page: result.page
14297
14685
  };
14298
14686
  });
14299
14687
  router.post("/api/v1/conversations", async (ctx) => {
@@ -14661,7 +15049,7 @@ function createHttpErrorMiddleware(logger) {
14661
15049
  }
14662
15050
 
14663
15051
  // src/hermes/profiles.ts
14664
- import { mkdir as mkdir10, readdir as readdir8, readFile as readFile12, rename as rename4, rm as rm6, stat as stat11 } from "fs/promises";
15052
+ import { readdir as readdir9, readFile as readFile12, rename as rename3, rm as rm6, stat as stat12 } from "fs/promises";
14665
15053
  import os5 from "os";
14666
15054
  import path17 from "path";
14667
15055
  import YAML2 from "yaml";
@@ -14671,9 +15059,9 @@ async function listHermesProfiles(paths = resolveRuntimePaths()) {
14671
15059
  const profiles = /* @__PURE__ */ new Map();
14672
15060
  profiles.set(DEFAULT_PROFILE, await profileInfo(DEFAULT_PROFILE, paths));
14673
15061
  const profilesDir = path17.join(os5.homedir(), ".hermes", "profiles");
14674
- const entries = await readdir8(profilesDir, { withFileTypes: true }).catch(
15062
+ const entries = await readdir9(profilesDir, { withFileTypes: true }).catch(
14675
15063
  (error) => {
14676
- if (isNodeError13(error, "ENOENT")) {
15064
+ if (isNodeError14(error, "ENOENT")) {
14677
15065
  return [];
14678
15066
  }
14679
15067
  throw error;
@@ -14697,8 +15085,8 @@ async function listHermesProfiles(paths = resolveRuntimePaths()) {
14697
15085
  async function getHermesProfileStatus(name, paths = resolveRuntimePaths()) {
14698
15086
  assertProfileName(name);
14699
15087
  const profile = await profileInfo(name, paths);
14700
- const exists = await stat11(profile.path).then((value) => value.isDirectory()).catch((error) => {
14701
- if (isNodeError13(error, "ENOENT")) {
15088
+ const exists = await stat12(profile.path).then((value) => value.isDirectory()).catch((error) => {
15089
+ if (isNodeError14(error, "ENOENT")) {
14702
15090
  return false;
14703
15091
  }
14704
15092
  throw error;
@@ -14715,7 +15103,7 @@ async function renameHermesProfile(oldName, newName, paths = resolveRuntimePaths
14715
15103
  assertMutableProfile(newName);
14716
15104
  const oldProfile = await profileInfo(oldName, paths);
14717
15105
  const newProfilePath = resolveHermesProfileDir(newName);
14718
- await rename4(oldProfile.path, newProfilePath);
15106
+ await rename3(oldProfile.path, newProfilePath);
14719
15107
  const identity = await renameProfileIdentity(paths, {
14720
15108
  oldProfileName: oldName,
14721
15109
  newProfileName: newName,
@@ -14738,8 +15126,8 @@ async function updateHermesProfileMetadata(name, metadata, paths = resolveRuntim
14738
15126
  async function deleteHermesProfile(name, paths = resolveRuntimePaths()) {
14739
15127
  assertMutableProfile(name);
14740
15128
  const profile = await profileInfo(name, paths);
14741
- const exists = await stat11(profile.path).then((value) => value.isDirectory()).catch((error) => {
14742
- if (isNodeError13(error, "ENOENT")) {
15129
+ const exists = await stat12(profile.path).then((value) => value.isDirectory()).catch((error) => {
15130
+ if (isNodeError14(error, "ENOENT")) {
14743
15131
  return false;
14744
15132
  }
14745
15133
  throw error;
@@ -14807,13 +15195,13 @@ function assertProfileName(name) {
14807
15195
  throw new LinkHttpError(400, "invalid_profile_name", "invalid profile name");
14808
15196
  }
14809
15197
  }
14810
- function isNodeError13(error, code) {
15198
+ function isNodeError14(error, code) {
14811
15199
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
14812
15200
  }
14813
15201
  async function countSkills(root) {
14814
- const entries = await readdir8(root, { withFileTypes: true }).catch(
15202
+ const entries = await readdir9(root, { withFileTypes: true }).catch(
14815
15203
  (error) => {
14816
- if (isNodeError13(error, "ENOENT")) {
15204
+ if (isNodeError14(error, "ENOENT")) {
14817
15205
  return [];
14818
15206
  }
14819
15207
  throw error;
@@ -14840,7 +15228,7 @@ async function countConfiguredTools(profileName) {
14840
15228
  resolveHermesConfigPath(profileName),
14841
15229
  "utf8"
14842
15230
  ).catch((error) => {
14843
- if (isNodeError13(error, "ENOENT")) {
15231
+ if (isNodeError14(error, "ENOENT")) {
14844
15232
  return "";
14845
15233
  }
14846
15234
  throw error;
@@ -15517,14 +15905,11 @@ function errorMessage(error) {
15517
15905
  import { spawn as spawn2 } from "child_process";
15518
15906
  import { EventEmitter as EventEmitter2 } from "events";
15519
15907
  import {
15520
- copyFile as copyFile2,
15521
15908
  cp,
15522
- mkdir as mkdir11,
15909
+ mkdir as mkdir9,
15523
15910
  readFile as readFile13,
15524
15911
  rm as rm7,
15525
- stat as stat12,
15526
- writeFile as writeFile4,
15527
- rename as rename5
15912
+ stat as stat13
15528
15913
  } from "fs/promises";
15529
15914
  import path18 from "path";
15530
15915
  import YAML3 from "yaml";
@@ -15574,7 +15959,7 @@ async function startHermesProfileCreation(input, options) {
15574
15959
  signal: null,
15575
15960
  error: null
15576
15961
  };
15577
- await mkdir11(options.paths.runDir, { recursive: true, mode: 448 });
15962
+ await mkdir9(options.paths.runDir, { recursive: true, mode: 448 });
15578
15963
  await writeProfileCreationState(options.paths, started);
15579
15964
  await writer.write(`
15580
15965
  === profile creation started ${startedAt} ===
@@ -15851,7 +16236,7 @@ function randomSuffix(attempt) {
15851
16236
  async function applyProfileCreationPostSteps(input) {
15852
16237
  const profilePath = resolveHermesProfileDir(input.profileName);
15853
16238
  if (!await pathExists(profilePath)) {
15854
- await mkdir11(profilePath, { recursive: true, mode: 448 });
16239
+ await ensureDirectoryWithInheritedMetadata(profilePath, 448);
15855
16240
  }
15856
16241
  if (input.sourceProfile && input.copyScopes.length > 0) {
15857
16242
  await copyProfileScopes({
@@ -15979,7 +16364,7 @@ function collectEnvKeys(value, keys = /* @__PURE__ */ new Set()) {
15979
16364
  async function writeEnvValues(profileName, values) {
15980
16365
  const envPath = path18.join(resolveHermesProfileDir(profileName), ".env");
15981
16366
  const existingRaw = await readFile13(envPath, "utf8").catch((error) => {
15982
- if (isNodeError14(error, "ENOENT")) {
16367
+ if (isNodeError15(error, "ENOENT")) {
15983
16368
  return "";
15984
16369
  }
15985
16370
  throw error;
@@ -16004,13 +16389,14 @@ async function writeEnvValues(profileName, values) {
16004
16389
  nextLines.push(`${key}=${formatEnvValue2(value)}`);
16005
16390
  }
16006
16391
  const nextRaw = nextLines.join("\n").replace(/\n*$/u, "\n");
16007
- await mkdir11(path18.dirname(envPath), { recursive: true, mode: 448 });
16008
16392
  if (existingRaw) {
16009
- await copyFile2(envPath, `${envPath}.bak.${Date.now()}`);
16393
+ await atomicWriteFilePreservingMetadata(
16394
+ `${envPath}.bak.${Date.now()}`,
16395
+ existingRaw,
16396
+ { metadataSourcePath: envPath }
16397
+ );
16010
16398
  }
16011
- const tempPath = `${envPath}.tmp.${process.pid}.${Date.now()}`;
16012
- await writeFile4(tempPath, nextRaw, { mode: 384 });
16013
- await rename5(tempPath, envPath);
16399
+ await atomicWriteFilePreservingMetadata(envPath, nextRaw);
16014
16400
  }
16015
16401
  async function copySkills(sourceProfile, targetProfile) {
16016
16402
  const sourceSkills = path18.join(resolveHermesProfileDir(sourceProfile), "skills");
@@ -16024,6 +16410,10 @@ async function copySkills(sourceProfile, targetProfile) {
16024
16410
  force: true,
16025
16411
  errorOnExist: false
16026
16412
  });
16413
+ await inheritOwnerRecursively(
16414
+ targetSkills,
16415
+ resolveHermesProfileDir(targetProfile)
16416
+ );
16027
16417
  }
16028
16418
  function copyProperty(source, target, key) {
16029
16419
  if (Object.prototype.hasOwnProperty.call(source, key)) {
@@ -16035,7 +16425,7 @@ function copyProperty(source, target, key) {
16035
16425
  async function readYamlConfig(configPath) {
16036
16426
  const existingRaw = await readFile13(configPath, "utf8").catch(
16037
16427
  (error) => {
16038
- if (isNodeError14(error, "ENOENT")) {
16428
+ if (isNodeError15(error, "ENOENT")) {
16039
16429
  return null;
16040
16430
  }
16041
16431
  throw error;
@@ -16047,14 +16437,15 @@ async function readYamlConfig(configPath) {
16047
16437
  };
16048
16438
  }
16049
16439
  async function writeYamlConfig(configPath, input) {
16050
- await mkdir11(path18.dirname(configPath), { recursive: true, mode: 448 });
16051
16440
  if (input.existingRaw) {
16052
- await copyFile2(configPath, `${configPath}.bak.${Date.now()}`);
16441
+ await atomicWriteFilePreservingMetadata(
16442
+ `${configPath}.bak.${Date.now()}`,
16443
+ input.existingRaw,
16444
+ { metadataSourcePath: configPath }
16445
+ );
16053
16446
  }
16054
16447
  const document = new YAML3.Document(input.config);
16055
- const tempPath = `${configPath}.tmp.${process.pid}.${Date.now()}`;
16056
- await writeFile4(tempPath, document.toString(), { mode: 384 });
16057
- await rename5(tempPath, configPath);
16448
+ await atomicWriteFilePreservingMetadata(configPath, document.toString());
16058
16449
  }
16059
16450
  async function failProfileCreation(input) {
16060
16451
  if (input.rollbackProfileName) {
@@ -16120,8 +16511,8 @@ async function clearProfileCreationLogFiles(paths) {
16120
16511
  ]);
16121
16512
  }
16122
16513
  async function pathExists(targetPath) {
16123
- return await stat12(targetPath).then(() => true).catch((error) => {
16124
- if (isNodeError14(error, "ENOENT")) {
16514
+ return await stat13(targetPath).then(() => true).catch((error) => {
16515
+ if (isNodeError15(error, "ENOENT")) {
16125
16516
  return false;
16126
16517
  }
16127
16518
  throw error;
@@ -16168,7 +16559,7 @@ function formatEnvValue2(value) {
16168
16559
  function escapeRegExp2(value) {
16169
16560
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
16170
16561
  }
16171
- function isNodeError14(error, code) {
16562
+ function isNodeError15(error, code) {
16172
16563
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
16173
16564
  }
16174
16565
 
@@ -16377,13 +16768,9 @@ function toProfileToolConfigHttpError(error) {
16377
16768
  // src/hermes/memory.ts
16378
16769
  import {
16379
16770
  access as access3,
16380
- copyFile as copyFile3,
16381
- mkdir as mkdir12,
16382
- readdir as readdir9,
16771
+ readdir as readdir10,
16383
16772
  readFile as readFile14,
16384
- rename as rename6,
16385
- stat as stat13,
16386
- writeFile as writeFile5
16773
+ stat as stat14
16387
16774
  } from "fs/promises";
16388
16775
  import path19 from "path";
16389
16776
  import YAML4 from "yaml";
@@ -16696,12 +17083,11 @@ function isSensitiveConfigKey(key) {
16696
17083
  }
16697
17084
  async function writeCustomProviderConfig(profileName, provider, config) {
16698
17085
  const configPath = customProviderConfigPath(profileName, provider);
16699
- await mkdir12(path19.dirname(configPath), { recursive: true, mode: 448 });
16700
- await writeFile5(configPath, `${JSON.stringify(config, null, 2)}
16701
- `, {
16702
- encoding: "utf8",
16703
- mode: 384
16704
- });
17086
+ await atomicWriteFilePreservingMetadata(
17087
+ configPath,
17088
+ `${JSON.stringify(config, null, 2)}
17089
+ `
17090
+ );
16705
17091
  }
16706
17092
  function normalizeConfigurableProvider(provider, patch) {
16707
17093
  if (provider === CUSTOM_PROVIDER_CARD_ID) {
@@ -16749,7 +17135,7 @@ async function patchHermesMemoryProvider(profileName, provider) {
16749
17135
  const configPath = resolveHermesConfigPath(profileName);
16750
17136
  const existingRaw = await readFile14(configPath, "utf8").catch(
16751
17137
  (error) => {
16752
- if (isNodeError15(error, "ENOENT")) {
17138
+ if (isNodeError16(error, "ENOENT")) {
16753
17139
  return null;
16754
17140
  }
16755
17141
  throw error;
@@ -16761,17 +17147,13 @@ async function patchHermesMemoryProvider(profileName, provider) {
16761
17147
  memory.provider = provider === "built-in" ? "" : provider;
16762
17148
  config.memory = memory;
16763
17149
  const backupPath = existingRaw ? `${configPath}.bak.${Date.now()}` : null;
16764
- await mkdir12(path19.dirname(configPath), { recursive: true, mode: 448 });
16765
- if (backupPath) {
16766
- await copyFile3(configPath, backupPath);
17150
+ if (backupPath && existingRaw !== null) {
17151
+ await atomicWriteFilePreservingMetadata(backupPath, existingRaw, {
17152
+ metadataSourcePath: configPath
17153
+ });
16767
17154
  }
16768
17155
  document.contents = document.createNode(config);
16769
- const tempPath = `${configPath}.tmp.${process.pid}.${Date.now()}`;
16770
- await writeFile5(tempPath, document.toString(), {
16771
- encoding: "utf8",
16772
- mode: 384
16773
- });
16774
- await rename6(tempPath, configPath);
17156
+ await atomicWriteFilePreservingMetadata(configPath, document.toString());
16775
17157
  }
16776
17158
  function resolveMemoryDir(profileName) {
16777
17159
  return path19.join(resolveHermesProfileDir(profileName), "memories");
@@ -16779,8 +17161,8 @@ function resolveMemoryDir(profileName) {
16779
17161
  async function readMemoryStore(profileName, target, limits) {
16780
17162
  const filePath = memoryFilePath(profileName, target);
16781
17163
  const entries = await readMemoryEntries(filePath);
16782
- const fileStat = await stat13(filePath).catch((error) => {
16783
- if (isNodeError15(error, "ENOENT")) {
17164
+ const fileStat = await stat14(filePath).catch((error) => {
17165
+ if (isNodeError16(error, "ENOENT")) {
16784
17166
  return null;
16785
17167
  }
16786
17168
  throw error;
@@ -16809,7 +17191,7 @@ async function readMemoryStore(profileName, target, limits) {
16809
17191
  }
16810
17192
  async function readMemoryEntries(filePath) {
16811
17193
  const raw = await readFile14(filePath, "utf8").catch((error) => {
16812
- if (isNodeError15(error, "ENOENT")) {
17194
+ if (isNodeError16(error, "ENOENT")) {
16813
17195
  return "";
16814
17196
  }
16815
17197
  throw error;
@@ -16829,17 +17211,10 @@ async function mutateMemoryEntries(profileName, target, mutate) {
16829
17211
  async function writeMemoryEntries(profileName, target, entries) {
16830
17212
  assertWithinLimit(target, entries, await readMemoryLimits(profileName));
16831
17213
  const filePath = memoryFilePath(profileName, target);
16832
- const dir = path19.dirname(filePath);
16833
- await mkdir12(dir, { recursive: true, mode: 448 });
16834
- const tempPath = path19.join(
16835
- dir,
16836
- `.mem_${process.pid}_${Date.now()}_${target}.tmp`
17214
+ await atomicWriteFilePreservingMetadata(
17215
+ filePath,
17216
+ entries.join(ENTRY_DELIMITER)
16837
17217
  );
16838
- await writeFile5(tempPath, entries.join(ENTRY_DELIMITER), {
16839
- encoding: "utf8",
16840
- mode: 384
16841
- });
16842
- await rename6(tempPath, filePath);
16843
17218
  }
16844
17219
  function memoryFilePath(profileName, target) {
16845
17220
  return path19.join(
@@ -17320,7 +17695,7 @@ function customProviderRegistryPath(profileName) {
17320
17695
  async function readCustomProviderRegistry(profileName) {
17321
17696
  const raw = await readFile14(customProviderRegistryPath(profileName), "utf8").catch(
17322
17697
  (error) => {
17323
- if (isNodeError15(error, "ENOENT")) {
17698
+ if (isNodeError16(error, "ENOENT")) {
17324
17699
  return "";
17325
17700
  }
17326
17701
  throw error;
@@ -17357,19 +17732,17 @@ async function saveCustomProviderRegistryEntry(profileName, provider) {
17357
17732
  { id: providerId, label: providerId, description: "\u81EA\u5B9A\u4E49 memory provider\u3002" }
17358
17733
  ].sort((left, right) => left.id.localeCompare(right.id));
17359
17734
  const registryPath2 = customProviderRegistryPath(profileName);
17360
- await mkdir12(path19.dirname(registryPath2), { recursive: true, mode: 448 });
17361
- await writeFile5(
17735
+ await atomicWriteFilePreservingMetadata(
17362
17736
  registryPath2,
17363
17737
  `${JSON.stringify({ providers }, null, 2)}
17364
- `,
17365
- { encoding: "utf8", mode: 384 }
17738
+ `
17366
17739
  );
17367
17740
  }
17368
17741
  async function discoverUserMemoryProviderDescriptors(profileName) {
17369
17742
  const pluginsDir = path19.join(resolveHermesProfileDir(profileName), "plugins");
17370
- const entries = await readdir9(pluginsDir, { withFileTypes: true }).catch(
17743
+ const entries = await readdir10(pluginsDir, { withFileTypes: true }).catch(
17371
17744
  (error) => {
17372
- if (isNodeError15(error, "ENOENT")) {
17745
+ if (isNodeError16(error, "ENOENT")) {
17373
17746
  return [];
17374
17747
  }
17375
17748
  throw error;
@@ -17410,7 +17783,7 @@ async function isUserMemoryProviderInstalled(profileName, provider) {
17410
17783
  async function isMemoryProviderPluginDir(providerDir) {
17411
17784
  const source = await readFile14(path19.join(providerDir, "__init__.py"), "utf8").catch(
17412
17785
  (error) => {
17413
- if (isNodeError15(error, "ENOENT")) {
17786
+ if (isNodeError16(error, "ENOENT")) {
17414
17787
  return "";
17415
17788
  }
17416
17789
  throw error;
@@ -17422,7 +17795,7 @@ async function isMemoryProviderPluginDir(providerDir) {
17422
17795
  async function readPluginMetadata(providerDir) {
17423
17796
  const raw = await readFile14(path19.join(providerDir, "plugin.yaml"), "utf8").catch(
17424
17797
  (error) => {
17425
- if (isNodeError15(error, "ENOENT")) {
17798
+ if (isNodeError16(error, "ENOENT")) {
17426
17799
  return "";
17427
17800
  }
17428
17801
  throw error;
@@ -17448,7 +17821,7 @@ async function resolveByteRoverCli() {
17448
17821
  async function readHolographicProviderConfig(profileName) {
17449
17822
  const raw = await readFile14(resolveHermesConfigPath(profileName), "utf8").catch(
17450
17823
  (error) => {
17451
- if (isNodeError15(error, "ENOENT")) {
17824
+ if (isNodeError16(error, "ENOENT")) {
17452
17825
  return "";
17453
17826
  }
17454
17827
  throw error;
@@ -17462,7 +17835,7 @@ async function patchHolographicProviderConfig(profileName, patch) {
17462
17835
  const configPath = resolveHermesConfigPath(profileName);
17463
17836
  const existingRaw = await readFile14(configPath, "utf8").catch(
17464
17837
  (error) => {
17465
- if (isNodeError15(error, "ENOENT")) {
17838
+ if (isNodeError16(error, "ENOENT")) {
17466
17839
  return null;
17467
17840
  }
17468
17841
  throw error;
@@ -17479,17 +17852,15 @@ async function patchHolographicProviderConfig(profileName, patch) {
17479
17852
  }
17480
17853
  plugins["hermes-memory-store"] = memoryStore;
17481
17854
  config.plugins = plugins;
17482
- await mkdir12(path19.dirname(configPath), { recursive: true, mode: 448 });
17483
17855
  if (existingRaw) {
17484
- await copyFile3(configPath, `${configPath}.bak.${Date.now()}`);
17856
+ await atomicWriteFilePreservingMetadata(
17857
+ `${configPath}.bak.${Date.now()}`,
17858
+ existingRaw,
17859
+ { metadataSourcePath: configPath }
17860
+ );
17485
17861
  }
17486
17862
  document.contents = document.createNode(config);
17487
- const tempPath = `${configPath}.tmp.${process.pid}.${Date.now()}`;
17488
- await writeFile5(tempPath, document.toString(), {
17489
- encoding: "utf8",
17490
- mode: 384
17491
- });
17492
- await rename6(tempPath, configPath);
17863
+ await atomicWriteFilePreservingMetadata(configPath, document.toString());
17493
17864
  }
17494
17865
  async function patchHermesMemoryEnv(profileName, patch) {
17495
17866
  const entries = Object.entries(patch).filter(
@@ -17500,7 +17871,7 @@ async function patchHermesMemoryEnv(profileName, patch) {
17500
17871
  }
17501
17872
  const envPath = path19.join(resolveHermesProfileDir(profileName), ".env");
17502
17873
  const existingRaw = await readFile14(envPath, "utf8").catch((error) => {
17503
- if (isNodeError15(error, "ENOENT")) {
17874
+ if (isNodeError16(error, "ENOENT")) {
17504
17875
  return "";
17505
17876
  }
17506
17877
  throw error;
@@ -17530,13 +17901,14 @@ async function patchHermesMemoryEnv(profileName, patch) {
17530
17901
  }
17531
17902
  }
17532
17903
  const nextRaw = nextLines.join("\n").replace(/\n*$/u, "\n");
17533
- await mkdir12(path19.dirname(envPath), { recursive: true, mode: 448 });
17534
17904
  if (existingRaw) {
17535
- await copyFile3(envPath, `${envPath}.bak.${Date.now()}`);
17905
+ await atomicWriteFilePreservingMetadata(
17906
+ `${envPath}.bak.${Date.now()}`,
17907
+ existingRaw,
17908
+ { metadataSourcePath: envPath }
17909
+ );
17536
17910
  }
17537
- const tempPath = `${envPath}.tmp.${process.pid}.${Date.now()}`;
17538
- await writeFile5(tempPath, nextRaw, { encoding: "utf8", mode: 384 });
17539
- await rename6(tempPath, envPath);
17911
+ await atomicWriteFilePreservingMetadata(envPath, nextRaw);
17540
17912
  }
17541
17913
  function isMemoryEnvKeyWritable(key) {
17542
17914
  return [
@@ -17557,7 +17929,7 @@ async function readActiveMemoryProvider(profileName) {
17557
17929
  resolveHermesConfigPath(profileName),
17558
17930
  "utf8"
17559
17931
  ).catch((error) => {
17560
- if (isNodeError15(error, "ENOENT")) {
17932
+ if (isNodeError16(error, "ENOENT")) {
17561
17933
  return "";
17562
17934
  }
17563
17935
  throw error;
@@ -17582,16 +17954,15 @@ async function patchJsonProviderConfig(profileName, relativePath, patch) {
17582
17954
  next[key] = value;
17583
17955
  }
17584
17956
  }
17585
- await mkdir12(path19.dirname(configPath), { recursive: true, mode: 448 });
17586
- await writeFile5(configPath, `${JSON.stringify(next, null, 2)}
17587
- `, {
17588
- encoding: "utf8",
17589
- mode: 384
17590
- });
17957
+ await atomicWriteFilePreservingMetadata(
17958
+ configPath,
17959
+ `${JSON.stringify(next, null, 2)}
17960
+ `
17961
+ );
17591
17962
  }
17592
17963
  async function readJsonObject(filePath) {
17593
17964
  const raw = await readFile14(filePath, "utf8").catch((error) => {
17594
- if (isNodeError15(error, "ENOENT")) {
17965
+ if (isNodeError16(error, "ENOENT")) {
17595
17966
  return "{}";
17596
17967
  }
17597
17968
  throw error;
@@ -17645,7 +18016,7 @@ async function readMemoryLimits(profileName) {
17645
18016
  resolveHermesConfigPath(profileName),
17646
18017
  "utf8"
17647
18018
  ).catch((error) => {
17648
- if (isNodeError15(error, "ENOENT")) {
18019
+ if (isNodeError16(error, "ENOENT")) {
17649
18020
  return "";
17650
18021
  }
17651
18022
  throw error;
@@ -17738,7 +18109,7 @@ function formatEnvValue3(value) {
17738
18109
  function escapeRegExp3(value) {
17739
18110
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
17740
18111
  }
17741
- function isNodeError15(error, code) {
18112
+ function isNodeError16(error, code) {
17742
18113
  return error instanceof Error && "code" in error && error.code === code;
17743
18114
  }
17744
18115
 
@@ -18146,14 +18517,7 @@ function toMemoryHttpError(error) {
18146
18517
  }
18147
18518
 
18148
18519
  // src/hermes/skills.ts
18149
- import {
18150
- copyFile as copyFile4,
18151
- mkdir as mkdir13,
18152
- readFile as readFile15,
18153
- readdir as readdir10,
18154
- rename as rename7,
18155
- writeFile as writeFile6
18156
- } from "fs/promises";
18520
+ import { readFile as readFile15, readdir as readdir11 } from "fs/promises";
18157
18521
  import path20 from "path";
18158
18522
  import YAML5 from "yaml";
18159
18523
  var HermesSkillNotFoundError = class extends Error {
@@ -18245,9 +18609,9 @@ async function findSkillFiles(root) {
18245
18609
  return results.sort((left, right) => left.localeCompare(right));
18246
18610
  }
18247
18611
  async function collectSkillFiles(directory, results) {
18248
- const entries = await readdir10(directory, { withFileTypes: true }).catch(
18612
+ const entries = await readdir11(directory, { withFileTypes: true }).catch(
18249
18613
  (error) => {
18250
- if (isNodeError16(error, "ENOENT")) {
18614
+ if (isNodeError17(error, "ENOENT")) {
18251
18615
  return [];
18252
18616
  }
18253
18617
  throw error;
@@ -18272,7 +18636,7 @@ async function collectSkillFiles(directory, results) {
18272
18636
  async function readSkillMetadata(input) {
18273
18637
  const raw = await readFile15(input.skillFile, "utf8").catch(
18274
18638
  (error) => {
18275
- if (isNodeError16(error, "ENOENT") || isNodeError16(error, "EACCES")) {
18639
+ if (isNodeError17(error, "ENOENT") || isNodeError17(error, "EACCES")) {
18276
18640
  return null;
18277
18641
  }
18278
18642
  throw error;
@@ -18349,7 +18713,7 @@ function normalizeDescription(value) {
18349
18713
  }
18350
18714
  async function readDisabledSkillNames(configPath) {
18351
18715
  const raw = await readFile15(configPath, "utf8").catch((error) => {
18352
- if (isNodeError16(error, "ENOENT")) {
18716
+ if (isNodeError17(error, "ENOENT")) {
18353
18717
  return "";
18354
18718
  }
18355
18719
  throw error;
@@ -18374,7 +18738,7 @@ async function readSkillProvenance(root) {
18374
18738
  async function readBundledSkillNames(root) {
18375
18739
  const raw = await readFile15(path20.join(root, ".bundled_manifest"), "utf8").catch(
18376
18740
  (error) => {
18377
- if (isNodeError16(error, "ENOENT")) {
18741
+ if (isNodeError17(error, "ENOENT")) {
18378
18742
  return "";
18379
18743
  }
18380
18744
  throw error;
@@ -18397,7 +18761,7 @@ async function readBundledSkillNames(root) {
18397
18761
  async function readHubInstalledSkills(root) {
18398
18762
  const raw = await readFile15(path20.join(root, ".hub", "lock.json"), "utf8").catch(
18399
18763
  (error) => {
18400
- if (isNodeError16(error, "ENOENT")) {
18764
+ if (isNodeError17(error, "ENOENT")) {
18401
18765
  return "";
18402
18766
  }
18403
18767
  throw error;
@@ -18469,7 +18833,7 @@ function compareCategoryNames(left, right) {
18469
18833
  async function readHermesConfigDocument2(configPath) {
18470
18834
  const existingRaw = await readFile15(configPath, "utf8").catch(
18471
18835
  (error) => {
18472
- if (isNodeError16(error, "ENOENT")) {
18836
+ if (isNodeError17(error, "ENOENT")) {
18473
18837
  return null;
18474
18838
  }
18475
18839
  throw error;
@@ -18484,14 +18848,16 @@ async function readHermesConfigDocument2(configPath) {
18484
18848
  }
18485
18849
  async function writeHermesConfigDocument2(input) {
18486
18850
  const backupPath = input.existingRaw ? `${input.configPath}.bak.${Date.now()}` : null;
18487
- await mkdir13(path20.dirname(input.configPath), { recursive: true, mode: 448 });
18488
18851
  if (backupPath) {
18489
- await copyFile4(input.configPath, backupPath);
18852
+ await atomicWriteFilePreservingMetadata(backupPath, input.existingRaw, {
18853
+ metadataSourcePath: input.configPath
18854
+ });
18490
18855
  }
18491
18856
  input.document.contents = input.document.createNode(input.config);
18492
- const tempPath = `${input.configPath}.tmp.${process.pid}.${Date.now()}`;
18493
- await writeFile6(tempPath, input.document.toString(), { mode: 384 });
18494
- await rename7(tempPath, input.configPath);
18857
+ await atomicWriteFilePreservingMetadata(
18858
+ input.configPath,
18859
+ input.document.toString()
18860
+ );
18495
18861
  return backupPath;
18496
18862
  }
18497
18863
  function readStringList3(value) {
@@ -18514,7 +18880,7 @@ function ensureRecord3(target, key) {
18514
18880
  target[key] = current;
18515
18881
  return current;
18516
18882
  }
18517
- function isNodeError16(error, code) {
18883
+ function isNodeError17(error, code) {
18518
18884
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
18519
18885
  }
18520
18886
 
@@ -18996,7 +19362,7 @@ function readModelList(payload) {
18996
19362
  // src/hermes/updates.ts
18997
19363
  import { EventEmitter as EventEmitter3 } from "events";
18998
19364
  import { spawn as spawn3 } from "child_process";
18999
- import { mkdir as mkdir14, readFile as readFile16, rm as rm8 } from "fs/promises";
19365
+ import { mkdir as mkdir10, readFile as readFile16, rm as rm8 } from "fs/promises";
19000
19366
  import path21 from "path";
19001
19367
  var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
19002
19368
  var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
@@ -19057,7 +19423,7 @@ async function startHermesUpdate(options) {
19057
19423
  signal: null,
19058
19424
  error: null
19059
19425
  };
19060
- await mkdir14(options.paths.runDir, { recursive: true, mode: 448 });
19426
+ await mkdir10(options.paths.runDir, { recursive: true, mode: 448 });
19061
19427
  await writer.write(`
19062
19428
  === hermes update started ${startedAt} ===
19063
19429
  `);
@@ -19361,17 +19727,17 @@ function readString17(payload, key) {
19361
19727
  // src/link/updates.ts
19362
19728
  import { spawn as spawn5 } from "child_process";
19363
19729
  import { EventEmitter as EventEmitter4 } from "events";
19364
- import { mkdir as mkdir17, readFile as readFile18, rm as rm11 } from "fs/promises";
19730
+ import { mkdir as mkdir13, readFile as readFile18, rm as rm11 } from "fs/promises";
19365
19731
  import path23 from "path";
19366
19732
 
19367
19733
  // src/daemon/process.ts
19368
19734
  import { spawn as spawn4 } from "child_process";
19369
- import { mkdir as mkdir16, readFile as readFile17, rm as rm10 } from "fs/promises";
19735
+ import { mkdir as mkdir12, readFile as readFile17, rm as rm10 } from "fs/promises";
19370
19736
  import path22 from "path";
19371
19737
 
19372
19738
  // src/daemon/service.ts
19373
19739
  import { createServer } from "http";
19374
- import { mkdir as mkdir15, rm as rm9, writeFile as writeFile7 } from "fs/promises";
19740
+ import { mkdir as mkdir11, rm as rm9, writeFile as writeFile3 } from "fs/promises";
19375
19741
 
19376
19742
  // src/relay/control-client.ts
19377
19743
  import WebSocket from "ws";
@@ -20093,6 +20459,7 @@ var DEFAULT_STARTUP_REPORT_MIN_INTERVAL_MS = 15 * 6e4;
20093
20459
  function startLanIpMonitor(options) {
20094
20460
  let running = false;
20095
20461
  let closed = false;
20462
+ let current = Promise.resolve();
20096
20463
  const check = async (context = {}) => {
20097
20464
  if (running || closed) {
20098
20465
  return;
@@ -20108,15 +20475,16 @@ function startLanIpMonitor(options) {
20108
20475
  running = false;
20109
20476
  }
20110
20477
  };
20111
- void check({ forceReport: true, publishToRelay: true });
20478
+ current = check({ forceReport: true, publishToRelay: true });
20112
20479
  const timer = setInterval(() => {
20113
- void check();
20480
+ current = check();
20114
20481
  }, options.intervalMs ?? DEFAULT_INTERVAL_MS);
20115
20482
  timer.unref?.();
20116
20483
  return {
20117
- close() {
20484
+ async close() {
20118
20485
  closed = true;
20119
20486
  clearInterval(timer);
20487
+ await current.catch(() => void 0);
20120
20488
  }
20121
20489
  };
20122
20490
  }
@@ -20186,6 +20554,7 @@ async function checkLanIpChange(options, context = {}) {
20186
20554
  // src/daemon/scheduler.ts
20187
20555
  function startCronDeliveryScheduler(options) {
20188
20556
  let running = false;
20557
+ let current = Promise.resolve();
20189
20558
  const syncCronDeliveries = async () => {
20190
20559
  if (running) {
20191
20560
  return;
@@ -20206,17 +20575,19 @@ function startCronDeliveryScheduler(options) {
20206
20575
  }
20207
20576
  };
20208
20577
  const timer = setInterval(() => {
20209
- void syncCronDeliveries();
20578
+ current = syncCronDeliveries();
20210
20579
  }, options.intervalMs ?? 3e4);
20211
20580
  timer.unref?.();
20212
20581
  return {
20213
- close() {
20582
+ async close() {
20214
20583
  clearInterval(timer);
20584
+ await current.catch(() => void 0);
20215
20585
  }
20216
20586
  };
20217
20587
  }
20218
20588
  function startHermesSessionSyncScheduler(options) {
20219
20589
  let running = false;
20590
+ let current = Promise.resolve();
20220
20591
  const syncSessions = async () => {
20221
20592
  if (running) {
20222
20593
  return;
@@ -20233,12 +20604,13 @@ function startHermesSessionSyncScheduler(options) {
20233
20604
  }
20234
20605
  };
20235
20606
  const timer = setInterval(() => {
20236
- void syncSessions();
20607
+ current = syncSessions();
20237
20608
  }, options.intervalMs ?? 10 * 60 * 1e3);
20238
20609
  timer.unref?.();
20239
20610
  return {
20240
- close() {
20611
+ async close() {
20241
20612
  clearInterval(timer);
20613
+ await current.catch(() => void 0);
20242
20614
  }
20243
20615
  };
20244
20616
  }
@@ -20262,8 +20634,9 @@ async function startLinkService(options = {}) {
20262
20634
  }
20263
20635
  const conversations = new ConversationService(paths, logger);
20264
20636
  await conversations.rebuildStatisticsIndex();
20637
+ let hermesSessionSync = Promise.resolve();
20265
20638
  const triggerHermesSessionSync = () => {
20266
- void conversations.syncHermesSessions().catch((error) => {
20639
+ hermesSessionSync = Promise.resolve().then(() => conversations.syncHermesSessions()).then(() => void 0).catch((error) => {
20267
20640
  void logger.warn("hermes_session_sync_failed", {
20268
20641
  error: error instanceof Error ? error.message : String(error)
20269
20642
  });
@@ -20337,11 +20710,14 @@ async function startLinkService(options = {}) {
20337
20710
  }
20338
20711
  return {
20339
20712
  async close() {
20340
- scheduler.close();
20341
- hermesSessionSyncScheduler.close();
20342
- lanIpMonitor.close();
20343
20713
  relay?.close();
20344
20714
  await closeServer(server);
20715
+ await Promise.all([
20716
+ scheduler.close(),
20717
+ hermesSessionSyncScheduler.close(),
20718
+ lanIpMonitor.close(),
20719
+ hermesSessionSync.catch(() => void 0)
20720
+ ]);
20345
20721
  await logger.info("service_stopped");
20346
20722
  await logger.flush();
20347
20723
  if (options.writePidFile) {
@@ -20354,8 +20730,8 @@ function pidFilePath(paths = resolveRuntimePaths()) {
20354
20730
  return `${paths.runDir}/hermeslink.pid`;
20355
20731
  }
20356
20732
  async function writePidFile(paths) {
20357
- await mkdir15(paths.runDir, { recursive: true, mode: 448 });
20358
- await writeFile7(pidFilePath(paths), `${process.pid}
20733
+ await mkdir11(paths.runDir, { recursive: true, mode: 448 });
20734
+ await writeFile3(pidFilePath(paths), `${process.pid}
20359
20735
  `, { mode: 384 });
20360
20736
  }
20361
20737
  async function closeServer(server) {
@@ -20429,8 +20805,8 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
20429
20805
  return status;
20430
20806
  }
20431
20807
  }
20432
- await mkdir16(paths.logsDir, { recursive: true, mode: 448 });
20433
- await mkdir16(paths.runDir, { recursive: true, mode: 448 });
20808
+ await mkdir12(paths.logsDir, { recursive: true, mode: 448 });
20809
+ await mkdir12(paths.runDir, { recursive: true, mode: 448 });
20434
20810
  const scriptPath = currentCliScriptPath();
20435
20811
  const child = spawn4(process.execPath, [scriptPath, "daemon-supervisor"], {
20436
20812
  detached: true,
@@ -20448,7 +20824,7 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
20448
20824
  return await getDaemonStatus(paths);
20449
20825
  }
20450
20826
  async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
20451
- await mkdir16(paths.logsDir, { recursive: true, mode: 448 });
20827
+ await mkdir12(paths.logsDir, { recursive: true, mode: 448 });
20452
20828
  const log = createRotatingTextLogWriter({
20453
20829
  paths,
20454
20830
  fileName: path22.basename(daemonLogFile(paths))
@@ -20692,7 +21068,7 @@ async function startLinkUpdate(options) {
20692
21068
  error: null,
20693
21069
  manual_command: manualCommand
20694
21070
  };
20695
- await mkdir17(options.paths.runDir, { recursive: true, mode: 448 });
21071
+ await mkdir13(options.paths.runDir, { recursive: true, mode: 448 });
20696
21072
  await writer.write(
20697
21073
  `
20698
21074
  === link update started ${startedAt} target=${targetVersion} ===