@agent-native/dispatch 0.1.1 → 0.2.3

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 (146) hide show
  1. package/dist/actions/index.d.ts.map +1 -1
  2. package/dist/actions/index.js +2 -0
  3. package/dist/actions/index.js.map +1 -1
  4. package/dist/actions/list-dispatch-usage-metrics.d.ts +3 -0
  5. package/dist/actions/list-dispatch-usage-metrics.d.ts.map +1 -0
  6. package/dist/actions/list-dispatch-usage-metrics.js +18 -0
  7. package/dist/actions/list-dispatch-usage-metrics.js.map +1 -0
  8. package/dist/actions/navigate.d.ts +1 -0
  9. package/dist/actions/navigate.d.ts.map +1 -1
  10. package/dist/actions/navigate.js +3 -17
  11. package/dist/actions/navigate.js.map +1 -1
  12. package/dist/actions/view-screen.d.ts.map +1 -1
  13. package/dist/actions/view-screen.js +19 -0
  14. package/dist/actions/view-screen.js.map +1 -1
  15. package/dist/components/agents-panel.js +3 -3
  16. package/dist/components/app-keys-popover.js +2 -2
  17. package/dist/components/create-app-popover.js +2 -2
  18. package/dist/components/dispatch-shell.js +2 -2
  19. package/dist/components/index.d.ts +1 -0
  20. package/dist/components/index.d.ts.map +1 -1
  21. package/dist/components/index.js.map +1 -1
  22. package/dist/components/layout/Header.js +5 -5
  23. package/dist/components/layout/Header.js.map +1 -1
  24. package/dist/components/layout/Layout.d.ts +28 -3
  25. package/dist/components/layout/Layout.d.ts.map +1 -1
  26. package/dist/components/layout/Layout.js +138 -28
  27. package/dist/components/layout/Layout.js.map +1 -1
  28. package/dist/components/messaging-setup-panel.js +4 -4
  29. package/dist/components/ui/accordion.js +1 -1
  30. package/dist/components/ui/alert-dialog.js +2 -2
  31. package/dist/components/ui/alert.js +1 -1
  32. package/dist/components/ui/avatar.js +1 -1
  33. package/dist/components/ui/badge.js +1 -1
  34. package/dist/components/ui/breadcrumb.js +1 -1
  35. package/dist/components/ui/button.js +1 -1
  36. package/dist/components/ui/calendar.js +2 -2
  37. package/dist/components/ui/card.js +1 -1
  38. package/dist/components/ui/carousel.d.ts +2 -2
  39. package/dist/components/ui/carousel.js +2 -2
  40. package/dist/components/ui/chart.js +1 -1
  41. package/dist/components/ui/checkbox.js +1 -1
  42. package/dist/components/ui/command.js +2 -2
  43. package/dist/components/ui/context-menu.js +1 -1
  44. package/dist/components/ui/dialog.js +1 -1
  45. package/dist/components/ui/drawer.js +1 -1
  46. package/dist/components/ui/dropdown-menu.js +1 -1
  47. package/dist/components/ui/form.js +2 -2
  48. package/dist/components/ui/hover-card.js +1 -1
  49. package/dist/components/ui/input-otp.js +1 -1
  50. package/dist/components/ui/input.js +1 -1
  51. package/dist/components/ui/label.js +1 -1
  52. package/dist/components/ui/menubar.js +1 -1
  53. package/dist/components/ui/navigation-menu.js +1 -1
  54. package/dist/components/ui/pagination.d.ts +1 -1
  55. package/dist/components/ui/pagination.js +2 -2
  56. package/dist/components/ui/popover.js +1 -1
  57. package/dist/components/ui/progress.js +1 -1
  58. package/dist/components/ui/radio-group.js +1 -1
  59. package/dist/components/ui/resizable.js +1 -1
  60. package/dist/components/ui/scroll-area.js +1 -1
  61. package/dist/components/ui/select.js +1 -1
  62. package/dist/components/ui/separator.js +1 -1
  63. package/dist/components/ui/sheet.js +1 -1
  64. package/dist/components/ui/sidebar.d.ts +2 -2
  65. package/dist/components/ui/sidebar.d.ts.map +1 -1
  66. package/dist/components/ui/sidebar.js +9 -9
  67. package/dist/components/ui/sidebar.js.map +1 -1
  68. package/dist/components/ui/skeleton.js +1 -1
  69. package/dist/components/ui/slider.js +1 -1
  70. package/dist/components/ui/sonner.js +1 -1
  71. package/dist/components/ui/spinner.js +1 -1
  72. package/dist/components/ui/switch.js +1 -1
  73. package/dist/components/ui/table.js +1 -1
  74. package/dist/components/ui/tabs.js +1 -1
  75. package/dist/components/ui/textarea.js +1 -1
  76. package/dist/components/ui/toast.js +1 -1
  77. package/dist/components/ui/toaster.js +2 -2
  78. package/dist/components/ui/toggle-group.js +2 -2
  79. package/dist/components/ui/toggle.js +1 -1
  80. package/dist/components/ui/tooltip.js +1 -1
  81. package/dist/components/ui/use-toast.d.ts +1 -1
  82. package/dist/components/ui/use-toast.js +1 -1
  83. package/dist/hooks/use-navigation-state.d.ts +2 -1
  84. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  85. package/dist/hooks/use-navigation-state.js +36 -8
  86. package/dist/hooks/use-navigation-state.js.map +1 -1
  87. package/dist/hooks/use-toast.d.ts +1 -1
  88. package/dist/routes/index.d.ts.map +1 -1
  89. package/dist/routes/index.js +3 -2
  90. package/dist/routes/index.js.map +1 -1
  91. package/dist/routes/pages/_index.js +1 -1
  92. package/dist/routes/pages/agents.js +2 -2
  93. package/dist/routes/pages/approval.js +2 -2
  94. package/dist/routes/pages/approvals.js +4 -4
  95. package/dist/routes/pages/apps.$appId.js +3 -3
  96. package/dist/routes/pages/apps.js +5 -5
  97. package/dist/routes/pages/audit.js +1 -1
  98. package/dist/routes/pages/destinations.js +6 -6
  99. package/dist/routes/pages/extensions.$id.d.ts +2 -0
  100. package/dist/routes/pages/extensions.$id.d.ts.map +1 -0
  101. package/dist/routes/pages/extensions.$id.js +6 -0
  102. package/dist/routes/pages/extensions.$id.js.map +1 -0
  103. package/dist/routes/pages/extensions._index.d.ts +2 -0
  104. package/dist/routes/pages/extensions._index.d.ts.map +1 -0
  105. package/dist/routes/pages/extensions._index.js +6 -0
  106. package/dist/routes/pages/extensions._index.js.map +1 -0
  107. package/dist/routes/pages/identities.js +2 -2
  108. package/dist/routes/pages/integrations.js +4 -4
  109. package/dist/routes/pages/messaging.js +2 -2
  110. package/dist/routes/pages/metrics.d.ts +5 -0
  111. package/dist/routes/pages/metrics.d.ts.map +1 -0
  112. package/dist/routes/pages/metrics.js +135 -0
  113. package/dist/routes/pages/metrics.js.map +1 -0
  114. package/dist/routes/pages/new-app.js +1 -1
  115. package/dist/routes/pages/overview.d.ts.map +1 -1
  116. package/dist/routes/pages/overview.js +9 -17
  117. package/dist/routes/pages/overview.js.map +1 -1
  118. package/dist/routes/pages/team.js +1 -1
  119. package/dist/routes/pages/vault.js +10 -10
  120. package/dist/routes/pages/workspace.js +10 -10
  121. package/dist/server/lib/pre-auth-routing.d.ts.map +1 -1
  122. package/dist/server/lib/pre-auth-routing.js +9 -2
  123. package/dist/server/lib/pre-auth-routing.js.map +1 -1
  124. package/dist/server/lib/usage-metrics-store.d.ts +93 -0
  125. package/dist/server/lib/usage-metrics-store.d.ts.map +1 -0
  126. package/dist/server/lib/usage-metrics-store.js +386 -0
  127. package/dist/server/lib/usage-metrics-store.js.map +1 -0
  128. package/package.json +11 -6
  129. package/src/actions/index.ts +2 -0
  130. package/src/actions/list-dispatch-usage-metrics.ts +19 -0
  131. package/src/actions/navigate.ts +5 -17
  132. package/src/actions/view-screen.ts +18 -0
  133. package/src/components/index.ts +6 -0
  134. package/src/components/layout/Header.tsx +2 -2
  135. package/src/components/layout/Layout.tsx +197 -48
  136. package/src/components/ui/sidebar.tsx +22 -18
  137. package/src/hooks/use-navigation-state.ts +57 -8
  138. package/src/routes/index.ts +3 -2
  139. package/src/routes/pages/extensions.$id.tsx +5 -0
  140. package/src/routes/pages/extensions._index.tsx +5 -0
  141. package/src/routes/pages/metrics.tsx +667 -0
  142. package/src/routes/pages/overview.tsx +0 -10
  143. package/src/server/lib/pre-auth-routing.ts +10 -2
  144. package/src/server/lib/usage-metrics-store.ts +605 -0
  145. package/src/styles/dispatch-css.spec.ts +55 -0
  146. package/src/styles/dispatch.css +9 -0
