@nordbyte/nordrelay 0.5.0 → 0.5.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 (44) hide show
  1. package/.env.example +2 -0
  2. package/README.md +23 -14
  3. package/dist/access-control.js +2 -0
  4. package/dist/agent-updates.js +61 -10
  5. package/dist/bot-ui.js +1 -0
  6. package/dist/bot.js +142 -1065
  7. package/dist/channel-actions.js +8 -8
  8. package/dist/codex-cli.js +1 -1
  9. package/dist/config-metadata.js +2 -0
  10. package/dist/operations.js +233 -122
  11. package/dist/relay-artifact-service.js +126 -0
  12. package/dist/relay-external-activity-monitor.js +216 -0
  13. package/dist/relay-queue-service.js +66 -0
  14. package/dist/relay-runtime-types.js +1 -0
  15. package/dist/relay-runtime.js +119 -371
  16. package/dist/state-backend.js +3 -0
  17. package/dist/support-bundle.js +221 -0
  18. package/dist/telegram-agent-commands.js +212 -0
  19. package/dist/telegram-artifact-commands.js +139 -0
  20. package/dist/telegram-command-menu.js +1 -0
  21. package/dist/telegram-command-types.js +1 -0
  22. package/dist/telegram-diagnostics-command.js +102 -0
  23. package/dist/telegram-general-commands.js +52 -0
  24. package/dist/telegram-operational-commands.js +153 -0
  25. package/dist/telegram-preference-commands.js +198 -0
  26. package/dist/telegram-queue-commands.js +278 -0
  27. package/dist/telegram-support-command.js +53 -0
  28. package/dist/telegram-update-commands.js +6 -1
  29. package/dist/web-api-contract.js +79 -31
  30. package/dist/web-api-types.js +1 -0
  31. package/dist/web-dashboard-access-routes.js +163 -0
  32. package/dist/web-dashboard-artifact-routes.js +65 -0
  33. package/dist/web-dashboard-assets.js +2 -0
  34. package/dist/web-dashboard-http.js +143 -0
  35. package/dist/web-dashboard-pages.js +257 -0
  36. package/dist/web-dashboard-runtime-routes.js +92 -0
  37. package/dist/web-dashboard-session-routes.js +209 -0
  38. package/dist/web-dashboard.js +44 -882
  39. package/dist/webui-assets/dashboard.css +74 -4
  40. package/dist/webui-assets/dashboard.js +163 -24
  41. package/dist/zip-writer.js +83 -0
  42. package/package.json +10 -4
  43. package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
  44. package/plugins/nordrelay/scripts/nordrelay.mjs +258 -5
@@ -62,7 +62,9 @@
62
62
  background: var(--surface);
63
63
  color: var(--text);
64
64
  border-color: var(--border);
65
+ min-height: 32px;
65
66
  height: 32px;
67
+ line-height: 1;
66
68
  }
67
69
  .agent-settings-nav button.active {
68
70
  background: var(--accent);
@@ -84,10 +86,13 @@
84
86
  .chip {
85
87
  display: inline-flex;
86
88
  align-items: center;
89
+ justify-content: center;
90
+ min-height: 20px;
87
91
  border-radius: 999px;
88
92
  border: 1px solid var(--border);
89
- padding: 2px 8px;
93
+ padding: 0 8px;
90
94
  font-size: 12px;
95
+ line-height: 1;
91
96
  color: var(--muted);
92
97
  margin-right: 6px;
93
98
  }
@@ -139,6 +144,7 @@
139
144
  cursor: pointer;
140
145
  font-weight: 700;
141
146
  margin-bottom: 0;
147
+ line-height: 1.25;
142
148
  }
143
149
  .session-detail-section[open] summary {
144
150
  margin-bottom: 10px;
@@ -239,9 +245,11 @@
239
245
  margin-top: 22px;
240
246
  }
241
247
  .mini-button {
248
+ min-height: 26px;
242
249
  height: 26px;
243
250
  padding: 0 8px;
244
251
  font-size: 12px;
252
+ line-height: 1;
245
253
  }
