@nordbyte/nordrelay 0.5.0 → 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.
- package/README.md +16 -10
- package/dist/access-control.js +2 -0
- package/dist/agent-updates.js +43 -8
- package/dist/bot-ui.js +1 -0
- package/dist/bot.js +108 -1063
- package/dist/channel-actions.js +8 -8
- package/dist/operations.js +63 -9
- package/dist/relay-artifact-service.js +126 -0
- package/dist/relay-external-activity-monitor.js +216 -0
- package/dist/relay-queue-service.js +66 -0
- package/dist/relay-runtime-types.js +1 -0
- package/dist/relay-runtime.js +77 -359
- package/dist/support-bundle.js +205 -0
- package/dist/telegram-agent-commands.js +212 -0
- package/dist/telegram-artifact-commands.js +139 -0
- package/dist/telegram-command-menu.js +1 -0
- package/dist/telegram-command-types.js +1 -0
- package/dist/telegram-diagnostics-command.js +102 -0
- package/dist/telegram-general-commands.js +52 -0
- package/dist/telegram-operational-commands.js +153 -0
- package/dist/telegram-preference-commands.js +198 -0
- package/dist/telegram-queue-commands.js +278 -0
- package/dist/telegram-support-command.js +53 -0
- package/dist/telegram-update-commands.js +6 -1
- package/dist/web-api-contract.js +79 -31
- package/dist/web-api-types.js +1 -0
- package/dist/web-dashboard-access-routes.js +163 -0
- package/dist/web-dashboard-artifact-routes.js +65 -0
- package/dist/web-dashboard-assets.js +2 -0
- package/dist/web-dashboard-http.js +143 -0
- package/dist/web-dashboard-pages.js +257 -0
- package/dist/web-dashboard-runtime-routes.js +92 -0
- package/dist/web-dashboard-session-routes.js +209 -0
- package/dist/web-dashboard.js +43 -882
- package/dist/webui-assets/dashboard.css +74 -4
- package/dist/webui-assets/dashboard.js +163 -24
- package/dist/zip-writer.js +83 -0
- package/package.json +10 -4
package/dist/channel-actions.js
CHANGED
|
@@ -52,13 +52,13 @@ export function renderAgentUpdatePickerAction(descriptors) {
|
|
|
52
52
|
"Agent updates:",
|
|
53
53
|
...available.map((descriptor) => `${descriptor.label}: /update ${descriptor.id}`),
|
|
54
54
|
"",
|
|
55
|
-
"Use /update jobs to list running and recent agent updates.",
|
|
55
|
+
"Use /update install <agent> for missing CLIs and /update jobs to list running and recent agent updates.",
|
|
56
56
|
].join("\n"),
|
|
57
57
|
html: [
|
|
58
58
|
"<b>Agent updates:</b>",
|
|
59
59
|
...available.map((descriptor) => `<b>${escapeHTML(descriptor.label)}:</b> <code>/update ${escapeHTML(descriptor.id)}</code>`),
|
|
60
60
|
"",
|
|
61
|
-
"Use <code>/update jobs</code> to list running and recent agent updates.",
|
|
61
|
+
"Use <code>/update install <agent></code> for missing CLIs and <code>/update jobs</code> to list running and recent agent updates.",
|
|
62
62
|
].join("\n"),
|
|
63
63
|
buttons,
|
|
64
64
|
};
|
|
@@ -86,13 +86,13 @@ export function renderAgentUpdateJobsAction(jobs) {
|
|
|
86
86
|
return {
|
|
87
87
|
plain: [
|
|
88
88
|
"Agent update jobs:",
|
|
89
|
-
...limited.map((job) => `${job.id}: ${job.agentLabel} · ${job.status} · ${formatLocalDateTime(new Date(job.updatedAt))}`),
|
|
89
|
+
...limited.map((job) => `${job.id}: ${job.agentLabel} ${job.operation ?? "update"} · ${job.status} · ${formatLocalDateTime(new Date(job.updatedAt))}`),
|
|
90
90
|
"",
|
|
91
91
|
"Use /update log <id>, /update cancel <id>, or /update input <id> <text>.",
|
|
92
92
|
].join("\n"),
|
|
93
93
|
html: [
|
|
94
94
|
"<b>Agent update jobs:</b>",
|
|
95
|
-
...limited.map((job) => `<code>${escapeHTML(job.id)}</code> ${escapeHTML(job.agentLabel)} · <b>${escapeHTML(job.status)}</b> · <code>${escapeHTML(formatLocalDateTime(new Date(job.updatedAt)))}</code>`),
|
|
95
|
+
...limited.map((job) => `<code>${escapeHTML(job.id)}</code> ${escapeHTML(job.agentLabel)} ${escapeHTML(job.operation ?? "update")} · <b>${escapeHTML(job.status)}</b> · <code>${escapeHTML(formatLocalDateTime(new Date(job.updatedAt)))}</code>`),
|
|
96
96
|
"",
|
|
97
97
|
"Use <code>/update log <id></code>, <code>/update cancel <id></code>, or <code>/update input <id> <text></code>.",
|
|
98
98
|
].join("\n"),
|
|
@@ -106,7 +106,7 @@ export function renderAgentUpdateJobAction(job) {
|
|
|
106
106
|
const tail = trimLine(job.outputTail || "(waiting for output)", 1200);
|
|
107
107
|
return {
|
|
108
108
|
plain: [
|
|
109
|
-
`${job.agentLabel} update ${job.status}.`,
|
|
109
|
+
`${job.agentLabel} ${job.operation ?? "update"} ${job.status}.`,
|
|
110
110
|
`ID: ${job.id}`,
|
|
111
111
|
`Method: ${job.method}`,
|
|
112
112
|
`Command: ${command}`,
|
|
@@ -120,7 +120,7 @@ export function renderAgentUpdateJobAction(job) {
|
|
|
120
120
|
tail,
|
|
121
121
|
].filter(Boolean).join("\n"),
|
|
122
122
|
html: [
|
|
123
|
-
`<b>${escapeHTML(job.agentLabel)} update ${escapeHTML(job.status)}.</b>`,
|
|
123
|
+
`<b>${escapeHTML(job.agentLabel)} ${escapeHTML(job.operation ?? "update")} ${escapeHTML(job.status)}.</b>`,
|
|
124
124
|
`<b>ID:</b> <code>${escapeHTML(job.id)}</code>`,
|
|
125
125
|
`<b>Method:</b> <code>${escapeHTML(job.method)}</code>`,
|
|
126
126
|
`<b>Command:</b> <code>${escapeHTML(command)}</code>`,
|
|
@@ -145,7 +145,7 @@ export function renderAgentUpdateLogAction(result) {
|
|
|
145
145
|
const tail = trimLine(result.plain || "(empty)", 3000);
|
|
146
146
|
return {
|
|
147
147
|
plain: [
|
|
148
|
-
`${result.job.agentLabel} update log`,
|
|
148
|
+
`${result.job.agentLabel} ${result.job.operation ?? "update"} log`,
|
|
149
149
|
`ID: ${result.job.id}`,
|
|
150
150
|
`Status: ${result.job.status}`,
|
|
151
151
|
`File: ${result.job.logPath}`,
|
|
@@ -153,7 +153,7 @@ export function renderAgentUpdateLogAction(result) {
|
|
|
153
153
|
tail,
|
|
154
154
|
].join("\n"),
|
|
155
155
|
html: [
|
|
156
|
-
`<b>${escapeHTML(result.job.agentLabel)} update log</b>`,
|
|
156
|
+
`<b>${escapeHTML(result.job.agentLabel)} ${escapeHTML(result.job.operation ?? "update")} log</b>`,
|
|
157
157
|
`<b>ID:</b> <code>${escapeHTML(result.job.id)}</code>`,
|
|
158
158
|
`<b>Status:</b> <code>${escapeHTML(result.job.status)}</code>`,
|
|
159
159
|
`<b>File:</b> <code>${escapeHTML(result.job.logPath)}</code>`,
|
package/dist/operations.js
CHANGED
|
@@ -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
|
|
298
|
-
if (
|
|
299
|
-
return
|
|
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:
|
|
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:
|
|
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
|
|
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 {};
|