@nagellabs/libi 0.1.7 → 0.1.9

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 (165) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +5 -5
  3. package/.next/diagnostics/route-bundle-stats.json +16 -16
  4. package/.next/externals-manifest.json +1 -1
  5. package/.next/fallback-build-manifest.json +3 -3
  6. package/.next/prerender-manifest.json +3 -3
  7. package/.next/required-server-files.js +1 -1
  8. package/.next/required-server-files.json +1 -1
  9. package/.next/server/app/(app)/editor/page/build-manifest.json +2 -2
  10. package/.next/server/app/(app)/instructions/page/build-manifest.json +2 -2
  11. package/.next/server/app/(app)/mcps-skills/page/build-manifest.json +2 -2
  12. package/.next/server/app/(app)/mcps-skills/skills/[name]/page/build-manifest.json +2 -2
  13. package/.next/server/app/(app)/mcps-skills/skills/new/page/build-manifest.json +2 -2
  14. package/.next/server/app/(app)/settings/page/build-manifest.json +2 -2
  15. package/.next/server/app/_global-error/page/build-manifest.json +2 -2
  16. package/.next/server/app/_global-error.html +1 -1
  17. package/.next/server/app/_global-error.rsc +1 -1
  18. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  19. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  20. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  21. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  22. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  23. package/.next/server/app/_not-found/page/build-manifest.json +2 -2
  24. package/.next/server/app/api/agent/cancel/route.js +8 -8
  25. package/.next/server/app/api/agent/cancel/route.js.nft.json +1 -1
  26. package/.next/server/app/api/agent/detect/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/dispatch/route.js +5 -5
  28. package/.next/server/app/api/agent/dispatch/route.js.nft.json +1 -1
  29. package/.next/server/app/api/agent/events/route.js +11 -11
  30. package/.next/server/app/api/agent/events/route.js.nft.json +1 -1
  31. package/.next/server/app/api/agent/messages/route.js +7 -7
  32. package/.next/server/app/api/agent/messages/route.js.nft.json +1 -1
  33. package/.next/server/app/api/agent/providers/route.js.nft.json +1 -1
  34. package/.next/server/app/api/agent/send/route.js +7 -7
  35. package/.next/server/app/api/agent/send/route.js.nft.json +1 -1
  36. package/.next/server/app/api/agent/start/route.js +7 -7
  37. package/.next/server/app/api/agent/start/route.js.nft.json +1 -1
  38. package/.next/server/app/api/agent/stop/route.js +8 -8
  39. package/.next/server/app/api/agent/stop/route.js.nft.json +1 -1
  40. package/.next/server/app/api/agents/[agentId]/install/route.js +4 -4
  41. package/.next/server/app/api/agents/[agentId]/install/route.js.nft.json +1 -1
  42. package/.next/server/app/api/codex/connect/route.js.nft.json +1 -1
  43. package/.next/server/app/api/db/resolve/route.js +9 -9
  44. package/.next/server/app/api/db/resolve/route.js.nft.json +1 -1
  45. package/.next/server/app/api/export/chromium/route.js +5 -5
  46. package/.next/server/app/api/export/chromium/route.js.nft.json +1 -1
  47. package/.next/server/app/api/export/route.js +4 -4
  48. package/.next/server/app/api/export/route.js.nft.json +1 -1
  49. package/.next/server/app/api/files/by-id/[fileId]/filmstrip-status/route.js +4 -4
  50. package/.next/server/app/api/files/by-id/[fileId]/filmstrip-status/route.js.nft.json +1 -1
  51. package/.next/server/app/api/files/by-id/[fileId]/rename/route.js +7 -7
  52. package/.next/server/app/api/files/by-id/[fileId]/rename/route.js.nft.json +1 -1
  53. package/.next/server/app/api/folders/[folderId]/duplicate/route.js +6 -6
  54. package/.next/server/app/api/folders/[folderId]/duplicate/route.js.nft.json +1 -1
  55. package/.next/server/app/api/install-from-url/route.js +10 -10
  56. package/.next/server/app/api/install-from-url/route.js.nft.json +1 -1
  57. package/.next/server/app/api/instructions/override/route.js.nft.json +1 -1
  58. package/.next/server/app/api/jobs/[id]/events/route.js +8 -8
  59. package/.next/server/app/api/jobs/[id]/events/route.js.nft.json +1 -1
  60. package/.next/server/app/api/jobs/[id]/retry/route.js +4 -4
  61. package/.next/server/app/api/jobs/[id]/retry/route.js.nft.json +1 -1
  62. package/.next/server/app/api/jobs/[id]/route.js +4 -4
  63. package/.next/server/app/api/jobs/[id]/route.js.nft.json +1 -1
  64. package/.next/server/app/api/jobs/[id]/status/route.js +3 -3
  65. package/.next/server/app/api/jobs/[id]/status/route.js.nft.json +1 -1
  66. package/.next/server/app/api/jobs/route.js +9 -9
  67. package/.next/server/app/api/jobs/route.js.nft.json +1 -1
  68. package/.next/server/app/api/notify/route.js.nft.json +1 -1
  69. package/.next/server/app/api/pieces/[pieceId]/duplicate/route.js +4 -4
  70. package/.next/server/app/api/pieces/[pieceId]/duplicate/route.js.nft.json +1 -1
  71. package/.next/server/app/api/pieces/[pieceId]/tracks/[trackId]/manual-anchors/apply-deletes/route.js +9 -9
  72. package/.next/server/app/api/pieces/[pieceId]/tracks/[trackId]/manual-anchors/apply-deletes/route.js.nft.json +1 -1
  73. package/.next/server/app/api/pieces/[pieceId]/tracks/[trackId]/manual-anchors/route.js +9 -9
  74. package/.next/server/app/api/pieces/[pieceId]/tracks/[trackId]/manual-anchors/route.js.nft.json +1 -1
  75. package/.next/server/app/api/render/frames/route.js +7 -7
  76. package/.next/server/app/api/render/frames/route.js.nft.json +1 -1
  77. package/.next/server/app/api/runtime/update/route.js +5 -5
  78. package/.next/server/app/api/runtime/update/route.js.nft.json +1 -1
  79. package/.next/server/app/api/sessions/[sessionId]/activate/route.js +8 -8
  80. package/.next/server/app/api/sessions/[sessionId]/activate/route.js.nft.json +1 -1
  81. package/.next/server/app/api/sessions/[sessionId]/context/route.js +8 -8
  82. package/.next/server/app/api/sessions/[sessionId]/context/route.js.nft.json +1 -1
  83. package/.next/server/app/api/sessions/[sessionId]/deactivate/route.js +8 -8
  84. package/.next/server/app/api/sessions/[sessionId]/deactivate/route.js.nft.json +1 -1
  85. package/.next/server/app/api/sessions/[sessionId]/model/route.js +7 -7
  86. package/.next/server/app/api/sessions/[sessionId]/model/route.js.nft.json +1 -1
  87. package/.next/server/app/api/sessions/[sessionId]/permission/route.js +7 -7
  88. package/.next/server/app/api/sessions/[sessionId]/permission/route.js.nft.json +1 -1
  89. package/.next/server/app/api/sessions/permission-modes/route.js +7 -7
  90. package/.next/server/app/api/sessions/permission-modes/route.js.nft.json +1 -1
  91. package/.next/server/app/api/sessions/route.js +7 -7
  92. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  93. package/.next/server/app/api/settings/mcp-servers/[id]/dependencies/route.js +7 -7
  94. package/.next/server/app/api/settings/mcp-servers/[id]/dependencies/route.js.nft.json +1 -1
  95. package/.next/server/app/api/settings/mcp-servers/[id]/install-plan/route.js +5 -5
  96. package/.next/server/app/api/settings/mcp-servers/[id]/install-plan/route.js.nft.json +1 -1
  97. package/.next/server/app/api/settings/mcp-servers/[id]/retry/route.js +7 -7
  98. package/.next/server/app/api/settings/mcp-servers/[id]/retry/route.js.nft.json +1 -1
  99. package/.next/server/app/api/settings/mcp-servers/[id]/retry-dep/route.js +7 -7
  100. package/.next/server/app/api/settings/mcp-servers/[id]/retry-dep/route.js.nft.json +1 -1
  101. package/.next/server/app/api/settings/mcp-servers/[id]/route.js.nft.json +1 -1
  102. package/.next/server/app/api/settings/mcp-servers/resync/route.js +7 -7
  103. package/.next/server/app/api/settings/mcp-servers/resync/route.js.nft.json +1 -1
  104. package/.next/server/app/api/settings/memories/route.js.nft.json +1 -1
  105. package/.next/server/app/api/skill-eval/configure/route.js +8 -8
  106. package/.next/server/app/api/skill-eval/configure/route.js.nft.json +1 -1
  107. package/.next/server/app/api/terminal/sessions/route.js +2 -2
  108. package/.next/server/app/api/terminal/ws-info/route.js.nft.json +1 -1
  109. package/.next/server/app/api/tracking/run/route.js +4 -4
  110. package/.next/server/app/api/tracking/run/route.js.nft.json +1 -1
  111. package/.next/server/app/page/build-manifest.json +2 -2
  112. package/.next/server/chunks/[root-of-the-server]__04thdbt._.js +3 -3
  113. package/.next/server/chunks/[root-of-the-server]__04thdbt._.js.map +2 -2
  114. package/.next/server/chunks/[root-of-the-server]__05bfu4m._.js +3 -3
  115. package/.next/server/chunks/[root-of-the-server]__05bfu4m._.js.map +2 -2
  116. package/.next/server/chunks/[root-of-the-server]__09qyvq1._.js +3 -3
  117. package/.next/server/chunks/[root-of-the-server]__09qyvq1._.js.map +2 -2
  118. package/.next/server/chunks/[root-of-the-server]__0gnut6f._.js +3 -3
  119. package/.next/server/chunks/[root-of-the-server]__0gnut6f._.js.map +1 -1
  120. package/.next/server/chunks/[root-of-the-server]__0l_-iy7._.js +3 -3
  121. package/.next/server/chunks/[root-of-the-server]__0l_-iy7._.js.map +1 -1
  122. package/.next/server/chunks/[root-of-the-server]__0s0b6ty._.js +3 -3
  123. package/.next/server/chunks/[root-of-the-server]__0s0b6ty._.js.map +1 -1
  124. package/.next/server/chunks/[root-of-the-server]__0sj68ta._.js +3 -3
  125. package/.next/server/chunks/[root-of-the-server]__0sj68ta._.js.map +1 -1
  126. package/.next/server/chunks/[root-of-the-server]__10oxitf._.js +3 -3
  127. package/.next/server/chunks/[root-of-the-server]__10oxitf._.js.map +2 -2
  128. package/.next/server/chunks/[root-of-the-server]__11.cp_g._.js +3 -3
  129. package/.next/server/chunks/[root-of-the-server]__11.cp_g._.js.map +2 -2
  130. package/.next/server/chunks/_04i643i._.js +3 -3
  131. package/.next/server/chunks/_0_t4b-f._.js +4 -4
  132. package/.next/server/chunks/_0gc3hkv._.js +3 -3
  133. package/.next/server/chunks/_0hainf0._.js +4 -4
  134. package/.next/server/chunks/_0jrwb4i._.js +3 -3
  135. package/.next/server/chunks/_0ms~zo_._.js +3 -3
  136. package/.next/server/chunks/_0ou5g0c._.js +4 -4
  137. package/.next/server/chunks/_0ovzm6d._.js +4 -4
  138. package/.next/server/chunks/_0pwx1vo._.js +3 -3
  139. package/.next/server/chunks/_0snzqta._.js +4 -4
  140. package/.next/server/chunks/lib_08x49mw._.js +3 -3
  141. package/.next/server/chunks/lib_runtime_runtime-prune_ts_0eaw~-q._.js +3 -3
  142. package/.next/server/chunks/node_modules_003ody_._.js +3 -3
  143. package/.next/server/chunks/ssr/_0675jux._.js +3 -3
  144. package/.next/server/instrumentation.js.nft.json +1 -1
  145. package/.next/server/middleware-build-manifest.js +5 -5
  146. package/.next/server/pages/500.html +1 -1
  147. package/.next/server/server-reference-manifest.js +1 -1
  148. package/.next/server/server-reference-manifest.json +1 -1
  149. package/.next/static/chunks/{0tfripjpjgh0x.js → 0rgnvt2t.fexk.js} +3 -3
  150. package/.next/static/chunks/{turbopack-0nj3aucu1vtlz.js → turbopack-06tait4frhc5a.js} +3 -3
  151. package/.next/trace-build +1 -1
  152. package/THIRD-PARTY-NOTICES.md +1 -1
  153. package/dist-cli/BUILD_INFO.json +6 -6
  154. package/dist-cli/lib/analytics/config.js +1 -1
  155. package/dist-cli/lib/install/next-externals.js +6 -1
  156. package/dist-cli/lib/onboarding/piece/v1/index.js +5 -1
  157. package/dist-cli/lib/server/next-server.js +12 -2
  158. package/lib/analytics/config.ts +15 -2
  159. package/lib/install/next-externals.ts +36 -1
  160. package/lib/onboarding/piece/v1/index.ts +5 -1
  161. package/lib/server/next-server.ts +29 -4
  162. package/package.json +1 -1
  163. /package/.next/static/{AKrgjfjGvLuWkXofXFBpx → 05vWgPQtQKJ6EuLHg8nZ3}/_buildManifest.js +0 -0
  164. /package/.next/static/{AKrgjfjGvLuWkXofXFBpx → 05vWgPQtQKJ6EuLHg8nZ3}/_clientMiddlewareManifest.js +0 -0
  165. /package/.next/static/{AKrgjfjGvLuWkXofXFBpx → 05vWgPQtQKJ6EuLHg8nZ3}/_ssgManifest.js +0 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": [],