@@ -0,0 +1,93 @@
1
+ import { type WorkspaceAppSummary } from "./app-creation-store.js";
2
+ export interface UsageMetricBucket {
3
+ key: string;
4
+ label: string;
5
+ costCents: number;
6
+ calls: number;
7
+ chatCalls: number;
8
+ inputTokens: number;
9
+ outputTokens: number;
10
+ cacheReadTokens: number;
11
+ cacheWriteTokens: number;
12
+ activeUsers: number;
13
+ lastActiveAt: number | null;
14
+ }
15
+ export interface UserUsageMetric extends UsageMetricBucket {
16
+ ownerEmail: string;
17
+ chatThreads: number;
18
+ chatMessages: number;
19
+ lastChatAt: number | null;
20
+ topApp: string | null;
21
+ role: string | null;
22
+ }
23
+ export interface AppAccessMetric {
24
+ id: string;
25
+ name: string;
26
+ path: string;
27
+ status: WorkspaceAppSummary["status"];
28
+ isDispatch: boolean;
29
+ accessModel: "workspace" | "solo";
30
+ accessLabel: string;
31
+ accessUsers: number;
32
+ usersWithUsage: number;
33
+ usageCalls: number;
34
+ chatCalls: number;
35
+ costCents: number;
36
+ lastActiveAt: number | null;
37
+ }
38
+ export interface DailyUsageMetric {
39
+ date: string;
40
+ costCents: number;
41
+ calls: number;
42
+ chatCalls: number;
43
+ activeUsers: number;
44
+ }
45
+ export interface RecentUsageMetric {
46
+ id: number;
47
+ createdAt: number;
48
+ ownerEmail: string;
49
+ app: string;
50
+ label: string;
51
+ model: string;
52
+ inputTokens: number;
53
+ outputTokens: number;
54
+ cacheReadTokens: number;
55
+ cacheWriteTokens: number;
56
+ costCents: number;
57
+ }
58
+ export interface DispatchUsageMetrics {
59
+ sinceMs: number;
60
+ sinceDays: number;
61
+ generatedAt: number;
62
+ access: {
63
+ viewerEmail: string;
64
+ orgId: string | null;
65
+ role: string | null;
66
+ scope: "organization" | "solo";
67
+ totalUsers: number;
68
+ };
69
+ totals: {
70
+ costCents: number;
71
+ calls: number;
72
+ chatCalls: number;
73
+ inputTokens: number;
74
+ outputTokens: number;
75
+ cacheReadTokens: number;
76
+ cacheWriteTokens: number;
77
+ activeUsers: number;
78
+ chatThreads: number;
79
+ chatMessages: number;
80
+ workspaceApps: number;
81
+ };
82
+ byApp: UsageMetricBucket[];
83
+ byUser: UserUsageMetric[];
84
+ byLabel: UsageMetricBucket[];
85
+ byModel: UsageMetricBucket[];
86
+ daily: DailyUsageMetric[];
87
+ appAccess: AppAccessMetric[];
88
+ recent: RecentUsageMetric[];
89
+ }
90
+ export declare function listDispatchUsageMetrics(input: {
91
+ sinceDays?: number;
92
+ }): Promise<DispatchUsageMetrics>;
93
+ //# sourceMappingURL=usage-metrics-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-metrics-store.d.ts","sourceRoot":"","sources":["../../../src/server/lib/usage-metrics-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,386 @@
1
+ import { getUsageSummary } from "@agent-native/core/usage";
2
+ import { getDbExec } from "@agent-native/core/db";
3
+ import { currentOrgId, currentOwnerEmail } from "./dispatch-store.js";
4
+ import { listWorkspaceApps, } from "./app-creation-store.js";
5
+ const DAY_MS = 86_400_000;
6
+ function numberField(row, key) {
7
+ return Number(row[key] ?? 0) || 0;
8
+ }
9
+ function stringField(row, key) {
10
+ return String(row[key] ?? "");
11
+ }
12
+ function nullableNumberField(row, key) {
13
+ const value = row[key];
14
+ if (value == null)
15
+ return null;
16
+ const numberValue = Number(value);
17
+ return Number.isFinite(numberValue) ? numberValue : null;
18
+ }
19
+ function labelForKey(value) {
20
+ const trimmed = value.trim();
21
+ return trimmed || "Unattributed";
22
+ }
23
+ function normalizeAppKey(value) {
24
+ const raw = (value ?? "").trim().toLowerCase();
25
+ if (!raw)
26
+ return "unattributed";
27
+ return raw.replace(/^agent-native-/, "");
28
+ }
29
+ function envEmails(name) {
30
+ return (process.env[name] ?? "")
31
+ .split(",")
32
+ .map((value) => value.trim().toLowerCase())
33
+ .filter(Boolean);
34
+ }
35
+ function isEnvAdmin(email) {
36
+ const normalized = email.trim().toLowerCase();
37
+ return [
38
+ ...envEmails("DISPATCH_ADMIN_EMAILS"),
39
+ ...envEmails("WORKSPACE_OWNER_EMAIL"),
40
+ ...envEmails("DISPATCH_DEFAULT_OWNER_EMAIL"),
41
+ ].includes(normalized);
42
+ }
43
+ async function queryRows(sql, args = []) {
44
+ try {
45
+ const result = await getDbExec().execute({ sql, args });
46
+ return result.rows;
47
+ }
48
+ catch {
49
+ return [];
50
+ }
51
+ }
52
+ async function getViewerOrgRole(orgId, email) {
53
+ if (!orgId)
54
+ return null;
55
+ const rows = await queryRows(`SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`, [orgId, email.toLowerCase()]);
56
+ const role = rows[0]?.role;
57
+ return typeof role === "string" ? role : null;
58
+ }
59
+ async function listOrgMembers(orgId) {
60
+ if (!orgId)
61
+ return [];
62
+ const rows = await queryRows(`SELECT email, role, joined_at AS joined_at FROM org_members WHERE org_id = ? ORDER BY joined_at ASC`, [orgId]);
63
+ return rows
64
+ .map((row) => ({
65
+ email: stringField(row, "email").trim(),
66
+ role: stringField(row, "role") || null,
67
+ joinedAt: nullableNumberField(row, "joined_at"),
68
+ }))
69
+ .filter((member) => member.email);
70
+ }
71
+ async function listSignedInUsers() {
72
+ const authRows = await queryRows(`SELECT email, created_at AS joined_at FROM "user" ORDER BY created_at ASC`);
73
+ if (authRows.length > 0) {
74
+ return authRows
75
+ .map((row) => ({
76
+ email: stringField(row, "email").trim(),
77
+ role: null,
78
+ joinedAt: nullableNumberField(row, "joined_at"),
79
+ }))
80
+ .filter((member) => member.email);
81
+ }
82
+ const usageRows = await queryRows(`SELECT DISTINCT owner_email AS email FROM token_usage`);
83
+ const threadRows = await queryRows(`SELECT DISTINCT owner_email AS email FROM chat_threads`);
84
+ const emails = new Set();
85
+ for (const row of [...usageRows, ...threadRows]) {
86
+ if (row.email)
87
+ emails.add(row.email);
88
+ }
89
+ return [...emails].sort().map((email) => ({
90
+ email,
91
+ role: null,
92
+ joinedAt: null,
93
+ }));
94
+ }
95
+ function usageScope(sinceMs, memberEmails) {
96
+ if (memberEmails.length === 0) {
97
+ return { where: "created_at >= ?", args: [sinceMs] };
98
+ }
99
+ const placeholders = memberEmails.map(() => "?").join(", ");
100
+ return {
101
+ where: `created_at >= ? AND owner_email IN (${placeholders})`,
102
+ args: [sinceMs, ...memberEmails],
103
+ };
104
+ }
105
+ function threadScope(sinceMs, memberEmails) {
106
+ if (memberEmails.length === 0) {
107
+ return { where: "updated_at >= ?", args: [sinceMs] };
108
+ }
109
+ const placeholders = memberEmails.map(() => "?").join(", ");
110
+ return {
111
+ where: `updated_at >= ? AND owner_email IN (${placeholders})`,
112
+ args: [sinceMs, ...memberEmails],
113
+ };
114
+ }
115
+ function bucketFromRow(row) {
116
+ const key = stringField(row, "k");
117
+ return {
118
+ key,
119
+ label: labelForKey(key),
120
+ costCents: numberField(row, "cost_x100") / 100,
121
+ calls: numberField(row, "calls"),
122
+ chatCalls: numberField(row, "chat_calls"),
123
+ inputTokens: numberField(row, "input_tokens"),
124
+ outputTokens: numberField(row, "output_tokens"),
125
+ cacheReadTokens: numberField(row, "cache_read_tokens"),
126
+ cacheWriteTokens: numberField(row, "cache_write_tokens"),
127
+ activeUsers: numberField(row, "active_users"),
128
+ lastActiveAt: nullableNumberField(row, "last_active_at"),
129
+ };
130
+ }
131
+ async function usageBuckets(columnExpression, where, args, limit) {
132
+ const rows = await queryRows(`SELECT ${columnExpression} AS k,
133
+ COALESCE(SUM(cost_cents_x100), 0) AS cost_x100,
134
+ COUNT(*) AS calls,
135
+ SUM(CASE WHEN label = 'chat' THEN 1 ELSE 0 END) AS chat_calls,
136
+ COALESCE(SUM(input_tokens), 0) AS input_tokens,
137
+ COALESCE(SUM(output_tokens), 0) AS output_tokens,
138
+ COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
139
+ COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
140
+ COUNT(DISTINCT owner_email) AS active_users,
141
+ MAX(created_at) AS last_active_at
142
+ FROM token_usage
143
+ WHERE ${where}
144
+ GROUP BY ${columnExpression}
145
+ ORDER BY cost_x100 DESC
146
+ LIMIT ?`, [...args, limit]);
147
+ return rows.map(bucketFromRow);
148
+ }
149
+ async function loadChatStats(where, args) {
150
+ const rows = await queryRows(`SELECT owner_email AS owner_email,
151
+ COUNT(*) AS threads,
152
+ COALESCE(SUM(message_count), 0) AS messages,
153
+ MAX(updated_at) AS last_chat_at
154
+ FROM chat_threads
155
+ WHERE ${where}
156
+ GROUP BY owner_email`, args);
157
+ return new Map(rows.map((row) => [
158
+ stringField(row, "owner_email"),
159
+ {
160
+ threads: numberField(row, "threads"),
161
+ messages: numberField(row, "messages"),
162
+ lastChatAt: nullableNumberField(row, "last_chat_at"),
163
+ },
164
+ ]));
165
+ }
166
+ async function assertCanViewMetrics() {
167
+ const viewerEmail = currentOwnerEmail();
168
+ const orgId = currentOrgId();
169
+ const role = await getViewerOrgRole(orgId, viewerEmail);
170
+ if (isEnvAdmin(viewerEmail) || role === "owner" || role === "admin") {
171
+ return { viewerEmail, orgId, role };
172
+ }
173
+ if (!orgId) {
174
+ return { viewerEmail, orgId, role };
175
+ }
176
+ throw new Error("Only organization owners and admins can view workspace usage metrics.");
177
+ }
178
+ export async function listDispatchUsageMetrics(input) {
179
+ const { viewerEmail, orgId, role } = await assertCanViewMetrics();
180
+ const sinceDays = Math.max(1, Math.min(365, input.sinceDays ?? 30));
181
+ const sinceMs = Date.now() - sinceDays * DAY_MS;
182
+ // Initializes token_usage on fresh deployments before the read-only
183
+ // aggregate queries below. The fake owner avoids changing visible data.
184
+ await getUsageSummary({ ownerEmail: "__dispatch_metrics_init__", sinceMs });
185
+ const rawMembers = orgId
186
+ ? await listOrgMembers(orgId)
187
+ : await listSignedInUsers();
188
+ const members = orgId && rawMembers.length === 0
189
+ ? [{ email: viewerEmail, role, joinedAt: null }]
190
+ : rawMembers;
191
+ const memberEmails = orgId ? members.map((member) => member.email) : [];
192
+ const memberByEmail = new Map(members.map((member) => [member.email.toLowerCase(), member]));
193
+ const usage = usageScope(sinceMs, memberEmails);
194
+ const threads = threadScope(sinceMs, memberEmails);
195
+ const [apps, totalsRows, byApp, byUserBase, byLabel, byModel, chatStats] = await Promise.all([
196
+ listWorkspaceApps({ includeAgentCards: false }),
197
+ queryRows(`SELECT
198
+ COALESCE(SUM(cost_cents_x100), 0) AS cost_x100,
199
+ COUNT(*) AS calls,
200
+ SUM(CASE WHEN label = 'chat' THEN 1 ELSE 0 END) AS chat_calls,
201
+ COALESCE(SUM(input_tokens), 0) AS input_tokens,
202
+ COALESCE(SUM(output_tokens), 0) AS output_tokens,
203
+ COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
204
+ COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
205
+ COUNT(DISTINCT owner_email) AS active_users
206
+ FROM token_usage
207
+ WHERE ${usage.where}`, usage.args),
208
+ usageBuckets(`COALESCE(NULLIF(app, ''), 'unattributed')`, usage.where, usage.args, 20),
209
+ usageBuckets("owner_email", usage.where, usage.args, 50),
210
+ usageBuckets(`COALESCE(NULLIF(label, ''), 'chat')`, usage.where, usage.args, 20),
211
+ usageBuckets(`COALESCE(NULLIF(model, ''), 'unknown')`, usage.where, usage.args, 20),
212
+ loadChatStats(threads.where, threads.args),
213
+ ]);
214
+ const topAppRows = await queryRows(`SELECT owner_email AS owner_email,
215
+ COALESCE(NULLIF(app, ''), 'unattributed') AS app,
216
+ COALESCE(SUM(cost_cents_x100), 0) AS cost_x100
217
+ FROM token_usage
218
+ WHERE ${usage.where}
219
+ GROUP BY owner_email, COALESCE(NULLIF(app, ''), 'unattributed')
220
+ ORDER BY owner_email ASC, cost_x100 DESC`, usage.args);
221
+ const topAppByUser = new Map();
222
+ for (const row of topAppRows) {
223
+ const email = stringField(row, "owner_email");
224
+ if (!topAppByUser.has(email)) {
225
+ topAppByUser.set(email, stringField(row, "app"));
226
+ }
227
+ }
228
+ const byUserMap = new Map();
229
+ for (const bucket of byUserBase) {
230
+ const ownerEmail = bucket.key;
231
+ const stats = chatStats.get(ownerEmail) ?? {
232
+ threads: 0,
233
+ messages: 0,
234
+ lastChatAt: null,
235
+ };
236
+ const member = memberByEmail.get(ownerEmail.toLowerCase());
237
+ byUserMap.set(ownerEmail, {
238
+ ...bucket,
239
+ ownerEmail,
240
+ chatThreads: stats.threads,
241
+ chatMessages: stats.messages,
242
+ lastChatAt: stats.lastChatAt,
243
+ topApp: topAppByUser.get(ownerEmail) ?? null,
244
+ role: member?.role ?? null,
245
+ });
246
+ }
247
+ for (const [ownerEmail, stats] of chatStats) {
248
+ if (byUserMap.has(ownerEmail))
249
+ continue;
250
+ const member = memberByEmail.get(ownerEmail.toLowerCase());
251
+ byUserMap.set(ownerEmail, {
252
+ key: ownerEmail,
253
+ label: ownerEmail,
254
+ ownerEmail,
255
+ costCents: 0,
256
+ calls: 0,
257
+ chatCalls: 0,
258
+ inputTokens: 0,
259
+ outputTokens: 0,
260
+ cacheReadTokens: 0,
261
+ cacheWriteTokens: 0,
262
+ activeUsers: 1,
263
+ lastActiveAt: stats.lastChatAt,
264
+ chatThreads: stats.threads,
265
+ chatMessages: stats.messages,
266
+ lastChatAt: stats.lastChatAt,
267
+ topApp: null,
268
+ role: member?.role ?? null,
269
+ });
270
+ }
271
+ const dayRows = await queryRows(`SELECT created_at, owner_email, label, cost_cents_x100
272
+ FROM token_usage
273
+ WHERE ${usage.where}
274
+ ORDER BY created_at ASC`, usage.args);
275
+ const dailyMap = new Map();
276
+ for (const row of dayRows) {
277
+ const date = new Date(numberField(row, "created_at"))
278
+ .toISOString()
279
+ .slice(0, 10);
280
+ const current = dailyMap.get(date) ?? {
281
+ costX100: 0,
282
+ calls: 0,
283
+ chatCalls: 0,
284
+ users: new Set(),
285
+ };
286
+ current.costX100 += numberField(row, "cost_cents_x100");
287
+ current.calls += 1;
288
+ if (stringField(row, "label") === "chat")
289
+ current.chatCalls += 1;
290
+ current.users.add(stringField(row, "owner_email"));
291
+ dailyMap.set(date, current);
292
+ }
293
+ const daily = [...dailyMap.entries()]
294
+ .map(([date, value]) => ({
295
+ date,
296
+ costCents: value.costX100 / 100,
297
+ calls: value.calls,
298
+ chatCalls: value.chatCalls,
299
+ activeUsers: value.users.size,
300
+ }))
301
+ .sort((a, b) => a.date.localeCompare(b.date));
302
+ const recentRows = await queryRows(`SELECT id, created_at, owner_email, app, label, model,
303
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
304
+ cost_cents_x100
305
+ FROM token_usage
306
+ WHERE ${usage.where}
307
+ ORDER BY created_at DESC
308
+ LIMIT 50`, usage.args);
309
+ const recent = recentRows.map((row) => ({
310
+ id: numberField(row, "id"),
311
+ createdAt: numberField(row, "created_at"),
312
+ ownerEmail: stringField(row, "owner_email"),
313
+ app: stringField(row, "app") || "unattributed",
314
+ label: stringField(row, "label") || "chat",
315
+ model: stringField(row, "model") || "unknown",
316
+ inputTokens: numberField(row, "input_tokens"),
317
+ outputTokens: numberField(row, "output_tokens"),
318
+ cacheReadTokens: numberField(row, "cache_read_tokens"),
319
+ cacheWriteTokens: numberField(row, "cache_write_tokens"),
320
+ costCents: numberField(row, "cost_cents_x100") / 100,
321
+ }));
322
+ const appUsageByKey = new Map(byApp.map((bucket) => [normalizeAppKey(bucket.key), bucket]));
323
+ const accessUsers = members.length || byUserMap.size;
324
+ const accessModel = orgId ? "workspace" : "solo";
325
+ const accessLabel = orgId ? "Workspace members" : "Signed-in users";
326
+ const appAccess = apps.map((app) => {
327
+ const usageBucket = appUsageByKey.get(normalizeAppKey(app.id));
328
+ return {
329
+ id: app.id,
330
+ name: app.name,
331
+ path: app.path,
332
+ status: app.status,
333
+ isDispatch: app.isDispatch,
334
+ accessModel,
335
+ accessLabel,
336
+ accessUsers,
337
+ usersWithUsage: usageBucket?.activeUsers ?? 0,
338
+ usageCalls: usageBucket?.calls ?? 0,
339
+ chatCalls: usageBucket?.chatCalls ?? 0,
340
+ costCents: usageBucket?.costCents ?? 0,
341
+ lastActiveAt: usageBucket?.lastActiveAt ?? null,
342
+ };
343
+ });
344
+ const totals = totalsRows[0] ?? {};
345
+ const chatThreadTotals = [...chatStats.values()].reduce((acc, value) => ({
346
+ threads: acc.threads + value.threads,
347
+ messages: acc.messages + value.messages,
348
+ }), { threads: 0, messages: 0 });
349
+ return {
350
+ sinceMs,
351
+ sinceDays,
352
+ generatedAt: Date.now(),
353
+ access: {
354
+ viewerEmail,
355
+ orgId,
356
+ role,
357
+ scope: orgId ? "organization" : "solo",
358
+ totalUsers: accessUsers,
359
+ },
360
+ totals: {
361
+ costCents: numberField(totals, "cost_x100") / 100,
362
+ calls: numberField(totals, "calls"),
363
+ chatCalls: numberField(totals, "chat_calls"),
364
+ inputTokens: numberField(totals, "input_tokens"),
365
+ outputTokens: numberField(totals, "output_tokens"),
366
+ cacheReadTokens: numberField(totals, "cache_read_tokens"),
367
+ cacheWriteTokens: numberField(totals, "cache_write_tokens"),
368
+ activeUsers: numberField(totals, "active_users"),
369
+ chatThreads: chatThreadTotals.threads,
370
+ chatMessages: chatThreadTotals.messages,
371
+ workspaceApps: apps.filter((app) => !app.isDispatch).length,
372
+ },
373
+ byApp,
374
+ byUser: [...byUserMap.values()].sort((a, b) => {
375
+ if (b.costCents !== a.costCents)
376
+ return b.costCents - a.costCents;
377
+ return (b.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0);
378
+ }),
379
+ byLabel,
380
+ byModel,
381
+ daily,
382
+ appAccess,
383
+ recent,
384
+ };
385
+ }
386
+ //# sourceMappingURL=usage-metrics-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-metrics-store.js","sourceRoot":"","sources":["../../../src/server/lib/usage-metrics-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { getUsageSummary } from \"@agent-native/core/usage\";\nimport { getDbExec } from \"@agent-native/core/db\";\nimport { currentOrgId, currentOwnerEmail } from \"./dispatch-store.js\";\nimport {\n listWorkspaceApps,\n type WorkspaceAppSummary,\n} from \"./app-creation-store.js\";\n\nconst DAY_MS = 86_400_000;\n\nexport interface UsageMetricBucket {\n key: string;\n label: string;\n costCents: number;\n calls: number;\n chatCalls: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n activeUsers: number;\n lastActiveAt: number | null;\n}\n\nexport interface UserUsageMetric extends UsageMetricBucket {\n ownerEmail: string;\n chatThreads: number;\n chatMessages: number;\n lastChatAt: number | null;\n topApp: string | null;\n role: string | null;\n}\n\nexport interface AppAccessMetric {\n id: string;\n name: string;\n path: string;\n status: WorkspaceAppSummary[\"status\"];\n isDispatch: boolean;\n accessModel: \"workspace\" | \"solo\";\n accessLabel: string;\n accessUsers: number;\n usersWithUsage: number;\n usageCalls: number;\n chatCalls: number;\n costCents: number;\n lastActiveAt: number | null;\n}\n\nexport interface DailyUsageMetric {\n date: string;\n costCents: number;\n calls: number;\n chatCalls: number;\n activeUsers: number;\n}\n\nexport interface RecentUsageMetric {\n id: number;\n createdAt: number;\n ownerEmail: string;\n app: string;\n label: string;\n model: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n costCents: number;\n}\n\nexport interface DispatchUsageMetrics {\n sinceMs: number;\n sinceDays: number;\n generatedAt: number;\n access: {\n viewerEmail: string;\n orgId: string | null;\n role: string | null;\n scope: \"organization\" | \"solo\";\n totalUsers: number;\n };\n totals: {\n costCents: number;\n calls: number;\n chatCalls: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n activeUsers: number;\n chatThreads: number;\n chatMessages: number;\n workspaceApps: number;\n };\n byApp: UsageMetricBucket[];\n byUser: UserUsageMetric[];\n byLabel: UsageMetricBucket[];\n byModel: UsageMetricBucket[];\n daily: DailyUsageMetric[];\n appAccess: AppAccessMetric[];\n recent: RecentUsageMetric[];\n}\n\ninterface MemberRecord {\n email: string;\n role: string | null;\n joinedAt: number | null;\n}\n\ninterface ChatStats {\n threads: number;\n messages: number;\n lastChatAt: number | null;\n}\n\nfunction numberField(row: Record<string, unknown>, key: string): number {\n return Number(row[key] ?? 0) || 0;\n}\n\nfunction stringField(row: Record<string, unknown>, key: string): string {\n return String(row[key] ?? \"\");\n}\n\nfunction nullableNumberField(\n row: Record<string, unknown>,\n key: string,\n): number | null {\n const value = row[key];\n if (value == null) return null;\n const numberValue = Number(value);\n return Number.isFinite(numberValue) ? numberValue : null;\n}\n\nfunction labelForKey(value: string): string {\n const trimmed = value.trim();\n return trimmed || \"Unattributed\";\n}\n\nfunction normalizeAppKey(value: string | null | undefined): string {\n const raw = (value ?? \"\").trim().toLowerCase();\n if (!raw) return \"unattributed\";\n return raw.replace(/^agent-native-/, \"\");\n}\n\nfunction envEmails(name: string): string[] {\n return (process.env[name] ?? \"\")\n .split(\",\")\n .map((value) => value.trim().toLowerCase())\n .filter(Boolean);\n}\n\nfunction isEnvAdmin(email: string): boolean {\n const normalized = email.trim().toLowerCase();\n return [\n ...envEmails(\"DISPATCH_ADMIN_EMAILS\"),\n ...envEmails(\"WORKSPACE_OWNER_EMAIL\"),\n ...envEmails(\"DISPATCH_DEFAULT_OWNER_EMAIL\"),\n ].includes(normalized);\n}\n\nasync function queryRows<T extends Record<string, unknown>>(\n sql: string,\n args: unknown[] = [],\n): Promise<T[]> {\n try {\n const result = await getDbExec().execute({ sql, args });\n return result.rows as T[];\n } catch {\n return [];\n }\n}\n\nasync function getViewerOrgRole(\n orgId: string | null,\n email: string,\n): Promise<string | null> {\n if (!orgId) return null;\n const rows = await queryRows<{ role?: string }>(\n `SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,\n [orgId, email.toLowerCase()],\n );\n const role = rows[0]?.role;\n return typeof role === \"string\" ? role : null;\n}\n\nasync function listOrgMembers(orgId: string | null): Promise<MemberRecord[]> {\n if (!orgId) return [];\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT email, role, joined_at AS joined_at FROM org_members WHERE org_id = ? ORDER BY joined_at ASC`,\n [orgId],\n );\n return rows\n .map((row) => ({\n email: stringField(row, \"email\").trim(),\n role: stringField(row, \"role\") || null,\n joinedAt: nullableNumberField(row, \"joined_at\"),\n }))\n .filter((member) => member.email);\n}\n\nasync function listSignedInUsers(): Promise<MemberRecord[]> {\n const authRows = await queryRows<Record<string, unknown>>(\n `SELECT email, created_at AS joined_at FROM \"user\" ORDER BY created_at ASC`,\n );\n if (authRows.length > 0) {\n return authRows\n .map((row) => ({\n email: stringField(row, \"email\").trim(),\n role: null,\n joinedAt: nullableNumberField(row, \"joined_at\"),\n }))\n .filter((member) => member.email);\n }\n\n const usageRows = await queryRows<{ email?: string }>(\n `SELECT DISTINCT owner_email AS email FROM token_usage`,\n );\n const threadRows = await queryRows<{ email?: string }>(\n `SELECT DISTINCT owner_email AS email FROM chat_threads`,\n );\n const emails = new Set<string>();\n for (const row of [...usageRows, ...threadRows]) {\n if (row.email) emails.add(row.email);\n }\n return [...emails].sort().map((email) => ({\n email,\n role: null,\n joinedAt: null,\n }));\n}\n\nfunction usageScope(\n sinceMs: number,\n memberEmails: string[],\n): { where: string; args: unknown[] } {\n if (memberEmails.length === 0) {\n return { where: \"created_at >= ?\", args: [sinceMs] };\n }\n const placeholders = memberEmails.map(() => \"?\").join(\", \");\n return {\n where: `created_at >= ? AND owner_email IN (${placeholders})`,\n args: [sinceMs, ...memberEmails],\n };\n}\n\nfunction threadScope(\n sinceMs: number,\n memberEmails: string[],\n): { where: string; args: unknown[] } {\n if (memberEmails.length === 0) {\n return { where: \"updated_at >= ?\", args: [sinceMs] };\n }\n const placeholders = memberEmails.map(() => \"?\").join(\", \");\n return {\n where: `updated_at >= ? AND owner_email IN (${placeholders})`,\n args: [sinceMs, ...memberEmails],\n };\n}\n\nfunction bucketFromRow(row: Record<string, unknown>): UsageMetricBucket {\n const key = stringField(row, \"k\");\n return {\n key,\n label: labelForKey(key),\n costCents: numberField(row, \"cost_x100\") / 100,\n calls: numberField(row, \"calls\"),\n chatCalls: numberField(row, \"chat_calls\"),\n inputTokens: numberField(row, \"input_tokens\"),\n outputTokens: numberField(row, \"output_tokens\"),\n cacheReadTokens: numberField(row, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(row, \"cache_write_tokens\"),\n activeUsers: numberField(row, \"active_users\"),\n lastActiveAt: nullableNumberField(row, \"last_active_at\"),\n };\n}\n\nasync function usageBuckets(\n columnExpression: string,\n where: string,\n args: unknown[],\n limit: number,\n): Promise<UsageMetricBucket[]> {\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT ${columnExpression} AS k,\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100,\n COUNT(*) AS calls,\n SUM(CASE WHEN label = 'chat' THEN 1 ELSE 0 END) AS chat_calls,\n COALESCE(SUM(input_tokens), 0) AS input_tokens,\n COALESCE(SUM(output_tokens), 0) AS output_tokens,\n COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,\n COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,\n COUNT(DISTINCT owner_email) AS active_users,\n MAX(created_at) AS last_active_at\n FROM token_usage\n WHERE ${where}\n GROUP BY ${columnExpression}\n ORDER BY cost_x100 DESC\n LIMIT ?`,\n [...args, limit],\n );\n return rows.map(bucketFromRow);\n}\n\nasync function loadChatStats(\n where: string,\n args: unknown[],\n): Promise<Map<string, ChatStats>> {\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT owner_email AS owner_email,\n COUNT(*) AS threads,\n COALESCE(SUM(message_count), 0) AS messages,\n MAX(updated_at) AS last_chat_at\n FROM chat_threads\n WHERE ${where}\n GROUP BY owner_email`,\n args,\n );\n return new Map(\n rows.map((row) => [\n stringField(row, \"owner_email\"),\n {\n threads: numberField(row, \"threads\"),\n messages: numberField(row, \"messages\"),\n lastChatAt: nullableNumberField(row, \"last_chat_at\"),\n },\n ]),\n );\n}\n\nasync function assertCanViewMetrics(): Promise<{\n viewerEmail: string;\n orgId: string | null;\n role: string | null;\n}> {\n const viewerEmail = currentOwnerEmail();\n const orgId = currentOrgId();\n const role = await getViewerOrgRole(orgId, viewerEmail);\n if (isEnvAdmin(viewerEmail) || role === \"owner\" || role === \"admin\") {\n return { viewerEmail, orgId, role };\n }\n if (!orgId) {\n return { viewerEmail, orgId, role };\n }\n throw new Error(\n \"Only organization owners and admins can view workspace usage metrics.\",\n );\n}\n\nexport async function listDispatchUsageMetrics(input: {\n sinceDays?: number;\n}): Promise<DispatchUsageMetrics> {\n const { viewerEmail, orgId, role } = await assertCanViewMetrics();\n const sinceDays = Math.max(1, Math.min(365, input.sinceDays ?? 30));\n const sinceMs = Date.now() - sinceDays * DAY_MS;\n\n // Initializes token_usage on fresh deployments before the read-only\n // aggregate queries below. The fake owner avoids changing visible data.\n await getUsageSummary({ ownerEmail: \"__dispatch_metrics_init__\", sinceMs });\n\n const rawMembers = orgId\n ? await listOrgMembers(orgId)\n : await listSignedInUsers();\n const members =\n orgId && rawMembers.length === 0\n ? [{ email: viewerEmail, role, joinedAt: null }]\n : rawMembers;\n const memberEmails = orgId ? members.map((member) => member.email) : [];\n const memberByEmail = new Map(\n members.map((member) => [member.email.toLowerCase(), member]),\n );\n const usage = usageScope(sinceMs, memberEmails);\n const threads = threadScope(sinceMs, memberEmails);\n\n const [apps, totalsRows, byApp, byUserBase, byLabel, byModel, chatStats] =\n await Promise.all([\n listWorkspaceApps({ includeAgentCards: false }),\n queryRows<Record<string, unknown>>(\n `SELECT\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100,\n COUNT(*) AS calls,\n SUM(CASE WHEN label = 'chat' THEN 1 ELSE 0 END) AS chat_calls,\n COALESCE(SUM(input_tokens), 0) AS input_tokens,\n COALESCE(SUM(output_tokens), 0) AS output_tokens,\n COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,\n COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,\n COUNT(DISTINCT owner_email) AS active_users\n FROM token_usage\n WHERE ${usage.where}`,\n usage.args,\n ),\n usageBuckets(\n `COALESCE(NULLIF(app, ''), 'unattributed')`,\n usage.where,\n usage.args,\n 20,\n ),\n usageBuckets(\"owner_email\", usage.where, usage.args, 50),\n usageBuckets(\n `COALESCE(NULLIF(label, ''), 'chat')`,\n usage.where,\n usage.args,\n 20,\n ),\n usageBuckets(\n `COALESCE(NULLIF(model, ''), 'unknown')`,\n usage.where,\n usage.args,\n 20,\n ),\n loadChatStats(threads.where, threads.args),\n ]);\n\n const topAppRows = await queryRows<Record<string, unknown>>(\n `SELECT owner_email AS owner_email,\n COALESCE(NULLIF(app, ''), 'unattributed') AS app,\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100\n FROM token_usage\n WHERE ${usage.where}\n GROUP BY owner_email, COALESCE(NULLIF(app, ''), 'unattributed')\n ORDER BY owner_email ASC, cost_x100 DESC`,\n usage.args,\n );\n const topAppByUser = new Map<string, string>();\n for (const row of topAppRows) {\n const email = stringField(row, \"owner_email\");\n if (!topAppByUser.has(email)) {\n topAppByUser.set(email, stringField(row, \"app\"));\n }\n }\n\n const byUserMap = new Map<string, UserUsageMetric>();\n for (const bucket of byUserBase) {\n const ownerEmail = bucket.key;\n const stats = chatStats.get(ownerEmail) ?? {\n threads: 0,\n messages: 0,\n lastChatAt: null,\n };\n const member = memberByEmail.get(ownerEmail.toLowerCase());\n byUserMap.set(ownerEmail, {\n ...bucket,\n ownerEmail,\n chatThreads: stats.threads,\n chatMessages: stats.messages,\n lastChatAt: stats.lastChatAt,\n topApp: topAppByUser.get(ownerEmail) ?? null,\n role: member?.role ?? null,\n });\n }\n for (const [ownerEmail, stats] of chatStats) {\n if (byUserMap.has(ownerEmail)) continue;\n const member = memberByEmail.get(ownerEmail.toLowerCase());\n byUserMap.set(ownerEmail, {\n key: ownerEmail,\n label: ownerEmail,\n ownerEmail,\n costCents: 0,\n calls: 0,\n chatCalls: 0,\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n activeUsers: 1,\n lastActiveAt: stats.lastChatAt,\n chatThreads: stats.threads,\n chatMessages: stats.messages,\n lastChatAt: stats.lastChatAt,\n topApp: null,\n role: member?.role ?? null,\n });\n }\n\n const dayRows = await queryRows<Record<string, unknown>>(\n `SELECT created_at, owner_email, label, cost_cents_x100\n FROM token_usage\n WHERE ${usage.where}\n ORDER BY created_at ASC`,\n usage.args,\n );\n const dailyMap = new Map<\n string,\n { costX100: number; calls: number; chatCalls: number; users: Set<string> }\n >();\n for (const row of dayRows) {\n const date = new Date(numberField(row, \"created_at\"))\n .toISOString()\n .slice(0, 10);\n const current = dailyMap.get(date) ?? {\n costX100: 0,\n calls: 0,\n chatCalls: 0,\n users: new Set<string>(),\n };\n current.costX100 += numberField(row, \"cost_cents_x100\");\n current.calls += 1;\n if (stringField(row, \"label\") === \"chat\") current.chatCalls += 1;\n current.users.add(stringField(row, \"owner_email\"));\n dailyMap.set(date, current);\n }\n const daily = [...dailyMap.entries()]\n .map(([date, value]) => ({\n date,\n costCents: value.costX100 / 100,\n calls: value.calls,\n chatCalls: value.chatCalls,\n activeUsers: value.users.size,\n }))\n .sort((a, b) => a.date.localeCompare(b.date));\n\n const recentRows = await queryRows<Record<string, unknown>>(\n `SELECT id, created_at, owner_email, app, label, model,\n input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,\n cost_cents_x100\n FROM token_usage\n WHERE ${usage.where}\n ORDER BY created_at DESC\n LIMIT 50`,\n usage.args,\n );\n const recent = recentRows.map((row) => ({\n id: numberField(row, \"id\"),\n createdAt: numberField(row, \"created_at\"),\n ownerEmail: stringField(row, \"owner_email\"),\n app: stringField(row, \"app\") || \"unattributed\",\n label: stringField(row, \"label\") || \"chat\",\n model: stringField(row, \"model\") || \"unknown\",\n inputTokens: numberField(row, \"input_tokens\"),\n outputTokens: numberField(row, \"output_tokens\"),\n cacheReadTokens: numberField(row, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(row, \"cache_write_tokens\"),\n costCents: numberField(row, \"cost_cents_x100\") / 100,\n }));\n\n const appUsageByKey = new Map(\n byApp.map((bucket) => [normalizeAppKey(bucket.key), bucket]),\n );\n const accessUsers = members.length || byUserMap.size;\n const accessModel = orgId ? \"workspace\" : \"solo\";\n const accessLabel = orgId ? \"Workspace members\" : \"Signed-in users\";\n const appAccess = apps.map((app) => {\n const usageBucket = appUsageByKey.get(normalizeAppKey(app.id));\n return {\n id: app.id,\n name: app.name,\n path: app.path,\n status: app.status,\n isDispatch: app.isDispatch,\n accessModel,\n accessLabel,\n accessUsers,\n usersWithUsage: usageBucket?.activeUsers ?? 0,\n usageCalls: usageBucket?.calls ?? 0,\n chatCalls: usageBucket?.chatCalls ?? 0,\n costCents: usageBucket?.costCents ?? 0,\n lastActiveAt: usageBucket?.lastActiveAt ?? null,\n } satisfies AppAccessMetric;\n });\n\n const totals = totalsRows[0] ?? {};\n const chatThreadTotals = [...chatStats.values()].reduce(\n (acc, value) => ({\n threads: acc.threads + value.threads,\n messages: acc.messages + value.messages,\n }),\n { threads: 0, messages: 0 },\n );\n\n return {\n sinceMs,\n sinceDays,\n generatedAt: Date.now(),\n access: {\n viewerEmail,\n orgId,\n role,\n scope: orgId ? \"organization\" : \"solo\",\n totalUsers: accessUsers,\n },\n totals: {\n costCents: numberField(totals, \"cost_x100\") / 100,\n calls: numberField(totals, \"calls\"),\n chatCalls: numberField(totals, \"chat_calls\"),\n inputTokens: numberField(totals, \"input_tokens\"),\n outputTokens: numberField(totals, \"output_tokens\"),\n cacheReadTokens: numberField(totals, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(totals, \"cache_write_tokens\"),\n activeUsers: numberField(totals, \"active_users\"),\n chatThreads: chatThreadTotals.threads,\n chatMessages: chatThreadTotals.messages,\n workspaceApps: apps.filter((app) => !app.isDispatch).length,\n },\n byApp,\n byUser: [...byUserMap.values()].sort((a, b) => {\n if (b.costCents !== a.costCents) return b.costCents - a.costCents;\n return (b.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0);\n }),\n byLabel,\n byModel,\n daily,\n appAccess,\n recent,\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.1.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
6
6
  "license": "MIT",
