agent-nuvira 2.7.0 → 2.7.2

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 (114) hide show
  1. package/dist/agents/orchestrator.d.ts.map +1 -1
  2. package/dist/agents/orchestrator.js +17 -3
  3. package/dist/agents/orchestrator.js.map +1 -1
  4. package/dist/app.js +26 -0
  5. package/dist/cli/chat.d.ts.map +1 -1
  6. package/dist/cli/chat.js +108 -2
  7. package/dist/cli/chat.js.map +1 -1
  8. package/dist/config/auth.js +18 -0
  9. package/dist/config/jwt.js +24 -0
  10. package/dist/config/keys.js +14 -0
  11. package/dist/gateway/chat-store.d.ts +32 -5
  12. package/dist/gateway/chat-store.d.ts.map +1 -1
  13. package/dist/gateway/chat-store.js +52 -6
  14. package/dist/gateway/chat-store.js.map +1 -1
  15. package/dist/gateway/registry.d.ts.map +1 -1
  16. package/dist/gateway/registry.js +54 -10
  17. package/dist/gateway/registry.js.map +1 -1
  18. package/dist/inference/model-validator.d.ts.map +1 -1
  19. package/dist/inference/model-validator.js +11 -1
  20. package/dist/inference/model-validator.js.map +1 -1
  21. package/dist/inference/tool-call-utils.d.ts +31 -0
  22. package/dist/inference/tool-call-utils.d.ts.map +1 -1
  23. package/dist/inference/tool-call-utils.js +41 -0
  24. package/dist/inference/tool-call-utils.js.map +1 -1
  25. package/dist/learning/reasoning-trace.d.ts +5 -3
  26. package/dist/learning/reasoning-trace.d.ts.map +1 -1
  27. package/dist/learning/reasoning-trace.js +2 -2
  28. package/dist/learning/reasoning-trace.js.map +1 -1
  29. package/dist/middleware/auth.js +30 -0
  30. package/dist/nlu/conversation-gate.js +1 -1
  31. package/dist/nlu/conversation-gate.js.map +1 -1
  32. package/dist/nlu/entities.d.ts.map +1 -1
  33. package/dist/nlu/entities.js +12 -0
  34. package/dist/nlu/entities.js.map +1 -1
  35. package/dist/nlu/intent.d.ts.map +1 -1
  36. package/dist/nlu/intent.js +83 -15
  37. package/dist/nlu/intent.js.map +1 -1
  38. package/dist/passport.js +34 -0
  39. package/dist/routes/auth.js +42 -0
  40. package/dist/routes/user.js +31 -0
  41. package/dist/server.js +17 -0
  42. package/dist/tools/pipeline-tool.d.ts.map +1 -1
  43. package/dist/tools/pipeline-tool.js +14 -0
  44. package/dist/tools/pipeline-tool.js.map +1 -1
  45. package/dist/tools/release-sync.d.ts +52 -16
  46. package/dist/tools/release-sync.d.ts.map +1 -1
  47. package/dist/tools/release-sync.js +143 -79
  48. package/dist/tools/release-sync.js.map +1 -1
  49. package/dist/web-dashboard/src/App.js +85 -0
  50. package/dist/web-dashboard/src/admin-auth.test.js +186 -0
  51. package/dist/web-dashboard/src/ansi.js +23 -0
  52. package/dist/web-dashboard/src/ansi.test.js +31 -0
  53. package/dist/web-dashboard/src/api-admin-auth.test.js +172 -0
  54. package/dist/web-dashboard/src/api-admin.test.js +65 -0
  55. package/dist/web-dashboard/src/api-hub.test.js +117 -0
  56. package/dist/web-dashboard/src/api.js +1421 -0
  57. package/dist/web-dashboard/src/api.test.js +51 -0
  58. package/dist/web-dashboard/src/artifacts.js +128 -0
  59. package/dist/web-dashboard/src/artifacts.test.js +139 -0
  60. package/dist/web-dashboard/src/components/AdminPanel.js +567 -0
  61. package/dist/web-dashboard/src/components/AdminPanel.test.js +288 -0
  62. package/dist/web-dashboard/src/components/AgentHub.js +1580 -0
  63. package/dist/web-dashboard/src/components/AgentHub.test.js +343 -0
  64. package/dist/web-dashboard/src/components/BedrockOnboarding.js +320 -0
  65. package/dist/web-dashboard/src/components/BenchmarkCharts.js +228 -0
  66. package/dist/web-dashboard/src/components/ChatPage.js +1586 -0
  67. package/dist/web-dashboard/src/components/ChatPage.test.js +899 -0
  68. package/dist/web-dashboard/src/components/ContactsPage.js +141 -0
  69. package/dist/web-dashboard/src/components/CostDashboard.js +105 -0
  70. package/dist/web-dashboard/src/components/DAGView.js +477 -0
  71. package/dist/web-dashboard/src/components/DAGView.test.js +147 -0
  72. package/dist/web-dashboard/src/components/EnvVarEditor.js +138 -0
  73. package/dist/web-dashboard/src/components/EvalsPage.js +73 -0
  74. package/dist/web-dashboard/src/components/EvalsPage.test.js +120 -0
  75. package/dist/web-dashboard/src/components/ExecutionHistory.js +201 -0
  76. package/dist/web-dashboard/src/components/GatewayPage.js +40 -0
  77. package/dist/web-dashboard/src/components/GatewayPage.test.js +74 -0
  78. package/dist/web-dashboard/src/components/HealthPanel.js +68 -0
  79. package/dist/web-dashboard/src/components/HistoryBrowser.js +34 -0
  80. package/dist/web-dashboard/src/components/Layout.js +50 -0
  81. package/dist/web-dashboard/src/components/Markdown.js +152 -0
  82. package/dist/web-dashboard/src/components/Markdown.test.js +126 -0
  83. package/dist/web-dashboard/src/components/MarkdownZeroDep.js +237 -0
  84. package/dist/web-dashboard/src/components/MemoryPanel.js +81 -0
  85. package/dist/web-dashboard/src/components/ModelTimeline.js +298 -0
  86. package/dist/web-dashboard/src/components/ModelsPanel.js +1484 -0
  87. package/dist/web-dashboard/src/components/ModelsPanel.test.js +460 -0
  88. package/dist/web-dashboard/src/components/Overview.js +123 -0
  89. package/dist/web-dashboard/src/components/PhaseTimeline.js +359 -0
  90. package/dist/web-dashboard/src/components/PhaseTimeline.test.js +234 -0
  91. package/dist/web-dashboard/src/components/PlatformConfigSection.js +384 -0
  92. package/dist/web-dashboard/src/components/PlatformConfigSection.test.js +106 -0
  93. package/dist/web-dashboard/src/components/PlatformsPage.js +250 -0
  94. package/dist/web-dashboard/src/components/QuotaPanel.js +159 -0
  95. package/dist/web-dashboard/src/components/QuotaPanel.test.js +85 -0
  96. package/dist/web-dashboard/src/components/RequestsPanel.js +229 -0
  97. package/dist/web-dashboard/src/components/RequestsPanel.test.js +103 -0
  98. package/dist/web-dashboard/src/components/RoutingInsightsPanel.js +938 -0
  99. package/dist/web-dashboard/src/components/RoutingInsightsPanel.test.js +339 -0
  100. package/dist/web-dashboard/src/components/RoutingWalkthrough.js +408 -0
  101. package/dist/web-dashboard/src/components/RoutingWalkthrough.test.js +209 -0
  102. package/dist/web-dashboard/src/components/TaskConsole.js +119 -0
  103. package/dist/web-dashboard/src/components/TaskConsole.test.js +123 -0
  104. package/dist/web-dashboard/src/components/TasksPage.js +256 -0
  105. package/dist/web-dashboard/src/components/TasksPage.test.js +103 -0
  106. package/dist/web-dashboard/src/components/TracePanel.js +306 -0
  107. package/dist/web-dashboard/src/components/WhatsAppPanel.js +195 -0
  108. package/dist/web-dashboard/src/components/WhatsAppPanel.test.js +122 -0
  109. package/dist/web-dashboard/src/jsonOrNull.js +37 -0
  110. package/dist/web-dashboard/src/main.js +15 -0
  111. package/dist/web-dashboard/src/mask.js +11 -0
  112. package/dist/web-dashboard/vite.config.js +23 -0
  113. package/dist/web-dashboard/vitest.config.js +15 -0
  114. package/package.json +5 -3
