@nordbyte/nordrelay 0.5.1 → 0.6.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.
Files changed (57) hide show
  1. package/.env.example +65 -11
  2. package/README.md +97 -23
  3. package/dist/access-control.js +1 -0
  4. package/dist/activity-events.js +44 -0
  5. package/dist/agent-updates.js +18 -2
  6. package/dist/audit-log.js +40 -2
  7. package/dist/bot-rendering.js +10 -7
  8. package/dist/bot.js +492 -7
  9. package/dist/channel-actions.js +7 -2
  10. package/dist/channel-adapter.js +34 -7
  11. package/dist/channel-command-service.js +156 -0
  12. package/dist/channel-turn-service.js +237 -0
  13. package/dist/codex-cli.js +1 -1
  14. package/dist/config-metadata.js +80 -13
  15. package/dist/config.js +77 -7
  16. package/dist/context-key.js +77 -5
  17. package/dist/discord-artifacts.js +165 -0
  18. package/dist/discord-bot.js +2014 -0
  19. package/dist/discord-channel-runtime.js +133 -0
  20. package/dist/discord-command-surface.js +119 -0
  21. package/dist/discord-rate-limit.js +141 -0
  22. package/dist/index.js +16 -5
  23. package/dist/job-store.js +127 -0
  24. package/dist/metrics.js +41 -0
  25. package/dist/operations.js +176 -119
  26. package/dist/relay-external-activity-monitor.js +47 -6
  27. package/dist/relay-runtime.js +1003 -268
  28. package/dist/runtime-cache.js +57 -0
  29. package/dist/session-locks.js +10 -7
  30. package/dist/state-backend.js +3 -0
  31. package/dist/support-bundle.js +18 -1
  32. package/dist/telegram-access-commands.js +15 -2
  33. package/dist/telegram-access-middleware.js +16 -3
  34. package/dist/telegram-agent-commands.js +25 -0
  35. package/dist/telegram-artifact-commands.js +46 -0
  36. package/dist/telegram-diagnostics-command.js +5 -50
  37. package/dist/telegram-general-commands.js +2 -6
  38. package/dist/telegram-operational-commands.js +14 -6
  39. package/dist/telegram-queue-commands.js +74 -4
  40. package/dist/telegram-support-command.js +7 -0
  41. package/dist/telegram-update-commands.js +27 -0
  42. package/dist/user-management.js +208 -0
  43. package/dist/web-api-contract.js +9 -0
  44. package/dist/web-dashboard-access-routes.js +74 -1
  45. package/dist/web-dashboard-artifact-routes.js +3 -3
  46. package/dist/web-dashboard-assets.js +2 -0
  47. package/dist/web-dashboard-pages.js +97 -13
  48. package/dist/web-dashboard-runtime-routes.js +53 -8
  49. package/dist/web-dashboard-session-routes.js +27 -20
  50. package/dist/web-dashboard-ui.js +1 -0
  51. package/dist/web-dashboard.js +149 -6
  52. package/dist/web-state.js +33 -2
  53. package/dist/webui-assets/dashboard.css +75 -1
  54. package/dist/webui-assets/dashboard.js +358 -47
  55. package/package.json +3 -1
  56. package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
  57. package/plugins/nordrelay/scripts/nordrelay.mjs +468 -22
package/.env.example CHANGED
@@ -1,9 +1,11 @@
1
1
  # NordRelay runtime config example.
2
- # Access is managed with NordRelay users, groups, linked Telegram identities, and enabled Telegram group chats.
2
+ # Access is managed with NordRelay users, groups, linked chat identities, and enabled group/guild channels.
3
3
  # Create the first admin with `nordrelay init` or `nordrelay user create-admin`.
4
4
 
5
5
  # Telegram
6
- # Required Telegram bot and transport settings.
6
+ # Telegram bot and transport settings.
7
+ # Start the Telegram bot adapter.
8
+ TELEGRAM_ENABLED=true
7
9
  # BotFather token.
8
10
  TELEGRAM_BOT_TOKEN=123456789:replace-me
9
11
  # polling or webhook.
@@ -20,6 +22,40 @@ TELEGRAM_WEBHOOK_PATH=/telegram/webhook
20
22
  # Optional Telegram webhook secret token.
21
23
  TELEGRAM_WEBHOOK_SECRET=
22
24
 
25
+ # Discord
26
+ # Discord bot settings. Discord is opt-in and uses the same NordRelay users, groups, and permissions as Telegram.
27
+ # Start the Discord bot adapter.
28
+ DISCORD_ENABLED=false
29
+ # Discord bot token.
30
+ DISCORD_BOT_TOKEN=
31
+ # Discord application/client id used for slash command registration.
32
+ DISCORD_CLIENT_ID=
33
+ # Comma-separated guild ids for instant guild slash-command registration.
34
+ DISCORD_GUILD_IDS=
35
+ # Optional comma-separated guild allow-list.
36
+ DISCORD_ALLOWED_GUILD_IDS=
37
+ # Optional comma-separated channel allow-list before user/group checks.
38
+ DISCORD_ALLOWED_CHANNEL_IDS=
39
+ # Read regular Discord text messages as prompts. Requires enabling the privileged intent in Discord.
40
+ DISCORD_MESSAGE_CONTENT_ENABLED=true
41
+ # slash, message, or both.
42
+ # Options: slash, message, both
43
+ DISCORD_COMMAND_MODE=both
44
+ # Register Discord slash commands on startup when client id is configured.
45
+ DISCORD_AUTO_REGISTER_COMMANDS=true
46
+ # Optional Discord override for CLI mirror mode. Uses the NordRelay default when unset.
47
+ # Options: off, status, final, full
48
+ DISCORD_CLI_MIRROR_MODE=
49
+ # Optional Discord override for mirrored edit interval.
50
+ DISCORD_CLI_MIRROR_MIN_UPDATE_MS=
51
+ # Optional Discord override for completion notifications.
52
+ # Options: off, minimal, all
53
+ DISCORD_NOTIFY_MODE=
54
+ # Optional Discord quiet hours override. Use HH-HH, off, or leave blank for default.
55
+ DISCORD_QUIET_HOURS=
56
+ # Optional Discord override for automatic artifact summaries/uploads.
57
+ DISCORD_AUTO_SEND_ARTIFACTS=
58
+
23
59
  # Agents
24
60
  # Agent access. Codex is enabled by default; Pi, Hermes, OpenClaw, and Claude Code are opt-in.
25
61
  # Allow Codex sessions.
@@ -161,15 +197,27 @@ ENABLE_TELEGRAM_REACTIONS=false
161
197
  TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS=80
162
198
  # Minimum edit interval.
