@nordbyte/nordrelay 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +45 -2
- package/README.md +204 -30
- package/dist/agent-activity.js +300 -0
- package/dist/agent-adapter.js +17 -30
- package/dist/agent-factory.js +27 -0
- package/dist/agent.js +123 -9
- package/dist/artifacts.js +1 -1
- package/dist/audit-log.js +1 -1
- package/dist/bot-ui.js +1 -1
- package/dist/bot.js +328 -159
- package/dist/claude-code-auth.js +121 -0
- package/dist/claude-code-cli.js +19 -0
- package/dist/claude-code-launch.js +73 -0
- package/dist/claude-code-session.js +660 -0
- package/dist/claude-code-state.js +590 -0
- package/dist/codex-session.js +12 -1
- package/dist/config.js +113 -9
- package/dist/hermes-api.js +150 -0
- package/dist/hermes-auth.js +96 -0
- package/dist/hermes-cli.js +19 -0
- package/dist/hermes-launch.js +57 -0
- package/dist/hermes-session.js +477 -0
- package/dist/hermes-state.js +609 -0
- package/dist/index.js +51 -8
- package/dist/openclaw-auth.js +27 -0
- package/dist/openclaw-cli.js +19 -0
- package/dist/openclaw-gateway.js +285 -0
- package/dist/openclaw-launch.js +65 -0
- package/dist/openclaw-session.js +549 -0
- package/dist/openclaw-state.js +409 -0
- package/dist/operations.js +83 -2
- package/dist/pi-auth.js +59 -0
- package/dist/pi-launch.js +61 -0
- package/dist/pi-rpc.js +18 -0
- package/dist/pi-session.js +103 -15
- package/dist/pi-state.js +253 -0
- package/dist/relay-runtime.js +673 -51
- package/dist/session-format.js +28 -18
- package/dist/session-registry.js +40 -15
- package/dist/settings-service.js +35 -4
- package/dist/web-dashboard-ui.js +18 -0
- package/dist/web-dashboard.js +329 -47
- package/package.json +8 -3
- package/plugins/nordrelay/.codex-plugin/plugin.json +7 -4
- package/plugins/nordrelay/commands/remote.md +2 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +131 -3
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
- package/CHANGELOG.md +0 -26
package/dist/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createBuiltinLaunchProfiles, createDefaultLaunchProfile, findLaunchProfile, isCodexApprovalPolicy, isCodexSandboxMode, parseLaunchProfilesJson, } from "./codex-launch.js";
|
|
4
|
-
import { isAgentId, PI_THINKING_LEVELS } from "./agent.js";
|
|
4
|
+
import { CLAUDE_CODE_EFFORT_LEVELS, HERMES_REASONING_EFFORTS, OPENCLAW_THINKING_LEVELS, isAgentId, PI_THINKING_LEVELS, } from "./agent.js";
|
|
5
5
|
import { parseRolePoliciesJson, } from "./access-control.js";
|
|
6
6
|
import { parseMirrorMode, parseNotifyMode, parseQuietHours, parseVoiceBackendPreference, } from "./bot-preferences.js";
|
|
7
7
|
export function loadConfig() {
|
|
@@ -53,12 +53,40 @@ export function loadConfig() {
|
|
|
53
53
|
const launchProfiles = parseLaunchProfiles(optionalString(process.env.CODEX_LAUNCH_PROFILES_JSON), codexSandboxMode, codexApprovalPolicy, enableUnsafeLaunchProfiles);
|
|
54
54
|
const defaultLaunchProfileId = parseDefaultLaunchProfileId(optionalString(process.env.CODEX_DEFAULT_LAUNCH_PROFILE), launchProfiles);
|
|
55
55
|
const piEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_PI_ENABLED), false);
|
|
56
|
-
ensureAtLeastOneAgentEnabled(codexEnabled, piEnabled);
|
|
57
56
|
const piCliPath = optionalString(process.env.PI_CLI_PATH);
|
|
58
57
|
const piSessionDir = optionalString(process.env.PI_SESSION_DIR);
|
|
59
58
|
const piDefaultModel = optionalString(process.env.PI_DEFAULT_MODEL);
|
|
60
59
|
const piDefaultThinking = parsePiThinkingLevel(optionalString(process.env.PI_DEFAULT_THINKING));
|
|
61
|
-
const
|
|
60
|
+
const piDefaultLaunchProfileId = optionalString(process.env.PI_DEFAULT_PROFILE) ?? "default";
|
|
61
|
+
const hermesEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_HERMES_ENABLED), false);
|
|
62
|
+
const hermesCliPath = optionalString(process.env.HERMES_CLI_PATH);
|
|
63
|
+
const hermesHome = optionalString(process.env.HERMES_HOME);
|
|
64
|
+
const hermesStateDbPath = optionalString(process.env.HERMES_STATE_DB_PATH);
|
|
65
|
+
const hermesApiBaseUrl = optionalString(process.env.HERMES_API_BASE_URL) ?? "http://127.0.0.1:8642";
|
|
66
|
+
const hermesApiKey = optionalString(process.env.HERMES_API_KEY);
|
|
67
|
+
const hermesDefaultModel = optionalString(process.env.HERMES_DEFAULT_MODEL);
|
|
68
|
+
const hermesDefaultReasoning = parseHermesReasoningEffort(optionalString(process.env.HERMES_DEFAULT_REASONING));
|
|
69
|
+
const hermesDefaultLaunchProfileId = optionalString(process.env.HERMES_DEFAULT_PROFILE) ?? "default";
|
|
70
|
+
const openClawEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_OPENCLAW_ENABLED), false);
|
|
71
|
+
const openClawCliPath = optionalString(process.env.OPENCLAW_CLI_PATH);
|
|
72
|
+
const openClawGatewayUrl = optionalString(process.env.OPENCLAW_GATEWAY_URL) ?? "ws://127.0.0.1:18789";
|
|
73
|
+
const openClawGatewayToken = optionalString(process.env.OPENCLAW_GATEWAY_TOKEN);
|
|
74
|
+
const openClawGatewayPassword = optionalString(process.env.OPENCLAW_GATEWAY_PASSWORD);
|
|
75
|
+
const openClawAgentId = optionalString(process.env.OPENCLAW_AGENT_ID) ?? "main";
|
|
76
|
+
const openClawHome = optionalString(process.env.OPENCLAW_HOME);
|
|
77
|
+
const openClawStateDir = optionalString(process.env.OPENCLAW_STATE_DIR);
|
|
78
|
+
const openClawDefaultModel = optionalString(process.env.OPENCLAW_DEFAULT_MODEL);
|
|
79
|
+
const openClawDefaultThinking = parseOpenClawThinkingLevel(optionalString(process.env.OPENCLAW_DEFAULT_THINKING));
|
|
80
|
+
const openClawDefaultLaunchProfileId = optionalString(process.env.OPENCLAW_DEFAULT_PROFILE) ?? "default";
|
|
81
|
+
const claudeCodeEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_CLAUDE_CODE_ENABLED), false);
|
|
82
|
+
ensureAtLeastOneAgentEnabled(codexEnabled, piEnabled, hermesEnabled, openClawEnabled, claudeCodeEnabled);
|
|
83
|
+
const claudeCodeCliPath = optionalString(process.env.CLAUDE_CODE_CLI_PATH);
|
|
84
|
+
const claudeCodeConfigDir = optionalString(process.env.CLAUDE_CONFIG_DIR);
|
|
85
|
+
const claudeCodeDefaultModel = optionalString(process.env.CLAUDE_CODE_DEFAULT_MODEL);
|
|
86
|
+
const claudeCodeDefaultEffort = parseClaudeCodeEffort(optionalString(process.env.CLAUDE_CODE_DEFAULT_EFFORT));
|
|
87
|
+
const claudeCodeDefaultLaunchProfileId = optionalString(process.env.CLAUDE_CODE_DEFAULT_PROFILE) ?? "default";
|
|
88
|
+
const claudeCodeMaxTurns = parsePositiveIntegerEnv(optionalString(process.env.CLAUDE_CODE_MAX_TURNS), 100, "CLAUDE_CODE_MAX_TURNS");
|
|
89
|
+
const defaultAgent = parseDefaultAgent(optionalString(process.env.NORDRELAY_DEFAULT_AGENT), codexEnabled, piEnabled, hermesEnabled, openClawEnabled, claudeCodeEnabled);
|
|
62
90
|
const toolVerbosity = parseToolVerbosity(optionalString(process.env.TOOL_VERBOSITY));
|
|
63
91
|
const logFormat = parseLogFormat(optionalString(process.env.CONNECTOR_LOG_FORMAT));
|
|
64
92
|
const showTurnTokenUsage = parseBooleanEnv(optionalString(process.env.SHOW_TURN_TOKEN_USAGE), false);
|
|
@@ -124,6 +152,34 @@ export function loadConfig() {
|
|
|
124
152
|
piSessionDir,
|
|
125
153
|
piDefaultModel,
|
|
126
154
|
piDefaultThinking,
|
|
155
|
+
piDefaultLaunchProfileId,
|
|
156
|
+
hermesEnabled,
|
|
157
|
+
hermesCliPath,
|
|
158
|
+
hermesHome,
|
|
159
|
+
hermesStateDbPath,
|
|
160
|
+
hermesApiBaseUrl,
|
|
161
|
+
hermesApiKey,
|
|
162
|
+
hermesDefaultModel,
|
|
163
|
+
hermesDefaultReasoning,
|
|
164
|
+
hermesDefaultLaunchProfileId,
|
|
165
|
+
openClawEnabled,
|
|
166
|
+
openClawCliPath,
|
|
167
|
+
openClawGatewayUrl,
|
|
168
|
+
openClawGatewayToken,
|
|
169
|
+
openClawGatewayPassword,
|
|
170
|
+
openClawAgentId,
|
|
171
|
+
openClawHome,
|
|
172
|
+
openClawStateDir,
|
|
173
|
+
openClawDefaultModel,
|
|
174
|
+
openClawDefaultThinking,
|
|
175
|
+
openClawDefaultLaunchProfileId,
|
|
176
|
+
claudeCodeEnabled,
|
|
177
|
+
claudeCodeCliPath,
|
|
178
|
+
claudeCodeConfigDir,
|
|
179
|
+
claudeCodeDefaultModel,
|
|
180
|
+
claudeCodeDefaultEffort,
|
|
181
|
+
claudeCodeDefaultLaunchProfileId,
|
|
182
|
+
claudeCodeMaxTurns,
|
|
127
183
|
defaultAgent,
|
|
128
184
|
toolVerbosity,
|
|
129
185
|
logFormat,
|
|
@@ -400,17 +456,25 @@ function parseDefaultLaunchProfileId(raw, launchProfiles) {
|
|
|
400
456
|
}
|
|
401
457
|
return profile.id;
|
|
402
458
|
}
|
|
403
|
-
function ensureAtLeastOneAgentEnabled(codexEnabled, piEnabled) {
|
|
404
|
-
if (!codexEnabled && !piEnabled) {
|
|
405
|
-
throw new Error("At least one agent must be enabled: set NORDRELAY_CODEX_ENABLED=true or
|
|
459
|
+
function ensureAtLeastOneAgentEnabled(codexEnabled, piEnabled, hermesEnabled = false, openClawEnabled = false, claudeCodeEnabled = false) {
|
|
460
|
+
if (!codexEnabled && !piEnabled && !hermesEnabled && !openClawEnabled && !claudeCodeEnabled) {
|
|
461
|
+
throw new Error("At least one agent must be enabled: set NORDRELAY_CODEX_ENABLED=true, NORDRELAY_PI_ENABLED=true, NORDRELAY_HERMES_ENABLED=true, NORDRELAY_OPENCLAW_ENABLED=true, or NORDRELAY_CLAUDE_CODE_ENABLED=true");
|
|
406
462
|
}
|
|
407
463
|
}
|
|
408
|
-
function parseDefaultAgent(raw, codexEnabled, piEnabled) {
|
|
464
|
+
function parseDefaultAgent(raw, codexEnabled, piEnabled, hermesEnabled, openClawEnabled, claudeCodeEnabled) {
|
|
409
465
|
if (!raw) {
|
|
410
|
-
|
|
466
|
+
if (codexEnabled)
|
|
467
|
+
return "codex";
|
|
468
|
+
if (piEnabled)
|
|
469
|
+
return "pi";
|
|
470
|
+
if (hermesEnabled)
|
|
471
|
+
return "hermes";
|
|
472
|
+
if (openClawEnabled)
|
|
473
|
+
return "openclaw";
|
|
474
|
+
return "claude-code";
|
|
411
475
|
}
|
|
412
476
|
if (!isAgentId(raw)) {
|
|
413
|
-
throw new Error(`Invalid NORDRELAY_DEFAULT_AGENT: ${raw}. Expected codex or
|
|
477
|
+
throw new Error(`Invalid NORDRELAY_DEFAULT_AGENT: ${raw}. Expected codex, pi, hermes, openclaw, or claude-code`);
|
|
414
478
|
}
|
|
415
479
|
if (raw === "codex" && !codexEnabled) {
|
|
416
480
|
throw new Error("NORDRELAY_DEFAULT_AGENT=codex requires NORDRELAY_CODEX_ENABLED=true");
|
|
@@ -418,6 +482,15 @@ function parseDefaultAgent(raw, codexEnabled, piEnabled) {
|
|
|
418
482
|
if (raw === "pi" && !piEnabled) {
|
|
419
483
|
throw new Error("NORDRELAY_DEFAULT_AGENT=pi requires NORDRELAY_PI_ENABLED=true");
|
|
420
484
|
}
|
|
485
|
+
if (raw === "hermes" && !hermesEnabled) {
|
|
486
|
+
throw new Error("NORDRELAY_DEFAULT_AGENT=hermes requires NORDRELAY_HERMES_ENABLED=true");
|
|
487
|
+
}
|
|
488
|
+
if (raw === "openclaw" && !openClawEnabled) {
|
|
489
|
+
throw new Error("NORDRELAY_DEFAULT_AGENT=openclaw requires NORDRELAY_OPENCLAW_ENABLED=true");
|
|
490
|
+
}
|
|
491
|
+
if (raw === "claude-code" && !claudeCodeEnabled) {
|
|
492
|
+
throw new Error("NORDRELAY_DEFAULT_AGENT=claude-code requires NORDRELAY_CLAUDE_CODE_ENABLED=true");
|
|
493
|
+
}
|
|
421
494
|
return raw;
|
|
422
495
|
}
|
|
423
496
|
function parsePiThinkingLevel(raw) {
|
|
@@ -430,3 +503,34 @@ function parsePiThinkingLevel(raw) {
|
|
|
430
503
|
console.warn(`Invalid PI_DEFAULT_THINKING value: "${raw}". Expected one of: ${PI_THINKING_LEVELS.join(", ")}. Falling back to "medium".`);
|
|
431
504
|
return "medium";
|
|
432
505
|
}
|
|
506
|
+
function parseHermesReasoningEffort(raw) {
|
|
507
|
+
if (!raw) {
|
|
508
|
+
return undefined;
|
|
509
|
+
}
|
|
510
|
+
const normalized = raw === "off" ? "none" : raw;
|
|
511
|
+
if (HERMES_REASONING_EFFORTS.includes(normalized)) {
|
|
512
|
+
return normalized;
|
|
513
|
+
}
|
|
514
|
+
console.warn(`Invalid HERMES_DEFAULT_REASONING value: "${raw}". Expected one of: ${HERMES_REASONING_EFFORTS.join(", ")}. Falling back to model default.`);
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
function parseOpenClawThinkingLevel(raw) {
|
|
518
|
+
if (!raw) {
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
if (OPENCLAW_THINKING_LEVELS.includes(raw)) {
|
|
522
|
+
return raw;
|
|
523
|
+
}
|
|
524
|
+
console.warn(`Invalid OPENCLAW_DEFAULT_THINKING value: "${raw}". Expected one of: ${OPENCLAW_THINKING_LEVELS.join(", ")}. Falling back to OpenClaw default.`);
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
function parseClaudeCodeEffort(raw) {
|
|
528
|
+
if (!raw) {
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
if (CLAUDE_CODE_EFFORT_LEVELS.includes(raw)) {
|
|
532
|
+
return raw;
|
|
533
|
+
}
|
|
534
|
+
console.warn(`Invalid CLAUDE_CODE_DEFAULT_EFFORT value: "${raw}". Expected one of: ${CLAUDE_CODE_EFFORT_LEVELS.join(", ")}. Falling back to Claude Code default.`);
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
export class HermesApiClient {
|
|
2
|
+
options;
|
|
3
|
+
fetchImpl;
|
|
4
|
+
baseUrl;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
8
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
9
|
+
}
|
|
10
|
+
async health() {
|
|
11
|
+
return this.getJson("/health");
|
|
12
|
+
}
|
|
13
|
+
async detailedHealth() {
|
|
14
|
+
return this.getJson("/health/detailed");
|
|
15
|
+
}
|
|
16
|
+
async capabilities() {
|
|
17
|
+
return this.getJson("/v1/capabilities", true);
|
|
18
|
+
}
|
|
19
|
+
async models() {
|
|
20
|
+
const payload = await this.getJson("/v1/models", true);
|
|
21
|
+
return (payload.data ?? [])
|
|
22
|
+
.map((model) => ({
|
|
23
|
+
id: typeof model.id === "string" ? model.id : "",
|
|
24
|
+
ownedBy: typeof model.owned_by === "string" ? model.owned_by : undefined,
|
|
25
|
+
}))
|
|
26
|
+
.filter((model) => model.id);
|
|
27
|
+
}
|
|
28
|
+
async startRun(request, sessionKey) {
|
|
29
|
+
return this.postJson("/v1/runs", request, { sessionKey, expectStatus: [202] });
|
|
30
|
+
}
|
|
31
|
+
async getRun(runId) {
|
|
32
|
+
return this.getJson(`/v1/runs/${encodeURIComponent(runId)}`, true);
|
|
33
|
+
}
|
|
34
|
+
async stopRun(runId) {
|
|
35
|
+
await this.postJson(`/v1/runs/${encodeURIComponent(runId)}/stop`, {}, { expectStatus: [200, 202, 404] });
|
|
36
|
+
}
|
|
37
|
+
async approveRun(runId, choice) {
|
|
38
|
+
await this.postJson(`/v1/runs/${encodeURIComponent(runId)}/approval`, { choice }, { expectStatus: [200, 409, 404] });
|
|
39
|
+
}
|
|
40
|
+
async streamRunEvents(runId, onEvent, signal) {
|
|
41
|
+
const response = await this.fetchImpl(`${this.baseUrl}/v1/runs/${encodeURIComponent(runId)}/events`, {
|
|
42
|
+
method: "GET",
|
|
43
|
+
headers: this.headers(true),
|
|
44
|
+
signal,
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(await this.formatHttpError(response));
|
|
48
|
+
}
|
|
49
|
+
if (!response.body) {
|
|
50
|
+
throw new Error("Hermes run event stream is empty");
|
|
51
|
+
}
|
|
52
|
+
const reader = response.body.getReader();
|
|
53
|
+
const decoder = new TextDecoder();
|
|
54
|
+
let buffer = "";
|
|
55
|
+
try {
|
|
56
|
+
while (true) {
|
|
57
|
+
const { done, value } = await reader.read();
|
|
58
|
+
if (done) {
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
buffer += decoder.decode(value, { stream: true });
|
|
62
|
+
const parts = buffer.split(/\n\n/);
|
|
63
|
+
buffer = parts.pop() ?? "";
|
|
64
|
+
for (const part of parts) {
|
|
65
|
+
const event = parseSseEvent(part);
|
|
66
|
+
if (event) {
|
|
67
|
+
onEvent(event);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
buffer += decoder.decode();
|
|
72
|
+
const trailing = parseSseEvent(buffer);
|
|
73
|
+
if (trailing) {
|
|
74
|
+
onEvent(trailing);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
reader.releaseLock();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async getJson(path, authenticated = false) {
|
|
82
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
83
|
+
method: "GET",
|
|
84
|
+
headers: this.headers(authenticated),
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
throw new Error(await this.formatHttpError(response));
|
|
88
|
+
}
|
|
89
|
+
return response.json();
|
|
90
|
+
}
|
|
91
|
+
async postJson(path, payload, options = {}) {
|
|
92
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: this.headers(true, options.sessionKey),
|
|
95
|
+
body: JSON.stringify(payload),
|
|
96
|
+
});
|
|
97
|
+
const expected = options.expectStatus ?? [200];
|
|
98
|
+
if (!expected.includes(response.status)) {
|
|
99
|
+
throw new Error(await this.formatHttpError(response));
|
|
100
|
+
}
|
|
101
|
+
const text = await response.text();
|
|
102
|
+
return text.trim() ? JSON.parse(text) : {};
|
|
103
|
+
}
|
|
104
|
+
headers(authenticated, sessionKey) {
|
|
105
|
+
const headers = {
|
|
106
|
+
accept: "application/json",
|
|
107
|
+
"content-type": "application/json",
|
|
108
|
+
};
|
|
109
|
+
if (authenticated && this.options.apiKey) {
|
|
110
|
+
headers.authorization = `Bearer ${this.options.apiKey}`;
|
|
111
|
+
}
|
|
112
|
+
if (sessionKey) {
|
|
113
|
+
headers["x-hermes-session-key"] = sessionKey;
|
|
114
|
+
}
|
|
115
|
+
return headers;
|
|
116
|
+
}
|
|
117
|
+
async formatHttpError(response) {
|
|
118
|
+
const text = await response.text().catch(() => "");
|
|
119
|
+
if (!text.trim()) {
|
|
120
|
+
return `Hermes API request failed: HTTP ${response.status}`;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(text);
|
|
124
|
+
const message = typeof parsed.error?.message === "string" ? parsed.error.message : text;
|
|
125
|
+
const code = typeof parsed.error?.code === "string" ? ` (${parsed.error.code})` : "";
|
|
126
|
+
return `Hermes API request failed: ${message}${code}`;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return `Hermes API request failed: ${text}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function parseSseEvent(raw) {
|
|
134
|
+
const data = raw
|
|
135
|
+
.split(/\r?\n/)
|
|
136
|
+
.filter((line) => line.startsWith("data:"))
|
|
137
|
+
.map((line) => line.slice(5).trimStart())
|
|
138
|
+
.join("\n")
|
|
139
|
+
.trim();
|
|
140
|
+
if (!data) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(data);
|
|
145
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { HermesApiClient } from "./hermes-api.js";
|
|
3
|
+
const COMMAND_TIMEOUT_MS = 10_000;
|
|
4
|
+
const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
5
|
+
export async function checkHermesAuthStatus(options) {
|
|
6
|
+
const client = new HermesApiClient(options);
|
|
7
|
+
try {
|
|
8
|
+
await client.capabilities();
|
|
9
|
+
return {
|
|
10
|
+
authenticated: true,
|
|
11
|
+
method: options.apiKey ? "api-key" : "local-api",
|
|
12
|
+
detail: `Hermes API server reachable at ${options.baseUrl}`,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17
|
+
return {
|
|
18
|
+
authenticated: false,
|
|
19
|
+
method: options.apiKey ? "api-key" : "local-api",
|
|
20
|
+
detail: message,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function startHermesLogin(cliPath) {
|
|
25
|
+
try {
|
|
26
|
+
const { stdout, stderr } = await runHermesCommand(cliPath ?? "hermes", ["login", "--no-browser"], LOGIN_TIMEOUT_MS);
|
|
27
|
+
const output = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
message: output || "Hermes login completed.",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
message: extractCommandError(error) || "Hermes login failed. Run 'hermes login --no-browser' on the host.",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function startHermesLogout(cliPath) {
|
|
41
|
+
try {
|
|
42
|
+
const { stdout, stderr } = await runHermesCommand(cliPath ?? "hermes", ["logout"], COMMAND_TIMEOUT_MS);
|
|
43
|
+
const output = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
44
|
+
return {
|
|
45
|
+
success: true,
|
|
46
|
+
message: output || "Logged out from Hermes.",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
message: extractCommandError(error) || "Hermes logout failed. Run 'hermes logout' on the host.",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function runHermesCommand(command, args, timeout) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
execFile(command, args, {
|
|
59
|
+
encoding: "utf8",
|
|
60
|
+
timeout,
|
|
61
|
+
windowsHide: true,
|
|
62
|
+
env: { ...process.env },
|
|
63
|
+
maxBuffer: 1024 * 1024,
|
|
64
|
+
}, (error, stdout, stderr) => {
|
|
65
|
+
if (error) {
|
|
66
|
+
const enriched = error;
|
|
67
|
+
enriched.stdout = typeof stdout === "string" ? stdout : "";
|
|
68
|
+
enriched.stderr = typeof stderr === "string" ? stderr : "";
|
|
69
|
+
reject(enriched);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
resolve({
|
|
73
|
+
stdout: typeof stdout === "string" ? stdout : "",
|
|
74
|
+
stderr: typeof stderr === "string" ? stderr : "",
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function extractCommandError(error) {
|
|
80
|
+
if (typeof error === "object" && error !== null) {
|
|
81
|
+
const enriched = error;
|
|
82
|
+
const stderr = enriched.stderr?.trim();
|
|
83
|
+
if (stderr)
|
|
84
|
+
return stderr;
|
|
85
|
+
const stdout = enriched.stdout?.trim();
|
|
86
|
+
if (stdout)
|
|
87
|
+
return stdout;
|
|
88
|
+
if (enriched.code === "ENOENT")
|
|
89
|
+
return "Hermes CLI not found. Install Hermes or set HERMES_CLI_PATH.";
|
|
90
|
+
if (enriched.signal)
|
|
91
|
+
return `Command terminated with signal ${enriched.signal}.`;
|
|
92
|
+
if (enriched.message)
|
|
93
|
+
return enriched.message;
|
|
94
|
+
}
|
|
95
|
+
return error instanceof Error ? error.message : String(error);
|
|
96
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { findExecutableOnPath } from "./codex-cli.js";
|
|
2
|
+
export function resolveHermesCli(env = process.env, explicitPath) {
|
|
3
|
+
const configuredPath = optionalString(explicitPath) ?? optionalString(env.HERMES_CLI_PATH);
|
|
4
|
+
if (configuredPath) {
|
|
5
|
+
return { path: configuredPath, source: "env" };
|
|
6
|
+
}
|
|
7
|
+
const pathMatch = findExecutableOnPath("hermes", env.PATH);
|
|
8
|
+
return pathMatch ? { path: pathMatch, source: "path" } : { source: "missing" };
|
|
9
|
+
}
|
|
10
|
+
export function describeHermesCli(resolution) {
|
|
11
|
+
if (resolution.path) {
|
|
12
|
+
return `${resolution.source} (${resolution.path})`;
|
|
13
|
+
}
|
|
14
|
+
return "missing";
|
|
15
|
+
}
|
|
16
|
+
function optionalString(value) {
|
|
17
|
+
const trimmed = value?.trim();
|
|
18
|
+
return trimmed ? trimmed : undefined;
|
|
19
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createLaunchProfile } from "./codex-launch.js";
|
|
2
|
+
export const HERMES_LAUNCH_PROFILES = [
|
|
3
|
+
{
|
|
4
|
+
id: "default",
|
|
5
|
+
label: "Default",
|
|
6
|
+
behavior: "Hermes API server defaults / auto-approve once",
|
|
7
|
+
unsafe: false,
|
|
8
|
+
approvalChoice: "once",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
id: "safe",
|
|
12
|
+
label: "Safe",
|
|
13
|
+
behavior: "normal tools / deny dangerous approvals",
|
|
14
|
+
unsafe: false,
|
|
15
|
+
approvalChoice: "deny",
|
|
16
|
+
instructions: "Do not perform destructive operations. If a dangerous action requires approval, stop and explain what needs approval.",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "readonly",
|
|
20
|
+
label: "Read Only",
|
|
21
|
+
behavior: "read-only intent / deny dangerous approvals",
|
|
22
|
+
unsafe: false,
|
|
23
|
+
approvalChoice: "deny",
|
|
24
|
+
instructions: "Treat this run as read-only. Inspect, explain, and plan, but do not modify files or execute destructive commands.",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "yolo",
|
|
28
|
+
label: "YOLO",
|
|
29
|
+
behavior: "auto-approve for session",
|
|
30
|
+
unsafe: true,
|
|
31
|
+
approvalChoice: "session",
|
|
32
|
+
instructions: "Proceed autonomously within the configured Hermes API-server runtime.",
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
export function listHermesLaunchProfiles() {
|
|
36
|
+
return HERMES_LAUNCH_PROFILES.map((profile) => ({
|
|
37
|
+
id: profile.id,
|
|
38
|
+
label: profile.label,
|
|
39
|
+
behavior: profile.behavior,
|
|
40
|
+
unsafe: profile.unsafe,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
export function findHermesLaunchProfile(profileId) {
|
|
44
|
+
const profile = HERMES_LAUNCH_PROFILES.find((candidate) => candidate.id === profileId);
|
|
45
|
+
if (profile) {
|
|
46
|
+
return profile;
|
|
47
|
+
}
|
|
48
|
+
return HERMES_LAUNCH_PROFILES[0];
|
|
49
|
+
}
|
|
50
|
+
export function hermesProfileAsLaunchProfile(profile) {
|
|
51
|
+
return createLaunchProfile({
|
|
52
|
+
id: profile.id,
|
|
53
|
+
label: profile.label,
|
|
54
|
+
sandboxMode: profile.unsafe ? "danger-full-access" : "workspace-write",
|
|
55
|
+
approvalPolicy: profile.unsafe ? "never" : "on-request",
|
|
56
|
+
});
|
|
57
|
+
}
|