@nordbyte/nordrelay 0.3.0 → 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 +227 -47
- 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 +333 -161
- 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 +15 -2
- package/dist/config.js +113 -9
- package/dist/context-key.js +23 -0
- 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 +84 -3
- 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 +1073 -22
- package/dist/session-format.js +28 -18
- package/dist/session-registry.js +43 -18
- package/dist/settings-service.js +80 -26
- package/dist/state-backend.js +17 -8
- package/dist/web-dashboard-ui.js +18 -0
- package/dist/web-dashboard.js +463 -55
- package/dist/web-state.js +131 -0
- package/docker-compose.yml +1 -1
- 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 +173 -20
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
- package/CHANGELOG.md +0 -17
package/dist/relay-runtime.js
CHANGED
|
@@ -1,35 +1,70 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
|
-
import { createArtifactZipBundle, getArtifactTurnReport, ensureOutDir, listRecentArtifactReports, removeArtifactTurn, totalArtifactSize, } from "./artifacts.js";
|
|
4
|
+
import { createArtifactZipBundle, collectRecentWorkspaceArtifacts, getArtifactTurnReport, ensureOutDir, listRecentArtifactReports, persistWorkspaceArtifactReport, removeArtifactTurn, totalArtifactSize, } from "./artifacts.js";
|
|
4
5
|
import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js";
|
|
5
|
-
import { CODEX_AGENT_CAPABILITIES,
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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";
|
|
8
13
|
import { friendlyErrorText } from "./error-messages.js";
|
|
9
|
-
import {
|
|
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";
|
|
10
18
|
import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
|
|
11
19
|
import { renderSessionInfoPlain } from "./session-format.js";
|
|
20
|
+
import { SessionLockStore } from "./session-locks.js";
|
|
12
21
|
import { SessionRegistry } from "./session-registry.js";
|
|
13
22
|
import { transcribeAudio } from "./voice.js";
|
|
23
|
+
import { WebActivityStore, WebChatStore, } from "./web-state.js";
|
|
14
24
|
import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "./workspace-policy.js";
|
|
15
|
-
const WEB_CONTEXT_KEY = "
|
|
25
|
+
const WEB_CONTEXT_KEY = "web:dashboard";
|
|
16
26
|
const MAX_WEB_SESSION_PAGE_SIZE = 50;
|
|
27
|
+
const MAX_CHAT_HISTORY = 250;
|
|
28
|
+
const MAX_TEXT_PREVIEW_BYTES = 256 * 1024;
|
|
17
29
|
export class RelayRuntime {
|
|
18
30
|
config;
|
|
19
31
|
registry;
|
|
20
32
|
promptStore;
|
|
33
|
+
chatStore;
|
|
34
|
+
activityStore;
|
|
35
|
+
auditStore;
|
|
36
|
+
lockStore;
|
|
21
37
|
subscribers = new Set();
|
|
38
|
+
externalMonitor;
|
|
22
39
|
draining = false;
|
|
23
40
|
currentTurnId = null;
|
|
24
41
|
accumulatedText = "";
|
|
42
|
+
currentTurnStartedAt = 0;
|
|
43
|
+
currentProgress = null;
|
|
44
|
+
externalMirror = null;
|
|
25
45
|
constructor(config) {
|
|
26
46
|
this.config = config;
|
|
27
|
-
this.registry = new SessionRegistry(config
|
|
47
|
+
this.registry = new SessionRegistry(config, {
|
|
48
|
+
fileName: "web-contexts.json",
|
|
49
|
+
sqliteKey: "web-contexts",
|
|
50
|
+
});
|
|
28
51
|
this.promptStore = new PromptStore(config.workspace, config.stateBackend);
|
|
52
|
+
this.chatStore = new WebChatStore(config.workspace, config.stateBackend, MAX_CHAT_HISTORY);
|
|
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);
|
|
56
|
+
if (config.codexExternalBusyCheckMs > 0) {
|
|
57
|
+
this.externalMonitor = setInterval(() => {
|
|
58
|
+
void this.monitorExternalActivity().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
|
|
59
|
+
}, config.codexExternalBusyCheckMs);
|
|
60
|
+
this.externalMonitor.unref?.();
|
|
61
|
+
}
|
|
29
62
|
}
|
|
30
63
|
subscribe(callback) {
|
|
31
64
|
this.subscribers.add(callback);
|
|
32
65
|
void this.snapshot().then((data) => callback({ type: "snapshot", data })).catch(() => { });
|
|
66
|
+
void this.chatHistory().then((messages) => callback({ type: "chat_history", messages })).catch(() => { });
|
|
67
|
+
callback({ type: "activity_update", events: this.activity({ limit: 50 }) });
|
|
33
68
|
return () => this.subscribers.delete(callback);
|
|
34
69
|
}
|
|
35
70
|
async snapshot() {
|
|
@@ -39,6 +74,7 @@ export class RelayRuntime {
|
|
|
39
74
|
session: info,
|
|
40
75
|
sessionText: renderSessionInfoPlain(info),
|
|
41
76
|
queue: this.queue(),
|
|
77
|
+
queuePaused: this.queuePaused(),
|
|
42
78
|
processing: session.isProcessing(),
|
|
43
79
|
enabledAgents: enabledAgents(this.config),
|
|
44
80
|
workspaces: filterAllowedWorkspaces(session.listWorkspaces(), this.config),
|
|
@@ -46,11 +82,366 @@ export class RelayRuntime {
|
|
|
46
82
|
}
|
|
47
83
|
async status() {
|
|
48
84
|
return {
|
|
49
|
-
health: await getConnectorHealth(),
|
|
50
|
-
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 }),
|
|
87
|
+
snapshot: await this.snapshot(),
|
|
88
|
+
};
|
|
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
|
+
}
|
|
117
|
+
async diagnostics() {
|
|
118
|
+
return {
|
|
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 }),
|
|
51
121
|
snapshot: await this.snapshot(),
|
|
122
|
+
runtime: {
|
|
123
|
+
stateBackend: this.config.stateBackend,
|
|
124
|
+
sourceWorkspace: this.config.workspace,
|
|
125
|
+
queuePaused: this.promptStore.isPaused(WEB_CONTEXT_KEY),
|
|
126
|
+
externalMirror: this.externalMirror ? { ...this.externalMirror } : null,
|
|
127
|
+
agentDiagnostics: getAgentDiagnostics(await this.getSession(true), this.config),
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
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,
|
|
145
|
+
}))
|
|
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,
|
|
180
|
+
};
|
|
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
|
+
}
|
|
376
|
+
async chatHistory(limit = 200) {
|
|
377
|
+
const session = await this.getSession(true);
|
|
378
|
+
return this.chatStore.list(this.publicInfo(session).threadId, limit);
|
|
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),
|
|
52
388
|
};
|
|
53
389
|
}
|
|
390
|
+
async clearChatHistory() {
|
|
391
|
+
const session = await this.getSession(true);
|
|
392
|
+
const removed = this.chatStore.clear(this.publicInfo(session).threadId);
|
|
393
|
+
const messages = await this.chatHistory();
|
|
394
|
+
this.broadcast({ type: "chat_history", messages });
|
|
395
|
+
return { removed, messages };
|
|
396
|
+
}
|
|
397
|
+
activity(options = {}) {
|
|
398
|
+
return this.activityStore.list(options);
|
|
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
|
+
}
|
|
54
445
|
async listSessions(limit = 80, query = "") {
|
|
55
446
|
return this.filteredSessions(await this.getSession(true), query, Math.max(1, limit * 3)).slice(0, limit);
|
|
56
447
|
}
|
|
@@ -90,7 +481,12 @@ export class RelayRuntime {
|
|
|
90
481
|
});
|
|
91
482
|
}
|
|
92
483
|
async listModels() {
|
|
93
|
-
|
|
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();
|
|
94
490
|
}
|
|
95
491
|
async setAgent(agentId) {
|
|
96
492
|
if (!enabledAgents(this.config).includes(agentId)) {
|
|
@@ -101,10 +497,32 @@ export class RelayRuntime {
|
|
|
101
497
|
return this.publicInfo(session);
|
|
102
498
|
}
|
|
103
499
|
async newSession(options = {}) {
|
|
104
|
-
const session = await this.getSession(true);
|
|
500
|
+
const session = options.agentId ? await this.registry.switchAgent(WEB_CONTEXT_KEY, options.agentId) : await this.getSession(true);
|
|
105
501
|
this.ensureIdle(session);
|
|
502
|
+
if (options.reasoningEffort) {
|
|
503
|
+
const reasoningOptions = agentReasoningOptions(session.getInfo().agentId);
|
|
504
|
+
if (!reasoningOptions.includes(options.reasoningEffort)) {
|
|
505
|
+
throw new Error(`Invalid ${agentReasoningLabel(session.getInfo().agentId)} value: ${options.reasoningEffort}`);
|
|
506
|
+
}
|
|
507
|
+
session.setReasoningEffort(options.reasoningEffort);
|
|
508
|
+
}
|
|
509
|
+
if (options.launchProfileId && (session.getInfo().capabilities ?? CODEX_AGENT_CAPABILITIES).launchProfiles) {
|
|
510
|
+
session.setLaunchProfile(options.launchProfileId);
|
|
511
|
+
}
|
|
512
|
+
if (typeof options.fastMode === "boolean" && (session.getInfo().capabilities ?? CODEX_AGENT_CAPABILITIES).fastMode) {
|
|
513
|
+
session.setFastMode(options.fastMode);
|
|
514
|
+
}
|
|
106
515
|
const info = await session.newThread(options.workspace, options.model);
|
|
107
516
|
this.updateSession(session);
|
|
517
|
+
this.appendActivity({
|
|
518
|
+
source: "web",
|
|
519
|
+
status: "info",
|
|
520
|
+
type: "session_new",
|
|
521
|
+
threadId: info.threadId,
|
|
522
|
+
workspace: info.workspace,
|
|
523
|
+
agentId: info.agentId,
|
|
524
|
+
detail: "New dashboard session created.",
|
|
525
|
+
});
|
|
108
526
|
return this.publicInfo(session);
|
|
109
527
|
}
|
|
110
528
|
async switchSession(threadId) {
|
|
@@ -112,6 +530,16 @@ export class RelayRuntime {
|
|
|
112
530
|
this.ensureIdle(session);
|
|
113
531
|
const info = await session.switchSession(threadId);
|
|
114
532
|
this.updateSession(session);
|
|
533
|
+
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
534
|
+
this.appendActivity({
|
|
535
|
+
source: "web",
|
|
536
|
+
status: "info",
|
|
537
|
+
type: "session_switch",
|
|
538
|
+
threadId: info.threadId,
|
|
539
|
+
workspace: info.workspace,
|
|
540
|
+
agentId: info.agentId,
|
|
541
|
+
detail: "Dashboard switched session.",
|
|
542
|
+
});
|
|
115
543
|
return this.publicInfo(session);
|
|
116
544
|
}
|
|
117
545
|
async attachSession(threadId) {
|
|
@@ -127,7 +555,7 @@ export class RelayRuntime {
|
|
|
127
555
|
async setReasoningEffort(effort) {
|
|
128
556
|
const session = await this.getSession(true);
|
|
129
557
|
this.ensureIdle(session);
|
|
130
|
-
const options = session.getInfo().agentId
|
|
558
|
+
const options = agentReasoningOptions(session.getInfo().agentId);
|
|
131
559
|
if (!options.includes(effort)) {
|
|
132
560
|
throw new Error(`Invalid ${agentReasoningLabel(session.getInfo().agentId)} value: ${effort}`);
|
|
133
561
|
}
|
|
@@ -161,6 +589,16 @@ export class RelayRuntime {
|
|
|
161
589
|
}
|
|
162
590
|
async abort() {
|
|
163
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
|
+
}
|
|
164
602
|
await session.abort();
|
|
165
603
|
this.broadcast({ type: "status", level: "warn", message: "Current operation aborted.", at: new Date().toISOString() });
|
|
166
604
|
}
|
|
@@ -238,8 +676,35 @@ export class RelayRuntime {
|
|
|
238
676
|
}
|
|
239
677
|
async sendEnvelope(envelope) {
|
|
240
678
|
const session = await this.getSession(false);
|
|
241
|
-
|
|
679
|
+
const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
680
|
+
if (session.isProcessing() || external?.activity.active) {
|
|
242
681
|
const queued = this.promptStore.enqueue(WEB_CONTEXT_KEY, envelope);
|
|
682
|
+
const info = this.publicInfo(session);
|
|
683
|
+
this.appendActivity({
|
|
684
|
+
source: "web",
|
|
685
|
+
status: "queued",
|
|
686
|
+
type: "prompt_queued",
|
|
687
|
+
threadId: info.threadId,
|
|
688
|
+
workspace: info.workspace,
|
|
689
|
+
agentId: info.agentId,
|
|
690
|
+
prompt: envelope.description,
|
|
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,
|
|
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
|
+
}
|
|
243
708
|
this.broadcastQueue();
|
|
244
709
|
return { queued: true, queueId: queued.id };
|
|
245
710
|
}
|
|
@@ -251,6 +716,9 @@ export class RelayRuntime {
|
|
|
251
716
|
queue() {
|
|
252
717
|
return this.promptStore.list(WEB_CONTEXT_KEY).map(queueItemDto);
|
|
253
718
|
}
|
|
719
|
+
queuePaused() {
|
|
720
|
+
return this.promptStore.isPaused(WEB_CONTEXT_KEY);
|
|
721
|
+
}
|
|
254
722
|
queueAction(action, id) {
|
|
255
723
|
if (action === "pause")
|
|
256
724
|
this.promptStore.pause(WEB_CONTEXT_KEY);
|
|
@@ -272,6 +740,20 @@ export class RelayRuntime {
|
|
|
272
740
|
this.promptStore.enqueueFront(WEB_CONTEXT_KEY, item);
|
|
273
741
|
void this.drainQueue().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
|
|
274
742
|
}
|
|
743
|
+
this.appendActivity({
|
|
744
|
+
source: "web",
|
|
745
|
+
status: "info",
|
|
746
|
+
type: "queue_updated",
|
|
747
|
+
threadId: null,
|
|
748
|
+
workspace: this.config.workspace,
|
|
749
|
+
detail: id ? `${action}: ${id}` : action,
|
|
750
|
+
});
|
|
751
|
+
this.appendAudit({
|
|
752
|
+
action: "queue_updated",
|
|
753
|
+
status: "ok",
|
|
754
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
755
|
+
description: id ? `${action}: ${id}` : action,
|
|
756
|
+
});
|
|
275
757
|
this.broadcastQueue();
|
|
276
758
|
return this.queue();
|
|
277
759
|
}
|
|
@@ -298,6 +780,38 @@ export class RelayRuntime {
|
|
|
298
780
|
});
|
|
299
781
|
return bundle ? { path: bundle.localPath, name: bundle.name } : null;
|
|
300
782
|
}
|
|
783
|
+
async artifactPreview(turnId, relativePath) {
|
|
784
|
+
const report = await this.artifact(turnId);
|
|
785
|
+
const artifact = report?.artifacts.find((candidate) => candidate.relativePath.split(path.sep).join("/") === relativePath);
|
|
786
|
+
if (!artifact) {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
const extension = path.extname(artifact.name).toLowerCase();
|
|
790
|
+
if ([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"].includes(extension)) {
|
|
791
|
+
return {
|
|
792
|
+
kind: "image",
|
|
793
|
+
name: artifact.name,
|
|
794
|
+
sizeBytes: artifact.sizeBytes,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
if (!isPreviewableTextFile(extension, artifact.sizeBytes)) {
|
|
798
|
+
return {
|
|
799
|
+
kind: "unsupported",
|
|
800
|
+
name: artifact.name,
|
|
801
|
+
sizeBytes: artifact.sizeBytes,
|
|
802
|
+
detail: artifact.sizeBytes > MAX_TEXT_PREVIEW_BYTES ? "File is too large for inline preview." : "File type is not previewable.",
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
const buffer = await readFile(artifact.localPath);
|
|
806
|
+
const truncated = buffer.byteLength > MAX_TEXT_PREVIEW_BYTES;
|
|
807
|
+
return {
|
|
808
|
+
kind: "text",
|
|
809
|
+
name: artifact.name,
|
|
810
|
+
sizeBytes: artifact.sizeBytes,
|
|
811
|
+
truncated,
|
|
812
|
+
text: buffer.subarray(0, MAX_TEXT_PREVIEW_BYTES).toString("utf8"),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
301
815
|
async logs(target = "connector", lines = 100) {
|
|
302
816
|
if (target === "update") {
|
|
303
817
|
const { getUpdateLogPath } = await import("./operations.js");
|
|
@@ -305,19 +819,239 @@ export class RelayRuntime {
|
|
|
305
819
|
}
|
|
306
820
|
return readFormattedLogTail(lines);
|
|
307
821
|
}
|
|
822
|
+
restartConnector() {
|
|
823
|
+
spawnConnectorRestart();
|
|
824
|
+
this.broadcastStatus("Restart requested. The dashboard may disconnect briefly.", "warn");
|
|
825
|
+
this.appendActivity({
|
|
826
|
+
source: "web",
|
|
827
|
+
status: "info",
|
|
828
|
+
type: "restart_requested",
|
|
829
|
+
threadId: null,
|
|
830
|
+
workspace: this.config.workspace,
|
|
831
|
+
detail: "Dashboard requested a connector restart.",
|
|
832
|
+
});
|
|
833
|
+
return { ok: true, message: "Restart requested." };
|
|
834
|
+
}
|
|
308
835
|
dispose() {
|
|
836
|
+
if (this.externalMonitor) {
|
|
837
|
+
clearInterval(this.externalMonitor);
|
|
838
|
+
}
|
|
309
839
|
this.registry.disposeAll();
|
|
310
840
|
this.subscribers.clear();
|
|
311
841
|
}
|
|
842
|
+
async monitorExternalActivity() {
|
|
843
|
+
const session = await this.getSession(true);
|
|
844
|
+
const info = this.publicInfo(session);
|
|
845
|
+
if (!info.capabilities.externalActivity || !info.threadId || session.isProcessing()) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const snapshot = getExternalSnapshotForSession(session, this.config, {
|
|
849
|
+
afterLine: this.externalMirror?.threadId === info.threadId ? this.externalMirror.lastLine : Number.MAX_SAFE_INTEGER,
|
|
850
|
+
}) ?? getExternalSnapshotForSession(session, this.config, {
|
|
851
|
+
maxEvents: 0,
|
|
852
|
+
});
|
|
853
|
+
if (!snapshot) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (!this.externalMirror || this.externalMirror.threadId !== snapshot.threadId || this.externalMirror.rolloutPath !== snapshot.sourcePath) {
|
|
857
|
+
this.externalMirror = {
|
|
858
|
+
threadId: snapshot.threadId,
|
|
859
|
+
rolloutPath: snapshot.sourcePath,
|
|
860
|
+
lastLine: snapshot.lineCount,
|
|
861
|
+
turnId: snapshot.activity.turnId,
|
|
862
|
+
startedAt: snapshot.activity.startedAt?.toISOString() ?? null,
|
|
863
|
+
};
|
|
864
|
+
if (snapshot.activity.active) {
|
|
865
|
+
this.startExternalTurn(snapshot);
|
|
866
|
+
}
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const mirror = this.externalMirror;
|
|
870
|
+
if (snapshot.activity.active) {
|
|
871
|
+
if (mirror.turnId !== snapshot.activity.turnId) {
|
|
872
|
+
mirror.turnId = snapshot.activity.turnId;
|
|
873
|
+
mirror.startedAt = snapshot.activity.startedAt?.toISOString() ?? null;
|
|
874
|
+
mirror.latestAgentLine = undefined;
|
|
875
|
+
this.startExternalTurn(snapshot);
|
|
876
|
+
}
|
|
877
|
+
this.broadcastExternalEvents(snapshot, snapshot.events.filter((event) => event.lineNumber > mirror.lastLine));
|
|
878
|
+
mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
|
|
879
|
+
mirror.latestStatus = externalStatusLine(snapshot, this.queue().length);
|
|
880
|
+
this.broadcastStatus(mirror.latestStatus, "info");
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const terminalEvent = [...snapshot.events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
|
|
884
|
+
if (terminalEvent && terminalEvent.lineNumber > mirror.lastLine) {
|
|
885
|
+
const finalAgent = snapshot.events.filter((event) => event.kind === "agent" && event.text).at(-1);
|
|
886
|
+
const finalText = finalAgent?.text ?? snapshot.latestAgentMessage;
|
|
887
|
+
const finalLine = finalAgent?.lineNumber ?? snapshot.lineCount;
|
|
888
|
+
if (finalText && finalLine !== mirror.latestAgentLine) {
|
|
889
|
+
this.chatStore.append({
|
|
890
|
+
threadId: snapshot.threadId,
|
|
891
|
+
role: "agent",
|
|
892
|
+
text: finalText,
|
|
893
|
+
source: "cli",
|
|
894
|
+
turnId: terminalEvent.turnId ?? undefined,
|
|
895
|
+
});
|
|
896
|
+
this.broadcast({ type: "text_delta", id: terminalEvent.turnId ?? "cli", delta: finalText });
|
|
897
|
+
mirror.latestAgentLine = finalLine;
|
|
898
|
+
}
|
|
899
|
+
const externalStartedAt = mirror.startedAt ? new Date(mirror.startedAt) : snapshot.activity.startedAt;
|
|
900
|
+
this.broadcast({
|
|
901
|
+
type: "turn_complete",
|
|
902
|
+
id: terminalEvent.turnId ?? "cli",
|
|
903
|
+
at: terminalEvent.timestamp?.toISOString() ?? new Date().toISOString(),
|
|
904
|
+
});
|
|
905
|
+
this.appendActivity({
|
|
906
|
+
source: "cli",
|
|
907
|
+
status: terminalEvent.status === "aborted" ? "aborted" : terminalEvent.status === "failed" ? "failed" : "completed",
|
|
908
|
+
type: "cli_turn_finished",
|
|
909
|
+
threadId: snapshot.threadId,
|
|
910
|
+
workspace: info.workspace,
|
|
911
|
+
agentId: info.agentId,
|
|
912
|
+
prompt: snapshot.latestUserMessage ?? undefined,
|
|
913
|
+
detail: `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`,
|
|
914
|
+
durationMs: durationFromDates(externalStartedAt, terminalEvent.timestamp),
|
|
915
|
+
});
|
|
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");
|
|
921
|
+
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
922
|
+
await this.drainQueue();
|
|
923
|
+
}
|
|
924
|
+
mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
|
|
925
|
+
}
|
|
926
|
+
startExternalTurn(snapshot) {
|
|
927
|
+
const prompt = snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`;
|
|
928
|
+
this.chatStore.append({
|
|
929
|
+
threadId: snapshot.threadId,
|
|
930
|
+
role: "user",
|
|
931
|
+
text: prompt,
|
|
932
|
+
source: "cli",
|
|
933
|
+
turnId: snapshot.activity.turnId ?? undefined,
|
|
934
|
+
timestamp: snapshot.activity.startedAt?.toISOString(),
|
|
935
|
+
});
|
|
936
|
+
this.broadcast({
|
|
937
|
+
type: "turn_start",
|
|
938
|
+
id: snapshot.activity.turnId ?? "cli",
|
|
939
|
+
prompt,
|
|
940
|
+
at: snapshot.activity.startedAt?.toISOString() ?? new Date().toISOString(),
|
|
941
|
+
source: "cli",
|
|
942
|
+
});
|
|
943
|
+
this.appendActivity({
|
|
944
|
+
source: "cli",
|
|
945
|
+
status: "running",
|
|
946
|
+
type: "cli_turn_started",
|
|
947
|
+
threadId: snapshot.threadId,
|
|
948
|
+
prompt,
|
|
949
|
+
detail: `${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
broadcastExternalEvents(snapshot, events) {
|
|
953
|
+
for (const event of events) {
|
|
954
|
+
if (event.kind === "tool" && event.status === "started") {
|
|
955
|
+
this.broadcast({
|
|
956
|
+
type: "tool_start",
|
|
957
|
+
id: snapshot.activity.turnId ?? "cli",
|
|
958
|
+
toolCallId: `cli-${event.lineNumber}`,
|
|
959
|
+
toolName: event.toolName ?? "tool",
|
|
960
|
+
});
|
|
961
|
+
this.appendActivity({
|
|
962
|
+
source: "cli",
|
|
963
|
+
status: "running",
|
|
964
|
+
type: "cli_tool_started",
|
|
965
|
+
threadId: snapshot.threadId,
|
|
966
|
+
detail: event.toolName ?? "tool",
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
if (event.kind === "tool" && event.status === "finished") {
|
|
970
|
+
this.broadcast({
|
|
971
|
+
type: "tool_end",
|
|
972
|
+
id: snapshot.activity.turnId ?? "cli",
|
|
973
|
+
toolCallId: `cli-${event.lineNumber}`,
|
|
974
|
+
isError: false,
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
312
979
|
async getSession(deferThreadStart) {
|
|
313
980
|
return this.registry.getOrCreate(WEB_CONTEXT_KEY, { deferThreadStart });
|
|
314
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
|
+
}
|
|
315
997
|
async ensureActiveThread(session) {
|
|
316
998
|
if (!session.hasActiveThread()) {
|
|
317
999
|
await session.newThread();
|
|
318
1000
|
this.updateSession(session);
|
|
319
1001
|
}
|
|
320
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
|
+
}
|
|
321
1055
|
ensureIdle(session) {
|
|
322
1056
|
if (session.isProcessing()) {
|
|
323
1057
|
throw new Error("The active session is still processing a turn.");
|
|
@@ -327,9 +1061,9 @@ export class RelayRuntime {
|
|
|
327
1061
|
await this.ensureActiveThread(session);
|
|
328
1062
|
const info = session.getInfo();
|
|
329
1063
|
if ((info.capabilities ?? CODEX_AGENT_CAPABILITIES).auth) {
|
|
330
|
-
const auth = await
|
|
1064
|
+
const auth = await this.checkAgentAuth(info);
|
|
331
1065
|
if (!auth.authenticated) {
|
|
332
|
-
throw new Error(
|
|
1066
|
+
throw new Error(`${agentLabel(info.agentId)} is not authenticated: ${auth.detail}`);
|
|
333
1067
|
}
|
|
334
1068
|
}
|
|
335
1069
|
const workspacePolicy = evaluateWorkspacePolicy(session.getInfo().workspace, this.config);
|
|
@@ -338,32 +1072,155 @@ export class RelayRuntime {
|
|
|
338
1072
|
}
|
|
339
1073
|
const turnId = randomUUID().slice(0, 12);
|
|
340
1074
|
this.currentTurnId = turnId;
|
|
1075
|
+
this.currentTurnStartedAt = Date.now();
|
|
341
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
|
+
};
|
|
342
1092
|
this.promptStore.setLastPrompt(WEB_CONTEXT_KEY, envelope);
|
|
343
|
-
|
|
1093
|
+
const startedDate = new Date();
|
|
1094
|
+
const startedAt = startedDate.toISOString();
|
|
1095
|
+
this.chatStore.append({
|
|
1096
|
+
threadId: info.threadId ?? "pending",
|
|
1097
|
+
role: "user",
|
|
1098
|
+
text: envelope.description,
|
|
1099
|
+
source: "web",
|
|
1100
|
+
turnId,
|
|
1101
|
+
timestamp: startedAt,
|
|
1102
|
+
});
|
|
1103
|
+
this.appendActivity({
|
|
1104
|
+
source: "web",
|
|
1105
|
+
status: "running",
|
|
1106
|
+
type: "prompt_started",
|
|
1107
|
+
threadId: info.threadId,
|
|
1108
|
+
workspace: info.workspace,
|
|
1109
|
+
agentId: info.agentId,
|
|
1110
|
+
prompt: envelope.description,
|
|
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
|
+
});
|
|
1121
|
+
this.broadcast({ type: "turn_start", id: turnId, prompt: envelope.description, at: startedAt, source: "web" });
|
|
344
1122
|
const callbacks = {
|
|
345
1123
|
onTextDelta: (delta) => {
|
|
346
1124
|
this.accumulatedText += delta;
|
|
1125
|
+
this.updateCurrentProgress({ outputChars: this.accumulatedText.length });
|
|
347
1126
|
this.broadcast({ type: "text_delta", id: turnId, delta });
|
|
348
1127
|
},
|
|
349
|
-
onToolStart: (toolName, toolCallId) =>
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
+
},
|
|
353
1144
|
onTurnComplete: () => { },
|
|
354
1145
|
onAgentEnd: () => this.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() }),
|
|
355
1146
|
};
|
|
356
1147
|
try {
|
|
357
1148
|
await session.prompt(envelope.input, callbacks);
|
|
358
1149
|
this.updateSession(session);
|
|
1150
|
+
await this.persistWorkspaceArtifactsForTurn(session.getInfo().workspace, turnId, startedDate);
|
|
1151
|
+
if (this.accumulatedText.trim()) {
|
|
1152
|
+
this.chatStore.append({
|
|
1153
|
+
threadId: info.threadId ?? "pending",
|
|
1154
|
+
role: "agent",
|
|
1155
|
+
text: this.accumulatedText,
|
|
1156
|
+
source: "web",
|
|
1157
|
+
turnId,
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
this.appendActivity({
|
|
1161
|
+
source: "web",
|
|
1162
|
+
status: "completed",
|
|
1163
|
+
type: "prompt_completed",
|
|
1164
|
+
threadId: info.threadId,
|
|
1165
|
+
workspace: info.workspace,
|
|
1166
|
+
agentId: info.agentId,
|
|
1167
|
+
prompt: envelope.description,
|
|
1168
|
+
durationMs: Date.now() - this.currentTurnStartedAt,
|
|
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" });
|
|
359
1180
|
this.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() });
|
|
1181
|
+
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
360
1182
|
}
|
|
361
1183
|
catch (error) {
|
|
362
|
-
|
|
1184
|
+
const errorText = friendlyErrorText(error);
|
|
1185
|
+
this.chatStore.append({
|
|
1186
|
+
threadId: info.threadId ?? "pending",
|
|
1187
|
+
role: "system",
|
|
1188
|
+
text: `Error: ${errorText}`,
|
|
1189
|
+
source: "web",
|
|
1190
|
+
turnId,
|
|
1191
|
+
});
|
|
1192
|
+
this.appendActivity({
|
|
1193
|
+
source: "web",
|
|
1194
|
+
status: "failed",
|
|
1195
|
+
type: "prompt_failed",
|
|
1196
|
+
threadId: info.threadId,
|
|
1197
|
+
workspace: info.workspace,
|
|
1198
|
+
agentId: info.agentId,
|
|
1199
|
+
prompt: envelope.description,
|
|
1200
|
+
detail: errorText,
|
|
1201
|
+
durationMs: Date.now() - this.currentTurnStartedAt,
|
|
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 });
|
|
1214
|
+
this.broadcast({ type: "turn_error", id: turnId, error: errorText, at: new Date().toISOString() });
|
|
1215
|
+
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
363
1216
|
throw error;
|
|
364
1217
|
}
|
|
365
1218
|
finally {
|
|
366
1219
|
this.currentTurnId = null;
|
|
1220
|
+
if (this.currentProgress) {
|
|
1221
|
+
this.currentProgress.durationMs = Date.now() - this.currentTurnStartedAt;
|
|
1222
|
+
this.currentProgress.updatedAt = new Date().toISOString();
|
|
1223
|
+
}
|
|
367
1224
|
await this.drainQueue();
|
|
368
1225
|
}
|
|
369
1226
|
}
|
|
@@ -375,6 +1232,11 @@ export class RelayRuntime {
|
|
|
375
1232
|
try {
|
|
376
1233
|
const session = await this.getSession(false);
|
|
377
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
|
+
}
|
|
378
1240
|
const next = this.promptStore.dequeue(WEB_CONTEXT_KEY);
|
|
379
1241
|
this.broadcastQueue();
|
|
380
1242
|
if (!next) {
|
|
@@ -391,8 +1253,69 @@ export class RelayRuntime {
|
|
|
391
1253
|
this.registry.updateMetadata(WEB_CONTEXT_KEY, session);
|
|
392
1254
|
this.broadcast({ type: "session_update", session: this.publicInfo(session) });
|
|
393
1255
|
}
|
|
1256
|
+
appendActivity(input) {
|
|
1257
|
+
const event = this.activityStore.append(input);
|
|
1258
|
+
this.broadcast({ type: "activity_update", events: this.activity({ limit: 50 }) });
|
|
1259
|
+
return event;
|
|
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
|
+
}
|
|
394
1317
|
broadcastQueue() {
|
|
395
|
-
this.broadcast({ type: "queue_update", queue: this.queue() });
|
|
1318
|
+
this.broadcast({ type: "queue_update", queue: this.queue(), paused: this.queuePaused() });
|
|
396
1319
|
}
|
|
397
1320
|
broadcastStatus(message, level = "info") {
|
|
398
1321
|
this.broadcast({ type: "status", message, level, at: new Date().toISOString() });
|
|
@@ -417,6 +1340,20 @@ export class RelayRuntime {
|
|
|
417
1340
|
capabilities: info.capabilities ?? CODEX_AGENT_CAPABILITIES,
|
|
418
1341
|
};
|
|
419
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
|
+
}
|
|
420
1357
|
}
|
|
421
1358
|
function queueItemDto(item) {
|
|
422
1359
|
return {
|
|
@@ -444,6 +1381,86 @@ function artifactDto(report) {
|
|
|
444
1381
|
})),
|
|
445
1382
|
};
|
|
446
1383
|
}
|
|
1384
|
+
function externalStatusLine(snapshot, queueLength) {
|
|
1385
|
+
const elapsed = snapshot.activity.startedAt
|
|
1386
|
+
? formatDuration((Date.now() - snapshot.activity.startedAt.getTime()) / 1000)
|
|
1387
|
+
: "-";
|
|
1388
|
+
const tool = snapshot.latestToolName ?? "-";
|
|
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";
|
|
1446
|
+
}
|
|
1447
|
+
function durationFromDates(start, end) {
|
|
1448
|
+
if (!start || !end) {
|
|
1449
|
+
return undefined;
|
|
1450
|
+
}
|
|
1451
|
+
return Math.max(0, end.getTime() - start.getTime());
|
|
1452
|
+
}
|
|
1453
|
+
function formatDuration(seconds) {
|
|
1454
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
1455
|
+
return "-";
|
|
1456
|
+
}
|
|
1457
|
+
if (seconds < 60) {
|
|
1458
|
+
return `${Math.round(seconds)}s`;
|
|
1459
|
+
}
|
|
1460
|
+
const minutes = Math.floor(seconds / 60);
|
|
1461
|
+
const remainder = Math.round(seconds % 60);
|
|
1462
|
+
return `${minutes}m ${remainder}s`;
|
|
1463
|
+
}
|
|
447
1464
|
function normalizeMimeType(value, name) {
|
|
448
1465
|
const configured = value?.trim();
|
|
449
1466
|
if (configured) {
|
|
@@ -477,3 +1494,37 @@ function uploadFileDtos(files) {
|
|
|
477
1494
|
sizeBytes: file.sizeBytes,
|
|
478
1495
|
}));
|
|
479
1496
|
}
|
|
1497
|
+
function isPreviewableTextFile(extension, sizeBytes) {
|
|
1498
|
+
if (sizeBytes > MAX_TEXT_PREVIEW_BYTES * 4) {
|
|
1499
|
+
return false;
|
|
1500
|
+
}
|
|
1501
|
+
return [
|
|
1502
|
+
"",
|
|
1503
|
+
".c",
|
|
1504
|
+
".conf",
|
|
1505
|
+
".cpp",
|
|
1506
|
+
".css",
|
|
1507
|
+
".csv",
|
|
1508
|
+
".env",
|
|
1509
|
+
".go",
|
|
1510
|
+
".html",
|
|
1511
|
+
".java",
|
|
1512
|
+
".js",
|
|
1513
|
+
".json",
|
|
1514
|
+
".jsx",
|
|
1515
|
+
".log",
|
|
1516
|
+
".md",
|
|
1517
|
+
".py",
|
|
1518
|
+
".rb",
|
|
1519
|
+
".rs",
|
|
1520
|
+
".sh",
|
|
1521
|
+
".sql",
|
|
1522
|
+
".toml",
|
|
1523
|
+
".ts",
|
|
1524
|
+
".tsx",
|
|
1525
|
+
".txt",
|
|
1526
|
+
".xml",
|
|
1527
|
+
".yaml",
|
|
1528
|
+
".yml",
|
|
1529
|
+
].includes(extension);
|
|
1530
|
+
}
|