@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
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { HERMES_AGENT_CAPABILITIES, HERMES_REASONING_EFFORTS, } from "./agent.js";
|
|
5
|
+
import { HermesApiClient } from "./hermes-api.js";
|
|
6
|
+
import { resolveHermesCli } from "./hermes-cli.js";
|
|
7
|
+
import { findHermesLaunchProfile, hermesProfileAsLaunchProfile, listHermesLaunchProfiles, } from "./hermes-launch.js";
|
|
8
|
+
import { getHermesSession, listHermesSessions, listHermesWorkspaces, resolveHermesStateDbPath, } from "./hermes-state.js";
|
|
9
|
+
export class HermesSessionService {
|
|
10
|
+
config;
|
|
11
|
+
api;
|
|
12
|
+
stateDbPath;
|
|
13
|
+
cliPath;
|
|
14
|
+
currentWorkspace;
|
|
15
|
+
currentThreadId = null;
|
|
16
|
+
currentModel;
|
|
17
|
+
currentReasoning;
|
|
18
|
+
currentLaunchProfile;
|
|
19
|
+
cachedModels = [];
|
|
20
|
+
cachedUsage;
|
|
21
|
+
processing = false;
|
|
22
|
+
abortController = null;
|
|
23
|
+
currentRunId = null;
|
|
24
|
+
modelsLoadedAt = 0;
|
|
25
|
+
static MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
this.api = new HermesApiClient({
|
|
29
|
+
baseUrl: config.hermesApiBaseUrl,
|
|
30
|
+
apiKey: config.hermesApiKey,
|
|
31
|
+
});
|
|
32
|
+
this.stateDbPath = resolveHermesStateDbPath({
|
|
33
|
+
hermesHome: config.hermesHome,
|
|
34
|
+
stateDbPath: config.hermesStateDbPath,
|
|
35
|
+
});
|
|
36
|
+
this.cliPath = resolveHermesCli(process.env, config.hermesCliPath).path;
|
|
37
|
+
this.currentWorkspace = config.workspace;
|
|
38
|
+
this.currentModel = config.hermesDefaultModel;
|
|
39
|
+
this.currentReasoning = config.hermesDefaultReasoning;
|
|
40
|
+
this.currentLaunchProfile = findHermesLaunchProfile(config.hermesDefaultLaunchProfileId);
|
|
41
|
+
}
|
|
42
|
+
static async create(config, options) {
|
|
43
|
+
const service = new HermesSessionService(config);
|
|
44
|
+
service.currentWorkspace = options?.workspace ?? config.workspace;
|
|
45
|
+
service.currentModel = options?.model ?? config.hermesDefaultModel;
|
|
46
|
+
service.currentReasoning = normalizeHermesReasoning(options?.reasoningEffort ?? config.hermesDefaultReasoning);
|
|
47
|
+
service.currentLaunchProfile = findHermesLaunchProfile(options?.launchProfileId ?? config.hermesDefaultLaunchProfileId);
|
|
48
|
+
await service.refreshModels().catch(() => { });
|
|
49
|
+
if (options?.resumeThreadId) {
|
|
50
|
+
await service.resumeThread(options.resumeThreadId);
|
|
51
|
+
return service;
|
|
52
|
+
}
|
|
53
|
+
if (!options?.deferThreadStart) {
|
|
54
|
+
await service.newThread(service.currentWorkspace, service.currentModel);
|
|
55
|
+
}
|
|
56
|
+
return service;
|
|
57
|
+
}
|
|
58
|
+
getInfo() {
|
|
59
|
+
this.refreshFromState();
|
|
60
|
+
return {
|
|
61
|
+
agentId: "hermes",
|
|
62
|
+
agentLabel: "Hermes",
|
|
63
|
+
threadId: this.currentThreadId,
|
|
64
|
+
workspace: this.currentWorkspace,
|
|
65
|
+
model: this.currentModel,
|
|
66
|
+
reasoningEffort: this.currentReasoning,
|
|
67
|
+
launchProfileId: this.currentLaunchProfile.id,
|
|
68
|
+
launchProfileLabel: this.currentLaunchProfile.label,
|
|
69
|
+
launchProfileBehavior: this.currentLaunchProfile.behavior,
|
|
70
|
+
sandboxMode: "host",
|
|
71
|
+
approvalPolicy: "never",
|
|
72
|
+
fastMode: false,
|
|
73
|
+
unsafeLaunch: this.currentLaunchProfile.unsafe,
|
|
74
|
+
sessionPath: this.stateDbPath,
|
|
75
|
+
sessionUsage: this.cachedUsage,
|
|
76
|
+
capabilities: HERMES_AGENT_CAPABILITIES,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
isProcessing() {
|
|
80
|
+
return this.processing;
|
|
81
|
+
}
|
|
82
|
+
getActiveThreadId() {
|
|
83
|
+
return this.currentThreadId;
|
|
84
|
+
}
|
|
85
|
+
hasActiveThread() {
|
|
86
|
+
return Boolean(this.currentThreadId);
|
|
87
|
+
}
|
|
88
|
+
getCurrentWorkspace() {
|
|
89
|
+
return this.currentWorkspace;
|
|
90
|
+
}
|
|
91
|
+
async prompt(input, callbacks) {
|
|
92
|
+
if (this.processing) {
|
|
93
|
+
throw new Error("A Hermes turn is already in progress");
|
|
94
|
+
}
|
|
95
|
+
await this.ensureSessionStarted();
|
|
96
|
+
const threadId = this.currentThreadId;
|
|
97
|
+
const promptInput = await this.buildHermesInput(input);
|
|
98
|
+
const instructions = this.buildInstructions();
|
|
99
|
+
const abortController = new AbortController();
|
|
100
|
+
const openTools = new Map();
|
|
101
|
+
let finalOutput = "";
|
|
102
|
+
let streamedOutput = "";
|
|
103
|
+
let finalError = null;
|
|
104
|
+
let didEnd = false;
|
|
105
|
+
let toolCounter = 0;
|
|
106
|
+
this.processing = true;
|
|
107
|
+
this.abortController = abortController;
|
|
108
|
+
try {
|
|
109
|
+
const run = await this.api.startRun({
|
|
110
|
+
input: promptInput,
|
|
111
|
+
session_id: threadId,
|
|
112
|
+
model: this.currentModel,
|
|
113
|
+
reasoning_effort: this.currentReasoning,
|
|
114
|
+
...(instructions ? { instructions } : {}),
|
|
115
|
+
}, this.sessionKey(threadId));
|
|
116
|
+
this.currentRunId = run.run_id;
|
|
117
|
+
await this.api.streamRunEvents(run.run_id, (event) => {
|
|
118
|
+
const eventName = stringValue(event.event);
|
|
119
|
+
switch (eventName) {
|
|
120
|
+
case "message.delta": {
|
|
121
|
+
const delta = stringValue(event.delta);
|
|
122
|
+
if (delta) {
|
|
123
|
+
streamedOutput += delta;
|
|
124
|
+
callbacks.onTextDelta(delta);
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case "tool.started": {
|
|
129
|
+
const toolName = stringValue(event.tool) ?? "tool";
|
|
130
|
+
toolCounter += 1;
|
|
131
|
+
const toolId = `${run.run_id}-${toolName}-${toolCounter}`;
|
|
132
|
+
const openTool = { id: toolId, name: toolName };
|
|
133
|
+
const tools = openTools.get(toolName) ?? [];
|
|
134
|
+
tools.push(openTool);
|
|
135
|
+
openTools.set(toolName, tools);
|
|
136
|
+
callbacks.onToolStart(toolName, toolId);
|
|
137
|
+
const preview = stringValue(event.preview);
|
|
138
|
+
if (preview) {
|
|
139
|
+
callbacks.onToolUpdate(toolId, preview);
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "tool.completed": {
|
|
144
|
+
const toolName = stringValue(event.tool) ?? "tool";
|
|
145
|
+
const openTool = openTools.get(toolName)?.shift();
|
|
146
|
+
if (openTool) {
|
|
147
|
+
callbacks.onToolEnd(openTool.id, Boolean(event.error));
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case "reasoning.available": {
|
|
152
|
+
const toolId = `${run.run_id}-reasoning`;
|
|
153
|
+
callbacks.onToolStart("reasoning", toolId);
|
|
154
|
+
callbacks.onToolUpdate(toolId, stringValue(event.text) ?? "Reasoning available");
|
|
155
|
+
callbacks.onToolEnd(toolId, false);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case "approval.request": {
|
|
159
|
+
const toolId = `${run.run_id}-approval`;
|
|
160
|
+
callbacks.onToolStart("approval", toolId);
|
|
161
|
+
callbacks.onToolUpdate(toolId, "Hermes requested command approval.");
|
|
162
|
+
void this.api.approveRun(run.run_id, this.currentLaunchProfile.approvalChoice)
|
|
163
|
+
.then(() => callbacks.onToolUpdate(toolId, `Approval response: ${this.currentLaunchProfile.approvalChoice}`))
|
|
164
|
+
.catch((error) => callbacks.onToolUpdate(toolId, `Approval response failed: ${error instanceof Error ? error.message : String(error)}`))
|
|
165
|
+
.finally(() => callbacks.onToolEnd(toolId, this.currentLaunchProfile.approvalChoice === "deny"));
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case "run.completed": {
|
|
169
|
+
finalOutput = stringValue(event.output) ?? finalOutput;
|
|
170
|
+
const usage = objectValue(event.usage);
|
|
171
|
+
callbacks.onTurnComplete?.({
|
|
172
|
+
inputTokens: numberValue(usage?.input_tokens) ?? 0,
|
|
173
|
+
cachedInputTokens: 0,
|
|
174
|
+
outputTokens: numberValue(usage?.output_tokens) ?? 0,
|
|
175
|
+
});
|
|
176
|
+
if (finalOutput && !streamedOutput) {
|
|
177
|
+
streamedOutput = finalOutput;
|
|
178
|
+
callbacks.onTextDelta(finalOutput);
|
|
179
|
+
}
|
|
180
|
+
if (!didEnd) {
|
|
181
|
+
didEnd = true;
|
|
182
|
+
callbacks.onAgentEnd();
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case "run.failed":
|
|
187
|
+
finalError = stringValue(event.error) ?? "Hermes run failed";
|
|
188
|
+
break;
|
|
189
|
+
case "run.cancelled":
|
|
190
|
+
finalError = "Hermes run was cancelled";
|
|
191
|
+
break;
|
|
192
|
+
default:
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}, abortController.signal);
|
|
196
|
+
if (finalError) {
|
|
197
|
+
throw new Error(finalError);
|
|
198
|
+
}
|
|
199
|
+
if (!didEnd) {
|
|
200
|
+
callbacks.onAgentEnd();
|
|
201
|
+
}
|
|
202
|
+
this.refreshFromState();
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (isAbortError(error)) {
|
|
206
|
+
throw new Error("Hermes run was aborted");
|
|
207
|
+
}
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
this.currentRunId = null;
|
|
212
|
+
this.abortController = null;
|
|
213
|
+
this.processing = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async abort() {
|
|
217
|
+
const runId = this.currentRunId;
|
|
218
|
+
if (runId) {
|
|
219
|
+
await this.api.stopRun(runId).catch(() => { });
|
|
220
|
+
}
|
|
221
|
+
this.abortController?.abort();
|
|
222
|
+
this.processing = false;
|
|
223
|
+
}
|
|
224
|
+
async newThread(workspace, model) {
|
|
225
|
+
this.ensureIdle("start a new Hermes session");
|
|
226
|
+
this.currentWorkspace = workspace ?? this.currentWorkspace;
|
|
227
|
+
if (model) {
|
|
228
|
+
this.currentModel = model;
|
|
229
|
+
}
|
|
230
|
+
this.currentThreadId = createHermesSessionId();
|
|
231
|
+
this.cachedUsage = undefined;
|
|
232
|
+
return this.getInfo();
|
|
233
|
+
}
|
|
234
|
+
async resumeThread(threadId) {
|
|
235
|
+
return this.switchSession(threadId);
|
|
236
|
+
}
|
|
237
|
+
async switchSession(threadId) {
|
|
238
|
+
this.ensureIdle("switch Hermes session");
|
|
239
|
+
const record = this.getRecord(threadId);
|
|
240
|
+
if (!record) {
|
|
241
|
+
throw new Error(`Unknown Hermes session: ${threadId}`);
|
|
242
|
+
}
|
|
243
|
+
this.applyRecord(record);
|
|
244
|
+
return this.getInfo();
|
|
245
|
+
}
|
|
246
|
+
listAllSessions(limit) {
|
|
247
|
+
return listHermesSessions(limit ?? 20, this.stateOptions());
|
|
248
|
+
}
|
|
249
|
+
listWorkspaces() {
|
|
250
|
+
const workspaces = new Set(listHermesWorkspaces(this.stateOptions()));
|
|
251
|
+
workspaces.add(this.currentWorkspace);
|
|
252
|
+
workspaces.add(this.config.workspace);
|
|
253
|
+
return [...workspaces].sort((left, right) => left.localeCompare(right));
|
|
254
|
+
}
|
|
255
|
+
async refreshModels(options = {}) {
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
if (!options.force &&
|
|
258
|
+
this.cachedModels.length > 0 &&
|
|
259
|
+
now - this.modelsLoadedAt < HermesSessionService.MODEL_CACHE_TTL_MS) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const models = await this.api.models();
|
|
263
|
+
this.cachedModels = models.map((model) => ({
|
|
264
|
+
slug: model.id,
|
|
265
|
+
displayName: model.id,
|
|
266
|
+
supportsThinking: true,
|
|
267
|
+
supportsImages: true,
|
|
268
|
+
}));
|
|
269
|
+
this.modelsLoadedAt = now;
|
|
270
|
+
}
|
|
271
|
+
listModels() {
|
|
272
|
+
const models = [...this.cachedModels];
|
|
273
|
+
if (this.currentModel && !models.some((model) => model.slug === this.currentModel)) {
|
|
274
|
+
models.unshift({ slug: this.currentModel, displayName: this.currentModel, supportsThinking: true, supportsImages: true });
|
|
275
|
+
}
|
|
276
|
+
if (models.length === 0) {
|
|
277
|
+
models.push({ slug: "hermes-agent", displayName: "hermes-agent", supportsThinking: true, supportsImages: true });
|
|
278
|
+
}
|
|
279
|
+
return models;
|
|
280
|
+
}
|
|
281
|
+
listLaunchProfiles() {
|
|
282
|
+
return listHermesLaunchProfiles();
|
|
283
|
+
}
|
|
284
|
+
getSessionRecord(threadId) {
|
|
285
|
+
return this.getRecord(threadId);
|
|
286
|
+
}
|
|
287
|
+
setModel(slug) {
|
|
288
|
+
this.currentModel = slug;
|
|
289
|
+
return slug;
|
|
290
|
+
}
|
|
291
|
+
setModelForCurrentSession(slug) {
|
|
292
|
+
this.ensureIdle("change Hermes model");
|
|
293
|
+
this.currentModel = slug;
|
|
294
|
+
return { value: slug, appliedToActiveThread: Boolean(this.currentThreadId) };
|
|
295
|
+
}
|
|
296
|
+
setReasoningEffort(effort) {
|
|
297
|
+
this.currentReasoning = normalizeHermesReasoning(effort);
|
|
298
|
+
}
|
|
299
|
+
setReasoningEffortForCurrentSession(effort) {
|
|
300
|
+
this.ensureIdle("change Hermes reasoning");
|
|
301
|
+
const value = normalizeHermesReasoning(effort);
|
|
302
|
+
if (!value) {
|
|
303
|
+
throw new Error("Hermes reasoning effort is empty");
|
|
304
|
+
}
|
|
305
|
+
this.currentReasoning = value;
|
|
306
|
+
return { value, appliedToActiveThread: Boolean(this.currentThreadId) };
|
|
307
|
+
}
|
|
308
|
+
setLaunchProfile(profileId) {
|
|
309
|
+
this.ensureIdle("change Hermes profile");
|
|
310
|
+
this.currentLaunchProfile = findHermesLaunchProfile(profileId);
|
|
311
|
+
return hermesProfileAsLaunchProfile(this.currentLaunchProfile);
|
|
312
|
+
}
|
|
313
|
+
setFastMode() {
|
|
314
|
+
throw new Error("Fast mode is only supported by Codex sessions");
|
|
315
|
+
}
|
|
316
|
+
getSelectedLaunchProfile() {
|
|
317
|
+
return hermesProfileAsLaunchProfile(this.currentLaunchProfile);
|
|
318
|
+
}
|
|
319
|
+
syncFromAgentState() {
|
|
320
|
+
const before = this.getInfo();
|
|
321
|
+
this.refreshFromState();
|
|
322
|
+
const after = this.getInfo();
|
|
323
|
+
const changedFields = [];
|
|
324
|
+
if (before.model !== after.model)
|
|
325
|
+
changedFields.push("model");
|
|
326
|
+
if (before.reasoningEffort !== after.reasoningEffort)
|
|
327
|
+
changedFields.push("reasoningEffort");
|
|
328
|
+
return {
|
|
329
|
+
threadId: this.currentThreadId,
|
|
330
|
+
changed: changedFields.length > 0,
|
|
331
|
+
reattached: false,
|
|
332
|
+
changedFields,
|
|
333
|
+
info: after,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
handback() {
|
|
337
|
+
const threadId = this.currentThreadId;
|
|
338
|
+
const workspace = this.currentWorkspace;
|
|
339
|
+
this.currentThreadId = null;
|
|
340
|
+
return {
|
|
341
|
+
threadId,
|
|
342
|
+
workspace,
|
|
343
|
+
command: threadId
|
|
344
|
+
? `cd ${shellQuote(workspace)} && ${shellQuote(this.cliPath ?? "hermes")} --resume ${shellQuote(threadId)}`
|
|
345
|
+
: undefined,
|
|
346
|
+
label: "Hermes CLI",
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
dispose() {
|
|
350
|
+
this.abortController?.abort();
|
|
351
|
+
this.processing = false;
|
|
352
|
+
this.currentRunId = null;
|
|
353
|
+
}
|
|
354
|
+
async ensureSessionStarted() {
|
|
355
|
+
if (!this.currentThreadId) {
|
|
356
|
+
await this.newThread(this.currentWorkspace, this.currentModel);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
ensureIdle(action) {
|
|
360
|
+
if (this.processing) {
|
|
361
|
+
throw new Error(`Cannot ${action} while a turn is in progress`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
refreshFromState() {
|
|
365
|
+
if (!this.currentThreadId) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const record = this.getRecord(this.currentThreadId);
|
|
369
|
+
if (record) {
|
|
370
|
+
this.applyRecord(record);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
applyRecord(record) {
|
|
374
|
+
this.currentThreadId = record.id;
|
|
375
|
+
this.currentWorkspace = record.cwd || this.currentWorkspace;
|
|
376
|
+
this.currentModel = record.model ?? this.currentModel;
|
|
377
|
+
this.currentReasoning = normalizeHermesReasoning(record.reasoningEffort ?? this.currentReasoning);
|
|
378
|
+
this.cachedUsage = record.usage;
|
|
379
|
+
}
|
|
380
|
+
getRecord(threadId) {
|
|
381
|
+
return getHermesSession(threadId, this.stateOptions());
|
|
382
|
+
}
|
|
383
|
+
stateOptions() {
|
|
384
|
+
return {
|
|
385
|
+
hermesHome: this.config.hermesHome,
|
|
386
|
+
stateDbPath: this.config.hermesStateDbPath,
|
|
387
|
+
workspace: this.currentWorkspace || this.config.workspace,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
async buildHermesInput(input) {
|
|
391
|
+
if (typeof input === "string") {
|
|
392
|
+
return input;
|
|
393
|
+
}
|
|
394
|
+
const textParts = [input.stagedFileInstructions, input.text].filter((part) => Boolean(part?.trim()));
|
|
395
|
+
const content = [];
|
|
396
|
+
const text = textParts.join("\n\n").trim();
|
|
397
|
+
if (text) {
|
|
398
|
+
content.push({ type: "text", text });
|
|
399
|
+
}
|
|
400
|
+
for (const imagePath of input.imagePaths ?? []) {
|
|
401
|
+
const data = (await readFile(imagePath)).toString("base64");
|
|
402
|
+
content.push({
|
|
403
|
+
type: "image_url",
|
|
404
|
+
image_url: {
|
|
405
|
+
url: `data:${mimeTypeForImage(imagePath)};base64,${data}`,
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (content.length === 0) {
|
|
410
|
+
return "Please inspect the attached file(s).";
|
|
411
|
+
}
|
|
412
|
+
if (content.length === 1 && content[0]?.type === "text") {
|
|
413
|
+
return String(content[0].text ?? "");
|
|
414
|
+
}
|
|
415
|
+
return [{ role: "user", content }];
|
|
416
|
+
}
|
|
417
|
+
buildInstructions() {
|
|
418
|
+
const parts = [
|
|
419
|
+
this.currentLaunchProfile.instructions,
|
|
420
|
+
this.currentReasoning ? `Use Hermes reasoning effort "${this.currentReasoning}" when the configured provider supports it.` : undefined,
|
|
421
|
+
].filter((part) => Boolean(part?.trim()));
|
|
422
|
+
return parts.join("\n\n") || undefined;
|
|
423
|
+
}
|
|
424
|
+
sessionKey(threadId) {
|
|
425
|
+
return `nordrelay:hermes:${threadId}`;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function normalizeHermesReasoning(value) {
|
|
429
|
+
if (!value) {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
const normalized = value === "off" ? "none" : value;
|
|
433
|
+
if (HERMES_REASONING_EFFORTS.includes(normalized)) {
|
|
434
|
+
return normalized;
|
|
435
|
+
}
|
|
436
|
+
throw new Error(`Unsupported Hermes reasoning effort: ${value}`);
|
|
437
|
+
}
|
|
438
|
+
function createHermesSessionId() {
|
|
439
|
+
const date = new Date();
|
|
440
|
+
const stamp = [
|
|
441
|
+
date.getUTCFullYear(),
|
|
442
|
+
String(date.getUTCMonth() + 1).padStart(2, "0"),
|
|
443
|
+
String(date.getUTCDate()).padStart(2, "0"),
|
|
444
|
+
"_",
|
|
445
|
+
String(date.getUTCHours()).padStart(2, "0"),
|
|
446
|
+
String(date.getUTCMinutes()).padStart(2, "0"),
|
|
447
|
+
String(date.getUTCSeconds()).padStart(2, "0"),
|
|
448
|
+
].join("");
|
|
449
|
+
return `${stamp}_${randomUUID().replace(/-/g, "").slice(0, 8)}`;
|
|
450
|
+
}
|
|
451
|
+
function stringValue(value) {
|
|
452
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
453
|
+
}
|
|
454
|
+
function numberValue(value) {
|
|
455
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
456
|
+
}
|
|
457
|
+
function objectValue(value) {
|
|
458
|
+
return typeof value === "object" && value !== null ? value : null;
|
|
459
|
+
}
|
|
460
|
+
function isAbortError(error) {
|
|
461
|
+
return error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted"));
|
|
462
|
+
}
|
|
463
|
+
function mimeTypeForImage(filePath) {
|
|
464
|
+
switch (path.extname(filePath).toLowerCase()) {
|
|
465
|
+
case ".png":
|
|
466
|
+
return "image/png";
|
|
467
|
+
case ".webp":
|
|
468
|
+
return "image/webp";
|
|
469
|
+
case ".gif":
|
|
470
|
+
return "image/gif";
|
|
471
|
+
default:
|
|
472
|
+
return "image/jpeg";
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function shellQuote(value) {
|
|
476
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
477
|
+
}
|