@stackbilt/aegis-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/package.json +96 -0
  2. package/schema.sql +586 -0
  3. package/src/adapters/voice/cloudflare-agent.ts +34 -0
  4. package/src/auth.ts +124 -0
  5. package/src/bluesky.ts +464 -0
  6. package/src/claude-tools/content.ts +188 -0
  7. package/src/claude-tools/email.ts +69 -0
  8. package/src/claude-tools/github.ts +440 -0
  9. package/src/claude-tools/goals.ts +116 -0
  10. package/src/claude-tools/index.ts +353 -0
  11. package/src/claude-tools/web.ts +59 -0
  12. package/src/claude.ts +406 -0
  13. package/src/codebeast.ts +200 -0
  14. package/src/composite.ts +715 -0
  15. package/src/content/column.ts +80 -0
  16. package/src/content/hero-image.ts +47 -0
  17. package/src/content/index.ts +27 -0
  18. package/src/content/journal.ts +91 -0
  19. package/src/content/roundtable.ts +163 -0
  20. package/src/core.ts +309 -0
  21. package/src/dashboard.ts +620 -0
  22. package/src/decision-docs.ts +284 -0
  23. package/src/dispatch.ts +13 -0
  24. package/src/edge-env.ts +58 -0
  25. package/src/email.ts +850 -0
  26. package/src/exports.ts +156 -0
  27. package/src/github-projects.ts +312 -0
  28. package/src/github.ts +670 -0
  29. package/src/groq.ts +247 -0
  30. package/src/health-page.ts +578 -0
  31. package/src/index.ts +89 -0
  32. package/src/kernel/argus-actions.ts +397 -0
  33. package/src/kernel/argus-correlation.ts +639 -0
  34. package/src/kernel/board.ts +91 -0
  35. package/src/kernel/briefing.ts +177 -0
  36. package/src/kernel/classify-memory-topic.ts +166 -0
  37. package/src/kernel/cognition.ts +377 -0
  38. package/src/kernel/court-cards.ts +163 -0
  39. package/src/kernel/dispatch.ts +587 -0
  40. package/src/kernel/domain.ts +50 -0
  41. package/src/kernel/dynamic-tools.ts +322 -0
  42. package/src/kernel/executor-port.ts +45 -0
  43. package/src/kernel/executors/claude.ts +73 -0
  44. package/src/kernel/executors/direct.ts +237 -0
  45. package/src/kernel/executors/groq.ts +18 -0
  46. package/src/kernel/executors/index.ts +87 -0
  47. package/src/kernel/executors/tarotscript.ts +104 -0
  48. package/src/kernel/executors/workers-ai.ts +54 -0
  49. package/src/kernel/insight-cache.ts +76 -0
  50. package/src/kernel/memory/agenda.ts +200 -0
  51. package/src/kernel/memory/blocks.ts +188 -0
  52. package/src/kernel/memory/consolidation.ts +194 -0
  53. package/src/kernel/memory/episodic.ts +241 -0
  54. package/src/kernel/memory/goals.ts +156 -0
  55. package/src/kernel/memory/graph.ts +290 -0
  56. package/src/kernel/memory/index.ts +11 -0
  57. package/src/kernel/memory/insights.ts +316 -0
  58. package/src/kernel/memory/procedural.ts +467 -0
  59. package/src/kernel/memory/pruning.ts +67 -0
  60. package/src/kernel/memory/recall.ts +367 -0
  61. package/src/kernel/memory/semantic.ts +315 -0
  62. package/src/kernel/memory/synthesis.ts +161 -0
  63. package/src/kernel/memory-adapter.ts +369 -0
  64. package/src/kernel/memory-guardrails.ts +76 -0
  65. package/src/kernel/port.ts +23 -0
  66. package/src/kernel/resilience.ts +322 -0
  67. package/src/kernel/router.ts +471 -0
  68. package/src/kernel/scheduled/agent-dispatch.ts +252 -0
  69. package/src/kernel/scheduled/argus-analytics.ts +247 -0
  70. package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
  71. package/src/kernel/scheduled/argus-notify.ts +348 -0
  72. package/src/kernel/scheduled/board-sync.ts +110 -0
  73. package/src/kernel/scheduled/ci-watcher.ts +125 -0
  74. package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
  75. package/src/kernel/scheduled/consolidation.ts +229 -0
  76. package/src/kernel/scheduled/content-drip.ts +47 -0
  77. package/src/kernel/scheduled/content.ts +6 -0
  78. package/src/kernel/scheduled/conversation-facts.ts +204 -0
  79. package/src/kernel/scheduled/cost-report.ts +84 -0
  80. package/src/kernel/scheduled/curiosity.ts +219 -0
  81. package/src/kernel/scheduled/dev-activity.ts +44 -0
  82. package/src/kernel/scheduled/digest.ts +317 -0
  83. package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
  84. package/src/kernel/scheduled/dreaming/facts.ts +239 -0
  85. package/src/kernel/scheduled/dreaming/index.ts +8 -0
  86. package/src/kernel/scheduled/dreaming/llm.ts +33 -0
  87. package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
  88. package/src/kernel/scheduled/dreaming/persona.ts +75 -0
  89. package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
  90. package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
  91. package/src/kernel/scheduled/dreaming.ts +66 -0
  92. package/src/kernel/scheduled/entropy.ts +149 -0
  93. package/src/kernel/scheduled/escalation.ts +192 -0
  94. package/src/kernel/scheduled/feed-watcher.ts +206 -0
  95. package/src/kernel/scheduled/goals.ts +214 -0
  96. package/src/kernel/scheduled/governance.ts +41 -0
  97. package/src/kernel/scheduled/heartbeat.ts +220 -0
  98. package/src/kernel/scheduled/inbox-processor.ts +174 -0
  99. package/src/kernel/scheduled/index.ts +245 -0
  100. package/src/kernel/scheduled/issue-proposer.ts +478 -0
  101. package/src/kernel/scheduled/issue-watcher.ts +128 -0
  102. package/src/kernel/scheduled/pr-automerge.ts +213 -0
  103. package/src/kernel/scheduled/product-health.ts +107 -0
  104. package/src/kernel/scheduled/reflection.ts +373 -0
  105. package/src/kernel/scheduled/self-improvement.ts +114 -0
  106. package/src/kernel/scheduled/social-engage.ts +175 -0
  107. package/src/kernel/scheduled/task-audit.ts +60 -0
  108. package/src/kernel/symbolic.ts +156 -0
  109. package/src/kernel/types.ts +145 -0
  110. package/src/landing.ts +1190 -0
  111. package/src/lib/audit-chain/chain.ts +28 -0
  112. package/src/lib/audit-chain/types.ts +12 -0
  113. package/src/lib/observability/errors.ts +55 -0
  114. package/src/markdown.ts +164 -0
  115. package/src/mcp/handlers.ts +647 -0
  116. package/src/mcp/server.ts +184 -0
  117. package/src/mcp/tools.ts +316 -0
  118. package/src/mcp-client.ts +275 -0
  119. package/src/mcp-server.ts +2 -0
  120. package/src/operator/config.example.ts +60 -0
  121. package/src/operator/config.ts +60 -0
  122. package/src/operator/index.ts +46 -0
  123. package/src/operator/persona.example.ts +34 -0
  124. package/src/operator/persona.ts +34 -0
  125. package/src/operator/prompt-builder.ts +190 -0
  126. package/src/operator/types.ts +43 -0
  127. package/src/pulse.ts +1179 -0
  128. package/src/routes/bluesky.ts +116 -0
  129. package/src/routes/cc-tasks.ts +328 -0
  130. package/src/routes/codebeast.ts +1 -0
  131. package/src/routes/content.ts +194 -0
  132. package/src/routes/conversations.ts +25 -0
  133. package/src/routes/dynamic-tools.ts +111 -0
  134. package/src/routes/feedback.ts +192 -0
  135. package/src/routes/health.ts +147 -0
  136. package/src/routes/messages.ts +228 -0
  137. package/src/routes/observability.ts +82 -0
  138. package/src/routes/operator-logs.ts +42 -0
  139. package/src/routes/pages.ts +96 -0
  140. package/src/routes/sessions.ts +54 -0
  141. package/src/sanitize.ts +73 -0
  142. package/src/schema-enums.ts +155 -0
  143. package/src/search.ts +112 -0
  144. package/src/task-intelligence.ts +497 -0
  145. package/src/types.ts +194 -0
  146. package/src/ui.ts +5 -0
  147. package/src/version.ts +3 -0
  148. package/src/workers-ai-chat.ts +333 -0
