@adonis-agora/agent-dashboard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +57 -0
  4. package/dist/providers/agent_dashboard_provider.d.ts +33 -0
  5. package/dist/providers/agent_dashboard_provider.d.ts.map +1 -0
  6. package/dist/providers/agent_dashboard_provider.js +101 -0
  7. package/dist/providers/agent_dashboard_provider.js.map +1 -0
  8. package/dist/spa/assets/index-BYLm0nq8.css +1 -0
  9. package/dist/spa/assets/index-DR5D-Xzw.js +9 -0
  10. package/dist/spa/index.html +13 -0
  11. package/dist/src/client/agent-client.d.ts +55 -0
  12. package/dist/src/client/agent-client.d.ts.map +1 -0
  13. package/dist/src/client/agent-client.js +141 -0
  14. package/dist/src/client/agent-client.js.map +1 -0
  15. package/dist/src/client/api-base.d.ts +23 -0
  16. package/dist/src/client/api-base.d.ts.map +1 -0
  17. package/dist/src/client/api-base.js +32 -0
  18. package/dist/src/client/api-base.js.map +1 -0
  19. package/dist/src/client/default-range.d.ts +10 -0
  20. package/dist/src/client/default-range.d.ts.map +1 -0
  21. package/dist/src/client/default-range.js +26 -0
  22. package/dist/src/client/default-range.js.map +1 -0
  23. package/dist/src/client/format.d.ts +22 -0
  24. package/dist/src/client/format.d.ts.map +1 -0
  25. package/dist/src/client/format.js +76 -0
  26. package/dist/src/client/format.js.map +1 -0
  27. package/dist/src/client/index.d.ts +9 -0
  28. package/dist/src/client/index.d.ts.map +1 -0
  29. package/dist/src/client/index.js +8 -0
  30. package/dist/src/client/index.js.map +1 -0
  31. package/dist/src/client/spend-summary.d.ts +43 -0
  32. package/dist/src/client/spend-summary.d.ts.map +1 -0
  33. package/dist/src/client/spend-summary.js +59 -0
  34. package/dist/src/client/spend-summary.js.map +1 -0
  35. package/dist/src/client/trend-path.d.ts +27 -0
  36. package/dist/src/client/trend-path.d.ts.map +1 -0
  37. package/dist/src/client/trend-path.js +25 -0
  38. package/dist/src/client/trend-path.js.map +1 -0
  39. package/dist/src/client/types.d.ts +202 -0
  40. package/dist/src/client/types.d.ts.map +1 -0
  41. package/dist/src/client/types.js +8 -0
  42. package/dist/src/client/types.js.map +1 -0
  43. package/dist/src/server/define_config.d.ts +18 -0
  44. package/dist/src/server/define_config.d.ts.map +1 -0
  45. package/dist/src/server/define_config.js +6 -0
  46. package/dist/src/server/define_config.js.map +1 -0
  47. package/dist/src/server/paths.d.ts +27 -0
  48. package/dist/src/server/paths.d.ts.map +1 -0
  49. package/dist/src/server/paths.js +78 -0
  50. package/dist/src/server/paths.js.map +1 -0
  51. package/package.json +84 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ # @adonis-agora/agent-dashboard
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Davi Carvalho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @adonis-agora/agent-dashboard
2
+
3
+ A read-only **governance console** for [`@adonis-agora/agent`](../adonis) — a dependency-light React
4
+ SPA served by a thin AdonisJS provider. It visualizes the agent's own governance read-model: spend by
5
+ model and by actor, the daily usage/cost trend, recent threads and tool calls, and today's quota.
6
+
7
+ Part of the Agora ecosystem, themed with the Agora design tokens (AdonisJS violet on a dusk-ink
8
+ canvas, warm-paper in light).
9
+
10
+ ## What it shows
11
+
12
+ All data comes from the agent provider's **real** routes — the SPA is a pure consumer:
13
+
14
+ | View | Route(s) consumed |
15
+ | ---------- | ---------------------------------------------------------------------------- |
16
+ | Overview | `GET /agent/governance/spend/model`, `spend/actor`, `usage/trend` |
17
+ | Threads | `GET /agent/governance/threads/recent` |
18
+ | Tool calls | `GET /agent/governance/tool-calls/recent` |
19
+ | Quota | `GET /agent/quota/today` |
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ node ace add @adonis-agora/agent-dashboard
25
+ ```
26
+
27
+ Register the provider (after the agent provider) in `adonisrc.ts`:
28
+
29
+ ```ts
30
+ providers: [
31
+ () => import('@adonis-agora/agent/agent_provider'),
32
+ () => import('@adonis-agora/agent-dashboard'),
33
+ ]
34
+ ```
35
+
36
+ The dashboard reads `config/agent.ts` for the agent `path` and `actorResolver`, mounts the SPA at
37
+ `<agentPath>/dashboard` (default `/agent/dashboard`), and gates every request through the **same**
38
+ actor resolver as the governance routes. Toggle or relocate it with an optional block:
39
+
40
+ ```ts
41
+ // config/agent.ts
42
+ export default defineConfig({
43
+ // ...
44
+ dashboard: { enabled: true, path: '/agent/dashboard' },
45
+ })
46
+ ```
47
+
48
+ ## Browser client
49
+
50
+ The framework-free fetch client is exported for reuse:
51
+
52
+ ```ts
53
+ import { AgentClient } from '@adonis-agora/agent-dashboard/client'
54
+
55
+ const client = new AgentClient({ baseUrl: '/agent' })
56
+ const spend = await client.spendByModel({ fromDay: '2026-03-01', toDay: '2026-03-07' })
57
+ ```
@@ -0,0 +1,33 @@
1
+ import type { ApplicationService } from '@adonisjs/core/types';
2
+ /**
3
+ * Serves the `@adonis-agora/agent-dashboard` governance SPA (a Vite build in `dist/spa`) as static
4
+ * assets under `<agentPath>/dashboard`, behind the SAME actor gating as the agent's governance
5
+ * routes: every request resolves the actor through the agent config's `actorResolver`, replying `401`
6
+ * on failure (an app that never configured a resolver exposes nothing — identical to the
7
+ * `/agent/governance/*` routes). This is a thin, idiomatic AdonisJS static server — no NestJS module,
8
+ * no bundled API. The SPA calls the agent's own real read routes.
9
+ *
10
+ * Routes (mount = `<agentPath>/dashboard`, default `/agent/dashboard`):
11
+ * - `GET <mount>` → `302` to `<mount>/` (canonical trailing slash so relative assets resolve)
12
+ * - `GET <mount>/` → the SPA shell (`index.html`, with the resolved API base injected)
13
+ * - `GET <mount>/*` → a built asset, or the SPA shell as a fallback (client-rendered console)
14
+ *
15
+ * Enable/disable and override the mount via the optional `config('agent').dashboard` block.
16
+ */
17
+ export default class AgentDashboardProvider {
18
+ protected app: ApplicationService;
19
+ constructor(app: ApplicationService);
20
+ /** The built SPA directory (`dist/spa`) as a `file://` URL, next to this compiled provider. */
21
+ private spaDirUrl;
22
+ boot(): Promise<void>;
23
+ /**
24
+ * Resolve the actor through the agent config's resolver, mirroring the governance routes. Returns
25
+ * `true` to proceed; on a missing/failed resolver replies `401` and returns `false`.
26
+ */
27
+ private gate;
28
+ /** Read `dist/spa/index.html`, inject the API base, and send it. */
29
+ private sendIndex;
30
+ /** Send the requested built asset, or fall back to the SPA shell when it does not exist. */
31
+ private sendAsset;
32
+ }
33
+ //# sourceMappingURL=agent_dashboard_provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent_dashboard_provider.d.ts","sourceRoot":"","sources":["../../providers/agent_dashboard_provider.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAU/D;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,OAAO,OAAO,sBAAsB;IAC7B,SAAS,CAAC,GAAG,EAAE,kBAAkB;gBAAvB,GAAG,EAAE,kBAAkB;IAE7C,+FAA+F;IAC/F,OAAO,CAAC,SAAS,CAAuC;IAElD,IAAI;IAkCV;;;OAGG;YACW,IAAI;IAgBlB,oEAAoE;YACtD,SAAS;IAOvB,4FAA4F;YAC9E,SAAS;CAcxB"}
@@ -0,0 +1,101 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import router from '@adonisjs/core/services/router';
3
+ import { resolveDashboardConfig } from '../src/server/define_config.js';
4
+ import { apiBaseFor, contentTypeFor, injectApiBase, mountPathFor, safeAssetSegments, } from '../src/server/paths.js';
5
+ /**
6
+ * Serves the `@adonis-agora/agent-dashboard` governance SPA (a Vite build in `dist/spa`) as static
7
+ * assets under `<agentPath>/dashboard`, behind the SAME actor gating as the agent's governance
8
+ * routes: every request resolves the actor through the agent config's `actorResolver`, replying `401`
9
+ * on failure (an app that never configured a resolver exposes nothing — identical to the
10
+ * `/agent/governance/*` routes). This is a thin, idiomatic AdonisJS static server — no NestJS module,
11
+ * no bundled API. The SPA calls the agent's own real read routes.
12
+ *
13
+ * Routes (mount = `<agentPath>/dashboard`, default `/agent/dashboard`):
14
+ * - `GET <mount>` → `302` to `<mount>/` (canonical trailing slash so relative assets resolve)
15
+ * - `GET <mount>/` → the SPA shell (`index.html`, with the resolved API base injected)
16
+ * - `GET <mount>/*` → a built asset, or the SPA shell as a fallback (client-rendered console)
17
+ *
18
+ * Enable/disable and override the mount via the optional `config('agent').dashboard` block.
19
+ */
20
+ export default class AgentDashboardProvider {
21
+ app;
22
+ constructor(app) {
23
+ this.app = app;
24
+ }
25
+ /** The built SPA directory (`dist/spa`) as a `file://` URL, next to this compiled provider. */
26
+ spaDirUrl = new URL('../spa/', import.meta.url);
27
+ async boot() {
28
+ const agentConfig = this.app.config.get('agent', {});
29
+ const dashboardConfig = resolveDashboardConfig(this.app.config.get('agent.dashboard', {}));
30
+ if (!dashboardConfig.enabled)
31
+ return;
32
+ const agentPath = agentConfig.path ?? 'agent';
33
+ const apiBase = apiBaseFor(agentPath);
34
+ const mount = mountPathFor(agentPath, dashboardConfig.path);
35
+ const actorResolver = agentConfig.actorResolver;
36
+ // Bare mount → canonical trailing slash so the SPA's relative `./assets/*` URLs resolve against
37
+ // the mount directory rather than its parent.
38
+ router.get(mount, (ctx) => ctx.response.redirect().toPath(`${mount}/`));
39
+ // The SPA shell.
40
+ router.get(`${mount}/`, async (ctx) => {
41
+ if (!(await this.gate(ctx, actorResolver)))
42
+ return;
43
+ await this.sendIndex(ctx, apiBase);
44
+ });
45
+ // Built assets (JS/CSS/fonts/...), with an index fallback for any unmatched path so the
46
+ // client-rendered console still boots on a deep link.
47
+ router.get(`${mount}/*`, async (ctx) => {
48
+ if (!(await this.gate(ctx, actorResolver)))
49
+ return;
50
+ const segments = safeAssetSegments(ctx.params['*']);
51
+ if (segments === null) {
52
+ return ctx.response.status(400).json({ error: 'bad asset path' });
53
+ }
54
+ await this.sendAsset(ctx, segments, apiBase);
55
+ });
56
+ }
57
+ /**
58
+ * Resolve the actor through the agent config's resolver, mirroring the governance routes. Returns
59
+ * `true` to proceed; on a missing/failed resolver replies `401` and returns `false`.
60
+ */
61
+ async gate(ctx, actorResolver) {
62
+ if (actorResolver === undefined) {
63
+ ctx.response.status(401).json({ error: 'no actor resolver configured' });
64
+ return false;
65
+ }
66
+ try {
67
+ await actorResolver.resolve(ctx);
68
+ return true;
69
+ }
70
+ catch (error) {
71
+ ctx.response
72
+ .status(401)
73
+ .json({ error: error instanceof Error ? error.message : 'unauthorized' });
74
+ return false;
75
+ }
76
+ }
77
+ /** Read `dist/spa/index.html`, inject the API base, and send it. */
78
+ async sendIndex(ctx, apiBase) {
79
+ const html = await readFile(new URL('index.html', this.spaDirUrl), 'utf8');
80
+ ctx.response.header('content-type', 'text/html; charset=utf-8');
81
+ ctx.response.header('cache-control', 'no-store, must-revalidate');
82
+ ctx.response.send(injectApiBase(html, apiBase));
83
+ }
84
+ /** Send the requested built asset, or fall back to the SPA shell when it does not exist. */
85
+ async sendAsset(ctx, segments, apiBase) {
86
+ const filename = segments[segments.length - 1] ?? 'index.html';
87
+ try {
88
+ const buffer = await readFile(new URL(segments.join('/'), this.spaDirUrl));
89
+ ctx.response.header('content-type', contentTypeFor(filename));
90
+ ctx.response.send(buffer);
91
+ }
92
+ catch (error) {
93
+ if (error.code === 'ENOENT') {
94
+ await this.sendIndex(ctx, apiBase);
95
+ return;
96
+ }
97
+ throw error;
98
+ }
99
+ }
100
+ }
101
+ //# sourceMappingURL=agent_dashboard_provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent_dashboard_provider.js","sourceRoot":"","sources":["../../providers/agent_dashboard_provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ :root{--agora-primary: #5a45ff;--agora-primary-soft: hsl(250 74% 51% / .14);--agora-accent: #06b6d4;--bg: hsl(240 10% 6.5%);--panel: hsl(240 9% 9%);--panel-2: hsl(240 9% 11%);--line: hsl(40 30% 82% / .1);--line-soft: hsl(40 30% 82% / .05);--text: hsl(40 28% 92%);--muted: hsl(40 12% 66%);--primary: var(--agora-primary);--primary-fg: hsl(40 40% 96%);--good: #34d399;--warn: #fbbf24;--bad: #f87171;--info: var(--agora-accent)}:root[data-theme=light],:root:not([data-theme=dark]){color-scheme:light}:root[data-theme=dark]{color-scheme:dark}@media(prefers-color-scheme:light){:root:not([data-theme=dark]){--bg: hsl(40 30% 96%);--panel: hsl(40 28% 98%);--panel-2: hsl(40 24% 95%);--line: hsl(240 10% 14% / .12);--line-soft: hsl(240 10% 14% / .06);--text: hsl(240 14% 12%);--muted: hsl(240 6% 42%);--primary-fg: hsl(40 40% 97%)}}:root[data-theme=light]{--bg: hsl(40 30% 96%);--panel: hsl(40 28% 98%);--panel-2: hsl(40 24% 95%);--line: hsl(240 10% 14% / .12);--line-soft: hsl(240 10% 14% / .06);--text: hsl(240 14% 12%);--muted: hsl(240 6% 42%);--primary-fg: hsl(40 40% 97%)}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--bg);color:var(--text);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;-webkit-font-smoothing:antialiased}.mono{font-family:ui-monospace,JetBrains Mono,SF Mono,Menlo,monospace}.tnum{font-variant-numeric:tabular-nums}.app-bg{position:fixed;inset:0;z-index:0;pointer-events:none;background-image:radial-gradient(900px 420px at 82% -10%,hsl(250 74% 51% / .12),transparent 70%),radial-gradient(700px 360px at 8% -12%,hsl(190 90% 50% / .06),transparent 70%);opacity:.9}.shell{position:relative;z-index:1;max-width:1180px;margin:0 auto;padding:28px 24px 64px}.masthead{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:22px}.brand{display:flex;align-items:center;gap:12px}.brand-mark{width:34px;height:34px;border-radius:9px;display:grid;place-items:center;color:var(--primary-fg);background:linear-gradient(140deg,var(--agora-primary),#8b78ff);box-shadow:0 6px 20px #4426df59;font-weight:700}.brand h1{font-size:17px;margin:0;letter-spacing:-.01em}.brand p{margin:1px 0 0;font-size:12px;color:var(--muted)}.tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:20px}.tab{appearance:none;border:1px solid var(--line);background:var(--panel);color:var(--muted);border-radius:999px;padding:7px 14px;font-size:13px;cursor:pointer;display:inline-flex;align-items:center;gap:7px;transition:color .15s,border-color .15s,background .15s}.tab:hover{color:var(--text)}.tab[aria-selected=true]{color:var(--primary-fg);background:var(--primary);border-color:var(--primary)}.controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.range-input{background:var(--panel);border:1px solid var(--line);color:var(--text);border-radius:8px;padding:6px 9px;font-size:12px;font-family:inherit}.icon-btn{appearance:none;border:1px solid var(--line);background:var(--panel);color:var(--muted);border-radius:8px;width:34px;height:32px;cursor:pointer;display:grid;place-items:center}.icon-btn:hover{color:var(--text)}.panel{background:linear-gradient(180deg,var(--panel-2),var(--panel));border:1px solid var(--line);border-radius:14px;padding:18px}.grid{display:grid;gap:16px}.grid.cols-3{grid-template-columns:repeat(3,1fr)}.grid.cols-2{grid-template-columns:repeat(2,1fr)}.grid.stat-4{grid-template-columns:repeat(4,1fr)}@media(max-width:900px){.grid.cols-3,.grid.cols-2,.grid.stat-4{grid-template-columns:repeat(2,1fr)}}@media(max-width:560px){.grid.cols-3,.grid.cols-2,.grid.stat-4{grid-template-columns:1fr}}.stat .label{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:8px}.stat .value{font-size:24px;font-weight:600;letter-spacing:-.02em}.stat .sub{font-size:12px;color:var(--muted);margin-top:4px}.section-title{display:flex;align-items:center;justify-content:space-between;margin:0 0 14px;gap:12px}.section-title h2{font-size:14px;margin:0;letter-spacing:.01em}.section-title .hint{font-size:12px;color:var(--muted)}.table{width:100%;border-collapse:collapse;font-size:13px}.table th{text-align:left;font-weight:500;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);padding:0 10px 10px;border-bottom:1px solid var(--line)}.table td{padding:10px;border-bottom:1px solid var(--line-soft);vertical-align:middle}.table tr:last-child td{border-bottom:none}.table .num{text-align:right}.table-wrap{overflow-x:auto}.bar{height:6px;border-radius:999px;background:var(--line);overflow:hidden;min-width:60px}.bar>span{display:block;height:100%;border-radius:999px;background:var(--primary)}.legend{display:flex;flex-direction:column;gap:8px;font-size:12px}.legend .row{display:flex;align-items:center;gap:8px}.swatch{width:10px;height:10px;border-radius:3px;flex:none}.legend .name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pill{display:inline-flex;align-items:center;gap:6px;padding:2px 9px;border-radius:999px;font-size:11px;border:1px solid var(--line);color:var(--muted)}.dot{width:7px;height:7px;border-radius:999px;background:currentColor;display:inline-block}.s-completed,.s-ok,.s-success,.s-approved{color:var(--good)}.s-running,.s-pending,.s-pending_approval,.s-awaiting_approval{color:var(--info)}.s-failed,.s-error,.s-rejected,.s-forbidden{color:var(--bad)}.s-cancelled,.s-canceled{color:var(--warn)}.btn{appearance:none;border:1px solid var(--line);background:var(--panel);color:var(--text);border-radius:8px;padding:6px 12px;font-size:12px;font-family:inherit;cursor:pointer;transition:color .15s,border-color .15s,background .15s}.btn:hover:not(:disabled){border-color:var(--muted)}.btn:disabled{opacity:.45;cursor:not-allowed}.btn.approve{color:var(--good);border-color:color-mix(in srgb,var(--good) 40%,var(--line))}.btn.reject{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 40%,var(--line))}.rowbtns{display:inline-flex;gap:6px;justify-content:flex-end}.table tr.clickable{cursor:pointer}.table tr.clickable:hover td{background:var(--panel-2)}.th-sort{appearance:none;background:none;border:none;padding:0;margin:0;font:inherit;color:inherit;text-transform:inherit;letter-spacing:inherit;cursor:pointer}.th-sort:hover{color:var(--text)}.warnnum{color:var(--warn)}.msg{border:1px solid var(--line);border-radius:10px;padding:10px 12px;background:var(--panel-2)}.msg-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}.msg-body{font-size:13px;white-space:pre-wrap;word-break:break-word}.empty{color:var(--muted);font-size:13px;text-align:center;padding:26px 10px}.err{color:var(--bad);font-size:13px;padding:20px 10px;text-align:center}.skeleton{border-radius:8px;background:linear-gradient(90deg,var(--line-soft),var(--line),var(--line-soft));background-size:200% 100%;animation:shimmer 1.3s ease-in-out infinite;height:14px}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.stack{display:flex;flex-direction:column;gap:16px}.muted{color:var(--muted)}.right{text-align:right}.chart-svg{width:100%;height:auto;display:block}.foot{margin-top:26px;font-size:11px;color:var(--muted);text-align:center}