@@ -0,0 +1,1421 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dashboardAPI = exports.DashboardAPI = void 0;
4
+ exports.getAdminToken = getAdminToken;
5
+ exports.setAdminToken = setAdminToken;
6
+ exports.authHeaders = authHeaders;
7
+ const jsonOrNull_1 = require("./jsonOrNull");
8
+ // ─── Admin session token persistence (Session 18) ────────────────────────────
9
+ // The Bearer token issued at login/setup rides in localStorage so a refresh of
10
+ // the dashboard keeps the session. localStorage may be unavailable (SSR/node
11
+ // tests) — every access is guarded.
12
+ const ADMIN_TOKEN_KEY = 'buff-dashboard-admin-token';
13
+ /**
14
+ * window.localStorage — NOT the bare `localStorage` identifier: newer Node
15
+ * versions expose an experimental global localStorage that is undefined
16
+ * unless --localstorage-file is passed, and it shadows jsdom's window
17
+ * storage. window.localStorage works in the browser; some environments
18
+ * (jsdom under certain Node versions, private browsing, embedded webviews)
19
+ * expose no usable storage at all — the in-memory fallback keeps the session
20
+ * working for the page's lifetime there. Helpers never throw.
21
+ */
22
+ function getLocalStorage() {
23
+ try {
24
+ if (typeof window !== 'undefined' && window.localStorage)
25
+ return window.localStorage;
26
+ }
27
+ catch {
28
+ /* not a browser-like environment */
29
+ }
30
+ return null;
31
+ }
32
+ /** In-memory fallback when window.localStorage is unavailable. */
33
+ const adminTokenMemory = new Map();
34
+ function readAdminToken() {
35
+ const ls = getLocalStorage();
36
+ if (ls) {
37
+ try {
38
+ const v = ls.getItem(ADMIN_TOKEN_KEY);
39
+ if (v)
40
+ return v;
41
+ }
42
+ catch {
43
+ /* fall through to memory */
44
+ }
45
+ }
46
+ return adminTokenMemory.get(ADMIN_TOKEN_KEY) ?? null;
47
+ }
48
+ function writeAdminToken(token) {
49
+ const ls = getLocalStorage();
50
+ if (ls) {
51
+ try {
52
+ if (token)
53
+ ls.setItem(ADMIN_TOKEN_KEY, token);
54
+ else
55
+ ls.removeItem(ADMIN_TOKEN_KEY);
56
+ return;
57
+ }
58
+ catch {
59
+ /* fall through to memory */
60
+ }
61
+ }
62
+ if (token)
63
+ adminTokenMemory.set(ADMIN_TOKEN_KEY, token);
64
+ else
65
+ adminTokenMemory.delete(ADMIN_TOKEN_KEY);
66
+ }
67
+ function getAdminToken() {
68
+ return readAdminToken();
69
+ }
70
+ function setAdminToken(token) {
71
+ writeAdminToken(token);
72
+ }
73
+ /** Authorization header for admin-gated endpoints (empty when unauthenticated). */
74
+ function authHeaders() {
75
+ const token = getAdminToken();
76
+ return token ? { Authorization: `Bearer ${token}` } : {};
77
+ }
78
+ class DashboardAPI {
79
+ sse = null;
80
+ listeners = new Set();
81
+ connectionListeners = new Set();
82
+ dagListeners = new Set();
83
+ typingListeners = new Set();
84
+ reconnectTimer = null;
85
+ baseUrl;
86
+ lastData = null;
87
+ constructor(baseUrl = '') {
88
+ this.baseUrl = baseUrl;
89
+ }
90
+ subscribe(listener) {
91
+ this.listeners.add(listener);
92
+ return () => this.listeners.delete(listener);
93
+ }
94
+ onConnectionChange(listener) {
95
+ this.connectionListeners.add(listener);
96
+ return () => this.connectionListeners.delete(listener);
97
+ }
98
+ onDAGEvent(listener) {
99
+ this.dagListeners.add(listener);
100
+ return () => this.dagListeners.delete(listener);
101
+ }
102
+ onTypingEvent(listener) {
103
+ this.typingListeners.add(listener);
104
+ return () => this.typingListeners.delete(listener);
105
+ }
106
+ connect() {
107
+ if (this.sse)
108
+ return;
109
+ this.sse = new EventSource(`${this.baseUrl}/api/sse`);
110
+ this.sse.addEventListener('init', (event) => {
111
+ try {
112
+ const data = JSON.parse(event.data);
113
+ this.lastData = data;
114
+ this.notify(data);
115
+ this.notifyConnection(true);
116
+ }
117
+ catch (e) {
118
+ console.error('Failed to parse SSE init data:', e);
119
+ }
120
+ });
121
+ this.sse.addEventListener('refresh', (event) => {
122
+ try {
123
+ const data = JSON.parse(event.data);
124
+ this.lastData = data;
125
+ this.notify(data);
126
+ this.notifyConnection(true);
127
+ }
128
+ catch (e) {
129
+ console.error('Failed to parse SSE refresh data:', e);
130
+ }
131
+ });
132
+ this.sse.addEventListener('dag', (event) => {
133
+ try {
134
+ const dag = JSON.parse(event.data);
135
+ this.notifyDAG(dag);
136
+ // Also merge DAG into lastData and notify dashboard listeners
137
+ if (this.lastData) {
138
+ const updated = { ...this.lastData, dag };
139
+ this.lastData = updated;
140
+ this.notify(updated);
141
+ }
142
+ }
143
+ catch (e) {
144
+ console.error('Failed to parse SSE dag event:', e);
145
+ }
146
+ });
147
+ // Real-time quota pushes: the server watches quota-events.jsonl /
148
+ // quota-ledger.json and emits a `quota` event the moment a failover,
149
+ // park, or window reset lands — so the Failover Timeline updates without
150
+ // waiting for the next 10s refresh tick. Merge into routing.quota.
151
+ this.sse.addEventListener('quota', (event) => {
152
+ try {
153
+ const payload = JSON.parse(event.data);
154
+ if (this.lastData && payload.quota) {
155
+ const updated = {
156
+ ...this.lastData,
157
+ routing: {
158
+ ...(this.lastData.routing || {}),
159
+ quota: payload.quota,
160
+ },
161
+ serverTime: payload.serverTime || this.lastData.serverTime,
162
+ };
163
+ this.lastData = updated;
164
+ this.notify(updated);
165
+ }
166
+ }
167
+ catch (e) {
168
+ console.error('Failed to parse SSE quota event:', e);
169
+ }
170
+ });
171
+ // Real-time conversation updates: the server watches chat-history.json
172
+ // and emits a `conversation` event when a new message arrives.
173
+ this.sse.addEventListener('conversation', (event) => {
174
+ try {
175
+ const payload = JSON.parse(event.data);
176
+ if (this.lastData && payload.conversations) {
177
+ const updated = {
178
+ ...this.lastData,
179
+ conversations: {
180
+ total: payload.total ?? payload.conversations.length,
181
+ recent: payload.conversations,
182
+ },
183
+ serverTime: payload.serverTime || this.lastData.serverTime,
184
+ };
185
+ this.lastData = updated;
186
+ this.notify(updated);
187
+ }
188
+ }
189
+ catch (e) {
190
+ console.error('Failed to parse SSE conversation event:', e);
191
+ }
192
+ });
193
+ // Real-time typing indicator: gateway writes typing.json when processing,
194
+ // dashboard broadcasts it via SSE so the UI can show a live typing bubble.
195
+ this.sse.addEventListener('typing', (event) => {
196
+ try {
197
+ const payload = JSON.parse(event.data);
198
+ for (const cb of this.typingListeners) {
199
+ try {
200
+ cb(payload);
201
+ }
202
+ catch { /* listener error */ }
203
+ }
204
+ }
205
+ catch (e) {
206
+ console.error('Failed to parse SSE typing event:', e);
207
+ }
208
+ });
209
+ this.sse.onerror = () => {
210
+ this.notifyConnection(false);
211
+ this.disconnect();
212
+ this.reconnect();
213
+ };
214
+ this.sse.onopen = () => {
215
+ this.notifyConnection(true);
216
+ };
217
+ }
218
+ disconnect() {
219
+ if (this.sse) {
220
+ this.sse.close();
221
+ this.sse = null;
222
+ }
223
+ if (this.reconnectTimer) {
224
+ clearTimeout(this.reconnectTimer);
225
+ this.reconnectTimer = null;
226
+ }
227
+ }
228
+ reconnect() {
229
+ this.reconnectTimer = setTimeout(() => {
230
+ this.connect();
231
+ }, 3000);
232
+ }
233
+ notify(data) {
234
+ this.listeners.forEach((fn) => fn(data));
235
+ }
236
+ notifyDAG(dag) {
237
+ this.dagListeners.forEach((fn) => fn(dag));
238
+ }
239
+ notifyConnection(connected) {
240
+ this.connectionListeners.forEach((fn) => fn(connected));
241
+ }
242
+ async fetchAll() {
243
+ try {
244
+ const res = await fetch(`${this.baseUrl}/api/all`);
245
+ // parseJsonOrNull: an HTML-200 from a stale server degrades to null (with
246
+ // a console.warn hint) so App waits for the next SSE snapshot instead of
247
+ // crashing on "Unexpected token '<'".
248
+ const data = (await (0, jsonOrNull_1.parseJsonOrNull)(res));
249
+ if (!data)
250
+ return null;
251
+ this.lastData = data;
252
+ return data;
253
+ }
254
+ catch {
255
+ return null;
256
+ }
257
+ }
258
+ /** P0: fetch the reasoning-trace index (list view, no step previews). */
259
+ async fetchTraces() {
260
+ try {
261
+ const res = await fetch(`${this.baseUrl}/api/traces`, { signal: AbortSignal.timeout(8000) });
262
+ const data = (await (0, jsonOrNull_1.parseJsonOrNull)(res));
263
+ if (!data?.traces)
264
+ return null;
265
+ return data.traces;
266
+ }
267
+ catch {
268
+ return null;
269
+ }
270
+ }
271
+ /**
272
+ * E3c follow-up: run ALL state commands (doctor/system/enterprise checks)
273
+ * on demand — the dashboard command-runner. The server executes the checks
274
+ * (one source with `nuvira doctor`) and returns masked provider status.
275
+ */
276
+ async fetchAdminChecks() {
277
+ try {
278
+ const res = await fetch(`${this.baseUrl}/api/admin/checks`, { signal: AbortSignal.timeout(15000) });
279
+ const data = (await (0, jsonOrNull_1.parseJsonOrNull)(res));
280
+ if (!data || !Array.isArray(data.system) || !Array.isArray(data.enterprise) || !Array.isArray(data.providers))
281
+ return null;
282
+ return data;
283
+ }
284
+ catch {
285
+ return null;
286
+ }
287
+ }
288
+ /** I4: fetch the Agent Hub aggregate (toolsets, channels, artifacts, skills). */
289
+ async fetchHub() {
290
+ try {
291
+ const res = await fetch(`${this.baseUrl}/api/hub`, { signal: AbortSignal.timeout(8000) });
292
+ const data = (await (0, jsonOrNull_1.parseJsonOrNull)(res));
293
+ if (!data || !Array.isArray(data.toolsets?.toolsets))
294
+ return null;
295
+ return data;
296
+ }
297
+ catch {
298
+ return null;
299
+ }
300
+ }
301
+ /** P0: fetch a single trace's full detail (steps + previews). */
302
+ async fetchTraceDetail(id) {
303
+ try {
304
+ const res = await fetch(`${this.baseUrl}/api/traces/${encodeURIComponent(id)}`, { signal: AbortSignal.timeout(8000) });
305
+ const data = (await (0, jsonOrNull_1.parseJsonOrNull)(res));
306
+ return data && Array.isArray(data.steps) ? data : null;
307
+ }
308
+ catch {
309
+ return null;
310
+ }
311
+ }
312
+ // ─── Admin write surface (Session 18 — user-id + password control layer) ─
313
+ /**
314
+ * Send an admin request with the persisted Bearer token. Unlike
315
+ * parseJsonOrNull, this keeps non-2xx JSON bodies (401/400 carry the server's
316
+ * error message) while still guarding the content-type (an HTML-200 from a
317
+ * stale server must never crash on res.json()). Returns null on network
318
+ * failure.
319
+ */
320
+ async sendAdminRequest(path, method, body) {
321
+ try {
322
+ const token = getAdminToken();
323
+ const res = await fetch(`${this.baseUrl}${path}`, {
324
+ method,
325
+ headers: {
326
+ 'Content-Type': 'application/json',
327
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
328
+ },
329
+ body: body !== undefined ? JSON.stringify(body) : undefined,
330
+ signal: AbortSignal.timeout(15000),
331
+ });
332
+ const type = res.headers.get('content-type') || '';
333
+ let data = null;
334
+ if (type.includes('application/json') || type.includes('text/json')) {
335
+ try {
336
+ data = await res.json();
337
+ }
338
+ catch {
339
+ data = null;
340
+ }
341
+ }
342
+ return { status: res.status, data };
343
+ }
344
+ catch {
345
+ return null;
346
+ }
347
+ }
348
+ /** Is the admin surface configured + is the stored token still valid? */
349
+ async fetchAdminAuthStatus() {
350
+ const r = await this.sendAdminRequest('/api/admin/auth-status', 'GET');
351
+ if (!r || typeof r.data !== 'object' || r.data === null)
352
+ return null;
353
+ const d = r.data;
354
+ if (typeof d.configured !== 'boolean' || typeof d.authenticated !== 'boolean')
355
+ return null;
356
+ return {
357
+ configured: d.configured,
358
+ authenticated: d.authenticated,
359
+ user: typeof d.user === 'string' ? d.user : null,
360
+ role: typeof d.role === 'string' ? d.role : null,
361
+ };
362
+ }
363
+ /** The dashboard admin users (role.manage = admin only). */
364
+ async fetchAdminUsers() {
365
+ const r = await this.sendAdminRequest('/api/admin/users', 'GET');
366
+ if (!r)
367
+ return { ok: false, error: 'Could not reach the dashboard server.' };
368
+ const d = (r.data ?? {});
369
+ if (r.status === 200 && d.ok && Array.isArray(d.users))
370
+ return d;
371
+ return { ok: false, error: d.error || 'Failed to load users.', forbidden: r.status === 403, unauthorized: r.status === 401 };
372
+ }
373
+ /** Add a dashboard admin user with a role (admin only). */
374
+ async addAdminUser(user, password, role) {
375
+ const r = await this.sendAdminRequest('/api/admin/users', 'POST', { user, password, role });
376
+ if (!r)
377
+ return { ok: false, error: 'Could not reach the dashboard server.' };
378
+ const d = (r.data ?? {});
379
+ if (r.status === 200 && d.ok)
380
+ return d;
381
+ return { ok: false, error: d.error || 'Failed to add user.', forbidden: r.status === 403, unauthorized: r.status === 401 };
382
+ }
383
+ /** Remove a dashboard admin user (admin only). */
384
+ async removeAdminUser(user) {
385
+ const r = await this.sendAdminRequest(`/api/admin/users/${encodeURIComponent(user)}`, 'DELETE');
386
+ if (!r)
387
+ return { ok: false, error: 'Could not reach the dashboard server.' };
388
+ const d = (r.data ?? {});
389
+ if (r.status === 200 && d.ok)
390
+ return d;
391
+ return { ok: false, error: d.error || 'Failed to remove user.', forbidden: r.status === 403, unauthorized: r.status === 401 };
392
+ }
393
+ /** The provider catalog (Add-provider selector source). */
394
+ async fetchAdminCatalog() {
395
+ const r = await this.sendAdminRequest('/api/admin/catalog', 'GET');
396
+ const d = r?.data;
397
+ if (!d || !Array.isArray(d.providers))
398
+ return null;
399
+ return d.providers;
400
+ }
401
+ /** Bootstrap the admin credential (only valid while unconfigured). */
402
+ async adminSetup(user, password) {
403
+ const r = await this.sendAdminRequest('/api/admin/setup', 'POST', { user, password });
404
+ if (!r)
405
+ return { ok: false, error: 'Could not reach the dashboard server.' };
406
+ const d = (r.data ?? {});
407
+ if (r.status === 200 && typeof d.token === 'string') {
408
+ setAdminToken(d.token);
409
+ return { ok: true, user: d.user, token: d.token };
410
+ }
411
+ return { ok: false, error: d.error || 'Setup failed.', unauthorized: r.status === 401 };
412
+ }
413
+ /** Login with the admin user-id + password. Persists the returned token. */
414
+ async adminLogin(user, password) {
415
+ const r = await this.sendAdminRequest('/api/admin/login', 'POST', { user, password });
416
+ if (!r)
417
+ return { ok: false, error: 'Could not reach the dashboard server.' };
418
+ const d = (r.data ?? {});
419
+ if (r.status === 200 && typeof d.token === 'string') {
420
+ setAdminToken(d.token);
421
+ return { ok: true, user: d.user, token: d.token };
422
+ }
423
+ return { ok: false, error: d.error || 'Login failed.', unauthorized: r.status === 401 };
424
+ }
425
+ /** Log out — revoke the stored token server-side and locally. */
426
+ async adminLogout() {
427
+ await this.sendAdminRequest('/api/admin/logout', 'POST');
428
+ setAdminToken(null);
429
+ }
430
+ /**
431
+ * Shut down the gateway or the dashboard server itself (the GUI twin of
432
+ * `nuvira gateway stop` / `nuvira dashboard stop`). Admin-gated: dashboard
433
+ * requires system.manage (admin), gateway requires gateway.manage
434
+ * (admin + operator). Stopping the dashboard kills THIS page's server.
435
+ */
436
+ async shutdown(target) {
437
+ const r = await this.sendAdminRequest('/api/admin/shutdown', 'POST', { target });
438
+ if (!r)
439
+ return { ok: false, error: 'Could not reach the dashboard server.' };
440
+ const d = (r.data ?? {});
441
+ if (r.status === 200 && d.ok)
442
+ return d;
443
+ return { ok: false, error: d.error || 'Shutdown failed.', unauthorized: r.status === 401, forbidden: r.status === 403 };
444
+ }
445
+ /** Save/update a provider's key + config (authed). Mirrors `nuvira config set providers.*`. */
446
+ async saveProvider(type, fields) {
447
+ const r = await this.sendAdminRequest(`/api/admin/providers/${encodeURIComponent(type)}`, 'PUT', fields);
448
+ if (!r)
449
+ return { ok: false, error: 'Could not reach the dashboard server.' };
450
+ const d = (r.data ?? {});
451
+ if (r.status === 200 && d.ok)
452
+ return d;
453
+ return { ok: false, error: d.error || 'Save failed.', unauthorized: r.status === 401 };
454
+ }
455
+ /** Remove a provider's key + credential fields (authed). */
456
+ async deleteProvider(type) {
457
+ const r = await this.sendAdminRequest(`/api/admin/providers/${encodeURIComponent(type)}`, 'DELETE');
458
+ if (!r)
459
+ return { ok: false, error: 'Could not reach the dashboard server.' };
460
+ const d = (r.data ?? {});
461
+ if (r.status === 200 && d.ok)
462
+ return d;
463
+ return { ok: false, error: d.error || 'Remove failed.', unauthorized: r.status === 401 };
464
+ }
465
+ /** Test a provider's configured credentials (authed — lists its models). */
466
+ async testProvider(type) {
467
+ const r = await this.sendAdminRequest(`/api/admin/providers/${encodeURIComponent(type)}/test`, 'POST');
468
+ if (!r)
469
+ return { ok: false, error: 'Could not reach the dashboard server.' };
470
+ const d = (r.data ?? {});
471
+ if (r.status === 200)
472
+ return d;
473
+ return { ok: false, error: d.error || 'Test failed.', unauthorized: r.status === 401 };
474
+ }
475
+ /** The user-declared budget (routing.quota + cost cap). Read is open. */
476
+ async fetchAdminQuota() {
477
+ const r = await this.sendAdminRequest('/api/admin/quota', 'GET');
478
+ if (!r)
479
+ return null;
480
+ const d = r.data;
481
+ if (!d || !d.ok || typeof d.quota !== 'object')
482
+ return null;
483
+ return d;
484
+ }
485
+ /**
486
+ * I5: toggle a toolset's enabled state (authed — routing.operate). The
487
+ * server persists it to buffconfig; the I1 runtime gate (schema + execution)
488
+ * honors it immediately, so the dashboard toggle is never cosmetic.
489
+ */
490
+ async setToolsetEnabled(name, enabled) {
491
+ const r = await this.sendAdminRequest(`/api/admin/hub/toolsets/${encodeURIComponent(name)}`, 'PUT', { enabled });
492
+ if (!r)
493
+ return { ok: false, error: 'Could not reach the dashboard server.' };
494
+ const d = (r.data ?? {});
495
+ if (r.status === 200 && d.ok)
496
+ return d;
497
+ return {
498
+ ok: false,
499
+ error: d.error || 'Toggle failed.',
500
+ unauthorized: r.status === 401,
501
+ forbidden: r.status === 403,
502
+ };
503
+ }
504
+ /**
505
+ * P3: toggle a skill's enabled state (compiled OR hub). Admin-gated PUT
506
+ * that writes the SAME `skills.disabled[]` config the runtime match gate
507
+ * reads, so the toggle is never cosmetic.
508
+ */
509
+ async setSkillEnabled(name, enabled) {
510
+ const r = await this.sendAdminRequest(`/api/admin/hub/skills/${encodeURIComponent(name)}`, 'PUT', { enabled });
511
+ if (!r)
512
+ return { ok: false, error: 'Could not reach the dashboard server.' };
513
+ const d = (r.data ?? {});
514
+ if (r.status === 200 && d.ok)
515
+ return d;
516
+ return {
517
+ ok: false,
518
+ error: d.error || 'Toggle failed.',
519
+ unauthorized: r.status === 401,
520
+ forbidden: r.status === 403,
521
+ };
522
+ }
523
+ /**
524
+ * P6d — search the marketplace (every configured skill registry). The
525
+ * repo stays private — this READS other people's registries.
526
+ */
527
+ async marketplaceSearch(query) {
528
+ try {
529
+ const res = await fetch(`${this.baseUrl}/api/skills/marketplace?q=${encodeURIComponent(query)}`, {
530
+ headers: authHeaders(),
531
+ });
532
+ const d = (await res.json());
533
+ return Array.isArray(d.results) ? d.results : [];
534
+ }
535
+ catch {
536
+ return [];
537
+ }
538
+ }
539
+ /** P6d — install a marketplace skill (sandboxed + checksum-verified). */
540
+ async marketplaceInstall(name) {
541
+ try {
542
+ const res = await fetch(`${this.baseUrl}/api/skills/marketplace/install`, {
543
+ method: 'POST',
544
+ headers: { 'Content-Type': 'application/json', ...authHeaders() },
545
+ body: JSON.stringify({ name }),
546
+ });
547
+ const d = (await res.json());
548
+ if (res.status === 200 && d.ok)
549
+ return { ok: true };
550
+ return { ok: false, error: d.error || 'Install failed.', quarantined: d.quarantined === true };
551
+ }
552
+ catch (err) {
553
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
554
+ }
555
+ }
556
+ /** P6d — uninstall a marketplace skill (removes dir + provenance). */
557
+ async marketplaceUninstall(name) {
558
+ try {
559
+ const res = await fetch(`${this.baseUrl}/api/skills/marketplace/uninstall`, {
560
+ method: 'POST',
561
+ headers: { 'Content-Type': 'application/json', ...authHeaders() },
562
+ body: JSON.stringify({ name }),
563
+ });
564
+ const d = (await res.json());
565
+ if (res.status === 200 && d.ok)
566
+ return { ok: true };
567
+ return { ok: false, error: d.error || 'Uninstall failed.' };
568
+ }
569
+ catch (err) {
570
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
571
+ }
572
+ }
573
+ /**
574
+ * I11: send a test message through the gateway (authed — routing.operate).
575
+ * Mirrors `nuvira gateway send <target> <text>`; the server resolves the
576
+ * target (alias or platform:channelId) and sends through the SAME
577
+ * GatewayRegistry the CLI uses, so a Channels-tab test is identical to a
578
+ * CLI send.
579
+ */
580
+ async sendChannelMessage(target, text) {
581
+ const r = await this.sendAdminRequest('/api/admin/hub/channels/send', 'POST', { target, text });
582
+ if (!r)
583
+ return { ok: false, error: 'Could not reach the dashboard server.' };
584
+ const d = (r.data ?? {});
585
+ if (r.status === 200 && d.ok)
586
+ return d;
587
+ return {
588
+ ok: false,
589
+ error: d.error || 'Send failed.',
590
+ unauthorized: r.status === 401,
591
+ forbidden: r.status === 403,
592
+ };
593
+ }
594
+ /**
595
+ * Gateway Permissions (validated senders): read the effective per-platform
596
+ * policies plus the saved verified contacts, or replace per-platform
597
+ * policies (the running gateway re-reads config per inbound, so changes
598
+ * apply without a restart).
599
+ */
600
+ async gatewayPolicies() {
601
+ const r = await this.sendAdminRequest('/api/admin/gateway/policies', 'GET');
602
+ if (!r)
603
+ return { ok: false, error: 'Could not reach the dashboard server.' };
604
+ const d = (r.data ?? {});
605
+ if (r.status === 200 && d.ok)
606
+ return { ok: true, policies: d.policies, statusRecipients: d.statusRecipients, contacts: d.contacts };
607
+ return { ok: false, error: d.error || 'Failed to read policies.', unauthorized: r.status === 401, forbidden: r.status === 403 };
608
+ }
609
+ async saveGatewayPolicies(policies, statusRecipients, contacts) {
610
+ const body = { policies };
611
+ if (Array.isArray(statusRecipients))
612
+ body.statusRecipients = statusRecipients;
613
+ if (Array.isArray(contacts))
614
+ body.contacts = contacts;
615
+ const r = await this.sendAdminRequest('/api/admin/gateway/policies', 'PUT', body);
616
+ if (!r)
617
+ return { ok: false, error: 'Could not reach the dashboard server.' };
618
+ const d = (r.data ?? {});
619
+ if (r.status === 200 && d.ok)
620
+ return d;
621
+ return { ok: false, error: d.error || 'Failed to save policies.', unauthorized: r.status === 401, forbidden: r.status === 403 };
622
+ }
623
+ /** Fetch paginated gateway conversations with optional search. */
624
+ async fetchGatewayConversations(opts = {}) {
625
+ const params = new URLSearchParams();
626
+ if (opts.offset)
627
+ params.set('offset', String(opts.offset));
628
+ if (opts.limit)
629
+ params.set('limit', String(opts.limit));
630
+ if (opts.q)
631
+ params.set('q', opts.q);
632
+ const qs = params.toString();
633
+ const r = await this.sendAdminRequest(`/api/admin/gateway/conversations${qs ? `?${qs}` : ''}`, 'GET');
634
+ if (!r)
635
+ return { ok: false, error: 'Could not reach the dashboard server.' };
636
+ const d = (r.data ?? {});
637
+ if (r.status === 200 && d.ok)
638
+ return { ok: true, conversations: d.conversations, total: d.total, hasMore: d.hasMore };
639
+ return { ok: false, error: d.error || 'Failed to load conversations.', unauthorized: r.status === 401, forbidden: r.status === 403 };
640
+ }
641
+ /** Clear a single gateway conversation by its key (e.g. "whatsapp:918800663237"). */
642
+ async clearGatewayConversation(key) {
643
+ const r = await this.sendAdminRequest('/api/admin/gateway/conversations', 'DELETE', { key });
644
+ if (!r)
645
+ return { ok: false, error: 'Could not reach the dashboard server.' };
646
+ const d = (r.data ?? {});
647
+ if (r.status === 200 && d.ok)
648
+ return { ok: true };
649
+ return { ok: false, error: d.error || 'Failed to clear conversation.', unauthorized: r.status === 401, forbidden: r.status === 403 };
650
+ }
651
+ /** Add a tag to a conversation. */
652
+ async addConversationTag(key, tag) {
653
+ const r = await this.sendAdminRequest('/api/admin/gateway/conversations/tags', 'PUT', { action: 'add', key, tag });
654
+ if (!r)
655
+ return { ok: false, error: 'Could not reach the dashboard server.' };
656
+ const d = (r.data ?? {});
657
+ if (r.status === 200 && d.ok)
658
+ return { ok: true };
659
+ return { ok: false, error: d.error || 'Failed to add tag.', unauthorized: r.status === 401, forbidden: r.status === 403 };
660
+ }
661
+ /** Remove a tag from a conversation. */
662
+ async removeConversationTag(key, tag) {
663
+ const r = await this.sendAdminRequest('/api/admin/gateway/conversations/tags', 'PUT', { action: 'remove', key, tag });
664
+ if (!r)
665
+ return { ok: false, error: 'Could not reach the dashboard server.' };
666
+ const d = (r.data ?? {});
667
+ if (r.status === 200 && d.ok)
668
+ return { ok: true };
669
+ return { ok: false, error: d.error || 'Failed to remove tag.', unauthorized: r.status === 401, forbidden: r.status === 403 };
670
+ }
671
+ /** Get all unique tags across all conversations. */
672
+ async getAllConversationTags() {
673
+ const r = await this.sendAdminRequest('/api/admin/gateway/conversations/tags', 'PUT', { action: 'getAllTags' });
674
+ if (!r)
675
+ return { ok: false, error: 'Could not reach the dashboard server.' };
676
+ const d = (r.data ?? {});
677
+ if (r.status === 200 && d.ok)
678
+ return { ok: true, tags: d.tags };
679
+ return { ok: false, error: d.error || 'Failed to load tags.', unauthorized: r.status === 401, forbidden: r.status === 403 };
680
+ }
681
+ /** Bulk export conversations as a ZIP file. Triggers a browser download. */
682
+ async exportGatewayConversations(keys) {
683
+ try {
684
+ const token = getAdminToken();
685
+ const res = await fetch(`${this.baseUrl}/api/admin/gateway/conversations/export`, {
686
+ method: 'POST',
687
+ headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
688
+ body: JSON.stringify({ keys }),
689
+ signal: AbortSignal.timeout(30_000),
690
+ });
691
+ if (res.status === 200 && res.headers.get('content-type')?.includes('application/zip')) {
692
+ const blob = await res.blob();
693
+ const url = URL.createObjectURL(blob);
694
+ const a = document.createElement('a');
695
+ a.href = url;
696
+ a.download = res.headers.get('content-disposition')?.match(/filename="(.+)"/)?.[1] || 'conversations.zip';
697
+ document.body.appendChild(a);
698
+ a.click();
699
+ document.body.removeChild(a);
700
+ URL.revokeObjectURL(url);
701
+ return { ok: true };
702
+ }
703
+ const d = await res.json().catch(() => ({}));
704
+ return { ok: false, error: d.error || 'Export failed.' };
705
+ }
706
+ catch (err) {
707
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
708
+ }
709
+ }
710
+ // ─── Contacts management (name-centric outbound contacts) ────────────────
711
+ /** List all contacts (name, platform, id, phone, status). */
712
+ async getContacts() {
713
+ const r = await this.sendAdminRequest('/api/admin/contacts', 'GET');
714
+ if (!r)
715
+ return { ok: false, error: 'Could not reach the dashboard server.' };
716
+ const d = (r.data ?? {});
717
+ if (r.status === 200 && d.ok)
718
+ return { ok: true, contacts: d.contacts };
719
+ return { ok: false, error: d.error || 'Failed to read contacts.' };
720
+ }
721
+ /** Approve a contact by platform + name or ID. */
722
+ async approveContact(platform, nameOrId) {
723
+ return this.contactAction('approve', platform, nameOrId);
724
+ }
725
+ /** Reject a contact by platform + name or ID. */
726
+ async rejectContact(platform, nameOrId) {
727
+ return this.contactAction('reject', platform, nameOrId);
728
+ }
729
+ /** Delete a contact by platform + name or ID. */
730
+ async deleteContact(platform, nameOrId) {
731
+ return this.contactAction('delete', platform, nameOrId);
732
+ }
733
+ /** Update a contact's name, phone, or status. */
734
+ async updateContact(platform, id, fields) {
735
+ const r = await this.sendAdminRequest('/api/admin/contacts', 'PUT', { action: 'update', platform, id, ...fields });
736
+ if (!r)
737
+ return { ok: false, error: 'Could not reach the dashboard server.' };
738
+ const d = (r.data ?? {});
739
+ if (r.status === 200 && d.ok)
740
+ return d;
741
+ return { ok: false, error: d.error || 'Update failed.' };
742
+ }
743
+ async contactAction(action, platform, nameOrId) {
744
+ const r = await this.sendAdminRequest('/api/admin/contacts', 'PUT', { action, platform, nameOrId });
745
+ if (!r)
746
+ return { ok: false, error: 'Could not reach the dashboard server.' };
747
+ const d = (r.data ?? {});
748
+ if (r.status === 200 && d.ok)
749
+ return d;
750
+ return { ok: false, error: d.error || `${action} failed.` };
751
+ }
752
+ /**
753
+ * Platform transport config (GUI parity with `nuvira config gateway`): list
754
+ * every env-configurable platform with current per-var values (full values
755
+ * only for admin/operator). Authed.
756
+ */
757
+ async getPlatformConfigs() {
758
+ const r = await this.sendAdminRequest('/api/config/platforms', 'GET');
759
+ if (!r)
760
+ return [];
761
+ const d = (r.data ?? {});
762
+ return Array.isArray(d.platforms) ? d.platforms : [];
763
+ }
764
+ /** Write a platform's env values to ~/.nuvira/.env (authed — routing.operate). */
765
+ async setPlatformConfig(platform, values) {
766
+ const r = await this.sendAdminRequest(`/api/config/platforms/${encodeURIComponent(platform)}`, 'POST', { values });
767
+ if (!r)
768
+ return { ok: false, error: 'Could not reach the dashboard server.' };
769
+ const d = (r.data ?? {});
770
+ if (r.status === 200 && d.ok)
771
+ return d;
772
+ return {
773
+ ok: false,
774
+ error: d.error || 'Save failed.',
775
+ unauthorized: r.status === 401,
776
+ forbidden: r.status === 403,
777
+ };
778
+ }
779
+ /** Remove a platform's env values from ~/.nuvira/.env (authed — routing.operate). */
780
+ async removePlatformConfig(platform) {
781
+ const r = await this.sendAdminRequest(`/api/config/platforms/${encodeURIComponent(platform)}`, 'DELETE');
782
+ if (!r)
783
+ return { ok: false, error: 'Could not reach the dashboard server.' };
784
+ const d = (r.data ?? {});
785
+ if (r.status === 200 && d.ok)
786
+ return d;
787
+ return {
788
+ ok: false,
789
+ error: d.error || 'Remove failed.',
790
+ unauthorized: r.status === 401,
791
+ forbidden: r.status === 403,
792
+ };
793
+ }
794
+ /** Verify a platform's token by calling the platform's API (e.g. Telegram getMe). */
795
+ async verifyPlatformConfig(platform, values) {
796
+ const r = await this.sendAdminRequest(`/api/config/platforms/${encodeURIComponent(platform)}/verify`, 'POST', { values });
797
+ if (!r)
798
+ return { ok: false, error: 'Could not reach the dashboard server.' };
799
+ const d = (r.data ?? {});
800
+ return { ok: d.ok ?? false, info: d.info, error: d.error };
801
+ }
802
+ /**
803
+ * Save the user-declared budget (authed). Quota fields are gated by
804
+ * routing.operate (admin + operator); the cost cap by policy.write (admin).
805
+ */
806
+ async saveAdminQuota(body) {
807
+ const r = await this.sendAdminRequest('/api/admin/quota', 'PUT', body);
808
+ if (!r)
809
+ return { ok: false, error: 'Could not reach the dashboard server.' };
810
+ const d = (r.data ?? {});
811
+ if (r.status === 200 && d.ok)
812
+ return d;
813
+ return { ok: false, error: d.error || 'Save failed.', unauthorized: r.status === 401 };
814
+ }
815
+ // ─── Bedrock onboarding ───────────────────────────────────────────────────
816
+ /** Get current Bedrock configuration status. */
817
+ async getBedrockStatus() {
818
+ const r = await this.sendAdminRequest('/api/bedrock/status', 'GET');
819
+ if (!r || r.status !== 200) {
820
+ return { configured: false, region: 'us-east-1', authMethod: 'none', apiKeySet: false, iamKeySet: false };
821
+ }
822
+ return (r.data ?? {});
823
+ }
824
+ /** Save Bedrock env vars (credentials + region) to ~/.nuvira/.env. */
825
+ async setupBedrock(envVars) {
826
+ const r = await this.sendAdminRequest('/api/bedrock/setup', 'POST', { envVars });
827
+ if (!r)
828
+ return { ok: false, error: 'Could not reach the dashboard server.' };
829
+ const d = (r.data ?? {});
830
+ if (r.status === 200 && d.ok)
831
+ return { ok: true, envVarsWritten: d.envVarsWritten };
832
+ return { ok: false, error: d.error || 'Save failed.' };
833
+ }
834
+ /** Probe Bedrock models in a region — returns accessibility status for each. */
835
+ async probeBedrock(region) {
836
+ const r = await this.sendAdminRequest('/api/bedrock/probe', 'POST', { region });
837
+ if (!r)
838
+ return { ok: false, error: 'Could not reach the dashboard server.' };
839
+ const d = (r.data ?? {});
840
+ if (r.status === 200 && d.ok)
841
+ return { ok: true, models: d.models };
842
+ return { ok: false, error: d.error || 'Probe failed.' };
843
+ }
844
+ // ─── P1 task runner (command console) ─────────────────────────────────────
845
+ /** Recent task history, newest first (authed — running commands is a write action). */
846
+ async listTasks() {
847
+ const r = await this.sendAdminRequest('/api/tasks', 'GET');
848
+ if (!r)
849
+ return null;
850
+ const d = (r.data ?? {});
851
+ return { status: r.status, tasks: Array.isArray(d.tasks) ? d.tasks : [] };
852
+ }
853
+ /** Start a CLI task: args = the command line split into argv (e.g. ['eval','run','--task','smoke']). */
854
+ async startTask(args, timeoutMs) {
855
+ const r = await this.sendAdminRequest('/api/tasks', 'POST', { args, timeoutMs });
856
+ if (!r)
857
+ return { ok: false, error: 'Could not reach the dashboard server.' };
858
+ const d = (r.data ?? {});
859
+ if (r.status === 200 && d.ok && d.task)
860
+ return d;
861
+ return { ok: false, error: d.error || 'Start failed.', unauthorized: r.status === 401, forbidden: r.status === 403 };
862
+ }
863
+ /** Full task detail (logs included). */
864
+ async getTask(id) {
865
+ const r = await this.sendAdminRequest(`/api/tasks/${encodeURIComponent(id)}`, 'GET');
866
+ if (!r)
867
+ return null;
868
+ const d = (r.data ?? {});
869
+ return { status: r.status, task: d.task ?? null };
870
+ }
871
+ /** Cancel a running task (SIGTERM). */
872
+ async cancelTask(id) {
873
+ const r = await this.sendAdminRequest(`/api/tasks/${encodeURIComponent(id)}/cancel`, 'POST');
874
+ if (!r)
875
+ return { ok: false, error: 'Could not reach the dashboard server.' };
876
+ const d = (r.data ?? {});
877
+ if (r.status === 200 && d.ok)
878
+ return d;
879
+ return { ok: false, error: d.error || 'Cancel failed.', unauthorized: r.status === 401, forbidden: r.status === 403 };
880
+ }
881
+ /**
882
+ * Subscribe to a task's live log/status events over SSE. EventSource can't
883
+ * set Authorization headers, so the admin token rides the ?token= query.
884
+ * Returns an unsubscribe function.
885
+ */
886
+ subscribeTask(id, handlers) {
887
+ const token = getAdminToken();
888
+ const es = new EventSource(`${this.baseUrl}/api/tasks/${encodeURIComponent(id)}/events?token=${encodeURIComponent(token ?? '')}`);
889
+ es.addEventListener('log', (event) => {
890
+ try {
891
+ handlers.onLog?.(JSON.parse(event.data));
892
+ }
893
+ catch { /* ignore malformed */ }
894
+ });
895
+ es.addEventListener('status', (event) => {
896
+ try {
897
+ const payload = JSON.parse(event.data);
898
+ if (payload.status)
899
+ handlers.onStatus?.(payload.status);
900
+ }
901
+ catch { /* ignore malformed */ }
902
+ });
903
+ return () => es.close();
904
+ }
905
+ // ─── P2 — in-page WhatsApp pairing (GUI parity with `nuvira whatsapp pair`) ──
906
+ /** Current pairing status (state, QR data URL, code, session dir). */
907
+ async getWhatsAppStatus() {
908
+ const r = await this.sendAdminRequest('/api/whatsapp', 'GET');
909
+ if (!r)
910
+ return null;
911
+ const d = (r.data ?? {});
912
+ return r.status === 200 && d.ok && d.status ? { status: d.status, contacts: d.contacts ?? {} } : null;
913
+ }
914
+ /** Start pairing — QR mode, or phone mode when `phone` (intl, no +) is set. */
915
+ async startWhatsAppPair(phone) {
916
+ const r = await this.sendAdminRequest('/api/whatsapp/pair', 'POST', { phone: phone || undefined });
917
+ if (!r)
918
+ return { ok: false, error: 'Could not reach the dashboard server.' };
919
+ const d = (r.data ?? {});
920
+ if (r.status === 200 && d.ok)
921
+ return d;
922
+ return { ok: false, error: d.error || 'Pairing failed to start.', unauthorized: r.status === 401, forbidden: r.status === 403 };
923
+ }
924
+ /** Abort the active pairing. */
925
+ async cancelWhatsAppPair() {
926
+ const r = await this.sendAdminRequest('/api/whatsapp/cancel', 'POST');
927
+ if (!r)
928
+ return { ok: false, error: 'Could not reach the dashboard server.' };
929
+ const d = (r.data ?? {});
930
+ if (r.status === 200 && d.ok)
931
+ return d;
932
+ return { ok: false, error: d.error || 'Cancel failed.', unauthorized: r.status === 401, forbidden: r.status === 403 };
933
+ }
934
+ /** Remove the paired WhatsApp session from disk. */
935
+ async unpairWhatsApp() {
936
+ const r = await this.sendAdminRequest('/api/whatsapp/unpair', 'POST');
937
+ if (!r)
938
+ return { ok: false, error: 'Could not reach the dashboard server.' };
939
+ const d = (r.data ?? {});
940
+ if (r.status === 200 && d.ok)
941
+ return d;
942
+ return { ok: false, error: d.error || 'Unpair failed.', unauthorized: r.status === 401, forbidden: r.status === 403 };
943
+ }
944
+ /**
945
+ * Subscribe to pairing events over SSE: qr (PNG data URL) / code (8-char)
946
+ * / status. EventSource can't set Authorization headers, so the admin
947
+ * token rides the ?token= query. Returns an unsubscribe function.
948
+ */
949
+ subscribeWhatsApp(handlers) {
950
+ const token = getAdminToken();
951
+ const es = new EventSource(`${this.baseUrl}/api/whatsapp/events?token=${encodeURIComponent(token ?? '')}`);
952
+ es.addEventListener('qr', (event) => {
953
+ try {
954
+ const payload = JSON.parse(event.data);
955
+ if (payload.qr)
956
+ handlers.onQr?.(payload.qr);
957
+ }
958
+ catch { /* ignore malformed */ }
959
+ });
960
+ es.addEventListener('code', (event) => {
961
+ try {
962
+ const payload = JSON.parse(event.data);
963
+ if (payload.code)
964
+ handlers.onCode?.(payload.code);
965
+ }
966
+ catch { /* ignore malformed */ }
967
+ });
968
+ es.addEventListener('status', (event) => {
969
+ try {
970
+ handlers.onStatus?.(JSON.parse(event.data));
971
+ }
972
+ catch { /* ignore malformed */ }
973
+ });
974
+ return () => es.close();
975
+ }
976
+ // ─── P3 — chat console (GUI parity with `nuvira chat "<prompt>"`) ─────────
977
+ /**
978
+ * Resolve a plain-English ask into the CLI command(s) the intent router
979
+ * would run — the Chat UI calls this BEFORE the agent so deterministic
980
+ * commands ("stop the dashboard") short-circuit to a confirm card.
981
+ */
982
+ async chatResolve(message) {
983
+ const token = getAdminToken();
984
+ try {
985
+ const res = await fetch(`${this.baseUrl}/api/chat/resolve`, {
986
+ method: 'POST',
987
+ headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
988
+ body: JSON.stringify({ message }),
989
+ signal: AbortSignal.timeout(10_000),
990
+ });
991
+ const d = (await res.json());
992
+ if (res.status === 200 && d.ok && Array.isArray(d.matches))
993
+ return { ok: true, matches: d.matches };
994
+ return { ok: false, matches: [], error: typeof d.error === 'string' ? d.error : 'Resolve failed.' };
995
+ }
996
+ catch {
997
+ return { ok: false, matches: [], error: 'Could not reach the dashboard server.' };
998
+ }
999
+ }
1000
+ /**
1001
+ * P3 — project picker: the dashboard's cwd + recently attached paths.
1002
+ */
1003
+ async listProjects() {
1004
+ const token = getAdminToken();
1005
+ try {
1006
+ const res = await fetch(`${this.baseUrl}/api/projects`, {
1007
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
1008
+ signal: AbortSignal.timeout(10_000),
1009
+ });
1010
+ const d = (await res.json());
1011
+ if (res.status === 200 && d.ok && Array.isArray(d.projects))
1012
+ return d.projects;
1013
+ return [];
1014
+ }
1015
+ catch {
1016
+ return [];
1017
+ }
1018
+ }
1019
+ /**
1020
+ * P3 — attach a project directory (server builds the bounded context
1021
+ * snapshot: code map + file tree, cached by path).
1022
+ */
1023
+ async attachProject(path) {
1024
+ const token = getAdminToken();
1025
+ try {
1026
+ const res = await fetch(`${this.baseUrl}/api/projects/attach`, {
1027
+ method: 'POST',
1028
+ headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
1029
+ body: JSON.stringify({ path }),
1030
+ signal: AbortSignal.timeout(30_000),
1031
+ });
1032
+ const d = (await res.json());
1033
+ if (res.status === 200 && d.ok && d.project)
1034
+ return { ok: true, project: d.project };
1035
+ return { ok: false, error: typeof d.error === 'string' ? d.error : 'Attach failed.' };
1036
+ }
1037
+ catch {
1038
+ return { ok: false, error: 'Could not reach the dashboard server.' };
1039
+ }
1040
+ }
1041
+ /**
1042
+ * Browse directories for the project picker folder browser.
1043
+ * Returns subdirectories of the given path (or home dir if empty).
1044
+ * showDrives=true returns drive roots (Windows drives, Mac /Volumes).
1045
+ */
1046
+ async browseDirectories(path, opts) {
1047
+ const token = getAdminToken();
1048
+ try {
1049
+ const params = new URLSearchParams();
1050
+ if (path)
1051
+ params.set('path', path);
1052
+ if (opts?.showDrives)
1053
+ params.set('showDrives', '1');
1054
+ const query = params.toString() ? `?${params.toString()}` : '';
1055
+ const res = await fetch(`${this.baseUrl}/api/browse${query}`, {
1056
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
1057
+ signal: AbortSignal.timeout(10_000),
1058
+ });
1059
+ const d = (await res.json());
1060
+ if (res.status === 200 && d.ok && Array.isArray(d.entries)) {
1061
+ return {
1062
+ ok: true, path: d.path ?? '', entries: d.entries, parent: d.parent ?? null,
1063
+ isProject: d.isProject ?? false, drives: d.drives, breadcrumbs: d.breadcrumbs,
1064
+ };
1065
+ }
1066
+ return { ok: false, path: '', entries: [], parent: null, isProject: false, error: typeof d.error === 'string' ? d.error : 'Browse failed.' };
1067
+ }
1068
+ catch {
1069
+ return { ok: false, path: '', entries: [], parent: null, isProject: false, error: 'Could not reach the dashboard server.' };
1070
+ }
1071
+ }
1072
+ /**
1073
+ * Resolve a folder name to its absolute path by searching common locations.
1074
+ * Used by the native folder picker (webkitdirectory) which only returns the name.
1075
+ */
1076
+ async resolveFolder(name, subPath) {
1077
+ const token = getAdminToken();
1078
+ try {
1079
+ const res = await fetch(this.baseUrl + '/api/browse/resolve-folder', {
1080
+ method: 'POST',
1081
+ headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: 'Bearer ' + token } : {}) },
1082
+ body: JSON.stringify({ name, subPath: subPath || '' }),
1083
+ signal: AbortSignal.timeout(10_000),
1084
+ });
1085
+ const d = (await res.json());
1086
+ if (res.status === 200 && d.ok && d.path)
1087
+ return { ok: true, path: d.path };
1088
+ return { ok: false, path: '', error: typeof d.error === 'string' ? d.error : 'Could not resolve folder.' };
1089
+ }
1090
+ catch {
1091
+ return { ok: false, path: '', error: 'Could not reach the dashboard server.' };
1092
+ }
1093
+ }
1094
+ /**
1095
+ * P4 — session sidebar: list past conversations (title, preview, counts).
1096
+ */
1097
+ async listChatSessions() {
1098
+ const token = getAdminToken();
1099
+ try {
1100
+ const res = await fetch(`${this.baseUrl}/api/sessions`, {
1101
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
1102
+ signal: AbortSignal.timeout(10_000),
1103
+ });
1104
+ const d = (await res.json());
1105
+ if (res.status === 200 && d.ok && Array.isArray(d.sessions))
1106
+ return d.sessions;
1107
+ return [];
1108
+ }
1109
+ catch {
1110
+ return [];
1111
+ }
1112
+ }
1113
+ /**
1114
+ * P8 — delete a past session (sidebar ✕).
1115
+ */
1116
+ async deleteChatSession(id) {
1117
+ const token = getAdminToken();
1118
+ try {
1119
+ const res = await fetch(`${this.baseUrl}/api/sessions/${encodeURIComponent(id)}`, {
1120
+ method: 'DELETE',
1121
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
1122
+ signal: AbortSignal.timeout(10_000),
1123
+ });
1124
+ const d = (await res.json());
1125
+ if (res.status === 200 && d.ok)
1126
+ return { ok: true };
1127
+ return { ok: false, error: d.error || 'Could not delete the session.' };
1128
+ }
1129
+ catch {
1130
+ return { ok: false, error: 'Network error — could not delete the session.' };
1131
+ }
1132
+ }
1133
+ /**
1134
+ * P8 — rename a past session (sidebar ✏️).
1135
+ */
1136
+ async renameChatSession(id, title) {
1137
+ const token = getAdminToken();
1138
+ try {
1139
+ const res = await fetch(`${this.baseUrl}/api/sessions/${encodeURIComponent(id)}/rename`, {
1140
+ method: 'POST',
1141
+ headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
1142
+ body: JSON.stringify({ title }),
1143
+ signal: AbortSignal.timeout(10_000),
1144
+ });
1145
+ const d = (await res.json());
1146
+ if (res.status === 200 && d.ok)
1147
+ return { ok: true };
1148
+ return { ok: false, error: d.error || 'Could not rename the session.' };
1149
+ }
1150
+ catch {
1151
+ return { ok: false, error: 'Network error — could not rename the session.' };
1152
+ }
1153
+ }
1154
+ /**
1155
+ * P4 — load one past session's full transcript (resume in the thread).
1156
+ */
1157
+ async getChatSession(id) {
1158
+ const token = getAdminToken();
1159
+ try {
1160
+ const res = await fetch(`${this.baseUrl}/api/sessions/${encodeURIComponent(id)}`, {
1161
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
1162
+ signal: AbortSignal.timeout(10_000),
1163
+ });
1164
+ const d = (await res.json());
1165
+ if (res.status === 200 && d.ok && d.session)
1166
+ return d.session;
1167
+ return null;
1168
+ }
1169
+ catch {
1170
+ return null;
1171
+ }
1172
+ }
1173
+ /**
1174
+ * Send one chat message. A turn runs the whole agent tool loop and can take
1175
+ * minutes — the 15s admin-request budget would kill it, so this uses its
1176
+ * own fetch with a 5-minute cap.
1177
+ */
1178
+ async chatSend(sessionId, message, opts, signal) {
1179
+ const token = getAdminToken();
1180
+ try {
1181
+ const res = await fetch(`${this.baseUrl}/api/chat`, {
1182
+ method: 'POST',
1183
+ headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
1184
+ body: JSON.stringify({ sessionId, message, provider: opts?.provider, model: opts?.model, projectPath: opts?.projectPath, attachments: opts?.attachments }),
1185
+ // P4 — the Cancel button aborts the POST; the 5-minute ceiling still
1186
+ // applies alongside the caller's signal.
1187
+ signal: signal ? AbortSignal.any([AbortSignal.timeout(300_000), signal]) : AbortSignal.timeout(300_000),
1188
+ });
1189
+ const d = (await res.json());
1190
+ if (res.status === 200 && d.ok) {
1191
+ return {
1192
+ ok: true,
1193
+ content: typeof d.content === 'string' ? d.content : '',
1194
+ followups: Array.isArray(d.followups)
1195
+ ? d.followups
1196
+ : [],
1197
+ provider: typeof d.provider === 'string' ? d.provider : null,
1198
+ model: typeof d.model === 'string' ? d.model : null,
1199
+ generationFailed: d.generationFailed === true,
1200
+ };
1201
+ }
1202
+ return {
1203
+ ok: false,
1204
+ error: typeof d.error === 'string' ? d.error : 'The agent could not answer.',
1205
+ unauthorized: res.status === 401,
1206
+ forbidden: res.status === 403,
1207
+ };
1208
+ }
1209
+ catch {
1210
+ return { ok: false, error: 'Could not reach the dashboard server, or the turn timed out.' };
1211
+ }
1212
+ }
1213
+ /** Forget a chat session's conversation history. */
1214
+ async chatReset(sessionId) {
1215
+ const r = await this.sendAdminRequest('/api/chat/reset', 'POST', { sessionId });
1216
+ if (!r)
1217
+ return { ok: false, error: 'Could not reach the dashboard server.' };
1218
+ const d = (r.data ?? {});
1219
+ return r.status === 200 && d.ok ? { ok: true } : { ok: false, error: d.error || 'Reset failed.' };
1220
+ }
1221
+ /**
1222
+ * Subscribe to a chat session's LIVE progress (agent working steps) and
1223
+ * status over SSE. Subscribe BEFORE sending a message so no step is missed.
1224
+ * Returns an unsubscribe function.
1225
+ */
1226
+ subscribeChat(sessionId, handlers) {
1227
+ const token = getAdminToken();
1228
+ const es = new EventSource(`${this.baseUrl}/api/chat/${encodeURIComponent(sessionId)}/events?token=${encodeURIComponent(token ?? '')}`);
1229
+ es.addEventListener('progress', (event) => {
1230
+ try {
1231
+ const payload = JSON.parse(event.data);
1232
+ if (payload.line)
1233
+ handlers.onProgress?.(payload.line);
1234
+ }
1235
+ catch { /* ignore malformed */ }
1236
+ });
1237
+ es.addEventListener('status', (event) => {
1238
+ try {
1239
+ const payload = JSON.parse(event.data);
1240
+ if (payload.status)
1241
+ handlers.onStatus?.(payload.status);
1242
+ }
1243
+ catch { /* ignore malformed */ }
1244
+ });
1245
+ es.addEventListener('token', (event) => {
1246
+ try {
1247
+ const payload = JSON.parse(event.data);
1248
+ if (typeof payload.text === 'string' && payload.text.length > 0)
1249
+ handlers.onToken?.(payload.text);
1250
+ }
1251
+ catch { /* ignore malformed */ }
1252
+ });
1253
+ es.addEventListener('tool', (event) => {
1254
+ try {
1255
+ const payload = JSON.parse(event.data);
1256
+ if (payload.tool && (payload.phase === 'started' || payload.phase === 'called')) {
1257
+ handlers.onTool?.({
1258
+ id: payload.id || `call_${Math.random().toString(36).slice(2, 8)}`,
1259
+ tool: payload.tool,
1260
+ phase: payload.phase,
1261
+ args: payload.args,
1262
+ ok: payload.ok,
1263
+ result: payload.result,
1264
+ error: payload.error,
1265
+ durationMs: payload.durationMs,
1266
+ });
1267
+ }
1268
+ }
1269
+ catch { /* ignore malformed */ }
1270
+ });
1271
+ es.addEventListener('plan', (event) => {
1272
+ try {
1273
+ const payload = JSON.parse(event.data);
1274
+ if (payload.goal && Array.isArray(payload.steps)) {
1275
+ handlers.onPlan?.({
1276
+ goal: payload.goal,
1277
+ steps: payload.steps,
1278
+ revision: payload.revision ?? 0,
1279
+ });
1280
+ }
1281
+ }
1282
+ catch { /* ignore malformed */ }
1283
+ });
1284
+ es.addEventListener('diff', (event) => {
1285
+ try {
1286
+ const payload = JSON.parse(event.data);
1287
+ if (Array.isArray(payload.files)) {
1288
+ handlers.onDiff?.({
1289
+ files: payload.files,
1290
+ summary: payload.summary ?? '',
1291
+ });
1292
+ }
1293
+ }
1294
+ catch { /* ignore malformed */ }
1295
+ });
1296
+ es.addEventListener('skill_draft', (event) => {
1297
+ try {
1298
+ const payload = JSON.parse(event.data);
1299
+ if (payload.name && payload.markdown) {
1300
+ handlers.onSkillDraft?.({
1301
+ name: payload.name,
1302
+ description: payload.description ?? '',
1303
+ markdown: payload.markdown,
1304
+ updatedAt: payload.updatedAt ?? 0,
1305
+ });
1306
+ }
1307
+ }
1308
+ catch { /* ignore malformed */ }
1309
+ });
1310
+ es.addEventListener('secret_request', (event) => {
1311
+ try {
1312
+ const payload = JSON.parse(event.data);
1313
+ if (payload.skillName && Array.isArray(payload.missing)) {
1314
+ handlers.onSecretRequest?.({
1315
+ skillName: payload.skillName,
1316
+ missing: payload.missing,
1317
+ persisted: payload.persisted ?? {},
1318
+ });
1319
+ }
1320
+ }
1321
+ catch { /* ignore malformed */ }
1322
+ });
1323
+ es.addEventListener('execution_result', (event) => {
1324
+ try {
1325
+ const payload = JSON.parse(event.data);
1326
+ if (payload.skillName && payload.runtime !== undefined) {
1327
+ handlers.onExecutionResult?.({
1328
+ skillName: payload.skillName,
1329
+ runtime: payload.runtime,
1330
+ success: payload.success ?? false,
1331
+ durationMs: payload.durationMs ?? 0,
1332
+ exitCode: payload.exitCode ?? 0,
1333
+ stdout: payload.stdout ?? '',
1334
+ stderr: payload.stderr ?? '',
1335
+ timestamp: payload.timestamp ?? Date.now(),
1336
+ });
1337
+ }
1338
+ }
1339
+ catch { /* ignore malformed */ }
1340
+ });
1341
+ es.addEventListener('question', (event) => {
1342
+ try {
1343
+ const payload = JSON.parse(event.data);
1344
+ if (payload.questionId && payload.question) {
1345
+ handlers.onQuestion?.({
1346
+ questionId: payload.questionId,
1347
+ question: payload.question,
1348
+ choices: payload.choices ?? [],
1349
+ multiSelect: payload.multiSelect === true,
1350
+ });
1351
+ }
1352
+ }
1353
+ catch { /* ignore malformed */ }
1354
+ });
1355
+ return () => es.close();
1356
+ }
1357
+ /**
1358
+ * Answer a pending ask_user question (P0.1). Sends the selected option
1359
+ * index (or indices for multiSelect), or -1 to skip and let the agent
1360
+ * proceed on best judgment.
1361
+ */
1362
+ async chatRespond(sessionId, questionId, selection = {}) {
1363
+ try {
1364
+ const res = await fetch(`${this.baseUrl}/api/chat/${encodeURIComponent(sessionId)}/respond`, {
1365
+ method: 'POST',
1366
+ headers: { 'Content-Type': 'application/json', ...authHeaders() },
1367
+ body: JSON.stringify({ questionId, ...selection }),
1368
+ });
1369
+ const data = (await res.json());
1370
+ return { ok: data.ok === true, error: data.error };
1371
+ }
1372
+ catch (err) {
1373
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
1374
+ }
1375
+ }
1376
+ /** P6a — accept a skill draft: promote it into the live stores (hub + compiled). */
1377
+ async skillDraftAccept(name) {
1378
+ try {
1379
+ const res = await fetch(`${this.baseUrl}/api/skills/drafts/${encodeURIComponent(name)}/accept`, {
1380
+ method: 'POST',
1381
+ headers: { ...authHeaders() },
1382
+ });
1383
+ const data = (await res.json());
1384
+ return { ok: data.ok === true, error: data.error };
1385
+ }
1386
+ catch (err) {
1387
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
1388
+ }
1389
+ }
1390
+ /** P6a — reject a skill draft: discard it (nothing is saved). */
1391
+ async skillDraftReject(name) {
1392
+ try {
1393
+ const res = await fetch(`${this.baseUrl}/api/skills/drafts/${encodeURIComponent(name)}`, {
1394
+ method: 'DELETE',
1395
+ headers: { ...authHeaders() },
1396
+ });
1397
+ const data = (await res.json());
1398
+ return { ok: data.ok === true, error: data.error };
1399
+ }
1400
+ catch (err) {
1401
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
1402
+ }
1403
+ }
1404
+ /** PA4 — save a skill's env vars (writes to ~/.nuvira/.env or ~/.nuvira/.env). */
1405
+ async saveSecrets(vars) {
1406
+ try {
1407
+ const res = await fetch(`${this.baseUrl}/api/skills/secrets`, {
1408
+ method: 'POST',
1409
+ headers: { 'Content-Type': 'application/json', ...authHeaders() },
1410
+ body: JSON.stringify({ vars }),
1411
+ });
1412
+ const data = (await res.json());
1413
+ return { ok: data.ok === true, error: data.error, saved: data.saved };
1414
+ }
1415
+ catch (err) {
1416
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
1417
+ }
1418
+ }
1419
+ }
1420
+ exports.DashboardAPI = DashboardAPI;
1421
+ exports.dashboardAPI = new DashboardAPI();