@agent-native/core 0.70.3 → 0.72.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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +51 -0
- package/corpus/core/docs/content/actions.md +23 -0
- package/corpus/core/docs/content/audit-log.md +111 -0
- package/corpus/core/docs/content/multi-app-workspace.md +2 -2
- package/corpus/core/docs/design/durable-agent-runs.md +458 -4
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/action.ts +80 -1
- package/corpus/core/src/agent/durable-background.ts +192 -0
- package/corpus/core/src/agent/production-agent.ts +357 -13
- package/corpus/core/src/agent/run-manager.ts +66 -3
- package/corpus/core/src/agent/run-store.ts +143 -23
- package/corpus/core/src/agent/types.ts +20 -0
- package/corpus/core/src/application-state/store.ts +5 -0
- package/corpus/core/src/audit/actions/get-audit-event.ts +23 -0
- package/corpus/core/src/audit/actions/list-audit-events.ts +65 -0
- package/corpus/core/src/audit/cleanup-job.ts +100 -0
- package/corpus/core/src/audit/config.ts +91 -0
- package/corpus/core/src/audit/index.ts +43 -0
- package/corpus/core/src/audit/record.ts +143 -0
- package/corpus/core/src/audit/redact.ts +109 -0
- package/corpus/core/src/audit/store.ts +244 -0
- package/corpus/core/src/audit/types.ts +125 -0
- package/corpus/core/src/chat-threads/store.ts +10 -0
- package/corpus/core/src/client/AgentPanel.tsx +12 -9
- package/corpus/core/src/client/AssistantChat.tsx +6 -0
- package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
- package/corpus/core/src/client/chat/repo-helpers.ts +38 -0
- package/corpus/core/src/db/client.ts +4 -0
- package/corpus/core/src/db/widen-columns.ts +75 -0
- package/corpus/core/src/deploy/build.ts +96 -0
- package/corpus/core/src/deploy/workspace-deploy.ts +121 -0
- package/corpus/core/src/mcp/build-server.ts +23 -13
- package/corpus/core/src/oauth-tokens/store.ts +6 -0
- package/corpus/core/src/provider-api/custom-registry.ts +8 -0
- package/corpus/core/src/resources/store.ts +10 -0
- package/corpus/core/src/scripts/runner.ts +6 -3
- package/corpus/core/src/server/action-discovery.ts +6 -0
- package/corpus/core/src/server/action-routes.ts +1 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +133 -66
- package/corpus/core/src/server/auth.ts +5 -0
- package/corpus/core/src/server/core-routes-plugin.ts +14 -0
- package/corpus/core/src/server/security-headers.ts +9 -6
- package/corpus/core/src/settings/store.ts +6 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
- package/corpus/core/src/usage/store.ts +6 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
- package/corpus/templates/analytics/AGENTS.md +8 -0
- package/corpus/templates/analytics/actions/compose-dashboard.ts +317 -0
- package/corpus/templates/analytics/changelog/2026-06-23-build-large-first-party-analytics-dashboards-in-one-fast-cal.md +6 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +574 -0
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +60 -6
- package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +6 -0
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +20 -43
- package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +5 -1
- package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +3 -51
- package/corpus/templates/clips/changelog/2026-06-23-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-public-clips-now-play-inline-in-slack-connect-a-workspac.md +6 -0
- package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +72 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -10
- package/corpus/templates/clips/chrome-extension/src/background.ts +163 -32
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +301 -24
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +245 -67
- package/corpus/templates/clips/chrome-extension/src/overlay.css +77 -43
- package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +102 -38
- package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
- package/corpus/templates/clips/chrome-extension/src/popup.ts +275 -4
- package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +10 -17
- package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +73 -23
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +0 -6
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +56 -46
- package/corpus/templates/clips/desktop/src/styles.css +33 -5
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +113 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
- package/corpus/templates/clips/server/lib/media-permissions.ts +5 -1
- package/corpus/templates/clips/shared/recording-audio.ts +62 -0
- package/corpus/templates/clips/shared/recording-core.ts +94 -0
- package/dist/action.d.ts +31 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +45 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/context-xray/schema.d.ts +1 -1
- package/dist/agent/durable-background.d.ts +60 -0
- package/dist/agent/durable-background.d.ts.map +1 -0
- package/dist/agent/durable-background.js +144 -0
- package/dist/agent/durable-background.js.map +1 -0
- package/dist/agent/observational-memory/schema.d.ts +1 -1
- package/dist/agent/production-agent.d.ts +20 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +320 -14
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +48 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +45 -3
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +30 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +138 -24
- package/dist/agent/run-store.js.map +1 -1
- package/dist/agent/types.d.ts +20 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +5 -0
- package/dist/application-state/store.js.map +1 -1
- package/dist/audit/actions/get-audit-event.d.ts +11 -0
- package/dist/audit/actions/get-audit-event.d.ts.map +1 -0
- package/dist/audit/actions/get-audit-event.js +22 -0
- package/dist/audit/actions/get-audit-event.js.map +1 -0
- package/dist/audit/actions/list-audit-events.d.ts +22 -0
- package/dist/audit/actions/list-audit-events.d.ts.map +1 -0
- package/dist/audit/actions/list-audit-events.js +61 -0
- package/dist/audit/actions/list-audit-events.js.map +1 -0
- package/dist/audit/cleanup-job.d.ts +12 -0
- package/dist/audit/cleanup-job.d.ts.map +1 -0
- package/dist/audit/cleanup-job.js +93 -0
- package/dist/audit/cleanup-job.js.map +1 -0
- package/dist/audit/config.d.ts +30 -0
- package/dist/audit/config.d.ts.map +1 -0
- package/dist/audit/config.js +65 -0
- package/dist/audit/config.js.map +1 -0
- package/dist/audit/index.d.ts +13 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +6 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/record.d.ts +24 -0
- package/dist/audit/record.d.ts.map +1 -0
- package/dist/audit/record.js +106 -0
- package/dist/audit/record.js.map +1 -0
- package/dist/audit/redact.d.ts +28 -0
- package/dist/audit/redact.d.ts.map +1 -0
- package/dist/audit/redact.js +109 -0
- package/dist/audit/redact.js.map +1 -0
- package/dist/audit/store.d.ts +14 -0
- package/dist/audit/store.d.ts.map +1 -0
- package/dist/audit/store.js +219 -0
- package/dist/audit/store.js.map +1 -0
- package/dist/audit/types.d.ts +114 -0
- package/dist/audit/types.d.ts.map +1 -0
- package/dist/audit/types.js +15 -0
- package/dist/audit/types.js.map +1 -0
- package/dist/chat-threads/store.d.ts.map +1 -1
- package/dist/chat-threads/store.js +10 -0
- package/dist/chat-threads/store.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -2
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +6 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.js +37 -4
- package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
- package/dist/client/chat/repo-helpers.d.ts +16 -0
- package/dist/client/chat/repo-helpers.d.ts.map +1 -1
- package/dist/client/chat/repo-helpers.js +40 -0
- package/dist/client/chat/repo-helpers.js.map +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +3 -0
- package/dist/db/client.js.map +1 -1
- package/dist/db/widen-columns.d.ts +39 -0
- package/dist/db/widen-columns.d.ts.map +1 -0
- package/dist/db/widen-columns.js +73 -0
- package/dist/db/widen-columns.js.map +1 -0
- package/dist/deploy/build.d.ts +29 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +85 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.d.ts.map +1 -1
- package/dist/deploy/workspace-deploy.js +108 -0
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +24 -9
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/oauth-tokens/store.d.ts.map +1 -1
- package/dist/oauth-tokens/store.js +6 -0
- package/dist/oauth-tokens/store.js.map +1 -1
- package/dist/provider-api/custom-registry.d.ts.map +1 -1
- package/dist/provider-api/custom-registry.js +8 -0
- package/dist/provider-api/custom-registry.js.map +1 -1
- package/dist/resources/store.d.ts.map +1 -1
- package/dist/resources/store.js +9 -0
- package/dist/resources/store.js.map +1 -1
- package/dist/scripts/runner.js +4 -3
- package/dist/scripts/runner.js.map +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +6 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +1 -0
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +68 -13
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +5 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +13 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/security-headers.d.ts +8 -5
- package/dist/server/security-headers.d.ts.map +1 -1
- package/dist/server/security-headers.js +9 -6
- package/dist/server/security-headers.js.map +1 -1
- package/dist/settings/store.d.ts.map +1 -1
- package/dist/settings/store.js +6 -0
- package/dist/settings/store.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
- package/dist/usage/store.d.ts.map +1 -1
- package/dist/usage/store.js +5 -0
- package/dist/usage/store.js.map +1 -1
- package/docs/content/actions.md +23 -0
- package/docs/content/audit-log.md +111 -0
- package/docs/content/multi-app-workspace.md +2 -2
- package/docs/design/durable-agent-runs.md +458 -4
- package/package.json +2 -1
- package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-log
|
|
3
|
+
description: >-
|
|
4
|
+
Durable, access-scoped, append-only record of who changed what app data,
|
|
5
|
+
when, and whether it was the agent or a human. Use when adding an activity
|
|
6
|
+
feed or change history, declaring what a mutating action targets, auditing
|
|
7
|
+
sensitive reads, or answering "what did the agent change / who edited this".
|
|
8
|
+
scope: dev
|
|
9
|
+
metadata:
|
|
10
|
+
internal: true
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Audit Log
|
|
14
|
+
|
|
15
|
+
## Rule
|
|
16
|
+
|
|
17
|
+
Every mutating action automatically records an audit event — **no wiring
|
|
18
|
+
needed**. The framework captures who/what/when/from-where at the `defineAction`
|
|
19
|
+
seam, redacts credentials, and attributes the change to a human or the agent
|
|
20
|
+
(with the agent thread/turn that caused it). You only touch `audit` config to
|
|
21
|
+
make events *more useful* (declare the target) or to opt a read in / a noisy
|
|
22
|
+
write out.
|
|
23
|
+
|
|
24
|
+
This is distinct from:
|
|
25
|
+
|
|
26
|
+
- **observability** — sampled agent-run telemetry (traces, evals), developer-facing.
|
|
27
|
+
- **tracking** — fire-and-forget product analytics to external SaaS.
|
|
28
|
+
|
|
29
|
+
Audit is complete, durable, locally queryable, and scoped to the data it
|
|
30
|
+
describes.
|
|
31
|
+
|
|
32
|
+
## Declare the target so it lands in the owner's trail
|
|
33
|
+
|
|
34
|
+
By default an event is scoped to the **actor** (you see your own changes and the
|
|
35
|
+
agent's changes on your behalf). To make a change to a *shared* resource show up
|
|
36
|
+
in the **owner's** audit trail, declare the target:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
defineAction({
|
|
40
|
+
description: "Delete a recording",
|
|
41
|
+
schema: z.object({ id: z.string() }),
|
|
42
|
+
audit: {
|
|
43
|
+
// type + id label the event; ownerEmail/orgId/visibility scope who can read it.
|
|
44
|
+
target: (args, result, meta) => ({
|
|
45
|
+
type: "recording",
|
|
46
|
+
id: args.id,
|
|
47
|
+
// Optional — defaults to the actor. Set when editing someone else's resource.
|
|
48
|
+
ownerEmail: result?.ownerEmail,
|
|
49
|
+
visibility: "org",
|
|
50
|
+
}),
|
|
51
|
+
summary: (args) => `Deleted recording ${args.id}`,
|
|
52
|
+
},
|
|
53
|
+
run: async (args, ctx) => { /* ... */ },
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`target`, `ownerEmail`, `visibility`, and `summary` are all optional. The
|
|
58
|
+
minimum useful addition is `target: () => ({ type, id })`.
|
|
59
|
+
|
|
60
|
+
## Defaults and how to override them
|
|
61
|
+
|
|
62
|
+
- **Mutations** (anything not GET / `readOnly`) are audited automatically.
|
|
63
|
+
- **Read-only** actions are skipped. Audit a sensitive read (secret access, bulk
|
|
64
|
+
export) with `audit: { onRead: true }`.
|
|
65
|
+
- **High-frequency framework actions** (app-state sync, context-xray, navigate,
|
|
66
|
+
appearance) are skipped by default. Force one on with `audit: { enabled: true }`.
|
|
67
|
+
- **Opt a noisy write out** with `audit: { enabled: false }`.
|
|
68
|
+
- **Skip capturing arguments** (large/sensitive payloads) with
|
|
69
|
+
`audit: { recordInputs: false }`. Inputs are credential-redacted regardless.
|
|
70
|
+
|
|
71
|
+
## Reading the log
|
|
72
|
+
|
|
73
|
+
Two actions are available to the agent and the frontend in every app, scoped in
|
|
74
|
+
SQL to the caller — they never leak another tenant's rows:
|
|
75
|
+
|
|
76
|
+
- `list-audit-events` — filter by `targetType`/`targetId`, `actorKind`
|
|
77
|
+
(`agent` | `human` | `system`), `status`, `threadId`/`turnId`, `action`,
|
|
78
|
+
`sinceMs`, `limit`.
|
|
79
|
+
- `get-audit-event` — one event by id, with its redacted input payload.
|
|
80
|
+
|
|
81
|
+
Call them from the UI with `useActionQuery` to build an activity feed or a
|
|
82
|
+
"who changed this" line — never hand-write a fetch to the audit table.
|
|
83
|
+
|
|
84
|
+
## Never
|
|
85
|
+
|
|
86
|
+
- Don't write a parallel "history" table for a resource — declare an `audit.target`
|
|
87
|
+
and read it back instead.
|
|
88
|
+
- Don't put secrets in `summary` or rely on inputs being safe — redaction covers
|
|
89
|
+
credential-shaped values, but keep summaries free of sensitive data.
|
|
90
|
+
- Don't expose an update/delete path for audit rows. The log is append-only; the
|
|
91
|
+
only deletion is the retention purge (`AGENT_NATIVE_AUDIT_RETENTION_DAYS`,
|
|
92
|
+
default 365 days; `0` = keep forever). Global kill switch:
|
|
93
|
+
`AGENT_NATIVE_AUDIT_ENABLED=false`.
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Cost is stored as "centicents" (1/100th of a cent) for integer precision.
|
|
9
9
|
*/
|
|
10
10
|
import { getDbExec, intType, isPostgres } from "../db/client.js";
|
|
11
|
+
import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Per-million-token pricing in cents. Cache read is typically ~10% of
|
|
@@ -233,6 +234,11 @@ async function ensureUsageTable(): Promise<void> {
|
|
|
233
234
|
}
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
// Older deployments created `created_at` as 32-bit `INTEGER`; on Postgres
|
|
238
|
+
// the `Date.now()` written per run by recordUsage() overflows int4. Widen
|
|
239
|
+
// it in place (no-op once done / on fresh BIGINT databases).
|
|
240
|
+
await widenIntColumnsToBigInt("token_usage", ["created_at"]);
|
|
241
|
+
|
|
236
242
|
try {
|
|
237
243
|
await client.execute(
|
|
238
244
|
`CREATE INDEX IF NOT EXISTS idx_token_usage_owner_created ON token_usage (owner_email, created_at)`,
|
|
@@ -174,12 +174,31 @@ pnpm action update-dashboard --dashboardId weekly-metrics --config '<full json>'
|
|
|
174
174
|
|
|
175
175
|
After a mutation, navigate to the dashboard if the user is elsewhere. The app syncs through the framework's polling/query invalidation path.
|
|
176
176
|
|
|
177
|
+
## Building Large First-Party Dashboards (compose-dashboard)
|
|
178
|
+
|
|
179
|
+
For a **first-party analytics** dashboard, prefer `compose-dashboard` over hand-authoring a big `update-dashboard` config. You name the metrics; the SERVER expands each into a full, validated panel (SQL + chart config) from the shipped metric catalog and saves them in ONE atomic call. This avoids the failure mode where the agent must stream a giant multi-panel `update-dashboard` argument inside the ~40s budget — that big tool-call can't be resumed mid-stream and is all-or-nothing on validation, so the agent thrashes (repeated update-dashboard + tool-search, never landing).
|
|
180
|
+
|
|
181
|
+
- **Never hand-author large first-party configs panel-by-panel.** Call `compose-dashboard` with the metric keys instead.
|
|
182
|
+
- Unknown metric keys are skipped and reported in `unknownMetrics` (not fatal). Each panel's SQL is validated independently — valid panels save, invalid ones are reported in `invalidMetrics`.
|
|
183
|
+
- By default (no `overwrite`), composing into an existing dashboard APPENDS the new panels and skips ids already present. `overwrite: true` replaces the whole config.
|
|
184
|
+
- Each metric accepts an optional per-metric `window` of `'30d' | '90d' | 'all'` (only affects windowed virality/time metrics) and `title` / `chartType` / `width` overrides.
|
|
185
|
+
- Returns `{ dashboardId, panelCount, createdMetrics, unknownMetrics, invalidMetrics, skippedExistingIds }` — report `panelCount` as proof-of-done.
|
|
186
|
+
|
|
187
|
+
Available metric keys: `total-signups`, `signups-over-time`, `signups-by-template`, `sessions-by-app`, `sessions-over-time`, `signed-in-vs-anon`, `total-template-clicks`, `total-demo-clicks`, `total-cli-copies`, `template-interest-over-time`, `clicks-by-template`, `demo-clicks-by-template`, `cli-copies-by-template`, `cli-copies-over-time`, `pageviews-over-time`, `referred-signups-30d`, `viral-signup-share-30d`, `clip-share-signups-30d`, `signups-by-referral-source`, `referred-signups-over-time`, `top-referrers`, `share-funnel-30d`, `viral-participation-rate-90d`, `viral-coefficient-90d`, `activated-referrers-90d`.
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Build a large first-party dashboard in ONE call (server generates the panels)
|
|
191
|
+
pnpm action compose-dashboard --dashboardId first-party-overview --title "First-Party Overview" \
|
|
192
|
+
--metrics '["total-signups","signups-over-time","signups-by-template","sessions-by-app","viral-coefficient-90d","top-referrers","share-funnel-30d"]'
|
|
193
|
+
```
|
|
194
|
+
|
|
177
195
|
## Reliable Bulk Edits
|
|
178
196
|
|
|
179
197
|
This is the dashboard-specific application of the framework-wide `reliable-mutations` skill — read that for the general rule (one atomic write, verify end state, report proof-of-done).
|
|
180
198
|
|
|
181
199
|
Hosted agent runs have a **~40s budget**. Many sequential `update-dashboard` calls (one per panel, plus schema-discovery calls) will blow that budget and leave the dashboard in a partial state — earlier inserts looked like they succeeded (✓), but nothing actually persisted. Avoid this:
|
|
182
200
|
|
|
201
|
+
- **For a large first-party dashboard, use `compose-dashboard`** (see the section above): name the metrics, the server generates the panels in one call. Do not hand-author the big config.
|
|
183
202
|
- **Batch ALL changes into ONE `update-dashboard` call.** A single `update-dashboard` is atomic: it applies every op to an in-memory config, validates all panel SQL, then upserts once. Never loop the action.
|
|
184
203
|
- To add N panels, pass N ops in one call: `ops: [{op:"insert", path:"/panels/-", value:<panel>}, … ]` (`/panels/-` appends to the end).
|
|
185
204
|
- The `ops` format needs no discovery: each op is `{ op, path, from?, value? }`, `op ∈ set | replace | remove | insert | move | move-before`, and `path` is a JSON Pointer (e.g. `/panels/3`, `/panels/3/title`, `/name`).
|
|
@@ -83,6 +83,14 @@ details live in `.agents/skills/`.
|
|
|
83
83
|
|
|
84
84
|
## Dashboard Template Catalog
|
|
85
85
|
|
|
86
|
+
- To build or extend a LARGE first-party dashboard, prefer `compose-dashboard`:
|
|
87
|
+
name the metrics and the server generates the validated SQL/config for every
|
|
88
|
+
panel in ONE fast call. Do NOT hand-author big `update-dashboard` configs
|
|
89
|
+
panel-by-panel or loop `update-dashboard` — streaming a giant multi-panel
|
|
90
|
+
argument inside the ~40s budget fails and thrashes. Unknown metric keys are
|
|
91
|
+
skipped and reported; per-panel SQL validates independently; existing
|
|
92
|
+
dashboards append by default (`overwrite: true` replaces). Report the returned
|
|
93
|
+
`panelCount` as proof-of-done.
|
|
86
94
|
- `list-dashboard-templates` lists source-controlled dashboard templates with
|
|
87
95
|
`id`, category, data sources, panel count, and installed dashboard IDs.
|
|
88
96
|
- `install-dashboard-template` installs a catalog template into normal
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { defineAction, embedApp } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
getRequestUserEmail,
|
|
4
|
+
getRequestOrgId,
|
|
5
|
+
buildDeepLink,
|
|
6
|
+
} from "@agent-native/core/server";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { getDashboard, upsertDashboard } from "../server/lib/dashboards-store";
|
|
9
|
+
import {
|
|
10
|
+
buildPanel,
|
|
11
|
+
listMetricKeys,
|
|
12
|
+
type ComposedPanel,
|
|
13
|
+
type MetricWindow,
|
|
14
|
+
} from "../server/lib/first-party-metric-catalog";
|
|
15
|
+
import { validateFirstPartyAnalyticsSql } from "../server/lib/first-party-analytics.js";
|
|
16
|
+
import {
|
|
17
|
+
hasCollabState,
|
|
18
|
+
applyText,
|
|
19
|
+
seedFromText,
|
|
20
|
+
} from "@agent-native/core/collab";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Push the saved config through the collab layer so open dashboard editors get
|
|
24
|
+
* the change in real time (mirrors update-dashboard / install-dashboard-template).
|
|
25
|
+
*/
|
|
26
|
+
async function syncToCollab(
|
|
27
|
+
dashboardId: string,
|
|
28
|
+
config: Record<string, unknown>,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const docId = `dash-${dashboardId}`;
|
|
31
|
+
const configStr = JSON.stringify(config);
|
|
32
|
+
try {
|
|
33
|
+
if (await hasCollabState(docId)) {
|
|
34
|
+
await applyText(docId, configStr, "content", "agent");
|
|
35
|
+
} else {
|
|
36
|
+
await seedFromText(docId, configStr);
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// Collab sync is best-effort — the SQL write is the source of truth.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const WINDOWS = new Set<MetricWindow>(["30d", "90d", "all"]);
|
|
44
|
+
|
|
45
|
+
function normalizeWindow(raw: unknown): MetricWindow | undefined {
|
|
46
|
+
if (typeof raw !== "string") return undefined;
|
|
47
|
+
const v = raw.trim() as MetricWindow;
|
|
48
|
+
return WINDOWS.has(v) ? v : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One requested metric, normalized to a key plus optional overrides. */
|
|
52
|
+
interface NormalizedRequest {
|
|
53
|
+
metric: string;
|
|
54
|
+
id?: string;
|
|
55
|
+
title?: string;
|
|
56
|
+
chartType?: string;
|
|
57
|
+
width?: number;
|
|
58
|
+
window?: MetricWindow;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeRequest(raw: unknown): NormalizedRequest | null {
|
|
62
|
+
if (typeof raw === "string") {
|
|
63
|
+
const metric = raw.trim();
|
|
64
|
+
return metric ? { metric } : null;
|
|
65
|
+
}
|
|
66
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
67
|
+
const obj = raw as Record<string, unknown>;
|
|
68
|
+
const metric = typeof obj.metric === "string" ? obj.metric.trim() : "";
|
|
69
|
+
if (!metric) return null;
|
|
70
|
+
const width =
|
|
71
|
+
typeof obj.width === "number" && Number.isFinite(obj.width)
|
|
72
|
+
? obj.width
|
|
73
|
+
: undefined;
|
|
74
|
+
return {
|
|
75
|
+
metric,
|
|
76
|
+
id: typeof obj.id === "string" ? obj.id : undefined,
|
|
77
|
+
title: typeof obj.title === "string" ? obj.title : undefined,
|
|
78
|
+
chartType: typeof obj.chartType === "string" ? obj.chartType : undefined,
|
|
79
|
+
width,
|
|
80
|
+
window: normalizeWindow(obj.window),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const metricSchema = z.union([
|
|
87
|
+
z.string(),
|
|
88
|
+
z.object({
|
|
89
|
+
metric: z.string(),
|
|
90
|
+
id: z.string().optional(),
|
|
91
|
+
title: z.string().optional(),
|
|
92
|
+
chartType: z.string().optional(),
|
|
93
|
+
width: z.number().optional(),
|
|
94
|
+
window: z.string().optional(),
|
|
95
|
+
}),
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
const METRIC_KEYS = listMetricKeys();
|
|
99
|
+
|
|
100
|
+
export default defineAction({
|
|
101
|
+
description:
|
|
102
|
+
"Build a large first-party analytics dashboard in ONE fast call: name the metrics you want and the SERVER generates the validated SQL + chart config for every panel. " +
|
|
103
|
+
"Do NOT hand-author large `update-dashboard` configs panel-by-panel — producing/streaming a big multi-panel config inside the ~40s run budget fails and thrashes. " +
|
|
104
|
+
"Each metric expands into a complete first-party panel from the shipped, already-validated metric catalog. Unknown metric keys are skipped and reported (not fatal); each panel's SQL is validated independently (valid panels are saved, invalid ones reported), and the dashboard is assembled and saved in a single atomic store write. " +
|
|
105
|
+
"If the dashboard already exists and `overwrite` is false (default), the new panels are APPENDED (panels whose id is already present are skipped); with `overwrite: true` the config is replaced. " +
|
|
106
|
+
"Returns { dashboardId, panelCount, createdMetrics, unknownMetrics, invalidMetrics, urlPath, deepLink, message } — use panelCount as proof-of-done. " +
|
|
107
|
+
`Available metric keys: ${METRIC_KEYS.join(", ")}. ` +
|
|
108
|
+
"Each metric accepts an optional per-metric `window` of '30d' | '90d' | 'all' (only affects windowed virality/time metrics).",
|
|
109
|
+
schema: z.object({
|
|
110
|
+
dashboardId: z
|
|
111
|
+
.string()
|
|
112
|
+
.describe(
|
|
113
|
+
"Dashboard id (without the `sql-dashboard-` prefix), e.g. 'first-party-overview'.",
|
|
114
|
+
),
|
|
115
|
+
title: z
|
|
116
|
+
.string()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe(
|
|
119
|
+
"Dashboard name. Used on create; on append it is applied only if the existing dashboard has no name.",
|
|
120
|
+
),
|
|
121
|
+
metrics: z
|
|
122
|
+
.preprocess(
|
|
123
|
+
(v) => (typeof v === "string" ? JSON.parse(v) : v),
|
|
124
|
+
z.array(metricSchema),
|
|
125
|
+
)
|
|
126
|
+
.describe(
|
|
127
|
+
"Metric keys to include, in order. Each item is either a key string or { metric, title?, chartType?, width?, window? }. " +
|
|
128
|
+
`Valid keys: ${METRIC_KEYS.join(", ")}.`,
|
|
129
|
+
),
|
|
130
|
+
overwrite: z
|
|
131
|
+
.boolean()
|
|
132
|
+
.optional()
|
|
133
|
+
.describe(
|
|
134
|
+
"If true, replace the whole dashboard config. If false (default) and the dashboard exists, APPEND the new panels (skipping ids already present).",
|
|
135
|
+
),
|
|
136
|
+
}),
|
|
137
|
+
mcpApp: {
|
|
138
|
+
compactCatalog: true,
|
|
139
|
+
resource: embedApp({
|
|
140
|
+
title: "Dashboard preview",
|
|
141
|
+
description: "Open the composed dashboard in the real Analytics UI.",
|
|
142
|
+
iframeTitle: "Agent-Native Analytics",
|
|
143
|
+
openLabel: "Open dashboard",
|
|
144
|
+
height: 680,
|
|
145
|
+
}),
|
|
146
|
+
},
|
|
147
|
+
run: async (args) => {
|
|
148
|
+
const email = getRequestUserEmail();
|
|
149
|
+
if (!email) throw new Error("no authenticated user");
|
|
150
|
+
const ctx = { email, orgId: getRequestOrgId() || null };
|
|
151
|
+
|
|
152
|
+
// The CLI/gateway may hand `metrics` over as a JSON string; the schema
|
|
153
|
+
// preprocess handles that, but normalize defensively here too.
|
|
154
|
+
const rawMetrics: unknown[] = Array.isArray(args.metrics)
|
|
155
|
+
? (args.metrics as unknown[])
|
|
156
|
+
: typeof args.metrics === "string"
|
|
157
|
+
? (JSON.parse(args.metrics) as unknown[])
|
|
158
|
+
: [];
|
|
159
|
+
|
|
160
|
+
const requests: NormalizedRequest[] = [];
|
|
161
|
+
for (const raw of rawMetrics) {
|
|
162
|
+
const normalized = normalizeRequest(raw);
|
|
163
|
+
if (normalized) requests.push(normalized);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const createdMetrics: string[] = [];
|
|
167
|
+
const unknownMetrics: string[] = [];
|
|
168
|
+
const invalidMetrics: Array<{ metric: string; reason: string }> = [];
|
|
169
|
+
const composedPanels: ComposedPanel[] = [];
|
|
170
|
+
|
|
171
|
+
for (const req of requests) {
|
|
172
|
+
const panel = buildPanel(req.metric, {
|
|
173
|
+
id: req.id,
|
|
174
|
+
title: req.title,
|
|
175
|
+
chartType: req.chartType,
|
|
176
|
+
width: req.width,
|
|
177
|
+
window: req.window,
|
|
178
|
+
});
|
|
179
|
+
if (!panel) {
|
|
180
|
+
// Unknown key — report, never throw.
|
|
181
|
+
if (!unknownMetrics.includes(req.metric)) {
|
|
182
|
+
unknownMetrics.push(req.metric);
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
// Per-panel graceful validation: a bad panel is dropped + reported, the
|
|
187
|
+
// rest of the dashboard still builds. (Catalog SQL is known-good, so this
|
|
188
|
+
// is a defensive net, e.g. if a future window/override produces bad SQL.)
|
|
189
|
+
try {
|
|
190
|
+
validateFirstPartyAnalyticsSql(panel.sql);
|
|
191
|
+
} catch (e: any) {
|
|
192
|
+
invalidMetrics.push({
|
|
193
|
+
metric: req.metric,
|
|
194
|
+
reason: e?.message ?? String(e),
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
composedPanels.push(panel);
|
|
199
|
+
createdMetrics.push(req.metric);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const existing = await getDashboard(args.dashboardId, ctx);
|
|
203
|
+
const dashboardName =
|
|
204
|
+
args.title?.trim() ||
|
|
205
|
+
(existing && typeof existing.config?.name === "string"
|
|
206
|
+
? (existing.config.name as string)
|
|
207
|
+
: args.dashboardId);
|
|
208
|
+
|
|
209
|
+
let finalConfig: Record<string, unknown>;
|
|
210
|
+
let appendedCount = composedPanels.length;
|
|
211
|
+
let skippedExistingIds: string[] = [];
|
|
212
|
+
|
|
213
|
+
if (existing && !args.overwrite) {
|
|
214
|
+
// Append: preserve existing panels + order, add only new panel ids.
|
|
215
|
+
const existingConfig = existing.config as Record<string, unknown>;
|
|
216
|
+
const existingPanels = Array.isArray(existingConfig.panels)
|
|
217
|
+
? (existingConfig.panels as Array<Record<string, unknown>>)
|
|
218
|
+
: [];
|
|
219
|
+
const existingIds = new Set(
|
|
220
|
+
existingPanels
|
|
221
|
+
.map((p) => (typeof p?.id === "string" ? p.id : null))
|
|
222
|
+
.filter((id): id is string => !!id),
|
|
223
|
+
);
|
|
224
|
+
const toAppend: ComposedPanel[] = [];
|
|
225
|
+
for (const panel of composedPanels) {
|
|
226
|
+
if (existingIds.has(panel.id)) {
|
|
227
|
+
skippedExistingIds.push(panel.id);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
toAppend.push(panel);
|
|
231
|
+
existingIds.add(panel.id);
|
|
232
|
+
}
|
|
233
|
+
appendedCount = toAppend.length;
|
|
234
|
+
finalConfig = {
|
|
235
|
+
...existingConfig,
|
|
236
|
+
name:
|
|
237
|
+
typeof existingConfig.name === "string" && existingConfig.name.trim()
|
|
238
|
+
? existingConfig.name
|
|
239
|
+
: dashboardName,
|
|
240
|
+
panels: [...existingPanels, ...toAppend],
|
|
241
|
+
};
|
|
242
|
+
} else {
|
|
243
|
+
// Create or overwrite: a fresh config with exactly the composed panels.
|
|
244
|
+
finalConfig = {
|
|
245
|
+
name: dashboardName,
|
|
246
|
+
description:
|
|
247
|
+
"First-party analytics dashboard composed from the metric catalog.",
|
|
248
|
+
panels: composedPanels,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const panelCount = Array.isArray(finalConfig.panels)
|
|
253
|
+
? (finalConfig.panels as unknown[]).length
|
|
254
|
+
: 0;
|
|
255
|
+
|
|
256
|
+
// Single atomic save through the same store path update-dashboard uses.
|
|
257
|
+
await upsertDashboard(args.dashboardId, "sql", finalConfig, ctx);
|
|
258
|
+
await syncToCollab(args.dashboardId, finalConfig);
|
|
259
|
+
|
|
260
|
+
const parts: string[] = [];
|
|
261
|
+
if (existing && !args.overwrite) {
|
|
262
|
+
parts.push(`Appended ${appendedCount} panel(s) to "${args.dashboardId}"`);
|
|
263
|
+
if (skippedExistingIds.length > 0) {
|
|
264
|
+
parts.push(`${skippedExistingIds.length} already present`);
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
parts.push(
|
|
268
|
+
`${existing ? "Replaced" : "Created"} "${args.dashboardId}" with ${createdMetrics.length} panel(s)`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (unknownMetrics.length > 0) {
|
|
272
|
+
parts.push(
|
|
273
|
+
`skipped ${unknownMetrics.length} unknown metric(s): ${unknownMetrics.join(", ")}`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (invalidMetrics.length > 0) {
|
|
277
|
+
parts.push(
|
|
278
|
+
`skipped ${invalidMetrics.length} invalid metric(s): ${invalidMetrics.map((m) => m.metric).join(", ")}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
parts.push(`Dashboard now has ${panelCount} panel(s).`);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
id: args.dashboardId,
|
|
285
|
+
dashboardId: args.dashboardId,
|
|
286
|
+
name: dashboardName,
|
|
287
|
+
panelCount,
|
|
288
|
+
createdMetrics,
|
|
289
|
+
unknownMetrics,
|
|
290
|
+
invalidMetrics,
|
|
291
|
+
skippedExistingIds,
|
|
292
|
+
urlPath: `/dashboards/${args.dashboardId}`,
|
|
293
|
+
deepLink: buildDeepLink({
|
|
294
|
+
app: "analytics",
|
|
295
|
+
view: "adhoc",
|
|
296
|
+
params: { dashboardId: args.dashboardId },
|
|
297
|
+
}),
|
|
298
|
+
message: parts.join("; ") + ".",
|
|
299
|
+
};
|
|
300
|
+
},
|
|
301
|
+
link: ({ result }) => {
|
|
302
|
+
const dashboardId =
|
|
303
|
+
result && typeof result === "object"
|
|
304
|
+
? (result as { dashboardId?: string }).dashboardId
|
|
305
|
+
: undefined;
|
|
306
|
+
if (!dashboardId) return null;
|
|
307
|
+
return {
|
|
308
|
+
url: buildDeepLink({
|
|
309
|
+
app: "analytics",
|
|
310
|
+
view: "adhoc",
|
|
311
|
+
params: { dashboardId },
|
|
312
|
+
}),
|
|
313
|
+
label: "Open composed dashboard in Analytics",
|
|
314
|
+
view: "adhoc",
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
});
|