@hermespilot/link 0.3.8 → 0.4.0
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-RJB5VUT4.js → chunk-FWPHQZP6.js} +1148 -277
- package/dist/cli/index.js +144 -71
- package/dist/http/app.d.ts +17 -0
- package/dist/http/app.js +1 -1
- 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";
|
|
@@ -3883,7 +3883,7 @@ async function listCronOutputFiles(profileName, jobId) {
|
|
|
3883
3883
|
mtimeMs: fileStat.mtimeMs
|
|
3884
3884
|
});
|
|
3885
3885
|
}
|
|
3886
|
-
return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path:
|
|
3886
|
+
return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path26, mtime }) => ({ path: path26, mtime }));
|
|
3887
3887
|
}
|
|
3888
3888
|
async function readCronOutput(outputPath) {
|
|
3889
3889
|
const content = await readFile3(outputPath, "utf8");
|
|
@@ -3970,7 +3970,7 @@ import os2 from "os";
|
|
|
3970
3970
|
import path5 from "path";
|
|
3971
3971
|
|
|
3972
3972
|
// src/constants.ts
|
|
3973
|
-
var LINK_VERSION = "0.
|
|
3973
|
+
var LINK_VERSION = "0.4.0";
|
|
3974
3974
|
var LINK_COMMAND = "hermeslink";
|
|
3975
3975
|
var LINK_DEFAULT_PORT = 52379;
|
|
3976
3976
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -4455,6 +4455,9 @@ var DEFAULT_START_TIMEOUT_MS = 12e3;
|
|
|
4455
4455
|
var HEALTH_TIMEOUT_MS = 1500;
|
|
4456
4456
|
var MIN_API_SERVER_VERSION = "0.4.0";
|
|
4457
4457
|
var PROFILE_NAME_PATTERN = /^[a-zA-Z0-9._-]{1,64}$/u;
|
|
4458
|
+
var DASHBOARD_STATUS_URL = "http://127.0.0.1:9119/api/status";
|
|
4459
|
+
var DASHBOARD_STATUS_TIMEOUT_MS = 1500;
|
|
4460
|
+
var MAX_VERSION_LOG_OUTPUT_LENGTH = 1200;
|
|
4458
4461
|
var gatewayStartInFlightByProfile = /* @__PURE__ */ new Map();
|
|
4459
4462
|
async function ensureHermesApiServerAvailable(options = {}) {
|
|
4460
4463
|
const profileName = normalizeProfileName(options.profileName);
|
|
@@ -4571,29 +4574,57 @@ async function reloadHermesGateway(options = {}) {
|
|
|
4571
4574
|
}
|
|
4572
4575
|
return ensureHermesApiServerAvailable({ ...options, forceRestart: true });
|
|
4573
4576
|
}
|
|
4574
|
-
async function readHermesVersion() {
|
|
4575
|
-
const { stdout } = await execHermesVersion();
|
|
4576
|
-
const raw = stdout.trim();
|
|
4577
|
-
const version = parseHermesVersion(raw);
|
|
4578
|
-
return {
|
|
4579
|
-
raw,
|
|
4580
|
-
version,
|
|
4581
|
-
supportsApiServer: version ? compareSemver(version, MIN_API_SERVER_VERSION) >= 0 : null
|
|
4582
|
-
};
|
|
4583
|
-
}
|
|
4584
|
-
async function execHermesVersion() {
|
|
4577
|
+
async function readHermesVersion(options = {}) {
|
|
4585
4578
|
try {
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
} catch {
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4579
|
+
const { stdout } = await execHermesVersion(options.logger);
|
|
4580
|
+
const raw = stdout.trim();
|
|
4581
|
+
const version = parseHermesVersion(raw);
|
|
4582
|
+
return buildHermesVersionInfo(raw, version);
|
|
4583
|
+
} catch (cliError) {
|
|
4584
|
+
const dashboardStatusUrl = options.dashboardStatusUrl ?? DASHBOARD_STATUS_URL;
|
|
4585
|
+
void options.logger?.warn("hermes_version_dashboard_fallback_requested", {
|
|
4586
|
+
dashboard_status_url: dashboardStatusUrl,
|
|
4587
|
+
reason: cliError instanceof Error ? cliError.message : String(cliError)
|
|
4594
4588
|
});
|
|
4589
|
+
try {
|
|
4590
|
+
const fallback = await readHermesDashboardVersion({
|
|
4591
|
+
fetchImpl: options.fetchImpl,
|
|
4592
|
+
statusUrl: dashboardStatusUrl,
|
|
4593
|
+
timeoutMs: options.dashboardTimeoutMs
|
|
4594
|
+
});
|
|
4595
|
+
void options.logger?.info("hermes_version_dashboard_fallback_succeeded", {
|
|
4596
|
+
dashboard_status_url: dashboardStatusUrl,
|
|
4597
|
+
hermes_version: fallback.version
|
|
4598
|
+
});
|
|
4599
|
+
return fallback;
|
|
4600
|
+
} catch (dashboardError) {
|
|
4601
|
+
void options.logger?.warn("hermes_version_dashboard_fallback_failed", {
|
|
4602
|
+
dashboard_status_url: dashboardStatusUrl,
|
|
4603
|
+
error: dashboardError instanceof Error ? dashboardError.message : String(dashboardError)
|
|
4604
|
+
});
|
|
4605
|
+
throw new Error(
|
|
4606
|
+
`Hermes version detection failed. CLI: ${cliError instanceof Error ? cliError.message : String(cliError)}; dashboard fallback: ${dashboardError instanceof Error ? dashboardError.message : String(dashboardError)}`
|
|
4607
|
+
);
|
|
4608
|
+
}
|
|
4595
4609
|
}
|
|
4596
4610
|
}
|
|
4611
|
+
async function execHermesVersion(logger) {
|
|
4612
|
+
const hermesBin = resolveHermesBin();
|
|
4613
|
+
const failures = [];
|
|
4614
|
+
for (const args of [["version"], ["--version"]]) {
|
|
4615
|
+
try {
|
|
4616
|
+
return await execFileAsync2(hermesBin, args, {
|
|
4617
|
+
timeout: 5e3,
|
|
4618
|
+
windowsHide: true
|
|
4619
|
+
});
|
|
4620
|
+
} catch (error) {
|
|
4621
|
+
const failure = describeVersionCommandFailure(hermesBin, args, error);
|
|
4622
|
+
failures.push(failure.summary);
|
|
4623
|
+
void logger?.warn("hermes_version_cli_command_failed", failure.fields);
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
throw new Error(failures.join("; "));
|
|
4627
|
+
}
|
|
4597
4628
|
function assertHermesRunsApiSupported(version, status) {
|
|
4598
4629
|
if (status !== 404) {
|
|
4599
4630
|
return;
|
|
@@ -4617,7 +4648,7 @@ async function startHermesGatewayOnce(paths, profileName, logger) {
|
|
|
4617
4648
|
return await gatewayStartInFlightByProfile.get(profileName);
|
|
4618
4649
|
}
|
|
4619
4650
|
async function startHermesGateway(paths, profileName, logger) {
|
|
4620
|
-
const version = await readHermesVersion().catch((error) => {
|
|
4651
|
+
const version = await readHermesVersion({ logger }).catch((error) => {
|
|
4621
4652
|
void logger?.error("gateway_hermes_cli_unavailable", {
|
|
4622
4653
|
error: error instanceof Error ? error.message : String(error)
|
|
4623
4654
|
});
|
|
@@ -4729,7 +4760,7 @@ async function restartHermesGatewayServiceIfAvailable(options) {
|
|
|
4729
4760
|
return {
|
|
4730
4761
|
pid: null,
|
|
4731
4762
|
logPath,
|
|
4732
|
-
version: await readHermesVersion().catch(() => null),
|
|
4763
|
+
version: await readHermesVersion({ logger: options.logger }).catch(() => null),
|
|
4733
4764
|
...logHint ? { logHint } : {}
|
|
4734
4765
|
};
|
|
4735
4766
|
}
|
|
@@ -4972,6 +5003,72 @@ function parseHermesVersion(value) {
|
|
|
4972
5003
|
const match = /\bv?(\d+\.\d+\.\d+)\b/u.exec(value);
|
|
4973
5004
|
return match?.[1] ?? null;
|
|
4974
5005
|
}
|
|
5006
|
+
function buildHermesVersionInfo(raw, version) {
|
|
5007
|
+
return {
|
|
5008
|
+
raw,
|
|
5009
|
+
version,
|
|
5010
|
+
supportsApiServer: version ? compareSemver(version, MIN_API_SERVER_VERSION) >= 0 : null
|
|
5011
|
+
};
|
|
5012
|
+
}
|
|
5013
|
+
async function readHermesDashboardVersion(options = {}) {
|
|
5014
|
+
const fetcher = options.fetchImpl ?? fetch;
|
|
5015
|
+
const controller = new AbortController();
|
|
5016
|
+
const timer = setTimeout(
|
|
5017
|
+
() => controller.abort(),
|
|
5018
|
+
options.timeoutMs ?? DASHBOARD_STATUS_TIMEOUT_MS
|
|
5019
|
+
);
|
|
5020
|
+
try {
|
|
5021
|
+
const response = await fetcher(options.statusUrl ?? DASHBOARD_STATUS_URL, {
|
|
5022
|
+
method: "GET",
|
|
5023
|
+
headers: { accept: "application/json" },
|
|
5024
|
+
signal: controller.signal
|
|
5025
|
+
});
|
|
5026
|
+
if (!response.ok) {
|
|
5027
|
+
throw new Error(`Hermes dashboard returned HTTP ${response.status}`);
|
|
5028
|
+
}
|
|
5029
|
+
const payload = await response.json().catch(() => null);
|
|
5030
|
+
const record = toRecord2(payload);
|
|
5031
|
+
const versionText = readString4(record, "version");
|
|
5032
|
+
if (!versionText) {
|
|
5033
|
+
throw new Error("Hermes dashboard status did not include a version");
|
|
5034
|
+
}
|
|
5035
|
+
const raw = truncateVersionLogOutput(JSON.stringify(record));
|
|
5036
|
+
const version = parseHermesVersion(versionText) ?? parseHermesVersion(raw);
|
|
5037
|
+
return buildHermesVersionInfo(raw, version);
|
|
5038
|
+
} catch (error) {
|
|
5039
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
5040
|
+
throw new Error("Hermes dashboard version probe timed out");
|
|
5041
|
+
}
|
|
5042
|
+
throw error;
|
|
5043
|
+
} finally {
|
|
5044
|
+
clearTimeout(timer);
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
function describeVersionCommandFailure(hermesBin, args, error) {
|
|
5048
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5049
|
+
const output = truncateVersionLogOutput(readExecErrorOutput2(error));
|
|
5050
|
+
return {
|
|
5051
|
+
summary: `${hermesBin} ${args.join(" ")} failed: ${message}`,
|
|
5052
|
+
fields: {
|
|
5053
|
+
hermes_bin: hermesBin,
|
|
5054
|
+
command: args.join(" "),
|
|
5055
|
+
error: message,
|
|
5056
|
+
...output ? { output } : {}
|
|
5057
|
+
}
|
|
5058
|
+
};
|
|
5059
|
+
}
|
|
5060
|
+
function readExecErrorOutput2(error) {
|
|
5061
|
+
if (typeof error !== "object" || error === null) {
|
|
5062
|
+
return "";
|
|
5063
|
+
}
|
|
5064
|
+
const stdout = "stdout" in error && error.stdout != null ? String(error.stdout) : "";
|
|
5065
|
+
const stderr = "stderr" in error && error.stderr != null ? String(error.stderr) : "";
|
|
5066
|
+
return `${stdout}
|
|
5067
|
+
${stderr}`.trim();
|
|
5068
|
+
}
|
|
5069
|
+
function truncateVersionLogOutput(value) {
|
|
5070
|
+
return value.length > MAX_VERSION_LOG_OUTPUT_LENGTH ? `${value.slice(0, MAX_VERSION_LOG_OUTPUT_LENGTH)}...` : value;
|
|
5071
|
+
}
|
|
4975
5072
|
function compareSemver(left, right) {
|
|
4976
5073
|
const leftParts = left.split(".").map((part) => Number.parseInt(part, 10));
|
|
4977
5074
|
const rightParts = right.split(".").map((part) => Number.parseInt(part, 10));
|
|
@@ -6663,6 +6760,70 @@ function safePathSegment(value, fallback) {
|
|
|
6663
6760
|
return safe.length > 0 ? safe.slice(0, 120) : fallback;
|
|
6664
6761
|
}
|
|
6665
6762
|
|
|
6763
|
+
// src/conversations/conversation-clear-plans.ts
|
|
6764
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
6765
|
+
import { mkdir as mkdir7 } from "fs/promises";
|
|
6766
|
+
import path11 from "path";
|
|
6767
|
+
var PLAN_ID_PATTERN = /^clear_[a-f0-9]{32}$/u;
|
|
6768
|
+
var ConversationClearPlanStore = class {
|
|
6769
|
+
constructor(paths) {
|
|
6770
|
+
this.paths = paths;
|
|
6771
|
+
}
|
|
6772
|
+
paths;
|
|
6773
|
+
async create(conversationIds) {
|
|
6774
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6775
|
+
const plan = {
|
|
6776
|
+
id: `clear_${randomUUID5().replaceAll("-", "")}`,
|
|
6777
|
+
status: "prepared",
|
|
6778
|
+
created_at: now,
|
|
6779
|
+
updated_at: now,
|
|
6780
|
+
total_count: conversationIds.length,
|
|
6781
|
+
deleted_count: 0,
|
|
6782
|
+
failed_count: 0,
|
|
6783
|
+
conversation_ids: conversationIds,
|
|
6784
|
+
conversations: []
|
|
6785
|
+
};
|
|
6786
|
+
await this.write(plan);
|
|
6787
|
+
return plan;
|
|
6788
|
+
}
|
|
6789
|
+
async read(planId) {
|
|
6790
|
+
const normalizedPlanId = normalizePlanId(planId);
|
|
6791
|
+
const plan = await readJsonFile(
|
|
6792
|
+
this.planPath(normalizedPlanId)
|
|
6793
|
+
);
|
|
6794
|
+
if (!plan) {
|
|
6795
|
+
throw new LinkHttpError(
|
|
6796
|
+
404,
|
|
6797
|
+
"conversation_clear_plan_not_found",
|
|
6798
|
+
"Conversation clear plan was not found"
|
|
6799
|
+
);
|
|
6800
|
+
}
|
|
6801
|
+
return plan;
|
|
6802
|
+
}
|
|
6803
|
+
async write(plan) {
|
|
6804
|
+
const normalizedPlanId = normalizePlanId(plan.id);
|
|
6805
|
+
await mkdir7(this.plansDir(), { recursive: true, mode: 448 });
|
|
6806
|
+
await writeJsonFile(this.planPath(normalizedPlanId), plan);
|
|
6807
|
+
}
|
|
6808
|
+
plansDir() {
|
|
6809
|
+
return path11.join(this.paths.indexesDir, "conversation-clear-plans");
|
|
6810
|
+
}
|
|
6811
|
+
planPath(planId) {
|
|
6812
|
+
return path11.join(this.plansDir(), `${planId}.json`);
|
|
6813
|
+
}
|
|
6814
|
+
};
|
|
6815
|
+
function normalizePlanId(planId) {
|
|
6816
|
+
const normalized = planId.trim();
|
|
6817
|
+
if (!PLAN_ID_PATTERN.test(normalized)) {
|
|
6818
|
+
throw new LinkHttpError(
|
|
6819
|
+
400,
|
|
6820
|
+
"conversation_clear_plan_id_invalid",
|
|
6821
|
+
"Conversation clear plan id is invalid"
|
|
6822
|
+
);
|
|
6823
|
+
}
|
|
6824
|
+
return normalized;
|
|
6825
|
+
}
|
|
6826
|
+
|
|
6666
6827
|
// src/conversations/conversation-session-ids.ts
|
|
6667
6828
|
function normalizeHermesSessionIds(values) {
|
|
6668
6829
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -6713,8 +6874,134 @@ var MAX_UPLOADED_BLOB_BYTES = 50 * 1024 * 1024;
|
|
|
6713
6874
|
var ConversationMaintenanceCoordinator = class {
|
|
6714
6875
|
constructor(deps) {
|
|
6715
6876
|
this.deps = deps;
|
|
6877
|
+
this.clearPlans = new ConversationClearPlanStore(deps.paths);
|
|
6716
6878
|
}
|
|
6717
6879
|
deps;
|
|
6880
|
+
clearPlans;
|
|
6881
|
+
async prepareClearAllConversationPlan() {
|
|
6882
|
+
const targets = [];
|
|
6883
|
+
for (const conversationId of await this.deps.store.listConversationIds()) {
|
|
6884
|
+
const manifest = await this.deps.store.readManifest(conversationId).catch(() => null);
|
|
6885
|
+
if (manifest?.status !== "active") {
|
|
6886
|
+
continue;
|
|
6887
|
+
}
|
|
6888
|
+
targets.push({
|
|
6889
|
+
id: conversationId,
|
|
6890
|
+
updatedAt: manifest.updated_at
|
|
6891
|
+
});
|
|
6892
|
+
}
|
|
6893
|
+
targets.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
6894
|
+
return this.clearPlans.create(
|
|
6895
|
+
targets.map((target) => target.id)
|
|
6896
|
+
);
|
|
6897
|
+
}
|
|
6898
|
+
async readClearAllConversationPlan(planId) {
|
|
6899
|
+
return this.clearPlans.read(planId);
|
|
6900
|
+
}
|
|
6901
|
+
async executeClearAllConversationPlan(planId) {
|
|
6902
|
+
let plan = await this.clearPlans.read(planId);
|
|
6903
|
+
if (plan.status === "completed") {
|
|
6904
|
+
return plan;
|
|
6905
|
+
}
|
|
6906
|
+
if (plan.status === "failed") {
|
|
6907
|
+
throw new LinkHttpError(
|
|
6908
|
+
409,
|
|
6909
|
+
"conversation_clear_plan_already_failed",
|
|
6910
|
+
"Conversation clear plan has already failed"
|
|
6911
|
+
);
|
|
6912
|
+
}
|
|
6913
|
+
const results = [];
|
|
6914
|
+
if (plan.status !== "executing") {
|
|
6915
|
+
plan = await this.writeClearPlan({
|
|
6916
|
+
...plan,
|
|
6917
|
+
status: "executing",
|
|
6918
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6919
|
+
conversations: results,
|
|
6920
|
+
deleted_count: 0,
|
|
6921
|
+
failed_count: 0
|
|
6922
|
+
});
|
|
6923
|
+
}
|
|
6924
|
+
for (const conversationId of plan.conversation_ids) {
|
|
6925
|
+
try {
|
|
6926
|
+
const deleted = await this.deleteConversation(conversationId);
|
|
6927
|
+
results.push({ ...deleted, status: "deleted" });
|
|
6928
|
+
} catch (error) {
|
|
6929
|
+
if (error instanceof LinkHttpError && error.code === "conversation_not_found") {
|
|
6930
|
+
results.push({
|
|
6931
|
+
conversation_id: conversationId,
|
|
6932
|
+
status: "deleted",
|
|
6933
|
+
hermes_deleted: false,
|
|
6934
|
+
deleted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6935
|
+
});
|
|
6936
|
+
} else {
|
|
6937
|
+
results.push({
|
|
6938
|
+
conversation_id: conversationId,
|
|
6939
|
+
status: "failed",
|
|
6940
|
+
error: {
|
|
6941
|
+
code: error instanceof LinkHttpError ? error.code : "internal_error",
|
|
6942
|
+
message: error instanceof Error ? error.message : "Internal error"
|
|
6943
|
+
}
|
|
6944
|
+
});
|
|
6945
|
+
}
|
|
6946
|
+
}
|
|
6947
|
+
plan = await this.writeClearPlan({
|
|
6948
|
+
...plan,
|
|
6949
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6950
|
+
conversations: [...results],
|
|
6951
|
+
deleted_count: results.filter((result) => result.status === "deleted").length,
|
|
6952
|
+
failed_count: results.filter((result) => result.status === "failed").length
|
|
6953
|
+
});
|
|
6954
|
+
}
|
|
6955
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6956
|
+
return this.writeClearPlan({
|
|
6957
|
+
...plan,
|
|
6958
|
+
status: plan.failed_count === 0 ? "completed" : "failed",
|
|
6959
|
+
updated_at: completedAt,
|
|
6960
|
+
completed_at: completedAt
|
|
6961
|
+
});
|
|
6962
|
+
}
|
|
6963
|
+
async startClearAllConversationPlan(planId) {
|
|
6964
|
+
const plan = await this.clearPlans.read(planId);
|
|
6965
|
+
if (plan.status === "completed" || plan.status === "executing") {
|
|
6966
|
+
return plan;
|
|
6967
|
+
}
|
|
6968
|
+
if (plan.status === "failed") {
|
|
6969
|
+
throw new LinkHttpError(
|
|
6970
|
+
409,
|
|
6971
|
+
"conversation_clear_plan_already_failed",
|
|
6972
|
+
"Conversation clear plan has already failed"
|
|
6973
|
+
);
|
|
6974
|
+
}
|
|
6975
|
+
const started = await this.writeClearPlan({
|
|
6976
|
+
...plan,
|
|
6977
|
+
status: "executing",
|
|
6978
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6979
|
+
conversations: [],
|
|
6980
|
+
deleted_count: 0,
|
|
6981
|
+
failed_count: 0
|
|
6982
|
+
});
|
|
6983
|
+
void this.executeClearAllConversationPlan(started.id).catch(
|
|
6984
|
+
async (error) => {
|
|
6985
|
+
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6986
|
+
await this.writeClearPlan({
|
|
6987
|
+
...started,
|
|
6988
|
+
status: "failed",
|
|
6989
|
+
updated_at: failedAt,
|
|
6990
|
+
completed_at: failedAt,
|
|
6991
|
+
failed_count: started.total_count,
|
|
6992
|
+
conversations: started.conversation_ids.map((conversationId) => ({
|
|
6993
|
+
conversation_id: conversationId,
|
|
6994
|
+
status: "failed",
|
|
6995
|
+
error: {
|
|
6996
|
+
code: error instanceof LinkHttpError ? error.code : "internal_error",
|
|
6997
|
+
message: error instanceof Error ? error.message : "Internal error"
|
|
6998
|
+
}
|
|
6999
|
+
}))
|
|
7000
|
+
}).catch(() => void 0);
|
|
7001
|
+
}
|
|
7002
|
+
);
|
|
7003
|
+
return started;
|
|
7004
|
+
}
|
|
6718
7005
|
async deleteConversation(conversationId) {
|
|
6719
7006
|
return this.deps.withConversationLock(
|
|
6720
7007
|
conversationId,
|
|
@@ -6920,6 +7207,10 @@ var ConversationMaintenanceCoordinator = class {
|
|
|
6920
7207
|
async listConversationBlobIds(conversationId) {
|
|
6921
7208
|
return listConversationBlobIds(this.deps.paths, conversationId);
|
|
6922
7209
|
}
|
|
7210
|
+
async writeClearPlan(plan) {
|
|
7211
|
+
await this.clearPlans.write(plan);
|
|
7212
|
+
return plan;
|
|
7213
|
+
}
|
|
6923
7214
|
};
|
|
6924
7215
|
function isVoiceAttachmentInput(attachment) {
|
|
6925
7216
|
return attachment.kind === "voice" || attachment.type === "voice" || attachment.is_voice_note === true || attachment.isVoiceNote === true;
|
|
@@ -6999,14 +7290,14 @@ function isUsableLanIpv4(value) {
|
|
|
6999
7290
|
|
|
7000
7291
|
// src/hermes/session-title.ts
|
|
7001
7292
|
import { stat as stat7 } from "fs/promises";
|
|
7002
|
-
import
|
|
7293
|
+
import path12 from "path";
|
|
7003
7294
|
async function readHermesSessionTitle(sessionId, paths, profileName) {
|
|
7004
7295
|
const trimmedSessionId = sessionId.trim();
|
|
7005
7296
|
if (!trimmedSessionId) {
|
|
7006
7297
|
return void 0;
|
|
7007
7298
|
}
|
|
7008
7299
|
const resolvedProfileName = isValidProfileName(profileName) ? profileName : "default";
|
|
7009
|
-
const dbPath =
|
|
7300
|
+
const dbPath = path12.join(
|
|
7010
7301
|
resolveHermesProfileDir(resolvedProfileName),
|
|
7011
7302
|
"state.db"
|
|
7012
7303
|
);
|
|
@@ -7027,7 +7318,7 @@ async function readHermesCompressionTip(sessionId, paths, profileName) {
|
|
|
7027
7318
|
return void 0;
|
|
7028
7319
|
}
|
|
7029
7320
|
const resolvedProfileName = isValidProfileName(profileName) ? profileName : "default";
|
|
7030
|
-
const dbPath =
|
|
7321
|
+
const dbPath = path12.join(
|
|
7031
7322
|
resolveHermesProfileDir(resolvedProfileName),
|
|
7032
7323
|
"state.db"
|
|
7033
7324
|
);
|
|
@@ -7122,11 +7413,19 @@ var ConversationMetadataCoordinator = class {
|
|
|
7122
7413
|
const snapshot = options.snapshot ?? await this.deps.store.readSnapshot(manifest.id).catch(
|
|
7123
7414
|
() => createEmptySnapshot()
|
|
7124
7415
|
);
|
|
7125
|
-
const
|
|
7416
|
+
const profileName = latestRuntimeRun(snapshot)?.profile ?? manifest.profile_name_snapshot ?? manifest.profile;
|
|
7417
|
+
const compressionRootSessionId = manifest.hermes_lineage?.kind === "compression" ? manifest.hermes_lineage.root_session_id : void 0;
|
|
7418
|
+
const rootTitle = compressionRootSessionId ? await readHermesSessionTitle(
|
|
7419
|
+
compressionRootSessionId,
|
|
7420
|
+
this.deps.paths,
|
|
7421
|
+
profileName
|
|
7422
|
+
) : void 0;
|
|
7423
|
+
const tipTitle = await readHermesSessionTitle(
|
|
7126
7424
|
manifest.hermes_session_id,
|
|
7127
7425
|
this.deps.paths,
|
|
7128
|
-
|
|
7426
|
+
profileName
|
|
7129
7427
|
);
|
|
7428
|
+
const title = rootTitle ?? (compressionRootSessionId ? stripCompressionTitleSuffix(tipTitle) : tipTitle);
|
|
7130
7429
|
if (!title || title === manifest.title) {
|
|
7131
7430
|
return manifest;
|
|
7132
7431
|
}
|
|
@@ -7440,9 +7739,18 @@ function canAutoGenerateTitle(manifest) {
|
|
|
7440
7739
|
function normalizeConversationTitleSource(title) {
|
|
7441
7740
|
return isDefaultConversationTitle(title) ? "default" : "hermes";
|
|
7442
7741
|
}
|
|
7742
|
+
function stripCompressionTitleSuffix(value) {
|
|
7743
|
+
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
7744
|
+
if (!normalized) {
|
|
7745
|
+
return void 0;
|
|
7746
|
+
}
|
|
7747
|
+
const match = /^(.*?) #\d+$/u.exec(normalized);
|
|
7748
|
+
const stripped = match?.[1]?.trim();
|
|
7749
|
+
return stripped || normalized;
|
|
7750
|
+
}
|
|
7443
7751
|
|
|
7444
7752
|
// src/conversations/conversation-turns.ts
|
|
7445
|
-
import { randomUUID as
|
|
7753
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
7446
7754
|
var MESSAGE_ORDER_STEP_MS = 10;
|
|
7447
7755
|
var ASSISTANT_ORDER_OFFSET_MS = 1;
|
|
7448
7756
|
function createAgentTurnDraft(input) {
|
|
@@ -7676,10 +7984,10 @@ function createAssistantMessage(input) {
|
|
|
7676
7984
|
};
|
|
7677
7985
|
}
|
|
7678
7986
|
function createMessageId() {
|
|
7679
|
-
return `msg_${
|
|
7987
|
+
return `msg_${randomUUID6().replaceAll("-", "")}`;
|
|
7680
7988
|
}
|
|
7681
7989
|
function createRunId() {
|
|
7682
|
-
return `run_${
|
|
7990
|
+
return `run_${randomUUID6().replaceAll("-", "")}`;
|
|
7683
7991
|
}
|
|
7684
7992
|
function hasActiveOrQueuedRuns(snapshot) {
|
|
7685
7993
|
return snapshot.runs.some(
|
|
@@ -8794,20 +9102,20 @@ function hydrateAgentEventBlocks(blocks, agentEvents) {
|
|
|
8794
9102
|
// src/conversations/conversation-store.ts
|
|
8795
9103
|
import {
|
|
8796
9104
|
appendFile as appendFile2,
|
|
8797
|
-
mkdir as
|
|
9105
|
+
mkdir as mkdir8,
|
|
8798
9106
|
readdir as readdir5,
|
|
8799
9107
|
readFile as readFile7,
|
|
8800
9108
|
rm as rm5,
|
|
8801
9109
|
writeFile as writeFile2
|
|
8802
9110
|
} from "fs/promises";
|
|
8803
|
-
import
|
|
9111
|
+
import path13 from "path";
|
|
8804
9112
|
var ConversationStore = class {
|
|
8805
9113
|
constructor(paths) {
|
|
8806
9114
|
this.paths = paths;
|
|
8807
9115
|
}
|
|
8808
9116
|
paths;
|
|
8809
9117
|
async ensureConversationsDir() {
|
|
8810
|
-
await
|
|
9118
|
+
await mkdir8(this.paths.conversationsDir, { recursive: true, mode: 448 });
|
|
8811
9119
|
}
|
|
8812
9120
|
async listConversationIds() {
|
|
8813
9121
|
await this.ensureConversationsDir();
|
|
@@ -8822,7 +9130,7 @@ var ConversationStore = class {
|
|
|
8822
9130
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
8823
9131
|
}
|
|
8824
9132
|
async createConversation(manifest, snapshot = createEmptySnapshot2()) {
|
|
8825
|
-
await
|
|
9133
|
+
await mkdir8(this.conversationDir(manifest.id), {
|
|
8826
9134
|
recursive: true,
|
|
8827
9135
|
mode: 448
|
|
8828
9136
|
});
|
|
@@ -8862,7 +9170,7 @@ var ConversationStore = class {
|
|
|
8862
9170
|
conversation_id: conversationId,
|
|
8863
9171
|
created_at: now
|
|
8864
9172
|
};
|
|
8865
|
-
await
|
|
9173
|
+
await mkdir8(this.conversationDir(conversationId), {
|
|
8866
9174
|
recursive: true,
|
|
8867
9175
|
mode: 448
|
|
8868
9176
|
});
|
|
@@ -8916,23 +9224,23 @@ var ConversationStore = class {
|
|
|
8916
9224
|
return manifest?.status === "active";
|
|
8917
9225
|
}
|
|
8918
9226
|
removeConversationAttachments(conversationId) {
|
|
8919
|
-
return rm5(
|
|
9227
|
+
return rm5(path13.join(this.conversationDir(conversationId), "attachments"), {
|
|
8920
9228
|
recursive: true,
|
|
8921
9229
|
force: true
|
|
8922
9230
|
});
|
|
8923
9231
|
}
|
|
8924
9232
|
conversationDir(conversationId) {
|
|
8925
9233
|
assertValidConversationId(conversationId);
|
|
8926
|
-
return
|
|
9234
|
+
return path13.join(this.paths.conversationsDir, conversationId);
|
|
8927
9235
|
}
|
|
8928
9236
|
manifestPath(conversationId) {
|
|
8929
|
-
return
|
|
9237
|
+
return path13.join(this.conversationDir(conversationId), "manifest.json");
|
|
8930
9238
|
}
|
|
8931
9239
|
snapshotPath(conversationId) {
|
|
8932
|
-
return
|
|
9240
|
+
return path13.join(this.conversationDir(conversationId), "snapshot.json");
|
|
8933
9241
|
}
|
|
8934
9242
|
eventsPath(conversationId) {
|
|
8935
|
-
return
|
|
9243
|
+
return path13.join(this.conversationDir(conversationId), "events.ndjson");
|
|
8936
9244
|
}
|
|
8937
9245
|
};
|
|
8938
9246
|
function createEmptySnapshot2() {
|
|
@@ -8943,14 +9251,14 @@ function isNodeError9(error, code) {
|
|
|
8943
9251
|
}
|
|
8944
9252
|
|
|
8945
9253
|
// src/conversations/hermes-session-sync.ts
|
|
8946
|
-
import { randomUUID as
|
|
9254
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
8947
9255
|
import { readdir as readdir7, readFile as readFile9, stat as stat9 } from "fs/promises";
|
|
8948
9256
|
import os4 from "os";
|
|
8949
|
-
import
|
|
9257
|
+
import path15 from "path";
|
|
8950
9258
|
|
|
8951
9259
|
// src/conversations/delivery-import.ts
|
|
8952
9260
|
import { lstat as lstat2, readFile as readFile8, readdir as readdir6, stat as stat8 } from "fs/promises";
|
|
8953
|
-
import
|
|
9261
|
+
import path14 from "path";
|
|
8954
9262
|
var MAX_IMPORTED_BLOB_BYTES = 100 * 1024 * 1024;
|
|
8955
9263
|
var MAX_MEDIA_IMPORT_FAILURES = 20;
|
|
8956
9264
|
var MAX_DELIVERY_FILES = 50;
|
|
@@ -9021,16 +9329,16 @@ var SUPPORTED_DELIVERY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
9021
9329
|
".m4a"
|
|
9022
9330
|
]);
|
|
9023
9331
|
function resolveDeliveryStagingTarget(paths, stagingDir) {
|
|
9024
|
-
const resolvedDir =
|
|
9025
|
-
const relative =
|
|
9026
|
-
if (!relative || relative.startsWith("..") ||
|
|
9332
|
+
const resolvedDir = path14.resolve(stagingDir);
|
|
9333
|
+
const relative = path14.relative(path14.resolve(paths.conversationsDir), resolvedDir);
|
|
9334
|
+
if (!relative || relative.startsWith("..") || path14.isAbsolute(relative)) {
|
|
9027
9335
|
throw new LinkHttpError(
|
|
9028
9336
|
400,
|
|
9029
9337
|
"delivery_staging_invalid",
|
|
9030
9338
|
"delivery staging directory must be inside Hermes Link conversations"
|
|
9031
9339
|
);
|
|
9032
9340
|
}
|
|
9033
|
-
const segments = relative.split(
|
|
9341
|
+
const segments = relative.split(path14.sep);
|
|
9034
9342
|
if (segments.length !== 3 || segments[1] !== DELIVERY_STAGING_SEGMENT || !segments[0] || !segments[2]) {
|
|
9035
9343
|
throw new LinkHttpError(
|
|
9036
9344
|
400,
|
|
@@ -9066,7 +9374,7 @@ async function collectStagedDeliveryReferences(stagingDir) {
|
|
|
9066
9374
|
return entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).filter((entry) => isSupportedDeliveryFilename(entry.name)).sort(
|
|
9067
9375
|
(left, right) => left.name.localeCompare(right.name, "en", { numeric: true })
|
|
9068
9376
|
).slice(0, MAX_DELIVERY_FILES).map((entry) => {
|
|
9069
|
-
const sourcePath =
|
|
9377
|
+
const sourcePath = path14.join(stagingDir, entry.name);
|
|
9070
9378
|
const mime = inferMimeType(sourcePath);
|
|
9071
9379
|
return {
|
|
9072
9380
|
path: sourcePath,
|
|
@@ -9252,7 +9560,7 @@ async function writeBlobFromFile(deps, conversationId, source) {
|
|
|
9252
9560
|
}
|
|
9253
9561
|
return deps.writeBlob(conversationId, {
|
|
9254
9562
|
bytes: await readFile8(sourcePath),
|
|
9255
|
-
filename:
|
|
9563
|
+
filename: path14.basename(sourcePath),
|
|
9256
9564
|
mime: source.mime ?? inferMimeType(sourcePath)
|
|
9257
9565
|
});
|
|
9258
9566
|
}
|
|
@@ -9265,7 +9573,7 @@ function describeMediaImportFailure(reference, sourceKey, error) {
|
|
|
9265
9573
|
};
|
|
9266
9574
|
}
|
|
9267
9575
|
function isSupportedDeliveryFilename(filename) {
|
|
9268
|
-
return SUPPORTED_DELIVERY_EXTENSIONS.has(
|
|
9576
|
+
return SUPPORTED_DELIVERY_EXTENSIONS.has(path14.extname(filename).toLowerCase());
|
|
9269
9577
|
}
|
|
9270
9578
|
function readString8(payload, key) {
|
|
9271
9579
|
const value = payload[key];
|
|
@@ -9322,7 +9630,7 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
9322
9630
|
const candidates = [];
|
|
9323
9631
|
for (const profileName of profileNames) {
|
|
9324
9632
|
const profileDir = resolveHermesProfileDir(profileName);
|
|
9325
|
-
const dbPath =
|
|
9633
|
+
const dbPath = path15.join(profileDir, "state.db");
|
|
9326
9634
|
const sessions = await listProfileSessions(dbPath).catch((error) => {
|
|
9327
9635
|
result.errors.push({
|
|
9328
9636
|
profile: profileName,
|
|
@@ -9352,16 +9660,19 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
9352
9660
|
const importableCandidates = candidates.slice(0, maxImports);
|
|
9353
9661
|
result.skipped_over_limit = Math.max(0, candidates.length - maxImports);
|
|
9354
9662
|
for (const candidate of importableCandidates) {
|
|
9355
|
-
|
|
9663
|
+
const candidateSessionIds = lineageSessionIds(candidate);
|
|
9664
|
+
const knownConversationIds = findKnownConversationIdsForCandidate(
|
|
9665
|
+
knownHermesSessions,
|
|
9666
|
+
candidate
|
|
9667
|
+
);
|
|
9668
|
+
if (knownConversationIds.length > 0) {
|
|
9356
9669
|
result.skipped_existing += 1;
|
|
9357
|
-
const reprojected = await
|
|
9670
|
+
const reprojected = await mergeExistingHermesConversation({
|
|
9358
9671
|
paths,
|
|
9359
9672
|
store,
|
|
9360
9673
|
logger,
|
|
9361
9674
|
candidate,
|
|
9362
|
-
conversationIds:
|
|
9363
|
-
candidate.session.id
|
|
9364
|
-
) ?? []
|
|
9675
|
+
conversationIds: knownConversationIds
|
|
9365
9676
|
}).catch((error) => {
|
|
9366
9677
|
result.errors.push({
|
|
9367
9678
|
profile: candidate.profileName,
|
|
@@ -9372,14 +9683,22 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
9372
9683
|
if (reprojected) {
|
|
9373
9684
|
result.reprojected_count += 1;
|
|
9374
9685
|
}
|
|
9686
|
+
for (const sessionId of candidateSessionIds) {
|
|
9687
|
+
knownHermesSessions.sessionIds.add(sessionId);
|
|
9688
|
+
}
|
|
9689
|
+
continue;
|
|
9690
|
+
}
|
|
9691
|
+
if (candidateSessionIds.some(
|
|
9692
|
+
(sessionId) => knownHermesSessions.sessionIds.has(sessionId)
|
|
9693
|
+
)) {
|
|
9694
|
+
result.skipped_existing += 1;
|
|
9375
9695
|
continue;
|
|
9376
9696
|
}
|
|
9377
9697
|
const imported = await importHermesSession({
|
|
9378
9698
|
paths,
|
|
9379
9699
|
store,
|
|
9380
9700
|
logger,
|
|
9381
|
-
candidate
|
|
9382
|
-
existingHermesSessionIds: knownHermesSessions.ids
|
|
9701
|
+
candidate
|
|
9383
9702
|
}).catch((error) => {
|
|
9384
9703
|
result.errors.push({
|
|
9385
9704
|
profile: candidate.profileName,
|
|
@@ -9389,6 +9708,9 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
9389
9708
|
});
|
|
9390
9709
|
if (imported) {
|
|
9391
9710
|
result.imported_count += 1;
|
|
9711
|
+
for (const sessionId of candidateSessionIds) {
|
|
9712
|
+
knownHermesSessions.sessionIds.add(sessionId);
|
|
9713
|
+
}
|
|
9392
9714
|
}
|
|
9393
9715
|
}
|
|
9394
9716
|
if (result.imported_count > 0 || result.reprojected_count > 0 || result.errors.length > 0) {
|
|
@@ -9399,13 +9721,14 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
9399
9721
|
return result;
|
|
9400
9722
|
}
|
|
9401
9723
|
async function importHermesSession(input) {
|
|
9402
|
-
const { paths, store, logger, candidate
|
|
9724
|
+
const { paths, store, logger, candidate } = input;
|
|
9403
9725
|
const profile = await resolveConversationProfileTarget(
|
|
9404
9726
|
paths,
|
|
9405
9727
|
candidate.profileName
|
|
9406
9728
|
);
|
|
9407
9729
|
const sessionId = candidate.session.id;
|
|
9408
|
-
const
|
|
9730
|
+
const hermesSessionIds = lineageSessionIds(candidate);
|
|
9731
|
+
const messages = await readHermesLineageMessages(candidate);
|
|
9409
9732
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9410
9733
|
const createdAt = isoFromHermesTime(candidate.session.started_at) ?? now;
|
|
9411
9734
|
const updatedAt = isoFromHermesTime(candidate.session.last_active) ?? isoFromHermesTime(messages.at(-1)?.timestamp) ?? createdAt;
|
|
@@ -9422,7 +9745,14 @@ async function importHermesSession(input) {
|
|
|
9422
9745
|
}),
|
|
9423
9746
|
runs: []
|
|
9424
9747
|
};
|
|
9425
|
-
const title = readString9(candidate.session, "title") ?? firstUserText(snapshot);
|
|
9748
|
+
const title = lineageTitle(candidate) ?? readString9(candidate.session, "title") ?? firstUserText(snapshot);
|
|
9749
|
+
const importedStats = buildImportedHermesStats({
|
|
9750
|
+
candidate,
|
|
9751
|
+
snapshot,
|
|
9752
|
+
profileUid: profile.profileUid,
|
|
9753
|
+
profileName: profile.profileName,
|
|
9754
|
+
updatedAt
|
|
9755
|
+
});
|
|
9426
9756
|
const manifest = {
|
|
9427
9757
|
id: conversationId,
|
|
9428
9758
|
schema_version: 1,
|
|
@@ -9431,13 +9761,15 @@ async function importHermesSession(input) {
|
|
|
9431
9761
|
title_source: title ? "hermes" : "default",
|
|
9432
9762
|
status: "active",
|
|
9433
9763
|
hermes_session_id: sessionId,
|
|
9434
|
-
hermes_session_ids:
|
|
9764
|
+
hermes_session_ids: hermesSessionIds,
|
|
9765
|
+
...lineageManifestPatch(candidate),
|
|
9435
9766
|
profile_uid: profile.profileUid,
|
|
9436
9767
|
profile_name_snapshot: profile.profileName,
|
|
9437
9768
|
profile: profile.profileName,
|
|
9438
9769
|
created_at: createdAt,
|
|
9439
9770
|
updated_at: updatedAt,
|
|
9440
|
-
last_event_seq: 0
|
|
9771
|
+
last_event_seq: 0,
|
|
9772
|
+
stats: importedStats
|
|
9441
9773
|
};
|
|
9442
9774
|
await store.createConversation(manifest, snapshot);
|
|
9443
9775
|
await hydrateImportedConversationMedia({
|
|
@@ -9480,59 +9812,288 @@ async function importHermesSession(input) {
|
|
|
9480
9812
|
paths,
|
|
9481
9813
|
toStatsIndexRecord(await store.readManifest(conversationId), stats)
|
|
9482
9814
|
);
|
|
9483
|
-
existingHermesSessionIds.add(sessionId);
|
|
9484
9815
|
return true;
|
|
9485
9816
|
}
|
|
9486
|
-
async function
|
|
9817
|
+
async function mergeExistingHermesConversation(input) {
|
|
9818
|
+
const conversations = await readExistingHermesConversations(
|
|
9819
|
+
input.store,
|
|
9820
|
+
input.conversationIds
|
|
9821
|
+
);
|
|
9822
|
+
const active = conversations.filter((item) => item.manifest.status === "active");
|
|
9823
|
+
const canonical = selectCanonicalHermesConversation(active);
|
|
9824
|
+
if (!canonical) {
|
|
9825
|
+
return false;
|
|
9826
|
+
}
|
|
9487
9827
|
let changed = false;
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9828
|
+
const candidateMessages = await readHermesLineageMessages(input.candidate);
|
|
9829
|
+
const nextCanonical = await mergeHermesCandidateIntoConversation({
|
|
9830
|
+
...input,
|
|
9831
|
+
existing: canonical,
|
|
9832
|
+
candidateMessages
|
|
9833
|
+
});
|
|
9834
|
+
changed = nextCanonical || changed;
|
|
9835
|
+
for (const duplicate of active) {
|
|
9836
|
+
if (duplicate.conversationId === canonical.conversationId) {
|
|
9495
9837
|
continue;
|
|
9496
9838
|
}
|
|
9497
|
-
|
|
9498
|
-
if (!prefix?.needsUpgrade || prefix.messages.length === 0) {
|
|
9839
|
+
if (!isSafeHermesImportConversation(duplicate) || duplicate.manifest.status !== "active") {
|
|
9499
9840
|
continue;
|
|
9500
9841
|
}
|
|
9501
|
-
|
|
9502
|
-
input.paths,
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9842
|
+
await softDeleteMergedHermesDuplicate({
|
|
9843
|
+
paths: input.paths,
|
|
9844
|
+
store: input.store,
|
|
9845
|
+
duplicate,
|
|
9846
|
+
candidate: input.candidate
|
|
9847
|
+
});
|
|
9848
|
+
changed = true;
|
|
9849
|
+
}
|
|
9850
|
+
return changed;
|
|
9851
|
+
}
|
|
9852
|
+
async function mergeHermesCandidateIntoConversation(input) {
|
|
9853
|
+
const { candidate, existing } = input;
|
|
9854
|
+
const profile = await resolveConversationProfileTarget(
|
|
9855
|
+
input.paths,
|
|
9856
|
+
existing.manifest.profile_name_snapshot ?? existing.manifest.profile ?? candidate.profileName
|
|
9857
|
+
);
|
|
9858
|
+
let nextSnapshot = existing.snapshot;
|
|
9859
|
+
const pureImport = isSafeHermesImportConversation(existing);
|
|
9860
|
+
const prefix = collectImportedHermesPrefix(existing.snapshot);
|
|
9861
|
+
if (pureImport || prefix?.needsUpgrade && prefix.messages.length > 0) {
|
|
9862
|
+
const messages = pureImport ? input.candidateMessages : prefix.messages;
|
|
9863
|
+
nextSnapshot = {
|
|
9864
|
+
...existing.snapshot,
|
|
9507
9865
|
messages: [
|
|
9508
9866
|
...toLinkMessages({
|
|
9509
|
-
conversationId,
|
|
9867
|
+
conversationId: existing.conversationId,
|
|
9510
9868
|
profileName: profile.profileName,
|
|
9511
9869
|
profileUid: profile.profileUid,
|
|
9512
9870
|
profileDisplayName: profile.profileDisplayName,
|
|
9513
|
-
sessionId:
|
|
9514
|
-
messages
|
|
9871
|
+
sessionId: candidate.session.id,
|
|
9872
|
+
messages
|
|
9515
9873
|
}),
|
|
9516
|
-
...snapshot.messages.slice(prefix.endIndex)
|
|
9874
|
+
...pureImport ? [] : existing.snapshot.messages.slice(prefix.endIndex)
|
|
9517
9875
|
]
|
|
9518
9876
|
};
|
|
9519
|
-
await input.store.writeSnapshot(conversationId, nextSnapshot);
|
|
9877
|
+
await input.store.writeSnapshot(existing.conversationId, nextSnapshot);
|
|
9520
9878
|
await hydrateImportedConversationMedia({
|
|
9521
9879
|
paths: input.paths,
|
|
9522
9880
|
store: input.store,
|
|
9523
9881
|
logger: input.logger,
|
|
9524
|
-
conversationId
|
|
9882
|
+
conversationId: existing.conversationId
|
|
9525
9883
|
});
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9884
|
+
nextSnapshot = await input.store.readSnapshot(existing.conversationId);
|
|
9885
|
+
}
|
|
9886
|
+
const updatedAt = isoFromHermesTime(candidate.session.last_active) ?? existing.manifest.updated_at;
|
|
9887
|
+
const nextManifest = mergeHermesLineageIntoManifest({
|
|
9888
|
+
manifest: existing.manifest,
|
|
9889
|
+
candidate,
|
|
9890
|
+
snapshot: nextSnapshot,
|
|
9891
|
+
profileUid: profile.profileUid,
|
|
9892
|
+
profileName: profile.profileName,
|
|
9893
|
+
updatedAt
|
|
9894
|
+
});
|
|
9895
|
+
if (manifestEquivalent(existing.manifest, nextManifest)) {
|
|
9896
|
+
return nextSnapshot !== existing.snapshot;
|
|
9534
9897
|
}
|
|
9535
|
-
|
|
9898
|
+
await input.store.writeManifest(nextManifest);
|
|
9899
|
+
await upsertConversationStats(
|
|
9900
|
+
input.paths,
|
|
9901
|
+
toStatsIndexRecord(nextManifest, nextManifest.stats)
|
|
9902
|
+
);
|
|
9903
|
+
return true;
|
|
9904
|
+
}
|
|
9905
|
+
function findKnownConversationIdsForCandidate(known, candidate) {
|
|
9906
|
+
const conversationIds = [];
|
|
9907
|
+
for (const sessionId of lineageSessionIds(candidate)) {
|
|
9908
|
+
for (const conversationId of known.conversationIdsBySessionId.get(sessionId) ?? []) {
|
|
9909
|
+
if (!conversationIds.includes(conversationId)) {
|
|
9910
|
+
conversationIds.push(conversationId);
|
|
9911
|
+
}
|
|
9912
|
+
}
|
|
9913
|
+
}
|
|
9914
|
+
return conversationIds;
|
|
9915
|
+
}
|
|
9916
|
+
function lineageSessionIds(candidate) {
|
|
9917
|
+
return normalizeSessionIds([
|
|
9918
|
+
candidate.session._lineage_root_id,
|
|
9919
|
+
...candidate.session._lineage_session_ids ?? [],
|
|
9920
|
+
candidate.session.id
|
|
9921
|
+
]);
|
|
9922
|
+
}
|
|
9923
|
+
function lineageTitle(candidate) {
|
|
9924
|
+
return candidate.session._lineage_title ?? stripCompressionTitleSuffix2(readString9(candidate.session, "title") ?? "");
|
|
9925
|
+
}
|
|
9926
|
+
function lineageManifestPatch(candidate) {
|
|
9927
|
+
const sessionIds = lineageSessionIds(candidate);
|
|
9928
|
+
const rootSessionId = candidate.session._lineage_root_id ?? sessionIds[0];
|
|
9929
|
+
const currentSessionId = candidate.session.id;
|
|
9930
|
+
if (!rootSessionId || sessionIds.length <= 1 || rootSessionId === currentSessionId) {
|
|
9931
|
+
return {};
|
|
9932
|
+
}
|
|
9933
|
+
return {
|
|
9934
|
+
hermes_lineage: {
|
|
9935
|
+
kind: "compression",
|
|
9936
|
+
root_session_id: rootSessionId,
|
|
9937
|
+
current_session_id: currentSessionId,
|
|
9938
|
+
session_ids: sessionIds
|
|
9939
|
+
}
|
|
9940
|
+
};
|
|
9941
|
+
}
|
|
9942
|
+
function buildImportedHermesStats(input) {
|
|
9943
|
+
const usage = input.candidate.session._lineage_usage;
|
|
9944
|
+
const base = input.baseStats;
|
|
9945
|
+
const usageInput = usage?.input_tokens ?? 0;
|
|
9946
|
+
const usageOutput = usage?.output_tokens ?? 0;
|
|
9947
|
+
const usageTotal = usage?.total_tokens ?? usageInput + usageOutput;
|
|
9948
|
+
const inputTokens = Math.max(base?.input_tokens ?? 0, usageInput);
|
|
9949
|
+
const outputTokens = Math.max(base?.output_tokens ?? 0, usageOutput);
|
|
9950
|
+
const totalTokens = Math.max(
|
|
9951
|
+
base?.total_tokens ?? 0,
|
|
9952
|
+
usageTotal,
|
|
9953
|
+
inputTokens + outputTokens
|
|
9954
|
+
);
|
|
9955
|
+
const updatedAt = base?.updated_at ? latestTimestamp(base.updated_at, input.updatedAt) : input.updatedAt;
|
|
9956
|
+
return {
|
|
9957
|
+
input_tokens: inputTokens,
|
|
9958
|
+
output_tokens: outputTokens,
|
|
9959
|
+
total_tokens: totalTokens,
|
|
9960
|
+
message_count: Math.max(
|
|
9961
|
+
base?.message_count ?? 0,
|
|
9962
|
+
input.snapshot.messages.length
|
|
9963
|
+
),
|
|
9964
|
+
run_count: Math.max(base?.run_count ?? 0, usage?.api_call_count ?? 0),
|
|
9965
|
+
profile_uid: base?.profile_uid ?? input.profileUid,
|
|
9966
|
+
profile_name_snapshot: base?.profile_name_snapshot ?? base?.profile ?? input.profileName,
|
|
9967
|
+
profile: base?.profile ?? base?.profile_name_snapshot ?? input.profileName,
|
|
9968
|
+
...base?.model ?? usage?.model ? { model: base?.model ?? usage?.model } : {},
|
|
9969
|
+
...base?.provider ?? usage?.provider ? { provider: base?.provider ?? usage?.provider } : {},
|
|
9970
|
+
...base?.context_window ? { context_window: base.context_window } : {},
|
|
9971
|
+
updated_at: updatedAt
|
|
9972
|
+
};
|
|
9973
|
+
}
|
|
9974
|
+
async function readExistingHermesConversations(store, conversationIds) {
|
|
9975
|
+
const conversations = [];
|
|
9976
|
+
for (const conversationId of [...new Set(conversationIds)]) {
|
|
9977
|
+
const manifest = await store.readManifest(conversationId).catch(() => null);
|
|
9978
|
+
if (!manifest) {
|
|
9979
|
+
continue;
|
|
9980
|
+
}
|
|
9981
|
+
const snapshot = await store.readSnapshot(conversationId).catch(() => ({
|
|
9982
|
+
schema_version: 1,
|
|
9983
|
+
messages: [],
|
|
9984
|
+
runs: []
|
|
9985
|
+
}));
|
|
9986
|
+
conversations.push({ conversationId, manifest, snapshot });
|
|
9987
|
+
}
|
|
9988
|
+
return conversations;
|
|
9989
|
+
}
|
|
9990
|
+
function selectCanonicalHermesConversation(conversations) {
|
|
9991
|
+
const ranked = [...conversations].sort((left, right) => {
|
|
9992
|
+
const rightScore = canonicalConversationScore(right);
|
|
9993
|
+
const leftScore = canonicalConversationScore(left);
|
|
9994
|
+
if (rightScore !== leftScore) {
|
|
9995
|
+
return rightScore - leftScore;
|
|
9996
|
+
}
|
|
9997
|
+
return Date.parse(left.manifest.created_at) - Date.parse(right.manifest.created_at) || left.conversationId.localeCompare(right.conversationId);
|
|
9998
|
+
});
|
|
9999
|
+
return ranked[0] ?? null;
|
|
10000
|
+
}
|
|
10001
|
+
function canonicalConversationScore(item) {
|
|
10002
|
+
let score = 0;
|
|
10003
|
+
if (!isSafeHermesImportConversation(item)) {
|
|
10004
|
+
score += 1e3;
|
|
10005
|
+
}
|
|
10006
|
+
if (item.snapshot.runs.length > 0) {
|
|
10007
|
+
score += 500;
|
|
10008
|
+
}
|
|
10009
|
+
if (item.manifest.title_source === "manual") {
|
|
10010
|
+
score += 200;
|
|
10011
|
+
}
|
|
10012
|
+
if (!hasCompressionTitleSuffix(item.manifest.title)) {
|
|
10013
|
+
score += 100;
|
|
10014
|
+
}
|
|
10015
|
+
if (item.manifest.hermes_lineage?.root_session_id && item.manifest.hermes_session_id === item.manifest.hermes_lineage.root_session_id) {
|
|
10016
|
+
score += 50;
|
|
10017
|
+
}
|
|
10018
|
+
return score;
|
|
10019
|
+
}
|
|
10020
|
+
function isSafeHermesImportConversation(item) {
|
|
10021
|
+
const snapshot = item.snapshot;
|
|
10022
|
+
if (snapshot.runs.length > 0) {
|
|
10023
|
+
return false;
|
|
10024
|
+
}
|
|
10025
|
+
if (item.manifest.title_source === "manual" || item.manifest.title_source === "generated") {
|
|
10026
|
+
return false;
|
|
10027
|
+
}
|
|
10028
|
+
return snapshot.messages.length === 0 || snapshot.messages.every((message) => isHermesImportedMessage(message));
|
|
10029
|
+
}
|
|
10030
|
+
async function softDeleteMergedHermesDuplicate(input) {
|
|
10031
|
+
const deletedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10032
|
+
const emptySnapshot3 = {
|
|
10033
|
+
schema_version: 1,
|
|
10034
|
+
messages: [],
|
|
10035
|
+
runs: []
|
|
10036
|
+
};
|
|
10037
|
+
const hermesSessionIds = normalizeSessionIds([
|
|
10038
|
+
input.duplicate.manifest.hermes_session_id,
|
|
10039
|
+
...input.duplicate.manifest.hermes_session_ids ?? [],
|
|
10040
|
+
...input.duplicate.manifest.hermes_lineage?.session_ids ?? [],
|
|
10041
|
+
...lineageSessionIds(input.candidate)
|
|
10042
|
+
]);
|
|
10043
|
+
const nextManifest = {
|
|
10044
|
+
...input.duplicate.manifest,
|
|
10045
|
+
status: "deleted_soft",
|
|
10046
|
+
hermes_session_ids: hermesSessionIds,
|
|
10047
|
+
...lineageManifestPatch(input.candidate),
|
|
10048
|
+
updated_at: deletedAt,
|
|
10049
|
+
deleted_at: deletedAt
|
|
10050
|
+
};
|
|
10051
|
+
const stats = buildConversationStats(
|
|
10052
|
+
{ ...nextManifest, stats: input.duplicate.manifest.stats },
|
|
10053
|
+
emptySnapshot3
|
|
10054
|
+
);
|
|
10055
|
+
const tombstone = { ...nextManifest, stats };
|
|
10056
|
+
await input.store.writeManifest(tombstone);
|
|
10057
|
+
await input.store.writeSnapshot(input.duplicate.conversationId, emptySnapshot3);
|
|
10058
|
+
await upsertConversationStats(input.paths, toStatsIndexRecord(tombstone, stats));
|
|
10059
|
+
}
|
|
10060
|
+
function mergeHermesLineageIntoManifest(input) {
|
|
10061
|
+
const hermesSessionIds = normalizeSessionIds([
|
|
10062
|
+
input.manifest.hermes_session_id,
|
|
10063
|
+
...input.manifest.hermes_session_ids ?? [],
|
|
10064
|
+
...input.manifest.hermes_lineage?.session_ids ?? [],
|
|
10065
|
+
...lineageSessionIds(input.candidate)
|
|
10066
|
+
]);
|
|
10067
|
+
const nextBase = {
|
|
10068
|
+
...input.manifest,
|
|
10069
|
+
hermes_session_id: input.candidate.session.id,
|
|
10070
|
+
hermes_session_ids: hermesSessionIds,
|
|
10071
|
+
...lineageManifestPatch(input.candidate),
|
|
10072
|
+
profile_uid: input.manifest.profile_uid ?? input.profileUid,
|
|
10073
|
+
profile_name_snapshot: input.manifest.profile_name_snapshot ?? input.manifest.profile ?? input.profileName,
|
|
10074
|
+
profile: input.manifest.profile ?? input.manifest.profile_name_snapshot ?? input.profileName,
|
|
10075
|
+
updated_at: latestTimestamp(input.manifest.updated_at, input.updatedAt)
|
|
10076
|
+
};
|
|
10077
|
+
const title = lineageTitle(input.candidate);
|
|
10078
|
+
if (title && canSyncHermesTitle(input.manifest)) {
|
|
10079
|
+
nextBase.title = normalizeTitle(title);
|
|
10080
|
+
nextBase.title_source = "hermes";
|
|
10081
|
+
}
|
|
10082
|
+
const baseStats = buildConversationStats(nextBase, input.snapshot);
|
|
10083
|
+
return {
|
|
10084
|
+
...nextBase,
|
|
10085
|
+
stats: buildImportedHermesStats({
|
|
10086
|
+
candidate: input.candidate,
|
|
10087
|
+
snapshot: input.snapshot,
|
|
10088
|
+
profileUid: input.profileUid,
|
|
10089
|
+
profileName: input.profileName,
|
|
10090
|
+
updatedAt: input.updatedAt,
|
|
10091
|
+
baseStats
|
|
10092
|
+
})
|
|
10093
|
+
};
|
|
10094
|
+
}
|
|
10095
|
+
function manifestEquivalent(left, right) {
|
|
10096
|
+
return stableJson(left) === stableJson(right);
|
|
9536
10097
|
}
|
|
9537
10098
|
function collectImportedHermesPrefix(snapshot) {
|
|
9538
10099
|
const rows = [];
|
|
@@ -10006,7 +10567,7 @@ function latestTimestamp(left, right) {
|
|
|
10006
10567
|
return leftTime >= rightTime ? left : right;
|
|
10007
10568
|
}
|
|
10008
10569
|
async function readKnownHermesSessions(store) {
|
|
10009
|
-
const
|
|
10570
|
+
const sessionIds = /* @__PURE__ */ new Set();
|
|
10010
10571
|
const conversationIdsBySessionId = /* @__PURE__ */ new Map();
|
|
10011
10572
|
for (const conversationId of await store.listConversationIds()) {
|
|
10012
10573
|
const manifest = await store.readManifest(conversationId).catch(() => null);
|
|
@@ -10014,7 +10575,7 @@ async function readKnownHermesSessions(store) {
|
|
|
10014
10575
|
continue;
|
|
10015
10576
|
}
|
|
10016
10577
|
if (manifest.hermes_session_id) {
|
|
10017
|
-
|
|
10578
|
+
sessionIds.add(manifest.hermes_session_id);
|
|
10018
10579
|
rememberKnownHermesConversation(
|
|
10019
10580
|
conversationIdsBySessionId,
|
|
10020
10581
|
manifest.hermes_session_id,
|
|
@@ -10022,7 +10583,22 @@ async function readKnownHermesSessions(store) {
|
|
|
10022
10583
|
);
|
|
10023
10584
|
}
|
|
10024
10585
|
for (const sessionId of manifest.hermes_session_ids ?? []) {
|
|
10025
|
-
|
|
10586
|
+
sessionIds.add(sessionId);
|
|
10587
|
+
rememberKnownHermesConversation(
|
|
10588
|
+
conversationIdsBySessionId,
|
|
10589
|
+
sessionId,
|
|
10590
|
+
conversationId
|
|
10591
|
+
);
|
|
10592
|
+
}
|
|
10593
|
+
for (const sessionId of [
|
|
10594
|
+
manifest.hermes_lineage?.root_session_id,
|
|
10595
|
+
manifest.hermes_lineage?.current_session_id,
|
|
10596
|
+
...manifest.hermes_lineage?.session_ids ?? []
|
|
10597
|
+
]) {
|
|
10598
|
+
if (!sessionId) {
|
|
10599
|
+
continue;
|
|
10600
|
+
}
|
|
10601
|
+
sessionIds.add(sessionId);
|
|
10026
10602
|
rememberKnownHermesConversation(
|
|
10027
10603
|
conversationIdsBySessionId,
|
|
10028
10604
|
sessionId,
|
|
@@ -10030,7 +10606,7 @@ async function readKnownHermesSessions(store) {
|
|
|
10030
10606
|
);
|
|
10031
10607
|
}
|
|
10032
10608
|
}
|
|
10033
|
-
return {
|
|
10609
|
+
return { sessionIds, conversationIdsBySessionId };
|
|
10034
10610
|
}
|
|
10035
10611
|
function rememberKnownHermesConversation(map, sessionId, conversationId) {
|
|
10036
10612
|
const current = map.get(sessionId) ?? [];
|
|
@@ -10040,7 +10616,7 @@ function rememberKnownHermesConversation(map, sessionId, conversationId) {
|
|
|
10040
10616
|
}
|
|
10041
10617
|
async function discoverHermesProfileNames() {
|
|
10042
10618
|
const names = /* @__PURE__ */ new Set([DEFAULT_PROFILE_NAME]);
|
|
10043
|
-
const profilesDir =
|
|
10619
|
+
const profilesDir = path15.join(os4.homedir(), ".hermes", "profiles");
|
|
10044
10620
|
const entries = await readdir7(profilesDir, { withFileTypes: true }).catch(
|
|
10045
10621
|
(error) => {
|
|
10046
10622
|
if (isNodeError11(error, "ENOENT")) {
|
|
@@ -10148,14 +10724,12 @@ function joinImportedText(left, right) {
|
|
|
10148
10724
|
${right}`;
|
|
10149
10725
|
}
|
|
10150
10726
|
function projectCompressionTips(rows) {
|
|
10151
|
-
const byId = /* @__PURE__ */ new Map();
|
|
10152
10727
|
const childrenByParent = /* @__PURE__ */ new Map();
|
|
10153
10728
|
for (const row of rows) {
|
|
10154
10729
|
const id = readString9(row, "id");
|
|
10155
10730
|
if (!id) {
|
|
10156
10731
|
continue;
|
|
10157
10732
|
}
|
|
10158
|
-
byId.set(id, row);
|
|
10159
10733
|
const parentId = readString9(row, "parent_session_id");
|
|
10160
10734
|
if (parentId) {
|
|
10161
10735
|
const children = childrenByParent.get(parentId) ?? [];
|
|
@@ -10171,13 +10745,14 @@ function projectCompressionTips(rows) {
|
|
|
10171
10745
|
}
|
|
10172
10746
|
let tip = row;
|
|
10173
10747
|
const visited = /* @__PURE__ */ new Set([id]);
|
|
10748
|
+
const chain = [row];
|
|
10174
10749
|
while (readString9(tip, "end_reason") === "compression") {
|
|
10175
10750
|
const tipId2 = readString9(tip, "id");
|
|
10176
10751
|
if (!tipId2) {
|
|
10177
10752
|
break;
|
|
10178
10753
|
}
|
|
10179
|
-
const next = (childrenByParent.get(tipId2) ?? []).filter((child) =>
|
|
10180
|
-
(left, right) => (readNumber2(right.last_active) ?? 0) - (readNumber2(left.last_active) ?? 0)
|
|
10754
|
+
const next = (childrenByParent.get(tipId2) ?? []).filter((child) => isCompressionContinuation(tip, child)).sort(
|
|
10755
|
+
(left, right) => (readNumber2(right.started_at) ?? 0) - (readNumber2(left.started_at) ?? 0) || (readNumber2(right.last_active) ?? 0) - (readNumber2(left.last_active) ?? 0)
|
|
10181
10756
|
)[0];
|
|
10182
10757
|
const nextId = next ? readString9(next, "id") : null;
|
|
10183
10758
|
if (!next || !nextId || visited.has(nextId)) {
|
|
@@ -10185,25 +10760,75 @@ function projectCompressionTips(rows) {
|
|
|
10185
10760
|
}
|
|
10186
10761
|
tip = next;
|
|
10187
10762
|
visited.add(nextId);
|
|
10763
|
+
chain.push(next);
|
|
10188
10764
|
}
|
|
10189
10765
|
const tipId = readString9(tip, "id");
|
|
10190
10766
|
if (tipId) {
|
|
10767
|
+
const sessionIds = chain.map((item) => readString9(item, "id")).filter((item) => Boolean(item));
|
|
10191
10768
|
projected.push({
|
|
10192
10769
|
...tip,
|
|
10193
10770
|
id: tipId,
|
|
10194
10771
|
_lineage_root_id: id,
|
|
10772
|
+
_lineage_session_ids: sessionIds,
|
|
10773
|
+
_lineage_title: readString9(row, "title") ?? stripCompressionTitleSuffix2(readString9(tip, "title") ?? ""),
|
|
10774
|
+
_lineage_usage: aggregateHermesSessionUsage(chain),
|
|
10195
10775
|
started_at: readNumber2(row.started_at) ?? readNumber2(tip.started_at)
|
|
10196
10776
|
});
|
|
10197
10777
|
}
|
|
10198
10778
|
}
|
|
10199
10779
|
return projected;
|
|
10200
10780
|
}
|
|
10781
|
+
function isCompressionContinuation(parent, child) {
|
|
10782
|
+
const childId = readString9(child, "id");
|
|
10783
|
+
const parentEndedAt = readNumber2(parent.ended_at);
|
|
10784
|
+
const childStartedAt = readNumber2(child.started_at);
|
|
10785
|
+
return Boolean(childId) && readString9(parent, "end_reason") === "compression" && parentEndedAt !== null && childStartedAt !== null && childStartedAt >= parentEndedAt;
|
|
10786
|
+
}
|
|
10787
|
+
function aggregateHermesSessionUsage(rows) {
|
|
10788
|
+
let inputTokens = 0;
|
|
10789
|
+
let outputTokens = 0;
|
|
10790
|
+
let apiCallCount = 0;
|
|
10791
|
+
let model;
|
|
10792
|
+
let provider;
|
|
10793
|
+
for (const row of rows) {
|
|
10794
|
+
inputTokens += readNumber2(row.input_tokens) ?? 0;
|
|
10795
|
+
outputTokens += readNumber2(row.output_tokens) ?? 0;
|
|
10796
|
+
apiCallCount += readNumber2(row.api_call_count) ?? 0;
|
|
10797
|
+
model = readString9(row, "model") ?? model;
|
|
10798
|
+
provider = readString9(row, "billing_provider") ?? readString9(row, "provider") ?? provider;
|
|
10799
|
+
}
|
|
10800
|
+
return {
|
|
10801
|
+
input_tokens: inputTokens,
|
|
10802
|
+
output_tokens: outputTokens,
|
|
10803
|
+
total_tokens: inputTokens + outputTokens,
|
|
10804
|
+
api_call_count: apiCallCount,
|
|
10805
|
+
...model ? { model } : {},
|
|
10806
|
+
...provider ? { provider } : {}
|
|
10807
|
+
};
|
|
10808
|
+
}
|
|
10809
|
+
async function readHermesLineageMessages(candidate) {
|
|
10810
|
+
const rows = [];
|
|
10811
|
+
for (const sessionId of lineageSessionIds(candidate)) {
|
|
10812
|
+
const messages = await readHermesSessionMessages({
|
|
10813
|
+
...candidate,
|
|
10814
|
+
session: {
|
|
10815
|
+
...candidate.session,
|
|
10816
|
+
id: sessionId
|
|
10817
|
+
}
|
|
10818
|
+
});
|
|
10819
|
+
rows.push(...messages);
|
|
10820
|
+
}
|
|
10821
|
+
return rows;
|
|
10822
|
+
}
|
|
10201
10823
|
async function readHermesSessionMessages(candidate) {
|
|
10202
10824
|
const [dbMessages, jsonlMessages] = await Promise.all([
|
|
10203
10825
|
readStateDbMessages(candidate.dbPath, candidate.session.id),
|
|
10204
10826
|
readJsonlMessages(candidate.profileName, candidate.session.id)
|
|
10205
10827
|
]);
|
|
10206
|
-
|
|
10828
|
+
const selected = jsonlMessages.length > dbMessages.length ? jsonlMessages : dbMessages;
|
|
10829
|
+
return selected.map(
|
|
10830
|
+
(message) => readString9(message, "session_id") ? message : { ...message, session_id: candidate.session.id }
|
|
10831
|
+
);
|
|
10207
10832
|
}
|
|
10208
10833
|
async function readStateDbMessages(dbPath, sessionId) {
|
|
10209
10834
|
if (!await isFile(dbPath)) {
|
|
@@ -10241,8 +10866,8 @@ async function readJsonlMessages(profileName, sessionId) {
|
|
|
10241
10866
|
return [];
|
|
10242
10867
|
}
|
|
10243
10868
|
const profileDir = resolveHermesProfileDir(profileName);
|
|
10244
|
-
const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() =>
|
|
10245
|
-
const transcriptPath =
|
|
10869
|
+
const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path15.join(profileDir, "sessions"));
|
|
10870
|
+
const transcriptPath = path15.join(sessionsDir, `${sessionId}.jsonl`);
|
|
10246
10871
|
const raw = await readFile9(transcriptPath, "utf8").catch((error) => {
|
|
10247
10872
|
if (isNodeError11(error, "ENOENT")) {
|
|
10248
10873
|
return "";
|
|
@@ -10394,9 +11019,10 @@ function formatImportedFilenameList(filenames) {
|
|
|
10394
11019
|
function toLinkMessage(input) {
|
|
10395
11020
|
const role = normalizeMessageRole(input.message.role);
|
|
10396
11021
|
const text = normalizeContent(input.message.content);
|
|
11022
|
+
const sessionId = readString9(input.message, "session_id") ?? input.sessionId;
|
|
10397
11023
|
const createdAt = isoFromHermesTime(input.message.timestamp) ?? new Date(Date.now() + input.index).toISOString();
|
|
10398
11024
|
return {
|
|
10399
|
-
id: `msg_${
|
|
11025
|
+
id: `msg_${randomUUID7().replaceAll("-", "")}`,
|
|
10400
11026
|
schema_version: 1,
|
|
10401
11027
|
conversation_id: input.conversationId,
|
|
10402
11028
|
role,
|
|
@@ -10412,7 +11038,7 @@ function toLinkMessage(input) {
|
|
|
10412
11038
|
parts: text ? [{ type: "text", text }] : [],
|
|
10413
11039
|
attachments: [],
|
|
10414
11040
|
hermes: {
|
|
10415
|
-
session_id:
|
|
11041
|
+
session_id: sessionId,
|
|
10416
11042
|
message_id: input.message.id,
|
|
10417
11043
|
imported_from: "hermes",
|
|
10418
11044
|
import_projection: HERMES_IMPORT_PROJECTION_VERSION
|
|
@@ -10448,6 +11074,47 @@ function normalizeTitle(value) {
|
|
|
10448
11074
|
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
10449
11075
|
return normalized || DEFAULT_CONVERSATION_TITLE;
|
|
10450
11076
|
}
|
|
11077
|
+
function canSyncHermesTitle(manifest) {
|
|
11078
|
+
return manifest.title_source !== "manual" && manifest.title_source !== "generated" && manifest.title_source !== "temporary_user_message" && manifest.title_source !== "temporary_fallback";
|
|
11079
|
+
}
|
|
11080
|
+
function stripCompressionTitleSuffix2(value) {
|
|
11081
|
+
const normalized = value.replace(/\s+/gu, " ").trim();
|
|
11082
|
+
if (!normalized) {
|
|
11083
|
+
return void 0;
|
|
11084
|
+
}
|
|
11085
|
+
const match = /^(.*?) #\d+$/u.exec(normalized);
|
|
11086
|
+
const stripped = match?.[1]?.trim();
|
|
11087
|
+
return stripped || normalized;
|
|
11088
|
+
}
|
|
11089
|
+
function hasCompressionTitleSuffix(value) {
|
|
11090
|
+
return / #\d+$/u.test(value.trim());
|
|
11091
|
+
}
|
|
11092
|
+
function normalizeSessionIds(values) {
|
|
11093
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11094
|
+
for (const value of values) {
|
|
11095
|
+
const sessionId = value?.trim();
|
|
11096
|
+
if (!sessionId || seen.has(sessionId)) {
|
|
11097
|
+
continue;
|
|
11098
|
+
}
|
|
11099
|
+
seen.add(sessionId);
|
|
11100
|
+
}
|
|
11101
|
+
return [...seen];
|
|
11102
|
+
}
|
|
11103
|
+
function stableJson(value) {
|
|
11104
|
+
return JSON.stringify(stabilizeJsonValue(value));
|
|
11105
|
+
}
|
|
11106
|
+
function stabilizeJsonValue(value) {
|
|
11107
|
+
if (Array.isArray(value)) {
|
|
11108
|
+
return value.map((item) => stabilizeJsonValue(item));
|
|
11109
|
+
}
|
|
11110
|
+
if (!value || typeof value !== "object") {
|
|
11111
|
+
return value;
|
|
11112
|
+
}
|
|
11113
|
+
const record = value;
|
|
11114
|
+
return Object.fromEntries(
|
|
11115
|
+
Object.keys(record).sort().map((key) => [key, stabilizeJsonValue(record[key])])
|
|
11116
|
+
);
|
|
11117
|
+
}
|
|
10451
11118
|
function normalizeMessageRole(value) {
|
|
10452
11119
|
switch (value?.trim().toLowerCase()) {
|
|
10453
11120
|
case "user":
|
|
@@ -10527,7 +11194,7 @@ async function isFile(filePath) {
|
|
|
10527
11194
|
});
|
|
10528
11195
|
}
|
|
10529
11196
|
function createConversationId() {
|
|
10530
|
-
return `conv_${
|
|
11197
|
+
return `conv_${randomUUID7().replaceAll("-", "")}`;
|
|
10531
11198
|
}
|
|
10532
11199
|
function isoFromHermesTime(value) {
|
|
10533
11200
|
const numeric = readNumber2(value);
|
|
@@ -10671,7 +11338,7 @@ async function createHermesRun(input, options = {}) {
|
|
|
10671
11338
|
);
|
|
10672
11339
|
if (response.status === 404 || response.status === 503) {
|
|
10673
11340
|
assertHermesRunsApiSupported(
|
|
10674
|
-
await readHermesVersion().catch(() => null),
|
|
11341
|
+
await readHermesVersion({ logger: options.logger }).catch(() => null),
|
|
10675
11342
|
response.status
|
|
10676
11343
|
);
|
|
10677
11344
|
throw new LinkHttpError(
|
|
@@ -10698,7 +11365,7 @@ async function streamHermesRunEvents(runId, options = {}) {
|
|
|
10698
11365
|
options
|
|
10699
11366
|
);
|
|
10700
11367
|
assertHermesRunsApiSupported(
|
|
10701
|
-
await readHermesVersion().catch(() => null),
|
|
11368
|
+
await readHermesVersion({ logger: options.logger }).catch(() => null),
|
|
10702
11369
|
response.status
|
|
10703
11370
|
);
|
|
10704
11371
|
if (!response.ok || !response.body) {
|
|
@@ -10741,7 +11408,7 @@ async function streamHermesResponses(input, options = {}) {
|
|
|
10741
11408
|
);
|
|
10742
11409
|
if (response.status === 404 || response.status === 503) {
|
|
10743
11410
|
assertHermesRunsApiSupported(
|
|
10744
|
-
await readHermesVersion().catch(() => null),
|
|
11411
|
+
await readHermesVersion({ logger: options.logger }).catch(() => null),
|
|
10745
11412
|
response.status
|
|
10746
11413
|
);
|
|
10747
11414
|
throw new LinkHttpError(
|
|
@@ -10794,10 +11461,10 @@ async function cancelHermesRun(runId, options = {}) {
|
|
|
10794
11461
|
);
|
|
10795
11462
|
}
|
|
10796
11463
|
}
|
|
10797
|
-
async function callHermesApi(
|
|
11464
|
+
async function callHermesApi(path26, init, options) {
|
|
10798
11465
|
const method = init.method ?? "GET";
|
|
10799
11466
|
const startedAt = Date.now();
|
|
10800
|
-
void options.logger?.debug("hermes_api_request_started", { method, path:
|
|
11467
|
+
void options.logger?.debug("hermes_api_request_started", { method, path: path26 });
|
|
10801
11468
|
const availability = await ensureHermesApiServerAvailable({
|
|
10802
11469
|
fetchImpl: options.fetchImpl,
|
|
10803
11470
|
logger: options.logger,
|
|
@@ -10805,21 +11472,21 @@ async function callHermesApi(path25, init, options) {
|
|
|
10805
11472
|
});
|
|
10806
11473
|
let config = availability.configResult.apiServer;
|
|
10807
11474
|
const fetcher = options.fetchImpl ?? fetch;
|
|
10808
|
-
const request = () => fetchHermesApi(fetcher, config,
|
|
11475
|
+
const request = () => fetchHermesApi(fetcher, config, path26, init, options);
|
|
10809
11476
|
let response;
|
|
10810
11477
|
try {
|
|
10811
11478
|
response = await request();
|
|
10812
11479
|
} catch (error) {
|
|
10813
|
-
logHermesApiError(options.logger, method,
|
|
11480
|
+
logHermesApiError(options.logger, method, path26, startedAt, error);
|
|
10814
11481
|
throw error;
|
|
10815
11482
|
}
|
|
10816
11483
|
if (response.status !== 401) {
|
|
10817
|
-
logHermesApiResponse(options.logger, method,
|
|
11484
|
+
logHermesApiResponse(options.logger, method, path26, startedAt, response);
|
|
10818
11485
|
return response;
|
|
10819
11486
|
}
|
|
10820
11487
|
void options.logger?.warn("hermes_api_request_retrying_after_401", {
|
|
10821
11488
|
method,
|
|
10822
|
-
path:
|
|
11489
|
+
path: path26,
|
|
10823
11490
|
duration_ms: Date.now() - startedAt
|
|
10824
11491
|
});
|
|
10825
11492
|
const refreshedAvailability = await ensureHermesApiServerAvailable({
|
|
@@ -10832,20 +11499,20 @@ async function callHermesApi(path25, init, options) {
|
|
|
10832
11499
|
try {
|
|
10833
11500
|
response = await request();
|
|
10834
11501
|
} catch (error) {
|
|
10835
|
-
logHermesApiError(options.logger, method,
|
|
11502
|
+
logHermesApiError(options.logger, method, path26, startedAt, error);
|
|
10836
11503
|
throw error;
|
|
10837
11504
|
}
|
|
10838
|
-
logHermesApiResponse(options.logger, method,
|
|
11505
|
+
logHermesApiResponse(options.logger, method, path26, startedAt, response);
|
|
10839
11506
|
return response;
|
|
10840
11507
|
}
|
|
10841
|
-
async function fetchHermesApi(fetcher, config,
|
|
11508
|
+
async function fetchHermesApi(fetcher, config, path26, init, options) {
|
|
10842
11509
|
const headers = new Headers(init.headers);
|
|
10843
11510
|
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
10844
11511
|
if (config.key) {
|
|
10845
11512
|
headers.set("x-api-key", config.key);
|
|
10846
11513
|
headers.set("authorization", `Bearer ${config.key}`);
|
|
10847
11514
|
}
|
|
10848
|
-
return await fetcher(`http://127.0.0.1:${config.port}${
|
|
11515
|
+
return await fetcher(`http://127.0.0.1:${config.port}${path26}`, {
|
|
10849
11516
|
...init,
|
|
10850
11517
|
headers
|
|
10851
11518
|
}).catch((error) => {
|
|
@@ -10853,7 +11520,7 @@ async function fetchHermesApi(fetcher, config, path25, init, options) {
|
|
|
10853
11520
|
throw error;
|
|
10854
11521
|
}
|
|
10855
11522
|
void options.logger?.warn("hermes_api_server_connect_failed", {
|
|
10856
|
-
path:
|
|
11523
|
+
path: path26,
|
|
10857
11524
|
port: config.port ?? null,
|
|
10858
11525
|
error: error instanceof Error ? error.message : String(error)
|
|
10859
11526
|
});
|
|
@@ -10864,10 +11531,10 @@ async function fetchHermesApi(fetcher, config, path25, init, options) {
|
|
|
10864
11531
|
);
|
|
10865
11532
|
});
|
|
10866
11533
|
}
|
|
10867
|
-
function logHermesApiResponse(logger, method,
|
|
11534
|
+
function logHermesApiResponse(logger, method, path26, startedAt, response) {
|
|
10868
11535
|
const fields = {
|
|
10869
11536
|
method,
|
|
10870
|
-
path:
|
|
11537
|
+
path: path26,
|
|
10871
11538
|
status: response.status,
|
|
10872
11539
|
duration_ms: Date.now() - startedAt
|
|
10873
11540
|
};
|
|
@@ -10887,10 +11554,10 @@ async function logHermesApiFailureResponse(logger, fields, response) {
|
|
|
10887
11554
|
...upstreamError ? { upstream_error: upstreamError } : {}
|
|
10888
11555
|
});
|
|
10889
11556
|
}
|
|
10890
|
-
function logHermesApiError(logger, method,
|
|
11557
|
+
function logHermesApiError(logger, method, path26, startedAt, error) {
|
|
10891
11558
|
void logger?.warn("hermes_api_request_failed", {
|
|
10892
11559
|
method,
|
|
10893
|
-
path:
|
|
11560
|
+
path: path26,
|
|
10894
11561
|
duration_ms: Date.now() - startedAt,
|
|
10895
11562
|
...error instanceof LinkHttpError ? { status: error.status, code: error.code } : {},
|
|
10896
11563
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -10953,7 +11620,7 @@ function readString10(payload, key) {
|
|
|
10953
11620
|
|
|
10954
11621
|
// src/conversations/history-builder.ts
|
|
10955
11622
|
import { readFile as readFile10, stat as stat10 } from "fs/promises";
|
|
10956
|
-
import
|
|
11623
|
+
import path16 from "path";
|
|
10957
11624
|
var HISTORY_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
|
|
10958
11625
|
var HERMES_HISTORY_COLUMNS = [
|
|
10959
11626
|
"role",
|
|
@@ -11014,13 +11681,13 @@ async function readHermesTranscriptHistory(sessionId, profileName) {
|
|
|
11014
11681
|
}
|
|
11015
11682
|
const normalizedProfileName = isValidProfileName2(profileName) ? profileName : "default";
|
|
11016
11683
|
const profileDir = resolveHermesProfileDir(normalizedProfileName);
|
|
11017
|
-
const dbPath =
|
|
11684
|
+
const dbPath = path16.join(profileDir, "state.db");
|
|
11018
11685
|
const sessionsDirConfig = await readHermesSessionsDir(normalizedProfileName).then((value) => ({
|
|
11019
11686
|
sessionsDir: value.sessionsDir,
|
|
11020
11687
|
configured: value.configured,
|
|
11021
11688
|
configError: false
|
|
11022
11689
|
})).catch(() => ({
|
|
11023
|
-
sessionsDir:
|
|
11690
|
+
sessionsDir: path16.join(profileDir, "sessions"),
|
|
11024
11691
|
configured: false,
|
|
11025
11692
|
configError: true
|
|
11026
11693
|
}));
|
|
@@ -11078,7 +11745,7 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
|
|
|
11078
11745
|
if (!isValidSessionFileStem(sessionId)) {
|
|
11079
11746
|
return empty;
|
|
11080
11747
|
}
|
|
11081
|
-
const transcriptPath =
|
|
11748
|
+
const transcriptPath = path16.join(sessionsDir, `${sessionId}.jsonl`);
|
|
11082
11749
|
const raw = await readFile10(transcriptPath, "utf8").catch((error) => {
|
|
11083
11750
|
if (isNodeError12(error, "ENOENT")) {
|
|
11084
11751
|
return "";
|
|
@@ -11321,7 +11988,7 @@ function normalizeProfileForCompare(value) {
|
|
|
11321
11988
|
// src/hermes/stt.ts
|
|
11322
11989
|
import { execFile as execFile3 } from "child_process";
|
|
11323
11990
|
import { access as access2, readFile as readFile11, stat as stat11 } from "fs/promises";
|
|
11324
|
-
import
|
|
11991
|
+
import path17 from "path";
|
|
11325
11992
|
import { promisify as promisify3 } from "util";
|
|
11326
11993
|
var execFileAsync3 = promisify3(execFile3);
|
|
11327
11994
|
var STT_RESULT_PREFIX = "__HERMES_LINK_STT__";
|
|
@@ -11416,7 +12083,7 @@ async function buildHermesSttEnv(profileName) {
|
|
|
11416
12083
|
};
|
|
11417
12084
|
const devSource = await findDevHermesAgentSource();
|
|
11418
12085
|
if (devSource) {
|
|
11419
|
-
env.PYTHONPATH = [devSource, env.PYTHONPATH].filter(Boolean).join(
|
|
12086
|
+
env.PYTHONPATH = [devSource, env.PYTHONPATH].filter(Boolean).join(path17.delimiter);
|
|
11420
12087
|
}
|
|
11421
12088
|
return env;
|
|
11422
12089
|
}
|
|
@@ -11463,14 +12130,14 @@ async function resolveHermesPythonCommand() {
|
|
|
11463
12130
|
};
|
|
11464
12131
|
}
|
|
11465
12132
|
async function resolveExecutablePath(command) {
|
|
11466
|
-
if (
|
|
12133
|
+
if (path17.isAbsolute(command)) {
|
|
11467
12134
|
return await isExecutableFile(command) ? command : null;
|
|
11468
12135
|
}
|
|
11469
12136
|
const pathEnv = process.env.PATH ?? "";
|
|
11470
12137
|
const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
11471
|
-
for (const dir of pathEnv.split(
|
|
12138
|
+
for (const dir of pathEnv.split(path17.delimiter)) {
|
|
11472
12139
|
for (const extension of extensions) {
|
|
11473
|
-
const candidate =
|
|
12140
|
+
const candidate = path17.join(dir, `${command}${extension}`);
|
|
11474
12141
|
if (await isExecutableFile(candidate)) {
|
|
11475
12142
|
return candidate;
|
|
11476
12143
|
}
|
|
@@ -11510,8 +12177,8 @@ function shebangToPythonCommand(shebang) {
|
|
|
11510
12177
|
}
|
|
11511
12178
|
async function findDevHermesAgentSource() {
|
|
11512
12179
|
const candidates = [
|
|
11513
|
-
|
|
11514
|
-
|
|
12180
|
+
path17.resolve(process.cwd(), "reference/hermes-agent"),
|
|
12181
|
+
path17.resolve(process.cwd(), "../../reference/hermes-agent")
|
|
11515
12182
|
];
|
|
11516
12183
|
for (const candidate of candidates) {
|
|
11517
12184
|
if (await isDirectory(candidate)) {
|
|
@@ -13152,7 +13819,7 @@ var ConversationService = class {
|
|
|
13152
13819
|
async createConversation(input = {}) {
|
|
13153
13820
|
await this.store.ensureConversationsDir();
|
|
13154
13821
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13155
|
-
const id = `conv_${
|
|
13822
|
+
const id = `conv_${randomUUID8().replaceAll("-", "")}`;
|
|
13156
13823
|
const title = input.title?.trim() || DEFAULT_CONVERSATION_TITLE;
|
|
13157
13824
|
const profile = await resolveConversationProfileTarget(
|
|
13158
13825
|
this.paths,
|
|
@@ -13239,7 +13906,7 @@ var ConversationService = class {
|
|
|
13239
13906
|
manifest.profile_name_snapshot ?? manifest.profile ?? input.profileName
|
|
13240
13907
|
);
|
|
13241
13908
|
const message = {
|
|
13242
|
-
id: `msg_${
|
|
13909
|
+
id: `msg_${randomUUID8().replaceAll("-", "")}`,
|
|
13243
13910
|
schema_version: 1,
|
|
13244
13911
|
conversation_id: manifest.id,
|
|
13245
13912
|
role: "assistant",
|
|
@@ -13703,6 +14370,18 @@ var ConversationService = class {
|
|
|
13703
14370
|
async deleteConversation(conversationId) {
|
|
13704
14371
|
return this.maintenance.deleteConversation(conversationId);
|
|
13705
14372
|
}
|
|
14373
|
+
prepareClearAllConversationPlan() {
|
|
14374
|
+
return this.maintenance.prepareClearAllConversationPlan();
|
|
14375
|
+
}
|
|
14376
|
+
readClearAllConversationPlan(planId) {
|
|
14377
|
+
return this.maintenance.readClearAllConversationPlan(planId);
|
|
14378
|
+
}
|
|
14379
|
+
executeClearAllConversationPlan(planId) {
|
|
14380
|
+
return this.maintenance.executeClearAllConversationPlan(planId);
|
|
14381
|
+
}
|
|
14382
|
+
startClearAllConversationPlan(planId) {
|
|
14383
|
+
return this.maintenance.startClearAllConversationPlan(planId);
|
|
14384
|
+
}
|
|
13706
14385
|
async deleteConversations(conversationIds) {
|
|
13707
14386
|
return this.maintenance.deleteConversations(conversationIds);
|
|
13708
14387
|
}
|
|
@@ -13831,8 +14510,8 @@ function findApproval(snapshot, approvalId) {
|
|
|
13831
14510
|
}
|
|
13832
14511
|
|
|
13833
14512
|
// src/identity/identity.ts
|
|
13834
|
-
import { generateKeyPairSync, randomUUID as
|
|
13835
|
-
import { mkdir as
|
|
14513
|
+
import { generateKeyPairSync, randomUUID as randomUUID9, sign } from "crypto";
|
|
14514
|
+
import { mkdir as mkdir9, chmod as chmod2 } from "fs/promises";
|
|
13836
14515
|
import { z } from "zod";
|
|
13837
14516
|
var linkIdentitySchema = z.object({
|
|
13838
14517
|
install_id: z.string().min(1),
|
|
@@ -13854,12 +14533,12 @@ async function ensureIdentity(paths = resolveRuntimePaths()) {
|
|
|
13854
14533
|
if (existing) {
|
|
13855
14534
|
return existing;
|
|
13856
14535
|
}
|
|
13857
|
-
await
|
|
14536
|
+
await mkdir9(paths.homeDir, { recursive: true, mode: 448 });
|
|
13858
14537
|
await chmod2(paths.homeDir, 448).catch(() => void 0);
|
|
13859
14538
|
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
13860
14539
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13861
14540
|
const identity = {
|
|
13862
|
-
install_id: `install_${
|
|
14541
|
+
install_id: `install_${randomUUID9().replaceAll("-", "")}`,
|
|
13863
14542
|
link_id: null,
|
|
13864
14543
|
public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
13865
14544
|
private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
@@ -13896,7 +14575,7 @@ function getIdentityStatus(identity) {
|
|
|
13896
14575
|
}
|
|
13897
14576
|
|
|
13898
14577
|
// src/security/devices.ts
|
|
13899
|
-
import { randomBytes as randomBytes2, randomUUID as
|
|
14578
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID10, timingSafeEqual, createHash as createHash4 } from "crypto";
|
|
13900
14579
|
var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
|
|
13901
14580
|
var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
13902
14581
|
var DEVICE_SEEN_WRITE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
@@ -13914,7 +14593,7 @@ async function createDeviceSession(input, paths = resolveRuntimePaths()) {
|
|
|
13914
14593
|
}
|
|
13915
14594
|
}
|
|
13916
14595
|
const device = {
|
|
13917
|
-
id: `dev_${
|
|
14596
|
+
id: `dev_${randomUUID10().replaceAll("-", "")}`,
|
|
13918
14597
|
label: normalizeDeviceLabel(input.label),
|
|
13919
14598
|
platform: normalizeDevicePlatform(input.platform),
|
|
13920
14599
|
model: normalizeDeviceModel(input.model),
|
|
@@ -14828,6 +15507,37 @@ function registerConversationRoutes(router, options) {
|
|
|
14828
15507
|
await authenticateRequest(ctx, paths);
|
|
14829
15508
|
ctx.body = { ok: true };
|
|
14830
15509
|
});
|
|
15510
|
+
router.post("/api/v1/conversations/clear-plans", async (ctx) => {
|
|
15511
|
+
await authenticateRequest(ctx, paths);
|
|
15512
|
+
const plan = await conversations.prepareClearAllConversationPlan();
|
|
15513
|
+
ctx.status = 201;
|
|
15514
|
+
ctx.body = {
|
|
15515
|
+
ok: true,
|
|
15516
|
+
plan
|
|
15517
|
+
};
|
|
15518
|
+
});
|
|
15519
|
+
router.get("/api/v1/conversations/clear-plans/:planId", async (ctx) => {
|
|
15520
|
+
await authenticateRequest(ctx, paths);
|
|
15521
|
+
ctx.set("cache-control", "no-store");
|
|
15522
|
+
ctx.body = {
|
|
15523
|
+
ok: true,
|
|
15524
|
+
plan: await conversations.readClearAllConversationPlan(ctx.params.planId)
|
|
15525
|
+
};
|
|
15526
|
+
});
|
|
15527
|
+
router.post(
|
|
15528
|
+
"/api/v1/conversations/clear-plans/:planId/execute",
|
|
15529
|
+
async (ctx) => {
|
|
15530
|
+
await authenticateRequest(ctx, paths);
|
|
15531
|
+
const plan = await conversations.startClearAllConversationPlan(
|
|
15532
|
+
ctx.params.planId
|
|
15533
|
+
);
|
|
15534
|
+
ctx.status = plan.status === "completed" ? 200 : 202;
|
|
15535
|
+
ctx.body = {
|
|
15536
|
+
ok: true,
|
|
15537
|
+
plan
|
|
15538
|
+
};
|
|
15539
|
+
}
|
|
15540
|
+
);
|
|
14831
15541
|
router.delete("/api/v1/conversations", async (ctx) => {
|
|
14832
15542
|
await authenticateRequest(ctx, paths);
|
|
14833
15543
|
const body = await readJsonBody(ctx.req);
|
|
@@ -15051,14 +15761,14 @@ function createHttpErrorMiddleware(logger) {
|
|
|
15051
15761
|
// src/hermes/profiles.ts
|
|
15052
15762
|
import { readdir as readdir9, readFile as readFile12, rename as rename3, rm as rm6, stat as stat12 } from "fs/promises";
|
|
15053
15763
|
import os5 from "os";
|
|
15054
|
-
import
|
|
15764
|
+
import path18 from "path";
|
|
15055
15765
|
import YAML2 from "yaml";
|
|
15056
15766
|
var DEFAULT_PROFILE = "default";
|
|
15057
15767
|
var PROFILE_NAME_PATTERN4 = /^[a-zA-Z0-9._-]{1,64}$/;
|
|
15058
15768
|
async function listHermesProfiles(paths = resolveRuntimePaths()) {
|
|
15059
15769
|
const profiles = /* @__PURE__ */ new Map();
|
|
15060
15770
|
profiles.set(DEFAULT_PROFILE, await profileInfo(DEFAULT_PROFILE, paths));
|
|
15061
|
-
const profilesDir =
|
|
15771
|
+
const profilesDir = path18.join(os5.homedir(), ".hermes", "profiles");
|
|
15062
15772
|
const entries = await readdir9(profilesDir, { withFileTypes: true }).catch(
|
|
15063
15773
|
(error) => {
|
|
15064
15774
|
if (isNodeError14(error, "ENOENT")) {
|
|
@@ -15153,7 +15863,7 @@ async function readHermesProfileCapabilities(name) {
|
|
|
15153
15863
|
return {
|
|
15154
15864
|
defaultModel: listedModels?.defaultModel ?? null,
|
|
15155
15865
|
modelCount: listedModels?.models.length ?? 0,
|
|
15156
|
-
skillCount: await countSkills(
|
|
15866
|
+
skillCount: await countSkills(path18.join(profileDir, "skills")).catch(
|
|
15157
15867
|
() => 0
|
|
15158
15868
|
),
|
|
15159
15869
|
toolCount: await countConfiguredTools(name).catch(() => 0)
|
|
@@ -15209,7 +15919,7 @@ async function countSkills(root) {
|
|
|
15209
15919
|
);
|
|
15210
15920
|
let count = 0;
|
|
15211
15921
|
for (const entry of entries) {
|
|
15212
|
-
const entryPath =
|
|
15922
|
+
const entryPath = path18.join(root, entry.name);
|
|
15213
15923
|
if (entry.name === ".git" || entry.name === ".hub") {
|
|
15214
15924
|
continue;
|
|
15215
15925
|
}
|
|
@@ -15906,12 +16616,12 @@ import { spawn as spawn2 } from "child_process";
|
|
|
15906
16616
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
15907
16617
|
import {
|
|
15908
16618
|
cp,
|
|
15909
|
-
mkdir as
|
|
16619
|
+
mkdir as mkdir10,
|
|
15910
16620
|
readFile as readFile13,
|
|
15911
16621
|
rm as rm7,
|
|
15912
16622
|
stat as stat13
|
|
15913
16623
|
} from "fs/promises";
|
|
15914
|
-
import
|
|
16624
|
+
import path19 from "path";
|
|
15915
16625
|
import YAML3 from "yaml";
|
|
15916
16626
|
var PROFILE_CREATE_LOG_FILE = "profile-create.log";
|
|
15917
16627
|
var PROFILE_CREATE_LOG_MAX_FILES = 3;
|
|
@@ -15959,7 +16669,7 @@ async function startHermesProfileCreation(input, options) {
|
|
|
15959
16669
|
signal: null,
|
|
15960
16670
|
error: null
|
|
15961
16671
|
};
|
|
15962
|
-
await
|
|
16672
|
+
await mkdir10(options.paths.runDir, { recursive: true, mode: 448 });
|
|
15963
16673
|
await writeProfileCreationState(options.paths, started);
|
|
15964
16674
|
await writer.write(`
|
|
15965
16675
|
=== profile creation started ${startedAt} ===
|
|
@@ -16362,7 +17072,7 @@ function collectEnvKeys(value, keys = /* @__PURE__ */ new Set()) {
|
|
|
16362
17072
|
return keys;
|
|
16363
17073
|
}
|
|
16364
17074
|
async function writeEnvValues(profileName, values) {
|
|
16365
|
-
const envPath =
|
|
17075
|
+
const envPath = path19.join(resolveHermesProfileDir(profileName), ".env");
|
|
16366
17076
|
const existingRaw = await readFile13(envPath, "utf8").catch((error) => {
|
|
16367
17077
|
if (isNodeError15(error, "ENOENT")) {
|
|
16368
17078
|
return "";
|
|
@@ -16399,8 +17109,8 @@ async function writeEnvValues(profileName, values) {
|
|
|
16399
17109
|
await atomicWriteFilePreservingMetadata(envPath, nextRaw);
|
|
16400
17110
|
}
|
|
16401
17111
|
async function copySkills(sourceProfile, targetProfile) {
|
|
16402
|
-
const sourceSkills =
|
|
16403
|
-
const targetSkills =
|
|
17112
|
+
const sourceSkills = path19.join(resolveHermesProfileDir(sourceProfile), "skills");
|
|
17113
|
+
const targetSkills = path19.join(resolveHermesProfileDir(targetProfile), "skills");
|
|
16404
17114
|
if (!await pathExists(sourceSkills)) {
|
|
16405
17115
|
return;
|
|
16406
17116
|
}
|
|
@@ -16495,10 +17205,10 @@ async function readProfileCreationLogLines(paths) {
|
|
|
16495
17205
|
);
|
|
16496
17206
|
}
|
|
16497
17207
|
function profileCreationStatePath(paths) {
|
|
16498
|
-
return
|
|
17208
|
+
return path19.join(paths.runDir, "profile-create-state.json");
|
|
16499
17209
|
}
|
|
16500
17210
|
function profileCreationLogPath(paths) {
|
|
16501
|
-
return
|
|
17211
|
+
return path19.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
|
|
16502
17212
|
}
|
|
16503
17213
|
async function clearProfileCreationLogFiles(paths) {
|
|
16504
17214
|
const primary = profileCreationLogPath(paths);
|
|
@@ -16772,7 +17482,7 @@ import {
|
|
|
16772
17482
|
readFile as readFile14,
|
|
16773
17483
|
stat as stat14
|
|
16774
17484
|
} from "fs/promises";
|
|
16775
|
-
import
|
|
17485
|
+
import path20 from "path";
|
|
16776
17486
|
import YAML4 from "yaml";
|
|
16777
17487
|
var ENTRY_DELIMITER = "\n\xA7\n";
|
|
16778
17488
|
var DEFAULT_MEMORY_LIMIT = 2200;
|
|
@@ -16989,7 +17699,7 @@ async function saveProviderSettings(profileName, provider, patch) {
|
|
|
16989
17699
|
if (provider === "hindsight") {
|
|
16990
17700
|
await patchJsonProviderConfig(
|
|
16991
17701
|
profileName,
|
|
16992
|
-
|
|
17702
|
+
path20.join("hindsight", "config.json"),
|
|
16993
17703
|
{
|
|
16994
17704
|
mode: patch.mode,
|
|
16995
17705
|
api_url: patch.apiUrl,
|
|
@@ -17156,7 +17866,7 @@ async function patchHermesMemoryProvider(profileName, provider) {
|
|
|
17156
17866
|
await atomicWriteFilePreservingMetadata(configPath, document.toString());
|
|
17157
17867
|
}
|
|
17158
17868
|
function resolveMemoryDir(profileName) {
|
|
17159
|
-
return
|
|
17869
|
+
return path20.join(resolveHermesProfileDir(profileName), "memories");
|
|
17160
17870
|
}
|
|
17161
17871
|
async function readMemoryStore(profileName, target, limits) {
|
|
17162
17872
|
const filePath = memoryFilePath(profileName, target);
|
|
@@ -17217,7 +17927,7 @@ async function writeMemoryEntries(profileName, target, entries) {
|
|
|
17217
17927
|
);
|
|
17218
17928
|
}
|
|
17219
17929
|
function memoryFilePath(profileName, target) {
|
|
17220
|
-
return
|
|
17930
|
+
return path20.join(
|
|
17221
17931
|
resolveMemoryDir(profileName),
|
|
17222
17932
|
target === "user" ? "USER.md" : "MEMORY.md"
|
|
17223
17933
|
);
|
|
@@ -17277,7 +17987,7 @@ async function readCustomProviderSetupSummary(profileName) {
|
|
|
17277
17987
|
configurable: true,
|
|
17278
17988
|
configured: true,
|
|
17279
17989
|
configurationIssue: null,
|
|
17280
|
-
providerConfigPath:
|
|
17990
|
+
providerConfigPath: path20.join(
|
|
17281
17991
|
resolveHermesProfileDir(profileName),
|
|
17282
17992
|
"<provider>.json"
|
|
17283
17993
|
),
|
|
@@ -17631,7 +18341,7 @@ async function readProviderSettings(profileName, provider) {
|
|
|
17631
18341
|
stringSetting(
|
|
17632
18342
|
"dbPath",
|
|
17633
18343
|
"SQLite \u6570\u636E\u5E93\u8DEF\u5F84",
|
|
17634
|
-
config.db_path ??
|
|
18344
|
+
config.db_path ?? path20.join(resolveHermesProfileDir(profileName), "memory_store.db")
|
|
17635
18345
|
),
|
|
17636
18346
|
booleanSetting("autoExtract", "\u4F1A\u8BDD\u7ED3\u675F\u81EA\u52A8\u62BD\u53D6", config.auto_extract ?? false),
|
|
17637
18347
|
numberSetting("defaultTrust", "\u9ED8\u8BA4\u4FE1\u4EFB\u5206", config.default_trust ?? 0.5),
|
|
@@ -17654,7 +18364,7 @@ async function readProviderSettings(profileName, provider) {
|
|
|
17654
18364
|
stringSetting(
|
|
17655
18365
|
"workingDirectory",
|
|
17656
18366
|
"\u5DE5\u4F5C\u76EE\u5F55",
|
|
17657
|
-
|
|
18367
|
+
path20.join(resolveHermesProfileDir(profileName), "byterover"),
|
|
17658
18368
|
false
|
|
17659
18369
|
)
|
|
17660
18370
|
];
|
|
@@ -17663,16 +18373,16 @@ async function readProviderSettings(profileName, provider) {
|
|
|
17663
18373
|
}
|
|
17664
18374
|
function memoryProviderConfigPath(profileName, provider) {
|
|
17665
18375
|
if (provider === "honcho") {
|
|
17666
|
-
return
|
|
18376
|
+
return path20.join(resolveHermesProfileDir(profileName), "honcho.json");
|
|
17667
18377
|
}
|
|
17668
18378
|
if (provider === "mem0") {
|
|
17669
|
-
return
|
|
18379
|
+
return path20.join(resolveHermesProfileDir(profileName), "mem0.json");
|
|
17670
18380
|
}
|
|
17671
18381
|
if (provider === "supermemory") {
|
|
17672
|
-
return
|
|
18382
|
+
return path20.join(resolveHermesProfileDir(profileName), "supermemory.json");
|
|
17673
18383
|
}
|
|
17674
18384
|
if (provider === "hindsight") {
|
|
17675
|
-
return
|
|
18385
|
+
return path20.join(
|
|
17676
18386
|
resolveHermesProfileDir(profileName),
|
|
17677
18387
|
"hindsight",
|
|
17678
18388
|
"config.json"
|
|
@@ -17681,13 +18391,13 @@ function memoryProviderConfigPath(profileName, provider) {
|
|
|
17681
18391
|
return null;
|
|
17682
18392
|
}
|
|
17683
18393
|
function customProviderConfigPath(profileName, provider) {
|
|
17684
|
-
return
|
|
18394
|
+
return path20.join(
|
|
17685
18395
|
resolveHermesProfileDir(profileName),
|
|
17686
18396
|
`${normalizeCustomProviderId(provider)}.json`
|
|
17687
18397
|
);
|
|
17688
18398
|
}
|
|
17689
18399
|
function customProviderRegistryPath(profileName) {
|
|
17690
|
-
return
|
|
18400
|
+
return path20.join(
|
|
17691
18401
|
resolveHermesProfileDir(profileName),
|
|
17692
18402
|
CUSTOM_PROVIDER_REGISTRY_FILE
|
|
17693
18403
|
);
|
|
@@ -17739,7 +18449,7 @@ async function saveCustomProviderRegistryEntry(profileName, provider) {
|
|
|
17739
18449
|
);
|
|
17740
18450
|
}
|
|
17741
18451
|
async function discoverUserMemoryProviderDescriptors(profileName) {
|
|
17742
|
-
const pluginsDir =
|
|
18452
|
+
const pluginsDir = path20.join(resolveHermesProfileDir(profileName), "plugins");
|
|
17743
18453
|
const entries = await readdir10(pluginsDir, { withFileTypes: true }).catch(
|
|
17744
18454
|
(error) => {
|
|
17745
18455
|
if (isNodeError16(error, "ENOENT")) {
|
|
@@ -17759,7 +18469,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
|
|
|
17759
18469
|
} catch {
|
|
17760
18470
|
continue;
|
|
17761
18471
|
}
|
|
17762
|
-
const providerDir =
|
|
18472
|
+
const providerDir = path20.join(pluginsDir, entry.name);
|
|
17763
18473
|
if (!await isMemoryProviderPluginDir(providerDir)) {
|
|
17764
18474
|
continue;
|
|
17765
18475
|
}
|
|
@@ -17773,7 +18483,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
|
|
|
17773
18483
|
return descriptors;
|
|
17774
18484
|
}
|
|
17775
18485
|
async function isUserMemoryProviderInstalled(profileName, provider) {
|
|
17776
|
-
const providerDir =
|
|
18486
|
+
const providerDir = path20.join(
|
|
17777
18487
|
resolveHermesProfileDir(profileName),
|
|
17778
18488
|
"plugins",
|
|
17779
18489
|
normalizeCustomProviderId(provider)
|
|
@@ -17781,7 +18491,7 @@ async function isUserMemoryProviderInstalled(profileName, provider) {
|
|
|
17781
18491
|
return isMemoryProviderPluginDir(providerDir);
|
|
17782
18492
|
}
|
|
17783
18493
|
async function isMemoryProviderPluginDir(providerDir) {
|
|
17784
|
-
const source = await readFile14(
|
|
18494
|
+
const source = await readFile14(path20.join(providerDir, "__init__.py"), "utf8").catch(
|
|
17785
18495
|
(error) => {
|
|
17786
18496
|
if (isNodeError16(error, "ENOENT")) {
|
|
17787
18497
|
return "";
|
|
@@ -17793,7 +18503,7 @@ async function isMemoryProviderPluginDir(providerDir) {
|
|
|
17793
18503
|
return sample.includes("register_memory_provider") || sample.includes("MemoryProvider");
|
|
17794
18504
|
}
|
|
17795
18505
|
async function readPluginMetadata(providerDir) {
|
|
17796
|
-
const raw = await readFile14(
|
|
18506
|
+
const raw = await readFile14(path20.join(providerDir, "plugin.yaml"), "utf8").catch(
|
|
17797
18507
|
(error) => {
|
|
17798
18508
|
if (isNodeError16(error, "ENOENT")) {
|
|
17799
18509
|
return "";
|
|
@@ -17805,10 +18515,10 @@ async function readPluginMetadata(providerDir) {
|
|
|
17805
18515
|
}
|
|
17806
18516
|
async function resolveByteRoverCli() {
|
|
17807
18517
|
const candidates = [
|
|
17808
|
-
...(process.env.PATH ?? "").split(
|
|
17809
|
-
|
|
18518
|
+
...(process.env.PATH ?? "").split(path20.delimiter).filter(Boolean).map((dir) => path20.join(dir, "brv")),
|
|
18519
|
+
path20.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
|
|
17810
18520
|
"/usr/local/bin/brv",
|
|
17811
|
-
|
|
18521
|
+
path20.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
|
|
17812
18522
|
].filter(Boolean);
|
|
17813
18523
|
for (const candidate of candidates) {
|
|
17814
18524
|
const found = await access3(candidate).then(() => true).catch(() => false);
|
|
@@ -17869,7 +18579,7 @@ async function patchHermesMemoryEnv(profileName, patch) {
|
|
|
17869
18579
|
if (entries.length === 0) {
|
|
17870
18580
|
return;
|
|
17871
18581
|
}
|
|
17872
|
-
const envPath =
|
|
18582
|
+
const envPath = path20.join(resolveHermesProfileDir(profileName), ".env");
|
|
17873
18583
|
const existingRaw = await readFile14(envPath, "utf8").catch((error) => {
|
|
17874
18584
|
if (isNodeError16(error, "ENOENT")) {
|
|
17875
18585
|
return "";
|
|
@@ -17943,7 +18653,7 @@ async function readActiveMemoryProvider(profileName) {
|
|
|
17943
18653
|
return provider;
|
|
17944
18654
|
}
|
|
17945
18655
|
async function patchJsonProviderConfig(profileName, relativePath, patch) {
|
|
17946
|
-
const configPath =
|
|
18656
|
+
const configPath = path20.join(
|
|
17947
18657
|
resolveHermesProfileDir(profileName),
|
|
17948
18658
|
relativePath
|
|
17949
18659
|
);
|
|
@@ -17972,7 +18682,7 @@ async function readJsonObject(filePath) {
|
|
|
17972
18682
|
} catch {
|
|
17973
18683
|
throw new HermesMemoryError(
|
|
17974
18684
|
"memory_provider_config_invalid",
|
|
17975
|
-
`${
|
|
18685
|
+
`${path20.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
|
|
17976
18686
|
);
|
|
17977
18687
|
}
|
|
17978
18688
|
}
|
|
@@ -18518,7 +19228,7 @@ function toMemoryHttpError(error) {
|
|
|
18518
19228
|
|
|
18519
19229
|
// src/hermes/skills.ts
|
|
18520
19230
|
import { readFile as readFile15, readdir as readdir11 } from "fs/promises";
|
|
18521
|
-
import
|
|
19231
|
+
import path21 from "path";
|
|
18522
19232
|
import YAML5 from "yaml";
|
|
18523
19233
|
var HermesSkillNotFoundError = class extends Error {
|
|
18524
19234
|
constructor(skillName) {
|
|
@@ -18532,7 +19242,7 @@ var EXCLUDED_SKILL_DIRS = /* @__PURE__ */ new Set([".git", ".github", ".hub"]);
|
|
|
18532
19242
|
async function listHermesProfileSkills(profileName, paths = resolveRuntimePaths()) {
|
|
18533
19243
|
const profile = await readExistingProfile(profileName, paths);
|
|
18534
19244
|
const profileDir = resolveHermesProfileDir(profile.name);
|
|
18535
|
-
const skillsRoot =
|
|
19245
|
+
const skillsRoot = path21.join(profileDir, "skills");
|
|
18536
19246
|
const [skillFiles, disabled, provenance] = await Promise.all([
|
|
18537
19247
|
findSkillFiles(skillsRoot),
|
|
18538
19248
|
readDisabledSkillNames(resolveHermesConfigPath(profile.name)),
|
|
@@ -18623,7 +19333,7 @@ async function collectSkillFiles(directory, results) {
|
|
|
18623
19333
|
if (EXCLUDED_SKILL_DIRS.has(entry.name)) {
|
|
18624
19334
|
continue;
|
|
18625
19335
|
}
|
|
18626
|
-
const entryPath =
|
|
19336
|
+
const entryPath = path21.join(directory, entry.name);
|
|
18627
19337
|
if (entry.isDirectory()) {
|
|
18628
19338
|
await collectSkillFiles(entryPath, results);
|
|
18629
19339
|
continue;
|
|
@@ -18645,10 +19355,10 @@ async function readSkillMetadata(input) {
|
|
|
18645
19355
|
if (raw === null) {
|
|
18646
19356
|
return null;
|
|
18647
19357
|
}
|
|
18648
|
-
const skillDir =
|
|
19358
|
+
const skillDir = path21.dirname(input.skillFile);
|
|
18649
19359
|
const { frontmatter, body } = parseSkillDocument(raw.slice(0, 4e3));
|
|
18650
19360
|
const name = normalizeSkillName(
|
|
18651
|
-
readString16(frontmatter.name) ??
|
|
19361
|
+
readString16(frontmatter.name) ?? path21.basename(skillDir)
|
|
18652
19362
|
);
|
|
18653
19363
|
if (!name) {
|
|
18654
19364
|
return null;
|
|
@@ -18667,7 +19377,7 @@ async function readSkillMetadata(input) {
|
|
|
18667
19377
|
enabled: !input.disabled.has(name),
|
|
18668
19378
|
source: provenance.source,
|
|
18669
19379
|
trust: provenance.trust,
|
|
18670
|
-
relativePath:
|
|
19380
|
+
relativePath: path21.relative(input.skillsRoot, skillDir)
|
|
18671
19381
|
};
|
|
18672
19382
|
}
|
|
18673
19383
|
function parseSkillDocument(raw) {
|
|
@@ -18688,8 +19398,8 @@ function parseSkillDocument(raw) {
|
|
|
18688
19398
|
}
|
|
18689
19399
|
}
|
|
18690
19400
|
function categoryFromPath(skillsRoot, skillFile) {
|
|
18691
|
-
const relative =
|
|
18692
|
-
const parts = relative.split(
|
|
19401
|
+
const relative = path21.relative(skillsRoot, skillFile);
|
|
19402
|
+
const parts = relative.split(path21.sep).filter(Boolean);
|
|
18693
19403
|
return parts.length >= 3 ? parts[0] : null;
|
|
18694
19404
|
}
|
|
18695
19405
|
function firstBodyDescription(body) {
|
|
@@ -18736,7 +19446,7 @@ async function readSkillProvenance(root) {
|
|
|
18736
19446
|
return provenance;
|
|
18737
19447
|
}
|
|
18738
19448
|
async function readBundledSkillNames(root) {
|
|
18739
|
-
const raw = await readFile15(
|
|
19449
|
+
const raw = await readFile15(path21.join(root, ".bundled_manifest"), "utf8").catch(
|
|
18740
19450
|
(error) => {
|
|
18741
19451
|
if (isNodeError17(error, "ENOENT")) {
|
|
18742
19452
|
return "";
|
|
@@ -18759,7 +19469,7 @@ async function readBundledSkillNames(root) {
|
|
|
18759
19469
|
return names;
|
|
18760
19470
|
}
|
|
18761
19471
|
async function readHubInstalledSkills(root) {
|
|
18762
|
-
const raw = await readFile15(
|
|
19472
|
+
const raw = await readFile15(path21.join(root, ".hub", "lock.json"), "utf8").catch(
|
|
18763
19473
|
(error) => {
|
|
18764
19474
|
if (isNodeError17(error, "ENOENT")) {
|
|
18765
19475
|
return "";
|
|
@@ -19362,8 +20072,8 @@ function readModelList(payload) {
|
|
|
19362
20072
|
// src/hermes/updates.ts
|
|
19363
20073
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
19364
20074
|
import { spawn as spawn3 } from "child_process";
|
|
19365
|
-
import { mkdir as
|
|
19366
|
-
import
|
|
20075
|
+
import { mkdir as mkdir11, readFile as readFile16, rm as rm8 } from "fs/promises";
|
|
20076
|
+
import path22 from "path";
|
|
19367
20077
|
var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
|
|
19368
20078
|
var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
19369
20079
|
var RELEASE_FETCH_TIMEOUT_MS = 5e3;
|
|
@@ -19376,7 +20086,10 @@ var runningUpdate = null;
|
|
|
19376
20086
|
async function readHermesUpdateCheck(options) {
|
|
19377
20087
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
19378
20088
|
const [local, remoteResult] = await Promise.all([
|
|
19379
|
-
readHermesVersion(
|
|
20089
|
+
readHermesVersion({
|
|
20090
|
+
fetchImpl: options.fetchImpl,
|
|
20091
|
+
logger: options.logger
|
|
20092
|
+
}).catch((error) => ({
|
|
19380
20093
|
raw: error instanceof Error ? error.message : String(error),
|
|
19381
20094
|
version: null
|
|
19382
20095
|
})),
|
|
@@ -19423,7 +20136,7 @@ async function startHermesUpdate(options) {
|
|
|
19423
20136
|
signal: null,
|
|
19424
20137
|
error: null
|
|
19425
20138
|
};
|
|
19426
|
-
await
|
|
20139
|
+
await mkdir11(options.paths.runDir, { recursive: true, mode: 448 });
|
|
19427
20140
|
await writer.write(`
|
|
19428
20141
|
=== hermes update started ${startedAt} ===
|
|
19429
20142
|
`);
|
|
@@ -19629,13 +20342,13 @@ async function readUpdateLogLines(paths) {
|
|
|
19629
20342
|
);
|
|
19630
20343
|
}
|
|
19631
20344
|
function releaseCachePath(paths) {
|
|
19632
|
-
return
|
|
20345
|
+
return path22.join(paths.indexesDir, "hermes-release-check.json");
|
|
19633
20346
|
}
|
|
19634
20347
|
function updateStatePath(paths) {
|
|
19635
|
-
return
|
|
20348
|
+
return path22.join(paths.runDir, "hermes-update-state.json");
|
|
19636
20349
|
}
|
|
19637
20350
|
function updateLogPath(paths) {
|
|
19638
|
-
return
|
|
20351
|
+
return path22.join(paths.logsDir, UPDATE_LOG_FILE);
|
|
19639
20352
|
}
|
|
19640
20353
|
async function clearUpdateLogFiles(paths) {
|
|
19641
20354
|
const primary = updateLogPath(paths);
|
|
@@ -19727,17 +20440,17 @@ function readString17(payload, key) {
|
|
|
19727
20440
|
// src/link/updates.ts
|
|
19728
20441
|
import { spawn as spawn5 } from "child_process";
|
|
19729
20442
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
19730
|
-
import { mkdir as
|
|
19731
|
-
import
|
|
20443
|
+
import { mkdir as mkdir14, readFile as readFile18, rm as rm11 } from "fs/promises";
|
|
20444
|
+
import path24 from "path";
|
|
19732
20445
|
|
|
19733
20446
|
// src/daemon/process.ts
|
|
19734
20447
|
import { spawn as spawn4 } from "child_process";
|
|
19735
|
-
import { mkdir as
|
|
19736
|
-
import
|
|
20448
|
+
import { mkdir as mkdir13, readFile as readFile17, rm as rm10 } from "fs/promises";
|
|
20449
|
+
import path23 from "path";
|
|
19737
20450
|
|
|
19738
20451
|
// src/daemon/service.ts
|
|
19739
20452
|
import { createServer } from "http";
|
|
19740
|
-
import { mkdir as
|
|
20453
|
+
import { mkdir as mkdir12, rm as rm9, writeFile as writeFile3 } from "fs/promises";
|
|
19741
20454
|
|
|
19742
20455
|
// src/relay/control-client.ts
|
|
19743
20456
|
import WebSocket from "ws";
|
|
@@ -20616,6 +21329,7 @@ function startHermesSessionSyncScheduler(options) {
|
|
|
20616
21329
|
}
|
|
20617
21330
|
|
|
20618
21331
|
// src/daemon/service.ts
|
|
21332
|
+
var DEFAULT_RELAY_READY_TIMEOUT_MS = 2e3;
|
|
20619
21333
|
async function startLinkService(options = {}) {
|
|
20620
21334
|
const paths = options.paths ?? resolveRuntimePaths();
|
|
20621
21335
|
const logger = createFileLogger({ paths });
|
|
@@ -20681,6 +21395,10 @@ async function startLinkService(options = {}) {
|
|
|
20681
21395
|
});
|
|
20682
21396
|
let relay = null;
|
|
20683
21397
|
if (identity?.link_id) {
|
|
21398
|
+
let resolveRelayReady = null;
|
|
21399
|
+
const relayReady = new Promise((resolve) => {
|
|
21400
|
+
resolveRelayReady = resolve;
|
|
21401
|
+
});
|
|
20684
21402
|
relay = connectRelayControl({
|
|
20685
21403
|
relayBaseUrl: config.relayBaseUrl,
|
|
20686
21404
|
linkId: identity.link_id,
|
|
@@ -20690,8 +21408,22 @@ async function startLinkService(options = {}) {
|
|
|
20690
21408
|
backoffMaxMs: 3e4,
|
|
20691
21409
|
onStatus: (status) => {
|
|
20692
21410
|
void logger.info("relay_status", status);
|
|
21411
|
+
if (status.state === "connected") {
|
|
21412
|
+
resolveRelayReady?.(true);
|
|
21413
|
+
resolveRelayReady = null;
|
|
21414
|
+
} else if (status.state === "failed") {
|
|
21415
|
+
resolveRelayReady?.(false);
|
|
21416
|
+
resolveRelayReady = null;
|
|
21417
|
+
}
|
|
20693
21418
|
}
|
|
20694
21419
|
});
|
|
21420
|
+
if (options.waitForRelayReady) {
|
|
21421
|
+
await Promise.race([
|
|
21422
|
+
relayReady,
|
|
21423
|
+
waitForRelayReadyTimeout(options.relayReadyTimeoutMs)
|
|
21424
|
+
]);
|
|
21425
|
+
resolveRelayReady = null;
|
|
21426
|
+
}
|
|
20695
21427
|
} else {
|
|
20696
21428
|
void logger.info("relay_skipped", { reason: "link_not_paired" });
|
|
20697
21429
|
}
|
|
@@ -20726,11 +21458,20 @@ async function startLinkService(options = {}) {
|
|
|
20726
21458
|
}
|
|
20727
21459
|
};
|
|
20728
21460
|
}
|
|
21461
|
+
function waitForRelayReadyTimeout(timeoutMs) {
|
|
21462
|
+
return new Promise((resolve) => {
|
|
21463
|
+
const timer = setTimeout(
|
|
21464
|
+
() => resolve(false),
|
|
21465
|
+
timeoutMs ?? DEFAULT_RELAY_READY_TIMEOUT_MS
|
|
21466
|
+
);
|
|
21467
|
+
timer.unref?.();
|
|
21468
|
+
});
|
|
21469
|
+
}
|
|
20729
21470
|
function pidFilePath(paths = resolveRuntimePaths()) {
|
|
20730
21471
|
return `${paths.runDir}/hermeslink.pid`;
|
|
20731
21472
|
}
|
|
20732
21473
|
async function writePidFile(paths) {
|
|
20733
|
-
await
|
|
21474
|
+
await mkdir12(paths.runDir, { recursive: true, mode: 448 });
|
|
20734
21475
|
await writeFile3(pidFilePath(paths), `${process.pid}
|
|
20735
21476
|
`, { mode: 384 });
|
|
20736
21477
|
}
|
|
@@ -20805,8 +21546,8 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
|
|
|
20805
21546
|
return status;
|
|
20806
21547
|
}
|
|
20807
21548
|
}
|
|
20808
|
-
await
|
|
20809
|
-
await
|
|
21549
|
+
await mkdir13(paths.logsDir, { recursive: true, mode: 448 });
|
|
21550
|
+
await mkdir13(paths.runDir, { recursive: true, mode: 448 });
|
|
20810
21551
|
const scriptPath = currentCliScriptPath();
|
|
20811
21552
|
const child = spawn4(process.execPath, [scriptPath, "daemon-supervisor"], {
|
|
20812
21553
|
detached: true,
|
|
@@ -20824,10 +21565,10 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
|
|
|
20824
21565
|
return await getDaemonStatus(paths);
|
|
20825
21566
|
}
|
|
20826
21567
|
async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
|
|
20827
|
-
await
|
|
21568
|
+
await mkdir13(paths.logsDir, { recursive: true, mode: 448 });
|
|
20828
21569
|
const log = createRotatingTextLogWriter({
|
|
20829
21570
|
paths,
|
|
20830
|
-
fileName:
|
|
21571
|
+
fileName: path23.basename(daemonLogFile(paths))
|
|
20831
21572
|
});
|
|
20832
21573
|
const scriptPath = currentCliScriptPath();
|
|
20833
21574
|
const child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
|
|
@@ -21068,7 +21809,7 @@ async function startLinkUpdate(options) {
|
|
|
21068
21809
|
error: null,
|
|
21069
21810
|
manual_command: manualCommand
|
|
21070
21811
|
};
|
|
21071
|
-
await
|
|
21812
|
+
await mkdir14(options.paths.runDir, { recursive: true, mode: 448 });
|
|
21072
21813
|
await writer.write(
|
|
21073
21814
|
`
|
|
21074
21815
|
=== link update started ${startedAt} target=${targetVersion} ===
|
|
@@ -21366,10 +22107,10 @@ async function readUpdateLogLines2(paths) {
|
|
|
21366
22107
|
);
|
|
21367
22108
|
}
|
|
21368
22109
|
function updateStatePath2(paths) {
|
|
21369
|
-
return
|
|
22110
|
+
return path24.join(paths.runDir, "link-update-state.json");
|
|
21370
22111
|
}
|
|
21371
22112
|
function updateLogPath2(paths) {
|
|
21372
|
-
return
|
|
22113
|
+
return path24.join(paths.logsDir, UPDATE_LOG_FILE2);
|
|
21373
22114
|
}
|
|
21374
22115
|
async function clearUpdateLogFiles2(paths) {
|
|
21375
22116
|
const primary = updateLogPath2(paths);
|
|
@@ -21433,10 +22174,21 @@ function readString18(payload, key) {
|
|
|
21433
22174
|
}
|
|
21434
22175
|
|
|
21435
22176
|
// src/pairing/pairing.ts
|
|
21436
|
-
import
|
|
22177
|
+
import path25 from "path";
|
|
21437
22178
|
import { rm as rm12 } from "fs/promises";
|
|
21438
22179
|
|
|
21439
22180
|
// src/relay/bootstrap.ts
|
|
22181
|
+
var RelayNetworkError = class extends Error {
|
|
22182
|
+
constructor(relayBaseUrl, causeMessage) {
|
|
22183
|
+
super(
|
|
22184
|
+
`Cannot reach Hermes Relay at ${relayBaseUrl}. Please check your network connection, VPN, or proxy settings, then try again.`
|
|
22185
|
+
);
|
|
22186
|
+
this.relayBaseUrl = relayBaseUrl;
|
|
22187
|
+
this.causeMessage = causeMessage;
|
|
22188
|
+
}
|
|
22189
|
+
relayBaseUrl;
|
|
22190
|
+
causeMessage;
|
|
22191
|
+
};
|
|
21440
22192
|
async function bootstrapRelayLink(options) {
|
|
21441
22193
|
const fetcher = options.fetchImpl ?? fetch;
|
|
21442
22194
|
const baseUrl = options.relayBaseUrl.replace(/\/+$/u, "");
|
|
@@ -21477,14 +22229,23 @@ async function bootstrapRelayLink(options) {
|
|
|
21477
22229
|
};
|
|
21478
22230
|
}
|
|
21479
22231
|
async function postJson(fetcher, url, token, body) {
|
|
21480
|
-
|
|
21481
|
-
|
|
21482
|
-
|
|
21483
|
-
|
|
21484
|
-
|
|
21485
|
-
|
|
21486
|
-
|
|
21487
|
-
|
|
22232
|
+
let response;
|
|
22233
|
+
try {
|
|
22234
|
+
response = await fetcher(url, {
|
|
22235
|
+
method: "POST",
|
|
22236
|
+
headers: {
|
|
22237
|
+
authorization: `Bearer ${token}`,
|
|
22238
|
+
"content-type": "application/json"
|
|
22239
|
+
},
|
|
22240
|
+
body: JSON.stringify(body)
|
|
22241
|
+
});
|
|
22242
|
+
} catch (error) {
|
|
22243
|
+
const baseUrl = new URL(url).origin;
|
|
22244
|
+
throw new RelayNetworkError(
|
|
22245
|
+
baseUrl,
|
|
22246
|
+
error instanceof Error ? error.message : String(error)
|
|
22247
|
+
);
|
|
22248
|
+
}
|
|
21488
22249
|
const payload = await response.json().catch(() => null);
|
|
21489
22250
|
if (!response.ok) {
|
|
21490
22251
|
const message = readErrorMessage4(payload) ?? `Relay request failed with HTTP ${response.status}`;
|
|
@@ -21512,14 +22273,22 @@ async function preparePairing(paths = resolveRuntimePaths()) {
|
|
|
21512
22273
|
const config = await loadConfig(paths);
|
|
21513
22274
|
const identity = await ensureIdentity(paths);
|
|
21514
22275
|
const systemInfo = readLinkSystemInfo();
|
|
21515
|
-
const created = await postServerJson(
|
|
21516
|
-
|
|
21517
|
-
|
|
21518
|
-
|
|
21519
|
-
|
|
21520
|
-
|
|
21521
|
-
|
|
21522
|
-
|
|
22276
|
+
const created = await postServerJson(
|
|
22277
|
+
config.serverBaseUrl,
|
|
22278
|
+
"/api/v1/link-pairings",
|
|
22279
|
+
{
|
|
22280
|
+
install_id: identity.install_id,
|
|
22281
|
+
link_id: identity.link_id ?? void 0,
|
|
22282
|
+
display_name: systemInfo.defaultDisplayName,
|
|
22283
|
+
platform: systemInfo.platform,
|
|
22284
|
+
hostname: systemInfo.hostname ?? void 0,
|
|
22285
|
+
public_key_pem: identity.public_key_pem
|
|
22286
|
+
},
|
|
22287
|
+
{
|
|
22288
|
+
target: "server",
|
|
22289
|
+
action: "create pairing session"
|
|
22290
|
+
}
|
|
22291
|
+
);
|
|
21523
22292
|
const relayBaseUrl = created.relayBaseUrl || config.relayBaseUrl;
|
|
21524
22293
|
let assigned;
|
|
21525
22294
|
let updatedIdentity;
|
|
@@ -21541,28 +22310,43 @@ async function preparePairing(paths = resolveRuntimePaths()) {
|
|
|
21541
22310
|
installId: updatedIdentity.install_id,
|
|
21542
22311
|
publicKeyPem: updatedIdentity.public_key_pem
|
|
21543
22312
|
});
|
|
21544
|
-
await patchServerJson(
|
|
21545
|
-
|
|
21546
|
-
|
|
21547
|
-
|
|
21548
|
-
|
|
21549
|
-
|
|
21550
|
-
|
|
21551
|
-
|
|
21552
|
-
|
|
21553
|
-
|
|
21554
|
-
|
|
21555
|
-
|
|
21556
|
-
|
|
22313
|
+
await patchServerJson(
|
|
22314
|
+
config.serverBaseUrl,
|
|
22315
|
+
`/api/v1/link-pairings/${created.sessionId}/link`,
|
|
22316
|
+
created.pairingToken,
|
|
22317
|
+
{
|
|
22318
|
+
install_id: updatedIdentity.install_id,
|
|
22319
|
+
link_id: assigned.linkId,
|
|
22320
|
+
link_version: LINK_VERSION,
|
|
22321
|
+
display_name: systemInfo.defaultDisplayName,
|
|
22322
|
+
platform: systemInfo.platform,
|
|
22323
|
+
hostname: systemInfo.hostname ?? void 0,
|
|
22324
|
+
lan_ips: routes.lanIps,
|
|
22325
|
+
public_ipv4s: routes.publicIpv4s,
|
|
22326
|
+
public_ipv6s: routes.publicIpv6s,
|
|
22327
|
+
preferred_urls: routes.preferredUrls,
|
|
22328
|
+
environment: routes.environment
|
|
22329
|
+
},
|
|
22330
|
+
{
|
|
22331
|
+
target: "server",
|
|
22332
|
+
action: "finalize pairing"
|
|
22333
|
+
}
|
|
22334
|
+
);
|
|
21557
22335
|
} catch (error) {
|
|
22336
|
+
const reportedError = error instanceof RelayNetworkError ? createPairingNetworkError({
|
|
22337
|
+
target: "relay",
|
|
22338
|
+
action: "connect to Relay",
|
|
22339
|
+
baseUrl: error.relayBaseUrl,
|
|
22340
|
+
detail: error.causeMessage
|
|
22341
|
+
}) : error;
|
|
21558
22342
|
await reportPairingErrorToServer({
|
|
21559
22343
|
serverBaseUrl: config.serverBaseUrl,
|
|
21560
22344
|
sessionId: created.sessionId,
|
|
21561
22345
|
source: "link",
|
|
21562
22346
|
pairingToken: created.pairingToken,
|
|
21563
|
-
error: pairingErrorSnapshot("prepare_pairing",
|
|
22347
|
+
error: pairingErrorSnapshot("prepare_pairing", reportedError)
|
|
21564
22348
|
});
|
|
21565
|
-
throw
|
|
22349
|
+
throw reportedError;
|
|
21566
22350
|
}
|
|
21567
22351
|
const qrPayload = {
|
|
21568
22352
|
kind: "hermes_link_pairing",
|
|
@@ -21662,6 +22446,10 @@ async function claimPairing(input) {
|
|
|
21662
22446
|
{
|
|
21663
22447
|
claim_token: input.claimToken,
|
|
21664
22448
|
app_instance_id: input.appInstanceId ?? void 0
|
|
22449
|
+
},
|
|
22450
|
+
{
|
|
22451
|
+
target: "server",
|
|
22452
|
+
action: "verify pairing claim"
|
|
21665
22453
|
}
|
|
21666
22454
|
);
|
|
21667
22455
|
} catch (error) {
|
|
@@ -21709,15 +22497,25 @@ async function loadRequiredIdentity2(paths) {
|
|
|
21709
22497
|
}
|
|
21710
22498
|
return identity;
|
|
21711
22499
|
}
|
|
21712
|
-
async function postServerJson(serverBaseUrl,
|
|
21713
|
-
|
|
21714
|
-
|
|
21715
|
-
|
|
21716
|
-
|
|
21717
|
-
|
|
21718
|
-
|
|
21719
|
-
|
|
21720
|
-
|
|
22500
|
+
async function postServerJson(serverBaseUrl, path26, body, options) {
|
|
22501
|
+
let response;
|
|
22502
|
+
try {
|
|
22503
|
+
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path26}`, {
|
|
22504
|
+
method: "POST",
|
|
22505
|
+
headers: {
|
|
22506
|
+
accept: "application/json",
|
|
22507
|
+
"content-type": "application/json"
|
|
22508
|
+
},
|
|
22509
|
+
body: JSON.stringify(body)
|
|
22510
|
+
});
|
|
22511
|
+
} catch (error) {
|
|
22512
|
+
throw createPairingNetworkError({
|
|
22513
|
+
target: options.target,
|
|
22514
|
+
action: options.action,
|
|
22515
|
+
baseUrl: serverBaseUrl,
|
|
22516
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
22517
|
+
});
|
|
22518
|
+
}
|
|
21721
22519
|
return readJsonResponse2(response);
|
|
21722
22520
|
}
|
|
21723
22521
|
async function reportPairingErrorToServer(input) {
|
|
@@ -21750,16 +22548,26 @@ function pairingErrorSnapshot(stage, error) {
|
|
|
21750
22548
|
occurred_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
21751
22549
|
};
|
|
21752
22550
|
}
|
|
21753
|
-
async function patchServerJson(serverBaseUrl,
|
|
21754
|
-
|
|
21755
|
-
|
|
21756
|
-
|
|
21757
|
-
|
|
21758
|
-
|
|
21759
|
-
|
|
21760
|
-
|
|
21761
|
-
|
|
21762
|
-
|
|
22551
|
+
async function patchServerJson(serverBaseUrl, path26, token, body, options) {
|
|
22552
|
+
let response;
|
|
22553
|
+
try {
|
|
22554
|
+
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path26}`, {
|
|
22555
|
+
method: "PATCH",
|
|
22556
|
+
headers: {
|
|
22557
|
+
accept: "application/json",
|
|
22558
|
+
authorization: `Bearer ${token}`,
|
|
22559
|
+
"content-type": "application/json"
|
|
22560
|
+
},
|
|
22561
|
+
body: JSON.stringify(body)
|
|
22562
|
+
});
|
|
22563
|
+
} catch (error) {
|
|
22564
|
+
throw createPairingNetworkError({
|
|
22565
|
+
target: options.target,
|
|
22566
|
+
action: options.action,
|
|
22567
|
+
baseUrl: serverBaseUrl,
|
|
22568
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
22569
|
+
});
|
|
22570
|
+
}
|
|
21763
22571
|
return readJsonResponse2(response);
|
|
21764
22572
|
}
|
|
21765
22573
|
async function readJsonResponse2(response) {
|
|
@@ -21781,11 +22589,20 @@ function readErrorMessage5(payload) {
|
|
|
21781
22589
|
const message = error.message;
|
|
21782
22590
|
return typeof message === "string" ? message : null;
|
|
21783
22591
|
}
|
|
22592
|
+
function createPairingNetworkError(input) {
|
|
22593
|
+
const baseMessage = input.target === "server" ? `HermesPilot Server is unreachable while trying to ${input.action}.` : `Hermes Relay is unreachable while trying to ${input.action}.`;
|
|
22594
|
+
const hint = "If you are using a VPN, proxy, or corporate network, try turning it off and retrying.";
|
|
22595
|
+
return new LinkHttpError(
|
|
22596
|
+
503,
|
|
22597
|
+
input.target === "server" ? "pairing_server_unreachable" : "pairing_relay_unreachable",
|
|
22598
|
+
`${baseMessage} Please check whether ${input.baseUrl} is reachable. ${hint} Detail: ${input.detail}`
|
|
22599
|
+
);
|
|
22600
|
+
}
|
|
21784
22601
|
function pairingClaimPath(sessionId, paths) {
|
|
21785
|
-
return
|
|
22602
|
+
return path25.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
|
|
21786
22603
|
}
|
|
21787
22604
|
function pairingSessionPath(sessionId, paths) {
|
|
21788
|
-
return
|
|
22605
|
+
return path25.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
|
|
21789
22606
|
}
|
|
21790
22607
|
function qrPreferredUrls(routes) {
|
|
21791
22608
|
return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
|
|
@@ -21839,6 +22656,7 @@ function registerSystemRoutes(router, options) {
|
|
|
21839
22656
|
conversation_events: true,
|
|
21840
22657
|
conversation_delete: true,
|
|
21841
22658
|
conversation_bulk_delete: true,
|
|
22659
|
+
conversation_clear_plan: true,
|
|
21842
22660
|
conversation_cancel: true,
|
|
21843
22661
|
conversation_rename: true,
|
|
21844
22662
|
blobs: true,
|
|
@@ -22432,12 +23250,16 @@ function registerPairingRoutes(router, options) {
|
|
|
22432
23250
|
if (!session) {
|
|
22433
23251
|
throw new LinkHttpError(404, "pairing_session_not_found", "Pairing session was not found");
|
|
22434
23252
|
}
|
|
23253
|
+
const state = await readPairingState(sessionId, paths);
|
|
23254
|
+
if (!state.claimed && isExpiredAt(session.expires_at)) {
|
|
23255
|
+
throw new LinkHttpError(404, "pairing_session_expired", "Pairing session has expired");
|
|
23256
|
+
}
|
|
22435
23257
|
ctx.set("cache-control", "no-store");
|
|
22436
23258
|
ctx.body = {
|
|
22437
23259
|
ok: true,
|
|
22438
23260
|
session: {
|
|
22439
23261
|
...session,
|
|
22440
|
-
claimed:
|
|
23262
|
+
claimed: state.claimed
|
|
22441
23263
|
}
|
|
22442
23264
|
};
|
|
22443
23265
|
});
|
|
@@ -22453,6 +23275,8 @@ async function readPairingState(sessionId, paths) {
|
|
|
22453
23275
|
}
|
|
22454
23276
|
async function renderPairingPage(input) {
|
|
22455
23277
|
const session = input.session;
|
|
23278
|
+
const expiresAtMs = Date.parse(session.expires_at);
|
|
23279
|
+
const isExpired = !input.state.claimed && Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs;
|
|
22456
23280
|
const qrPayload = JSON.stringify({
|
|
22457
23281
|
kind: "hermes_link_pairing",
|
|
22458
23282
|
version: 1,
|
|
@@ -22472,8 +23296,9 @@ async function renderPairingPage(input) {
|
|
|
22472
23296
|
const currentUrl = session.local_api_url.replace(/\/+$/u, "");
|
|
22473
23297
|
const linkIdLabel = escapeHtml(input.linkId ?? session.link_id);
|
|
22474
23298
|
const expiresLabel = escapeHtml(formatDate(session.expires_at));
|
|
22475
|
-
const statusLabel = input.state.claimed ? "\u5DF2\u5B8C\u6210\u914D\u5BF9" : "\u7B49\u5F85 App \u626B\u7801";
|
|
22476
|
-
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";
|
|
23299
|
+
const statusLabel = input.state.claimed ? "\u5DF2\u5B8C\u6210\u914D\u5BF9" : isExpired ? "\u914D\u5BF9\u5DF2\u8FC7\u671F" : "\u7B49\u5F85 App \u626B\u7801";
|
|
23300
|
+
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";
|
|
23301
|
+
const statusPillLabel = input.state.claimed ? "\u5DF2\u626B\u7801" : isExpired ? "\u5DF2\u8FC7\u671F" : "\u7B49\u5F85\u4E2D";
|
|
22477
23302
|
return `<!doctype html>
|
|
22478
23303
|
<html lang="zh-CN">
|
|
22479
23304
|
<head>
|
|
@@ -22778,7 +23603,7 @@ async function renderPairingPage(input) {
|
|
|
22778
23603
|
<div class="card">
|
|
22779
23604
|
<div class="status">
|
|
22780
23605
|
<h2 class="status-title" id="statusTitle">${escapeHtml(statusLabel)}</h2>
|
|
22781
|
-
<span class="pill" id="statusPill">${
|
|
23606
|
+
<span class="pill" id="statusPill">${escapeHtml(statusPillLabel)}</span>
|
|
22782
23607
|
</div>
|
|
22783
23608
|
<div class="qr-frame">
|
|
22784
23609
|
<img src="${qrDataUri}" alt="Hermes Link pairing QR code" />
|
|
@@ -22795,22 +23620,62 @@ async function renderPairingPage(input) {
|
|
|
22795
23620
|
</main>
|
|
22796
23621
|
<script>
|
|
22797
23622
|
const sessionId = ${JSON.stringify(session.session_id)};
|
|
23623
|
+
const expiresAtMs = ${Number.isFinite(expiresAtMs) ? String(expiresAtMs) : "Number.NaN"};
|
|
23624
|
+
const initialClaimed = ${JSON.stringify(input.state.claimed)};
|
|
23625
|
+
const statusTitle = document.querySelector('#statusTitle');
|
|
23626
|
+
const statusPill = document.querySelector('#statusPill');
|
|
23627
|
+
const statusHint = document.querySelector('#statusHint');
|
|
23628
|
+
let refreshTimer = null;
|
|
23629
|
+
|
|
23630
|
+
const stopPolling = () => {
|
|
23631
|
+
if (refreshTimer !== null) {
|
|
23632
|
+
clearInterval(refreshTimer);
|
|
23633
|
+
refreshTimer = null;
|
|
23634
|
+
}
|
|
23635
|
+
};
|
|
23636
|
+
|
|
23637
|
+
const markClaimed = () => {
|
|
23638
|
+
statusTitle.textContent = '\u5DF2\u5B8C\u6210\u914D\u5BF9';
|
|
23639
|
+
statusPill.textContent = '\u5DF2\u626B\u7801';
|
|
23640
|
+
statusHint.textContent = 'App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002';
|
|
23641
|
+
stopPolling();
|
|
23642
|
+
};
|
|
23643
|
+
|
|
23644
|
+
const markExpired = () => {
|
|
23645
|
+
statusTitle.textContent = '\u914D\u5BF9\u5DF2\u8FC7\u671F';
|
|
23646
|
+
statusPill.textContent = '\u5DF2\u8FC7\u671F';
|
|
23647
|
+
statusHint.textContent = '\u8FD9\u6B21\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C hermeslink pair\u3002';
|
|
23648
|
+
stopPolling();
|
|
23649
|
+
};
|
|
23650
|
+
|
|
22798
23651
|
const refresh = async () => {
|
|
23652
|
+
if (Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs) {
|
|
23653
|
+
markExpired();
|
|
23654
|
+
return;
|
|
23655
|
+
}
|
|
22799
23656
|
try {
|
|
22800
23657
|
const response = await fetch('/api/v1/pairing/session?session_id=' + encodeURIComponent(sessionId), {
|
|
22801
23658
|
headers: { accept: 'application/json' },
|
|
22802
23659
|
});
|
|
23660
|
+
if (response.status === 404) {
|
|
23661
|
+
markExpired();
|
|
23662
|
+
return;
|
|
23663
|
+
}
|
|
22803
23664
|
if (!response.ok) return;
|
|
22804
23665
|
const payload = await response.json();
|
|
22805
23666
|
if (payload?.session?.claimed) {
|
|
22806
|
-
|
|
22807
|
-
document.querySelector('#statusTitle').textContent = '\u5DF2\u5B8C\u6210\u914D\u5BF9';
|
|
22808
|
-
document.querySelector('#statusHint').textContent = 'App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002';
|
|
23667
|
+
markClaimed();
|
|
22809
23668
|
}
|
|
22810
23669
|
} catch (_) {}
|
|
22811
23670
|
};
|
|
22812
|
-
|
|
22813
|
-
|
|
23671
|
+
if (initialClaimed) {
|
|
23672
|
+
stopPolling();
|
|
23673
|
+
} else if (Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs) {
|
|
23674
|
+
markExpired();
|
|
23675
|
+
} else {
|
|
23676
|
+
refreshTimer = window.setInterval(refresh, 1500);
|
|
23677
|
+
refresh();
|
|
23678
|
+
}
|
|
22814
23679
|
</script>
|
|
22815
23680
|
</body>
|
|
22816
23681
|
</html>`;
|
|
@@ -22822,6 +23687,10 @@ function formatDate(value) {
|
|
|
22822
23687
|
const date = new Date(value);
|
|
22823
23688
|
return Number.isNaN(date.getTime()) ? value : date.toLocaleString("zh-CN", { hour12: false });
|
|
22824
23689
|
}
|
|
23690
|
+
function isExpiredAt(value) {
|
|
23691
|
+
const expiresAtMs = Date.parse(value);
|
|
23692
|
+
return Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs;
|
|
23693
|
+
}
|
|
22825
23694
|
|
|
22826
23695
|
// src/http/routes/internal.ts
|
|
22827
23696
|
function registerInternalRoutes(router, options) {
|
|
@@ -22916,6 +23785,7 @@ export {
|
|
|
22916
23785
|
createFileLogger,
|
|
22917
23786
|
getLinkLogFile,
|
|
22918
23787
|
ensureHermesApiServerAvailable,
|
|
23788
|
+
readHermesVersion,
|
|
22919
23789
|
loadConfig,
|
|
22920
23790
|
saveConfig,
|
|
22921
23791
|
normalizeLanHost,
|
|
@@ -22929,6 +23799,7 @@ export {
|
|
|
22929
23799
|
readPairingClaim,
|
|
22930
23800
|
clearPairingClaim,
|
|
22931
23801
|
createApp,
|
|
23802
|
+
connectRelayControl,
|
|
22932
23803
|
reportLinkStatusToServer,
|
|
22933
23804
|
startLinkService,
|
|
22934
23805
|
startDaemonProcess,
|