163
199
  TELEGRAM_EDIT_MIN_INTERVAL_MS=1200
164
- # off, status, final, or full.
200
+ # Default mirror mode for chat adapters: off, status, final, or full.
201
+ # Options: off, status, final, full
202
+ NORDRELAY_CLI_MIRROR_MODE=status
203
+ # Default minimum mirrored edit interval.
204
+ NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS=4000
205
+ # Default completion notifications: off, minimal, or all.
206
+ # Options: off, minimal, all
207
+ NORDRELAY_NOTIFY_MODE=minimal
208
+ # Default quiet hours. Use HH-HH, off, or leave blank.
209
+ NORDRELAY_QUIET_HOURS=
210
+ # Default automatic artifact summaries/uploads for chat adapters.
211
+ NORDRELAY_AUTO_SEND_ARTIFACTS=false
212
+ # Optional Telegram override for CLI mirror mode. Uses the NordRelay default when unset.
165
213
  # Options: off, status, final, full
166
- TELEGRAM_CLI_MIRROR_MODE=status
167
- # Minimum mirrored edit interval.
168
- TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS=4000
169
- # off, minimal, or all.
214
+ TELEGRAM_CLI_MIRROR_MODE=
215
+ # Optional Telegram override for mirrored edit interval.
216
+ TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS=
217
+ # Optional Telegram override for completion notifications.
170
218
  # Options: off, minimal, all
171
- TELEGRAM_NOTIFY_MODE=minimal
172
- # HH-HH or blank.
219
+ TELEGRAM_NOTIFY_MODE=
220
+ # Optional Telegram quiet hours override. Use HH-HH, off, or leave blank for default.
173
221
  TELEGRAM_QUIET_HOURS=
174
222
  # Additional comma-separated regex patterns.
175
223
  TELEGRAM_REDACT_PATTERNS=
@@ -191,8 +239,8 @@ ARTIFACT_MAX_INBOX_DIRS=30
191
239
  ARTIFACT_IGNORE_DIRS=
192
240
  # Extra ignored glob patterns.
193
241
  ARTIFACT_IGNORE_GLOBS=
194
- # Automatically send artifact files.
195
- TELEGRAM_AUTO_SEND_ARTIFACTS=false
242
+ # Optional Telegram override for automatic artifact summaries/uploads.
243
+ TELEGRAM_AUTO_SEND_ARTIFACTS=
196
244
 
197
245
  # Workspace
198
246
  # State and workspace guardrails.
@@ -207,8 +255,14 @@ NORDRELAY_STATE_BACKEND=json
207
255
  NORDRELAY_AUDIT_MAX_EVENTS=1000
208
256
  # Write-lock TTL.
209
257
  NORDRELAY_SESSION_LOCK_TTL_MS=1800000
258
+ # Stale-while-refresh TTL for expensive dashboard API snapshots.
259
+ NORDRELAY_DASHBOARD_CACHE_TTL_MS=10000
260
+ # Maximum persisted unified jobs retained for the WebUI jobs view.
261
+ NORDRELAY_UNIFIED_JOB_MAX_ITEMS=1000
210
262
  # NPM version cache TTL.
211
263
  NORDRELAY_VERSION_CACHE_TTL_MS=3600000
264
+ # Installed agent CLI version cache TTL.
265
+ NORDRELAY_CLI_VERSION_CACHE_TTL_MS=60000
212
266
 
213
267
  # Voice
214
268
  # Optional voice transcription settings.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # NordRelay
2
2
 
3
- NordRelay is a remote control plane for coding agents across messaging channels. The current implementation connects Codex, Pi, Hermes, OpenClaw, and Claude Code coding-agent sessions to Telegram, keeps independent sessions per chat or forum topic, streams replies and tool activity back to Telegram, supports files, photos, voice input, model controls, session browsing, retry/abort, and CLI handback.
3
+ NordRelay is a remote control plane for coding agents across messaging channels. The current implementation connects Codex, Pi, Hermes, OpenClaw, and Claude Code coding-agent sessions to Telegram and Discord, keeps independent sessions per chat, thread, forum topic, or DM, streams replies and tool activity back to the active channel, supports files, photos, voice input, model controls, session browsing, retry/abort, and CLI handback.
4
4
 
5
5
  The repo is both a local Codex marketplace and a standalone Node app. The plugin lives in `plugins/nordrelay/`; the full bot runtime lives in `src/` and uses `@openai/codex-sdk` for Codex, Pi RPC mode for Pi, the Hermes API Server for Hermes, the OpenClaw Gateway WebSocket RPC surface for OpenClaw, and the Claude Agent SDK for Claude Code.
6
6
 
@@ -43,11 +43,12 @@ Session control:
43
43
 
44
44
  Adapter architecture:
45
45
 
46
- - Telegram is implemented as the first channel adapter with text, typing, streaming edits, inline buttons, files, photos, voice, topics, and webhook capability metadata.
47
- - `/channels` shows available and planned messaging adapters for Discord, WhatsApp, Slack, and Matrix.
46
+ - Telegram supports text, typing, streaming edits, inline buttons, files, photos, voice, forum topics, and polling/webhook transport.
47
+ - Discord supports text, typing, streaming edits, buttons, files, photos, voice/audio transcription, guild channels, threads, DMs, message commands, and slash commands.
48
+ - `/channels` shows available and planned messaging adapters for Telegram, Discord, WhatsApp, Slack, and Matrix.
48
49
  - Codex, Pi, Hermes, OpenClaw, and Claude Code are implemented as agent adapters.
49
50
  - `/agents` shows available/planned agent adapters and whether Codex, Pi, Hermes, OpenClaw, and Claude Code are enabled.
50
- - Shared command-action renderers and a channel runtime contract keep inbound commands, outbound messages, typing, files, inline actions, and streaming-ready delivery separate from Telegram-specific API calls.
51
+ - Shared command-action renderers and a channel runtime contract keep inbound commands, outbound messages, typing, files, inline actions, and streaming-ready delivery separate from channel-specific API calls.
51
52
 
52
53
  Codex runtime:
53
54
 
@@ -159,19 +160,33 @@ Telegram output:
159
160
  - Old artifact and inbox turn directories are pruned automatically with configurable retention.
160
161
  - Optional Telegram message reactions can acknowledge work start and completion with `ENABLE_TELEGRAM_REACTIONS=true`.
161
162
 
