@hermespilot/link 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist/{chunk-VCQJ5DSN.js → chunk-L2NM2XMX.js} +446 -141
- package/dist/chunk-L2NM2XMX.js.map +1 -0
- package/dist/cli/index.js +16 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/http/app.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-VCQJ5DSN.js.map +0 -1
|
@@ -4,7 +4,7 @@ import Router from "@koa/router";
|
|
|
4
4
|
import { Readable } from "stream";
|
|
5
5
|
|
|
6
6
|
// src/constants.ts
|
|
7
|
-
var LINK_VERSION = "0.1.
|
|
7
|
+
var LINK_VERSION = "0.1.8";
|
|
8
8
|
var LINK_COMMAND = "hermeslink";
|
|
9
9
|
var LINK_DEFAULT_PORT = 52379;
|
|
10
10
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -96,12 +96,30 @@ function normalizeConfiguredLanguage(language) {
|
|
|
96
96
|
|
|
97
97
|
// src/conversations/conversation-service.ts
|
|
98
98
|
import { EventEmitter } from "events";
|
|
99
|
-
import { appendFile, mkdir as
|
|
100
|
-
import
|
|
101
|
-
import { createHash, randomUUID
|
|
99
|
+
import { appendFile, mkdir as mkdir4, readdir, readFile as readFile3, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
100
|
+
import path5 from "path";
|
|
101
|
+
import { createHash, randomUUID } from "crypto";
|
|
102
102
|
|
|
103
|
-
// src/
|
|
104
|
-
|
|
103
|
+
// src/http/errors.ts
|
|
104
|
+
var LinkHttpError = class extends Error {
|
|
105
|
+
constructor(status, code, message) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.status = status;
|
|
108
|
+
this.code = code;
|
|
109
|
+
}
|
|
110
|
+
status;
|
|
111
|
+
code;
|
|
112
|
+
};
|
|
113
|
+
function isLinkHttpError(error) {
|
|
114
|
+
return error instanceof LinkHttpError;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/hermes/gateway.ts
|
|
118
|
+
import { execFile as execFile2, spawn } from "child_process";
|
|
119
|
+
import { mkdir as mkdir3, open as open2 } from "fs/promises";
|
|
120
|
+
import path4 from "path";
|
|
121
|
+
import { setTimeout as delay } from "timers/promises";
|
|
122
|
+
import { promisify as promisify2 } from "util";
|
|
105
123
|
|
|
106
124
|
// src/hermes/config.ts
|
|
107
125
|
import { randomBytes } from "crypto";
|
|
@@ -109,6 +127,8 @@ import { copyFile, mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/
|
|
|
109
127
|
import os2 from "os";
|
|
110
128
|
import path3 from "path";
|
|
111
129
|
import YAML from "yaml";
|
|
130
|
+
var DEFAULT_HERMES_API_SERVER_HOST = "127.0.0.1";
|
|
131
|
+
var DEFAULT_HERMES_API_SERVER_PORT = 8642;
|
|
112
132
|
function resolveHermesProfileDir(profileName = "default") {
|
|
113
133
|
if (profileName === "default") {
|
|
114
134
|
return path3.join(os2.homedir(), ".hermes");
|
|
@@ -126,14 +146,21 @@ async function readHermesApiServerConfig(profileName = "default", configPath = r
|
|
|
126
146
|
throw error;
|
|
127
147
|
});
|
|
128
148
|
if (!existingRaw) {
|
|
129
|
-
return {};
|
|
149
|
+
return applyEnvOverrides({}, await readHermesApiServerEnvOverrides(profileName), false);
|
|
130
150
|
}
|
|
131
151
|
const config = toRecord(YAML.parse(existingRaw));
|
|
132
152
|
const platforms = toRecord(config.platforms);
|
|
133
153
|
const apiServer = toRecord(platforms.api_server);
|
|
134
|
-
return
|
|
154
|
+
return applyEnvOverrides(
|
|
155
|
+
readApiServerConfig(apiServer, true),
|
|
156
|
+
await readHermesApiServerEnvOverrides(profileName),
|
|
157
|
+
true
|
|
158
|
+
);
|
|
135
159
|
}
|
|
136
160
|
async function ensureHermesApiServerKey(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
|
|
161
|
+
return ensureHermesApiServerConfig(profileName, configPath);
|
|
162
|
+
}
|
|
163
|
+
async function ensureHermesApiServerConfig(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
|
|
137
164
|
const existingRaw = await readFile2(configPath, "utf8").catch((error) => {
|
|
138
165
|
if (isNodeError2(error, "ENOENT")) {
|
|
139
166
|
return null;
|
|
@@ -145,8 +172,31 @@ async function ensureHermesApiServerKey(profileName = "default", configPath = re
|
|
|
145
172
|
const platforms = ensureRecord(config, "platforms");
|
|
146
173
|
const apiServer = ensureRecord(platforms, "api_server");
|
|
147
174
|
const extra = ensureRecord(apiServer, "extra");
|
|
148
|
-
const
|
|
175
|
+
const envOverrides = await readHermesApiServerEnvOverrides(profileName);
|
|
176
|
+
const before = applyEnvOverrides(readApiServerConfig(apiServer), envOverrides, false);
|
|
177
|
+
const beforeKey = before.key?.trim() ? before.key : null;
|
|
178
|
+
const beforeEnabled = before.enabled === true;
|
|
179
|
+
const beforeHost = before.host?.trim() ? before.host : null;
|
|
180
|
+
const beforePort = typeof before.port === "number" && Number.isFinite(before.port) ? before.port : null;
|
|
149
181
|
let changed = false;
|
|
182
|
+
let enabledAdded = false;
|
|
183
|
+
let hostAdded = false;
|
|
184
|
+
let portAdded = false;
|
|
185
|
+
if (!beforeEnabled) {
|
|
186
|
+
apiServer.enabled = true;
|
|
187
|
+
enabledAdded = true;
|
|
188
|
+
changed = true;
|
|
189
|
+
}
|
|
190
|
+
if (!beforeHost) {
|
|
191
|
+
extra.host = DEFAULT_HERMES_API_SERVER_HOST;
|
|
192
|
+
hostAdded = true;
|
|
193
|
+
changed = true;
|
|
194
|
+
}
|
|
195
|
+
if (!beforePort) {
|
|
196
|
+
extra.port = DEFAULT_HERMES_API_SERVER_PORT;
|
|
197
|
+
portAdded = true;
|
|
198
|
+
changed = true;
|
|
199
|
+
}
|
|
150
200
|
if (!beforeKey) {
|
|
151
201
|
extra.key = randomBytes(32).toString("base64url");
|
|
152
202
|
changed = true;
|
|
@@ -154,9 +204,12 @@ async function ensureHermesApiServerKey(profileName = "default", configPath = re
|
|
|
154
204
|
if (!changed) {
|
|
155
205
|
return {
|
|
156
206
|
configPath,
|
|
157
|
-
apiServer: readApiServerConfig(
|
|
207
|
+
apiServer: applyEnvOverrides(readApiServerConfig(apiServer, true), envOverrides, true),
|
|
158
208
|
changed: false,
|
|
159
209
|
keyAdded: false,
|
|
210
|
+
enabledAdded: false,
|
|
211
|
+
hostAdded: false,
|
|
212
|
+
portAdded: false,
|
|
160
213
|
backupPath: null,
|
|
161
214
|
notice: null
|
|
162
215
|
};
|
|
@@ -170,20 +223,111 @@ async function ensureHermesApiServerKey(profileName = "default", configPath = re
|
|
|
170
223
|
await writeFile(configPath, document.toString(), { mode: 384 });
|
|
171
224
|
return {
|
|
172
225
|
configPath,
|
|
173
|
-
apiServer: readApiServerConfig(
|
|
226
|
+
apiServer: applyEnvOverrides(readApiServerConfig(apiServer, true), envOverrides, true),
|
|
174
227
|
changed: true,
|
|
175
|
-
keyAdded:
|
|
228
|
+
keyAdded: !beforeKey,
|
|
229
|
+
enabledAdded,
|
|
230
|
+
hostAdded,
|
|
231
|
+
portAdded,
|
|
176
232
|
backupPath,
|
|
177
|
-
notice:
|
|
233
|
+
notice: buildNotice({ keyAdded: !beforeKey, enabledAdded, hostAdded, portAdded })
|
|
178
234
|
};
|
|
179
235
|
}
|
|
180
|
-
function readApiServerConfig(
|
|
236
|
+
function readApiServerConfig(apiServerOrExtra, withDefaults = false) {
|
|
237
|
+
const apiServer = "extra" in apiServerOrExtra ? apiServerOrExtra : {};
|
|
238
|
+
const extra = toRecord("extra" in apiServerOrExtra ? apiServerOrExtra.extra : apiServerOrExtra);
|
|
239
|
+
const port = typeof extra.port === "number" ? extra.port : void 0;
|
|
240
|
+
const host = typeof extra.host === "string" ? extra.host : void 0;
|
|
181
241
|
return {
|
|
182
|
-
|
|
183
|
-
|
|
242
|
+
enabled: apiServer.enabled === true,
|
|
243
|
+
host: withDefaults ? host ?? DEFAULT_HERMES_API_SERVER_HOST : host,
|
|
244
|
+
port: withDefaults ? port ?? DEFAULT_HERMES_API_SERVER_PORT : port,
|
|
184
245
|
key: typeof extra.key === "string" ? extra.key : void 0
|
|
185
246
|
};
|
|
186
247
|
}
|
|
248
|
+
async function readHermesApiServerEnvOverrides(profileName) {
|
|
249
|
+
const values = await readHermesEnvFile(profileName);
|
|
250
|
+
for (const key of ["API_SERVER_ENABLED", "API_SERVER_HOST", "API_SERVER_PORT", "API_SERVER_KEY"]) {
|
|
251
|
+
const value = process.env[key];
|
|
252
|
+
if (typeof value === "string" && value.trim()) {
|
|
253
|
+
values[key] = value;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const port = Number.parseInt(values.API_SERVER_PORT ?? "", 10);
|
|
257
|
+
return {
|
|
258
|
+
enabled: parseEnvBoolean(values.API_SERVER_ENABLED),
|
|
259
|
+
host: values.API_SERVER_HOST?.trim() || void 0,
|
|
260
|
+
port: Number.isFinite(port) ? port : void 0,
|
|
261
|
+
key: values.API_SERVER_KEY?.trim() || void 0
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async function readHermesEnvFile(profileName) {
|
|
265
|
+
const envPath = path3.join(resolveHermesProfileDir(profileName), ".env");
|
|
266
|
+
const raw = await readFile2(envPath, "utf8").catch((error) => {
|
|
267
|
+
if (isNodeError2(error, "ENOENT")) {
|
|
268
|
+
return "";
|
|
269
|
+
}
|
|
270
|
+
throw error;
|
|
271
|
+
});
|
|
272
|
+
const values = {};
|
|
273
|
+
for (const line of raw.split(/\r?\n/u)) {
|
|
274
|
+
const trimmed = line.trim();
|
|
275
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const match = /^(?:export\s+)?(?<key>[A-Za-z_][A-Za-z0-9_]*)=(?<value>.*)$/u.exec(trimmed);
|
|
279
|
+
if (!match?.groups) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
values[match.groups.key] = unquoteEnvValue(match.groups.value.trim());
|
|
283
|
+
}
|
|
284
|
+
return values;
|
|
285
|
+
}
|
|
286
|
+
function applyEnvOverrides(config, env, withDefaults) {
|
|
287
|
+
const host = env.host ?? config.host;
|
|
288
|
+
const port = env.port ?? config.port;
|
|
289
|
+
return {
|
|
290
|
+
enabled: env.enabled ?? config.enabled,
|
|
291
|
+
host: withDefaults ? host ?? DEFAULT_HERMES_API_SERVER_HOST : host,
|
|
292
|
+
port: withDefaults ? port ?? DEFAULT_HERMES_API_SERVER_PORT : port,
|
|
293
|
+
key: env.key ?? config.key
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function parseEnvBoolean(value) {
|
|
297
|
+
if (!value) {
|
|
298
|
+
return void 0;
|
|
299
|
+
}
|
|
300
|
+
const normalized = value.trim().toLowerCase();
|
|
301
|
+
if (["1", "true", "yes", "on"].includes(normalized)) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
if (["0", "false", "no", "off"].includes(normalized)) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
return void 0;
|
|
308
|
+
}
|
|
309
|
+
function unquoteEnvValue(value) {
|
|
310
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
311
|
+
return value.slice(1, -1);
|
|
312
|
+
}
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
function buildNotice(flags) {
|
|
316
|
+
const fields = [];
|
|
317
|
+
if (flags.enabledAdded) {
|
|
318
|
+
fields.push("enabled");
|
|
319
|
+
}
|
|
320
|
+
if (flags.hostAdded) {
|
|
321
|
+
fields.push("host=127.0.0.1");
|
|
322
|
+
}
|
|
323
|
+
if (flags.portAdded) {
|
|
324
|
+
fields.push("port=8642");
|
|
325
|
+
}
|
|
326
|
+
if (flags.keyAdded) {
|
|
327
|
+
fields.push("key");
|
|
328
|
+
}
|
|
329
|
+
return `\u5DF2\u4E3A Hermes API Server \u81EA\u52A8\u8865\u5145 ${fields.join("\u3001")}\uFF1B\u672A\u8986\u76D6\u5DF2\u6709 port/host/key\u3002`;
|
|
330
|
+
}
|
|
187
331
|
function toRecord(value) {
|
|
188
332
|
return typeof value === "object" && value !== null ? value : {};
|
|
189
333
|
}
|
|
@@ -200,22 +344,199 @@ function isNodeError2(error, code) {
|
|
|
200
344
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
201
345
|
}
|
|
202
346
|
|
|
203
|
-
// src/
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
347
|
+
// src/hermes/cli.ts
|
|
348
|
+
import { execFile } from "child_process";
|
|
349
|
+
import { promisify } from "util";
|
|
350
|
+
var execFileAsync = promisify(execFile);
|
|
351
|
+
async function deleteHermesSession(sessionId) {
|
|
352
|
+
if (!sessionId.trim()) {
|
|
353
|
+
throw new LinkHttpError(400, "hermes_session_id_required", "Hermes session id is required");
|
|
209
354
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
355
|
+
try {
|
|
356
|
+
await execFileAsync(resolveHermesBin(), ["sessions", "delete", sessionId, "--yes"], {
|
|
357
|
+
timeout: 1e4,
|
|
358
|
+
windowsHide: true
|
|
359
|
+
});
|
|
360
|
+
} catch (error) {
|
|
361
|
+
throw new LinkHttpError(
|
|
362
|
+
502,
|
|
363
|
+
"hermes_session_delete_failed",
|
|
364
|
+
error instanceof Error ? error.message : "Hermes session delete failed"
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function resolveHermesBin() {
|
|
369
|
+
return process.env.HERMES_BIN?.trim() || "hermes";
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/hermes/gateway.ts
|
|
373
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
374
|
+
var DEFAULT_START_TIMEOUT_MS = 12e3;
|
|
375
|
+
var HEALTH_TIMEOUT_MS = 1500;
|
|
376
|
+
var MIN_API_SERVER_VERSION = "0.4.0";
|
|
377
|
+
var gatewayStartInFlight = null;
|
|
378
|
+
async function ensureHermesApiServerAvailable(options = {}) {
|
|
379
|
+
const configResult = await ensureHermesApiServerConfig();
|
|
380
|
+
const fetcher = options.fetchImpl ?? fetch;
|
|
381
|
+
if (!options.forceRestart && await isHermesApiHealthy(configResult.apiServer, fetcher)) {
|
|
382
|
+
return { available: true, configResult, started: false };
|
|
383
|
+
}
|
|
384
|
+
if (!shouldAutoStart(options.autoStart)) {
|
|
385
|
+
throw new LinkHttpError(503, "hermes_api_server_unavailable", unavailableMessage());
|
|
386
|
+
}
|
|
387
|
+
const start = await startHermesGatewayOnce(options.paths ?? resolveRuntimePaths());
|
|
388
|
+
const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_START_TIMEOUT_MS);
|
|
389
|
+
while (Date.now() < deadline) {
|
|
390
|
+
if (await isHermesApiHealthy(configResult.apiServer, fetcher)) {
|
|
391
|
+
return { available: true, configResult, started: true, start };
|
|
392
|
+
}
|
|
393
|
+
await delay(400);
|
|
394
|
+
}
|
|
395
|
+
throw new LinkHttpError(
|
|
396
|
+
503,
|
|
397
|
+
"hermes_api_server_unavailable",
|
|
398
|
+
`${unavailableMessage()} Link tried to start it with \`${resolveHermesBin()} gateway run --replace\`, but it did not become ready. Gateway log: ${start.logPath}`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
async function readHermesVersion() {
|
|
402
|
+
const { stdout } = await execHermesVersion();
|
|
403
|
+
const raw = stdout.trim();
|
|
404
|
+
const version = parseHermesVersion(raw);
|
|
405
|
+
return {
|
|
406
|
+
raw,
|
|
407
|
+
version,
|
|
408
|
+
supportsApiServer: version ? compareSemver(version, MIN_API_SERVER_VERSION) >= 0 : null
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
async function execHermesVersion() {
|
|
412
|
+
try {
|
|
413
|
+
return await execFileAsync2(resolveHermesBin(), ["version"], {
|
|
414
|
+
timeout: 5e3,
|
|
415
|
+
windowsHide: true
|
|
416
|
+
});
|
|
417
|
+
} catch {
|
|
418
|
+
return await execFileAsync2(resolveHermesBin(), ["--version"], {
|
|
419
|
+
timeout: 5e3,
|
|
420
|
+
windowsHide: true
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function assertHermesRunsApiSupported(version, status) {
|
|
425
|
+
if (status !== 404) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const versionText = version?.version ? `\u5F53\u524D\u68C0\u6D4B\u5230 Hermes Agent ${version.version}\u3002` : "";
|
|
429
|
+
throw new LinkHttpError(
|
|
430
|
+
502,
|
|
431
|
+
"hermes_runs_api_unsupported",
|
|
432
|
+
`${versionText}\u5F53\u524D Hermes Agent API Server \u4E0D\u652F\u6301 HermesPilot \u9700\u8981\u7684 /v1/runs \u63A5\u53E3\uFF0C\u8BF7\u5148\u8FD0\u884C \`hermes update\` \u5347\u7EA7 Hermes Agent\u3002`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
async function startHermesGatewayOnce(paths) {
|
|
436
|
+
if (!gatewayStartInFlight) {
|
|
437
|
+
gatewayStartInFlight = startHermesGateway(paths).finally(() => {
|
|
438
|
+
gatewayStartInFlight = null;
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return await gatewayStartInFlight;
|
|
442
|
+
}
|
|
443
|
+
async function startHermesGateway(paths) {
|
|
444
|
+
const version = await readHermesVersion().catch((error) => {
|
|
445
|
+
throw new LinkHttpError(
|
|
446
|
+
503,
|
|
447
|
+
"hermes_agent_not_available",
|
|
448
|
+
`\u6CA1\u6709\u627E\u5230\u53EF\u7528\u7684 Hermes Agent CLI\u3002\u8BF7\u5148\u5B89\u88C5 Hermes Agent\uFF0C\u6216\u8BBE\u7F6E HERMES_BIN\u3002${formatErrorSuffix(error)}`
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
if (version.supportsApiServer === false) {
|
|
452
|
+
throw new LinkHttpError(
|
|
453
|
+
503,
|
|
454
|
+
"hermes_agent_too_old",
|
|
455
|
+
`\u5F53\u524D Hermes Agent ${version.version} \u592A\u65E7\uFF0C\u53EF\u80FD\u4E0D\u652F\u6301 Hermes API Server\u3002\u8BF7\u5148\u8FD0\u884C \`hermes update\` \u5347\u7EA7\u3002`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
await mkdir3(paths.logsDir, { recursive: true, mode: 448 });
|
|
459
|
+
const logPath = path4.join(paths.logsDir, "hermes-gateway.log");
|
|
460
|
+
const logFile = await open2(logPath, "a", 384);
|
|
461
|
+
try {
|
|
462
|
+
const child = spawn(resolveHermesBin(), ["gateway", "run", "--replace"], {
|
|
463
|
+
detached: true,
|
|
464
|
+
env: process.env,
|
|
465
|
+
stdio: ["ignore", logFile.fd, logFile.fd],
|
|
466
|
+
windowsHide: true
|
|
467
|
+
});
|
|
468
|
+
await new Promise((resolve, reject) => {
|
|
469
|
+
let settled = false;
|
|
470
|
+
const finish = (callback) => {
|
|
471
|
+
if (settled) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
settled = true;
|
|
475
|
+
callback();
|
|
476
|
+
};
|
|
477
|
+
child.once("spawn", () => finish(resolve));
|
|
478
|
+
child.once("error", (error) => finish(() => reject(error)));
|
|
479
|
+
});
|
|
480
|
+
child.unref();
|
|
481
|
+
return { pid: child.pid ?? null, logPath, version };
|
|
482
|
+
} finally {
|
|
483
|
+
await logFile.close().catch(() => void 0);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function isHermesApiHealthy(config, fetcher) {
|
|
487
|
+
const response = await fetchWithTimeout(`http://127.0.0.1:${config.port}/health`, {
|
|
488
|
+
method: "GET",
|
|
489
|
+
headers: authHeaders(config)
|
|
490
|
+
}, fetcher);
|
|
491
|
+
return Boolean(response?.ok);
|
|
492
|
+
}
|
|
493
|
+
function fetchWithTimeout(input, init, fetcher) {
|
|
494
|
+
const controller = new AbortController();
|
|
495
|
+
const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
|
|
496
|
+
return fetcher(input, { ...init, signal: controller.signal }).catch(() => null).finally(() => clearTimeout(timer));
|
|
497
|
+
}
|
|
498
|
+
function authHeaders(config) {
|
|
499
|
+
const headers = new Headers();
|
|
500
|
+
headers.set("accept", "application/json");
|
|
501
|
+
if (config.key) {
|
|
502
|
+
headers.set("x-api-key", config.key);
|
|
503
|
+
headers.set("authorization", `Bearer ${config.key}`);
|
|
504
|
+
}
|
|
505
|
+
return headers;
|
|
506
|
+
}
|
|
507
|
+
function shouldAutoStart(value) {
|
|
508
|
+
if (value !== void 0) {
|
|
509
|
+
return value;
|
|
510
|
+
}
|
|
511
|
+
const raw = process.env.HERMESLINK_GATEWAY_AUTOSTART?.trim().toLowerCase();
|
|
512
|
+
return raw !== "0" && raw !== "false" && raw !== "off";
|
|
513
|
+
}
|
|
514
|
+
function unavailableMessage() {
|
|
515
|
+
return "Hermes API Server \u5F53\u524D\u4E0D\u53EF\u7528\u3002\u8BF7\u786E\u8BA4 Hermes Agent \u5DF2\u5B89\u88C5\uFF0C\u5E76\u53EF\u901A\u8FC7 `hermes gateway run` \u542F\u52A8\uFF1BHermesPilot Link \u9700\u8981 Hermes Agent \u7684\u672C\u673A API Server\u3002";
|
|
516
|
+
}
|
|
517
|
+
function parseHermesVersion(value) {
|
|
518
|
+
const match = /\bv?(\d+\.\d+\.\d+)\b/u.exec(value);
|
|
519
|
+
return match?.[1] ?? null;
|
|
520
|
+
}
|
|
521
|
+
function compareSemver(left, right) {
|
|
522
|
+
const leftParts = left.split(".").map((part) => Number.parseInt(part, 10));
|
|
523
|
+
const rightParts = right.split(".").map((part) => Number.parseInt(part, 10));
|
|
524
|
+
for (let index = 0; index < 3; index += 1) {
|
|
525
|
+
const diff = (leftParts[index] || 0) - (rightParts[index] || 0);
|
|
526
|
+
if (diff !== 0) {
|
|
527
|
+
return diff;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return 0;
|
|
531
|
+
}
|
|
532
|
+
function formatErrorSuffix(error) {
|
|
533
|
+
if (!(error instanceof Error) || !error.message) {
|
|
534
|
+
return "";
|
|
535
|
+
}
|
|
536
|
+
return ` \u539F\u59CB\u9519\u8BEF\uFF1A${error.message}`;
|
|
215
537
|
}
|
|
216
538
|
|
|
217
539
|
// src/hermes/api-server.ts
|
|
218
|
-
var fallbackRuns = /* @__PURE__ */ new Map();
|
|
219
540
|
async function listHermesModels(options = {}) {
|
|
220
541
|
const response = await callHermesApi("/v1/models", { method: "GET" }, options);
|
|
221
542
|
if (response.status === 404) {
|
|
@@ -234,9 +555,8 @@ async function createHermesRun(input, options = {}) {
|
|
|
234
555
|
options
|
|
235
556
|
);
|
|
236
557
|
if (response.status === 404 || response.status === 503) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return { run_id: runId2, fallback: true };
|
|
558
|
+
assertHermesRunsApiSupported(await readHermesVersion().catch(() => null), response.status);
|
|
559
|
+
throw new LinkHttpError(503, "hermes_api_server_unavailable", "Hermes API Server is unavailable");
|
|
240
560
|
}
|
|
241
561
|
const payload = await readJsonResponse(response);
|
|
242
562
|
const runId = readString(payload, "run_id") ?? readString(payload, "runId") ?? readString(payload, "id");
|
|
@@ -246,17 +566,8 @@ async function createHermesRun(input, options = {}) {
|
|
|
246
566
|
return { run_id: runId, fallback: false };
|
|
247
567
|
}
|
|
248
568
|
async function streamHermesRunEvents(runId, options = {}) {
|
|
249
|
-
const fallback = fallbackRuns.get(runId);
|
|
250
|
-
if (fallback) {
|
|
251
|
-
fallbackRuns.delete(runId);
|
|
252
|
-
return new Response(createFallbackSseStream(fallback.input), {
|
|
253
|
-
headers: {
|
|
254
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
255
|
-
"cache-control": "no-store"
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
569
|
const response = await callHermesApi(`/v1/runs/${encodeURIComponent(runId)}/events`, { method: "GET" }, options);
|
|
570
|
+
assertHermesRunsApiSupported(await readHermesVersion().catch(() => null), response.status);
|
|
260
571
|
if (!response.ok || !response.body) {
|
|
261
572
|
throw new LinkHttpError(502, "hermes_events_unavailable", "Hermes run event stream is unavailable");
|
|
262
573
|
}
|
|
@@ -277,75 +588,68 @@ async function cancelHermesRun(runId, options = {}) {
|
|
|
277
588
|
if (!response.ok && response.status !== 404) {
|
|
278
589
|
throw new LinkHttpError(502, "hermes_cancel_failed", "Hermes run cancel failed");
|
|
279
590
|
}
|
|
280
|
-
fallbackRuns.delete(runId);
|
|
281
591
|
}
|
|
282
|
-
async function callHermesApi(
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
return new Response(null, { status: 503 });
|
|
286
|
-
}
|
|
592
|
+
async function callHermesApi(path9, init, options) {
|
|
593
|
+
const availability = await ensureHermesApiServerAvailable({ fetchImpl: options.fetchImpl });
|
|
594
|
+
const config = availability.configResult.apiServer;
|
|
287
595
|
const fetcher = options.fetchImpl ?? fetch;
|
|
596
|
+
const request = () => fetchHermesApi(fetcher, config, path9, init);
|
|
597
|
+
const response = await request();
|
|
598
|
+
if (response.status !== 401) {
|
|
599
|
+
return response;
|
|
600
|
+
}
|
|
601
|
+
await ensureHermesApiServerAvailable({ fetchImpl: options.fetchImpl, forceRestart: true });
|
|
602
|
+
return await request();
|
|
603
|
+
}
|
|
604
|
+
async function fetchHermesApi(fetcher, config, path9, init) {
|
|
288
605
|
const headers = new Headers(init.headers);
|
|
289
606
|
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
607
|
+
if (config.key) {
|
|
608
|
+
headers.set("x-api-key", config.key);
|
|
609
|
+
headers.set("authorization", `Bearer ${config.key}`);
|
|
610
|
+
}
|
|
611
|
+
return await fetcher(`http://127.0.0.1:${config.port}${path9}`, {
|
|
293
612
|
...init,
|
|
294
613
|
headers
|
|
295
|
-
}).catch(() =>
|
|
614
|
+
}).catch(() => {
|
|
615
|
+
throw new LinkHttpError(503, "hermes_api_server_unavailable", "Hermes API Server is unavailable");
|
|
616
|
+
});
|
|
296
617
|
}
|
|
297
618
|
async function readJsonResponse(response) {
|
|
298
|
-
const
|
|
619
|
+
const raw = await response.text().catch(() => "");
|
|
620
|
+
const payload = parseJsonObject(raw);
|
|
299
621
|
if (!response.ok || typeof payload !== "object" || payload === null) {
|
|
300
|
-
throw new LinkHttpError(
|
|
622
|
+
throw new LinkHttpError(
|
|
623
|
+
502,
|
|
624
|
+
"hermes_response_invalid",
|
|
625
|
+
`Hermes API Server returned HTTP ${response.status}: ${readUpstreamMessage(payload, raw)}`
|
|
626
|
+
);
|
|
301
627
|
}
|
|
302
628
|
return payload;
|
|
303
629
|
}
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
start(controller) {
|
|
308
|
-
const message = `Hermes API Server is not running yet. Link received: ${input}`;
|
|
309
|
-
controller.enqueue(encoder.encode(`event: message.delta
|
|
310
|
-
data: ${JSON.stringify({ type: "message.delta", delta: message })}
|
|
311
|
-
|
|
312
|
-
`));
|
|
313
|
-
controller.enqueue(encoder.encode(`event: run.completed
|
|
314
|
-
data: ${JSON.stringify({ type: "run.completed" })}
|
|
315
|
-
|
|
316
|
-
`));
|
|
317
|
-
controller.close();
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
function readString(payload, key) {
|
|
322
|
-
const value = payload[key];
|
|
323
|
-
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// src/hermes/cli.ts
|
|
327
|
-
import { execFile } from "child_process";
|
|
328
|
-
import { promisify } from "util";
|
|
329
|
-
var execFileAsync = promisify(execFile);
|
|
330
|
-
async function deleteHermesSession(sessionId) {
|
|
331
|
-
if (!sessionId.trim()) {
|
|
332
|
-
throw new LinkHttpError(400, "hermes_session_id_required", "Hermes session id is required");
|
|
630
|
+
function parseJsonObject(raw) {
|
|
631
|
+
if (!raw.trim()) {
|
|
632
|
+
return null;
|
|
333
633
|
}
|
|
334
634
|
try {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
} catch (error) {
|
|
340
|
-
throw new LinkHttpError(
|
|
341
|
-
502,
|
|
342
|
-
"hermes_session_delete_failed",
|
|
343
|
-
error instanceof Error ? error.message : "Hermes session delete failed"
|
|
344
|
-
);
|
|
635
|
+
const parsed = JSON.parse(raw);
|
|
636
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
637
|
+
} catch {
|
|
638
|
+
return null;
|
|
345
639
|
}
|
|
346
640
|
}
|
|
347
|
-
function
|
|
348
|
-
|
|
641
|
+
function readUpstreamMessage(payload, raw) {
|
|
642
|
+
const error = typeof payload?.error === "object" && payload.error !== null ? payload.error : null;
|
|
643
|
+
const message = readString(error ?? {}, "message") ?? readString(payload ?? {}, "message");
|
|
644
|
+
if (message) {
|
|
645
|
+
return message;
|
|
646
|
+
}
|
|
647
|
+
const body = raw.trim().replace(/\s+/gu, " ").slice(0, 500);
|
|
648
|
+
return body || "empty response body";
|
|
649
|
+
}
|
|
650
|
+
function readString(payload, key) {
|
|
651
|
+
const value = payload[key];
|
|
652
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
349
653
|
}
|
|
350
654
|
|
|
351
655
|
// src/conversations/conversation-service.ts
|
|
@@ -364,7 +668,7 @@ var ConversationService = class {
|
|
|
364
668
|
logger;
|
|
365
669
|
emitter = new EventEmitter();
|
|
366
670
|
async listConversations() {
|
|
367
|
-
await
|
|
671
|
+
await mkdir4(this.paths.conversationsDir, { recursive: true, mode: 448 });
|
|
368
672
|
const entries = await readdir(this.paths.conversationsDir, { withFileTypes: true }).catch((error) => {
|
|
369
673
|
if (isNodeError3(error, "ENOENT")) {
|
|
370
674
|
return [];
|
|
@@ -386,9 +690,9 @@ var ConversationService = class {
|
|
|
386
690
|
return summaries.sort((left, right) => Date.parse(right.updated_at) - Date.parse(left.updated_at));
|
|
387
691
|
}
|
|
388
692
|
async createConversation(input = {}) {
|
|
389
|
-
await
|
|
693
|
+
await mkdir4(this.paths.conversationsDir, { recursive: true, mode: 448 });
|
|
390
694
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
391
|
-
const id = `conv_${
|
|
695
|
+
const id = `conv_${randomUUID().replaceAll("-", "")}`;
|
|
392
696
|
const title = input.title?.trim() || "Untitled";
|
|
393
697
|
const manifest = {
|
|
394
698
|
id,
|
|
@@ -401,7 +705,7 @@ var ConversationService = class {
|
|
|
401
705
|
updated_at: now,
|
|
402
706
|
last_event_seq: 0
|
|
403
707
|
};
|
|
404
|
-
await
|
|
708
|
+
await mkdir4(this.conversationDir(id), { recursive: true, mode: 448 });
|
|
405
709
|
await this.writeManifest(manifest);
|
|
406
710
|
await this.writeSnapshot(id, emptySnapshot());
|
|
407
711
|
const event = await this.appendEvent(manifest.id, {
|
|
@@ -462,7 +766,7 @@ var ConversationService = class {
|
|
|
462
766
|
}
|
|
463
767
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
464
768
|
const userMessage = {
|
|
465
|
-
id: `msg_${
|
|
769
|
+
id: `msg_${randomUUID().replaceAll("-", "")}`,
|
|
466
770
|
schema_version: 1,
|
|
467
771
|
conversation_id: manifest.id,
|
|
468
772
|
role: "user",
|
|
@@ -481,7 +785,7 @@ var ConversationService = class {
|
|
|
481
785
|
raw: { format: "hermes-link-user-message", payload: { content, attachments: input.attachments ?? [] } }
|
|
482
786
|
};
|
|
483
787
|
const assistantMessage = {
|
|
484
|
-
id: `msg_${
|
|
788
|
+
id: `msg_${randomUUID().replaceAll("-", "")}`,
|
|
485
789
|
schema_version: 1,
|
|
486
790
|
conversation_id: manifest.id,
|
|
487
791
|
role: "assistant",
|
|
@@ -493,7 +797,7 @@ var ConversationService = class {
|
|
|
493
797
|
attachments: []
|
|
494
798
|
};
|
|
495
799
|
const run = {
|
|
496
|
-
id: `run_${
|
|
800
|
+
id: `run_${randomUUID().replaceAll("-", "")}`,
|
|
497
801
|
conversation_id: manifest.id,
|
|
498
802
|
trigger_message_id: userMessage.id,
|
|
499
803
|
assistant_message_id: assistantMessage.id,
|
|
@@ -563,9 +867,9 @@ var ConversationService = class {
|
|
|
563
867
|
if (input.bytes.byteLength > MAX_UPLOADED_BLOB_BYTES) {
|
|
564
868
|
throw new LinkHttpError(413, "blob_too_large", "Blob is too large");
|
|
565
869
|
}
|
|
566
|
-
const id = `blob_${
|
|
870
|
+
const id = `blob_${randomUUID().replaceAll("-", "")}`;
|
|
567
871
|
const filePath = this.blobPath(id);
|
|
568
|
-
await
|
|
872
|
+
await mkdir4(path5.dirname(filePath), { recursive: true, mode: 448 });
|
|
569
873
|
await writeFile2(filePath, input.bytes, { mode: 384 });
|
|
570
874
|
const manifestPath = `${filePath}.json`;
|
|
571
875
|
const blob = {
|
|
@@ -665,7 +969,7 @@ ${attachmentLines.join("\n")}`;
|
|
|
665
969
|
}
|
|
666
970
|
return this.writeBlob(conversationId, {
|
|
667
971
|
bytes: await readFile3(sourcePath),
|
|
668
|
-
filename:
|
|
972
|
+
filename: path5.basename(sourcePath),
|
|
669
973
|
mime: source.mime ?? inferMimeType(sourcePath)
|
|
670
974
|
});
|
|
671
975
|
}
|
|
@@ -916,7 +1220,7 @@ ${attachmentLines.join("\n")}`;
|
|
|
916
1220
|
conversation_id: conversationId,
|
|
917
1221
|
created_at: now
|
|
918
1222
|
};
|
|
919
|
-
await
|
|
1223
|
+
await mkdir4(this.conversationDir(conversationId), { recursive: true, mode: 448 });
|
|
920
1224
|
await appendFile(this.eventsPath(conversationId), `${JSON.stringify(event)}
|
|
921
1225
|
`, { mode: 384 });
|
|
922
1226
|
await this.writeManifest({
|
|
@@ -956,20 +1260,20 @@ ${attachmentLines.join("\n")}`;
|
|
|
956
1260
|
}
|
|
957
1261
|
conversationDir(conversationId) {
|
|
958
1262
|
assertValidConversationId(conversationId);
|
|
959
|
-
return
|
|
1263
|
+
return path5.join(this.paths.conversationsDir, conversationId);
|
|
960
1264
|
}
|
|
961
1265
|
manifestPath(conversationId) {
|
|
962
|
-
return
|
|
1266
|
+
return path5.join(this.conversationDir(conversationId), "manifest.json");
|
|
963
1267
|
}
|
|
964
1268
|
snapshotPath(conversationId) {
|
|
965
|
-
return
|
|
1269
|
+
return path5.join(this.conversationDir(conversationId), "snapshot.json");
|
|
966
1270
|
}
|
|
967
1271
|
eventsPath(conversationId) {
|
|
968
|
-
return
|
|
1272
|
+
return path5.join(this.conversationDir(conversationId), "events.ndjson");
|
|
969
1273
|
}
|
|
970
1274
|
blobPath(blobId) {
|
|
971
1275
|
assertValidBlobId(blobId);
|
|
972
|
-
return
|
|
1276
|
+
return path5.join(this.paths.blobsDir, `${blobId}.bin`);
|
|
973
1277
|
}
|
|
974
1278
|
liveEventName(conversationId) {
|
|
975
1279
|
return `conversation:${conversationId}`;
|
|
@@ -996,7 +1300,7 @@ ${attachmentLines.join("\n")}`;
|
|
|
996
1300
|
}
|
|
997
1301
|
}
|
|
998
1302
|
async listConversationBlobIds(conversationId) {
|
|
999
|
-
await
|
|
1303
|
+
await mkdir4(this.paths.blobsDir, { recursive: true, mode: 448 });
|
|
1000
1304
|
const entries = await readdir(this.paths.blobsDir, { withFileTypes: true }).catch((error) => {
|
|
1001
1305
|
if (isNodeError3(error, "ENOENT")) {
|
|
1002
1306
|
return [];
|
|
@@ -1013,7 +1317,7 @@ ${attachmentLines.join("\n")}`;
|
|
|
1013
1317
|
continue;
|
|
1014
1318
|
}
|
|
1015
1319
|
const manifest = await readJsonFile(
|
|
1016
|
-
|
|
1320
|
+
path5.join(this.paths.blobsDir, entry.name)
|
|
1017
1321
|
).catch(() => null);
|
|
1018
1322
|
if (manifest?.conversation_ids?.includes(conversationId)) {
|
|
1019
1323
|
blobIds.push(blobId);
|
|
@@ -1199,7 +1503,7 @@ function isExplicitMediaPathKey(key) {
|
|
|
1199
1503
|
].includes(key);
|
|
1200
1504
|
}
|
|
1201
1505
|
function inferMimeType(filePath) {
|
|
1202
|
-
const extension =
|
|
1506
|
+
const extension = path5.extname(filePath).toLowerCase();
|
|
1203
1507
|
return {
|
|
1204
1508
|
".png": "image/png",
|
|
1205
1509
|
".jpg": "image/jpeg",
|
|
@@ -1261,15 +1565,15 @@ function mediaSourceKey(sourcePath) {
|
|
|
1261
1565
|
return createHash("sha256").update(resolveMediaSourcePath(sourcePath)).digest("hex").slice(0, 32);
|
|
1262
1566
|
}
|
|
1263
1567
|
function sanitizeFilename(value, fallback) {
|
|
1264
|
-
const base =
|
|
1568
|
+
const base = path5.basename((value ?? "").replace(/[\r\n\t]/gu, " ").trim());
|
|
1265
1569
|
const safe = base.replace(/[/:\\]/gu, "_").slice(0, 200).trim();
|
|
1266
1570
|
return safe || fallback;
|
|
1267
1571
|
}
|
|
1268
1572
|
function resolveMediaSourcePath(sourcePath) {
|
|
1269
1573
|
const trimmed = sourcePath.trim();
|
|
1270
|
-
const expanded = trimmed.startsWith("~/") ?
|
|
1271
|
-
const resolved =
|
|
1272
|
-
if (!
|
|
1574
|
+
const expanded = trimmed.startsWith("~/") ? path5.join(process.env.HOME ?? "", trimmed.slice(2)) : trimmed;
|
|
1575
|
+
const resolved = path5.resolve(expanded);
|
|
1576
|
+
if (!path5.isAbsolute(expanded)) {
|
|
1273
1577
|
throw new LinkHttpError(400, "media_source_path_not_absolute", "Hermes output media source must be an absolute path");
|
|
1274
1578
|
}
|
|
1275
1579
|
return resolved;
|
|
@@ -1296,16 +1600,16 @@ function isNodeError3(error, code) {
|
|
|
1296
1600
|
}
|
|
1297
1601
|
|
|
1298
1602
|
// src/hermes/profiles.ts
|
|
1299
|
-
import { mkdir as
|
|
1603
|
+
import { mkdir as mkdir5, readdir as readdir2, rename as rename2, rm as rm3, stat as stat2 } from "fs/promises";
|
|
1300
1604
|
import os3 from "os";
|
|
1301
|
-
import
|
|
1605
|
+
import path6 from "path";
|
|
1302
1606
|
var DEFAULT_PROFILE = "default";
|
|
1303
1607
|
var PROFILE_NAME_PATTERN = /^[a-zA-Z0-9._-]{1,64}$/;
|
|
1304
1608
|
async function listHermesProfiles(paths = resolveRuntimePaths()) {
|
|
1305
1609
|
const activeProfile = await getActiveProfile(paths);
|
|
1306
1610
|
const profiles = /* @__PURE__ */ new Map();
|
|
1307
1611
|
profiles.set(DEFAULT_PROFILE, profileInfo(DEFAULT_PROFILE, activeProfile));
|
|
1308
|
-
const profilesDir =
|
|
1612
|
+
const profilesDir = path6.join(os3.homedir(), ".hermes", "profiles");
|
|
1309
1613
|
const entries = await readdir2(profilesDir, { withFileTypes: true }).catch((error) => {
|
|
1310
1614
|
if (isNodeError4(error, "ENOENT")) {
|
|
1311
1615
|
return [];
|
|
@@ -1352,14 +1656,14 @@ async function createHermesProfile(name) {
|
|
|
1352
1656
|
if (await pathExists(profile.path)) {
|
|
1353
1657
|
throw new Error("profile already exists");
|
|
1354
1658
|
}
|
|
1355
|
-
await
|
|
1659
|
+
await mkdir5(profile.path, { recursive: true, mode: 448 });
|
|
1356
1660
|
await ensureHermesApiServerKey(name, profile.configPath);
|
|
1357
1661
|
return profile;
|
|
1358
1662
|
}
|
|
1359
1663
|
async function useHermesProfile(name, paths = resolveRuntimePaths()) {
|
|
1360
1664
|
assertProfileName(name);
|
|
1361
1665
|
const profile = profileInfo(name, name);
|
|
1362
|
-
await
|
|
1666
|
+
await mkdir5(profile.path, { recursive: true, mode: 448 });
|
|
1363
1667
|
const current = await readJsonFile(paths.stateFile) ?? {};
|
|
1364
1668
|
await writeJsonFile(paths.stateFile, { ...current, activeProfile: name });
|
|
1365
1669
|
return profile;
|
|
@@ -1412,8 +1716,8 @@ function isNodeError4(error, code) {
|
|
|
1412
1716
|
}
|
|
1413
1717
|
|
|
1414
1718
|
// src/identity/identity.ts
|
|
1415
|
-
import { generateKeyPairSync, randomUUID as
|
|
1416
|
-
import { mkdir as
|
|
1719
|
+
import { generateKeyPairSync, randomUUID as randomUUID2, sign } from "crypto";
|
|
1720
|
+
import { mkdir as mkdir6, chmod } from "fs/promises";
|
|
1417
1721
|
import { z } from "zod";
|
|
1418
1722
|
var linkIdentitySchema = z.object({
|
|
1419
1723
|
install_id: z.string().min(1),
|
|
@@ -1435,12 +1739,12 @@ async function ensureIdentity(paths = resolveRuntimePaths()) {
|
|
|
1435
1739
|
if (existing) {
|
|
1436
1740
|
return existing;
|
|
1437
1741
|
}
|
|
1438
|
-
await
|
|
1742
|
+
await mkdir6(paths.homeDir, { recursive: true, mode: 448 });
|
|
1439
1743
|
await chmod(paths.homeDir, 448).catch(() => void 0);
|
|
1440
1744
|
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
1441
1745
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1442
1746
|
const identity = {
|
|
1443
|
-
install_id: `install_${
|
|
1747
|
+
install_id: `install_${randomUUID2().replaceAll("-", "")}`,
|
|
1444
1748
|
link_id: null,
|
|
1445
1749
|
public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
1446
1750
|
private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
@@ -1474,11 +1778,11 @@ function getIdentityStatus(identity) {
|
|
|
1474
1778
|
}
|
|
1475
1779
|
|
|
1476
1780
|
// src/pairing/pairing.ts
|
|
1477
|
-
import
|
|
1781
|
+
import path7 from "path";
|
|
1478
1782
|
import { rm as rm4 } from "fs/promises";
|
|
1479
1783
|
|
|
1480
1784
|
// src/security/devices.ts
|
|
1481
|
-
import { randomBytes as randomBytes2, randomUUID as
|
|
1785
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID3, timingSafeEqual, createHash as createHash2 } from "crypto";
|
|
1482
1786
|
var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
|
|
1483
1787
|
var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
1484
1788
|
async function createDeviceSession(input, paths = resolveRuntimePaths()) {
|
|
@@ -1487,7 +1791,7 @@ async function createDeviceSession(input, paths = resolveRuntimePaths()) {
|
|
|
1487
1791
|
const accessToken = randomToken("hpat_");
|
|
1488
1792
|
const refreshToken = randomToken("hprt_");
|
|
1489
1793
|
const device = {
|
|
1490
|
-
id: `dev_${
|
|
1794
|
+
id: `dev_${randomUUID3().replaceAll("-", "")}`,
|
|
1491
1795
|
label: input.label,
|
|
1492
1796
|
platform: input.platform,
|
|
1493
1797
|
scope: "admin",
|
|
@@ -1942,8 +2246,8 @@ async function loadRequiredIdentity(paths) {
|
|
|
1942
2246
|
}
|
|
1943
2247
|
return identity;
|
|
1944
2248
|
}
|
|
1945
|
-
async function postServerJson(serverBaseUrl,
|
|
1946
|
-
const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${
|
|
2249
|
+
async function postServerJson(serverBaseUrl, path9, body) {
|
|
2250
|
+
const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path9}`, {
|
|
1947
2251
|
method: "POST",
|
|
1948
2252
|
headers: {
|
|
1949
2253
|
accept: "application/json",
|
|
@@ -1953,8 +2257,8 @@ async function postServerJson(serverBaseUrl, path8, body) {
|
|
|
1953
2257
|
});
|
|
1954
2258
|
return readJsonResponse2(response);
|
|
1955
2259
|
}
|
|
1956
|
-
async function patchServerJson(serverBaseUrl,
|
|
1957
|
-
const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${
|
|
2260
|
+
async function patchServerJson(serverBaseUrl, path9, token, body) {
|
|
2261
|
+
const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path9}`, {
|
|
1958
2262
|
method: "PATCH",
|
|
1959
2263
|
headers: {
|
|
1960
2264
|
accept: "application/json",
|
|
@@ -1988,7 +2292,7 @@ function defaultDisplayName() {
|
|
|
1988
2292
|
return `Hermes Link ${process.platform}`;
|
|
1989
2293
|
}
|
|
1990
2294
|
function pairingClaimPath(sessionId, paths) {
|
|
1991
|
-
return
|
|
2295
|
+
return path7.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
|
|
1992
2296
|
}
|
|
1993
2297
|
function qrPreferredUrls(routes) {
|
|
1994
2298
|
return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
|
|
@@ -2064,8 +2368,8 @@ function base64UrlToBase64(value) {
|
|
|
2064
2368
|
}
|
|
2065
2369
|
|
|
2066
2370
|
// src/runtime/logger.ts
|
|
2067
|
-
import { appendFile as appendFile2, mkdir as
|
|
2068
|
-
import
|
|
2371
|
+
import { appendFile as appendFile2, mkdir as mkdir7, open as open3, readFile as readFile4, rename as rename3, rm as rm5, stat as stat3 } from "fs/promises";
|
|
2372
|
+
import path8 from "path";
|
|
2069
2373
|
var DEFAULT_LOG_FILE = "hermeslink.log";
|
|
2070
2374
|
var DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
|
|
2071
2375
|
var DEFAULT_MAX_FILES = 5;
|
|
@@ -2113,7 +2417,7 @@ var FileLogger = class {
|
|
|
2113
2417
|
return this.queue;
|
|
2114
2418
|
}
|
|
2115
2419
|
async appendEntry(entry) {
|
|
2116
|
-
await
|
|
2420
|
+
await mkdir7(this.paths.logsDir, { recursive: true, mode: 448 });
|
|
2117
2421
|
const line = `${JSON.stringify(entry)}
|
|
2118
2422
|
`;
|
|
2119
2423
|
await this.rotateIfNeeded(Buffer.byteLength(line, "utf8"));
|
|
@@ -2139,7 +2443,7 @@ function createFileLogger(options = {}) {
|
|
|
2139
2443
|
return new FileLogger(options);
|
|
2140
2444
|
}
|
|
2141
2445
|
function getLinkLogFile(paths = resolveRuntimePaths(), fileName = DEFAULT_LOG_FILE) {
|
|
2142
|
-
return
|
|
2446
|
+
return path8.join(paths.logsDir, fileName);
|
|
2143
2447
|
}
|
|
2144
2448
|
async function readRecentLogEntries(options = {}) {
|
|
2145
2449
|
const paths = options.paths ?? resolveRuntimePaths();
|
|
@@ -2241,7 +2545,7 @@ async function readTail(filePath, maxBytes) {
|
|
|
2241
2545
|
if (info.size <= maxBytes) {
|
|
2242
2546
|
return await readFile4(filePath, "utf8").catch(() => null);
|
|
2243
2547
|
}
|
|
2244
|
-
const handle = await
|
|
2548
|
+
const handle = await open3(filePath, "r").catch(() => null);
|
|
2245
2549
|
if (!handle) {
|
|
2246
2550
|
return null;
|
|
2247
2551
|
}
|
|
@@ -2780,7 +3084,8 @@ export {
|
|
|
2780
3084
|
LINK_COMMAND,
|
|
2781
3085
|
resolveRuntimePaths,
|
|
2782
3086
|
loadConfig,
|
|
2783
|
-
|
|
3087
|
+
ensureHermesApiServerConfig,
|
|
3088
|
+
ensureHermesApiServerAvailable,
|
|
2784
3089
|
loadIdentity,
|
|
2785
3090
|
ensureIdentity,
|
|
2786
3091
|
getIdentityStatus,
|
|
@@ -2791,4 +3096,4 @@ export {
|
|
|
2791
3096
|
getLinkLogFile,
|
|
2792
3097
|
createApp
|
|
2793
3098
|
};
|
|
2794
|
-
//# sourceMappingURL=chunk-
|
|
3099
|
+
//# sourceMappingURL=chunk-L2NM2XMX.js.map
|