@hermespilot/link 0.1.2 → 0.1.4
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 +8 -0
- package/dist/{chunk-E2BRK5JT.js → chunk-T35GPRKF.js} +288 -34
- package/dist/chunk-T35GPRKF.js.map +1 -0
- package/dist/cli/index.js +680 -107
- package/dist/cli/index.js.map +1 -1
- package/dist/http/app.d.ts +45 -2
- package/dist/http/app.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-E2BRK5JT.js.map +0 -1
package/README.md
CHANGED
|
@@ -22,11 +22,17 @@ hermeslink --version
|
|
|
22
22
|
hermeslink status
|
|
23
23
|
hermeslink pair
|
|
24
24
|
hermeslink start
|
|
25
|
+
hermeslink stop
|
|
26
|
+
hermeslink autostart on
|
|
27
|
+
hermeslink autostart off
|
|
25
28
|
hermeslink doctor
|
|
29
|
+
hermeslink logs
|
|
26
30
|
```
|
|
27
31
|
|
|
28
32
|
`hermeslink pair` requires HermesPilot Server and Relay to be available. The terminal side does not ask for a HermesPilot account; the App must be logged in before it scans or claims a pairing session.
|
|
29
33
|
|
|
34
|
+
After a successful QR claim, `hermeslink pair` starts Hermes Link in the background and enables boot autostart. Boot autostart does not configure launchd/systemd restart policies; if the user stops Hermes Link, the operating system should not automatically relaunch it until the next login/boot autostart cycle.
|
|
35
|
+
|
|
30
36
|
CLI output follows the current system language when it is Chinese or English. You can override it for a single command with `HERMESLINK_LANG=zh-CN` or `HERMESLINK_LANG=en`.
|
|
31
37
|
|
|
32
38
|
## Runtime data
|
|
@@ -38,3 +44,5 @@ Hermes Link keeps its local identity and runtime state under:
|
|
|
38
44
|
```
|
|
39
45
|
|
|
40
46
|
Uninstalling the npm package does not remove this directory, so the same Link ID can be reused after reinstalling.
|
|
47
|
+
|
|
48
|
+
Service logs are written as rotated JSONL files under `~/.hermeslink/logs/hermeslink.log`. A paired App can read the same service log stream through `GET /api/v1/logs` using the normal Link access token.
|
|
@@ -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.4";
|
|
8
8
|
var LINK_COMMAND = "hermeslink";
|
|
9
9
|
var LINK_DEFAULT_PORT = 52379;
|
|
10
10
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -270,7 +270,7 @@ async function cancelHermesRun(runId, options = {}) {
|
|
|
270
270
|
}
|
|
271
271
|
fallbackRuns.delete(runId);
|
|
272
272
|
}
|
|
273
|
-
async function callHermesApi(
|
|
273
|
+
async function callHermesApi(path6, init, options) {
|
|
274
274
|
const config = await readHermesApiServerConfig();
|
|
275
275
|
if (!config.port || !config.key) {
|
|
276
276
|
return new Response(null, { status: 503 });
|
|
@@ -280,7 +280,7 @@ async function callHermesApi(path5, init, options) {
|
|
|
280
280
|
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
281
281
|
headers.set("x-api-key", config.key);
|
|
282
282
|
headers.set("authorization", `Bearer ${config.key}`);
|
|
283
|
-
return await fetcher(`http://127.0.0.1:${config.port}${
|
|
283
|
+
return await fetcher(`http://127.0.0.1:${config.port}${path6}`, {
|
|
284
284
|
...init,
|
|
285
285
|
headers
|
|
286
286
|
}).catch(() => new Response(null, { status: 503 }));
|
|
@@ -930,8 +930,8 @@ async function loadRequiredIdentity(paths) {
|
|
|
930
930
|
}
|
|
931
931
|
return identity;
|
|
932
932
|
}
|
|
933
|
-
async function postServerJson(serverBaseUrl,
|
|
934
|
-
const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${
|
|
933
|
+
async function postServerJson(serverBaseUrl, path6, body) {
|
|
934
|
+
const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path6}`, {
|
|
935
935
|
method: "POST",
|
|
936
936
|
headers: {
|
|
937
937
|
accept: "application/json",
|
|
@@ -941,8 +941,8 @@ async function postServerJson(serverBaseUrl, path5, body) {
|
|
|
941
941
|
});
|
|
942
942
|
return readJsonResponse2(response);
|
|
943
943
|
}
|
|
944
|
-
async function patchServerJson(serverBaseUrl,
|
|
945
|
-
const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${
|
|
944
|
+
async function patchServerJson(serverBaseUrl, path6, token, body) {
|
|
945
|
+
const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path6}`, {
|
|
946
946
|
method: "PATCH",
|
|
947
947
|
headers: {
|
|
948
948
|
accept: "application/json",
|
|
@@ -1048,11 +1048,217 @@ function base64UrlToBase64(value) {
|
|
|
1048
1048
|
return normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
1049
1049
|
}
|
|
1050
1050
|
|
|
1051
|
+
// src/runtime/logger.ts
|
|
1052
|
+
import { appendFile, mkdir as mkdir5, open as open2, readFile as readFile3, rename as rename3, rm as rm3, stat as stat2 } from "fs/promises";
|
|
1053
|
+
import path5 from "path";
|
|
1054
|
+
var DEFAULT_LOG_FILE = "hermeslink.log";
|
|
1055
|
+
var DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
|
|
1056
|
+
var DEFAULT_MAX_FILES = 5;
|
|
1057
|
+
var DEFAULT_READ_LIMIT = 200;
|
|
1058
|
+
var MAX_READ_LIMIT = 1e3;
|
|
1059
|
+
var DEFAULT_MAX_BYTES_PER_FILE = 512 * 1024;
|
|
1060
|
+
var FileLogger = class {
|
|
1061
|
+
filePath;
|
|
1062
|
+
paths;
|
|
1063
|
+
maxFileBytes;
|
|
1064
|
+
maxFiles;
|
|
1065
|
+
now;
|
|
1066
|
+
queue = Promise.resolve();
|
|
1067
|
+
constructor(options = {}) {
|
|
1068
|
+
this.paths = options.paths ?? resolveRuntimePaths();
|
|
1069
|
+
this.filePath = getLinkLogFile(this.paths, options.fileName);
|
|
1070
|
+
this.maxFileBytes = Math.max(256, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
|
|
1071
|
+
this.maxFiles = Math.max(0, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
|
|
1072
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1073
|
+
}
|
|
1074
|
+
debug(message, fields) {
|
|
1075
|
+
return this.write("debug", message, fields);
|
|
1076
|
+
}
|
|
1077
|
+
info(message, fields) {
|
|
1078
|
+
return this.write("info", message, fields);
|
|
1079
|
+
}
|
|
1080
|
+
warn(message, fields) {
|
|
1081
|
+
return this.write("warn", message, fields);
|
|
1082
|
+
}
|
|
1083
|
+
error(message, fields) {
|
|
1084
|
+
return this.write("error", message, fields);
|
|
1085
|
+
}
|
|
1086
|
+
write(level, message, fields) {
|
|
1087
|
+
const entry = {
|
|
1088
|
+
ts: this.now().toISOString(),
|
|
1089
|
+
level,
|
|
1090
|
+
message,
|
|
1091
|
+
...fields ? { fields: sanitizeFields(fields) } : {}
|
|
1092
|
+
};
|
|
1093
|
+
const next = this.queue.then(() => this.appendEntry(entry)).catch(() => void 0);
|
|
1094
|
+
this.queue = next;
|
|
1095
|
+
return next;
|
|
1096
|
+
}
|
|
1097
|
+
flush() {
|
|
1098
|
+
return this.queue;
|
|
1099
|
+
}
|
|
1100
|
+
async appendEntry(entry) {
|
|
1101
|
+
await mkdir5(this.paths.logsDir, { recursive: true, mode: 448 });
|
|
1102
|
+
const line = `${JSON.stringify(entry)}
|
|
1103
|
+
`;
|
|
1104
|
+
await this.rotateIfNeeded(Buffer.byteLength(line, "utf8"));
|
|
1105
|
+
await appendFile(this.filePath, line, { mode: 384 });
|
|
1106
|
+
}
|
|
1107
|
+
async rotateIfNeeded(nextBytes) {
|
|
1108
|
+
const current = await stat2(this.filePath).catch(() => null);
|
|
1109
|
+
if (!current || current.size === 0 || current.size + nextBytes <= this.maxFileBytes) {
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
if (this.maxFiles === 0) {
|
|
1113
|
+
await rm3(this.filePath, { force: true }).catch(() => void 0);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
await rm3(rotatedLogFile(this.filePath, this.maxFiles), { force: true }).catch(() => void 0);
|
|
1117
|
+
for (let index = this.maxFiles - 1; index >= 1; index -= 1) {
|
|
1118
|
+
await moveIfExists(rotatedLogFile(this.filePath, index), rotatedLogFile(this.filePath, index + 1));
|
|
1119
|
+
}
|
|
1120
|
+
await moveIfExists(this.filePath, rotatedLogFile(this.filePath, 1));
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
function createFileLogger(options = {}) {
|
|
1124
|
+
return new FileLogger(options);
|
|
1125
|
+
}
|
|
1126
|
+
function getLinkLogFile(paths = resolveRuntimePaths(), fileName = DEFAULT_LOG_FILE) {
|
|
1127
|
+
return path5.join(paths.logsDir, fileName);
|
|
1128
|
+
}
|
|
1129
|
+
async function readRecentLogEntries(options = {}) {
|
|
1130
|
+
const paths = options.paths ?? resolveRuntimePaths();
|
|
1131
|
+
const filePath = getLinkLogFile(paths, options.fileName);
|
|
1132
|
+
const limit = clampLimit(options.limit);
|
|
1133
|
+
const maxFiles = Math.max(0, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
|
|
1134
|
+
const maxBytesPerFile = Math.max(1024, Math.floor(options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE));
|
|
1135
|
+
const files = [filePath, ...Array.from({ length: maxFiles }, (_, index) => rotatedLogFile(filePath, index + 1))];
|
|
1136
|
+
const entries = [];
|
|
1137
|
+
for (const file of files) {
|
|
1138
|
+
const raw = await readTail(file, maxBytesPerFile);
|
|
1139
|
+
if (!raw) {
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
const lines = raw.split(/\r?\n/u).filter(Boolean);
|
|
1143
|
+
for (let index = lines.length - 1; index >= 0 && entries.length < limit; index -= 1) {
|
|
1144
|
+
const entry = parseLogLine(lines[index]);
|
|
1145
|
+
if (entry) {
|
|
1146
|
+
entries.push(entry);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (entries.length >= limit) {
|
|
1150
|
+
break;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return entries.reverse();
|
|
1154
|
+
}
|
|
1155
|
+
function clampLimit(value) {
|
|
1156
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1157
|
+
return DEFAULT_READ_LIMIT;
|
|
1158
|
+
}
|
|
1159
|
+
return Math.min(MAX_READ_LIMIT, Math.max(1, Math.floor(value)));
|
|
1160
|
+
}
|
|
1161
|
+
function sanitizeFields(fields) {
|
|
1162
|
+
return sanitizeObject(fields, 0);
|
|
1163
|
+
}
|
|
1164
|
+
function sanitizeValue(value, depth) {
|
|
1165
|
+
if (value === null || typeof value === "boolean") {
|
|
1166
|
+
return value;
|
|
1167
|
+
}
|
|
1168
|
+
if (typeof value === "number") {
|
|
1169
|
+
return Number.isFinite(value) ? value : null;
|
|
1170
|
+
}
|
|
1171
|
+
if (typeof value === "string") {
|
|
1172
|
+
return value.length > 2e3 ? `${value.slice(0, 2e3)}...` : value;
|
|
1173
|
+
}
|
|
1174
|
+
if (Array.isArray(value)) {
|
|
1175
|
+
if (depth >= 3) {
|
|
1176
|
+
return "[array]";
|
|
1177
|
+
}
|
|
1178
|
+
return value.slice(0, 20).map((item) => sanitizeValue(item, depth + 1));
|
|
1179
|
+
}
|
|
1180
|
+
if (typeof value === "object" && value !== null) {
|
|
1181
|
+
if (depth >= 3) {
|
|
1182
|
+
return "[object]";
|
|
1183
|
+
}
|
|
1184
|
+
return sanitizeObject(value, depth + 1);
|
|
1185
|
+
}
|
|
1186
|
+
return String(value);
|
|
1187
|
+
}
|
|
1188
|
+
function sanitizeObject(value, depth) {
|
|
1189
|
+
const result = {};
|
|
1190
|
+
for (const [key, child] of Object.entries(value).slice(0, 50)) {
|
|
1191
|
+
if (isSensitiveKey(key)) {
|
|
1192
|
+
result[key] = "[redacted]";
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
result[key] = sanitizeValue(child, depth);
|
|
1196
|
+
}
|
|
1197
|
+
return result;
|
|
1198
|
+
}
|
|
1199
|
+
function isSensitiveKey(key) {
|
|
1200
|
+
return /(authorization|cookie|token|secret|password|private[_-]?key|api[_-]?key)/iu.test(key);
|
|
1201
|
+
}
|
|
1202
|
+
function parseLogLine(line) {
|
|
1203
|
+
try {
|
|
1204
|
+
const value = JSON.parse(line);
|
|
1205
|
+
if (!value || typeof value.ts !== "string" || !isLogLevel(value.level) || typeof value.message !== "string") {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
return {
|
|
1209
|
+
ts: value.ts,
|
|
1210
|
+
level: value.level,
|
|
1211
|
+
message: value.message,
|
|
1212
|
+
...value.fields && typeof value.fields === "object" ? { fields: value.fields } : {}
|
|
1213
|
+
};
|
|
1214
|
+
} catch {
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
function isLogLevel(value) {
|
|
1219
|
+
return value === "debug" || value === "info" || value === "warn" || value === "error";
|
|
1220
|
+
}
|
|
1221
|
+
async function readTail(filePath, maxBytes) {
|
|
1222
|
+
const info = await stat2(filePath).catch(() => null);
|
|
1223
|
+
if (!info || info.size <= 0) {
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
if (info.size <= maxBytes) {
|
|
1227
|
+
return await readFile3(filePath, "utf8").catch(() => null);
|
|
1228
|
+
}
|
|
1229
|
+
const handle = await open2(filePath, "r").catch(() => null);
|
|
1230
|
+
if (!handle) {
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
try {
|
|
1234
|
+
const length = Math.min(info.size, maxBytes);
|
|
1235
|
+
const buffer = Buffer.alloc(length);
|
|
1236
|
+
await handle.read(buffer, 0, length, info.size - length);
|
|
1237
|
+
return buffer.toString("utf8");
|
|
1238
|
+
} finally {
|
|
1239
|
+
await handle.close();
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
async function moveIfExists(from, to) {
|
|
1243
|
+
await rm3(to, { force: true }).catch(() => void 0);
|
|
1244
|
+
await rename3(from, to).catch((error) => {
|
|
1245
|
+
if (error.code !== "ENOENT") {
|
|
1246
|
+
throw error;
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
function rotatedLogFile(filePath, index) {
|
|
1251
|
+
return `${filePath}.${index}`;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1051
1254
|
// src/http/app.ts
|
|
1052
|
-
async function createApp() {
|
|
1255
|
+
async function createApp(options = {}) {
|
|
1256
|
+
const paths = options.paths ?? resolveRuntimePaths();
|
|
1257
|
+
const logger = options.logger ?? createFileLogger({ paths });
|
|
1053
1258
|
const app = new Koa();
|
|
1054
1259
|
const router = new Router();
|
|
1055
1260
|
app.use(async (ctx, next) => {
|
|
1261
|
+
const startedAt = Date.now();
|
|
1056
1262
|
try {
|
|
1057
1263
|
await next();
|
|
1058
1264
|
} catch (error) {
|
|
@@ -1066,10 +1272,24 @@ async function createApp() {
|
|
|
1066
1272
|
message: error instanceof Error ? error.message : "Internal error"
|
|
1067
1273
|
}
|
|
1068
1274
|
};
|
|
1275
|
+
void logger.write(status >= 500 ? "error" : "warn", "http_request_failed", {
|
|
1276
|
+
method: ctx.method,
|
|
1277
|
+
path: ctx.path,
|
|
1278
|
+
status,
|
|
1279
|
+
code: isLinkHttpError(error) ? error.code : status === 400 ? "invalid_profile_name" : "internal_error",
|
|
1280
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1281
|
+
});
|
|
1282
|
+
} finally {
|
|
1283
|
+
void logger.info("http_request", {
|
|
1284
|
+
method: ctx.method,
|
|
1285
|
+
path: ctx.path,
|
|
1286
|
+
status: ctx.status,
|
|
1287
|
+
duration_ms: Date.now() - startedAt
|
|
1288
|
+
});
|
|
1069
1289
|
}
|
|
1070
1290
|
});
|
|
1071
1291
|
router.get("/api/v1/bootstrap", async (ctx) => {
|
|
1072
|
-
const [identity, config] = await Promise.all([loadIdentity(), loadConfig()]);
|
|
1292
|
+
const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
|
|
1073
1293
|
const routes = identity?.link_id ? await discoverRouteCandidates({
|
|
1074
1294
|
port: config.port,
|
|
1075
1295
|
relayBaseUrl: config.relayBaseUrl,
|
|
@@ -1091,7 +1311,8 @@ async function createApp() {
|
|
|
1091
1311
|
runs: true,
|
|
1092
1312
|
sse: true,
|
|
1093
1313
|
relay: true,
|
|
1094
|
-
profiles: true
|
|
1314
|
+
profiles: true,
|
|
1315
|
+
logs: true
|
|
1095
1316
|
}
|
|
1096
1317
|
};
|
|
1097
1318
|
});
|
|
@@ -1102,16 +1323,28 @@ async function createApp() {
|
|
|
1102
1323
|
if (!sessionId || !claimToken) {
|
|
1103
1324
|
throw new LinkHttpError(400, "pairing_claim_invalid", "session_id and claim_token are required");
|
|
1104
1325
|
}
|
|
1105
|
-
|
|
1326
|
+
const claimed = await claimPairing({
|
|
1106
1327
|
sessionId,
|
|
1107
1328
|
claimToken,
|
|
1108
1329
|
deviceLabel: readString2(body, "device_label") ?? readString2(body, "deviceLabel") ?? "HermesPilot App",
|
|
1109
|
-
devicePlatform: readString2(body, "device_platform") ?? readString2(body, "devicePlatform") ?? "unknown"
|
|
1330
|
+
devicePlatform: readString2(body, "device_platform") ?? readString2(body, "devicePlatform") ?? "unknown",
|
|
1331
|
+
paths
|
|
1110
1332
|
});
|
|
1333
|
+
ctx.body = claimed;
|
|
1334
|
+
void logger.info("pairing_claimed", {
|
|
1335
|
+
device_id: claimed.device.device_id,
|
|
1336
|
+
device_platform: claimed.device.platform
|
|
1337
|
+
});
|
|
1338
|
+
if (options.onPairingClaimed) {
|
|
1339
|
+
const timer = setTimeout(() => {
|
|
1340
|
+
void options.onPairingClaimed?.();
|
|
1341
|
+
}, 250);
|
|
1342
|
+
timer.unref?.();
|
|
1343
|
+
}
|
|
1111
1344
|
});
|
|
1112
1345
|
router.get("/api/v1/auth/me", async (ctx) => {
|
|
1113
|
-
const auth = await authenticateRequest(ctx);
|
|
1114
|
-
const identity = await loadRequiredIdentity2();
|
|
1346
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
1347
|
+
const identity = await loadRequiredIdentity2(paths);
|
|
1115
1348
|
ctx.body = {
|
|
1116
1349
|
ok: true,
|
|
1117
1350
|
auth: { kind: auth.kind, account_id: auth.accountId ?? null },
|
|
@@ -1134,7 +1367,7 @@ async function createApp() {
|
|
|
1134
1367
|
if (!refreshToken) {
|
|
1135
1368
|
throw new LinkHttpError(400, "refresh_token_required", "refresh_token is required");
|
|
1136
1369
|
}
|
|
1137
|
-
const session = await refreshDeviceSession(refreshToken);
|
|
1370
|
+
const session = await refreshDeviceSession(refreshToken, paths);
|
|
1138
1371
|
ctx.body = {
|
|
1139
1372
|
ok: true,
|
|
1140
1373
|
device: session.device,
|
|
@@ -1152,13 +1385,13 @@ async function createApp() {
|
|
|
1152
1385
|
const body = await readJsonBody(ctx.req);
|
|
1153
1386
|
const refreshToken = readString2(body, "refresh_token") ?? readString2(body, "refreshToken");
|
|
1154
1387
|
if (refreshToken) {
|
|
1155
|
-
await revokeDeviceRefreshToken(refreshToken);
|
|
1388
|
+
await revokeDeviceRefreshToken(refreshToken, paths);
|
|
1156
1389
|
}
|
|
1157
1390
|
ctx.body = { ok: true };
|
|
1158
1391
|
});
|
|
1159
1392
|
router.get("/api/v1/status", async (ctx) => {
|
|
1160
|
-
await authenticateRequest(ctx);
|
|
1161
|
-
const [identity, config] = await Promise.all([loadIdentity(), loadConfig()]);
|
|
1393
|
+
await authenticateRequest(ctx, paths);
|
|
1394
|
+
const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
|
|
1162
1395
|
ctx.body = {
|
|
1163
1396
|
ok: true,
|
|
1164
1397
|
version: LINK_VERSION,
|
|
@@ -1167,12 +1400,23 @@ async function createApp() {
|
|
|
1167
1400
|
port: config.port
|
|
1168
1401
|
};
|
|
1169
1402
|
});
|
|
1403
|
+
router.get("/api/v1/logs", async (ctx) => {
|
|
1404
|
+
await authenticateRequest(ctx, paths);
|
|
1405
|
+
ctx.set("cache-control", "no-store");
|
|
1406
|
+
ctx.body = {
|
|
1407
|
+
ok: true,
|
|
1408
|
+
logs: await readRecentLogEntries({
|
|
1409
|
+
paths,
|
|
1410
|
+
limit: readLimit(ctx.query.limit)
|
|
1411
|
+
})
|
|
1412
|
+
};
|
|
1413
|
+
});
|
|
1170
1414
|
router.get("/api/v1/models", async (ctx) => {
|
|
1171
|
-
await authenticateRequest(ctx);
|
|
1415
|
+
await authenticateRequest(ctx, paths);
|
|
1172
1416
|
ctx.body = await listHermesModels();
|
|
1173
1417
|
});
|
|
1174
1418
|
router.post("/api/v1/runs", async (ctx) => {
|
|
1175
|
-
await authenticateRequest(ctx);
|
|
1419
|
+
await authenticateRequest(ctx, paths);
|
|
1176
1420
|
const body = await readJsonBody(ctx.req);
|
|
1177
1421
|
const input = readString2(body, "input");
|
|
1178
1422
|
if (!input) {
|
|
@@ -1186,7 +1430,7 @@ async function createApp() {
|
|
|
1186
1430
|
});
|
|
1187
1431
|
});
|
|
1188
1432
|
router.get("/api/v1/runs/:runId/events", async (ctx) => {
|
|
1189
|
-
await authenticateRequest(ctx);
|
|
1433
|
+
await authenticateRequest(ctx, paths);
|
|
1190
1434
|
const response = await streamHermesRunEvents(ctx.params.runId);
|
|
1191
1435
|
ctx.status = response.status;
|
|
1192
1436
|
for (const [key, value] of response.headers.entries()) {
|
|
@@ -1205,12 +1449,12 @@ async function createApp() {
|
|
|
1205
1449
|
}
|
|
1206
1450
|
});
|
|
1207
1451
|
router.post("/api/v1/runs/:runId/cancel", async (ctx) => {
|
|
1208
|
-
await authenticateRequest(ctx);
|
|
1452
|
+
await authenticateRequest(ctx, paths);
|
|
1209
1453
|
await cancelHermesRun(ctx.params.runId);
|
|
1210
1454
|
ctx.body = { ok: true };
|
|
1211
1455
|
});
|
|
1212
1456
|
router.get("/api/v1/profiles", async (ctx) => {
|
|
1213
|
-
await authenticateRequest(ctx);
|
|
1457
|
+
await authenticateRequest(ctx, paths);
|
|
1214
1458
|
ctx.set("cache-control", "no-store");
|
|
1215
1459
|
ctx.body = {
|
|
1216
1460
|
ok: true,
|
|
@@ -1218,7 +1462,7 @@ async function createApp() {
|
|
|
1218
1462
|
};
|
|
1219
1463
|
});
|
|
1220
1464
|
router.get("/api/v1/profiles/:name/status", async (ctx) => {
|
|
1221
|
-
await authenticateRequest(ctx);
|
|
1465
|
+
await authenticateRequest(ctx, paths);
|
|
1222
1466
|
ctx.set("cache-control", "no-store");
|
|
1223
1467
|
ctx.body = {
|
|
1224
1468
|
ok: true,
|
|
@@ -1226,7 +1470,7 @@ async function createApp() {
|
|
|
1226
1470
|
};
|
|
1227
1471
|
});
|
|
1228
1472
|
router.post("/api/v1/profiles", async (ctx) => {
|
|
1229
|
-
await authenticateRequest(ctx);
|
|
1473
|
+
await authenticateRequest(ctx, paths);
|
|
1230
1474
|
const body = await readJsonBody(ctx.req);
|
|
1231
1475
|
const name = readProfileName(body);
|
|
1232
1476
|
ctx.status = 201;
|
|
@@ -1236,14 +1480,14 @@ async function createApp() {
|
|
|
1236
1480
|
};
|
|
1237
1481
|
});
|
|
1238
1482
|
router.post("/api/v1/profiles/:name/use", async (ctx) => {
|
|
1239
|
-
await authenticateRequest(ctx);
|
|
1483
|
+
await authenticateRequest(ctx, paths);
|
|
1240
1484
|
ctx.body = {
|
|
1241
1485
|
ok: true,
|
|
1242
1486
|
profile: await useHermesProfile(ctx.params.name)
|
|
1243
1487
|
};
|
|
1244
1488
|
});
|
|
1245
1489
|
router.patch("/api/v1/profiles/:name", async (ctx) => {
|
|
1246
|
-
await authenticateRequest(ctx);
|
|
1490
|
+
await authenticateRequest(ctx, paths);
|
|
1247
1491
|
const body = await readJsonBody(ctx.req);
|
|
1248
1492
|
const name = readProfileName(body);
|
|
1249
1493
|
ctx.body = {
|
|
@@ -1252,7 +1496,7 @@ async function createApp() {
|
|
|
1252
1496
|
};
|
|
1253
1497
|
});
|
|
1254
1498
|
router.delete("/api/v1/profiles/:name", async (ctx) => {
|
|
1255
|
-
await authenticateRequest(ctx);
|
|
1499
|
+
await authenticateRequest(ctx, paths);
|
|
1256
1500
|
await deleteHermesProfile(ctx.params.name);
|
|
1257
1501
|
ctx.status = 204;
|
|
1258
1502
|
});
|
|
@@ -1276,24 +1520,24 @@ function readProfileName(body) {
|
|
|
1276
1520
|
}
|
|
1277
1521
|
return body.name;
|
|
1278
1522
|
}
|
|
1279
|
-
async function authenticateRequest(ctx) {
|
|
1523
|
+
async function authenticateRequest(ctx, paths) {
|
|
1280
1524
|
const token = readBearerToken(ctx.get("authorization"));
|
|
1281
1525
|
if (!token) {
|
|
1282
1526
|
throw new LinkHttpError(401, "auth_required", "Authorization bearer token is required");
|
|
1283
1527
|
}
|
|
1284
|
-
const device = await authenticateDeviceAccessToken(token);
|
|
1528
|
+
const device = await authenticateDeviceAccessToken(token, paths);
|
|
1285
1529
|
if (device) {
|
|
1286
1530
|
return { kind: "device", device };
|
|
1287
1531
|
}
|
|
1288
|
-
const [identity, config] = await Promise.all([loadRequiredIdentity2(), loadConfig()]);
|
|
1532
|
+
const [identity, config] = await Promise.all([loadRequiredIdentity2(paths), loadConfig(paths)]);
|
|
1289
1533
|
const claims = await verifyAppConnectToken(token, {
|
|
1290
1534
|
config,
|
|
1291
1535
|
linkId: identity.link_id
|
|
1292
1536
|
});
|
|
1293
1537
|
return { kind: "app-connect", accountId: claims.sub };
|
|
1294
1538
|
}
|
|
1295
|
-
async function loadRequiredIdentity2() {
|
|
1296
|
-
const identity = await loadIdentity();
|
|
1539
|
+
async function loadRequiredIdentity2(paths) {
|
|
1540
|
+
const identity = await loadIdentity(paths);
|
|
1297
1541
|
if (!identity?.link_id) {
|
|
1298
1542
|
throw new LinkHttpError(409, "link_not_paired", "Hermes Link is not paired");
|
|
1299
1543
|
}
|
|
@@ -1311,6 +1555,14 @@ function readString2(body, key) {
|
|
|
1311
1555
|
const value = body[key];
|
|
1312
1556
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1313
1557
|
}
|
|
1558
|
+
function readLimit(value) {
|
|
1559
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
1560
|
+
if (typeof raw !== "string") {
|
|
1561
|
+
return void 0;
|
|
1562
|
+
}
|
|
1563
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1564
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1565
|
+
}
|
|
1314
1566
|
function readConversationHistory(value) {
|
|
1315
1567
|
if (!Array.isArray(value)) {
|
|
1316
1568
|
return [];
|
|
@@ -1345,6 +1597,8 @@ export {
|
|
|
1345
1597
|
ensureIdentity,
|
|
1346
1598
|
getIdentityStatus,
|
|
1347
1599
|
preparePairing,
|
|
1600
|
+
createFileLogger,
|
|
1601
|
+
getLinkLogFile,
|
|
1348
1602
|
createApp
|
|
1349
1603
|
};
|
|
1350
|
-
//# sourceMappingURL=chunk-
|
|
1604
|
+
//# sourceMappingURL=chunk-T35GPRKF.js.map
|