163
+ Discord input and output:
164
+
165
+ - Enable Discord with `DISCORD_ENABLED=true` and `DISCORD_BOT_TOKEN`.
166
+ - Set `DISCORD_CLIENT_ID` to let NordRelay register slash commands automatically.
167
+ - `DISCORD_COMMAND_MODE=both` supports slash commands and `/command` text messages. Set it to `slash` if the bot should not read message commands.
168
+ - `DISCORD_MESSAGE_CONTENT_ENABLED=true` lets regular Discord messages become prompts. The matching privileged intent must also be enabled in the Discord Developer Portal.
169
+ - Discord DMs, guild channels, and threads get independent NordRelay contexts.
170
+ - Discord attachments are staged like Telegram uploads; images are passed as image inputs and audio files are transcribed before prompting.
171
+ - Discord buttons cover session picks, model/reasoning/launch picks, queue actions, artifact actions, update jobs, and abort where Discord component limits allow.
172
+ - Discord slash commands mirror the Telegram command surface where Discord supports it: `/agent`, `/auth`, `/login`, `/logout`, `/session`, `/sessions`, `/new`, `/switch`, `/attach`, `/handback`, `/workspaces`, `/pin`, `/unpin`, `/pinned`, `/model`, `/reasoning`, `/fast`, `/launch`, `/launch_profiles`, `/queue`, `/stop`, `/retry`, `/sync`, `/progress`, `/activity`, `/audit`, `/artifacts`, `/logs`, `/version`, `/diagnostics`, `/restart`, `/update`, `/lock`, `/unlock`, `/mirror`, `/notify`, `/voice`, `/link`, `/whoami`, and `/register_channel`.
173
+
162
174
  Authentication and safety:
163
175
 
164
176
  - WebUI login is required for every dashboard page, API route, SSE stream, artifact download, and health endpoint.
165
- - Access is managed through NordRelay users, groups, permissions, web sessions, and linked Telegram identities.
166
- - Built-in groups are `Admin`, `User`, and `Read Only`; custom groups can be created in the WebUI and can restrict allowed agents, workspace roots, and Telegram chats.
177
+ - Access is managed through NordRelay users, groups, permissions, web sessions, linked Telegram identities, and linked Discord identities.
178
+ - Built-in groups are `Admin`, `User`, and `Read Only`; custom groups can be created in the WebUI and can restrict allowed agents, workspace roots, Telegram chats, and Discord channels.
167
179
  - The last active admin cannot be disabled or demoted, and web sessions are revoked when passwords or group memberships change.
168
180
  - Admins can review and revoke active WebUI sessions from the Users page.
169
181
  - Telegram private chats require a linked active NordRelay user.
170
182
  - Telegram group and forum chats must be registered before use; admins can run `/register_chat` in the chat or enable chats in the WebUI.
183
+ - Discord DMs require a linked active NordRelay user.
184
+ - Discord guild channels and threads must be registered before use; admins can run `/register_channel` in the channel or enable channels in the WebUI.
171
185
  - `/whoami` shows the linked NordRelay account and groups.
172
186
  - `/link <code>` links a Telegram account to a NordRelay user after a link code is created in the WebUI or with `nordrelay user link-code`.
173
- - WebUI login and Telegram link attempts are rate-limited to reduce brute-force risk.
174
- - User, group, Telegram-link, Telegram-chat, web-session, login, and permission-denied events are written to the audit log.
187
+ - `/link <code>` also links a Discord account when the code was created as a Discord link code.
188
+ - WebUI login and chat-account link attempts are rate-limited to reduce brute-force risk.
189
+ - User, group, Telegram-link, Telegram-chat, Discord-link, Discord-channel, web-session, login, and permission-denied events are written to the audit log.
175
190
  - `/auth` reports Codex authentication, Pi provider environment health, Hermes API Server reachability, OpenClaw Gateway reachability, or Claude Code CLI auth for the selected agent.
176
191
  - `/login` starts Telegram-managed CLI auth for Codex, Hermes, or Claude Code when enabled.
177
192
  - `/logout` signs out of CLI auth for Codex, Hermes, or Claude Code; Codex logout is disabled while `CODEX_API_KEY` is in use.
@@ -183,23 +198,29 @@ Authentication and safety:
183
198
  Operations:
184
199
 
185
200
  - Plugin command/skill starts, stops, restarts, and inspects the connector process.
186
- - Manual process commands support `start`, `stop`, `restart`, `status`, and `foreground`.
201
+ - Manual process commands support `start`, `stop`, `restart`, `status`, `update`, and `foreground`.
187
202
  - Telegram admin commands support `/logs`, `/diagnostics`, `/support`, `/restart`, and `/update` for NordRelay and agent CLIs.
188
- - `/update` detects the install type: npm installs update with `npm install -g @nordbyte/nordrelay@latest`; source checkouts pull `origin/main`, install dependencies, run check, tests, and build, then restart.
203
+ - `nordrelay update`, `/update`, and the WebUI update button detect the install type: npm installs update with `npm install -g @nordbyte/nordrelay@latest`; source checkouts pull `origin/main`, install dependencies, run check, tests, and build, then restart if the connector is running.
189
204
  - `/update agents`, `/update <agent>`, `/update install <agent>`, `/update jobs`, `/update log <id>`, `/update cancel <id>`, and `/update input <id> <text>` manage Codex, Pi, Hermes, OpenClaw, and Claude Code updater or installer jobs from Telegram.
190
205
  - `/logs` renders redacted connector, NordRelay update, and agent update logs with local-time timestamps, levels, file path, last-modified time, and highlighted warnings/errors.
191
206
  - Logs can be emitted as timestamped plain text or JSON records with `CONNECTOR_LOG_FORMAT`.
192
207
  - Telegram sends/edits/documents are routed through a rate-limit queue that honors Telegram retry-after responses.
208
+ - Mirror, notification, quiet-hour, and automatic artifact-delivery defaults are configured through channel-neutral `NORDRELAY_*` settings, with Telegram and Discord override keys when a channel should differ.
209
+ - The WebUI Tasks page includes a unified Jobs view for active WebUI turns, external CLI turns, queued prompts, agent update/install jobs, self-updates, and diagnostics bundle exports, with log, cancel, and retry actions where supported.
210
+ - Unified Jobs are persisted across restarts and retain recent prompt, queue, update, connector-update, and support-bundle history for WebUI inspection.
211
+ - The WebUI Metrics page reports queue state, active/completed/failed turns, job counts, average prompt duration, and Telegram/Discord rate-limit counters.
212
+ - Expensive dashboard views such as version checks, adapter health, and diagnostics use a short stale-while-refresh server cache so the UI can render recent data while fresh checks run in the background.
193
213
  - Context metadata, queues, and preferences are written atomically with backup recovery.
194
214
  - Context metadata, queues, preferences, audit events, and locks can use JSON files or the optional SQLite state backend with `NORDRELAY_STATE_BACKEND=sqlite`.
