@hermespilot/link 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{chunk-773L37RF.js → chunk-TZVQZFWU.js} +301 -49
- package/dist/cli/index.js +1 -1
- package/dist/http/app.d.ts +95 -77
- package/dist/http/app.js +1 -1
- package/package.json +6 -4
- package/scripts/check-node-version.mjs +7 -7
package/README.md
CHANGED
|
@@ -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
|
|
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();
|
|
@@ -3724,7 +3824,7 @@ import os2 from "os";
|
|
|
3724
3824
|
import path5 from "path";
|
|
3725
3825
|
|
|
3726
3826
|
// src/constants.ts
|
|
3727
|
-
var LINK_VERSION = "0.3.
|
|
3827
|
+
var LINK_VERSION = "0.3.7";
|
|
3728
3828
|
var LINK_COMMAND = "hermeslink";
|
|
3729
3829
|
var LINK_DEFAULT_PORT = 52379;
|
|
3730
3830
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -6753,9 +6853,7 @@ function isUsableLanIpv4(value) {
|
|
|
6753
6853
|
|
|
6754
6854
|
// src/hermes/session-title.ts
|
|
6755
6855
|
import { stat as stat6 } from "fs/promises";
|
|
6756
|
-
import { createRequire as createRequire2 } from "module";
|
|
6757
6856
|
import path11 from "path";
|
|
6758
|
-
var nodeRequire2 = createRequire2(import.meta.url);
|
|
6759
6857
|
async function readHermesSessionTitle(sessionId, paths, profileName) {
|
|
6760
6858
|
const trimmedSessionId = sessionId.trim();
|
|
6761
6859
|
if (!trimmedSessionId) {
|
|
@@ -6801,11 +6899,8 @@ async function readHermesCompressionTip(sessionId, paths, profileName) {
|
|
|
6801
6899
|
function readSessionTitleFromStateDb(dbPath, sessionId) {
|
|
6802
6900
|
let db = null;
|
|
6803
6901
|
try {
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
);
|
|
6807
|
-
db = new DatabaseSync(dbPath, {
|
|
6808
|
-
readOnly: true,
|
|
6902
|
+
db = openSqliteDatabase(dbPath, {
|
|
6903
|
+
readonly: true,
|
|
6809
6904
|
timeout: 1e3
|
|
6810
6905
|
});
|
|
6811
6906
|
const row = db.prepare("SELECT title FROM sessions WHERE id = ? LIMIT 1").get(sessionId);
|
|
@@ -6820,11 +6915,8 @@ function readSessionTitleFromStateDb(dbPath, sessionId) {
|
|
|
6820
6915
|
function readCompressionTipFromStateDb(dbPath, sessionId) {
|
|
6821
6916
|
let db = null;
|
|
6822
6917
|
try {
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
);
|
|
6826
|
-
db = new DatabaseSync(dbPath, {
|
|
6827
|
-
readOnly: true,
|
|
6918
|
+
db = openSqliteDatabase(dbPath, {
|
|
6919
|
+
readonly: true,
|
|
6828
6920
|
timeout: 1e3
|
|
6829
6921
|
});
|
|
6830
6922
|
let current = sessionId;
|
|
@@ -8284,12 +8376,63 @@ function toRecord6(value) {
|
|
|
8284
8376
|
}
|
|
8285
8377
|
|
|
8286
8378
|
// src/conversations/conversation-queries.ts
|
|
8379
|
+
var DEFAULT_CONVERSATION_LIST_PAGE_SIZE = 25;
|
|
8380
|
+
var MAX_CONVERSATION_LIST_PAGE_SIZE = 100;
|
|
8287
8381
|
var ConversationQueryCoordinator = class {
|
|
8288
8382
|
constructor(deps) {
|
|
8289
8383
|
this.deps = deps;
|
|
8290
8384
|
}
|
|
8291
8385
|
deps;
|
|
8292
8386
|
async listConversations() {
|
|
8387
|
+
return this.listConversationsFromStore();
|
|
8388
|
+
}
|
|
8389
|
+
async listConversationPage(options = {}) {
|
|
8390
|
+
const limit = normalizeConversationListPageLimit(options.limit);
|
|
8391
|
+
const cursor = decodeConversationListCursor(options.cursor);
|
|
8392
|
+
const indexedPage = await listConversationStatsPage(this.deps.paths, {
|
|
8393
|
+
limit,
|
|
8394
|
+
cursor
|
|
8395
|
+
});
|
|
8396
|
+
if (indexedPage.records.length === 0) {
|
|
8397
|
+
return this.listConversationPageFromStore({ limit, cursor });
|
|
8398
|
+
}
|
|
8399
|
+
const summaries = await this.summarizeIndexedConversations(
|
|
8400
|
+
indexedPage.records
|
|
8401
|
+
);
|
|
8402
|
+
return {
|
|
8403
|
+
conversations: summaries,
|
|
8404
|
+
page: {
|
|
8405
|
+
limit,
|
|
8406
|
+
has_more: indexedPage.hasMore,
|
|
8407
|
+
next_cursor: indexedPage.hasMore && indexedPage.records.length > 0 ? encodeConversationListCursor(indexedPage.records.at(-1)) : null
|
|
8408
|
+
}
|
|
8409
|
+
};
|
|
8410
|
+
}
|
|
8411
|
+
async searchConversationPage(options = {}) {
|
|
8412
|
+
const query = normalizeConversationSearchQuery(options.query);
|
|
8413
|
+
if (!query) {
|
|
8414
|
+
return this.listConversationPage(options);
|
|
8415
|
+
}
|
|
8416
|
+
const limit = normalizeConversationListPageLimit(options.limit);
|
|
8417
|
+
const cursor = decodeConversationListCursor(options.cursor);
|
|
8418
|
+
const indexedPage = await searchConversationStatsPage(this.deps.paths, {
|
|
8419
|
+
limit,
|
|
8420
|
+
cursor,
|
|
8421
|
+
query
|
|
8422
|
+
});
|
|
8423
|
+
const summaries = await this.summarizeIndexedConversations(
|
|
8424
|
+
indexedPage.records
|
|
8425
|
+
);
|
|
8426
|
+
return {
|
|
8427
|
+
conversations: summaries,
|
|
8428
|
+
page: {
|
|
8429
|
+
limit,
|
|
8430
|
+
has_more: indexedPage.hasMore,
|
|
8431
|
+
next_cursor: indexedPage.hasMore && indexedPage.records.length > 0 ? encodeConversationListCursor(indexedPage.records.at(-1)) : null
|
|
8432
|
+
}
|
|
8433
|
+
};
|
|
8434
|
+
}
|
|
8435
|
+
async listConversationsFromStore() {
|
|
8293
8436
|
const summaries = [];
|
|
8294
8437
|
for (const conversationId of await this.deps.store.listConversationIds()) {
|
|
8295
8438
|
const manifest = await this.deps.store.readManifest(conversationId).catch(
|
|
@@ -8305,9 +8448,41 @@ var ConversationQueryCoordinator = class {
|
|
|
8305
8448
|
summaries.push(await this.summarizeConversation(refreshed, snapshot));
|
|
8306
8449
|
}
|
|
8307
8450
|
return summaries.sort(
|
|
8308
|
-
(left, right) => Date.parse(right.updated_at) - Date.parse(left.updated_at)
|
|
8451
|
+
(left, right) => Date.parse(right.updated_at) - Date.parse(left.updated_at) || right.id.localeCompare(left.id)
|
|
8309
8452
|
);
|
|
8310
8453
|
}
|
|
8454
|
+
async listConversationPageFromStore(input) {
|
|
8455
|
+
const all = await this.listConversationsFromStore();
|
|
8456
|
+
const startIndex = input.cursor ? all.findIndex((summary) => isAfterConversationListCursor(summary, input.cursor)) : 0;
|
|
8457
|
+
const safeStartIndex = startIndex < 0 ? all.length : startIndex;
|
|
8458
|
+
const conversations = all.slice(safeStartIndex, safeStartIndex + input.limit);
|
|
8459
|
+
const hasMore = safeStartIndex + input.limit < all.length;
|
|
8460
|
+
return {
|
|
8461
|
+
conversations,
|
|
8462
|
+
page: {
|
|
8463
|
+
limit: input.limit,
|
|
8464
|
+
has_more: hasMore,
|
|
8465
|
+
next_cursor: hasMore && conversations.length > 0 ? encodeConversationListCursorFromSummary(conversations.at(-1)) : null
|
|
8466
|
+
}
|
|
8467
|
+
};
|
|
8468
|
+
}
|
|
8469
|
+
async summarizeIndexedConversations(records) {
|
|
8470
|
+
const summaries = [];
|
|
8471
|
+
for (const record of records) {
|
|
8472
|
+
const manifest = await this.deps.store.readManifest(record.conversationId).catch(
|
|
8473
|
+
() => null
|
|
8474
|
+
);
|
|
8475
|
+
if (!manifest || manifest.status !== "active") {
|
|
8476
|
+
continue;
|
|
8477
|
+
}
|
|
8478
|
+
const snapshot = await this.deps.store.readSnapshot(record.conversationId).catch(
|
|
8479
|
+
() => emptySnapshot2()
|
|
8480
|
+
);
|
|
8481
|
+
const refreshed = await this.deps.metadata.refreshTitleFromHermes(manifest, { snapshot }).catch(() => manifest);
|
|
8482
|
+
summaries.push(await this.summarizeConversation(refreshed, snapshot));
|
|
8483
|
+
}
|
|
8484
|
+
return summaries;
|
|
8485
|
+
}
|
|
8311
8486
|
async getMessages(conversationId, options = {}) {
|
|
8312
8487
|
const manifest = await this.deps.store.readActiveManifest(conversationId);
|
|
8313
8488
|
const snapshot = await this.deps.store.readSnapshot(conversationId);
|
|
@@ -8394,6 +8569,61 @@ var ConversationQueryCoordinator = class {
|
|
|
8394
8569
|
});
|
|
8395
8570
|
}
|
|
8396
8571
|
};
|
|
8572
|
+
function normalizeConversationListPageLimit(value) {
|
|
8573
|
+
if (value === void 0 || !Number.isFinite(value)) {
|
|
8574
|
+
return DEFAULT_CONVERSATION_LIST_PAGE_SIZE;
|
|
8575
|
+
}
|
|
8576
|
+
return Math.max(
|
|
8577
|
+
1,
|
|
8578
|
+
Math.min(MAX_CONVERSATION_LIST_PAGE_SIZE, Math.floor(value))
|
|
8579
|
+
);
|
|
8580
|
+
}
|
|
8581
|
+
function encodeConversationListCursor(record) {
|
|
8582
|
+
return Buffer.from(
|
|
8583
|
+
JSON.stringify({
|
|
8584
|
+
updated_at: record.updatedAt,
|
|
8585
|
+
conversation_id: record.conversationId
|
|
8586
|
+
}),
|
|
8587
|
+
"utf8"
|
|
8588
|
+
).toString("base64url");
|
|
8589
|
+
}
|
|
8590
|
+
function encodeConversationListCursorFromSummary(summary) {
|
|
8591
|
+
return encodeConversationListCursor({
|
|
8592
|
+
conversationId: summary.id,
|
|
8593
|
+
updatedAt: summary.updated_at
|
|
8594
|
+
});
|
|
8595
|
+
}
|
|
8596
|
+
function decodeConversationListCursor(value) {
|
|
8597
|
+
if (!value) {
|
|
8598
|
+
return null;
|
|
8599
|
+
}
|
|
8600
|
+
try {
|
|
8601
|
+
const decoded = JSON.parse(
|
|
8602
|
+
Buffer.from(value, "base64url").toString("utf8")
|
|
8603
|
+
);
|
|
8604
|
+
const updatedAt = readNonEmptyString(decoded.updated_at);
|
|
8605
|
+
const conversationId = readNonEmptyString(decoded.conversation_id);
|
|
8606
|
+
if (!updatedAt || !conversationId) {
|
|
8607
|
+
throw new Error("invalid cursor");
|
|
8608
|
+
}
|
|
8609
|
+
return { updatedAt, conversationId };
|
|
8610
|
+
} catch {
|
|
8611
|
+
throw new LinkHttpError(
|
|
8612
|
+
400,
|
|
8613
|
+
"conversation_cursor_invalid",
|
|
8614
|
+
"Conversation list cursor is invalid"
|
|
8615
|
+
);
|
|
8616
|
+
}
|
|
8617
|
+
}
|
|
8618
|
+
function readNonEmptyString(value) {
|
|
8619
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
8620
|
+
}
|
|
8621
|
+
function normalizeConversationSearchQuery(value) {
|
|
8622
|
+
return typeof value === "string" ? value.trim() : "";
|
|
8623
|
+
}
|
|
8624
|
+
function isAfterConversationListCursor(summary, cursor) {
|
|
8625
|
+
return summary.updated_at < cursor.updatedAt || summary.updated_at === cursor.updatedAt && summary.id < cursor.conversationId;
|
|
8626
|
+
}
|
|
8397
8627
|
function hydrateAgentEventBlocks(blocks, agentEvents) {
|
|
8398
8628
|
if (!blocks?.length || agentEvents.length === 0) {
|
|
8399
8629
|
return blocks;
|
|
@@ -8569,7 +8799,6 @@ function isNodeError8(error, code) {
|
|
|
8569
8799
|
// src/conversations/hermes-session-sync.ts
|
|
8570
8800
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
8571
8801
|
import { readdir as readdir6, readFile as readFile9, stat as stat8 } from "fs/promises";
|
|
8572
|
-
import { createRequire as createRequire3 } from "module";
|
|
8573
8802
|
import os4 from "os";
|
|
8574
8803
|
import path14 from "path";
|
|
8575
8804
|
|
|
@@ -8904,7 +9133,6 @@ function isNodeError9(error, code) {
|
|
|
8904
9133
|
}
|
|
8905
9134
|
|
|
8906
9135
|
// src/conversations/hermes-session-sync.ts
|
|
8907
|
-
var nodeRequire3 = createRequire3(import.meta.url);
|
|
8908
9136
|
var PROFILE_NAME_PATTERN3 = /^[a-zA-Z0-9._-]{1,64}$/u;
|
|
8909
9137
|
var DEFAULT_PROFILE_NAME = "default";
|
|
8910
9138
|
var MAX_IMPORTABLE_SESSIONS = 100;
|
|
@@ -9696,11 +9924,8 @@ async function listProfileSessions(dbPath) {
|
|
|
9696
9924
|
}
|
|
9697
9925
|
let db = null;
|
|
9698
9926
|
try {
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
);
|
|
9702
|
-
db = new DatabaseSync(dbPath, {
|
|
9703
|
-
readOnly: true,
|
|
9927
|
+
db = openSqliteDatabase(dbPath, {
|
|
9928
|
+
readonly: true,
|
|
9704
9929
|
timeout: 1e3
|
|
9705
9930
|
});
|
|
9706
9931
|
const sessionColumns = readTableColumns(db, "sessions");
|
|
@@ -9840,11 +10065,8 @@ async function readStateDbMessages(dbPath, sessionId) {
|
|
|
9840
10065
|
}
|
|
9841
10066
|
let db = null;
|
|
9842
10067
|
try {
|
|
9843
|
-
|
|
9844
|
-
|
|
9845
|
-
);
|
|
9846
|
-
db = new DatabaseSync(dbPath, {
|
|
9847
|
-
readOnly: true,
|
|
10068
|
+
db = openSqliteDatabase(dbPath, {
|
|
10069
|
+
readonly: true,
|
|
9848
10070
|
timeout: 1e3
|
|
9849
10071
|
});
|
|
9850
10072
|
const columns = readTableColumns(db, "messages");
|
|
@@ -10585,9 +10807,7 @@ function readString10(payload, key) {
|
|
|
10585
10807
|
|
|
10586
10808
|
// src/conversations/history-builder.ts
|
|
10587
10809
|
import { readFile as readFile10, stat as stat9 } from "fs/promises";
|
|
10588
|
-
import { createRequire as createRequire4 } from "module";
|
|
10589
10810
|
import path15 from "path";
|
|
10590
|
-
var nodeRequire4 = createRequire4(import.meta.url);
|
|
10591
10811
|
var HISTORY_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
|
|
10592
10812
|
var HERMES_HISTORY_COLUMNS = [
|
|
10593
10813
|
"role",
|
|
@@ -10749,11 +10969,8 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
|
|
|
10749
10969
|
function readHistoryRows(dbPath, sessionId) {
|
|
10750
10970
|
let db = null;
|
|
10751
10971
|
try {
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
);
|
|
10755
|
-
db = new DatabaseSync(dbPath, {
|
|
10756
|
-
readOnly: true,
|
|
10972
|
+
db = openSqliteDatabase(dbPath, {
|
|
10973
|
+
readonly: true,
|
|
10757
10974
|
timeout: 1e3
|
|
10758
10975
|
});
|
|
10759
10976
|
const columns = readTableColumns2(db, "messages");
|
|
@@ -12744,6 +12961,12 @@ var ConversationService = class {
|
|
|
12744
12961
|
async listConversations() {
|
|
12745
12962
|
return this.queries.listConversations();
|
|
12746
12963
|
}
|
|
12964
|
+
listConversationPage(input = {}) {
|
|
12965
|
+
return this.queries.listConversationPage(input);
|
|
12966
|
+
}
|
|
12967
|
+
searchConversationPage(input = {}) {
|
|
12968
|
+
return this.queries.searchConversationPage(input);
|
|
12969
|
+
}
|
|
12747
12970
|
async getStatistics(filter = {}) {
|
|
12748
12971
|
return readLinkStatistics(this.paths, filter);
|
|
12749
12972
|
}
|
|
@@ -14291,9 +14514,28 @@ function registerConversationRoutes(router, options) {
|
|
|
14291
14514
|
router.get("/api/v1/conversations", async (ctx) => {
|
|
14292
14515
|
await authenticateRequest(ctx, paths);
|
|
14293
14516
|
ctx.set("cache-control", "no-store");
|
|
14517
|
+
const result = await conversations.listConversationPage({
|
|
14518
|
+
limit: readLimit(ctx.query.limit),
|
|
14519
|
+
cursor: readQueryString(ctx.query.cursor) ?? readQueryString(ctx.query.after) ?? readQueryString(ctx.query.page_cursor)
|
|
14520
|
+
});
|
|
14521
|
+
ctx.body = {
|
|
14522
|
+
ok: true,
|
|
14523
|
+
conversations: result.conversations,
|
|
14524
|
+
page: result.page
|
|
14525
|
+
};
|
|
14526
|
+
});
|
|
14527
|
+
router.get("/api/v1/conversations/search", async (ctx) => {
|
|
14528
|
+
await authenticateRequest(ctx, paths);
|
|
14529
|
+
ctx.set("cache-control", "no-store");
|
|
14530
|
+
const result = await conversations.searchConversationPage({
|
|
14531
|
+
limit: readLimit(ctx.query.limit),
|
|
14532
|
+
cursor: readQueryString(ctx.query.cursor) ?? readQueryString(ctx.query.after) ?? readQueryString(ctx.query.page_cursor),
|
|
14533
|
+
query: readQueryString(ctx.query.query) ?? readQueryString(ctx.query.q) ?? readQueryString(ctx.query.keyword) ?? ""
|
|
14534
|
+
});
|
|
14294
14535
|
ctx.body = {
|
|
14295
14536
|
ok: true,
|
|
14296
|
-
conversations:
|
|
14537
|
+
conversations: result.conversations,
|
|
14538
|
+
page: result.page
|
|
14297
14539
|
};
|
|
14298
14540
|
});
|
|
14299
14541
|
router.post("/api/v1/conversations", async (ctx) => {
|
|
@@ -20093,6 +20335,7 @@ var DEFAULT_STARTUP_REPORT_MIN_INTERVAL_MS = 15 * 6e4;
|
|
|
20093
20335
|
function startLanIpMonitor(options) {
|
|
20094
20336
|
let running = false;
|
|
20095
20337
|
let closed = false;
|
|
20338
|
+
let current = Promise.resolve();
|
|
20096
20339
|
const check = async (context = {}) => {
|
|
20097
20340
|
if (running || closed) {
|
|
20098
20341
|
return;
|
|
@@ -20108,15 +20351,16 @@ function startLanIpMonitor(options) {
|
|
|
20108
20351
|
running = false;
|
|
20109
20352
|
}
|
|
20110
20353
|
};
|
|
20111
|
-
|
|
20354
|
+
current = check({ forceReport: true, publishToRelay: true });
|
|
20112
20355
|
const timer = setInterval(() => {
|
|
20113
|
-
|
|
20356
|
+
current = check();
|
|
20114
20357
|
}, options.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
20115
20358
|
timer.unref?.();
|
|
20116
20359
|
return {
|
|
20117
|
-
close() {
|
|
20360
|
+
async close() {
|
|
20118
20361
|
closed = true;
|
|
20119
20362
|
clearInterval(timer);
|
|
20363
|
+
await current.catch(() => void 0);
|
|
20120
20364
|
}
|
|
20121
20365
|
};
|
|
20122
20366
|
}
|
|
@@ -20186,6 +20430,7 @@ async function checkLanIpChange(options, context = {}) {
|
|
|
20186
20430
|
// src/daemon/scheduler.ts
|
|
20187
20431
|
function startCronDeliveryScheduler(options) {
|
|
20188
20432
|
let running = false;
|
|
20433
|
+
let current = Promise.resolve();
|
|
20189
20434
|
const syncCronDeliveries = async () => {
|
|
20190
20435
|
if (running) {
|
|
20191
20436
|
return;
|
|
@@ -20206,17 +20451,19 @@ function startCronDeliveryScheduler(options) {
|
|
|
20206
20451
|
}
|
|
20207
20452
|
};
|
|
20208
20453
|
const timer = setInterval(() => {
|
|
20209
|
-
|
|
20454
|
+
current = syncCronDeliveries();
|
|
20210
20455
|
}, options.intervalMs ?? 3e4);
|
|
20211
20456
|
timer.unref?.();
|
|
20212
20457
|
return {
|
|
20213
|
-
close() {
|
|
20458
|
+
async close() {
|
|
20214
20459
|
clearInterval(timer);
|
|
20460
|
+
await current.catch(() => void 0);
|
|
20215
20461
|
}
|
|
20216
20462
|
};
|
|
20217
20463
|
}
|
|
20218
20464
|
function startHermesSessionSyncScheduler(options) {
|
|
20219
20465
|
let running = false;
|
|
20466
|
+
let current = Promise.resolve();
|
|
20220
20467
|
const syncSessions = async () => {
|
|
20221
20468
|
if (running) {
|
|
20222
20469
|
return;
|
|
@@ -20233,12 +20480,13 @@ function startHermesSessionSyncScheduler(options) {
|
|
|
20233
20480
|
}
|
|
20234
20481
|
};
|
|
20235
20482
|
const timer = setInterval(() => {
|
|
20236
|
-
|
|
20483
|
+
current = syncSessions();
|
|
20237
20484
|
}, options.intervalMs ?? 10 * 60 * 1e3);
|
|
20238
20485
|
timer.unref?.();
|
|
20239
20486
|
return {
|
|
20240
|
-
close() {
|
|
20487
|
+
async close() {
|
|
20241
20488
|
clearInterval(timer);
|
|
20489
|
+
await current.catch(() => void 0);
|
|
20242
20490
|
}
|
|
20243
20491
|
};
|
|
20244
20492
|
}
|
|
@@ -20262,8 +20510,9 @@ async function startLinkService(options = {}) {
|
|
|
20262
20510
|
}
|
|
20263
20511
|
const conversations = new ConversationService(paths, logger);
|
|
20264
20512
|
await conversations.rebuildStatisticsIndex();
|
|
20513
|
+
let hermesSessionSync = Promise.resolve();
|
|
20265
20514
|
const triggerHermesSessionSync = () => {
|
|
20266
|
-
|
|
20515
|
+
hermesSessionSync = Promise.resolve().then(() => conversations.syncHermesSessions()).then(() => void 0).catch((error) => {
|
|
20267
20516
|
void logger.warn("hermes_session_sync_failed", {
|
|
20268
20517
|
error: error instanceof Error ? error.message : String(error)
|
|
20269
20518
|
});
|
|
@@ -20337,11 +20586,14 @@ async function startLinkService(options = {}) {
|
|
|
20337
20586
|
}
|
|
20338
20587
|
return {
|
|
20339
20588
|
async close() {
|
|
20340
|
-
scheduler.close();
|
|
20341
|
-
hermesSessionSyncScheduler.close();
|
|
20342
|
-
lanIpMonitor.close();
|
|
20343
20589
|
relay?.close();
|
|
20344
20590
|
await closeServer(server);
|
|
20591
|
+
await Promise.all([
|
|
20592
|
+
scheduler.close(),
|
|
20593
|
+
hermesSessionSyncScheduler.close(),
|
|
20594
|
+
lanIpMonitor.close(),
|
|
20595
|
+
hermesSessionSync.catch(() => void 0)
|
|
20596
|
+
]);
|
|
20345
20597
|
await logger.info("service_stopped");
|
|
20346
20598
|
await logger.flush();
|
|
20347
20599
|
if (options.writePidFile) {
|
package/dist/cli/index.js
CHANGED
package/dist/http/app.d.ts
CHANGED
|
@@ -1,82 +1,5 @@
|
|
|
1
1
|
import Koa from 'koa';
|
|
2
2
|
|
|
3
|
-
interface RuntimePaths {
|
|
4
|
-
homeDir: string;
|
|
5
|
-
identityFile: string;
|
|
6
|
-
configFile: string;
|
|
7
|
-
stateFile: string;
|
|
8
|
-
credentialsFile: string;
|
|
9
|
-
databaseFile: string;
|
|
10
|
-
conversationsDir: string;
|
|
11
|
-
blobsDir: string;
|
|
12
|
-
indexesDir: string;
|
|
13
|
-
logsDir: string;
|
|
14
|
-
runDir: string;
|
|
15
|
-
pairingDir: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface LinkStatistics {
|
|
19
|
-
conversations: {
|
|
20
|
-
total: number;
|
|
21
|
-
active: number;
|
|
22
|
-
deleted: number;
|
|
23
|
-
};
|
|
24
|
-
tokens: {
|
|
25
|
-
input_tokens: number;
|
|
26
|
-
output_tokens: number;
|
|
27
|
-
total_tokens: number;
|
|
28
|
-
};
|
|
29
|
-
messages: {
|
|
30
|
-
total: number;
|
|
31
|
-
};
|
|
32
|
-
runs: {
|
|
33
|
-
total: number;
|
|
34
|
-
};
|
|
35
|
-
models: {
|
|
36
|
-
total: number;
|
|
37
|
-
};
|
|
38
|
-
profiles: {
|
|
39
|
-
total: number;
|
|
40
|
-
};
|
|
41
|
-
skills: {
|
|
42
|
-
total: number;
|
|
43
|
-
};
|
|
44
|
-
tools: {
|
|
45
|
-
total: number;
|
|
46
|
-
};
|
|
47
|
-
updated_at?: string;
|
|
48
|
-
}
|
|
49
|
-
interface LinkStatisticsFilter {
|
|
50
|
-
profileUid?: string | null;
|
|
51
|
-
profileName?: string | null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
55
|
-
interface FileLoggerOptions {
|
|
56
|
-
paths?: RuntimePaths;
|
|
57
|
-
fileName?: string;
|
|
58
|
-
maxFileBytes?: number;
|
|
59
|
-
maxFiles?: number;
|
|
60
|
-
now?: () => Date;
|
|
61
|
-
}
|
|
62
|
-
declare class FileLogger {
|
|
63
|
-
readonly filePath: string;
|
|
64
|
-
private readonly paths;
|
|
65
|
-
private readonly maxFileBytes;
|
|
66
|
-
private readonly maxFiles;
|
|
67
|
-
private readonly now;
|
|
68
|
-
private queue;
|
|
69
|
-
constructor(options?: FileLoggerOptions);
|
|
70
|
-
debug(message: string, fields?: Record<string, unknown>): Promise<void>;
|
|
71
|
-
info(message: string, fields?: Record<string, unknown>): Promise<void>;
|
|
72
|
-
warn(message: string, fields?: Record<string, unknown>): Promise<void>;
|
|
73
|
-
error(message: string, fields?: Record<string, unknown>): Promise<void>;
|
|
74
|
-
write(level: LogLevel, message: string, fields?: Record<string, unknown>): Promise<void>;
|
|
75
|
-
flush(): Promise<void>;
|
|
76
|
-
private appendEntry;
|
|
77
|
-
private rotateIfNeeded;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
3
|
interface ConversationProfileSummary {
|
|
81
4
|
uid?: string;
|
|
82
5
|
name: string;
|
|
@@ -102,6 +25,15 @@ interface ConversationSummary {
|
|
|
102
25
|
content_preview: string;
|
|
103
26
|
} | null;
|
|
104
27
|
}
|
|
28
|
+
interface ConversationListPageInfo {
|
|
29
|
+
limit: number;
|
|
30
|
+
has_more: boolean;
|
|
31
|
+
next_cursor: string | null;
|
|
32
|
+
}
|
|
33
|
+
interface ConversationListPage {
|
|
34
|
+
conversations: ConversationSummary[];
|
|
35
|
+
page: ConversationListPageInfo;
|
|
36
|
+
}
|
|
105
37
|
interface ConversationRuntimeMetadata {
|
|
106
38
|
profile: ConversationProfileSummary;
|
|
107
39
|
model: {
|
|
@@ -322,6 +254,83 @@ interface CancelRunResult {
|
|
|
322
254
|
}
|
|
323
255
|
type ConversationEventListener = (event: ConversationEvent) => void;
|
|
324
256
|
|
|
257
|
+
interface RuntimePaths {
|
|
258
|
+
homeDir: string;
|
|
259
|
+
identityFile: string;
|
|
260
|
+
configFile: string;
|
|
261
|
+
stateFile: string;
|
|
262
|
+
credentialsFile: string;
|
|
263
|
+
databaseFile: string;
|
|
264
|
+
conversationsDir: string;
|
|
265
|
+
blobsDir: string;
|
|
266
|
+
indexesDir: string;
|
|
267
|
+
logsDir: string;
|
|
268
|
+
runDir: string;
|
|
269
|
+
pairingDir: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface LinkStatistics {
|
|
273
|
+
conversations: {
|
|
274
|
+
total: number;
|
|
275
|
+
active: number;
|
|
276
|
+
deleted: number;
|
|
277
|
+
};
|
|
278
|
+
tokens: {
|
|
279
|
+
input_tokens: number;
|
|
280
|
+
output_tokens: number;
|
|
281
|
+
total_tokens: number;
|
|
282
|
+
};
|
|
283
|
+
messages: {
|
|
284
|
+
total: number;
|
|
285
|
+
};
|
|
286
|
+
runs: {
|
|
287
|
+
total: number;
|
|
288
|
+
};
|
|
289
|
+
models: {
|
|
290
|
+
total: number;
|
|
291
|
+
};
|
|
292
|
+
profiles: {
|
|
293
|
+
total: number;
|
|
294
|
+
};
|
|
295
|
+
skills: {
|
|
296
|
+
total: number;
|
|
297
|
+
};
|
|
298
|
+
tools: {
|
|
299
|
+
total: number;
|
|
300
|
+
};
|
|
301
|
+
updated_at?: string;
|
|
302
|
+
}
|
|
303
|
+
interface LinkStatisticsFilter {
|
|
304
|
+
profileUid?: string | null;
|
|
305
|
+
profileName?: string | null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
309
|
+
interface FileLoggerOptions {
|
|
310
|
+
paths?: RuntimePaths;
|
|
311
|
+
fileName?: string;
|
|
312
|
+
maxFileBytes?: number;
|
|
313
|
+
maxFiles?: number;
|
|
314
|
+
now?: () => Date;
|
|
315
|
+
}
|
|
316
|
+
declare class FileLogger {
|
|
317
|
+
readonly filePath: string;
|
|
318
|
+
private readonly paths;
|
|
319
|
+
private readonly maxFileBytes;
|
|
320
|
+
private readonly maxFiles;
|
|
321
|
+
private readonly now;
|
|
322
|
+
private queue;
|
|
323
|
+
constructor(options?: FileLoggerOptions);
|
|
324
|
+
debug(message: string, fields?: Record<string, unknown>): Promise<void>;
|
|
325
|
+
info(message: string, fields?: Record<string, unknown>): Promise<void>;
|
|
326
|
+
warn(message: string, fields?: Record<string, unknown>): Promise<void>;
|
|
327
|
+
error(message: string, fields?: Record<string, unknown>): Promise<void>;
|
|
328
|
+
write(level: LogLevel, message: string, fields?: Record<string, unknown>): Promise<void>;
|
|
329
|
+
flush(): Promise<void>;
|
|
330
|
+
private appendEntry;
|
|
331
|
+
private rotateIfNeeded;
|
|
332
|
+
}
|
|
333
|
+
|
|
325
334
|
interface HermesSessionSyncResult {
|
|
326
335
|
scanned_profiles: number;
|
|
327
336
|
scanned_sessions: number;
|
|
@@ -367,6 +376,15 @@ declare class ConversationService {
|
|
|
367
376
|
constructor(paths: RuntimePaths, logger: FileLogger);
|
|
368
377
|
private withConversationLock;
|
|
369
378
|
listConversations(): Promise<ConversationSummary[]>;
|
|
379
|
+
listConversationPage(input?: {
|
|
380
|
+
limit?: number;
|
|
381
|
+
cursor?: string;
|
|
382
|
+
}): Promise<ConversationListPage>;
|
|
383
|
+
searchConversationPage(input?: {
|
|
384
|
+
limit?: number;
|
|
385
|
+
cursor?: string;
|
|
386
|
+
query?: string;
|
|
387
|
+
}): Promise<ConversationListPage>;
|
|
370
388
|
getStatistics(filter?: LinkStatisticsFilter): Promise<LinkStatistics>;
|
|
371
389
|
rebuildStatisticsIndex(): Promise<void>;
|
|
372
390
|
createConversation(input?: {
|
package/dist/http/app.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hermespilot/link",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
|
-
"build": "tsup src/cli/index.ts src/http/app.ts --format esm --target
|
|
30
|
+
"build": "tsup src/cli/index.ts src/http/app.ts --format esm --target node20 --dts --clean",
|
|
31
31
|
"check": "tsc --noEmit",
|
|
32
32
|
"dev": "tsx src/cli/index.ts",
|
|
33
33
|
"preinstall": "node ./scripts/check-node-version.mjs",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@koa/router": "^15.4.0",
|
|
44
|
+
"better-sqlite3": "^12.9.0",
|
|
44
45
|
"commander": "^12.1.0",
|
|
45
46
|
"koa": "^2.15.3",
|
|
46
47
|
"qrcode": "^1.5.4",
|
|
@@ -50,8 +51,9 @@
|
|
|
50
51
|
"zod": "^3.24.1"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
54
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
53
55
|
"@types/koa": "^2.15.0",
|
|
54
|
-
"@types/node": "^
|
|
56
|
+
"@types/node": "^20.19.39",
|
|
55
57
|
"@types/qrcode": "^1.5.6",
|
|
56
58
|
"@types/qrcode-terminal": "^0.12.2",
|
|
57
59
|
"@types/ws": "^8.5.13",
|
|
@@ -61,6 +63,6 @@
|
|
|
61
63
|
"vitest": "^2.1.8"
|
|
62
64
|
},
|
|
63
65
|
"engines": {
|
|
64
|
-
"node": ">=
|
|
66
|
+
"node": ">=20.0.0"
|
|
65
67
|
}
|
|
66
68
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
|
|
3
|
-
const MINIMUM_NODE_VERSION = "
|
|
3
|
+
const MINIMUM_NODE_VERSION = "20.0.0";
|
|
4
4
|
|
|
5
5
|
function parseVersion(input) {
|
|
6
6
|
const normalized = String(input ?? "")
|
|
@@ -55,21 +55,21 @@ if (compareVersions(current, minimum) < 0) {
|
|
|
55
55
|
const language = detectLanguage();
|
|
56
56
|
console.error("");
|
|
57
57
|
if (language === "zh-CN") {
|
|
58
|
-
console.error("Hermes Link 需要 Node.js
|
|
58
|
+
console.error("Hermes Link 需要 Node.js 20.0.0 或更新版本。");
|
|
59
59
|
console.error(`当前使用的是 Node.js ${process.versions.node}。`);
|
|
60
60
|
console.error("");
|
|
61
61
|
console.error("为什么需要这样做:");
|
|
62
|
-
console.error("- Hermes Link
|
|
63
|
-
console.error("- 如果继续在旧版 Node.js
|
|
62
|
+
console.error("- Hermes Link 与 hermes-agent 的 Node.js 20+ 要求保持一致。");
|
|
63
|
+
console.error("- 如果继续在旧版 Node.js 上安装,后续配对、后台服务或本地数据库可能会失败。");
|
|
64
64
|
console.error("");
|
|
65
65
|
console.error("请先升级 Node.js,然后重新运行安装命令。");
|
|
66
66
|
} else {
|
|
67
|
-
console.error("Hermes Link needs Node.js
|
|
67
|
+
console.error("Hermes Link needs Node.js 20.0.0 or newer.");
|
|
68
68
|
console.error(`You are using Node.js ${process.versions.node}.`);
|
|
69
69
|
console.error("");
|
|
70
70
|
console.error("Why this is required:");
|
|
71
|
-
console.error("- Hermes Link
|
|
72
|
-
console.error("- If installation continued on an older Node.js version, pairing
|
|
71
|
+
console.error("- Hermes Link now matches hermes-agent's Node.js 20+ requirement.");
|
|
72
|
+
console.error("- If installation continued on an older Node.js version, pairing, the background service, or the local database could fail later.");
|
|
73
73
|
console.error("");
|
|
74
74
|
console.error("Please update Node.js first, then run the install command again.");
|
|
75
75
|
}
|