@hermespilot/link 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http/app.js CHANGED
@@ -1,240 +1,6 @@
1
1
  import {
2
- LINK_VERSION,
3
- ensureHermesApiServerKey,
4
- loadConfig,
5
- loadIdentity,
6
- readHermesApiServerConfig,
7
- readJsonFile,
8
- resolveHermesConfigPath,
9
- resolveHermesProfileDir,
10
- resolveRuntimePaths,
11
- writeJsonFile
12
- } from "../chunk-YTX3DQGX.js";
13
-
14
- // src/http/app.ts
15
- import Koa from "koa";
16
- import Router from "@koa/router";
17
-
18
- // src/hermes/profiles.ts
19
- import { mkdir, readdir, rename, rm, stat } from "fs/promises";
20
- import os from "os";
21
- import path from "path";
22
- var DEFAULT_PROFILE = "default";
23
- var PROFILE_NAME_PATTERN = /^[a-zA-Z0-9._-]{1,64}$/;
24
- async function listHermesProfiles(paths = resolveRuntimePaths()) {
25
- const activeProfile = await getActiveProfile(paths);
26
- const profiles = /* @__PURE__ */ new Map();
27
- profiles.set(DEFAULT_PROFILE, profileInfo(DEFAULT_PROFILE, activeProfile));
28
- const profilesDir = path.join(os.homedir(), ".hermes", "profiles");
29
- const entries = await readdir(profilesDir, { withFileTypes: true }).catch((error) => {
30
- if (isNodeError(error, "ENOENT")) {
31
- return [];
32
- }
33
- throw error;
34
- });
35
- for (const entry of entries) {
36
- if (entry.isDirectory() && PROFILE_NAME_PATTERN.test(entry.name)) {
37
- profiles.set(entry.name, profileInfo(entry.name, activeProfile));
38
- }
39
- }
40
- return [...profiles.values()].sort((left, right) => {
41
- if (left.name === DEFAULT_PROFILE) {
42
- return -1;
43
- }
44
- if (right.name === DEFAULT_PROFILE) {
45
- return 1;
46
- }
47
- return left.name.localeCompare(right.name);
48
- });
49
- }
50
- async function getHermesProfileStatus(name, paths = resolveRuntimePaths()) {
51
- assertProfileName(name);
52
- const profile = profileInfo(name, await getActiveProfile(paths));
53
- const exists = await stat(profile.path).then((value) => value.isDirectory()).catch((error) => {
54
- if (isNodeError(error, "ENOENT")) {
55
- return false;
56
- }
57
- throw error;
58
- });
59
- const config = await readHermesApiServerConfig(name, profile.configPath);
60
- return {
61
- ...profile,
62
- exists,
63
- apiKeyConfigured: Boolean(config.key)
64
- };
65
- }
66
- async function createHermesProfile(name) {
67
- assertProfileName(name);
68
- if (name === DEFAULT_PROFILE) {
69
- throw new Error("default profile already exists");
70
- }
71
- const profile = profileInfo(name, DEFAULT_PROFILE);
72
- if (await pathExists(profile.path)) {
73
- throw new Error("profile already exists");
74
- }
75
- await mkdir(profile.path, { recursive: true, mode: 448 });
76
- await ensureHermesApiServerKey(name, profile.configPath);
77
- return profile;
78
- }
79
- async function useHermesProfile(name, paths = resolveRuntimePaths()) {
80
- assertProfileName(name);
81
- const profile = profileInfo(name, name);
82
- await mkdir(profile.path, { recursive: true, mode: 448 });
83
- const current = await readJsonFile(paths.stateFile) ?? {};
84
- await writeJsonFile(paths.stateFile, { ...current, activeProfile: name });
85
- return profile;
86
- }
87
- async function renameHermesProfile(oldName, newName) {
88
- assertMutableProfile(oldName);
89
- assertMutableProfile(newName);
90
- const oldProfile = profileInfo(oldName, DEFAULT_PROFILE);
91
- const newProfile = profileInfo(newName, DEFAULT_PROFILE);
92
- await rename(oldProfile.path, newProfile.path);
93
- return newProfile;
94
- }
95
- async function deleteHermesProfile(name) {
96
- assertMutableProfile(name);
97
- await rm(resolveHermesProfileDir(name), { recursive: true, force: true });
98
- }
99
- async function getActiveProfile(paths) {
100
- const state = await readJsonFile(paths.stateFile);
101
- return typeof state?.activeProfile === "string" && PROFILE_NAME_PATTERN.test(state.activeProfile) ? state.activeProfile : DEFAULT_PROFILE;
102
- }
103
- function profileInfo(name, activeProfile) {
104
- return {
105
- name,
106
- active: name === activeProfile,
107
- path: resolveHermesProfileDir(name),
108
- configPath: resolveHermesConfigPath(name)
109
- };
110
- }
111
- function assertMutableProfile(name) {
112
- assertProfileName(name);
113
- if (name === DEFAULT_PROFILE) {
114
- throw new Error("default profile cannot be renamed or deleted");
115
- }
116
- }
117
- function assertProfileName(name) {
118
- if (!PROFILE_NAME_PATTERN.test(name)) {
119
- throw new Error("invalid profile name");
120
- }
121
- }
122
- async function pathExists(targetPath) {
123
- return await stat(targetPath).then(() => true).catch((error) => {
124
- if (isNodeError(error, "ENOENT")) {
125
- return false;
126
- }
127
- throw error;
128
- });
129
- }
130
- function isNodeError(error, code) {
131
- return typeof error === "object" && error !== null && "code" in error && error.code === code;
132
- }
133
-
134
- // src/http/app.ts
135
- async function createApp() {
136
- const app = new Koa();
137
- const router = new Router();
138
- app.use(async (ctx, next) => {
139
- try {
140
- await next();
141
- } catch (error) {
142
- ctx.status = error instanceof Error && error.message === "invalid profile name" ? 400 : 500;
143
- ctx.body = {
144
- ok: false,
145
- error: {
146
- code: ctx.status === 400 ? "invalid_profile_name" : "internal_error",
147
- message: error instanceof Error ? error.message : "Internal error"
148
- }
149
- };
150
- }
151
- });
152
- router.get("/api/v1/bootstrap", async (ctx) => {
153
- const [identity, config] = await Promise.all([loadIdentity(), loadConfig()]);
154
- ctx.set("cache-control", "no-store");
155
- ctx.body = {
156
- link_id: identity?.link_id ?? null,
157
- display_name: identity?.link_id ? "Hermes Link" : "Unpaired Hermes Link",
158
- version: LINK_VERSION,
159
- api_version: 1,
160
- paired: Boolean(identity?.link_id),
161
- routes: identity?.link_id ? [
162
- {
163
- kind: "lan",
164
- url: `http://127.0.0.1:${config.port}`,
165
- priority: 10,
166
- observed_at: (/* @__PURE__ */ new Date()).toISOString()
167
- }
168
- ] : [],
169
- capabilities: {
170
- runs: true,
171
- sse: true,
172
- relay: true,
173
- profiles: true
174
- }
175
- };
176
- });
177
- router.get("/api/v1/profiles", async (ctx) => {
178
- ctx.set("cache-control", "no-store");
179
- ctx.body = {
180
- ok: true,
181
- profiles: await listHermesProfiles()
182
- };
183
- });
184
- router.get("/api/v1/profiles/:name/status", async (ctx) => {
185
- ctx.set("cache-control", "no-store");
186
- ctx.body = {
187
- ok: true,
188
- profile: await getHermesProfileStatus(ctx.params.name)
189
- };
190
- });
191
- router.post("/api/v1/profiles", async (ctx) => {
192
- const body = await readJsonBody(ctx.req);
193
- const name = readProfileName(body);
194
- ctx.status = 201;
195
- ctx.body = {
196
- ok: true,
197
- profile: await createHermesProfile(name)
198
- };
199
- });
200
- router.post("/api/v1/profiles/:name/use", async (ctx) => {
201
- ctx.body = {
202
- ok: true,
203
- profile: await useHermesProfile(ctx.params.name)
204
- };
205
- });
206
- router.patch("/api/v1/profiles/:name", async (ctx) => {
207
- const body = await readJsonBody(ctx.req);
208
- const name = readProfileName(body);
209
- ctx.body = {
210
- ok: true,
211
- profile: await renameHermesProfile(ctx.params.name, name)
212
- };
213
- });
214
- router.delete("/api/v1/profiles/:name", async (ctx) => {
215
- await deleteHermesProfile(ctx.params.name);
216
- ctx.status = 204;
217
- });
218
- app.use(router.routes());
219
- app.use(router.allowedMethods());
220
- return app;
221
- }
222
- async function readJsonBody(request) {
223
- const chunks = [];
224
- for await (const chunk of request) {
225
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
226
- }
227
- if (chunks.length === 0) {
228
- return {};
229
- }
230
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
231
- }
232
- function readProfileName(body) {
233
- if (typeof body.name !== "string") {
234
- throw new Error("invalid profile name");
235
- }
236
- return body.name;
237
- }
2
+ createApp
3
+ } from "../chunk-4CDHEW3J.js";
238
4
  export {
239
5
  createApp
240
6
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/http/app.ts","../../src/hermes/profiles.ts"],"sourcesContent":["import Koa from 'koa'\nimport Router from '@koa/router'\nimport { LINK_VERSION } from '../constants.js'\nimport { loadConfig } from '../config/config.js'\nimport {\n createHermesProfile,\n deleteHermesProfile,\n getHermesProfileStatus,\n listHermesProfiles,\n renameHermesProfile,\n useHermesProfile,\n} from '../hermes/profiles.js'\nimport { loadIdentity } from '../identity/identity.js'\n\nexport async function createApp(): Promise<Koa> {\n const app = new Koa()\n const router = new Router()\n\n app.use(async (ctx, next) => {\n try {\n await next()\n } catch (error) {\n ctx.status = error instanceof Error && error.message === 'invalid profile name' ? 400 : 500\n ctx.body = {\n ok: false,\n error: {\n code: ctx.status === 400 ? 'invalid_profile_name' : 'internal_error',\n message: error instanceof Error ? error.message : 'Internal error',\n },\n }\n }\n })\n\n router.get('/api/v1/bootstrap', async (ctx) => {\n const [identity, config] = await Promise.all([loadIdentity(), loadConfig()])\n ctx.set('cache-control', 'no-store')\n ctx.body = {\n link_id: identity?.link_id ?? null,\n display_name: identity?.link_id ? 'Hermes Link' : 'Unpaired Hermes Link',\n version: LINK_VERSION,\n api_version: 1,\n paired: Boolean(identity?.link_id),\n routes: identity?.link_id\n ? [\n {\n kind: 'lan',\n url: `http://127.0.0.1:${config.port}`,\n priority: 10,\n observed_at: new Date().toISOString(),\n },\n ]\n : [],\n capabilities: {\n runs: true,\n sse: true,\n relay: true,\n profiles: true,\n },\n }\n })\n\n router.get('/api/v1/profiles', async (ctx) => {\n ctx.set('cache-control', 'no-store')\n ctx.body = {\n ok: true,\n profiles: await listHermesProfiles(),\n }\n })\n\n router.get('/api/v1/profiles/:name/status', async (ctx) => {\n ctx.set('cache-control', 'no-store')\n ctx.body = {\n ok: true,\n profile: await getHermesProfileStatus(ctx.params.name),\n }\n })\n\n router.post('/api/v1/profiles', async (ctx) => {\n const body = await readJsonBody(ctx.req)\n const name = readProfileName(body)\n ctx.status = 201\n ctx.body = {\n ok: true,\n profile: await createHermesProfile(name),\n }\n })\n\n router.post('/api/v1/profiles/:name/use', async (ctx) => {\n ctx.body = {\n ok: true,\n profile: await useHermesProfile(ctx.params.name),\n }\n })\n\n router.patch('/api/v1/profiles/:name', async (ctx) => {\n const body = await readJsonBody(ctx.req)\n const name = readProfileName(body)\n ctx.body = {\n ok: true,\n profile: await renameHermesProfile(ctx.params.name, name),\n }\n })\n\n router.delete('/api/v1/profiles/:name', async (ctx) => {\n await deleteHermesProfile(ctx.params.name)\n ctx.status = 204\n })\n\n app.use(router.routes())\n app.use(router.allowedMethods())\n return app\n}\n\nasync function readJsonBody(request: NodeJS.ReadableStream): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = []\n for await (const chunk of request) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))\n }\n if (chunks.length === 0) {\n return {}\n }\n return JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>\n}\n\nfunction readProfileName(body: Record<string, unknown>): string {\n if (typeof body.name !== 'string') {\n throw new Error('invalid profile name')\n }\n return body.name\n}\n","import { mkdir, readdir, rename, rm, stat } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\nimport { resolveRuntimePaths, type RuntimePaths } from '../runtime/paths.js'\nimport { readJsonFile, writeJsonFile } from '../storage/atomic-json.js'\nimport {\n ensureHermesApiServerKey,\n readHermesApiServerConfig,\n resolveHermesConfigPath,\n resolveHermesProfileDir,\n} from './config.js'\n\nexport interface HermesProfile {\n name: string\n active: boolean\n path: string\n configPath: string\n}\n\ninterface ProfileState {\n activeProfile: string\n}\n\nconst DEFAULT_PROFILE = 'default'\nconst PROFILE_NAME_PATTERN = /^[a-zA-Z0-9._-]{1,64}$/\n\nexport async function listHermesProfiles(paths: RuntimePaths = resolveRuntimePaths()): Promise<HermesProfile[]> {\n const activeProfile = await getActiveProfile(paths)\n const profiles = new Map<string, HermesProfile>()\n profiles.set(DEFAULT_PROFILE, profileInfo(DEFAULT_PROFILE, activeProfile))\n\n const profilesDir = path.join(os.homedir(), '.hermes', 'profiles')\n const entries = await readdir(profilesDir, { withFileTypes: true }).catch((error: unknown) => {\n if (isNodeError(error, 'ENOENT')) {\n return []\n }\n throw error\n })\n for (const entry of entries) {\n if (entry.isDirectory() && PROFILE_NAME_PATTERN.test(entry.name)) {\n profiles.set(entry.name, profileInfo(entry.name, activeProfile))\n }\n }\n\n return [...profiles.values()].sort((left, right) => {\n if (left.name === DEFAULT_PROFILE) {\n return -1\n }\n if (right.name === DEFAULT_PROFILE) {\n return 1\n }\n return left.name.localeCompare(right.name)\n })\n}\n\nexport async function getHermesProfileStatus(\n name: string,\n paths: RuntimePaths = resolveRuntimePaths(),\n): Promise<HermesProfile & { exists: boolean; apiKeyConfigured: boolean }> {\n assertProfileName(name)\n const profile = profileInfo(name, await getActiveProfile(paths))\n const exists = await stat(profile.path)\n .then((value) => value.isDirectory())\n .catch((error: unknown) => {\n if (isNodeError(error, 'ENOENT')) {\n return false\n }\n throw error\n })\n const config = await readHermesApiServerConfig(name, profile.configPath)\n return {\n ...profile,\n exists,\n apiKeyConfigured: Boolean(config.key),\n }\n}\n\nexport async function createHermesProfile(name: string): Promise<HermesProfile> {\n assertProfileName(name)\n if (name === DEFAULT_PROFILE) {\n throw new Error('default profile already exists')\n }\n const profile = profileInfo(name, DEFAULT_PROFILE)\n if (await pathExists(profile.path)) {\n throw new Error('profile already exists')\n }\n await mkdir(profile.path, { recursive: true, mode: 0o700 })\n await ensureHermesApiServerKey(name, profile.configPath)\n return profile\n}\n\nexport async function useHermesProfile(\n name: string,\n paths: RuntimePaths = resolveRuntimePaths(),\n): Promise<HermesProfile> {\n assertProfileName(name)\n const profile = profileInfo(name, name)\n await mkdir(profile.path, { recursive: true, mode: 0o700 })\n const current = (await readJsonFile<Record<string, unknown>>(paths.stateFile)) ?? {}\n await writeJsonFile(paths.stateFile, { ...current, activeProfile: name } satisfies ProfileState)\n return profile\n}\n\nexport async function renameHermesProfile(oldName: string, newName: string): Promise<HermesProfile> {\n assertMutableProfile(oldName)\n assertMutableProfile(newName)\n const oldProfile = profileInfo(oldName, DEFAULT_PROFILE)\n const newProfile = profileInfo(newName, DEFAULT_PROFILE)\n await rename(oldProfile.path, newProfile.path)\n return newProfile\n}\n\nexport async function deleteHermesProfile(name: string): Promise<void> {\n assertMutableProfile(name)\n await rm(resolveHermesProfileDir(name), { recursive: true, force: true })\n}\n\nasync function getActiveProfile(paths: RuntimePaths): Promise<string> {\n const state = await readJsonFile<Partial<ProfileState>>(paths.stateFile)\n return typeof state?.activeProfile === 'string' && PROFILE_NAME_PATTERN.test(state.activeProfile)\n ? state.activeProfile\n : DEFAULT_PROFILE\n}\n\nfunction profileInfo(name: string, activeProfile: string): HermesProfile {\n return {\n name,\n active: name === activeProfile,\n path: resolveHermesProfileDir(name),\n configPath: resolveHermesConfigPath(name),\n }\n}\n\nfunction assertMutableProfile(name: string): void {\n assertProfileName(name)\n if (name === DEFAULT_PROFILE) {\n throw new Error('default profile cannot be renamed or deleted')\n }\n}\n\nfunction assertProfileName(name: string): void {\n if (!PROFILE_NAME_PATTERN.test(name)) {\n throw new Error('invalid profile name')\n }\n}\n\nasync function pathExists(targetPath: string): Promise<boolean> {\n return await stat(targetPath)\n .then(() => true)\n .catch((error: unknown) => {\n if (isNodeError(error, 'ENOENT')) {\n return false\n }\n throw error\n })\n}\n\nfunction isNodeError(error: unknown, code: string): boolean {\n return typeof error === 'object' && error !== null && 'code' in error && error.code === code\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hermespilot/link",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
6
6
  "license": "MIT",
@@ -40,7 +40,7 @@
40
40
  "publish:npm": "npm publish --access public"
41
41
  },
