@nordbyte/nordrelay 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +155 -64
- package/README.md +81 -65
- package/dist/access-control.js +126 -115
- package/dist/agent-updates.js +62 -9
- package/dist/bot-rendering.js +838 -0
- package/dist/bot-ui.js +1 -0
- package/dist/bot.js +342 -2498
- package/dist/channel-actions.js +8 -8
- package/dist/channel-runtime.js +89 -0
- package/dist/config-metadata.js +238 -0
- package/dist/config.js +0 -58
- package/dist/index.js +8 -0
- package/dist/operations.js +63 -9
- package/dist/relay-artifact-service.js +126 -0
- package/dist/relay-external-activity-monitor.js +216 -0
- package/dist/relay-queue-service.js +66 -0
- package/dist/relay-runtime-types.js +1 -0
- package/dist/relay-runtime.js +96 -354
- package/dist/settings-service.js +2 -117
- package/dist/support-bundle.js +205 -0
- package/dist/telegram-access-commands.js +123 -0
- package/dist/telegram-access-middleware.js +129 -0
- package/dist/telegram-agent-commands.js +212 -0
- package/dist/telegram-artifact-commands.js +139 -0
- package/dist/telegram-channel-runtime.js +132 -0
- package/dist/telegram-command-menu.js +55 -0
- package/dist/telegram-command-types.js +1 -0
- package/dist/telegram-diagnostics-command.js +102 -0
- package/dist/telegram-general-commands.js +52 -0
- package/dist/telegram-operational-commands.js +153 -0
- package/dist/telegram-output.js +216 -0
- package/dist/telegram-preference-commands.js +198 -0
- package/dist/telegram-queue-commands.js +278 -0
- package/dist/telegram-support-command.js +53 -0
- package/dist/telegram-update-commands.js +93 -0
- package/dist/user-management.js +708 -0
- package/dist/web-api-contract.js +104 -0
- package/dist/web-api-types.js +1 -0
- package/dist/web-dashboard-access-routes.js +163 -0
- package/dist/web-dashboard-artifact-routes.js +65 -0
- package/dist/web-dashboard-assets.js +35 -2
- package/dist/web-dashboard-http.js +143 -0
- package/dist/web-dashboard-pages.js +257 -0
- package/dist/web-dashboard-runtime-routes.js +92 -0
- package/dist/web-dashboard-session-routes.js +209 -0
- package/dist/web-dashboard-ui.js +14 -14
- package/dist/web-dashboard.js +330 -707
- package/dist/webui-assets/dashboard.css +989 -0
- package/dist/webui-assets/dashboard.js +1750 -0
- package/dist/zip-writer.js +83 -0
- package/package.json +13 -4
- package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
- package/plugins/nordrelay/commands/remote.md +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +227 -78
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +1 -1
- package/dist/web-dashboard-client.js +0 -275
- package/dist/web-dashboard-style.js +0 -9
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { renderDashboardNav } from "./web-dashboard-ui.js";
|
|
2
|
+
export function renderLoginPage(options) {
|
|
3
|
+
return `<!doctype html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="utf-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
8
|
+
<title>NordRelay Login</title>
|
|
9
|
+
<style>
|
|
10
|
+
body{margin:0;min-height:100vh;display:grid;place-items:center;background:#f4f5f2;color:#181c19;font-family:Inter,system-ui,-apple-system,Segoe UI,sans-serif}
|
|
11
|
+
form{width:min(420px,calc(100vw - 32px));background:white;border:1px solid #dfe3dc;border-radius:8px;padding:24px;box-shadow:0 20px 60px rgba(20,30,24,.08)}
|
|
12
|
+
h1{font-size:24px;margin:0 0 8px}
|
|
13
|
+
p{color:#5d665d;margin:0 0 18px}
|
|
14
|
+
label{display:block;font-size:13px;color:#4b544d;margin:14px 0 6px}
|
|
15
|
+
input{box-sizing:border-box;width:100%;height:40px;border:1px solid #cfd6ce;border-radius:6px;padding:0 10px;font:inherit}
|
|
16
|
+
button{margin-top:18px;width:100%;height:42px;border:0;border-radius:6px;background:#205c43;color:white;font-weight:650;cursor:pointer}
|
|
17
|
+
.error{color:#9b1c1c;min-height:22px;margin-top:12px}
|
|
18
|
+
</style>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<form id="login">
|
|
22
|
+
<h1>NordRelay Dashboard</h1>
|
|
23
|
+
<p>${options.adminConfigured ? "Sign in with your NordRelay user account." : "No admin user exists. Run nordrelay user create-admin on this host first."}</p>
|
|
24
|
+
<label>Email</label><input id="email" name="email" type="email" autocomplete="username" ${options.adminConfigured ? "" : "disabled"}>
|
|
25
|
+
<label>Password</label><input id="password" name="password" type="password" autocomplete="current-password" ${options.adminConfigured ? "" : "disabled"}>
|
|
26
|
+
<button ${options.adminConfigured ? "" : "disabled"}>Sign in</button>
|
|
27
|
+
<div class="error" id="error"></div>
|
|
28
|
+
</form>
|
|
29
|
+
<script>
|
|
30
|
+
document.getElementById('login').addEventListener('submit', async (event) => {
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
const payload = {
|
|
33
|
+
email: document.getElementById('email')?.value || undefined,
|
|
34
|
+
password: document.getElementById('password')?.value || undefined,
|
|
35
|
+
};
|
|
36
|
+
const res = await fetch('/api/auth', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify(payload) });
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
document.getElementById('error').textContent = 'Invalid credentials';
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
location.href = '/';
|
|
42
|
+
});
|
|
43
|
+
</script>
|
|
44
|
+
</body>
|
|
45
|
+
</html>`;
|
|
46
|
+
}
|
|
47
|
+
export function renderDashboardApp() {
|
|
48
|
+
return `<!doctype html>
|
|
49
|
+
<html lang="en">
|
|
50
|
+
<head>
|
|
51
|
+
<meta charset="utf-8">
|
|
52
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
53
|
+
<title>NordRelay Dashboard</title>
|
|
54
|
+
<script>document.documentElement.dataset.theme = localStorage.getItem('nordrelayTheme') || 'light';</script>
|
|
55
|
+
<link rel="stylesheet" href="/assets/dashboard.css">
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
<div class="app">
|
|
59
|
+
<aside class="sidebar" id="sidebar">
|
|
60
|
+
<div class="brand"><span class="mark">NR</span><div><strong>NordRelay</strong><small>Remote control</small></div></div>
|
|
61
|
+
<nav>
|
|
62
|
+
${renderDashboardNav()}
|
|
63
|
+
</nav>
|
|
64
|
+
</aside>
|
|
65
|
+
<main>
|
|
66
|
+
<header>
|
|
67
|
+
<button class="menu" id="menuBtn">Menu</button>
|
|
68
|
+
<div>
|
|
69
|
+
<h1 id="pageTitle">Overview</h1>
|
|
70
|
+
<p id="sessionLine">Loading session...</p>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="header-actions">
|
|
73
|
+
<span id="connectionStatus" class="badge">Connecting</span>
|
|
74
|
+
<select id="agentSelect"></select>
|
|
75
|
+
<button id="themeBtn" class="secondary" title="Toggle dark theme">Dark</button>
|
|
76
|
+
<button id="refreshBtn">Refresh</button>
|
|
77
|
+
<button id="logoutBtn" class="secondary">Logout</button>
|
|
78
|
+
</div>
|
|
79
|
+
</header>
|
|
80
|
+
|
|
81
|
+
<section class="page active" id="page-overview">
|
|
82
|
+
<div class="metrics" id="metrics"></div>
|
|
83
|
+
<div class="stack">
|
|
84
|
+
<div class="panel"><h2>Current Session</h2><pre id="sessionText"></pre></div>
|
|
85
|
+
<div class="overview-adapter-grid">
|
|
86
|
+
<div class="panel"><h2>Agent Adapters</h2><div id="agentAdapters"></div></div>
|
|
87
|
+
<div class="panel"><h2>Chat Adapters</h2><div id="chatAdapters"></div></div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</section>
|
|
91
|
+
|
|
92
|
+
<section class="page" id="page-chat">
|
|
93
|
+
<div class="chat-layout">
|
|
94
|
+
<div class="panel chat-panel">
|
|
95
|
+
<div class="chat-toolbar">
|
|
96
|
+
<button id="newSessionBtn">New session</button>
|
|
97
|
+
<button id="retryBtn" class="secondary">Retry</button>
|
|
98
|
+
<button id="editLastBtn" class="secondary">Edit last</button>
|
|
99
|
+
<button id="syncBtn" class="secondary">Sync</button>
|
|
100
|
+
<button id="notifyBtn" class="secondary">Notify</button>
|
|
101
|
+
<button id="clearChatBtn" class="secondary">Clear history</button>
|
|
102
|
+
<button id="abortBtn">Abort</button>
|
|
103
|
+
<button id="handbackBtn">Handback</button>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="control-grid" id="sessionControls"></div>
|
|
106
|
+
<div id="messages" class="messages"></div>
|
|
107
|
+
<form id="promptForm" class="composer">
|
|
108
|
+
<div class="composer-fields">
|
|
109
|
+
<textarea id="promptInput" placeholder="Send a message to the active coding agent..." rows="3"></textarea>
|
|
110
|
+
<div class="attachment-row">
|
|
111
|
+
<label class="file-button" for="fileInput">Attach files</label>
|
|
112
|
+
<input id="fileInput" type="file" multiple>
|
|
113
|
+
<button type="button" id="recordBtn" class="secondary">Record voice</button>
|
|
114
|
+
<span id="fileSummary">No files selected</span>
|
|
115
|
+
<button type="button" id="clearFilesBtn" class="secondary">Clear</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<button>Send</button>
|
|
119
|
+
</form>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="panel side-panel"><h2>Tools / Plan</h2><div id="toolStream" class="tool-stream"></div></div>
|
|
122
|
+
</div>
|
|
123
|
+
</section>
|
|
124
|
+
|
|
125
|
+
<section class="page" id="page-tasks">
|
|
126
|
+
<div class="panel">
|
|
127
|
+
<div class="row"><button id="reloadTasksBtn">Reload tasks</button></div>
|
|
128
|
+
<div id="tasksList" class="list"></div>
|
|
129
|
+
</div>
|
|
130
|
+
</section>
|
|
131
|
+
|
|
132
|
+
<section class="page" id="page-sessions">
|
|
133
|
+
<div class="panel">
|
|
134
|
+
<div class="sessions-toolbar">
|
|
135
|
+
<div class="row search-row"><input id="sessionSearch" placeholder="Search sessions"><button id="sessionSearchBtn">Search</button></div>
|
|
136
|
+
<div class="row attach-row"><input id="attachInput" placeholder="Thread ID to attach/switch"><button id="attachBtn">Attach</button></div>
|
|
137
|
+
</div>
|
|
138
|
+
<div id="sessionsList" class="list"></div>
|
|
139
|
+
<div id="sessionsPager" class="pager"></div>
|
|
140
|
+
</div>
|
|
141
|
+
</section>
|
|
142
|
+
|
|
143
|
+
<section class="page" id="page-queue">
|
|
144
|
+
<div class="panel">
|
|
145
|
+
<div class="row"><button data-queue="pause">Pause</button><button data-queue="resume">Resume</button><button data-queue="clear" class="danger">Clear</button><span id="queueStatus"></span></div>
|
|
146
|
+
<div id="queueList" class="list"></div>
|
|
147
|
+
</div>
|
|
148
|
+
</section>
|
|
149
|
+
|
|
150
|
+
<section class="page" id="page-activity">
|
|
151
|
+
<div class="panel">
|
|
152
|
+
<div class="row"><select id="activitySource"><option value="all">All sources</option><option value="web">Web</option><option value="cli">CLI</option></select><select id="activityStatus"><option value="all">All statuses</option><option value="queued">Queued</option><option value="running">Running</option><option value="completed">Completed</option><option value="failed">Failed</option><option value="aborted">Aborted</option><option value="info">Info</option></select><input id="activitySince" type="datetime-local"><input id="activityLimit" type="number" value="100" min="1" max="500"><button id="loadActivityBtn">Load activity</button><button id="exportActivityBtn" class="secondary">Export</button></div>
|
|
153
|
+
<div id="activityList" class="list"></div>
|
|
154
|
+
</div>
|
|
155
|
+
</section>
|
|
156
|
+
|
|
157
|
+
<section class="page" id="page-artifacts">
|
|
158
|
+
<div class="panel">
|
|
159
|
+
<div class="row"><button id="reloadArtifactsBtn">Reload artifacts</button><input id="artifactSearch" placeholder="Search artifacts"><select id="artifactKind"><option value="all">All files</option><option value="images">Images</option><option value="docs">Docs/code</option></select><button id="zipSelectedArtifactsBtn" class="secondary">ZIP selected</button><button id="deleteSelectedArtifactsBtn" class="danger">Delete selected</button></div>
|
|
160
|
+
<div id="artifactPreview" class="preview"></div>
|
|
161
|
+
<div id="artifactList" class="list"></div>
|
|
162
|
+
</div>
|
|
163
|
+
</section>
|
|
164
|
+
|
|
165
|
+
<section class="page" id="page-adapters">
|
|
166
|
+
<div class="panel">
|
|
167
|
+
<div class="row"><button id="reloadAdaptersBtn">Reload adapters</button></div>
|
|
168
|
+
<div id="adapterHealth" class="list"></div>
|
|
169
|
+
</div>
|
|
170
|
+
</section>
|
|
171
|
+
|
|
172
|
+
<section class="page" id="page-access">
|
|
173
|
+
<div class="panel">
|
|
174
|
+
<div class="row"><button id="loadAccessBtn">Reload users</button><button id="createUserBtn">Create user</button><button id="createGroupBtn" class="secondary">Create group</button><button id="createChatBtn" class="secondary">Add Telegram chat</button><button id="lockSessionBtn" class="secondary">Lock web session</button><button id="unlockSessionBtn" class="secondary">Unlock web session</button></div>
|
|
175
|
+
<div id="accessPanel" class="settings-grid"></div>
|
|
176
|
+
<h2>Groups</h2>
|
|
177
|
+
<div id="groupsList" class="list"></div>
|
|
178
|
+
<h2>Telegram chats</h2>
|
|
179
|
+
<div id="telegramChatsList" class="list"></div>
|
|
180
|
+
<h2>Locks</h2>
|
|
181
|
+
<div id="locksList" class="list"></div>
|
|
182
|
+
<h2>Audit</h2>
|
|
183
|
+
<div class="row"><input id="auditLimit" type="number" value="50" min="1" max="200"><button id="loadAuditBtn">Load audit</button></div>
|
|
184
|
+
<div id="auditList" class="list"></div>
|
|
185
|
+
</div>
|
|
186
|
+
</section>
|
|
187
|
+
|
|
188
|
+
<section class="page" id="page-version">
|
|
189
|
+
<div class="panel">
|
|
190
|
+
<div class="row version-actions"><button id="loadVersionBtn">Check versions</button><button id="updateBtn" class="secondary">Update NordRelay</button></div>
|
|
191
|
+
<div id="versionPanel" class="list"></div>
|
|
192
|
+
<h2 class="version-update-title">Agent update jobs</h2>
|
|
193
|
+
<div id="agentUpdateJobs" class="list"></div>
|
|
194
|
+
</div>
|
|
195
|
+
</section>
|
|
196
|
+
|
|
197
|
+
<section class="page" id="page-settings">
|
|
198
|
+
<div class="panel">
|
|
199
|
+
<div class="row"><button id="saveSettingsBtn">Save settings</button><button id="restartBtn" class="secondary">Restart NordRelay</button><span id="settingsStatus"></span></div>
|
|
200
|
+
<div id="settingsTabs" class="tabs"></div>
|
|
201
|
+
<div id="settingsForm" class="settings-grid"></div>
|
|
202
|
+
</div>
|
|
203
|
+
</section>
|
|
204
|
+
|
|
205
|
+
<section class="page" id="page-logs">
|
|
206
|
+
<div class="panel">
|
|
207
|
+
<div class="row"><select id="logTarget"><option value="connector">Connector</option><option value="update">NordRelay Update</option><option value="agent-updates">Agent Updates</option></select><select id="logLevel"><option value="all">All levels</option><option value="ERROR">Error</option><option value="WARN">Warn</option><option value="INFO">Info</option></select><input id="logSearch" placeholder="Search logs"><input id="logSince" type="datetime-local" title="Show entries after this time"><input id="logLines" type="number" value="120" min="1" max="300"><label class="checkbox"><input id="logAutoRefresh" type="checkbox"> Auto</label><label class="checkbox"><input id="logFollow" type="checkbox"> Follow</label><button id="loadLogsBtn">Load logs</button><button id="downloadLogsBtn" class="secondary">Download</button><button id="clearLogsBtn" class="danger">Clear</button></div>
|
|
208
|
+
<pre id="logs" class="log-view"></pre>
|
|
209
|
+
</div>
|
|
210
|
+
</section>
|
|
211
|
+
|
|
212
|
+
<section class="page" id="page-diagnostics">
|
|
213
|
+
<div class="panel">
|
|
214
|
+
<div class="row"><button id="exportDiagnosticsBundleBtn" class="secondary">Export diagnostics bundle</button></div>
|
|
215
|
+
<div id="diagnostics" class="list"></div>
|
|
216
|
+
</div>
|
|
217
|
+
</section>
|
|
218
|
+
|
|
219
|
+
<footer>
|
|
220
|
+
<span id="footerVersion">NordRelay</span>
|
|
221
|
+
<span id="footerHealth">Health: loading</span>
|
|
222
|
+
<span id="footerUser">User: loading</span>
|
|
223
|
+
</footer>
|
|
224
|
+
</main>
|
|
225
|
+
</div>
|
|
226
|
+
<dialog id="newSessionDialog">
|
|
227
|
+
<form method="dialog" id="newSessionForm">
|
|
228
|
+
<h2>New Session</h2>
|
|
229
|
+
<div class="form-grid">
|
|
230
|
+
<label>Agent<select id="newAgent"></select></label>
|
|
231
|
+
<label>Workspace<input id="newWorkspace" list="workspaceOptions" placeholder="Current workspace"></label>
|
|
232
|
+
<label>Model<select id="newModel"></select></label>
|
|
233
|
+
<label id="newReasoningWrap">Reasoning<select id="newReasoning"></select></label>
|
|
234
|
+
<label id="newLaunchWrap">Launch profile<select id="newLaunch"></select></label>
|
|
235
|
+
<label id="newFastWrap" class="checkbox"><input id="newFast" type="checkbox"> Fast mode</label>
|
|
236
|
+
</div>
|
|
237
|
+
<datalist id="workspaceOptions"></datalist>
|
|
238
|
+
<div class="row dialog-actions"><button type="button" id="cancelSessionBtn" class="secondary">Cancel</button><button id="createSessionBtn" value="default">Create session</button></div>
|
|
239
|
+
</form>
|
|
240
|
+
</dialog>
|
|
241
|
+
<dialog id="sessionDetailDialog">
|
|
242
|
+
<div id="sessionDetail"></div>
|
|
243
|
+
<div class="row dialog-actions"><button id="closeSessionDetailBtn" class="secondary">Close</button></div>
|
|
244
|
+
</dialog>
|
|
245
|
+
<dialog id="adminDialog">
|
|
246
|
+
<form method="dialog" id="adminDialogForm">
|
|
247
|
+
<h2 id="adminDialogTitle">Edit</h2>
|
|
248
|
+
<div id="adminDialogBody" class="form-grid"></div>
|
|
249
|
+
<div class="row dialog-actions"><button type="button" id="adminDialogCancel" class="secondary">Cancel</button><button id="adminDialogSubmit" value="default">Save</button></div>
|
|
250
|
+
</form>
|
|
251
|
+
</dialog>
|
|
252
|
+
<div id="toolTooltip" class="tool-tooltip"></div>
|
|
253
|
+
<div id="toast"></div>
|
|
254
|
+
<script src="/assets/dashboard.js"></script>
|
|
255
|
+
</body>
|
|
256
|
+
</html>`;
|
|
257
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { numberParam, optionalStringField, parseAgentUpdateOperation, parseLogTarget, readJsonBody, sendFile, sendJson, stringField, } from "./web-dashboard-http.js";
|
|
2
|
+
export async function handleDashboardRuntimeRoute(req, res, url, options) {
|
|
3
|
+
const { runtime, users, authUser } = options;
|
|
4
|
+
if (req.method === "GET" && url.pathname === "/api/health") {
|
|
5
|
+
await options.assertCurrentSessionScope(authUser);
|
|
6
|
+
sendJson(res, 200, await runtime.status());
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
if (req.method === "GET" && url.pathname === "/api/version") {
|
|
10
|
+
sendJson(res, 200, await runtime.version());
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
if (req.method === "POST" && url.pathname === "/api/update") {
|
|
14
|
+
sendJson(res, 202, runtime.updateConnector());
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (req.method === "GET" && url.pathname === "/api/agent-updates") {
|
|
18
|
+
sendJson(res, 200, { jobs: runtime.agentUpdateJobs().filter((job) => users.canUseAgent(authUser, job.agentId)) });
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (req.method === "POST" && url.pathname === "/api/agent-update") {
|
|
22
|
+
const body = await readJsonBody(req);
|
|
23
|
+
const agentId = options.parseAgentIdRequired(stringField(body, "agentId"));
|
|
24
|
+
const operation = parseAgentUpdateOperation(optionalStringField(body, "operation"));
|
|
25
|
+
options.assertScopedAgent(authUser, agentId);
|
|
26
|
+
sendJson(res, 202, { job: runtime.startAgentUpdate(agentId, operation) });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
const agentUpdateLogMatch = url.pathname.match(/^\/api\/agent-update\/([^/]+)\/log$/);
|
|
30
|
+
if (req.method === "GET" && agentUpdateLogMatch?.[1]) {
|
|
31
|
+
const id = decodeURIComponent(agentUpdateLogMatch[1]);
|
|
32
|
+
options.assertAgentUpdateJobScope(authUser, id);
|
|
33
|
+
sendJson(res, 200, runtime.agentUpdateLog(id));
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (req.method === "DELETE" && agentUpdateLogMatch?.[1]) {
|
|
37
|
+
const id = decodeURIComponent(agentUpdateLogMatch[1]);
|
|
38
|
+
options.assertAgentUpdateJobScope(authUser, id);
|
|
39
|
+
sendJson(res, 200, { deletedId: id, job: runtime.deleteAgentUpdateLog(id) });
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
const agentUpdateInputMatch = url.pathname.match(/^\/api\/agent-update\/([^/]+)\/input$/);
|
|
43
|
+
if (req.method === "POST" && agentUpdateInputMatch?.[1]) {
|
|
44
|
+
const body = await readJsonBody(req);
|
|
45
|
+
const id = decodeURIComponent(agentUpdateInputMatch[1]);
|
|
46
|
+
options.assertAgentUpdateJobScope(authUser, id);
|
|
47
|
+
sendJson(res, 200, { job: runtime.sendAgentUpdateInput(id, stringField(body, "input")) });
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
const agentUpdateCancelMatch = url.pathname.match(/^\/api\/agent-update\/([^/]+)\/cancel$/);
|
|
51
|
+
if (req.method === "POST" && agentUpdateCancelMatch?.[1]) {
|
|
52
|
+
const id = decodeURIComponent(agentUpdateCancelMatch[1]);
|
|
53
|
+
options.assertAgentUpdateJobScope(authUser, id);
|
|
54
|
+
sendJson(res, 200, { job: runtime.cancelAgentUpdate(id) });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (req.method === "GET" && (url.pathname === "/api/tasks" || url.pathname === "/api/progress")) {
|
|
58
|
+
sendJson(res, 200, await options.scopedTasks(authUser, runtime.tasks()));
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (req.method === "GET" && url.pathname === "/api/adapters/health") {
|
|
62
|
+
sendJson(res, 200, { adapters: (await runtime.adapterHealth()).filter((adapter) => users.canUseAgent(authUser, adapter.id)) });
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (req.method === "GET" && url.pathname === "/api/logs") {
|
|
66
|
+
const target = parseLogTarget(url.searchParams.get("target") ?? undefined);
|
|
67
|
+
sendJson(res, 200, await runtime.logs(target, numberParam(url, "lines", 100)));
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (req.method === "POST" && url.pathname === "/api/logs/clear") {
|
|
71
|
+
const body = await readJsonBody(req);
|
|
72
|
+
const target = parseLogTarget(optionalStringField(body, "target"));
|
|
73
|
+
sendJson(res, 200, runtime.clearLogs(target));
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (req.method === "GET" && url.pathname === "/api/diagnostics") {
|
|
77
|
+
await options.assertCurrentSessionScope(authUser);
|
|
78
|
+
sendJson(res, 200, await runtime.diagnostics());
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (req.method === "GET" && url.pathname === "/api/diagnostics/bundle") {
|
|
82
|
+
await options.assertCurrentSessionScope(authUser);
|
|
83
|
+
const bundle = await runtime.supportBundle();
|
|
84
|
+
sendFile(res, bundle.path, bundle.name);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (req.method === "POST" && url.pathname === "/api/runtime/restart") {
|
|
88
|
+
sendJson(res, 202, runtime.restartConnector());
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { isAgentId } from "./agent.js";
|
|
2
|
+
import { numberParam, optionalBooleanField, optionalStringField, parseUploadFiles, readJsonBody, requiredSearch, sendJson, stringField, } from "./web-dashboard-http.js";
|
|
3
|
+
export async function handleDashboardSessionRoute(req, res, url, options) {
|
|
4
|
+
const { runtime, authUser } = options;
|
|
5
|
+
if (req.method === "GET" && url.pathname === "/api/locks") {
|
|
6
|
+
await options.assertCurrentSessionScope(authUser);
|
|
7
|
+
sendJson(res, 200, { locks: runtime.locks() });
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
if (req.method === "POST" && url.pathname === "/api/locks") {
|
|
11
|
+
const body = await readJsonBody(req);
|
|
12
|
+
await options.assertCurrentSessionScope(authUser);
|
|
13
|
+
sendJson(res, 200, { lock: runtime.lockWebSession(optionalStringField(body, "ownerName")), locks: runtime.locks() });
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (req.method === "DELETE" && url.pathname === "/api/locks") {
|
|
17
|
+
await options.assertCurrentSessionScope(authUser);
|
|
18
|
+
sendJson(res, 200, runtime.unlockWebSession());
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (req.method === "GET" && url.pathname === "/api/auth/status") {
|
|
22
|
+
const agentId = options.parseAgentId(url.searchParams.get("agent") ?? undefined);
|
|
23
|
+
options.assertScopedAgent(authUser, agentId);
|
|
24
|
+
sendJson(res, 200, await runtime.authStatus(agentId));
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (req.method === "POST" && url.pathname === "/api/auth/login") {
|
|
28
|
+
const body = await readJsonBody(req);
|
|
29
|
+
const agentId = options.parseAgentId(optionalStringField(body, "agentId"));
|
|
30
|
+
options.assertScopedAgent(authUser, agentId);
|
|
31
|
+
sendJson(res, 200, await runtime.login(agentId));
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (req.method === "POST" && url.pathname === "/api/auth/logout") {
|
|
35
|
+
const body = await readJsonBody(req);
|
|
36
|
+
const agentId = options.parseAgentId(optionalStringField(body, "agentId"));
|
|
37
|
+
options.assertScopedAgent(authUser, agentId);
|
|
38
|
+
sendJson(res, 200, await runtime.logout(agentId));
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (req.method === "GET" && url.pathname === "/api/snapshot") {
|
|
42
|
+
await options.assertCurrentSessionScope(authUser);
|
|
43
|
+
sendJson(res, 200, await runtime.snapshot());
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (req.method === "GET" && url.pathname === "/api/sessions") {
|
|
47
|
+
const agentId = options.parseAgentId(url.searchParams.get("agent") ?? undefined);
|
|
48
|
+
if (agentId) {
|
|
49
|
+
options.assertScopedAgent(authUser, agentId);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
await options.assertCurrentSessionScope(authUser);
|
|
53
|
+
}
|
|
54
|
+
const page = await runtime.listSessionsPage(numberParam(url, "page", 1), numberParam(url, "limit", 50), url.searchParams.get("query") ?? "", agentId);
|
|
55
|
+
sendJson(res, 200, options.scopedSessionPage(authUser, page));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (req.method === "POST" && url.pathname === "/api/agent") {
|
|
59
|
+
const body = await readJsonBody(req);
|
|
60
|
+
const agentId = stringField(body, "agentId");
|
|
61
|
+
if (!isAgentId(agentId)) {
|
|
62
|
+
throw new Error(`Invalid agent: ${agentId}`);
|
|
63
|
+
}
|
|
64
|
+
options.assertScopedAgent(authUser, agentId);
|
|
65
|
+
sendJson(res, 200, { session: await runtime.setAgent(agentId) });
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (req.method === "POST" && url.pathname === "/api/sessions/new") {
|
|
69
|
+
const body = await readJsonBody(req);
|
|
70
|
+
const agentId = options.parseAgentId(optionalStringField(body, "agentId"));
|
|
71
|
+
const workspace = optionalStringField(body, "workspace");
|
|
72
|
+
options.assertScopedAgent(authUser, agentId);
|
|
73
|
+
options.assertScopedWorkspace(authUser, workspace);
|
|
74
|
+
sendJson(res, 200, {
|
|
75
|
+
session: await runtime.newSession({
|
|
76
|
+
agentId,
|
|
77
|
+
workspace,
|
|
78
|
+
model: optionalStringField(body, "model"),
|
|
79
|
+
reasoningEffort: optionalStringField(body, "reasoningEffort"),
|
|
80
|
+
launchProfileId: optionalStringField(body, "launchProfileId"),
|
|
81
|
+
fastMode: optionalBooleanField(body, "fastMode"),
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (req.method === "POST" && url.pathname === "/api/sessions/switch") {
|
|
87
|
+
const body = await readJsonBody(req);
|
|
88
|
+
const threadId = stringField(body, "threadId");
|
|
89
|
+
const detail = await runtime.sessionDetail(threadId);
|
|
90
|
+
if (detail.record && typeof detail.record === "object") {
|
|
91
|
+
options.assertSessionScope(authUser, detail.record);
|
|
92
|
+
}
|
|
93
|
+
const session = await runtime.switchSession(threadId);
|
|
94
|
+
options.assertSessionScope(authUser, session);
|
|
95
|
+
sendJson(res, 200, { session });
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (req.method === "POST" && url.pathname === "/api/sessions/attach") {
|
|
99
|
+
const body = await readJsonBody(req);
|
|
100
|
+
const session = await runtime.attachSession(stringField(body, "threadId"));
|
|
101
|
+
options.assertSessionScope(authUser, session);
|
|
102
|
+
sendJson(res, 200, { session });
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if (req.method === "GET" && url.pathname === "/api/sessions/detail") {
|
|
106
|
+
const threadId = requiredSearch(url, "threadId");
|
|
107
|
+
const detail = await runtime.sessionDetail(threadId);
|
|
108
|
+
options.assertSessionDetailScope(authUser, threadId, detail);
|
|
109
|
+
sendJson(res, 200, detail);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (req.method === "GET" && url.pathname === "/api/models") {
|
|
113
|
+
await options.assertCurrentSessionScope(authUser);
|
|
114
|
+
sendJson(res, 200, { models: await runtime.listModels() });
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (req.method === "POST" && url.pathname === "/api/session/model") {
|
|
118
|
+
const body = await readJsonBody(req);
|
|
119
|
+
await options.assertCurrentSessionScope(authUser);
|
|
120
|
+
sendJson(res, 200, { session: await runtime.setModel(stringField(body, "model")) });
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (req.method === "POST" && url.pathname === "/api/session/reasoning") {
|
|
124
|
+
const body = await readJsonBody(req);
|
|
125
|
+
await options.assertCurrentSessionScope(authUser);
|
|
126
|
+
sendJson(res, 200, { session: await runtime.setReasoningEffort(stringField(body, "reasoning")) });
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (req.method === "POST" && url.pathname === "/api/session/fast") {
|
|
130
|
+
const body = await readJsonBody(req);
|
|
131
|
+
await options.assertCurrentSessionScope(authUser);
|
|
132
|
+
sendJson(res, 200, { session: await runtime.setFastMode(Boolean(body?.enabled)) });
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (req.method === "POST" && url.pathname === "/api/session/launch") {
|
|
136
|
+
const body = await readJsonBody(req);
|
|
137
|
+
await options.assertCurrentSessionScope(authUser);
|
|
138
|
+
sendJson(res, 200, { session: await runtime.setLaunchProfile(stringField(body, "profileId")) });
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
if (req.method === "POST" && url.pathname === "/api/prompt") {
|
|
142
|
+
const body = await readJsonBody(req);
|
|
143
|
+
await options.assertCurrentSessionScope(authUser);
|
|
144
|
+
sendJson(res, 202, await runtime.sendPrompt(stringField(body, "text")));
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (req.method === "POST" && url.pathname === "/api/prompt/upload") {
|
|
148
|
+
const body = await readJsonBody(req);
|
|
149
|
+
await options.assertCurrentSessionScope(authUser);
|
|
150
|
+
sendJson(res, 202, await runtime.sendUploadPrompt({
|
|
151
|
+
text: optionalStringField(body, "text"),
|
|
152
|
+
files: parseUploadFiles(body.files),
|
|
153
|
+
}));
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if (req.method === "POST" && (url.pathname === "/api/abort" || url.pathname === "/api/stop")) {
|
|
157
|
+
await options.assertCurrentSessionScope(authUser);
|
|
158
|
+
await runtime.abort();
|
|
159
|
+
sendJson(res, 200, { ok: true });
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (req.method === "POST" && url.pathname === "/api/handback") {
|
|
163
|
+
await options.assertCurrentSessionScope(authUser);
|
|
164
|
+
sendJson(res, 200, await runtime.handback());
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
if (req.method === "POST" && url.pathname === "/api/retry") {
|
|
168
|
+
await options.assertCurrentSessionScope(authUser);
|
|
169
|
+
sendJson(res, 202, await runtime.retry());
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
if (req.method === "POST" && url.pathname === "/api/sync") {
|
|
173
|
+
await options.assertCurrentSessionScope(authUser);
|
|
174
|
+
sendJson(res, 200, await runtime.sync());
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (req.method === "GET" && url.pathname === "/api/queue") {
|
|
178
|
+
await options.assertCurrentSessionScope(authUser);
|
|
179
|
+
sendJson(res, 200, { queue: runtime.queue(), paused: runtime.queuePaused() });
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
if (req.method === "POST" && url.pathname === "/api/queue") {
|
|
183
|
+
const body = await readJsonBody(req);
|
|
184
|
+
await options.assertCurrentSessionScope(authUser);
|
|
185
|
+
sendJson(res, 200, { queue: runtime.queueAction(stringField(body, "action"), optionalStringField(body, "id")), paused: runtime.queuePaused() });
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (req.method === "GET" && url.pathname === "/api/chat/history") {
|
|
189
|
+
await options.assertCurrentSessionScope(authUser);
|
|
190
|
+
sendJson(res, 200, { messages: await runtime.chatHistory(numberParam(url, "limit", 200)) });
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
if (req.method === "DELETE" && url.pathname === "/api/chat/history") {
|
|
194
|
+
await options.assertCurrentSessionScope(authUser);
|
|
195
|
+
sendJson(res, 200, await runtime.clearChatHistory());
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
if (req.method === "GET" && url.pathname === "/api/activity") {
|
|
199
|
+
sendJson(res, 200, {
|
|
200
|
+
events: options.filterActivityByScope(authUser, runtime.activity({
|
|
201
|
+
limit: numberParam(url, "limit", 100),
|
|
202
|
+
source: (url.searchParams.get("source") || "all"),
|
|
203
|
+
status: (url.searchParams.get("status") || "all"),
|
|
204
|
+
})),
|
|
205
|
+
});
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
package/dist/web-dashboard-ui.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
export const DASHBOARD_PAGES = [
|
|
2
|
-
{ id: "overview", label: "Overview" },
|
|
3
|
-
{ id: "chat", label: "Chat" },
|
|
4
|
-
{ id: "sessions", label: "Sessions" },
|
|
5
|
-
{ id: "queue", label: "Queue" },
|
|
6
|
-
{ id: "tasks", label: "Tasks" },
|
|
7
|
-
{ id: "activity", label: "Activity" },
|
|
8
|
-
{ id: "artifacts", label: "Artifacts" },
|
|
9
|
-
{ id: "adapters", label: "Adapters" },
|
|
10
|
-
{ id: "access", label: "
|
|
11
|
-
{ id: "version", label: "Version" },
|
|
12
|
-
{ id: "settings", label: "Settings" },
|
|
13
|
-
{ id: "logs", label: "Logs" },
|
|
14
|
-
{ id: "diagnostics", label: "Diagnostics" },
|
|
2
|
+
{ id: "overview", label: "Overview", permission: "inspect" },
|
|
3
|
+
{ id: "chat", label: "Chat", permission: "sessions.read" },
|
|
4
|
+
{ id: "sessions", label: "Sessions", permission: "sessions.read" },
|
|
5
|
+
{ id: "queue", label: "Queue", permission: "queue.read" },
|
|
6
|
+
{ id: "tasks", label: "Tasks", permission: "inspect" },
|
|
7
|
+
{ id: "activity", label: "Activity", permission: "sessions.read" },
|
|
8
|
+
{ id: "artifacts", label: "Artifacts", permission: "files.read" },
|
|
9
|
+
{ id: "adapters", label: "Adapters", permission: "inspect" },
|
|
10
|
+
{ id: "access", label: "Users", permission: "users.read" },
|
|
11
|
+
{ id: "version", label: "Version", permission: "inspect" },
|
|
12
|
+
{ id: "settings", label: "Settings", permission: "settings.read" },
|
|
13
|
+
{ id: "logs", label: "Logs", permission: "logs.read" },
|
|
14
|
+
{ id: "diagnostics", label: "Diagnostics", permission: "diagnostics.read" },
|
|
15
15
|
];
|
|
16
16
|
export function renderDashboardNav(activePage = "overview") {
|
|
17
|
-
return DASHBOARD_PAGES.map((page) => `<button data-page="${page.id}"${page.id === activePage ? ' class="active"' : ""}>${page.label}</button>`).join("\n ");
|
|
17
|
+
return DASHBOARD_PAGES.map((page) => `<button data-page="${page.id}" data-permission="${page.permission}"${page.id === activePage ? ' class="active"' : ""}>${page.label}</button>`).join("\n ");
|
|
18
18
|
}
|