@nordbyte/nordrelay 0.4.0 → 0.4.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 +17 -5
- package/dist/access-control.js +3 -0
- package/dist/agent-feature-matrix.js +42 -0
- package/dist/agent-updates.js +294 -0
- package/dist/bot.js +169 -209
- package/dist/channel-actions.js +372 -0
- package/dist/operations.js +33 -8
- package/dist/relay-runtime.js +128 -24
- package/dist/session-format.js +72 -3
- package/dist/web-dashboard-assets.js +2 -0
- package/dist/web-dashboard-client.js +275 -0
- package/dist/web-dashboard-style.js +9 -0
- package/dist/web-dashboard.js +62 -244
- package/package.json +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +56 -9
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { formatAgentFeatureSummaryHTML, formatAgentFeatureSummaryPlain } from "./agent-feature-matrix.js";
|
|
2
|
+
import { totalArtifactSize } from "./artifacts.js";
|
|
3
|
+
import { escapeHTML } from "./format.js";
|
|
4
|
+
import { getAgentUpdateLogPath, getUpdateLogPath } from "./operations.js";
|
|
5
|
+
import { formatFileSize } from "./session-format.js";
|
|
6
|
+
export function renderChannelsAction(descriptors) {
|
|
7
|
+
const plain = [
|
|
8
|
+
"Channel adapters:",
|
|
9
|
+
...descriptors.map((descriptor) => {
|
|
10
|
+
const status = descriptor.status === "available" ? "available" : "planned";
|
|
11
|
+
return `${descriptor.label}: ${status} · ${descriptor.capabilities.join(", ")}`;
|
|
12
|
+
}),
|
|
13
|
+
].join("\n");
|
|
14
|
+
const html = [
|
|
15
|
+
"<b>Channel adapters:</b>",
|
|
16
|
+
...descriptors.map((descriptor) => {
|
|
17
|
+
const statusIcon = descriptor.status === "available" ? "✅" : "🟡";
|
|
18
|
+
const notes = descriptor.notes ? `\n ${escapeHTML(descriptor.notes)}` : "";
|
|
19
|
+
return `${statusIcon} <b>${escapeHTML(descriptor.label)}</b> <code>${escapeHTML(descriptor.status)}</code>\n <code>${escapeHTML(descriptor.capabilities.join(", "))}</code>${notes}`;
|
|
20
|
+
}),
|
|
21
|
+
].join("\n");
|
|
22
|
+
return { plain, html };
|
|
23
|
+
}
|
|
24
|
+
export function renderAgentsAction(descriptors, enabledAgents) {
|
|
25
|
+
const enabled = new Set(enabledAgents);
|
|
26
|
+
const plain = [
|
|
27
|
+
"Agent adapters:",
|
|
28
|
+
...descriptors.flatMap((descriptor) => [
|
|
29
|
+
`${descriptor.label}: ${descriptor.status}${descriptor.status === "available" ? ` · ${enabled.has(descriptor.id) ? "enabled" : "disabled"}` : ""}`,
|
|
30
|
+
...formatAgentFeatureSummaryPlain(descriptor.capabilities).map((line) => ` ${line}`),
|
|
31
|
+
]),
|
|
32
|
+
].join("\n");
|
|
33
|
+
const html = [
|
|
34
|
+
"<b>Agent adapters:</b>",
|
|
35
|
+
...descriptors.map((descriptor) => {
|
|
36
|
+
const status = descriptor.status === "available" ? `${enabled.has(descriptor.id) ? "enabled" : "disabled"}` : "planned";
|
|
37
|
+
const notes = descriptor.notes ? `\n ${escapeHTML(descriptor.notes)}` : "";
|
|
38
|
+
return [
|
|
39
|
+
`${descriptor.status === "available" ? "✅" : "🟡"} <b>${escapeHTML(descriptor.label)}</b> <code>${escapeHTML(status)}</code>${notes}`,
|
|
40
|
+
...formatAgentFeatureSummaryHTML(descriptor.capabilities).map((line) => ` ${line}`),
|
|
41
|
+
].join("\n");
|
|
42
|
+
}),
|
|
43
|
+
].join("\n");
|
|
44
|
+
return { plain, html };
|
|
45
|
+
}
|
|
46
|
+
export function renderAgentUpdatePickerAction(descriptors) {
|
|
47
|
+
const available = descriptors.filter((descriptor) => descriptor.status === "available");
|
|
48
|
+
const buttons = available.map((descriptor) => [{ label: `Update ${descriptor.label}`, action: `agent-update:start:${descriptor.id}` }]);
|
|
49
|
+
buttons.push([{ label: "Show update jobs", action: "agent-update:jobs" }]);
|
|
50
|
+
return {
|
|
51
|
+
plain: [
|
|
52
|
+
"Agent updates:",
|
|
53
|
+
...available.map((descriptor) => `${descriptor.label}: /update ${descriptor.id}`),
|
|
54
|
+
"",
|
|
55
|
+
"Use /update jobs to list running and recent agent updates.",
|
|
56
|
+
].join("\n"),
|
|
57
|
+
html: [
|
|
58
|
+
"<b>Agent updates:</b>",
|
|
59
|
+
...available.map((descriptor) => `<b>${escapeHTML(descriptor.label)}:</b> <code>/update ${escapeHTML(descriptor.id)}</code>`),
|
|
60
|
+
"",
|
|
61
|
+
"Use <code>/update jobs</code> to list running and recent agent updates.",
|
|
62
|
+
].join("\n"),
|
|
63
|
+
buttons,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function parseAgentUpdateId(value) {
|
|
67
|
+
const normalized = value?.toLowerCase();
|
|
68
|
+
if (!normalized) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (normalized === "claude") {
|
|
72
|
+
return "claude-code";
|
|
73
|
+
}
|
|
74
|
+
return ["codex", "pi", "hermes", "openclaw", "claude-code"].includes(normalized)
|
|
75
|
+
? normalized
|
|
76
|
+
: null;
|
|
77
|
+
}
|
|
78
|
+
export function renderAgentUpdateJobsAction(jobs) {
|
|
79
|
+
if (jobs.length === 0) {
|
|
80
|
+
return {
|
|
81
|
+
plain: "No agent update jobs yet. Use /update agents to start one.",
|
|
82
|
+
html: "No agent update jobs yet. Use <code>/update agents</code> to start one.",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const limited = jobs.slice(0, 10);
|
|
86
|
+
return {
|
|
87
|
+
plain: [
|
|
88
|
+
"Agent update jobs:",
|
|
89
|
+
...limited.map((job) => `${job.id}: ${job.agentLabel} · ${job.status} · ${formatLocalDateTime(new Date(job.updatedAt))}`),
|
|
90
|
+
"",
|
|
91
|
+
"Use /update log <id>, /update cancel <id>, or /update input <id> <text>.",
|
|
92
|
+
].join("\n"),
|
|
93
|
+
html: [
|
|
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>`),
|
|
96
|
+
"",
|
|
97
|
+
"Use <code>/update log <id></code>, <code>/update cancel <id></code>, or <code>/update input <id> <text></code>.",
|
|
98
|
+
].join("\n"),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function renderAgentUpdateJobAction(job) {
|
|
102
|
+
const command = [job.command, ...job.args].join(" ");
|
|
103
|
+
const inputLine = job.canInput
|
|
104
|
+
? `If the updater asks a question, reply with /update input ${job.id} <text>.`
|
|
105
|
+
: "This update job is no longer accepting input.";
|
|
106
|
+
const tail = trimLine(job.outputTail || "(waiting for output)", 1200);
|
|
107
|
+
return {
|
|
108
|
+
plain: [
|
|
109
|
+
`${job.agentLabel} update ${job.status}.`,
|
|
110
|
+
`ID: ${job.id}`,
|
|
111
|
+
`Method: ${job.method}`,
|
|
112
|
+
`Command: ${command}`,
|
|
113
|
+
`Started: ${formatLocalDateTime(new Date(job.startedAt))}`,
|
|
114
|
+
job.finishedAt ? `Finished: ${formatLocalDateTime(new Date(job.finishedAt))}` : undefined,
|
|
115
|
+
job.error ? `Error: ${job.error}` : undefined,
|
|
116
|
+
`Log: ${job.logPath}`,
|
|
117
|
+
`Agent update log: ${getAgentUpdateLogPath()}`,
|
|
118
|
+
inputLine,
|
|
119
|
+
"",
|
|
120
|
+
tail,
|
|
121
|
+
].filter(Boolean).join("\n"),
|
|
122
|
+
html: [
|
|
123
|
+
`<b>${escapeHTML(job.agentLabel)} update ${escapeHTML(job.status)}.</b>`,
|
|
124
|
+
`<b>ID:</b> <code>${escapeHTML(job.id)}</code>`,
|
|
125
|
+
`<b>Method:</b> <code>${escapeHTML(job.method)}</code>`,
|
|
126
|
+
`<b>Command:</b> <code>${escapeHTML(command)}</code>`,
|
|
127
|
+
`<b>Started:</b> <code>${escapeHTML(formatLocalDateTime(new Date(job.startedAt)))}</code>`,
|
|
128
|
+
job.finishedAt ? `<b>Finished:</b> <code>${escapeHTML(formatLocalDateTime(new Date(job.finishedAt)))}</code>` : undefined,
|
|
129
|
+
job.error ? `<b>Error:</b> ${escapeHTML(job.error)}` : undefined,
|
|
130
|
+
`<b>Log:</b> <code>${escapeHTML(job.logPath)}</code>`,
|
|
131
|
+
`<b>Agent update log:</b> <code>${escapeHTML(getAgentUpdateLogPath())}</code>`,
|
|
132
|
+
escapeHTML(inputLine),
|
|
133
|
+
"",
|
|
134
|
+
`<pre>${escapeHTML(tail)}</pre>`,
|
|
135
|
+
].filter(Boolean).join("\n"),
|
|
136
|
+
buttons: [
|
|
137
|
+
[
|
|
138
|
+
{ label: "Full log", action: `agent-update:log:${job.id}` },
|
|
139
|
+
...(job.canInput ? [{ label: "Cancel", action: `agent-update:cancel:${job.id}` }] : []),
|
|
140
|
+
],
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function renderAgentUpdateLogAction(result) {
|
|
145
|
+
const tail = trimLine(result.plain || "(empty)", 3000);
|
|
146
|
+
return {
|
|
147
|
+
plain: [
|
|
148
|
+
`${result.job.agentLabel} update log`,
|
|
149
|
+
`ID: ${result.job.id}`,
|
|
150
|
+
`Status: ${result.job.status}`,
|
|
151
|
+
`File: ${result.job.logPath}`,
|
|
152
|
+
"",
|
|
153
|
+
tail,
|
|
154
|
+
].join("\n"),
|
|
155
|
+
html: [
|
|
156
|
+
`<b>${escapeHTML(result.job.agentLabel)} update log</b>`,
|
|
157
|
+
`<b>ID:</b> <code>${escapeHTML(result.job.id)}</code>`,
|
|
158
|
+
`<b>Status:</b> <code>${escapeHTML(result.job.status)}</code>`,
|
|
159
|
+
`<b>File:</b> <code>${escapeHTML(result.job.logPath)}</code>`,
|
|
160
|
+
"",
|
|
161
|
+
`<pre>${escapeHTML(tail)}</pre>`,
|
|
162
|
+
].join("\n"),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function renderSelfUpdateStartedAction(update) {
|
|
166
|
+
return {
|
|
167
|
+
plain: [
|
|
168
|
+
"Update started.",
|
|
169
|
+
`Method: ${update.method}`,
|
|
170
|
+
update.summary,
|
|
171
|
+
`Source: ${update.sourceRoot}`,
|
|
172
|
+
`Log: ${update.logPath}`,
|
|
173
|
+
"Use /logs update after the restart or inspect update.log on the host.",
|
|
174
|
+
"Use /update agents for agent CLI updates.",
|
|
175
|
+
].join("\n"),
|
|
176
|
+
html: [
|
|
177
|
+
"<b>Update started.</b>",
|
|
178
|
+
`<b>Method:</b> <code>${escapeHTML(update.method)}</code>`,
|
|
179
|
+
escapeHTML(update.summary),
|
|
180
|
+
`<b>Source:</b> <code>${escapeHTML(update.sourceRoot)}</code>`,
|
|
181
|
+
`<b>Log:</b> <code>${escapeHTML(update.logPath)}</code>`,
|
|
182
|
+
`Use <code>/logs update</code> after the restart or inspect <code>${escapeHTML(getUpdateLogPath())}</code> on the host.`,
|
|
183
|
+
"Use <code>/update agents</code> for agent CLI updates.",
|
|
184
|
+
].join("\n"),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
export function parseLogsCommand(argument) {
|
|
188
|
+
const tokens = argument.split(/\s+/).filter(Boolean);
|
|
189
|
+
let target = "connector";
|
|
190
|
+
let lines = 80;
|
|
191
|
+
for (const token of tokens) {
|
|
192
|
+
const normalized = token.toLowerCase();
|
|
193
|
+
if (normalized === "connector" || normalized === "main") {
|
|
194
|
+
target = "connector";
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (normalized === "update" || normalized === "self-update" || normalized === "self") {
|
|
198
|
+
target = "update";
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (normalized === "agent" || normalized === "agents" || normalized === "agent-update" || normalized === "agent-updates") {
|
|
202
|
+
target = "agent-updates";
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (normalized === "all") {
|
|
206
|
+
target = "all";
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const parsedLines = Number.parseInt(token, 10);
|
|
210
|
+
if (!Number.isNaN(parsedLines)) {
|
|
211
|
+
lines = parsedLines;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { target, lines };
|
|
215
|
+
}
|
|
216
|
+
export function logTailRequests(target) {
|
|
217
|
+
if (target === "all") {
|
|
218
|
+
return [
|
|
219
|
+
{ title: "Connector" },
|
|
220
|
+
{ title: "Update", path: getUpdateLogPath() },
|
|
221
|
+
{ title: "Agent updates", path: getAgentUpdateLogPath() },
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
return [{ title: logTargetTitle(target), path: logTargetPath(target) }];
|
|
225
|
+
}
|
|
226
|
+
export function renderLogTailsAction(logs) {
|
|
227
|
+
return {
|
|
228
|
+
plain: logs.map(({ title, tail }) => renderLogTailPlain(title, tail)).join("\n\n"),
|
|
229
|
+
html: logs.map(({ title, tail }) => renderLogTailHTML(title, tail)).join("\n\n"),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
export function renderArtifactReportsAction(reports) {
|
|
233
|
+
const lines = reports.slice(0, 5).map((report, index) => {
|
|
234
|
+
const size = formatFileSize(totalArtifactSize(report.artifacts));
|
|
235
|
+
const skipped = report.skippedCount > 0 ? `, ${report.skippedCount} skipped` : "";
|
|
236
|
+
return `${index + 1}. ${report.turnId} · ${formatRelativeTime(report.updatedAt)} · ${report.artifacts.length} file${report.artifacts.length === 1 ? "" : "s"} · ${size}${skipped}`;
|
|
237
|
+
});
|
|
238
|
+
const usage = "Tap an action below, or use /artifacts latest, /artifacts zip latest, /artifacts images, /artifacts docs, /artifacts search <text>, or /artifacts delete <turn-id>.";
|
|
239
|
+
return {
|
|
240
|
+
plain: ["Recent artifacts:", ...lines, "", usage].join("\n"),
|
|
241
|
+
html: ["<b>Recent artifacts:</b>", ...lines.map(escapeHTML), "", escapeHTML(usage)].join("\n"),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
export function renderQueueListAction(queue, paused) {
|
|
245
|
+
if (queue.length === 0) {
|
|
246
|
+
return {
|
|
247
|
+
plain: paused ? "Queue is empty and paused." : "Queue is empty.",
|
|
248
|
+
html: escapeHTML(paused ? "Queue is empty and paused." : "Queue is empty."),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const lines = queue.map((item, index) => {
|
|
252
|
+
const age = formatRelativeTime(new Date(item.createdAt));
|
|
253
|
+
const attempts = item.attempts && item.attempts > 0 ? ` · attempts ${item.attempts}` : "";
|
|
254
|
+
const error = item.lastError ? ` · last error: ${trimLine(item.lastError, 80)}` : "";
|
|
255
|
+
const scheduled = item.notBefore && item.notBefore > Date.now()
|
|
256
|
+
? `scheduled ${formatLocalDateTime(new Date(item.notBefore))}`
|
|
257
|
+
: index === 0 ? "next" : `after ${index} queued item${index === 1 ? "" : "s"}`;
|
|
258
|
+
return `${index + 1}. ${item.id} · ${age} · ${scheduled}${attempts}${error} · ${item.description}`;
|
|
259
|
+
});
|
|
260
|
+
return {
|
|
261
|
+
plain: [paused ? "Queued prompts (paused):" : "Queued prompts:", ...lines].join("\n"),
|
|
262
|
+
html: [paused ? "<b>Queued prompts:</b> <code>paused</code>" : "<b>Queued prompts:</b>", ...lines.map(escapeHTML)].join("\n"),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
export function renderQueuedPromptDetailAction(item) {
|
|
266
|
+
const lines = [
|
|
267
|
+
"Queued prompt:",
|
|
268
|
+
`ID: ${item.id}`,
|
|
269
|
+
`Created: ${formatLocalDateTime(new Date(item.createdAt))}`,
|
|
270
|
+
item.notBefore ? `Scheduled: ${formatLocalDateTime(new Date(item.notBefore))}` : undefined,
|
|
271
|
+
`Attempts: ${item.attempts ?? 0}`,
|
|
272
|
+
item.lastError ? `Last error: ${item.lastError}` : undefined,
|
|
273
|
+
`Description: ${item.description}`,
|
|
274
|
+
].filter((line) => Boolean(line));
|
|
275
|
+
return {
|
|
276
|
+
plain: lines.join("\n"),
|
|
277
|
+
html: [
|
|
278
|
+
"<b>Queued prompt:</b>",
|
|
279
|
+
`<b>ID:</b> <code>${escapeHTML(item.id)}</code>`,
|
|
280
|
+
`<b>Created:</b> <code>${escapeHTML(formatLocalDateTime(new Date(item.createdAt)))}</code>`,
|
|
281
|
+
item.notBefore ? `<b>Scheduled:</b> <code>${escapeHTML(formatLocalDateTime(new Date(item.notBefore)))}</code>` : undefined,
|
|
282
|
+
`<b>Attempts:</b> <code>${item.attempts ?? 0}</code>`,
|
|
283
|
+
item.lastError ? `<b>Last error:</b> ${escapeHTML(item.lastError)}` : undefined,
|
|
284
|
+
`<b>Description:</b> ${escapeHTML(item.description)}`,
|
|
285
|
+
].filter((line) => Boolean(line)).join("\n"),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function logTargetTitle(target) {
|
|
289
|
+
if (target === "update") {
|
|
290
|
+
return "Update";
|
|
291
|
+
}
|
|
292
|
+
if (target === "agent-updates") {
|
|
293
|
+
return "Agent updates";
|
|
294
|
+
}
|
|
295
|
+
return "Connector";
|
|
296
|
+
}
|
|
297
|
+
function logTargetPath(target) {
|
|
298
|
+
if (target === "update") {
|
|
299
|
+
return getUpdateLogPath();
|
|
300
|
+
}
|
|
301
|
+
if (target === "agent-updates") {
|
|
302
|
+
return getAgentUpdateLogPath();
|
|
303
|
+
}
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
function renderLogTailPlain(title, tail) {
|
|
307
|
+
return [
|
|
308
|
+
`${title} log tail`,
|
|
309
|
+
`File: ${tail.filePath}`,
|
|
310
|
+
`Updated: ${tail.updatedAt ? formatLocalDateTime(tail.updatedAt) : "-"}`,
|
|
311
|
+
`Lines: ${tail.lineCount}/${tail.requestedLines}`,
|
|
312
|
+
"",
|
|
313
|
+
tail.plain || "(empty)",
|
|
314
|
+
].join("\n");
|
|
315
|
+
}
|
|
316
|
+
function renderLogTailHTML(title, tail) {
|
|
317
|
+
const body = tail.plain
|
|
318
|
+
? tail.plain.split("\n").map(renderLogLineHTML).join("\n")
|
|
319
|
+
: "<code>(empty)</code>";
|
|
320
|
+
return [
|
|
321
|
+
`<b>${escapeHTML(title)} log tail</b>`,
|
|
322
|
+
`<b>File:</b> <code>${escapeHTML(tail.filePath)}</code>`,
|
|
323
|
+
`<b>Updated:</b> <code>${escapeHTML(tail.updatedAt ? formatLocalDateTime(tail.updatedAt) : "-")}</code>`,
|
|
324
|
+
`<b>Lines:</b> <code>${tail.lineCount}/${tail.requestedLines}</code>`,
|
|
325
|
+
"",
|
|
326
|
+
body,
|
|
327
|
+
].join("\n");
|
|
328
|
+
}
|
|
329
|
+
function renderLogLineHTML(line) {
|
|
330
|
+
const structured = line.match(/^(?<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}|unknown time\s*)\s+(?<level>INFO|WARN|ERROR)\s+(?<message>.*)$/);
|
|
331
|
+
if (structured?.groups) {
|
|
332
|
+
const level = structured.groups.level;
|
|
333
|
+
const levelHtml = level === "INFO" ? escapeHTML(level) : `<b>${escapeHTML(level)}</b>`;
|
|
334
|
+
return [
|
|
335
|
+
`<code>${escapeHTML(structured.groups.timestamp.trim())}</code>`,
|
|
336
|
+
levelHtml,
|
|
337
|
+
escapeHTML(structured.groups.message),
|
|
338
|
+
].join(" ");
|
|
339
|
+
}
|
|
340
|
+
return escapeHTML(line);
|
|
341
|
+
}
|
|
342
|
+
function formatLocalDateTime(date) {
|
|
343
|
+
if (Number.isNaN(date.getTime())) {
|
|
344
|
+
return "-";
|
|
345
|
+
}
|
|
346
|
+
return [
|
|
347
|
+
`${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`,
|
|
348
|
+
`${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`,
|
|
349
|
+
].join(" ");
|
|
350
|
+
}
|
|
351
|
+
function formatRelativeTime(date) {
|
|
352
|
+
const seconds = Math.max(0, Math.round((Date.now() - date.getTime()) / 1000));
|
|
353
|
+
if (seconds < 60) {
|
|
354
|
+
return `${seconds}s ago`;
|
|
355
|
+
}
|
|
356
|
+
const minutes = Math.round(seconds / 60);
|
|
357
|
+
if (minutes < 60) {
|
|
358
|
+
return `${minutes}m ago`;
|
|
359
|
+
}
|
|
360
|
+
const hours = Math.round(minutes / 60);
|
|
361
|
+
if (hours < 24) {
|
|
362
|
+
return `${hours}h ago`;
|
|
363
|
+
}
|
|
364
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
365
|
+
}
|
|
366
|
+
function trimLine(text, maxLength) {
|
|
367
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
368
|
+
return normalized.length > maxLength ? `${normalized.slice(0, Math.max(0, maxLength - 3))}...` : normalized;
|
|
369
|
+
}
|
|
370
|
+
function pad2(value) {
|
|
371
|
+
return String(value).padStart(2, "0");
|
|
372
|
+
}
|
package/dist/operations.js
CHANGED
|
@@ -12,7 +12,8 @@ import { describePiCli, resolvePiCli } from "./pi-cli.js";
|
|
|
12
12
|
const APP_NAME = "nordrelay";
|
|
13
13
|
const PACKAGE_NAME = "@nordbyte/nordrelay";
|
|
14
14
|
const CODEX_PACKAGE_NAME = "@openai/codex";
|
|
15
|
-
const PI_PACKAGE_NAME = "@
|
|
15
|
+
const PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
16
|
+
const LEGACY_PI_PACKAGE_NAME = "@mariozechner/pi-coding-agent";
|
|
16
17
|
const HERMES_PACKAGE_NAME = "hermes-agent";
|
|
17
18
|
const OPENCLAW_PACKAGE_NAME = "openclaw";
|
|
18
19
|
const CLAUDE_CODE_PACKAGE_NAME = "@anthropic-ai/claude-code";
|
|
@@ -32,6 +33,9 @@ export function getConnectorLogPath() {
|
|
|
32
33
|
export function getUpdateLogPath() {
|
|
33
34
|
return path.join(getConnectorHome(), "update.log");
|
|
34
35
|
}
|
|
36
|
+
export function getAgentUpdateLogPath(home = getConnectorHome()) {
|
|
37
|
+
return path.join(home, "agent-updates.log");
|
|
38
|
+
}
|
|
35
39
|
export async function readConnectorState() {
|
|
36
40
|
try {
|
|
37
41
|
return JSON.parse(await readFile(getConnectorStatePath(), "utf8"));
|
|
@@ -74,6 +78,14 @@ export async function readFormattedLogTail(lines = 80, filePath = getConnectorLo
|
|
|
74
78
|
};
|
|
75
79
|
}
|
|
76
80
|
}
|
|
81
|
+
export function clearLogFile(filePath = getConnectorLogPath()) {
|
|
82
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
83
|
+
writeFileSync(filePath, "", "utf8");
|
|
84
|
+
return {
|
|
85
|
+
filePath,
|
|
86
|
+
clearedAt: new Date(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
77
89
|
export async function getPackageVersion() {
|
|
78
90
|
try {
|
|
79
91
|
const pkg = JSON.parse(await readFile(path.join(getSourceRoot(), "package.json"), "utf8"));
|
|
@@ -93,7 +105,10 @@ export async function getVersionChecks(options = {}) {
|
|
|
93
105
|
const codexVersionLabel = codexCli.path
|
|
94
106
|
? detectCliVersion(codexCli.path)
|
|
95
107
|
: readInstalledPackageVersion(CODEX_PACKAGE_NAME) ?? "not installed";
|
|
96
|
-
const piVersionLabel = piCli.path
|
|
108
|
+
const piVersionLabel = piCli.path
|
|
109
|
+
? detectCliVersion(piCli.path)
|
|
110
|
+
: readInstalledPackageVersion(PI_PACKAGE_NAME) ?? readInstalledPackageVersion(LEGACY_PI_PACKAGE_NAME) ?? "not installed";
|
|
111
|
+
const legacyPiPackageVersion = readInstalledPackageVersion(LEGACY_PI_PACKAGE_NAME);
|
|
97
112
|
const hermesVersionLabel = hermesCli.path ? detectCliVersion(hermesCli.path) : "not installed";
|
|
98
113
|
const openClawVersionLabel = openClawCli.path ? detectCliVersion(openClawCli.path) : "not installed";
|
|
99
114
|
const claudeCodeVersionLabel = claudeCodeCli.path
|
|
@@ -120,6 +135,7 @@ export async function getVersionChecks(options = {}) {
|
|
|
120
135
|
installedLabel: piVersionLabel,
|
|
121
136
|
installedVersion: extractVersion(piVersionLabel),
|
|
122
137
|
notInstalled: piVersionLabel === "not installed",
|
|
138
|
+
detail: legacyPiPackageVersion ? `Legacy package ${LEGACY_PI_PACKAGE_NAME} is present; current package is ${PI_PACKAGE_NAME}.` : undefined,
|
|
123
139
|
}),
|
|
124
140
|
hermes: buildHermesVersionCheck(hermesVersionLabel),
|
|
125
141
|
openclaw: buildVersionCheck({
|
|
@@ -139,10 +155,11 @@ export async function getVersionChecks(options = {}) {
|
|
|
139
155
|
};
|
|
140
156
|
}
|
|
141
157
|
export async function getConnectorHealth(options = {}) {
|
|
142
|
-
const
|
|
158
|
+
const rawState = await readConnectorState();
|
|
143
159
|
const version = await getPackageVersion();
|
|
144
|
-
const pidRunning = isProcessRunning(
|
|
145
|
-
const appPidRunning = isProcessRunning(
|
|
160
|
+
const pidRunning = isProcessRunning(rawState.pid);
|
|
161
|
+
const appPidRunning = isProcessRunning(rawState.appPid);
|
|
162
|
+
const state = normalizeConnectorState(rawState, pidRunning, appPidRunning);
|
|
146
163
|
const codexCli = resolveCodexCli();
|
|
147
164
|
const piCli = resolvePiCli(process.env, options.piCliPath);
|
|
148
165
|
const hermesCli = resolveHermesCli(process.env, options.hermesCliPath);
|
|
@@ -247,6 +264,13 @@ function isProcessRunning(pid) {
|
|
|
247
264
|
return false;
|
|
248
265
|
}
|
|
249
266
|
}
|
|
267
|
+
function normalizeConnectorState(state, pidRunning, appPidRunning) {
|
|
268
|
+
const stoppedSignal = state.signal === "SIGTERM" || state.signal === "SIGINT";
|
|
269
|
+
if (state.status === "error" && stoppedSignal && !state.error && !pidRunning && !appPidRunning) {
|
|
270
|
+
return { ...state, status: "stopped" };
|
|
271
|
+
}
|
|
272
|
+
return state;
|
|
273
|
+
}
|
|
250
274
|
function redactSecrets(text) {
|
|
251
275
|
return text.replace(SECRET_RE, "$1$2[redacted]");
|
|
252
276
|
}
|
|
@@ -328,6 +352,7 @@ function buildVersionCheck(options) {
|
|
|
328
352
|
installedVersion: null,
|
|
329
353
|
latestVersion: null,
|
|
330
354
|
status: "not-installed",
|
|
355
|
+
detail: options.detail,
|
|
331
356
|
};
|
|
332
357
|
}
|
|
333
358
|
if (options.skipLatest) {
|
|
@@ -338,7 +363,7 @@ function buildVersionCheck(options) {
|
|
|
338
363
|
installedVersion: options.installedVersion,
|
|
339
364
|
latestVersion: null,
|
|
340
365
|
status: options.installedVersion ? "unknown" : "unknown",
|
|
341
|
-
detail: "Latest-version lookup is not available for this package source",
|
|
366
|
+
detail: options.detail ?? "Latest-version lookup is not available for this package source",
|
|
342
367
|
};
|
|
343
368
|
}
|
|
344
369
|
const latest = detectLatestNpmVersion(options.packageName);
|
|
@@ -350,7 +375,7 @@ function buildVersionCheck(options) {
|
|
|
350
375
|
installedVersion: options.installedVersion,
|
|
351
376
|
latestVersion: latest.version,
|
|
352
377
|
status: "unknown",
|
|
353
|
-
detail: latest.error ?? "Could not parse installed version",
|
|
378
|
+
detail: [options.detail, latest.error ?? "Could not parse installed version"].filter(Boolean).join(" "),
|
|
354
379
|
};
|
|
355
380
|
}
|
|
356
381
|
return {
|
|
@@ -360,7 +385,7 @@ function buildVersionCheck(options) {
|
|
|
360
385
|
installedVersion: options.installedVersion,
|
|
361
386
|
latestVersion: latest.version,
|
|
362
387
|
status: compareVersions(options.installedVersion, latest.version) < 0 ? "outdated" : "current",
|
|
363
|
-
detail: latest.error,
|
|
388
|
+
detail: [options.detail, latest.error].filter(Boolean).join(" ") || undefined,
|
|
364
389
|
};
|
|
365
390
|
}
|
|
366
391
|
function detectLatestNpmVersion(packageName) {
|