@@ -13,6 +13,10 @@
13
13
  "url": "https://github.com/BuilderIO/agent-native/issues"
14
14
  },
15
15
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "provenance": true
19
+ },
16
20
  "exports": {
17
21
  ".": "./dist/index.js",
18
22
  "./routes": "./dist/routes/index.js",
@@ -20,14 +24,15 @@
20
24
  "./server": "./dist/server/index.js",
21
25
  "./actions": "./dist/actions/index.js",
22
26
  "./db": "./dist/db/index.js",
23
- "./components": "./dist/components/index.js"
27
+ "./components": "./dist/components/index.js",
28
+ "./styles/dispatch.css": "./src/styles/dispatch.css"
24
29
  },
25
30
  "files": [
26
31
  "dist",
27
32
  "src"
28
33
  ],
29
34
  "peerDependencies": {
30
- "@agent-native/core": ">=0.7.82",
35
+ "@agent-native/core": ">=0.8.0",
31
36
  "react": ">=18",
32
37
  "react-dom": ">=18",
33
38
  "react-router": ">=7"
@@ -92,11 +97,11 @@
92
97
  "typescript": "^6.0.3",
93
98
  "vite": "8.0.3",
94
99
  "vitest": "^4.1.5",
95
- "@agent-native/core": "0.7.83"
100
+ "@agent-native/core": "0.9.1"
96
101
  },