195
215
  - Runtime config, state, and logs are written under `~/.nordrelay/`.
196
216
  - `nordrelay init` creates a private runtime config, `nordrelay doctor` validates host prerequisites, and `nordrelay web` starts the connector plus a full local WebUI dashboard.
217
+ - On first WebUI startup without an admin account, NordRelay shows a setup wizard for creating the first admin; remote setup requires the one-time token printed in the server console.
197
218
  - The WebUI has responsive header/sidebar/footer navigation, live chat streaming, session controls, queue/artifact/log/diagnostic views, and settings management.
198
219
  - The WebUI supports light and dark themes, tabbed settings groups, paginated session browsing, and chat uploads for images, documents, and audio transcription.
199
220
  - The WebUI exposes REST and SSE endpoints for chat streaming, sessions, settings, queue, artifacts, logs, health, diagnostics, and redacted diagnostics bundle export.
200
221
  - The dashboard can bind to `127.0.0.1` or `0.0.0.0`; user login and session cookies are mandatory in both modes.
201
222
  - Telegram can run with long polling or an HTTP webhook via `TELEGRAM_TRANSPORT=webhook`.
202
- - Version freshness checks are cached with `NORDRELAY_VERSION_CACHE_TTL_MS` to keep `/version` responsive.
223
+ - Version freshness checks are cached with `NORDRELAY_VERSION_CACHE_TTL_MS`, and installed agent CLI version checks are cached with `NORDRELAY_CLI_VERSION_CACHE_TTL_MS`, to keep `/version` and adapter health responsive.
203
224
  - CI runs on Ubuntu, Windows, and macOS with typecheck, Vitest, Playwright WebUI browser tests, package dry run, npm audit, and a separate secret-scan workflow.
204
225
  - `npm run dev`, `npm run build`, `npm run check`, `npm test`, `npm run test:e2e`, `npm start`, `npm stop`, and `npm run status` are available.
205
226
  - Dockerfile and `docker-compose.yml` are included for containerized operation.
@@ -218,12 +239,16 @@ nordrelay start
218
239
  ```
219
240
 
220
241
  npm is the fastest install path and is the recommended default for normal use. `nordrelay init` writes the private runtime config to `~/.nordrelay/nordrelay.env`.
242
+ If you start `nordrelay web` before creating an admin, the dashboard opens a first-run setup wizard. Remote setup requires the one-time setup token printed in the NordRelay console.
221
243
 
222
244
  Non-interactive setup is also supported:
223
245
 
224
246
  ```bash
225
247
  nordrelay init \
226
248
  --token 123456789:replace-me \
249
+ --enable-discord \
250
+ --discord-token "discord-bot-token" \
251
+ --discord-client-id "discord-client-id" \
227
252
  --admin-email you@example.com \
228
253
  --admin-name "Your Name" \
229
254
  --admin-password "replace-with-a-long-password" \
@@ -231,6 +256,7 @@ nordrelay init \
231
256
  ```
232
257
 
233
258
  `--telegram-user-id` is optional, but linking the first admin during setup is the fastest way to use Telegram immediately.
259
+ Use `--discord-user-id <id>` with `nordrelay user create-admin` or `nordrelay user link-discord` to link Discord directly.
234
260
 
235
261
  Source checkout setup:
236
262
 
@@ -253,10 +279,23 @@ Create the Telegram bot:
253
279
  5. Create the first admin user with `nordrelay init` or `nordrelay user create-admin`.
254
280
  6. Link Telegram from the WebUI, with `nordrelay user link-telegram`, or by creating a link code and sending `/link <code>` to the bot.
255
281
 
282
+ Create the Discord bot:
283
+
284
+ 1. Open the Discord Developer Portal and create an application.
285
+ 2. Add a bot user and copy the bot token into `DISCORD_BOT_TOKEN`.
286
+ 3. Copy the application client ID into `DISCORD_CLIENT_ID`.
287
+ 4. Enable the Message Content intent if you want regular Discord messages to become prompts.
288
+ 5. Invite the bot with application-command, message-send, message-read, attachment, and thread permissions.
289
+ 6. Link Discord from the WebUI, with `nordrelay user link-discord`, or by creating a Discord link code and sending `/link <code>` to the bot.
290
+ 7. In guild channels, run `/register_channel` once from an admin-linked Discord account.
291
+
256
292
  Minimal private-bot `~/.nordrelay/nordrelay.env`:
257
293
 
258
294
  ```dotenv
259
295
  TELEGRAM_BOT_TOKEN=123456789:replace-me
296
+ DISCORD_ENABLED=false
297
+ DISCORD_BOT_TOKEN=
298
+ DISCORD_CLIENT_ID=
260
299
  NORDRELAY_CODEX_ENABLED=true
261
300
  NORDRELAY_PI_ENABLED=false
262
301
  NORDRELAY_HERMES_ENABLED=false
@@ -267,14 +306,17 @@ CODEX_SANDBOX_MODE=workspace-write
267
306
  CODEX_APPROVAL_POLICY=never
268
307
  ```
269
308
 
270
- User and Telegram access management:
309
+ User and chat access management:
271
310
 
272
311
  - `nordrelay init` creates the first admin user and writes `~/.nordrelay/users.json`.
273
312
  - `nordrelay user create-admin --email you@example.com --name "Your Name"` creates another admin.
274
313
  - `nordrelay user create --email dev@example.com --name "Dev" --group user` creates a normal user.
275
314
  - `nordrelay user link-telegram --email you@example.com --telegram-user-id 123456789` links a Telegram account directly.
276
- - `nordrelay user link-code --email you@example.com` creates a short-lived code that the user sends as `/link <code>` to the bot.
277
- - Group chats are disabled until an admin enables them from the WebUI or runs `/register_chat` inside the group.
315
+ - `nordrelay user link-discord --email you@example.com --discord-user-id 123456789012345678` links a Discord account directly.
316
+ - `nordrelay user link-code --email you@example.com` creates a short-lived Telegram code that the user sends as `/link <code>` to the Telegram bot.
317
+ - `nordrelay user discord-link-code --email you@example.com` creates a short-lived Discord code that the user sends as `/link <code>` to the Discord bot.
318
+ - Telegram group chats are disabled until an admin enables them from the WebUI or runs `/register_chat` inside the group.
319
+ - Discord guild channels are disabled until an admin enables them from the WebUI or runs `/register_channel` inside the channel.
278
320
 
279
321
  Codex authentication:
280
322
 
@@ -356,6 +398,7 @@ nordrelay init
356
398
  nordrelay doctor
357
399
  nordrelay start
358
400
  nordrelay status
401
+ nordrelay update
359
402
  nordrelay restart
360
403
  nordrelay stop
361
404
  nordrelay foreground
@@ -367,6 +410,7 @@ Source checkout process commands:
367
410
  ```bash