246
254
  .update-log {
247
255
  max-height: 280px;
@@ -264,16 +272,26 @@
264
272
  * {
265
273
  box-sizing: border-box;
266
274
  }
275
+ html {
276
+ font-size: 16px;
277
+ -webkit-text-size-adjust: 100%;
278
+ text-size-adjust: 100%;
279
+ }
267
280
  body {
268
281
  margin: 0;
269
282
  background: var(--bg);
270
283
  color: var(--text);
271
284
  font-family:
272
285
  Inter,
286
+ "Segoe UI",
273
287
  system-ui,
274
288
  -apple-system,
275
- Segoe UI,
289
+ BlinkMacSystemFont,
290
+ Roboto,
291
+ Arial,
276
292
  sans-serif;
293
+ line-height: 1.4;
294
+ font-synthesis: none;
277
295
  }
278
296
  .app {
279
297
  min-height: 100vh;
@@ -421,14 +439,30 @@ textarea {
421
439
  background: var(--surface);
422
440
  color: var(--text);
423
441
  font: inherit;
442
+ line-height: 1.2;
443
+ vertical-align: middle;
424
444
  }
425
445
  button {
446
+ appearance: none;
447
+ -webkit-appearance: none;
448
+ display: inline-flex;
449
+ align-items: center;
450
+ justify-content: center;
451
+ gap: 6px;
452
+ min-height: 36px;
426
453
  height: 36px;
427
454
  padding: 0 12px;
428
455
  background: var(--accent);
429
456
  color: white;
430
457
  border-color: var(--accent);
431
458
  cursor: pointer;
459
+ line-height: 1;
460
+ text-align: center;
461
+ white-space: nowrap;
462
+ }
463
+ button::-moz-focus-inner {
464
+ border: 0;
465
+ padding: 0;
432
466
  }
433
467
  button:hover {
434
468
  background: var(--accent-strong);
@@ -437,8 +471,12 @@ button.secondary {
437
471
  background: var(--surface);
438
472
  color: var(--text);
439
473
  }
474
+ button:disabled {
475
+ cursor: not-allowed;
476
+ }
440
477
  input,
441
478
  select {
479
+ min-height: 36px;
442
480
  height: 36px;
443
481
  padding: 0 10px;
444
482
  }
@@ -446,6 +484,23 @@ textarea {
446
484
  width: 100%;
447
485
  padding: 10px;
448
486
  resize: vertical;
487
+ line-height: 1.4;
488
+ }
489
+ nav button {
490
+ display: flex;
491
+ align-items: center;
492
+ justify-content: flex-start;
493
+ width: 100%;
494
+ min-height: 40px;
495
+ height: 40px;
496
+ padding: 0 12px;
497
+ line-height: 1.1;
498
+ text-align: left;
499
+ }
500
+ .menu {
501
+ align-items: center;
502
+ justify-content: center;
503
+ line-height: 1;
449
504
  }
450
505
  .chat-layout {
451
506
  display: grid;
@@ -520,6 +575,8 @@ textarea {
520
575
  .file-button {
521
576
  display: inline-flex;
522
577
  align-items: center;
578
+ justify-content: center;
579
+ min-height: 34px;
523
580
  height: 34px;
524
581
  padding: 0 10px;
525
582
  border: 1px solid var(--border);
@@ -527,6 +584,7 @@ textarea {
527
584
  background: var(--surface);
528
585
  color: var(--text);
529
586
  cursor: pointer;
587
+ line-height: 1;
530
588
  }
531
589
  input[type=file] {
532
590
  display: none;
@@ -551,6 +609,7 @@ input[type=file] {
551
609
  }
552
610
  .copy-id {
553
611
  height: auto;
612
+ min-height: 0;
554
613
  padding: 0;
555
614
  border: 0;
556
615
  background: transparent;
@@ -562,6 +621,8 @@ input[type=file] {
562
621
  Consolas,
563
622
  monospace;
564
623
  font-size: 12px;
624
+ line-height: 1.25;
625
+ white-space: normal;
565
626
  }
566
627
  .copy-id:hover {
567
628
  background: transparent;
@@ -603,11 +664,13 @@ input[type=file] {
603
664
  .item strong {
604
665
  display: block;
605
666
  overflow-wrap: anywhere;
667
+ line-height: 1.25;
606
668
  }
607
669
  .item small {
608
670
  display: block;
609
671
  color: var(--muted);
610
672
  overflow-wrap: anywhere;
673
+ line-height: 1.35;
611
674
  }
612
675
  .queue-item {
613
676
  cursor: grab;
@@ -619,11 +682,14 @@ input[type=file] {
619
682
  .adapter-status {
620
683
  display: inline-flex;
621
684
  align-items: center;
685
+ justify-content: center;
686
+ min-height: 20px;
622
687
  border: 1px solid var(--border);
623
688
  border-radius: 999px;
624
- padding: 2px 8px;
689
+ padding: 0 8px;
625
690
  color: var(--muted);
626
691
  font-size: 12px;
692
+ line-height: 1;
627
693
  }
628
694
  .adapter-status {
629
695
  margin-left: 6px;
@@ -651,12 +717,14 @@ input[type=file] {
651
717
  }
652
718
  .feature-chip {
653
719
  display: flex;
720
+ align-items: center;
654
721
  justify-content: space-between;
655
722
  gap: 8px;
656
723
  border: 1px solid var(--border-soft);
657
724
  border-radius: 6px;
658
725
  padding: 5px 7px;
659
726
  font-size: 12px;
727
+ line-height: 1.2;
660
728
  color: var(--muted);
661
729
  background: var(--surface);
662
730
  }
@@ -726,7 +794,9 @@ input[type=file] {
726
794
  background: var(--surface);
727
795
  color: var(--text);
728
796
  border-color: var(--border);
797
+ min-height: 34px;
729
798
  height: 34px;
799
+ line-height: 1;
730
800
  }
731
801
  .tabs button.active {
732
802
  background: var(--accent);
@@ -836,7 +906,7 @@ dialog::backdrop {
836
906
  transform: translateX(0);
837
907
  }
838
908
  .menu {
839
- display: inline-block;
909
+ display: inline-flex;
840
910
  }
841
911
  .header-actions {
842
912
  justify-content: flex-end;
@@ -1,17 +1,145 @@
1
1
  (() => {
2
- const state = { snapshot: null, controls: null, newSessionControls: null, enabledAgents: [], auth: null, permissions: [], settings: [], currentPage: "overview", settingsGroup: null, logsPlain: "", logTimer: null, toastTimer: null, cliStatusActive: false, selectedArtifactTurns: /* @__PURE__ */ new Set(), mediaRecorder: null, recordedChunks: [], events: null, reconnectTimer: null, notifications: false, toolTooltipTimer: null, toolTooltipTarget: null, agentUpdateJobs: [], sessionsRequestId: 0 };
2
+ const WEB_API_CLIENT_ROUTE_RULES = [
3
+ { path: "/api/auth/me", methods: ["GET"] },
4
+ { path: "/api/dashboard/logout", methods: ["POST"] },
5
+ { path: "/api/bootstrap", methods: ["GET"] },
6
+ { path: "/api/health", methods: ["GET"] },
7
+ { path: "/api/snapshot", methods: ["GET"] },
8
+ { path: "/api/tasks", methods: ["GET"] },
9
+ { path: "/api/progress", methods: ["GET"] },
10
+ { path: "/api/version", methods: ["GET"] },
11
+ { path: "/api/update", methods: ["POST"] },
12
+ { path: "/api/agent-updates", methods: ["GET"] },
13
+ { path: "/api/agent-update", methods: ["POST"] },
14
+ { re: /^\/api\/agent-update\/[^\/]+\/log$/, methods: ["GET", "DELETE"] },
15
+ { re: /^\/api\/agent-update\/[^\/]+\/input$/, methods: ["POST"] },
16
+ { re: /^\/api\/agent-update\/[^\/]+\/cancel$/, methods: ["POST"] },
17
+ { path: "/api/adapters/health", methods: ["GET"] },
18
+ { path: "/api/permissions", methods: ["GET"] },
19
+ { path: "/api/users", methods: ["GET", "POST"] },
20
+ { re: /^\/api\/users\/[^\/]+$/, methods: ["PATCH"] },
21
+ { re: /^\/api\/users\/[^\/]+\/password$/, methods: ["POST"] },
22
+ { re: /^\/api\/users\/[^\/]+\/sessions$/, methods: ["GET", "DELETE"] },
23
+ { re: /^\/api\/users\/[^\/]+\/sessions\/[^\/]+$/, methods: ["DELETE"] },
24
+ { re: /^\/api\/users\/[^\/]+\/telegram$/, methods: ["POST"] },
25
+ { re: /^\/api\/users\/[^\/]+\/telegram\/[^\/]+$/, methods: ["DELETE"] },
26
+ { path: "/api/groups", methods: ["GET", "POST"] },
27
+ { re: /^\/api\/groups\/[^\/]+$/, methods: ["PATCH"] },
28
+ { path: "/api/telegram-chats", methods: ["GET", "POST"] },
29
+ { re: /^\/api\/telegram-chats\/[^\/]+$/, methods: ["PATCH"] },
30
+ { path: "/api/audit", methods: ["GET"] },
31
+ { path: "/api/locks", methods: ["GET", "POST", "DELETE"] },
32
+ { path: "/api/auth/status", methods: ["GET"] },
33
+ { path: "/api/auth/login", methods: ["POST"] },
34
+ { path: "/api/auth/logout", methods: ["POST"] },
35
+ { path: "/api/settings", methods: ["GET", "PATCH"] },
36
+ { path: "/api/control-options", methods: ["GET"] },
37
+ { path: "/api/sessions", methods: ["GET"] },
38
+ { path: "/api/sessions/new", methods: ["POST"] },
39
+ { path: "/api/sessions/switch", methods: ["POST"] },
40
+ { path: "/api/sessions/attach", methods: ["POST"] },
41
+ { path: "/api/sessions/detail", methods: ["GET"] },
42
+ { path: "/api/agent", methods: ["POST"] },
43
+ { path: "/api/models", methods: ["GET"] },
44
+ { path: "/api/session/model", methods: ["POST"] },
45
+ { path: "/api/session/reasoning", methods: ["POST"] },
46
+ { path: "/api/session/fast", methods: ["POST"] },
47
+ { path: "/api/session/launch", methods: ["POST"] },
48
+ { path: "/api/prompt", methods: ["POST"] },
49
+ { path: "/api/prompt/upload", methods: ["POST"] },
50
+ { path: "/api/abort", methods: ["POST"] },
51
+ { path: "/api/stop", methods: ["POST"] },
52
+ { path: "/api/handback", methods: ["POST"] },
53
+ { path: "/api/retry", methods: ["POST"] },
54
+ { path: "/api/sync", methods: ["POST"] },
55
+ { path: "/api/queue", methods: ["GET", "POST"] },
56
+ { path: "/api/chat/history", methods: ["GET", "DELETE"] },
57
+ { path: "/api/activity", methods: ["GET"] },
58
+ { path: "/api/artifacts", methods: ["GET", "DELETE"] },
59
+ { path: "/api/artifacts/bulk", methods: ["POST"] },
60
+ { path: "/api/artifacts/zip", methods: ["GET"] },
61
+ { path: "/api/artifacts/file", methods: ["GET"] },
62
+ { path: "/api/artifacts/preview", methods: ["GET"] },
63
+ { path: "/api/logs", methods: ["GET"] },
64
+ { path: "/api/logs/clear", methods: ["POST"] },
65
+ { path: "/api/diagnostics", methods: ["GET"] },
66
+ { path: "/api/diagnostics/bundle", methods: ["GET"] },
67
+ { path: "/api/runtime/restart", methods: ["POST"] }
68
+ ];
69
+ globalThis.NORDRELAY_WEB_API_CLIENT_ROUTE_RULES = WEB_API_CLIENT_ROUTE_RULES;
70
+ const API_ROUTE_RULES = (
71
+ /** @type {{ NORDRELAY_WEB_API_CLIENT_ROUTE_RULES?: ApiRouteRule[] }} */
72
+ globalThis.NORDRELAY_WEB_API_CLIENT_ROUTE_RULES ?? []
73
+ );
3
74
  async function api(path, options = {}) {
4
- const headers = { ...options.body ? { "content-type": "application/json" } : {}, ...options.headers || {} };
5
- const res = await fetch(path, { ...options, headers });
75
+ const method = normalizeMethod(options.method, options.body);
76
+ const url = apiUrl(path, options.query);
77
+ assertApiRoute(url.pathname, method);
78
+ const body = normalizeBody(options.body);
79
+ const headers = {
80
+ ...body !== void 0 && shouldSendJsonHeader(options.body) ? { "content-type": "application/json" } : {},
81
+ ...options.headers || {}
82
+ };
83
+ const res = await fetch(url.pathname + url.search, { method, headers, body });
6
84
  if (res.status === 401) {
7
85
  location.reload();
8
- return;
86
+ return (
87
+ /** @type {never} */
88
+ void 0
89
+ );
9
90
  }
10
91
  const text = await res.text();
11
92
  const data = text ? JSON.parse(text) : {};
12
93
  if (!res.ok) throw new Error(data.error || res.statusText);
13
94
  return data;
14
95
  }
96
+ function apiUrl(path, query) {
97
+ const url = new URL(path, location.origin);
98
+ if (query) {
99
+ for (const [key, rawValue] of Object.entries(query)) {
100
+ const values = Array.isArray(rawValue) ? rawValue : [rawValue];
101
+ for (const value of values) {
102
+ if (value !== void 0 && value !== null && value !== "") {
103
+ url.searchParams.append(key, String(value));
104
+ }
105
+ }
106
+ }
107
+ }
108
+ return url;
109
+ }
110
+ function normalizeMethod(method, body) {
111
+ if (method) {
112
+ const upper = method.toUpperCase();
113
+ if (upper === "GET" || upper === "POST" || upper === "PATCH" || upper === "PUT" || upper === "DELETE") {
114
+ return upper;
115
+ }
116
+ }
117
+ return body === void 0 || body === null ? "GET" : "POST";
118
+ }
119
+ function normalizeBody(body) {
120
+ if (body === void 0 || body === null) return void 0;
121
+ if (typeof body === "string") return body;
122
+ if (isNativeBody(body)) return body;
123
+ return JSON.stringify(body);
124
+ }
125
+ function shouldSendJsonHeader(body) {
126
+ return body !== void 0 && body !== null && !isNativeBody(body);
127
+ }
128
+ function isNativeBody(body) {
129
+ return typeof FormData !== "undefined" && body instanceof FormData || typeof Blob !== "undefined" && body instanceof Blob || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer;
130
+ }
131
+ function assertApiRoute(path, method) {
132
+ const rule = API_ROUTE_RULES.find(
133
+ (candidate) => "path" in candidate && candidate.path === path || "re" in candidate && candidate.re.test(path)
134
+ );
135
+ if (!rule) {
136
+ throw new Error("Unknown WebUI API route: " + path);
137
+ }
138
+ if (!rule.methods.includes(method)) {
139
+ throw new Error("Unsupported WebUI API method: " + method + " " + path);
140
+ }
141
+ }
142
+ const state = { snapshot: null, controls: null, newSessionControls: null, enabledAgents: [], auth: null, permissions: [], settings: [], currentPage: "overview", settingsGroup: null, logsPlain: "", logTimer: null, toastTimer: null, cliStatusActive: false, selectedArtifactTurns: /* @__PURE__ */ new Set(), mediaRecorder: null, recordedChunks: [], events: null, reconnectTimer: null, notifications: false, toolTooltipTimer: null, toolTooltipTarget: null, agentUpdateJobs: [], sessionsRequestId: 0 };
15
143
  function toast(msg, options = {}) {
16
144
  const el = document.getElementById("toast");
17
145
  el.textContent = msg;
@@ -772,7 +900,7 @@
772
900
  state.newSessionControls = state.controls || {};
773
901
  renderNewSessionControls(state.newSessionControls);
774
902
  agentSelect.onchange = () => safe(async () => {
775
- state.newSessionControls = await api("/api/control-options?agent=" + encodeURIComponent(agentSelect.value));
903
+ state.newSessionControls = await api("/api/control-options", { query: { agent: agentSelect.value } });
776
904
  renderNewSessionControls(state.newSessionControls);
777
905
  });
778
906
  }
@@ -806,8 +934,7 @@
806
934
  const requestId = ++state.sessionsRequestId;
807
935
  setLoading("sessionsList", "Loading " + (expectedAgent || "agent") + " sessions...");
808
936
  const q = document.getElementById("sessionSearch").value || "";
809
- const agentParam = expectedAgent ? "&agent=" + encodeURIComponent(expectedAgent) : "";
810
- const data = await api("/api/sessions?query=" + encodeURIComponent(q) + "&page=" + sessionsPager.page + "&limit=" + sessionsPager.pageSize + agentParam);
937
+ const data = await api("/api/sessions", { query: { query: q, page: sessionsPager.page, limit: sessionsPager.pageSize, agent: expectedAgent || void 0 } });
811
938
  if (requestId !== state.sessionsRequestId || expectedAgent !== activeAgentId()) return;
812
939
  document.getElementById("sessionsList").innerHTML = data.sessions.map((s) => '<div class="item"><strong title="' + attr(s.title || s.firstUserMessage || s.id) + '">' + esc(short(s.title || s.firstUserMessage || s.id)) + '</strong><small><button type="button" class="copy-id" data-copy-id="' + attr(s.id) + '" title="Copy thread ID">' + esc(short(s.id, 64)) + "</button> / " + esc(short((s.cwd || "") + " / " + fmtDate(s.updatedAt))) + '</small><div class="row"><button data-switch="' + attr(s.id) + '"' + disabledAttr("sessions.write") + '>Switch</button><button class="secondary" data-session-detail="' + attr(s.id) + '">Details</button></div></div>').join("") || '<div class="item">No sessions found.</div>';
813
940
  sessionsPager.render(data.pagination || {});
@@ -817,7 +944,7 @@
817
944
  toast("Permission required: sessions.write");
818
945
  return;
819
946
  }
820
- await api("/api/sessions/switch", { method: "POST", body: JSON.stringify({ threadId: b.dataset.switch }) });
947
+ await api("/api/sessions/switch", { method: "POST", body: { threadId: b.dataset.switch } });
821
948
  toast("Session switched");
822
949
  loadBootstrap();
823
950
  }));
@@ -836,7 +963,7 @@
836
963
  return '<details class="session-detail-section"><summary>' + esc(title + " (" + count + ")") + '</summary><div class="list">' + (body || '<div class="item">No entries.</div>') + "</div></details>";
837
964
  }
838
965
  async function loadSessionDetail(threadId) {
839
- const d = await api("/api/sessions/detail?threadId=" + encodeURIComponent(threadId));
966
+ const d = await api("/api/sessions/detail", { query: { threadId } });
840
967
  const r = d.record || {};
841
968
  const messages = d.messages || [];
842
969
  const activity = d.activity || [];
@@ -972,7 +1099,7 @@
972
1099
  return;
973
1100
  }
974
1101
  if (confirm("Delete artifact turn " + b.dataset.delArt + "?")) {
975
- await api("/api/artifacts?turnId=" + encodeURIComponent(b.dataset.delArt), { method: "DELETE" });
1102
+ await api("/api/artifacts", { method: "DELETE", query: { turnId: b.dataset.delArt } });
976
1103
  state.selectedArtifactTurns.delete(b.dataset.delArt);
977
1104
  loadArtifacts();
978
1105
  }
@@ -1016,7 +1143,7 @@
1016
1143
  target.innerHTML = '<div class="panel">' + loadingHtml("Loading preview...") + "</div>";
1017
1144
  target.scrollIntoView({ block: "start", behavior: "smooth" });
1018
1145
  try {
1019
- const data = await api("/api/artifacts/preview?turnId=" + encodeURIComponent(turnId) + "&path=" + encodeURIComponent(path));
1146
+ const data = await api("/api/artifacts/preview", { query: { turnId, path } });
1020
1147
  if (data.kind === "image") {
1021
1148
  target.innerHTML = '<div class="panel"><h2>' + esc(data.name) + '</h2><img src="/api/artifacts/file?turnId=' + encodeURIComponent(turnId) + "&path=" + encodeURIComponent(path) + '"></div>';
1022
1149
  return;
@@ -1033,8 +1160,7 @@
1033
1160
  }
1034
1161
  async function loadActivity() {
1035
1162
  setLoading("activityList", "Loading activity...");
1036
- const q = "?source=" + encodeURIComponent(val("activitySource")) + "&status=" + encodeURIComponent(val("activityStatus")) + "&limit=" + encodeURIComponent(val("activityLimit") || "100");
1037
- const data = await api("/api/activity" + q);
1163
+ const data = await api("/api/activity", { query: { source: val("activitySource"), status: val("activityStatus"), limit: val("activityLimit") || "100" } });
1038
1164
  state.activityEvents = data.events || [];
1039
1165
  renderActivity(state.activityEvents);
1040
1166
  }
@@ -1376,7 +1502,7 @@
1376
1502
  document.getElementById("auditList").innerHTML = '<div class="item">Permission required: audit.read</div>';
1377
1503
  return;
1378
1504
  }
1379
- const d = await api("/api/audit?limit=" + encodeURIComponent(val("auditLimit") || "50"));
1505
+ const d = await api("/api/audit", { query: { limit: val("auditLimit") || "50" } });
1380
1506
  document.getElementById("auditList").innerHTML = (d.events || []).map((e) => '<div class="item"><strong>' + esc(fmtDate(e.timestamp) + " / " + (e.channelId || "-") + " / " + e.status + " / " + e.action) + "</strong><small>" + esc((e.contextKey || "-") + " / " + (e.agentId || "-") + " / " + (e.threadId || "-")) + "</small><small>" + esc(e.description || e.detail || "") + "</small></div>").join("") || '<div class="item">No audit events.</div>';
1381
1507
  }
1382
1508
  document.getElementById("loadAuditBtn").onclick = () => loadAudit();
@@ -1384,7 +1510,7 @@
1384
1510
  if (!document.getElementById("logAutoRefresh").checked) setLoading("logs", "Loading logs...");
1385
1511
  const target = document.getElementById("logTarget").value;
1386
1512
  const lines = document.getElementById("logLines").value;
1387
- const data = await api("/api/logs?target=" + target + "&lines=" + lines);
1513
+ const data = await api("/api/logs", { query: { target, lines } });
1388
1514
  state.logsPlain = data.plain || "";
1389
1515
  renderLogs();
1390
1516
  if (document.getElementById("logFollow").checked) document.getElementById("logs").scrollTop = document.getElementById("logs").scrollHeight;
@@ -1441,7 +1567,7 @@
1441
1567
  const d = await api("/api/adapters/health");
1442
1568
  document.getElementById("adapterHealth").innerHTML = (d.adapters || []).map((a) => '<div class="item"><strong>' + esc(a.label) + ' <span class="adapter-status ' + esc(a.status) + '">' + esc(a.status) + "</span></strong><small>" + esc("CLI: " + (a.cli.label || "-") + " / path " + (a.cli.path || "-") + " / version " + (a.cli.version || "-")) + "</small><small>" + esc("Auth: " + (a.auth.supported ? a.auth.authenticated ? "authenticated" : "not authenticated" : "not managed") + " " + (a.auth.detail || "")) + "</small><small>" + esc("Version: " + a.version.installed + " / latest " + (a.version.latest || "-") + " / " + a.version.status) + "</small>" + featureMatrix(a.capabilities) + '<div class="row"><button data-auth-status="' + attr(a.id) + '">Auth status</button><button data-auth-login="' + attr(a.id) + '" class="secondary" ' + (!a.capabilities.login ? "disabled" : "") + disabledAttr("auth.manage") + '>Login</button><button data-auth-logout="' + attr(a.id) + '" class="secondary" ' + (!a.capabilities.logout ? "disabled" : "") + disabledAttr("auth.manage") + ">Logout</button></div></div>").join("") || '<div class="item">No adapters.</div>';
1443
1569
  document.querySelectorAll("[data-auth-status]").forEach((b) => b.onclick = () => safe(async () => {
1444
- const r = await api("/api/auth/status?agent=" + encodeURIComponent(b.dataset.authStatus));
1570
+ const r = await api("/api/auth/status", { query: { agent: b.dataset.authStatus } });
1445
1571
  toast(r.agentLabel + ": " + r.detail, { duration: 6e3 });
1446
1572
  }));
1447
1573
  document.querySelectorAll("[data-auth-login]").forEach((b) => b.onclick = () => safe(async () => {
@@ -1449,7 +1575,7 @@
1449
1575
  toast("Permission required: auth.manage");
1450
1576
  return;
1451
1577
  }
1452
- const r = await api("/api/auth/login", { method: "POST", body: JSON.stringify({ agentId: b.dataset.authLogin }) });
1578
+ const r = await api("/api/auth/login", { method: "POST", body: { agentId: b.dataset.authLogin } });
1453
1579
  toast(r.result?.message || r.detail, { duration: 8e3 });
1454
1580
  loadAdapterHealth();
1455
1581
  }));
@@ -1458,7 +1584,7 @@
1458
1584
  toast("Permission required: auth.manage");
1459
1585
  return;
1460
1586
  }
1461
- const r = await api("/api/auth/logout", { method: "POST", body: JSON.stringify({ agentId: b.dataset.authLogout }) });
1587
+ const r = await api("/api/auth/logout", { method: "POST", body: { agentId: b.dataset.authLogout } });
1462
1588
  toast(r.result?.message || r.detail, { duration: 8e3 });
1463
1589
  loadAdapterHealth();
1464
1590
  }));
@@ -1482,7 +1608,10 @@
1482
1608
  function versionCard(key, v) {
1483
1609
  const agentId = versionAgentIds[key];
1484
1610
  const running = agentId && state.agentUpdateJobs.some((j) => j.agentId === agentId && j.status === "running");
1485
- const button = agentId && v.status === "outdated" ? '<button class="secondary mini-button" data-update-agent="' + attr(agentId) + '" ' + (running ? "disabled" : "") + ">" + (running ? "Updating" : "Update") + "</button>" : "";
1611
+ const operation = v.status === "not-installed" ? "install" : v.status === "outdated" ? "update" : "";
1612
+ const actionLabel = operation === "install" ? "Install" : "Update";
1613
+ const runningLabel = operation === "install" ? "Installing" : "Updating";
1614
+ const button = agentId && operation ? '<button class="secondary mini-button" data-update-agent="' + attr(agentId) + '" data-update-operation="' + attr(operation) + '" ' + (running ? "disabled" : "") + ">" + (running ? runningLabel : actionLabel) + "</button>" : "";
1486
1615
  return '<div class="item"><strong>' + esc(v.label) + ' <span class="adapter-status ' + esc(versionStatusClass(v.status)) + '">' + esc(versionStatusLabel(v.status)) + "</span> " + button + "</strong><small>" + esc("Installed: " + (v.installedLabel || "-")) + "</small><small>" + esc("Latest: " + (v.latestVersion || "-")) + "</small>" + (v.detail ? "<small>" + esc(v.detail) + "</small>" : "") + "</div>";
1487
1616
  }
1488
1617
  async function loadAgentUpdateJobs(showLoading = true) {
@@ -1516,11 +1645,12 @@
1516
1645
  }
1517
1646
  function updateJobCard(job) {
1518
1647
  const command = [job.command].concat(job.args || []).join(" ");
1648
+ const operation = job.operation || "update";
1519
1649
  const needs = job.needsInput ? '<small><span class="chip warn">Input may be required</span></small>' : "";
1520
1650
  const logDeleted = job.logDeletedAt ? "<small>Log deleted " + esc(fmtDate(job.logDeletedAt)) + "</small>" : "";
1521
1651
  const deleteLogButton = job.status !== "running" && !job.logDeletedAt ? '<button class="danger mini-button" data-update-delete-log="' + attr(job.id) + '"' + disabledAttr("updates.run") + ">Delete Log</button>" : "";
1522
- const input = job.canInput ? '<div class="update-input"><input data-update-input="' + attr(job.id) + '" placeholder="Send response to update process"><button data-update-send="' + attr(job.id) + '" class="secondary"' + disabledAttr("updates.run") + '>Send</button><button data-update-cancel="' + attr(job.id) + '" class="danger"' + disabledAttr("updates.run") + ">Cancel</button></div>" : "";
1523
- return '<div class="item"><div class="update-job-header"><strong>' + esc(job.agentLabel) + ' <span class="adapter-status ' + esc(jobStatusClass(job.status)) + '">' + esc(job.status) + '</span></strong><div class="row">' + deleteLogButton + "</div></div><small>" + esc(job.method + " / " + fmtDate(job.startedAt) + (job.finishedAt ? " - " + fmtDate(job.finishedAt) : "")) + "</small><small>" + esc(command) + "</small><small>" + esc(job.error || job.summary || "") + "</small>" + logDeleted + needs + '<pre class="update-log">' + esc(job.outputTail || (job.logDeletedAt ? "(log deleted)" : "(waiting for output)")) + "</pre>" + input + "</div>";
1652
+ const input = job.canInput ? '<div class="update-input"><input data-update-input="' + attr(job.id) + '" placeholder="Send response to ' + operation + ' process"><button data-update-send="' + attr(job.id) + '" class="secondary"' + disabledAttr("updates.run") + '>Send</button><button data-update-cancel="' + attr(job.id) + '" class="danger"' + disabledAttr("updates.run") + ">Cancel</button></div>" : "";
1653
+ return '<div class="item"><div class="update-job-header"><strong>' + esc(job.agentLabel + " " + operation) + ' <span class="adapter-status ' + esc(jobStatusClass(job.status)) + '">' + esc(job.status) + '</span></strong><div class="row">' + deleteLogButton + "</div></div><small>" + esc(job.method + " / " + fmtDate(job.startedAt) + (job.finishedAt ? " - " + fmtDate(job.finishedAt) : "")) + "</small><small>" + esc(command) + "</small><small>" + esc(job.error || job.summary || "") + "</small>" + logDeleted + needs + '<pre class="update-log">' + esc(job.outputTail || (job.logDeletedAt ? "(log deleted)" : "(waiting for output)")) + "</pre>" + input + "</div>";
1524
1654
  }
1525
1655
  function bindAgentUpdateButtons() {
1526
1656
  document.querySelectorAll("[data-update-agent]").forEach((b) => b.onclick = () => safe(async () => {
@@ -1528,11 +1658,12 @@
1528
1658
  toast("Permission required: updates.run");
1529
1659
  return;
1530
1660
  }
1531
- if (confirm("Start update for " + b.dataset.updateAgent + "?")) {
1532
- const r = await api("/api/agent-update", { method: "POST", body: JSON.stringify({ agentId: b.dataset.updateAgent }) });
1661
+ const operation = b.dataset.updateOperation || "update";
1662
+ if (confirm("Start " + operation + " for " + b.dataset.updateAgent + "?")) {
1663
+ const r = await api("/api/agent-update", { method: "POST", body: JSON.stringify({ agentId: b.dataset.updateAgent, operation }) });
1533
1664
  upsertAgentUpdateJob(r.job);
1534
1665
  renderAgentUpdateJobs();
1535
- toast(r.job.agentLabel + " update started", { duration: 6e3 });
1666
+ toast(r.job.agentLabel + " " + (r.job.operation || operation) + " started", { duration: 6e3 });
1536
1667
  loadVersion();
1537
1668
  }
1538
1669
  }));
@@ -1592,6 +1723,14 @@
1592
1723
  const data = await api("/api/diagnostics");
1593
1724
  document.getElementById("diagnostics").innerHTML = diagnosticsHtml(data);
1594
1725
  }
1726
+ const exportDiagnosticsBundleBtn = document.getElementById("exportDiagnosticsBundleBtn");
1727
+ if (exportDiagnosticsBundleBtn) exportDiagnosticsBundleBtn.onclick = () => {
1728
+ if (!can("diagnostics.read")) {
1729
+ toast("Permission required: diagnostics.read");
1730
+ return;
1731
+ }
1732
+ window.open("/api/diagnostics/bundle", "_blank");
1733
+ };
1595
1734
  function diagnosticsHtml(d) {
1596
1735
  const h = d.health || {};
1597
1736
  const s = d.snapshot?.session || {};
@@ -0,0 +1,83 @@
1
+ export function createZipBuffer(entries) {
2
+ const localParts = [];
3
+ const centralParts = [];
4
+ let offset = 0;
5
+ for (const entry of entries) {
6
+ const name = normalizeZipEntryName(entry.name);
7
+ const nameBuffer = Buffer.from(name, "utf8");
8
+ const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data, "utf8");
9
+ const crc = crc32(data);
10
+ const date = entry.date ?? new Date();
11
+ const { dosTime, dosDate } = dateToDos(date);
12
+ const localHeader = Buffer.alloc(30);
13
+ localHeader.writeUInt32LE(0x04034b50, 0);
14
+ localHeader.writeUInt16LE(20, 4);
15
+ localHeader.writeUInt16LE(0x0800, 6);
16
+ localHeader.writeUInt16LE(0, 8);
17
+ localHeader.writeUInt16LE(dosTime, 10);
18
+ localHeader.writeUInt16LE(dosDate, 12);
19
+ localHeader.writeUInt32LE(crc, 14);
20
+ localHeader.writeUInt32LE(data.byteLength, 18);
21
+ localHeader.writeUInt32LE(data.byteLength, 22);
22
+ localHeader.writeUInt16LE(nameBuffer.byteLength, 26);
23
+ localHeader.writeUInt16LE(0, 28);
24
+ localParts.push(localHeader, nameBuffer, data);
25
+ const centralHeader = Buffer.alloc(46);
26
+ centralHeader.writeUInt32LE(0x02014b50, 0);
27
+ centralHeader.writeUInt16LE(20, 4);
28
+ centralHeader.writeUInt16LE(20, 6);
29
+ centralHeader.writeUInt16LE(0x0800, 8);
30
+ centralHeader.writeUInt16LE(0, 10);
31
+ centralHeader.writeUInt16LE(dosTime, 12);
32
+ centralHeader.writeUInt16LE(dosDate, 14);
33
+ centralHeader.writeUInt32LE(crc, 16);
34
+ centralHeader.writeUInt32LE(data.byteLength, 20);
35
+ centralHeader.writeUInt32LE(data.byteLength, 24);
36
+ centralHeader.writeUInt16LE(nameBuffer.byteLength, 28);
37
+ centralHeader.writeUInt16LE(0, 30);
38
+ centralHeader.writeUInt16LE(0, 32);
39
+ centralHeader.writeUInt16LE(0, 34);
40
+ centralHeader.writeUInt16LE(0, 36);
41
+ centralHeader.writeUInt32LE(0, 38);
42
+ centralHeader.writeUInt32LE(offset, 42);
43
+ centralParts.push(centralHeader, nameBuffer);
44
+ offset += localHeader.byteLength + nameBuffer.byteLength + data.byteLength;
45
+ }
46
+ const centralOffset = offset;
47
+ const centralDirectory = Buffer.concat(centralParts);
48
+ const end = Buffer.alloc(22);
49
+ end.writeUInt32LE(0x06054b50, 0);
50
+ end.writeUInt16LE(0, 4);
51
+ end.writeUInt16LE(0, 6);
52
+ end.writeUInt16LE(entries.length, 8);
53
+ end.writeUInt16LE(entries.length, 10);
54
+ end.writeUInt32LE(centralDirectory.byteLength, 12);
55
+ end.writeUInt32LE(centralOffset, 16);
56
+ end.writeUInt16LE(0, 20);
57
+ return Buffer.concat([...localParts, centralDirectory, end]);
58
+ }
59
+ function normalizeZipEntryName(name) {
60
+ return name.replace(/\\/g, "/").replace(/^\/+/, "");
61
+ }
62
+ function dateToDos(date) {
63
+ const year = Math.max(1980, date.getFullYear());
64
+ return {
65
+ dosTime: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
66
+ dosDate: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
67
+ };
68
+ }
69
+ const CRC32_TABLE = new Uint32Array(256);
70
+ for (let index = 0; index < CRC32_TABLE.length; index += 1) {
71
+ let value = index;
72
+ for (let bit = 0; bit < 8; bit += 1) {
73
+ value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
74
+ }
75
+ CRC32_TABLE[index] = value >>> 0;
76
+ }
77
+ function crc32(data) {
78
+ let crc = 0xffffffff;
79
+ for (const byte of data) {
80
+ crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
81
+ }
82
+ return (crc ^ 0xffffffff) >>> 0;
83
+ }