@nordbyte/nordrelay 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.env.example +155 -64
  2. package/README.md +81 -65
  3. package/dist/access-control.js +126 -115
  4. package/dist/agent-updates.js +62 -9
  5. package/dist/bot-rendering.js +838 -0
  6. package/dist/bot-ui.js +1 -0
  7. package/dist/bot.js +342 -2498
  8. package/dist/channel-actions.js +8 -8
  9. package/dist/channel-runtime.js +89 -0
  10. package/dist/config-metadata.js +238 -0
  11. package/dist/config.js +0 -58
  12. package/dist/index.js +8 -0
  13. package/dist/operations.js +63 -9
  14. package/dist/relay-artifact-service.js +126 -0
  15. package/dist/relay-external-activity-monitor.js +216 -0
  16. package/dist/relay-queue-service.js +66 -0
  17. package/dist/relay-runtime-types.js +1 -0
  18. package/dist/relay-runtime.js +96 -354
  19. package/dist/settings-service.js +2 -117
  20. package/dist/support-bundle.js +205 -0
  21. package/dist/telegram-access-commands.js +123 -0
  22. package/dist/telegram-access-middleware.js +129 -0
  23. package/dist/telegram-agent-commands.js +212 -0
  24. package/dist/telegram-artifact-commands.js +139 -0
  25. package/dist/telegram-channel-runtime.js +132 -0
  26. package/dist/telegram-command-menu.js +55 -0
  27. package/dist/telegram-command-types.js +1 -0
  28. package/dist/telegram-diagnostics-command.js +102 -0
  29. package/dist/telegram-general-commands.js +52 -0
  30. package/dist/telegram-operational-commands.js +153 -0
  31. package/dist/telegram-output.js +216 -0
  32. package/dist/telegram-preference-commands.js +198 -0
  33. package/dist/telegram-queue-commands.js +278 -0
  34. package/dist/telegram-support-command.js +53 -0
  35. package/dist/telegram-update-commands.js +93 -0
  36. package/dist/user-management.js +708 -0
  37. package/dist/web-api-contract.js +104 -0
  38. package/dist/web-api-types.js +1 -0
  39. package/dist/web-dashboard-access-routes.js +163 -0
  40. package/dist/web-dashboard-artifact-routes.js +65 -0
  41. package/dist/web-dashboard-assets.js +35 -2
  42. package/dist/web-dashboard-http.js +143 -0
  43. package/dist/web-dashboard-pages.js +257 -0
  44. package/dist/web-dashboard-runtime-routes.js +92 -0
  45. package/dist/web-dashboard-session-routes.js +209 -0
  46. package/dist/web-dashboard-ui.js +14 -14
  47. package/dist/web-dashboard.js +330 -707
  48. package/dist/webui-assets/dashboard.css +989 -0
  49. package/dist/webui-assets/dashboard.js +1750 -0
  50. package/dist/zip-writer.js +83 -0
  51. package/package.json +13 -4
  52. package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
  53. package/plugins/nordrelay/commands/remote.md +1 -1
  54. package/plugins/nordrelay/scripts/nordrelay.mjs +227 -78
  55. package/plugins/nordrelay/skills/telegram-remote/SKILL.md +1 -1
  56. package/dist/web-dashboard-client.js +0 -275
  57. package/dist/web-dashboard-style.js +0 -9
@@ -3,7 +3,7 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync
3
3
  import { readFile, stat } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
- import { describeCodexCli, resolveCodexCli } from "./codex-cli.js";
6
+ import { describeCodexCli, findExecutableOnPath, resolveCodexCli } from "./codex-cli.js";
7
7
  import { findLatestDatabase } from "./codex-state.js";
8
8
  import { describeClaudeCodeCli, resolveClaudeCodeCli } from "./claude-code-cli.js";
9
9
  import { describeHermesCli, resolveHermesCli } from "./hermes-cli.js";
@@ -294,9 +294,9 @@ function buildNpmSelfUpdateCommands() {
294
294
  ];