368
411
  node plugins/nordrelay/scripts/nordrelay.mjs start
369
412
  node plugins/nordrelay/scripts/nordrelay.mjs status
413
+ node plugins/nordrelay/scripts/nordrelay.mjs update
370
414
  node plugins/nordrelay/scripts/nordrelay.mjs restart
371
415
  node plugins/nordrelay/scripts/nordrelay.mjs stop
372
416
  node plugins/nordrelay/scripts/nordrelay.mjs foreground
@@ -420,6 +464,7 @@ The dashboard is a second NordRelay client next to Telegram. It can:
420
464
  - Control the active session model, reasoning/thinking, fast mode, and launch profile directly from the chat view.
421
465
  - Abort turns, hand sessions back to the native CLI, and inspect the active session.
422
466
  - Manage queued prompts with pause/resume, run, cancel, reorder buttons, and drag-and-drop prioritization.
467
+ - Inspect unified jobs across queued prompts, active turns, mirrored CLI work, agent updates, self-updates, and support-bundle exports.
423
468
  - Browse, preview, download, ZIP, and delete artifacts.
424
469
  - Inspect the activity timeline for WebUI and mirrored CLI turns.
425
470
  - Edit all supported runtime settings from tabbed Settings groups with option selects, validation feedback, and restart actions.
