@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.
Files changed (48) hide show
  1. package/.env.example +45 -2
  2. package/README.md +204 -30
  3. package/dist/agent-activity.js +300 -0
  4. package/dist/agent-adapter.js +17 -30
  5. package/dist/agent-factory.js +27 -0
  6. package/dist/agent.js +123 -9
  7. package/dist/artifacts.js +1 -1
  8. package/dist/audit-log.js +1 -1
  9. package/dist/bot-ui.js +1 -1
  10. package/dist/bot.js +328 -159
  11. package/dist/claude-code-auth.js +121 -0
  12. package/dist/claude-code-cli.js +19 -0
  13. package/dist/claude-code-launch.js +73 -0
  14. package/dist/claude-code-session.js +660 -0
  15. package/dist/claude-code-state.js +590 -0
  16. package/dist/codex-session.js +12 -1
  17. package/dist/config.js +113 -9
  18. package/dist/hermes-api.js +150 -0
  19. package/dist/hermes-auth.js +96 -0
  20. package/dist/hermes-cli.js +19 -0
  21. package/dist/hermes-launch.js +57 -0
  22. package/dist/hermes-session.js +477 -0
  23. package/dist/hermes-state.js +609 -0
  24. package/dist/index.js +51 -8
  25. package/dist/openclaw-auth.js +27 -0
  26. package/dist/openclaw-cli.js +19 -0
  27. package/dist/openclaw-gateway.js +285 -0
  28. package/dist/openclaw-launch.js +65 -0
  29. package/dist/openclaw-session.js +549 -0
  30. package/dist/openclaw-state.js +409 -0
  31. package/dist/operations.js +83 -2
  32. package/dist/pi-auth.js +59 -0
  33. package/dist/pi-launch.js +61 -0
  34. package/dist/pi-rpc.js +18 -0
  35. package/dist/pi-session.js +103 -15
  36. package/dist/pi-state.js +253 -0
  37. package/dist/relay-runtime.js +673 -51
  38. package/dist/session-format.js +28 -18
  39. package/dist/session-registry.js +40 -15
  40. package/dist/settings-service.js +35 -4
  41. package/dist/web-dashboard-ui.js +18 -0
  42. package/dist/web-dashboard.js +329 -47
  43. package/package.json +8 -3
  44. package/plugins/nordrelay/.codex-plugin/plugin.json +7 -4
  45. package/plugins/nordrelay/commands/remote.md +2 -2
  46. package/plugins/nordrelay/scripts/nordrelay.mjs +131 -3
  47. package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
  48. package/CHANGELOG.md +0 -26
@@ -1,16 +1,23 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { createArtifactZipBundle, getArtifactTurnReport, ensureOutDir, listRecentArtifactReports, removeArtifactTurn, totalArtifactSize, } from "./artifacts.js";
4
+ import { createArtifactZipBundle, collectRecentWorkspaceArtifacts, getArtifactTurnReport, ensureOutDir, listRecentArtifactReports, persistWorkspaceArtifactReport, removeArtifactTurn, totalArtifactSize, } from "./artifacts.js";
5
5
  import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js";
6
- import { CODEX_AGENT_CAPABILITIES, CODEX_REASONING_EFFORTS, PI_THINKING_LEVELS, agentLabel, agentReasoningLabel, } from "./agent.js";
7
- import { enabledAgents } from "./agent-factory.js";
8
- import { checkAuthStatus } from "./codex-auth.js";
9
- import { getThreadRolloutSnapshot, } from "./codex-state.js";
6
+ import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
7
+ import { getAgentDiagnostics, getExternalSnapshotForSession, } from "./agent-activity.js";
8
+ import { listAgentAdapterDescriptors } from "./agent-adapter.js";
9
+ import { createAgentSessionService, enabledAgents } from "./agent-factory.js";
10
+ import { AuditLogStore } from "./audit-log.js";
11
+ import { checkAuthStatus, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
12
+ import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout } from "./claude-code-auth.js";
10
13
  import { friendlyErrorText } from "./error-messages.js";
11
- import { getConnectorHealth, getVersionChecks, readFormattedLogTail, spawnConnectorRestart } from "./operations.js";
14
+ import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
15
+ import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
16
+ import { getConnectorHealth, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate } from "./operations.js";
17
+ import { checkPiAuthStatus } from "./pi-auth.js";
12
18
  import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
13
19
  import { renderSessionInfoPlain } from "./session-format.js";
20
+ import { SessionLockStore } from "./session-locks.js";
14
21
  import { SessionRegistry } from "./session-registry.js";
15
22
  import { transcribeAudio } from "./voice.js";
16
23
  import { WebActivityStore, WebChatStore, } from "./web-state.js";