97
102
  "scripts": {
98
- "build": "tsc && tsc-alias",
99
- "dev": "tsc --watch & tsc-alias --watch",
103
+ "build": "tsc && tsc-alias --resolve-full-paths",
104
+ "dev": "tsc --watch & tsc-alias --watch --resolve-full-paths",
100
105
  "typecheck": "tsc --noEmit",
101
106
  "test": "vitest --run src --passWithNoTests"
102
107
  }
@@ -19,6 +19,7 @@ import listDestinations from "./list-destinations.js";
19
19
  import listDispatchApprovals from "./list-dispatch-approvals.js";
20
20
  import listDispatchAudit from "./list-dispatch-audit.js";
21
21
  import listDispatchOverview from "./list-dispatch-overview.js";
22
+ import listDispatchUsageMetrics from "./list-dispatch-usage-metrics.js";
22
23
  import listIntegrationsCatalog from "./list-integrations-catalog.js";
23
24
  import listLinkedIdentities from "./list-linked-identities.js";
24
25
  import listVaultAudit from "./list-vault-audit.js";
@@ -73,6 +74,7 @@ export const dispatchActions: Record<string, ActionEntry> = {
73
74
  "list-dispatch-approvals": listDispatchApprovals,
74
75
  "list-dispatch-audit": listDispatchAudit,
75
76
  "list-dispatch-overview": listDispatchOverview,
77
+ "list-dispatch-usage-metrics": listDispatchUsageMetrics,
76
78
  "list-integrations-catalog": listIntegrationsCatalog,
77
79
  "list-linked-identities": listLinkedIdentities,
78
80
  "list-vault-audit": listVaultAudit,
@@ -0,0 +1,19 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { z } from "zod";
3
+ import { listDispatchUsageMetrics } from "../server/lib/usage-metrics-store.js";
4
+
5
+ export default defineAction({
6
+ description:
7
+ "Get workspace-level LLM usage, estimated cost, users, app access, and recent activity metrics for Dispatch admins.",
8
+ schema: z.object({
9
+ sinceDays: z.coerce
10
+ .number()
11
+ .int()
12
+ .min(1)
13
+ .max(365)
14
+ .default(30)
15
+ .describe("Lookback window in days. Defaults to 30."),
16
+ }),
17
+ http: { method: "GET" },
18
+ run: async (args) => listDispatchUsageMetrics(args),
19
+ });