@hermespilot/link 0.1.4 → 0.1.6
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/dist/{chunk-T35GPRKF.js → chunk-VCQJ5DSN.js} +1270 -80
- package/dist/chunk-VCQJ5DSN.js.map +1 -0
- package/dist/cli/index.js +84 -31
- package/dist/cli/index.js.map +1 -1
- package/dist/http/app.d.ts +3 -0
- package/dist/http/app.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-T35GPRKF.js.map +0 -1
|
@@ -4,7 +4,7 @@ import Router from "@koa/router";
|
|
|
4
4
|
import { Readable } from "stream";
|
|
5
5
|
|
|
6
6
|
// src/constants.ts
|
|
7
|
-
var LINK_VERSION = "0.1.
|
|
7
|
+
var LINK_VERSION = "0.1.6";
|
|
8
8
|
var LINK_COMMAND = "hermeslink";
|
|
9
9
|
var LINK_DEFAULT_PORT = 52379;
|
|
10
10
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -23,6 +23,9 @@ function resolveRuntimePaths(homeDir = resolveRuntimeHome()) {
|
|
|
23
23
|
stateFile: path.join(homeDir, "state.json"),
|
|
24
24
|
credentialsFile: path.join(homeDir, "credentials.json"),
|
|
25
25
|
databaseFile: path.join(homeDir, "link.db"),
|
|
26
|
+
conversationsDir: path.join(homeDir, "conversations"),
|
|
27
|
+
blobsDir: path.join(homeDir, "blobs"),
|
|
28
|
+
indexesDir: path.join(homeDir, "indexes"),
|
|
26
29
|
logsDir: path.join(homeDir, "logs"),
|
|
27
30
|
runDir: path.join(homeDir, "run"),
|
|
28
31
|
pairingDir: path.join(homeDir, "pairing")
|
|
@@ -91,19 +94,11 @@ function normalizeConfiguredLanguage(language) {
|
|
|
91
94
|
return defaultLinkConfig.language;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
// src/
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.code = code;
|
|
100
|
-
}
|
|
101
|
-
status;
|
|
102
|
-
code;
|
|
103
|
-
};
|
|
104
|
-
function isLinkHttpError(error) {
|
|
105
|
-
return error instanceof LinkHttpError;
|
|
106
|
-
}
|
|
97
|
+
// src/conversations/conversation-service.ts
|
|
98
|
+
import { EventEmitter } from "events";
|
|
99
|
+
import { appendFile, mkdir as mkdir3, readdir, readFile as readFile3, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
100
|
+
import path4 from "path";
|
|
101
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
107
102
|
|
|
108
103
|
// src/hermes/api-server.ts
|
|
109
104
|
import { randomUUID } from "crypto";
|
|
@@ -205,6 +200,20 @@ function isNodeError2(error, code) {
|
|
|
205
200
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
206
201
|
}
|
|
207
202
|
|
|
203
|
+
// src/http/errors.ts
|
|
204
|
+
var LinkHttpError = class extends Error {
|
|
205
|
+
constructor(status, code, message) {
|
|
206
|
+
super(message);
|
|
207
|
+
this.status = status;
|
|
208
|
+
this.code = code;
|
|
209
|
+
}
|
|
210
|
+
status;
|
|
211
|
+
code;
|
|
212
|
+
};
|
|
213
|
+
function isLinkHttpError(error) {
|
|
214
|
+
return error instanceof LinkHttpError;
|
|
215
|
+
}
|
|
216
|
+
|
|
208
217
|
// src/hermes/api-server.ts
|
|
209
218
|
var fallbackRuns = /* @__PURE__ */ new Map();
|
|
210
219
|
async function listHermesModels(options = {}) {
|
|
@@ -270,7 +279,7 @@ async function cancelHermesRun(runId, options = {}) {
|
|
|
270
279
|
}
|
|
271
280
|
fallbackRuns.delete(runId);
|
|
272
281
|
}
|
|
273
|
-
async function callHermesApi(
|
|
282
|
+
async function callHermesApi(path8, init, options) {
|
|
274
283
|
const config = await readHermesApiServerConfig();
|
|
275
284
|
if (!config.port || !config.key) {
|
|
276
285
|
return new Response(null, { status: 503 });
|
|
@@ -280,7 +289,7 @@ async function callHermesApi(path6, init, options) {
|
|
|
280
289
|
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
281
290
|
headers.set("x-api-key", config.key);
|
|
282
291
|
headers.set("authorization", `Bearer ${config.key}`);
|
|
283
|
-
return await fetcher(`http://127.0.0.1:${config.port}${
|
|
292
|
+
return await fetcher(`http://127.0.0.1:${config.port}${path8}`, {
|
|
284
293
|
...init,
|
|
285
294
|
headers
|
|
286
295
|
}).catch(() => new Response(null, { status: 503 }));
|
|
@@ -314,19 +323,991 @@ function readString(payload, key) {
|
|
|
314
323
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
315
324
|
}
|
|
316
325
|
|
|
326
|
+
// src/hermes/cli.ts
|
|
327
|
+
import { execFile } from "child_process";
|
|
328
|
+
import { promisify } from "util";
|
|
329
|
+
var execFileAsync = promisify(execFile);
|
|
330
|
+
async function deleteHermesSession(sessionId) {
|
|
331
|
+
if (!sessionId.trim()) {
|
|
332
|
+
throw new LinkHttpError(400, "hermes_session_id_required", "Hermes session id is required");
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
await execFileAsync(hermesBin(), ["sessions", "delete", sessionId, "--yes"], {
|
|
336
|
+
timeout: 1e4,
|
|
337
|
+
windowsHide: true
|
|
338
|
+
});
|
|
339
|
+
} catch (error) {
|
|
340
|
+
throw new LinkHttpError(
|
|
341
|
+
502,
|
|
342
|
+
"hermes_session_delete_failed",
|
|
343
|
+
error instanceof Error ? error.message : "Hermes session delete failed"
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function hermesBin() {
|
|
348
|
+
return process.env.HERMES_BIN?.trim() || "hermes";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/conversations/conversation-service.ts
|
|
352
|
+
var MAX_IMPORTED_BLOB_BYTES = 100 * 1024 * 1024;
|
|
353
|
+
var MAX_UPLOADED_BLOB_BYTES = 50 * 1024 * 1024;
|
|
354
|
+
var CONVERSATION_ID_PATTERN = /^conv_[a-f0-9]{32}$/u;
|
|
355
|
+
var BLOB_ID_PATTERN = /^blob_[a-f0-9]{32}$/u;
|
|
356
|
+
var MEDIA_TAG_PATTERN = /[`"']?MEDIA:\s*(`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~\/|\/)\S+(?:[^\S\n]+\S+)*?\.(?:png|jpe?g|gif|webp|heic|pdf|txt|md|json|csv|mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a)(?=[\s`"',;:)\]}]|$)|\S+)[`"']?/giu;
|
|
357
|
+
var ConversationService = class {
|
|
358
|
+
constructor(paths, logger) {
|
|
359
|
+
this.paths = paths;
|
|
360
|
+
this.logger = logger;
|
|
361
|
+
this.emitter.setMaxListeners(0);
|
|
362
|
+
}
|
|
363
|
+
paths;
|
|
364
|
+
logger;
|
|
365
|
+
emitter = new EventEmitter();
|
|
366
|
+
async listConversations() {
|
|
367
|
+
await mkdir3(this.paths.conversationsDir, { recursive: true, mode: 448 });
|
|
368
|
+
const entries = await readdir(this.paths.conversationsDir, { withFileTypes: true }).catch((error) => {
|
|
369
|
+
if (isNodeError3(error, "ENOENT")) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
throw error;
|
|
373
|
+
});
|
|
374
|
+
const summaries = [];
|
|
375
|
+
for (const entry of entries) {
|
|
376
|
+
if (!entry.isDirectory()) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const manifest = await this.readManifest(entry.name).catch(() => null);
|
|
380
|
+
if (!manifest || manifest.status !== "active") {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const snapshot = await this.readSnapshot(entry.name).catch(() => emptySnapshot());
|
|
384
|
+
summaries.push(toSummary(manifest, snapshot));
|
|
385
|
+
}
|
|
386
|
+
return summaries.sort((left, right) => Date.parse(right.updated_at) - Date.parse(left.updated_at));
|
|
387
|
+
}
|
|
388
|
+
async createConversation(input = {}) {
|
|
389
|
+
await mkdir3(this.paths.conversationsDir, { recursive: true, mode: 448 });
|
|
390
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
391
|
+
const id = `conv_${randomUUID2().replaceAll("-", "")}`;
|
|
392
|
+
const title = input.title?.trim() || "Untitled";
|
|
393
|
+
const manifest = {
|
|
394
|
+
id,
|
|
395
|
+
schema_version: 1,
|
|
396
|
+
kind: "direct",
|
|
397
|
+
title,
|
|
398
|
+
status: "active",
|
|
399
|
+
hermes_session_id: `hp_${id}`,
|
|
400
|
+
created_at: now,
|
|
401
|
+
updated_at: now,
|
|
402
|
+
last_event_seq: 0
|
|
403
|
+
};
|
|
404
|
+
await mkdir3(this.conversationDir(id), { recursive: true, mode: 448 });
|
|
405
|
+
await this.writeManifest(manifest);
|
|
406
|
+
await this.writeSnapshot(id, emptySnapshot());
|
|
407
|
+
const event = await this.appendEvent(manifest.id, {
|
|
408
|
+
type: "conversation.created",
|
|
409
|
+
payload: { title }
|
|
410
|
+
});
|
|
411
|
+
return { ...toSummary({ ...manifest, last_event_seq: event.seq }, emptySnapshot()), last_event_seq: event.seq };
|
|
412
|
+
}
|
|
413
|
+
async getMessages(conversationId) {
|
|
414
|
+
const manifest = await this.readActiveManifest(conversationId);
|
|
415
|
+
const snapshot = await this.readSnapshot(conversationId);
|
|
416
|
+
return {
|
|
417
|
+
messages: snapshot.messages,
|
|
418
|
+
last_event_seq: manifest.last_event_seq
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
async listEvents(conversationId, after = 0) {
|
|
422
|
+
await this.readManifest(conversationId);
|
|
423
|
+
const eventsPath = this.eventsPath(conversationId);
|
|
424
|
+
const raw = await readFile3(eventsPath, "utf8").catch((error) => {
|
|
425
|
+
if (isNodeError3(error, "ENOENT")) {
|
|
426
|
+
return "";
|
|
427
|
+
}
|
|
428
|
+
throw error;
|
|
429
|
+
});
|
|
430
|
+
return raw.split("\n").filter(Boolean).map((line) => JSON.parse(line)).filter((event) => event.seq > after);
|
|
431
|
+
}
|
|
432
|
+
subscribe(conversationId, listener) {
|
|
433
|
+
const eventName = this.liveEventName(conversationId);
|
|
434
|
+
this.emitter.on(eventName, listener);
|
|
435
|
+
return () => this.emitter.off(eventName, listener);
|
|
436
|
+
}
|
|
437
|
+
async sendMessage(input) {
|
|
438
|
+
const manifest = await this.readActiveManifest(input.conversationId);
|
|
439
|
+
const content = input.content.trim();
|
|
440
|
+
const userAttachmentParts = await this.resolveMessageAttachmentParts(
|
|
441
|
+
manifest.id,
|
|
442
|
+
input.attachments ?? []
|
|
443
|
+
);
|
|
444
|
+
if (!content && userAttachmentParts.length === 0) {
|
|
445
|
+
throw new LinkHttpError(400, "message_content_required", "message content is required");
|
|
446
|
+
}
|
|
447
|
+
const idempotencyKey = input.clientMessageId ?? input.idempotencyKey;
|
|
448
|
+
const snapshot = await this.readSnapshot(input.conversationId);
|
|
449
|
+
if (idempotencyKey) {
|
|
450
|
+
const existingUser = snapshot.messages.find((message) => message.client_message_id === idempotencyKey);
|
|
451
|
+
if (existingUser) {
|
|
452
|
+
const existingRun = snapshot.runs.find((run2) => run2.trigger_message_id === existingUser.id);
|
|
453
|
+
const existingAssistant = existingRun ? snapshot.messages.find((message) => message.id === existingRun.assistant_message_id) : null;
|
|
454
|
+
return {
|
|
455
|
+
conversation_id: manifest.id,
|
|
456
|
+
user_message: { id: existingUser.id, status: existingUser.status },
|
|
457
|
+
assistant_message: { id: existingAssistant?.id ?? "", status: existingAssistant?.status ?? "failed" },
|
|
458
|
+
run: { id: existingRun?.id ?? "", status: existingRun?.status ?? "unknown" },
|
|
459
|
+
last_event_seq: manifest.last_event_seq
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
464
|
+
const userMessage = {
|
|
465
|
+
id: `msg_${randomUUID2().replaceAll("-", "")}`,
|
|
466
|
+
schema_version: 1,
|
|
467
|
+
conversation_id: manifest.id,
|
|
468
|
+
role: "user",
|
|
469
|
+
status: "completed",
|
|
470
|
+
client_message_id: idempotencyKey,
|
|
471
|
+
created_at: now,
|
|
472
|
+
updated_at: now,
|
|
473
|
+
sender: { id: "app_user", type: "human", display_name: "Me" },
|
|
474
|
+
parts: [{ type: "text", text: content }, ...userAttachmentParts],
|
|
475
|
+
attachments: userAttachmentParts.map((part) => ({
|
|
476
|
+
blob_id: part.blob,
|
|
477
|
+
mime: part.mime,
|
|
478
|
+
size: part.size,
|
|
479
|
+
filename: part.filename
|
|
480
|
+
})),
|
|
481
|
+
raw: { format: "hermes-link-user-message", payload: { content, attachments: input.attachments ?? [] } }
|
|
482
|
+
};
|
|
483
|
+
const assistantMessage = {
|
|
484
|
+
id: `msg_${randomUUID2().replaceAll("-", "")}`,
|
|
485
|
+
schema_version: 1,
|
|
486
|
+
conversation_id: manifest.id,
|
|
487
|
+
role: "assistant",
|
|
488
|
+
status: "streaming",
|
|
489
|
+
created_at: now,
|
|
490
|
+
updated_at: now,
|
|
491
|
+
sender: { id: "agent_default", type: "agent", display_name: "Hermes", profile: "default" },
|
|
492
|
+
parts: [{ type: "text", text: "" }],
|
|
493
|
+
attachments: []
|
|
494
|
+
};
|
|
495
|
+
const run = {
|
|
496
|
+
id: `run_${randomUUID2().replaceAll("-", "")}`,
|
|
497
|
+
conversation_id: manifest.id,
|
|
498
|
+
trigger_message_id: userMessage.id,
|
|
499
|
+
assistant_message_id: assistantMessage.id,
|
|
500
|
+
hermes_session_id: manifest.hermes_session_id,
|
|
501
|
+
status: "running",
|
|
502
|
+
started_at: now
|
|
503
|
+
};
|
|
504
|
+
snapshot.messages.push(userMessage, assistantMessage);
|
|
505
|
+
snapshot.runs.push(run);
|
|
506
|
+
await this.writeSnapshot(manifest.id, snapshot);
|
|
507
|
+
await this.appendEvent(manifest.id, {
|
|
508
|
+
type: "message.created",
|
|
509
|
+
message_id: userMessage.id,
|
|
510
|
+
payload: { message: userMessage }
|
|
511
|
+
});
|
|
512
|
+
await this.appendEvent(manifest.id, {
|
|
513
|
+
type: "message.created",
|
|
514
|
+
message_id: assistantMessage.id,
|
|
515
|
+
run_id: run.id,
|
|
516
|
+
payload: { message: assistantMessage }
|
|
517
|
+
});
|
|
518
|
+
const runEvent = await this.appendEvent(manifest.id, {
|
|
519
|
+
type: "run.started",
|
|
520
|
+
message_id: assistantMessage.id,
|
|
521
|
+
run_id: run.id,
|
|
522
|
+
payload: { run }
|
|
523
|
+
});
|
|
524
|
+
void this.startRunWorker(manifest.id, run.id, this.buildHermesInput(content, userAttachmentParts)).catch((error) => {
|
|
525
|
+
void this.failRun(manifest.id, run.id, error instanceof Error ? error.message : String(error));
|
|
526
|
+
});
|
|
527
|
+
return {
|
|
528
|
+
conversation_id: manifest.id,
|
|
529
|
+
user_message: { id: userMessage.id, status: userMessage.status },
|
|
530
|
+
assistant_message: { id: assistantMessage.id, status: assistantMessage.status },
|
|
531
|
+
run: { id: run.id, status: run.status },
|
|
532
|
+
last_event_seq: runEvent.seq
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
async deleteConversation(conversationId) {
|
|
536
|
+
const manifest = await this.readActiveManifest(conversationId);
|
|
537
|
+
const snapshot = await this.readSnapshot(conversationId).catch(() => emptySnapshot());
|
|
538
|
+
const referencedBlobIds = /* @__PURE__ */ new Set([
|
|
539
|
+
...collectBlobIds(snapshot),
|
|
540
|
+
...await this.listConversationBlobIds(conversationId)
|
|
541
|
+
]);
|
|
542
|
+
await deleteHermesSession(manifest.hermes_session_id);
|
|
543
|
+
const deletedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
544
|
+
const next = {
|
|
545
|
+
...manifest,
|
|
546
|
+
status: "deleted_soft",
|
|
547
|
+
updated_at: deletedAt,
|
|
548
|
+
deleted_at: deletedAt
|
|
549
|
+
};
|
|
550
|
+
await this.writeManifest(next);
|
|
551
|
+
const deleteEvent = await this.appendEvent(conversationId, {
|
|
552
|
+
type: "conversation.deleted",
|
|
553
|
+
payload: { deleted_at: deletedAt, hermes_session_id: manifest.hermes_session_id }
|
|
554
|
+
});
|
|
555
|
+
await this.writeSnapshot(conversationId, emptySnapshot());
|
|
556
|
+
await writeFile2(this.eventsPath(conversationId), `${JSON.stringify(deleteEvent)}
|
|
557
|
+
`, { mode: 384 });
|
|
558
|
+
await this.pruneConversationBlobReferences(conversationId, [...referencedBlobIds]);
|
|
559
|
+
return { conversation_id: conversationId, hermes_deleted: true, deleted_at: deletedAt };
|
|
560
|
+
}
|
|
561
|
+
async writeBlob(conversationId, input) {
|
|
562
|
+
await this.readActiveManifest(conversationId);
|
|
563
|
+
if (input.bytes.byteLength > MAX_UPLOADED_BLOB_BYTES) {
|
|
564
|
+
throw new LinkHttpError(413, "blob_too_large", "Blob is too large");
|
|
565
|
+
}
|
|
566
|
+
const id = `blob_${randomUUID2().replaceAll("-", "")}`;
|
|
567
|
+
const filePath = this.blobPath(id);
|
|
568
|
+
await mkdir3(path4.dirname(filePath), { recursive: true, mode: 448 });
|
|
569
|
+
await writeFile2(filePath, input.bytes, { mode: 384 });
|
|
570
|
+
const manifestPath = `${filePath}.json`;
|
|
571
|
+
const blob = {
|
|
572
|
+
id,
|
|
573
|
+
size: input.bytes.byteLength,
|
|
574
|
+
mime: input.mime || "application/octet-stream",
|
|
575
|
+
filename: sanitizeFilename(input.filename, id),
|
|
576
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
577
|
+
conversation_ids: [conversationId]
|
|
578
|
+
};
|
|
579
|
+
await writeJsonFile(manifestPath, blob);
|
|
580
|
+
await this.appendEvent(conversationId, {
|
|
581
|
+
type: "blob.created",
|
|
582
|
+
payload: blob
|
|
583
|
+
});
|
|
584
|
+
return blob;
|
|
585
|
+
}
|
|
586
|
+
async readBlob(conversationId, blobId) {
|
|
587
|
+
await this.readActiveManifest(conversationId);
|
|
588
|
+
const filePath = this.blobPath(blobId);
|
|
589
|
+
const manifest = await this.readBlobManifest(conversationId, blobId);
|
|
590
|
+
const bytes = await readFile3(filePath).catch((error) => {
|
|
591
|
+
if (isNodeError3(error, "ENOENT")) {
|
|
592
|
+
throw new LinkHttpError(404, "blob_not_found", "Blob was not found");
|
|
593
|
+
}
|
|
594
|
+
throw error;
|
|
595
|
+
});
|
|
596
|
+
return {
|
|
597
|
+
bytes,
|
|
598
|
+
mime: manifest.mime || "application/octet-stream",
|
|
599
|
+
filename: manifest.filename || blobId,
|
|
600
|
+
size: manifest.size || bytes.byteLength
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
async resolveMessageAttachmentParts(conversationId, attachments) {
|
|
604
|
+
const parts = [];
|
|
605
|
+
const seen = /* @__PURE__ */ new Set();
|
|
606
|
+
for (const attachment of attachments) {
|
|
607
|
+
const blobId = attachment.blob_id ?? attachment.blobId;
|
|
608
|
+
if (!blobId || seen.has(blobId)) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
seen.add(blobId);
|
|
612
|
+
const manifest = await this.readBlobManifest(conversationId, blobId);
|
|
613
|
+
const mime = manifest.mime || "application/octet-stream";
|
|
614
|
+
parts.push({
|
|
615
|
+
type: mediaKindForMime(mime),
|
|
616
|
+
blob: blobId,
|
|
617
|
+
mime,
|
|
618
|
+
size: manifest.size,
|
|
619
|
+
filename: manifest.filename || blobId,
|
|
620
|
+
url: `/api/v1/conversations/${encodeURIComponent(conversationId)}/blobs/${encodeURIComponent(blobId)}`
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
return parts;
|
|
624
|
+
}
|
|
625
|
+
buildHermesInput(content, parts) {
|
|
626
|
+
const attachmentLines = parts.filter((part) => part.blob).map((part) => {
|
|
627
|
+
const label = part.filename ?? part.blob;
|
|
628
|
+
const mime = part.mime ? `, ${part.mime}` : "";
|
|
629
|
+
const size = part.size ? `, ${part.size} bytes` : "";
|
|
630
|
+
return `- ${label}${mime}${size}: ${this.blobPath(part.blob)}`;
|
|
631
|
+
});
|
|
632
|
+
if (attachmentLines.length === 0) {
|
|
633
|
+
return content;
|
|
634
|
+
}
|
|
635
|
+
const prefix = content ? `${content}
|
|
636
|
+
|
|
637
|
+
` : "";
|
|
638
|
+
return `${prefix}Attachments available on this computer:
|
|
639
|
+
${attachmentLines.join("\n")}`;
|
|
640
|
+
}
|
|
641
|
+
async readBlobManifest(conversationId, blobId) {
|
|
642
|
+
assertValidConversationId(conversationId);
|
|
643
|
+
assertValidBlobId(blobId);
|
|
644
|
+
const manifest = await readJsonFile(
|
|
645
|
+
`${this.blobPath(blobId)}.json`
|
|
646
|
+
);
|
|
647
|
+
if (!manifest?.conversation_ids?.includes(conversationId)) {
|
|
648
|
+
throw new LinkHttpError(404, "blob_not_found", "Blob was not found");
|
|
649
|
+
}
|
|
650
|
+
return manifest;
|
|
651
|
+
}
|
|
652
|
+
async writeBlobFromFile(conversationId, source) {
|
|
653
|
+
const sourcePath = resolveMediaSourcePath(source.path);
|
|
654
|
+
const fileStat = await stat(sourcePath).catch((error) => {
|
|
655
|
+
if (isNodeError3(error, "ENOENT")) {
|
|
656
|
+
throw new LinkHttpError(404, "media_source_not_found", "Hermes output file was not found");
|
|
657
|
+
}
|
|
658
|
+
throw error;
|
|
659
|
+
});
|
|
660
|
+
if (!fileStat.isFile()) {
|
|
661
|
+
throw new LinkHttpError(400, "media_source_not_file", "Hermes output media source is not a file");
|
|
662
|
+
}
|
|
663
|
+
if (fileStat.size > MAX_IMPORTED_BLOB_BYTES) {
|
|
664
|
+
throw new LinkHttpError(413, "media_source_too_large", "Hermes output media source is too large");
|
|
665
|
+
}
|
|
666
|
+
return this.writeBlob(conversationId, {
|
|
667
|
+
bytes: await readFile3(sourcePath),
|
|
668
|
+
filename: path4.basename(sourcePath),
|
|
669
|
+
mime: source.mime ?? inferMimeType(sourcePath)
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
async startRunWorker(conversationId, runId, input) {
|
|
673
|
+
const snapshot = await this.readSnapshot(conversationId);
|
|
674
|
+
const run = snapshot.runs.find((item) => item.id === runId);
|
|
675
|
+
if (!run) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const runResponse = await createHermesRun({
|
|
679
|
+
input,
|
|
680
|
+
session_id: run.hermes_session_id,
|
|
681
|
+
conversation_history: buildConversationHistory(snapshot, run)
|
|
682
|
+
});
|
|
683
|
+
await this.updateRun(conversationId, runId, { hermes_run_id: runResponse.run_id });
|
|
684
|
+
const response = await streamHermesRunEvents(runResponse.run_id);
|
|
685
|
+
for await (const event of parseSseResponse(response)) {
|
|
686
|
+
if (!await this.isConversationActive(conversationId)) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const type = event.payloadType;
|
|
690
|
+
if (type === "run.completed") {
|
|
691
|
+
await this.importMediaReferencesForEvent(conversationId, runId, event);
|
|
692
|
+
await this.completeRun(conversationId, runId, event);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (type === "run.failed") {
|
|
696
|
+
await this.importMediaReferencesForEvent(conversationId, runId, event);
|
|
697
|
+
await this.failRun(conversationId, runId, readErrorMessage(event.payload) ?? "Hermes run failed", event);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
await this.persistHermesEvent(conversationId, runId, event);
|
|
701
|
+
}
|
|
702
|
+
await this.completeRun(conversationId, runId);
|
|
703
|
+
}
|
|
704
|
+
async persistHermesEvent(conversationId, runId, event) {
|
|
705
|
+
if (!await this.isConversationActive(conversationId)) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const type = event.payloadType;
|
|
709
|
+
if (type === "message.delta") {
|
|
710
|
+
const delta = readDelta(event.payload);
|
|
711
|
+
if (delta) {
|
|
712
|
+
await this.appendAssistantDelta(conversationId, runId, delta, event.payload);
|
|
713
|
+
}
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const messageId = await this.assistantMessageIdForRun(conversationId, runId);
|
|
717
|
+
await this.appendEvent(conversationId, {
|
|
718
|
+
type,
|
|
719
|
+
message_id: messageId,
|
|
720
|
+
run_id: runId,
|
|
721
|
+
payload: event.payload,
|
|
722
|
+
raw: { format: "hermes-run-event", payload: event.rawPayload }
|
|
723
|
+
});
|
|
724
|
+
if (messageId) {
|
|
725
|
+
await this.importMediaReferences(conversationId, runId, messageId, collectMediaReferences(event.payload));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async importMediaReferencesForEvent(conversationId, runId, event) {
|
|
729
|
+
const messageId = await this.assistantMessageIdForRun(conversationId, runId);
|
|
730
|
+
if (!messageId) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
await this.importMediaReferences(conversationId, runId, messageId, collectMediaReferences(event.payload));
|
|
734
|
+
}
|
|
735
|
+
async appendAssistantDelta(conversationId, runId, delta, rawPayload) {
|
|
736
|
+
const snapshot = await this.readSnapshot(conversationId);
|
|
737
|
+
const run = snapshot.runs.find((item) => item.id === runId);
|
|
738
|
+
if (!run) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const assistant = snapshot.messages.find((message) => message.id === run.assistant_message_id);
|
|
742
|
+
if (!assistant) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const textPart = assistant.parts.find((part) => part.type === "text");
|
|
746
|
+
if (textPart) {
|
|
747
|
+
textPart.text = `${textPart.text ?? ""}${delta}`;
|
|
748
|
+
} else {
|
|
749
|
+
assistant.parts.push({ type: "text", text: delta });
|
|
750
|
+
}
|
|
751
|
+
assistant.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
752
|
+
assistant.raw = { format: "hermes-run-event", payload: rawPayload };
|
|
753
|
+
await this.writeSnapshot(conversationId, snapshot);
|
|
754
|
+
await this.appendEvent(conversationId, {
|
|
755
|
+
type: "message.delta",
|
|
756
|
+
message_id: assistant.id,
|
|
757
|
+
run_id: runId,
|
|
758
|
+
payload: { delta },
|
|
759
|
+
raw: { format: "hermes-run-event", payload: rawPayload }
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
async completeRun(conversationId, runId, source) {
|
|
763
|
+
let snapshot = await this.readSnapshot(conversationId);
|
|
764
|
+
let run = snapshot.runs.find((item) => item.id === runId);
|
|
765
|
+
if (!run) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const initialRun = run;
|
|
769
|
+
let assistant = snapshot.messages.find((message) => message.id === initialRun.assistant_message_id);
|
|
770
|
+
if (assistant) {
|
|
771
|
+
await this.importMediaReferences(conversationId, runId, assistant.id, collectMediaTags(messageText(assistant)));
|
|
772
|
+
snapshot = await this.readSnapshot(conversationId);
|
|
773
|
+
const refreshedRun = snapshot.runs.find((item) => item.id === runId);
|
|
774
|
+
if (!refreshedRun) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
run = refreshedRun;
|
|
778
|
+
assistant = snapshot.messages.find((message) => message.id === refreshedRun.assistant_message_id);
|
|
779
|
+
}
|
|
780
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
781
|
+
run.status = "completed";
|
|
782
|
+
run.completed_at = completedAt;
|
|
783
|
+
if (assistant) {
|
|
784
|
+
assistant.status = "completed";
|
|
785
|
+
assistant.updated_at = completedAt;
|
|
786
|
+
cleanMessageTextParts(assistant);
|
|
787
|
+
}
|
|
788
|
+
await this.writeSnapshot(conversationId, snapshot);
|
|
789
|
+
if (assistant) {
|
|
790
|
+
await this.appendEvent(conversationId, {
|
|
791
|
+
type: "message.completed",
|
|
792
|
+
message_id: assistant.id,
|
|
793
|
+
run_id: runId,
|
|
794
|
+
payload: { message: assistant }
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
await this.appendEvent(conversationId, {
|
|
798
|
+
type: "run.completed",
|
|
799
|
+
message_id: assistant?.id,
|
|
800
|
+
run_id: runId,
|
|
801
|
+
payload: { run, ...source ? { hermes: source.payload } : {} },
|
|
802
|
+
...source ? { raw: { format: "hermes-run-event", payload: source.rawPayload } } : {}
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
async failRun(conversationId, runId, message, source) {
|
|
806
|
+
const snapshot = await this.readSnapshot(conversationId).catch(() => null);
|
|
807
|
+
if (!snapshot) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const run = snapshot.runs.find((item) => item.id === runId);
|
|
811
|
+
if (!run) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
run.status = "failed";
|
|
815
|
+
run.completed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
816
|
+
run.error_message = message;
|
|
817
|
+
const assistant = snapshot.messages.find((item) => item.id === run.assistant_message_id);
|
|
818
|
+
if (assistant) {
|
|
819
|
+
assistant.status = "failed";
|
|
820
|
+
assistant.updated_at = run.completed_at;
|
|
821
|
+
const textPart = assistant.parts.find((part) => part.type === "text");
|
|
822
|
+
if (textPart && !textPart.text) {
|
|
823
|
+
textPart.text = message;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
await this.writeSnapshot(conversationId, snapshot);
|
|
827
|
+
await this.appendEvent(conversationId, {
|
|
828
|
+
type: "run.failed",
|
|
829
|
+
message_id: assistant?.id,
|
|
830
|
+
run_id: runId,
|
|
831
|
+
payload: { error: { message }, run, ...source ? { hermes: source.payload } : {} },
|
|
832
|
+
...source ? { raw: { format: "hermes-run-event", payload: source.rawPayload } } : {}
|
|
833
|
+
});
|
|
834
|
+
void this.logger.warn("conversation_run_failed", { conversation_id: conversationId, run_id: runId, error: message });
|
|
835
|
+
}
|
|
836
|
+
async updateRun(conversationId, runId, patch) {
|
|
837
|
+
const snapshot = await this.readSnapshot(conversationId);
|
|
838
|
+
const run = snapshot.runs.find((item) => item.id === runId);
|
|
839
|
+
if (!run) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
Object.assign(run, patch);
|
|
843
|
+
await this.writeSnapshot(conversationId, snapshot);
|
|
844
|
+
}
|
|
845
|
+
async assistantMessageIdForRun(conversationId, runId) {
|
|
846
|
+
const snapshot = await this.readSnapshot(conversationId).catch(() => null);
|
|
847
|
+
return snapshot?.runs.find((item) => item.id === runId)?.assistant_message_id;
|
|
848
|
+
}
|
|
849
|
+
async importMediaReferences(conversationId, runId, messageId, references) {
|
|
850
|
+
if (references.length === 0) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const snapshot = await this.readSnapshot(conversationId);
|
|
854
|
+
const assistant = snapshot.messages.find((message) => message.id === messageId);
|
|
855
|
+
if (!assistant) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const importedSourceKeys = readImportedMediaSourceKeys(assistant);
|
|
859
|
+
const importedParts = [];
|
|
860
|
+
for (const reference of references.slice(0, 10)) {
|
|
861
|
+
try {
|
|
862
|
+
const sourceKey = mediaSourceKey(reference.path);
|
|
863
|
+
if (importedSourceKeys.has(sourceKey)) {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
const blob = await this.writeBlobFromFile(conversationId, reference);
|
|
867
|
+
const part = {
|
|
868
|
+
type: reference.kind ?? mediaKindForMime(blob.mime),
|
|
869
|
+
blob: blob.id,
|
|
870
|
+
mime: blob.mime,
|
|
871
|
+
size: blob.size,
|
|
872
|
+
filename: blob.filename,
|
|
873
|
+
url: `/api/v1/conversations/${encodeURIComponent(conversationId)}/blobs/${encodeURIComponent(blob.id)}`
|
|
874
|
+
};
|
|
875
|
+
assistant.parts.push(part);
|
|
876
|
+
assistant.attachments.push({
|
|
877
|
+
blob_id: blob.id,
|
|
878
|
+
mime: blob.mime,
|
|
879
|
+
size: blob.size,
|
|
880
|
+
filename: blob.filename,
|
|
881
|
+
source: "hermes_output"
|
|
882
|
+
});
|
|
883
|
+
importedSourceKeys.add(sourceKey);
|
|
884
|
+
importedParts.push(part);
|
|
885
|
+
} catch (error) {
|
|
886
|
+
void this.logger.warn("conversation_media_import_failed", {
|
|
887
|
+
conversation_id: conversationId,
|
|
888
|
+
run_id: runId,
|
|
889
|
+
message_id: messageId,
|
|
890
|
+
error: error instanceof Error ? error.message : String(error)
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if (importedParts.length === 0) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
assistant.hermes = {
|
|
898
|
+
...toRecord2(assistant.hermes),
|
|
899
|
+
imported_media_source_keys: [...importedSourceKeys]
|
|
900
|
+
};
|
|
901
|
+
assistant.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
902
|
+
await this.writeSnapshot(conversationId, snapshot);
|
|
903
|
+
await this.appendEvent(conversationId, {
|
|
904
|
+
type: "message.parts.created",
|
|
905
|
+
message_id: messageId,
|
|
906
|
+
run_id: runId,
|
|
907
|
+
payload: { parts: importedParts }
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
async appendEvent(conversationId, input) {
|
|
911
|
+
const manifest = await this.readManifest(conversationId);
|
|
912
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
913
|
+
const event = {
|
|
914
|
+
...input,
|
|
915
|
+
seq: manifest.last_event_seq + 1,
|
|
916
|
+
conversation_id: conversationId,
|
|
917
|
+
created_at: now
|
|
918
|
+
};
|
|
919
|
+
await mkdir3(this.conversationDir(conversationId), { recursive: true, mode: 448 });
|
|
920
|
+
await appendFile(this.eventsPath(conversationId), `${JSON.stringify(event)}
|
|
921
|
+
`, { mode: 384 });
|
|
922
|
+
await this.writeManifest({
|
|
923
|
+
...manifest,
|
|
924
|
+
last_event_seq: event.seq,
|
|
925
|
+
updated_at: now
|
|
926
|
+
});
|
|
927
|
+
this.emitter.emit(this.liveEventName(conversationId), event);
|
|
928
|
+
return event;
|
|
929
|
+
}
|
|
930
|
+
async readActiveManifest(conversationId) {
|
|
931
|
+
const manifest = await this.readManifest(conversationId);
|
|
932
|
+
if (manifest.status !== "active") {
|
|
933
|
+
throw new LinkHttpError(404, "conversation_not_found", "Conversation was not found");
|
|
934
|
+
}
|
|
935
|
+
return manifest;
|
|
936
|
+
}
|
|
937
|
+
async isConversationActive(conversationId) {
|
|
938
|
+
const manifest = await this.readManifest(conversationId).catch(() => null);
|
|
939
|
+
return manifest?.status === "active";
|
|
940
|
+
}
|
|
941
|
+
async readManifest(conversationId) {
|
|
942
|
+
const manifest = await readJsonFile(this.manifestPath(conversationId));
|
|
943
|
+
if (!manifest) {
|
|
944
|
+
throw new LinkHttpError(404, "conversation_not_found", "Conversation was not found");
|
|
945
|
+
}
|
|
946
|
+
return manifest;
|
|
947
|
+
}
|
|
948
|
+
writeManifest(manifest) {
|
|
949
|
+
return writeJsonFile(this.manifestPath(manifest.id), manifest);
|
|
950
|
+
}
|
|
951
|
+
async readSnapshot(conversationId) {
|
|
952
|
+
return await readJsonFile(this.snapshotPath(conversationId)) ?? emptySnapshot();
|
|
953
|
+
}
|
|
954
|
+
writeSnapshot(conversationId, snapshot) {
|
|
955
|
+
return writeJsonFile(this.snapshotPath(conversationId), snapshot);
|
|
956
|
+
}
|
|
957
|
+
conversationDir(conversationId) {
|
|
958
|
+
assertValidConversationId(conversationId);
|
|
959
|
+
return path4.join(this.paths.conversationsDir, conversationId);
|
|
960
|
+
}
|
|
961
|
+
manifestPath(conversationId) {
|
|
962
|
+
return path4.join(this.conversationDir(conversationId), "manifest.json");
|
|
963
|
+
}
|
|
964
|
+
snapshotPath(conversationId) {
|
|
965
|
+
return path4.join(this.conversationDir(conversationId), "snapshot.json");
|
|
966
|
+
}
|
|
967
|
+
eventsPath(conversationId) {
|
|
968
|
+
return path4.join(this.conversationDir(conversationId), "events.ndjson");
|
|
969
|
+
}
|
|
970
|
+
blobPath(blobId) {
|
|
971
|
+
assertValidBlobId(blobId);
|
|
972
|
+
return path4.join(this.paths.blobsDir, `${blobId}.bin`);
|
|
973
|
+
}
|
|
974
|
+
liveEventName(conversationId) {
|
|
975
|
+
return `conversation:${conversationId}`;
|
|
976
|
+
}
|
|
977
|
+
async pruneConversationBlobReferences(conversationId, blobIds) {
|
|
978
|
+
for (const blobId of blobIds) {
|
|
979
|
+
try {
|
|
980
|
+
const manifestPath = `${this.blobPath(blobId)}.json`;
|
|
981
|
+
const manifest = await readJsonFile(manifestPath);
|
|
982
|
+
const nextConversationIds = (manifest?.conversation_ids ?? []).filter((id) => id !== conversationId);
|
|
983
|
+
if (nextConversationIds.length > 0) {
|
|
984
|
+
await writeJsonFile(manifestPath, { ...manifest, conversation_ids: nextConversationIds });
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
await rm2(this.blobPath(blobId), { force: true });
|
|
988
|
+
await rm2(manifestPath, { force: true });
|
|
989
|
+
} catch (error) {
|
|
990
|
+
void this.logger.warn("conversation_blob_gc_failed", {
|
|
991
|
+
conversation_id: conversationId,
|
|
992
|
+
blob_id: blobId,
|
|
993
|
+
error: error instanceof Error ? error.message : String(error)
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
async listConversationBlobIds(conversationId) {
|
|
999
|
+
await mkdir3(this.paths.blobsDir, { recursive: true, mode: 448 });
|
|
1000
|
+
const entries = await readdir(this.paths.blobsDir, { withFileTypes: true }).catch((error) => {
|
|
1001
|
+
if (isNodeError3(error, "ENOENT")) {
|
|
1002
|
+
return [];
|
|
1003
|
+
}
|
|
1004
|
+
throw error;
|
|
1005
|
+
});
|
|
1006
|
+
const blobIds = [];
|
|
1007
|
+
for (const entry of entries) {
|
|
1008
|
+
if (!entry.isFile() || !entry.name.endsWith(".bin.json")) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
const blobId = entry.name.slice(0, -".bin.json".length);
|
|
1012
|
+
if (!BLOB_ID_PATTERN.test(blobId)) {
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
const manifest = await readJsonFile(
|
|
1016
|
+
path4.join(this.paths.blobsDir, entry.name)
|
|
1017
|
+
).catch(() => null);
|
|
1018
|
+
if (manifest?.conversation_ids?.includes(conversationId)) {
|
|
1019
|
+
blobIds.push(blobId);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return blobIds;
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
async function* parseSseResponse(response) {
|
|
1026
|
+
if (!response.body) {
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const decoder = new TextDecoder();
|
|
1030
|
+
let buffer = "";
|
|
1031
|
+
for await (const chunk of response.body) {
|
|
1032
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
1033
|
+
let separatorIndex = buffer.indexOf("\n\n");
|
|
1034
|
+
while (separatorIndex >= 0) {
|
|
1035
|
+
const block = buffer.slice(0, separatorIndex);
|
|
1036
|
+
buffer = buffer.slice(separatorIndex + 2);
|
|
1037
|
+
const parsed = parseSseBlock(block);
|
|
1038
|
+
if (parsed) {
|
|
1039
|
+
yield parsed;
|
|
1040
|
+
}
|
|
1041
|
+
separatorIndex = buffer.indexOf("\n\n");
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
const trailing = parseSseBlock(buffer);
|
|
1045
|
+
if (trailing) {
|
|
1046
|
+
yield trailing;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function parseSseBlock(block) {
|
|
1050
|
+
const lines = block.split("\n");
|
|
1051
|
+
let eventName = "";
|
|
1052
|
+
const data = [];
|
|
1053
|
+
for (const rawLine of lines) {
|
|
1054
|
+
const line = rawLine.trimEnd();
|
|
1055
|
+
if (line.startsWith("event:")) {
|
|
1056
|
+
eventName = line.slice(6).trim();
|
|
1057
|
+
} else if (line.startsWith("data:")) {
|
|
1058
|
+
data.push(line.slice(5).trimStart());
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (!eventName && data.length === 0) {
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
1064
|
+
const raw = data.join("\n");
|
|
1065
|
+
const decoded = decodeJson(raw);
|
|
1066
|
+
const payload = toRecord2(decoded);
|
|
1067
|
+
const payloadType = (readString2(payload, "type") ?? readString2(payload, "event") ?? eventName) || "message";
|
|
1068
|
+
return { eventName, payloadType, payload, rawPayload: decoded ?? raw };
|
|
1069
|
+
}
|
|
1070
|
+
function decodeJson(value) {
|
|
1071
|
+
if (!value.trim()) {
|
|
1072
|
+
return {};
|
|
1073
|
+
}
|
|
1074
|
+
try {
|
|
1075
|
+
return JSON.parse(value);
|
|
1076
|
+
} catch {
|
|
1077
|
+
return { type: "message.delta", delta: value };
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function emptySnapshot() {
|
|
1081
|
+
return { schema_version: 1, messages: [], runs: [] };
|
|
1082
|
+
}
|
|
1083
|
+
function toSummary(manifest, snapshot) {
|
|
1084
|
+
const lastMessage = [...snapshot.messages].reverse().find((message) => message.parts.some((part) => part.type === "text" && part.text));
|
|
1085
|
+
return {
|
|
1086
|
+
id: manifest.id,
|
|
1087
|
+
title: manifest.title,
|
|
1088
|
+
created_at: manifest.created_at,
|
|
1089
|
+
updated_at: manifest.updated_at,
|
|
1090
|
+
last_event_seq: manifest.last_event_seq,
|
|
1091
|
+
last_message: lastMessage ? {
|
|
1092
|
+
id: lastMessage.id,
|
|
1093
|
+
role: lastMessage.role,
|
|
1094
|
+
content_preview: previewText(lastMessage)
|
|
1095
|
+
} : null
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function previewText(message) {
|
|
1099
|
+
return messageText(message).slice(0, 160);
|
|
1100
|
+
}
|
|
1101
|
+
function messageText(message) {
|
|
1102
|
+
return message.parts.filter((part) => part.type === "text" && part.text).map((part) => part.text).join("").trim();
|
|
1103
|
+
}
|
|
1104
|
+
function buildConversationHistory(snapshot, run) {
|
|
1105
|
+
const triggerIndex = snapshot.messages.findIndex((message) => message.id === run.trigger_message_id);
|
|
1106
|
+
const previousMessages = triggerIndex >= 0 ? snapshot.messages.slice(0, triggerIndex) : snapshot.messages;
|
|
1107
|
+
return previousMessages.filter((message) => (message.role === "user" || message.role === "assistant") && message.status === "completed").map((message) => ({
|
|
1108
|
+
role: message.role,
|
|
1109
|
+
content: messageText(message)
|
|
1110
|
+
})).filter((message) => message.content);
|
|
1111
|
+
}
|
|
1112
|
+
function readDelta(payload) {
|
|
1113
|
+
return readString2(payload, "delta") ?? readString2(payload, "text") ?? readString2(payload, "content");
|
|
1114
|
+
}
|
|
1115
|
+
function readErrorMessage(payload) {
|
|
1116
|
+
const error = toRecord2(payload.error);
|
|
1117
|
+
return readString2(error, "message") ?? readString2(payload, "message");
|
|
1118
|
+
}
|
|
1119
|
+
function collectMediaReferences(payload) {
|
|
1120
|
+
const references = [];
|
|
1121
|
+
collectStructuredMediaReferences(payload, references);
|
|
1122
|
+
for (const key of ["output", "content", "message", "text"]) {
|
|
1123
|
+
const value = payload[key];
|
|
1124
|
+
if (typeof value === "string") {
|
|
1125
|
+
references.push(...collectMediaTags(value));
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const unique2 = /* @__PURE__ */ new Set();
|
|
1129
|
+
return references.filter((reference) => {
|
|
1130
|
+
const key = `${reference.path}|${reference.mime ?? ""}|${reference.kind ?? ""}`;
|
|
1131
|
+
if (unique2.has(key)) {
|
|
1132
|
+
return false;
|
|
1133
|
+
}
|
|
1134
|
+
unique2.add(key);
|
|
1135
|
+
return true;
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
function collectStructuredMediaReferences(value, references, depth = 0) {
|
|
1139
|
+
if (depth > 4 || value === null || typeof value !== "object") {
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (Array.isArray(value)) {
|
|
1143
|
+
for (const item of value) {
|
|
1144
|
+
collectStructuredMediaReferences(item, references, depth + 1);
|
|
1145
|
+
}
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const record = value;
|
|
1149
|
+
for (const [key, item] of Object.entries(record)) {
|
|
1150
|
+
if (typeof item === "string" && isExplicitMediaPathKey(key)) {
|
|
1151
|
+
references.push({
|
|
1152
|
+
path: item,
|
|
1153
|
+
kind: mediaKindForMime(readString2(record, "mime") ?? readString2(record, "mime_type") ?? inferMimeType(item)),
|
|
1154
|
+
mime: readString2(record, "mime") ?? readString2(record, "mime_type") ?? void 0
|
|
1155
|
+
});
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (Array.isArray(item) || item !== null && typeof item === "object") {
|
|
1159
|
+
collectStructuredMediaReferences(item, references, depth + 1);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function collectMediaTags(text) {
|
|
1164
|
+
const references = [];
|
|
1165
|
+
for (const match of text.matchAll(MEDIA_TAG_PATTERN)) {
|
|
1166
|
+
const rawPath = match[1]?.trim();
|
|
1167
|
+
const mediaPath = rawPath?.replace(/^["'`]|["'`]$/gu, "").replace(/["'`,.;:)}\]]+$/gu, "");
|
|
1168
|
+
if (mediaPath) {
|
|
1169
|
+
references.push({
|
|
1170
|
+
path: mediaPath,
|
|
1171
|
+
kind: mediaKindForMime(inferMimeType(mediaPath))
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return references;
|
|
1176
|
+
}
|
|
1177
|
+
function cleanMessageTextParts(message) {
|
|
1178
|
+
for (const part of message.parts) {
|
|
1179
|
+
if (part.type === "text" && part.text) {
|
|
1180
|
+
part.text = cleanHermesDisplayText(part.text);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function cleanHermesDisplayText(text) {
|
|
1185
|
+
if (!text.includes("MEDIA:") && !text.includes("[[audio_as_voice]]")) {
|
|
1186
|
+
return text;
|
|
1187
|
+
}
|
|
1188
|
+
return text.replaceAll("[[audio_as_voice]]", "").replace(MEDIA_TAG_PATTERN, "").replace(/\n{3,}/gu, "\n\n").trimEnd();
|
|
1189
|
+
}
|
|
1190
|
+
function isExplicitMediaPathKey(key) {
|
|
1191
|
+
return [
|
|
1192
|
+
"file_path",
|
|
1193
|
+
"screenshot_path",
|
|
1194
|
+
"image_path",
|
|
1195
|
+
"audio_path",
|
|
1196
|
+
"video_path",
|
|
1197
|
+
"media_path",
|
|
1198
|
+
"artifact_path"
|
|
1199
|
+
].includes(key);
|
|
1200
|
+
}
|
|
1201
|
+
function inferMimeType(filePath) {
|
|
1202
|
+
const extension = path4.extname(filePath).toLowerCase();
|
|
1203
|
+
return {
|
|
1204
|
+
".png": "image/png",
|
|
1205
|
+
".jpg": "image/jpeg",
|
|
1206
|
+
".jpeg": "image/jpeg",
|
|
1207
|
+
".gif": "image/gif",
|
|
1208
|
+
".webp": "image/webp",
|
|
1209
|
+
".heic": "image/heic",
|
|
1210
|
+
".pdf": "application/pdf",
|
|
1211
|
+
".txt": "text/plain",
|
|
1212
|
+
".md": "text/markdown",
|
|
1213
|
+
".json": "application/json",
|
|
1214
|
+
".csv": "text/csv",
|
|
1215
|
+
".mp3": "audio/mpeg",
|
|
1216
|
+
".wav": "audio/wav",
|
|
1217
|
+
".m4a": "audio/mp4",
|
|
1218
|
+
".ogg": "audio/ogg",
|
|
1219
|
+
".opus": "audio/ogg",
|
|
1220
|
+
".mp4": "video/mp4",
|
|
1221
|
+
".mov": "video/quicktime",
|
|
1222
|
+
".webm": "video/webm"
|
|
1223
|
+
}[extension] ?? "application/octet-stream";
|
|
1224
|
+
}
|
|
1225
|
+
function mediaKindForMime(mime) {
|
|
1226
|
+
if (mime.startsWith("image/")) {
|
|
1227
|
+
return "image";
|
|
1228
|
+
}
|
|
1229
|
+
if (mime.startsWith("audio/")) {
|
|
1230
|
+
return "audio";
|
|
1231
|
+
}
|
|
1232
|
+
return "file";
|
|
1233
|
+
}
|
|
1234
|
+
function collectBlobIds(snapshot) {
|
|
1235
|
+
const blobIds = /* @__PURE__ */ new Set();
|
|
1236
|
+
for (const message of snapshot.messages) {
|
|
1237
|
+
for (const part of message.parts) {
|
|
1238
|
+
if (part.blob) {
|
|
1239
|
+
blobIds.add(part.blob);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
for (const attachment of message.attachments) {
|
|
1243
|
+
const record = toRecord2(attachment);
|
|
1244
|
+
const blobId = readString2(record, "blob_id") ?? readString2(record, "blobId");
|
|
1245
|
+
if (blobId) {
|
|
1246
|
+
blobIds.add(blobId);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return [...blobIds];
|
|
1251
|
+
}
|
|
1252
|
+
function readImportedMediaSourceKeys(message) {
|
|
1253
|
+
const hermes = toRecord2(message.hermes);
|
|
1254
|
+
const keys = hermes.imported_media_source_keys;
|
|
1255
|
+
if (!Array.isArray(keys)) {
|
|
1256
|
+
return /* @__PURE__ */ new Set();
|
|
1257
|
+
}
|
|
1258
|
+
return new Set(keys.filter((key) => typeof key === "string" && key.length > 0));
|
|
1259
|
+
}
|
|
1260
|
+
function mediaSourceKey(sourcePath) {
|
|
1261
|
+
return createHash("sha256").update(resolveMediaSourcePath(sourcePath)).digest("hex").slice(0, 32);
|
|
1262
|
+
}
|
|
1263
|
+
function sanitizeFilename(value, fallback) {
|
|
1264
|
+
const base = path4.basename((value ?? "").replace(/[\r\n\t]/gu, " ").trim());
|
|
1265
|
+
const safe = base.replace(/[/:\\]/gu, "_").slice(0, 200).trim();
|
|
1266
|
+
return safe || fallback;
|
|
1267
|
+
}
|
|
1268
|
+
function resolveMediaSourcePath(sourcePath) {
|
|
1269
|
+
const trimmed = sourcePath.trim();
|
|
1270
|
+
const expanded = trimmed.startsWith("~/") ? path4.join(process.env.HOME ?? "", trimmed.slice(2)) : trimmed;
|
|
1271
|
+
const resolved = path4.resolve(expanded);
|
|
1272
|
+
if (!path4.isAbsolute(expanded)) {
|
|
1273
|
+
throw new LinkHttpError(400, "media_source_path_not_absolute", "Hermes output media source must be an absolute path");
|
|
1274
|
+
}
|
|
1275
|
+
return resolved;
|
|
1276
|
+
}
|
|
1277
|
+
function assertValidConversationId(conversationId) {
|
|
1278
|
+
if (!CONVERSATION_ID_PATTERN.test(conversationId)) {
|
|
1279
|
+
throw new LinkHttpError(404, "conversation_not_found", "Conversation was not found");
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
function assertValidBlobId(blobId) {
|
|
1283
|
+
if (!BLOB_ID_PATTERN.test(blobId)) {
|
|
1284
|
+
throw new LinkHttpError(404, "blob_not_found", "Blob was not found");
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
function readString2(payload, key) {
|
|
1288
|
+
const value = payload[key];
|
|
1289
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1290
|
+
}
|
|
1291
|
+
function toRecord2(value) {
|
|
1292
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
1293
|
+
}
|
|
1294
|
+
function isNodeError3(error, code) {
|
|
1295
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
317
1298
|
// src/hermes/profiles.ts
|
|
318
|
-
import { mkdir as
|
|
1299
|
+
import { mkdir as mkdir4, readdir as readdir2, rename as rename2, rm as rm3, stat as stat2 } from "fs/promises";
|
|
319
1300
|
import os3 from "os";
|
|
320
|
-
import
|
|
1301
|
+
import path5 from "path";
|
|
321
1302
|
var DEFAULT_PROFILE = "default";
|
|
322
1303
|
var PROFILE_NAME_PATTERN = /^[a-zA-Z0-9._-]{1,64}$/;
|
|
323
1304
|
async function listHermesProfiles(paths = resolveRuntimePaths()) {
|
|
324
1305
|
const activeProfile = await getActiveProfile(paths);
|
|
325
1306
|
const profiles = /* @__PURE__ */ new Map();
|
|
326
1307
|
profiles.set(DEFAULT_PROFILE, profileInfo(DEFAULT_PROFILE, activeProfile));
|
|
327
|
-
const profilesDir =
|
|
328
|
-
const entries = await
|
|
329
|
-
if (
|
|
1308
|
+
const profilesDir = path5.join(os3.homedir(), ".hermes", "profiles");
|
|
1309
|
+
const entries = await readdir2(profilesDir, { withFileTypes: true }).catch((error) => {
|
|
1310
|
+
if (isNodeError4(error, "ENOENT")) {
|
|
330
1311
|
return [];
|
|
331
1312
|
}
|
|
332
1313
|
throw error;
|
|
@@ -349,8 +1330,8 @@ async function listHermesProfiles(paths = resolveRuntimePaths()) {
|
|
|
349
1330
|
async function getHermesProfileStatus(name, paths = resolveRuntimePaths()) {
|
|
350
1331
|
assertProfileName(name);
|
|
351
1332
|
const profile = profileInfo(name, await getActiveProfile(paths));
|
|
352
|
-
const exists = await
|
|
353
|
-
if (
|
|
1333
|
+
const exists = await stat2(profile.path).then((value) => value.isDirectory()).catch((error) => {
|
|
1334
|
+
if (isNodeError4(error, "ENOENT")) {
|
|
354
1335
|
return false;
|
|
355
1336
|
}
|
|
356
1337
|
throw error;
|
|
@@ -371,14 +1352,14 @@ async function createHermesProfile(name) {
|
|
|
371
1352
|
if (await pathExists(profile.path)) {
|
|
372
1353
|
throw new Error("profile already exists");
|
|
373
1354
|
}
|
|
374
|
-
await
|
|
1355
|
+
await mkdir4(profile.path, { recursive: true, mode: 448 });
|
|
375
1356
|
await ensureHermesApiServerKey(name, profile.configPath);
|
|
376
1357
|
return profile;
|
|
377
1358
|
}
|
|
378
1359
|
async function useHermesProfile(name, paths = resolveRuntimePaths()) {
|
|
379
1360
|
assertProfileName(name);
|
|
380
1361
|
const profile = profileInfo(name, name);
|
|
381
|
-
await
|
|
1362
|
+
await mkdir4(profile.path, { recursive: true, mode: 448 });
|
|
382
1363
|
const current = await readJsonFile(paths.stateFile) ?? {};
|
|
383
1364
|
await writeJsonFile(paths.stateFile, { ...current, activeProfile: name });
|
|
384
1365
|
return profile;
|
|
@@ -393,7 +1374,7 @@ async function renameHermesProfile(oldName, newName) {
|
|
|
393
1374
|
}
|
|
394
1375
|
async function deleteHermesProfile(name) {
|
|
395
1376
|
assertMutableProfile(name);
|
|
396
|
-
await
|
|
1377
|
+
await rm3(resolveHermesProfileDir(name), { recursive: true, force: true });
|
|
397
1378
|
}
|
|
398
1379
|
async function getActiveProfile(paths) {
|
|
399
1380
|
const state = await readJsonFile(paths.stateFile);
|
|
@@ -419,20 +1400,20 @@ function assertProfileName(name) {
|
|
|
419
1400
|
}
|
|
420
1401
|
}
|
|
421
1402
|
async function pathExists(targetPath) {
|
|
422
|
-
return await
|
|
423
|
-
if (
|
|
1403
|
+
return await stat2(targetPath).then(() => true).catch((error) => {
|
|
1404
|
+
if (isNodeError4(error, "ENOENT")) {
|
|
424
1405
|
return false;
|
|
425
1406
|
}
|
|
426
1407
|
throw error;
|
|
427
1408
|
});
|
|
428
1409
|
}
|
|
429
|
-
function
|
|
1410
|
+
function isNodeError4(error, code) {
|
|
430
1411
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
431
1412
|
}
|
|
432
1413
|
|
|
433
1414
|
// src/identity/identity.ts
|
|
434
|
-
import { generateKeyPairSync, randomUUID as
|
|
435
|
-
import { mkdir as
|
|
1415
|
+
import { generateKeyPairSync, randomUUID as randomUUID3, sign } from "crypto";
|
|
1416
|
+
import { mkdir as mkdir5, chmod } from "fs/promises";
|
|
436
1417
|
import { z } from "zod";
|
|
437
1418
|
var linkIdentitySchema = z.object({
|
|
438
1419
|
install_id: z.string().min(1),
|
|
@@ -454,12 +1435,12 @@ async function ensureIdentity(paths = resolveRuntimePaths()) {
|
|
|
454
1435
|
if (existing) {
|
|
455
1436
|
return existing;
|
|
456
1437
|
}
|
|
457
|
-
await
|
|
1438
|
+
await mkdir5(paths.homeDir, { recursive: true, mode: 448 });
|
|
458
1439
|
await chmod(paths.homeDir, 448).catch(() => void 0);
|
|
459
1440
|
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
460
1441
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
461
1442
|
const identity = {
|
|
462
|
-
install_id: `install_${
|
|
1443
|
+
install_id: `install_${randomUUID3().replaceAll("-", "")}`,
|
|
463
1444
|
link_id: null,
|
|
464
1445
|
public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
465
1446
|
private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
@@ -492,8 +1473,12 @@ function getIdentityStatus(identity) {
|
|
|
492
1473
|
};
|
|
493
1474
|
}
|
|
494
1475
|
|
|
1476
|
+
// src/pairing/pairing.ts
|
|
1477
|
+
import path6 from "path";
|
|
1478
|
+
import { rm as rm4 } from "fs/promises";
|
|
1479
|
+
|
|
495
1480
|
// src/security/devices.ts
|
|
496
|
-
import { randomBytes as randomBytes2, randomUUID as
|
|
1481
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID4, timingSafeEqual, createHash as createHash2 } from "crypto";
|
|
497
1482
|
var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
|
|
498
1483
|
var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
499
1484
|
async function createDeviceSession(input, paths = resolveRuntimePaths()) {
|
|
@@ -502,7 +1487,7 @@ async function createDeviceSession(input, paths = resolveRuntimePaths()) {
|
|
|
502
1487
|
const accessToken = randomToken("hpat_");
|
|
503
1488
|
const refreshToken = randomToken("hprt_");
|
|
504
1489
|
const device = {
|
|
505
|
-
id: `dev_${
|
|
1490
|
+
id: `dev_${randomUUID4().replaceAll("-", "")}`,
|
|
506
1491
|
label: input.label,
|
|
507
1492
|
platform: input.platform,
|
|
508
1493
|
scope: "admin",
|
|
@@ -591,7 +1576,7 @@ function randomToken(prefix) {
|
|
|
591
1576
|
return `${prefix}${randomBytes2(24).toString("base64url")}`;
|
|
592
1577
|
}
|
|
593
1578
|
function sha256(value) {
|
|
594
|
-
return
|
|
1579
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
595
1580
|
}
|
|
596
1581
|
function safeEqual(left, right) {
|
|
597
1582
|
const leftBytes = Buffer.from(left);
|
|
@@ -650,7 +1635,7 @@ async function postJson(fetcher, url, token, body) {
|
|
|
650
1635
|
});
|
|
651
1636
|
const payload = await response.json().catch(() => null);
|
|
652
1637
|
if (!response.ok) {
|
|
653
|
-
const message =
|
|
1638
|
+
const message = readErrorMessage2(payload) ?? `Relay request failed with HTTP ${response.status}`;
|
|
654
1639
|
throw new Error(message);
|
|
655
1640
|
}
|
|
656
1641
|
if (!payload) {
|
|
@@ -658,7 +1643,7 @@ async function postJson(fetcher, url, token, body) {
|
|
|
658
1643
|
}
|
|
659
1644
|
return payload;
|
|
660
1645
|
}
|
|
661
|
-
function
|
|
1646
|
+
function readErrorMessage2(payload) {
|
|
662
1647
|
if (typeof payload !== "object" || payload === null) {
|
|
663
1648
|
return null;
|
|
664
1649
|
}
|
|
@@ -887,6 +1872,33 @@ async function preparePairing(paths = resolveRuntimePaths()) {
|
|
|
887
1872
|
qrPayload
|
|
888
1873
|
};
|
|
889
1874
|
}
|
|
1875
|
+
async function recordPairingClaim(input, paths = resolveRuntimePaths()) {
|
|
1876
|
+
const record = {
|
|
1877
|
+
session_id: input.sessionId,
|
|
1878
|
+
device_id: input.deviceId,
|
|
1879
|
+
device_label: input.deviceLabel,
|
|
1880
|
+
device_platform: input.devicePlatform,
|
|
1881
|
+
claimed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1882
|
+
};
|
|
1883
|
+
await writeJsonFile(pairingClaimPath(input.sessionId, paths), record);
|
|
1884
|
+
return record;
|
|
1885
|
+
}
|
|
1886
|
+
async function readPairingClaim(sessionId, paths = resolveRuntimePaths()) {
|
|
1887
|
+
const record = await readJsonFile(pairingClaimPath(sessionId, paths));
|
|
1888
|
+
if (!record || record.session_id !== sessionId || typeof record.device_id !== "string") {
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
return {
|
|
1892
|
+
session_id: record.session_id,
|
|
1893
|
+
device_id: record.device_id,
|
|
1894
|
+
device_label: typeof record.device_label === "string" ? record.device_label : "",
|
|
1895
|
+
device_platform: typeof record.device_platform === "string" ? record.device_platform : "unknown",
|
|
1896
|
+
claimed_at: typeof record.claimed_at === "string" ? record.claimed_at : ""
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
async function clearPairingClaim(sessionId, paths = resolveRuntimePaths()) {
|
|
1900
|
+
await rm4(pairingClaimPath(sessionId, paths), { force: true }).catch(() => void 0);
|
|
1901
|
+
}
|
|
890
1902
|
async function claimPairing(input) {
|
|
891
1903
|
const paths = input.paths ?? resolveRuntimePaths();
|
|
892
1904
|
const [identity, config] = await Promise.all([loadRequiredIdentity(paths), loadConfig(paths)]);
|
|
@@ -930,8 +1942,8 @@ async function loadRequiredIdentity(paths) {
|
|
|
930
1942
|
}
|
|
931
1943
|
return identity;
|
|
932
1944
|
}
|
|
933
|
-
async function postServerJson(serverBaseUrl,
|
|
934
|
-
const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${
|
|
1945
|
+
async function postServerJson(serverBaseUrl, path8, body) {
|
|
1946
|
+
const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path8}`, {
|
|
935
1947
|
method: "POST",
|
|
936
1948
|
headers: {
|
|
937
1949
|
accept: "application/json",
|
|
@@ -941,8 +1953,8 @@ async function postServerJson(serverBaseUrl, path6, body) {
|
|
|
941
1953
|
});
|
|
942
1954
|
return readJsonResponse2(response);
|
|
943
1955
|
}
|
|
944
|
-
async function patchServerJson(serverBaseUrl,
|
|
945
|
-
const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${
|
|
1956
|
+
async function patchServerJson(serverBaseUrl, path8, token, body) {
|
|
1957
|
+
const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path8}`, {
|
|
946
1958
|
method: "PATCH",
|
|
947
1959
|
headers: {
|
|
948
1960
|
accept: "application/json",
|
|
@@ -956,12 +1968,12 @@ async function patchServerJson(serverBaseUrl, path6, token, body) {
|
|
|
956
1968
|
async function readJsonResponse2(response) {
|
|
957
1969
|
const payload = await response.json().catch(() => null);
|
|
958
1970
|
if (!response.ok || !payload) {
|
|
959
|
-
const message =
|
|
1971
|
+
const message = readErrorMessage3(payload) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
|
|
960
1972
|
throw new LinkHttpError(response.status, "server_request_failed", message);
|
|
961
1973
|
}
|
|
962
1974
|
return payload;
|
|
963
1975
|
}
|
|
964
|
-
function
|
|
1976
|
+
function readErrorMessage3(payload) {
|
|
965
1977
|
if (typeof payload !== "object" || payload === null) {
|
|
966
1978
|
return null;
|
|
967
1979
|
}
|
|
@@ -975,6 +1987,9 @@ function readErrorMessage2(payload) {
|
|
|
975
1987
|
function defaultDisplayName() {
|
|
976
1988
|
return `Hermes Link ${process.platform}`;
|
|
977
1989
|
}
|
|
1990
|
+
function pairingClaimPath(sessionId, paths) {
|
|
1991
|
+
return path6.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
|
|
1992
|
+
}
|
|
978
1993
|
function qrPreferredUrls(routes) {
|
|
979
1994
|
return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
|
|
980
1995
|
}
|
|
@@ -997,8 +2012,8 @@ async function verifyAppConnectToken(token, options) {
|
|
|
997
2012
|
throw new LinkHttpError(401, "app_connect_token_invalid", "App connect token is malformed");
|
|
998
2013
|
}
|
|
999
2014
|
const [encodedHeader, encodedPayload, encodedSignature] = segments;
|
|
1000
|
-
const header =
|
|
1001
|
-
const payload =
|
|
2015
|
+
const header = decodeJson2(encodedHeader);
|
|
2016
|
+
const payload = decodeJson2(encodedPayload);
|
|
1002
2017
|
if (header.alg !== "ES256" || header.typ !== "JWT") {
|
|
1003
2018
|
throw new LinkHttpError(401, "app_connect_token_invalid", "App connect token algorithm is unsupported");
|
|
1004
2019
|
}
|
|
@@ -1040,7 +2055,7 @@ async function getJwks(config, fetcher) {
|
|
|
1040
2055
|
};
|
|
1041
2056
|
return keys;
|
|
1042
2057
|
}
|
|
1043
|
-
function
|
|
2058
|
+
function decodeJson2(value) {
|
|
1044
2059
|
return JSON.parse(Buffer.from(base64UrlToBase64(value), "base64").toString("utf8"));
|
|
1045
2060
|
}
|
|
1046
2061
|
function base64UrlToBase64(value) {
|
|
@@ -1049,8 +2064,8 @@ function base64UrlToBase64(value) {
|
|
|
1049
2064
|
}
|
|
1050
2065
|
|
|
1051
2066
|
// src/runtime/logger.ts
|
|
1052
|
-
import { appendFile, mkdir as
|
|
1053
|
-
import
|
|
2067
|
+
import { appendFile as appendFile2, mkdir as mkdir6, open as open2, readFile as readFile4, rename as rename3, rm as rm5, stat as stat3 } from "fs/promises";
|
|
2068
|
+
import path7 from "path";
|
|
1054
2069
|
var DEFAULT_LOG_FILE = "hermeslink.log";
|
|
1055
2070
|
var DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
|
|
1056
2071
|
var DEFAULT_MAX_FILES = 5;
|
|
@@ -1098,22 +2113,22 @@ var FileLogger = class {
|
|
|
1098
2113
|
return this.queue;
|
|
1099
2114
|
}
|
|
1100
2115
|
async appendEntry(entry) {
|
|
1101
|
-
await
|
|
2116
|
+
await mkdir6(this.paths.logsDir, { recursive: true, mode: 448 });
|
|
1102
2117
|
const line = `${JSON.stringify(entry)}
|
|
1103
2118
|
`;
|
|
1104
2119
|
await this.rotateIfNeeded(Buffer.byteLength(line, "utf8"));
|
|
1105
|
-
await
|
|
2120
|
+
await appendFile2(this.filePath, line, { mode: 384 });
|
|
1106
2121
|
}
|
|
1107
2122
|
async rotateIfNeeded(nextBytes) {
|
|
1108
|
-
const current = await
|
|
2123
|
+
const current = await stat3(this.filePath).catch(() => null);
|
|
1109
2124
|
if (!current || current.size === 0 || current.size + nextBytes <= this.maxFileBytes) {
|
|
1110
2125
|
return;
|
|
1111
2126
|
}
|
|
1112
2127
|
if (this.maxFiles === 0) {
|
|
1113
|
-
await
|
|
2128
|
+
await rm5(this.filePath, { force: true }).catch(() => void 0);
|
|
1114
2129
|
return;
|
|
1115
2130
|
}
|
|
1116
|
-
await
|
|
2131
|
+
await rm5(rotatedLogFile(this.filePath, this.maxFiles), { force: true }).catch(() => void 0);
|
|
1117
2132
|
for (let index = this.maxFiles - 1; index >= 1; index -= 1) {
|
|
1118
2133
|
await moveIfExists(rotatedLogFile(this.filePath, index), rotatedLogFile(this.filePath, index + 1));
|
|
1119
2134
|
}
|
|
@@ -1124,7 +2139,7 @@ function createFileLogger(options = {}) {
|
|
|
1124
2139
|
return new FileLogger(options);
|
|
1125
2140
|
}
|
|
1126
2141
|
function getLinkLogFile(paths = resolveRuntimePaths(), fileName = DEFAULT_LOG_FILE) {
|
|
1127
|
-
return
|
|
2142
|
+
return path7.join(paths.logsDir, fileName);
|
|
1128
2143
|
}
|
|
1129
2144
|
async function readRecentLogEntries(options = {}) {
|
|
1130
2145
|
const paths = options.paths ?? resolveRuntimePaths();
|
|
@@ -1219,12 +2234,12 @@ function isLogLevel(value) {
|
|
|
1219
2234
|
return value === "debug" || value === "info" || value === "warn" || value === "error";
|
|
1220
2235
|
}
|
|
1221
2236
|
async function readTail(filePath, maxBytes) {
|
|
1222
|
-
const info = await
|
|
2237
|
+
const info = await stat3(filePath).catch(() => null);
|
|
1223
2238
|
if (!info || info.size <= 0) {
|
|
1224
2239
|
return null;
|
|
1225
2240
|
}
|
|
1226
2241
|
if (info.size <= maxBytes) {
|
|
1227
|
-
return await
|
|
2242
|
+
return await readFile4(filePath, "utf8").catch(() => null);
|
|
1228
2243
|
}
|
|
1229
2244
|
const handle = await open2(filePath, "r").catch(() => null);
|
|
1230
2245
|
if (!handle) {
|
|
@@ -1240,7 +2255,7 @@ async function readTail(filePath, maxBytes) {
|
|
|
1240
2255
|
}
|
|
1241
2256
|
}
|
|
1242
2257
|
async function moveIfExists(from, to) {
|
|
1243
|
-
await
|
|
2258
|
+
await rm5(to, { force: true }).catch(() => void 0);
|
|
1244
2259
|
await rename3(from, to).catch((error) => {
|
|
1245
2260
|
if (error.code !== "ENOENT") {
|
|
1246
2261
|
throw error;
|
|
@@ -1252,9 +2267,12 @@ function rotatedLogFile(filePath, index) {
|
|
|
1252
2267
|
}
|
|
1253
2268
|
|
|
1254
2269
|
// src/http/app.ts
|
|
2270
|
+
var MAX_JSON_BODY_BYTES = 1024 * 1024;
|
|
2271
|
+
var MAX_BLOB_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
1255
2272
|
async function createApp(options = {}) {
|
|
1256
2273
|
const paths = options.paths ?? resolveRuntimePaths();
|
|
1257
2274
|
const logger = options.logger ?? createFileLogger({ paths });
|
|
2275
|
+
const conversations = new ConversationService(paths, logger);
|
|
1258
2276
|
const app = new Koa();
|
|
1259
2277
|
const router = new Router();
|
|
1260
2278
|
app.use(async (ctx, next) => {
|
|
@@ -1312,22 +2330,25 @@ async function createApp(options = {}) {
|
|
|
1312
2330
|
sse: true,
|
|
1313
2331
|
relay: true,
|
|
1314
2332
|
profiles: true,
|
|
1315
|
-
logs: true
|
|
2333
|
+
logs: true,
|
|
2334
|
+
conversations: true,
|
|
2335
|
+
conversation_delete: true,
|
|
2336
|
+
blobs: true
|
|
1316
2337
|
}
|
|
1317
2338
|
};
|
|
1318
2339
|
});
|
|
1319
2340
|
router.post("/api/v1/pairing/claim", async (ctx) => {
|
|
1320
2341
|
const body = await readJsonBody(ctx.req);
|
|
1321
|
-
const sessionId =
|
|
1322
|
-
const claimToken =
|
|
2342
|
+
const sessionId = readString3(body, "session_id") ?? readString3(body, "sessionId");
|
|
2343
|
+
const claimToken = readString3(body, "claim_token") ?? readString3(body, "claimToken");
|
|
1323
2344
|
if (!sessionId || !claimToken) {
|
|
1324
2345
|
throw new LinkHttpError(400, "pairing_claim_invalid", "session_id and claim_token are required");
|
|
1325
2346
|
}
|
|
1326
2347
|
const claimed = await claimPairing({
|
|
1327
2348
|
sessionId,
|
|
1328
2349
|
claimToken,
|
|
1329
|
-
deviceLabel:
|
|
1330
|
-
devicePlatform:
|
|
2350
|
+
deviceLabel: readString3(body, "device_label") ?? readString3(body, "deviceLabel") ?? "HermesPilot App",
|
|
2351
|
+
devicePlatform: readString3(body, "device_platform") ?? readString3(body, "devicePlatform") ?? "unknown",
|
|
1331
2352
|
paths
|
|
1332
2353
|
});
|
|
1333
2354
|
ctx.body = claimed;
|
|
@@ -1335,12 +2356,24 @@ async function createApp(options = {}) {
|
|
|
1335
2356
|
device_id: claimed.device.device_id,
|
|
1336
2357
|
device_platform: claimed.device.platform
|
|
1337
2358
|
});
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
2359
|
+
const timer = setTimeout(() => {
|
|
2360
|
+
void recordPairingClaim(
|
|
2361
|
+
{
|
|
2362
|
+
sessionId,
|
|
2363
|
+
deviceId: claimed.device.device_id,
|
|
2364
|
+
deviceLabel: claimed.device.label,
|
|
2365
|
+
devicePlatform: claimed.device.platform
|
|
2366
|
+
},
|
|
2367
|
+
paths
|
|
2368
|
+
).catch((error) => {
|
|
2369
|
+
void logger.warn("pairing_claim_record_failed", {
|
|
2370
|
+
session_id: sessionId,
|
|
2371
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2372
|
+
});
|
|
2373
|
+
});
|
|
2374
|
+
void options.onPairingClaimed?.();
|
|
2375
|
+
}, 250);
|
|
2376
|
+
timer.unref?.();
|
|
1344
2377
|
});
|
|
1345
2378
|
router.get("/api/v1/auth/me", async (ctx) => {
|
|
1346
2379
|
const auth = await authenticateRequest(ctx, paths);
|
|
@@ -1363,7 +2396,7 @@ async function createApp(options = {}) {
|
|
|
1363
2396
|
});
|
|
1364
2397
|
router.post("/api/v1/auth/refresh", async (ctx) => {
|
|
1365
2398
|
const body = await readJsonBody(ctx.req);
|
|
1366
|
-
const refreshToken =
|
|
2399
|
+
const refreshToken = readString3(body, "refresh_token") ?? readString3(body, "refreshToken");
|
|
1367
2400
|
if (!refreshToken) {
|
|
1368
2401
|
throw new LinkHttpError(400, "refresh_token_required", "refresh_token is required");
|
|
1369
2402
|
}
|
|
@@ -1383,7 +2416,7 @@ async function createApp(options = {}) {
|
|
|
1383
2416
|
});
|
|
1384
2417
|
router.post("/api/v1/auth/logout", async (ctx) => {
|
|
1385
2418
|
const body = await readJsonBody(ctx.req);
|
|
1386
|
-
const refreshToken =
|
|
2419
|
+
const refreshToken = readString3(body, "refresh_token") ?? readString3(body, "refreshToken");
|
|
1387
2420
|
if (refreshToken) {
|
|
1388
2421
|
await revokeDeviceRefreshToken(refreshToken, paths);
|
|
1389
2422
|
}
|
|
@@ -1415,10 +2448,116 @@ async function createApp(options = {}) {
|
|
|
1415
2448
|
await authenticateRequest(ctx, paths);
|
|
1416
2449
|
ctx.body = await listHermesModels();
|
|
1417
2450
|
});
|
|
2451
|
+
router.get("/api/v1/conversations", async (ctx) => {
|
|
2452
|
+
await authenticateRequest(ctx, paths);
|
|
2453
|
+
ctx.set("cache-control", "no-store");
|
|
2454
|
+
ctx.body = {
|
|
2455
|
+
ok: true,
|
|
2456
|
+
conversations: await conversations.listConversations()
|
|
2457
|
+
};
|
|
2458
|
+
});
|
|
2459
|
+
router.post("/api/v1/conversations", async (ctx) => {
|
|
2460
|
+
await authenticateRequest(ctx, paths);
|
|
2461
|
+
const body = await readJsonBody(ctx.req);
|
|
2462
|
+
ctx.status = 201;
|
|
2463
|
+
ctx.body = {
|
|
2464
|
+
ok: true,
|
|
2465
|
+
conversation: await conversations.createConversation({
|
|
2466
|
+
title: readString3(body, "title") ?? void 0
|
|
2467
|
+
})
|
|
2468
|
+
};
|
|
2469
|
+
});
|
|
2470
|
+
router.get("/api/v1/conversations/:conversationId/messages", async (ctx) => {
|
|
2471
|
+
await authenticateRequest(ctx, paths);
|
|
2472
|
+
ctx.set("cache-control", "no-store");
|
|
2473
|
+
const result = await conversations.getMessages(ctx.params.conversationId);
|
|
2474
|
+
ctx.body = {
|
|
2475
|
+
ok: true,
|
|
2476
|
+
conversation_id: ctx.params.conversationId,
|
|
2477
|
+
...result
|
|
2478
|
+
};
|
|
2479
|
+
});
|
|
2480
|
+
router.get("/api/v1/conversations/:conversationId/events", async (ctx) => {
|
|
2481
|
+
await authenticateRequest(ctx, paths);
|
|
2482
|
+
const after = readInteger(ctx.query.after) ?? 0;
|
|
2483
|
+
const history = await conversations.listEvents(ctx.params.conversationId, after);
|
|
2484
|
+
ctx.respond = false;
|
|
2485
|
+
const response = ctx.res;
|
|
2486
|
+
response.statusCode = 200;
|
|
2487
|
+
response.setHeader("content-type", "text/event-stream; charset=utf-8");
|
|
2488
|
+
response.setHeader("cache-control", "no-store");
|
|
2489
|
+
response.setHeader("connection", "keep-alive");
|
|
2490
|
+
for (const event of history) {
|
|
2491
|
+
writeSseEvent(response, event);
|
|
2492
|
+
}
|
|
2493
|
+
const unsubscribe = conversations.subscribe(ctx.params.conversationId, (event) => {
|
|
2494
|
+
writeSseEvent(response, event);
|
|
2495
|
+
});
|
|
2496
|
+
const cleanup = () => {
|
|
2497
|
+
unsubscribe();
|
|
2498
|
+
response.end();
|
|
2499
|
+
};
|
|
2500
|
+
ctx.req.on("close", cleanup);
|
|
2501
|
+
});
|
|
2502
|
+
router.post("/api/v1/conversations/:conversationId/messages", async (ctx) => {
|
|
2503
|
+
await authenticateRequest(ctx, paths);
|
|
2504
|
+
const body = await readJsonBody(ctx.req);
|
|
2505
|
+
const content = readString3(body, "content") ?? readString3(body, "text") ?? readString3(body, "input") ?? "";
|
|
2506
|
+
const attachments = readMessageAttachments(body.attachments ?? body.blobs);
|
|
2507
|
+
if (!content && attachments.length === 0) {
|
|
2508
|
+
throw new LinkHttpError(400, "message_content_required", "message content is required");
|
|
2509
|
+
}
|
|
2510
|
+
ctx.status = 202;
|
|
2511
|
+
ctx.body = {
|
|
2512
|
+
ok: true,
|
|
2513
|
+
...await conversations.sendMessage({
|
|
2514
|
+
conversationId: ctx.params.conversationId,
|
|
2515
|
+
content,
|
|
2516
|
+
attachments,
|
|
2517
|
+
clientMessageId: readString3(body, "client_message_id") ?? readString3(body, "clientMessageId") ?? void 0,
|
|
2518
|
+
idempotencyKey: readHeader(ctx, "idempotency-key") ?? void 0
|
|
2519
|
+
})
|
|
2520
|
+
};
|
|
2521
|
+
});
|
|
2522
|
+
router.post("/api/v1/conversations/:conversationId/ack", async (ctx) => {
|
|
2523
|
+
await authenticateRequest(ctx, paths);
|
|
2524
|
+
ctx.body = { ok: true };
|
|
2525
|
+
});
|
|
2526
|
+
router.delete("/api/v1/conversations/:conversationId", async (ctx) => {
|
|
2527
|
+
await authenticateRequest(ctx, paths);
|
|
2528
|
+
ctx.body = {
|
|
2529
|
+
ok: true,
|
|
2530
|
+
...await conversations.deleteConversation(ctx.params.conversationId),
|
|
2531
|
+
blob_gc_completed: true
|
|
2532
|
+
};
|
|
2533
|
+
});
|
|
2534
|
+
router.post("/api/v1/conversations/:conversationId/blobs", async (ctx) => {
|
|
2535
|
+
await authenticateRequest(ctx, paths);
|
|
2536
|
+
const bytes = await readRawBody(ctx.req, MAX_BLOB_UPLOAD_BYTES);
|
|
2537
|
+
if (bytes.byteLength === 0) {
|
|
2538
|
+
throw new LinkHttpError(400, "blob_empty", "Blob body is empty");
|
|
2539
|
+
}
|
|
2540
|
+
const blob = await conversations.writeBlob(ctx.params.conversationId, {
|
|
2541
|
+
bytes,
|
|
2542
|
+
filename: readHeader(ctx, "x-filename") ?? void 0,
|
|
2543
|
+
mime: ctx.get("content-type") || void 0
|
|
2544
|
+
});
|
|
2545
|
+
ctx.status = 201;
|
|
2546
|
+
ctx.body = { ok: true, blob };
|
|
2547
|
+
});
|
|
2548
|
+
router.get("/api/v1/conversations/:conversationId/blobs/:blobId", async (ctx) => {
|
|
2549
|
+
await authenticateRequest(ctx, paths);
|
|
2550
|
+
const blob = await conversations.readBlob(ctx.params.conversationId, ctx.params.blobId);
|
|
2551
|
+
ctx.set("content-type", blob.mime);
|
|
2552
|
+
ctx.set("content-length", String(blob.size));
|
|
2553
|
+
ctx.set("cache-control", "private, max-age=86400");
|
|
2554
|
+
ctx.set("content-disposition", `inline; filename="${blob.filename.replaceAll('"', "")}"`);
|
|
2555
|
+
ctx.body = blob.bytes;
|
|
2556
|
+
});
|
|
1418
2557
|
router.post("/api/v1/runs", async (ctx) => {
|
|
1419
2558
|
await authenticateRequest(ctx, paths);
|
|
1420
2559
|
const body = await readJsonBody(ctx.req);
|
|
1421
|
-
const input =
|
|
2560
|
+
const input = readString3(body, "input");
|
|
1422
2561
|
if (!input) {
|
|
1423
2562
|
throw new LinkHttpError(400, "run_input_required", "input is required");
|
|
1424
2563
|
}
|
|
@@ -1426,7 +2565,7 @@ async function createApp(options = {}) {
|
|
|
1426
2565
|
ctx.body = await createHermesRun({
|
|
1427
2566
|
input,
|
|
1428
2567
|
conversation_history: readConversationHistory(body.conversation_history ?? body.conversationHistory),
|
|
1429
|
-
session_id:
|
|
2568
|
+
session_id: readString3(body, "session_id") ?? readString3(body, "sessionId") ?? void 0
|
|
1430
2569
|
});
|
|
1431
2570
|
});
|
|
1432
2571
|
router.get("/api/v1/runs/:runId/events", async (ctx) => {
|
|
@@ -1505,14 +2644,28 @@ async function createApp(options = {}) {
|
|
|
1505
2644
|
return app;
|
|
1506
2645
|
}
|
|
1507
2646
|
async function readJsonBody(request) {
|
|
2647
|
+
const raw = await readRawBody(request, MAX_JSON_BODY_BYTES);
|
|
2648
|
+
if (raw.byteLength === 0) {
|
|
2649
|
+
return {};
|
|
2650
|
+
}
|
|
2651
|
+
try {
|
|
2652
|
+
return JSON.parse(Buffer.from(raw).toString("utf8"));
|
|
2653
|
+
} catch {
|
|
2654
|
+
throw new LinkHttpError(400, "invalid_json", "Request body must be valid JSON");
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
async function readRawBody(request, maxBytes) {
|
|
1508
2658
|
const chunks = [];
|
|
2659
|
+
let totalBytes = 0;
|
|
1509
2660
|
for await (const chunk of request) {
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
2661
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
2662
|
+
totalBytes += buffer.byteLength;
|
|
2663
|
+
if (totalBytes > maxBytes) {
|
|
2664
|
+
throw new LinkHttpError(413, "request_body_too_large", "Request body is too large");
|
|
2665
|
+
}
|
|
2666
|
+
chunks.push(buffer);
|
|
1514
2667
|
}
|
|
1515
|
-
return
|
|
2668
|
+
return Buffer.concat(chunks);
|
|
1516
2669
|
}
|
|
1517
2670
|
function readProfileName(body) {
|
|
1518
2671
|
if (typeof body.name !== "string") {
|
|
@@ -1551,7 +2704,7 @@ function readBearerToken(value) {
|
|
|
1551
2704
|
const token = trimmed.slice(7).trim();
|
|
1552
2705
|
return token || null;
|
|
1553
2706
|
}
|
|
1554
|
-
function
|
|
2707
|
+
function readString3(body, key) {
|
|
1555
2708
|
const value = body[key];
|
|
1556
2709
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1557
2710
|
}
|
|
@@ -1563,6 +2716,25 @@ function readLimit(value) {
|
|
|
1563
2716
|
const parsed = Number.parseInt(raw, 10);
|
|
1564
2717
|
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1565
2718
|
}
|
|
2719
|
+
function readInteger(value) {
|
|
2720
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
2721
|
+
if (typeof raw !== "string") {
|
|
2722
|
+
return void 0;
|
|
2723
|
+
}
|
|
2724
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2725
|
+
return Number.isFinite(parsed) ? Math.max(0, parsed) : void 0;
|
|
2726
|
+
}
|
|
2727
|
+
function readHeader(ctx, name) {
|
|
2728
|
+
const value = ctx.get(name).trim();
|
|
2729
|
+
return value ? value : null;
|
|
2730
|
+
}
|
|
2731
|
+
function writeSseEvent(response, event) {
|
|
2732
|
+
response.write(`event: ${event.type}
|
|
2733
|
+
`);
|
|
2734
|
+
response.write(`data: ${JSON.stringify(event)}
|
|
2735
|
+
|
|
2736
|
+
`);
|
|
2737
|
+
}
|
|
1566
2738
|
function readConversationHistory(value) {
|
|
1567
2739
|
if (!Array.isArray(value)) {
|
|
1568
2740
|
return [];
|
|
@@ -1579,6 +2751,22 @@ function readConversationHistory(value) {
|
|
|
1579
2751
|
return { role, content };
|
|
1580
2752
|
}).filter((item) => Boolean(item));
|
|
1581
2753
|
}
|
|
2754
|
+
function readMessageAttachments(value) {
|
|
2755
|
+
if (!Array.isArray(value)) {
|
|
2756
|
+
return [];
|
|
2757
|
+
}
|
|
2758
|
+
return value.map((item) => {
|
|
2759
|
+
if (typeof item === "string" && item.trim()) {
|
|
2760
|
+
return { blob_id: item.trim() };
|
|
2761
|
+
}
|
|
2762
|
+
if (typeof item !== "object" || item === null) {
|
|
2763
|
+
return null;
|
|
2764
|
+
}
|
|
2765
|
+
const record = item;
|
|
2766
|
+
const blobId = typeof record.blob_id === "string" ? record.blob_id.trim() : typeof record.blobId === "string" ? record.blobId.trim() : "";
|
|
2767
|
+
return blobId ? { blob_id: blobId } : null;
|
|
2768
|
+
}).filter((item) => Boolean(item));
|
|
2769
|
+
}
|
|
1582
2770
|
function routeObjects(urls) {
|
|
1583
2771
|
return urls.map((url) => ({
|
|
1584
2772
|
kind: url.includes("/api/v1/relay/links/") ? "relay" : "lan",
|
|
@@ -1597,8 +2785,10 @@ export {
|
|
|
1597
2785
|
ensureIdentity,
|
|
1598
2786
|
getIdentityStatus,
|
|
1599
2787
|
preparePairing,
|
|
2788
|
+
readPairingClaim,
|
|
2789
|
+
clearPairingClaim,
|
|
1600
2790
|
createFileLogger,
|
|
1601
2791
|
getLinkLogFile,
|
|
1602
2792
|
createApp
|
|
1603
2793
|
};
|
|
1604
|
-
//# sourceMappingURL=chunk-
|
|
2794
|
+
//# sourceMappingURL=chunk-VCQJ5DSN.js.map
|