@@ -25,12 +32,15 @@ export class RelayRuntime {
25
32
  promptStore;
26
33
  chatStore;
27
34
  activityStore;
35
+ auditStore;
36
+ lockStore;
28
37
  subscribers = new Set();
29
38
  externalMonitor;
30
39
  draining = false;
31
40
  currentTurnId = null;
32
41
  accumulatedText = "";
33
42
  currentTurnStartedAt = 0;
43
+ currentProgress = null;
34
44
  externalMirror = null;
35
45
  constructor(config) {
36
46
  this.config = config;
@@ -41,6 +51,8 @@ export class RelayRuntime {
41
51
  this.promptStore = new PromptStore(config.workspace, config.stateBackend);
42
52
  this.chatStore = new WebChatStore(config.workspace, config.stateBackend, MAX_CHAT_HISTORY);
43
53
  this.activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
54
+ this.auditStore = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
55
+ this.lockStore = new SessionLockStore(config.workspace, config.stateBackend);
44
56
  if (config.codexExternalBusyCheckMs > 0) {
45
57
  this.externalMonitor = setInterval(() => {
46
58
  void this.monitorExternalActivity().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
@@ -70,48 +82,311 @@ export class RelayRuntime {
70
82
  }
71
83
  async status() {
72
84
  return {
73
- health: await getConnectorHealth(),
74
- versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath }),
85
+ health: await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
86
+ versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
75
87
  snapshot: await this.snapshot(),
76
88
  };
77
89
  }
90
+ async version() {
91
+ return {
92
+ health: await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
93
+ state: await readConnectorState(),
94
+ versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
95
+ };
96
+ }
97
+ updateConnector() {
98
+ const update = spawnSelfUpdate();
99
+ this.broadcastStatus(`Update started with ${update.method}. Log: ${update.logPath}`, "warn");
100
+ this.appendActivity({
101
+ source: "web",
102
+ status: "info",
103
+ type: "update_started",
104
+ threadId: null,
105
+ workspace: this.config.workspace,
106
+ detail: `${update.method}: ${update.summary}`,
107
+ });
108
+ this.appendAudit({
109
+ action: "command",
110
+ status: "ok",
111
+ contextKey: WEB_CONTEXT_KEY,
112
+ description: "update",
113
+ detail: update.summary,
114
+ });
115
+ return update;
116
+ }
78
117
  async diagnostics() {
79
118
  return {
80
- health: await getConnectorHealth(),
81
- versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath }),
119
+ health: await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
120
+ versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
82
121
  snapshot: await this.snapshot(),
83
122
  runtime: {
84
123
  stateBackend: this.config.stateBackend,
85
124
  sourceWorkspace: this.config.workspace,
86
125
  queuePaused: this.promptStore.isPaused(WEB_CONTEXT_KEY),
87
126
  externalMirror: this.externalMirror ? { ...this.externalMirror } : null,
127
+ agentDiagnostics: getAgentDiagnostics(await this.getSession(true), this.config),
88
128
  },
89
129
  };
90
130
  }
91
- async controlOptions() {
92
- const session = await this.getSession(true);
93
- const info = this.publicInfo(session);
94
- const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
95
- return {
96
- models: capabilities.modelSelection ? session.listModels() : [],
97
- reasoningLabel: agentReasoningLabel(info.agentId),
98
- reasoningOptions: info.agentId === "pi" ? PI_THINKING_LEVELS : CODEX_REASONING_EFFORTS,
99
- launchProfiles: capabilities.launchProfiles
100
- ? this.config.launchProfiles.map((profile) => ({
101
- id: profile.id,
102
- label: profile.label,
103
- behavior: `${profile.sandboxMode} / ${profile.approvalPolicy}`,
104
- unsafe: profile.unsafe,
131
+ async adapterHealth() {
132
+ const health = await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath });
133
+ const versions = await getVersionChecks({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath });
134
+ return Promise.all(listAgentAdapterDescriptors().map(async (descriptor) => {
135
+ const enabled = enabledAgents(this.config).includes(descriptor.id);
136
+ const auth = descriptor.capabilities.auth && enabled
137
+ ? await this.authStatus(descriptor.id).catch((error) => ({
138
+ agentId: descriptor.id,
139
+ agentLabel: descriptor.label,
140
+ supported: descriptor.capabilities.auth,
141
+ authenticated: false,
142
+ detail: friendlyErrorText(error),
143
+ loginSupported: descriptor.capabilities.login,
144
+ logoutSupported: descriptor.capabilities.logout,
105
145
  }))
106
- : [],
107
- workspaces: filterAllowedWorkspaces(session.listWorkspaces(), this.config),
108
- capabilities,
146
+ : null;
147
+ const cli = cliHealthForAgent(descriptor.id, health);
148
+ const version = versionCheckForAgent(descriptor.id, versions);
149
+ return {
150
+ id: descriptor.id,
151
+ label: descriptor.label,
152
+ enabled,
153
+ status: descriptor.status === "available" ? (enabled ? "enabled" : "disabled") : "planned",
154
+ auth: {
155
+ supported: descriptor.capabilities.auth,
156
+ authenticated: auth ? auth.authenticated : null,
157
+ method: auth?.method,
158
+ detail: auth?.detail,
159
+ },
160
+ cli,
161
+ version: {
162
+ installed: version.installedLabel,
163
+ latest: version.latestVersion,
164
+ status: version.status,
165
+ detail: version.detail,
166
+ },
167
+ capabilities: descriptor.capabilities,
168
+ notes: descriptor.notes,
169
+ };
170
+ }));
171
+ }
172
+ permissions() {
173
+ return {
174
+ telegramAllowAnyChat: this.config.telegramAllowAnyChat,
175
+ telegramAdminUserIds: this.config.telegramAdminUserIds,
176
+ telegramAllowedUserIds: this.config.telegramAllowedUserIds,
177
+ telegramReadOnlyUserIds: this.config.telegramReadOnlyUserIds,
178
+ telegramAllowedChatIds: this.config.telegramAllowedChatIds,
179
+ telegramRolePolicies: this.config.telegramRolePolicies,
109
180
  };
110
181
  }
182
+ tasks() {
183
+ return {
184
+ current: this.currentProgress ? { ...this.currentProgress, tools: [...this.currentProgress.tools] } : null,
185
+ external: this.externalTask(),
186
+ queue: this.queue(),
187
+ queuePaused: this.queuePaused(),
188
+ recent: this.activity({ limit: 20 }),
189
+ };
190
+ }
191
+ audit(limit = 50) {
192
+ return this.auditStore.list(limit);
193
+ }
194
+ locks() {
195
+ return this.lockStore.list();
196
+ }
197
+ lockWebSession(ownerName = "Web dashboard") {
198
+ const lock = this.lockStore.set(WEB_CONTEXT_KEY, 0, ownerName, this.config.sessionLockTtlMs);
199
+ this.appendAudit({
200
+ action: "lock_updated",
201
+ status: "ok",
202
+ contextKey: WEB_CONTEXT_KEY,
203
+ description: "lock",
204
+ detail: `locked by ${ownerName}`,
205
+ });
206
+ return lock;
207
+ }
208
+ unlockWebSession() {
209
+ const removed = this.lockStore.clear(WEB_CONTEXT_KEY);
210
+ this.appendAudit({
211
+ action: "lock_updated",
212
+ status: "ok",
213
+ contextKey: WEB_CONTEXT_KEY,
214
+ description: "unlock",
215
+ detail: removed ? "unlocked" : "no lock",
216
+ });
217
+ return { removed, locks: this.locks() };
218
+ }
219
+ async controlOptions(agentId) {
220
+ const { session, dispose } = await this.getControlSession(agentId);
221
+ try {
222
+ const info = this.publicInfo(session);
223
+ const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
224
+ if (capabilities.modelSelection) {
225
+ await session.refreshModels().catch((error) => {
226
+ console.warn(`Failed to refresh ${agentLabel(info.agentId)} models: ${error instanceof Error ? error.message : String(error)}`);
227
+ });
228
+ }
229
+ return {
230
+ models: capabilities.modelSelection ? session.listModels() : [],
231
+ reasoningLabel: agentReasoningLabel(info.agentId),
232
+ reasoningOptions: agentReasoningOptions(info.agentId),
233
+ launchProfiles: capabilities.launchProfiles ? session.listLaunchProfiles() : [],
234
+ workspaces: filterAllowedWorkspaces(session.listWorkspaces(), this.config),
235
+ capabilities,
236
+ };
237
+ }
238
+ finally {
239
+ if (dispose) {
240
+ session.dispose();
241
+ }
242
+ }
243
+ }
244
+ async authStatus(agentId) {
245
+ const { session, dispose } = await this.getControlSession(agentId);
246
+ try {
247
+ const info = this.publicInfo(session);
248
+ const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
249
+ if (!capabilities.auth) {
250
+ return {
251
+ agentId: info.agentId,
252
+ agentLabel: info.agentLabel,
253
+ supported: false,
254
+ authenticated: null,
255
+ detail: `${info.agentLabel} authentication is managed outside NordRelay.`,
256
+ loginSupported: false,
257
+ logoutSupported: false,
258
+ hostLoginCommand: hostLoginCommand(info, this.config),
259
+ hostLogoutCommand: hostLogoutCommand(info, this.config),
260
+ };
261
+ }
262
+ const status = await this.checkAgentAuth(info);
263
+ return {
264
+ agentId: info.agentId,
265
+ agentLabel: info.agentLabel,
266
+ supported: true,
267
+ authenticated: status.authenticated,
268
+ method: status.method,
269
+ detail: status.detail,
270
+ loginSupported: capabilities.login,
271
+ logoutSupported: capabilities.logout,
272
+ hostLoginCommand: hostLoginCommand(info, this.config),
273
+ hostLogoutCommand: hostLogoutCommand(info, this.config),
274
+ };
275
+ }
276
+ finally {
277
+ if (dispose) {
278
+ session.dispose();
279
+ }
280
+ }
281
+ }
282
+ async login(agentId) {
283
+ const { session, dispose } = await this.getControlSession(agentId);
284
+ try {
285
+ const info = this.publicInfo(session);
286
+ const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
287
+ if (!capabilities.login) {
288
+ return {
289
+ ...(await this.authStatus(info.agentId)),
290
+ result: {
291
+ success: false,
292
+ message: `${info.agentLabel} login is not managed by NordRelay. Run ${hostLoginCommand(info, this.config)} on the host.`,
293
+ },
294
+ };
295
+ }
296
+ if (!this.config.enableTelegramLogin) {
297
+ return {
298
+ ...(await this.authStatus(info.agentId)),
299
+ result: {
300
+ success: false,
301
+ message: `Remote login is disabled. Run ${hostLoginCommand(info, this.config)} on the host.`,
302
+ },
303
+ };
304
+ }
305
+ const result = await this.startAgentLogin(info);
306
+ this.appendAudit({
307
+ action: "command",
308
+ status: result.success ? "ok" : "failed",
309
+ contextKey: WEB_CONTEXT_KEY,
310
+ agentId: info.agentId,
311
+ threadId: info.threadId,
312
+ workspace: info.workspace,
313
+ description: "login",
314
+ detail: result.message,
315
+ });
316
+ return { ...(await this.authStatus(info.agentId)), result };
317
+ }
318
+ finally {
319
+ if (dispose) {
320
+ session.dispose();
321
+ }
322
+ }
323
+ }
324
+ async logout(agentId) {
325
+ const { session, dispose } = await this.getControlSession(agentId);
326
+ try {
327
+ const info = this.publicInfo(session);
328
+ const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
329
+ if (!capabilities.logout) {
330
+ return {
331
+ ...(await this.authStatus(info.agentId)),
332
+ result: {
333
+ success: false,
334
+ message: `${info.agentLabel} logout is not managed by NordRelay. Run ${hostLogoutCommand(info, this.config)} on the host.`,
335
+ },
336
+ };
337
+ }
338
+ if (!this.config.enableTelegramLogin) {
339
+ return {
340
+ ...(await this.authStatus(info.agentId)),
341
+ result: {
342
+ success: false,
343
+ message: `Remote auth management is disabled. Run ${hostLogoutCommand(info, this.config)} on the host.`,
344
+ },
345
+ };
346
+ }
347
+ const current = await this.checkAgentAuth(info);
348
+ if (current.method === "api-key") {
349
+ return {
350
+ ...(await this.authStatus(info.agentId)),
351
+ result: {
352
+ success: false,
353
+ message: "Cannot logout while API-key authentication is configured. Remove the API key from .env to use CLI auth.",
354
+ },
355
+ };
356
+ }
357
+ const result = await this.startAgentLogout(info);
358
+ this.appendAudit({
359
+ action: "command",
360
+ status: result.success ? "ok" : "failed",
361
+ contextKey: WEB_CONTEXT_KEY,
362
+ agentId: info.agentId,
363
+ threadId: info.threadId,
364
+ workspace: info.workspace,
365
+ description: "logout",
366
+ detail: result.message,
367
+ });
368
+ return { ...(await this.authStatus(info.agentId)), result };
369
+ }
370
+ finally {
371
+ if (dispose) {
372
+ session.dispose();
373
+ }
374
+ }
375
+ }
111
376
  async chatHistory(limit = 200) {
112
377
  const session = await this.getSession(true);
113
378
  return this.chatStore.list(this.publicInfo(session).threadId, limit);
114
379
  }
380
+ async sessionDetail(threadId) {
381
+ const session = await this.getSession(true);
382
+ const record = session.getSessionRecord(threadId);
383
+ return {
384
+ record,
385
+ active: this.publicInfo(session),
386
+ messages: this.chatStore.list(threadId, 100),
387
+ activity: this.activity({ limit: 100 }).filter((event) => event.threadId === threadId),
388
+ };
389
+ }
115
390
  async clearChatHistory() {
116
391
  const session = await this.getSession(true);
117
392
  const removed = this.chatStore.clear(this.publicInfo(session).threadId);
@@ -122,6 +397,51 @@ export class RelayRuntime {
122
397
  activity(options = {}) {
123
398
  return this.activityStore.list(options);
124
399
  }
400
+ async retry() {
401
+ const cached = this.promptStore.getLastPrompt(WEB_CONTEXT_KEY);
402
+ if (!cached) {
403
+ throw new Error("Nothing to retry. Send a message first.");
404
+ }
405
+ this.appendAudit({
406
+ action: "command",
407
+ status: "ok",
408
+ contextKey: WEB_CONTEXT_KEY,
409
+ description: "retry",
410
+ detail: cached.description,
411
+ });
412
+ return this.sendEnvelope(cached);
413
+ }
414
+ async sync() {
415
+ const session = await this.getSession(true);
416
+ const info = this.publicInfo(session);
417
+ if (!(info.capabilities ?? CODEX_AGENT_CAPABILITIES).externalActivity) {
418
+ throw new Error(`${info.agentLabel} has no external state watcher to sync.`);
419
+ }
420
+ const result = session.syncFromAgentState({ reattach: true });
421
+ if (result.changed) {
422
+ this.updateSession(session);
423
+ }
424
+ this.appendActivity({
425
+ source: "web",
426
+ status: "info",
427
+ type: "session_sync",
428
+ threadId: result.info.threadId,
429
+ workspace: result.info.workspace,
430
+ agentId: result.info.agentId,
431
+ detail: result.changedFields.length > 0 ? result.changedFields.join(", ") : "already in sync",
432
+ });
433
+ this.appendAudit({
434
+ action: "command",
435
+ status: "ok",
436
+ contextKey: WEB_CONTEXT_KEY,
437
+ agentId: result.info.agentId,
438
+ threadId: result.info.threadId,
439
+ workspace: result.info.workspace,
440
+ description: "sync",
441
+ detail: result.changedFields.join(", ") || "none",
442
+ });
443
+ return result;
444
+ }
125
445
  async listSessions(limit = 80, query = "") {
126
446
  return this.filteredSessions(await this.getSession(true), query, Math.max(1, limit * 3)).slice(0, limit);
127
447
  }
@@ -161,7 +481,12 @@ export class RelayRuntime {
161
481
  });
162
482
  }
163
483
  async listModels() {
164
- return (await this.getSession(true)).listModels();
484
+ const session = await this.getSession(true);
485
+ const info = this.publicInfo(session);
486
+ await session.refreshModels({ force: true }).catch((error) => {
487
+ console.warn(`Failed to refresh ${agentLabel(info.agentId)} models: ${error instanceof Error ? error.message : String(error)}`);
488
+ });
489
+ return session.listModels();
165
490
  }
166
491
  async setAgent(agentId) {
167
492
  if (!enabledAgents(this.config).includes(agentId)) {
@@ -175,7 +500,7 @@ export class RelayRuntime {
175
500
  const session = options.agentId ? await this.registry.switchAgent(WEB_CONTEXT_KEY, options.agentId) : await this.getSession(true);
176
501
  this.ensureIdle(session);
177
502
  if (options.reasoningEffort) {
178
- const reasoningOptions = session.getInfo().agentId === "pi" ? PI_THINKING_LEVELS : CODEX_REASONING_EFFORTS;
503
+ const reasoningOptions = agentReasoningOptions(session.getInfo().agentId);
179
504
  if (!reasoningOptions.includes(options.reasoningEffort)) {
180
505
  throw new Error(`Invalid ${agentReasoningLabel(session.getInfo().agentId)} value: ${options.reasoningEffort}`);
181
506
  }
@@ -230,7 +555,7 @@ export class RelayRuntime {
230
555
  async setReasoningEffort(effort) {
231
556
  const session = await this.getSession(true);
232
557
  this.ensureIdle(session);
233
- const options = session.getInfo().agentId === "pi" ? PI_THINKING_LEVELS : CODEX_REASONING_EFFORTS;
558
+ const options = agentReasoningOptions(session.getInfo().agentId);
234
559
  if (!options.includes(effort)) {
235
560
  throw new Error(`Invalid ${agentReasoningLabel(session.getInfo().agentId)} value: ${effort}`);
236
561
  }
@@ -264,6 +589,16 @@ export class RelayRuntime {
264
589
  }
265
590
  async abort() {
266
591
  const session = await this.getSession(true);
592
+ const snapshot = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
593
+ if (snapshot?.activity.active && !session.isProcessing()) {
594
+ this.broadcast({
595
+ type: "status",
596
+ level: "warn",
597
+ message: `Cannot abort the external ${snapshot.agentLabel} CLI task from NordRelay. Stop it in the terminal where it is running.`,
598
+ at: new Date().toISOString(),
599
+ });
600
+ return;
601
+ }
267
602
  await session.abort();
268
603
  this.broadcast({ type: "status", level: "warn", message: "Current operation aborted.", at: new Date().toISOString() });
269
604
  }
@@ -341,7 +676,8 @@ export class RelayRuntime {
341
676
  }
342
677
  async sendEnvelope(envelope) {
343
678
  const session = await this.getSession(false);
344
- if (session.isProcessing()) {
679
+ const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
680
+ if (session.isProcessing() || external?.activity.active) {
345
681
  const queued = this.promptStore.enqueue(WEB_CONTEXT_KEY, envelope);
346
682
  const info = this.publicInfo(session);
347
683
  this.appendActivity({
@@ -352,8 +688,23 @@ export class RelayRuntime {
352
688
  workspace: info.workspace,
353
689
  agentId: info.agentId,
354
690
  prompt: envelope.description,
355
- detail: `Queued at position ${this.promptStore.list(WEB_CONTEXT_KEY).length}.`,
691
+ detail: external?.activity.active
692
+ ? `Queued because ${external.agentLabel} CLI is still processing another task.`
693
+ : `Queued at position ${this.promptStore.list(WEB_CONTEXT_KEY).length}.`,
694
+ });
695
+ this.appendAudit({
696
+ action: "prompt_queued",
697
+ status: "ok",
698
+ contextKey: WEB_CONTEXT_KEY,
699
+ agentId: info.agentId,
700
+ threadId: info.threadId,
701
+ workspace: info.workspace,
702
+ promptId: queued.id,
703
+ description: envelope.description,
356
704
  });
705
+ if (external?.activity.active) {
706
+ this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.promptStore.list(WEB_CONTEXT_KEY).length} queued.`, "info");
707
+ }
357
708
  this.broadcastQueue();
358
709
  return { queued: true, queueId: queued.id };
359
710
  }
@@ -397,6 +748,12 @@ export class RelayRuntime {
397
748
  workspace: this.config.workspace,
398
749
  detail: id ? `${action}: ${id}` : action,
399
750
  });
751
+ this.appendAudit({
752
+ action: "queue_updated",
753
+ status: "ok",
754
+ contextKey: WEB_CONTEXT_KEY,
755
+ description: id ? `${action}: ${id}` : action,
756
+ });
400
757
  this.broadcastQueue();
401
758
  return this.queue();
402
759
  }
@@ -485,23 +842,21 @@ export class RelayRuntime {
485
842
  async monitorExternalActivity() {
486
843
  const session = await this.getSession(true);
487
844
  const info = this.publicInfo(session);
488
- if (!info.capabilities.externalActivity || info.agentId !== "codex" || !info.threadId || session.isProcessing()) {
845
+ if (!info.capabilities.externalActivity || !info.threadId || session.isProcessing()) {
489
846
  return;
490
847
  }
491
- const snapshot = getThreadRolloutSnapshot(info.threadId, {
848
+ const snapshot = getExternalSnapshotForSession(session, this.config, {
492
849
  afterLine: this.externalMirror?.threadId === info.threadId ? this.externalMirror.lastLine : Number.MAX_SAFE_INTEGER,
493
- staleAfterMs: this.config.codexExternalBusyStaleMs,
494
- }) ?? getThreadRolloutSnapshot(info.threadId, {
495
- staleAfterMs: this.config.codexExternalBusyStaleMs,
850
+ }) ?? getExternalSnapshotForSession(session, this.config, {
496
851
  maxEvents: 0,
497
852
  });
498
853
  if (!snapshot) {
499
854
  return;
500
855
  }
501
- if (!this.externalMirror || this.externalMirror.threadId !== snapshot.threadId || this.externalMirror.rolloutPath !== snapshot.rolloutPath) {
856
+ if (!this.externalMirror || this.externalMirror.threadId !== snapshot.threadId || this.externalMirror.rolloutPath !== snapshot.sourcePath) {
502
857
  this.externalMirror = {
503
858
  threadId: snapshot.threadId,
504
- rolloutPath: snapshot.rolloutPath,
859
+ rolloutPath: snapshot.sourcePath,
505
860
  lastLine: snapshot.lineCount,
506
861
  turnId: snapshot.activity.turnId,
507
862
  startedAt: snapshot.activity.startedAt?.toISOString() ?? null,
@@ -555,16 +910,21 @@ export class RelayRuntime {
555
910
  workspace: info.workspace,
556
911
  agentId: info.agentId,
557
912
  prompt: snapshot.latestUserMessage ?? undefined,
558
- detail: `Codex CLI task ${terminalEvent.status ?? "finished"}.`,
913
+ detail: `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`,
559
914
  durationMs: durationFromDates(externalStartedAt, terminalEvent.timestamp),
560
915
  });
561
- this.broadcastStatus(`Codex CLI task ${terminalEvent.status ?? "finished"}.`, terminalEvent.status === "failed" ? "error" : terminalEvent.status === "aborted" ? "warn" : "info");
916
+ if (externalStartedAt && terminalEvent.turnId) {
917
+ await this.persistWorkspaceArtifactsForTurn(info.workspace, terminalEvent.turnId, externalStartedAt);
918
+ }
919
+ mirror.latestStatus = `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`;
920
+ this.broadcastStatus(`${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`, terminalEvent.status === "failed" ? "error" : terminalEvent.status === "aborted" ? "warn" : "info");
562
921
  this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
922
+ await this.drainQueue();
563
923
  }
564
924
  mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
565
925
  }
566
926
  startExternalTurn(snapshot) {
567
- const prompt = snapshot.latestUserMessage ?? "Codex CLI task";
927
+ const prompt = snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`;
568
928
  this.chatStore.append({
569
929
  threadId: snapshot.threadId,
570
930
  role: "user",
@@ -586,7 +946,7 @@ export class RelayRuntime {
586
946
  type: "cli_turn_started",
587
947
  threadId: snapshot.threadId,
588
948
  prompt,
589
- detail: `Rollout: ${snapshot.rolloutPath}`,
949
+ detail: `${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
590
950
  });
591
951
  }
592
952
  broadcastExternalEvents(snapshot, events) {
@@ -619,12 +979,79 @@ export class RelayRuntime {
619
979
  async getSession(deferThreadStart) {
620
980
  return this.registry.getOrCreate(WEB_CONTEXT_KEY, { deferThreadStart });
621
981
  }
982
+ async getControlSession(agentId) {
983
+ const active = await this.getSession(true);
984
+ const activeInfo = this.publicInfo(active);
985
+ if (!agentId || agentId === activeInfo.agentId) {
986
+ return { session: active, dispose: false };
987
+ }
988
+ if (!enabledAgents(this.config).includes(agentId)) {
989
+ throw new Error(`Agent is not enabled: ${agentId}`);
990
+ }
991
+ const session = await createAgentSessionService(this.config, agentId, {
992
+ deferThreadStart: true,
993
+ workspace: activeInfo.workspace,
994
+ });
995
+ return { session, dispose: true };
996
+ }
622
997
  async ensureActiveThread(session) {
623
998
  if (!session.hasActiveThread()) {
624
999
  await session.newThread();
625
1000
  this.updateSession(session);
626
1001
  }
627
1002
  }
1003
+ async checkAgentAuth(info) {
1004
+ if (info.agentId === "pi") {
1005
+ return checkPiAuthStatus(info.model);
1006
+ }
1007
+ if (info.agentId === "hermes") {
1008
+ return checkHermesAuthStatus({
1009
+ baseUrl: this.config.hermesApiBaseUrl,
1010
+ apiKey: this.config.hermesApiKey,
1011
+ });
1012
+ }
1013
+ if (info.agentId === "openclaw") {
1014
+ return checkOpenClawAuthStatus({
1015
+ gatewayUrl: this.config.openClawGatewayUrl,
1016
+ token: this.config.openClawGatewayToken,
1017
+ password: this.config.openClawGatewayPassword,
1018
+ });
1019
+ }
1020
+ if (info.agentId === "claude-code") {
1021
+ return checkClaudeCodeAuthStatus(this.config.claudeCodeCliPath);
1022
+ }
1023
+ return checkAuthStatus(this.config.codexApiKey);
1024
+ }
1025
+ async startAgentLogin(info) {
1026
+ if (info.agentId === "hermes") {
1027
+ return startHermesLogin(this.config.hermesCliPath);
1028
+ }
1029
+ if (info.agentId === "claude-code") {
1030
+ return startClaudeCodeLogin(this.config.claudeCodeCliPath);
1031
+ }
1032
+ if (info.agentId === "codex") {
1033
+ return startCodexLogin();
1034
+ }
1035
+ return {
1036
+ success: false,
1037
+ message: `${info.agentLabel} login is not managed by NordRelay. Run the agent login flow on the host.`,
1038
+ };
1039
+ }
1040
+ async startAgentLogout(info) {
1041
+ if (info.agentId === "hermes") {
1042
+ return startHermesLogout(this.config.hermesCliPath);
1043
+ }
1044
+ if (info.agentId === "claude-code") {
1045
+ return startClaudeCodeLogout(this.config.claudeCodeCliPath);
1046
+ }
1047
+ if (info.agentId === "codex") {
1048
+ return startCodexLogout();
1049
+ }
1050
+ return {
1051
+ success: false,
1052
+ message: `${info.agentLabel} logout is not managed by NordRelay. Run the agent logout flow on the host.`,
1053
+ };
1054
+ }
628
1055
  ensureIdle(session) {
629
1056
  if (session.isProcessing()) {
630
1057
  throw new Error("The active session is still processing a turn.");
@@ -634,9 +1061,9 @@ export class RelayRuntime {
634
1061
  await this.ensureActiveThread(session);
635
1062
  const info = session.getInfo();
636
1063
  if ((info.capabilities ?? CODEX_AGENT_CAPABILITIES).auth) {
637
- const auth = await checkAuthStatus(this.config.codexApiKey);
1064
+ const auth = await this.checkAgentAuth(info);
638
1065
  if (!auth.authenticated) {
639
- throw new Error(`Codex is not authenticated: ${auth.detail}`);
1066
+ throw new Error(`${agentLabel(info.agentId)} is not authenticated: ${auth.detail}`);
640
1067
  }
641
1068
  }
642
1069
  const workspacePolicy = evaluateWorkspacePolicy(session.getInfo().workspace, this.config);
@@ -647,8 +1074,24 @@ export class RelayRuntime {
647
1074
  this.currentTurnId = turnId;
648
1075
  this.currentTurnStartedAt = Date.now();
649
1076
  this.accumulatedText = "";
1077
+ this.currentProgress = {
1078
+ id: turnId,
1079
+ source: "web",
1080
+ status: "running",
1081
+ prompt: envelope.description,
1082
+ agentId: info.agentId,
1083
+ agentLabel: info.agentLabel,
1084
+ threadId: info.threadId,
1085
+ workspace: info.workspace,
1086
+ startedAt: new Date(this.currentTurnStartedAt).toISOString(),
1087
+ updatedAt: new Date(this.currentTurnStartedAt).toISOString(),
1088
+ durationMs: 0,
1089
+ outputChars: 0,
1090
+ tools: [],
1091
+ };
650
1092
  this.promptStore.setLastPrompt(WEB_CONTEXT_KEY, envelope);
651
- const startedAt = new Date().toISOString();
1093
+ const startedDate = new Date();
1094
+ const startedAt = startedDate.toISOString();
652
1095
  this.chatStore.append({
653
1096
  threadId: info.threadId ?? "pending",
654
1097
  role: "user",
@@ -666,22 +1109,45 @@ export class RelayRuntime {
666
1109
  agentId: info.agentId,
667
1110
  prompt: envelope.description,
668
1111
  });
1112
+ this.appendAudit({
1113
+ action: "prompt_started",
1114
+ status: "ok",
1115
+ contextKey: WEB_CONTEXT_KEY,
1116
+ agentId: info.agentId,
1117
+ threadId: info.threadId,
1118
+ workspace: info.workspace,
1119
+ description: envelope.description,
1120
+ });
669
1121
  this.broadcast({ type: "turn_start", id: turnId, prompt: envelope.description, at: startedAt, source: "web" });
670
1122
  const callbacks = {
671
1123
  onTextDelta: (delta) => {
672
1124
  this.accumulatedText += delta;
1125
+ this.updateCurrentProgress({ outputChars: this.accumulatedText.length });
673
1126
  this.broadcast({ type: "text_delta", id: turnId, delta });
674
1127
  },
675
- onToolStart: (toolName, toolCallId) => this.broadcast({ type: "tool_start", id: turnId, toolCallId, toolName }),
676
- onToolUpdate: (toolCallId, partialResult) => this.broadcast({ type: "tool_update", id: turnId, toolCallId, partialResult }),
677
- onToolEnd: (toolCallId, isError) => this.broadcast({ type: "tool_end", id: turnId, toolCallId, isError }),
678
- onTodoUpdate: (items) => this.broadcast({ type: "todo_update", id: turnId, items }),
1128
+ onToolStart: (toolName, toolCallId) => {
1129
+ this.addCurrentTool(toolName);
1130
+ this.broadcast({ type: "tool_start", id: turnId, toolCallId, toolName });
1131
+ },
1132
+ onToolUpdate: (toolCallId, partialResult) => {
1133
+ this.updateCurrentProgress();
1134
+ this.broadcast({ type: "tool_update", id: turnId, toolCallId, partialResult });
1135
+ },
1136
+ onToolEnd: (toolCallId, isError) => {
1137
+ this.updateCurrentProgress({ currentTool: undefined });
1138
+ this.broadcast({ type: "tool_end", id: turnId, toolCallId, isError });
1139
+ },
1140
+ onTodoUpdate: (items) => {
1141
+ this.updateCurrentProgress({ detail: `Plan: ${items.filter((item) => item.completed).length}/${items.length} done` });
1142
+ this.broadcast({ type: "todo_update", id: turnId, items });
1143
+ },
679
1144
  onTurnComplete: () => { },
680
1145
  onAgentEnd: () => this.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() }),
681
1146
  };
682
1147
  try {
683
1148
  await session.prompt(envelope.input, callbacks);
684
1149
  this.updateSession(session);
1150
+ await this.persistWorkspaceArtifactsForTurn(session.getInfo().workspace, turnId, startedDate);
685
1151
  if (this.accumulatedText.trim()) {
686
1152
  this.chatStore.append({
687
1153
  threadId: info.threadId ?? "pending",
@@ -701,6 +1167,16 @@ export class RelayRuntime {
701
1167
  prompt: envelope.description,
702
1168
  durationMs: Date.now() - this.currentTurnStartedAt,
703
1169
  });
1170
+ this.appendAudit({
1171
+ action: "prompt_completed",
1172
+ status: "ok",
1173
+ contextKey: WEB_CONTEXT_KEY,
1174
+ agentId: info.agentId,
1175
+ threadId: info.threadId,
1176
+ workspace: info.workspace,
1177
+ description: envelope.description,
1178
+ });
1179
+ this.updateCurrentProgress({ status: "completed" });
704
1180
  this.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() });
705
1181
  this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
706
1182
  }
@@ -724,12 +1200,27 @@ export class RelayRuntime {
724
1200
  detail: errorText,
725
1201
  durationMs: Date.now() - this.currentTurnStartedAt,
726
1202
  });
1203
+ this.appendAudit({
1204
+ action: "prompt_failed",
1205
+ status: "failed",
1206
+ contextKey: WEB_CONTEXT_KEY,
1207
+ agentId: info.agentId,
1208
+ threadId: info.threadId,
1209
+ workspace: info.workspace,
1210
+ description: envelope.description,
1211
+ detail: errorText,
1212
+ });
1213
+ this.updateCurrentProgress({ status: "failed", detail: errorText });
727
1214
  this.broadcast({ type: "turn_error", id: turnId, error: errorText, at: new Date().toISOString() });
728
1215
  this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
729
1216
  throw error;
730
1217
  }
731
1218
  finally {
732
1219
  this.currentTurnId = null;
1220
+ if (this.currentProgress) {
1221
+ this.currentProgress.durationMs = Date.now() - this.currentTurnStartedAt;
1222
+ this.currentProgress.updatedAt = new Date().toISOString();
1223
+ }
733
1224
  await this.drainQueue();
734
1225
  }
735
1226
  }
@@ -741,6 +1232,11 @@ export class RelayRuntime {
741
1232
  try {
742
1233
  const session = await this.getSession(false);
743
1234
  while (!session.isProcessing()) {
1235
+ const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
1236
+ if (external?.activity.active) {
1237
+ this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.queue().length} queued.`, "info");
1238
+ return;
1239
+ }
744
1240
  const next = this.promptStore.dequeue(WEB_CONTEXT_KEY);
745
1241
  this.broadcastQueue();
746
1242
  if (!next) {
@@ -762,6 +1258,62 @@ export class RelayRuntime {
762
1258
  this.broadcast({ type: "activity_update", events: this.activity({ limit: 50 }) });
763
1259
  return event;
764
1260
  }
1261
+ appendAudit(input) {
1262
+ return this.auditStore.append({ ...input, channelId: "web" });
1263
+ }
1264
+ updateCurrentProgress(patch = {}) {
1265
+ if (!this.currentProgress) {
1266
+ return;
1267
+ }
1268
+ if ("currentTool" in patch) {
1269
+ this.currentProgress.currentTool = patch.currentTool;
1270
+ const { currentTool: _currentTool, ...rest } = patch;
1271
+ Object.assign(this.currentProgress, rest);
1272
+ }
1273
+ else {
1274
+ Object.assign(this.currentProgress, patch);
1275
+ }
1276
+ this.currentProgress.durationMs = Date.now() - this.currentTurnStartedAt;
1277
+ this.currentProgress.updatedAt = new Date().toISOString();
1278
+ }
1279
+ addCurrentTool(toolName) {
1280
+ if (!this.currentProgress) {
1281
+ return;
1282
+ }
1283
+ const existing = this.currentProgress.tools.find((tool) => tool.name === toolName);
1284
+ if (existing) {
1285
+ existing.count += 1;
1286
+ }
1287
+ else {
1288
+ this.currentProgress.tools.push({ name: toolName, count: 1 });
1289
+ }
1290
+ this.updateCurrentProgress({ currentTool: toolName, lastTool: toolName });
1291
+ }
1292
+ externalTask() {
1293
+ if (!this.externalMirror) {
1294
+ return null;
1295
+ }
1296
+ const startedAt = this.externalMirror.startedAt ?? new Date().toISOString();
1297
+ const startedMs = new Date(startedAt).getTime();
1298
+ return {
1299
+ id: this.externalMirror.turnId ?? "cli",
1300
+ source: "cli",
1301
+ status: this.externalMirror.latestStatus?.includes("failed")
1302
+ ? "failed"
1303
+ : this.externalMirror.latestStatus?.includes("aborted")
1304
+ ? "aborted"
1305
+ : this.externalMirror.latestStatus?.includes("finished") || this.externalMirror.latestStatus?.includes("completed")
1306
+ ? "completed"
1307
+ : "running",
1308
+ threadId: this.externalMirror.threadId,
1309
+ startedAt,
1310
+ updatedAt: new Date().toISOString(),
1311
+ durationMs: Number.isFinite(startedMs) ? Math.max(0, Date.now() - startedMs) : 0,
1312
+ outputChars: 0,
1313
+ tools: [],
1314
+ detail: this.externalMirror.latestStatus ?? this.externalMirror.rolloutPath,
1315
+ };
1316
+ }
765
1317
  broadcastQueue() {
766
1318
  this.broadcast({ type: "queue_update", queue: this.queue(), paused: this.queuePaused() });
767
1319
  }
@@ -788,6 +1340,20 @@ export class RelayRuntime {
788
1340
  capabilities: info.capabilities ?? CODEX_AGENT_CAPABILITIES,
789
1341
  };
790
1342
  }
1343
+ async persistWorkspaceArtifactsForTurn(workspace, turnId, startedAt) {
1344
+ const report = await collectRecentWorkspaceArtifacts(workspace, {
1345
+ since: startedAt,
1346
+ until: new Date(),
1347
+ maxFileSize: this.config.maxFileSize,
1348
+ limit: 20,
1349
+ ignoreDirs: this.config.artifactIgnoreDirs,
1350
+ ignoreGlobs: this.config.artifactIgnoreGlobs,
1351
+ });
1352
+ if (report.artifacts.length === 0 && report.skippedCount === 0 && !report.omittedCount) {
1353
+ return;
1354
+ }
1355
+ await persistWorkspaceArtifactReport(workspace, turnId, report);
1356
+ }
791
1357
  }
792
1358
  function queueItemDto(item) {
793
1359
  return {
@@ -820,7 +1386,63 @@ function externalStatusLine(snapshot, queueLength) {
820
1386
  ? formatDuration((Date.now() - snapshot.activity.startedAt.getTime()) / 1000)
821
1387
  : "-";
822
1388
  const tool = snapshot.latestToolName ?? "-";
823
- return `Codex CLI running · ${elapsed} · tool ${tool} · ${queueLength} queued`;
1389
+ return `${snapshot.agentLabel} CLI running · ${elapsed} · tool ${tool} · ${queueLength} queued`;
1390
+ }
1391
+ function cliHealthForAgent(agentId, health) {
1392
+ if (agentId === "pi") {
1393
+ return { path: health.piCliPath, label: health.piCli, version: health.piCliVersion };
1394
+ }
1395
+ if (agentId === "hermes") {
1396
+ return { path: health.hermesCliPath, label: health.hermesCli, version: health.hermesCliVersion };
1397
+ }
1398
+ if (agentId === "openclaw") {
1399
+ return { path: health.openClawCliPath, label: health.openClawCli, version: health.openClawCliVersion };
1400
+ }
1401
+ if (agentId === "claude-code") {
1402
+ return { path: health.claudeCodeCliPath, label: health.claudeCodeCli, version: health.claudeCodeCliVersion };
1403
+ }
1404
+ return { path: health.codexCliPath, label: health.codexCli, version: health.codexCliVersion };
1405
+ }
1406
+ function versionCheckForAgent(agentId, versions) {
1407
+ if (agentId === "pi")
1408
+ return versions.pi;
1409
+ if (agentId === "hermes")
1410
+ return versions.hermes;
1411
+ if (agentId === "openclaw")
1412
+ return versions.openclaw;
1413
+ if (agentId === "claude-code")
1414
+ return versions.claudeCode;
1415
+ return versions.codex;
1416
+ }
1417
+ function hostLoginCommand(info, config) {
1418
+ if (info.agentId === "hermes") {
1419
+ return `${config.hermesCliPath ?? "hermes"} login --no-browser`;
1420
+ }
1421
+ if (info.agentId === "claude-code") {
1422
+ return `${config.claudeCodeCliPath ?? "claude"} auth login`;
1423
+ }
1424
+ if (info.agentId === "pi") {
1425
+ return `${config.piCliPath ?? "pi"} auth login`;
1426
+ }
1427
+ if (info.agentId === "openclaw") {
1428
+ return `${config.openClawCliPath ?? "openclaw"} login`;
1429
+ }
1430
+ return "codex login --device-auth";
1431
+ }
1432
+ function hostLogoutCommand(info, config) {
1433
+ if (info.agentId === "hermes") {
1434
+ return `${config.hermesCliPath ?? "hermes"} logout`;
1435
+ }
1436
+ if (info.agentId === "claude-code") {
1437
+ return `${config.claudeCodeCliPath ?? "claude"} auth logout`;
1438
+ }
1439
+ if (info.agentId === "pi") {
1440
+ return `${config.piCliPath ?? "pi"} auth logout`;
1441
+ }
1442
+ if (info.agentId === "openclaw") {
1443
+ return `${config.openClawCliPath ?? "openclaw"} logout`;
1444
+ }
1445
+ return "codex logout";
824
1446
  }
825
1447
  function durationFromDates(start, end) {
826
1448
  if (!start || !end) {