4
- "debugId": "b5baa402-342f-88c8-fc44-4cfa14d6486c",
4
+ "debugId": "96ca584e-6e31-dcad-1d35-a2fbc20221f1",
5
5
  "sections": [
6
- {"offset": {"line": 1, "column": 0}, "map": {"version":3,"sources":["../../../lib/db/settings.ts","../../../lib/analytics/settings-logic.ts","../../../lib/export/folder.ts","../../../lib/test-mode.ts","../../../lib/analytics/config.ts","../../../node_modules/src/sql/functions/aggregate.ts","../../../lib/analytics/events.ts","../../../lib/analytics/queue.ts","../../../lib/analytics/server.ts"],"sourcesContent":["import { eq } from \"drizzle-orm\";\nimport { getDb } from \"./client\";\nimport { settings } from \"./schema\";\nimport { serverLogger } from \"@/lib/logger\";\nimport {\n type AnalyticsSettings,\n parseAnalyticsSettings,\n mergeAnalyticsSettings,\n markMilestone,\n} from \"@/lib/analytics/settings-logic\";\nimport { crashReportChoiceAllowsReporting } from \"@/lib/sentry/enabled\";\n\nexport type { AnalyticsSettings };\n\nexport interface AppSettings {\n preferredAgent: string | null;\n panelChatSize: number;\n panelEditorSize: number;\n panelResourcesSize: number;\n panelChatVisible: boolean;\n panelResourcesVisible: boolean;\n agentApprovalModes: string | null;\n agentModelPreferences: string | null;\n onboardingPersona: string | null;\n personaSelectedAt: Date | null;\n agentEverConnected: boolean;\n /** Set once, server-side, the first time an agent connects. Null = the\n * first-run demo chip has never been armed. See lib/sessions/session-manager.ts#markAgentConnected. */\n onboardingDemoOfferedAt: Date | null;\n /** Set once the user dismisses OR takes the demo offer — final, and kept\n * independent of onboardingDemoOfferedAt so a dismissal is never confused\n * with \"never offered\". */\n onboardingDemoDismissedAt: Date | null;\n}\n\nconst DEFAULTS: AppSettings = {\n preferredAgent: null,\n panelChatSize: 40,\n panelEditorSize: 40,\n panelResourcesSize: 20,\n panelChatVisible: true,\n panelResourcesVisible: false,\n agentApprovalModes: null,\n agentModelPreferences: null,\n onboardingPersona: null,\n personaSelectedAt: null,\n agentEverConnected: false,\n onboardingDemoOfferedAt: null,\n onboardingDemoDismissedAt: null,\n};\n\n/**\n * Read the app settings (single row, id=1).\n * Auto-creates the row with defaults if it doesn't exist.\n */\nexport function getSettings(): AppSettings {\n const db = getDb();\n\n const [row] = db.select().from(settings).where(eq(settings.id, 1)).limit(1).all();\n\n if (!row) {\n db.insert(settings).values({ id: 1 }).run();\n return { ...DEFAULTS };\n }\n\n return {\n preferredAgent: row.preferredAgent ?? null,\n panelChatSize: row.panelChatSize,\n panelEditorSize: row.panelEditorSize,\n panelResourcesSize: row.panelResourcesSize,\n panelChatVisible: row.panelChatVisible,\n panelResourcesVisible: row.panelResourcesVisible,\n agentApprovalModes: row.agentApprovalModes ?? null,\n agentModelPreferences: row.agentModelPreferences ?? null,\n onboardingPersona: row.onboardingPersona ?? null,\n personaSelectedAt: row.personaSelectedAt ?? null,\n agentEverConnected: row.agentEverConnected,\n onboardingDemoOfferedAt: row.onboardingDemoOfferedAt ?? null,\n onboardingDemoDismissedAt: row.onboardingDemoDismissedAt ?? null,\n };\n}\n\n/**\n * Update specific settings fields. Only provided fields are updated.\n */\nexport function updateSettings(partial: Partial<AppSettings>): void {\n const db = getDb();\n\n // Build the SET clause from provided fields\n const set: Record<string, unknown> = { updatedAt: new Date() };\n\n if (partial.preferredAgent !== undefined) set.preferredAgent = partial.preferredAgent;\n if (partial.panelChatSize !== undefined) set.panelChatSize = partial.panelChatSize;\n if (partial.panelEditorSize !== undefined) set.panelEditorSize = partial.panelEditorSize;\n if (partial.panelResourcesSize !== undefined) set.panelResourcesSize = partial.panelResourcesSize;\n if (partial.panelChatVisible !== undefined) set.panelChatVisible = partial.panelChatVisible;\n if (partial.panelResourcesVisible !== undefined) set.panelResourcesVisible = partial.panelResourcesVisible;\n if (partial.agentApprovalModes !== undefined) set.agentApprovalModes = partial.agentApprovalModes;\n if (partial.agentModelPreferences !== undefined) set.agentModelPreferences = partial.agentModelPreferences;\n if (partial.onboardingPersona !== undefined) set.onboardingPersona = partial.onboardingPersona;\n if (partial.personaSelectedAt !== undefined) set.personaSelectedAt = partial.personaSelectedAt;\n if (partial.agentEverConnected !== undefined) set.agentEverConnected = partial.agentEverConnected;\n if (partial.onboardingDemoOfferedAt !== undefined) set.onboardingDemoOfferedAt = partial.onboardingDemoOfferedAt;\n if (partial.onboardingDemoDismissedAt !== undefined) set.onboardingDemoDismissedAt = partial.onboardingDemoDismissedAt;\n\n // Upsert: insert defaults if row doesn't exist, update if it does\n db.insert(settings)\n .values({ id: 1, ...set })\n .onConflictDoUpdate({ target: settings.id, set })\n .run();\n}\n\n// ---------------------------------------------------------------------------\n// Notifications setting (typed helpers over the settings.notifications JSON column)\n// ---------------------------------------------------------------------------\n\nexport type NotificationsSetting = {\n /** Push a system notification when a background job completes while the window is backgrounded. */\n backgroundJobComplete: boolean;\n};\n\nconst NOTIFICATIONS_DEFAULTS: NotificationsSetting = {\n backgroundJobComplete: true,\n};\n\n/**\n * Read the notifications setting. Returns defaults if the row is missing,\n * the column is null/empty, or the stored JSON is malformed / wrong-shape.\n */\nexport function getNotificationsSetting(): NotificationsSetting {\n const db = getDb();\n const [row] = db\n .select({ notifications: settings.notifications })\n .from(settings)\n .where(eq(settings.id, 1))\n .limit(1)\n .all();\n\n const raw = row?.notifications;\n if (!raw) return { ...NOTIFICATIONS_DEFAULTS };\n\n try {\n const parsed = JSON.parse(raw) as Partial<NotificationsSetting> | null;\n if (!parsed || typeof parsed !== \"object\") return { ...NOTIFICATIONS_DEFAULTS };\n if (typeof parsed.backgroundJobComplete !== \"boolean\") {\n return { ...NOTIFICATIONS_DEFAULTS };\n }\n return { backgroundJobComplete: parsed.backgroundJobComplete };\n } catch {\n return { ...NOTIFICATIONS_DEFAULTS };\n }\n}\n\n/**\n * Persist the notifications setting as a JSON string in the settings table.\n * Upserts the single-row settings record if it doesn't already exist.\n */\nexport function setNotificationsSetting(s: NotificationsSetting): void {\n const db = getDb();\n const value = JSON.stringify(s);\n const set = { notifications: value, updatedAt: new Date() };\n\n db.insert(settings)\n .values({ id: 1, ...set })\n .onConflictDoUpdate({ target: settings.id, set })\n .run();\n}\n\n// ---------------------------------------------------------------------------\n// Codex connect setting (typed helpers over the settings.codex JSON column)\n// ---------------------------------------------------------------------------\n\nexport type CodexConnectSetting = {\n /** Whether the user opted in to keep their global ~/.codex MCP entries in sync with libi. */\n connected: boolean;\n /** Outcome of the last sync attempt. */\n lastSyncStatus: \"ok\" | \"error\" | \"idle\";\n};\n\nconst CODEX_CONNECT_DEFAULTS: CodexConnectSetting = {\n connected: false,\n lastSyncStatus: \"idle\",\n};\n\n/**\n * Read the codex connect setting. Returns defaults if the row is missing,\n * the column is null/empty, the stored JSON is malformed / wrong-shape, or\n * the DB read itself fails (e.g. a pending migration) — this is a defensive\n * read that must never throw.\n */\nexport function getCodexConnectSetting(): CodexConnectSetting {\n try {\n const db = getDb();\n const [row] = db\n .select({ codex: settings.codex })\n .from(settings)\n .where(eq(settings.id, 1))\n .limit(1)\n .all();\n\n const raw = row?.codex;\n if (!raw) return { ...CODEX_CONNECT_DEFAULTS };\n\n const parsed = JSON.parse(raw) as Partial<CodexConnectSetting> | null;\n if (!parsed || typeof parsed !== \"object\") return { ...CODEX_CONNECT_DEFAULTS };\n if (typeof parsed.connected !== \"boolean\") return { ...CODEX_CONNECT_DEFAULTS };\n const status = parsed.lastSyncStatus;\n if (status !== \"ok\" && status !== \"error\" && status !== \"idle\") {\n return { ...CODEX_CONNECT_DEFAULTS };\n }\n return { connected: parsed.connected, lastSyncStatus: status };\n } catch (err) {\n serverLogger.warn(\n { err, tag: \"codex-connect\", op: \"read_degraded\" },\n \"getCodexConnectSetting: read failed, returning defaults\",\n );\n return { ...CODEX_CONNECT_DEFAULTS };\n }\n}\n\n/**\n * Persist the codex connect setting as a JSON string in the settings table.\n * Upserts the single-row settings record if it doesn't already exist.\n */\nexport function setCodexConnectSetting(s: CodexConnectSetting): void {\n const db = getDb();\n const value = JSON.stringify(s);\n const set = { codex: value, updatedAt: new Date() };\n\n db.insert(settings)\n .values({ id: 1, ...set })\n .onConflictDoUpdate({ target: settings.id, set })\n .run();\n}\n\n// ---------------------------------------------------------------------------\n// Export defaults setting (folder + default format + default quality)\n// ---------------------------------------------------------------------------\n\nimport { defaultExportFolder } from \"@/lib/export/folder\";\n\nexport type ExportDefaultsSetting = {\n /** Absolute path. Null = use OS-aware default (~/Movies/libi on darwin, ~/Videos/libi elsewhere). */\n folder: string | null;\n format: \"mp4\" | \"webm\";\n quality: \"source\" | \"1080p\" | \"1440p\" | \"4k\";\n};\n\nconst EXPORT_DEFAULTS_FALLBACK: ExportDefaultsSetting = {\n folder: null,\n format: \"mp4\",\n quality: \"source\",\n};\n\n/** Read the export defaults. Falls back to OS-aware folder + MP4/Source if\n * the row is missing, the column is empty, or the JSON is malformed. */\nexport function getExportDefaults(): ExportDefaultsSetting {\n const db = getDb();\n const [row] = db\n .select({ exportDefaults: settings.exportDefaults })\n .from(settings)\n .where(eq(settings.id, 1))\n .limit(1)\n .all();\n\n const raw = row?.exportDefaults;\n if (!raw) return { ...EXPORT_DEFAULTS_FALLBACK };\n\n try {\n const parsed = JSON.parse(raw) as Partial<ExportDefaultsSetting> | null;\n if (!parsed || typeof parsed !== \"object\") return { ...EXPORT_DEFAULTS_FALLBACK };\n return {\n folder: typeof parsed.folder === \"string\" && parsed.folder ? parsed.folder : null,\n format: parsed.format === \"webm\" ? \"webm\" : \"mp4\",\n quality:\n parsed.quality === \"1080p\" || parsed.quality === \"1440p\" || parsed.quality === \"4k\"\n ? parsed.quality\n : \"source\",\n };\n } catch {\n return { ...EXPORT_DEFAULTS_FALLBACK };\n }\n}\n\n/** Resolve the effective export folder — settings if set, otherwise OS default. */\nexport function resolveExportFolder(): string {\n const defaults = getExportDefaults();\n return defaults.folder ?? defaultExportFolder();\n}\n\n/** Persist the export defaults as JSON in the settings table. */\nexport function setExportDefaults(s: ExportDefaultsSetting): void {\n const db = getDb();\n const value = JSON.stringify(s);\n const set = { exportDefaults: value, updatedAt: new Date() };\n\n db.insert(settings)\n .values({ id: 1, ...set })\n .onConflictDoUpdate({ target: settings.id, set })\n .run();\n}\n\n// ---------------------------------------------------------------------------\n// Bundled-skill digest cache (per-app-version, see mcp/skills/digest.ts)\n// ---------------------------------------------------------------------------\n\nimport type { SkillDigestCache } from \"@/mcp/skills/digest\";\n\n/** Read the cached bundled-skill digests. Null when missing, malformed,\n * or wrong-shape — callers treat null as \"recompute\". */\nexport function getSkillDigestCacheSetting(): SkillDigestCache | null {\n const db = getDb();\n const [row] = db\n .select({ skillDigestCache: settings.skillDigestCache })\n .from(settings)\n .where(eq(settings.id, 1))\n .limit(1)\n .all();\n\n const raw = row?.skillDigestCache;\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<SkillDigestCache> | null;\n if (!parsed || typeof parsed !== \"object\") return null;\n if (typeof parsed.version !== \"string\" || !parsed.version) return null;\n if (!parsed.digests || typeof parsed.digests !== \"object\") return null;\n return { version: parsed.version, digests: parsed.digests as Record<string, string> };\n } catch {\n return null;\n }\n}\n\n/** Persist the bundled-skill digest cache as JSON in the settings table. */\nexport function setSkillDigestCacheSetting(c: SkillDigestCache): void {\n const db = getDb();\n const set = { skillDigestCache: JSON.stringify(c), updatedAt: new Date() };\n db.insert(settings)\n .values({ id: 1, ...set })\n .onConflictDoUpdate({ target: settings.id, set })\n .run();\n}\n\n// ---------------------------------------------------------------------------\n// Analytics settings (typed helpers over the settings.analytics JSON column)\n// ---------------------------------------------------------------------------\n\nexport function getAnalyticsSettings(): AnalyticsSettings {\n const db = getDb();\n const [row] = db\n .select({ analytics: settings.analytics })\n .from(settings)\n .where(eq(settings.id, 1))\n .limit(1)\n .all();\n return parseAnalyticsSettings(row?.analytics ?? null);\n}\n\nexport function setAnalyticsSettings(partial: Partial<AnalyticsSettings>): AnalyticsSettings {\n const db = getDb();\n const next = mergeAnalyticsSettings(getAnalyticsSettings(), partial);\n const set = { analytics: JSON.stringify(next), updatedAt: new Date() };\n db.insert(settings)\n .values({ id: 1, ...set })\n .onConflictDoUpdate({ target: settings.id, set })\n .run();\n return next;\n}\n\n/** Return the per-install analytics UUID, generating + persisting it on first call. */\nexport function getOrCreateAnalyticsUserId(): string {\n const cur = getAnalyticsSettings();\n if (cur.userId) return cur.userId;\n const userId = crypto.randomUUID();\n setAnalyticsSettings({ userId });\n return userId;\n}\n\n/** Mark an activation milestone; returns true only the first time. */\nexport function markAnalyticsMilestoneOnce(name: string): boolean {\n const { settings: next, added } = markMilestone(getAnalyticsSettings(), name);\n if (added) setAnalyticsSettings({ milestones: next.milestones });\n return added;\n}\n\n// ---------------------------------------------------------------------------\n// Crash report settings (typed helpers over the settings.crashReports JSON column)\n// ---------------------------------------------------------------------------\n\nexport type CrashReportChoice = \"unset\" | \"on\" | \"off\";\n\nexport interface CrashReportSettings {\n choice: CrashReportChoice;\n /** ms epoch when the user last made an explicit choice; null while \"unset\". */\n decidedAt: number | null;\n}\n\nconst CRASH_REPORT_DEFAULTS: CrashReportSettings = {\n choice: \"unset\",\n decidedAt: null,\n};\n\n/**\n * Parse the persisted crash-report settings. Tolerant of null, malformed\n * JSON, and unknown `choice` values — any of those return the default.\n */\nexport function parseCrashReportSettings(raw: string | null): CrashReportSettings {\n if (!raw) return { ...CRASH_REPORT_DEFAULTS };\n try {\n const parsed = JSON.parse(raw) as Partial<CrashReportSettings> | null;\n if (!parsed || typeof parsed !== \"object\") return { ...CRASH_REPORT_DEFAULTS };\n const choice = parsed.choice;\n if (choice !== \"unset\" && choice !== \"on\" && choice !== \"off\") {\n return { ...CRASH_REPORT_DEFAULTS };\n }\n const decidedAt = typeof parsed.decidedAt === \"number\" ? parsed.decidedAt : null;\n return { choice, decidedAt };\n } catch {\n return { ...CRASH_REPORT_DEFAULTS };\n }\n}\n\n/** Read the crash-report settings. Returns defaults if the row is missing,\n * the column is null/empty, or the stored JSON is malformed / wrong-shape. */\nexport function getCrashReportSettings(): CrashReportSettings {\n const db = getDb();\n const [row] = db\n .select({ crashReports: settings.crashReports })\n .from(settings)\n .where(eq(settings.id, 1))\n .limit(1)\n .all();\n return parseCrashReportSettings(row?.crashReports ?? null);\n}\n\n/** Persist the crash-report settings as JSON in the settings table. */\nexport function setCrashReportSettings(next: CrashReportSettings): CrashReportSettings {\n const db = getDb();\n const value = JSON.stringify(next);\n const set = { crashReports: value, updatedAt: new Date() };\n db.insert(settings)\n .values({ id: 1, ...set })\n .onConflictDoUpdate({ target: settings.id, set })\n .run();\n return next;\n}\n\n/** \"unset\" means we have not asked yet, and libi currently reports by default,\n * so it resolves to true. Only an explicit \"off\" disables. Delegates to\n * lib/sentry/enabled.ts#crashReportChoiceAllowsReporting so this predicate\n * can't drift from the one the Sentry hooks actually gate on. */\nexport function crashReportsAllowed(s: CrashReportSettings): boolean {\n return crashReportChoiceAllowsReporting(s.choice);\n}\n","// lib/analytics/settings-logic.ts\n// Pure, dependency-free transforms over the analytics settings object. The DB\n// wrappers in lib/db/settings.ts call these; keeping them pure makes them\n// trivially unit-testable without a database.\n\nexport interface AnalyticsSettings {\n /** Master opt-out toggle. Default true (analytics on by default). */\n enabled: boolean;\n /** Per-install UUID used as GA4 client_id + user_id. Generated server-side. */\n userId: string | null;\n /** Epoch ms when the user last opted out (null if never). */\n optOutAt: number | null;\n /** Whether the one-time first-run privacy notice has been shown. */\n firstRunNoticeShown: boolean;\n /** Activation milestones already fired (e.g. \"launch\", \"piece\", \"export\"). */\n milestones: string[];\n}\n\nexport const DEFAULT_ANALYTICS_SETTINGS: AnalyticsSettings = {\n enabled: true,\n userId: null,\n optOutAt: null,\n firstRunNoticeShown: false,\n milestones: [],\n};\n\nexport function parseAnalyticsSettings(raw: string | null | undefined): AnalyticsSettings {\n if (!raw) return { ...DEFAULT_ANALYTICS_SETTINGS };\n try {\n const p = JSON.parse(raw) as Partial<AnalyticsSettings> | null;\n if (!p || typeof p !== \"object\") return { ...DEFAULT_ANALYTICS_SETTINGS };\n return {\n enabled: typeof p.enabled === \"boolean\" ? p.enabled : true,\n userId: typeof p.userId === \"string\" ? p.userId : null,\n optOutAt: typeof p.optOutAt === \"number\" ? p.optOutAt : null,\n firstRunNoticeShown: p.firstRunNoticeShown === true,\n milestones: Array.isArray(p.milestones)\n ? p.milestones.filter((m): m is string => typeof m === \"string\")\n : [],\n };\n } catch {\n return { ...DEFAULT_ANALYTICS_SETTINGS };\n }\n}\n\nexport function mergeAnalyticsSettings(\n cur: AnalyticsSettings,\n partial: Partial<AnalyticsSettings>,\n): AnalyticsSettings {\n return { ...cur, ...partial };\n}\n\nexport function markMilestone(\n cur: AnalyticsSettings,\n name: string,\n): { settings: AnalyticsSettings; added: boolean } {\n if (cur.milestones.includes(name)) return { settings: cur, added: false };\n return { settings: { ...cur, milestones: [...cur.milestones, name] }, added: true };\n}\n\nexport function applyOptOut(cur: AnalyticsSettings, nowMs: number): AnalyticsSettings {\n return { ...cur, enabled: false, optOutAt: nowMs };\n}\n\nexport function applyOptIn(cur: AnalyticsSettings): AnalyticsSettings {\n return { ...cur, enabled: true, optOutAt: null };\n}\n","import os from \"node:os\";\nimport path from \"node:path\";\nimport fs from \"node:fs\";\n\n/** Default per-OS root for exported videos. Picked to match user\n * expectations: macOS ships a Movies folder; Windows + Linux ship Videos. */\nexport function defaultExportFolder(): string {\n const home = os.homedir();\n switch (process.platform) {\n case \"darwin\":\n return path.join(home, \"Movies\", \"libi\");\n case \"win32\":\n return path.join(home, \"Videos\", \"libi\");\n default:\n return path.join(home, \"Videos\", \"libi\");\n }\n}\n\n/** Ensure a folder exists. Throws on permission errors so the export\n * surfaces a clear error to the user rather than silently writing nowhere. */\nexport function ensureFolderExists(absPath: string): void {\n fs.mkdirSync(absPath, { recursive: true });\n}\n\n/** Returns true iff the folder exists and is writable by the current user.\n * Used by the export modal to pre-flight before submitting. */\nexport function isFolderWritable(absPath: string): boolean {\n try {\n fs.accessSync(absPath, fs.constants.W_OK);\n return true;\n } catch {\n return false;\n }\n}\n","/**\n * Centralized test-mode detection. Returns true when the user launched libi\n * with `LIBI_TEST_MODE=1 npx libi`. Used by:\n * - lib/mcp-config.ts to swap fal-ai's transport to the fake-fal stdio\n * server (mcp/dev/fake-fal/) so the agent uses real fal-ai tool names\n * but gets deterministic placeholder outputs at zero cost\n * - mcp/instructions.ts for the TEST MODE banner\n *\n * Read at every call site rather than cached so test code can stub the env\n * via vi.stubEnv() without rebuilding modules.\n */\nexport function isTestMode(): boolean {\n const v = process.env.LIBI_TEST_MODE;\n return v === \"1\" || v === \"true\";\n}\n","// lib/analytics/config.ts\n// GA4 analytics config — committed constants + enablement, mirroring\n// lib/sentry/config.ts. The packaged Electron build and `npx libi` CLI have no\n// .env at runtime, so the measurement ID is HARDCODED here.\n//\n// SECURITY: a hardcoded measurement ID is fine to commit — GA4 web streams\n// (`/g/collect`, see lib/analytics/collect-url.ts) accept events with NO\n// secret at all, the same way every public gtag site on the internet works.\n// Both transports (browser and server) send through that one web-stream path\n// now, so there is no server/client asymmetry left to guard and no secret to\n// lose. This file used to also hold a Measurement Protocol api_secret, whose\n// absence silently disabled every server-side event in every install; it is\n// gone along with the path that needed it.\n\n/** GA4 web data stream measurement ID (public). Env override for staging/self-host. */\nexport const MEASUREMENT_ID =\n process.env.NEXT_PUBLIC_LIBI_GA4_MEASUREMENT_ID || \"G-7XJTYHWMFN\";\n\n/** Loose env shape — accepts process.env and plain object literals (for tests). */\ntype EnvLike = Record<string, string | undefined>;\n\n/** Pure enablement resolver (testable). Default OFF; opt-in flag set by launchers\n * for real installs; hard kill-switch wins. */\nexport function resolveAnalyticsEnabled(env: EnvLike): boolean {\n return env.LIBI_ANALYTICS_DISABLED !== \"1\" && env.NEXT_PUBLIC_LIBI_ANALYTICS === \"1\";\n}\n\n/** debug_mode → GA4 DebugView; true only in `next dev` (sandbox), never packaged. */\nexport function resolveAnalyticsDebug(env: EnvLike): boolean {\n return env.NODE_ENV !== \"production\";\n}\n\n/** Process-resolved values for runtime use. */\nexport const ANALYTICS_ENABLED = resolveAnalyticsEnabled(process.env);\nexport const ANALYTICS_DEBUG = resolveAnalyticsDebug(process.env);\n\n/** The user's hard kill-switch. Deliberately separate from ANALYTICS_ENABLED,\n * which also folds in the build-time opt-in: the build flag decides whether\n * we SEND, this decides whether we may RECORD ANYTHING AT ALL. */\nexport const ANALYTICS_KILL_SWITCH = process.env.LIBI_ANALYTICS_DISABLED === \"1\";\n","import { type AnyColumn, Column } from '~/column.ts';\nimport { is } from '~/entity.ts';\nimport { type SQL, sql, type SQLWrapper } from '../sql.ts';\n\n/**\n * Returns the number of values in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // Number employees with null values\n * db.select({ value: count() }).from(employees)\n * // Number of employees where `name` is not null\n * db.select({ value: count(employees.name) }).from(employees)\n * ```\n *\n * @see countDistinct to get the number of non-duplicate values in `expression`\n */\nexport function count(expression?: SQLWrapper): SQL<number> {\n\treturn sql`count(${expression || sql.raw('*')})`.mapWith(Number);\n}\n\n/**\n * Returns the number of non-duplicate values in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // Number of employees where `name` is distinct\n * db.select({ value: countDistinct(employees.name) }).from(employees)\n * ```\n *\n * @see count to get the number of values in `expression`, including duplicates\n */\nexport function countDistinct(expression: SQLWrapper): SQL<number> {\n\treturn sql`count(distinct ${expression})`.mapWith(Number);\n}\n\n/**\n * Returns the average (arithmetic mean) of all non-null values in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // Average salary of an employee\n * db.select({ value: avg(employees.salary) }).from(employees)\n * ```\n *\n * @see avgDistinct to get the average of all non-null and non-duplicate values in `expression`\n */\nexport function avg(expression: SQLWrapper): SQL<string | null> {\n\treturn sql`avg(${expression})`.mapWith(String);\n}\n\n/**\n * Returns the average (arithmetic mean) of all non-null and non-duplicate values in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // Average salary of an employee where `salary` is distinct\n * db.select({ value: avgDistinct(employees.salary) }).from(employees)\n * ```\n *\n * @see avg to get the average of all non-null values in `expression`, including duplicates\n */\nexport function avgDistinct(expression: SQLWrapper): SQL<string | null> {\n\treturn sql`avg(distinct ${expression})`.mapWith(String);\n}\n\n/**\n * Returns the sum of all non-null values in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // Sum of every employee's salary\n * db.select({ value: sum(employees.salary) }).from(employees)\n * ```\n *\n * @see sumDistinct to get the sum of all non-null and non-duplicate values in `expression`\n */\nexport function sum(expression: SQLWrapper): SQL<string | null> {\n\treturn sql`sum(${expression})`.mapWith(String);\n}\n\n/**\n * Returns the sum of all non-null and non-duplicate values in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // Sum of every employee's salary where `salary` is distinct (no duplicates)\n * db.select({ value: sumDistinct(employees.salary) }).from(employees)\n * ```\n *\n * @see sum to get the sum of all non-null values in `expression`, including duplicates\n */\nexport function sumDistinct(expression: SQLWrapper): SQL<string | null> {\n\treturn sql`sum(distinct ${expression})`.mapWith(String);\n}\n\n/**\n * Returns the maximum value in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // The employee with the highest salary\n * db.select({ value: max(employees.salary) }).from(employees)\n * ```\n */\nexport function max<T extends SQLWrapper>(expression: T): SQL<(T extends AnyColumn ? T['_']['data'] : string) | null> {\n\treturn sql`max(${expression})`.mapWith(is(expression, Column) ? expression : String) as any;\n}\n\n/**\n * Returns the minimum value in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // The employee with the lowest salary\n * db.select({ value: min(employees.salary) }).from(employees)\n * ```\n */\nexport function min<T extends SQLWrapper>(expression: T): SQL<(T extends AnyColumn ? T['_']['data'] : string) | null> {\n\treturn sql`min(${expression})`.mapWith(is(expression, Column) ? expression : String) as any;\n}\n","// lib/analytics/events.ts\n// Single source of truth for analytics event names + param shapes. Imported by\n// both the client (gtag) and server (Measurement Protocol) transports.\n// Discipline: snake_case, <=40 chars, bounded-cardinality params (enums only —\n// never raw user text, file names, prompts, or IDs).\n\nexport const EVENT_NAMES = [\n // activation / onboarding (fired once via mark-once milestones)\n \"first_launch\",\n \"first_export\",\n \"persona_selected\",\n \"agent_connected\",\n // activation / onboarding (per-occurrence — NOT mark-once)\n //\n // The first-run demo film was built end to end (download + verify + write).\n // Param is the definition version only — bounded by construction. This is\n // the adoption signal for onboarding actually completing, as distinct from\n // `persona_selected`, which fires before anything is built.\n //\n // Emitted by `lib/jobs/runners/onboarding-piece.ts` on every non-reused\n // build, so a `force` rebuild fires it again; a dedupe hit fires nothing.\n // That is why it sits below the mark-once group rather than in it.\n \"onboarding_piece_built\",\n // Setting up an agent (Task 14) — the funnel between \"the app is open\" and\n // \"a message got sent\". Every step below fires on a REAL success path,\n // never on a click alone, or the funnel looks healthy while people are\n // stuck. Most carry no params (there's nothing bounded to say beyond\n // \"this step was reached\"); the ones that do are documented per event.\n //\n // The persona question actually painted on screen — not merely \"the\n // onboarding-state fetch resolved\". Guarded to fire once per appearance\n // (a ref, not a render count), so React re-rendering the modal while it's\n // still up never double-counts it.\n \"persona_prompt_shown\",\n // The connect-an-agent takeover replaced the editor (`rightRegionMode`\n // flipped to \"onboarding\" in app/(app)/editor/page.tsx). Same\n // once-per-appearance guard as `persona_prompt_shown`.\n \"agent_connect_shown\",\n // The install/sign-in action was started — emitted from the ONE place all\n // three surfaces run it through, `useAgentSetupCardState().onAction`, so\n // the sidebar's own `InstallChip` (which renders differently and never used\n // the shared card) reports its installs too. `agent` is the two-entry\n // registry id (`claude-code` | `codex`); `action` is `install` | `sign_in`;\n // `surface` is `onboarding` | `chat` | `sidebar` — the three surfaces that\n // can actually emit this. (`AnalyticsSurface` also carries `settings`,\n // which only `agent_sign_in_opened` reaches.) All closed, construction-time\n // enums, never free text.\n \"agent_setup_started\",\n // The manual \"Or do it yourself\" Copy button, in `ManualInstructionsBlock`.\n // Same bounded `agent`/`action` pair as `agent_setup_started` — this is\n // the do-it-yourself fork of that step, not a separate funnel branch.\n \"agent_setup_command_copied\",\n // `lib/jobs/runners/agent-install.ts` reached its VERIFY step and the\n // adapter is actually usable — not merely \"npm exited 0\" (see the\n // partial-install trap documented in lib/agents/claude-native-binary.ts).\n // `agent` is the bounded registry id; fires for the already-installed fast\n // path too, since that path re-verifies the same two primitives.\n \"agent_install_completed\",\n // The same runner's failure path. `reason` is mapped through\n // `installFailureReason` (lib/agents/runtime-install.ts) — the raw\n // npm/verification diagnostic routinely contains an absolute filesystem\n // path (see `claudeNativeBinaryMissingError`) and must never reach an\n // event param; only the bounded verdict does.\n \"agent_install_failed\",\n // libi handed a sign-in command to its OWN terminal\n // (`useRunRemedyInTerminal`), fired only once the terminal has actually\n // spawned — never on the button click, which can still fail to open one.\n // `agent`/`surface` are the same bounded pair the setup-card events use.\n \"agent_sign_in_opened\",\n // First user-sent chat message this install — via the mark-once milestone\n // primitive (`markAnalyticsMilestoneOnce`, the same one `POST\n // /api/analytics/milestone` wraps for client callers), NOT fired per\n // message, or this would just be a message counter wearing a first_*\n // name. No params.\n \"first_message_sent\",\n // core creation (UI-initiated; agent-driven creation is covered by tool_used)\n \"piece_created\",\n \"export_started\",\n \"export_completed\",\n \"agent_message_sent\",\n // asset / feature usage (UI-initiated; not covered by the tool_used wrap)\n \"file_uploaded\",\n \"terminal_session_started\",\n \"mcp_server_added\",\n \"codex_connect_toggled\",\n // A newer @nagellabs/libi runtime was fetched + laid down from Settings\n // (applies at next launch). Fired server-side at the install success path in\n // `lib/jobs/runners/runtime-update.ts`. The only param is\n // `previous_source: \"bundled\" | \"user\" | \"dev\"` — bounded by construction;\n // version strings are deliberately NOT sent (unbounded over the product's\n // life). This is the adoption signal for the npm-as-runtime model: how many\n // installs actually move off the snapshot their .app shipped with.\n \"runtime_update_installed\",\n \"version_restore\",\n \"overlay_added\",\n \"overlay_edited\",\n \"preset_applied\",\n \"effect_applied\",\n \"effect_previewed\",\n \"text_3d_used\",\n // \"Make it 3D\" toggled ON for any flat overlay kind (text/image/video/code).\n // Reports the kind so all flat kinds funnel through one bounded event.\n \"overlay_3d_used\",\n // chat command palette + context meter (UI-initiated; the resulting agent\n // turn is covered by agent_message_sent / tool_used)\n \"chat_slash_command\",\n \"context_meter_opened\",\n // surface / engagement\n \"page_view\",\n \"page_engagement\",\n // cross-cutting\n \"tool_used\",\n // membership interest — fired only after the server accepts the signup, so\n // this counts waitlist rows that exist and not form submissions that failed.\n // The only param is `plan`, bounded by the WaitlistCard's featureKey union;\n // the address itself is never sent here.\n \"pro_waitlist_joined\",\n // A site announcement banner was displayed (components/layout/\n // announcement-banner.tsx). The only param is `kind: \"feature\" | \"issue\"`\n // — bounded by construction; announcement ids and text are never sent.\n \"announcement_shown\",\n // privacy\n \"analytics_opt_out\",\n \"analytics_opt_in\",\n] as const;\n\nexport type AnalyticsEventName = (typeof EVENT_NAMES)[number];\n\nconst EVENT_NAME_SET = new Set<string>(EVENT_NAMES);\nexport function isEventName(name: string): name is AnalyticsEventName {\n return EVENT_NAME_SET.has(name);\n}\n\n/** Where a setup action (an install, a sign-in, a copy) was initiated from —\n * a closed, bounded set of UI surfaces. Shared by `AgentSetupCard`'s own\n * `surface` prop (`\"onboarding\"` | `\"chat\"`, the only two places that card\n * renders), by `useAgentSetupCardState` (those two plus `\"sidebar\"`, whose\n * install chip is its own rendering of the same state), and by\n * `useRunRemedyInTerminal`'s callers (`\"sidebar\"` from\n * `app-sidebar.tsx`/`sidebar-session-list.tsx`, `\"settings\"` from the MCPs &\n * Skills page's `connect-tools-section.tsx`) — every emit site draws from\n * the same set instead of each call site inventing its own string. Not every\n * event can produce every member; each event's own comment says which. */\nexport type AnalyticsSurface = \"onboarding\" | \"sidebar\" | \"chat\" | \"settings\";\n\n/** The two agent ids the setup/funnel events report. Narrows an\n * unconstrained id (an `AgentSetup.id`, a `SessionEntry.agentId`, an\n * `AgentReadiness`'s `agentId`, etc. — all plain `string` in their own\n * modules) down to the bounded param GA4 requires. Returns undefined for\n * `\"terminal\"` or anything else so a caller can skip the event entirely\n * rather than ever send an unbounded id as a param. */\nexport type AnalyticsAgentId = \"claude-code\" | \"codex\";\nexport function toAgentEventId(id: string | null | undefined): AnalyticsAgentId | undefined {\n return id === \"claude-code\" || id === \"codex\" ? id : undefined;\n}\n\nexport type AnalyticsParams = Record<string, string | number | boolean>;\n\n/** Drop undefined/null, coerce, truncate string values to GA4's 100-char limit. */\nexport function sanitizeParams(\n params: Record<string, unknown> | undefined,\n): AnalyticsParams {\n if (!params) return {};\n const out: AnalyticsParams = {};\n for (const [k, v] of Object.entries(params)) {\n if (v === undefined || v === null) continue;\n if (typeof v === \"string\") out[k] = v.slice(0, 100);\n else if (typeof v === \"number\" || typeof v === \"boolean\") out[k] = v;\n else out[k] = String(v).slice(0, 100);\n }\n return out;\n}\n","// lib/analytics/queue.ts\n//\n// Durable delivery for GA4 events. Every event lands in SQLite before it\n// ever touches the network, and only leaves the queue once GA4 has actually\n// accepted it (a 2xx from `/g/collect`). libi's \"server\" is a process on\n// the user's own machine that stops the instant they quit — and the most\n// valuable events in the funnel are fired by people who are about to do\n// exactly that: someone who gives up during agent setup quits DURING the\n// step we most need to see. A fire-and-forget `fetch` loses precisely\n// those.\n//\n// Opting out is the one deliberate exception: `drainAnalyticsQueue` clears\n// the whole queue the instant `enabled` is false rather than merely\n// pausing it — holding events for someone who opted out is exactly the\n// behaviour a privacy toggle must not have.\n\nimport { asc, count, eq, inArray, lte, sql } from \"drizzle-orm\";\nimport { getDb, migrateDatabase } from \"@/lib/db/client\";\nimport { analyticsQueue } from \"@/lib/db/schema/sqlite\";\nimport { buildCollectUrl, type CollectEvent } from \"@/lib/analytics/collect-url\";\nimport { advanceSession, type SessionState } from \"@/lib/analytics/identity\";\nimport { sanitizeParams } from \"@/lib/analytics/events\";\nimport {\n getAnalyticsSettings,\n getOrCreateAnalyticsUserId,\n markAnalyticsMilestoneOnce,\n} from \"@/lib/db/settings\";\nimport type { AnalyticsSettings } from \"@/lib/analytics/settings-logic\";\nimport {\n ANALYTICS_DEBUG,\n ANALYTICS_ENABLED,\n ANALYTICS_KILL_SWITCH,\n MEASUREMENT_ID,\n} from \"@/lib/analytics/config\";\nimport { isTestMode } from \"@/lib/test-mode\";\nimport { serverLogger as logger } from \"@/lib/logger\";\n\nexport const MAX_QUEUE_ROWS = 500;\nexport const MAX_ATTEMPTS = 8;\nexport const DRAIN_INTERVAL_MS = 15_000;\nexport const DRAIN_BATCH = 20;\n\nexport interface DrainDeps {\n fetchImpl?: typeof fetch;\n now?: () => number;\n getSettings?: () => AnalyticsSettings;\n /** Injected so the first-visit test can drive the milestone without\n * reaching into persisted settings. Defaults to the real, PERSISTED\n * primitive — see FIRST_HIT_MILESTONE below for why that matters. */\n markMilestoneOnce?: (name: string) => boolean;\n}\n\nexport interface DrainResult {\n sent: number;\n failed: number;\n dropped: number;\n}\n\n/** Exponential backoff for retrying a failed send: 30s, 60s, 120s, ... —\n * capped at an hour so a long-offline install still retries on a sane\n * cadence instead of hammering a dead network. */\nexport function backoffMs(attempts: number): number {\n return Math.min(30_000 * 2 ** (attempts - 1), 60 * 60 * 1000);\n}\n\n// ---------------------------------------------------------------------------\n// Module-level, in-process state\n// ---------------------------------------------------------------------------\n//\n// Session state is intentionally NOT persisted: a process restart\n// legitimately starts a new GA4 session (see lib/analytics/identity.ts).\n// Durability lives entirely in the `analytics_queue` table — never in a\n// module-level cache of pending rows, which a restart would silently drop.\n\nlet sessionState: SessionState | null = null;\n/** Distinct non-2xx statuses already warned about, so a persistently\n * broken endpoint logs once instead of once per queued event. */\nlet loggedStatuses = new Set<number>();\nlet drainTimer: ReturnType<typeof setInterval> | null = null;\n/** The drain currently outstanding, or null. See `runScheduledDrain`. */\nlet drainInFlight: Promise<void> | null = null;\nlet testDbReady = false;\n\n/** The persisted milestone behind GA4's `_fv` (first visit) flag.\n *\n * This USED to be a module-level `hasAttemptedAnyHit` boolean, whose own\n * comment admitted it only approximated \"first hit this install ever sent\"\n * — it reset with the process. GA4 derives New Users from `_fv`, so every\n * relaunch of libi registered a brand-new user and the funnel's denominator\n * inflated over the life of an install. `markAnalyticsMilestoneOnce` is the\n * already-built persisted primitive for exactly this.\n *\n * Its own name, deliberately: `\"launch\"` and `\"first_message\"` are taken and\n * mean other things. */\nexport const FIRST_HIT_MILESTONE = \"analytics_first_hit\";\n\n/** Test-only: lazily migrate a real, file-backed DB so unit tests here —\n * which have no lifecycle prelude to do it for them — have a table to\n * exercise. Deliberately NOT called from enqueue/drain: production\n * migrates via lib/server/lifecycle, and this module must not duplicate\n * that (a second migrateDatabase() call there would re-run backupDb()\n * for no reason). */\nfunction ensureTestDbReady(): void {\n if (testDbReady) return;\n testDbReady = true;\n try {\n migrateDatabase();\n } catch {\n // Already migrated (idempotent), or some other caller owns it — either\n // way there's nothing for this test helper to do about it.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Enqueue\n// ---------------------------------------------------------------------------\n\n/** Queue one analytics event for later delivery. Synchronous and total: it\n * never throws and never awaits, so a caller measuring a flow can never be\n * taken down by analytics.\n *\n * Three different gates apply here, and they are NOT interchangeable:\n * - isTestMode(): test-mode runs must not pollute real GA4 data.\n * - ANALYTICS_KILL_SWITCH (LIBI_ANALYTICS_DISABLED=1): the user's hard\n * kill-switch. A kill-switch that still writes rows to disk — even rows\n * that will never be sent, because nothing clears them without the\n * drain — is not a kill-switch, so this one stops recording, not just\n * sending.\n * - getAnalyticsSettings().enabled: the in-app opt-out toggle. Same\n * reasoning — an opted-out user must not accumulate analytics data on\n * disk just because the build that happens to be running never starts\n * the drain that would otherwise clear the table.\n *\n * ANALYTICS_ENABLED, by contrast, is deliberately NOT checked here. It is\n * the build-time install flag (see lib/analytics/config.ts) and gates only\n * startAnalyticsDrain below — the chokepoint that actually reaches the\n * network. Queuing itself is cheap and reversible — capped at\n * MAX_QUEUE_ROWS by evicting the oldest rows — so keeping it ungated on\n * the build flag here is the \"lose fewer events\" choice: an event queued\n * before a restart that flips the flag on is still there to send. */\nexport function enqueueAnalyticsEvent(name: string, params?: Record<string, unknown>): void {\n try {\n if (isTestMode() || ANALYTICS_KILL_SWITCH) return;\n // A settings-read failure falls through to the outer catch below, which\n // skips the write — the safe default when we can't tell whether the\n // user opted out.\n if (!getAnalyticsSettings().enabled) return;\n\n const clean = sanitizeParams(params);\n const db = getDb();\n db.insert(analyticsQueue)\n .values({\n id: crypto.randomUUID(),\n name,\n paramsJson: JSON.stringify(clean),\n // Always immediately due — a brand-new event has never failed yet,\n // so there is no backoff to honor.\n nextAttemptAt: new Date(0),\n })\n .run();\n\n // Cheap on the common path: only the row count is read here. The id\n // list is fetched (bounded by LIMIT) only on the rare over-cap branch.\n const [{ n }] = db.select({ n: count() }).from(analyticsQueue).all();\n if (n > MAX_QUEUE_ROWS) {\n const excess = db\n .select({ id: analyticsQueue.id })\n .from(analyticsQueue)\n .orderBy(asc(analyticsQueue.createdAt))\n .limit(n - MAX_QUEUE_ROWS)\n .all()\n .map((r) => r.id);\n db.delete(analyticsQueue).where(inArray(analyticsQueue.id, excess)).run();\n }\n } catch {\n // Total: analytics must never be the reason a caller's own flow breaks.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Drain\n// ---------------------------------------------------------------------------\n\n/** Attempt to deliver queued events to GA4. Reads the opt-out setting once,\n * up front: if analytics is disabled the WHOLE queue is dropped (not\n * merely paused — see module doc) and nothing is sent. Otherwise, up to\n * DRAIN_BATCH due rows are sent in order, advancing the in-process GA4\n * session once per actual send attempt so `_s` matches hits on the wire. */\nexport async function drainAnalyticsQueue(deps: DrainDeps = {}): Promise<DrainResult> {\n const now = deps.now ?? Date.now;\n const fetchImpl = deps.fetchImpl ?? fetch;\n const getSettings = deps.getSettings ?? getAnalyticsSettings;\n const markMilestoneOnce = deps.markMilestoneOnce ?? markAnalyticsMilestoneOnce;\n\n const db = getDb();\n const settings = getSettings();\n\n if (!settings.enabled) {\n const rows = db.select({ id: analyticsQueue.id }).from(analyticsQueue).all();\n if (rows.length > 0) db.delete(analyticsQueue).run();\n return { sent: 0, failed: 0, dropped: rows.length };\n }\n\n const nowMs = now();\n const due = db\n .select()\n .from(analyticsQueue)\n .where(lte(analyticsQueue.nextAttemptAt, new Date(nowMs)))\n // nextAttemptAt/createdAt are millisecond-resolution, so two events\n // queued in the same millisecond would otherwise have no defined\n // order. `rowid` is SQLite's own monotonically-increasing insertion\n // order for this table (no WITHOUT ROWID), so it's a free, exact\n // tiebreaker rather than one relying on query-plan incidence.\n .orderBy(asc(analyticsQueue.nextAttemptAt), asc(analyticsQueue.createdAt), asc(sql`rowid`))\n .limit(DRAIN_BATCH)\n .all();\n\n let sent = 0;\n let failed = 0;\n let dropped = 0;\n // Asked at most once per drain, and only when there is a row to attach it\n // to — an empty batch must not burn the milestone, and a 20-row batch has\n // no reason to re-read settings twenty times.\n let firstVisitAsked = false;\n const userId = settings.userId ?? getOrCreateAnalyticsUserId();\n\n for (const row of due) {\n const { state, sessionStart } = advanceSession(sessionState, nowMs);\n sessionState = state;\n // Persisted, not per-process: `_fv` is how GA4 counts New Users, so a\n // per-process flag made every relaunch look like a new install. Total by\n // construction — a settings failure must never cost us the hit, so it\n // just means we don't claim first-visit.\n let firstVisit = false;\n if (!firstVisitAsked) {\n firstVisitAsked = true;\n try {\n firstVisit = markMilestoneOnce(FIRST_HIT_MILESTONE);\n } catch {\n firstVisit = false;\n }\n }\n\n let params: Record<string, unknown> = {};\n try {\n params = JSON.parse(row.paramsJson) as Record<string, unknown>;\n } catch {\n params = {};\n }\n\n const ev: CollectEvent = {\n name: row.name,\n // Sanitized once already at enqueue time, into paramsJson. Sanitizing\n // again here is deliberate defense-in-depth against a hand-edited or\n // older row whose paramsJson predates a sanitization rule change —\n // sanitizeParams is idempotent, so this is a no-op on the common path.\n params: sanitizeParams(params),\n clientId: userId,\n userId,\n sessionId: state.sessionId,\n hitNumber: state.hitNumber,\n sessionStart,\n firstVisit,\n debug: ANALYTICS_DEBUG,\n // These are background/queued hits, not a live timed user session —\n // there's no real engagement span to report, so send the floor value\n // buildCollectUrl would clamp to anyway.\n engagementMs: 1,\n };\n const url = buildCollectUrl(ev, MEASUREMENT_ID);\n\n let ok = false;\n let status: number | null = null;\n try {\n const res = await fetchImpl(url, { method: \"POST\", signal: AbortSignal.timeout(5000) });\n ok = res.ok;\n status = res.status;\n } catch {\n ok = false;\n }\n\n if (ok) {\n db.delete(analyticsQueue).where(eq(analyticsQueue.id, row.id)).run();\n sent++;\n continue;\n }\n\n // Non-2xx is the one mitigation for `/g/collect` being undocumented: if\n // Google changes it, our own logs say so — once per distinct status,\n // not once per event.\n if (status !== null && !loggedStatuses.has(status)) {\n loggedStatuses.add(status);\n logger.warn(\n { tag: \"analytics\", op: \"collect_rejected\", status },\n \"GA4 /g/collect rejected an event\",\n );\n }\n\n const attempts = row.attempts + 1;\n if (attempts >= MAX_ATTEMPTS) {\n db.delete(analyticsQueue).where(eq(analyticsQueue.id, row.id)).run();\n dropped++;\n logger.warn(\n { tag: \"analytics\", op: \"event_dropped\", name: row.name, attempts },\n \"Dropping analytics event after exhausting retries\",\n );\n } else {\n db.update(analyticsQueue)\n .set({ attempts, nextAttemptAt: new Date(nowMs + backoffMs(attempts)) })\n .where(eq(analyticsQueue.id, row.id))\n .run();\n failed++;\n }\n }\n\n return { sent, failed, dropped };\n}\n\n// ---------------------------------------------------------------------------\n// Interval driver\n// ---------------------------------------------------------------------------\n\n/** Start the periodic drain. Idempotent — a second call while already\n * running is a no-op. Drains immediately, then every DRAIN_INTERVAL_MS.\n * The interval is unref'd so it can never keep the process alive on its\n * own. No-ops entirely when ANALYTICS_ENABLED is false — this is the\n * chokepoint that keeps a local dev build (analytics off by default) from\n * ever reaching the real network; see the note on enqueueAnalyticsEvent\n * above for why the gate lives here and not at enqueue time. */\nexport function startAnalyticsDrain(deps: DrainDeps = {}): void {\n if (!ANALYTICS_ENABLED || drainTimer) return;\n runScheduledDrain(deps);\n drainTimer = setInterval(() => runScheduledDrain(deps), DRAIN_INTERVAL_MS);\n drainTimer.unref?.();\n}\n\n/**\n * One scheduled drain tick, with a module-level in-flight guard.\n *\n * The interval used to fire `drainAnalyticsQueue()` without awaiting the\n * previous one, and rows are NOT reserved: the SELECT filters only on\n * `nextAttemptAt <= now`, and a row is deleted (or backed off) only AFTER its\n * send resolves. With DRAIN_BATCH = 20 rows sent SERIALLY, GA4 answering in a\n * perfectly ordinary ~1s means a batch takes 20s against a 15s interval — so\n * the next tick selected the same still-unresolved rows and POSTed them\n * again. On a degraded link (the 5s `AbortSignal.timeout`) one batch runs\n * ~100s, i.e. up to seven concurrent drains replaying one queue.\n * `/g/collect` has no idempotency, so every duplicate counts — and\n * over-reporting looks like healthy numbers, so nothing surfaces it. A fresh\n * install, the exact case the funnel exists to measure, fills the queue\n * fastest.\n *\n * Skipping (rather than queueing) the tick is right: the work is not lost,\n * the next interval picks up whatever is still due.\n */\nexport function runScheduledDrain(deps: DrainDeps = {}): Promise<void> {\n if (drainInFlight) return drainInFlight;\n const pass: Promise<void> = drainAnalyticsQueue(deps)\n .then(() => undefined)\n .catch((err) => {\n logger.warn({ tag: \"analytics\", op: \"drain_failed\", err }, \"Analytics drain failed\");\n })\n .finally(() => {\n if (drainInFlight === pass) drainInFlight = null;\n });\n drainInFlight = pass;\n return pass;\n}\n\n/** Stop the periodic drain. For tests. */\nexport function stopAnalyticsDrain(): void {\n if (drainTimer) {\n clearInterval(drainTimer);\n drainTimer = null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Test support\n// ---------------------------------------------------------------------------\n\n/** Clears the queue table, the in-process session state, and the logged-\n * status set. Test-only. Also lazily migrates a real, file-backed test DB\n * the first time it's called — unit tests here have no lifecycle prelude\n * to do that for them, and it must be a real DB on disk (not an in-memory\n * module cache) for the restart-durability test to prove anything. */\nexport function __resetAnalyticsQueueForTests(): void {\n ensureTestDbReady();\n getDb().delete(analyticsQueue).run();\n sessionState = null;\n drainInFlight = null;\n loggedStatuses = new Set<number>();\n}\n","// lib/analytics/server.ts\n// Server-side analytics entry point. Every server, route, job and MCP event\n// lands here, and here just means \"onto the durable queue\" — the queue owns\n// identity, sessionisation, sending and retry.\nimport { enqueueAnalyticsEvent } from \"@/lib/analytics/queue\";\n\n/** Record one analytics event. Synchronous, total, and fire-and-forget: it\n * cannot throw, cannot block, and cannot fail the flow that called it.\n *\n * The signature is deliberately `void` and not `Promise<void>`. The old one\n * returned a promise that every caller discarded with `void`, which made it\n * look awaitable when awaiting it meant \"wait for Google\" on a user's\n * critical path. There is nothing to await now.\n *\n * enqueueAnalyticsEvent is itself total (it has its own internal catch),\n * but this wrapper does not lean on that alone — \"cannot throw\" is a\n * promise this function makes on its own behalf, so it holds even if the\n * queue's contract ever changes underneath it. */\nexport function trackServerEvent(\n name: string,\n params?: Record<string, unknown>,\n): void {\n try {\n enqueueAnalyticsEvent(name, params);\n } catch {\n // Total: analytics must never be the reason a caller's own flow breaks.\n }\n}\n"],"names":["DEFAULT_ANALYTICS_SETTINGS","enabled","userId","optOutAt","firstRunNoticeShown","milestones","parseAnalyticsSettings","raw","p","JSON","parse","Array","isArray","filter","m","mergeAnalyticsSettings","cur","partial","markMilestone","name","includes","settings","added","applyOptOut","nowMs","applyOptIn","isTestMode","v","process","env","LIBI_TEST_MODE","MEASUREMENT_ID","NEXT_PUBLIC_LIBI_GA4_MEASUREMENT_ID","resolveAnalyticsEnabled","LIBI_ANALYTICS_DISABLED","NEXT_PUBLIC_LIBI_ANALYTICS","resolveAnalyticsDebug","NODE_ENV","ANALYTICS_ENABLED","ANALYTICS_DEBUG","ANALYTICS_KILL_SWITCH","EVENT_NAMES","EVENT_NAME_SET","Set","isEventName","has","toAgentEventId","id","undefined","sanitizeParams","params","out","k","Object","entries","slice","String","MAX_QUEUE_ROWS","MAX_ATTEMPTS","DRAIN_INTERVAL_MS","DRAIN_BATCH","backoffMs","attempts","Math","min","sessionState","loggedStatuses","drainTimer","drainInFlight","testDbReady","FIRST_HIT_MILESTONE","ensureTestDbReady","enqueueAnalyticsEvent","clean","db","insert","values","crypto","randomUUID","paramsJson","stringify","nextAttemptAt","Date","run","n","select","from","all","excess","orderBy","createdAt","limit","map","r","delete","where","drainAnalyticsQueue","deps","now","fetchImpl","fetch","getSettings","markMilestoneOnce","rows","length","sent","failed","dropped","due","firstVisitAsked","row","state","sessionStart","firstVisit","ev","clientId","sessionId","hitNumber","debug","engagementMs","url","ok","status","res","method","signal","AbortSignal","timeout","add","warn","tag","op","update","set","startAnalyticsDrain","runScheduledDrain","setInterval","unref","pass","then","catch","err","finally","stopAnalyticsDrain","clearInterval","__resetAnalyticsQueueForTests"],"mappings":"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"}}]
6
+ {"offset": {"line": 1, "column": 0}, "map": {"version":3,"sources":["../../../lib/db/settings.ts","../../../lib/analytics/settings-logic.ts","../../../lib/export/folder.ts","../../../lib/test-mode.ts","../../../lib/analytics/config.ts","../../../node_modules/src/sql/functions/aggregate.ts","../../../lib/analytics/events.ts","../../../lib/analytics/queue.ts","../../../lib/analytics/server.ts"],"sourcesContent":["import { eq } from \"drizzle-orm\";\nimport { getDb } from \"./client\";\nimport { settings } from \"./schema\";\nimport { serverLogger } from \"@/lib/logger\";\nimport {\n type AnalyticsSettings,\n parseAnalyticsSettings,\n mergeAnalyticsSettings,\n markMilestone,\n} from \"@/lib/analytics/settings-logic\";\nimport { crashReportChoiceAllowsReporting } from \"@/lib/sentry/enabled\";\n\nexport type { AnalyticsSettings };\n\nexport interface AppSettings {\n preferredAgent: string | null;\n panelChatSize: number;\n panelEditorSize: number;\n panelResourcesSize: number;\n panelChatVisible: boolean;\n panelResourcesVisible: boolean;\n agentApprovalModes: string | null;\n agentModelPreferences: string | null;\n onboardingPersona: string | null;\n personaSelectedAt: Date | null;\n agentEverConnected: boolean;\n /** Set once, server-side, the first time an agent connects. Null = the\n * first-run demo chip has never been armed. See lib/sessions/session-manager.ts#markAgentConnected. */\n onboardingDemoOfferedAt: Date | null;\n /** Set once the user dismisses OR takes the demo offer — final, and kept\n * independent of onboardingDemoOfferedAt so a dismissal is never confused\n * with \"never offered\". */\n onboardingDemoDismissedAt: Date | null;\n}\n\nconst DEFAULTS: AppSettings = {\n preferredAgent: null,\n panelChatSize: 40,\n panelEditorSize: 40,\n panelResourcesSize: 20,\n panelChatVisible: true,\n panelResourcesVisible: false,\n agentApprovalModes: null,\n agentModelPreferences: null,\n onboardingPersona: null,\n personaSelectedAt: null,\n agentEverConnected: false,\n onboardingDemoOfferedAt: null,\n onboardingDemoDismissedAt: null,\n};\n\n/**\n * Read the app settings (single row, id=1).\n * Auto-creates the row with defaults if it doesn't exist.\n */\nexport function getSettings(): AppSettings {\n const db = getDb();\n\n const [row] = db.select().from(settings).where(eq(settings.id, 1)).limit(1).all();\n\n if (!row) {\n db.insert(settings).values({ id: 1 }).run();\n return { ...DEFAULTS };\n }\n\n return {\n preferredAgent: row.preferredAgent ?? null,\n panelChatSize: row.panelChatSize,\n panelEditorSize: row.panelEditorSize,\n panelResourcesSize: row.panelResourcesSize,\n panelChatVisible: row.panelChatVisible,\n panelResourcesVisible: row.panelResourcesVisible,\n agentApprovalModes: row.agentApprovalModes ?? null,\n agentModelPreferences: row.agentModelPreferences ?? null,\n onboardingPersona: row.onboardingPersona ?? null,\n personaSelectedAt: row.personaSelectedAt ?? null,\n agentEverConnected: row.agentEverConnected,\n onboardingDemoOfferedAt: row.onboardingDemoOfferedAt ?? null,\n onboardingDemoDismissedAt: row.onboardingDemoDismissedAt ?? null,\n };\n}\n\n/**\n * Update specific settings fields. Only provided fields are updated.\n */\nexport function updateSettings(partial: Partial<AppSettings>): void {\n const db = getDb();\n\n // Build the SET clause from provided fields\n const set: Record<string, unknown> = { updatedAt: new Date() };\n\n if (partial.preferredAgent !== undefined) set.preferredAgent = partial.preferredAgent;\n if (partial.panelChatSize !== undefined) set.panelChatSize = partial.panelChatSize;\n if (partial.panelEditorSize !== undefined) set.panelEditorSize = partial.panelEditorSize;\n if (partial.panelResourcesSize !== undefined) set.panelResourcesSize = partial.panelResourcesSize;\n if (partial.panelChatVisible !== undefined) set.panelChatVisible = partial.panelChatVisible;\n if (partial.panelResourcesVisible !== undefined) set.panelResourcesVisible = partial.panelResourcesVisible;\n if (partial.agentApprovalModes !== undefined) set.agentApprovalModes = partial.agentApprovalModes;\n if (partial.agentModelPreferences !== undefined) set.agentModelPreferences = partial.agentModelPreferences;\n if (partial.onboardingPersona !== undefined) set.onboardingPersona = partial.onboardingPersona;\n if (partial.personaSelectedAt !== undefined) set.personaSelectedAt = partial.personaSelectedAt;\n if (partial.agentEverConnected !== undefined) set.agentEverConnected = partial.agentEverConnected;\n if (partial.onboardingDemoOfferedAt !== undefined) set.onboardingDemoOfferedAt = partial.onboardingDemoOfferedAt;\n if (partial.onboardingDemoDismissedAt !== undefined) set.onboardingDemoDismissedAt = partial.onboardingDemoDismissedAt;\n\n // Upsert: insert defaults if row doesn't exist, update if it does\n db.insert(settings)\n .values({ id: 1, ...set })\n .onConflictDoUpdate({ target: settings.id, set })\n .run();\n}\n\n// ---------------------------------------------------------------------------\n// Notifications setting (typed helpers over the settings.notifications JSON column)\n// ---------------------------------------------------------------------------\n\nexport type NotificationsSetting = {\n /** Push a system notification when a background job completes while the window is backgrounded. */\n backgroundJobComplete: boolean;\n};\n\nconst NOTIFICATIONS_DEFAULTS: NotificationsSetting = {\n backgroundJobComplete: true,\n};\n\n/**\n * Read the notifications setting. Returns defaults if the row is missing,\n * the column is null/empty, or the stored JSON is malformed / wrong-shape.\n */\nexport function getNotificationsSetting(): NotificationsSetting {\n const db = getDb();\n const [row] = db\n .select({ notifications: settings.notifications })\n .from(settings)\n .where(eq(settings.id, 1))\n .limit(1)\n .all();\n\n const raw = row?.notifications;\n if (!raw) return { ...NOTIFICATIONS_DEFAULTS };\n\n try {\n const parsed = JSON.parse(raw) as Partial<NotificationsSetting> | null;\n if (!parsed || typeof parsed !== \"object\") return { ...NOTIFICATIONS_DEFAULTS };\n if (typeof parsed.backgroundJobComplete !== \"boolean\") {\n return { ...NOTIFICATIONS_DEFAULTS };\n }\n return { backgroundJobComplete: parsed.backgroundJobComplete };\n } catch {\n return { ...NOTIFICATIONS_DEFAULTS };\n }\n}\n\n/**\n * Persist the notifications setting as a JSON string in the settings table.\n * Upserts the single-row settings record if it doesn't already exist.\n */\nexport function setNotificationsSetting(s: NotificationsSetting): void {\n const db = getDb();\n const value = JSON.stringify(s);\n const set = { notifications: value, updatedAt: new Date() };\n\n db.insert(settings)\n .values({ id: 1, ...set })\n .onConflictDoUpdate({ target: settings.id, set })\n .run();\n}\n\n// ---------------------------------------------------------------------------\n// Codex connect setting (typed helpers over the settings.codex JSON column)\n// ---------------------------------------------------------------------------\n\nexport type CodexConnectSetting = {\n /** Whether the user opted in to keep their global ~/.codex MCP entries in sync with libi. */\n connected: boolean;\n /** Outcome of the last sync attempt. */\n lastSyncStatus: \"ok\" | \"error\" | \"idle\";\n};\n\nconst CODEX_CONNECT_DEFAULTS: CodexConnectSetting = {\n connected: false,\n lastSyncStatus: \"idle\",\n};\n\n/**\n * Read the codex connect setting. Returns defaults if the row is missing,\n * the column is null/empty, the stored JSON is malformed / wrong-shape, or\n * the DB read itself fails (e.g. a pending migration) — this is a defensive\n * read that must never throw.\n */\nexport function getCodexConnectSetting(): CodexConnectSetting {\n try {\n const db = getDb();\n const [row] = db\n .select({ codex: settings.codex })\n .from(settings)\n .where(eq(settings.id, 1))\n .limit(1)\n .all();\n\n const raw = row?.codex;\n if (!raw) return { ...CODEX_CONNECT_DEFAULTS };\n\n const parsed = JSON.parse(raw) as Partial<CodexConnectSetting> | null;\n if (!parsed || typeof parsed !== \"object\") return { ...CODEX_CONNECT_DEFAULTS };\n if (typeof parsed.connected !== \"boolean\") return { ...CODEX_CONNECT_DEFAULTS };\n const status = parsed.lastSyncStatus;\n if (status !== \"ok\" && status !== \"error\" && status !== \"idle\") {\n return { ...CODEX_CONNECT_DEFAULTS };\n }\n return { connected: parsed.connected, lastSyncStatus: status };\n } catch (err) {\n serverLogger.warn(\n { err, tag: \"codex-connect\", op: \"read_degraded\" },\n \"getCodexConnectSetting: read failed, returning defaults\",\n );\n return { ...CODEX_CONNECT_DEFAULTS };\n }\n}\n\n/**\n * Persist the codex connect setting as a JSON string in the settings table.\n * Upserts the single-row settings record if it doesn't already exist.\n */\nexport function setCodexConnectSetting(s: CodexConnectSetting): void {\n const db = getDb();\n const value = JSON.stringify(s);\n const set = { codex: value, updatedAt: new Date() };\n\n db.insert(settings)\n .values({ id: 1, ...set })\n .onConflictDoUpdate({ target: settings.id, set })\n .run();\n}\n\n// ---------------------------------------------------------------------------\n// Export defaults setting (folder + default format + default quality)\n// ---------------------------------------------------------------------------\n\nimport { defaultExportFolder } from \"@/lib/export/folder\";\n\nexport type ExportDefaultsSetting = {\n /** Absolute path. Null = use OS-aware default (~/Movies/libi on darwin, ~/Videos/libi elsewhere). */\n folder: string | null;\n format: \"mp4\" | \"webm\";\n quality: \"source\" | \"1080p\" | \"1440p\" | \"4k\";\n};\n\nconst EXPORT_DEFAULTS_FALLBACK: ExportDefaultsSetting = {\n folder: null,\n format: \"mp4\",\n quality: \"source\",\n};\n\n/** Read the export defaults. Falls back to OS-aware folder + MP4/Source if\n * the row is missing, the column is empty, or the JSON is malformed. */\nexport function getExportDefaults(): ExportDefaultsSetting {\n const db = getDb();\n const [row] = db\n .select({ exportDefaults: settings.exportDefaults })\n .from(settings)\n .where(eq(settings.id, 1))\n .limit(1)\n .all();\n\n const raw = row?.exportDefaults;\n if (!raw) return { ...EXPORT_DEFAULTS_FALLBACK };\n\n try {\n const parsed = JSON.parse(raw) as Partial<ExportDefaultsSetting> | null;\n if (!parsed || typeof parsed !== \"object\") return { ...EXPORT_DEFAULTS_FALLBACK };\n return {\n folder: typeof parsed.folder === \"string\" && parsed.folder ? parsed.folder : null,\n format: parsed.format === \"webm\" ? \"webm\" : \"mp4\",\n quality:\n parsed.quality === \"1080p\" || parsed.quality === \"1440p\" || parsed.quality === \"4k\"\n ? parsed.quality\n : \"source\",\n };\n } catch {\n return { ...EXPORT_DEFAULTS_FALLBACK };\n }\n}\n\n/** Resolve the effective export folder — settings if set, otherwise OS default. */\nexport function resolveExportFolder(): string {\n const defaults = getExportDefaults();\n return defaults.folder ?? defaultExportFolder();\n}\n\n/** Persist the export defaults as JSON in the settings table. */\nexport function setExportDefaults(s: ExportDefaultsSetting): void {\n const db = getDb();\n const value = JSON.stringify(s);\n const set = { exportDefaults: value, updatedAt: new Date() };\n\n db.insert(settings)\n .values({ id: 1, ...set })\n .onConflictDoUpdate({ target: settings.id, set })\n .run();\n}\n\n// ---------------------------------------------------------------------------\n// Bundled-skill digest cache (per-app-version, see mcp/skills/digest.ts)\n// ---------------------------------------------------------------------------\n\nimport type { SkillDigestCache } from \"@/mcp/skills/digest\";\n\n/** Read the cached bundled-skill digests. Null when missing, malformed,\n * or wrong-shape — callers treat null as \"recompute\". */\nexport function getSkillDigestCacheSetting(): SkillDigestCache | null {\n const db = getDb();\n const [row] = db\n .select({ skillDigestCache: settings.skillDigestCache })\n .from(settings)\n .where(eq(settings.id, 1))\n .limit(1)\n .all();\n\n const raw = row?.skillDigestCache;\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<SkillDigestCache> | null;\n if (!parsed || typeof parsed !== \"object\") return null;\n if (typeof parsed.version !== \"string\" || !parsed.version) return null;\n if (!parsed.digests || typeof parsed.digests !== \"object\") return null;\n return { version: parsed.version, digests: parsed.digests as Record<string, string> };\n } catch {\n return null;\n }\n}\n\n/** Persist the bundled-skill digest cache as JSON in the settings table. */\nexport function setSkillDigestCacheSetting(c: SkillDigestCache): void {\n const db = getDb();\n const set = { skillDigestCache: JSON.stringify(c), updatedAt: new Date() };\n db.insert(settings)\n .values({ id: 1, ...set })\n .onConflictDoUpdate({ target: settings.id, set })\n .run();\n}\n\n// ---------------------------------------------------------------------------\n// Analytics settings (typed helpers over the settings.analytics JSON column)\n// ---------------------------------------------------------------------------\n\nexport function getAnalyticsSettings(): AnalyticsSettings {\n const db = getDb();\n const [row] = db\n .select({ analytics: settings.analytics })\n .from(settings)\n .where(eq(settings.id, 1))\n .limit(1)\n .all();\n return parseAnalyticsSettings(row?.analytics ?? null);\n}\n\nexport function setAnalyticsSettings(partial: Partial<AnalyticsSettings>): AnalyticsSettings {\n const db = getDb();\n const next = mergeAnalyticsSettings(getAnalyticsSettings(), partial);\n const set = { analytics: JSON.stringify(next), updatedAt: new Date() };\n db.insert(settings)\n .values({ id: 1, ...set })\n .onConflictDoUpdate({ target: settings.id, set })\n .run();\n return next;\n}\n\n/** Return the per-install analytics UUID, generating + persisting it on first call. */\nexport function getOrCreateAnalyticsUserId(): string {\n const cur = getAnalyticsSettings();\n if (cur.userId) return cur.userId;\n const userId = crypto.randomUUID();\n setAnalyticsSettings({ userId });\n return userId;\n}\n\n/** Mark an activation milestone; returns true only the first time. */\nexport function markAnalyticsMilestoneOnce(name: string): boolean {\n const { settings: next, added } = markMilestone(getAnalyticsSettings(), name);\n if (added) setAnalyticsSettings({ milestones: next.milestones });\n return added;\n}\n\n// ---------------------------------------------------------------------------\n// Crash report settings (typed helpers over the settings.crashReports JSON column)\n// ---------------------------------------------------------------------------\n\nexport type CrashReportChoice = \"unset\" | \"on\" | \"off\";\n\nexport interface CrashReportSettings {\n choice: CrashReportChoice;\n /** ms epoch when the user last made an explicit choice; null while \"unset\". */\n decidedAt: number | null;\n}\n\nconst CRASH_REPORT_DEFAULTS: CrashReportSettings = {\n choice: \"unset\",\n decidedAt: null,\n};\n\n/**\n * Parse the persisted crash-report settings. Tolerant of null, malformed\n * JSON, and unknown `choice` values — any of those return the default.\n */\nexport function parseCrashReportSettings(raw: string | null): CrashReportSettings {\n if (!raw) return { ...CRASH_REPORT_DEFAULTS };\n try {\n const parsed = JSON.parse(raw) as Partial<CrashReportSettings> | null;\n if (!parsed || typeof parsed !== \"object\") return { ...CRASH_REPORT_DEFAULTS };\n const choice = parsed.choice;\n if (choice !== \"unset\" && choice !== \"on\" && choice !== \"off\") {\n return { ...CRASH_REPORT_DEFAULTS };\n }\n const decidedAt = typeof parsed.decidedAt === \"number\" ? parsed.decidedAt : null;\n return { choice, decidedAt };\n } catch {\n return { ...CRASH_REPORT_DEFAULTS };\n }\n}\n\n/** Read the crash-report settings. Returns defaults if the row is missing,\n * the column is null/empty, or the stored JSON is malformed / wrong-shape. */\nexport function getCrashReportSettings(): CrashReportSettings {\n const db = getDb();\n const [row] = db\n .select({ crashReports: settings.crashReports })\n .from(settings)\n .where(eq(settings.id, 1))\n .limit(1)\n .all();\n return parseCrashReportSettings(row?.crashReports ?? null);\n}\n\n/** Persist the crash-report settings as JSON in the settings table. */\nexport function setCrashReportSettings(next: CrashReportSettings): CrashReportSettings {\n const db = getDb();\n const value = JSON.stringify(next);\n const set = { crashReports: value, updatedAt: new Date() };\n db.insert(settings)\n .values({ id: 1, ...set })\n .onConflictDoUpdate({ target: settings.id, set })\n .run();\n return next;\n}\n\n/** \"unset\" means we have not asked yet, and libi currently reports by default,\n * so it resolves to true. Only an explicit \"off\" disables. Delegates to\n * lib/sentry/enabled.ts#crashReportChoiceAllowsReporting so this predicate\n * can't drift from the one the Sentry hooks actually gate on. */\nexport function crashReportsAllowed(s: CrashReportSettings): boolean {\n return crashReportChoiceAllowsReporting(s.choice);\n}\n","// lib/analytics/settings-logic.ts\n// Pure, dependency-free transforms over the analytics settings object. The DB\n// wrappers in lib/db/settings.ts call these; keeping them pure makes them\n// trivially unit-testable without a database.\n\nexport interface AnalyticsSettings {\n /** Master opt-out toggle. Default true (analytics on by default). */\n enabled: boolean;\n /** Per-install UUID used as GA4 client_id + user_id. Generated server-side. */\n userId: string | null;\n /** Epoch ms when the user last opted out (null if never). */\n optOutAt: number | null;\n /** Whether the one-time first-run privacy notice has been shown. */\n firstRunNoticeShown: boolean;\n /** Activation milestones already fired (e.g. \"launch\", \"piece\", \"export\"). */\n milestones: string[];\n}\n\nexport const DEFAULT_ANALYTICS_SETTINGS: AnalyticsSettings = {\n enabled: true,\n userId: null,\n optOutAt: null,\n firstRunNoticeShown: false,\n milestones: [],\n};\n\nexport function parseAnalyticsSettings(raw: string | null | undefined): AnalyticsSettings {\n if (!raw) return { ...DEFAULT_ANALYTICS_SETTINGS };\n try {\n const p = JSON.parse(raw) as Partial<AnalyticsSettings> | null;\n if (!p || typeof p !== \"object\") return { ...DEFAULT_ANALYTICS_SETTINGS };\n return {\n enabled: typeof p.enabled === \"boolean\" ? p.enabled : true,\n userId: typeof p.userId === \"string\" ? p.userId : null,\n optOutAt: typeof p.optOutAt === \"number\" ? p.optOutAt : null,\n firstRunNoticeShown: p.firstRunNoticeShown === true,\n milestones: Array.isArray(p.milestones)\n ? p.milestones.filter((m): m is string => typeof m === \"string\")\n : [],\n };\n } catch {\n return { ...DEFAULT_ANALYTICS_SETTINGS };\n }\n}\n\nexport function mergeAnalyticsSettings(\n cur: AnalyticsSettings,\n partial: Partial<AnalyticsSettings>,\n): AnalyticsSettings {\n return { ...cur, ...partial };\n}\n\nexport function markMilestone(\n cur: AnalyticsSettings,\n name: string,\n): { settings: AnalyticsSettings; added: boolean } {\n if (cur.milestones.includes(name)) return { settings: cur, added: false };\n return { settings: { ...cur, milestones: [...cur.milestones, name] }, added: true };\n}\n\nexport function applyOptOut(cur: AnalyticsSettings, nowMs: number): AnalyticsSettings {\n return { ...cur, enabled: false, optOutAt: nowMs };\n}\n\nexport function applyOptIn(cur: AnalyticsSettings): AnalyticsSettings {\n return { ...cur, enabled: true, optOutAt: null };\n}\n","import os from \"node:os\";\nimport path from \"node:path\";\nimport fs from \"node:fs\";\n\n/** Default per-OS root for exported videos. Picked to match user\n * expectations: macOS ships a Movies folder; Windows + Linux ship Videos. */\nexport function defaultExportFolder(): string {\n const home = os.homedir();\n switch (process.platform) {\n case \"darwin\":\n return path.join(home, \"Movies\", \"libi\");\n case \"win32\":\n return path.join(home, \"Videos\", \"libi\");\n default:\n return path.join(home, \"Videos\", \"libi\");\n }\n}\n\n/** Ensure a folder exists. Throws on permission errors so the export\n * surfaces a clear error to the user rather than silently writing nowhere. */\nexport function ensureFolderExists(absPath: string): void {\n fs.mkdirSync(absPath, { recursive: true });\n}\n\n/** Returns true iff the folder exists and is writable by the current user.\n * Used by the export modal to pre-flight before submitting. */\nexport function isFolderWritable(absPath: string): boolean {\n try {\n fs.accessSync(absPath, fs.constants.W_OK);\n return true;\n } catch {\n return false;\n }\n}\n","/**\n * Centralized test-mode detection. Returns true when the user launched libi\n * with `LIBI_TEST_MODE=1 npx libi`. Used by:\n * - lib/mcp-config.ts to swap fal-ai's transport to the fake-fal stdio\n * server (mcp/dev/fake-fal/) so the agent uses real fal-ai tool names\n * but gets deterministic placeholder outputs at zero cost\n * - mcp/instructions.ts for the TEST MODE banner\n *\n * Read at every call site rather than cached so test code can stub the env\n * via vi.stubEnv() without rebuilding modules.\n */\nexport function isTestMode(): boolean {\n const v = process.env.LIBI_TEST_MODE;\n return v === \"1\" || v === \"true\";\n}\n","// lib/analytics/config.ts\n// GA4 analytics config — committed constants + enablement, mirroring\n// lib/sentry/config.ts. The packaged Electron build and `npx libi` CLI have no\n// .env at runtime, so the measurement ID is HARDCODED here.\n//\n// SECURITY: a hardcoded measurement ID is fine to commit — GA4 web streams\n// (`/g/collect`, see lib/analytics/collect-url.ts) accept events with NO\n// secret at all, the same way every public gtag site on the internet works.\n// Both transports (browser and server) send through that one web-stream path\n// now, so there is no server/client asymmetry left to guard and no secret to\n// lose. This file used to also hold a Measurement Protocol api_secret, whose\n// absence silently disabled every server-side event in every install; it is\n// gone along with the path that needed it.\n\n/** GA4 web data stream measurement ID (public). Env override for staging/self-host. */\nexport const MEASUREMENT_ID =\n process.env.NEXT_PUBLIC_LIBI_GA4_MEASUREMENT_ID || \"G-7XJTYHWMFN\";\n\n/** Loose env shape — accepts process.env and plain object literals (for tests). */\ntype EnvLike = Record<string, string | undefined>;\n\n/** Pure enablement resolver (testable). Default OFF; opt-in flag set by launchers\n * for real installs; hard kill-switch wins. */\nexport function resolveAnalyticsEnabled(env: EnvLike): boolean {\n return env.LIBI_ANALYTICS_DISABLED !== \"1\" && env.NEXT_PUBLIC_LIBI_ANALYTICS === \"1\";\n}\n\n/** debug_mode → GA4 DebugView.\n *\n * On by default outside production, so a dev clone's events land in DebugView\n * instead of the real reports. `LIBI_ANALYTICS_DEBUG=1` turns it on explicitly,\n * and that opt-in is the ONLY way to inspect a RELEASED build: every artifact we\n * publish is built with NODE_ENV=production, so without it a shipped install can\n * never appear in DebugView — which is where a hit's full parameter payload is\n * visible, and therefore the only place \"the funnel sends the right enums\" can\n * actually be checked. Realtime shows that events arrived; it does not show what\n * they carried. Release-day QA sets this on the QA instance and nowhere else.\n *\n * The cost of leaving it on is real, which is why it stays opt-in: GA4 excludes\n * debug_mode hits from standard reports, so a user who set it would go\n * uncounted. */\nexport function resolveAnalyticsDebug(env: EnvLike): boolean {\n return env.LIBI_ANALYTICS_DEBUG === \"1\" || env.NODE_ENV !== \"production\";\n}\n\n/** Process-resolved values for runtime use. */\nexport const ANALYTICS_ENABLED = resolveAnalyticsEnabled(process.env);\nexport const ANALYTICS_DEBUG = resolveAnalyticsDebug(process.env);\n\n/** The user's hard kill-switch. Deliberately separate from ANALYTICS_ENABLED,\n * which also folds in the build-time opt-in: the build flag decides whether\n * we SEND, this decides whether we may RECORD ANYTHING AT ALL. */\nexport const ANALYTICS_KILL_SWITCH = process.env.LIBI_ANALYTICS_DISABLED === \"1\";\n","import { type AnyColumn, Column } from '~/column.ts';\nimport { is } from '~/entity.ts';\nimport { type SQL, sql, type SQLWrapper } from '../sql.ts';\n\n/**\n * Returns the number of values in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // Number employees with null values\n * db.select({ value: count() }).from(employees)\n * // Number of employees where `name` is not null\n * db.select({ value: count(employees.name) }).from(employees)\n * ```\n *\n * @see countDistinct to get the number of non-duplicate values in `expression`\n */\nexport function count(expression?: SQLWrapper): SQL<number> {\n\treturn sql`count(${expression || sql.raw('*')})`.mapWith(Number);\n}\n\n/**\n * Returns the number of non-duplicate values in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // Number of employees where `name` is distinct\n * db.select({ value: countDistinct(employees.name) }).from(employees)\n * ```\n *\n * @see count to get the number of values in `expression`, including duplicates\n */\nexport function countDistinct(expression: SQLWrapper): SQL<number> {\n\treturn sql`count(distinct ${expression})`.mapWith(Number);\n}\n\n/**\n * Returns the average (arithmetic mean) of all non-null values in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // Average salary of an employee\n * db.select({ value: avg(employees.salary) }).from(employees)\n * ```\n *\n * @see avgDistinct to get the average of all non-null and non-duplicate values in `expression`\n */\nexport function avg(expression: SQLWrapper): SQL<string | null> {\n\treturn sql`avg(${expression})`.mapWith(String);\n}\n\n/**\n * Returns the average (arithmetic mean) of all non-null and non-duplicate values in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // Average salary of an employee where `salary` is distinct\n * db.select({ value: avgDistinct(employees.salary) }).from(employees)\n * ```\n *\n * @see avg to get the average of all non-null values in `expression`, including duplicates\n */\nexport function avgDistinct(expression: SQLWrapper): SQL<string | null> {\n\treturn sql`avg(distinct ${expression})`.mapWith(String);\n}\n\n/**\n * Returns the sum of all non-null values in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // Sum of every employee's salary\n * db.select({ value: sum(employees.salary) }).from(employees)\n * ```\n *\n * @see sumDistinct to get the sum of all non-null and non-duplicate values in `expression`\n */\nexport function sum(expression: SQLWrapper): SQL<string | null> {\n\treturn sql`sum(${expression})`.mapWith(String);\n}\n\n/**\n * Returns the sum of all non-null and non-duplicate values in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // Sum of every employee's salary where `salary` is distinct (no duplicates)\n * db.select({ value: sumDistinct(employees.salary) }).from(employees)\n * ```\n *\n * @see sum to get the sum of all non-null values in `expression`, including duplicates\n */\nexport function sumDistinct(expression: SQLWrapper): SQL<string | null> {\n\treturn sql`sum(distinct ${expression})`.mapWith(String);\n}\n\n/**\n * Returns the maximum value in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // The employee with the highest salary\n * db.select({ value: max(employees.salary) }).from(employees)\n * ```\n */\nexport function max<T extends SQLWrapper>(expression: T): SQL<(T extends AnyColumn ? T['_']['data'] : string) | null> {\n\treturn sql`max(${expression})`.mapWith(is(expression, Column) ? expression : String) as any;\n}\n\n/**\n * Returns the minimum value in `expression`.\n *\n * ## Examples\n *\n * ```ts\n * // The employee with the lowest salary\n * db.select({ value: min(employees.salary) }).from(employees)\n * ```\n */\nexport function min<T extends SQLWrapper>(expression: T): SQL<(T extends AnyColumn ? T['_']['data'] : string) | null> {\n\treturn sql`min(${expression})`.mapWith(is(expression, Column) ? expression : String) as any;\n}\n","// lib/analytics/events.ts\n// Single source of truth for analytics event names + param shapes. Imported by\n// both the client (gtag) and server (Measurement Protocol) transports.\n// Discipline: snake_case, <=40 chars, bounded-cardinality params (enums only —\n// never raw user text, file names, prompts, or IDs).\n\nexport const EVENT_NAMES = [\n // activation / onboarding (fired once via mark-once milestones)\n \"first_launch\",\n \"first_export\",\n \"persona_selected\",\n \"agent_connected\",\n // activation / onboarding (per-occurrence — NOT mark-once)\n //\n // The first-run demo film was built end to end (download + verify + write).\n // Param is the definition version only — bounded by construction. This is\n // the adoption signal for onboarding actually completing, as distinct from\n // `persona_selected`, which fires before anything is built.\n //\n // Emitted by `lib/jobs/runners/onboarding-piece.ts` on every non-reused\n // build, so a `force` rebuild fires it again; a dedupe hit fires nothing.\n // That is why it sits below the mark-once group rather than in it.\n \"onboarding_piece_built\",\n // Setting up an agent (Task 14) — the funnel between \"the app is open\" and\n // \"a message got sent\". Every step below fires on a REAL success path,\n // never on a click alone, or the funnel looks healthy while people are\n // stuck. Most carry no params (there's nothing bounded to say beyond\n // \"this step was reached\"); the ones that do are documented per event.\n //\n // The persona question actually painted on screen — not merely \"the\n // onboarding-state fetch resolved\". Guarded to fire once per appearance\n // (a ref, not a render count), so React re-rendering the modal while it's\n // still up never double-counts it.\n \"persona_prompt_shown\",\n // The connect-an-agent takeover replaced the editor (`rightRegionMode`\n // flipped to \"onboarding\" in app/(app)/editor/page.tsx). Same\n // once-per-appearance guard as `persona_prompt_shown`.\n \"agent_connect_shown\",\n // The install/sign-in action was started — emitted from the ONE place all\n // three surfaces run it through, `useAgentSetupCardState().onAction`, so\n // the sidebar's own `InstallChip` (which renders differently and never used\n // the shared card) reports its installs too. `agent` is the two-entry\n // registry id (`claude-code` | `codex`); `action` is `install` | `sign_in`;\n // `surface` is `onboarding` | `chat` | `sidebar` — the three surfaces that\n // can actually emit this. (`AnalyticsSurface` also carries `settings`,\n // which only `agent_sign_in_opened` reaches.) All closed, construction-time\n // enums, never free text.\n \"agent_setup_started\",\n // The manual \"Or do it yourself\" Copy button, in `ManualInstructionsBlock`.\n // Same bounded `agent`/`action` pair as `agent_setup_started` — this is\n // the do-it-yourself fork of that step, not a separate funnel branch.\n \"agent_setup_command_copied\",\n // `lib/jobs/runners/agent-install.ts` reached its VERIFY step and the\n // adapter is actually usable — not merely \"npm exited 0\" (see the\n // partial-install trap documented in lib/agents/claude-native-binary.ts).\n // `agent` is the bounded registry id; fires for the already-installed fast\n // path too, since that path re-verifies the same two primitives.\n \"agent_install_completed\",\n // The same runner's failure path. `reason` is mapped through\n // `installFailureReason` (lib/agents/runtime-install.ts) — the raw\n // npm/verification diagnostic routinely contains an absolute filesystem\n // path (see `claudeNativeBinaryMissingError`) and must never reach an\n // event param; only the bounded verdict does.\n \"agent_install_failed\",\n // libi handed a sign-in command to its OWN terminal\n // (`useRunRemedyInTerminal`), fired only once the terminal has actually\n // spawned — never on the button click, which can still fail to open one.\n // `agent`/`surface` are the same bounded pair the setup-card events use.\n \"agent_sign_in_opened\",\n // First user-sent chat message this install — via the mark-once milestone\n // primitive (`markAnalyticsMilestoneOnce`, the same one `POST\n // /api/analytics/milestone` wraps for client callers), NOT fired per\n // message, or this would just be a message counter wearing a first_*\n // name. No params.\n \"first_message_sent\",\n // core creation (UI-initiated; agent-driven creation is covered by tool_used)\n \"piece_created\",\n \"export_started\",\n \"export_completed\",\n \"agent_message_sent\",\n // asset / feature usage (UI-initiated; not covered by the tool_used wrap)\n \"file_uploaded\",\n \"terminal_session_started\",\n \"mcp_server_added\",\n \"codex_connect_toggled\",\n // A newer @nagellabs/libi runtime was fetched + laid down from Settings\n // (applies at next launch). Fired server-side at the install success path in\n // `lib/jobs/runners/runtime-update.ts`. The only param is\n // `previous_source: \"bundled\" | \"user\" | \"dev\"` — bounded by construction;\n // version strings are deliberately NOT sent (unbounded over the product's\n // life). This is the adoption signal for the npm-as-runtime model: how many\n // installs actually move off the snapshot their .app shipped with.\n \"runtime_update_installed\",\n \"version_restore\",\n \"overlay_added\",\n \"overlay_edited\",\n \"preset_applied\",\n \"effect_applied\",\n \"effect_previewed\",\n \"text_3d_used\",\n // \"Make it 3D\" toggled ON for any flat overlay kind (text/image/video/code).\n // Reports the kind so all flat kinds funnel through one bounded event.\n \"overlay_3d_used\",\n // chat command palette + context meter (UI-initiated; the resulting agent\n // turn is covered by agent_message_sent / tool_used)\n \"chat_slash_command\",\n \"context_meter_opened\",\n // surface / engagement\n \"page_view\",\n \"page_engagement\",\n // cross-cutting\n \"tool_used\",\n // membership interest — fired only after the server accepts the signup, so\n // this counts waitlist rows that exist and not form submissions that failed.\n // The only param is `plan`, bounded by the WaitlistCard's featureKey union;\n // the address itself is never sent here.\n \"pro_waitlist_joined\",\n // A site announcement banner was displayed (components/layout/\n // announcement-banner.tsx). The only param is `kind: \"feature\" | \"issue\"`\n // — bounded by construction; announcement ids and text are never sent.\n \"announcement_shown\",\n // privacy\n \"analytics_opt_out\",\n \"analytics_opt_in\",\n] as const;\n\nexport type AnalyticsEventName = (typeof EVENT_NAMES)[number];\n\nconst EVENT_NAME_SET = new Set<string>(EVENT_NAMES);\nexport function isEventName(name: string): name is AnalyticsEventName {\n return EVENT_NAME_SET.has(name);\n}\n\n/** Where a setup action (an install, a sign-in, a copy) was initiated from —\n * a closed, bounded set of UI surfaces. Shared by `AgentSetupCard`'s own\n * `surface` prop (`\"onboarding\"` | `\"chat\"`, the only two places that card\n * renders), by `useAgentSetupCardState` (those two plus `\"sidebar\"`, whose\n * install chip is its own rendering of the same state), and by\n * `useRunRemedyInTerminal`'s callers (`\"sidebar\"` from\n * `app-sidebar.tsx`/`sidebar-session-list.tsx`, `\"settings\"` from the MCPs &\n * Skills page's `connect-tools-section.tsx`) — every emit site draws from\n * the same set instead of each call site inventing its own string. Not every\n * event can produce every member; each event's own comment says which. */\nexport type AnalyticsSurface = \"onboarding\" | \"sidebar\" | \"chat\" | \"settings\";\n\n/** The two agent ids the setup/funnel events report. Narrows an\n * unconstrained id (an `AgentSetup.id`, a `SessionEntry.agentId`, an\n * `AgentReadiness`'s `agentId`, etc. — all plain `string` in their own\n * modules) down to the bounded param GA4 requires. Returns undefined for\n * `\"terminal\"` or anything else so a caller can skip the event entirely\n * rather than ever send an unbounded id as a param. */\nexport type AnalyticsAgentId = \"claude-code\" | \"codex\";\nexport function toAgentEventId(id: string | null | undefined): AnalyticsAgentId | undefined {\n return id === \"claude-code\" || id === \"codex\" ? id : undefined;\n}\n\nexport type AnalyticsParams = Record<string, string | number | boolean>;\n\n/** Drop undefined/null, coerce, truncate string values to GA4's 100-char limit. */\nexport function sanitizeParams(\n params: Record<string, unknown> | undefined,\n): AnalyticsParams {\n if (!params) return {};\n const out: AnalyticsParams = {};\n for (const [k, v] of Object.entries(params)) {\n if (v === undefined || v === null) continue;\n if (typeof v === \"string\") out[k] = v.slice(0, 100);\n else if (typeof v === \"number\" || typeof v === \"boolean\") out[k] = v;\n else out[k] = String(v).slice(0, 100);\n }\n return out;\n}\n","// lib/analytics/queue.ts\n//\n// Durable delivery for GA4 events. Every event lands in SQLite before it\n// ever touches the network, and only leaves the queue once GA4 has actually\n// accepted it (a 2xx from `/g/collect`). libi's \"server\" is a process on\n// the user's own machine that stops the instant they quit — and the most\n// valuable events in the funnel are fired by people who are about to do\n// exactly that: someone who gives up during agent setup quits DURING the\n// step we most need to see. A fire-and-forget `fetch` loses precisely\n// those.\n//\n// Opting out is the one deliberate exception: `drainAnalyticsQueue` clears\n// the whole queue the instant `enabled` is false rather than merely\n// pausing it — holding events for someone who opted out is exactly the\n// behaviour a privacy toggle must not have.\n\nimport { asc, count, eq, inArray, lte, sql } from \"drizzle-orm\";\nimport { getDb, migrateDatabase } from \"@/lib/db/client\";\nimport { analyticsQueue } from \"@/lib/db/schema/sqlite\";\nimport { buildCollectUrl, type CollectEvent } from \"@/lib/analytics/collect-url\";\nimport { advanceSession, type SessionState } from \"@/lib/analytics/identity\";\nimport { sanitizeParams } from \"@/lib/analytics/events\";\nimport {\n getAnalyticsSettings,\n getOrCreateAnalyticsUserId,\n markAnalyticsMilestoneOnce,\n} from \"@/lib/db/settings\";\nimport type { AnalyticsSettings } from \"@/lib/analytics/settings-logic\";\nimport {\n ANALYTICS_DEBUG,\n ANALYTICS_ENABLED,\n ANALYTICS_KILL_SWITCH,\n MEASUREMENT_ID,\n} from \"@/lib/analytics/config\";\nimport { isTestMode } from \"@/lib/test-mode\";\nimport { serverLogger as logger } from \"@/lib/logger\";\n\nexport const MAX_QUEUE_ROWS = 500;\nexport const MAX_ATTEMPTS = 8;\nexport const DRAIN_INTERVAL_MS = 15_000;\nexport const DRAIN_BATCH = 20;\n\nexport interface DrainDeps {\n fetchImpl?: typeof fetch;\n now?: () => number;\n getSettings?: () => AnalyticsSettings;\n /** Injected so the first-visit test can drive the milestone without\n * reaching into persisted settings. Defaults to the real, PERSISTED\n * primitive — see FIRST_HIT_MILESTONE below for why that matters. */\n markMilestoneOnce?: (name: string) => boolean;\n}\n\nexport interface DrainResult {\n sent: number;\n failed: number;\n dropped: number;\n}\n\n/** Exponential backoff for retrying a failed send: 30s, 60s, 120s, ... —\n * capped at an hour so a long-offline install still retries on a sane\n * cadence instead of hammering a dead network. */\nexport function backoffMs(attempts: number): number {\n return Math.min(30_000 * 2 ** (attempts - 1), 60 * 60 * 1000);\n}\n\n// ---------------------------------------------------------------------------\n// Module-level, in-process state\n// ---------------------------------------------------------------------------\n//\n// Session state is intentionally NOT persisted: a process restart\n// legitimately starts a new GA4 session (see lib/analytics/identity.ts).\n// Durability lives entirely in the `analytics_queue` table — never in a\n// module-level cache of pending rows, which a restart would silently drop.\n\nlet sessionState: SessionState | null = null;\n/** Distinct non-2xx statuses already warned about, so a persistently\n * broken endpoint logs once instead of once per queued event. */\nlet loggedStatuses = new Set<number>();\nlet drainTimer: ReturnType<typeof setInterval> | null = null;\n/** The drain currently outstanding, or null. See `runScheduledDrain`. */\nlet drainInFlight: Promise<void> | null = null;\nlet testDbReady = false;\n\n/** The persisted milestone behind GA4's `_fv` (first visit) flag.\n *\n * This USED to be a module-level `hasAttemptedAnyHit` boolean, whose own\n * comment admitted it only approximated \"first hit this install ever sent\"\n * — it reset with the process. GA4 derives New Users from `_fv`, so every\n * relaunch of libi registered a brand-new user and the funnel's denominator\n * inflated over the life of an install. `markAnalyticsMilestoneOnce` is the\n * already-built persisted primitive for exactly this.\n *\n * Its own name, deliberately: `\"launch\"` and `\"first_message\"` are taken and\n * mean other things. */\nexport const FIRST_HIT_MILESTONE = \"analytics_first_hit\";\n\n/** Test-only: lazily migrate a real, file-backed DB so unit tests here —\n * which have no lifecycle prelude to do it for them — have a table to\n * exercise. Deliberately NOT called from enqueue/drain: production\n * migrates via lib/server/lifecycle, and this module must not duplicate\n * that (a second migrateDatabase() call there would re-run backupDb()\n * for no reason). */\nfunction ensureTestDbReady(): void {\n if (testDbReady) return;\n testDbReady = true;\n try {\n migrateDatabase();\n } catch {\n // Already migrated (idempotent), or some other caller owns it — either\n // way there's nothing for this test helper to do about it.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Enqueue\n// ---------------------------------------------------------------------------\n\n/** Queue one analytics event for later delivery. Synchronous and total: it\n * never throws and never awaits, so a caller measuring a flow can never be\n * taken down by analytics.\n *\n * Three different gates apply here, and they are NOT interchangeable:\n * - isTestMode(): test-mode runs must not pollute real GA4 data.\n * - ANALYTICS_KILL_SWITCH (LIBI_ANALYTICS_DISABLED=1): the user's hard\n * kill-switch. A kill-switch that still writes rows to disk — even rows\n * that will never be sent, because nothing clears them without the\n * drain — is not a kill-switch, so this one stops recording, not just\n * sending.\n * - getAnalyticsSettings().enabled: the in-app opt-out toggle. Same\n * reasoning — an opted-out user must not accumulate analytics data on\n * disk just because the build that happens to be running never starts\n * the drain that would otherwise clear the table.\n *\n * ANALYTICS_ENABLED, by contrast, is deliberately NOT checked here. It is\n * the build-time install flag (see lib/analytics/config.ts) and gates only\n * startAnalyticsDrain below — the chokepoint that actually reaches the\n * network. Queuing itself is cheap and reversible — capped at\n * MAX_QUEUE_ROWS by evicting the oldest rows — so keeping it ungated on\n * the build flag here is the \"lose fewer events\" choice: an event queued\n * before a restart that flips the flag on is still there to send. */\nexport function enqueueAnalyticsEvent(name: string, params?: Record<string, unknown>): void {\n try {\n if (isTestMode() || ANALYTICS_KILL_SWITCH) return;\n // A settings-read failure falls through to the outer catch below, which\n // skips the write — the safe default when we can't tell whether the\n // user opted out.\n if (!getAnalyticsSettings().enabled) return;\n\n const clean = sanitizeParams(params);\n const db = getDb();\n db.insert(analyticsQueue)\n .values({\n id: crypto.randomUUID(),\n name,\n paramsJson: JSON.stringify(clean),\n // Always immediately due — a brand-new event has never failed yet,\n // so there is no backoff to honor.\n nextAttemptAt: new Date(0),\n })\n .run();\n\n // Cheap on the common path: only the row count is read here. The id\n // list is fetched (bounded by LIMIT) only on the rare over-cap branch.\n const [{ n }] = db.select({ n: count() }).from(analyticsQueue).all();\n if (n > MAX_QUEUE_ROWS) {\n const excess = db\n .select({ id: analyticsQueue.id })\n .from(analyticsQueue)\n .orderBy(asc(analyticsQueue.createdAt))\n .limit(n - MAX_QUEUE_ROWS)\n .all()\n .map((r) => r.id);\n db.delete(analyticsQueue).where(inArray(analyticsQueue.id, excess)).run();\n }\n } catch {\n // Total: analytics must never be the reason a caller's own flow breaks.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Drain\n// ---------------------------------------------------------------------------\n\n/** Attempt to deliver queued events to GA4. Reads the opt-out setting once,\n * up front: if analytics is disabled the WHOLE queue is dropped (not\n * merely paused — see module doc) and nothing is sent. Otherwise, up to\n * DRAIN_BATCH due rows are sent in order, advancing the in-process GA4\n * session once per actual send attempt so `_s` matches hits on the wire. */\nexport async function drainAnalyticsQueue(deps: DrainDeps = {}): Promise<DrainResult> {\n const now = deps.now ?? Date.now;\n const fetchImpl = deps.fetchImpl ?? fetch;\n const getSettings = deps.getSettings ?? getAnalyticsSettings;\n const markMilestoneOnce = deps.markMilestoneOnce ?? markAnalyticsMilestoneOnce;\n\n const db = getDb();\n const settings = getSettings();\n\n if (!settings.enabled) {\n const rows = db.select({ id: analyticsQueue.id }).from(analyticsQueue).all();\n if (rows.length > 0) db.delete(analyticsQueue).run();\n return { sent: 0, failed: 0, dropped: rows.length };\n }\n\n const nowMs = now();\n const due = db\n .select()\n .from(analyticsQueue)\n .where(lte(analyticsQueue.nextAttemptAt, new Date(nowMs)))\n // nextAttemptAt/createdAt are millisecond-resolution, so two events\n // queued in the same millisecond would otherwise have no defined\n // order. `rowid` is SQLite's own monotonically-increasing insertion\n // order for this table (no WITHOUT ROWID), so it's a free, exact\n // tiebreaker rather than one relying on query-plan incidence.\n .orderBy(asc(analyticsQueue.nextAttemptAt), asc(analyticsQueue.createdAt), asc(sql`rowid`))\n .limit(DRAIN_BATCH)\n .all();\n\n let sent = 0;\n let failed = 0;\n let dropped = 0;\n // Asked at most once per drain, and only when there is a row to attach it\n // to — an empty batch must not burn the milestone, and a 20-row batch has\n // no reason to re-read settings twenty times.\n let firstVisitAsked = false;\n const userId = settings.userId ?? getOrCreateAnalyticsUserId();\n\n for (const row of due) {\n const { state, sessionStart } = advanceSession(sessionState, nowMs);\n sessionState = state;\n // Persisted, not per-process: `_fv` is how GA4 counts New Users, so a\n // per-process flag made every relaunch look like a new install. Total by\n // construction — a settings failure must never cost us the hit, so it\n // just means we don't claim first-visit.\n let firstVisit = false;\n if (!firstVisitAsked) {\n firstVisitAsked = true;\n try {\n firstVisit = markMilestoneOnce(FIRST_HIT_MILESTONE);\n } catch {\n firstVisit = false;\n }\n }\n\n let params: Record<string, unknown> = {};\n try {\n params = JSON.parse(row.paramsJson) as Record<string, unknown>;\n } catch {\n params = {};\n }\n\n const ev: CollectEvent = {\n name: row.name,\n // Sanitized once already at enqueue time, into paramsJson. Sanitizing\n // again here is deliberate defense-in-depth against a hand-edited or\n // older row whose paramsJson predates a sanitization rule change —\n // sanitizeParams is idempotent, so this is a no-op on the common path.\n params: sanitizeParams(params),\n clientId: userId,\n userId,\n sessionId: state.sessionId,\n hitNumber: state.hitNumber,\n sessionStart,\n firstVisit,\n debug: ANALYTICS_DEBUG,\n // These are background/queued hits, not a live timed user session —\n // there's no real engagement span to report, so send the floor value\n // buildCollectUrl would clamp to anyway.\n engagementMs: 1,\n };\n const url = buildCollectUrl(ev, MEASUREMENT_ID);\n\n let ok = false;\n let status: number | null = null;\n try {\n const res = await fetchImpl(url, { method: \"POST\", signal: AbortSignal.timeout(5000) });\n ok = res.ok;\n status = res.status;\n } catch {\n ok = false;\n }\n\n if (ok) {\n db.delete(analyticsQueue).where(eq(analyticsQueue.id, row.id)).run();\n sent++;\n continue;\n }\n\n // Non-2xx is the one mitigation for `/g/collect` being undocumented: if\n // Google changes it, our own logs say so — once per distinct status,\n // not once per event.\n if (status !== null && !loggedStatuses.has(status)) {\n loggedStatuses.add(status);\n logger.warn(\n { tag: \"analytics\", op: \"collect_rejected\", status },\n \"GA4 /g/collect rejected an event\",\n );\n }\n\n const attempts = row.attempts + 1;\n if (attempts >= MAX_ATTEMPTS) {\n db.delete(analyticsQueue).where(eq(analyticsQueue.id, row.id)).run();\n dropped++;\n logger.warn(\n { tag: \"analytics\", op: \"event_dropped\", name: row.name, attempts },\n \"Dropping analytics event after exhausting retries\",\n );\n } else {\n db.update(analyticsQueue)\n .set({ attempts, nextAttemptAt: new Date(nowMs + backoffMs(attempts)) })\n .where(eq(analyticsQueue.id, row.id))\n .run();\n failed++;\n }\n }\n\n return { sent, failed, dropped };\n}\n\n// ---------------------------------------------------------------------------\n// Interval driver\n// ---------------------------------------------------------------------------\n\n/** Start the periodic drain. Idempotent — a second call while already\n * running is a no-op. Drains immediately, then every DRAIN_INTERVAL_MS.\n * The interval is unref'd so it can never keep the process alive on its\n * own. No-ops entirely when ANALYTICS_ENABLED is false — this is the\n * chokepoint that keeps a local dev build (analytics off by default) from\n * ever reaching the real network; see the note on enqueueAnalyticsEvent\n * above for why the gate lives here and not at enqueue time. */\nexport function startAnalyticsDrain(deps: DrainDeps = {}): void {\n if (!ANALYTICS_ENABLED || drainTimer) return;\n runScheduledDrain(deps);\n drainTimer = setInterval(() => runScheduledDrain(deps), DRAIN_INTERVAL_MS);\n drainTimer.unref?.();\n}\n\n/**\n * One scheduled drain tick, with a module-level in-flight guard.\n *\n * The interval used to fire `drainAnalyticsQueue()` without awaiting the\n * previous one, and rows are NOT reserved: the SELECT filters only on\n * `nextAttemptAt <= now`, and a row is deleted (or backed off) only AFTER its\n * send resolves. With DRAIN_BATCH = 20 rows sent SERIALLY, GA4 answering in a\n * perfectly ordinary ~1s means a batch takes 20s against a 15s interval — so\n * the next tick selected the same still-unresolved rows and POSTed them\n * again. On a degraded link (the 5s `AbortSignal.timeout`) one batch runs\n * ~100s, i.e. up to seven concurrent drains replaying one queue.\n * `/g/collect` has no idempotency, so every duplicate counts — and\n * over-reporting looks like healthy numbers, so nothing surfaces it. A fresh\n * install, the exact case the funnel exists to measure, fills the queue\n * fastest.\n *\n * Skipping (rather than queueing) the tick is right: the work is not lost,\n * the next interval picks up whatever is still due.\n */\nexport function runScheduledDrain(deps: DrainDeps = {}): Promise<void> {\n if (drainInFlight) return drainInFlight;\n const pass: Promise<void> = drainAnalyticsQueue(deps)\n .then(() => undefined)\n .catch((err) => {\n logger.warn({ tag: \"analytics\", op: \"drain_failed\", err }, \"Analytics drain failed\");\n })\n .finally(() => {\n if (drainInFlight === pass) drainInFlight = null;\n });\n drainInFlight = pass;\n return pass;\n}\n\n/** Stop the periodic drain. For tests. */\nexport function stopAnalyticsDrain(): void {\n if (drainTimer) {\n clearInterval(drainTimer);\n drainTimer = null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Test support\n// ---------------------------------------------------------------------------\n\n/** Clears the queue table, the in-process session state, and the logged-\n * status set. Test-only. Also lazily migrates a real, file-backed test DB\n * the first time it's called — unit tests here have no lifecycle prelude\n * to do that for them, and it must be a real DB on disk (not an in-memory\n * module cache) for the restart-durability test to prove anything. */\nexport function __resetAnalyticsQueueForTests(): void {\n ensureTestDbReady();\n getDb().delete(analyticsQueue).run();\n sessionState = null;\n drainInFlight = null;\n loggedStatuses = new Set<number>();\n}\n","// lib/analytics/server.ts\n// Server-side analytics entry point. Every server, route, job and MCP event\n// lands here, and here just means \"onto the durable queue\" — the queue owns\n// identity, sessionisation, sending and retry.\nimport { enqueueAnalyticsEvent } from \"@/lib/analytics/queue\";\n\n/** Record one analytics event. Synchronous, total, and fire-and-forget: it\n * cannot throw, cannot block, and cannot fail the flow that called it.\n *\n * The signature is deliberately `void` and not `Promise<void>`. The old one\n * returned a promise that every caller discarded with `void`, which made it\n * look awaitable when awaiting it meant \"wait for Google\" on a user's\n * critical path. There is nothing to await now.\n *\n * enqueueAnalyticsEvent is itself total (it has its own internal catch),\n * but this wrapper does not lean on that alone — \"cannot throw\" is a\n * promise this function makes on its own behalf, so it holds even if the\n * queue's contract ever changes underneath it. */\nexport function trackServerEvent(\n name: string,\n params?: Record<string, unknown>,\n): void {\n try {\n enqueueAnalyticsEvent(name, params);\n } catch {\n // Total: analytics must never be the reason a caller's own flow breaks.\n }\n}\n"],"names":["DEFAULT_ANALYTICS_SETTINGS","enabled","userId","optOutAt","firstRunNoticeShown","milestones","parseAnalyticsSettings","raw","p","JSON","parse","Array","isArray","filter","m","mergeAnalyticsSettings","cur","partial","markMilestone","name","includes","settings","added","applyOptOut","nowMs","applyOptIn","isTestMode","v","process","env","LIBI_TEST_MODE","MEASUREMENT_ID","NEXT_PUBLIC_LIBI_GA4_MEASUREMENT_ID","resolveAnalyticsEnabled","LIBI_ANALYTICS_DISABLED","NEXT_PUBLIC_LIBI_ANALYTICS","resolveAnalyticsDebug","LIBI_ANALYTICS_DEBUG","NODE_ENV","ANALYTICS_ENABLED","ANALYTICS_DEBUG","ANALYTICS_KILL_SWITCH","EVENT_NAMES","EVENT_NAME_SET","Set","isEventName","has","toAgentEventId","id","undefined","sanitizeParams","params","out","k","Object","entries","slice","String","MAX_QUEUE_ROWS","MAX_ATTEMPTS","DRAIN_INTERVAL_MS","DRAIN_BATCH","backoffMs","attempts","Math","min","sessionState","loggedStatuses","drainTimer","drainInFlight","testDbReady","FIRST_HIT_MILESTONE","ensureTestDbReady","enqueueAnalyticsEvent","clean","db","insert","values","crypto","randomUUID","paramsJson","stringify","nextAttemptAt","Date","run","n","select","from","all","excess","orderBy","createdAt","limit","map","r","delete","where","drainAnalyticsQueue","deps","now","fetchImpl","fetch","getSettings","markMilestoneOnce","rows","length","sent","failed","dropped","due","firstVisitAsked","row","state","sessionStart","firstVisit","ev","clientId","sessionId","hitNumber","debug","engagementMs","url","ok","status","res","method","signal","AbortSignal","timeout","add","warn","tag","op","update","set","startAnalyticsDrain","runScheduledDrain","setInterval","unref","pass","then","catch","err","finally","stopAnalyticsDrain","clearInterval","__resetAnalyticsQueueForTests"],"mappings":"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"}}]
7
7
  }