@@ -522,6 +567,18 @@ Run NordRelay behind your reverse proxy so the public URL forwards to `http://12
522
567
  - `/update` updates through npm or git depending on the detected install type, then restarts only on success. Requires `updates.run`.
523
568
  - `/update agents`, `/update <agent>`, `/update install <agent>`, `/update jobs`, `/update log <id>`, `/update cancel <id>`, and `/update input <id> <text>` manage agent CLI update and install jobs. Requires `updates.run`.
524
569
 
570
+ ## Discord Commands
571
+
572
+ Discord supports slash commands and `/command` text messages for the shared command set. The primary differences from Telegram are:
573
+
574
+ - `/register_channel` replaces `/register_chat` for guild channels and threads.
575
+ - `/prompt <text>` is available for slash-command-only deployments where regular message content is disabled.
576
+ - `/link <code>` consumes Discord link codes created in the WebUI or with `nordrelay user discord-link-code`.
577
+ - `/queue`, `/sessions`, `/agent`, `/model`, `/reasoning`, `/launch`, `/artifacts`, `/update`, and `/stop` use Discord buttons where component limits allow.
578
+ - `/artifacts latest`, `/artifacts zip latest`, `/artifacts images`, `/artifacts docs`, `/artifacts search <text>`, and `/artifacts delete <turn-id>` are available in Discord.
579
+ - Unsafe launch profiles require explicit confirmation with `/launch <profile-id> confirm`.
580
+ - Discord does not support Telegram reactions or Telegram webhook transport; typing, message edits, attachments, files, DMs, guild channels, and threads are supported.
581
+
525
582
  ## Command Examples
526
583
 
527
584
  Switching to an existing thread:
@@ -702,6 +759,7 @@ Voice transcription uses `OPENAI_API_KEY`, not `CODEX_API_KEY`.
702
759
 
703
760
  Telegram:
704
761
 
762
+ - `TELEGRAM_ENABLED`: starts the Telegram adapter. Defaults to `true`.
705
763
  - `TELEGRAM_BOT_TOKEN`: required BotFather token.
706
764
  - `TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS`: minimum interval for normal Telegram API sends. Defaults to `80`.
707
765
  - `TELEGRAM_EDIT_MIN_INTERVAL_MS`: minimum interval for Telegram message edits. Defaults to `1200`.
@@ -711,19 +769,34 @@ Telegram:
711
769
  - `TELEGRAM_WEBHOOK_PORT`: local bind port for webhook mode. Defaults to `8080`.
712
770
  - `TELEGRAM_WEBHOOK_PATH`: webhook request path. Defaults to `/telegram/webhook`.
713
771
  - `TELEGRAM_WEBHOOK_SECRET`: optional Telegram webhook secret token.
714
- - `TELEGRAM_CLI_MIRROR_MODE`: default CLI mirror mode: `off`, `status`, `final`, or `full`. Defaults to `status`.
715
- - `TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS`: minimum interval for mirrored CLI status edits. Defaults to `4000`.
716
- - `TELEGRAM_NOTIFY_MODE`: default notification mode: `off`, `minimal`, or `all`. Defaults to `minimal`.
717
- - `TELEGRAM_QUIET_HOURS`: optional quiet-hour range in `HH-HH` format, for example `22-7`.
772
+ - `NORDRELAY_CLI_MIRROR_MODE`: default CLI mirror mode for chat adapters: `off`, `status`, `final`, or `full`. Defaults to `status`.
773
+ - `NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS`: default minimum interval for mirrored CLI status edits. Defaults to `4000`.
774
+ - `NORDRELAY_NOTIFY_MODE`: default notification mode for chat adapters: `off`, `minimal`, or `all`. Defaults to `minimal`.
775
+ - `NORDRELAY_QUIET_HOURS`: optional default quiet-hour range in `HH-HH` format, for example `22-7`; use `off` in a channel override to disable inherited quiet hours.
776
+ - `NORDRELAY_AUTO_SEND_ARTIFACTS`: default automatic artifact summaries/uploads for chat adapters. Defaults to `false`.
777
+ - `TELEGRAM_CLI_MIRROR_MODE`, `TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS`, `TELEGRAM_NOTIFY_MODE`, `TELEGRAM_QUIET_HOURS`, and `TELEGRAM_AUTO_SEND_ARTIFACTS`: optional Telegram-specific overrides.
718
778
  - `TELEGRAM_REDACT_PATTERNS`: comma-separated regular expressions for additional Telegram/log redaction.
719
779
 
780
+ Discord:
781
+
782
+ - `DISCORD_ENABLED`: starts the Discord adapter. Defaults to `false`.
783
+ - `DISCORD_BOT_TOKEN`: required Discord bot token when Discord is enabled.
784
+ - `DISCORD_CLIENT_ID`: Discord application/client id used for slash-command registration.
785
+ - `DISCORD_GUILD_IDS`: optional comma-separated guild ids for instant guild slash-command registration.
786
+ - `DISCORD_ALLOWED_GUILD_IDS`: optional guild allow-list before user/group permissions are checked.
787
+ - `DISCORD_ALLOWED_CHANNEL_IDS`: optional channel allow-list before user/group permissions are checked.
788
+ - `DISCORD_MESSAGE_CONTENT_ENABLED`: reads regular Discord text messages as prompts. Defaults to `true`.
789
+ - `DISCORD_COMMAND_MODE`: `slash`, `message`, or `both`. Defaults to `both`.
790
+ - `DISCORD_AUTO_REGISTER_COMMANDS`: registers slash commands on startup when `DISCORD_CLIENT_ID` is set. Defaults to `true`.
791
+ - `DISCORD_CLI_MIRROR_MODE`, `DISCORD_CLI_MIRROR_MIN_UPDATE_MS`, `DISCORD_NOTIFY_MODE`, `DISCORD_QUIET_HOURS`, and `DISCORD_AUTO_SEND_ARTIFACTS`: optional Discord-specific overrides for the channel-neutral defaults.
792
+
720
793
  User management:
721
794
 
722
- - Users, groups, Telegram identities, Telegram group-chat access, and web sessions are stored in `~/.nordrelay/users.json`.
723
- - Manage users in the WebUI Users page or with `nordrelay user list`, `create-admin`, `create`, `reset-password`, `link-telegram`, and `link-code`.
795
+ - Users, groups, Telegram identities, Telegram group-chat access, Discord identities, Discord channel access, and web sessions are stored in `~/.nordrelay/users.json`.
796
+ - Manage users in the WebUI Users page or with `nordrelay user list`, `create-admin`, `create`, `reset-password`, `link-telegram`, `link-discord`, `link-code`, and `discord-link-code`.
724
797
  - Built-in groups are `admin`, `user`, and `readonly`.
725
798
  - Group permissions include `inspect`, `sessions.read`, `sessions.write`, `prompt.send`, `prompt.abort`, `files.read`, `files.write`, `settings.read`, `settings.write`, `auth.manage`, `diagnostics.read`, `logs.read`, `logs.clear`, `queue.read`, `queue.write`, `updates.run`, `system.restart`, `users.read`, `users.write`, and `audit.read`.
726
- - Custom groups can also restrict access to specific agent ids, workspace roots, and Telegram chat ids.
799
+ - Custom groups can also restrict access to specific agent ids, workspace roots, Telegram chat ids, and Discord channel ids.
727
800
 
728
801
  Agent selection:
729
802
 
@@ -732,11 +805,12 @@ Agent selection:
732
805
  - `NORDRELAY_HERMES_ENABLED`: enables Hermes contexts through the Hermes API Server. Defaults to `false`.
733
806
  - `NORDRELAY_OPENCLAW_ENABLED`: enables OpenClaw contexts through the OpenClaw Gateway. Defaults to `false`.
734
807
  - `NORDRELAY_CLAUDE_CODE_ENABLED`: enables Claude Code contexts through the Claude Agent SDK. Defaults to `false`.
735
- - `NORDRELAY_DEFAULT_AGENT`: `codex`, `pi`, `hermes`, `openclaw`, or `claude-code`, used for new Telegram contexts. Defaults to the first enabled agent.
808
+ - `NORDRELAY_DEFAULT_AGENT`: `codex`, `pi`, `hermes`, `openclaw`, or `claude-code`, used for new chat contexts. Defaults to the first enabled agent.
736
809
  - `NORDRELAY_STATE_BACKEND`: `json` or `sqlite`. JSON is the default; SQLite requires `better-sqlite3`.
737
810
  - `NORDRELAY_AUDIT_MAX_EVENTS`: maximum audit events retained. Defaults to `1000`.
738
811
  - `NORDRELAY_SESSION_LOCK_TTL_MS`: session write-lock TTL. Defaults to `1800000`.
739
812
  - `NORDRELAY_VERSION_CACHE_TTL_MS`: npm version freshness cache TTL. Defaults to `3600000`; set `0` to disable.
813
+ - `NORDRELAY_CLI_VERSION_CACHE_TTL_MS`: installed agent CLI version cache TTL. Defaults to `60000`; set `0` to disable.
740
814
 
741
815
  Dashboard:
742
816
 
@@ -837,7 +911,7 @@ NordRelay wrapper:
837
911
 
838
912
  - `NORDRELAY_HOME`: config/state/log directory override. Defaults to `~/.nordrelay`.
839
913
  - `NORDRELAY_SOURCE_ROOT`: runtime source root override. Useful when the plugin is launched from Codex cache.
840
- - `NORDRELAY_UPDATE_METHOD`: optional `auto`, `npm`, or `git` self-update method override. Auto uses git when the runtime root has a `.git` directory and npm otherwise.
914
+ - `NORDRELAY_UPDATE_METHOD`: optional `auto`, `npm`, or `git` self-update method override used by `nordrelay update`, `/update`, and the WebUI update button. Auto uses git when the runtime root has a `.git` directory and npm otherwise.
841
915
  - Agent updates from the dashboard and Telegram use each agent's native updater where possible: `codex update`, `pi update pi`, `hermes update --yes`, `openclaw update --yes`, and `claude update`. Not-installed agents can be installed from the dashboard or with `/update install <agent>` using npm global installs.
842
916
  - `NORDRELAY_KEEP_PENDING_UPDATES`: set true to avoid dropping pending Telegram updates on start.
843
917
  - `NORDRELAY_FORWARD_TOOL_OUTPUT`: backward-compatible alias that sets `TOOL_VERBOSITY=all` when `TOOL_VERBOSITY` is unset.
@@ -119,6 +119,7 @@ const COMMAND_PERMISSIONS = new Map([
119
119
  ["abort", "prompt.abort"],
120
120
  ["stop", "prompt.abort"],
121
121
  ["register_chat", "users.write"],
122
+ ["register_channel", "users.write"],
122
123
  ["chat_access", "users.write"],
123
124
  ["link", "inspect"],
124
125
  ]);
@@ -0,0 +1,44 @@
1
+ export function activityCategoryForType(type) {
2
+ if (/^(prompt|cli_turn|voice|upload|attachment)/.test(type))
3
+ return "prompt";
4
+ if (/^(session|agent_switch|handback|model_|reasoning_|fast_|launch_)/.test(type))
5
+ return "session";
6
+ if (/^queue_/.test(type))
7
+ return "queue";
8
+ if (/^agent_(install|update)/.test(type))
9
+ return "agent-update";
10
+ if (/^(artifact|artifacts)/.test(type))
11
+ return "artifact";
12
+ if (/^(auth|login|logout)/.test(type))
13
+ return "auth";
14
+ if (/^(user_|group_|telegram_chat_|telegram_link|discord_channel_|discord_link|permission_|access_|lock_)/.test(type))
15
+ return "security";
16
+ if (/^(tool_|cli_tool)/.test(type))
17
+ return "tool";
18
+ return "system";
19
+ }
20
+ export function activityActorLabel(actor) {
21
+ if (!actor) {
22
+ return "system";
23
+ }
24
+ return actor.label || actor.username || actor.id || actor.channelUserId || actor.channel;
25
+ }
26
+ export function auditCategoryForAction(action) {
27
+ if (/^prompt_/.test(action))
28
+ return "prompt";
29
+ if (/^queue_/.test(action))
30
+ return "queue";
31
+ if (/^lock_/.test(action))
32
+ return "security";
33
+ if (/^auth_/.test(action))
34
+ return "auth";
35
+ if (/^(permission_|user_|group_|telegram_|discord_)/.test(action))
36
+ return "security";
37
+ if (/^(artifact|file)/.test(action))
38
+ return "artifact";
39
+ if (/^(model_|reasoning_|fast_|launch_|session_|handback)/.test(action))
40
+ return "session";
41
+ if (/^(tool_)/.test(action))
42
+ return "tool";
43
+ return "system";
44
+ }
@@ -105,10 +105,11 @@ export class AgentUpdateManager {
105
105
  `Working directory: ${job.cwd}`,
106
106
  "",
107
107
  ].join("\n"));
108
- const child = spawn(plan.command, plan.args, {
108
+ const useShell = process.platform === "win32";
109
+ const child = spawn(useShell ? formatShellCommand(plan.command, plan.args) : plan.command, useShell ? [] : plan.args, {
109
110
  cwd: plan.cwd,
110
111
  env: { ...process.env, ...(this.options.env ?? {}), ...(context.env ?? {}) },
111
- shell: process.platform === "win32",
112
+ shell: useShell,
112
113
  windowsHide: true,
113
114
  stdio: "pipe",
114
115
  });
@@ -342,6 +343,21 @@ function looksLikePrompt(text) {
342
343
  const tail = text.split(/\r?\n/).slice(-4).join("\n");
343
344
  return /\b(y\/n|yes\/no|continue|proceed|confirm|password|passphrase|token|api key|enter|select)\b|[?>]\s*$/i.test(tail);
344
345
  }
346
+ function formatShellCommand(command, args) {
347
+ return [command, ...args].map(quoteWindowsCmdArg).join(" ");
348
+ }
349
+ function quoteWindowsCmdArg(value) {
350
+ if (value.length === 0) {
351
+ return "\"\"";
352
+ }
353
+ if (!/[\s"&|<>()^%]/.test(value)) {
354
+ return value;
355
+ }
356
+ return `"${value
357
+ .replace(/%/g, "%%")
358
+ .replace(/(\\*)"/g, '$1$1\\"')
359
+ .replace(/(\\+)$/g, "$1$1")}"`;
360
+ }
345
361
  function capitalize(value) {
346
362
  return value.charAt(0).toUpperCase() + value.slice(1);
347
363
  }
