@bulolo/hermes-link 0.3.1 → 0.3.3
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 +116 -10
- package/dist/{chunk-ZO2S4ZIO.js → chunk-ELQBIHDQ.js} +414 -48
- package/dist/cli/index.js +4 -4
- package/dist/http/app.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -341,38 +341,144 @@ hermeslink config set log-level debug # 日志级别:debug / info / wa
|
|
|
341
341
|
|
|
342
342
|
### 对话(Conversations)
|
|
343
343
|
|
|
344
|
+
#### 活跃对话
|
|
345
|
+
|
|
344
346
|
| 方法 | 路径 | 说明 |
|
|
345
347
|
|------|------|------|
|
|
346
|
-
| GET | `/api/v1/conversations` |
|
|
347
|
-
| GET | `/api/v1/conversations/search` |
|
|
348
|
+
| GET | `/api/v1/conversations` | 列出活跃对话(`?limit=20&cursor=xxx`) |
|
|
349
|
+
| GET | `/api/v1/conversations/search` | 搜索活跃对话(`?q=关键词`) |
|
|
348
350
|
| POST | `/api/v1/conversations` | 新建对话 |
|
|
349
|
-
| DELETE | `/api/v1/conversations` |
|
|
351
|
+
| DELETE | `/api/v1/conversations` | 批量删除对话(`{"conversation_ids":["conv_xxx"]}`) |
|
|
350
352
|
| DELETE | `/api/v1/conversations/:id` | 删除单个对话 |
|
|
351
|
-
| GET | `/api/v1/conversations/:id/messages` |
|
|
353
|
+
| GET | `/api/v1/conversations/:id/messages` | 获取对话消息列表(含 `runtime` 上下文信息) |
|
|
352
354
|
| POST | `/api/v1/conversations/:id/messages` | 发送消息 |
|
|
353
355
|
| GET | `/api/v1/conversations/:id/events` | SSE 实时事件流 |
|
|
354
356
|
| GET | `/api/v1/conversations/events` | 所有对话事件流(SSE) |
|
|
355
357
|
| PATCH | `/api/v1/conversations/:id/title` | 重命名对话(`{"title":"新标题"}`) |
|
|
356
|
-
| PATCH | `/api/v1/conversations/:id/model` |
|
|
358
|
+
| PATCH | `/api/v1/conversations/:id/model` | 切换模型(`{"model_id":"xxx"}`) |
|
|
357
359
|
| PATCH | `/api/v1/conversations/:id/profile` | 切换 profile |
|
|
358
360
|
| POST | `/api/v1/conversations/:id/ack` | 确认已读 |
|
|
359
|
-
| POST | `/api/v1/conversations/clear-plans` | 创建批量清理计划 |
|
|
360
|
-
| GET | `/api/v1/conversations/clear-plans/:planId` | 查询清理计划状态 |
|
|
361
|
-
| POST | `/api/v1/conversations/clear-plans/:planId/execute` | 执行清理计划 |
|
|
362
361
|
| POST | `/api/v1/conversations/:id/runs/:runId/cancel` | 取消对话内的某次执行 |
|
|
363
|
-
| POST | `/api/v1/conversations/:id/approvals/:approvalId/approve` |
|
|
362
|
+
| POST | `/api/v1/conversations/:id/approvals/:approvalId/approve` | 审批工具调用(允许,`{"scope":"once\|session\|always"}`) |
|
|
364
363
|
| POST | `/api/v1/conversations/:id/approvals/:approvalId/deny` | 审批工具调用(拒绝) |
|
|
365
364
|
| POST | `/api/v1/conversations/:id/blobs` | 上传附件 |
|
|
366
365
|
| GET | `/api/v1/conversations/:id/blobs/:blobId` | 下载附件 |
|
|
367
366
|
| DELETE | `/api/v1/conversations/:id/blobs/:blobId` | 删除附件 |
|
|
368
367
|
|
|
368
|
+
#### 归档对话
|
|
369
|
+
|
|
370
|
+
| 方法 | 路径 | 说明 |
|
|
371
|
+
|------|------|------|
|
|
372
|
+
| GET | `/api/v1/conversations/archived` | 列出已归档对话(`?limit=20&cursor=xxx`) |
|
|
373
|
+
| GET | `/api/v1/conversations/archived/search` | 搜索已归档对话(`?q=关键词`) |
|
|
374
|
+
| POST | `/api/v1/conversations/:id/archive` | 归档对话(发送新消息时自动恢复为活跃) |
|
|
375
|
+
| POST | `/api/v1/conversations/:id/unarchive` | 取消归档 |
|
|
376
|
+
|
|
377
|
+
#### 批量清理计划
|
|
378
|
+
|
|
379
|
+
| 方法 | 路径 | 说明 |
|
|
380
|
+
|------|------|------|
|
|
381
|
+
| POST | `/api/v1/conversations/clear-plans` | 创建批量删除计划(`{"target_status":"active\|archived"}`) |
|
|
382
|
+
| GET | `/api/v1/conversations/clear-plans/:planId` | 查询计划状态 |
|
|
383
|
+
| POST | `/api/v1/conversations/clear-plans/:planId/execute` | 执行计划(批量删除) |
|
|
384
|
+
|
|
385
|
+
#### 批量归档计划
|
|
386
|
+
|
|
387
|
+
| 方法 | 路径 | 说明 |
|
|
388
|
+
|------|------|------|
|
|
389
|
+
| POST | `/api/v1/conversations/archive-plans` | 创建批量归档计划(`{"exclude_conversation_ids":[]}`) |
|
|
390
|
+
| GET | `/api/v1/conversations/archive-plans/:planId` | 查询计划状态 |
|
|
391
|
+
| POST | `/api/v1/conversations/archive-plans/:planId/execute` | 执行计划(批量归档) |
|
|
392
|
+
|
|
393
|
+
#### 响应结构说明
|
|
394
|
+
|
|
395
|
+
**ConversationSummary**(对话列表每项):
|
|
396
|
+
|
|
397
|
+
```json
|
|
398
|
+
{
|
|
399
|
+
"id": "conv_xxx",
|
|
400
|
+
"title": "对话标题",
|
|
401
|
+
"created_at": "2026-05-09T00:00:00.000Z",
|
|
402
|
+
"updated_at": "2026-05-09T00:00:00.000Z",
|
|
403
|
+
"last_event_seq": 12,
|
|
404
|
+
"usage": { "input_tokens": 100, "output_tokens": 200, "total_tokens": 300, "updated_at": "..." },
|
|
405
|
+
"profile": { "uid": "prof_xxx", "name": "default", "display_name": "default", "avatar_url": null },
|
|
406
|
+
"last_message": { "id": "msg_xxx", "role": "assistant", "content_preview": "消息摘要..." }
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**GET `/api/v1/conversations/:id/messages`** 响应新增 `runtime` 字段:
|
|
411
|
+
|
|
412
|
+
```json
|
|
413
|
+
{
|
|
414
|
+
"ok": true,
|
|
415
|
+
"messages": [...],
|
|
416
|
+
"last_event_seq": 12,
|
|
417
|
+
"runtime": {
|
|
418
|
+
"profile": { "name": "default", "display_name": "default", "avatar_url": null },
|
|
419
|
+
"model": { "id": "claude-3-5-sonnet", "provider": "anthropic", "context_window": 200000 },
|
|
420
|
+
"context": { "input_tokens": 100, "output_tokens": 200, "total_tokens": 300, "usage_percent": 0, "source": "estimated" }
|
|
421
|
+
},
|
|
422
|
+
"page": { "limit": 50, "has_more_before": false, "has_more_after": false, "oldest_message_id": "...", "newest_message_id": "..." }
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**LinkMessage**(消息对象):
|
|
427
|
+
|
|
428
|
+
```json
|
|
429
|
+
{
|
|
430
|
+
"id": "msg_xxx",
|
|
431
|
+
"schema_version": 1,
|
|
432
|
+
"conversation_id": "conv_xxx",
|
|
433
|
+
"role": "user|assistant|tool|system",
|
|
434
|
+
"status": "queued|streaming|completed|failed|cancelled",
|
|
435
|
+
"created_at": "...",
|
|
436
|
+
"updated_at": "...",
|
|
437
|
+
"sender": { "id": "app_user", "type": "human|agent|system|tool", "display_name": "Me" },
|
|
438
|
+
"parts": [{ "type": "text", "text": "消息内容" }],
|
|
439
|
+
"attachments": [],
|
|
440
|
+
"blocks": [],
|
|
441
|
+
"agent_events": [],
|
|
442
|
+
"approvals": []
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
**DELETE `/api/v1/conversations/:id`** 响应新增字段:
|
|
447
|
+
|
|
448
|
+
```json
|
|
449
|
+
{
|
|
450
|
+
"ok": true,
|
|
451
|
+
"conversation_id": "conv_xxx",
|
|
452
|
+
"hermes_deleted": false,
|
|
453
|
+
"deleted_at": "2026-05-09T00:00:00.000Z"
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
369
457
|
### 统计
|
|
370
458
|
|
|
371
459
|
| 方法 | 路径 | 说明 |
|
|
372
460
|
|------|------|------|
|
|
373
|
-
| GET | `/api/v1/statistics` |
|
|
461
|
+
| GET | `/api/v1/statistics` | 全局使用统计(`?profile=xxx&profile_uid=xxx`) |
|
|
374
462
|
| GET | `/api/v1/statistics/usage` | Token 用量统计(`?days=7&from=2026-05-01&to=2026-05-08&model=xxx&profile=xxx`) |
|
|
375
463
|
|
|
464
|
+
**GET `/api/v1/statistics`** 响应:
|
|
465
|
+
|
|
466
|
+
```json
|
|
467
|
+
{
|
|
468
|
+
"ok": true,
|
|
469
|
+
"statistics": {
|
|
470
|
+
"conversations": { "total": 10, "active": 8, "archived": 1, "deleted": 1 },
|
|
471
|
+
"tokens": { "input_tokens": 5000, "output_tokens": 8000, "total_tokens": 13000 },
|
|
472
|
+
"messages": { "total": 120 },
|
|
473
|
+
"runs": { "total": 50 },
|
|
474
|
+
"models": { "total": 0 },
|
|
475
|
+
"profiles": { "total": 0 },
|
|
476
|
+
"skills": { "total": 0 },
|
|
477
|
+
"tools": { "total": 0 }
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
376
482
|
### 模型(Models)
|
|
377
483
|
|
|
378
484
|
| 方法 | 路径 | 说明 |
|
|
@@ -1546,9 +1546,9 @@ async function discoverRouteCandidates(options) {
|
|
|
1546
1546
|
const publicIpv4s = unique(publicIps.publicIpv4s.filter(isUsablePublicIpv4)).slice(0, MAX_PUBLIC_IPV4S);
|
|
1547
1547
|
const publicIpv6s = unique(publicIps.publicIpv6s.filter(isUsablePublicIpv6)).slice(0, MAX_PUBLIC_IPV6S);
|
|
1548
1548
|
const preferredUrls = [
|
|
1549
|
-
...lanIps.map((ip) => buildDirectUrl(ip, options.port)),
|
|
1550
1549
|
...publicIpv4s.map((ip) => buildDirectUrl(ip, options.port)),
|
|
1551
|
-
...publicIpv6s.map((ip) => buildDirectUrl(ip, options.port))
|
|
1550
|
+
...publicIpv6s.map((ip) => buildDirectUrl(ip, options.port)),
|
|
1551
|
+
...lanIps.map((ip) => buildDirectUrl(ip, options.port))
|
|
1552
1552
|
];
|
|
1553
1553
|
return { lanIps, publicIpv4s, publicIpv6s, preferredUrls, environment };
|
|
1554
1554
|
}
|
|
@@ -2046,6 +2046,43 @@ function createConversationsRouter(options) {
|
|
|
2046
2046
|
});
|
|
2047
2047
|
ctx.body = { ok: true, conversations: result.conversations, page: result.page };
|
|
2048
2048
|
});
|
|
2049
|
+
router.get("/api/v1/conversations/archived", async (ctx) => {
|
|
2050
|
+
await authenticateRequest(ctx, paths);
|
|
2051
|
+
ctx.set("cache-control", "no-store");
|
|
2052
|
+
const result = await conversations.listArchivedConversationPage({
|
|
2053
|
+
limit: readLimit(ctx.query.limit),
|
|
2054
|
+
cursor: readQueryString(ctx.query.cursor) ?? readQueryString(ctx.query.after)
|
|
2055
|
+
});
|
|
2056
|
+
ctx.body = { ok: true, conversations: result.conversations, page: result.page };
|
|
2057
|
+
});
|
|
2058
|
+
router.get("/api/v1/conversations/archived/search", async (ctx) => {
|
|
2059
|
+
await authenticateRequest(ctx, paths);
|
|
2060
|
+
ctx.set("cache-control", "no-store");
|
|
2061
|
+
const result = await conversations.searchArchivedConversationPage({
|
|
2062
|
+
limit: readLimit(ctx.query.limit),
|
|
2063
|
+
cursor: readQueryString(ctx.query.cursor) ?? readQueryString(ctx.query.after),
|
|
2064
|
+
query: readQueryString(ctx.query.query) ?? readQueryString(ctx.query.q) ?? readQueryString(ctx.query.keyword) ?? ""
|
|
2065
|
+
});
|
|
2066
|
+
ctx.body = { ok: true, conversations: result.conversations, page: result.page };
|
|
2067
|
+
});
|
|
2068
|
+
router.post("/api/v1/conversations/archive-plans", async (ctx) => {
|
|
2069
|
+
await authenticateRequest(ctx, paths);
|
|
2070
|
+
const body = await readJsonBody3(ctx.req);
|
|
2071
|
+
const excludeIds = readStringArray2(body, "exclude_conversation_ids", "excludeConversationIds") ?? [];
|
|
2072
|
+
const plan = await conversations.prepareArchiveAllConversationPlan({ excludeConversationIds: excludeIds });
|
|
2073
|
+
ctx.status = 201;
|
|
2074
|
+
ctx.body = { ok: true, plan };
|
|
2075
|
+
});
|
|
2076
|
+
router.get("/api/v1/conversations/archive-plans/:planId", async (ctx) => {
|
|
2077
|
+
await authenticateRequest(ctx, paths);
|
|
2078
|
+
ctx.set("cache-control", "no-store");
|
|
2079
|
+
ctx.body = { ok: true, plan: await conversations.readArchiveAllConversationPlan(ctx.params.planId) };
|
|
2080
|
+
});
|
|
2081
|
+
router.post("/api/v1/conversations/archive-plans/:planId/execute", async (ctx) => {
|
|
2082
|
+
await authenticateRequest(ctx, paths);
|
|
2083
|
+
const plan = await conversations.executeArchiveAllConversationPlan(ctx.params.planId);
|
|
2084
|
+
ctx.body = { ok: true, plan };
|
|
2085
|
+
});
|
|
2049
2086
|
router.post("/api/v1/conversations/clear-plans", async (ctx) => {
|
|
2050
2087
|
await authenticateRequest(ctx, paths);
|
|
2051
2088
|
const plan = await conversations.prepareClearAllConversationPlan();
|
|
@@ -2173,6 +2210,14 @@ function createConversationsRouter(options) {
|
|
|
2173
2210
|
if (!title) throw new LinkHttpError(400, "title_required", "title is required");
|
|
2174
2211
|
ctx.body = { ok: true, ...await conversations.renameConversation(ctx.params.conversationId, title) };
|
|
2175
2212
|
});
|
|
2213
|
+
router.post("/api/v1/conversations/:conversationId/archive", async (ctx) => {
|
|
2214
|
+
await authenticateRequest(ctx, paths);
|
|
2215
|
+
ctx.body = { ok: true, ...await conversations.archiveConversation(ctx.params.conversationId) };
|
|
2216
|
+
});
|
|
2217
|
+
router.post("/api/v1/conversations/:conversationId/unarchive", async (ctx) => {
|
|
2218
|
+
await authenticateRequest(ctx, paths);
|
|
2219
|
+
ctx.body = { ok: true, ...await conversations.unarchiveConversation(ctx.params.conversationId) };
|
|
2220
|
+
});
|
|
2176
2221
|
router.post("/api/v1/conversations/:conversationId/ack", async (ctx) => {
|
|
2177
2222
|
await authenticateRequest(ctx, paths);
|
|
2178
2223
|
ctx.body = { ok: true };
|
|
@@ -7602,6 +7647,13 @@ async function readManifest(paths, conversationId) {
|
|
|
7602
7647
|
return readJson(manifestPath(paths, conversationId), null);
|
|
7603
7648
|
}
|
|
7604
7649
|
async function readActiveManifest(paths, conversationId) {
|
|
7650
|
+
const manifest = await readManifest(paths, conversationId);
|
|
7651
|
+
if (!manifest || manifest.status === "deleted_soft" || manifest.status === "archived") {
|
|
7652
|
+
throw new LinkHttpError(404, "conversation_not_found", "Conversation was not found");
|
|
7653
|
+
}
|
|
7654
|
+
return manifest;
|
|
7655
|
+
}
|
|
7656
|
+
async function readExistingManifest(paths, conversationId) {
|
|
7605
7657
|
const manifest = await readManifest(paths, conversationId);
|
|
7606
7658
|
if (!manifest || manifest.status === "deleted_soft") {
|
|
7607
7659
|
throw new LinkHttpError(404, "conversation_not_found", "Conversation was not found");
|
|
@@ -7884,6 +7936,8 @@ function normalizeLimit(value, defaultValue, max) {
|
|
|
7884
7936
|
if (!Number.isFinite(n) || n < 1) return defaultValue;
|
|
7885
7937
|
return Math.min(n, max);
|
|
7886
7938
|
}
|
|
7939
|
+
var clearPlans = /* @__PURE__ */ new Map();
|
|
7940
|
+
var archivePlans = /* @__PURE__ */ new Map();
|
|
7887
7941
|
var ConversationService = class extends EventEmitter4 {
|
|
7888
7942
|
paths;
|
|
7889
7943
|
logger;
|
|
@@ -7952,13 +8006,82 @@ var ConversationService = class extends EventEmitter4 {
|
|
|
7952
8006
|
if (after === void 0 || after === null) return events;
|
|
7953
8007
|
return events.filter((e) => e.seq > after);
|
|
7954
8008
|
}
|
|
8009
|
+
summarizeManifest(manifest, snapshot) {
|
|
8010
|
+
const stats = manifest.stats;
|
|
8011
|
+
const profileName = manifest.profile ?? manifest.profile_name_snapshot ?? "default";
|
|
8012
|
+
let lastMessage = null;
|
|
8013
|
+
if (snapshot && snapshot.messages.length > 0) {
|
|
8014
|
+
const last = snapshot.messages[snapshot.messages.length - 1];
|
|
8015
|
+
if (last) {
|
|
8016
|
+
const text = last.parts.find((p) => p.type === "text")?.text ?? "";
|
|
8017
|
+
lastMessage = { id: last.id, role: last.role, content_preview: text.slice(0, 200) };
|
|
8018
|
+
}
|
|
8019
|
+
}
|
|
8020
|
+
return {
|
|
8021
|
+
id: manifest.id,
|
|
8022
|
+
title: manifest.title,
|
|
8023
|
+
created_at: manifest.created_at,
|
|
8024
|
+
updated_at: manifest.updated_at,
|
|
8025
|
+
last_event_seq: manifest.last_event_seq,
|
|
8026
|
+
usage: {
|
|
8027
|
+
input_tokens: stats?.input_tokens ?? 0,
|
|
8028
|
+
output_tokens: stats?.output_tokens ?? 0,
|
|
8029
|
+
total_tokens: stats?.total_tokens ?? 0,
|
|
8030
|
+
...stats?.updated_at ? { updated_at: stats.updated_at } : {}
|
|
8031
|
+
},
|
|
8032
|
+
profile: {
|
|
8033
|
+
uid: manifest.profile_uid ?? void 0,
|
|
8034
|
+
name: profileName,
|
|
8035
|
+
display_name: profileName,
|
|
8036
|
+
avatar_url: null
|
|
8037
|
+
},
|
|
8038
|
+
last_message: lastMessage
|
|
8039
|
+
};
|
|
8040
|
+
}
|
|
8041
|
+
buildRuntimeMetadata(manifest) {
|
|
8042
|
+
const stats = manifest.stats;
|
|
8043
|
+
const profileName = manifest.profile ?? manifest.profile_name_snapshot ?? "default";
|
|
8044
|
+
const usagePercent = stats?.context_window && stats.total_tokens ? Math.min(100, Math.round(stats.total_tokens / stats.context_window * 100)) : void 0;
|
|
8045
|
+
return {
|
|
8046
|
+
profile: {
|
|
8047
|
+
uid: manifest.profile_uid ?? void 0,
|
|
8048
|
+
name: profileName,
|
|
8049
|
+
display_name: profileName,
|
|
8050
|
+
avatar_url: null
|
|
8051
|
+
},
|
|
8052
|
+
model: {
|
|
8053
|
+
id: stats?.model ?? "unknown",
|
|
8054
|
+
...stats?.provider ? { provider: stats.provider } : {},
|
|
8055
|
+
...stats?.context_window ? { context_window: stats.context_window } : {}
|
|
8056
|
+
},
|
|
8057
|
+
context: {
|
|
8058
|
+
input_tokens: stats?.input_tokens ?? 0,
|
|
8059
|
+
output_tokens: stats?.output_tokens ?? 0,
|
|
8060
|
+
total_tokens: stats?.total_tokens ?? 0,
|
|
8061
|
+
...stats?.context_window ? { context_window: stats.context_window } : {},
|
|
8062
|
+
...usagePercent !== void 0 ? { usage_percent: usagePercent } : {},
|
|
8063
|
+
source: stats?.model ? "estimated" : "unknown",
|
|
8064
|
+
...stats?.updated_at ? { updated_at: stats.updated_at } : {}
|
|
8065
|
+
}
|
|
8066
|
+
};
|
|
8067
|
+
}
|
|
8068
|
+
async listConversations() {
|
|
8069
|
+
const ids = await listConversationIds(this.paths);
|
|
8070
|
+
const manifests = [];
|
|
8071
|
+
for (const id of ids) {
|
|
8072
|
+
const m = await readManifest(this.paths, id);
|
|
8073
|
+
if (m && m.status !== "deleted_soft") manifests.push(m);
|
|
8074
|
+
}
|
|
8075
|
+
manifests.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
8076
|
+
return manifests.map((m) => this.summarizeManifest(m));
|
|
8077
|
+
}
|
|
7955
8078
|
async listConversationPage(options = {}) {
|
|
7956
8079
|
const limit = normalizeLimit(options.limit, 20, 100);
|
|
7957
8080
|
const ids = await listConversationIds(this.paths);
|
|
7958
8081
|
const manifests = [];
|
|
7959
8082
|
for (const id of ids) {
|
|
7960
8083
|
const m = await readManifest(this.paths, id);
|
|
7961
|
-
if (m && m.status
|
|
8084
|
+
if (m && m.status === "active") manifests.push(m);
|
|
7962
8085
|
}
|
|
7963
8086
|
manifests.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
7964
8087
|
let startIndex = 0;
|
|
@@ -7985,7 +8108,7 @@ var ConversationService = class extends EventEmitter4 {
|
|
|
7985
8108
|
const results = [];
|
|
7986
8109
|
for (const id of ids) {
|
|
7987
8110
|
const m = await readManifest(this.paths, id);
|
|
7988
|
-
if (m && m.status
|
|
8111
|
+
if (m && m.status === "active" && m.title.toLowerCase().includes(q)) {
|
|
7989
8112
|
results.push(m);
|
|
7990
8113
|
}
|
|
7991
8114
|
}
|
|
@@ -7996,6 +8119,48 @@ var ConversationService = class extends EventEmitter4 {
|
|
|
7996
8119
|
page: { limit, has_more: results.length > limit, next_cursor: null }
|
|
7997
8120
|
};
|
|
7998
8121
|
}
|
|
8122
|
+
async listArchivedConversationPage(options = {}) {
|
|
8123
|
+
const limit = normalizeLimit(options.limit, 20, 100);
|
|
8124
|
+
const ids = await listConversationIds(this.paths);
|
|
8125
|
+
const manifests = [];
|
|
8126
|
+
for (const id of ids) {
|
|
8127
|
+
const m = await readManifest(this.paths, id);
|
|
8128
|
+
if (m && m.status === "archived") manifests.push(m);
|
|
8129
|
+
}
|
|
8130
|
+
manifests.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
8131
|
+
let startIndex = 0;
|
|
8132
|
+
if (options.cursor) {
|
|
8133
|
+
const idx = manifests.findIndex((m) => m.id === options.cursor);
|
|
8134
|
+
if (idx >= 0) startIndex = idx + 1;
|
|
8135
|
+
}
|
|
8136
|
+
const page = manifests.slice(startIndex, startIndex + limit);
|
|
8137
|
+
const hasMore = startIndex + limit < manifests.length;
|
|
8138
|
+
return {
|
|
8139
|
+
conversations: page.map((m) => this.summarizeManifest(m)),
|
|
8140
|
+
page: {
|
|
8141
|
+
limit,
|
|
8142
|
+
has_more: hasMore,
|
|
8143
|
+
next_cursor: hasMore && page.length > 0 ? page[page.length - 1]?.id ?? null : null
|
|
8144
|
+
}
|
|
8145
|
+
};
|
|
8146
|
+
}
|
|
8147
|
+
async searchArchivedConversationPage(options = {}) {
|
|
8148
|
+
if (!options.query?.trim()) return this.listArchivedConversationPage(options);
|
|
8149
|
+
const q = options.query.trim().toLowerCase();
|
|
8150
|
+
const limit = normalizeLimit(options.limit, 20, 100);
|
|
8151
|
+
const ids = await listConversationIds(this.paths);
|
|
8152
|
+
const results = [];
|
|
8153
|
+
for (const id of ids) {
|
|
8154
|
+
const m = await readManifest(this.paths, id);
|
|
8155
|
+
if (m && m.status === "archived" && m.title.toLowerCase().includes(q)) results.push(m);
|
|
8156
|
+
}
|
|
8157
|
+
results.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
8158
|
+
const page = results.slice(0, limit);
|
|
8159
|
+
return {
|
|
8160
|
+
conversations: page.map((m) => this.summarizeManifest(m)),
|
|
8161
|
+
page: { limit, has_more: results.length > limit, next_cursor: null }
|
|
8162
|
+
};
|
|
8163
|
+
}
|
|
7999
8164
|
async createConversation(options = {}) {
|
|
8000
8165
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8001
8166
|
const id = createConversationId();
|
|
@@ -8017,7 +8182,7 @@ var ConversationService = class extends EventEmitter4 {
|
|
|
8017
8182
|
return this.summarizeManifest(manifest);
|
|
8018
8183
|
}
|
|
8019
8184
|
async getMessages(conversationId, options = {}) {
|
|
8020
|
-
const manifest = await
|
|
8185
|
+
const manifest = await readExistingManifest(this.paths, conversationId);
|
|
8021
8186
|
const snapshot = await readSnapshot(this.paths, conversationId);
|
|
8022
8187
|
const limit = normalizeLimit(options.limit, 50, 200);
|
|
8023
8188
|
const total = snapshot.messages.length;
|
|
@@ -8028,6 +8193,7 @@ var ConversationService = class extends EventEmitter4 {
|
|
|
8028
8193
|
return {
|
|
8029
8194
|
messages,
|
|
8030
8195
|
last_event_seq: manifest.last_event_seq,
|
|
8196
|
+
runtime: this.buildRuntimeMetadata(manifest),
|
|
8031
8197
|
page: {
|
|
8032
8198
|
limit,
|
|
8033
8199
|
has_more_before: startIndex > 0,
|
|
@@ -8043,7 +8209,16 @@ var ConversationService = class extends EventEmitter4 {
|
|
|
8043
8209
|
async sendMessageLocked(input) {
|
|
8044
8210
|
const content = input.content.trim();
|
|
8045
8211
|
if (!content) throw new LinkHttpError(400, "message_content_required", "message content is required");
|
|
8046
|
-
const
|
|
8212
|
+
const raw = await readManifest(this.paths, input.conversationId);
|
|
8213
|
+
if (!raw || raw.status === "deleted_soft") {
|
|
8214
|
+
throw new LinkHttpError(404, "conversation_not_found", "Conversation was not found");
|
|
8215
|
+
}
|
|
8216
|
+
if (raw.status === "archived") {
|
|
8217
|
+
raw.status = "active";
|
|
8218
|
+
raw.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
8219
|
+
await writeManifest(this.paths, raw);
|
|
8220
|
+
}
|
|
8221
|
+
const manifest = raw;
|
|
8047
8222
|
const snapshot = await readSnapshot(this.paths, input.conversationId);
|
|
8048
8223
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8049
8224
|
const runId = createRunId();
|
|
@@ -8109,9 +8284,14 @@ var ConversationService = class extends EventEmitter4 {
|
|
|
8109
8284
|
assistant_message: { id: assistantMessageId, status: assistantMessage.status },
|
|
8110
8285
|
run: { id: runId, status: run.status },
|
|
8111
8286
|
last_event_seq: latestEvent.seq,
|
|
8112
|
-
conversation: this.summarizeManifest(manifest)
|
|
8287
|
+
conversation: this.summarizeManifest(manifest, snapshot)
|
|
8113
8288
|
};
|
|
8114
8289
|
}
|
|
8290
|
+
async cancelRunById(runId) {
|
|
8291
|
+
const active = this.activeRunControllers.get(runId);
|
|
8292
|
+
if (!active) throw new LinkHttpError(404, "run_not_found", "Run was not found");
|
|
8293
|
+
return this.cancelRun(active.conversationId, runId);
|
|
8294
|
+
}
|
|
8115
8295
|
async cancelRun(conversationId, runId) {
|
|
8116
8296
|
const active = this.activeRunControllers.get(runId);
|
|
8117
8297
|
if (active) active.controller.abort();
|
|
@@ -8330,13 +8510,50 @@ var ConversationService = class extends EventEmitter4 {
|
|
|
8330
8510
|
async deleteConversation(conversationId) {
|
|
8331
8511
|
assertValidConversationId(conversationId);
|
|
8332
8512
|
return withConversationLock(conversationId, async () => {
|
|
8333
|
-
const manifest = await
|
|
8513
|
+
const manifest = await readExistingManifest(this.paths, conversationId);
|
|
8334
8514
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8515
|
+
const hermesSessionIds = manifest.hermes_session_ids ?? (manifest.hermes_session_id ? [manifest.hermes_session_id] : []);
|
|
8335
8516
|
manifest.status = "deleted_soft";
|
|
8336
8517
|
manifest.deleted_at = now;
|
|
8337
8518
|
manifest.updated_at = now;
|
|
8338
8519
|
await writeManifest(this.paths, manifest);
|
|
8339
|
-
return {
|
|
8520
|
+
return {
|
|
8521
|
+
conversation_id: conversationId,
|
|
8522
|
+
hermes_deleted: false,
|
|
8523
|
+
...hermesSessionIds.length > 0 ? { hermes_session_ids: hermesSessionIds } : {},
|
|
8524
|
+
deleted_at: now
|
|
8525
|
+
};
|
|
8526
|
+
});
|
|
8527
|
+
}
|
|
8528
|
+
async archiveConversation(conversationId) {
|
|
8529
|
+
assertValidConversationId(conversationId);
|
|
8530
|
+
return withConversationLock(conversationId, async () => {
|
|
8531
|
+
const manifest = await readActiveManifest(this.paths, conversationId);
|
|
8532
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8533
|
+
manifest.status = "archived";
|
|
8534
|
+
manifest.updated_at = now;
|
|
8535
|
+
await writeManifest(this.paths, manifest);
|
|
8536
|
+
const conv = this.summarizeManifest(manifest);
|
|
8537
|
+
await this.appendAndEmit(conversationId, { type: "conversation.archived", payload: { conversation: conv } }, manifest);
|
|
8538
|
+
await writeManifest(this.paths, manifest);
|
|
8539
|
+
return { conversation_id: conversationId, archived_at: now };
|
|
8540
|
+
});
|
|
8541
|
+
}
|
|
8542
|
+
async unarchiveConversation(conversationId) {
|
|
8543
|
+
assertValidConversationId(conversationId);
|
|
8544
|
+
return withConversationLock(conversationId, async () => {
|
|
8545
|
+
const manifest = await readExistingManifest(this.paths, conversationId);
|
|
8546
|
+
if (manifest.status !== "archived") {
|
|
8547
|
+
throw new LinkHttpError(400, "conversation_not_archived", "Conversation is not archived");
|
|
8548
|
+
}
|
|
8549
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8550
|
+
manifest.status = "active";
|
|
8551
|
+
manifest.updated_at = now;
|
|
8552
|
+
await writeManifest(this.paths, manifest);
|
|
8553
|
+
const conv = this.summarizeManifest(manifest);
|
|
8554
|
+
await this.appendAndEmit(conversationId, { type: "conversation.unarchived", payload: { conversation: conv } }, manifest);
|
|
8555
|
+
await writeManifest(this.paths, manifest);
|
|
8556
|
+
return { conversation_id: conversationId, unarchived_at: now };
|
|
8340
8557
|
});
|
|
8341
8558
|
}
|
|
8342
8559
|
async deleteConversations(conversationIds) {
|
|
@@ -8363,17 +8580,27 @@ var ConversationService = class extends EventEmitter4 {
|
|
|
8363
8580
|
manifest.title = title;
|
|
8364
8581
|
manifest.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
8365
8582
|
await writeManifest(this.paths, manifest);
|
|
8366
|
-
|
|
8583
|
+
const conv = this.summarizeManifest(manifest);
|
|
8584
|
+
const event = await this.appendAndEmit(conversationId, { type: "conversation.updated", payload: { conversation: conv } }, manifest);
|
|
8367
8585
|
await writeManifest(this.paths, manifest);
|
|
8368
|
-
return { conversation_id: conversationId, title };
|
|
8586
|
+
return { conversation_id: conversationId, title, conversation: conv, hermes_synced: false, last_event_seq: event.seq };
|
|
8369
8587
|
});
|
|
8370
8588
|
}
|
|
8371
8589
|
async setConversationModel(conversationId, modelId) {
|
|
8372
8590
|
return withConversationLock(conversationId, async () => {
|
|
8373
8591
|
const manifest = await readActiveManifest(this.paths, conversationId);
|
|
8592
|
+
if (manifest.stats) manifest.stats.model = modelId;
|
|
8374
8593
|
manifest.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
8375
8594
|
await writeManifest(this.paths, manifest);
|
|
8376
|
-
|
|
8595
|
+
const conv = this.summarizeManifest(manifest);
|
|
8596
|
+
const event = await this.appendAndEmit(conversationId, { type: "conversation.updated", payload: { conversation: conv } }, manifest);
|
|
8597
|
+
await writeManifest(this.paths, manifest);
|
|
8598
|
+
return {
|
|
8599
|
+
conversation_id: conversationId,
|
|
8600
|
+
model_override: modelId,
|
|
8601
|
+
runtime: this.buildRuntimeMetadata(manifest),
|
|
8602
|
+
last_event_seq: event.seq
|
|
8603
|
+
};
|
|
8377
8604
|
});
|
|
8378
8605
|
}
|
|
8379
8606
|
async setConversationProfile(conversationId, profileName) {
|
|
@@ -8383,7 +8610,21 @@ var ConversationService = class extends EventEmitter4 {
|
|
|
8383
8610
|
manifest.profile_name_snapshot = profileName;
|
|
8384
8611
|
manifest.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
8385
8612
|
await writeManifest(this.paths, manifest);
|
|
8386
|
-
|
|
8613
|
+
const conv = this.summarizeManifest(manifest);
|
|
8614
|
+
const event = await this.appendAndEmit(conversationId, { type: "conversation.updated", payload: { conversation: conv } }, manifest);
|
|
8615
|
+
await writeManifest(this.paths, manifest);
|
|
8616
|
+
return {
|
|
8617
|
+
conversation_id: conversationId,
|
|
8618
|
+
profile: {
|
|
8619
|
+
uid: manifest.profile_uid ?? "",
|
|
8620
|
+
name: profileName,
|
|
8621
|
+
display_name: profileName,
|
|
8622
|
+
avatar_url: null
|
|
8623
|
+
},
|
|
8624
|
+
runtime: this.buildRuntimeMetadata(manifest),
|
|
8625
|
+
conversation: conv,
|
|
8626
|
+
last_event_seq: event.seq
|
|
8627
|
+
};
|
|
8387
8628
|
});
|
|
8388
8629
|
}
|
|
8389
8630
|
async ackConversation(conversationId, lastEventSeq) {
|
|
@@ -8413,55 +8654,194 @@ var ConversationService = class extends EventEmitter4 {
|
|
|
8413
8654
|
}
|
|
8414
8655
|
return { deleted_count: count };
|
|
8415
8656
|
}
|
|
8416
|
-
async
|
|
8657
|
+
async shouldPublishNotificationEvent(event) {
|
|
8658
|
+
const publishable = ["conversation.updated", "message.created", "run.started", "run.completed", "run.failed", "run.cancelled"];
|
|
8659
|
+
return publishable.includes(event.type);
|
|
8660
|
+
}
|
|
8661
|
+
async prepareClearAllConversationPlan(targetStatus = "active") {
|
|
8417
8662
|
const planId = `plan_${crypto4.randomUUID().replaceAll("-", "")}`;
|
|
8663
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8418
8664
|
const ids = await listConversationIds(this.paths);
|
|
8419
|
-
const
|
|
8665
|
+
const targetIds = [];
|
|
8420
8666
|
for (const id of ids) {
|
|
8421
8667
|
const m = await readManifest(this.paths, id);
|
|
8422
|
-
if (m && m.status ===
|
|
8668
|
+
if (m && m.status === targetStatus) targetIds.push(id);
|
|
8423
8669
|
}
|
|
8424
|
-
|
|
8670
|
+
const plan = {
|
|
8425
8671
|
id: planId,
|
|
8426
|
-
status: "
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8672
|
+
status: "prepared",
|
|
8673
|
+
target_status: targetStatus,
|
|
8674
|
+
created_at: now,
|
|
8675
|
+
updated_at: now,
|
|
8676
|
+
total_count: targetIds.length,
|
|
8677
|
+
deleted_count: 0,
|
|
8678
|
+
failed_count: 0,
|
|
8679
|
+
conversation_ids: targetIds,
|
|
8680
|
+
conversations: []
|
|
8430
8681
|
};
|
|
8682
|
+
clearPlans.set(planId, plan);
|
|
8683
|
+
return plan;
|
|
8431
8684
|
}
|
|
8432
8685
|
async readClearAllConversationPlan(planId) {
|
|
8433
|
-
|
|
8686
|
+
const plan = clearPlans.get(planId);
|
|
8687
|
+
if (!plan) throw new LinkHttpError(404, "plan_not_found", "Clear plan was not found");
|
|
8688
|
+
return plan;
|
|
8689
|
+
}
|
|
8690
|
+
async executeClearAllConversationPlan(planId) {
|
|
8691
|
+
return this.startClearAllConversationPlan(planId);
|
|
8434
8692
|
}
|
|
8435
8693
|
async startClearAllConversationPlan(planId) {
|
|
8694
|
+
const plan = clearPlans.get(planId) ?? { id: planId, conversation_ids: [], total_count: 0 };
|
|
8695
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8696
|
+
clearPlans.set(planId, { ...plan, status: "executing", updated_at: now });
|
|
8697
|
+
const conversationIds = Array.isArray(plan.conversation_ids) ? plan.conversation_ids : [];
|
|
8698
|
+
let deletedCount = 0, failedCount = 0;
|
|
8699
|
+
const conversations = [];
|
|
8700
|
+
for (const id of conversationIds) {
|
|
8701
|
+
try {
|
|
8702
|
+
const result = await this.deleteConversation(id);
|
|
8703
|
+
conversations.push({ ...result, status: "deleted" });
|
|
8704
|
+
deletedCount++;
|
|
8705
|
+
} catch (err) {
|
|
8706
|
+
failedCount++;
|
|
8707
|
+
conversations.push({
|
|
8708
|
+
conversation_id: id,
|
|
8709
|
+
status: "failed",
|
|
8710
|
+
error: { code: err instanceof LinkHttpError ? err.code : "internal_error", message: err.message }
|
|
8711
|
+
});
|
|
8712
|
+
}
|
|
8713
|
+
}
|
|
8714
|
+
const completed = {
|
|
8715
|
+
...plan,
|
|
8716
|
+
status: "completed",
|
|
8717
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8718
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8719
|
+
deleted_count: deletedCount,
|
|
8720
|
+
failed_count: failedCount,
|
|
8721
|
+
conversations
|
|
8722
|
+
};
|
|
8723
|
+
clearPlans.set(planId, completed);
|
|
8724
|
+
return completed;
|
|
8725
|
+
}
|
|
8726
|
+
async prepareArchiveAllConversationPlan(input = {}) {
|
|
8727
|
+
const planId = `plan_${crypto4.randomUUID().replaceAll("-", "")}`;
|
|
8728
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8729
|
+
const excluded = new Set(input.excludeConversationIds ?? []);
|
|
8436
8730
|
const ids = await listConversationIds(this.paths);
|
|
8437
|
-
|
|
8731
|
+
const targetIds = [];
|
|
8438
8732
|
for (const id of ids) {
|
|
8733
|
+
if (excluded.has(id)) continue;
|
|
8439
8734
|
const m = await readManifest(this.paths, id);
|
|
8440
|
-
if (m && m.status === "active")
|
|
8441
|
-
|
|
8442
|
-
|
|
8735
|
+
if (m && m.status === "active") targetIds.push(id);
|
|
8736
|
+
}
|
|
8737
|
+
const plan = {
|
|
8738
|
+
id: planId,
|
|
8739
|
+
status: "prepared",
|
|
8740
|
+
created_at: now,
|
|
8741
|
+
updated_at: now,
|
|
8742
|
+
total_count: targetIds.length,
|
|
8743
|
+
archived_count: 0,
|
|
8744
|
+
failed_count: 0,
|
|
8745
|
+
conversation_ids: targetIds,
|
|
8746
|
+
conversations: []
|
|
8747
|
+
};
|
|
8748
|
+
archivePlans.set(planId, plan);
|
|
8749
|
+
return plan;
|
|
8750
|
+
}
|
|
8751
|
+
async readArchiveAllConversationPlan(planId) {
|
|
8752
|
+
const plan = archivePlans.get(planId);
|
|
8753
|
+
if (!plan) throw new LinkHttpError(404, "plan_not_found", "Archive plan was not found");
|
|
8754
|
+
return plan;
|
|
8755
|
+
}
|
|
8756
|
+
async executeArchiveAllConversationPlan(planId) {
|
|
8757
|
+
return this.startArchiveAllConversationPlan(planId);
|
|
8758
|
+
}
|
|
8759
|
+
async startArchiveAllConversationPlan(planId) {
|
|
8760
|
+
const plan = archivePlans.get(planId) ?? { id: planId, conversation_ids: [], total_count: 0 };
|
|
8761
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8762
|
+
archivePlans.set(planId, { ...plan, status: "executing", updated_at: now });
|
|
8763
|
+
const conversationIds = Array.isArray(plan.conversation_ids) ? plan.conversation_ids : [];
|
|
8764
|
+
let archivedCount = 0, failedCount = 0;
|
|
8765
|
+
const conversations = [];
|
|
8766
|
+
for (const id of conversationIds) {
|
|
8767
|
+
try {
|
|
8768
|
+
const result = await this.archiveConversation(id);
|
|
8769
|
+
conversations.push({ ...result, status: "archived" });
|
|
8770
|
+
archivedCount++;
|
|
8771
|
+
} catch (err) {
|
|
8772
|
+
failedCount++;
|
|
8773
|
+
conversations.push({
|
|
8774
|
+
conversation_id: id,
|
|
8775
|
+
status: "failed",
|
|
8776
|
+
error: { code: err instanceof LinkHttpError ? err.code : "internal_error", message: err.message }
|
|
8777
|
+
});
|
|
8443
8778
|
}
|
|
8444
8779
|
}
|
|
8445
|
-
|
|
8780
|
+
const completed = {
|
|
8781
|
+
...plan,
|
|
8782
|
+
status: "completed",
|
|
8783
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8784
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8785
|
+
archived_count: archivedCount,
|
|
8786
|
+
failed_count: failedCount,
|
|
8787
|
+
conversations
|
|
8788
|
+
};
|
|
8789
|
+
archivePlans.set(planId, completed);
|
|
8790
|
+
return completed;
|
|
8446
8791
|
}
|
|
8447
8792
|
async resolveApproval(input) {
|
|
8448
|
-
|
|
8793
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8794
|
+
let lastEventSeq = 0;
|
|
8795
|
+
try {
|
|
8796
|
+
const m = await readManifest(this.paths, input.conversationId);
|
|
8797
|
+
if (m) lastEventSeq = m.last_event_seq;
|
|
8798
|
+
} catch {
|
|
8799
|
+
}
|
|
8800
|
+
return {
|
|
8801
|
+
conversation_id: input.conversationId,
|
|
8802
|
+
message_id: "",
|
|
8803
|
+
approval: {
|
|
8804
|
+
id: input.approvalId,
|
|
8805
|
+
status: input.decision === "deny" ? "denied" : "approved",
|
|
8806
|
+
kind: "terminal_command",
|
|
8807
|
+
command: "",
|
|
8808
|
+
choices: ["once", "session", "always", "deny"],
|
|
8809
|
+
created_at: now,
|
|
8810
|
+
resolved_at: now,
|
|
8811
|
+
decision: input.decision,
|
|
8812
|
+
resume_available: false
|
|
8813
|
+
},
|
|
8814
|
+
command_allowlist_updated: false,
|
|
8815
|
+
requires_gateway_reload: false,
|
|
8816
|
+
resume_available: false,
|
|
8817
|
+
last_event_seq: lastEventSeq
|
|
8818
|
+
};
|
|
8449
8819
|
}
|
|
8450
|
-
async getStatistics(options) {
|
|
8820
|
+
async getStatistics(options = {}) {
|
|
8451
8821
|
const ids = await listConversationIds(this.paths);
|
|
8452
|
-
let total = 0, active = 0,
|
|
8822
|
+
let total = 0, active = 0, archived = 0, deleted = 0;
|
|
8823
|
+
let inputTokens = 0, outputTokens = 0, totalTokens = 0;
|
|
8824
|
+
let messages = 0, runs = 0;
|
|
8453
8825
|
for (const id of ids) {
|
|
8454
8826
|
const m = await readManifest(this.paths, id);
|
|
8455
8827
|
if (!m) continue;
|
|
8456
8828
|
if (options.profileName && m.profile !== options.profileName) continue;
|
|
8829
|
+
if (options.profileUid && m.profile_uid !== options.profileUid) continue;
|
|
8457
8830
|
total++;
|
|
8458
8831
|
if (m.status === "active") active++;
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8832
|
+
else if (m.status === "archived") archived++;
|
|
8833
|
+
else if (m.status === "deleted_soft") deleted++;
|
|
8834
|
+
if (m.stats) {
|
|
8835
|
+
inputTokens += m.stats.input_tokens;
|
|
8836
|
+
outputTokens += m.stats.output_tokens;
|
|
8837
|
+
totalTokens += m.stats.total_tokens;
|
|
8838
|
+
messages += m.stats.message_count;
|
|
8839
|
+
runs += m.stats.run_count;
|
|
8840
|
+
}
|
|
8462
8841
|
}
|
|
8463
8842
|
return {
|
|
8464
|
-
conversations: { total, active },
|
|
8843
|
+
conversations: { total, active, archived, deleted },
|
|
8844
|
+
tokens: { input_tokens: inputTokens, output_tokens: outputTokens, total_tokens: totalTokens },
|
|
8465
8845
|
messages: { total: messages },
|
|
8466
8846
|
runs: { total: runs },
|
|
8467
8847
|
models: { total: 0 },
|
|
@@ -8470,20 +8850,6 @@ var ConversationService = class extends EventEmitter4 {
|
|
|
8470
8850
|
profiles: { total: 0 }
|
|
8471
8851
|
};
|
|
8472
8852
|
}
|
|
8473
|
-
summarizeManifest(manifest) {
|
|
8474
|
-
return {
|
|
8475
|
-
id: manifest.id,
|
|
8476
|
-
kind: manifest.kind,
|
|
8477
|
-
title: manifest.title,
|
|
8478
|
-
status: manifest.status,
|
|
8479
|
-
profile: manifest.profile,
|
|
8480
|
-
profile_uid: manifest.profile_uid,
|
|
8481
|
-
last_event_seq: manifest.last_event_seq,
|
|
8482
|
-
created_at: manifest.created_at,
|
|
8483
|
-
updated_at: manifest.updated_at,
|
|
8484
|
-
stats: manifest.stats ?? null
|
|
8485
|
-
};
|
|
8486
|
-
}
|
|
8487
8853
|
};
|
|
8488
8854
|
|
|
8489
8855
|
// src/http/app.ts
|
package/dist/cli/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
saveConfig,
|
|
22
22
|
startLinkService,
|
|
23
23
|
writeJsonFile
|
|
24
|
-
} from "../chunk-
|
|
24
|
+
} from "../chunk-ELQBIHDQ.js";
|
|
25
25
|
import "../chunk-NP3Y2NVF.js";
|
|
26
26
|
|
|
27
27
|
// src/cli/index.ts
|
|
@@ -261,7 +261,7 @@ async function runPairingPreflight(options) {
|
|
|
261
261
|
code: token.token,
|
|
262
262
|
preferred_urls: preferredUrls
|
|
263
263
|
};
|
|
264
|
-
const pageUrl = buildLocalPairingPageUrl(options.config.port
|
|
264
|
+
const pageUrl = buildLocalPairingPageUrl(preferredUrls[0] ?? `http://127.0.0.1:${options.config.port}`, sessionId, token.token);
|
|
265
265
|
if (options.openBrowser !== false) {
|
|
266
266
|
await openSystemBrowser(pageUrl).catch(() => void 0);
|
|
267
267
|
}
|
|
@@ -273,9 +273,9 @@ async function runPairingPreflight(options) {
|
|
|
273
273
|
preferredUrls
|
|
274
274
|
};
|
|
275
275
|
}
|
|
276
|
-
function buildLocalPairingPageUrl(
|
|
276
|
+
function buildLocalPairingPageUrl(baseUrl, sessionId, connectToken) {
|
|
277
277
|
const qs = new URLSearchParams({ session_id: sessionId, connect_token: connectToken });
|
|
278
|
-
return
|
|
278
|
+
return `${baseUrl}/pair?${qs.toString()}`;
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
// src/cli/index.ts
|
package/dist/http/app.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bulolo/hermes-link",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "Provides full client API, multi-device auth and conversation management for Hermes Agent, with LAN and internet connectivity.",
|
|
5
|
+
"author": "Bulolo",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"bin": {
|