@agent-analytics/paperclip-live-analytics-plugin 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.
@@ -0,0 +1 @@
1
+ :root{--aa-bg: #f0e8dc;--aa-bg-deep: #e4d6c0;--aa-ink: #1f2a2a;--aa-muted: #576665;--aa-panel: rgba(255, 250, 242, .78);--aa-panel-stroke: rgba(31, 42, 42, .11);--aa-accent: #0f8b8d;--aa-accent-warm: #dc6f36;--aa-accent-cool: #265f88;--aa-live: #1a9a63;--aa-error: #b24a2f;--aa-shadow: 0 18px 50px rgba(31, 42, 42, .08);--aa-serif: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;--aa-sans: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;--aa-mono: "IBM Plex Mono", "SFMono-Regular", monospace}*{box-sizing:border-box}body{margin:0;font-family:var(--aa-sans);color:var(--aa-ink);background:radial-gradient(circle at top left,rgba(15,139,141,.16),transparent 28%),radial-gradient(circle at top right,rgba(220,111,54,.16),transparent 22%),linear-gradient(180deg,#f8f3eb 0%,var(--aa-bg) 45%,var(--aa-bg-deep) 100%)}a{color:inherit;text-decoration:none}button,input,select{font:inherit}.aa-app{min-height:100vh;padding:24px}.aa-page-shell,.aa-settings-shell{max-width:1400px;margin:0 auto}.aa-hero,.aa-panel,.aa-widget,.aa-metric-card,.aa-asset-card{background:var(--aa-panel);border:1px solid var(--aa-panel-stroke);box-shadow:var(--aa-shadow);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.aa-hero{display:flex;justify-content:space-between;gap:24px;padding:28px;border-radius:28px}.aa-kicker{margin:0 0 8px;font-family:var(--aa-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--aa-muted)}.aa-hero h1,.aa-panel h2,.aa-widget h2{margin:0;font-family:var(--aa-serif);line-height:1}.aa-hero h1{font-size:clamp(2.6rem,5vw,4.3rem);max-width:12ch}.aa-hero-copy{max-width:58ch;color:var(--aa-muted)}.aa-hero-status{min-width:220px;display:flex;flex-direction:column;align-items:flex-end;gap:12px}.aa-metric-grid,.aa-main-grid,.aa-evidence-grid,.aa-asset-grid,.aa-settings-grid,.aa-form-grid{display:grid;gap:18px}.aa-metric-grid{grid-template-columns:repeat(4,minmax(0,1fr));margin:18px 0}.aa-metric-card{border-radius:22px;padding:20px}.aa-metric-card span,.aa-label{display:block;font-size:.82rem;color:var(--aa-muted)}.aa-metric-card strong{display:block;margin-top:10px;font-size:2.3rem;font-family:var(--aa-serif)}.aa-main-grid{grid-template-columns:1.25fr 1fr;align-items:start}.aa-panel,.aa-widget,.aa-asset-card{border-radius:26px;padding:22px}.aa-panel-header,.aa-widget-header,.aa-asset-topline,.aa-widget-footer,.aa-inline-actions,.aa-settings-row{display:flex;justify-content:space-between;gap:14px;align-items:flex-start}.aa-world-grid{display:grid;grid-template-columns:minmax(260px,.9fr) 1.1fr;gap:20px;align-items:center}.aa-globe{position:relative;aspect-ratio:1;border-radius:999px;background:radial-gradient(circle at 30% 30%,rgba(15,139,141,.45),transparent 44%),radial-gradient(circle at 70% 65%,rgba(220,111,54,.24),transparent 32%),linear-gradient(180deg,#173838,#0f6465);overflow:hidden;box-shadow:inset 0 0 80px #ffffff26}.aa-globe-ring{position:absolute;inset:18%;border:1px solid rgba(255,255,255,.22);border-radius:999px}.aa-globe-ring-two{inset:8%}.aa-globe-ring-three{inset:32%}.aa-globe-core{position:absolute;inset:38%;display:grid;place-items:center;border-radius:999px;background:#fffaf2eb;color:var(--aa-ink);font-family:var(--aa-mono);letter-spacing:.12em;text-transform:uppercase;font-size:.75rem}.aa-country-list,.aa-feed,.aa-settings-stack{display:grid;gap:12px}.aa-country-row,.aa-feed-row,.aa-mini-row,.aa-settings-row{padding:12px 0;border-top:1px solid rgba(31,42,42,.08)}.aa-country-row:first-child,.aa-feed-row:first-child,.aa-mini-row:first-child,.aa-settings-row:first-child{border-top:0;padding-top:0}.aa-country-row strong,.aa-mini-row strong,.aa-feed-row strong,.aa-settings-row strong{display:block}.aa-country-row span,.aa-feed-row span,.aa-settings-row span{color:var(--aa-muted);font-size:.92rem}.aa-country-bar{width:100%;height:8px;margin-top:8px;border-radius:999px;background:#265f881f;overflow:hidden}.aa-country-bar-fill{height:100%;border-radius:999px;background:linear-gradient(90deg,var(--aa-accent) 0%,var(--aa-accent-warm) 100%)}.aa-world-hot,.aa-status-pill{padding:7px 12px;border-radius:999px;font-size:.78rem;font-family:var(--aa-mono);letter-spacing:.08em;text-transform:uppercase}.aa-world-hot,.aa-status-live,.aa-status-connected,.aa-status-streaming{background:#1a9a6324;color:var(--aa-live)}.aa-status-error,.aa-status-attention{background:#b24a2f24;color:var(--aa-error)}.aa-status-idle,.aa-status-pending,.aa-status-disconnected{background:#265f881f;color:var(--aa-accent-cool)}.aa-mini-panel{background:#ffffff6b;border-radius:20px;padding:18px;border:1px solid rgba(31,42,42,.08)}.aa-mini-panel h3,.aa-assets-section h2{margin:0 0 12px;font-family:var(--aa-serif)}.aa-feed-row{display:flex;justify-content:space-between;gap:12px}.aa-feed-row time{white-space:nowrap;font-size:.85rem;color:var(--aa-muted)}.aa-assets-section{margin-top:18px}.aa-asset-grid{grid-template-columns:repeat(auto-fit,minmax(280px,1fr));margin-top:18px}.aa-asset-card{display:grid;gap:16px}.aa-asset-metrics,.aa-asset-details,.aa-asset-evidence,.aa-widget-metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.aa-asset-metrics strong,.aa-widget-metrics strong{display:block;margin-top:8px;font-family:var(--aa-serif);font-size:1.6rem}.aa-button{border:0;border-radius:999px;padding:10px 14px;cursor:pointer;transition:transform .12s ease,opacity .12s ease}.aa-button:hover{transform:translateY(-1px)}.aa-button-primary{background:var(--aa-ink);color:#fff}.aa-button-secondary{background:#0f8b8d1f;color:var(--aa-accent)}.aa-button-ghost{background:transparent;color:var(--aa-muted);border:1px solid rgba(31,42,42,.12)}.aa-widget{max-width:520px;margin:0 auto}.aa-widget-metrics{margin:18px 0}.aa-settings-grid{grid-template-columns:1.15fr .85fr}.aa-form-grid{grid-template-columns:repeat(2,minmax(0,1fr));margin-top:14px}.aa-form-grid label,.aa-auth-box label{display:grid;gap:8px;font-size:.92rem;color:var(--aa-muted)}.aa-form-grid input,.aa-form-grid select,.aa-auth-box input{border-radius:14px;border:1px solid rgba(31,42,42,.12);padding:12px 14px;background:#ffffffb8}.aa-auth-box{display:grid;gap:12px;margin-top:16px;padding:16px;border-radius:18px;background:#ffffff7a}.aa-checkbox{display:flex!important;align-items:center;gap:10px}.aa-muted-note{color:var(--aa-muted);font-size:.92rem;display:inline-flex;align-items:center}.aa-panel-warning{border-color:#dc6f3647}@media(max-width:1100px){.aa-metric-grid,.aa-main-grid,.aa-settings-grid,.aa-world-grid{grid-template-columns:1fr}.aa-hero{flex-direction:column}.aa-hero-status{align-items:flex-start}}@media(max-width:720px){.aa-app{padding:14px}.aa-metric-grid,.aa-form-grid,.aa-asset-metrics,.aa-asset-details,.aa-asset-evidence,.aa-widget-metrics{grid-template-columns:1fr 1fr}.aa-feed-row,.aa-panel-header,.aa-widget-header,.aa-widget-footer,.aa-settings-row,.aa-inline-actions{flex-direction:column}}
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Agent Analytics Live</title>
7
+ <script type="module" crossorigin src="/assets/index-DCET-swI.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-dyBLLxDx.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
14
+
@@ -0,0 +1,30 @@
1
+ # Maintainer Notes
2
+
3
+ ## Host contract
4
+
5
+ - Manifest entrypoints:
6
+ - `entrypoints.worker` → `./src/worker/index.js`
7
+ - `entrypoints.ui` → `./dist`
8
+ - Declared surfaces:
9
+ - `page`
10
+ - `dashboardWidget`
11
+ - `settingsPage`
12
+
13
+ ## Worker/UI boundary
14
+
15
+ - Worker owns auth, refresh, `/stream`, `/live`, per-company cache, and stream emission
16
+ - UI consumes worker data/actions/streams only
17
+ - No third-party credentials leave the worker boundary
18
+
19
+ ## State ownership
20
+
21
+ - Company-scoped config: base URL, live window, poll cadence, enabled mappings
22
+ - Company-scoped auth: access token, refresh token, tier, pending detached login state
23
+ - Company-scoped UI state: snoozed assets
24
+
25
+ ## Stream delivery
26
+
27
+ - Worker opens one company-scoped host stream channel
28
+ - Worker emits normalized full-state payloads
29
+ - UI replaces local live state wholesale on each event
30
+
@@ -0,0 +1,22 @@
1
+ # Asset Mapping Guide
2
+
3
+ Mappings are explicit and company-scoped.
4
+
5
+ ## Required fields
6
+
7
+ - `assetKey`
8
+ - `label`
9
+ - `kind`
10
+ - `agentAnalyticsProject`
11
+
12
+ ## Optional fields
13
+
14
+ - `paperclipProjectId`
15
+ - `primaryHostname`
16
+ - `allowedOrigins`
17
+ - `enabled`
18
+
19
+ ## Important v1 rule
20
+
21
+ `/live` snapshots are project-scoped. If multiple mappings reuse the same Agent Analytics project, the plugin may mirror the same live totals across more than one asset card. The settings page warns when this happens.
22
+
@@ -0,0 +1,16 @@
1
+ # Limits And Troubleshooting
2
+
3
+ ## Hard limits
4
+
5
+ - `/stream` and `/live` are metered read routes
6
+ - `/live` is paid-only
7
+ - Upstream live streams are capped at 10 concurrent streams per account
8
+ - `/live` keeps only the most recent 5 minutes of backing event history
9
+
10
+ ## Common failure modes
11
+
12
+ - `unauthorized` or `invalid refresh token`: reconnect from settings
13
+ - duplicate project mappings: reduce repeated project use or accept mirrored project totals
14
+ - no live data with a free tier: upgrade the connected account
15
+ - stale UI while connection is healthy: check whether the asset is snoozed or disabled
16
+
@@ -0,0 +1,20 @@
1
+ # Live Behavior
2
+
3
+ ## Data sources
4
+
5
+ - `/stream`: incremental live event feed over Server-Sent Events
6
+ - `/live`: authoritative rollup snapshot for the short live window
7
+
8
+ ## What the metrics mean
9
+
10
+ - `Active visitors`: unique users inside the current `/live` window
11
+ - `Active sessions`: unique sessions inside the current `/live` window
12
+ - `Events / min`: current live-window event rate
13
+ - `Recent events`: operator evidence feed, not a full audit log
14
+
15
+ ## What this plugin is not
16
+
17
+ - Not a rebuild of the Agent Analytics reporting product
18
+ - Not a historical dashboard
19
+ - Not an automated issue router
20
+
@@ -0,0 +1,18 @@
1
+ # Operator Overview
2
+
3
+ Agent Analytics Live is the Paperclip surface for answering one question quickly:
4
+
5
+ Which company asset is moving right now, and is that movement worth operator attention?
6
+
7
+ ## Surfaces
8
+
9
+ - `page`: company-level live monitor with asset cards, world/country emphasis, top pages, top events, and recent evidence
10
+ - `dashboardWidget`: compact pulse for the main dashboard
11
+ - `settingsPage`: connection, asset mapping, and rollout controls
12
+
13
+ ## Assumptions
14
+
15
+ - One Paperclip company connects one Agent Analytics account
16
+ - `/live` and `/stream` are paid live routes, so the account tier must permit live reads
17
+ - The plugin is intentionally live-window-only; it is not historical reporting
18
+
@@ -0,0 +1,23 @@
1
+ # Setup And Auth
2
+
3
+ ## Default auth path
4
+
5
+ The plugin is login-first.
6
+
7
+ 1. Open the plugin `settingsPage`
8
+ 2. Click `Start login`
9
+ 3. Open the returned approval URL
10
+ 4. Sign in with Google or GitHub
11
+ 5. Paste the finish code into the settings page
12
+ 6. The worker exchanges the code, stores the session, validates `GET /projects`, and starts live sync
13
+
14
+ ## Worker-owned boundary
15
+
16
+ - Access token and refresh token are stored in worker-owned company state
17
+ - The browser UI never receives a raw Agent Analytics API key
18
+ - `/stream` and `/live` are called only from the worker
19
+
20
+ ## Compatibility fallback
21
+
22
+ The worker code keeps an internal API-key auth adapter for legacy or self-hosted environments, but v1 does not expose API-key entry in the UI.
23
+
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Agent Analytics Live</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/ui/main.jsx"></script>
11
+ </body>
12
+ </html>
13
+
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@agent-analytics/paperclip-live-analytics-plugin",
3
+ "version": "0.1.0",
4
+ "description": "Thin Paperclip plugin that exposes live Agent Analytics signals inside a company workspace.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Agent-Analytics/paperclip-live-analytics-plugin.git"
10
+ },
11
+ "homepage": "https://github.com/Agent-Analytics/paperclip-live-analytics-plugin#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/Agent-Analytics/paperclip-live-analytics-plugin/issues"
14
+ },
15
+ "keywords": [
16
+ "paperclip",
17
+ "plugin",
18
+ "analytics",
19
+ "live",
20
+ "agent-analytics"
21
+ ],
22
+ "files": [
23
+ "dist",
24
+ "docs",
25
+ "src",
26
+ "index.html",
27
+ "paperclip-plugin.manifest.json",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "scripts": {
35
+ "build": "vite build",
36
+ "dev": "vite",
37
+ "test": "node --test tests/*.test.mjs",
38
+ "prepack": "npm test && npm run build",
39
+ "pack:local": "npm pack"
40
+ },
41
+ "dependencies": {
42
+ "react": "^19.2.0",
43
+ "react-dom": "^19.2.0"
44
+ },
45
+ "devDependencies": {
46
+ "@vitejs/plugin-react": "^5.1.3",
47
+ "vite": "^7.2.4"
48
+ }
49
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "id": "@agent-analytics/paperclip-live-analytics-plugin",
3
+ "name": "Agent Analytics Live",
4
+ "displayName": "Agent Analytics Live",
5
+ "version": "0.1.0",
6
+ "categories": ["connector", "ui"],
7
+ "entrypoints": {
8
+ "worker": "./src/worker/index.js",
9
+ "ui": "./dist"
10
+ },
11
+ "surfaces": [
12
+ {
13
+ "type": "page",
14
+ "key": "agentAnalyticsLivePage",
15
+ "title": "Agent Analytics Live"
16
+ },
17
+ {
18
+ "type": "dashboardWidget",
19
+ "key": "agentAnalyticsLiveWidget",
20
+ "title": "Agent Analytics Live"
21
+ },
22
+ {
23
+ "type": "settingsPage",
24
+ "key": "agentAnalyticsLiveSettings",
25
+ "title": "Agent Analytics Live Settings"
26
+ }
27
+ ],
28
+ "capabilities": [
29
+ "http.outbound",
30
+ "plugin.state.read",
31
+ "plugin.state.write",
32
+ "companies.read",
33
+ "projects.read"
34
+ ]
35
+ }
36
+
@@ -0,0 +1,204 @@
1
+ import {
2
+ AGENT_SESSION_SCOPES,
3
+ DEFAULT_BASE_URL,
4
+ PLUGIN_DISPLAY_NAME,
5
+ PLUGIN_ID,
6
+ } from './constants.js';
7
+
8
+ function createJsonHeaders(auth) {
9
+ const headers = {
10
+ 'Content-Type': 'application/json',
11
+ };
12
+ if (auth?.access_token) headers.Authorization = `Bearer ${auth.access_token}`;
13
+ else if (auth?.api_key) headers['X-API-Key'] = auth.api_key;
14
+ return headers;
15
+ }
16
+
17
+ function buildQuery(params) {
18
+ return Object.entries(params)
19
+ .filter(([, value]) => value !== null && value !== undefined && value !== '')
20
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
21
+ .join('&');
22
+ }
23
+
24
+ function parseSseEvent(rawEvent) {
25
+ const event = {
26
+ event: 'message',
27
+ data: '',
28
+ comment: null,
29
+ };
30
+
31
+ const lines = rawEvent.split(/\r?\n/);
32
+ for (const line of lines) {
33
+ if (!line) continue;
34
+ if (line.startsWith(':')) {
35
+ event.comment = line.slice(1).trim();
36
+ continue;
37
+ }
38
+ const separatorIndex = line.indexOf(':');
39
+ const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
40
+ const value = separatorIndex === -1 ? '' : line.slice(separatorIndex + 1).trimStart();
41
+ if (field === 'event') event.event = value;
42
+ if (field === 'data') event.data = event.data ? `${event.data}\n${value}` : value;
43
+ }
44
+ return event;
45
+ }
46
+
47
+ export class AgentAnalyticsClient {
48
+ constructor({
49
+ auth = null,
50
+ baseUrl = DEFAULT_BASE_URL,
51
+ fetchImpl = globalThis.fetch,
52
+ onAuthUpdate = null,
53
+ } = {}) {
54
+ this.auth = auth ? { ...auth } : null;
55
+ this.baseUrl = baseUrl;
56
+ this.fetchImpl = fetchImpl;
57
+ this.onAuthUpdate = onAuthUpdate;
58
+ }
59
+
60
+ setAuth(auth) {
61
+ this.auth = auth ? { ...auth } : null;
62
+ }
63
+
64
+ async request(method, path, body, { retryOnRefresh = true } = {}) {
65
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
66
+ method,
67
+ headers: createJsonHeaders(this.auth),
68
+ body: body ? JSON.stringify(body) : undefined,
69
+ });
70
+
71
+ const data = await response.json().catch(() => ({}));
72
+
73
+ if (response.status === 401 && retryOnRefresh && this.auth?.refresh_token) {
74
+ const refreshed = await this.refreshAgentSession().catch(() => null);
75
+ if (refreshed?.access_token) {
76
+ return this.request(method, path, body, { retryOnRefresh: false });
77
+ }
78
+ }
79
+
80
+ if (!response.ok) {
81
+ throw new Error(data.message || data.error || `HTTP ${response.status}`);
82
+ }
83
+
84
+ return data;
85
+ }
86
+
87
+ async startPaperclipAuth({ companyId, label } = {}) {
88
+ return this.request(
89
+ 'POST',
90
+ '/agent-sessions/start',
91
+ {
92
+ mode: 'detached',
93
+ client_type: 'paperclip',
94
+ client_name: PLUGIN_DISPLAY_NAME,
95
+ client_instance_id: companyId || null,
96
+ label: label || `Paperclip Company ${companyId || ''}`.trim(),
97
+ scopes: AGENT_SESSION_SCOPES,
98
+ metadata: {
99
+ platform: 'paperclip',
100
+ plugin_id: PLUGIN_ID,
101
+ company_id: companyId || null,
102
+ },
103
+ },
104
+ { retryOnRefresh: false }
105
+ );
106
+ }
107
+
108
+ async exchangeAgentSession(authRequestId, exchangeCode) {
109
+ return this.request(
110
+ 'POST',
111
+ '/agent-sessions/exchange',
112
+ {
113
+ auth_request_id: authRequestId,
114
+ exchange_code: exchangeCode,
115
+ },
116
+ { retryOnRefresh: false }
117
+ );
118
+ }
119
+
120
+ async refreshAgentSession() {
121
+ if (!this.auth?.refresh_token) {
122
+ throw new Error('No refresh token available');
123
+ }
124
+
125
+ const refreshed = await this.request(
126
+ 'POST',
127
+ '/agent-sessions/refresh',
128
+ {
129
+ refresh_token: this.auth.refresh_token,
130
+ },
131
+ { retryOnRefresh: false }
132
+ );
133
+
134
+ this.auth = {
135
+ ...this.auth,
136
+ ...refreshed.agent_session,
137
+ };
138
+ this.onAuthUpdate?.(this.auth);
139
+ return this.auth;
140
+ }
141
+
142
+ async listProjects() {
143
+ return this.request('GET', '/projects');
144
+ }
145
+
146
+ async getLive(project, { window } = {}) {
147
+ const query = buildQuery({ project, window });
148
+ return this.request('GET', `/live?${query}`);
149
+ }
150
+
151
+ async subscribeToStream({ project, filter, signal, onConnected, onTrack, onComment }) {
152
+ const query = buildQuery({
153
+ project,
154
+ filter,
155
+ });
156
+
157
+ const response = await this.fetchImpl(`${this.baseUrl}/stream?${query}`, {
158
+ method: 'GET',
159
+ headers: createJsonHeaders(this.auth),
160
+ signal,
161
+ });
162
+
163
+ if (!response.ok || !response.body) {
164
+ const payload = await response.text().catch(() => '');
165
+ throw new Error(payload || `Stream failed with HTTP ${response.status}`);
166
+ }
167
+
168
+ const reader = response.body.getReader();
169
+ const decoder = new TextDecoder();
170
+ let buffer = '';
171
+
172
+ while (true) {
173
+ const { done, value } = await reader.read();
174
+ if (done) break;
175
+
176
+ buffer += decoder.decode(value, { stream: true });
177
+ let boundary = buffer.indexOf('\n\n');
178
+ while (boundary !== -1) {
179
+ const rawEvent = buffer.slice(0, boundary);
180
+ buffer = buffer.slice(boundary + 2);
181
+ const event = parseSseEvent(rawEvent);
182
+
183
+ if (event.comment) {
184
+ onComment?.(event.comment);
185
+ }
186
+
187
+ if (event.data) {
188
+ let parsed = {};
189
+ try {
190
+ parsed = JSON.parse(event.data);
191
+ } catch {
192
+ parsed = { raw: event.data };
193
+ }
194
+
195
+ if (event.event === 'connected') onConnected?.(parsed);
196
+ if (event.event === 'track') onTrack?.(parsed);
197
+ }
198
+
199
+ boundary = buffer.indexOf('\n\n');
200
+ }
201
+ }
202
+ }
203
+ }
204
+
@@ -0,0 +1,41 @@
1
+ export const PLUGIN_ID = '@agent-analytics/paperclip-live-analytics-plugin';
2
+ export const PLUGIN_DISPLAY_NAME = 'Agent Analytics Live';
3
+ export const DEFAULT_BASE_URL = 'https://api.agentanalytics.sh';
4
+ export const DEFAULT_LIVE_WINDOW_SECONDS = 60;
5
+ export const DEFAULT_POLL_INTERVAL_SECONDS = 15;
6
+ export const MIN_LIVE_WINDOW_SECONDS = 10;
7
+ export const MAX_LIVE_WINDOW_SECONDS = 300;
8
+ export const MIN_POLL_INTERVAL_SECONDS = 5;
9
+ export const MAX_POLL_INTERVAL_SECONDS = 60;
10
+ export const DEFAULT_SNOOZE_MINUTES = 30;
11
+ export const MAX_ENABLED_ASSET_STREAMS = 10;
12
+ export const LIVE_STREAM_CHANNEL = 'agent-analytics-live';
13
+ export const STATE_NAMESPACE = 'agent-analytics-live';
14
+
15
+ export const DATA_KEYS = {
16
+ livePageLoad: 'live.page.load',
17
+ liveWidgetLoad: 'live.widget.load',
18
+ settingsLoad: 'settings.load',
19
+ };
20
+
21
+ export const ACTION_KEYS = {
22
+ authStart: 'auth.start',
23
+ authComplete: 'auth.complete',
24
+ authDisconnect: 'auth.disconnect',
25
+ authReconnect: 'auth.reconnect',
26
+ settingsSave: 'settings.save',
27
+ mappingUpsert: 'mapping.upsert',
28
+ mappingRemove: 'mapping.remove',
29
+ assetSnooze: 'asset.snooze',
30
+ assetUnsnooze: 'asset.unsnooze',
31
+ };
32
+
33
+ export const AGENT_SESSION_SCOPES = [
34
+ 'account:read',
35
+ 'projects:write',
36
+ 'analytics:read',
37
+ 'live:read',
38
+ ];
39
+
40
+ export const ASSET_KINDS = ['website', 'docs', 'app', 'api', 'other'];
41
+
@@ -0,0 +1,73 @@
1
+ import {
2
+ DEFAULT_BASE_URL,
3
+ DEFAULT_LIVE_WINDOW_SECONDS,
4
+ DEFAULT_POLL_INTERVAL_SECONDS,
5
+ } from './constants.js';
6
+
7
+ export function createDefaultSettings() {
8
+ return {
9
+ agentAnalyticsBaseUrl: DEFAULT_BASE_URL,
10
+ liveWindowSeconds: DEFAULT_LIVE_WINDOW_SECONDS,
11
+ pollIntervalSeconds: DEFAULT_POLL_INTERVAL_SECONDS,
12
+ monitoredAssets: [],
13
+ pluginEnabled: true,
14
+ };
15
+ }
16
+
17
+ export function createDefaultAuthState() {
18
+ return {
19
+ mode: 'agent_session',
20
+ accessToken: null,
21
+ refreshToken: null,
22
+ accessExpiresAt: null,
23
+ refreshExpiresAt: null,
24
+ accountSummary: null,
25
+ tier: null,
26
+ status: 'disconnected',
27
+ pendingAuthRequest: null,
28
+ lastValidatedAt: null,
29
+ lastError: null,
30
+ };
31
+ }
32
+
33
+ export function createDefaultSnoozeState() {
34
+ return {};
35
+ }
36
+
37
+ export function createEmptyCompanyLiveState() {
38
+ return {
39
+ type: 'live_state',
40
+ generatedAt: Date.now(),
41
+ pluginEnabled: true,
42
+ authStatus: 'disconnected',
43
+ tier: null,
44
+ account: null,
45
+ connection: {
46
+ status: 'idle',
47
+ label: 'Not connected',
48
+ detail: 'Connect Agent Analytics from settings to start the live feed.',
49
+ },
50
+ metrics: {
51
+ activeVisitors: 0,
52
+ activeSessions: 0,
53
+ eventsPerMinute: 0,
54
+ assetsConfigured: 0,
55
+ assetsVisible: 0,
56
+ countriesTracked: 0,
57
+ },
58
+ world: {
59
+ hotCountry: null,
60
+ countries: [],
61
+ },
62
+ evidence: {
63
+ topPages: [],
64
+ topEvents: [],
65
+ recentEvents: [],
66
+ countries: [],
67
+ },
68
+ assets: [],
69
+ snoozedAssets: [],
70
+ warnings: [],
71
+ };
72
+ }
73
+