package/dist/audit-log.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { auditCategoryForAction, } from "./activity-events.js";
2
3
  import { createDocumentStore } from "./state-backend.js";
3
4
  export class AuditLogStore {
4
5
  store;
@@ -18,6 +19,7 @@ export class AuditLogStore {
18
19
  id: randomUUID().replace(/-/g, "").slice(0, 12),
19
20
  timestamp: new Date().toISOString(),
20
21
  ...event,
22
+ category: event.category ?? auditCategoryForAction(event.action),
21
23
  channelId: event.channelId ?? "telegram",
22
24
  };
23
25
  payload.events.push(next);
@@ -27,8 +29,22 @@ export class AuditLogStore {
27
29
  this.store.write(payload);
28
30
  return next;
29
31
  }
30
- list(limit = 20) {
31
- return this.readPayload().events.slice(-Math.max(1, Math.min(200, limit))).reverse();
32
+ list(options = 20) {
33
+ const resolved = typeof options === "number" ? { limit: options } : options;
34
+ const limit = Math.max(1, Math.min(500, resolved.limit ?? 20));
35
+ const since = normalizeSince(resolved.since);
36
+ return this.readPayload().events
37
+ .filter((event) => !resolved.channelId || resolved.channelId === "all" || event.channelId === resolved.channelId)
38
+ .filter((event) => !resolved.status || resolved.status === "all" || event.status === resolved.status)
39
+ .filter((event) => !resolved.action || resolved.action === "all" || event.action === resolved.action)
40
+ .filter((event) => !resolved.category || resolved.category === "all" || (event.category ?? auditCategoryForAction(event.action)) === resolved.category)
41
+ .filter((event) => !resolved.agentId || resolved.agentId === "all" || event.agentId === resolved.agentId)
42
+ .filter((event) => !resolved.threadId || event.threadId === resolved.threadId)
43
+ .filter((event) => !resolved.workspace || event.workspace === resolved.workspace)
44
+ .filter((event) => !resolved.actor || auditActorMatches(event, resolved.actor))
45
+ .filter((event) => !since || Date.parse(event.timestamp) >= since)
46
+ .slice(-limit)
47
+ .reverse();
32
48
  }
33
49
  readPayload() {
34
50
  const payload = this.store.read();
@@ -41,6 +57,28 @@ export class AuditLogStore {
41
57
  };
42
58
  }
43
59
  }
60
+ function normalizeSince(value) {
61
+ if (value === undefined || value === null || value === "") {
62
+ return null;
63
+ }
64
+ const time = typeof value === "number" ? value : Date.parse(value);
65
+ return Number.isFinite(time) ? time : null;
66
+ }
67
+ function auditActorMatches(event, query) {
68
+ const needle = query.trim().toLowerCase();
69
+ if (!needle) {
70
+ return true;
71
+ }
72
+ return [
73
+ event.actor?.id,
74
+ event.actor?.label,
75
+ event.actor?.username,
76
+ event.actor?.channelUserId,
77
+ event.actor?.channel,
78
+ event.actorId,
79
+ event.actorRole,
80
+ ].some((value) => String(value ?? "").toLowerCase().includes(needle));
81
+ }
44
82
  function isAuditEvent(value) {
45
83
  if (!value || typeof value !== "object" || Array.isArray(value)) {
46
84
  return false;
@@ -61,11 +61,12 @@ export function renderAuditEvents(events) {
61
61
  }
62
62
  const lines = events.map((event) => {
63
63
  const time = formatLocalDateTime(new Date(event.timestamp));
64
- const actor = event.actorId ? `user ${event.actorId}` : "system";
64
+ const actor = event.actor?.label || event.actor?.username || event.actor?.id || (event.actorId ? `user ${event.actorId}` : "system");
65
65
  const prompt = event.promptId ? ` · ${event.promptId}` : "";
66
66
  const detail = event.detail ? ` · ${trimLine(event.detail, 90)}` : "";
67
67
  const description = event.description ? ` · ${trimLine(event.description, 90)}` : "";
68
- return `${time} · ${event.status.toUpperCase()} · ${event.action} · ${actor}${prompt}${description}${detail}`;
68
+ const category = event.category ? ` · ${event.category}` : "";
69
+ return `${time} · ${event.status.toUpperCase()} · ${event.action}${category} · ${actor}${prompt}${description}${detail}`;
69
70
  });
70
71
  return {
71
72
  plain: ["Audit:", ...lines].join("\n"),
@@ -95,7 +96,9 @@ export function formatLockOwner(lock) {
95
96
  if (!lock) {
96
97
  return "nobody";
97
98
  }
98
- return lock.ownerName ? `${lock.ownerName} (${lock.ownerId})` : `user ${lock.ownerId}`;
99
+ const label = lock.ownerLabel || lock.ownerUserId;
100
+ const channel = lock.ownerChannel ? ` via ${lock.ownerChannel}` : "";
101
+ return `${label} (${lock.ownerUserId})${channel}`;
99
102
  }
100
103
  export function formatTelegramName(ctx) {
101
104
  const firstName = ctx.from?.first_name?.trim();
@@ -420,13 +423,13 @@ export function renderDiagnosticsPlain(config, registry, health, authenticated,
420
423
  `Tool verbosity: ${config.toolVerbosity}`,
421
424
  `Telegram rate limit queued/running/retries/429: ${runtime.rateLimit.queued}/${runtime.rateLimit.running}/${runtime.rateLimit.retries}/${runtime.rateLimit.rateLimitHits}`,
422
425
  `Telegram last retry_after: ${runtime.rateLimit.lastRetryAfterSeconds ?? "-"}s`,
423
- `CLI mirror mode/update: ${runtime.mirrorMode} / ${config.telegramMirrorMinUpdateMs} ms`,
426
+ `CLI mirror mode/update: ${runtime.mirrorMode} / ${config.telegramMirrorMinUpdateMs} ms (default ${config.mirrorMode} / ${config.mirrorMinUpdateMs} ms)`,
424
427
  `Notify/quiet: ${runtime.notifyMode} / ${runtime.quietHours}`,
425
428
  `Voice: ${runtime.voiceBackend} / ${runtime.voiceLanguage} / transcribe-only ${runtime.voiceTranscribeOnly ? "on" : "off"}`,
426
429
  `Sync interval: ${config.codexSyncIntervalMs} ms`,
427
430
  `External busy check/stale: ${config.codexExternalBusyCheckMs} ms / ${config.codexExternalBusyStaleMs} ms`,
428
431
  `External mirrors/timers/status messages: ${runtime.externalMirrors}/${runtime.externalQueueTimers}/${runtime.queueStatusMessages}`,
429
- `Auto-send artifacts: ${config.telegramAutoSendArtifacts ? "yes" : "no"}`,
432
+ `Auto-send artifacts: ${config.telegramAutoSendArtifacts ? "yes" : "no"} (default ${config.autoSendArtifacts ? "yes" : "no"})`,
430
433
  `Artifact ignore dirs/globs: ${config.artifactIgnoreDirs.length}/${config.artifactIgnoreGlobs.length}`,
431
434
  `Artifact retention: ${config.artifactRetentionDays}d / ${config.artifactMaxTurnDirs} turns / ${config.artifactMaxInboxDirs} inbox dirs`,
432
435
  `Workspace allowed/warn roots: ${config.workspaceAllowedRoots.length}/${config.workspaceWarnRoots.length}`,
@@ -465,13 +468,13 @@ export function renderDiagnosticsHTML(config, registry, health, authenticated, r
465
468
  `<b>Tool verbosity:</b> <code>${escapeHTML(config.toolVerbosity)}</code>`,
466
469
  `<b>Telegram rate limit queued/running/retries/429:</b> <code>${runtime.rateLimit.queued}/${runtime.rateLimit.running}/${runtime.rateLimit.retries}/${runtime.rateLimit.rateLimitHits}</code>`,
467
470
  `<b>Telegram last retry_after:</b> <code>${escapeHTML(String(runtime.rateLimit.lastRetryAfterSeconds ?? "-"))}s</code>`,
468
- `<b>CLI mirror mode/update:</b> <code>${escapeHTML(runtime.mirrorMode)} / ${config.telegramMirrorMinUpdateMs} ms</code>`,
471
+ `<b>CLI mirror mode/update:</b> <code>${escapeHTML(runtime.mirrorMode)} / ${config.telegramMirrorMinUpdateMs} ms (default ${config.mirrorMode} / ${config.mirrorMinUpdateMs} ms)</code>`,
469
472
  `<b>Notify/quiet:</b> <code>${escapeHTML(runtime.notifyMode)} / ${escapeHTML(runtime.quietHours)}</code>`,
470
473
  `<b>Voice:</b> <code>${escapeHTML(runtime.voiceBackend)} / ${escapeHTML(runtime.voiceLanguage)} / transcribe-only ${runtime.voiceTranscribeOnly ? "on" : "off"}</code>`,
471
474
  `<b>Sync interval:</b> <code>${config.codexSyncIntervalMs} ms</code>`,
472
475
  `<b>External busy check/stale:</b> <code>${config.codexExternalBusyCheckMs} ms / ${config.codexExternalBusyStaleMs} ms</code>`,
473
476
  `<b>External mirrors/timers/status messages:</b> <code>${runtime.externalMirrors}/${runtime.externalQueueTimers}/${runtime.queueStatusMessages}</code>`,
474
- `<b>Auto-send artifacts:</b> <code>${config.telegramAutoSendArtifacts ? "yes" : "no"}</code>`,
477
+ `<b>Auto-send artifacts:</b> <code>${config.telegramAutoSendArtifacts ? "yes" : "no"} (default ${config.autoSendArtifacts ? "yes" : "no"})</code>`,
475
478
  `<b>Artifact ignore dirs/globs:</b> <code>${config.artifactIgnoreDirs.length}/${config.artifactIgnoreGlobs.length}</code>`,
476
479
  `<b>Artifact retention:</b> <code>${config.artifactRetentionDays}d / ${config.artifactMaxTurnDirs} turns / ${config.artifactMaxInboxDirs} inbox dirs</code>`,
477
480
  `<b>Workspace allowed/warn roots:</b> <code>${config.workspaceAllowedRoots.length}/${config.workspaceWarnRoots.length}</code>`,