295
295
  }
296
296
  function resolveNpmCommand() {
297
- const npmExecPath = process.env.npm_execpath;
298
- if (npmExecPath && existsSync(npmExecPath)) {
299
- return `${shellQuote(process.execPath)} ${shellQuote(npmExecPath)}`;
297
+ const npm = resolveNpmSpawnCommand();
298
+ if (npm) {
299
+ return [npm.command, ...npm.argsPrefix].map(shellQuote).join(" ");
300
300
  }
301
301
  return "npm";
302
302
  }
@@ -306,6 +306,7 @@ function detectCliVersion(commandPath) {
306
306
  }
307
307
  const result = spawnSync(commandPath, ["--version"], {
308
308
  encoding: "utf8",
309
+ shell: isWindowsShellScript(commandPath),
309
310
  timeout: 3000,
310
311
  windowsHide: true,
311
312
  });
@@ -320,13 +321,15 @@ function detectCliVersion(commandPath) {
320
321
  }
321
322
  function buildHermesVersionCheck(installedLabel) {
322
323
  if (installedLabel === "not installed") {
324
+ const latest = detectLatestNpmVersion(HERMES_PACKAGE_NAME);
323
325
  return {
324
326
  label: "Hermes",
325
327
  packageName: HERMES_PACKAGE_NAME,
326
328
  installedLabel: "not installed",
327
329
  installedVersion: null,
328
- latestVersion: null,
330
+ latestVersion: latest.version,
329
331
  status: "not-installed",
332
+ detail: latest.error,
330
333
  };
331
334
  }
332
335
  const lines = installedLabel.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
@@ -345,14 +348,15 @@ function buildHermesVersionCheck(installedLabel) {
345
348
  }
346
349
  function buildVersionCheck(options) {
347
350
  if (options.notInstalled) {
351
+ const latest = options.skipLatest ? { version: null, error: undefined } : detectLatestNpmVersion(options.packageName);
348
352
  return {
349
353
  label: options.label,
350
354
  packageName: options.packageName,
351
355
  installedLabel: "not installed",
352
356
  installedVersion: null,
353
- latestVersion: null,
357
+ latestVersion: latest.version,
354
358
  status: "not-installed",
355
- detail: options.detail,
359
+ detail: [options.detail, latest.error].filter(Boolean).join(" ") || undefined,
356
360
  };
357
361
  }
358
362
  if (options.skipLatest) {
@@ -393,14 +397,19 @@ function detectLatestNpmVersion(packageName) {
393
397
  if (cached) {
394
398
  return cached;
395
399
  }
396
- const result = spawnSync("npm", ["view", packageName, "version", "--registry=https://registry.npmjs.org"], {
400
+ const npm = resolveNpmSpawnCommand();
401
+ if (!npm) {
402
+ return { version: null, error: "npm was not found on PATH; latest-version lookup is unavailable" };
403
+ }
404
+ const result = spawnSync(npm.command, [...npm.argsPrefix, "view", packageName, "version", "--registry=https://registry.npmjs.org"], {
397
405
  encoding: "utf8",
406
+ shell: npm.shell,
398
407
  timeout: 5000,
399
408
  windowsHide: true,
400
409
  });
401
410
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
402
411
  if (result.error) {
403
- return { version: null, error: result.error.message };
412
+ return { version: null, error: `${npm.display}: ${result.error.message}` };
404
413
  }
405
414
  if (result.status !== 0) {
406
415
  return { version: null, error: output || `npm exited ${result.status ?? "unknown"}` };
@@ -409,6 +418,51 @@ function detectLatestNpmVersion(packageName) {
409
418
  writeVersionCache(packageName, resolved.version);
410
419
  return resolved;
411
420
  }
421
+ export function resolveNpmSpawnCommand(env = process.env) {
422
+ const npmExecPath = env.npm_execpath?.trim();
423
+ if (npmExecPath && existsSync(npmExecPath)) {
424
+ return {
425
+ command: process.execPath,
426
+ argsPrefix: [npmExecPath],
427
+ display: `${process.execPath} ${npmExecPath}`,
428
+ shell: false,
429
+ };
430
+ }
431
+ const pathMatch = findExecutableOnPath("npm", env.PATH);
432
+ if (pathMatch) {
433
+ return {
434
+ command: pathMatch,
435
+ argsPrefix: [],
436
+ display: pathMatch,
437
+ shell: isWindowsShellScript(pathMatch),
438
+ };
439
+ }
440
+ for (const candidate of commonNpmCandidates(env)) {
441
+ if (!existsSync(candidate)) {
442
+ continue;
443
+ }
444
+ return {
445
+ command: candidate,
446
+ argsPrefix: [],
447
+ display: candidate,
448
+ shell: isWindowsShellScript(candidate),
449
+ };
450
+ }
451
+ return null;
452
+ }
453
+ function commonNpmCandidates(env) {
454
+ const names = process.platform === "win32" ? ["npm.cmd", "npm.bat", "npm"] : ["npm"];
455
+ const directories = [
456
+ path.dirname(process.execPath),
457
+ env.APPDATA ? path.join(env.APPDATA, "npm") : undefined,
458
+ env.ProgramFiles ? path.join(env.ProgramFiles, "nodejs") : undefined,
459
+ env["ProgramFiles(x86)"] ? path.join(env["ProgramFiles(x86)"], "nodejs") : undefined,
460
+ ].filter((value) => Boolean(value));
461
+ return directories.flatMap((directory) => names.map((name) => path.join(directory, name)));
462
+ }
463
+ function isWindowsShellScript(filePath) {
464
+ return process.platform === "win32" && /\.(?:cmd|bat)$/i.test(filePath);
465
+ }
412
466
  function readVersionCache(packageName) {
413
467
  const ttlMs = parseVersionCacheTtlMs();
414
468
  if (ttlMs <= 0) {
@@ -0,0 +1,126 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { collectRecentWorkspaceArtifacts, createArtifactZipBundle, getArtifactTurnReport, listRecentArtifactReports, persistWorkspaceArtifactReport, removeArtifactTurn, totalArtifactSize, } from "./artifacts.js";
4
+ const MAX_TEXT_PREVIEW_BYTES = 256 * 1024;
5
+ export class RelayArtifactService {
6
+ config;
7
+ constructor(config) {
8
+ this.config = config;
9
+ }
10
+ async list(workspace, limit = 20) {
11
+ return (await listRecentArtifactReports(workspace, limit, this.config.maxFileSize)).map(artifactDto);
12
+ }
13
+ async get(workspace, turnId) {
14
+ return getArtifactTurnReport(workspace, turnId, this.config.maxFileSize);
15
+ }
16
+ async delete(workspace, turnId) {
17
+ return removeArtifactTurn(workspace, turnId);
18
+ }
19
+ async createZip(workspace, turnId) {
20
+ const report = await this.get(workspace, turnId);
21
+ if (!report) {
22
+ return null;
23
+ }
24
+ const bundle = await createArtifactZipBundle(report.artifacts, report.outDir, {
25
+ maxFileSize: this.config.maxFileSize,
26
+ bundleName: `nordrelay-artifacts-${turnId}.zip`,
27
+ });
28
+ return bundle ? { path: bundle.localPath, name: bundle.name } : null;
29
+ }
30
+ async preview(workspace, turnId, relativePath) {
31
+ const report = await this.get(workspace, turnId);
32
+ const artifact = report?.artifacts.find((candidate) => candidate.relativePath.split(path.sep).join("/") === relativePath);
33
+ if (!artifact) {
34
+ return null;
35
+ }
36
+ const extension = path.extname(artifact.name).toLowerCase();
37
+ if ([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"].includes(extension)) {
38
+ return {
39
+ kind: "image",
40
+ name: artifact.name,
41
+ sizeBytes: artifact.sizeBytes,
42
+ };
43
+ }
44
+ if (!isPreviewableTextFile(extension, artifact.sizeBytes)) {
45
+ return {
46
+ kind: "unsupported",
47
+ name: artifact.name,
48
+ sizeBytes: artifact.sizeBytes,
49
+ detail: artifact.sizeBytes > MAX_TEXT_PREVIEW_BYTES ? "File is too large for inline preview." : "File type is not previewable.",
50
+ };
51
+ }
52
+ const buffer = await readFile(artifact.localPath);
53
+ const truncated = buffer.byteLength > MAX_TEXT_PREVIEW_BYTES;
54
+ return {
55
+ kind: "text",
56
+ name: artifact.name,
57
+ sizeBytes: artifact.sizeBytes,
58
+ truncated,
59
+ text: buffer.subarray(0, MAX_TEXT_PREVIEW_BYTES).toString("utf8"),
60
+ };
61
+ }
62
+ async persistWorkspaceArtifactsForTurn(workspace, turnId, startedAt) {
63
+ const report = await collectRecentWorkspaceArtifacts(workspace, {
64
+ since: startedAt,
65
+ until: new Date(),
66
+ maxFileSize: this.config.maxFileSize,
67
+ limit: 20,
68
+ ignoreDirs: this.config.artifactIgnoreDirs,
69
+ ignoreGlobs: this.config.artifactIgnoreGlobs,
70
+ });
71
+ if (report.artifacts.length === 0 && report.skippedCount === 0 && !report.omittedCount) {
72
+ return;
73
+ }
74
+ await persistWorkspaceArtifactReport(workspace, turnId, report);
75
+ }
76
+ }
77
+ function artifactDto(report) {
78
+ return {
79
+ turnId: report.turnId,
80
+ updatedAt: report.updatedAt.toISOString(),
81
+ source: report.source,
82
+ fileCount: report.artifacts.length,
83
+ totalSizeBytes: totalArtifactSize(report.artifacts),
84
+ skippedCount: report.skippedCount,
85
+ omittedCount: report.omittedCount,
86
+ artifacts: report.artifacts.map((artifact) => ({
87
+ name: artifact.name,
88
+ relativePath: artifact.relativePath.split(path.sep).join("/"),
89
+ sizeBytes: artifact.sizeBytes,
90
+ })),
91
+ };
92
+ }
93
+ function isPreviewableTextFile(extension, sizeBytes) {
94
+ if (sizeBytes > MAX_TEXT_PREVIEW_BYTES * 4) {
95
+ return false;
96
+ }
97
+ return [
98
+ "",
99
+ ".c",
100
+ ".conf",
101
+ ".cpp",
102
+ ".css",
103
+ ".csv",
104
+ ".env",
105
+ ".go",
106
+ ".html",
107
+ ".java",
108
+ ".js",
109
+ ".json",
110
+ ".jsx",
111
+ ".log",
112
+ ".md",
113
+ ".py",
114
+ ".rb",
115
+ ".rs",
116
+ ".sh",
117
+ ".sql",
118
+ ".toml",
119
+ ".ts",
120
+ ".tsx",
121
+ ".txt",
122
+ ".xml",
123
+ ".yaml",
124
+ ".yml",
125
+ ].includes(extension);
126
+ }
@@ -0,0 +1,216 @@
1
+ import {} from "./agent.js";
2
+ import { getExternalSnapshotForSession } from "./agent-activity.js";
3
+ import { friendlyErrorText } from "./error-messages.js";
4
+ import {} from "./web-state.js";
5
+ export class RelayExternalActivityMonitor {
6
+ options;
7
+ mirror = null;
8
+ running = false;
9
+ constructor(options) {
10
+ this.options = options;
11
+ }
12
+ snapshot() {
13
+ return this.mirror ? { ...this.mirror } : null;
14
+ }
15
+ task() {
16
+ if (!this.mirror) {
17
+ return null;
18
+ }
19
+ const startedAt = this.mirror.startedAt ?? new Date().toISOString();
20
+ const startedMs = new Date(startedAt).getTime();
21
+ return {
22
+ id: this.mirror.turnId ?? "cli",
23
+ source: "cli",
24
+ status: this.mirror.latestStatus?.includes("failed")
25
+ ? "failed"
26
+ : this.mirror.latestStatus?.includes("aborted")
27
+ ? "aborted"
28
+ : this.mirror.latestStatus?.includes("finished") || this.mirror.latestStatus?.includes("completed")
29
+ ? "completed"
30
+ : "running",
31
+ threadId: this.mirror.threadId,
32
+ startedAt,
33
+ updatedAt: new Date().toISOString(),
34
+ durationMs: Number.isFinite(startedMs) ? Math.max(0, Date.now() - startedMs) : 0,
35
+ outputChars: 0,
36
+ tools: [],
37
+ detail: this.mirror.latestStatus ?? this.mirror.rolloutPath,
38
+ };
39
+ }
40
+ async monitorSafe() {
41
+ if (this.running) {
42
+ return;
43
+ }
44
+ this.running = true;
45
+ try {
46
+ await this.monitor();
47
+ }
48
+ catch (error) {
49
+ this.options.broadcastStatus(friendlyErrorText(error), "error");
50
+ }
51
+ finally {
52
+ this.running = false;
53
+ }
54
+ }
55
+ async monitor() {
56
+ const session = await this.options.getSession();
57
+ const info = this.options.publicInfo(session);
58
+ if (!info.capabilities.externalActivity || !info.threadId || session.isProcessing()) {
59
+ return;
60
+ }
61
+ const snapshot = getExternalSnapshotForSession(session, this.options.config, {
62
+ afterLine: this.mirror?.threadId === info.threadId ? this.mirror.lastLine : Number.MAX_SAFE_INTEGER,
63
+ }) ?? getExternalSnapshotForSession(session, this.options.config, {
64
+ maxEvents: 0,
65
+ });
66
+ if (!snapshot) {
67
+ return;
68
+ }
69
+ if (!this.mirror || this.mirror.threadId !== snapshot.threadId || this.mirror.rolloutPath !== snapshot.sourcePath) {
70
+ this.mirror = {
71
+ threadId: snapshot.threadId,
72
+ rolloutPath: snapshot.sourcePath,
73
+ lastLine: snapshot.lineCount,
74
+ turnId: snapshot.activity.turnId,
75
+ startedAt: snapshot.activity.startedAt?.toISOString() ?? null,
76
+ };
77
+ if (snapshot.activity.active) {
78
+ this.startExternalTurn(snapshot);
79
+ }
80
+ return;
81
+ }
82
+ const mirror = this.mirror;
83
+ if (snapshot.activity.active) {
84
+ if (mirror.turnId !== snapshot.activity.turnId) {
85
+ mirror.turnId = snapshot.activity.turnId;
86
+ mirror.startedAt = snapshot.activity.startedAt?.toISOString() ?? null;
87
+ mirror.latestAgentLine = undefined;
88
+ this.startExternalTurn(snapshot);
89
+ }
90
+ this.broadcastExternalEvents(snapshot, snapshot.events.filter((event) => event.lineNumber > mirror.lastLine));
91
+ mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
92
+ mirror.latestStatus = externalStatusLine(snapshot, this.options.queueLength());
93
+ this.options.broadcastStatus(mirror.latestStatus, "info");
94
+ return;
95
+ }
96
+ const terminalEvent = [...snapshot.events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
97
+ if (terminalEvent && terminalEvent.lineNumber > mirror.lastLine) {
98
+ const finalAgent = snapshot.events.filter((event) => event.kind === "agent" && event.text).at(-1);
99
+ const finalText = finalAgent?.text ?? snapshot.latestAgentMessage;
100
+ const finalLine = finalAgent?.lineNumber ?? snapshot.lineCount;
101
+ if (finalText && finalLine !== mirror.latestAgentLine) {
102
+ this.options.chatStore.append({
103
+ threadId: snapshot.threadId,
104
+ role: "agent",
105
+ text: finalText,
106
+ source: "cli",
107
+ turnId: terminalEvent.turnId ?? undefined,
108
+ });
109
+ this.options.broadcast({ type: "text_delta", id: terminalEvent.turnId ?? "cli", delta: finalText });
110
+ mirror.latestAgentLine = finalLine;
111
+ }
112
+ const externalStartedAt = mirror.startedAt ? new Date(mirror.startedAt) : snapshot.activity.startedAt;
113
+ this.options.broadcast({
114
+ type: "turn_complete",
115
+ id: terminalEvent.turnId ?? "cli",
116
+ at: terminalEvent.timestamp?.toISOString() ?? new Date().toISOString(),
117
+ });
118
+ this.options.appendActivity({
119
+ source: "cli",
120
+ status: terminalEvent.status === "aborted" ? "aborted" : terminalEvent.status === "failed" ? "failed" : "completed",
121
+ type: "cli_turn_finished",
122
+ threadId: snapshot.threadId,
123
+ workspace: info.workspace,
124
+ agentId: info.agentId,
125
+ prompt: snapshot.latestUserMessage ?? undefined,
126
+ detail: `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`,
127
+ durationMs: durationFromDates(externalStartedAt, terminalEvent.timestamp),
128
+ });
129
+ if (externalStartedAt && terminalEvent.turnId) {
130
+ await this.options.persistWorkspaceArtifactsForTurn(info.workspace, terminalEvent.turnId, externalStartedAt);
131
+ }
132
+ mirror.latestStatus = `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`;
133
+ this.options.broadcastStatus(`${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`, terminalEvent.status === "failed" ? "error" : terminalEvent.status === "aborted" ? "warn" : "info");
134
+ this.options.broadcast({ type: "chat_history", messages: await this.options.chatHistory() });
135
+ await this.options.drainQueue();
136
+ }
137
+ mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
138
+ }
139
+ startExternalTurn(snapshot) {
140
+ const prompt = snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`;
141
+ this.options.chatStore.append({
142
+ threadId: snapshot.threadId,
143
+ role: "user",
144
+ text: prompt,
145
+ source: "cli",
146
+ turnId: snapshot.activity.turnId ?? undefined,
147
+ timestamp: snapshot.activity.startedAt?.toISOString(),
148
+ });
149
+ this.options.broadcast({
150
+ type: "turn_start",
151
+ id: snapshot.activity.turnId ?? "cli",
152
+ prompt,
153
+ at: snapshot.activity.startedAt?.toISOString() ?? new Date().toISOString(),
154
+ source: "cli",
155
+ });
156
+ this.options.appendActivity({
157
+ source: "cli",
158
+ status: "running",
159
+ type: "cli_turn_started",
160
+ threadId: snapshot.threadId,
161
+ prompt,
162
+ detail: `${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
163
+ });
164
+ }
165
+ broadcastExternalEvents(snapshot, events) {
166
+ for (const event of events) {
167
+ if (event.kind === "tool" && event.status === "started") {
168
+ this.options.broadcast({
169
+ type: "tool_start",
170
+ id: snapshot.activity.turnId ?? "cli",
171
+ toolCallId: `cli-${event.lineNumber}`,
172
+ toolName: event.toolName ?? "tool",
173
+ });
174
+ this.options.appendActivity({
175
+ source: "cli",
176
+ status: "running",
177
+ type: "cli_tool_started",
178
+ threadId: snapshot.threadId,
179
+ detail: event.toolName ?? "tool",
180
+ });
181
+ }
182
+ if (event.kind === "tool" && event.status === "finished") {
183
+ this.options.broadcast({
184
+ type: "tool_end",
185
+ id: snapshot.activity.turnId ?? "cli",
186
+ toolCallId: `cli-${event.lineNumber}`,
187
+ isError: false,
188
+ });
189
+ }
190
+ }
191
+ }
192
+ }
193
+ function externalStatusLine(snapshot, queueLength) {
194
+ const elapsed = snapshot.activity.startedAt
195
+ ? formatDuration((Date.now() - snapshot.activity.startedAt.getTime()) / 1000)
196
+ : "-";
197
+ const tool = snapshot.latestToolName ?? "-";
198
+ return `${snapshot.agentLabel} CLI running · ${elapsed} · tool ${tool} · ${queueLength} queued`;
199
+ }
200
+ function durationFromDates(start, end) {
201
+ if (!start || !end) {
202
+ return undefined;
203
+ }
204
+ return Math.max(0, end.getTime() - start.getTime());
205
+ }
206
+ function formatDuration(seconds) {
207
+ if (!Number.isFinite(seconds) || seconds < 0) {
208
+ return "-";
209
+ }
210
+ if (seconds < 60) {
211
+ return `${Math.round(seconds)}s`;
212
+ }
213
+ const minutes = Math.floor(seconds / 60);
214
+ const remainder = Math.round(seconds % 60);
215
+ return `${minutes}m ${remainder}s`;
216
+ }
@@ -0,0 +1,66 @@
1
+ export class RelayQueueService {
2
+ promptStore;
3
+ contextKey;
4
+ constructor(promptStore, contextKey) {
5
+ this.promptStore = promptStore;
6
+ this.contextKey = contextKey;
7
+ }
8
+ list() {
9
+ return this.rawList().map(queueItemDto);
10
+ }
11
+ rawList() {
12
+ return this.promptStore.list(this.contextKey);
13
+ }
14
+ length() {
15
+ return this.rawList().length;
16
+ }
17
+ isPaused() {
18
+ return this.promptStore.isPaused(this.contextKey);
19
+ }
20
+ enqueue(envelope) {
21
+ return this.promptStore.enqueue(this.contextKey, envelope);
22
+ }
23
+ enqueueFront(item) {
24
+ this.promptStore.enqueueFront(this.contextKey, item);
25
+ }
26
+ dequeue() {
27
+ return this.promptStore.dequeue(this.contextKey);
28
+ }
29
+ setLastPrompt(envelope) {
30
+ this.promptStore.setLastPrompt(this.contextKey, envelope);
31
+ }
32
+ getLastPrompt() {
33
+ return this.promptStore.getLastPrompt(this.contextKey);
34
+ }
35
+ apply(action, id) {
36
+ if (action === "pause")
37
+ this.promptStore.pause(this.contextKey);
38
+ if (action === "resume")
39
+ this.promptStore.resume(this.contextKey);
40
+ if (action === "clear")
41
+ this.promptStore.clear(this.contextKey);
42
+ if (id && action === "cancel")
43
+ this.promptStore.remove(this.contextKey, id);
44
+ if (id && action === "top")
45
+ this.promptStore.moveToTop(this.contextKey, id);
46
+ if (id && action === "up")
47
+ this.promptStore.moveUp(this.contextKey, id);
48
+ if (id && action === "down")
49
+ this.promptStore.moveDown(this.contextKey, id);
50
+ if (id && action === "run") {
51
+ const item = this.promptStore.remove(this.contextKey, id);
52
+ if (item)
53
+ this.promptStore.enqueueFront(this.contextKey, item);
54
+ }
55
+ }
56
+ }
57
+ export function queueItemDto(item) {
58
+ return {
59
+ id: item.id,
60
+ description: item.description,
61
+ createdAt: new Date(item.createdAt).toISOString(),
62
+ attempts: item.attempts ?? 0,
63
+ notBefore: item.notBefore ? new Date(item.notBefore).toISOString() : undefined,
64
+ lastError: item.lastError,
65
+ };
66
+ }
@@ -0,0 +1 @@
1
+ export {};