@hermespilot/link 0.3.9 → 0.4.1
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-7OVDWXR7.js → chunk-476X63MC.js} +930 -218
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +88 -49
- package/dist/http/app.d.ts +18 -15
- package/dist/http/app.js +1 -1
- package/dist/paths-CmAiZsna.d.ts +16 -0
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import Router from "@koa/router";
|
|
|
4
4
|
|
|
5
5
|
// src/conversations/conversation-service.ts
|
|
6
6
|
import { EventEmitter } from "events";
|
|
7
|
-
import { randomUUID as
|
|
7
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
8
8
|
|
|
9
9
|
// src/database/link-database.ts
|
|
10
10
|
import { mkdir } from "fs/promises";
|
|
@@ -1500,6 +1500,13 @@ function resolveHermesProfileDir(profileName = "default") {
|
|
|
1500
1500
|
}
|
|
1501
1501
|
return path3.join(os.homedir(), ".hermes", "profiles", profileName);
|
|
1502
1502
|
}
|
|
1503
|
+
function resolveHermesProfilesDir() {
|
|
1504
|
+
const hermesHome = process.env.HERMES_HOME?.trim();
|
|
1505
|
+
if (hermesHome) {
|
|
1506
|
+
return path3.join(resolveDefaultHermesRoot(path3.resolve(hermesHome)), "profiles");
|
|
1507
|
+
}
|
|
1508
|
+
return path3.join(os.homedir(), ".hermes", "profiles");
|
|
1509
|
+
}
|
|
1503
1510
|
function resolveHermesConfigPath(profileName = "default") {
|
|
1504
1511
|
return path3.join(resolveHermesProfileDir(profileName), "config.yaml");
|
|
1505
1512
|
}
|
|
@@ -3883,7 +3890,7 @@ async function listCronOutputFiles(profileName, jobId) {
|
|
|
3883
3890
|
mtimeMs: fileStat.mtimeMs
|
|
3884
3891
|
});
|
|
3885
3892
|
}
|
|
3886
|
-
return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path:
|
|
3893
|
+
return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path26, mtime }) => ({ path: path26, mtime }));
|
|
3887
3894
|
}
|
|
3888
3895
|
async function readCronOutput(outputPath) {
|
|
3889
3896
|
const content = await readFile3(outputPath, "utf8");
|
|
@@ -3970,7 +3977,7 @@ import os2 from "os";
|
|
|
3970
3977
|
import path5 from "path";
|
|
3971
3978
|
|
|
3972
3979
|
// src/constants.ts
|
|
3973
|
-
var LINK_VERSION = "0.
|
|
3980
|
+
var LINK_VERSION = "0.4.1";
|
|
3974
3981
|
var LINK_COMMAND = "hermeslink";
|
|
3975
3982
|
var LINK_DEFAULT_PORT = 52379;
|
|
3976
3983
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -6760,6 +6767,70 @@ function safePathSegment(value, fallback) {
|
|
|
6760
6767
|
return safe.length > 0 ? safe.slice(0, 120) : fallback;
|
|
6761
6768
|
}
|
|
6762
6769
|
|
|
6770
|
+
// src/conversations/conversation-clear-plans.ts
|
|
6771
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
6772
|
+
import { mkdir as mkdir7 } from "fs/promises";
|
|
6773
|
+
import path11 from "path";
|
|
6774
|
+
var PLAN_ID_PATTERN = /^clear_[a-f0-9]{32}$/u;
|
|
6775
|
+
var ConversationClearPlanStore = class {
|
|
6776
|
+
constructor(paths) {
|
|
6777
|
+
this.paths = paths;
|
|
6778
|
+
}
|
|
6779
|
+
paths;
|
|
6780
|
+
async create(conversationIds) {
|
|
6781
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6782
|
+
const plan = {
|
|
6783
|
+
id: `clear_${randomUUID5().replaceAll("-", "")}`,
|
|
6784
|
+
status: "prepared",
|
|
6785
|
+
created_at: now,
|
|
6786
|
+
updated_at: now,
|
|
6787
|
+
total_count: conversationIds.length,
|
|
6788
|
+
deleted_count: 0,
|
|
6789
|
+
failed_count: 0,
|
|
6790
|
+
conversation_ids: conversationIds,
|
|
6791
|
+
conversations: []
|
|
6792
|
+
};
|
|
6793
|
+
await this.write(plan);
|
|
6794
|
+
return plan;
|
|
6795
|
+
}
|
|
6796
|
+
async read(planId) {
|
|
6797
|
+
const normalizedPlanId = normalizePlanId(planId);
|
|
6798
|
+
const plan = await readJsonFile(
|
|
6799
|
+
this.planPath(normalizedPlanId)
|
|
6800
|
+
);
|
|
6801
|
+
if (!plan) {
|
|
6802
|
+
throw new LinkHttpError(
|
|
6803
|
+
404,
|
|
6804
|
+
"conversation_clear_plan_not_found",
|
|
6805
|
+
"Conversation clear plan was not found"
|
|
6806
|
+
);
|
|
6807
|
+
}
|
|
6808
|
+
return plan;
|
|
6809
|
+
}
|
|
6810
|
+
async write(plan) {
|
|
6811
|
+
const normalizedPlanId = normalizePlanId(plan.id);
|
|
6812
|
+
await mkdir7(this.plansDir(), { recursive: true, mode: 448 });
|
|
6813
|
+
await writeJsonFile(this.planPath(normalizedPlanId), plan);
|
|
6814
|
+
}
|
|
6815
|
+
plansDir() {
|
|
6816
|
+
return path11.join(this.paths.indexesDir, "conversation-clear-plans");
|
|
6817
|
+
}
|
|
6818
|
+
planPath(planId) {
|
|
6819
|
+
return path11.join(this.plansDir(), `${planId}.json`);
|
|
6820
|
+
}
|
|
6821
|
+
};
|
|
6822
|
+
function normalizePlanId(planId) {
|
|
6823
|
+
const normalized = planId.trim();
|
|
6824
|
+
if (!PLAN_ID_PATTERN.test(normalized)) {
|
|
6825
|
+
throw new LinkHttpError(
|
|
6826
|
+
400,
|
|
6827
|
+
"conversation_clear_plan_id_invalid",
|
|
6828
|
+
"Conversation clear plan id is invalid"
|
|
6829
|
+
);
|
|
6830
|
+
}
|
|
6831
|
+
return normalized;
|
|
6832
|
+
}
|
|
6833
|
+
|
|
6763
6834
|
// src/conversations/conversation-session-ids.ts
|
|
6764
6835
|
function normalizeHermesSessionIds(values) {
|
|
6765
6836
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -6810,8 +6881,134 @@ var MAX_UPLOADED_BLOB_BYTES = 50 * 1024 * 1024;
|
|
|
6810
6881
|
var ConversationMaintenanceCoordinator = class {
|
|
6811
6882
|
constructor(deps) {
|
|
6812
6883
|
this.deps = deps;
|
|
6884
|
+
this.clearPlans = new ConversationClearPlanStore(deps.paths);
|
|
6813
6885
|
}
|
|
6814
6886
|
deps;
|
|
6887
|
+
clearPlans;
|
|
6888
|
+
async prepareClearAllConversationPlan() {
|
|
6889
|
+
const targets = [];
|
|
6890
|
+
for (const conversationId of await this.deps.store.listConversationIds()) {
|
|
6891
|
+
const manifest = await this.deps.store.readManifest(conversationId).catch(() => null);
|
|
6892
|
+
if (manifest?.status !== "active") {
|
|
6893
|
+
continue;
|
|
6894
|
+
}
|
|
6895
|
+
targets.push({
|
|
6896
|
+
id: conversationId,
|
|
6897
|
+
updatedAt: manifest.updated_at
|
|
6898
|
+
});
|
|
6899
|
+
}
|
|
6900
|
+
targets.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
6901
|
+
return this.clearPlans.create(
|
|
6902
|
+
targets.map((target) => target.id)
|
|
6903
|
+
);
|
|
6904
|
+
}
|
|
6905
|
+
async readClearAllConversationPlan(planId) {
|
|
6906
|
+
return this.clearPlans.read(planId);
|
|
6907
|
+
}
|
|
6908
|
+
async executeClearAllConversationPlan(planId) {
|
|
6909
|
+
let plan = await this.clearPlans.read(planId);
|
|
6910
|
+
if (plan.status === "completed") {
|
|
6911
|
+
return plan;
|
|
6912
|
+
}
|
|
6913
|
+
if (plan.status === "failed") {
|
|
6914
|
+
throw new LinkHttpError(
|
|
6915
|
+
409,
|
|
6916
|
+
"conversation_clear_plan_already_failed",
|
|
6917
|
+
"Conversation clear plan has already failed"
|
|
6918
|
+
);
|
|
6919
|
+
}
|
|
6920
|
+
const results = [];
|
|
6921
|
+
if (plan.status !== "executing") {
|
|
6922
|
+
plan = await this.writeClearPlan({
|
|
6923
|
+
...plan,
|
|
6924
|
+
status: "executing",
|
|
6925
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6926
|
+
conversations: results,
|
|
6927
|
+
deleted_count: 0,
|
|
6928
|
+
failed_count: 0
|
|
6929
|
+
});
|
|
6930
|
+
}
|
|
6931
|
+
for (const conversationId of plan.conversation_ids) {
|
|
6932
|
+
try {
|
|
6933
|
+
const deleted = await this.deleteConversation(conversationId);
|
|
6934
|
+
results.push({ ...deleted, status: "deleted" });
|
|
6935
|
+
} catch (error) {
|
|
6936
|
+
if (error instanceof LinkHttpError && error.code === "conversation_not_found") {
|
|
6937
|
+
results.push({
|
|
6938
|
+
conversation_id: conversationId,
|
|
6939
|
+
status: "deleted",
|
|
6940
|
+
hermes_deleted: false,
|
|
6941
|
+
deleted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6942
|
+
});
|
|
6943
|
+
} else {
|
|
6944
|
+
results.push({
|
|
6945
|
+
conversation_id: conversationId,
|
|
6946
|
+
status: "failed",
|
|
6947
|
+
error: {
|
|
6948
|
+
code: error instanceof LinkHttpError ? error.code : "internal_error",
|
|
6949
|
+
message: error instanceof Error ? error.message : "Internal error"
|
|
6950
|
+
}
|
|
6951
|
+
});
|
|
6952
|
+
}
|
|
6953
|
+
}
|
|
6954
|
+
plan = await this.writeClearPlan({
|
|
6955
|
+
...plan,
|
|
6956
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6957
|
+
conversations: [...results],
|
|
6958
|
+
deleted_count: results.filter((result) => result.status === "deleted").length,
|
|
6959
|
+
failed_count: results.filter((result) => result.status === "failed").length
|
|
6960
|
+
});
|
|
6961
|
+
}
|
|
6962
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6963
|
+
return this.writeClearPlan({
|
|
6964
|
+
...plan,
|
|
6965
|
+
status: plan.failed_count === 0 ? "completed" : "failed",
|
|
6966
|
+
updated_at: completedAt,
|
|
6967
|
+
completed_at: completedAt
|
|
6968
|
+
});
|
|
6969
|
+
}
|
|
6970
|
+
async startClearAllConversationPlan(planId) {
|
|
6971
|
+
const plan = await this.clearPlans.read(planId);
|
|
6972
|
+
if (plan.status === "completed" || plan.status === "executing") {
|
|
6973
|
+
return plan;
|
|
6974
|
+
}
|
|
6975
|
+
if (plan.status === "failed") {
|
|
6976
|
+
throw new LinkHttpError(
|
|
6977
|
+
409,
|
|
6978
|
+
"conversation_clear_plan_already_failed",
|
|
6979
|
+
"Conversation clear plan has already failed"
|
|
6980
|
+
);
|
|
6981
|
+
}
|
|
6982
|
+
const started = await this.writeClearPlan({
|
|
6983
|
+
...plan,
|
|
6984
|
+
status: "executing",
|
|
6985
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6986
|
+
conversations: [],
|
|
6987
|
+
deleted_count: 0,
|
|
6988
|
+
failed_count: 0
|
|
6989
|
+
});
|
|
6990
|
+
void this.executeClearAllConversationPlan(started.id).catch(
|
|
6991
|
+
async (error) => {
|
|
6992
|
+
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6993
|
+
await this.writeClearPlan({
|
|
6994
|
+
...started,
|
|
6995
|
+
status: "failed",
|
|
6996
|
+
updated_at: failedAt,
|
|
6997
|
+
completed_at: failedAt,
|
|
6998
|
+
failed_count: started.total_count,
|
|
6999
|
+
conversations: started.conversation_ids.map((conversationId) => ({
|
|
7000
|
+
conversation_id: conversationId,
|
|
7001
|
+
status: "failed",
|
|
7002
|
+
error: {
|
|
7003
|
+
code: error instanceof LinkHttpError ? error.code : "internal_error",
|
|
7004
|
+
message: error instanceof Error ? error.message : "Internal error"
|
|
7005
|
+
}
|
|
7006
|
+
}))
|
|
7007
|
+
}).catch(() => void 0);
|
|
7008
|
+
}
|
|
7009
|
+
);
|
|
7010
|
+
return started;
|
|
7011
|
+
}
|
|
6815
7012
|
async deleteConversation(conversationId) {
|
|
6816
7013
|
return this.deps.withConversationLock(
|
|
6817
7014
|
conversationId,
|
|
@@ -7017,6 +7214,10 @@ var ConversationMaintenanceCoordinator = class {
|
|
|
7017
7214
|
async listConversationBlobIds(conversationId) {
|
|
7018
7215
|
return listConversationBlobIds(this.deps.paths, conversationId);
|
|
7019
7216
|
}
|
|
7217
|
+
async writeClearPlan(plan) {
|
|
7218
|
+
await this.clearPlans.write(plan);
|
|
7219
|
+
return plan;
|
|
7220
|
+
}
|
|
7020
7221
|
};
|
|
7021
7222
|
function isVoiceAttachmentInput(attachment) {
|
|
7022
7223
|
return attachment.kind === "voice" || attachment.type === "voice" || attachment.is_voice_note === true || attachment.isVoiceNote === true;
|
|
@@ -7096,14 +7297,14 @@ function isUsableLanIpv4(value) {
|
|
|
7096
7297
|
|
|
7097
7298
|
// src/hermes/session-title.ts
|
|
7098
7299
|
import { stat as stat7 } from "fs/promises";
|
|
7099
|
-
import
|
|
7300
|
+
import path12 from "path";
|
|
7100
7301
|
async function readHermesSessionTitle(sessionId, paths, profileName) {
|
|
7101
7302
|
const trimmedSessionId = sessionId.trim();
|
|
7102
7303
|
if (!trimmedSessionId) {
|
|
7103
7304
|
return void 0;
|
|
7104
7305
|
}
|
|
7105
7306
|
const resolvedProfileName = isValidProfileName(profileName) ? profileName : "default";
|
|
7106
|
-
const dbPath =
|
|
7307
|
+
const dbPath = path12.join(
|
|
7107
7308
|
resolveHermesProfileDir(resolvedProfileName),
|
|
7108
7309
|
"state.db"
|
|
7109
7310
|
);
|
|
@@ -7124,7 +7325,7 @@ async function readHermesCompressionTip(sessionId, paths, profileName) {
|
|
|
7124
7325
|
return void 0;
|
|
7125
7326
|
}
|
|
7126
7327
|
const resolvedProfileName = isValidProfileName(profileName) ? profileName : "default";
|
|
7127
|
-
const dbPath =
|
|
7328
|
+
const dbPath = path12.join(
|
|
7128
7329
|
resolveHermesProfileDir(resolvedProfileName),
|
|
7129
7330
|
"state.db"
|
|
7130
7331
|
);
|
|
@@ -7219,11 +7420,19 @@ var ConversationMetadataCoordinator = class {
|
|
|
7219
7420
|
const snapshot = options.snapshot ?? await this.deps.store.readSnapshot(manifest.id).catch(
|
|
7220
7421
|
() => createEmptySnapshot()
|
|
7221
7422
|
);
|
|
7222
|
-
const
|
|
7423
|
+
const profileName = latestRuntimeRun(snapshot)?.profile ?? manifest.profile_name_snapshot ?? manifest.profile;
|
|
7424
|
+
const compressionRootSessionId = manifest.hermes_lineage?.kind === "compression" ? manifest.hermes_lineage.root_session_id : void 0;
|
|
7425
|
+
const rootTitle = compressionRootSessionId ? await readHermesSessionTitle(
|
|
7426
|
+
compressionRootSessionId,
|
|
7427
|
+
this.deps.paths,
|
|
7428
|
+
profileName
|
|
7429
|
+
) : void 0;
|
|
7430
|
+
const tipTitle = await readHermesSessionTitle(
|
|
7223
7431
|
manifest.hermes_session_id,
|
|
7224
7432
|
this.deps.paths,
|
|
7225
|
-
|
|
7433
|
+
profileName
|
|
7226
7434
|
);
|
|
7435
|
+
const title = rootTitle ?? (compressionRootSessionId ? stripCompressionTitleSuffix(tipTitle) : tipTitle);
|
|
7227
7436
|
if (!title || title === manifest.title) {
|
|
7228
7437
|
return manifest;
|
|
7229
7438
|
}
|
|
@@ -7537,9 +7746,18 @@ function canAutoGenerateTitle(manifest) {
|
|
|
7537
7746
|
function normalizeConversationTitleSource(title) {
|
|
7538
7747
|
return isDefaultConversationTitle(title) ? "default" : "hermes";
|
|
7539
7748
|
}
|
|
7749
|
+
function stripCompressionTitleSuffix(value) {
|
|
7750
|
+
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
7751
|
+
if (!normalized) {
|
|
7752
|
+
return void 0;
|
|
7753
|
+
}
|
|
7754
|
+
const match = /^(.*?) #\d+$/u.exec(normalized);
|
|
7755
|
+
const stripped = match?.[1]?.trim();
|
|
7756
|
+
return stripped || normalized;
|
|
7757
|
+
}
|
|
7540
7758
|
|
|
7541
7759
|
// src/conversations/conversation-turns.ts
|
|
7542
|
-
import { randomUUID as
|
|
7760
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
7543
7761
|
var MESSAGE_ORDER_STEP_MS = 10;
|
|
7544
7762
|
var ASSISTANT_ORDER_OFFSET_MS = 1;
|
|
7545
7763
|
function createAgentTurnDraft(input) {
|
|
@@ -7773,10 +7991,10 @@ function createAssistantMessage(input) {
|
|
|
7773
7991
|
};
|
|
7774
7992
|
}
|
|
7775
7993
|
function createMessageId() {
|
|
7776
|
-
return `msg_${
|
|
7994
|
+
return `msg_${randomUUID6().replaceAll("-", "")}`;
|
|
7777
7995
|
}
|
|
7778
7996
|
function createRunId() {
|
|
7779
|
-
return `run_${
|
|
7997
|
+
return `run_${randomUUID6().replaceAll("-", "")}`;
|
|
7780
7998
|
}
|
|
7781
7999
|
function hasActiveOrQueuedRuns(snapshot) {
|
|
7782
8000
|
return snapshot.runs.some(
|
|
@@ -8891,20 +9109,20 @@ function hydrateAgentEventBlocks(blocks, agentEvents) {
|
|
|
8891
9109
|
// src/conversations/conversation-store.ts
|
|
8892
9110
|
import {
|
|
8893
9111
|
appendFile as appendFile2,
|
|
8894
|
-
mkdir as
|
|
9112
|
+
mkdir as mkdir8,
|
|
8895
9113
|
readdir as readdir5,
|
|
8896
9114
|
readFile as readFile7,
|
|
8897
9115
|
rm as rm5,
|
|
8898
9116
|
writeFile as writeFile2
|
|
8899
9117
|
} from "fs/promises";
|
|
8900
|
-
import
|
|
9118
|
+
import path13 from "path";
|
|
8901
9119
|
var ConversationStore = class {
|
|
8902
9120
|
constructor(paths) {
|
|
8903
9121
|
this.paths = paths;
|
|
8904
9122
|
}
|
|
8905
9123
|
paths;
|
|
8906
9124
|
async ensureConversationsDir() {
|
|
8907
|
-
await
|
|
9125
|
+
await mkdir8(this.paths.conversationsDir, { recursive: true, mode: 448 });
|
|
8908
9126
|
}
|
|
8909
9127
|
async listConversationIds() {
|
|
8910
9128
|
await this.ensureConversationsDir();
|
|
@@ -8919,7 +9137,7 @@ var ConversationStore = class {
|
|
|
8919
9137
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
8920
9138
|
}
|
|
8921
9139
|
async createConversation(manifest, snapshot = createEmptySnapshot2()) {
|
|
8922
|
-
await
|
|
9140
|
+
await mkdir8(this.conversationDir(manifest.id), {
|
|
8923
9141
|
recursive: true,
|
|
8924
9142
|
mode: 448
|
|
8925
9143
|
});
|
|
@@ -8959,7 +9177,7 @@ var ConversationStore = class {
|
|
|
8959
9177
|
conversation_id: conversationId,
|
|
8960
9178
|
created_at: now
|
|
8961
9179
|
};
|
|
8962
|
-
await
|
|
9180
|
+
await mkdir8(this.conversationDir(conversationId), {
|
|
8963
9181
|
recursive: true,
|
|
8964
9182
|
mode: 448
|
|
8965
9183
|
});
|
|
@@ -9013,23 +9231,23 @@ var ConversationStore = class {
|
|
|
9013
9231
|
return manifest?.status === "active";
|
|
9014
9232
|
}
|
|
9015
9233
|
removeConversationAttachments(conversationId) {
|
|
9016
|
-
return rm5(
|
|
9234
|
+
return rm5(path13.join(this.conversationDir(conversationId), "attachments"), {
|
|
9017
9235
|
recursive: true,
|
|
9018
9236
|
force: true
|
|
9019
9237
|
});
|
|
9020
9238
|
}
|
|
9021
9239
|
conversationDir(conversationId) {
|
|
9022
9240
|
assertValidConversationId(conversationId);
|
|
9023
|
-
return
|
|
9241
|
+
return path13.join(this.paths.conversationsDir, conversationId);
|
|
9024
9242
|
}
|
|
9025
9243
|
manifestPath(conversationId) {
|
|
9026
|
-
return
|
|
9244
|
+
return path13.join(this.conversationDir(conversationId), "manifest.json");
|
|
9027
9245
|
}
|
|
9028
9246
|
snapshotPath(conversationId) {
|
|
9029
|
-
return
|
|
9247
|
+
return path13.join(this.conversationDir(conversationId), "snapshot.json");
|
|
9030
9248
|
}
|
|
9031
9249
|
eventsPath(conversationId) {
|
|
9032
|
-
return
|
|
9250
|
+
return path13.join(this.conversationDir(conversationId), "events.ndjson");
|
|
9033
9251
|
}
|
|
9034
9252
|
};
|
|
9035
9253
|
function createEmptySnapshot2() {
|
|
@@ -9040,14 +9258,13 @@ function isNodeError9(error, code) {
|
|
|
9040
9258
|
}
|
|
9041
9259
|
|
|
9042
9260
|
// src/conversations/hermes-session-sync.ts
|
|
9043
|
-
import { randomUUID as
|
|
9261
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
9044
9262
|
import { readdir as readdir7, readFile as readFile9, stat as stat9 } from "fs/promises";
|
|
9045
|
-
import
|
|
9046
|
-
import path14 from "path";
|
|
9263
|
+
import path15 from "path";
|
|
9047
9264
|
|
|
9048
9265
|
// src/conversations/delivery-import.ts
|
|
9049
9266
|
import { lstat as lstat2, readFile as readFile8, readdir as readdir6, stat as stat8 } from "fs/promises";
|
|
9050
|
-
import
|
|
9267
|
+
import path14 from "path";
|
|
9051
9268
|
var MAX_IMPORTED_BLOB_BYTES = 100 * 1024 * 1024;
|
|
9052
9269
|
var MAX_MEDIA_IMPORT_FAILURES = 20;
|
|
9053
9270
|
var MAX_DELIVERY_FILES = 50;
|
|
@@ -9118,16 +9335,16 @@ var SUPPORTED_DELIVERY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
9118
9335
|
".m4a"
|
|
9119
9336
|
]);
|
|
9120
9337
|
function resolveDeliveryStagingTarget(paths, stagingDir) {
|
|
9121
|
-
const resolvedDir =
|
|
9122
|
-
const relative =
|
|
9123
|
-
if (!relative || relative.startsWith("..") ||
|
|
9338
|
+
const resolvedDir = path14.resolve(stagingDir);
|
|
9339
|
+
const relative = path14.relative(path14.resolve(paths.conversationsDir), resolvedDir);
|
|
9340
|
+
if (!relative || relative.startsWith("..") || path14.isAbsolute(relative)) {
|
|
9124
9341
|
throw new LinkHttpError(
|
|
9125
9342
|
400,
|
|
9126
9343
|
"delivery_staging_invalid",
|
|
9127
9344
|
"delivery staging directory must be inside Hermes Link conversations"
|
|
9128
9345
|
);
|
|
9129
9346
|
}
|
|
9130
|
-
const segments = relative.split(
|
|
9347
|
+
const segments = relative.split(path14.sep);
|
|
9131
9348
|
if (segments.length !== 3 || segments[1] !== DELIVERY_STAGING_SEGMENT || !segments[0] || !segments[2]) {
|
|
9132
9349
|
throw new LinkHttpError(
|
|
9133
9350
|
400,
|
|
@@ -9163,7 +9380,7 @@ async function collectStagedDeliveryReferences(stagingDir) {
|
|
|
9163
9380
|
return entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).filter((entry) => isSupportedDeliveryFilename(entry.name)).sort(
|
|
9164
9381
|
(left, right) => left.name.localeCompare(right.name, "en", { numeric: true })
|
|
9165
9382
|
).slice(0, MAX_DELIVERY_FILES).map((entry) => {
|
|
9166
|
-
const sourcePath =
|
|
9383
|
+
const sourcePath = path14.join(stagingDir, entry.name);
|
|
9167
9384
|
const mime = inferMimeType(sourcePath);
|
|
9168
9385
|
return {
|
|
9169
9386
|
path: sourcePath,
|
|
@@ -9349,7 +9566,7 @@ async function writeBlobFromFile(deps, conversationId, source) {
|
|
|
9349
9566
|
}
|
|
9350
9567
|
return deps.writeBlob(conversationId, {
|
|
9351
9568
|
bytes: await readFile8(sourcePath),
|
|
9352
|
-
filename:
|
|
9569
|
+
filename: path14.basename(sourcePath),
|
|
9353
9570
|
mime: source.mime ?? inferMimeType(sourcePath)
|
|
9354
9571
|
});
|
|
9355
9572
|
}
|
|
@@ -9362,7 +9579,7 @@ function describeMediaImportFailure(reference, sourceKey, error) {
|
|
|
9362
9579
|
};
|
|
9363
9580
|
}
|
|
9364
9581
|
function isSupportedDeliveryFilename(filename) {
|
|
9365
|
-
return SUPPORTED_DELIVERY_EXTENSIONS.has(
|
|
9582
|
+
return SUPPORTED_DELIVERY_EXTENSIONS.has(path14.extname(filename).toLowerCase());
|
|
9366
9583
|
}
|
|
9367
9584
|
function readString8(payload, key) {
|
|
9368
9585
|
const value = payload[key];
|
|
@@ -9419,7 +9636,7 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
9419
9636
|
const candidates = [];
|
|
9420
9637
|
for (const profileName of profileNames) {
|
|
9421
9638
|
const profileDir = resolveHermesProfileDir(profileName);
|
|
9422
|
-
const dbPath =
|
|
9639
|
+
const dbPath = path15.join(profileDir, "state.db");
|
|
9423
9640
|
const sessions = await listProfileSessions(dbPath).catch((error) => {
|
|
9424
9641
|
result.errors.push({
|
|
9425
9642
|
profile: profileName,
|
|
@@ -9449,16 +9666,19 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
9449
9666
|
const importableCandidates = candidates.slice(0, maxImports);
|
|
9450
9667
|
result.skipped_over_limit = Math.max(0, candidates.length - maxImports);
|
|
9451
9668
|
for (const candidate of importableCandidates) {
|
|
9452
|
-
|
|
9669
|
+
const candidateSessionIds = lineageSessionIds(candidate);
|
|
9670
|
+
const knownConversationIds = findKnownConversationIdsForCandidate(
|
|
9671
|
+
knownHermesSessions,
|
|
9672
|
+
candidate
|
|
9673
|
+
);
|
|
9674
|
+
if (knownConversationIds.length > 0) {
|
|
9453
9675
|
result.skipped_existing += 1;
|
|
9454
|
-
const reprojected = await
|
|
9676
|
+
const reprojected = await mergeExistingHermesConversation({
|
|
9455
9677
|
paths,
|
|
9456
9678
|
store,
|
|
9457
9679
|
logger,
|
|
9458
9680
|
candidate,
|
|
9459
|
-
conversationIds:
|
|
9460
|
-
candidate.session.id
|
|
9461
|
-
) ?? []
|
|
9681
|
+
conversationIds: knownConversationIds
|
|
9462
9682
|
}).catch((error) => {
|
|
9463
9683
|
result.errors.push({
|
|
9464
9684
|
profile: candidate.profileName,
|
|
@@ -9469,14 +9689,22 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
9469
9689
|
if (reprojected) {
|
|
9470
9690
|
result.reprojected_count += 1;
|
|
9471
9691
|
}
|
|
9692
|
+
for (const sessionId of candidateSessionIds) {
|
|
9693
|
+
knownHermesSessions.sessionIds.add(sessionId);
|
|
9694
|
+
}
|
|
9695
|
+
continue;
|
|
9696
|
+
}
|
|
9697
|
+
if (candidateSessionIds.some(
|
|
9698
|
+
(sessionId) => knownHermesSessions.sessionIds.has(sessionId)
|
|
9699
|
+
)) {
|
|
9700
|
+
result.skipped_existing += 1;
|
|
9472
9701
|
continue;
|
|
9473
9702
|
}
|
|
9474
9703
|
const imported = await importHermesSession({
|
|
9475
9704
|
paths,
|
|
9476
9705
|
store,
|
|
9477
9706
|
logger,
|
|
9478
|
-
candidate
|
|
9479
|
-
existingHermesSessionIds: knownHermesSessions.ids
|
|
9707
|
+
candidate
|
|
9480
9708
|
}).catch((error) => {
|
|
9481
9709
|
result.errors.push({
|
|
9482
9710
|
profile: candidate.profileName,
|
|
@@ -9486,6 +9714,9 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
9486
9714
|
});
|
|
9487
9715
|
if (imported) {
|
|
9488
9716
|
result.imported_count += 1;
|
|
9717
|
+
for (const sessionId of candidateSessionIds) {
|
|
9718
|
+
knownHermesSessions.sessionIds.add(sessionId);
|
|
9719
|
+
}
|
|
9489
9720
|
}
|
|
9490
9721
|
}
|
|
9491
9722
|
if (result.imported_count > 0 || result.reprojected_count > 0 || result.errors.length > 0) {
|
|
@@ -9496,13 +9727,14 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
9496
9727
|
return result;
|
|
9497
9728
|
}
|
|
9498
9729
|
async function importHermesSession(input) {
|
|
9499
|
-
const { paths, store, logger, candidate
|
|
9730
|
+
const { paths, store, logger, candidate } = input;
|
|
9500
9731
|
const profile = await resolveConversationProfileTarget(
|
|
9501
9732
|
paths,
|
|
9502
9733
|
candidate.profileName
|
|
9503
9734
|
);
|
|
9504
9735
|
const sessionId = candidate.session.id;
|
|
9505
|
-
const
|
|
9736
|
+
const hermesSessionIds = lineageSessionIds(candidate);
|
|
9737
|
+
const messages = await readHermesLineageMessages(candidate);
|
|
9506
9738
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9507
9739
|
const createdAt = isoFromHermesTime(candidate.session.started_at) ?? now;
|
|
9508
9740
|
const updatedAt = isoFromHermesTime(candidate.session.last_active) ?? isoFromHermesTime(messages.at(-1)?.timestamp) ?? createdAt;
|
|
@@ -9519,7 +9751,14 @@ async function importHermesSession(input) {
|
|
|
9519
9751
|
}),
|
|
9520
9752
|
runs: []
|
|
9521
9753
|
};
|
|
9522
|
-
const title = readString9(candidate.session, "title") ?? firstUserText(snapshot);
|
|
9754
|
+
const title = lineageTitle(candidate) ?? readString9(candidate.session, "title") ?? firstUserText(snapshot);
|
|
9755
|
+
const importedStats = buildImportedHermesStats({
|
|
9756
|
+
candidate,
|
|
9757
|
+
snapshot,
|
|
9758
|
+
profileUid: profile.profileUid,
|
|
9759
|
+
profileName: profile.profileName,
|
|
9760
|
+
updatedAt
|
|
9761
|
+
});
|
|
9523
9762
|
const manifest = {
|
|
9524
9763
|
id: conversationId,
|
|
9525
9764
|
schema_version: 1,
|
|
@@ -9528,13 +9767,15 @@ async function importHermesSession(input) {
|
|
|
9528
9767
|
title_source: title ? "hermes" : "default",
|
|
9529
9768
|
status: "active",
|
|
9530
9769
|
hermes_session_id: sessionId,
|
|
9531
|
-
hermes_session_ids:
|
|
9770
|
+
hermes_session_ids: hermesSessionIds,
|
|
9771
|
+
...lineageManifestPatch(candidate),
|
|
9532
9772
|
profile_uid: profile.profileUid,
|
|
9533
9773
|
profile_name_snapshot: profile.profileName,
|
|
9534
9774
|
profile: profile.profileName,
|
|
9535
9775
|
created_at: createdAt,
|
|
9536
9776
|
updated_at: updatedAt,
|
|
9537
|
-
last_event_seq: 0
|
|
9777
|
+
last_event_seq: 0,
|
|
9778
|
+
stats: importedStats
|
|
9538
9779
|
};
|
|
9539
9780
|
await store.createConversation(manifest, snapshot);
|
|
9540
9781
|
await hydrateImportedConversationMedia({
|
|
@@ -9577,59 +9818,288 @@ async function importHermesSession(input) {
|
|
|
9577
9818
|
paths,
|
|
9578
9819
|
toStatsIndexRecord(await store.readManifest(conversationId), stats)
|
|
9579
9820
|
);
|
|
9580
|
-
existingHermesSessionIds.add(sessionId);
|
|
9581
9821
|
return true;
|
|
9582
9822
|
}
|
|
9583
|
-
async function
|
|
9823
|
+
async function mergeExistingHermesConversation(input) {
|
|
9824
|
+
const conversations = await readExistingHermesConversations(
|
|
9825
|
+
input.store,
|
|
9826
|
+
input.conversationIds
|
|
9827
|
+
);
|
|
9828
|
+
const active = conversations.filter((item) => item.manifest.status === "active");
|
|
9829
|
+
const canonical = selectCanonicalHermesConversation(active);
|
|
9830
|
+
if (!canonical) {
|
|
9831
|
+
return false;
|
|
9832
|
+
}
|
|
9584
9833
|
let changed = false;
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9834
|
+
const candidateMessages = await readHermesLineageMessages(input.candidate);
|
|
9835
|
+
const nextCanonical = await mergeHermesCandidateIntoConversation({
|
|
9836
|
+
...input,
|
|
9837
|
+
existing: canonical,
|
|
9838
|
+
candidateMessages
|
|
9839
|
+
});
|
|
9840
|
+
changed = nextCanonical || changed;
|
|
9841
|
+
for (const duplicate of active) {
|
|
9842
|
+
if (duplicate.conversationId === canonical.conversationId) {
|
|
9592
9843
|
continue;
|
|
9593
9844
|
}
|
|
9594
|
-
|
|
9595
|
-
if (!prefix?.needsUpgrade || prefix.messages.length === 0) {
|
|
9845
|
+
if (!isSafeHermesImportConversation(duplicate) || duplicate.manifest.status !== "active") {
|
|
9596
9846
|
continue;
|
|
9597
9847
|
}
|
|
9598
|
-
|
|
9599
|
-
input.paths,
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9848
|
+
await softDeleteMergedHermesDuplicate({
|
|
9849
|
+
paths: input.paths,
|
|
9850
|
+
store: input.store,
|
|
9851
|
+
duplicate,
|
|
9852
|
+
candidate: input.candidate
|
|
9853
|
+
});
|
|
9854
|
+
changed = true;
|
|
9855
|
+
}
|
|
9856
|
+
return changed;
|
|
9857
|
+
}
|
|
9858
|
+
async function mergeHermesCandidateIntoConversation(input) {
|
|
9859
|
+
const { candidate, existing } = input;
|
|
9860
|
+
const profile = await resolveConversationProfileTarget(
|
|
9861
|
+
input.paths,
|
|
9862
|
+
existing.manifest.profile_name_snapshot ?? existing.manifest.profile ?? candidate.profileName
|
|
9863
|
+
);
|
|
9864
|
+
let nextSnapshot = existing.snapshot;
|
|
9865
|
+
const pureImport = isSafeHermesImportConversation(existing);
|
|
9866
|
+
const prefix = collectImportedHermesPrefix(existing.snapshot);
|
|
9867
|
+
if (pureImport || prefix?.needsUpgrade && prefix.messages.length > 0) {
|
|
9868
|
+
const messages = pureImport ? input.candidateMessages : prefix.messages;
|
|
9869
|
+
nextSnapshot = {
|
|
9870
|
+
...existing.snapshot,
|
|
9604
9871
|
messages: [
|
|
9605
9872
|
...toLinkMessages({
|
|
9606
|
-
conversationId,
|
|
9873
|
+
conversationId: existing.conversationId,
|
|
9607
9874
|
profileName: profile.profileName,
|
|
9608
9875
|
profileUid: profile.profileUid,
|
|
9609
9876
|
profileDisplayName: profile.profileDisplayName,
|
|
9610
|
-
sessionId:
|
|
9611
|
-
messages
|
|
9877
|
+
sessionId: candidate.session.id,
|
|
9878
|
+
messages
|
|
9612
9879
|
}),
|
|
9613
|
-
...snapshot.messages.slice(prefix.endIndex)
|
|
9880
|
+
...pureImport ? [] : existing.snapshot.messages.slice(prefix.endIndex)
|
|
9614
9881
|
]
|
|
9615
9882
|
};
|
|
9616
|
-
await input.store.writeSnapshot(conversationId, nextSnapshot);
|
|
9883
|
+
await input.store.writeSnapshot(existing.conversationId, nextSnapshot);
|
|
9617
9884
|
await hydrateImportedConversationMedia({
|
|
9618
9885
|
paths: input.paths,
|
|
9619
9886
|
store: input.store,
|
|
9620
9887
|
logger: input.logger,
|
|
9621
|
-
conversationId
|
|
9888
|
+
conversationId: existing.conversationId
|
|
9622
9889
|
});
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9628
|
-
|
|
9629
|
-
|
|
9630
|
-
|
|
9890
|
+
nextSnapshot = await input.store.readSnapshot(existing.conversationId);
|
|
9891
|
+
}
|
|
9892
|
+
const updatedAt = isoFromHermesTime(candidate.session.last_active) ?? existing.manifest.updated_at;
|
|
9893
|
+
const nextManifest = mergeHermesLineageIntoManifest({
|
|
9894
|
+
manifest: existing.manifest,
|
|
9895
|
+
candidate,
|
|
9896
|
+
snapshot: nextSnapshot,
|
|
9897
|
+
profileUid: profile.profileUid,
|
|
9898
|
+
profileName: profile.profileName,
|
|
9899
|
+
updatedAt
|
|
9900
|
+
});
|
|
9901
|
+
if (manifestEquivalent(existing.manifest, nextManifest)) {
|
|
9902
|
+
return nextSnapshot !== existing.snapshot;
|
|
9631
9903
|
}
|
|
9632
|
-
|
|
9904
|
+
await input.store.writeManifest(nextManifest);
|
|
9905
|
+
await upsertConversationStats(
|
|
9906
|
+
input.paths,
|
|
9907
|
+
toStatsIndexRecord(nextManifest, nextManifest.stats)
|
|
9908
|
+
);
|
|
9909
|
+
return true;
|
|
9910
|
+
}
|
|
9911
|
+
function findKnownConversationIdsForCandidate(known, candidate) {
|
|
9912
|
+
const conversationIds = [];
|
|
9913
|
+
for (const sessionId of lineageSessionIds(candidate)) {
|
|
9914
|
+
for (const conversationId of known.conversationIdsBySessionId.get(sessionId) ?? []) {
|
|
9915
|
+
if (!conversationIds.includes(conversationId)) {
|
|
9916
|
+
conversationIds.push(conversationId);
|
|
9917
|
+
}
|
|
9918
|
+
}
|
|
9919
|
+
}
|
|
9920
|
+
return conversationIds;
|
|
9921
|
+
}
|
|
9922
|
+
function lineageSessionIds(candidate) {
|
|
9923
|
+
return normalizeSessionIds([
|
|
9924
|
+
candidate.session._lineage_root_id,
|
|
9925
|
+
...candidate.session._lineage_session_ids ?? [],
|
|
9926
|
+
candidate.session.id
|
|
9927
|
+
]);
|
|
9928
|
+
}
|
|
9929
|
+
function lineageTitle(candidate) {
|
|
9930
|
+
return candidate.session._lineage_title ?? stripCompressionTitleSuffix2(readString9(candidate.session, "title") ?? "");
|
|
9931
|
+
}
|
|
9932
|
+
function lineageManifestPatch(candidate) {
|
|
9933
|
+
const sessionIds = lineageSessionIds(candidate);
|
|
9934
|
+
const rootSessionId = candidate.session._lineage_root_id ?? sessionIds[0];
|
|
9935
|
+
const currentSessionId = candidate.session.id;
|
|
9936
|
+
if (!rootSessionId || sessionIds.length <= 1 || rootSessionId === currentSessionId) {
|
|
9937
|
+
return {};
|
|
9938
|
+
}
|
|
9939
|
+
return {
|
|
9940
|
+
hermes_lineage: {
|
|
9941
|
+
kind: "compression",
|
|
9942
|
+
root_session_id: rootSessionId,
|
|
9943
|
+
current_session_id: currentSessionId,
|
|
9944
|
+
session_ids: sessionIds
|
|
9945
|
+
}
|
|
9946
|
+
};
|
|
9947
|
+
}
|
|
9948
|
+
function buildImportedHermesStats(input) {
|
|
9949
|
+
const usage = input.candidate.session._lineage_usage;
|
|
9950
|
+
const base = input.baseStats;
|
|
9951
|
+
const usageInput = usage?.input_tokens ?? 0;
|
|
9952
|
+
const usageOutput = usage?.output_tokens ?? 0;
|
|
9953
|
+
const usageTotal = usage?.total_tokens ?? usageInput + usageOutput;
|
|
9954
|
+
const inputTokens = Math.max(base?.input_tokens ?? 0, usageInput);
|
|
9955
|
+
const outputTokens = Math.max(base?.output_tokens ?? 0, usageOutput);
|
|
9956
|
+
const totalTokens = Math.max(
|
|
9957
|
+
base?.total_tokens ?? 0,
|
|
9958
|
+
usageTotal,
|
|
9959
|
+
inputTokens + outputTokens
|
|
9960
|
+
);
|
|
9961
|
+
const updatedAt = base?.updated_at ? latestTimestamp(base.updated_at, input.updatedAt) : input.updatedAt;
|
|
9962
|
+
return {
|
|
9963
|
+
input_tokens: inputTokens,
|
|
9964
|
+
output_tokens: outputTokens,
|
|
9965
|
+
total_tokens: totalTokens,
|
|
9966
|
+
message_count: Math.max(
|
|
9967
|
+
base?.message_count ?? 0,
|
|
9968
|
+
input.snapshot.messages.length
|
|
9969
|
+
),
|
|
9970
|
+
run_count: Math.max(base?.run_count ?? 0, usage?.api_call_count ?? 0),
|
|
9971
|
+
profile_uid: base?.profile_uid ?? input.profileUid,
|
|
9972
|
+
profile_name_snapshot: base?.profile_name_snapshot ?? base?.profile ?? input.profileName,
|
|
9973
|
+
profile: base?.profile ?? base?.profile_name_snapshot ?? input.profileName,
|
|
9974
|
+
...base?.model ?? usage?.model ? { model: base?.model ?? usage?.model } : {},
|
|
9975
|
+
...base?.provider ?? usage?.provider ? { provider: base?.provider ?? usage?.provider } : {},
|
|
9976
|
+
...base?.context_window ? { context_window: base.context_window } : {},
|
|
9977
|
+
updated_at: updatedAt
|
|
9978
|
+
};
|
|
9979
|
+
}
|
|
9980
|
+
async function readExistingHermesConversations(store, conversationIds) {
|
|
9981
|
+
const conversations = [];
|
|
9982
|
+
for (const conversationId of [...new Set(conversationIds)]) {
|
|
9983
|
+
const manifest = await store.readManifest(conversationId).catch(() => null);
|
|
9984
|
+
if (!manifest) {
|
|
9985
|
+
continue;
|
|
9986
|
+
}
|
|
9987
|
+
const snapshot = await store.readSnapshot(conversationId).catch(() => ({
|
|
9988
|
+
schema_version: 1,
|
|
9989
|
+
messages: [],
|
|
9990
|
+
runs: []
|
|
9991
|
+
}));
|
|
9992
|
+
conversations.push({ conversationId, manifest, snapshot });
|
|
9993
|
+
}
|
|
9994
|
+
return conversations;
|
|
9995
|
+
}
|
|
9996
|
+
function selectCanonicalHermesConversation(conversations) {
|
|
9997
|
+
const ranked = [...conversations].sort((left, right) => {
|
|
9998
|
+
const rightScore = canonicalConversationScore(right);
|
|
9999
|
+
const leftScore = canonicalConversationScore(left);
|
|
10000
|
+
if (rightScore !== leftScore) {
|
|
10001
|
+
return rightScore - leftScore;
|
|
10002
|
+
}
|
|
10003
|
+
return Date.parse(left.manifest.created_at) - Date.parse(right.manifest.created_at) || left.conversationId.localeCompare(right.conversationId);
|
|
10004
|
+
});
|
|
10005
|
+
return ranked[0] ?? null;
|
|
10006
|
+
}
|
|
10007
|
+
function canonicalConversationScore(item) {
|
|
10008
|
+
let score = 0;
|
|
10009
|
+
if (!isSafeHermesImportConversation(item)) {
|
|
10010
|
+
score += 1e3;
|
|
10011
|
+
}
|
|
10012
|
+
if (item.snapshot.runs.length > 0) {
|
|
10013
|
+
score += 500;
|
|
10014
|
+
}
|
|
10015
|
+
if (item.manifest.title_source === "manual") {
|
|
10016
|
+
score += 200;
|
|
10017
|
+
}
|
|
10018
|
+
if (!hasCompressionTitleSuffix(item.manifest.title)) {
|
|
10019
|
+
score += 100;
|
|
10020
|
+
}
|
|
10021
|
+
if (item.manifest.hermes_lineage?.root_session_id && item.manifest.hermes_session_id === item.manifest.hermes_lineage.root_session_id) {
|
|
10022
|
+
score += 50;
|
|
10023
|
+
}
|
|
10024
|
+
return score;
|
|
10025
|
+
}
|
|
10026
|
+
function isSafeHermesImportConversation(item) {
|
|
10027
|
+
const snapshot = item.snapshot;
|
|
10028
|
+
if (snapshot.runs.length > 0) {
|
|
10029
|
+
return false;
|
|
10030
|
+
}
|
|
10031
|
+
if (item.manifest.title_source === "manual" || item.manifest.title_source === "generated") {
|
|
10032
|
+
return false;
|
|
10033
|
+
}
|
|
10034
|
+
return snapshot.messages.length === 0 || snapshot.messages.every((message) => isHermesImportedMessage(message));
|
|
10035
|
+
}
|
|
10036
|
+
async function softDeleteMergedHermesDuplicate(input) {
|
|
10037
|
+
const deletedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10038
|
+
const emptySnapshot3 = {
|
|
10039
|
+
schema_version: 1,
|
|
10040
|
+
messages: [],
|
|
10041
|
+
runs: []
|
|
10042
|
+
};
|
|
10043
|
+
const hermesSessionIds = normalizeSessionIds([
|
|
10044
|
+
input.duplicate.manifest.hermes_session_id,
|
|
10045
|
+
...input.duplicate.manifest.hermes_session_ids ?? [],
|
|
10046
|
+
...input.duplicate.manifest.hermes_lineage?.session_ids ?? [],
|
|
10047
|
+
...lineageSessionIds(input.candidate)
|
|
10048
|
+
]);
|
|
10049
|
+
const nextManifest = {
|
|
10050
|
+
...input.duplicate.manifest,
|
|
10051
|
+
status: "deleted_soft",
|
|
10052
|
+
hermes_session_ids: hermesSessionIds,
|
|
10053
|
+
...lineageManifestPatch(input.candidate),
|
|
10054
|
+
updated_at: deletedAt,
|
|
10055
|
+
deleted_at: deletedAt
|
|
10056
|
+
};
|
|
10057
|
+
const stats = buildConversationStats(
|
|
10058
|
+
{ ...nextManifest, stats: input.duplicate.manifest.stats },
|
|
10059
|
+
emptySnapshot3
|
|
10060
|
+
);
|
|
10061
|
+
const tombstone = { ...nextManifest, stats };
|
|
10062
|
+
await input.store.writeManifest(tombstone);
|
|
10063
|
+
await input.store.writeSnapshot(input.duplicate.conversationId, emptySnapshot3);
|
|
10064
|
+
await upsertConversationStats(input.paths, toStatsIndexRecord(tombstone, stats));
|
|
10065
|
+
}
|
|
10066
|
+
function mergeHermesLineageIntoManifest(input) {
|
|
10067
|
+
const hermesSessionIds = normalizeSessionIds([
|
|
10068
|
+
input.manifest.hermes_session_id,
|
|
10069
|
+
...input.manifest.hermes_session_ids ?? [],
|
|
10070
|
+
...input.manifest.hermes_lineage?.session_ids ?? [],
|
|
10071
|
+
...lineageSessionIds(input.candidate)
|
|
10072
|
+
]);
|
|
10073
|
+
const nextBase = {
|
|
10074
|
+
...input.manifest,
|
|
10075
|
+
hermes_session_id: input.candidate.session.id,
|
|
10076
|
+
hermes_session_ids: hermesSessionIds,
|
|
10077
|
+
...lineageManifestPatch(input.candidate),
|
|
10078
|
+
profile_uid: input.manifest.profile_uid ?? input.profileUid,
|
|
10079
|
+
profile_name_snapshot: input.manifest.profile_name_snapshot ?? input.manifest.profile ?? input.profileName,
|
|
10080
|
+
profile: input.manifest.profile ?? input.manifest.profile_name_snapshot ?? input.profileName,
|
|
10081
|
+
updated_at: latestTimestamp(input.manifest.updated_at, input.updatedAt)
|
|
10082
|
+
};
|
|
10083
|
+
const title = lineageTitle(input.candidate);
|
|
10084
|
+
if (title && canSyncHermesTitle(input.manifest)) {
|
|
10085
|
+
nextBase.title = normalizeTitle(title);
|
|
10086
|
+
nextBase.title_source = "hermes";
|
|
10087
|
+
}
|
|
10088
|
+
const baseStats = buildConversationStats(nextBase, input.snapshot);
|
|
10089
|
+
return {
|
|
10090
|
+
...nextBase,
|
|
10091
|
+
stats: buildImportedHermesStats({
|
|
10092
|
+
candidate: input.candidate,
|
|
10093
|
+
snapshot: input.snapshot,
|
|
10094
|
+
profileUid: input.profileUid,
|
|
10095
|
+
profileName: input.profileName,
|
|
10096
|
+
updatedAt: input.updatedAt,
|
|
10097
|
+
baseStats
|
|
10098
|
+
})
|
|
10099
|
+
};
|
|
10100
|
+
}
|
|
10101
|
+
function manifestEquivalent(left, right) {
|
|
10102
|
+
return stableJson(left) === stableJson(right);
|
|
9633
10103
|
}
|
|
9634
10104
|
function collectImportedHermesPrefix(snapshot) {
|
|
9635
10105
|
const rows = [];
|
|
@@ -10103,7 +10573,7 @@ function latestTimestamp(left, right) {
|
|
|
10103
10573
|
return leftTime >= rightTime ? left : right;
|
|
10104
10574
|
}
|
|
10105
10575
|
async function readKnownHermesSessions(store) {
|
|
10106
|
-
const
|
|
10576
|
+
const sessionIds = /* @__PURE__ */ new Set();
|
|
10107
10577
|
const conversationIdsBySessionId = /* @__PURE__ */ new Map();
|
|
10108
10578
|
for (const conversationId of await store.listConversationIds()) {
|
|
10109
10579
|
const manifest = await store.readManifest(conversationId).catch(() => null);
|
|
@@ -10111,7 +10581,7 @@ async function readKnownHermesSessions(store) {
|
|
|
10111
10581
|
continue;
|
|
10112
10582
|
}
|
|
10113
10583
|
if (manifest.hermes_session_id) {
|
|
10114
|
-
|
|
10584
|
+
sessionIds.add(manifest.hermes_session_id);
|
|
10115
10585
|
rememberKnownHermesConversation(
|
|
10116
10586
|
conversationIdsBySessionId,
|
|
10117
10587
|
manifest.hermes_session_id,
|
|
@@ -10119,7 +10589,22 @@ async function readKnownHermesSessions(store) {
|
|
|
10119
10589
|
);
|
|
10120
10590
|
}
|
|
10121
10591
|
for (const sessionId of manifest.hermes_session_ids ?? []) {
|
|
10122
|
-
|
|
10592
|
+
sessionIds.add(sessionId);
|
|
10593
|
+
rememberKnownHermesConversation(
|
|
10594
|
+
conversationIdsBySessionId,
|
|
10595
|
+
sessionId,
|
|
10596
|
+
conversationId
|
|
10597
|
+
);
|
|
10598
|
+
}
|
|
10599
|
+
for (const sessionId of [
|
|
10600
|
+
manifest.hermes_lineage?.root_session_id,
|
|
10601
|
+
manifest.hermes_lineage?.current_session_id,
|
|
10602
|
+
...manifest.hermes_lineage?.session_ids ?? []
|
|
10603
|
+
]) {
|
|
10604
|
+
if (!sessionId) {
|
|
10605
|
+
continue;
|
|
10606
|
+
}
|
|
10607
|
+
sessionIds.add(sessionId);
|
|
10123
10608
|
rememberKnownHermesConversation(
|
|
10124
10609
|
conversationIdsBySessionId,
|
|
10125
10610
|
sessionId,
|
|
@@ -10127,7 +10612,7 @@ async function readKnownHermesSessions(store) {
|
|
|
10127
10612
|
);
|
|
10128
10613
|
}
|
|
10129
10614
|
}
|
|
10130
|
-
return {
|
|
10615
|
+
return { sessionIds, conversationIdsBySessionId };
|
|
10131
10616
|
}
|
|
10132
10617
|
function rememberKnownHermesConversation(map, sessionId, conversationId) {
|
|
10133
10618
|
const current = map.get(sessionId) ?? [];
|
|
@@ -10137,7 +10622,7 @@ function rememberKnownHermesConversation(map, sessionId, conversationId) {
|
|
|
10137
10622
|
}
|
|
10138
10623
|
async function discoverHermesProfileNames() {
|
|
10139
10624
|
const names = /* @__PURE__ */ new Set([DEFAULT_PROFILE_NAME]);
|
|
10140
|
-
const profilesDir =
|
|
10625
|
+
const profilesDir = resolveHermesProfilesDir();
|
|
10141
10626
|
const entries = await readdir7(profilesDir, { withFileTypes: true }).catch(
|
|
10142
10627
|
(error) => {
|
|
10143
10628
|
if (isNodeError11(error, "ENOENT")) {
|
|
@@ -10245,14 +10730,12 @@ function joinImportedText(left, right) {
|
|
|
10245
10730
|
${right}`;
|
|
10246
10731
|
}
|
|
10247
10732
|
function projectCompressionTips(rows) {
|
|
10248
|
-
const byId = /* @__PURE__ */ new Map();
|
|
10249
10733
|
const childrenByParent = /* @__PURE__ */ new Map();
|
|
10250
10734
|
for (const row of rows) {
|
|
10251
10735
|
const id = readString9(row, "id");
|
|
10252
10736
|
if (!id) {
|
|
10253
10737
|
continue;
|
|
10254
10738
|
}
|
|
10255
|
-
byId.set(id, row);
|
|
10256
10739
|
const parentId = readString9(row, "parent_session_id");
|
|
10257
10740
|
if (parentId) {
|
|
10258
10741
|
const children = childrenByParent.get(parentId) ?? [];
|
|
@@ -10268,13 +10751,14 @@ function projectCompressionTips(rows) {
|
|
|
10268
10751
|
}
|
|
10269
10752
|
let tip = row;
|
|
10270
10753
|
const visited = /* @__PURE__ */ new Set([id]);
|
|
10754
|
+
const chain = [row];
|
|
10271
10755
|
while (readString9(tip, "end_reason") === "compression") {
|
|
10272
10756
|
const tipId2 = readString9(tip, "id");
|
|
10273
10757
|
if (!tipId2) {
|
|
10274
10758
|
break;
|
|
10275
10759
|
}
|
|
10276
|
-
const next = (childrenByParent.get(tipId2) ?? []).filter((child) =>
|
|
10277
|
-
(left, right) => (readNumber2(right.last_active) ?? 0) - (readNumber2(left.last_active) ?? 0)
|
|
10760
|
+
const next = (childrenByParent.get(tipId2) ?? []).filter((child) => isCompressionContinuation(tip, child)).sort(
|
|
10761
|
+
(left, right) => (readNumber2(right.started_at) ?? 0) - (readNumber2(left.started_at) ?? 0) || (readNumber2(right.last_active) ?? 0) - (readNumber2(left.last_active) ?? 0)
|
|
10278
10762
|
)[0];
|
|
10279
10763
|
const nextId = next ? readString9(next, "id") : null;
|
|
10280
10764
|
if (!next || !nextId || visited.has(nextId)) {
|
|
@@ -10282,25 +10766,75 @@ function projectCompressionTips(rows) {
|
|
|
10282
10766
|
}
|
|
10283
10767
|
tip = next;
|
|
10284
10768
|
visited.add(nextId);
|
|
10769
|
+
chain.push(next);
|
|
10285
10770
|
}
|
|
10286
10771
|
const tipId = readString9(tip, "id");
|
|
10287
10772
|
if (tipId) {
|
|
10773
|
+
const sessionIds = chain.map((item) => readString9(item, "id")).filter((item) => Boolean(item));
|
|
10288
10774
|
projected.push({
|
|
10289
10775
|
...tip,
|
|
10290
10776
|
id: tipId,
|
|
10291
10777
|
_lineage_root_id: id,
|
|
10778
|
+
_lineage_session_ids: sessionIds,
|
|
10779
|
+
_lineage_title: readString9(row, "title") ?? stripCompressionTitleSuffix2(readString9(tip, "title") ?? ""),
|
|
10780
|
+
_lineage_usage: aggregateHermesSessionUsage(chain),
|
|
10292
10781
|
started_at: readNumber2(row.started_at) ?? readNumber2(tip.started_at)
|
|
10293
10782
|
});
|
|
10294
10783
|
}
|
|
10295
10784
|
}
|
|
10296
10785
|
return projected;
|
|
10297
10786
|
}
|
|
10787
|
+
function isCompressionContinuation(parent, child) {
|
|
10788
|
+
const childId = readString9(child, "id");
|
|
10789
|
+
const parentEndedAt = readNumber2(parent.ended_at);
|
|
10790
|
+
const childStartedAt = readNumber2(child.started_at);
|
|
10791
|
+
return Boolean(childId) && readString9(parent, "end_reason") === "compression" && parentEndedAt !== null && childStartedAt !== null && childStartedAt >= parentEndedAt;
|
|
10792
|
+
}
|
|
10793
|
+
function aggregateHermesSessionUsage(rows) {
|
|
10794
|
+
let inputTokens = 0;
|
|
10795
|
+
let outputTokens = 0;
|
|
10796
|
+
let apiCallCount = 0;
|
|
10797
|
+
let model;
|
|
10798
|
+
let provider;
|
|
10799
|
+
for (const row of rows) {
|
|
10800
|
+
inputTokens += readNumber2(row.input_tokens) ?? 0;
|
|
10801
|
+
outputTokens += readNumber2(row.output_tokens) ?? 0;
|
|
10802
|
+
apiCallCount += readNumber2(row.api_call_count) ?? 0;
|
|
10803
|
+
model = readString9(row, "model") ?? model;
|
|
10804
|
+
provider = readString9(row, "billing_provider") ?? readString9(row, "provider") ?? provider;
|
|
10805
|
+
}
|
|
10806
|
+
return {
|
|
10807
|
+
input_tokens: inputTokens,
|
|
10808
|
+
output_tokens: outputTokens,
|
|
10809
|
+
total_tokens: inputTokens + outputTokens,
|
|
10810
|
+
api_call_count: apiCallCount,
|
|
10811
|
+
...model ? { model } : {},
|
|
10812
|
+
...provider ? { provider } : {}
|
|
10813
|
+
};
|
|
10814
|
+
}
|
|
10815
|
+
async function readHermesLineageMessages(candidate) {
|
|
10816
|
+
const rows = [];
|
|
10817
|
+
for (const sessionId of lineageSessionIds(candidate)) {
|
|
10818
|
+
const messages = await readHermesSessionMessages({
|
|
10819
|
+
...candidate,
|
|
10820
|
+
session: {
|
|
10821
|
+
...candidate.session,
|
|
10822
|
+
id: sessionId
|
|
10823
|
+
}
|
|
10824
|
+
});
|
|
10825
|
+
rows.push(...messages);
|
|
10826
|
+
}
|
|
10827
|
+
return rows;
|
|
10828
|
+
}
|
|
10298
10829
|
async function readHermesSessionMessages(candidate) {
|
|
10299
10830
|
const [dbMessages, jsonlMessages] = await Promise.all([
|
|
10300
10831
|
readStateDbMessages(candidate.dbPath, candidate.session.id),
|
|
10301
10832
|
readJsonlMessages(candidate.profileName, candidate.session.id)
|
|
10302
10833
|
]);
|
|
10303
|
-
|
|
10834
|
+
const selected = jsonlMessages.length > dbMessages.length ? jsonlMessages : dbMessages;
|
|
10835
|
+
return selected.map(
|
|
10836
|
+
(message) => readString9(message, "session_id") ? message : { ...message, session_id: candidate.session.id }
|
|
10837
|
+
);
|
|
10304
10838
|
}
|
|
10305
10839
|
async function readStateDbMessages(dbPath, sessionId) {
|
|
10306
10840
|
if (!await isFile(dbPath)) {
|
|
@@ -10338,8 +10872,8 @@ async function readJsonlMessages(profileName, sessionId) {
|
|
|
10338
10872
|
return [];
|
|
10339
10873
|
}
|
|
10340
10874
|
const profileDir = resolveHermesProfileDir(profileName);
|
|
10341
|
-
const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() =>
|
|
10342
|
-
const transcriptPath =
|
|
10875
|
+
const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path15.join(profileDir, "sessions"));
|
|
10876
|
+
const transcriptPath = path15.join(sessionsDir, `${sessionId}.jsonl`);
|
|
10343
10877
|
const raw = await readFile9(transcriptPath, "utf8").catch((error) => {
|
|
10344
10878
|
if (isNodeError11(error, "ENOENT")) {
|
|
10345
10879
|
return "";
|
|
@@ -10491,9 +11025,10 @@ function formatImportedFilenameList(filenames) {
|
|
|
10491
11025
|
function toLinkMessage(input) {
|
|
10492
11026
|
const role = normalizeMessageRole(input.message.role);
|
|
10493
11027
|
const text = normalizeContent(input.message.content);
|
|
11028
|
+
const sessionId = readString9(input.message, "session_id") ?? input.sessionId;
|
|
10494
11029
|
const createdAt = isoFromHermesTime(input.message.timestamp) ?? new Date(Date.now() + input.index).toISOString();
|
|
10495
11030
|
return {
|
|
10496
|
-
id: `msg_${
|
|
11031
|
+
id: `msg_${randomUUID7().replaceAll("-", "")}`,
|
|
10497
11032
|
schema_version: 1,
|
|
10498
11033
|
conversation_id: input.conversationId,
|
|
10499
11034
|
role,
|
|
@@ -10509,7 +11044,7 @@ function toLinkMessage(input) {
|
|
|
10509
11044
|
parts: text ? [{ type: "text", text }] : [],
|
|
10510
11045
|
attachments: [],
|
|
10511
11046
|
hermes: {
|
|
10512
|
-
session_id:
|
|
11047
|
+
session_id: sessionId,
|
|
10513
11048
|
message_id: input.message.id,
|
|
10514
11049
|
imported_from: "hermes",
|
|
10515
11050
|
import_projection: HERMES_IMPORT_PROJECTION_VERSION
|
|
@@ -10545,6 +11080,47 @@ function normalizeTitle(value) {
|
|
|
10545
11080
|
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
10546
11081
|
return normalized || DEFAULT_CONVERSATION_TITLE;
|
|
10547
11082
|
}
|
|
11083
|
+
function canSyncHermesTitle(manifest) {
|
|
11084
|
+
return manifest.title_source !== "manual" && manifest.title_source !== "generated" && manifest.title_source !== "temporary_user_message" && manifest.title_source !== "temporary_fallback";
|
|
11085
|
+
}
|
|
11086
|
+
function stripCompressionTitleSuffix2(value) {
|
|
11087
|
+
const normalized = value.replace(/\s+/gu, " ").trim();
|
|
11088
|
+
if (!normalized) {
|
|
11089
|
+
return void 0;
|
|
11090
|
+
}
|
|
11091
|
+
const match = /^(.*?) #\d+$/u.exec(normalized);
|
|
11092
|
+
const stripped = match?.[1]?.trim();
|
|
11093
|
+
return stripped || normalized;
|
|
11094
|
+
}
|
|
11095
|
+
function hasCompressionTitleSuffix(value) {
|
|
11096
|
+
return / #\d+$/u.test(value.trim());
|
|
11097
|
+
}
|
|
11098
|
+
function normalizeSessionIds(values) {
|
|
11099
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11100
|
+
for (const value of values) {
|
|
11101
|
+
const sessionId = value?.trim();
|
|
11102
|
+
if (!sessionId || seen.has(sessionId)) {
|
|
11103
|
+
continue;
|
|
11104
|
+
}
|
|
11105
|
+
seen.add(sessionId);
|
|
11106
|
+
}
|
|
11107
|
+
return [...seen];
|
|
11108
|
+
}
|
|
11109
|
+
function stableJson(value) {
|
|
11110
|
+
return JSON.stringify(stabilizeJsonValue(value));
|
|
11111
|
+
}
|
|
11112
|
+
function stabilizeJsonValue(value) {
|
|
11113
|
+
if (Array.isArray(value)) {
|
|
11114
|
+
return value.map((item) => stabilizeJsonValue(item));
|
|
11115
|
+
}
|
|
11116
|
+
if (!value || typeof value !== "object") {
|
|
11117
|
+
return value;
|
|
11118
|
+
}
|
|
11119
|
+
const record = value;
|
|
11120
|
+
return Object.fromEntries(
|
|
11121
|
+
Object.keys(record).sort().map((key) => [key, stabilizeJsonValue(record[key])])
|
|
11122
|
+
);
|
|
11123
|
+
}
|
|
10548
11124
|
function normalizeMessageRole(value) {
|
|
10549
11125
|
switch (value?.trim().toLowerCase()) {
|
|
10550
11126
|
case "user":
|
|
@@ -10624,7 +11200,7 @@ async function isFile(filePath) {
|
|
|
10624
11200
|
});
|
|
10625
11201
|
}
|
|
10626
11202
|
function createConversationId() {
|
|
10627
|
-
return `conv_${
|
|
11203
|
+
return `conv_${randomUUID7().replaceAll("-", "")}`;
|
|
10628
11204
|
}
|
|
10629
11205
|
function isoFromHermesTime(value) {
|
|
10630
11206
|
const numeric = readNumber2(value);
|
|
@@ -10891,10 +11467,10 @@ async function cancelHermesRun(runId, options = {}) {
|
|
|
10891
11467
|
);
|
|
10892
11468
|
}
|
|
10893
11469
|
}
|
|
10894
|
-
async function callHermesApi(
|
|
11470
|
+
async function callHermesApi(path26, init, options) {
|
|
10895
11471
|
const method = init.method ?? "GET";
|
|
10896
11472
|
const startedAt = Date.now();
|
|
10897
|
-
void options.logger?.debug("hermes_api_request_started", { method, path:
|
|
11473
|
+
void options.logger?.debug("hermes_api_request_started", { method, path: path26 });
|
|
10898
11474
|
const availability = await ensureHermesApiServerAvailable({
|
|
10899
11475
|
fetchImpl: options.fetchImpl,
|
|
10900
11476
|
logger: options.logger,
|
|
@@ -10902,21 +11478,21 @@ async function callHermesApi(path25, init, options) {
|
|
|
10902
11478
|
});
|
|
10903
11479
|
let config = availability.configResult.apiServer;
|
|
10904
11480
|
const fetcher = options.fetchImpl ?? fetch;
|
|
10905
|
-
const request = () => fetchHermesApi(fetcher, config,
|
|
11481
|
+
const request = () => fetchHermesApi(fetcher, config, path26, init, options);
|
|
10906
11482
|
let response;
|
|
10907
11483
|
try {
|
|
10908
11484
|
response = await request();
|
|
10909
11485
|
} catch (error) {
|
|
10910
|
-
logHermesApiError(options.logger, method,
|
|
11486
|
+
logHermesApiError(options.logger, method, path26, startedAt, error);
|
|
10911
11487
|
throw error;
|
|
10912
11488
|
}
|
|
10913
11489
|
if (response.status !== 401) {
|
|
10914
|
-
logHermesApiResponse(options.logger, method,
|
|
11490
|
+
logHermesApiResponse(options.logger, method, path26, startedAt, response);
|
|
10915
11491
|
return response;
|
|
10916
11492
|
}
|
|
10917
11493
|
void options.logger?.warn("hermes_api_request_retrying_after_401", {
|
|
10918
11494
|
method,
|
|
10919
|
-
path:
|
|
11495
|
+
path: path26,
|
|
10920
11496
|
duration_ms: Date.now() - startedAt
|
|
10921
11497
|
});
|
|
10922
11498
|
const refreshedAvailability = await ensureHermesApiServerAvailable({
|
|
@@ -10929,20 +11505,20 @@ async function callHermesApi(path25, init, options) {
|
|
|
10929
11505
|
try {
|
|
10930
11506
|
response = await request();
|
|
10931
11507
|
} catch (error) {
|
|
10932
|
-
logHermesApiError(options.logger, method,
|
|
11508
|
+
logHermesApiError(options.logger, method, path26, startedAt, error);
|
|
10933
11509
|
throw error;
|
|
10934
11510
|
}
|
|
10935
|
-
logHermesApiResponse(options.logger, method,
|
|
11511
|
+
logHermesApiResponse(options.logger, method, path26, startedAt, response);
|
|
10936
11512
|
return response;
|
|
10937
11513
|
}
|
|
10938
|
-
async function fetchHermesApi(fetcher, config,
|
|
11514
|
+
async function fetchHermesApi(fetcher, config, path26, init, options) {
|
|
10939
11515
|
const headers = new Headers(init.headers);
|
|
10940
11516
|
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
10941
11517
|
if (config.key) {
|
|
10942
11518
|
headers.set("x-api-key", config.key);
|
|
10943
11519
|
headers.set("authorization", `Bearer ${config.key}`);
|
|
10944
11520
|
}
|
|
10945
|
-
return await fetcher(`http://127.0.0.1:${config.port}${
|
|
11521
|
+
return await fetcher(`http://127.0.0.1:${config.port}${path26}`, {
|
|
10946
11522
|
...init,
|
|
10947
11523
|
headers
|
|
10948
11524
|
}).catch((error) => {
|
|
@@ -10950,7 +11526,7 @@ async function fetchHermesApi(fetcher, config, path25, init, options) {
|
|
|
10950
11526
|
throw error;
|
|
10951
11527
|
}
|
|
10952
11528
|
void options.logger?.warn("hermes_api_server_connect_failed", {
|
|
10953
|
-
path:
|
|
11529
|
+
path: path26,
|
|
10954
11530
|
port: config.port ?? null,
|
|
10955
11531
|
error: error instanceof Error ? error.message : String(error)
|
|
10956
11532
|
});
|
|
@@ -10961,10 +11537,10 @@ async function fetchHermesApi(fetcher, config, path25, init, options) {
|
|
|
10961
11537
|
);
|
|
10962
11538
|
});
|
|
10963
11539
|
}
|
|
10964
|
-
function logHermesApiResponse(logger, method,
|
|
11540
|
+
function logHermesApiResponse(logger, method, path26, startedAt, response) {
|
|
10965
11541
|
const fields = {
|
|
10966
11542
|
method,
|
|
10967
|
-
path:
|
|
11543
|
+
path: path26,
|
|
10968
11544
|
status: response.status,
|
|
10969
11545
|
duration_ms: Date.now() - startedAt
|
|
10970
11546
|
};
|
|
@@ -10984,10 +11560,10 @@ async function logHermesApiFailureResponse(logger, fields, response) {
|
|
|
10984
11560
|
...upstreamError ? { upstream_error: upstreamError } : {}
|
|
10985
11561
|
});
|
|
10986
11562
|
}
|
|
10987
|
-
function logHermesApiError(logger, method,
|
|
11563
|
+
function logHermesApiError(logger, method, path26, startedAt, error) {
|
|
10988
11564
|
void logger?.warn("hermes_api_request_failed", {
|
|
10989
11565
|
method,
|
|
10990
|
-
path:
|
|
11566
|
+
path: path26,
|
|
10991
11567
|
duration_ms: Date.now() - startedAt,
|
|
10992
11568
|
...error instanceof LinkHttpError ? { status: error.status, code: error.code } : {},
|
|
10993
11569
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -11050,7 +11626,7 @@ function readString10(payload, key) {
|
|
|
11050
11626
|
|
|
11051
11627
|
// src/conversations/history-builder.ts
|
|
11052
11628
|
import { readFile as readFile10, stat as stat10 } from "fs/promises";
|
|
11053
|
-
import
|
|
11629
|
+
import path16 from "path";
|
|
11054
11630
|
var HISTORY_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
|
|
11055
11631
|
var HERMES_HISTORY_COLUMNS = [
|
|
11056
11632
|
"role",
|
|
@@ -11111,13 +11687,13 @@ async function readHermesTranscriptHistory(sessionId, profileName) {
|
|
|
11111
11687
|
}
|
|
11112
11688
|
const normalizedProfileName = isValidProfileName2(profileName) ? profileName : "default";
|
|
11113
11689
|
const profileDir = resolveHermesProfileDir(normalizedProfileName);
|
|
11114
|
-
const dbPath =
|
|
11690
|
+
const dbPath = path16.join(profileDir, "state.db");
|
|
11115
11691
|
const sessionsDirConfig = await readHermesSessionsDir(normalizedProfileName).then((value) => ({
|
|
11116
11692
|
sessionsDir: value.sessionsDir,
|
|
11117
11693
|
configured: value.configured,
|
|
11118
11694
|
configError: false
|
|
11119
11695
|
})).catch(() => ({
|
|
11120
|
-
sessionsDir:
|
|
11696
|
+
sessionsDir: path16.join(profileDir, "sessions"),
|
|
11121
11697
|
configured: false,
|
|
11122
11698
|
configError: true
|
|
11123
11699
|
}));
|
|
@@ -11175,7 +11751,7 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
|
|
|
11175
11751
|
if (!isValidSessionFileStem(sessionId)) {
|
|
11176
11752
|
return empty;
|
|
11177
11753
|
}
|
|
11178
|
-
const transcriptPath =
|
|
11754
|
+
const transcriptPath = path16.join(sessionsDir, `${sessionId}.jsonl`);
|
|
11179
11755
|
const raw = await readFile10(transcriptPath, "utf8").catch((error) => {
|
|
11180
11756
|
if (isNodeError12(error, "ENOENT")) {
|
|
11181
11757
|
return "";
|
|
@@ -11418,7 +11994,7 @@ function normalizeProfileForCompare(value) {
|
|
|
11418
11994
|
// src/hermes/stt.ts
|
|
11419
11995
|
import { execFile as execFile3 } from "child_process";
|
|
11420
11996
|
import { access as access2, readFile as readFile11, stat as stat11 } from "fs/promises";
|
|
11421
|
-
import
|
|
11997
|
+
import path17 from "path";
|
|
11422
11998
|
import { promisify as promisify3 } from "util";
|
|
11423
11999
|
var execFileAsync3 = promisify3(execFile3);
|
|
11424
12000
|
var STT_RESULT_PREFIX = "__HERMES_LINK_STT__";
|
|
@@ -11513,7 +12089,7 @@ async function buildHermesSttEnv(profileName) {
|
|
|
11513
12089
|
};
|
|
11514
12090
|
const devSource = await findDevHermesAgentSource();
|
|
11515
12091
|
if (devSource) {
|
|
11516
|
-
env.PYTHONPATH = [devSource, env.PYTHONPATH].filter(Boolean).join(
|
|
12092
|
+
env.PYTHONPATH = [devSource, env.PYTHONPATH].filter(Boolean).join(path17.delimiter);
|
|
11517
12093
|
}
|
|
11518
12094
|
return env;
|
|
11519
12095
|
}
|
|
@@ -11560,14 +12136,14 @@ async function resolveHermesPythonCommand() {
|
|
|
11560
12136
|
};
|
|
11561
12137
|
}
|
|
11562
12138
|
async function resolveExecutablePath(command) {
|
|
11563
|
-
if (
|
|
12139
|
+
if (path17.isAbsolute(command)) {
|
|
11564
12140
|
return await isExecutableFile(command) ? command : null;
|
|
11565
12141
|
}
|
|
11566
12142
|
const pathEnv = process.env.PATH ?? "";
|
|
11567
12143
|
const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
11568
|
-
for (const dir of pathEnv.split(
|
|
12144
|
+
for (const dir of pathEnv.split(path17.delimiter)) {
|
|
11569
12145
|
for (const extension of extensions) {
|
|
11570
|
-
const candidate =
|
|
12146
|
+
const candidate = path17.join(dir, `${command}${extension}`);
|
|
11571
12147
|
if (await isExecutableFile(candidate)) {
|
|
11572
12148
|
return candidate;
|
|
11573
12149
|
}
|
|
@@ -11607,8 +12183,8 @@ function shebangToPythonCommand(shebang) {
|
|
|
11607
12183
|
}
|
|
11608
12184
|
async function findDevHermesAgentSource() {
|
|
11609
12185
|
const candidates = [
|
|
11610
|
-
|
|
11611
|
-
|
|
12186
|
+
path17.resolve(process.cwd(), "reference/hermes-agent"),
|
|
12187
|
+
path17.resolve(process.cwd(), "../../reference/hermes-agent")
|
|
11612
12188
|
];
|
|
11613
12189
|
for (const candidate of candidates) {
|
|
11614
12190
|
if (await isDirectory(candidate)) {
|
|
@@ -13249,7 +13825,7 @@ var ConversationService = class {
|
|
|
13249
13825
|
async createConversation(input = {}) {
|
|
13250
13826
|
await this.store.ensureConversationsDir();
|
|
13251
13827
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13252
|
-
const id = `conv_${
|
|
13828
|
+
const id = `conv_${randomUUID8().replaceAll("-", "")}`;
|
|
13253
13829
|
const title = input.title?.trim() || DEFAULT_CONVERSATION_TITLE;
|
|
13254
13830
|
const profile = await resolveConversationProfileTarget(
|
|
13255
13831
|
this.paths,
|
|
@@ -13336,7 +13912,7 @@ var ConversationService = class {
|
|
|
13336
13912
|
manifest.profile_name_snapshot ?? manifest.profile ?? input.profileName
|
|
13337
13913
|
);
|
|
13338
13914
|
const message = {
|
|
13339
|
-
id: `msg_${
|
|
13915
|
+
id: `msg_${randomUUID8().replaceAll("-", "")}`,
|
|
13340
13916
|
schema_version: 1,
|
|
13341
13917
|
conversation_id: manifest.id,
|
|
13342
13918
|
role: "assistant",
|
|
@@ -13800,6 +14376,18 @@ var ConversationService = class {
|
|
|
13800
14376
|
async deleteConversation(conversationId) {
|
|
13801
14377
|
return this.maintenance.deleteConversation(conversationId);
|
|
13802
14378
|
}
|
|
14379
|
+
prepareClearAllConversationPlan() {
|
|
14380
|
+
return this.maintenance.prepareClearAllConversationPlan();
|
|
14381
|
+
}
|
|
14382
|
+
readClearAllConversationPlan(planId) {
|
|
14383
|
+
return this.maintenance.readClearAllConversationPlan(planId);
|
|
14384
|
+
}
|
|
14385
|
+
executeClearAllConversationPlan(planId) {
|
|
14386
|
+
return this.maintenance.executeClearAllConversationPlan(planId);
|
|
14387
|
+
}
|
|
14388
|
+
startClearAllConversationPlan(planId) {
|
|
14389
|
+
return this.maintenance.startClearAllConversationPlan(planId);
|
|
14390
|
+
}
|
|
13803
14391
|
async deleteConversations(conversationIds) {
|
|
13804
14392
|
return this.maintenance.deleteConversations(conversationIds);
|
|
13805
14393
|
}
|
|
@@ -13928,8 +14516,8 @@ function findApproval(snapshot, approvalId) {
|
|
|
13928
14516
|
}
|
|
13929
14517
|
|
|
13930
14518
|
// src/identity/identity.ts
|
|
13931
|
-
import { generateKeyPairSync, randomUUID as
|
|
13932
|
-
import { mkdir as
|
|
14519
|
+
import { generateKeyPairSync, randomUUID as randomUUID9, sign } from "crypto";
|
|
14520
|
+
import { mkdir as mkdir9, chmod as chmod2 } from "fs/promises";
|
|
13933
14521
|
import { z } from "zod";
|
|
13934
14522
|
var linkIdentitySchema = z.object({
|
|
13935
14523
|
install_id: z.string().min(1),
|
|
@@ -13951,12 +14539,12 @@ async function ensureIdentity(paths = resolveRuntimePaths()) {
|
|
|
13951
14539
|
if (existing) {
|
|
13952
14540
|
return existing;
|
|
13953
14541
|
}
|
|
13954
|
-
await
|
|
14542
|
+
await mkdir9(paths.homeDir, { recursive: true, mode: 448 });
|
|
13955
14543
|
await chmod2(paths.homeDir, 448).catch(() => void 0);
|
|
13956
14544
|
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
13957
14545
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13958
14546
|
const identity = {
|
|
13959
|
-
install_id: `install_${
|
|
14547
|
+
install_id: `install_${randomUUID9().replaceAll("-", "")}`,
|
|
13960
14548
|
link_id: null,
|
|
13961
14549
|
public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
13962
14550
|
private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
@@ -13993,7 +14581,7 @@ function getIdentityStatus(identity) {
|
|
|
13993
14581
|
}
|
|
13994
14582
|
|
|
13995
14583
|
// src/security/devices.ts
|
|
13996
|
-
import { randomBytes as randomBytes2, randomUUID as
|
|
14584
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID10, timingSafeEqual, createHash as createHash4 } from "crypto";
|
|
13997
14585
|
var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
|
|
13998
14586
|
var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
13999
14587
|
var DEVICE_SEEN_WRITE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
@@ -14011,7 +14599,7 @@ async function createDeviceSession(input, paths = resolveRuntimePaths()) {
|
|
|
14011
14599
|
}
|
|
14012
14600
|
}
|
|
14013
14601
|
const device = {
|
|
14014
|
-
id: `dev_${
|
|
14602
|
+
id: `dev_${randomUUID10().replaceAll("-", "")}`,
|
|
14015
14603
|
label: normalizeDeviceLabel(input.label),
|
|
14016
14604
|
platform: normalizeDevicePlatform(input.platform),
|
|
14017
14605
|
model: normalizeDeviceModel(input.model),
|
|
@@ -14925,6 +15513,37 @@ function registerConversationRoutes(router, options) {
|
|
|
14925
15513
|
await authenticateRequest(ctx, paths);
|
|
14926
15514
|
ctx.body = { ok: true };
|
|
14927
15515
|
});
|
|
15516
|
+
router.post("/api/v1/conversations/clear-plans", async (ctx) => {
|
|
15517
|
+
await authenticateRequest(ctx, paths);
|
|
15518
|
+
const plan = await conversations.prepareClearAllConversationPlan();
|
|
15519
|
+
ctx.status = 201;
|
|
15520
|
+
ctx.body = {
|
|
15521
|
+
ok: true,
|
|
15522
|
+
plan
|
|
15523
|
+
};
|
|
15524
|
+
});
|
|
15525
|
+
router.get("/api/v1/conversations/clear-plans/:planId", async (ctx) => {
|
|
15526
|
+
await authenticateRequest(ctx, paths);
|
|
15527
|
+
ctx.set("cache-control", "no-store");
|
|
15528
|
+
ctx.body = {
|
|
15529
|
+
ok: true,
|
|
15530
|
+
plan: await conversations.readClearAllConversationPlan(ctx.params.planId)
|
|
15531
|
+
};
|
|
15532
|
+
});
|
|
15533
|
+
router.post(
|
|
15534
|
+
"/api/v1/conversations/clear-plans/:planId/execute",
|
|
15535
|
+
async (ctx) => {
|
|
15536
|
+
await authenticateRequest(ctx, paths);
|
|
15537
|
+
const plan = await conversations.startClearAllConversationPlan(
|
|
15538
|
+
ctx.params.planId
|
|
15539
|
+
);
|
|
15540
|
+
ctx.status = plan.status === "completed" ? 200 : 202;
|
|
15541
|
+
ctx.body = {
|
|
15542
|
+
ok: true,
|
|
15543
|
+
plan
|
|
15544
|
+
};
|
|
15545
|
+
}
|
|
15546
|
+
);
|
|
14928
15547
|
router.delete("/api/v1/conversations", async (ctx) => {
|
|
14929
15548
|
await authenticateRequest(ctx, paths);
|
|
14930
15549
|
const body = await readJsonBody(ctx.req);
|
|
@@ -15147,15 +15766,14 @@ function createHttpErrorMiddleware(logger) {
|
|
|
15147
15766
|
|
|
15148
15767
|
// src/hermes/profiles.ts
|
|
15149
15768
|
import { readdir as readdir9, readFile as readFile12, rename as rename3, rm as rm6, stat as stat12 } from "fs/promises";
|
|
15150
|
-
import
|
|
15151
|
-
import path17 from "path";
|
|
15769
|
+
import path18 from "path";
|
|
15152
15770
|
import YAML2 from "yaml";
|
|
15153
15771
|
var DEFAULT_PROFILE = "default";
|
|
15154
15772
|
var PROFILE_NAME_PATTERN4 = /^[a-zA-Z0-9._-]{1,64}$/;
|
|
15155
15773
|
async function listHermesProfiles(paths = resolveRuntimePaths()) {
|
|
15156
15774
|
const profiles = /* @__PURE__ */ new Map();
|
|
15157
15775
|
profiles.set(DEFAULT_PROFILE, await profileInfo(DEFAULT_PROFILE, paths));
|
|
15158
|
-
const profilesDir =
|
|
15776
|
+
const profilesDir = resolveHermesProfilesDir();
|
|
15159
15777
|
const entries = await readdir9(profilesDir, { withFileTypes: true }).catch(
|
|
15160
15778
|
(error) => {
|
|
15161
15779
|
if (isNodeError14(error, "ENOENT")) {
|
|
@@ -15250,7 +15868,7 @@ async function readHermesProfileCapabilities(name) {
|
|
|
15250
15868
|
return {
|
|
15251
15869
|
defaultModel: listedModels?.defaultModel ?? null,
|
|
15252
15870
|
modelCount: listedModels?.models.length ?? 0,
|
|
15253
|
-
skillCount: await countSkills(
|
|
15871
|
+
skillCount: await countSkills(path18.join(profileDir, "skills")).catch(
|
|
15254
15872
|
() => 0
|
|
15255
15873
|
),
|
|
15256
15874
|
toolCount: await countConfiguredTools(name).catch(() => 0)
|
|
@@ -15306,7 +15924,7 @@ async function countSkills(root) {
|
|
|
15306
15924
|
);
|
|
15307
15925
|
let count = 0;
|
|
15308
15926
|
for (const entry of entries) {
|
|
15309
|
-
const entryPath =
|
|
15927
|
+
const entryPath = path18.join(root, entry.name);
|
|
15310
15928
|
if (entry.name === ".git" || entry.name === ".hub") {
|
|
15311
15929
|
continue;
|
|
15312
15930
|
}
|
|
@@ -16003,12 +16621,12 @@ import { spawn as spawn2 } from "child_process";
|
|
|
16003
16621
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
16004
16622
|
import {
|
|
16005
16623
|
cp,
|
|
16006
|
-
mkdir as
|
|
16624
|
+
mkdir as mkdir10,
|
|
16007
16625
|
readFile as readFile13,
|
|
16008
16626
|
rm as rm7,
|
|
16009
16627
|
stat as stat13
|
|
16010
16628
|
} from "fs/promises";
|
|
16011
|
-
import
|
|
16629
|
+
import path19 from "path";
|
|
16012
16630
|
import YAML3 from "yaml";
|
|
16013
16631
|
var PROFILE_CREATE_LOG_FILE = "profile-create.log";
|
|
16014
16632
|
var PROFILE_CREATE_LOG_MAX_FILES = 3;
|
|
@@ -16056,7 +16674,7 @@ async function startHermesProfileCreation(input, options) {
|
|
|
16056
16674
|
signal: null,
|
|
16057
16675
|
error: null
|
|
16058
16676
|
};
|
|
16059
|
-
await
|
|
16677
|
+
await mkdir10(options.paths.runDir, { recursive: true, mode: 448 });
|
|
16060
16678
|
await writeProfileCreationState(options.paths, started);
|
|
16061
16679
|
await writer.write(`
|
|
16062
16680
|
=== profile creation started ${startedAt} ===
|
|
@@ -16459,7 +17077,7 @@ function collectEnvKeys(value, keys = /* @__PURE__ */ new Set()) {
|
|
|
16459
17077
|
return keys;
|
|
16460
17078
|
}
|
|
16461
17079
|
async function writeEnvValues(profileName, values) {
|
|
16462
|
-
const envPath =
|
|
17080
|
+
const envPath = path19.join(resolveHermesProfileDir(profileName), ".env");
|
|
16463
17081
|
const existingRaw = await readFile13(envPath, "utf8").catch((error) => {
|
|
16464
17082
|
if (isNodeError15(error, "ENOENT")) {
|
|
16465
17083
|
return "";
|
|
@@ -16496,8 +17114,8 @@ async function writeEnvValues(profileName, values) {
|
|
|
16496
17114
|
await atomicWriteFilePreservingMetadata(envPath, nextRaw);
|
|
16497
17115
|
}
|
|
16498
17116
|
async function copySkills(sourceProfile, targetProfile) {
|
|
16499
|
-
const sourceSkills =
|
|
16500
|
-
const targetSkills =
|
|
17117
|
+
const sourceSkills = path19.join(resolveHermesProfileDir(sourceProfile), "skills");
|
|
17118
|
+
const targetSkills = path19.join(resolveHermesProfileDir(targetProfile), "skills");
|
|
16501
17119
|
if (!await pathExists(sourceSkills)) {
|
|
16502
17120
|
return;
|
|
16503
17121
|
}
|
|
@@ -16592,10 +17210,10 @@ async function readProfileCreationLogLines(paths) {
|
|
|
16592
17210
|
);
|
|
16593
17211
|
}
|
|
16594
17212
|
function profileCreationStatePath(paths) {
|
|
16595
|
-
return
|
|
17213
|
+
return path19.join(paths.runDir, "profile-create-state.json");
|
|
16596
17214
|
}
|
|
16597
17215
|
function profileCreationLogPath(paths) {
|
|
16598
|
-
return
|
|
17216
|
+
return path19.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
|
|
16599
17217
|
}
|
|
16600
17218
|
async function clearProfileCreationLogFiles(paths) {
|
|
16601
17219
|
const primary = profileCreationLogPath(paths);
|
|
@@ -16869,7 +17487,7 @@ import {
|
|
|
16869
17487
|
readFile as readFile14,
|
|
16870
17488
|
stat as stat14
|
|
16871
17489
|
} from "fs/promises";
|
|
16872
|
-
import
|
|
17490
|
+
import path20 from "path";
|
|
16873
17491
|
import YAML4 from "yaml";
|
|
16874
17492
|
var ENTRY_DELIMITER = "\n\xA7\n";
|
|
16875
17493
|
var DEFAULT_MEMORY_LIMIT = 2200;
|
|
@@ -17086,7 +17704,7 @@ async function saveProviderSettings(profileName, provider, patch) {
|
|
|
17086
17704
|
if (provider === "hindsight") {
|
|
17087
17705
|
await patchJsonProviderConfig(
|
|
17088
17706
|
profileName,
|
|
17089
|
-
|
|
17707
|
+
path20.join("hindsight", "config.json"),
|
|
17090
17708
|
{
|
|
17091
17709
|
mode: patch.mode,
|
|
17092
17710
|
api_url: patch.apiUrl,
|
|
@@ -17253,7 +17871,7 @@ async function patchHermesMemoryProvider(profileName, provider) {
|
|
|
17253
17871
|
await atomicWriteFilePreservingMetadata(configPath, document.toString());
|
|
17254
17872
|
}
|
|
17255
17873
|
function resolveMemoryDir(profileName) {
|
|
17256
|
-
return
|
|
17874
|
+
return path20.join(resolveHermesProfileDir(profileName), "memories");
|
|
17257
17875
|
}
|
|
17258
17876
|
async function readMemoryStore(profileName, target, limits) {
|
|
17259
17877
|
const filePath = memoryFilePath(profileName, target);
|
|
@@ -17314,7 +17932,7 @@ async function writeMemoryEntries(profileName, target, entries) {
|
|
|
17314
17932
|
);
|
|
17315
17933
|
}
|
|
17316
17934
|
function memoryFilePath(profileName, target) {
|
|
17317
|
-
return
|
|
17935
|
+
return path20.join(
|
|
17318
17936
|
resolveMemoryDir(profileName),
|
|
17319
17937
|
target === "user" ? "USER.md" : "MEMORY.md"
|
|
17320
17938
|
);
|
|
@@ -17374,7 +17992,7 @@ async function readCustomProviderSetupSummary(profileName) {
|
|
|
17374
17992
|
configurable: true,
|
|
17375
17993
|
configured: true,
|
|
17376
17994
|
configurationIssue: null,
|
|
17377
|
-
providerConfigPath:
|
|
17995
|
+
providerConfigPath: path20.join(
|
|
17378
17996
|
resolveHermesProfileDir(profileName),
|
|
17379
17997
|
"<provider>.json"
|
|
17380
17998
|
),
|
|
@@ -17728,7 +18346,7 @@ async function readProviderSettings(profileName, provider) {
|
|
|
17728
18346
|
stringSetting(
|
|
17729
18347
|
"dbPath",
|
|
17730
18348
|
"SQLite \u6570\u636E\u5E93\u8DEF\u5F84",
|
|
17731
|
-
config.db_path ??
|
|
18349
|
+
config.db_path ?? path20.join(resolveHermesProfileDir(profileName), "memory_store.db")
|
|
17732
18350
|
),
|
|
17733
18351
|
booleanSetting("autoExtract", "\u4F1A\u8BDD\u7ED3\u675F\u81EA\u52A8\u62BD\u53D6", config.auto_extract ?? false),
|
|
17734
18352
|
numberSetting("defaultTrust", "\u9ED8\u8BA4\u4FE1\u4EFB\u5206", config.default_trust ?? 0.5),
|
|
@@ -17751,7 +18369,7 @@ async function readProviderSettings(profileName, provider) {
|
|
|
17751
18369
|
stringSetting(
|
|
17752
18370
|
"workingDirectory",
|
|
17753
18371
|
"\u5DE5\u4F5C\u76EE\u5F55",
|
|
17754
|
-
|
|
18372
|
+
path20.join(resolveHermesProfileDir(profileName), "byterover"),
|
|
17755
18373
|
false
|
|
17756
18374
|
)
|
|
17757
18375
|
];
|
|
@@ -17760,16 +18378,16 @@ async function readProviderSettings(profileName, provider) {
|
|
|
17760
18378
|
}
|
|
17761
18379
|
function memoryProviderConfigPath(profileName, provider) {
|
|
17762
18380
|
if (provider === "honcho") {
|
|
17763
|
-
return
|
|
18381
|
+
return path20.join(resolveHermesProfileDir(profileName), "honcho.json");
|
|
17764
18382
|
}
|
|
17765
18383
|
if (provider === "mem0") {
|
|
17766
|
-
return
|
|
18384
|
+
return path20.join(resolveHermesProfileDir(profileName), "mem0.json");
|
|
17767
18385
|
}
|
|
17768
18386
|
if (provider === "supermemory") {
|
|
17769
|
-
return
|
|
18387
|
+
return path20.join(resolveHermesProfileDir(profileName), "supermemory.json");
|
|
17770
18388
|
}
|
|
17771
18389
|
if (provider === "hindsight") {
|
|
17772
|
-
return
|
|
18390
|
+
return path20.join(
|
|
17773
18391
|
resolveHermesProfileDir(profileName),
|
|
17774
18392
|
"hindsight",
|
|
17775
18393
|
"config.json"
|
|
@@ -17778,13 +18396,13 @@ function memoryProviderConfigPath(profileName, provider) {
|
|
|
17778
18396
|
return null;
|
|
17779
18397
|
}
|
|
17780
18398
|
function customProviderConfigPath(profileName, provider) {
|
|
17781
|
-
return
|
|
18399
|
+
return path20.join(
|
|
17782
18400
|
resolveHermesProfileDir(profileName),
|
|
17783
18401
|
`${normalizeCustomProviderId(provider)}.json`
|
|
17784
18402
|
);
|
|
17785
18403
|
}
|
|
17786
18404
|
function customProviderRegistryPath(profileName) {
|
|
17787
|
-
return
|
|
18405
|
+
return path20.join(
|
|
17788
18406
|
resolveHermesProfileDir(profileName),
|
|
17789
18407
|
CUSTOM_PROVIDER_REGISTRY_FILE
|
|
17790
18408
|
);
|
|
@@ -17836,7 +18454,7 @@ async function saveCustomProviderRegistryEntry(profileName, provider) {
|
|
|
17836
18454
|
);
|
|
17837
18455
|
}
|
|
17838
18456
|
async function discoverUserMemoryProviderDescriptors(profileName) {
|
|
17839
|
-
const pluginsDir =
|
|
18457
|
+
const pluginsDir = path20.join(resolveHermesProfileDir(profileName), "plugins");
|
|
17840
18458
|
const entries = await readdir10(pluginsDir, { withFileTypes: true }).catch(
|
|
17841
18459
|
(error) => {
|
|
17842
18460
|
if (isNodeError16(error, "ENOENT")) {
|
|
@@ -17856,7 +18474,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
|
|
|
17856
18474
|
} catch {
|
|
17857
18475
|
continue;
|
|
17858
18476
|
}
|
|
17859
|
-
const providerDir =
|
|
18477
|
+
const providerDir = path20.join(pluginsDir, entry.name);
|
|
17860
18478
|
if (!await isMemoryProviderPluginDir(providerDir)) {
|
|
17861
18479
|
continue;
|
|
17862
18480
|
}
|
|
@@ -17870,7 +18488,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
|
|
|
17870
18488
|
return descriptors;
|
|
17871
18489
|
}
|
|
17872
18490
|
async function isUserMemoryProviderInstalled(profileName, provider) {
|
|
17873
|
-
const providerDir =
|
|
18491
|
+
const providerDir = path20.join(
|
|
17874
18492
|
resolveHermesProfileDir(profileName),
|
|
17875
18493
|
"plugins",
|
|
17876
18494
|
normalizeCustomProviderId(provider)
|
|
@@ -17878,7 +18496,7 @@ async function isUserMemoryProviderInstalled(profileName, provider) {
|
|
|
17878
18496
|
return isMemoryProviderPluginDir(providerDir);
|
|
17879
18497
|
}
|
|
17880
18498
|
async function isMemoryProviderPluginDir(providerDir) {
|
|
17881
|
-
const source = await readFile14(
|
|
18499
|
+
const source = await readFile14(path20.join(providerDir, "__init__.py"), "utf8").catch(
|
|
17882
18500
|
(error) => {
|
|
17883
18501
|
if (isNodeError16(error, "ENOENT")) {
|
|
17884
18502
|
return "";
|
|
@@ -17890,7 +18508,7 @@ async function isMemoryProviderPluginDir(providerDir) {
|
|
|
17890
18508
|
return sample.includes("register_memory_provider") || sample.includes("MemoryProvider");
|
|
17891
18509
|
}
|
|
17892
18510
|
async function readPluginMetadata(providerDir) {
|
|
17893
|
-
const raw = await readFile14(
|
|
18511
|
+
const raw = await readFile14(path20.join(providerDir, "plugin.yaml"), "utf8").catch(
|
|
17894
18512
|
(error) => {
|
|
17895
18513
|
if (isNodeError16(error, "ENOENT")) {
|
|
17896
18514
|
return "";
|
|
@@ -17902,10 +18520,10 @@ async function readPluginMetadata(providerDir) {
|
|
|
17902
18520
|
}
|
|
17903
18521
|
async function resolveByteRoverCli() {
|
|
17904
18522
|
const candidates = [
|
|
17905
|
-
...(process.env.PATH ?? "").split(
|
|
17906
|
-
|
|
18523
|
+
...(process.env.PATH ?? "").split(path20.delimiter).filter(Boolean).map((dir) => path20.join(dir, "brv")),
|
|
18524
|
+
path20.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
|
|
17907
18525
|
"/usr/local/bin/brv",
|
|
17908
|
-
|
|
18526
|
+
path20.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
|
|
17909
18527
|
].filter(Boolean);
|
|
17910
18528
|
for (const candidate of candidates) {
|
|
17911
18529
|
const found = await access3(candidate).then(() => true).catch(() => false);
|
|
@@ -17966,7 +18584,7 @@ async function patchHermesMemoryEnv(profileName, patch) {
|
|
|
17966
18584
|
if (entries.length === 0) {
|
|
17967
18585
|
return;
|
|
17968
18586
|
}
|
|
17969
|
-
const envPath =
|
|
18587
|
+
const envPath = path20.join(resolveHermesProfileDir(profileName), ".env");
|
|
17970
18588
|
const existingRaw = await readFile14(envPath, "utf8").catch((error) => {
|
|
17971
18589
|
if (isNodeError16(error, "ENOENT")) {
|
|
17972
18590
|
return "";
|
|
@@ -18040,7 +18658,7 @@ async function readActiveMemoryProvider(profileName) {
|
|
|
18040
18658
|
return provider;
|
|
18041
18659
|
}
|
|
18042
18660
|
async function patchJsonProviderConfig(profileName, relativePath, patch) {
|
|
18043
|
-
const configPath =
|
|
18661
|
+
const configPath = path20.join(
|
|
18044
18662
|
resolveHermesProfileDir(profileName),
|
|
18045
18663
|
relativePath
|
|
18046
18664
|
);
|
|
@@ -18069,7 +18687,7 @@ async function readJsonObject(filePath) {
|
|
|
18069
18687
|
} catch {
|
|
18070
18688
|
throw new HermesMemoryError(
|
|
18071
18689
|
"memory_provider_config_invalid",
|
|
18072
|
-
`${
|
|
18690
|
+
`${path20.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
|
|
18073
18691
|
);
|
|
18074
18692
|
}
|
|
18075
18693
|
}
|
|
@@ -18615,7 +19233,7 @@ function toMemoryHttpError(error) {
|
|
|
18615
19233
|
|
|
18616
19234
|
// src/hermes/skills.ts
|
|
18617
19235
|
import { readFile as readFile15, readdir as readdir11 } from "fs/promises";
|
|
18618
|
-
import
|
|
19236
|
+
import path21 from "path";
|
|
18619
19237
|
import YAML5 from "yaml";
|
|
18620
19238
|
var HermesSkillNotFoundError = class extends Error {
|
|
18621
19239
|
constructor(skillName) {
|
|
@@ -18629,7 +19247,7 @@ var EXCLUDED_SKILL_DIRS = /* @__PURE__ */ new Set([".git", ".github", ".hub"]);
|
|
|
18629
19247
|
async function listHermesProfileSkills(profileName, paths = resolveRuntimePaths()) {
|
|
18630
19248
|
const profile = await readExistingProfile(profileName, paths);
|
|
18631
19249
|
const profileDir = resolveHermesProfileDir(profile.name);
|
|
18632
|
-
const skillsRoot =
|
|
19250
|
+
const skillsRoot = path21.join(profileDir, "skills");
|
|
18633
19251
|
const [skillFiles, disabled, provenance] = await Promise.all([
|
|
18634
19252
|
findSkillFiles(skillsRoot),
|
|
18635
19253
|
readDisabledSkillNames(resolveHermesConfigPath(profile.name)),
|
|
@@ -18720,7 +19338,7 @@ async function collectSkillFiles(directory, results) {
|
|
|
18720
19338
|
if (EXCLUDED_SKILL_DIRS.has(entry.name)) {
|
|
18721
19339
|
continue;
|
|
18722
19340
|
}
|
|
18723
|
-
const entryPath =
|
|
19341
|
+
const entryPath = path21.join(directory, entry.name);
|
|
18724
19342
|
if (entry.isDirectory()) {
|
|
18725
19343
|
await collectSkillFiles(entryPath, results);
|
|
18726
19344
|
continue;
|
|
@@ -18742,10 +19360,10 @@ async function readSkillMetadata(input) {
|
|
|
18742
19360
|
if (raw === null) {
|
|
18743
19361
|
return null;
|
|
18744
19362
|
}
|
|
18745
|
-
const skillDir =
|
|
19363
|
+
const skillDir = path21.dirname(input.skillFile);
|
|
18746
19364
|
const { frontmatter, body } = parseSkillDocument(raw.slice(0, 4e3));
|
|
18747
19365
|
const name = normalizeSkillName(
|
|
18748
|
-
readString16(frontmatter.name) ??
|
|
19366
|
+
readString16(frontmatter.name) ?? path21.basename(skillDir)
|
|
18749
19367
|
);
|
|
18750
19368
|
if (!name) {
|
|
18751
19369
|
return null;
|
|
@@ -18764,7 +19382,7 @@ async function readSkillMetadata(input) {
|
|
|
18764
19382
|
enabled: !input.disabled.has(name),
|
|
18765
19383
|
source: provenance.source,
|
|
18766
19384
|
trust: provenance.trust,
|
|
18767
|
-
relativePath:
|
|
19385
|
+
relativePath: path21.relative(input.skillsRoot, skillDir)
|
|
18768
19386
|
};
|
|
18769
19387
|
}
|
|
18770
19388
|
function parseSkillDocument(raw) {
|
|
@@ -18785,8 +19403,8 @@ function parseSkillDocument(raw) {
|
|
|
18785
19403
|
}
|
|
18786
19404
|
}
|
|
18787
19405
|
function categoryFromPath(skillsRoot, skillFile) {
|
|
18788
|
-
const relative =
|
|
18789
|
-
const parts = relative.split(
|
|
19406
|
+
const relative = path21.relative(skillsRoot, skillFile);
|
|
19407
|
+
const parts = relative.split(path21.sep).filter(Boolean);
|
|
18790
19408
|
return parts.length >= 3 ? parts[0] : null;
|
|
18791
19409
|
}
|
|
18792
19410
|
function firstBodyDescription(body) {
|
|
@@ -18833,7 +19451,7 @@ async function readSkillProvenance(root) {
|
|
|
18833
19451
|
return provenance;
|
|
18834
19452
|
}
|
|
18835
19453
|
async function readBundledSkillNames(root) {
|
|
18836
|
-
const raw = await readFile15(
|
|
19454
|
+
const raw = await readFile15(path21.join(root, ".bundled_manifest"), "utf8").catch(
|
|
18837
19455
|
(error) => {
|
|
18838
19456
|
if (isNodeError17(error, "ENOENT")) {
|
|
18839
19457
|
return "";
|
|
@@ -18856,7 +19474,7 @@ async function readBundledSkillNames(root) {
|
|
|
18856
19474
|
return names;
|
|
18857
19475
|
}
|
|
18858
19476
|
async function readHubInstalledSkills(root) {
|
|
18859
|
-
const raw = await readFile15(
|
|
19477
|
+
const raw = await readFile15(path21.join(root, ".hub", "lock.json"), "utf8").catch(
|
|
18860
19478
|
(error) => {
|
|
18861
19479
|
if (isNodeError17(error, "ENOENT")) {
|
|
18862
19480
|
return "";
|
|
@@ -19459,8 +20077,8 @@ function readModelList(payload) {
|
|
|
19459
20077
|
// src/hermes/updates.ts
|
|
19460
20078
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
19461
20079
|
import { spawn as spawn3 } from "child_process";
|
|
19462
|
-
import { mkdir as
|
|
19463
|
-
import
|
|
20080
|
+
import { mkdir as mkdir11, readFile as readFile16, rm as rm8 } from "fs/promises";
|
|
20081
|
+
import path22 from "path";
|
|
19464
20082
|
var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
|
|
19465
20083
|
var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
19466
20084
|
var RELEASE_FETCH_TIMEOUT_MS = 5e3;
|
|
@@ -19523,7 +20141,7 @@ async function startHermesUpdate(options) {
|
|
|
19523
20141
|
signal: null,
|
|
19524
20142
|
error: null
|
|
19525
20143
|
};
|
|
19526
|
-
await
|
|
20144
|
+
await mkdir11(options.paths.runDir, { recursive: true, mode: 448 });
|
|
19527
20145
|
await writer.write(`
|
|
19528
20146
|
=== hermes update started ${startedAt} ===
|
|
19529
20147
|
`);
|
|
@@ -19729,13 +20347,13 @@ async function readUpdateLogLines(paths) {
|
|
|
19729
20347
|
);
|
|
19730
20348
|
}
|
|
19731
20349
|
function releaseCachePath(paths) {
|
|
19732
|
-
return
|
|
20350
|
+
return path22.join(paths.indexesDir, "hermes-release-check.json");
|
|
19733
20351
|
}
|
|
19734
20352
|
function updateStatePath(paths) {
|
|
19735
|
-
return
|
|
20353
|
+
return path22.join(paths.runDir, "hermes-update-state.json");
|
|
19736
20354
|
}
|
|
19737
20355
|
function updateLogPath(paths) {
|
|
19738
|
-
return
|
|
20356
|
+
return path22.join(paths.logsDir, UPDATE_LOG_FILE);
|
|
19739
20357
|
}
|
|
19740
20358
|
async function clearUpdateLogFiles(paths) {
|
|
19741
20359
|
const primary = updateLogPath(paths);
|
|
@@ -19827,17 +20445,17 @@ function readString17(payload, key) {
|
|
|
19827
20445
|
// src/link/updates.ts
|
|
19828
20446
|
import { spawn as spawn5 } from "child_process";
|
|
19829
20447
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
19830
|
-
import { mkdir as
|
|
19831
|
-
import
|
|
20448
|
+
import { mkdir as mkdir14, readFile as readFile18, rm as rm11 } from "fs/promises";
|
|
20449
|
+
import path24 from "path";
|
|
19832
20450
|
|
|
19833
20451
|
// src/daemon/process.ts
|
|
19834
20452
|
import { spawn as spawn4 } from "child_process";
|
|
19835
|
-
import { mkdir as
|
|
19836
|
-
import
|
|
20453
|
+
import { mkdir as mkdir13, readFile as readFile17, rm as rm10 } from "fs/promises";
|
|
20454
|
+
import path23 from "path";
|
|
19837
20455
|
|
|
19838
20456
|
// src/daemon/service.ts
|
|
19839
20457
|
import { createServer } from "http";
|
|
19840
|
-
import { mkdir as
|
|
20458
|
+
import { mkdir as mkdir12, rm as rm9, writeFile as writeFile3 } from "fs/promises";
|
|
19841
20459
|
|
|
19842
20460
|
// src/relay/control-client.ts
|
|
19843
20461
|
import WebSocket from "ws";
|
|
@@ -20011,7 +20629,7 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
|
|
|
20011
20629
|
// src/runtime/system-info.ts
|
|
20012
20630
|
import { execFileSync } from "child_process";
|
|
20013
20631
|
import { readFileSync } from "fs";
|
|
20014
|
-
import
|
|
20632
|
+
import os4 from "os";
|
|
20015
20633
|
function readLinkSystemInfo() {
|
|
20016
20634
|
const platform = process.platform;
|
|
20017
20635
|
const hostname = readHostname(platform);
|
|
@@ -20050,7 +20668,7 @@ function readHostname(platform) {
|
|
|
20050
20668
|
return computerName;
|
|
20051
20669
|
}
|
|
20052
20670
|
}
|
|
20053
|
-
return normalizeText(
|
|
20671
|
+
return normalizeText(os4.hostname());
|
|
20054
20672
|
}
|
|
20055
20673
|
function readOsLabel(platform) {
|
|
20056
20674
|
if (platform === "darwin") {
|
|
@@ -20058,12 +20676,12 @@ function readOsLabel(platform) {
|
|
|
20058
20676
|
return version ? `macOS ${version}` : "macOS";
|
|
20059
20677
|
}
|
|
20060
20678
|
if (platform === "linux") {
|
|
20061
|
-
return readLinuxOsRelease() ?? `Linux ${
|
|
20679
|
+
return readLinuxOsRelease() ?? `Linux ${os4.release()}`;
|
|
20062
20680
|
}
|
|
20063
20681
|
if (platform === "win32") {
|
|
20064
|
-
return `Windows ${
|
|
20682
|
+
return `Windows ${os4.release()}`;
|
|
20065
20683
|
}
|
|
20066
|
-
return `${
|
|
20684
|
+
return `${os4.type()} ${os4.release()}`.trim();
|
|
20067
20685
|
}
|
|
20068
20686
|
function readLinuxOsRelease() {
|
|
20069
20687
|
for (const file of ["/etc/os-release", "/usr/lib/os-release"]) {
|
|
@@ -20110,11 +20728,11 @@ function truncateText(value, maxLength) {
|
|
|
20110
20728
|
}
|
|
20111
20729
|
|
|
20112
20730
|
// src/topology/network.ts
|
|
20113
|
-
import
|
|
20731
|
+
import os6 from "os";
|
|
20114
20732
|
|
|
20115
20733
|
// src/topology/environment.ts
|
|
20116
20734
|
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
20117
|
-
import
|
|
20735
|
+
import os5 from "os";
|
|
20118
20736
|
function detectRuntimeEnvironment(env = process.env) {
|
|
20119
20737
|
if (isWsl(env)) {
|
|
20120
20738
|
return {
|
|
@@ -20143,7 +20761,7 @@ function isWsl(env) {
|
|
|
20143
20761
|
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
|
|
20144
20762
|
return true;
|
|
20145
20763
|
}
|
|
20146
|
-
const release =
|
|
20764
|
+
const release = os5.release().toLowerCase();
|
|
20147
20765
|
return release.includes("microsoft") || release.includes("wsl");
|
|
20148
20766
|
}
|
|
20149
20767
|
function isContainer(env) {
|
|
@@ -20188,7 +20806,7 @@ async function discoverRouteCandidates(options) {
|
|
|
20188
20806
|
};
|
|
20189
20807
|
}
|
|
20190
20808
|
function discoverLanIps() {
|
|
20191
|
-
return discoverLanIpsFromInterfaces(
|
|
20809
|
+
return discoverLanIpsFromInterfaces(os6.networkInterfaces());
|
|
20192
20810
|
}
|
|
20193
20811
|
function discoverLanIpsFromInterfaces(interfaces) {
|
|
20194
20812
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -20716,6 +21334,7 @@ function startHermesSessionSyncScheduler(options) {
|
|
|
20716
21334
|
}
|
|
20717
21335
|
|
|
20718
21336
|
// src/daemon/service.ts
|
|
21337
|
+
var DEFAULT_RELAY_READY_TIMEOUT_MS = 2e3;
|
|
20719
21338
|
async function startLinkService(options = {}) {
|
|
20720
21339
|
const paths = options.paths ?? resolveRuntimePaths();
|
|
20721
21340
|
const logger = createFileLogger({ paths });
|
|
@@ -20781,6 +21400,10 @@ async function startLinkService(options = {}) {
|
|
|
20781
21400
|
});
|
|
20782
21401
|
let relay = null;
|
|
20783
21402
|
if (identity?.link_id) {
|
|
21403
|
+
let resolveRelayReady = null;
|
|
21404
|
+
const relayReady = new Promise((resolve) => {
|
|
21405
|
+
resolveRelayReady = resolve;
|
|
21406
|
+
});
|
|
20784
21407
|
relay = connectRelayControl({
|
|
20785
21408
|
relayBaseUrl: config.relayBaseUrl,
|
|
20786
21409
|
linkId: identity.link_id,
|
|
@@ -20790,8 +21413,22 @@ async function startLinkService(options = {}) {
|
|
|
20790
21413
|
backoffMaxMs: 3e4,
|
|
20791
21414
|
onStatus: (status) => {
|
|
20792
21415
|
void logger.info("relay_status", status);
|
|
21416
|
+
if (status.state === "connected") {
|
|
21417
|
+
resolveRelayReady?.(true);
|
|
21418
|
+
resolveRelayReady = null;
|
|
21419
|
+
} else if (status.state === "failed") {
|
|
21420
|
+
resolveRelayReady?.(false);
|
|
21421
|
+
resolveRelayReady = null;
|
|
21422
|
+
}
|
|
20793
21423
|
}
|
|
20794
21424
|
});
|
|
21425
|
+
if (options.waitForRelayReady) {
|
|
21426
|
+
await Promise.race([
|
|
21427
|
+
relayReady,
|
|
21428
|
+
waitForRelayReadyTimeout(options.relayReadyTimeoutMs)
|
|
21429
|
+
]);
|
|
21430
|
+
resolveRelayReady = null;
|
|
21431
|
+
}
|
|
20795
21432
|
} else {
|
|
20796
21433
|
void logger.info("relay_skipped", { reason: "link_not_paired" });
|
|
20797
21434
|
}
|
|
@@ -20826,11 +21463,20 @@ async function startLinkService(options = {}) {
|
|
|
20826
21463
|
}
|
|
20827
21464
|
};
|
|
20828
21465
|
}
|
|
21466
|
+
function waitForRelayReadyTimeout(timeoutMs) {
|
|
21467
|
+
return new Promise((resolve) => {
|
|
21468
|
+
const timer = setTimeout(
|
|
21469
|
+
() => resolve(false),
|
|
21470
|
+
timeoutMs ?? DEFAULT_RELAY_READY_TIMEOUT_MS
|
|
21471
|
+
);
|
|
21472
|
+
timer.unref?.();
|
|
21473
|
+
});
|
|
21474
|
+
}
|
|
20829
21475
|
function pidFilePath(paths = resolveRuntimePaths()) {
|
|
20830
21476
|
return `${paths.runDir}/hermeslink.pid`;
|
|
20831
21477
|
}
|
|
20832
21478
|
async function writePidFile(paths) {
|
|
20833
|
-
await
|
|
21479
|
+
await mkdir12(paths.runDir, { recursive: true, mode: 448 });
|
|
20834
21480
|
await writeFile3(pidFilePath(paths), `${process.pid}
|
|
20835
21481
|
`, { mode: 384 });
|
|
20836
21482
|
}
|
|
@@ -20905,8 +21551,8 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
|
|
|
20905
21551
|
return status;
|
|
20906
21552
|
}
|
|
20907
21553
|
}
|
|
20908
|
-
await
|
|
20909
|
-
await
|
|
21554
|
+
await mkdir13(paths.logsDir, { recursive: true, mode: 448 });
|
|
21555
|
+
await mkdir13(paths.runDir, { recursive: true, mode: 448 });
|
|
20910
21556
|
const scriptPath = currentCliScriptPath();
|
|
20911
21557
|
const child = spawn4(process.execPath, [scriptPath, "daemon-supervisor"], {
|
|
20912
21558
|
detached: true,
|
|
@@ -20924,10 +21570,10 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
|
|
|
20924
21570
|
return await getDaemonStatus(paths);
|
|
20925
21571
|
}
|
|
20926
21572
|
async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
|
|
20927
|
-
await
|
|
21573
|
+
await mkdir13(paths.logsDir, { recursive: true, mode: 448 });
|
|
20928
21574
|
const log = createRotatingTextLogWriter({
|
|
20929
21575
|
paths,
|
|
20930
|
-
fileName:
|
|
21576
|
+
fileName: path23.basename(daemonLogFile(paths))
|
|
20931
21577
|
});
|
|
20932
21578
|
const scriptPath = currentCliScriptPath();
|
|
20933
21579
|
const child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
|
|
@@ -21168,7 +21814,7 @@ async function startLinkUpdate(options) {
|
|
|
21168
21814
|
error: null,
|
|
21169
21815
|
manual_command: manualCommand
|
|
21170
21816
|
};
|
|
21171
|
-
await
|
|
21817
|
+
await mkdir14(options.paths.runDir, { recursive: true, mode: 448 });
|
|
21172
21818
|
await writer.write(
|
|
21173
21819
|
`
|
|
21174
21820
|
=== link update started ${startedAt} target=${targetVersion} ===
|
|
@@ -21466,10 +22112,10 @@ async function readUpdateLogLines2(paths) {
|
|
|
21466
22112
|
);
|
|
21467
22113
|
}
|
|
21468
22114
|
function updateStatePath2(paths) {
|
|
21469
|
-
return
|
|
22115
|
+
return path24.join(paths.runDir, "link-update-state.json");
|
|
21470
22116
|
}
|
|
21471
22117
|
function updateLogPath2(paths) {
|
|
21472
|
-
return
|
|
22118
|
+
return path24.join(paths.logsDir, UPDATE_LOG_FILE2);
|
|
21473
22119
|
}
|
|
21474
22120
|
async function clearUpdateLogFiles2(paths) {
|
|
21475
22121
|
const primary = updateLogPath2(paths);
|
|
@@ -21533,7 +22179,7 @@ function readString18(payload, key) {
|
|
|
21533
22179
|
}
|
|
21534
22180
|
|
|
21535
22181
|
// src/pairing/pairing.ts
|
|
21536
|
-
import
|
|
22182
|
+
import path25 from "path";
|
|
21537
22183
|
import { rm as rm12 } from "fs/promises";
|
|
21538
22184
|
|
|
21539
22185
|
// src/relay/bootstrap.ts
|
|
@@ -21767,6 +22413,10 @@ async function readPairingSession(sessionId, paths = resolveRuntimePaths()) {
|
|
|
21767
22413
|
expires_at: record.expires_at
|
|
21768
22414
|
};
|
|
21769
22415
|
}
|
|
22416
|
+
function isPairingSessionExpired(session) {
|
|
22417
|
+
const expiresAtMs = Date.parse(session.expires_at);
|
|
22418
|
+
return !Number.isFinite(expiresAtMs) || Date.now() >= expiresAtMs;
|
|
22419
|
+
}
|
|
21770
22420
|
async function recordPairingClaim(input, paths = resolveRuntimePaths()) {
|
|
21771
22421
|
const record = {
|
|
21772
22422
|
session_id: input.sessionId,
|
|
@@ -21796,7 +22446,20 @@ async function clearPairingClaim(sessionId, paths = resolveRuntimePaths()) {
|
|
|
21796
22446
|
}
|
|
21797
22447
|
async function claimPairing(input) {
|
|
21798
22448
|
const paths = input.paths ?? resolveRuntimePaths();
|
|
21799
|
-
const [identity, config] = await Promise.all([
|
|
22449
|
+
const [identity, config, localSession] = await Promise.all([
|
|
22450
|
+
loadRequiredIdentity2(paths),
|
|
22451
|
+
loadConfig(paths),
|
|
22452
|
+
readPairingSession(input.sessionId, paths)
|
|
22453
|
+
]);
|
|
22454
|
+
if (!localSession) {
|
|
22455
|
+
throw new LinkHttpError(404, "pairing_session_not_found", "Pairing session was not found");
|
|
22456
|
+
}
|
|
22457
|
+
if (isPairingSessionExpired(localSession)) {
|
|
22458
|
+
throw new LinkHttpError(404, "pairing_session_expired", "Pairing session has expired");
|
|
22459
|
+
}
|
|
22460
|
+
if (localSession.link_id !== identity.link_id) {
|
|
22461
|
+
throw new LinkHttpError(409, "pairing_claim_mismatch", "Pairing claim does not match this Link");
|
|
22462
|
+
}
|
|
21800
22463
|
let verified;
|
|
21801
22464
|
try {
|
|
21802
22465
|
verified = await postServerJson(
|
|
@@ -21856,10 +22519,10 @@ async function loadRequiredIdentity2(paths) {
|
|
|
21856
22519
|
}
|
|
21857
22520
|
return identity;
|
|
21858
22521
|
}
|
|
21859
|
-
async function postServerJson(serverBaseUrl,
|
|
22522
|
+
async function postServerJson(serverBaseUrl, path26, body, options) {
|
|
21860
22523
|
let response;
|
|
21861
22524
|
try {
|
|
21862
|
-
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${
|
|
22525
|
+
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path26}`, {
|
|
21863
22526
|
method: "POST",
|
|
21864
22527
|
headers: {
|
|
21865
22528
|
accept: "application/json",
|
|
@@ -21907,10 +22570,10 @@ function pairingErrorSnapshot(stage, error) {
|
|
|
21907
22570
|
occurred_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
21908
22571
|
};
|
|
21909
22572
|
}
|
|
21910
|
-
async function patchServerJson(serverBaseUrl,
|
|
22573
|
+
async function patchServerJson(serverBaseUrl, path26, token, body, options) {
|
|
21911
22574
|
let response;
|
|
21912
22575
|
try {
|
|
21913
|
-
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${
|
|
22576
|
+
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path26}`, {
|
|
21914
22577
|
method: "PATCH",
|
|
21915
22578
|
headers: {
|
|
21916
22579
|
accept: "application/json",
|
|
@@ -21958,10 +22621,10 @@ function createPairingNetworkError(input) {
|
|
|
21958
22621
|
);
|
|
21959
22622
|
}
|
|
21960
22623
|
function pairingClaimPath(sessionId, paths) {
|
|
21961
|
-
return
|
|
22624
|
+
return path25.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
|
|
21962
22625
|
}
|
|
21963
22626
|
function pairingSessionPath(sessionId, paths) {
|
|
21964
|
-
return
|
|
22627
|
+
return path25.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
|
|
21965
22628
|
}
|
|
21966
22629
|
function qrPreferredUrls(routes) {
|
|
21967
22630
|
return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
|
|
@@ -22015,6 +22678,7 @@ function registerSystemRoutes(router, options) {
|
|
|
22015
22678
|
conversation_events: true,
|
|
22016
22679
|
conversation_delete: true,
|
|
22017
22680
|
conversation_bulk_delete: true,
|
|
22681
|
+
conversation_clear_plan: true,
|
|
22018
22682
|
conversation_cancel: true,
|
|
22019
22683
|
conversation_rename: true,
|
|
22020
22684
|
blobs: true,
|
|
@@ -22608,12 +23272,16 @@ function registerPairingRoutes(router, options) {
|
|
|
22608
23272
|
if (!session) {
|
|
22609
23273
|
throw new LinkHttpError(404, "pairing_session_not_found", "Pairing session was not found");
|
|
22610
23274
|
}
|
|
23275
|
+
const state = await readPairingState(sessionId, paths);
|
|
23276
|
+
if (!state.claimed && isPairingSessionExpired(session)) {
|
|
23277
|
+
throw new LinkHttpError(404, "pairing_session_expired", "Pairing session has expired");
|
|
23278
|
+
}
|
|
22611
23279
|
ctx.set("cache-control", "no-store");
|
|
22612
23280
|
ctx.body = {
|
|
22613
23281
|
ok: true,
|
|
22614
23282
|
session: {
|
|
22615
23283
|
...session,
|
|
22616
|
-
claimed:
|
|
23284
|
+
claimed: state.claimed
|
|
22617
23285
|
}
|
|
22618
23286
|
};
|
|
22619
23287
|
});
|
|
@@ -22629,6 +23297,8 @@ async function readPairingState(sessionId, paths) {
|
|
|
22629
23297
|
}
|
|
22630
23298
|
async function renderPairingPage(input) {
|
|
22631
23299
|
const session = input.session;
|
|
23300
|
+
const expiresAtMs = Date.parse(session.expires_at);
|
|
23301
|
+
const isExpired = !input.state.claimed && isPairingSessionExpired(session);
|
|
22632
23302
|
const qrPayload = JSON.stringify({
|
|
22633
23303
|
kind: "hermes_link_pairing",
|
|
22634
23304
|
version: 1,
|
|
@@ -22648,8 +23318,9 @@ async function renderPairingPage(input) {
|
|
|
22648
23318
|
const currentUrl = session.local_api_url.replace(/\/+$/u, "");
|
|
22649
23319
|
const linkIdLabel = escapeHtml(input.linkId ?? session.link_id);
|
|
22650
23320
|
const expiresLabel = escapeHtml(formatDate(session.expires_at));
|
|
22651
|
-
const statusLabel = input.state.claimed ? "\u5DF2\u5B8C\u6210\u914D\u5BF9" : "\u7B49\u5F85 App \u626B\u7801";
|
|
22652
|
-
const statusHint = input.state.claimed ? "App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002" : "\u6253\u5F00 App \u626B\u7801\uFF0C\u6216\u8005\u590D\u5236\u914D\u5BF9\u7801\u624B\u52A8\u8F93\u5165\u3002";
|
|
23321
|
+
const statusLabel = input.state.claimed ? "\u5DF2\u5B8C\u6210\u914D\u5BF9" : isExpired ? "\u914D\u5BF9\u5DF2\u8FC7\u671F" : "\u7B49\u5F85 App \u626B\u7801";
|
|
23322
|
+
const statusHint = input.state.claimed ? "App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002" : isExpired ? "\u8FD9\u6B21\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C hermeslink pair\u3002" : "\u6253\u5F00 App \u626B\u7801\uFF0C\u6216\u8005\u590D\u5236\u914D\u5BF9\u7801\u624B\u52A8\u8F93\u5165\u3002";
|
|
23323
|
+
const statusPillLabel = input.state.claimed ? "\u5DF2\u626B\u7801" : isExpired ? "\u5DF2\u8FC7\u671F" : "\u7B49\u5F85\u4E2D";
|
|
22653
23324
|
return `<!doctype html>
|
|
22654
23325
|
<html lang="zh-CN">
|
|
22655
23326
|
<head>
|
|
@@ -22954,7 +23625,7 @@ async function renderPairingPage(input) {
|
|
|
22954
23625
|
<div class="card">
|
|
22955
23626
|
<div class="status">
|
|
22956
23627
|
<h2 class="status-title" id="statusTitle">${escapeHtml(statusLabel)}</h2>
|
|
22957
|
-
<span class="pill" id="statusPill">${
|
|
23628
|
+
<span class="pill" id="statusPill">${escapeHtml(statusPillLabel)}</span>
|
|
22958
23629
|
</div>
|
|
22959
23630
|
<div class="qr-frame">
|
|
22960
23631
|
<img src="${qrDataUri}" alt="Hermes Link pairing QR code" />
|
|
@@ -22971,22 +23642,62 @@ async function renderPairingPage(input) {
|
|
|
22971
23642
|
</main>
|
|
22972
23643
|
<script>
|
|
22973
23644
|
const sessionId = ${JSON.stringify(session.session_id)};
|
|
23645
|
+
const expiresAtMs = ${Number.isFinite(expiresAtMs) ? String(expiresAtMs) : "Number.NaN"};
|
|
23646
|
+
const initialClaimed = ${JSON.stringify(input.state.claimed)};
|
|
23647
|
+
const statusTitle = document.querySelector('#statusTitle');
|
|
23648
|
+
const statusPill = document.querySelector('#statusPill');
|
|
23649
|
+
const statusHint = document.querySelector('#statusHint');
|
|
23650
|
+
let refreshTimer = null;
|
|
23651
|
+
|
|
23652
|
+
const stopPolling = () => {
|
|
23653
|
+
if (refreshTimer !== null) {
|
|
23654
|
+
clearInterval(refreshTimer);
|
|
23655
|
+
refreshTimer = null;
|
|
23656
|
+
}
|
|
23657
|
+
};
|
|
23658
|
+
|
|
23659
|
+
const markClaimed = () => {
|
|
23660
|
+
statusTitle.textContent = '\u5DF2\u5B8C\u6210\u914D\u5BF9';
|
|
23661
|
+
statusPill.textContent = '\u5DF2\u626B\u7801';
|
|
23662
|
+
statusHint.textContent = 'App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002';
|
|
23663
|
+
stopPolling();
|
|
23664
|
+
};
|
|
23665
|
+
|
|
23666
|
+
const markExpired = () => {
|
|
23667
|
+
statusTitle.textContent = '\u914D\u5BF9\u5DF2\u8FC7\u671F';
|
|
23668
|
+
statusPill.textContent = '\u5DF2\u8FC7\u671F';
|
|
23669
|
+
statusHint.textContent = '\u8FD9\u6B21\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C hermeslink pair\u3002';
|
|
23670
|
+
stopPolling();
|
|
23671
|
+
};
|
|
23672
|
+
|
|
22974
23673
|
const refresh = async () => {
|
|
23674
|
+
if (Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs) {
|
|
23675
|
+
markExpired();
|
|
23676
|
+
return;
|
|
23677
|
+
}
|
|
22975
23678
|
try {
|
|
22976
23679
|
const response = await fetch('/api/v1/pairing/session?session_id=' + encodeURIComponent(sessionId), {
|
|
22977
23680
|
headers: { accept: 'application/json' },
|
|
22978
23681
|
});
|
|
23682
|
+
if (response.status === 404) {
|
|
23683
|
+
markExpired();
|
|
23684
|
+
return;
|
|
23685
|
+
}
|
|
22979
23686
|
if (!response.ok) return;
|
|
22980
23687
|
const payload = await response.json();
|
|
22981
23688
|
if (payload?.session?.claimed) {
|
|
22982
|
-
|
|
22983
|
-
document.querySelector('#statusTitle').textContent = '\u5DF2\u5B8C\u6210\u914D\u5BF9';
|
|
22984
|
-
document.querySelector('#statusHint').textContent = 'App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002';
|
|
23689
|
+
markClaimed();
|
|
22985
23690
|
}
|
|
22986
23691
|
} catch (_) {}
|
|
22987
23692
|
};
|
|
22988
|
-
|
|
22989
|
-
|
|
23693
|
+
if (initialClaimed) {
|
|
23694
|
+
stopPolling();
|
|
23695
|
+
} else if (Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs) {
|
|
23696
|
+
markExpired();
|
|
23697
|
+
} else {
|
|
23698
|
+
refreshTimer = window.setInterval(refresh, 1500);
|
|
23699
|
+
refresh();
|
|
23700
|
+
}
|
|
22990
23701
|
</script>
|
|
22991
23702
|
</body>
|
|
22992
23703
|
</html>`;
|
|
@@ -23106,6 +23817,7 @@ export {
|
|
|
23106
23817
|
readPairingClaim,
|
|
23107
23818
|
clearPairingClaim,
|
|
23108
23819
|
createApp,
|
|
23820
|
+
connectRelayControl,
|
|
23109
23821
|
reportLinkStatusToServer,
|
|
23110
23822
|
startLinkService,
|
|
23111
23823
|
startDaemonProcess,
|