@nordbyte/nordrelay 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +80 -11
- package/README.md +154 -22
- package/dist/access-control.js +7 -1
- package/dist/activity-events.js +44 -0
- package/dist/audit-log.js +40 -2
- package/dist/bot-preferences.js +1 -0
- package/dist/bot-rendering.js +10 -7
- package/dist/bot.js +535 -11
- package/dist/channel-actions.js +7 -2
- package/dist/channel-adapter.js +40 -7
- package/dist/channel-command-catalog.js +88 -0
- package/dist/channel-command-service.js +369 -0
- package/dist/channel-mirror-registry.js +77 -0
- package/dist/channel-peer-prompt.js +95 -0
- package/dist/channel-runtime.js +12 -5
- package/dist/channel-turn-service.js +237 -0
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +93 -13
- package/dist/config.js +103 -8
- package/dist/context-key.js +87 -5
- package/dist/discord-artifacts.js +165 -0
- package/dist/discord-bot.js +2073 -0
- package/dist/discord-channel-runtime.js +133 -0
- package/dist/discord-command-surface.js +57 -0
- package/dist/discord-rate-limit.js +141 -0
- package/dist/index.js +36 -5
- package/dist/job-store.js +127 -0
- package/dist/metrics.js +87 -0
- package/dist/peer-auth.js +85 -0
- package/dist/peer-client.js +256 -0
- package/dist/peer-context.js +21 -0
- package/dist/peer-identity.js +127 -0
- package/dist/peer-runtime-service.js +636 -0
- package/dist/peer-server.js +220 -0
- package/dist/peer-store.js +294 -0
- package/dist/peer-types.js +52 -0
- package/dist/relay-external-activity-monitor.js +47 -6
- package/dist/relay-runtime-helpers.js +208 -0
- package/dist/relay-runtime.js +897 -394
- package/dist/remote-prompt.js +98 -0
- package/dist/runtime-cache.js +57 -0
- package/dist/session-locks.js +10 -7
- package/dist/support-bundle.js +1 -0
- package/dist/telegram-access-commands.js +15 -2
- package/dist/telegram-access-middleware.js +16 -3
- package/dist/telegram-agent-commands.js +25 -0
- package/dist/telegram-artifact-commands.js +46 -0
- package/dist/telegram-command-menu.js +3 -53
- package/dist/telegram-diagnostics-command.js +5 -50
- package/dist/telegram-general-commands.js +16 -6
- package/dist/telegram-operational-commands.js +14 -6
- package/dist/telegram-preference-commands.js +23 -127
- package/dist/telegram-queue-commands.js +74 -4
- package/dist/telegram-support-command.js +7 -0
- package/dist/telegram-update-commands.js +27 -0
- package/dist/user-management.js +208 -0
- package/dist/web-api-contract.js +17 -0
- package/dist/web-dashboard-access-routes.js +74 -1
- package/dist/web-dashboard-artifact-routes.js +3 -3
- package/dist/web-dashboard-assets.js +2 -0
- package/dist/web-dashboard-pages.js +109 -13
- package/dist/web-dashboard-peer-routes.js +204 -0
- package/dist/web-dashboard-runtime-routes.js +53 -8
- package/dist/web-dashboard-session-routes.js +27 -20
- package/dist/web-dashboard-ui.js +2 -0
- package/dist/web-dashboard.js +160 -6
- package/dist/web-state.js +33 -2
- package/dist/webui-assets/dashboard.css +75 -1
- package/dist/webui-assets/dashboard.js +779 -55
- package/package.json +5 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +578 -19
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export async function peerPromptProxyPayload(prompt) {
|
|
4
|
+
if (typeof prompt.input === "string") {
|
|
5
|
+
return {
|
|
6
|
+
method: "POST",
|
|
7
|
+
path: "/api/prompt",
|
|
8
|
+
body: { text: prompt.input },
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const files = await remoteUploadFiles(prompt);
|
|
12
|
+
if (files.length > 0) {
|
|
13
|
+
return {
|
|
14
|
+
method: "POST",
|
|
15
|
+
path: "/api/prompt/upload",
|
|
16
|
+
body: {
|
|
17
|
+
text: prompt.input.text ?? "",
|
|
18
|
+
files,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
method: "POST",
|
|
24
|
+
path: "/api/prompt",
|
|
25
|
+
body: { text: prompt.input.text || prompt.description },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function remoteUploadFiles(prompt) {
|
|
29
|
+
if (typeof prompt.input === "string") {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const candidates = new Map();
|
|
33
|
+
for (const imagePath of prompt.input.imagePaths ?? []) {
|
|
34
|
+
candidates.set(imagePath, {
|
|
35
|
+
name: path.basename(imagePath),
|
|
36
|
+
mimeType: mimeTypeFromPath(imagePath),
|
|
37
|
+
localPath: imagePath,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
for (const file of parseStagedFileInstructions(prompt.input.stagedFileInstructions)) {
|
|
41
|
+
candidates.set(file.localPath, file);
|
|
42
|
+
}
|
|
43
|
+
const files = [];
|
|
44
|
+
for (const file of candidates.values()) {
|
|
45
|
+
const data = await readFile(file.localPath);
|
|
46
|
+
files.push({
|
|
47
|
+
name: file.name,
|
|
48
|
+
mimeType: file.mimeType,
|
|
49
|
+
dataBase64: data.toString("base64"),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return files;
|
|
53
|
+
}
|
|
54
|
+
function parseStagedFileInstructions(text) {
|
|
55
|
+
if (!text) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const files = [];
|
|
59
|
+
for (const line of text.split(/\r?\n/)) {
|
|
60
|
+
const match = line.match(/^- (.+?) \(([^,]+), [^)]+\) → (.+)$/);
|
|
61
|
+
if (!match)
|
|
62
|
+
continue;
|
|
63
|
+
files.push({
|
|
64
|
+
name: match[1] || path.basename(match[3] ?? "upload"),
|
|
65
|
+
mimeType: match[2],
|
|
66
|
+
localPath: match[3] ?? "",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return files.filter((file) => file.localPath);
|
|
70
|
+
}
|
|
71
|
+
function mimeTypeFromPath(filePath) {
|
|
72
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
73
|
+
if (ext === ".jpg" || ext === ".jpeg")
|
|
74
|
+
return "image/jpeg";
|
|
75
|
+
if (ext === ".png")
|
|
76
|
+
return "image/png";
|
|
77
|
+
if (ext === ".gif")
|
|
78
|
+
return "image/gif";
|
|
79
|
+
if (ext === ".webp")
|
|
80
|
+
return "image/webp";
|
|
81
|
+
if (ext === ".pdf")
|
|
82
|
+
return "application/pdf";
|
|
83
|
+
if (ext === ".txt" || ext === ".md" || ext === ".log")
|
|
84
|
+
return "text/plain";
|
|
85
|
+
if (ext === ".json")
|
|
86
|
+
return "application/json";
|
|
87
|
+
if (ext === ".csv")
|
|
88
|
+
return "text/csv";
|
|
89
|
+
if (ext === ".mp3")
|
|
90
|
+
return "audio/mpeg";
|
|
91
|
+
if (ext === ".wav")
|
|
92
|
+
return "audio/wav";
|
|
93
|
+
if (ext === ".ogg")
|
|
94
|
+
return "audio/ogg";
|
|
95
|
+
if (ext === ".webm")
|
|
96
|
+
return "audio/webm";
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export class RuntimeSnapshotCache {
|
|
2
|
+
entries = new Map();
|
|
3
|
+
async get(key, ttlMs, producer) {
|
|
4
|
+
const now = Date.now();
|
|
5
|
+
const entry = this.entries.get(key);
|
|
6
|
+
const hasFreshValue = entry?.value !== undefined && ttlMs > 0 && now - entry.refreshedAt <= ttlMs;
|
|
7
|
+
if (hasFreshValue) {
|
|
8
|
+
return {
|
|
9
|
+
value: entry.value,
|
|
10
|
+
refreshedAt: new Date(entry.refreshedAt).toISOString(),
|
|
11
|
+
stale: false,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (entry?.value !== undefined) {
|
|
15
|
+
if (!entry.refresh) {
|
|
16
|
+
entry.refresh = producer()
|
|
17
|
+
.then((value) => {
|
|
18
|
+
entry.value = value;
|
|
19
|
+
entry.refreshedAt = Date.now();
|
|
20
|
+
return value;
|
|
21
|
+
})
|
|
22
|
+
.catch(() => entry.value)
|
|
23
|
+
.finally(() => {
|
|
24
|
+
entry.refresh = undefined;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
value: entry.value,
|
|
29
|
+
refreshedAt: new Date(entry.refreshedAt).toISOString(),
|
|
30
|
+
stale: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const pending = entry?.refresh ?? producer();
|
|
34
|
+
this.entries.set(key, { refresh: pending, refreshedAt: now });
|
|
35
|
+
try {
|
|
36
|
+
const value = await pending;
|
|
37
|
+
const refreshedAt = Date.now();
|
|
38
|
+
this.entries.set(key, { value, refreshedAt });
|
|
39
|
+
return {
|
|
40
|
+
value,
|
|
41
|
+
refreshedAt: new Date(refreshedAt).toISOString(),
|
|
42
|
+
stale: false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
this.entries.delete(key);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
invalidate(key) {
|
|
51
|
+
if (key) {
|
|
52
|
+
this.entries.delete(key);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.entries.clear();
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/session-locks.js
CHANGED
|
@@ -22,14 +22,17 @@ export class SessionLockStore {
|
|
|
22
22
|
}
|
|
23
23
|
return lock;
|
|
24
24
|
}
|
|
25
|
-
set(contextKey,
|
|
25
|
+
set(contextKey, owner, ttlMs) {
|
|
26
26
|
const payload = this.readPayload();
|
|
27
|
+
const now = Date.now();
|
|
27
28
|
const lock = {
|
|
28
29
|
contextKey,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
ownerUserId: owner.userId,
|
|
31
|
+
ownerLabel: owner.label,
|
|
32
|
+
ownerChannel: owner.channel,
|
|
33
|
+
ownerChannelUserId: owner.channelUserId,
|
|
34
|
+
createdAt: now,
|
|
35
|
+
expiresAt: ttlMs > 0 ? now + ttlMs : undefined,
|
|
33
36
|
};
|
|
34
37
|
payload.locks[contextKey] = lock;
|
|
35
38
|
this.store.write(payload);
|
|
@@ -67,7 +70,7 @@ export function canWriteWithLock(lock, userId, isAdmin) {
|
|
|
67
70
|
if (!lock) {
|
|
68
71
|
return true;
|
|
69
72
|
}
|
|
70
|
-
return isAdmin || userId === lock.
|
|
73
|
+
return isAdmin || Boolean(userId && userId === lock.ownerUserId);
|
|
71
74
|
}
|
|
72
75
|
function isSessionLock(value) {
|
|
73
76
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -75,7 +78,7 @@ function isSessionLock(value) {
|
|
|
75
78
|
}
|
|
76
79
|
const candidate = value;
|
|
77
80
|
return typeof candidate.contextKey === "string" &&
|
|
78
|
-
|
|
81
|
+
typeof candidate.ownerUserId === "string" &&
|
|
79
82
|
typeof candidate.createdAt === "number" &&
|
|
80
83
|
(candidate.expiresAt === undefined || typeof candidate.expiresAt === "number");
|
|
81
84
|
}
|
package/dist/support-bundle.js
CHANGED
|
@@ -29,6 +29,7 @@ export function registerTelegramAccessCommands(deps) {
|
|
|
29
29
|
action: "auth_login_failed",
|
|
30
30
|
status: "denied",
|
|
31
31
|
contextKey: String(ctx.chat.id),
|
|
32
|
+
actor: telegramAuditActor(ctx),
|
|
32
33
|
actorId: ctx.from.id,
|
|
33
34
|
description: "Telegram link rate limited",
|
|
34
35
|
detail: `${seconds}s retry-after`,
|
|
@@ -51,7 +52,8 @@ export function registerTelegramAccessCommands(deps) {
|
|
|
51
52
|
action: "telegram_linked",
|
|
52
53
|
status: "ok",
|
|
53
54
|
contextKey: String(ctx.chat.id),
|
|
54
|
-
|
|
55
|
+
actor: telegramAuditActor(ctx, linked),
|
|
56
|
+
actorId: linked.user.id,
|
|
55
57
|
actorRole: linked.groups.map((group) => group.name).join(", "),
|
|
56
58
|
description: `Linked ${linked.user.email}`,
|
|
57
59
|
});
|
|
@@ -65,6 +67,7 @@ export function registerTelegramAccessCommands(deps) {
|
|
|
65
67
|
action: "auth_login_failed",
|
|
66
68
|
status: "failed",
|
|
67
69
|
contextKey: String(ctx.chat.id),
|
|
70
|
+
actor: telegramAuditActor(ctx),
|
|
68
71
|
actorId: ctx.from.id,
|
|
69
72
|
description: "Telegram link failed",
|
|
70
73
|
detail: message,
|
|
@@ -112,7 +115,8 @@ export function registerTelegramAccessCommands(deps) {
|
|
|
112
115
|
action: "telegram_chat_updated",
|
|
113
116
|
status: "ok",
|
|
114
117
|
contextKey: String(ctx.chat.id),
|
|
115
|
-
|
|
118
|
+
actor: telegramAuditActor(ctx, authUser),
|
|
119
|
+
actorId: authUser.user.id,
|
|
116
120
|
actorRole: getUserRole(ctx),
|
|
117
121
|
description: `Registered Telegram chat ${chat.chatId}`,
|
|
118
122
|
});
|
|
@@ -121,3 +125,12 @@ export function registerTelegramAccessCommands(deps) {
|
|
|
121
125
|
});
|
|
122
126
|
});
|
|
123
127
|
}
|
|
128
|
+
function telegramAuditActor(ctx, authUser) {
|
|
129
|
+
return {
|
|
130
|
+
channel: "telegram",
|
|
131
|
+
id: authUser?.user.id ?? (ctx.from?.id !== undefined ? `telegram:${ctx.from.id}` : undefined),
|
|
132
|
+
label: authUser?.user.displayName || authUser?.user.email || ctx.from?.username || (ctx.from?.id !== undefined ? String(ctx.from.id) : undefined),
|
|
133
|
+
username: authUser?.user.email ?? ctx.from?.username,
|
|
134
|
+
channelUserId: ctx.from?.id !== undefined ? String(ctx.from.id) : undefined,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -31,6 +31,7 @@ export function createTelegramAccessMiddleware(options) {
|
|
|
31
31
|
action: "permission_denied",
|
|
32
32
|
status: "denied",
|
|
33
33
|
contextKey: typeof chatId === "number" ? String(chatId) : "telegram",
|
|
34
|
+
actor: telegramAuditActor(ctx),
|
|
34
35
|
actorId: fromId,
|
|
35
36
|
description: "Telegram account is not linked",
|
|
36
37
|
});
|
|
@@ -50,7 +51,8 @@ export function createTelegramAccessMiddleware(options) {
|
|
|
50
51
|
action: "permission_denied",
|
|
51
52
|
status: "denied",
|
|
52
53
|
contextKey: typeof chatId === "number" ? String(chatId) : "telegram",
|
|
53
|
-
|
|
54
|
+
actor: telegramAuditActor(ctx, authUser),
|
|
55
|
+
actorId: authUser.user.id,
|
|
54
56
|
actorRole: getUserRole(contextUsers, ctx),
|
|
55
57
|
description: "Telegram chat is not enabled or outside user scope",
|
|
56
58
|
});
|
|
@@ -69,7 +71,8 @@ export function createTelegramAccessMiddleware(options) {
|
|
|
69
71
|
action: "permission_denied",
|
|
70
72
|
status: "denied",
|
|
71
73
|
contextKey: typeof chatId === "number" ? String(chatId) : "telegram",
|
|
72
|
-
|
|
74
|
+
actor: telegramAuditActor(ctx, authUser),
|
|
75
|
+
actorId: authUser.user.id,
|
|
73
76
|
actorRole: getUserRole(contextUsers, ctx),
|
|
74
77
|
description: commandName ? `Unsupported command /${commandName}` : "Unsupported callback",
|
|
75
78
|
});
|
|
@@ -87,7 +90,8 @@ export function createTelegramAccessMiddleware(options) {
|
|
|
87
90
|
action: "permission_denied",
|
|
88
91
|
status: "denied",
|
|
89
92
|
contextKey: typeof chatId === "number" ? String(chatId) : "telegram",
|
|
90
|
-
|
|
93
|
+
actor: telegramAuditActor(ctx, authUser),
|
|
94
|
+
actorId: authUser.user.id,
|
|
91
95
|
actorRole: getUserRole(contextUsers, ctx),
|
|
92
96
|
description: `${permission} required`,
|
|
93
97
|
});
|
|
@@ -102,6 +106,15 @@ export function createTelegramAccessMiddleware(options) {
|
|
|
102
106
|
await next();
|
|
103
107
|
};
|
|
104
108
|
}
|
|
109
|
+
function telegramAuditActor(ctx, authUser) {
|
|
110
|
+
return {
|
|
111
|
+
channel: "telegram",
|
|
112
|
+
id: authUser?.user.id ?? (ctx.from?.id !== undefined ? `telegram:${ctx.from.id}` : undefined),
|
|
113
|
+
label: authUser?.user.displayName || authUser?.user.email || ctx.from?.username || (ctx.from?.id !== undefined ? String(ctx.from.id) : undefined),
|
|
114
|
+
username: authUser?.user.email ?? ctx.from?.username,
|
|
115
|
+
channelUserId: ctx.from?.id !== undefined ? String(ctx.from.id) : undefined,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
105
118
|
function getUserRole(contextUsers, ctx) {
|
|
106
119
|
const authUser = contextUsers.get(ctx);
|
|
107
120
|
return authUser?.groups.map((group) => group.name).join(", ") || "unauthenticated";
|
|
@@ -99,6 +99,14 @@ export function registerTelegramAgentCommands(options) {
|
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
const result = await options.startAgentLogin(info);
|
|
102
|
+
options.appendActivity?.(ctx, {
|
|
103
|
+
status: result.success ? "info" : "failed",
|
|
104
|
+
type: result.success ? "login_started" : "login_failed",
|
|
105
|
+
threadId: info?.threadId ?? null,
|
|
106
|
+
workspace: info?.workspace,
|
|
107
|
+
agentId: options.agentIdForAuth(info),
|
|
108
|
+
detail: redactText(result.message),
|
|
109
|
+
});
|
|
102
110
|
if (result.success) {
|
|
103
111
|
await safeReply(ctx, `<b>🔑 Login initiated.</b>\n\n<code>${escapeHTML(redactText(result.message))}</code>`, {
|
|
104
112
|
fallbackText: `🔑 Login initiated.\n\n${redactText(result.message)}`,
|
|
@@ -156,6 +164,14 @@ export function registerTelegramAgentCommands(options) {
|
|
|
156
164
|
return;
|
|
157
165
|
}
|
|
158
166
|
const result = await options.startAgentLogout(info);
|
|
167
|
+
options.appendActivity?.(ctx, {
|
|
168
|
+
status: result.success ? "info" : "failed",
|
|
169
|
+
type: result.success ? "logout_completed" : "logout_failed",
|
|
170
|
+
threadId: info?.threadId ?? null,
|
|
171
|
+
workspace: info?.workspace,
|
|
172
|
+
agentId: options.agentIdForAuth(info),
|
|
173
|
+
detail: redactText(result.message),
|
|
174
|
+
});
|
|
159
175
|
if (result.success) {
|
|
160
176
|
await safeReply(ctx, `<b>🔓 Logged out.</b>\n\n${escapeHTML(redactText(result.message))}`, {
|
|
161
177
|
fallbackText: `🔓 Logged out.\n\n${redactText(result.message)}`,
|
|
@@ -189,6 +205,15 @@ export function registerTelegramAgentCommands(options) {
|
|
|
189
205
|
try {
|
|
190
206
|
const session = await options.registry.switchAgent(contextKey, selectedAgent);
|
|
191
207
|
const info = session.getInfo();
|
|
208
|
+
options.appendActivity?.(ctx, {
|
|
209
|
+
status: "info",
|
|
210
|
+
type: "agent_switch",
|
|
211
|
+
contextKey,
|
|
212
|
+
threadId: info.threadId,
|
|
213
|
+
workspace: info.workspace,
|
|
214
|
+
agentId: info.agentId,
|
|
215
|
+
detail: labelOf(info),
|
|
216
|
+
});
|
|
192
217
|
const html = [`<b>Agent switched to ${escapeHTML(labelOf(info))}.</b>`, "", renderSessionInfoHTML(info)].join("\n");
|
|
193
218
|
const plain = [`Agent switched to ${labelOf(info)}.`, "", renderSessionInfoPlain(info)].join("\n");
|
|
194
219
|
if (messageId) {
|
|
@@ -35,6 +35,16 @@ export function registerTelegramArtifactCommands(options) {
|
|
|
35
35
|
const removed = await removeArtifactTurn(workspace, selected.turnId);
|
|
36
36
|
const text = removed ? `Deleted artifact turn: ${selected.turnId}` : `Artifact turn not found: ${selected.turnId}`;
|
|
37
37
|
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
38
|
+
if (removed) {
|
|
39
|
+
options.appendActivity?.(ctx, {
|
|
40
|
+
status: "info",
|
|
41
|
+
type: "artifact_deleted",
|
|
42
|
+
threadId: contextSession.session.getInfo().threadId,
|
|
43
|
+
workspace,
|
|
44
|
+
agentId: contextSession.session.getInfo().agentId,
|
|
45
|
+
detail: selected.turnId,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
38
48
|
return;
|
|
39
49
|
}
|
|
40
50
|
const filtered = filterArtifactReports(reports, argument);
|
|
@@ -64,9 +74,25 @@ export function registerTelegramArtifactCommands(options) {
|
|
|
64
74
|
return;
|
|
65
75
|
}
|
|
66
76
|
if (shouldZip) {
|
|
77
|
+
options.appendActivity?.(ctx, {
|
|
78
|
+
status: "info",
|
|
79
|
+
type: "artifact_zip_sent",
|
|
80
|
+
threadId: contextSession.session.getInfo().threadId,
|
|
81
|
+
workspace,
|
|
82
|
+
agentId: contextSession.session.getInfo().agentId,
|
|
83
|
+
detail: selected.turnId,
|
|
84
|
+
});
|
|
67
85
|
await options.deliverArtifactReportZip(ctx, ctx.chat.id, selected, ctx.message?.message_thread_id);
|
|
68
86
|
}
|
|
69
87
|
else {
|
|
88
|
+
options.appendActivity?.(ctx, {
|
|
89
|
+
status: "info",
|
|
90
|
+
type: "artifacts_sent",
|
|
91
|
+
threadId: contextSession.session.getInfo().threadId,
|
|
92
|
+
workspace,
|
|
93
|
+
agentId: contextSession.session.getInfo().agentId,
|
|
94
|
+
detail: selected.turnId,
|
|
95
|
+
});
|
|
70
96
|
await options.deliverArtifactReport(ctx, ctx.chat.id, selected, ctx.message?.message_thread_id);
|
|
71
97
|
}
|
|
72
98
|
return;
|
|
@@ -111,6 +137,17 @@ export function registerTelegramArtifactCommands(options) {
|
|
|
111
137
|
if (action === "delete_confirm") {
|
|
112
138
|
const removed = await removeArtifactTurn(workspace, turnId);
|
|
113
139
|
await ctx.answerCallbackQuery({ text: removed ? "Deleted" : "Already gone" });
|
|
140
|
+
if (removed) {
|
|
141
|
+
const info = contextSession.session.getInfo();
|
|
142
|
+
options.appendActivity?.(ctx, {
|
|
143
|
+
status: "info",
|
|
144
|
+
type: "artifact_deleted",
|
|
145
|
+
threadId: info.threadId,
|
|
146
|
+
workspace,
|
|
147
|
+
agentId: info.agentId,
|
|
148
|
+
detail: turnId,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
114
151
|
const html = removed
|
|
115
152
|
? `<b>Deleted artifact turn:</b> <code>${escapeHTML(turnId)}</code>`
|
|
116
153
|
: `<b>Artifact turn not found:</b> <code>${escapeHTML(turnId)}</code>`;
|
|
@@ -129,6 +166,15 @@ export function registerTelegramArtifactCommands(options) {
|
|
|
129
166
|
return;
|
|
130
167
|
}
|
|
131
168
|
await ctx.answerCallbackQuery({ text: action === "zip" ? "Sending ZIP..." : "Sending artifacts..." });
|
|
169
|
+
const info = contextSession.session.getInfo();
|
|
170
|
+
options.appendActivity?.(ctx, {
|
|
171
|
+
status: "info",
|
|
172
|
+
type: action === "zip" ? "artifact_zip_sent" : "artifacts_sent",
|
|
173
|
+
threadId: info.threadId,
|
|
174
|
+
workspace,
|
|
175
|
+
agentId: info.agentId,
|
|
176
|
+
detail: turnId,
|
|
177
|
+
});
|
|
132
178
|
if (action === "zip") {
|
|
133
179
|
await options.deliverArtifactReportZip(ctx, chatId, report, ctx.callbackQuery.message?.message_thread_id);
|
|
134
180
|
}
|
|
@@ -1,55 +1,5 @@
|
|
|
1
|
+
import { telegramCommandCatalog } from "./channel-command-catalog.js";
|
|
2
|
+
export const TELEGRAM_COMMANDS = telegramCommandCatalog();
|
|
1
3
|
export async function registerCommands(bot) {
|
|
2
|
-
await bot.api.setMyCommands([
|
|
3
|
-
{ command: "start", description: "Welcome & status" },
|
|
4
|
-
{ command: "help", description: "Command reference" },
|
|
5
|
-
{ command: "link", description: "Link Telegram to NordRelay user" },
|
|
6
|
-
{ command: "whoami", description: "Show your NordRelay user" },
|
|
7
|
-
{ command: "register_chat", description: "Admin: enable this group chat" },
|
|
8
|
-
{ command: "channels", description: "Messaging adapter status" },
|
|
9
|
-
{ command: "agents", description: "Agent adapter status" },
|
|
10
|
-
{ command: "agent", description: "Select agent" },
|
|
11
|
-
{ command: "new", description: "Start a new thread" },
|
|
12
|
-
{ command: "session", description: "Current thread details" },
|
|
13
|
-
{ command: "sessions", description: "Browse & switch threads" },
|
|
14
|
-
{ command: "sync", description: "Sync active session from CLI state" },
|
|
15
|
-
{ command: "pinned", description: "Show pinned threads" },
|
|
16
|
-
{ command: "pin", description: "Pin current or given thread" },
|
|
17
|
-
{ command: "unpin", description: "Unpin current or given thread" },
|
|
18
|
-
{ command: "retry", description: "Resend the last prompt" },
|
|
19
|
-
{ command: "queue", description: "Show queued prompts" },
|
|
20
|
-
{ command: "cancel", description: "Cancel a queued prompt" },
|
|
21
|
-
{ command: "clearqueue", description: "Clear queued prompts" },
|
|
22
|
-
{ command: "artifacts", description: "List or resend generated files" },
|
|
23
|
-
{ command: "workspaces", description: "List allowed workspaces" },
|
|
24
|
-
{ command: "abort", description: "Cancel current operation" },
|
|
25
|
-
{ command: "stop", description: "Cancel current operation" },
|
|
26
|
-
{ command: "launch_profiles", description: "Select launch profile" },
|
|
27
|
-
{ command: "fast", description: "Toggle fast mode" },
|
|
28
|
-
{ command: "model", description: "View & change model" },
|
|
29
|
-
{ command: "reasoning", description: "Set reasoning effort" },
|
|
30
|
-
{ command: "mirror", description: "Control CLI mirroring" },
|
|
31
|
-
{ command: "notify", description: "Control notifications" },
|
|
32
|
-
{ command: "auth", description: "Check auth status" },
|
|
33
|
-
{ command: "login", description: "Start authentication" },
|
|
34
|
-
{ command: "logout", description: "Sign out" },
|
|
35
|
-
{ command: "voice", description: "Voice transcription status" },
|
|
36
|
-
{ command: "tasks", description: "Current turn progress" },
|
|
37
|
-
{ command: "progress", description: "Current turn progress" },
|
|
38
|
-
{ command: "activity", description: "Thread activity timeline" },
|
|
39
|
-
{ command: "audit", description: "Admin: recent audit events" },
|
|
40
|
-
{ command: "status", description: "Connector runtime status" },
|
|
41
|
-
{ command: "health", description: "Connector health report" },
|
|
42
|
-
{ command: "version", description: "Connector version" },
|
|
43
|
-
{ command: "logs", description: "Admin: show connector logs" },
|
|
44
|
-
{ command: "diagnostics", description: "Admin: connector diagnostics" },
|
|
45
|
-
{ command: "support", description: "Admin: export diagnostics bundle" },
|
|
46
|
-
{ command: "lock", description: "Lock session writes to you" },
|
|
47
|
-
{ command: "unlock", description: "Release session write lock" },
|
|
48
|
-
{ command: "locks", description: "List session write locks" },
|
|
49
|
-
{ command: "restart", description: "Admin: restart connector" },
|
|
50
|
-
{ command: "update", description: "Admin: update connector or agents" },
|
|
51
|
-
{ command: "handback", description: "Hand session back to CLI" },
|
|
52
|
-
{ command: "attach", description: "Bind a session to this topic" },
|
|
53
|
-
{ command: "switch", description: "Switch to a thread by ID" },
|
|
54
|
-
]);
|
|
4
|
+
await bot.api.setMyCommands([...TELEGRAM_COMMANDS]);
|
|
55
5
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { getAgentDiagnostics } from "./agent-activity.js";
|
|
2
2
|
import { formatQuietHours } from "./bot-preferences.js";
|
|
3
|
-
import {
|
|
3
|
+
import { cliPathOptions } from "./channel-command-service.js";
|
|
4
4
|
import { checkAuthStatus } from "./codex-auth.js";
|
|
5
5
|
import { contextKeyFromCtx } from "./context-key.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { formatCliPathHTML, formatCliPathPlain, renderAgentDiagnostics, renderDiagnosticsHTML, renderDiagnosticsPlain, renderHealthHTML, renderHealthPlain, renderVersionCheckHTML, renderVersionCheckPlain, } from "./bot-rendering.js";
|
|
6
|
+
import { getConnectorHealth, } from "./operations.js";
|
|
7
|
+
import { renderAgentDiagnostics, renderDiagnosticsHTML, renderDiagnosticsPlain, renderHealthHTML, renderHealthPlain, } from "./bot-rendering.js";
|
|
9
8
|
import { getTelegramRateLimitMetrics } from "./telegram-rate-limit.js";
|
|
10
9
|
import { safeReply } from "./telegram-output.js";
|
|
11
10
|
export function registerTelegramDiagnosticsCommands(options) {
|
|
@@ -20,38 +19,7 @@ export function registerTelegramDiagnosticsCommands(options) {
|
|
|
20
19
|
await safeReply(ctx, html, { fallbackText: plain });
|
|
21
20
|
});
|
|
22
21
|
options.bot.command("version", async (ctx) => {
|
|
23
|
-
|
|
24
|
-
const state = await readConnectorState();
|
|
25
|
-
const versions = await getVersionChecks(cliPathOptions(options.config));
|
|
26
|
-
const plain = [
|
|
27
|
-
renderVersionCheckPlain(versions.nordrelay),
|
|
28
|
-
`Runtime status: ${state.status ?? "unknown"}`,
|
|
29
|
-
formatCliPathPlain("Codex CLI", health.codexCliPath, health.codexCli),
|
|
30
|
-
renderVersionCheckPlain(versions.codex),
|
|
31
|
-
formatCliPathPlain("Pi CLI", health.piCliPath, health.piCli),
|
|
32
|
-
renderVersionCheckPlain(versions.pi),
|
|
33
|
-
formatCliPathPlain("Hermes CLI", health.hermesCliPath, health.hermesCli),
|
|
34
|
-
renderVersionCheckPlain(versions.hermes),
|
|
35
|
-
formatCliPathPlain("OpenClaw CLI", health.openClawCliPath, health.openClawCli),
|
|
36
|
-
renderVersionCheckPlain(versions.openclaw),
|
|
37
|
-
formatCliPathPlain("Claude Code CLI", health.claudeCodeCliPath, health.claudeCodeCli),
|
|
38
|
-
renderVersionCheckPlain(versions.claudeCode),
|
|
39
|
-
].join("\n");
|
|
40
|
-
const html = [
|
|
41
|
-
renderVersionCheckHTML(versions.nordrelay),
|
|
42
|
-
`<b>Runtime status:</b> <code>${escapeHTML(state.status ?? "unknown")}</code>`,
|
|
43
|
-
formatCliPathHTML("Codex CLI", health.codexCliPath, health.codexCli),
|
|
44
|
-
renderVersionCheckHTML(versions.codex),
|
|
45
|
-
formatCliPathHTML("Pi CLI", health.piCliPath, health.piCli),
|
|
46
|
-
renderVersionCheckHTML(versions.pi),
|
|
47
|
-
formatCliPathHTML("Hermes CLI", health.hermesCliPath, health.hermesCli),
|
|
48
|
-
renderVersionCheckHTML(versions.hermes),
|
|
49
|
-
formatCliPathHTML("OpenClaw CLI", health.openClawCliPath, health.openClawCli),
|
|
50
|
-
renderVersionCheckHTML(versions.openclaw),
|
|
51
|
-
formatCliPathHTML("Claude Code CLI", health.claudeCodeCliPath, health.claudeCodeCli),
|
|
52
|
-
renderVersionCheckHTML(versions.claudeCode),
|
|
53
|
-
].join("\n");
|
|
54
|
-
await safeReply(ctx, html, { fallbackText: plain });
|
|
22
|
+
await options.replyChannelAction(ctx, await options.commandService.renderVersion());
|
|
55
23
|
});
|
|
56
24
|
options.bot.command("diagnostics", async (ctx) => {
|
|
57
25
|
const health = await getConnectorHealth(cliPathOptions(options.config));
|
|
@@ -84,19 +52,6 @@ export function registerTelegramDiagnosticsCommands(options) {
|
|
|
84
52
|
options.bot.command("logs", async (ctx) => {
|
|
85
53
|
const rawText = ctx.message?.text ?? "";
|
|
86
54
|
const argument = rawText.replace(/^\/logs(?:@\w+)?\s*/i, "").trim();
|
|
87
|
-
|
|
88
|
-
const logs = await Promise.all(logTailRequests(logRequest.target).map(async (request) => ({
|
|
89
|
-
title: request.title,
|
|
90
|
-
tail: await readFormattedLogTail(logRequest.lines, request.path),
|
|
91
|
-
})));
|
|
92
|
-
await options.replyChannelAction(ctx, renderLogTailsAction(logs));
|
|
55
|
+
await options.replyChannelAction(ctx, await options.commandService.renderLogs(argument));
|
|
93
56
|
});
|
|
94
57
|
}
|
|
95
|
-
function cliPathOptions(config) {
|
|
96
|
-
return {
|
|
97
|
-
piCliPath: config.piCliPath,
|
|
98
|
-
hermesCliPath: config.hermesCliPath,
|
|
99
|
-
openClawCliPath: config.openClawCliPath,
|
|
100
|
-
claudeCodeCliPath: config.claudeCodeCliPath,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import { listAgentAdapterDescriptors } from "./agent-adapter.js";
|
|
2
|
-
import { enabledAgents } from "./agent-factory.js";
|
|
3
1
|
import { renderWelcomeFirstTime, renderWelcomeReturning, renderHelpMessage, } from "./bot-ui.js";
|
|
4
2
|
import { authHelpText, capabilitiesOf, labelOf, } from "./bot-rendering.js";
|
|
5
|
-
import { renderAgentsAction, renderChannelsAction, } from "./channel-actions.js";
|
|
6
|
-
import { listChannelDescriptors } from "./channel-adapter.js";
|
|
7
3
|
import { escapeHTML } from "./format.js";
|
|
8
4
|
import { spawnConnectorRestart } from "./operations.js";
|
|
9
5
|
import { renderLaunchSummaryHTML, renderLaunchSummaryPlain, renderSessionInfoHTML, renderSessionInfoPlain, } from "./session-format.js";
|
|
@@ -36,10 +32,24 @@ export function registerTelegramGeneralCommands(options) {
|
|
|
36
32
|
await safeReply(ctx, help.html, { fallbackText: help.plain });
|
|
37
33
|
});
|
|
38
34
|
options.bot.command("channels", async (ctx) => {
|
|
39
|
-
await options.replyChannelAction(ctx,
|
|
35
|
+
await options.replyChannelAction(ctx, options.commandService.renderChannels());
|
|
40
36
|
});
|
|
41
37
|
options.bot.command("agents", async (ctx) => {
|
|
42
|
-
await options.replyChannelAction(ctx,
|
|
38
|
+
await options.replyChannelAction(ctx, options.commandService.renderAgents());
|
|
39
|
+
});
|
|
40
|
+
options.bot.command("peers", async (ctx) => {
|
|
41
|
+
await options.replyChannelAction(ctx, options.commandService.renderPeers());
|
|
42
|
+
});
|
|
43
|
+
options.bot.command("target", async (ctx) => {
|
|
44
|
+
const contextSession = await options.getContextSession(ctx, { deferThreadStart: true });
|
|
45
|
+
if (!contextSession)
|
|
46
|
+
return;
|
|
47
|
+
await options.replyChannelAction(ctx, options.commandService.renderTargetPreference({
|
|
48
|
+
source: "telegram",
|
|
49
|
+
contextKey: contextSession.contextKey,
|
|
50
|
+
argument: ctx.match?.toString() ?? "",
|
|
51
|
+
preferencesStore: options.preferencesStore,
|
|
52
|
+
}));
|
|
43
53
|
});
|
|
44
54
|
options.bot.command("restart", async (ctx) => {
|
|
45
55
|
await safeReply(ctx, escapeHTML("Restarting connector..."), {
|
|
@@ -4,9 +4,10 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { InputFile } from "grammy";
|
|
6
6
|
import { getAgentActivityLog } from "./agent-activity.js";
|
|
7
|
-
import { capabilitiesOf, filterActivityEvents, formatLocalDateTime, formatLockOwner,
|
|
7
|
+
import { capabilitiesOf, filterActivityEvents, formatLocalDateTime, formatLockOwner, labelOf, parseActivityOptions, renderActivityTimeline, renderAuditEvents, renderProgressHTML, renderProgressPlain, renderSessionLocks, } from "./bot-rendering.js";
|
|
8
8
|
import { escapeHTML } from "./format.js";
|
|
9
9
|
import { renderSessionInfoHTML, renderSessionInfoPlain } from "./session-format.js";
|
|
10
|
+
import { canWriteWithLock } from "./session-locks.js";
|
|
10
11
|
import { chatBucket, safeReply } from "./telegram-output.js";
|
|
11
12
|
import { telegramRateLimiter } from "./telegram-rate-limit.js";
|
|
12
13
|
export function registerTelegramOperationalCommands(options) {
|
|
@@ -72,21 +73,27 @@ export function registerTelegramOperationalCommands(options) {
|
|
|
72
73
|
});
|
|
73
74
|
bot.command("lock", async (ctx) => {
|
|
74
75
|
const contextSession = await options.getContextSession(ctx, { deferThreadStart: true });
|
|
75
|
-
if (!contextSession
|
|
76
|
+
if (!contextSession) {
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
79
|
const { contextKey, session } = contextSession;
|
|
80
|
+
const owner = options.getLockOwner(ctx);
|
|
81
|
+
if (!owner) {
|
|
82
|
+
const text = "You must be authenticated before locking a session.";
|
|
83
|
+
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
79
86
|
const existing = lockStore.get(contextKey);
|
|
80
|
-
if (existing && existing
|
|
87
|
+
if (existing && !canWriteWithLock(existing, owner.userId, options.isAdminUser(ctx))) {
|
|
81
88
|
const text = `Session is already locked by ${formatLockOwner(existing)}.`;
|
|
82
89
|
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
83
90
|
return;
|
|
84
91
|
}
|
|
85
|
-
const lock = lockStore.set(contextKey,
|
|
92
|
+
const lock = lockStore.set(contextKey, owner, config.sessionLockTtlMs);
|
|
86
93
|
options.auditContext(ctx, contextKey, session, {
|
|
87
94
|
action: "lock_updated",
|
|
88
95
|
status: "ok",
|
|
89
|
-
detail: `locked by ${lock.
|
|
96
|
+
detail: `locked by ${lock.ownerUserId}`,
|
|
90
97
|
});
|
|
91
98
|
const text = `Session locked by ${formatLockOwner(lock)}${lock.expiresAt ? ` until ${formatLocalDateTime(new Date(lock.expiresAt))}` : ""}.`;
|
|
92
99
|
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
@@ -98,7 +105,8 @@ export function registerTelegramOperationalCommands(options) {
|
|
|
98
105
|
}
|
|
99
106
|
const { contextKey, session } = contextSession;
|
|
100
107
|
const lock = lockStore.get(contextKey);
|
|
101
|
-
|
|
108
|
+
const owner = options.getLockOwner(ctx);
|
|
109
|
+
if (lock && !canWriteWithLock(lock, owner?.userId, options.isAdminUser(ctx))) {
|
|
102
110
|
const text = `Only ${formatLockOwner(lock)} or an admin can unlock this session.`;
|
|
103
111
|
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
104
112
|
return;
|