@@ -0,0 +1,252 @@
1
+ // --- Agent Dispatch -- Proactive Inter-Agent Coordination ---
2
+ // Runs hourly in the heartbeat phase. Scans recent events, task
3
+ // completions, and repo activity, then inboxes the right agent
4
+ // without human intervention.
5
+ //
6
+ // AEGIS is the orchestrator. Agents are specialists:
7
+ // CodeBeast -- adversarial code review
8
+ // MARA -- colony governance, multi-agent coordination
9
+ // Sera -- symbolic classification
10
+ //
11
+ // This module replaces the operator as the manual message bus.
12
+
13
+ import { type EdgeEnv } from '../dispatch.js';
14
+
15
+ // --- Agent Capability Registry ---
16
+
17
+ interface AgentCapability {
18
+ id: string;
19
+ name: string;
20
+ capabilities: string[];
21
+ inboxRecipient: string;
22
+ }
23
+
24
+ const AGENTS: AgentCapability[] = [
25
+ {
26
+ id: 'codebeast',
27
+ name: 'CodeBeast',
28
+ capabilities: ['code_review', 'security_audit', 'adversarial_review', 'fix_suggestion'],
29
+ inboxRecipient: 'codebeast',
30
+ },
31
+ {
32
+ id: 'mara',
33
+ name: 'MARA',
34
+ capabilities: ['governance', 'colony_coordination', 'agent_lifecycle', 'resource_allocation'],
35
+ inboxRecipient: 'mara',
36
+ },
37
+ {
38
+ id: 'sera',
39
+ name: 'Sera',
40
+ capabilities: ['classification', 'symbolic_computation', 'tarotscript', 'intent_analysis'],
41
+ inboxRecipient: 'sera',
42
+ },
43
+ ];
44
+
45
+ // --- Inbox Helper ---
46
+
47
+ async function sendInboxMessage(
48
+ db: D1Database,
49
+ recipient: string,
50
+ msgType: string,
51
+ subject: string,
52
+ body: string,
53
+ channel = 'ops',
54
+ ): Promise<void> {
55
+ const id = crypto.randomUUID();
56
+ await db.prepare(`
57
+ INSERT INTO agent_inbox (id, sender, recipient, channel, msg_type, subject, body)
58
+ VALUES (?, 'aegis', ?, ?, ?, ?, ?)
59
+ `).bind(id, recipient, channel, msgType, subject, body).run();
60
+ console.log(`[agent-dispatch] Sent ${msgType} to ${recipient}: ${subject}`);
61
+ }
62
+
63
+ // --- Watermark Helper ---
64
+
65
+ async function getWatermark(db: D1Database, key: string): Promise<string | null> {
66
+ const row = await db.prepare(
67
+ "SELECT received_at FROM web_events WHERE event_id = ? LIMIT 1"
68
+ ).bind(`dispatch_watermark:${key}`).first<{ received_at: string }>();
69
+ return row?.received_at ?? null;
70
+ }
71
+
72
+ async function setWatermark(db: D1Database, key: string): Promise<void> {
73
+ await db.prepare(
74
+ "INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES (?, datetime('now'))"
75
+ ).bind(`dispatch_watermark:${key}`).run();
76
+ }
77
+
78
+ // --- Trigger: Merged PRs -> CodeBeast Review ---
79
+ // When PRs merge on watched repos, auto-request CodeBeast review.
80
+
81
+ async function dispatchMergedPrReviews(db: D1Database): Promise<number> {
82
+ const watermark = await getWatermark(db, 'pr_reviews');
83
+ const since = watermark || "datetime('now', '-2 hours')";
84
+
85
+ // Find recently merged PRs from ARGUS events
86
+ const events = await db.prepare(`
87
+ SELECT payload, ts FROM events
88
+ WHERE source = 'github'
89
+ AND event_type = 'pull_request.merged'
90
+ AND ts > ?
91
+ ORDER BY ts ASC
92
+ LIMIT 10
93
+ `).bind(since).all<{ payload: string; ts: string }>();
94
+
95
+ let dispatched = 0;
96
+ for (const event of events.results ?? []) {
97
+ try {
98
+ const payload = JSON.parse(event.payload);
99
+ const pr = payload.pull_request ?? payload;
100
+ const repo = (payload.repository?.full_name as string) ?? '';
101
+ const prNumber = pr.number ?? 0;
102
+ const title = pr.title ?? '';
103
+ const additions = pr.additions ?? 0;
104
+ const deletions = pr.deletions ?? 0;
105
+ const totalLoc = additions + deletions;
106
+
107
+ // Skip tiny PRs (< 10 LOC) and auto-branch PRs from taskrunner
108
+ if (totalLoc < 10) continue;
109
+ if (title.startsWith('[auto]')) continue;
110
+
111
+ // Dedup: check if we already requested review for this PR
112
+ const existingMsg = await db.prepare(
113
+ "SELECT 1 FROM agent_inbox WHERE sender = 'aegis' AND recipient = 'codebeast' AND subject LIKE ? LIMIT 1"
114
+ ).bind(`%${repo}#${prNumber}%`).first();
115
+ if (existingMsg) continue;
116
+
117
+ await sendInboxMessage(
118
+ db, 'codebeast', 'context_request',
119
+ `Review: ${repo}#${prNumber} -- ${title}`,
120
+ `PR merged: ${title}\nRepo: ${repo}\nPR: #${prNumber}\nLOC: +${additions}/-${deletions}\n\nPlease run adversarial review on this merge. Flag security issues, correctness problems, and test gaps. Post findings back via inbox as alert (critical) or task_proposal (suggested fix).`,
121
+ 'code-review',
122
+ );
123
+ dispatched++;
124
+ } catch {
125
+ // Skip malformed events
126
+ }
127
+ }
128
+
129
+ if (dispatched > 0 || (events.results?.length ?? 0) > 0) {
130
+ await setWatermark(db, 'pr_reviews');
131
+ }
132
+
133
+ return dispatched;
134
+ }
135
+
136
+ // --- Trigger: Completed Tasks -> Status Updates ---
137
+ // When cc_tasks complete, notify relevant agents if the work
138
+ // touches their domain.
139
+
140
+ async function dispatchTaskCompletions(db: D1Database): Promise<number> {
141
+ const watermark = await getWatermark(db, 'task_completions');
142
+ const since = watermark || "datetime('now', '-2 hours')";
143
+
144
+ const tasks = await db.prepare(`
145
+ SELECT id, title, repo, category, result, exit_code
146
+ FROM cc_tasks
147
+ WHERE status = 'completed'
148
+ AND completed_at > ?
149
+ AND exit_code = 0
150
+ ORDER BY completed_at ASC
151
+ LIMIT 10
152
+ `).bind(since).all<{
153
+ id: string; title: string; repo: string; category: string;
154
+ result: string | null; exit_code: number;
155
+ }>();
156
+
157
+ let dispatched = 0;
158
+ for (const task of tasks.results ?? []) {
159
+ // Route to agent based on repo/category
160
+ const repo = task.repo.toLowerCase();
161
+
162
+ // TarotScript work -> notify Sera
163
+ if (repo.includes('tarotscript') || repo.includes('tarot')) {
164
+ await sendInboxMessage(
165
+ db, 'sera', 'status_update',
166
+ `Task completed: ${task.title.slice(0, 100)}`,
167
+ `Repo: ${task.repo}\nCategory: ${task.category}\nResult: ${(task.result ?? '').slice(0, 500)}`,
168
+ );
169
+ dispatched++;
170
+ }
171
+
172
+ // Colony OS work -> notify MARA
173
+ if (repo.includes('colony') || repo.includes('colonyos')) {
174
+ await sendInboxMessage(
175
+ db, 'mara', 'status_update',
176
+ `Task completed: ${task.title.slice(0, 100)}`,
177
+ `Repo: ${task.repo}\nCategory: ${task.category}\nResult: ${(task.result ?? '').slice(0, 500)}`,
178
+ );
179
+ dispatched++;
180
+ }
181
+ }
182
+
183
+ if (dispatched > 0 || (tasks.results?.length ?? 0) > 0) {
184
+ await setWatermark(db, 'task_completions');
185
+ }
186
+
187
+ return dispatched;
188
+ }
189
+
190
+ // --- Trigger: Failed Tasks -> Alert Relevant Agent ---
191
+ // When tasks fail repeatedly on the same repo, alert the agent
192
+ // that owns that domain.
193
+
194
+ async function dispatchTaskFailures(db: D1Database): Promise<number> {
195
+ // Find repos with 2+ failures in last 24h
196
+ const failClusters = await db.prepare(`
197
+ SELECT repo, COUNT(*) as fail_count, GROUP_CONCAT(title, ' | ') as titles
198
+ FROM cc_tasks
199
+ WHERE status = 'failed'
200
+ AND completed_at > datetime('now', '-24 hours')
201
+ GROUP BY repo
202
+ HAVING COUNT(*) >= 2
203
+ LIMIT 5
204
+ `).all<{ repo: string; fail_count: number; titles: string }>();
205
+
206
+ let dispatched = 0;
207
+ for (const cluster of failClusters.results ?? []) {
208
+ // Dedup: don't re-alert for same cluster
209
+ const existing = await db.prepare(
210
+ "SELECT 1 FROM agent_inbox WHERE sender = 'aegis' AND subject LIKE ? AND created_at > datetime('now', '-24 hours') LIMIT 1"
211
+ ).bind(`%${cluster.repo}%failure cluster%`).first();
212
+ if (existing) continue;
213
+
214
+ // Route to CodeBeast for code-related failure analysis
215
+ await sendInboxMessage(
216
+ db, 'codebeast', 'alert',
217
+ `${cluster.repo}: failure cluster (${cluster.fail_count} tasks)`,
218
+ `${cluster.fail_count} tasks failed in ${cluster.repo} in the last 24h.\n\nTitles:\n${cluster.titles.split(' | ').map(t => `- ${t}`).join('\n')}\n\nInvestigate common root cause. Check for environment issues, missing dependencies, or systematic test failures.`,
219
+ 'code-review',
220
+ );
221
+ dispatched++;
222
+ }
223
+
224
+ return dispatched;
225
+ }
226
+
227
+ // --- Main Entry Point ---
228
+
229
+ export async function runAgentDispatch(env: EdgeEnv): Promise<void> {
230
+ if (!env.githubToken) {
231
+ console.log('[agent-dispatch] Skipped: missing githubToken');
232
+ return;
233
+ }
234
+
235
+ let totalDispatched = 0;
236
+
237
+ // 1. Merged PRs -> CodeBeast review
238
+ const prReviews = await dispatchMergedPrReviews(env.db);
239
+ totalDispatched += prReviews;
240
+
241
+ // 2. Completed tasks -> relevant agent notifications
242
+ const taskNotifs = await dispatchTaskCompletions(env.db);
243
+ totalDispatched += taskNotifs;
244
+
245
+ // 3. Failure clusters -> CodeBeast alert
246
+ const failAlerts = await dispatchTaskFailures(env.db);
247
+ totalDispatched += failAlerts;
248
+
249
+ if (totalDispatched > 0) {
250
+ console.log(`[agent-dispatch] Dispatched ${totalDispatched} messages (PR reviews: ${prReviews}, task notifs: ${taskNotifs}, fail alerts: ${failAlerts})`);
251
+ }
252
+ }
@@ -0,0 +1,247 @@
1
+ // ARGUS Analytics: GA4 insight extraction
2
+ // Queries Google Analytics Data API daily, stores findings for the digest.
3
+ // Surfaces: traffic trends, top pages, traffic sources, bounce anomalies.
4
+ // Zero inference — pure API queries and threshold logic.
5
+
6
+ import { type EdgeEnv } from '../dispatch.js';
7
+
8
+ // ─── GA4 OAuth2 ──────────────────────────────────────────────
9
+
10
+ interface GACredentials {
11
+ client_id: string;
12
+ client_secret: string;
13
+ refresh_token: string;
14
+ property_id: string;
15
+ }
16
+
17
+ async function getAccessToken(creds: GACredentials): Promise<string> {
18
+ const res = await fetch('https://oauth2.googleapis.com/token', {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
21
+ body: new URLSearchParams({
22
+ refresh_token: creds.refresh_token,
23
+ client_id: creds.client_id,
24
+ client_secret: creds.client_secret,
25
+ grant_type: 'refresh_token',
26
+ }),
27
+ signal: AbortSignal.timeout(10_000),
28
+ });
29
+
30
+ if (!res.ok) {
31
+ const err = await res.text();
32
+ throw new Error(`GA token refresh failed: ${res.status} ${err}`);
33
+ }
34
+
35
+ const data = await res.json<{ access_token: string }>();
36
+ return data.access_token;
37
+ }
38
+
39
+ // ─── GA4 Data API ────────────────────────────────────────────
40
+
41
+ interface GAReport {
42
+ rows?: Array<{
43
+ dimensionValues: Array<{ value: string }>;
44
+ metricValues: Array<{ value: string }>;
45
+ }>;
46
+ rowCount?: number;
47
+ }
48
+
49
+ async function runReport(
50
+ accessToken: string,
51
+ propertyId: string,
52
+ body: Record<string, unknown>,
53
+ ): Promise<GAReport> {
54
+ const res = await fetch(
55
+ `https://analyticsdata.googleapis.com/v1beta/properties/${propertyId}:runReport`,
56
+ {
57
+ method: 'POST',
58
+ headers: {
59
+ 'Authorization': `Bearer ${accessToken}`,
60
+ 'Content-Type': 'application/json',
61
+ },
62
+ body: JSON.stringify(body),
63
+ signal: AbortSignal.timeout(15_000),
64
+ },
65
+ );
66
+
67
+ if (!res.ok) {
68
+ const err = await res.text();
69
+ throw new Error(`GA4 report failed: ${res.status} ${err}`);
70
+ }
71
+
72
+ return res.json<GAReport>();
73
+ }
74
+
75
+ // ─── Analytics Snapshot ──────────────────────────────────────
76
+
77
+ export interface AnalyticsSnapshot {
78
+ // Traffic overview
79
+ sessions_7d: number;
80
+ sessions_prior_7d: number;
81
+ users_7d: number;
82
+ bounce_rate_7d: number;
83
+
84
+ // Top pages
85
+ top_pages: Array<{ path: string; sessions: number; bounce_rate: number }>;
86
+
87
+ // Traffic sources
88
+ top_sources: Array<{ source: string; medium: string; sessions: number }>;
89
+
90
+ // Insights (derived)
91
+ insights: string[];
92
+
93
+ computed_at: string;
94
+ }
95
+
96
+ // ─── Main ────────────────────────────────────────────────────
97
+
98
+ export async function runArgusAnalytics(env: EdgeEnv): Promise<void> {
99
+ // Daily gate — run at 08 UTC (before digest at 09)
100
+ const hour = new Date().getUTCHours();
101
+ if (hour !== 8) return;
102
+
103
+ // Cooldown: 22 hours
104
+ const lastRun = await env.db.prepare(
105
+ "SELECT received_at FROM web_events WHERE event_id = 'argus_analytics'"
106
+ ).first<{ received_at: string }>();
107
+
108
+ if (lastRun) {
109
+ const elapsed = Date.now() - new Date(lastRun.received_at + 'Z').getTime();
110
+ if (elapsed < 22 * 60 * 60 * 1000) return;
111
+ }
112
+
113
+ // Parse credentials from env (JSON: { client_id, client_secret, refresh_token, property_id })
114
+ const rawCreds = env.gaCredentials;
115
+ if (!rawCreds) {
116
+ console.log('[argus-analytics] Skipping — no GA_CREDENTIALS');
117
+ return;
118
+ }
119
+
120
+ let creds: GACredentials;
121
+ try {
122
+ creds = JSON.parse(rawCreds);
123
+ } catch {
124
+ console.error('[argus-analytics] Failed to parse GA_CREDENTIALS');
125
+ return;
126
+ }
127
+
128
+ const accessToken = await getAccessToken(creds);
129
+
130
+ // Run reports in parallel
131
+ const [trafficCurrent, trafficPrior, topPages, sources] = await Promise.all([
132
+ // Total sessions + users + bounce rate (last 7 days)
133
+ runReport(accessToken, creds.property_id, {
134
+ dateRanges: [{ startDate: '7daysAgo', endDate: 'today' }],
135
+ metrics: [
136
+ { name: 'sessions' },
137
+ { name: 'activeUsers' },
138
+ { name: 'bounceRate' },
139
+ ],
140
+ }),
141
+
142
+ // Prior 7 days for comparison
143
+ runReport(accessToken, creds.property_id, {
144
+ dateRanges: [{ startDate: '14daysAgo', endDate: '8daysAgo' }],
145
+ metrics: [{ name: 'sessions' }],
146
+ }),
147
+
148
+ // Top pages by sessions
149
+ runReport(accessToken, creds.property_id, {
150
+ dateRanges: [{ startDate: '7daysAgo', endDate: 'today' }],
151
+ dimensions: [{ name: 'pagePath' }],
152
+ metrics: [{ name: 'sessions' }, { name: 'bounceRate' }],
153
+ limit: 10,
154
+ orderBys: [{ metric: { metricName: 'sessions' }, desc: true }],
155
+ }),
156
+
157
+ // Traffic sources
158
+ runReport(accessToken, creds.property_id, {
159
+ dateRanges: [{ startDate: '7daysAgo', endDate: 'today' }],
160
+ dimensions: [{ name: 'sessionSource' }, { name: 'sessionMedium' }],
161
+ metrics: [{ name: 'sessions' }],
162
+ limit: 10,
163
+ orderBys: [{ metric: { metricName: 'sessions' }, desc: true }],
164
+ }),
165
+ ]);
166
+
167
+ // Parse results
168
+ const currentRow = trafficCurrent.rows?.[0];
169
+ const sessions_7d = parseInt(currentRow?.metricValues[0].value ?? '0');
170
+ const users_7d = parseInt(currentRow?.metricValues[1].value ?? '0');
171
+ const bounce_rate_7d = parseFloat(currentRow?.metricValues[2].value ?? '0');
172
+
173
+ const priorRow = trafficPrior.rows?.[0];
174
+ const sessions_prior_7d = parseInt(priorRow?.metricValues[0].value ?? '0');
175
+
176
+ const top_pages = (topPages.rows ?? []).map(row => ({
177
+ path: row.dimensionValues[0].value,
178
+ sessions: parseInt(row.metricValues[0].value),
179
+ bounce_rate: parseFloat(row.metricValues[1].value),
180
+ }));
181
+
182
+ const top_sources = (sources.rows ?? []).map(row => ({
183
+ source: row.dimensionValues[0].value,
184
+ medium: row.dimensionValues[1].value,
185
+ sessions: parseInt(row.metricValues[0].value),
186
+ }));
187
+
188
+ // Generate insights
189
+ const insights: string[] = [];
190
+
191
+ // Traffic trend
192
+ if (sessions_prior_7d > 0) {
193
+ const delta = ((sessions_7d - sessions_prior_7d) / sessions_prior_7d * 100);
194
+ if (delta > 50) {
195
+ insights.push(`Traffic up ${Math.round(delta)}% week-over-week (${sessions_prior_7d} → ${sessions_7d} sessions). Find out what's driving it.`);
196
+ } else if (delta < -30) {
197
+ insights.push(`Traffic down ${Math.round(Math.abs(delta))}% week-over-week (${sessions_prior_7d} → ${sessions_7d} sessions). Check if something broke or if last week was an anomaly.`);
198
+ }
199
+ }
200
+
201
+ // Bounce rate
202
+ if (bounce_rate_7d > 0.8 && sessions_7d > 10) {
203
+ insights.push(`Overall bounce rate is ${(bounce_rate_7d * 100).toFixed(0)}% — more than 4 out of 5 visitors leave immediately. Landing page isn't converting.`);
204
+ }
205
+
206
+ // High-bounce pages with traffic
207
+ for (const page of top_pages) {
208
+ if (page.bounce_rate > 0.9 && page.sessions >= 5) {
209
+ insights.push(`${page.path} has ${(page.bounce_rate * 100).toFixed(0)}% bounce rate with ${page.sessions} sessions. Users are landing and leaving.`);
210
+ }
211
+ }
212
+
213
+ // Zero-traffic detection (have pages but nobody's visiting)
214
+ if (sessions_7d < 5) {
215
+ insights.push(`Only ${sessions_7d} total sessions this week. No meaningful traffic. Distribution is the bottleneck, not product.`);
216
+ }
217
+
218
+ // New traffic source spike
219
+ for (const src of top_sources) {
220
+ if (src.sessions >= 10 && src.source !== '(direct)' && src.source !== '(not set)') {
221
+ insights.push(`${src.sessions} sessions from ${src.source} (${src.medium}). Worth investigating — is this organic or a mention?`);
222
+ }
223
+ }
224
+
225
+ const snapshot: AnalyticsSnapshot = {
226
+ sessions_7d,
227
+ sessions_prior_7d,
228
+ users_7d,
229
+ bounce_rate_7d,
230
+ top_pages,
231
+ top_sources,
232
+ insights,
233
+ computed_at: new Date().toISOString(),
234
+ };
235
+
236
+ // Store for digest
237
+ await env.db.prepare(
238
+ "INSERT INTO digest_sections (section, payload) VALUES ('analytics', ?)"
239
+ ).bind(JSON.stringify(snapshot)).run();
240
+
241
+ // Update watermark
242
+ await env.db.prepare(
243
+ "INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('argus_analytics', datetime('now'))"
244
+ ).run();
245
+
246
+ console.log(`[argus-analytics] ${sessions_7d} sessions (${sessions_prior_7d} prior), ${users_7d} users, ${(bounce_rate_7d * 100).toFixed(0)}% bounce, ${insights.length} insights`);
247
+ }