@nordbyte/nordrelay 0.5.0 → 0.5.2

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 (44) hide show
  1. package/.env.example +2 -0
  2. package/README.md +23 -14
  3. package/dist/access-control.js +2 -0
  4. package/dist/agent-updates.js +61 -10
  5. package/dist/bot-ui.js +1 -0
  6. package/dist/bot.js +142 -1065
  7. package/dist/channel-actions.js +8 -8
  8. package/dist/codex-cli.js +1 -1
  9. package/dist/config-metadata.js +2 -0
  10. package/dist/operations.js +233 -122
  11. package/dist/relay-artifact-service.js +126 -0
  12. package/dist/relay-external-activity-monitor.js +216 -0
  13. package/dist/relay-queue-service.js +66 -0
  14. package/dist/relay-runtime-types.js +1 -0
  15. package/dist/relay-runtime.js +119 -371
  16. package/dist/state-backend.js +3 -0
  17. package/dist/support-bundle.js +221 -0
  18. package/dist/telegram-agent-commands.js +212 -0
  19. package/dist/telegram-artifact-commands.js +139 -0
  20. package/dist/telegram-command-menu.js +1 -0
  21. package/dist/telegram-command-types.js +1 -0
  22. package/dist/telegram-diagnostics-command.js +102 -0
  23. package/dist/telegram-general-commands.js +52 -0
  24. package/dist/telegram-operational-commands.js +153 -0
  25. package/dist/telegram-preference-commands.js +198 -0
  26. package/dist/telegram-queue-commands.js +278 -0
  27. package/dist/telegram-support-command.js +53 -0
  28. package/dist/telegram-update-commands.js +6 -1
  29. package/dist/web-api-contract.js +79 -31
  30. package/dist/web-api-types.js +1 -0
  31. package/dist/web-dashboard-access-routes.js +163 -0
  32. package/dist/web-dashboard-artifact-routes.js +65 -0
  33. package/dist/web-dashboard-assets.js +2 -0
  34. package/dist/web-dashboard-http.js +143 -0
  35. package/dist/web-dashboard-pages.js +257 -0
  36. package/dist/web-dashboard-runtime-routes.js +92 -0
  37. package/dist/web-dashboard-session-routes.js +209 -0
  38. package/dist/web-dashboard.js +44 -882
  39. package/dist/webui-assets/dashboard.css +74 -4
  40. package/dist/webui-assets/dashboard.js +163 -24
  41. package/dist/zip-writer.js +83 -0
  42. package/package.json +10 -4
  43. package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
  44. package/plugins/nordrelay/scripts/nordrelay.mjs +258 -5
@@ -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 {};