@agent-analytics/paperclip-live-analytics-plugin 0.1.0 → 0.1.1

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/dist/worker.js ADDED
@@ -0,0 +1,1165 @@
1
+ // src/paperclip/worker-entry.js
2
+ import { definePlugin, runWorker } from "@paperclipai/plugin-sdk";
3
+
4
+ // src/shared/constants.js
5
+ var PLUGIN_ID = "@agent-analytics/paperclip-live-analytics-plugin";
6
+ var PLUGIN_DISPLAY_NAME = "Agent Analytics Live";
7
+ var DEFAULT_BASE_URL = "https://api.agentanalytics.sh";
8
+ var DEFAULT_LIVE_WINDOW_SECONDS = 60;
9
+ var DEFAULT_POLL_INTERVAL_SECONDS = 15;
10
+ var MIN_LIVE_WINDOW_SECONDS = 10;
11
+ var MAX_LIVE_WINDOW_SECONDS = 300;
12
+ var MIN_POLL_INTERVAL_SECONDS = 5;
13
+ var MAX_POLL_INTERVAL_SECONDS = 60;
14
+ var DEFAULT_SNOOZE_MINUTES = 30;
15
+ var MAX_ENABLED_ASSET_STREAMS = 10;
16
+ var LIVE_STREAM_CHANNEL = "agent-analytics-live";
17
+ var STATE_NAMESPACE = "agent-analytics-live";
18
+ var DATA_KEYS = {
19
+ livePageLoad: "live.page.load",
20
+ liveWidgetLoad: "live.widget.load",
21
+ settingsLoad: "settings.load"
22
+ };
23
+ var ACTION_KEYS = {
24
+ authStart: "auth.start",
25
+ authComplete: "auth.complete",
26
+ authDisconnect: "auth.disconnect",
27
+ authReconnect: "auth.reconnect",
28
+ settingsSave: "settings.save",
29
+ mappingUpsert: "mapping.upsert",
30
+ mappingRemove: "mapping.remove",
31
+ assetSnooze: "asset.snooze",
32
+ assetUnsnooze: "asset.unsnooze"
33
+ };
34
+ var AGENT_SESSION_SCOPES = [
35
+ "account:read",
36
+ "projects:write",
37
+ "analytics:read",
38
+ "live:read"
39
+ ];
40
+ var ASSET_KINDS = ["website", "docs", "app", "api", "other"];
41
+
42
+ // src/shared/defaults.js
43
+ function createDefaultSettings() {
44
+ return {
45
+ agentAnalyticsBaseUrl: DEFAULT_BASE_URL,
46
+ liveWindowSeconds: DEFAULT_LIVE_WINDOW_SECONDS,
47
+ pollIntervalSeconds: DEFAULT_POLL_INTERVAL_SECONDS,
48
+ monitoredAssets: [],
49
+ pluginEnabled: true
50
+ };
51
+ }
52
+ function createDefaultAuthState() {
53
+ return {
54
+ mode: "agent_session",
55
+ accessToken: null,
56
+ refreshToken: null,
57
+ accessExpiresAt: null,
58
+ refreshExpiresAt: null,
59
+ accountSummary: null,
60
+ tier: null,
61
+ status: "disconnected",
62
+ pendingAuthRequest: null,
63
+ lastValidatedAt: null,
64
+ lastError: null
65
+ };
66
+ }
67
+ function createDefaultSnoozeState() {
68
+ return {};
69
+ }
70
+ function createEmptyCompanyLiveState() {
71
+ return {
72
+ type: "live_state",
73
+ generatedAt: Date.now(),
74
+ pluginEnabled: true,
75
+ authStatus: "disconnected",
76
+ tier: null,
77
+ account: null,
78
+ connection: {
79
+ status: "idle",
80
+ label: "Not connected",
81
+ detail: "Connect Agent Analytics from settings to start the live feed."
82
+ },
83
+ metrics: {
84
+ activeVisitors: 0,
85
+ activeSessions: 0,
86
+ eventsPerMinute: 0,
87
+ assetsConfigured: 0,
88
+ assetsVisible: 0,
89
+ countriesTracked: 0
90
+ },
91
+ world: {
92
+ hotCountry: null,
93
+ countries: []
94
+ },
95
+ evidence: {
96
+ topPages: [],
97
+ topEvents: [],
98
+ recentEvents: [],
99
+ countries: []
100
+ },
101
+ assets: [],
102
+ snoozedAssets: [],
103
+ warnings: []
104
+ };
105
+ }
106
+
107
+ // src/shared/live-state.js
108
+ function clampNumber(value, min, max, fallback) {
109
+ const parsed = Number(value);
110
+ if (!Number.isFinite(parsed)) return fallback;
111
+ return Math.min(max, Math.max(min, Math.round(parsed)));
112
+ }
113
+ function sortCountRows(rows, countKey) {
114
+ return [...rows].sort((left, right) => {
115
+ if ((right[countKey] || 0) !== (left[countKey] || 0)) {
116
+ return (right[countKey] || 0) - (left[countKey] || 0);
117
+ }
118
+ return String(left.label || left.path || left.event || "").localeCompare(
119
+ String(right.label || right.path || right.event || "")
120
+ );
121
+ });
122
+ }
123
+ function sortCountries(rows) {
124
+ return [...rows].sort((left, right) => {
125
+ if ((right.visitors || 0) !== (left.visitors || 0)) return (right.visitors || 0) - (left.visitors || 0);
126
+ if ((right.sessions || 0) !== (left.sessions || 0)) return (right.sessions || 0) - (left.sessions || 0);
127
+ if ((right.events || 0) !== (left.events || 0)) return (right.events || 0) - (left.events || 0);
128
+ return String(left.country || "").localeCompare(String(right.country || ""));
129
+ });
130
+ }
131
+ function clampLiveWindowSeconds(value) {
132
+ return clampNumber(value, MIN_LIVE_WINDOW_SECONDS, MAX_LIVE_WINDOW_SECONDS, DEFAULT_LIVE_WINDOW_SECONDS);
133
+ }
134
+ function clampPollIntervalSeconds(value) {
135
+ return clampNumber(value, MIN_POLL_INTERVAL_SECONDS, MAX_POLL_INTERVAL_SECONDS, DEFAULT_POLL_INTERVAL_SECONDS);
136
+ }
137
+ function slugifyAssetKey(value) {
138
+ return String(value || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
139
+ }
140
+ function normalizeAssetMapping(input = {}) {
141
+ const label = String(input.label || "").trim();
142
+ const assetKey = slugifyAssetKey(input.assetKey || label || "asset");
143
+ const kind = ASSET_KINDS.includes(input.kind) ? input.kind : "other";
144
+ const project = String(input.agentAnalyticsProject || "").trim();
145
+ return {
146
+ assetKey,
147
+ label: label || assetKey,
148
+ kind,
149
+ paperclipProjectId: input.paperclipProjectId ? String(input.paperclipProjectId).trim() : "",
150
+ agentAnalyticsProject: project,
151
+ primaryHostname: input.primaryHostname ? String(input.primaryHostname).trim() : "",
152
+ allowedOrigins: Array.isArray(input.allowedOrigins) ? input.allowedOrigins.map((value) => String(value).trim()).filter(Boolean) : [],
153
+ enabled: input.enabled !== false
154
+ };
155
+ }
156
+ function buildStreamFilter(mapping) {
157
+ const clauses = [];
158
+ if (mapping.primaryHostname) clauses.push(`hostname:${mapping.primaryHostname}`);
159
+ return clauses.join(";") || null;
160
+ }
161
+ function mappingSignature(mapping) {
162
+ return JSON.stringify({
163
+ project: mapping.agentAnalyticsProject,
164
+ filter: buildStreamFilter(mapping)
165
+ });
166
+ }
167
+ function createEmptyAssetState(mapping, now = Date.now()) {
168
+ return {
169
+ assetKey: mapping.assetKey,
170
+ label: mapping.label,
171
+ kind: mapping.kind,
172
+ paperclipProjectId: mapping.paperclipProjectId || "",
173
+ agentAnalyticsProject: mapping.agentAnalyticsProject,
174
+ primaryHostname: mapping.primaryHostname || "",
175
+ allowedOrigins: mapping.allowedOrigins || [],
176
+ enabled: mapping.enabled !== false,
177
+ status: "idle",
178
+ lastUpdatedAt: now,
179
+ lastSnapshotAt: null,
180
+ lastPolledAt: null,
181
+ lastHotCountry: null,
182
+ activeVisitors: 0,
183
+ activeSessions: 0,
184
+ eventsPerMinute: 0,
185
+ topPages: [],
186
+ topEvents: [],
187
+ countries: [],
188
+ recentEvents: [],
189
+ warnings: [],
190
+ errors: []
191
+ };
192
+ }
193
+ function normalizeLiveSnapshot(snapshot = {}) {
194
+ return {
195
+ project: snapshot.project || "",
196
+ windowSeconds: clampLiveWindowSeconds(snapshot.window_seconds),
197
+ timestamp: snapshot.timestamp || Date.now(),
198
+ activeVisitors: Number(snapshot.active_visitors || 0),
199
+ activeSessions: Number(snapshot.active_sessions || 0),
200
+ eventsPerMinute: Number(snapshot.events_per_minute || 0),
201
+ topPages: (snapshot.top_pages || []).map((row) => ({
202
+ path: String(row.path || ""),
203
+ visitors: Number(row.visitors || 0)
204
+ })),
205
+ topEvents: (snapshot.top_events || []).map((row) => ({
206
+ event: String(row.event || ""),
207
+ count: Number(row.count || 0)
208
+ })),
209
+ countries: sortCountries(
210
+ (snapshot.countries || []).map((row) => ({
211
+ country: String(row.country || ""),
212
+ visitors: Number(row.visitors || 0),
213
+ sessions: Number(row.sessions || 0),
214
+ events: Number(row.events || 0)
215
+ }))
216
+ ),
217
+ recentEvents: (snapshot.recent_events || []).map((row, index) => ({
218
+ id: `${row.session_id || row.user_id || row.timestamp || "event"}-${index}`,
219
+ event: String(row.event || ""),
220
+ properties: row.properties || {},
221
+ userId: row.user_id || null,
222
+ sessionId: row.session_id || null,
223
+ timestamp: Number(row.timestamp || Date.now()),
224
+ country: row.country || null,
225
+ path: row.properties?.path || null,
226
+ assetKey: null,
227
+ assetLabel: null
228
+ }))
229
+ };
230
+ }
231
+ function normalizeTrackEvent(track = {}, mapping) {
232
+ return {
233
+ id: `${track.session_id || track.user_id || track.timestamp || Date.now()}-${track.event || "track"}`,
234
+ event: String(track.event || ""),
235
+ properties: track.properties || {},
236
+ userId: track.user_id || null,
237
+ sessionId: track.session_id || null,
238
+ timestamp: Number(track.timestamp || Date.now()),
239
+ country: track.country || null,
240
+ path: track.properties?.path || null,
241
+ assetKey: mapping.assetKey,
242
+ assetLabel: mapping.label
243
+ };
244
+ }
245
+ function applySnapshotToAssetState(currentState, snapshot, mapping, now = Date.now()) {
246
+ const normalized = normalizeLiveSnapshot(snapshot);
247
+ return {
248
+ ...createEmptyAssetState(mapping, now),
249
+ ...currentState,
250
+ ...mapping,
251
+ status: "live",
252
+ lastUpdatedAt: now,
253
+ lastSnapshotAt: normalized.timestamp,
254
+ lastPolledAt: now,
255
+ activeVisitors: normalized.activeVisitors,
256
+ activeSessions: normalized.activeSessions,
257
+ eventsPerMinute: normalized.eventsPerMinute,
258
+ topPages: normalized.topPages,
259
+ topEvents: normalized.topEvents,
260
+ countries: normalized.countries,
261
+ recentEvents: normalized.recentEvents.map((event) => ({
262
+ ...event,
263
+ assetKey: mapping.assetKey,
264
+ assetLabel: mapping.label
265
+ })),
266
+ errors: []
267
+ };
268
+ }
269
+ function applyTrackEventToAssetState(currentState, rawTrackEvent, mapping, now = Date.now()) {
270
+ const state = currentState || createEmptyAssetState(mapping, now);
271
+ const trackEvent = normalizeTrackEvent(rawTrackEvent, mapping);
272
+ const next = {
273
+ ...state,
274
+ ...mapping,
275
+ status: state.status === "idle" ? "streaming" : state.status,
276
+ lastUpdatedAt: now,
277
+ lastHotCountry: trackEvent.country || state.lastHotCountry,
278
+ recentEvents: [trackEvent, ...state.recentEvents].slice(0, 12)
279
+ };
280
+ if (trackEvent.path) {
281
+ const topPages = [...next.topPages];
282
+ const existing = topPages.find((row) => row.path === trackEvent.path);
283
+ if (existing) existing.visitors += 1;
284
+ else topPages.push({ path: trackEvent.path, visitors: 1 });
285
+ next.topPages = sortCountRows(topPages, "visitors").slice(0, 10);
286
+ }
287
+ if (trackEvent.event) {
288
+ const topEvents = [...next.topEvents];
289
+ const existing = topEvents.find((row) => row.event === trackEvent.event);
290
+ if (existing) existing.count += 1;
291
+ else topEvents.push({ event: trackEvent.event, count: 1 });
292
+ next.topEvents = sortCountRows(topEvents, "count").slice(0, 10);
293
+ }
294
+ if (trackEvent.country) {
295
+ const countries = [...next.countries];
296
+ const existing = countries.find((row) => row.country === trackEvent.country);
297
+ if (existing) {
298
+ existing.events += 1;
299
+ if (trackEvent.userId && existing.visitors === 0) existing.visitors = 1;
300
+ if (trackEvent.sessionId && existing.sessions === 0) existing.sessions = 1;
301
+ } else {
302
+ countries.push({
303
+ country: trackEvent.country,
304
+ visitors: trackEvent.userId ? 1 : 0,
305
+ sessions: trackEvent.sessionId ? 1 : 0,
306
+ events: 1
307
+ });
308
+ }
309
+ next.countries = sortCountries(countries).slice(0, 12);
310
+ }
311
+ next.eventsPerMinute = Math.max(next.eventsPerMinute, next.topEvents.reduce((sum, row) => sum + row.count, 0));
312
+ return next;
313
+ }
314
+ function createSnoozeExpiry(minutes = DEFAULT_SNOOZE_MINUTES, now = Date.now()) {
315
+ return now + clampNumber(minutes, 1, 240, DEFAULT_SNOOZE_MINUTES) * 6e4;
316
+ }
317
+ function isSnoozed(assetKey, snoozes = {}, now = Date.now()) {
318
+ const snoozeUntil = Number(snoozes[assetKey] || 0);
319
+ return Number.isFinite(snoozeUntil) && snoozeUntil > now;
320
+ }
321
+ function validateEnabledMappings(mappings = []) {
322
+ const enabled = mappings.filter((mapping) => mapping.enabled !== false);
323
+ const warnings = [];
324
+ const errors = [];
325
+ if (enabled.length > MAX_ENABLED_ASSET_STREAMS) {
326
+ errors.push(`Enable at most ${MAX_ENABLED_ASSET_STREAMS} assets at once because Agent Analytics live streams are capped per account.`);
327
+ }
328
+ const byProject = /* @__PURE__ */ new Map();
329
+ for (const mapping of enabled) {
330
+ if (!mapping.agentAnalyticsProject) {
331
+ errors.push(`Asset "${mapping.label}" is missing an Agent Analytics project.`);
332
+ continue;
333
+ }
334
+ const count = byProject.get(mapping.agentAnalyticsProject) || 0;
335
+ byProject.set(mapping.agentAnalyticsProject, count + 1);
336
+ }
337
+ for (const [project, count] of byProject.entries()) {
338
+ if (count > 1) {
339
+ warnings.push(`Project "${project}" is mapped multiple times. /live snapshots are project-scoped, so duplicate mappings can mirror the same activity.`);
340
+ }
341
+ }
342
+ return { warnings, errors };
343
+ }
344
+ function buildCompanyLiveState({ settings, auth, assets, snoozes = {}, now = Date.now() }) {
345
+ const liveState = createEmptyCompanyLiveState();
346
+ const visibleAssets = [];
347
+ const topPagesMap = /* @__PURE__ */ new Map();
348
+ const topEventsMap = /* @__PURE__ */ new Map();
349
+ const countryMap = /* @__PURE__ */ new Map();
350
+ const recentEvents = [];
351
+ let activeVisitors = 0;
352
+ let activeSessions = 0;
353
+ let eventsPerMinute = 0;
354
+ for (const asset of assets) {
355
+ if (isSnoozed(asset.assetKey, snoozes, now)) continue;
356
+ visibleAssets.push(asset);
357
+ activeVisitors += asset.activeVisitors || 0;
358
+ activeSessions += asset.activeSessions || 0;
359
+ eventsPerMinute += asset.eventsPerMinute || 0;
360
+ for (const page of asset.topPages || []) {
361
+ const current = topPagesMap.get(page.path) || 0;
362
+ topPagesMap.set(page.path, current + (page.visitors || 0));
363
+ }
364
+ for (const event of asset.topEvents || []) {
365
+ const current = topEventsMap.get(event.event) || 0;
366
+ topEventsMap.set(event.event, current + (event.count || 0));
367
+ }
368
+ for (const country of asset.countries || []) {
369
+ const current = countryMap.get(country.country) || { country: country.country, visitors: 0, sessions: 0, events: 0 };
370
+ current.visitors += country.visitors || 0;
371
+ current.sessions += country.sessions || 0;
372
+ current.events += country.events || 0;
373
+ countryMap.set(country.country, current);
374
+ }
375
+ for (const event of asset.recentEvents || []) {
376
+ recentEvents.push(event);
377
+ }
378
+ }
379
+ const warnings = validateEnabledMappings(settings.monitoredAssets).warnings;
380
+ const countries = sortCountries(Array.from(countryMap.values())).slice(0, 12);
381
+ const sortedAssets = [...visibleAssets].sort((left, right) => {
382
+ if ((right.eventsPerMinute || 0) !== (left.eventsPerMinute || 0)) return (right.eventsPerMinute || 0) - (left.eventsPerMinute || 0);
383
+ if ((right.activeVisitors || 0) !== (left.activeVisitors || 0)) return (right.activeVisitors || 0) - (left.activeVisitors || 0);
384
+ return String(left.label || "").localeCompare(String(right.label || ""));
385
+ });
386
+ liveState.generatedAt = now;
387
+ liveState.pluginEnabled = settings.pluginEnabled !== false;
388
+ liveState.authStatus = auth.status;
389
+ liveState.tier = auth.tier;
390
+ liveState.account = auth.accountSummary;
391
+ liveState.connection = {
392
+ status: auth.status === "connected" ? "live" : auth.status === "error" ? "error" : "idle",
393
+ label: auth.status === "connected" ? "Connected" : auth.status === "error" ? "Attention needed" : "Not connected",
394
+ detail: auth.status === "connected" ? `Showing ${sortedAssets.length} visible asset${sortedAssets.length === 1 ? "" : "s"} from the current live window.` : auth.lastError || "Connect Agent Analytics from settings to start the live feed."
395
+ };
396
+ liveState.metrics = {
397
+ activeVisitors,
398
+ activeSessions,
399
+ eventsPerMinute,
400
+ assetsConfigured: settings.monitoredAssets.length,
401
+ assetsVisible: sortedAssets.length,
402
+ countriesTracked: countries.length
403
+ };
404
+ liveState.world = {
405
+ hotCountry: sortedAssets.find((asset) => asset.lastHotCountry)?.lastHotCountry || countries[0]?.country || null,
406
+ countries
407
+ };
408
+ liveState.evidence = {
409
+ topPages: sortCountRows(
410
+ Array.from(topPagesMap.entries()).map(([path, visitors]) => ({ path, visitors })),
411
+ "visitors"
412
+ ).slice(0, 10),
413
+ topEvents: sortCountRows(
414
+ Array.from(topEventsMap.entries()).map(([event, count]) => ({ event, count })),
415
+ "count"
416
+ ).slice(0, 10),
417
+ recentEvents: recentEvents.sort((left, right) => (right.timestamp || 0) - (left.timestamp || 0)).slice(0, 20),
418
+ countries
419
+ };
420
+ liveState.assets = sortedAssets;
421
+ liveState.snoozedAssets = Object.keys(snoozes).filter((assetKey) => isSnoozed(assetKey, snoozes, now));
422
+ liveState.warnings = warnings;
423
+ return liveState;
424
+ }
425
+ function deriveWidgetSummary(companyLiveState) {
426
+ return {
427
+ connection: companyLiveState.connection,
428
+ tier: companyLiveState.tier,
429
+ metrics: companyLiveState.metrics,
430
+ topAsset: companyLiveState.assets[0] || null,
431
+ hotCountry: companyLiveState.world.hotCountry,
432
+ warnings: companyLiveState.warnings
433
+ };
434
+ }
435
+
436
+ // src/shared/agent-analytics-client.js
437
+ function createJsonHeaders(auth) {
438
+ const headers = {
439
+ "Content-Type": "application/json"
440
+ };
441
+ if (auth?.access_token) headers.Authorization = `Bearer ${auth.access_token}`;
442
+ else if (auth?.api_key) headers["X-API-Key"] = auth.api_key;
443
+ return headers;
444
+ }
445
+ function buildQuery(params) {
446
+ return Object.entries(params).filter(([, value]) => value !== null && value !== void 0 && value !== "").map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
447
+ }
448
+ function parseSseEvent(rawEvent) {
449
+ const event = {
450
+ event: "message",
451
+ data: "",
452
+ comment: null
453
+ };
454
+ const lines = rawEvent.split(/\r?\n/);
455
+ for (const line of lines) {
456
+ if (!line) continue;
457
+ if (line.startsWith(":")) {
458
+ event.comment = line.slice(1).trim();
459
+ continue;
460
+ }
461
+ const separatorIndex = line.indexOf(":");
462
+ const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
463
+ const value = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1).trimStart();
464
+ if (field === "event") event.event = value;
465
+ if (field === "data") event.data = event.data ? `${event.data}
466
+ ${value}` : value;
467
+ }
468
+ return event;
469
+ }
470
+ var AgentAnalyticsClient = class {
471
+ constructor({
472
+ auth = null,
473
+ baseUrl = DEFAULT_BASE_URL,
474
+ fetchImpl = globalThis.fetch,
475
+ onAuthUpdate = null
476
+ } = {}) {
477
+ this.auth = auth ? { ...auth } : null;
478
+ this.baseUrl = baseUrl;
479
+ this.fetchImpl = fetchImpl;
480
+ this.onAuthUpdate = onAuthUpdate;
481
+ }
482
+ setAuth(auth) {
483
+ this.auth = auth ? { ...auth } : null;
484
+ }
485
+ async request(method, path, body, { retryOnRefresh = true } = {}) {
486
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
487
+ method,
488
+ headers: createJsonHeaders(this.auth),
489
+ body: body ? JSON.stringify(body) : void 0
490
+ });
491
+ const data = await response.json().catch(() => ({}));
492
+ if (response.status === 401 && retryOnRefresh && this.auth?.refresh_token) {
493
+ const refreshed = await this.refreshAgentSession().catch(() => null);
494
+ if (refreshed?.access_token) {
495
+ return this.request(method, path, body, { retryOnRefresh: false });
496
+ }
497
+ }
498
+ if (!response.ok) {
499
+ throw new Error(data.message || data.error || `HTTP ${response.status}`);
500
+ }
501
+ return data;
502
+ }
503
+ async startPaperclipAuth({ companyId, label } = {}) {
504
+ return this.request(
505
+ "POST",
506
+ "/agent-sessions/start",
507
+ {
508
+ mode: "detached",
509
+ client_type: "paperclip",
510
+ client_name: PLUGIN_DISPLAY_NAME,
511
+ client_instance_id: companyId || null,
512
+ label: label || `Paperclip Company ${companyId || ""}`.trim(),
513
+ scopes: AGENT_SESSION_SCOPES,
514
+ metadata: {
515
+ platform: "paperclip",
516
+ plugin_id: PLUGIN_ID,
517
+ company_id: companyId || null
518
+ }
519
+ },
520
+ { retryOnRefresh: false }
521
+ );
522
+ }
523
+ async exchangeAgentSession(authRequestId, exchangeCode) {
524
+ return this.request(
525
+ "POST",
526
+ "/agent-sessions/exchange",
527
+ {
528
+ auth_request_id: authRequestId,
529
+ exchange_code: exchangeCode
530
+ },
531
+ { retryOnRefresh: false }
532
+ );
533
+ }
534
+ async refreshAgentSession() {
535
+ if (!this.auth?.refresh_token) {
536
+ throw new Error("No refresh token available");
537
+ }
538
+ const refreshed = await this.request(
539
+ "POST",
540
+ "/agent-sessions/refresh",
541
+ {
542
+ refresh_token: this.auth.refresh_token
543
+ },
544
+ { retryOnRefresh: false }
545
+ );
546
+ this.auth = {
547
+ ...this.auth,
548
+ ...refreshed.agent_session
549
+ };
550
+ this.onAuthUpdate?.(this.auth);
551
+ return this.auth;
552
+ }
553
+ async listProjects() {
554
+ return this.request("GET", "/projects");
555
+ }
556
+ async getLive(project, { window } = {}) {
557
+ const query = buildQuery({ project, window });
558
+ return this.request("GET", `/live?${query}`);
559
+ }
560
+ async subscribeToStream({ project, filter, signal, onConnected, onTrack, onComment }) {
561
+ const query = buildQuery({
562
+ project,
563
+ filter
564
+ });
565
+ const response = await this.fetchImpl(`${this.baseUrl}/stream?${query}`, {
566
+ method: "GET",
567
+ headers: createJsonHeaders(this.auth),
568
+ signal
569
+ });
570
+ if (!response.ok || !response.body) {
571
+ const payload = await response.text().catch(() => "");
572
+ throw new Error(payload || `Stream failed with HTTP ${response.status}`);
573
+ }
574
+ const reader = response.body.getReader();
575
+ const decoder = new TextDecoder();
576
+ let buffer = "";
577
+ while (true) {
578
+ const { done, value } = await reader.read();
579
+ if (done) break;
580
+ buffer += decoder.decode(value, { stream: true });
581
+ let boundary = buffer.indexOf("\n\n");
582
+ while (boundary !== -1) {
583
+ const rawEvent = buffer.slice(0, boundary);
584
+ buffer = buffer.slice(boundary + 2);
585
+ const event = parseSseEvent(rawEvent);
586
+ if (event.comment) {
587
+ onComment?.(event.comment);
588
+ }
589
+ if (event.data) {
590
+ let parsed = {};
591
+ try {
592
+ parsed = JSON.parse(event.data);
593
+ } catch {
594
+ parsed = { raw: event.data };
595
+ }
596
+ if (event.event === "connected") onConnected?.(parsed);
597
+ if (event.event === "track") onTrack?.(parsed);
598
+ }
599
+ boundary = buffer.indexOf("\n\n");
600
+ }
601
+ }
602
+ }
603
+ };
604
+
605
+ // src/worker/paperclip.js
606
+ async function registerDataHandler(ctx, key, handler) {
607
+ if (ctx?.data?.register) {
608
+ return ctx.data.register(key, handler);
609
+ }
610
+ if (ctx?.registerData) {
611
+ return ctx.registerData(key, handler);
612
+ }
613
+ throw new Error(`Paperclip data registration is unavailable for key "${key}"`);
614
+ }
615
+ async function registerActionHandler(ctx, key, handler) {
616
+ if (ctx?.actions?.register) {
617
+ return ctx.actions.register(key, handler);
618
+ }
619
+ if (ctx?.registerAction) {
620
+ return ctx.registerAction(key, handler);
621
+ }
622
+ throw new Error(`Paperclip action registration is unavailable for key "${key}"`);
623
+ }
624
+ async function openLiveChannel(ctx, companyId) {
625
+ if (!ctx?.streams?.open) return;
626
+ await ctx.streams.open(LIVE_STREAM_CHANNEL, companyId);
627
+ }
628
+ async function emitLiveState(ctx, companyId, payload) {
629
+ if (!ctx?.streams?.emit) return;
630
+ await ctx.streams.emit(LIVE_STREAM_CHANNEL, payload);
631
+ }
632
+ async function closeLiveChannel(ctx, companyId) {
633
+ if (!ctx?.streams?.close) return;
634
+ await ctx.streams.close(LIVE_STREAM_CHANNEL, companyId);
635
+ }
636
+
637
+ // src/worker/state.js
638
+ function createScope(companyId) {
639
+ return {
640
+ scopeKind: "company",
641
+ scopeId: companyId,
642
+ namespace: STATE_NAMESPACE
643
+ };
644
+ }
645
+ async function getValue(ctx, companyId, key, fallbackValue) {
646
+ if (!ctx?.state?.get) return fallbackValue;
647
+ const value = await ctx.state.get({
648
+ ...createScope(String(companyId)),
649
+ stateKey: key
650
+ });
651
+ return value ?? fallbackValue;
652
+ }
653
+ async function setValue(ctx, companyId, key, value) {
654
+ if (!ctx?.state?.set) return value;
655
+ await ctx.state.set({
656
+ ...createScope(String(companyId)),
657
+ stateKey: key,
658
+ value
659
+ });
660
+ return value;
661
+ }
662
+ async function loadSettings(ctx, companyId) {
663
+ return getValue(ctx, companyId, "config", createDefaultSettings());
664
+ }
665
+ async function saveSettings(ctx, companyId, settings) {
666
+ return setValue(ctx, companyId, "config", settings);
667
+ }
668
+ async function loadAuthState(ctx, companyId) {
669
+ return getValue(ctx, companyId, "auth", createDefaultAuthState());
670
+ }
671
+ async function saveAuthState(ctx, companyId, authState) {
672
+ return setValue(ctx, companyId, "auth", authState);
673
+ }
674
+ async function loadSnoozes(ctx, companyId) {
675
+ return getValue(ctx, companyId, "snoozes", createDefaultSnoozeState());
676
+ }
677
+ async function saveSnoozes(ctx, companyId, snoozes) {
678
+ return setValue(ctx, companyId, "snoozes", snoozes);
679
+ }
680
+
681
+ // src/worker/service.js
682
+ function delay(ms) {
683
+ return new Promise((resolve) => setTimeout(resolve, ms));
684
+ }
685
+ function serializeAccount(account) {
686
+ if (!account) return null;
687
+ return {
688
+ id: account.id,
689
+ email: account.email,
690
+ githubLogin: account.github_login || null,
691
+ googleName: account.google_name || null
692
+ };
693
+ }
694
+ function toPublicAuthState(auth) {
695
+ return {
696
+ status: auth.status,
697
+ mode: auth.mode,
698
+ tier: auth.tier,
699
+ accountSummary: auth.accountSummary,
700
+ accessExpiresAt: auth.accessExpiresAt,
701
+ refreshExpiresAt: auth.refreshExpiresAt,
702
+ pendingAuthRequest: auth.pendingAuthRequest,
703
+ lastValidatedAt: auth.lastValidatedAt,
704
+ lastError: auth.lastError,
705
+ connected: Boolean(auth.accessToken)
706
+ };
707
+ }
708
+ var PaperclipLiveAnalyticsService = class {
709
+ constructor(ctx, { fetchImpl = globalThis.fetch } = {}) {
710
+ this.ctx = ctx;
711
+ this.fetchImpl = fetchImpl;
712
+ this.runtimes = /* @__PURE__ */ new Map();
713
+ }
714
+ async register() {
715
+ await registerDataHandler(this.ctx, DATA_KEYS.livePageLoad, (input) => this.loadLivePage(input));
716
+ await registerDataHandler(this.ctx, DATA_KEYS.liveWidgetLoad, (input) => this.loadLiveWidget(input));
717
+ await registerDataHandler(this.ctx, DATA_KEYS.settingsLoad, (input) => this.loadSettingsData(input));
718
+ await registerActionHandler(this.ctx, ACTION_KEYS.authStart, (input) => this.startAuth(input));
719
+ await registerActionHandler(this.ctx, ACTION_KEYS.authComplete, (input) => this.completeAuth(input));
720
+ await registerActionHandler(this.ctx, ACTION_KEYS.authReconnect, (input) => this.reconnectAuth(input));
721
+ await registerActionHandler(this.ctx, ACTION_KEYS.authDisconnect, (input) => this.disconnectAuth(input));
722
+ await registerActionHandler(this.ctx, ACTION_KEYS.settingsSave, (input) => this.savePluginSettings(input));
723
+ await registerActionHandler(this.ctx, ACTION_KEYS.mappingUpsert, (input) => this.upsertMapping(input));
724
+ await registerActionHandler(this.ctx, ACTION_KEYS.mappingRemove, (input) => this.removeMapping(input));
725
+ await registerActionHandler(this.ctx, ACTION_KEYS.assetSnooze, (input) => this.snoozeAsset(input));
726
+ await registerActionHandler(this.ctx, ACTION_KEYS.assetUnsnooze, (input) => this.unsnoozeAsset(input));
727
+ }
728
+ async shutdown() {
729
+ for (const [companyId] of this.runtimes.entries()) {
730
+ await this.stopRuntime(companyId);
731
+ }
732
+ }
733
+ async loadLivePage({ companyId }) {
734
+ const liveState = await this.ensureLiveState(companyId);
735
+ return liveState;
736
+ }
737
+ async loadLiveWidget({ companyId }) {
738
+ const liveState = await this.ensureLiveState(companyId);
739
+ return deriveWidgetSummary(liveState);
740
+ }
741
+ async loadSettingsData({ companyId }) {
742
+ const settings = await loadSettings(this.ctx, companyId);
743
+ const auth = await loadAuthState(this.ctx, companyId);
744
+ const validation = validateEnabledMappings(settings.monitoredAssets);
745
+ const projects = await this.listProjectsForCompany(companyId).catch((error) => ({
746
+ projects: [],
747
+ tier: auth.tier,
748
+ error: error.message
749
+ }));
750
+ return {
751
+ settings,
752
+ auth: toPublicAuthState(auth),
753
+ discoveredProjects: projects.projects || [],
754
+ validation,
755
+ projectListError: projects.error || null
756
+ };
757
+ }
758
+ async startAuth({ companyId, label }) {
759
+ const settings = await loadSettings(this.ctx, companyId);
760
+ const auth = await loadAuthState(this.ctx, companyId);
761
+ const client = this.createClient(companyId, settings, auth);
762
+ const started = await client.startPaperclipAuth({ companyId, label });
763
+ const nextAuth = {
764
+ ...auth,
765
+ status: "pending",
766
+ lastError: null,
767
+ pendingAuthRequest: {
768
+ authRequestId: started.auth_request_id,
769
+ authorizeUrl: started.authorize_url,
770
+ approvalCode: started.approval_code,
771
+ pollToken: started.poll_token,
772
+ expiresAt: started.expires_at
773
+ }
774
+ };
775
+ await saveAuthState(this.ctx, companyId, nextAuth);
776
+ return {
777
+ auth: toPublicAuthState(nextAuth)
778
+ };
779
+ }
780
+ async completeAuth({ companyId, authRequestId, exchangeCode }) {
781
+ const settings = await loadSettings(this.ctx, companyId);
782
+ const auth = await loadAuthState(this.ctx, companyId);
783
+ const requestId = authRequestId || auth.pendingAuthRequest?.authRequestId;
784
+ if (!requestId || !exchangeCode) {
785
+ throw new Error("authRequestId and exchangeCode are required");
786
+ }
787
+ const client = this.createClient(companyId, settings, auth);
788
+ const exchanged = await client.exchangeAgentSession(requestId, exchangeCode);
789
+ const nextAuth = {
790
+ mode: "agent_session",
791
+ accessToken: exchanged.agent_session.access_token,
792
+ refreshToken: exchanged.agent_session.refresh_token,
793
+ accessExpiresAt: exchanged.agent_session.access_expires_at,
794
+ refreshExpiresAt: exchanged.agent_session.refresh_expires_at,
795
+ accountSummary: serializeAccount(exchanged.account),
796
+ tier: exchanged.account?.tier || null,
797
+ status: "connected",
798
+ pendingAuthRequest: null,
799
+ lastValidatedAt: Date.now(),
800
+ lastError: null
801
+ };
802
+ await saveAuthState(this.ctx, companyId, nextAuth);
803
+ await this.ensureLiveState(companyId, { forceSync: true });
804
+ return this.loadSettingsData({ companyId });
805
+ }
806
+ async reconnectAuth({ companyId }) {
807
+ const settings = await loadSettings(this.ctx, companyId);
808
+ const auth = await loadAuthState(this.ctx, companyId);
809
+ if (!auth.refreshToken) {
810
+ return this.startAuth({ companyId });
811
+ }
812
+ const client = this.createClient(companyId, settings, auth);
813
+ const refreshed = await client.refreshAgentSession();
814
+ const nextAuth = {
815
+ ...auth,
816
+ accessToken: refreshed.access_token,
817
+ refreshToken: refreshed.refresh_token || auth.refreshToken,
818
+ accessExpiresAt: refreshed.access_expires_at,
819
+ refreshExpiresAt: refreshed.refresh_expires_at || auth.refreshExpiresAt,
820
+ status: "connected",
821
+ lastValidatedAt: Date.now(),
822
+ lastError: null
823
+ };
824
+ await saveAuthState(this.ctx, companyId, nextAuth);
825
+ await this.ensureLiveState(companyId, { forceSync: true });
826
+ return this.loadSettingsData({ companyId });
827
+ }
828
+ async disconnectAuth({ companyId }) {
829
+ const auth = await loadAuthState(this.ctx, companyId);
830
+ const nextAuth = {
831
+ ...auth,
832
+ accessToken: null,
833
+ refreshToken: null,
834
+ accessExpiresAt: null,
835
+ refreshExpiresAt: null,
836
+ status: "disconnected",
837
+ pendingAuthRequest: null,
838
+ lastError: null
839
+ };
840
+ await saveAuthState(this.ctx, companyId, nextAuth);
841
+ await this.stopRuntime(companyId);
842
+ return this.loadSettingsData({ companyId });
843
+ }
844
+ async savePluginSettings({ companyId, settings: partialSettings = {} }) {
845
+ const currentSettings = await loadSettings(this.ctx, companyId);
846
+ const nextSettings = {
847
+ ...currentSettings,
848
+ agentAnalyticsBaseUrl: partialSettings.agentAnalyticsBaseUrl || currentSettings.agentAnalyticsBaseUrl || DEFAULT_BASE_URL,
849
+ liveWindowSeconds: clampLiveWindowSeconds(partialSettings.liveWindowSeconds ?? currentSettings.liveWindowSeconds),
850
+ pollIntervalSeconds: clampPollIntervalSeconds(partialSettings.pollIntervalSeconds ?? currentSettings.pollIntervalSeconds),
851
+ pluginEnabled: partialSettings.pluginEnabled ?? currentSettings.pluginEnabled
852
+ };
853
+ const validation = validateEnabledMappings(nextSettings.monitoredAssets);
854
+ if (validation.errors.length > 0) {
855
+ throw new Error(validation.errors.join(" "));
856
+ }
857
+ await saveSettings(this.ctx, companyId, nextSettings);
858
+ await this.ensureLiveState(companyId, { forceSync: true });
859
+ return this.loadSettingsData({ companyId });
860
+ }
861
+ async upsertMapping({ companyId, mapping }) {
862
+ const settings = await loadSettings(this.ctx, companyId);
863
+ const normalized = normalizeAssetMapping(mapping);
864
+ const monitoredAssets = [...settings.monitoredAssets];
865
+ const existingIndex = monitoredAssets.findIndex((entry) => entry.assetKey === normalized.assetKey);
866
+ if (existingIndex === -1) monitoredAssets.push(normalized);
867
+ else monitoredAssets.splice(existingIndex, 1, normalized);
868
+ const validation = validateEnabledMappings(monitoredAssets);
869
+ if (validation.errors.length > 0) {
870
+ throw new Error(validation.errors.join(" "));
871
+ }
872
+ const nextSettings = {
873
+ ...settings,
874
+ monitoredAssets
875
+ };
876
+ await saveSettings(this.ctx, companyId, nextSettings);
877
+ await this.ensureLiveState(companyId, { forceSync: true });
878
+ return this.loadSettingsData({ companyId });
879
+ }
880
+ async removeMapping({ companyId, assetKey }) {
881
+ const settings = await loadSettings(this.ctx, companyId);
882
+ const nextSettings = {
883
+ ...settings,
884
+ monitoredAssets: settings.monitoredAssets.filter((mapping) => mapping.assetKey !== assetKey)
885
+ };
886
+ await saveSettings(this.ctx, companyId, nextSettings);
887
+ await this.ensureLiveState(companyId, { forceSync: true });
888
+ return this.loadSettingsData({ companyId });
889
+ }
890
+ async snoozeAsset({ companyId, assetKey, minutes = DEFAULT_SNOOZE_MINUTES }) {
891
+ const snoozes = await loadSnoozes(this.ctx, companyId);
892
+ const nextSnoozes = {
893
+ ...snoozes,
894
+ [assetKey]: createSnoozeExpiry(minutes)
895
+ };
896
+ await saveSnoozes(this.ctx, companyId, nextSnoozes);
897
+ const liveState = await this.ensureLiveState(companyId);
898
+ return {
899
+ snoozes: nextSnoozes,
900
+ liveState
901
+ };
902
+ }
903
+ async unsnoozeAsset({ companyId, assetKey }) {
904
+ const snoozes = await loadSnoozes(this.ctx, companyId);
905
+ const nextSnoozes = { ...snoozes };
906
+ delete nextSnoozes[assetKey];
907
+ await saveSnoozes(this.ctx, companyId, nextSnoozes);
908
+ const liveState = await this.ensureLiveState(companyId);
909
+ return {
910
+ snoozes: nextSnoozes,
911
+ liveState
912
+ };
913
+ }
914
+ async ensureLiveState(companyId, { forceSync = false } = {}) {
915
+ const settings = await loadSettings(this.ctx, companyId);
916
+ const auth = await loadAuthState(this.ctx, companyId);
917
+ const snoozes = await loadSnoozes(this.ctx, companyId);
918
+ const runtime = this.getRuntime(companyId);
919
+ if (forceSync) {
920
+ await this.syncRuntime(companyId, settings, auth, runtime);
921
+ } else if (!runtime.lastState) {
922
+ await this.syncRuntime(companyId, settings, auth, runtime);
923
+ }
924
+ const assetStates = settings.monitoredAssets.map((mapping) => {
925
+ const normalized = normalizeAssetMapping(mapping);
926
+ return runtime.assetStates.get(normalized.assetKey) || createEmptyAssetState(normalized);
927
+ });
928
+ const liveState = buildCompanyLiveState({
929
+ settings,
930
+ auth,
931
+ assets: assetStates,
932
+ snoozes
933
+ });
934
+ runtime.lastState = liveState;
935
+ return liveState;
936
+ }
937
+ async syncRuntime(companyId, settings, auth, runtime) {
938
+ const mappings = settings.monitoredAssets.map(normalizeAssetMapping);
939
+ const validation = validateEnabledMappings(mappings);
940
+ if (!settings.pluginEnabled || !auth.accessToken || validation.errors.length > 0) {
941
+ auth.status = validation.errors.length > 0 ? "error" : auth.status;
942
+ if (validation.errors.length > 0) auth.lastError = validation.errors.join(" ");
943
+ await saveAuthState(this.ctx, companyId, auth);
944
+ await this.stopRuntime(companyId, { keepState: true });
945
+ return;
946
+ }
947
+ await openLiveChannel(this.ctx, companyId);
948
+ for (const mapping of mappings) {
949
+ const current = runtime.assetStates.get(mapping.assetKey) || createEmptyAssetState(mapping);
950
+ runtime.assetStates.set(mapping.assetKey, {
951
+ ...current,
952
+ ...mapping
953
+ });
954
+ }
955
+ for (const assetKey of Array.from(runtime.assetStates.keys())) {
956
+ if (!mappings.find((mapping) => mapping.assetKey === assetKey)) {
957
+ runtime.assetStates.delete(assetKey);
958
+ }
959
+ }
960
+ const enabledMappings = mappings.filter((mapping) => mapping.enabled !== false).slice(0, MAX_ENABLED_ASSET_STREAMS);
961
+ const groupedMappings = /* @__PURE__ */ new Map();
962
+ for (const mapping of enabledMappings) {
963
+ const signature = mappingSignature(mapping);
964
+ const group = groupedMappings.get(signature) || [];
965
+ group.push(mapping);
966
+ groupedMappings.set(signature, group);
967
+ }
968
+ for (const [signature, streamRuntime] of runtime.streams.entries()) {
969
+ if (!groupedMappings.has(signature)) {
970
+ streamRuntime.controller.abort();
971
+ runtime.streams.delete(signature);
972
+ }
973
+ }
974
+ for (const [signature, group] of groupedMappings.entries()) {
975
+ const existing = runtime.streams.get(signature);
976
+ if (existing) {
977
+ existing.mappings = group;
978
+ } else {
979
+ runtime.streams.set(signature, this.startStreamLoop(companyId, settings, auth, group));
980
+ }
981
+ }
982
+ for (const mapping of enabledMappings) {
983
+ const poller = runtime.pollers.get(mapping.assetKey);
984
+ if (poller) {
985
+ clearInterval(poller);
986
+ }
987
+ const intervalId = setInterval(() => {
988
+ this.refreshSnapshot(companyId, mapping).catch((error) => this.recordAssetError(companyId, mapping.assetKey, error));
989
+ }, settings.pollIntervalSeconds * 1e3);
990
+ runtime.pollers.set(mapping.assetKey, intervalId);
991
+ await this.refreshSnapshot(companyId, mapping);
992
+ }
993
+ for (const [assetKey, intervalId] of runtime.pollers.entries()) {
994
+ if (!enabledMappings.find((mapping) => mapping.assetKey === assetKey)) {
995
+ clearInterval(intervalId);
996
+ runtime.pollers.delete(assetKey);
997
+ }
998
+ }
999
+ }
1000
+ createClient(companyId, settings, auth) {
1001
+ return new AgentAnalyticsClient({
1002
+ auth: {
1003
+ access_token: auth.accessToken,
1004
+ refresh_token: auth.refreshToken
1005
+ },
1006
+ baseUrl: settings.agentAnalyticsBaseUrl || DEFAULT_BASE_URL,
1007
+ fetchImpl: this.fetchImpl,
1008
+ onAuthUpdate: async (nextAuth) => {
1009
+ const current = await loadAuthState(this.ctx, companyId);
1010
+ await saveAuthState(this.ctx, companyId, {
1011
+ ...current,
1012
+ accessToken: nextAuth.access_token,
1013
+ refreshToken: nextAuth.refresh_token || current.refreshToken,
1014
+ accessExpiresAt: nextAuth.access_expires_at,
1015
+ refreshExpiresAt: nextAuth.refresh_expires_at || current.refreshExpiresAt,
1016
+ status: "connected",
1017
+ lastValidatedAt: Date.now(),
1018
+ lastError: null
1019
+ });
1020
+ }
1021
+ });
1022
+ }
1023
+ getRuntime(companyId) {
1024
+ let runtime = this.runtimes.get(companyId);
1025
+ if (!runtime) {
1026
+ runtime = {
1027
+ pollers: /* @__PURE__ */ new Map(),
1028
+ streams: /* @__PURE__ */ new Map(),
1029
+ assetStates: /* @__PURE__ */ new Map(),
1030
+ lastState: null
1031
+ };
1032
+ this.runtimes.set(companyId, runtime);
1033
+ }
1034
+ return runtime;
1035
+ }
1036
+ startStreamLoop(companyId, settings, auth, mappings) {
1037
+ const controller = new AbortController();
1038
+ const runtime = {
1039
+ controller,
1040
+ mappings,
1041
+ run: (async () => {
1042
+ while (!controller.signal.aborted) {
1043
+ try {
1044
+ const currentSettings = await loadSettings(this.ctx, companyId);
1045
+ const currentAuth = await loadAuthState(this.ctx, companyId);
1046
+ const client = this.createClient(companyId, currentSettings, currentAuth);
1047
+ const primaryMapping = mappings[0];
1048
+ await client.subscribeToStream({
1049
+ project: primaryMapping.agentAnalyticsProject,
1050
+ filter: primaryMapping.primaryHostname ? `hostname:${primaryMapping.primaryHostname}` : null,
1051
+ signal: controller.signal,
1052
+ onTrack: async (track) => {
1053
+ await this.applyTrackEvent(companyId, mappings, track);
1054
+ }
1055
+ });
1056
+ } catch (error) {
1057
+ if (!controller.signal.aborted) {
1058
+ for (const mapping of mappings) {
1059
+ await this.recordAssetError(companyId, mapping.assetKey, error);
1060
+ }
1061
+ await delay(2e3);
1062
+ }
1063
+ }
1064
+ }
1065
+ })()
1066
+ };
1067
+ return runtime;
1068
+ }
1069
+ async refreshSnapshot(companyId, mapping) {
1070
+ const settings = await loadSettings(this.ctx, companyId);
1071
+ const auth = await loadAuthState(this.ctx, companyId);
1072
+ const client = this.createClient(companyId, settings, auth);
1073
+ const snapshot = await client.getLive(mapping.agentAnalyticsProject, {
1074
+ window: settings.liveWindowSeconds
1075
+ });
1076
+ const runtime = this.getRuntime(companyId);
1077
+ const currentState = runtime.assetStates.get(mapping.assetKey) || createEmptyAssetState(mapping);
1078
+ runtime.assetStates.set(mapping.assetKey, applySnapshotToAssetState(currentState, snapshot, mapping));
1079
+ await this.publish(companyId);
1080
+ }
1081
+ async applyTrackEvent(companyId, mappings, track) {
1082
+ const runtime = this.getRuntime(companyId);
1083
+ for (const mapping of mappings) {
1084
+ const currentState = runtime.assetStates.get(mapping.assetKey) || createEmptyAssetState(mapping);
1085
+ runtime.assetStates.set(mapping.assetKey, applyTrackEventToAssetState(currentState, track, mapping));
1086
+ }
1087
+ await this.publish(companyId);
1088
+ }
1089
+ async recordAssetError(companyId, assetKey, error) {
1090
+ const runtime = this.getRuntime(companyId);
1091
+ const current = runtime.assetStates.get(assetKey);
1092
+ if (!current) return;
1093
+ runtime.assetStates.set(assetKey, {
1094
+ ...current,
1095
+ status: "error",
1096
+ errors: [error.message || String(error)],
1097
+ lastUpdatedAt: Date.now()
1098
+ });
1099
+ const auth = await loadAuthState(this.ctx, companyId);
1100
+ await saveAuthState(this.ctx, companyId, {
1101
+ ...auth,
1102
+ status: "error",
1103
+ lastError: error.message || String(error)
1104
+ });
1105
+ await this.publish(companyId);
1106
+ }
1107
+ async publish(companyId) {
1108
+ const liveState = await this.ensureLiveState(companyId);
1109
+ await emitLiveState(this.ctx, companyId, liveState);
1110
+ }
1111
+ async listProjectsForCompany(companyId) {
1112
+ const settings = await loadSettings(this.ctx, companyId);
1113
+ const auth = await loadAuthState(this.ctx, companyId);
1114
+ if (!auth.accessToken) {
1115
+ return { projects: [], tier: auth.tier, error: null };
1116
+ }
1117
+ const client = this.createClient(companyId, settings, auth);
1118
+ return client.listProjects();
1119
+ }
1120
+ async stopRuntime(companyId, { keepState = false } = {}) {
1121
+ const runtime = this.runtimes.get(companyId);
1122
+ if (!runtime) return;
1123
+ for (const intervalId of runtime.pollers.values()) {
1124
+ clearInterval(intervalId);
1125
+ }
1126
+ runtime.pollers.clear();
1127
+ for (const streamRuntime of runtime.streams.values()) {
1128
+ streamRuntime.controller.abort();
1129
+ }
1130
+ runtime.streams.clear();
1131
+ if (!keepState) {
1132
+ runtime.assetStates.clear();
1133
+ runtime.lastState = null;
1134
+ this.runtimes.delete(companyId);
1135
+ }
1136
+ await closeLiveChannel(this.ctx, companyId);
1137
+ }
1138
+ };
1139
+
1140
+ // src/paperclip/worker-entry.js
1141
+ var service = null;
1142
+ var plugin = definePlugin({
1143
+ async setup(ctx) {
1144
+ service = new PaperclipLiveAnalyticsService(ctx, {
1145
+ fetchImpl: ctx.http?.fetch?.bind(ctx.http) || globalThis.fetch
1146
+ });
1147
+ await service.register();
1148
+ },
1149
+ async onHealth() {
1150
+ return {
1151
+ status: "ok",
1152
+ details: {
1153
+ pluginId: PLUGIN_ID
1154
+ }
1155
+ };
1156
+ },
1157
+ async onShutdown() {
1158
+ await service?.shutdown?.();
1159
+ }
1160
+ });
1161
+ var worker_entry_default = plugin;
1162
+ runWorker(plugin, import.meta.url);
1163
+ export {
1164
+ worker_entry_default as default
1165
+ };