42
42
  "dependencies": {
43
- "@koa/router": "^13.1.0",
43
+ "@koa/router": "^15.4.0",
44
44
  "commander": "^12.1.0",
45
45
  "koa": "^2.15.3",
46
46
  "qrcode-terminal": "^0.12.0",
@@ -50,7 +50,6 @@
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/koa": "^2.15.0",
53
- "@types/koa__router": "^12.0.4",
54
53
  "@types/node": "^22.10.2",
55
54
  "@types/qrcode-terminal": "^0.12.2",
56
55
  "@types/ws": "^8.5.13",
@@ -26,19 +26,53 @@ function compareVersions(left, right) {
26
26
  return 0;
27
27
  }
28
28
 
29
+ function detectLanguage() {
30
+ const candidates = [
31
+ process.env.HERMESLINK_LANG,
32
+ process.env.HERMESLINK_LANGUAGE,
33
+ process.env.LC_ALL,
34
+ process.env.LC_MESSAGES,
35
+ process.env.LANG,
36
+ process.env.LANGUAGE?.split(":")[0],
37
+ Intl.DateTimeFormat().resolvedOptions().locale,
38
+ ];
39
+ for (const candidate of candidates) {
40
+ const normalized = String(candidate ?? "").trim().replace("_", "-").toLowerCase();
41
+ if (normalized.startsWith("zh")) {
42
+ return "zh-CN";
43
+ }
44
+ if (normalized.startsWith("en")) {
45
+ return "en";
46
+ }
47
+ }
48
+ return "en";
49
+ }
50
+
29
51
  const current = parseVersion(process.versions.node);
30
52
  const minimum = parseVersion(MINIMUM_NODE_VERSION);
31
53
 
32
54
  if (compareVersions(current, minimum) < 0) {
55
+ const language = detectLanguage();
33
56
  console.error("");
34
- console.error("Hermes Link needs Node.js 22.14.0 or newer.");
35
- console.error(`You are using Node.js ${process.versions.node}.`);
36
- console.error("");
37
- console.error("Why this is required:");
38
- console.error("- Hermes Link targets the same modern Node.js runtime used by the Link daemon and CLI.");
39
- console.error("- If installation continued on an older Node.js version, pairing or the background service could fail later.");
40
- console.error("");
41
- console.error("Please update Node.js first, then run the install command again.");
57
+ if (language === "zh-CN") {
58
+ console.error("Hermes Link 需要 Node.js 22.14.0 或更新版本。");
59
+ console.error(`当前使用的是 Node.js ${process.versions.node}。`);
60
+ console.error("");
61
+ console.error("为什么需要这样做:");
62
+ console.error("- Hermes Link 使用和 Link daemon / CLI 一致的现代 Node.js runtime。");
63
+ console.error("- 如果继续在旧版 Node.js 上安装,后续配对或后台服务可能会失败。");
64
+ console.error("");
65
+ console.error("请先升级 Node.js,然后重新运行安装命令。");
66
+ } else {
67
+ console.error("Hermes Link needs Node.js 22.14.0 or newer.");
68
+ console.error(`You are using Node.js ${process.versions.node}.`);
69
+ console.error("");
70
+ console.error("Why this is required:");
71
+ console.error("- Hermes Link targets the same modern Node.js runtime used by the Link daemon and CLI.");
72
+ console.error("- If installation continued on an older Node.js version, pairing or the background service could fail later.");
73
+ console.error("");
74
+ console.error("Please update Node.js first, then run the install command again.");
75
+ }
42
76
  console.error("");
43
77
  process.exit(1);
44
78
  }
@@ -14,14 +14,42 @@ function shouldPrintInstallHint() {
14
14
  return process.env.npm_config_global === "true";
15
15
  }
16
16
 
17
+ function detectLanguage() {
18
+ const candidates = [
19
+ process.env.HERMESLINK_LANG,
20
+ process.env.HERMESLINK_LANGUAGE,
21
+ process.env.LC_ALL,
22
+ process.env.LC_MESSAGES,
23
+ process.env.LANG,
24
+ process.env.LANGUAGE?.split(":")[0],
25
+ Intl.DateTimeFormat().resolvedOptions().locale,
26
+ ];
27
+ for (const candidate of candidates) {
28
+ const normalized = String(candidate ?? "").trim().replace("_", "-").toLowerCase();
29
+ if (normalized.startsWith("zh")) {
30
+ return "zh-CN";
31
+ }
32
+ if (normalized.startsWith("en")) {
33
+ return "en";
34
+ }
35
+ }
36
+ return "en";
37
+ }
38
+
17
39
  async function main() {
18
40
  if (!shouldPrintInstallHint()) {
19
41
  return;
20
42
  }
21
43
 
44
+ const language = detectLanguage();
22
45
  console.log("");
23
- console.log("Hermes Link installed.");
24
- console.log("Run `hermeslink pair` to connect this computer with HermesPilot App.");
46
+ if (language === "zh-CN") {
47
+ console.log("Hermes Link 已安装。");
48
+ console.log("运行 `hermeslink pair`,把这台电脑连接到 HermesPilot App。");
49
+ } else {
50
+ console.log("Hermes Link installed.");
51
+ console.log("Run `hermeslink pair` to connect this computer with HermesPilot App.");
52
+ }
25
53
  console.log("");
26
54
  }
27
55
 
@@ -1,255 +0,0 @@
1
- // src/constants.ts
2
- var LINK_VERSION = "0.1.0";
3
- var LINK_COMMAND = "hermeslink";
4
- var LINK_DEFAULT_PORT = 52379;
5
- var LINK_RUNTIME_DIR_NAME = ".hermeslink";
6
-
7
- // src/runtime/paths.ts
8
- import os from "os";
9
- import path from "path";
10
- function resolveRuntimeHome() {
11
- return process.env.HERMESLINK_HOME?.trim() ? path.resolve(process.env.HERMESLINK_HOME) : path.join(os.homedir(), LINK_RUNTIME_DIR_NAME);
12
- }
13
- function resolveRuntimePaths(homeDir = resolveRuntimeHome()) {
14
- return {
15
- homeDir,
16
- identityFile: path.join(homeDir, "identity.json"),
17
- configFile: path.join(homeDir, "config.json"),
18
- stateFile: path.join(homeDir, "state.json"),
19
- credentialsFile: path.join(homeDir, "credentials.json"),
20
- databaseFile: path.join(homeDir, "link.db"),
21
- logsDir: path.join(homeDir, "logs"),
22
- runDir: path.join(homeDir, "run"),
23
- pairingDir: path.join(homeDir, "pairing")
24
- };
25
- }
26
-
27
- // src/storage/atomic-json.ts
28
- import { mkdir, open, readFile, rename, rm } from "fs/promises";
29
- import path2 from "path";
30
- async function readJsonFile(filePath) {
31
- try {
32
- const raw = await readFile(filePath, "utf8");
33
- return JSON.parse(raw);
34
- } catch (error) {
35
- if (isNodeError(error, "ENOENT")) {
36
- return null;
37
- }
38
- throw error;
39
- }
40
- }
41
- async function writeJsonFile(filePath, value, mode = 384) {
42
- await mkdir(path2.dirname(filePath), { recursive: true, mode: 448 });
43
- const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
44
- const payload = `${JSON.stringify(value, null, 2)}
45
- `;
46
- const handle = await open(tmpPath, "w", mode);
47
- try {
48
- await handle.writeFile(payload, "utf8");
49
- await handle.sync();
50
- } finally {
51
- await handle.close();
52
- }
53
- try {
54
- await rename(tmpPath, filePath);
55
- } catch (error) {
56
- await rm(tmpPath, { force: true });
57
- throw error;
58
- }
59
- }
60
- function isNodeError(error, code) {
61
- return typeof error === "object" && error !== null && "code" in error && error.code === code;
62
- }
63
-
64
- // src/config/config.ts
65
- var defaultLinkConfig = {
66
- port: LINK_DEFAULT_PORT,
67
- serverBaseUrl: "http://127.0.0.1:8787",
68
- relayBaseUrl: "http://127.0.0.1:8788",
69
- language: "zh-CN"
70
- };
71
- async function loadConfig(paths = resolveRuntimePaths()) {
72
- const existing = await readJsonFile(paths.configFile);
73
- return {
74
- ...defaultLinkConfig,
75
- ...existing ?? {}
76
- };
77
- }
78
-
79
- // src/hermes/config.ts
80
- import { randomBytes } from "crypto";
81
- import { copyFile, mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
82
- import os2 from "os";
83
- import path3 from "path";
84
- import YAML from "yaml";
85
- function resolveHermesProfileDir(profileName = "default") {
86
- if (profileName === "default") {
87
- return path3.join(os2.homedir(), ".hermes");
88
- }
89
- return path3.join(os2.homedir(), ".hermes", "profiles", profileName);
90
- }
91
- function resolveHermesConfigPath(profileName = "default") {
92
- return path3.join(resolveHermesProfileDir(profileName), "config.yaml");
93
- }
94
- async function readHermesApiServerConfig(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
95
- const existingRaw = await readFile2(configPath, "utf8").catch((error) => {
96
- if (isNodeError2(error, "ENOENT")) {
97
- return null;
98
- }
99
- throw error;
100
- });
101
- if (!existingRaw) {
102
- return {};
103
- }
104
- const config = toRecord(YAML.parse(existingRaw));
105
- const platforms = toRecord(config.platforms);
106
- const apiServer = toRecord(platforms.api_server);
107
- return readApiServerConfig(toRecord(apiServer.extra));
108
- }
109
- async function ensureHermesApiServerKey(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
110
- const existingRaw = await readFile2(configPath, "utf8").catch((error) => {
111
- if (isNodeError2(error, "ENOENT")) {
112
- return null;
113
- }
114
- throw error;
115
- });
116
- const document = existingRaw ? YAML.parseDocument(existingRaw) : new YAML.Document({});
117
- const config = toRecord(document.toJSON());
118
- const platforms = ensureRecord(config, "platforms");
119
- const apiServer = ensureRecord(platforms, "api_server");
120
- const extra = ensureRecord(apiServer, "extra");
121
- const beforeKey = typeof extra.key === "string" && extra.key.length > 0 ? extra.key : null;
122
- let changed = false;
123
- if (!beforeKey) {
124
- extra.key = randomBytes(32).toString("base64url");
125
- changed = true;
126
- }
127
- if (!changed) {
128
- return {
129
- configPath,
130
- apiServer: readApiServerConfig(extra),
131
- changed: false,
132
- keyAdded: false,
133
- backupPath: null,
134
- notice: null
135
- };
136
- }
137
- const backupPath = existingRaw ? `${configPath}.bak.${Date.now()}` : null;
138
- await mkdir2(path3.dirname(configPath), { recursive: true, mode: 448 });
139
- if (backupPath) {
140
- await copyFile(configPath, backupPath);
141
- }
142
- document.contents = document.createNode(config);
143
- await writeFile(configPath, document.toString(), { mode: 384 });
144
- return {
145
- configPath,
146
- apiServer: readApiServerConfig(extra),
147
- changed: true,
148
- keyAdded: true,
149
- backupPath,
150
- notice: "\u5DF2\u4E3A Hermes API Server \u81EA\u52A8\u8865\u5145 key\uFF1B\u672A\u8986\u76D6\u5DF2\u6709 port/host/key\u3002"
151
- };
152
- }
153
- function readApiServerConfig(extra) {
154
- return {
155
- host: typeof extra.host === "string" ? extra.host : void 0,
156
- port: typeof extra.port === "number" ? extra.port : void 0,
157
- key: typeof extra.key === "string" ? extra.key : void 0
158
- };
159
- }
160
- function toRecord(value) {
161
- return typeof value === "object" && value !== null ? value : {};
162
- }
163
- function ensureRecord(parent, key) {
164
- const value = parent[key];
165
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
166
- return value;
167
- }
168
- const next = {};
169
- parent[key] = next;
170
- return next;
171
- }
172
- function isNodeError2(error, code) {
173
- return typeof error === "object" && error !== null && "code" in error && error.code === code;
174
- }
175
-
176
- // src/identity/identity.ts
177
- import { generateKeyPairSync, randomUUID, sign } from "crypto";
178
- import { mkdir as mkdir3, chmod } from "fs/promises";
179
- import { z } from "zod";
180
- var linkIdentitySchema = z.object({
181
- install_id: z.string().min(1),
182
- link_id: z.string().min(1).nullable().optional(),
183
- public_key_pem: z.string().min(1),
184
- private_key_pem: z.string().min(1),
185
- created_at: z.string().min(1),
186
- updated_at: z.string().min(1)
187
- });
188
- async function loadIdentity(paths = resolveRuntimePaths()) {
189
- const value = await readJsonFile(paths.identityFile);
190
- if (value === null) {
191
- return null;
192
- }
193
- return linkIdentitySchema.parse(value);
194
- }
195
- async function ensureIdentity(paths = resolveRuntimePaths()) {
196
- const existing = await loadIdentity(paths);
197
- if (existing) {
198
- return existing;
199
- }
200
- await mkdir3(paths.homeDir, { recursive: true, mode: 448 });
201
- await chmod(paths.homeDir, 448).catch(() => void 0);
202
- const { publicKey, privateKey } = generateKeyPairSync("ed25519");
203
- const now = (/* @__PURE__ */ new Date()).toISOString();
204
- const identity = {
205
- install_id: `install_${randomUUID().replaceAll("-", "")}`,
206
- link_id: null,
207
- public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
208
- private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
209
- created_at: now,
210
- updated_at: now
211
- };
212
- await writeJsonFile(paths.identityFile, identity);
213
- return identity;
214
- }
215
- async function saveAssignedLinkId(linkId, paths = resolveRuntimePaths()) {
216
- const identity = await ensureIdentity(paths);
217
- const next = {
218
- ...identity,
219
- link_id: linkId,
220
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
221
- };
222
- await writeJsonFile(paths.identityFile, next);
223
- return next;
224
- }
225
- function signRelayNonce(identity, nonce) {
226
- const signature = sign(null, Buffer.from(nonce, "utf8"), identity.private_key_pem);
227
- return signature.toString("base64url");
228
- }
229
- function getIdentityStatus(identity) {
230
- return {
231
- installId: identity.install_id,
232
- linkId: identity.link_id ?? null,
233
- hasPrivateKey: identity.private_key_pem.trim().length > 0,
234
- publicKeyPem: identity.public_key_pem
235
- };
236
- }
237
-
238
- export {
239
- LINK_VERSION,
240
- LINK_COMMAND,
241
- resolveRuntimePaths,
242
- readJsonFile,
243
- writeJsonFile,
244
- loadConfig,
245
- resolveHermesProfileDir,
246
- resolveHermesConfigPath,
247
- readHermesApiServerConfig,
248
- ensureHermesApiServerKey,
249
- loadIdentity,
250
- ensureIdentity,
251
- saveAssignedLinkId,
252
- signRelayNonce,
253
- getIdentityStatus
254
- };
255
- //# sourceMappingURL=chunk-YTX3DQGX.js.map