@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": "c832d0a0-2260-db80-8035-c6d579534886",
4
+ "debugId": "f91c0519-5818-07c6-1b9f-059db6049a81",
5
5
  "sections": [
6
- {"offset": {"line": 1, "column": 0}, "map": {"version":3,"sources":["../../../lib/analytics/config.ts","../../../node_modules/src/sql/functions/aggregate.ts","../../../lib/analytics/events.ts","../../../lib/analytics/queue.ts","../../../lib/analytics/server.ts","../../../lib/proxy/lifecycle.ts","../../../lib/filmstrip/lifecycle.ts","../../../lib/export/render-jobs.ts","../../../lib/tracking/track-jobs.ts","../../../lib/analysis/script-providers/fal-api-key.ts","../../../lib/fal/billing.ts","../../../lib/pieces/delete-piece.ts","../../../mcp/registry/installers/tracking-pyenv.ts","../../../lib/effects/registry.ts","../../../lib/overlays/transform3d.ts","../../../lib/engine/overlay-transform.ts","../../../mcp/notify.ts","../../../lib/whisper/transcribe.ts","../../../lib/uv-path.ts","../../../lib/uv-env/hash-spec.ts","../../../lib/uv-env/install-token.ts","../../../lib/editor-state.ts","../../../lib/tracking/engine-deps.ts","../../../lib/tracking/not-installed.ts"],"sourcesContent":["// 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","/**\n * Proxy-file lifecycle helpers — drop the on-disk proxy and reset the\n * `files` row's proxy_* columns.\n *\n * Used by consumers that need to discard an existing proxy (file deletion,\n * proxy-drop tools) without scheduling a new job. Proxy GENERATION goes\n * through the JobManager via the `proxy_gen` runner — this module\n * intentionally does NOT enqueue anything.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { eq } from \"drizzle-orm\";\nimport { getDb } from \"@/lib/db/client\";\nimport { files } from \"@/lib/db/schema/sqlite\";\nimport { getLibiStorageDir } from \"@/lib/libi-home\";\nimport { proxyLogger as logger } from \"@/lib/logger\";\nimport { navigationEmitter } from \"@/lib/navigation-events\";\n\n/**\n * Best-effort: unlink the proxy file for a `fileId` (if one exists on disk)\n * and clear the row's proxy_* columns. Idempotent — safe to call on rows\n * whose proxy was never generated or already dropped.\n *\n * Does NOT remove the source file or the `files` row itself. Callers\n * responsible for those (delete-file.ts handles full file removal).\n */\nexport function dropProxyFile(\n fileId: string,\n reason: \"user\" | \"lru\" | \"delete\" | \"alpha_backfill\" = \"user\",\n): void {\n const db = getDb();\n const [row] = db.select().from(files).where(eq(files.id, fileId)).limit(1).all();\n if (!row) return;\n if (row.proxyStatus === \"idle\" && !row.proxyFilename) return;\n\n logger.info({ fileId, event: \"drop\", reason }, \"proxy.drop\");\n\n if (row.proxyFilename) {\n const proxyPath = path.join(\n getLibiStorageDir(),\n row.pieceId ?? \"_global\",\n row.proxyFilename,\n );\n try {\n fs.unlinkSync(proxyPath);\n } catch {\n /* ignore — already gone is fine */\n }\n }\n\n db.update(files)\n .set({ proxyFilename: null, proxyStatus: \"idle\", proxyGeneratedAt: null })\n .where(eq(files.id, fileId))\n .run();\n\n if (row.pieceId) {\n navigationEmitter.emit(\"refresh_query\", {\n queryKey: \"piece\",\n pieceId: row.pieceId,\n });\n } else {\n navigationEmitter.emit(\"refresh_query\", { queryKey: \"files\" });\n }\n}\n","/**\n * Filmstrip-file lifecycle helpers — drop the on-disk sprite and reset the\n * `files` row's filmstrip_* columns. Mirrors `lib/proxy/lifecycle.ts`.\n *\n * Used by consumers that need to discard an existing sprite (file deletion,\n * LRU eviction) without scheduling a new job. Filmstrip GENERATION goes\n * through the JobManager via the `filmstrip_gen` runner — this module\n * intentionally does NOT enqueue anything.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { eq } from \"drizzle-orm\";\nimport { getDb } from \"@/lib/db/client\";\nimport { files } from \"@/lib/db/schema/sqlite\";\nimport { getLibiStorageDir } from \"@/lib/libi-home\";\nimport { proxyLogger as logger } from \"@/lib/logger\";\nimport { navigationEmitter } from \"@/lib/navigation-events\";\n\n/**\n * Best-effort: unlink the filmstrip sprite for a `fileId` (if one exists on\n * disk) and clear the row's filmstrip_* columns. Idempotent — safe to call on\n * rows whose sprite was never generated or already dropped.\n *\n * Does NOT remove the source file or the `files` row itself.\n */\nexport function dropFilmstripFile(\n fileId: string,\n reason: \"user\" | \"lru\" | \"delete\" = \"user\",\n): void {\n const db = getDb();\n const [row] = db.select().from(files).where(eq(files.id, fileId)).limit(1).all();\n if (!row) return;\n if (row.filmstripStatus === \"idle\" && !row.filmstripFilename) return;\n\n logger.info({ fileId, event: \"drop\", reason, tag: \"filmstrip\" }, \"filmstrip.drop\");\n\n if (row.filmstripFilename) {\n const filmstripPath = path.join(\n getLibiStorageDir(),\n row.pieceId ?? \"_global\",\n row.filmstripFilename,\n );\n try {\n fs.unlinkSync(filmstripPath);\n } catch {\n /* ignore — already gone is fine */\n }\n }\n\n db.update(files)\n .set({\n filmstripFilename: null,\n filmstripStatus: \"idle\",\n filmstripGeneratedAt: null,\n filmstripFrames: null,\n filmstripHeight: null,\n })\n .where(eq(files.id, fileId))\n .run();\n\n if (row.pieceId) {\n navigationEmitter.emit(\"refresh_query\", {\n queryKey: \"piece\",\n pieceId: row.pieceId,\n });\n } else {\n navigationEmitter.emit(\"refresh_query\", { queryKey: \"files\" });\n }\n}\n","/**\n * NOTE: This is the in-process page→server callback bus for the Chromium\n * render page. The render page POSTs results back to\n * `/api/export/render-result/...` which calls `resolveRenderJob` here, and\n * errors land via `/api/export/render-error/...` calling `rejectRenderJob`.\n *\n * The job-tracking lifecycle (queue, dedupe, cancel, status, progress) is\n * owned by the generic JobManager (`lib/jobs/manager.ts`) — the\n * `exportRenderRunner` (in `lib/jobs/runners/export-render.ts`) drives this\n * registry as an IPC implementation detail.\n *\n * Both will probably consolidate in a future refactor; for now they're\n * complementary.\n */\nimport { randomUUID } from \"node:crypto\";\nimport type { ExportSettings, Overlay, AudioClip } from \"@/lib/engine/types\";\nimport type { FileRecord } from \"@/lib/db/schema/types\";\n\n/**\n * Payload the `/render` page needs to reconstruct a runtime `Composition`.\n *\n * We deliberately do NOT store a hydrated `Composition` here. Functions (the\n * compiled `scene.draw` callables) don't survive `JSON.stringify`, so anything\n * the render page fetches from `/api/export/render-job/[jobId]` has to be raw\n * data that the client re-hydrates via `buildComposition(...)`.\n */\nexport interface RenderPayload {\n overlays: Overlay[];\n audioClips: AudioClip[];\n width: number;\n height: number;\n fps: number;\n files: FileRecord[];\n /**\n * Total composition frame count (`getCompositionFrames`). Set by the chromium\n * route; the runner uses it to plan chunks. Absent on old persisted jobs →\n * the runner falls back to the single-page path.\n */\n totalFrames?: number;\n /**\n * When present, the render page renders only this sub-range (one chunk of a\n * chunked parallel render). The chunk file starts at t=0. Absent → full\n * composition.\n */\n frameRange?: { startFrame: number; endFrameExclusive: number };\n}\n\nexport interface RenderJobInit {\n pieceId: string;\n payload: RenderPayload;\n settings: ExportSettings;\n /** Overrides the absolute runaway cap (`RENDER_ABSOLUTE_CAP_MS`). Test seam. */\n timeoutMs?: number;\n /** Overrides the sliding stall timeout (`RENDER_STALL_TIMEOUT_MS`). Test seam. */\n stallTimeoutMs?: number;\n /** Optional callback fired when the in-page render bundle reports per-frame\n * progress via /api/export/render-progress. */\n onProgress?: (done: number, total: number) => void;\n}\n\nexport interface RenderJobSuccess {\n tempFilePath: string;\n durationSeconds: number;\n}\n\nexport interface RenderJobHandle {\n jobId: string;\n token: string;\n done: Promise<RenderJobSuccess>;\n}\n\ninterface JobEntry {\n jobId: string;\n token: string;\n pieceId: string;\n payload: RenderPayload;\n settings: ExportSettings;\n resolve: (value: RenderJobSuccess) => void;\n reject: (err: Error) => void;\n /** Sliding stall timer — reset on every progress tick. */\n stallTimer: NodeJS.Timeout;\n /** Runaway backstop — never reset. */\n absoluteTimer: NodeJS.Timeout;\n /** Effective stall duration, so a progress tick can re-arm identically. */\n stallTimeoutMs: number;\n /** Latest progress seen, used to build the stall-timeout message. */\n lastProgress: { done: number; total: number } | null;\n settled: boolean;\n onProgress?: (done: number, total: number) => void;\n}\n\n/**\n * Sliding stall timeout: a render is only killed if NO per-frame progress POST\n * arrives within this window. Armed at `createRenderJob` (covers page boot +\n * hydrate) and reset on every `recordRenderProgress` tick — a healthy render at\n * any fps stays alive indefinitely.\n */\nexport const RENDER_STALL_TIMEOUT_MS = 2 * 60 * 1000;\n\n/**\n * Runaway backstop: never reset. Even a render that keeps reporting progress is\n * killed once total wall time exceeds this cap.\n */\nexport const RENDER_ABSOLUTE_CAP_MS = 60 * 60 * 1000;\n\n/**\n * The registry must survive Next.js dev HMR — the runner (JobManager\n * background work) writes here, and the route handler at\n * `/api/export/render-job/[jobId]` reads from here. In dev, route\n * handlers can be reloaded into a different module instance than the\n * one the runner is holding a reference to, so a plain module-level\n * `Map` shows up empty on the read side and the render page gets a\n * 404. Stash the singleton on globalThis (same trick lib/db/client.ts\n * uses for the DB handle) so all imports of this module resolve to the\n * same Map. Verified by e2e/electron/libi-home-and-export.spec.ts.\n */\nconst globalForRenderJobs = globalThis as unknown as {\n __libi_render_jobs?: Map<string, JobEntry>;\n};\nconst jobs: Map<string, JobEntry> =\n globalForRenderJobs.__libi_render_jobs ?? new Map<string, JobEntry>();\nif (!globalForRenderJobs.__libi_render_jobs) {\n globalForRenderJobs.__libi_render_jobs = jobs;\n}\n\nexport function createRenderJob(init: RenderJobInit): RenderJobHandle {\n const jobId = randomUUID();\n const token = randomUUID();\n let resolve!: (v: RenderJobSuccess) => void;\n let reject!: (e: Error) => void;\n const done = new Promise<RenderJobSuccess>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n const stallTimeoutMs = init.stallTimeoutMs ?? RENDER_STALL_TIMEOUT_MS;\n const absoluteCapMs = init.timeoutMs ?? RENDER_ABSOLUTE_CAP_MS;\n\n // Sliding stall timer — reset on every progress tick (see recordRenderProgress).\n const stallTimer = setTimeout(() => {\n const entry = jobs.get(jobId);\n if (entry && !entry.settled) {\n entry.settled = true;\n clearTimeout(entry.absoluteTimer);\n jobs.delete(jobId);\n const seconds = stallTimeoutMs / 1000;\n const message = entry.lastProgress\n ? `Render job ${jobId} stalled — no progress for ${seconds}s (frame ${entry.lastProgress.done}/${entry.lastProgress.total})`\n : `Render job ${jobId} stalled — no progress reported within ${seconds}s`;\n reject(new Error(message));\n }\n }, stallTimeoutMs);\n\n // Runaway backstop — never reset.\n const absoluteTimer = setTimeout(() => {\n const entry = jobs.get(jobId);\n if (entry && !entry.settled) {\n entry.settled = true;\n clearTimeout(entry.stallTimer);\n jobs.delete(jobId);\n reject(\n new Error(\n `Render job ${jobId} exceeded the ${absoluteCapMs / 60000}-minute absolute cap`,\n ),\n );\n }\n }, absoluteCapMs);\n\n jobs.set(jobId, {\n jobId,\n token,\n pieceId: init.pieceId,\n payload: init.payload,\n settings: init.settings,\n resolve,\n reject,\n stallTimer,\n absoluteTimer,\n stallTimeoutMs,\n lastProgress: null,\n settled: false,\n onProgress: init.onProgress,\n });\n return { jobId, token, done };\n}\n\nexport function getRenderJob(jobId: string, token: string): JobEntry | null {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled || entry.token !== token) return null;\n return entry;\n}\n\n// Note: a timeout firing between getRenderJobTokenByJobId() and resolveRenderJob()\n// is harmless — this function re-checks entry.settled inside the critical section.\n// The worst case is that a driver-timed postback gets a false return value and the\n// client has already received the timeout rejection.\nexport function resolveRenderJob(jobId: string, token: string, value: RenderJobSuccess): boolean {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled || entry.token !== token) return false;\n entry.settled = true;\n clearTimeout(entry.stallTimer);\n clearTimeout(entry.absoluteTimer);\n jobs.delete(jobId);\n entry.resolve(value);\n return true;\n}\n\nexport function rejectRenderJob(jobId: string, token: string, message: string): boolean {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled || entry.token !== token) return false;\n entry.settled = true;\n clearTimeout(entry.stallTimer);\n clearTimeout(entry.absoluteTimer);\n jobs.delete(jobId);\n entry.reject(new Error(message));\n return true;\n}\n\n/** Called by the in-page render bundle to report per-frame progress.\n * Token-validated like the result endpoint. Stashes the latest progress and\n * resets the sliding stall timer so a healthy render never times out. */\nexport function recordRenderProgress(\n jobId: string,\n token: string,\n done: number,\n total: number,\n): void {\n const entry = jobs.get(jobId);\n if (!entry) return;\n if (entry.token !== token) return;\n if (entry.settled) return;\n entry.lastProgress = { done, total };\n clearTimeout(entry.stallTimer);\n entry.stallTimer = setTimeout(() => {\n const current = jobs.get(jobId);\n if (current && !current.settled) {\n current.settled = true;\n clearTimeout(current.absoluteTimer);\n jobs.delete(jobId);\n const seconds = current.stallTimeoutMs / 1000;\n const message = current.lastProgress\n ? `Render job ${jobId} stalled — no progress for ${seconds}s (frame ${current.lastProgress.done}/${current.lastProgress.total})`\n : `Render job ${jobId} stalled — no progress reported within ${seconds}s`;\n current.reject(new Error(message));\n }\n }, entry.stallTimeoutMs);\n entry.onProgress?.(done, total);\n}\n\n/** True while the job exists and has not settled. Driver-side polling seam. */\nexport function isRenderJobPending(jobId: string): boolean {\n const entry = jobs.get(jobId);\n return !!entry && !entry.settled;\n}\n\nexport function __resetRegistryForTests() {\n for (const entry of jobs.values()) {\n clearTimeout(entry.stallTimer);\n clearTimeout(entry.absoluteTimer);\n }\n jobs.clear();\n}\n\n/**\n * Returns the token for an unsettled job, without requiring the caller to know it.\n *\n * Driver-side only — drivers run in the trusted server context and need the token\n * to construct the `/render?jobId=X&token=Y` URL. Also used by tests to settle\n * jobs whose tokens the test didn't generate.\n *\n * Do NOT call this from untrusted surfaces (API handlers, render page). Those\n * must use `getRenderJob(jobId, token)` with an explicit token.\n */\nexport function getRenderJobTokenByJobId(jobId: string): { token: string } | null {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled) return null;\n return { token: entry.token };\n}\n","/**\n * NOTE: This is the page→server IPC bridge for `track-entry.ts` (running\n * inside Playwright Chromium) to post results back to `runFaceTracker` /\n * `runObjectTracker` via HTTP. It is NOT the same as the JobManager\n * (`lib/jobs/manager.ts`) which owns the job lifecycle.\n *\n * The split:\n * - JobManager — runs the runner, owns persistence, checkpoints, cancel.\n * - track-jobs (this file) — bridges the in-browser track-entry's `fetch`\n * calls back to the awaiting Promise inside the server-side runner.\n *\n * Both will probably consolidate in a future refactor; for now they're\n * complementary.\n */\nimport { randomUUID } from \"node:crypto\";\nimport type { TrackMethod, TrackSample } from \"@/lib/tracking/types\";\n\nexport interface TrackPayload {\n fileId: string;\n /** URL the browser will use to load the source video (e.g. /api/files/by-id/{id}/content). */\n fileUrl: string;\n fps: number;\n objectKind: \"face\" | \"object\";\n subjectQuery?: string;\n}\n\nexport interface TrackJobSuccess {\n samples: TrackSample[];\n framerate: number;\n method: TrackMethod;\n}\n\nexport interface TrackJobInit {\n pieceId: string;\n payload: TrackPayload;\n timeoutMs?: number;\n}\n\nexport interface TrackJobHandle {\n jobId: string;\n token: string;\n done: Promise<TrackJobSuccess>;\n}\n\ninterface JobEntry {\n jobId: string;\n token: string;\n pieceId: string;\n payload: TrackPayload;\n resolve: (value: TrackJobSuccess) => void;\n reject: (err: Error) => void;\n timeout: NodeJS.Timeout;\n settled: boolean;\n}\n\nexport const TRACK_REGISTRY_TIMEOUT_MS = 10 * 60 * 1000; // 10 min — tracking can be slow on long videos\n\n/**\n * Module-level state must survive Next.js dev-mode module fragmentation —\n * route handlers and the server-side runner can otherwise resolve to\n * different instances of this file and see different Maps. globalThis\n * caching keeps them sharing one registry.\n */\ndeclare global {\n var __libiTrackJobs: Map<string, JobEntry> | undefined;\n}\nconst jobs: Map<string, JobEntry> = globalThis.__libiTrackJobs ?? new Map<string, JobEntry>();\nglobalThis.__libiTrackJobs = jobs;\n\nexport function createTrackJob(init: TrackJobInit): TrackJobHandle {\n const jobId = randomUUID();\n const token = randomUUID();\n let resolve!: (v: TrackJobSuccess) => void;\n let reject!: (e: Error) => void;\n const done = new Promise<TrackJobSuccess>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n const timeout = setTimeout(() => {\n const entry = jobs.get(jobId);\n if (entry && !entry.settled) {\n entry.settled = true;\n jobs.delete(jobId);\n reject(new Error(`Track job ${jobId} timeout`));\n }\n }, init.timeoutMs ?? TRACK_REGISTRY_TIMEOUT_MS);\n jobs.set(jobId, {\n jobId,\n token,\n pieceId: init.pieceId,\n payload: init.payload,\n resolve,\n reject,\n timeout,\n settled: false,\n });\n return { jobId, token, done };\n}\n\nexport function getTrackJob(jobId: string, token: string): JobEntry | null {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled || entry.token !== token) return null;\n return entry;\n}\n\nexport function resolveTrackJob(\n jobId: string,\n token: string,\n value: TrackJobSuccess,\n): boolean {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled || entry.token !== token) return false;\n entry.settled = true;\n clearTimeout(entry.timeout);\n jobs.delete(jobId);\n entry.resolve(value);\n return true;\n}\n\nexport function rejectTrackJob(jobId: string, token: string, message: string): boolean {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled || entry.token !== token) return false;\n entry.settled = true;\n clearTimeout(entry.timeout);\n jobs.delete(jobId);\n entry.reject(new Error(message));\n return true;\n}\n\nexport function getTrackJobTokenByJobId(jobId: string): { token: string } | null {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled) return null;\n return { token: entry.token };\n}\n\nexport function __resetTrackRegistryForTests() {\n for (const entry of jobs.values()) clearTimeout(entry.timeout);\n jobs.clear();\n}\n","import { eq } from \"drizzle-orm\";\nimport { getDb } from \"@/lib/db/client\";\nimport { mcpServers } from \"@/lib/db/schema/sqlite\";\n\n/**\n * Reads the fal.ai API key from the mcp_servers[\"fal-ai\"] row's env_vars JSON.\n *\n * Returns null when:\n * - the row is missing\n * - env_vars is unset\n * - FAL_KEY is absent, not a string, or empty\n * - env_vars contains malformed JSON\n */\nexport async function readFalApiKey(): Promise<string | null> {\n const db = getDb();\n const [row] = db\n .select({ envVars: mcpServers.envVars })\n .from(mcpServers)\n .where(eq(mcpServers.id, \"fal-ai\"))\n .limit(1)\n .all();\n\n if (!row?.envVars) return null;\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(row.envVars) as Record<string, unknown>;\n } catch {\n return null;\n }\n\n const key = parsed[\"FAL_KEY\"];\n if (typeof key !== \"string\" || key.trim() === \"\") return null;\n return key.trim();\n}\n","/**\n * Shared fal.ai billing-events helper.\n *\n * Both the script-analysis cost-refresh path (script-providers/fal-video-understanding.ts)\n * and the AI-generation cost-refresh path (api/files/.../generation/refresh-cost) hit\n * fal's platform billing API to resolve actual cost-after-completion.\n * One helper, two call sites.\n *\n * Endpoint: https://api.fal.ai/v1/models/billing-events?request_id=<id>&start=<iso>\n * Auth: Authorization: Key <FAL_KEY> — and the key MUST be ADMIN-scoped.\n * API-scoped keys (the kind that runs generations) get 403\n * `authorization_error` here; ADMIN scope is a superset (\"everything\n * in API, plus…\"), so one ADMIN key can serve both generation and\n * billing lookups. Verified live 2026-07-04.\n * Window: `start` DEFAULTS TO 24 HOURS AGO on fal's side — without an\n * explicit `start`, any request older than a day returns zero\n * events and looks forever-pending. Callers pass the generation\n * timestamp; we widen it by an hour of clock-skew margin.\n * Response: { billing_events: [{ cost_estimate_nano_usd: number, ... }] }\n *\n * A literal 0 is \"not yet populated\" (fal occasionally returns the row with\n * cost_estimate_nano_usd === 0 before the real charge lands) → `pending`.\n *\n * Docs: https://fal.ai/docs/platform-apis/v1/models/billing-events.md\n */\n\nimport type { CostFetchOutcome } from \"@/lib/providers/cost-outcome\";\n\nexport const FAL_BILLING_URL = \"https://api.fal.ai/v1/models/billing-events\";\n\n/** User-facing explanation for the permanent 403 case. */\nexport const FAL_ADMIN_SCOPE_HINT =\n \"The configured fal.ai API key can't read billing data — fal's billing API requires an ADMIN-scoped key. Create one at fal.ai → Dashboard → Keys (scope: ADMIN — it runs generations too) and update the FAL_KEY on the fal-ai MCP server in Settings.\";\n\nconst FALLBACK_WINDOW_MS = 90 * 24 * 60 * 60 * 1000; // 90 days\n// Generous on purpose: stored generation timestamps aren't trustworthy —\n// real rows exist whose completedAt is local wall-clock mislabeled as UTC\n// (3h ahead of the billing event's true UTC timestamp). The query is\n// request_id-filtered, so a wide window can't match the wrong event; it only\n// bounds fal's scan.\nconst SKEW_MARGIN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days\n\n/** fal's `start` defaults to 24h ago; compute an explicit window start from\n * the generation timestamp (or a wide fallback when it's missing/invalid). */\nexport function billingWindowStart(\n generatedAt: string | undefined,\n now: number = Date.now(),\n): string {\n const parsed = generatedAt ? Date.parse(generatedAt) : NaN;\n const base = Number.isFinite(parsed) ? parsed : now - FALLBACK_WINDOW_MS;\n return new Date(base - SKEW_MARGIN_MS).toISOString();\n}\n\nexport async function fetchFalCost(args: {\n requestId: string;\n apiKey: string;\n /** ISO timestamp of the generation; drives the billing query's `start`. */\n generatedAt?: string;\n signal?: AbortSignal;\n}): Promise<CostFetchOutcome> {\n const { requestId, apiKey, generatedAt, signal } = args;\n const params = new URLSearchParams({\n request_id: requestId,\n start: billingWindowStart(generatedAt),\n });\n const url = `${FAL_BILLING_URL}?${params.toString()}`;\n\n let resp: Response;\n try {\n resp = await fetch(url, {\n headers: { Authorization: `Key ${apiKey}` },\n signal,\n });\n } catch (err) {\n return {\n kind: \"error\",\n message: `fal billing request failed: ${(err as Error).message}`,\n };\n }\n\n if (resp.status === 401 || resp.status === 403) {\n return { kind: \"forbidden\", message: FAL_ADMIN_SCOPE_HINT };\n }\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n return {\n kind: \"error\",\n status: resp.status,\n message: `fal billing API returned HTTP ${resp.status}: ${body.slice(0, 200)}`,\n };\n }\n\n const body = (await resp.json().catch(() => null)) as {\n billing_events?: Array<{ cost_estimate_nano_usd?: number }>;\n } | null;\n const event = body?.billing_events?.[0];\n if (!event || typeof event.cost_estimate_nano_usd !== \"number\") {\n return { kind: \"pending\" };\n }\n if (event.cost_estimate_nano_usd === 0) {\n return { kind: \"pending\" };\n }\n return { kind: \"resolved\", amountUsd: event.cost_estimate_nano_usd / 1_000_000_000 };\n}\n","import { eq } from \"drizzle-orm\";\nimport { getDb } from \"@/lib/db/client\";\nimport { pieces } from \"@/lib/db/schema/sqlite\";\nimport { getStorage } from \"@/lib/storage\";\nimport { removeAnalysisForPiece } from \"@/lib/analysis/cleanup\";\nimport { navigationEmitter } from \"@/lib/navigation-events\";\n\n/**\n * Fully delete a piece: DB row (files/assets/etc. cascade), on-disk storage\n * directory, and analysis byproducts. Emits refresh_query. Returns false if\n * the piece did not exist.\n */\nexport async function deletePieceCompletely(pieceId: string): Promise<boolean> {\n const db = getDb();\n const [deleted] = await db.delete(pieces).where(eq(pieces.id, pieceId)).returning();\n if (!deleted) return false;\n\n const storage = await getStorage();\n removeAnalysisForPiece(pieceId);\n await storage.deletePieceDir(pieceId);\n\n navigationEmitter.emit(\"refresh_query\", { queryKey: \"pieces\" });\n navigationEmitter.emit(\"refresh_query\", { queryKey: \"piece\", pieceId });\n return true;\n}\n","/**\n * Custom installer for the local tracking engine (Task 11).\n *\n * Two responsibilities, run as one `install()`:\n * 1. `uv sync` the uv-managed Python sidecar project at\n * `mcp/tracking/py/` so onnxruntime / boxmot / opencv / torch are\n * resolved into its `.venv` (creating it on first run).\n * 2. Ensure the tracking model artifacts declared in\n * `mcp/tracking/py/models.json` are present under\n * `~/.libi/models/tracking/` (= `trackingModelsDir()`) — sha-pinned\n * direct downloads plus the locally-exported yoloe11 ONNX.\n *\n * The `CustomInstaller` interface in `mcp/registry/types.ts` is a static\n * `{ verify, install: { command, args } }` spec spawned via `execFile`.\n * That cannot express this multi-step flow, so — exactly like the\n * `yt-dlp-uv` precedent in `mcp/registry/installers.ts` — the spec uses a\n * sentinel `command` (`__TRACKING_PYENV_INSTALL__`) that\n * `DependencyManager.runCustomInstaller` special-cases to call\n * `runTrackingPyenvInstall()` here.\n *\n * Model acquisition handles BOTH kinds declared in models.json\n * (`acquisition`):\n * - `direct-download`: if the dest file is missing OR its sha256\n * differs from models.json, download from `url` and verify sha256\n * (hard-fail on mismatch).\n * - `build-at-install`: the artifact is EXPORTED ON THE USER'S MACHINE\n * from sha-pinned inputs declared in the entry's `build` spec (source\n * .pt + text encoder + CLIP commit archive). The output sha256 is\n * host-specific (the ONNX export embeds non-deterministic metadata), so\n * integrity is anchored on the INPUTS: a `<dest>.build-info.json` marker\n * records the exact inputs + the produced output sha, and the build is\n * skipped iff the dest hashes to the recorded output AND the recorded\n * inputs equal the manifest's current inputs. Building locally (owner\n * decision, 2026-08-01) also avoids libi redistributing an AGPL-derived\n * binary — ultralytics, the YOLOE weights, and the ultralytics/CLIP fork\n * are all AGPL-3.0, while libi is GPL-3.0.\n *\n * This file is server-only (imports `fs`, spawns processes). It is\n * referenced from `installers.ts` (also server-only) so it never reaches\n * the client bundle that imports `bundled.ts` via the Settings UI.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport crypto from \"node:crypto\";\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport { buildUvEnv, trackingVenvDir } from \"@/lib/uv-env/spawn-env\";\nimport { downloadToFileWithStallGuard } from \"@/lib/security/download-verify\";\nimport {\n uvPath,\n sidecarProjectDir,\n trackingModelsDir,\n} from \"@/lib/tracking/engine-deps\";\nimport { serverLogger as logger } from \"@/lib/logger\";\nimport type { CustomInstaller } from \"../types\";\n\nconst pexec = promisify(execFile);\n\n/** Bump when changing the sidecar pin set or model manifest. Mirrors the BundledDependency.pinnedInstallToken date convention. */\n/*\n * Bumped 2026-08-01: yoloe11.onnx moved from reproduce-at-build (manual\n * export, blocked every clean install) to build-at-install (the installer\n * exports it unattended from sha-pinned inputs), and the dead `osnet_reid`\n * ONNX entry was removed (boxmot's runtime PyTorch ReID backend was already\n * the shipped behaviour). The bump makes existing installs run the new\n * model phase once (idempotent: an already-built yoloe11.onnx without a\n * build-info marker is rebuilt once, then skipped on every later run).\n */\nexport const TRACKING_PYENV_TOKEN = \"tracking-pyenv@2026-08-01-yoloe-build-at-install\";\n\n/** Sentinel `command`; DependencyManager.runCustomInstaller dispatches it. */\nexport const TRACKING_PYENV_SENTINEL = \"__TRACKING_PYENV_INSTALL__\";\n\nexport interface TextEncoderSpec {\n /** Path RELATIVE to the models dir (e.g. \".build/mobileclip_blt.ts\"). */\n file: string;\n url: string;\n sha256: string;\n sizeBytes: number;\n}\n\n/** Declared inputs of a `build-at-install` export (models.json `build`). */\nexport interface BuildSpec {\n /** models.json id of the sha-pinned source checkpoint (e.g. \"yoloe_vp\"). */\n sourceModelId: string;\n classes: string[];\n imgsz: number;\n opset: number;\n /** PEP 508 requirement for the export-only CLIP tokenizer package, pinned\n * to an immutable commit-archive URL. Supplied via `uv run --with` so the\n * locked sidecar venv is never mutated. */\n clipRequirement: string;\n textEncoder: TextEncoderSpec;\n}\n\nexport interface ModelEntry {\n id: string;\n file: string;\n url: string;\n sha256: string;\n sizeBytes: number;\n dest: string;\n acquisition: \"direct-download\" | \"build-at-install\";\n build?: BuildSpec;\n}\n\n/** The exact input pins a built artifact was produced from. Persisted in the\n * build-info marker and compared against the manifest to decide rebuilds. */\nexport interface BuildInputs {\n sourcePtSha256: string;\n textEncoderSha256: string;\n clipRequirement: string;\n classes: string[];\n imgsz: number;\n opset: number;\n}\n\nexport interface BuildInfo {\n inputs: BuildInputs;\n output: { sha256: string; sizeBytes: number; ultralytics?: string };\n builtAt: string;\n}\n\nfunction modelsManifestPath(): string {\n return path.join(sidecarProjectDir(), \"models.json\");\n}\n\nfunction readModelsManifest(): ModelEntry[] {\n const p = modelsManifestPath();\n const raw = fs.readFileSync(p, \"utf-8\");\n const parsed = JSON.parse(raw) as ModelEntry[];\n if (!Array.isArray(parsed)) {\n throw new Error(`models.json at ${p} is not a JSON array`);\n }\n return parsed;\n}\n\nfunction sha256File(p: string): string {\n return crypto\n .createHash(\"sha256\")\n .update(fs.readFileSync(p))\n .digest(\"hex\")\n .toLowerCase();\n}\n\n/** The token marker for the synced uv env + provisioned models. */\nfunction tokenMarkerPath(): string {\n return path.join(trackingModelsDir(), \".install-token\");\n}\n\nfunction readToken(): string | null {\n try {\n return fs.readFileSync(tokenMarkerPath(), \"utf-8\").trim();\n } catch {\n return null;\n }\n}\n\nfunction writeToken(): void {\n const p = tokenMarkerPath();\n fs.mkdirSync(path.dirname(p), { recursive: true });\n fs.writeFileSync(p, TRACKING_PYENV_TOKEN, \"utf-8\");\n}\n\n/**\n * Download `url` to `dest`, streaming to disk while hashing (the mobileclip\n * text encoder is ~572MB — never buffer whole files in memory). Verifies the\n * sha256 and renames atomically so a partial download never looks present.\n */\nexport async function downloadVerified(\n url: string,\n sha256: string,\n dest: string,\n label: string,\n): Promise<void> {\n fs.mkdirSync(path.dirname(dest), { recursive: true });\n logger.info(\n { tag: \"tracking-pyenv\", op: \"model_download_start\", id: label, url },\n `downloading ${label}`,\n );\n // Stall-guarded + retrying + HTTPS-only-redirect, streaming to disk.\n //\n // This used to be a bare `fetch(url, { redirect: \"follow\" })`. Measured\n // 2026-08-02 on a real clean install: the 572 MB MobileCLIP encoder stalled\n // mid-body, undici's default body timeout fired as UND_ERR_BODY_TIMEOUT, and\n // the whole tracking provision died with an unhandled stack trace and no\n // retry — the SAME failure #54 fixed for binary deps, on the path that pulls\n // by far the largest artifacts. Buffering is not an option at this size, so\n // this streams and hashes as it writes.\n const { tmpPath, bytesDownloaded, sha256: actual } =\n await downloadToFileWithStallGuard(url, dest, {\n label,\n onRetry: (info) => {\n logger.warn(\n {\n tag: \"tracking-pyenv\",\n op: \"model_download_retry\",\n id: label,\n url,\n attempt: info.attempt,\n maxAttempts: info.maxAttempts,\n bytesDownloaded: info.bytesDownloaded,\n err: info.error,\n },\n `${label} download failed (attempt ${info.attempt}/${info.maxAttempts}), retrying`,\n );\n },\n });\n\n if (actual !== sha256.toLowerCase()) {\n fs.rmSync(tmpPath, { force: true });\n throw new Error(\n `sha256 mismatch for ${label}: expected ${sha256.toLowerCase()}, ` +\n `got ${actual} (downloaded from ${url})`,\n );\n }\n fs.renameSync(tmpPath, dest);\n logger.info(\n {\n tag: \"tracking-pyenv\",\n op: \"model_download_done\",\n id: label,\n dest,\n size: bytesDownloaded,\n },\n `downloaded ${label} (${bytesDownloaded} bytes, sha ok)`,\n );\n}\n\n/**\n * Ensure a single `direct-download` model artifact is on disk.\n * Returns nothing; throws an actionable error on any unrecoverable state.\n * `build-at-install` entries go through {@link ensureBuiltModel} instead.\n */\nexport async function ensureModel(\n entry: ModelEntry,\n modelsDir: string,\n): Promise<void> {\n if (entry.acquisition !== \"direct-download\") {\n throw new Error(\n `ensureModel only handles direct-download entries; ${entry.id} is ` +\n `${entry.acquisition} — route it through ensureBuiltModel`,\n );\n }\n const dest = path.join(modelsDir, entry.dest);\n\n // present-and-matching short-circuits the fetch.\n if (fs.existsSync(dest)) {\n const actual = sha256File(dest);\n if (actual === entry.sha256.toLowerCase()) {\n logger.info(\n { tag: \"tracking-pyenv\", op: \"model_present\", id: entry.id, dest },\n `tracking model ${entry.id} present (sha matches)`,\n );\n return;\n }\n logger.warn(\n {\n tag: \"tracking-pyenv\",\n op: \"model_sha_mismatch\",\n id: entry.id,\n dest,\n expected: entry.sha256.toLowerCase(),\n actual,\n },\n `tracking model ${entry.id} sha mismatch — re-downloading`,\n );\n }\n\n // dest may be nested (e.g. \"matanyone/model.safetensors\") — downloadVerified\n // creates the full parent chain, not just the models root.\n await downloadVerified(entry.url, entry.sha256, dest, entry.id);\n}\n\n/**\n * Resolve the declared build inputs of a `build-at-install` entry against the\n * manifest (pure — exported for unit tests). The result is what gets recorded\n * in the build-info marker and compared to decide whether a rebuild is needed.\n */\nexport function buildInputsOf(\n entry: ModelEntry,\n manifest: ModelEntry[],\n): BuildInputs {\n const build = entry.build;\n if (!build) {\n throw new Error(`model ${entry.id} is build-at-install but has no build spec`);\n }\n const source = manifest.find((m) => m.id === build.sourceModelId);\n if (!source) {\n throw new Error(\n `model ${entry.id} build.sourceModelId \"${build.sourceModelId}\" not in models.json`,\n );\n }\n return {\n sourcePtSha256: source.sha256.toLowerCase(),\n textEncoderSha256: build.textEncoder.sha256.toLowerCase(),\n clipRequirement: build.clipRequirement,\n classes: [...build.classes],\n imgsz: build.imgsz,\n opset: build.opset,\n };\n}\n\nfunction readBuildInfo(destPath: string): BuildInfo | null {\n try {\n return JSON.parse(\n fs.readFileSync(`${destPath}.build-info.json`, \"utf-8\"),\n ) as BuildInfo;\n } catch {\n return null;\n }\n}\n\n/**\n * Is the built artifact at `destPath` current for `expected` inputs?\n * True iff dest exists, the build-info marker parses, the recorded inputs\n * deep-equal the manifest's current inputs, AND the file on disk still hashes\n * to the recorded output sha (guards a corrupted/partial file).\n */\nexport function isBuildCurrent(\n destPath: string,\n expected: BuildInputs,\n): boolean {\n if (!fs.existsSync(destPath)) return false;\n const info = readBuildInfo(destPath);\n if (!info?.inputs || !info.output?.sha256) return false;\n if (JSON.stringify(info.inputs) !== JSON.stringify(expected)) return false;\n return sha256File(destPath) === info.output.sha256.toLowerCase();\n}\n\ntype ExecFn = (\n cmd: string,\n args: string[],\n opts: Record<string, unknown>,\n) => Promise<{ stdout: string; stderr: string }>;\n\n/** Scan stdout lines from the END for the export script's protocol JSON\n * (`{\"type\":\"export\",...}`), skipping ultralytics/uv progress chatter. */\nexport function parseExportResult(\n stdout: string,\n): { ok: boolean; [k: string]: unknown } | null {\n const lines = stdout.split(\"\\n\");\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i].trim();\n if (!line.startsWith(\"{\")) continue;\n try {\n const parsed = JSON.parse(line) as { type?: string; ok?: boolean };\n if (parsed.type === \"export\") {\n return parsed as { ok: boolean; [k: string]: unknown };\n }\n } catch {\n /* not JSON — keep scanning */\n }\n }\n return null;\n}\n\n/**\n * Ensure a `build-at-install` artifact exists and is current, exporting it\n * on this machine when it isn't. All inputs are sha-pinned and verified\n * BEFORE the export runs; the export itself is validated by\n * `export_yoloe11.py` (onnxruntime tensor-shape check) and the result is\n * recorded in `<dest>.build-info.json` so later installs skip the work.\n *\n * `exec` is injectable for hermetic tests; production uses promisified\n * execFile.\n */\nexport async function ensureBuiltModel(\n entry: ModelEntry,\n manifest: ModelEntry[],\n modelsDir: string,\n exec: ExecFn = pexec as unknown as ExecFn,\n): Promise<void> {\n const build = entry.build;\n if (!build) {\n throw new Error(`model ${entry.id} is build-at-install but has no build spec`);\n }\n const dest = path.join(modelsDir, entry.dest);\n const expected = buildInputsOf(entry, manifest);\n\n if (isBuildCurrent(dest, expected)) {\n logger.info(\n { tag: \"tracking-pyenv\", op: \"model_present\", id: entry.id, dest },\n `tracking model ${entry.id} present (build-info current)`,\n );\n return;\n }\n\n // 1. The sha-pinned source checkpoint must already be provisioned (the\n // direct-download phase runs before the build phase).\n const source = manifest.find((m) => m.id === build.sourceModelId)!;\n const sourcePt = path.join(modelsDir, source.dest);\n if (!fs.existsSync(sourcePt)) {\n throw new Error(\n `cannot build ${entry.dest}: source checkpoint ${source.dest} is not ` +\n `provisioned at ${sourcePt} (the direct-download phase must run first)`,\n );\n }\n\n // 2. Ensure the export-only text encoder (sha-verified, cached under the\n // models dir so a rebuild after a token bump never re-downloads 572MB).\n const encoderPath = path.join(modelsDir, build.textEncoder.file);\n const encoderOk =\n fs.existsSync(encoderPath) &&\n sha256File(encoderPath) === build.textEncoder.sha256.toLowerCase();\n if (!encoderOk) {\n await downloadVerified(\n build.textEncoder.url,\n build.textEncoder.sha256,\n encoderPath,\n `${entry.id} text encoder`,\n );\n }\n\n // 3. Run the export inside the locked sidecar env, with the pinned CLIP\n // tokenizer supplied as an ephemeral `--with` overlay (the venv itself\n // is never mutated; uv caches the overlay under <LIBI_HOME>/uv/cache).\n const proj = sidecarProjectDir();\n const args = [\n \"run\",\n \"--locked\",\n \"--with\",\n build.clipRequirement,\n \"python\",\n \"export_yoloe11.py\",\n \"--pt\",\n sourcePt,\n \"--encoder\",\n encoderPath,\n \"--out\",\n dest,\n \"--classes\",\n build.classes.join(\",\"),\n \"--imgsz\",\n String(build.imgsz),\n \"--opset\",\n String(build.opset),\n ];\n logger.info(\n { tag: \"tracking-pyenv\", op: \"model_build_start\", id: entry.id, dest },\n `building tracking model ${entry.id} (local ONNX export)`,\n );\n let stdout = \"\";\n let stderr = \"\";\n try {\n ({ stdout, stderr } = await exec(uvPath(), args, {\n cwd: proj,\n windowsHide: true,\n timeout: 15 * 60_000, // first run resolves the CLIP overlay + exports\n maxBuffer: 32 * 1024 * 1024,\n env: buildUvEnv({\n UV_PROJECT_ENVIRONMENT: trackingVenvDir(),\n NO_COLOR: \"1\",\n }),\n }));\n } catch (e) {\n const err = e as Error & { stdout?: string; stderr?: string };\n const detail = (err.stderr ?? err.stdout ?? err.message ?? \"\").slice(-2000);\n throw new Error(\n `local ONNX export for ${entry.dest} failed: ${err.message}\\n${detail}`,\n );\n }\n const result = parseExportResult(stdout);\n if (!result || !result.ok) {\n const detail = result?.error ?? stderr.slice(-2000);\n throw new Error(\n `local ONNX export for ${entry.dest} did not succeed: ${String(detail)}`,\n );\n }\n if (!fs.existsSync(dest)) {\n throw new Error(\n `local ONNX export for ${entry.dest} reported ok but ${dest} is missing`,\n );\n }\n\n // 4. Record the build so later installs are a cheap hash + compare.\n const outSha = sha256File(dest);\n const info: BuildInfo = {\n inputs: expected,\n output: {\n sha256: outSha,\n sizeBytes: fs.statSync(dest).size,\n ultralytics:\n typeof result.ultralytics === \"string\" ? result.ultralytics : undefined,\n },\n builtAt: new Date().toISOString(),\n };\n fs.writeFileSync(`${dest}.build-info.json`, JSON.stringify(info, null, 2));\n logger.info(\n {\n tag: \"tracking-pyenv\",\n op: \"model_build_done\",\n id: entry.id,\n dest,\n sha256: outSha,\n sizeBytes: info.output.sizeBytes,\n },\n `built tracking model ${entry.id} (local export, inputs pinned)`,\n );\n}\n\n/**\n * The multi-step install. Called by\n * `DependencyManager.runCustomInstaller` when it sees the\n * `__TRACKING_PYENV_INSTALL__` sentinel command.\n */\nexport async function runTrackingPyenvInstall(): Promise<void> {\n const proj = sidecarProjectDir();\n const modelsDir = trackingModelsDir();\n\n // 1. uv sync the sidecar project. cwd-based invocation — the\n // `--project <abs>` form was found unreliable in Task 9 (resolves\n // relative to caller cwd). Mirror that here.\n //\n // UV_PROJECT_ENVIRONMENT (set by buildUvEnv) is what keeps the ~1.2 GB\n // venv OUT of `<proj>/.venv`. That default put it inside the signed\n // `.app`, breaking the code signature and failing on a read-only\n // install. With the env set, `proj` is only ever READ from (pyproject +\n // uv.lock), so the package directory stays read-only — verified against\n // a `chmod 555` project dir.\n const venv = trackingVenvDir();\n logger.info(\n { tag: \"tracking-pyenv\", op: \"uv_sync_start\", proj, venv, uv: uvPath() },\n \"uv sync of tracking sidecar\",\n );\n // `--locked` asserts the shipped uv.lock is already up to date with\n // pyproject.toml. Without it uv may REWRITE uv.lock — another write into the\n // read-only package. It also turns \"someone edited pyproject.toml without\n // re-running `uv lock`\" into a loud install-time error instead of a silent\n // drift between the lockfile we ship and the env we build.\n await pexec(uvPath(), [\"sync\", \"--locked\"], {\n cwd: proj,\n windowsHide: true,\n timeout: 10 * 60_000, // first sync resolves torch + boxmot + opencv\n maxBuffer: 32 * 1024 * 1024,\n // buildUvEnv prepends `~/.libi/bin` so uv-spawned subprocesses (python,\n // pip) find the libi-managed binaries, and pins every uv state location\n // under LIBI_HOME. In a packaged Electron app, bootstrapPath() has\n // already augmented PATH for Homebrew etc., which is inherited here.\n env: buildUvEnv({ UV_PROJECT_ENVIRONMENT: venv, NO_COLOR: \"1\" }),\n });\n logger.info(\n { tag: \"tracking-pyenv\", op: \"uv_sync_done\", proj },\n \"uv sync complete\",\n );\n\n // 2. Ensure model artifacts are on disk: sha-pinned direct downloads\n // first, then any build-at-install exports (which consume the\n // already-provisioned pinned inputs — e.g. yoloe11.onnx is exported\n // locally from the yoloe_vp checkpoint; see ensureBuiltModel).\n //\n // ReID note: there is deliberately NO shipped ReID artifact — on first\n // use boxmot 19 downloads `osnet_x0_25_msmt17.pt` itself and\n // libitrack/associate.py runs its PyTorch backend directly (best-effort,\n // motion-only fallback when offline).\n const manifest = readModelsManifest();\n for (const entry of manifest.filter((e) => e.acquisition === \"direct-download\")) {\n await ensureModel(entry, modelsDir);\n }\n for (const entry of manifest.filter((e) => e.acquisition === \"build-at-install\")) {\n await ensureBuiltModel(entry, manifest, modelsDir);\n }\n\n // 3. Write the install-token marker so resolveStatus() reports installed.\n writeToken();\n logger.info(\n { tag: \"tracking-pyenv\", op: \"install_done\", token: TRACKING_PYENV_TOKEN },\n \"tracking-pyenv install complete\",\n );\n}\n\n/**\n * `CustomInstaller` registered under id `\"tracking-pyenv\"`.\n *\n * `verify()` → returns the models dir path iff the token marker is present\n * AND matches the current token. (We don't re-hash every model on every\n * boot — the token is the cheap re-install lever, same convention as\n * BundledDependency.pinnedInstallToken.)\n *\n * `install` → sentinel command; the real work is `runTrackingPyenvInstall`.\n */\nexport const trackingPyenvInstaller: CustomInstaller & {\n install: { command: string; args: string[]; timeoutMs?: number };\n} = {\n verify: async () => {\n const marker = readToken();\n if (marker === TRACKING_PYENV_TOKEN) {\n return trackingModelsDir();\n }\n return null;\n },\n install: {\n command: TRACKING_PYENV_SENTINEL,\n args: [],\n // uv sync (cold torch/boxmot) + model downloads (incl. the 572MB\n // mobileclip export input) + the local yoloe11 ONNX export.\n timeoutMs: 30 * 60_000,\n },\n};\n\n/** Direct entry point for the dev dry-run / scripts. */\nexport async function install(): Promise<void> {\n await runTrackingPyenvInstall();\n}\n","// lib/effects/registry.ts\nimport type { EffectDef, EffectPhase, LayerKind } from \"./types\";\nimport { BUILTIN_EFFECTS } from \"./builtin\";\n\n/** Custom effects loaded from disk packages. REPLACED wholesale on each refresh\n * (the loader passes the full current disk set) so removal works by re-loading. */\nlet CUSTOM: EffectDef[] = [];\n\n/** Bumped on every registry mutation; lets React consumers subscribe via\n * useSyncExternalStore (see hooks/preview/use-register-custom-effects.ts)\n * instead of mirroring the registry into component state. */\nlet VERSION = 0;\nconst listeners = new Set<() => void>();\n\nfunction notifyRegistryChanged(): void {\n VERSION++;\n for (const l of listeners) l();\n}\n\n/** Subscribe to registry mutations (useSyncExternalStore contract). */\nexport function subscribeEffectsRegistry(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/** Monotonic registry version (useSyncExternalStore snapshot). */\nexport function effectsRegistryVersion(): number {\n return VERSION;\n}\n\n/** Register the full current set of custom effects, replacing any prior set. */\nexport function registerCustomEffects(defs: EffectDef[]): void {\n CUSTOM = defs;\n notifyRegistryChanged();\n}\n\n/** Clear all custom effects (used by test teardown). */\nexport function clearCustomEffects(): void {\n CUSTOM = [];\n notifyRegistryChanged();\n}\n\n/** All registered effects: built-ins first, then customs. A custom id shadows\n * a built-in of the same id (skills-style shadow semantics). */\nfunction allEffects(): EffectDef[] {\n const m = new Map<string, EffectDef>();\n for (const e of BUILTIN_EFFECTS) m.set(e.meta.id, e);\n for (const e of CUSTOM) m.set(e.meta.id, e);\n return [...m.values()];\n}\n\nconst byId = (): Map<string, EffectDef> => {\n const m = new Map<string, EffectDef>();\n for (const e of allEffects()) m.set(e.meta.id, e);\n return m;\n};\n\nexport function findEffect(effectId: string): EffectDef | undefined {\n return byId().get(effectId);\n}\n\nexport function isKnownEffectId(effectId: string): boolean {\n return byId().has(effectId);\n}\n\nexport interface ListEffectsFilter {\n kind?: LayerKind;\n phase?: EffectPhase;\n family?: \"animation\";\n}\n\nexport function listEffects(filter: ListEffectsFilter = {}): EffectDef[] {\n return allEffects().filter((e) => {\n if (filter.kind && !e.meta.supports.includes(filter.kind)) return false;\n if (filter.phase && !e.meta.phases.includes(filter.phase)) return false;\n if (filter.family && e.meta.family !== filter.family) return false;\n return true;\n });\n}\n\n/** Convenience resolver for composeEffects. */\nexport function resolveEffect(effectId: string): EffectDef | undefined {\n return findEffect(effectId);\n}\n","import type { Transform3D, Vec3 } from \"@/lib/engine/types\";\n\n/** The identity 3D transform: no translation, no rotation. */\nexport const IDENTITY_TRANSFORM3D: Transform3D = {\n position: { x: 0, y: 0, z: 0 },\n rotation: { x: 0, y: 0, z: 0 },\n};\n\nfunction vecIsZero(v: Vec3): boolean {\n return v.x === 0 && v.y === 0 && v.z === 0;\n}\n\n/** True only when `t` equals the identity transform. */\nexport function isIdentityTransform3d(t: Transform3D): boolean {\n return vecIsZero(t.position) && vecIsZero(t.rotation);\n}\n\nconst DEG_TO_RAD = Math.PI / 180;\nconst RAD_TO_DEG = 180 / Math.PI;\n\n/** Convert each component from degrees to radians. */\nexport function degToRadVec3(v: Vec3): Vec3 {\n return { x: v.x * DEG_TO_RAD, y: v.y * DEG_TO_RAD, z: v.z * DEG_TO_RAD };\n}\n\n/** Convert each component from radians to degrees. */\nexport function radToDegVec3(v: Vec3): Vec3 {\n return { x: v.x * RAD_TO_DEG, y: v.y * RAD_TO_DEG, z: v.z * RAD_TO_DEG };\n}\n\n/** Wrap an angle (radians) into the range [-PI, PI]. */\nfunction wrap(angle: number): number {\n const twoPi = Math.PI * 2;\n const a = ((angle + Math.PI) % twoPi + twoPi) % twoPi;\n return a - Math.PI;\n}\n\n/**\n * Apply an orbit drag to a transform: horizontal drag (dx) drives yaw\n * (rotation.y), vertical drag (dy) drives pitch (rotation.x). Both wrapped\n * into [-PI, PI]. Position is unchanged. Returns a NEW transform;\n * `base` is not mutated.\n */\nexport function applyOrbitDrag(\n base: Transform3D,\n dxPx: number,\n dyPx: number,\n sensitivity = 0.01,\n): Transform3D {\n return {\n position: { ...base.position },\n rotation: {\n x: wrap(base.rotation.x + dyPx * sensitivity),\n y: wrap(base.rotation.y + dxPx * sensitivity),\n z: base.rotation.z,\n },\n };\n}\n\n/**\n * Vertical drag → pitch (rotation.x). Wrapped into [-PI, PI]. Only rotation.x\n * changes; position and the other rotation axes are unchanged. Returns a\n * NEW transform; `base` is not mutated.\n */\nexport function applyPitchDrag(\n base: Transform3D,\n dyPx: number,\n sensitivity = 0.01,\n): Transform3D {\n return {\n position: { ...base.position },\n rotation: {\n x: wrap(base.rotation.x + dyPx * sensitivity),\n y: base.rotation.y,\n z: base.rotation.z,\n },\n };\n}\n\n/**\n * Horizontal drag → yaw (rotation.y). Wrapped into [-PI, PI]. Only rotation.y\n * changes. Returns a NEW transform; `base` is not mutated.\n */\nexport function applyYawDrag(\n base: Transform3D,\n dxPx: number,\n sensitivity = 0.01,\n): Transform3D {\n return {\n position: { ...base.position },\n rotation: {\n x: base.rotation.x,\n y: wrap(base.rotation.y + dxPx * sensitivity),\n z: base.rotation.z,\n },\n };\n}\n\n/**\n * Drag → roll (rotation.z). Wrapped into [-PI, PI]. Only rotation.z changes.\n * Returns a NEW transform; `base` is not mutated.\n */\nexport function applyRollDrag(\n base: Transform3D,\n dPx: number,\n sensitivity = 0.01,\n): Transform3D {\n return {\n position: { ...base.position },\n rotation: {\n x: base.rotation.x,\n y: base.rotation.y,\n z: wrap(base.rotation.z + dPx * sensitivity),\n },\n };\n}\n\n/**\n * Vertical drag → depth (position.z), in world units (NOT wrapped — depth is\n * unbounded). Default sensitivity is 0.5 (larger than the radian-based rotation\n * helpers): the inspector exposes `transformPosZ` over a ±2000 world-unit range\n * (`components/preview/three-d-fields.tsx`), so 0.5 maps a ~4000px vertical drag\n * to the full range — a sane 1:2 px→unit feel. Only position.z changes. Returns\n * a NEW transform; `base` is not mutated.\n */\nexport function applyDepthDrag(\n base: Transform3D,\n dyPx: number,\n sensitivity = 0.5,\n): Transform3D {\n return {\n position: {\n x: base.position.x,\n y: base.position.y,\n z: base.position.z + dyPx * sensitivity,\n },\n rotation: { ...base.rotation },\n };\n}\n\ntype Axis = \"x\" | \"y\" | \"z\";\n\n/** Immutable setter: returns a copy of `t` with `position[axis] = v`. */\nexport function withPosition(t: Transform3D, axis: Axis, v: number): Transform3D {\n return {\n position: { ...t.position, [axis]: v },\n rotation: { ...t.rotation },\n };\n}\n\n/** Immutable setter: returns a copy of `t` with `rotation[axis] = v`. */\nexport function withRotation(t: Transform3D, axis: Axis, v: number): Transform3D {\n return {\n position: { ...t.position },\n rotation: { ...t.rotation, [axis]: v },\n };\n}\n\n/** Default detent grid (radians): 0, ±45, ±90, ±135, ±180. */\nconst DEFAULT_DETENTS_RAD = [0, 45, 90, 135, 180, -45, -90, -135, -180].map(\n (d) => (d * Math.PI) / 180,\n);\n\n/**\n * Snap an angle (radians) to the nearest detent if it is within `thresholdRad`\n * (default ~5°). Otherwise return the angle unchanged. Pass `thresholdRad = 0`\n * (the Alt-bypass path) to disable snapping entirely. Pure.\n */\nexport function snapAngleToDetents(\n angleRad: number,\n detentsRad: number[] = DEFAULT_DETENTS_RAD,\n thresholdRad = 0.0873,\n): number {\n let best = angleRad;\n let bestDist = thresholdRad;\n for (const d of detentsRad) {\n const dist = Math.abs(angleRad - d);\n if (dist <= bestDist) {\n bestDist = dist;\n best = d;\n }\n }\n return best;\n}\n\n/** Outer \"stays visible\" clamp for `position.z` (world units). Pure.\n * @deprecated Use `clampDepthForKind` for new code — it applies the correct\n * per-kind limit. This overload is retained for callers that already use it. */\nexport function clampDepth(z: number, max = 1800): number {\n return Math.max(-max, Math.min(max, z));\n}\n\n/**\n * Z range for 3D-world overlays (three / text with threeD), in world units.\n * Widened to ±4 so Depth can push content nearly out of sight (far) or right up\n * to the camera (very zoomed-in) — the user-requested expressive range. The\n * billboard camera sits ~6 units back; at +4 the content is close (≈2 units\n * away, big) and at −4 it's distant (≈10 units, small) but still on-screen.\n */\nexport const DEPTH_LIMIT_WORLD = 4;\n\n/**\n * Z range for quad overlay kinds (image / video / code), in pixel-space\n * position.z. Widened to ±500 to match the expanded world range's expressiveness.\n */\nexport const DEPTH_LIMIT_PX = 500;\n\n/**\n * Per-kind safe Z range clamp. World units for three/text (3D-text); pixels\n * for quad kinds (image/video/code). Keeps content on-screen regardless of\n * what the drag or slider emits.\n */\nexport function clampDepthForKind(z: number, kind: string): number {\n const lim = kind === \"three\" || kind === \"text\" ? DEPTH_LIMIT_WORLD : DEPTH_LIMIT_PX;\n return Math.max(-lim, Math.min(lim, z));\n}\n","import type { Transform3D } from \"@/lib/engine/types\";\nimport { IDENTITY_TRANSFORM3D, isIdentityTransform3d } from \"@/lib/overlays/transform3d\";\n\n/** Orientation input: `transform3d` is the ONLY rotation storage; flip is the\n * canonical `flipH`/`flipV` booleans. */\nexport interface OverlayOrientation {\n transform3d?: Transform3D;\n flipH?: boolean;\n flipV?: boolean;\n}\n\n/** Resolved flip state — read from the canonical `flipH`/`flipV` booleans. */\nexport interface ResolvedFlip { flipH: boolean; flipV: boolean; }\n\n/** Flip is the canonical `flipH`/`flipV` booleans. */\nexport function resolveFlip(o: OverlayOrientation): ResolvedFlip {\n return { flipH: !!o.flipH, flipV: !!o.flipV };\n}\n\n/**\n * The single source of truth for an overlay's orientation. `transform3d` is the\n * ONLY rotation authority — in-plane spin lives on `rotation.z`. There is no\n * legacy `rotation` (degrees) field: it was deleted so a spun overlay can never\n * double-count its spin (dragging it used to ADD the legacy field on top of the\n * transform3d z). Flip is NOT part of the transform — it lives on\n * `flipH`/`flipV` and is read via `resolveFlip`. Bare input ⇒ identity.\n */\nexport function resolveOverlayTransform(o: OverlayOrientation): Transform3D {\n return o.transform3d ?? IDENTITY_TRANSFORM3D;\n}\n\n/**\n * Split a resolved transform into its out-of-plane 3D part (rotation.z zeroed)\n * and the in-plane screen-roll (`rotation.z`, radians).\n *\n * `rotation.z` is ALWAYS a 2D screen-roll about the overlay's rect center — it\n * is NEVER a 3D euler component. Applying it as a Canvas2D `ctx.rotate` AFTER\n * the (possibly perspective-projected) content is composited makes the in-plane\n * \"Spin\" render identically — same pivot, same direction (clockwise, the\n * Canvas2D convention) — across every path: planar 2D, the `three` overlay, 3D\n * text, and the spatial textured-quad. The 3D euler fed to three.js / the quad\n * projection uses only x (elevation) and y (angle). This is what keeps Spin\n * consistent (it used to roll CCW for 3D text via `scene.rotation.z`) and keeps\n * a tilted spatial quad's on-screen footprint STABLE while you spin (folding z\n * into the euler made the projected bbox swing wildly).\n */\nexport function splitScreenRoll(t: Transform3D): { spatial: Transform3D; rollRad: number } {\n const rollRad = t.rotation.z;\n if (!rollRad) return { spatial: t, rollRad: 0 };\n return {\n spatial: { position: t.position, rotation: { x: t.rotation.x, y: t.rotation.y, z: 0 } },\n rollRad,\n };\n}\n\n/** Rotate a screen point by `rad` (radians, clockwise) about `center`. */\nexport function rotatePointAround(\n px: number,\n py: number,\n cx: number,\n cy: number,\n rad: number,\n): { x: number; y: number } {\n if (!rad) return { x: px, y: py };\n const cos = Math.cos(rad), sin = Math.sin(rad);\n const dx = px - cx, dy = py - cy;\n return { x: cx + dx * cos - dy * sin, y: cy + dx * sin + dy * cos };\n}\n\nexport type TransformClass = \"identity\" | \"planar\" | \"spatial\";\n\n/**\n * Classify a transform to pick the render path:\n * - identity — no-op.\n * - planar — only in-plane components (z-rotation, x/y-position).\n * Representable as a Canvas2D affine; keeps the cheap path.\n * - spatial — any out-of-plane component (rotation.x/y ≠ 0, or position.z ≠ 0).\n * Needs perspective ⇒ the textured-quad path.\n */\nexport function classifyTransform(t: Transform3D): TransformClass {\n if (isIdentityTransform3d(t)) return \"identity\";\n if (t.rotation.x !== 0 || t.rotation.y !== 0 || t.position.z !== 0) return \"spatial\";\n return \"planar\";\n}\n\nexport interface TransformBox { x: number; y: number; width: number; height: number; }\n\n/** A minimal Canvas2D op (consumed by applyOverlayTransform). */\nexport type Canvas2DOp =\n | { kind: \"translate\"; x: number; y: number }\n | { kind: \"rotate\"; rad: number }\n | { kind: \"scale\"; x: number; y: number };\n\n/**\n * The Canvas2D ops that realize a PLANAR transform (plus flip) about the box center:\n * translate(cx + posX, cy + posY) · rotate(rot.z) · scale(sx, sy) · translate(-cx, -cy)\n * Returns [] only when the transform is identity AND there is no flip.\n * Spatial input ⇒ [] (caller routes to quad path). Legacy values (position 0)\n * reproduce the old center-anchored rotate+flip byte-for-byte.\n */\nexport function planarCanvas2DOps(t: Transform3D, box: TransformBox, flip: ResolvedFlip): Canvas2DOp[] {\n const cls = classifyTransform(t);\n const sx = flip.flipH ? -1 : 1;\n const sy = flip.flipV ? -1 : 1;\n if (cls === \"spatial\") return [];\n if (cls === \"identity\" && sx === 1 && sy === 1) return [];\n const cx = box.x + box.width / 2;\n const cy = box.y + box.height / 2;\n return [\n { kind: \"translate\", x: cx + t.position.x, y: cy + t.position.y },\n { kind: \"rotate\", rad: t.rotation.z },\n { kind: \"scale\", x: sx, y: sy },\n { kind: \"translate\", x: -cx, y: -cy },\n ];\n}\n\n/**\n * Inverse-map a screen point into the overlay's local (pre-transform) space for\n * a PLANAR transform — undoes the ops above in reverse order. Identity/spatial\n * ⇒ the point unchanged (spatial hit-test falls back to the rect upstream).\n */\nexport function inversePlanarPoint(\n t: Transform3D,\n box: TransformBox,\n flip: ResolvedFlip,\n px: number,\n py: number,\n): { x: number; y: number } {\n if (classifyTransform(t) === \"spatial\") return { x: px, y: py };\n const sx = flip.flipH ? -1 : 1;\n const sy = flip.flipV ? -1 : 1;\n const cx = box.x + box.width / 2;\n const cy = box.y + box.height / 2;\n // forward: translate(cx+posX, cy+posY) rotate(z) scale(sx,sy) translate(-cx,-cy)\n // inverse: translate(cx,cy) scale(1/sx,1/sy) rotate(-z) translate(-(cx+posX),-(cy+posY))\n let x = px - (cx + t.position.x);\n let y = py - (cy + t.position.y);\n const cos = Math.cos(-t.rotation.z), sin = Math.sin(-t.rotation.z);\n const rx = x * cos - y * sin;\n const ry = x * sin + y * cos;\n x = rx / sx; y = ry / sy;\n return { x: x + cx, y: y + cy };\n}\n","import { getCurrentPort } from \"@/lib/libi-home\";\n\n/**\n * Lightweight HTTP client for notifying the Next.js server about UI events.\n * All calls are fire-and-forget — silently no-op if the server isn't running.\n */\n\nfunction getServerUrl(): string | null {\n try {\n const port = getCurrentPort();\n return `http://127.0.0.1:${port}`;\n } catch {\n return null;\n }\n}\n\nasync function send(payload: Record<string, unknown>): Promise<void> {\n const url = getServerUrl();\n if (!url) return;\n\n try {\n await fetch(`${url}/api/notify`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(3000),\n });\n } catch {\n // Fire-and-forget — server may not be running.\n }\n}\n\nexport const notify = {\n navigate(event: {\n target: \"piece\" | \"asset\" | \"preview\" | \"storyboard\" | \"folder\";\n /** Required for piece/asset/preview/storyboard/folder targets. */\n pieceId?: string;\n fileId?: string;\n /** Optional id for special targets. */\n id?: string;\n }): void {\n send({ type: \"navigate\", ...event });\n },\n refreshQuery(event: { queryKey: string; pieceId?: string; fileId?: string; trackId?: string }): void {\n send({ type: \"refresh_query\", ...event });\n },\n refreshMcpConfig(): void {\n send({ type: \"refresh_mcp_config\" });\n },\n /** Tell the server that memories or the instruction override changed (files\n * under ~/.libi/). The server regenerates workspace files and restarts\n * sessions; the agent process firing this is about to be killed. */\n instructionsChanged(): void {\n send({ type: \"instructions_changed\" });\n },\n analysisChanged(event: { fileId: string }): void {\n send({ type: \"analysis_changed\", ...event });\n },\n /** Tell the server to navigate the user to Settings (optionally focusing an MCP card). */\n navigateSettings(event: { mcpId?: string }): void {\n send({ type: \"navigate_settings\", mcpId: event.mcpId });\n },\n /** Tell the server to open or switch the right-region panel (onboarding, api-config, etc.). */\n rightRegion(event: {\n mode: \"editor\" | \"onboarding\" | \"api-config\";\n mcpId?: string;\n }): void {\n send({ type: \"right_region\", ...event });\n },\n /** Flash an inspector field for an overlay (guided edit). */\n highlight(event: {\n pieceId: string;\n overlayId: string;\n property: string;\n note?: string;\n }): void {\n send({ type: \"highlight\", ...event });\n },\n /** Flash an effect in the catalog or on a layer's slot (guided edit). */\n highlightEffect(event: {\n pieceId: string;\n target: { kind: \"catalog\"; effectId: string; phase?: \"in\" | \"out\" | \"loop\" } | { kind: \"applied\"; layerId: string; phase: \"in\" | \"out\" | \"loop\" };\n note?: string;\n }): void {\n send({ type: \"highlight_effect\", ...event });\n },\n /** Set a specific overlay's inspector tab/group. */\n setComplexityMode(event: {\n pieceId?: string;\n overlayId?: string;\n mode: \"transform\" | \"style\" | \"text\" | \"3d\" | \"anchors\";\n }): void {\n send({\n type: \"set_complexity_mode\",\n pieceId: event.pieceId,\n overlayId: event.overlayId,\n mode: event.mode,\n });\n },\n /** Forward a job progress tick into the in-process event bus so\n * `SessionEventHandler` can synthesize an `agent-tool-progress` event\n * on the matching tool-call part in the active session. */\n jobProgress(event: {\n jobId: string;\n toolCallId?: string;\n kind: string;\n done: number;\n total: number;\n unit: string;\n etaMs: number | null;\n msSinceProgress?: number | null;\n toolName?: string;\n toolArgs?: unknown;\n progressLabel?: string;\n }): void {\n send({ type: \"job_progress\", ...event });\n },\n};\n\nexport type PushPayload = { title: string; body: string; pieceId?: string; jobId?: string };\nexport type Notifier = {\n isFocused: () => boolean;\n notify: (p: PushPayload) => void;\n};\n\nlet notifier: Notifier | null = null;\n\n/** Bind the runtime notifier. Called from `electron/main.ts` after window\n * creation. In `npx libi --connect-agent` mode this is never called and\n * `pushIfBackgrounded` silently no-ops. */\nexport function bindNotifier(n: Notifier): void {\n notifier = n;\n}\n\n/** Test-only — override notifier inside vitest. */\nexport function __setTestNotifier(n: Notifier | null): void {\n notifier = n;\n}\n\n/** Fire a system notification if the runtime window isn't focused. Used by\n * the job-progress bridge on job completion + failure for runs over\n * ~10 seconds. Caller suppresses based on the user's settings toggle. */\nexport async function pushIfBackgrounded(p: PushPayload): Promise<void> {\n if (!notifier) return;\n if (notifier.isFocused()) return;\n notifier.notify(p);\n}\n","import fs from \"fs\";\nimport path from \"path\";\nimport { spawn } from \"child_process\";\nimport { requireUvBinary } from \"@/lib/uv-path\";\nimport { buildUvEnv } from \"@/lib/uv-env/spawn-env\";\nimport { resolveWhisperModel, whisperModelsDir } from \"@/lib/whisper/models\";\nimport type { ElevenLabsTranscription } from \"@/lib/elevenlabs/transcribe\";\nimport { hashSpec } from \"@/lib/uv-env/hash-spec\";\nimport { isTokenCurrent, writeInstallToken } from \"@/lib/uv-env/install-token\";\nimport { packageRoot } from \"@/lib/runtime/package-root\";\n\nexport const FASTER_WHISPER_VERSION = \"1.1.1\";\n\n/** Pinned Python version for the faster-whisper env. We pin 3.12 to\n * match the rest of libi's uv-managed envs (analyze, generate, TTS)\n * and give every host a deterministic interpreter. 3.12 has full wheel\n * coverage for faster-whisper + its native deps. */\nexport const WHISPER_PYTHON_VERSION = \"3.12\";\n\n/** Single source of truth for the Whisper `--with` list. Consumed by\n * both buildWhisperArgs (what we actually spawn) and\n * whisperEnvSignature() (what the install-token marker hashes). Keep\n * these aligned via this constant so any new dep automatically updates\n * both. Mirrors lib/music/analyze.ts:ANALYZE_WITH_SPECS pattern.\n *\n * `requests` is required because `faster-whisper` 1.1.1 does\n * `import requests` at module load time in `faster_whisper/utils.py`\n * but does NOT declare it in install_requires — it used to get pulled\n * in transitively via `huggingface_hub` < 1.0, but huggingface_hub 1.x\n * dropped `requests` from core deps. Without this explicit pin the env\n * resolves cleanly then crashes at first `from faster_whisper import\n * WhisperModel` with `No module named 'requests'`. Upstream issue:\n * https://github.com/SYSTRAN/faster-whisper/issues — keep the pin\n * until a faster-whisper release declares it directly. */\nexport const WHISPER_WITH_SPECS = [\n `faster-whisper==${FASTER_WHISPER_VERSION}`,\n \"requests\",\n] as const;\n\nexport class WhisperTranscribeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"WhisperTranscribeError\";\n }\n}\n\nfunction whisperScriptPath(): string {\n // cwd is the package root under `npx libi`, but the *user's* project dir\n // under `npx libi --connect-agent` — so fall back to a __dirname-relative\n // path (this file lives at <pkg>/lib/whisper/, script at <pkg>/mcp/whisper/).\n const candidates = [\n path.join(process.cwd(), \"mcp/whisper/transcribe.py\"),\n path.resolve(__dirname, \"../../mcp/whisper/transcribe.py\"),\n // Depth-independent: the compiled mirror (scripts/build-cli.js) puts this\n // module at dist-cli/lib/whisper/, one level deeper than source, so the\n // `../..` above resolves inside dist-cli/ instead of the package root.\n path.join(packageRoot(), \"mcp/whisper/transcribe.py\"),\n ];\n for (const c of candidates) {\n if (fs.existsSync(c)) return c;\n }\n return candidates[0];\n}\n\nfunction resolveUvPath(): string {\n return requireUvBinary(WhisperTranscribeError, \"Whisper\");\n}\n\nexport function buildWhisperArgs(opts: {\n scriptPath: string;\n audioPath?: string;\n model: string;\n downloadRoot: string;\n languageCode?: string;\n downloadOnly?: boolean;\n}): string[] {\n const args = [\n \"run\",\n \"--python\",\n WHISPER_PYTHON_VERSION,\n ...WHISPER_WITH_SPECS.flatMap((s) => [\"--with\", s]),\n \"python\",\n opts.scriptPath,\n ];\n if (!opts.downloadOnly && opts.audioPath) args.push(opts.audioPath);\n args.push(\"--model\", opts.model, \"--download-root\", opts.downloadRoot);\n if (opts.languageCode) args.push(\"--language\", opts.languageCode);\n if (opts.downloadOnly) args.push(\"--download-only\");\n return args;\n}\n\nexport function parseWhisperStdout(stdout: string): ElevenLabsTranscription {\n const trimmed = stdout.trim();\n if (!trimmed)\n throw new WhisperTranscribeError(\"whisper produced empty stdout\");\n let json: unknown;\n try {\n json = JSON.parse(trimmed);\n } catch {\n throw new WhisperTranscribeError(\n `whisper stdout was not JSON: ${trimmed.slice(0, 200)}`,\n );\n }\n if (\n typeof json !== \"object\" ||\n json === null ||\n typeof (json as Record<string, unknown>).text !== \"string\"\n ) {\n throw new WhisperTranscribeError(\n `whisper JSON missing 'text': ${JSON.stringify(json).slice(0, 200)}`,\n );\n }\n const typed = json as ElevenLabsTranscription;\n if (!Array.isArray(typed.words)) typed.words = [];\n return typed;\n}\n\n/** Progress callback: `(filesDone, filesTotal)` parsed from `PROGRESS x/y`\n * stderr markers emitted by the Python download hook. */\ntype ProgressCb = (done: number, total: number) => void;\n\nfunction runUv(\n args: string[],\n timeoutMs: number,\n onProgress?: ProgressCb,\n): Promise<string> {\n const uv = resolveUvPath();\n return new Promise((resolve, reject) => {\n const child = spawn(uv, args, {\n windowsHide: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n env: buildUvEnv(),\n });\n let out = \"\";\n let err = \"\";\n const timer = setTimeout(() => {\n child.kill(\"SIGKILL\");\n reject(\n new WhisperTranscribeError(`whisper timed out after ${timeoutMs}ms`),\n );\n }, timeoutMs);\n child.stdout.on(\"data\", (d) => (out += d.toString()));\n child.stderr.on(\"data\", (d) => {\n const s = d.toString();\n err += s;\n for (const m of s.matchAll(/PROGRESS (\\d+)\\/(\\d+)/g)) {\n onProgress?.(Number(m[1]), Number(m[2]));\n }\n });\n child.on(\"error\", (e) => {\n clearTimeout(timer);\n reject(new WhisperTranscribeError(`uv spawn failed: ${e.message}`));\n });\n child.on(\"close\", (code) => {\n clearTimeout(timer);\n if (code !== 0) {\n reject(\n new WhisperTranscribeError(\n `whisper exited ${code}: ${err.trim().slice(0, 500)}`,\n ),\n );\n return;\n }\n resolve(out);\n });\n });\n}\n\nexport async function transcribeAudio(opts: {\n audioPath: string;\n model?: string;\n languageCode?: string;\n}): Promise<ElevenLabsTranscription> {\n const model = resolveWhisperModel(opts.model);\n const args = buildWhisperArgs({\n scriptPath: whisperScriptPath(),\n audioPath: opts.audioPath,\n model,\n downloadRoot: whisperModelsDir(),\n languageCode: opts.languageCode,\n });\n // 30 min ceiling: long files on CPU + first-run wheel fetch.\n const stdout = await runUv(args, 30 * 60_000);\n const result = parseWhisperStdout(stdout);\n // Warm-env attestation: once the subprocess returned valid output we\n // know the spec resolved end-to-end. Sticky marker so the next call\n // skips needs_install if nothing changed.\n writeWhisperEnvToken();\n return result;\n}\n\n/** Stable signature for the Whisper env spec. Bumping FASTER_WHISPER_VERSION,\n * WHISPER_PYTHON_VERSION, or adding a new --with will invalidate it. */\nexport function whisperEnvSignature(): string {\n return hashSpec(WHISPER_PYTHON_VERSION, WHISPER_WITH_SPECS);\n}\n\nconst WHISPER_ENV_TOKEN = \".libi-whisper-env.install-token\";\n\nexport function isWhisperEnvCurrent(): boolean {\n return isTokenCurrent(WHISPER_ENV_TOKEN, whisperEnvSignature());\n}\n\nexport function writeWhisperEnvToken(): void {\n writeInstallToken(WHISPER_ENV_TOKEN, whisperEnvSignature());\n}\n\nexport async function downloadModel(\n model: string,\n onProgress?: ProgressCb,\n): Promise<void> {\n const args = buildWhisperArgs({\n scriptPath: whisperScriptPath(),\n model: resolveWhisperModel(model),\n downloadRoot: whisperModelsDir(),\n downloadOnly: true,\n });\n // 60 min: large-v3 is ~3 GB. `onProgress` receives per-file ticks from\n // the Python snapshot_download hook.\n await runUv(args, 60 * 60_000, onProgress);\n}\n","import fs from \"fs\";\nimport path from \"path\";\nimport { execFileSync } from \"child_process\";\nimport { getLibiBinDir } from \"@/lib/libi-home\";\nimport { isWindows, exeSuffix } from \"@/lib/platform\";\n\n/**\n * Resolve the `uv` binary path with the same policy as `resolveStatus`\n * + `installYtDlpViaUv` in `mcp/registry/dependency-manager.ts`:\n *\n * 1. `<libi-bin-dir>/uv` — preferred (libi's own install via Category A)\n * 2. system PATH (`which uv` / `where uv`) — covers cases where Category A\n * already treats a PATH-resolved uv as \"installed\" (e.g. Homebrew)\n *\n * Returns the resolved absolute path, or `null` if neither exists. Callers\n * throw their own typed error so the user sees a domain-specific message\n * (Whisper / Kokoro / ACE-Step), but the resolution policy is shared so\n * the three runners can't drift apart.\n */\nexport function resolveUvBinary(): string | null {\n const bundled = path.join(getLibiBinDir(), `uv${exeSuffix()}`);\n if (fs.existsSync(bundled)) return bundled;\n\n const lookup = isWindows() ? \"where\" : \"which\";\n try {\n const stdout = execFileSync(lookup, [\"uv\"], {\n windowsHide: true,\n encoding: \"utf8\",\n timeout: 5_000,\n });\n const candidate = stdout.split(\"\\n\")[0]?.trim();\n if (candidate && fs.existsSync(candidate)) return candidate;\n } catch {\n /* uv not on PATH */\n }\n\n return null;\n}\n\n/**\n * Convenience wrapper for callers that prefer to throw. Pass a domain-specific\n * Error constructor so the user sees \"Whisper requires…\" / \"Local TTS\n * requires…\" / \"Local music requires…\" rather than a generic message.\n */\nexport function requireUvBinary(\n ErrorCtor: new (message: string) => Error,\n featureLabel: string,\n): string {\n const uv = resolveUvBinary();\n if (uv) return uv;\n const expected = path.join(getLibiBinDir(), `uv${exeSuffix()}`);\n throw new ErrorCtor(\n `uv not found at ${expected} or on PATH. ${featureLabel} requires the bundled uv (tier-1 dep).`,\n );\n}\n","import { createHash } from \"crypto\";\n\n/** Deterministic signature for a `uv run --python X --with A --with B …`\n * invocation. We hash the python version + the (sorted) list of pinned\n * dependency specs so any change to either triggers a marker mismatch on\n * the next call. Sorting means a refactor that reorders --with flags\n * WITHOUT semantic change does NOT churn the signature.\n *\n * Output is 16 hex chars (64 bits) — collision-resistant for our scale\n * and short enough to embed in a token filename or log line. */\nexport function hashSpec(pythonVersion: string, withSpecs: readonly string[]): string {\n const parts = [`python:${pythonVersion}`, ...[...withSpecs].sort()];\n return createHash(\"sha256\").update(parts.join(\"\\n\")).digest(\"hex\").slice(0, 16);\n}\n","import fs from \"fs\";\nimport path from \"path\";\nimport { getLibiHome } from \"@/lib/libi-home\";\n\n/** Read a one-line token file under LIBI_HOME. Returns null on any\n * filesystem error (missing file, permission, garbage). */\nexport function readInstallToken(filename: string): string | null {\n try {\n return fs\n .readFileSync(path.join(getLibiHome(), filename), \"utf-8\")\n .trim();\n } catch {\n return null;\n }\n}\n\n/** Write a one-line token file under LIBI_HOME. Creates parent dirs.\n * Single trailing newline so `cat <file>` prints cleanly. */\nexport function writeInstallToken(filename: string, signature: string): void {\n const full = path.join(getLibiHome(), filename);\n fs.mkdirSync(path.dirname(full), { recursive: true });\n fs.writeFileSync(full, signature + \"\\n\");\n}\n\n/** True iff the on-disk token equals the expected signature. */\nexport function isTokenCurrent(filename: string, signature: string): boolean {\n return readInstallToken(filename) === signature;\n}\n","/**\n * Simple in-memory tracking of which piece is currently open in the editor.\n * Used by MCP tools (open piece) and the proxy LRU (open-piece protection)\n * without threading state through every call.\n */\nconst globalForEditor = globalThis as unknown as {\n __openedPieceId?: string | null;\n};\n\nexport function getOpenedPieceId(): string | null {\n return globalForEditor.__openedPieceId ?? null;\n}\n\nexport function setOpenedPieceId(pieceId: string | null): void {\n globalForEditor.__openedPieceId = pieceId;\n}\n","import { join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { getLibiBinDir, getLibiHome } from \"@/lib/libi-home\";\nimport { exeSuffix } from \"@/lib/platform\";\n\nexport function uvPath(): string {\n const p = join(getLibiBinDir(), `uv${exeSuffix()}`);\n return existsSync(p) ? p : \"uv\";\n}\n\nexport function sidecarProjectDir(): string {\n return join(process.cwd(), \"mcp\", \"tracking\", \"py\");\n}\n\nexport function trackingModelsDir(): string {\n return join(getLibiHome(), \"models\", \"tracking\");\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { trackingModelsDir } from \"@/lib/tracking/engine-deps\";\n\nconst TOKEN_MARKER = \".install-token\";\n\n/** True when the tracking-pyenv install token marker exists (the cheap\n * signal the custom installer writes on success). */\nexport function trackingEngineInstalled(): boolean {\n try {\n return fs.existsSync(path.join(trackingModelsDir(), TOKEN_MARKER));\n } catch {\n return false;\n }\n}\n\nexport interface TrackingNotInstalled {\n error: \"tracking_engine_not_installed\";\n data: { hint: string; installPlanPath: string };\n}\n\nexport function trackingNotInstalledError(): TrackingNotInstalled {\n return {\n error: \"tracking_engine_not_installed\",\n data: {\n hint:\n \"The libi-tracking engine is not installed yet. Call libi.get_install_plan \" +\n \"(mcpId 'libi-tracking') and follow it — disclose the ~2 GB / ~10-20 min cost and get \" +\n \"the user's OK, then call libi.install_tracking_engine (it runs the real install \" +\n \"as a background job), then libi.verify_install, then retry.\",\n installPlanPath: \"mcp/bundled-mcps/plans/libi-tracking.md\",\n },\n };\n}\n"],"names":["MEASUREMENT_ID","process","env","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","name","has","toAgentEventId","id","undefined","sanitizeParams","params","out","k","v","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","enabled","clean","db","insert","values","crypto","randomUUID","paramsJson","JSON","stringify","nextAttemptAt","Date","run","n","select","from","all","excess","orderBy","createdAt","limit","map","r","delete","where","drainAnalyticsQueue","deps","now","fetchImpl","fetch","getSettings","markMilestoneOnce","settings","rows","length","sent","failed","dropped","nowMs","due","firstVisitAsked","userId","row","state","sessionStart","firstVisit","parse","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","dropProxyFile","fileId","reason","proxyStatus","proxyFilename","info","event","proxyPath","join","pieceId","unlinkSync","proxyGeneratedAt","emit","queryKey","RENDER_STALL_TIMEOUT_MS","RENDER_ABSOLUTE_CAP_MS","globalForRenderJobs","globalThis","jobs","__libi_render_jobs","Map","createRenderJob","init","jobId","token","resolve","reject","done","Promise","rej","stallTimeoutMs","absoluteCapMs","timeoutMs","stallTimer","setTimeout","entry","get","settled","clearTimeout","absoluteTimer","seconds","message","lastProgress","total","Error","payload","onProgress","getRenderJob","resolveRenderJob","value","rejectRenderJob","recordRenderProgress","current","isRenderJobPending","__resetRegistryForTests","clear","getRenderJobTokenByJobId","TRACK_REGISTRY_TIMEOUT_MS","__libiTrackJobs","createTrackJob","getTrackJob","resolveTrackJob","rejectTrackJob","getTrackJobTokenByJobId","__resetTrackRegistryForTests","readFalApiKey","envVars","parsed","key","trim","FAL_BILLING_URL","FAL_ADMIN_SCOPE_HINT","FALLBACK_WINDOW_MS","SKEW_MARGIN_MS","billingWindowStart","generatedAt","NaN","base","Number","isFinite","toISOString","fetchFalCost","args","requestId","apiKey","URLSearchParams","request_id","start","toString","resp","headers","Authorization","kind","body","text","json","billing_events","cost_estimate_nano_usd","amountUsd","deletePieceCompletely","deleted","returning","storage","deletePieceDir","pexec","TRACKING_PYENV_TOKEN","TRACKING_PYENV_SENTINEL","modelsManifestPath","readModelsManifest","p","raw","readFileSync","Array","isArray","sha256File","createHash","digest","toLowerCase","tokenMarkerPath","readToken","writeToken","mkdirSync","dirname","recursive","writeFileSync","downloadVerified","sha256","dest","label","tmpPath","bytesDownloaded","actual","onRetry","attempt","maxAttempts","error","rmSync","force","renameSync","size","ensureModel","modelsDir","acquisition","existsSync","expected","buildInputsOf","manifest","build","source","find","m","sourceModelId","sourcePtSha256","textEncoderSha256","textEncoder","clipRequirement","classes","imgsz","opset","readBuildInfo","destPath","isBuildCurrent","inputs","output","parseExportResult","stdout","lines","split","i","line","startsWith","type","ensureBuiltModel","exec","sourcePt","encoderPath","file","encoderOk","proj","stderr","cwd","windowsHide","maxBuffer","UV_PROJECT_ENVIRONMENT","NO_COLOR","e","detail","result","outSha","sizeBytes","statSync","ultralytics","builtAt","runTrackingPyenvInstall","venv","uv","filter","trackingPyenvInstaller","verify","marker","install","command","CUSTOM","VERSION","listeners","notifyRegistryChanged","l","subscribeEffectsRegistry","listener","effectsRegistryVersion","registerCustomEffects","defs","clearCustomEffects","allEffects","meta","byId","findEffect","effectId","isKnownEffectId","listEffects","supports","includes","phase","phases","family","resolveEffect","IDENTITY_TRANSFORM3D","position","x","y","z","rotation","vecIsZero","isIdentityTransform3d","t","DEG_TO_RAD","PI","RAD_TO_DEG","degToRadVec3","radToDegVec3","wrap","angle","twoPi","a","applyOrbitDrag","dxPx","dyPx","sensitivity","applyPitchDrag","applyYawDrag","applyRollDrag","dPx","applyDepthDrag","withPosition","axis","withRotation","DEFAULT_DETENTS_RAD","d","snapAngleToDetents","angleRad","detentsRad","thresholdRad","best","bestDist","dist","abs","clampDepth","max","DEPTH_LIMIT_WORLD","DEPTH_LIMIT_PX","clampDepthForKind","lim","getServerUrl","port","send","notify","navigate","refreshQuery","refreshMcpConfig","instructionsChanged","analysisChanged","navigateSettings","mcpId","rightRegion","highlight","highlightEffect","setComplexityMode","overlayId","mode","jobProgress","notifier","bindNotifier","__setTestNotifier","pushIfBackgrounded","isFocused","resolveUvBinary","bundled","lookup","encoding","candidate","requireUvBinary","ErrorCtor","featureLabel","globalForEditor","getOpenedPieceId","__openedPieceId","setOpenedPieceId","uvPath","sidecarProjectDir","trackingModelsDir","TOKEN_MARKER","trackingEngineInstalled","trackingNotInstalledError","data","hint","installPlanPath"],"mappings":"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"}}]
6
+ {"offset": {"line": 1, "column": 0}, "map": {"version":3,"sources":["../../../lib/analytics/config.ts","../../../node_modules/src/sql/functions/aggregate.ts","../../../lib/analytics/events.ts","../../../lib/analytics/queue.ts","../../../lib/analytics/server.ts","../../../lib/proxy/lifecycle.ts","../../../lib/filmstrip/lifecycle.ts","../../../lib/export/render-jobs.ts","../../../lib/tracking/track-jobs.ts","../../../lib/analysis/script-providers/fal-api-key.ts","../../../lib/fal/billing.ts","../../../lib/pieces/delete-piece.ts","../../../mcp/registry/installers/tracking-pyenv.ts","../../../lib/effects/registry.ts","../../../lib/overlays/transform3d.ts","../../../lib/engine/overlay-transform.ts","../../../mcp/notify.ts","../../../lib/whisper/transcribe.ts","../../../lib/uv-path.ts","../../../lib/uv-env/hash-spec.ts","../../../lib/uv-env/install-token.ts","../../../lib/editor-state.ts","../../../lib/tracking/engine-deps.ts","../../../lib/tracking/not-installed.ts"],"sourcesContent":["// 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","/**\n * Proxy-file lifecycle helpers — drop the on-disk proxy and reset the\n * `files` row's proxy_* columns.\n *\n * Used by consumers that need to discard an existing proxy (file deletion,\n * proxy-drop tools) without scheduling a new job. Proxy GENERATION goes\n * through the JobManager via the `proxy_gen` runner — this module\n * intentionally does NOT enqueue anything.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { eq } from \"drizzle-orm\";\nimport { getDb } from \"@/lib/db/client\";\nimport { files } from \"@/lib/db/schema/sqlite\";\nimport { getLibiStorageDir } from \"@/lib/libi-home\";\nimport { proxyLogger as logger } from \"@/lib/logger\";\nimport { navigationEmitter } from \"@/lib/navigation-events\";\n\n/**\n * Best-effort: unlink the proxy file for a `fileId` (if one exists on disk)\n * and clear the row's proxy_* columns. Idempotent — safe to call on rows\n * whose proxy was never generated or already dropped.\n *\n * Does NOT remove the source file or the `files` row itself. Callers\n * responsible for those (delete-file.ts handles full file removal).\n */\nexport function dropProxyFile(\n fileId: string,\n reason: \"user\" | \"lru\" | \"delete\" | \"alpha_backfill\" = \"user\",\n): void {\n const db = getDb();\n const [row] = db.select().from(files).where(eq(files.id, fileId)).limit(1).all();\n if (!row) return;\n if (row.proxyStatus === \"idle\" && !row.proxyFilename) return;\n\n logger.info({ fileId, event: \"drop\", reason }, \"proxy.drop\");\n\n if (row.proxyFilename) {\n const proxyPath = path.join(\n getLibiStorageDir(),\n row.pieceId ?? \"_global\",\n row.proxyFilename,\n );\n try {\n fs.unlinkSync(proxyPath);\n } catch {\n /* ignore — already gone is fine */\n }\n }\n\n db.update(files)\n .set({ proxyFilename: null, proxyStatus: \"idle\", proxyGeneratedAt: null })\n .where(eq(files.id, fileId))\n .run();\n\n if (row.pieceId) {\n navigationEmitter.emit(\"refresh_query\", {\n queryKey: \"piece\",\n pieceId: row.pieceId,\n });\n } else {\n navigationEmitter.emit(\"refresh_query\", { queryKey: \"files\" });\n }\n}\n","/**\n * Filmstrip-file lifecycle helpers — drop the on-disk sprite and reset the\n * `files` row's filmstrip_* columns. Mirrors `lib/proxy/lifecycle.ts`.\n *\n * Used by consumers that need to discard an existing sprite (file deletion,\n * LRU eviction) without scheduling a new job. Filmstrip GENERATION goes\n * through the JobManager via the `filmstrip_gen` runner — this module\n * intentionally does NOT enqueue anything.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { eq } from \"drizzle-orm\";\nimport { getDb } from \"@/lib/db/client\";\nimport { files } from \"@/lib/db/schema/sqlite\";\nimport { getLibiStorageDir } from \"@/lib/libi-home\";\nimport { proxyLogger as logger } from \"@/lib/logger\";\nimport { navigationEmitter } from \"@/lib/navigation-events\";\n\n/**\n * Best-effort: unlink the filmstrip sprite for a `fileId` (if one exists on\n * disk) and clear the row's filmstrip_* columns. Idempotent — safe to call on\n * rows whose sprite was never generated or already dropped.\n *\n * Does NOT remove the source file or the `files` row itself.\n */\nexport function dropFilmstripFile(\n fileId: string,\n reason: \"user\" | \"lru\" | \"delete\" = \"user\",\n): void {\n const db = getDb();\n const [row] = db.select().from(files).where(eq(files.id, fileId)).limit(1).all();\n if (!row) return;\n if (row.filmstripStatus === \"idle\" && !row.filmstripFilename) return;\n\n logger.info({ fileId, event: \"drop\", reason, tag: \"filmstrip\" }, \"filmstrip.drop\");\n\n if (row.filmstripFilename) {\n const filmstripPath = path.join(\n getLibiStorageDir(),\n row.pieceId ?? \"_global\",\n row.filmstripFilename,\n );\n try {\n fs.unlinkSync(filmstripPath);\n } catch {\n /* ignore — already gone is fine */\n }\n }\n\n db.update(files)\n .set({\n filmstripFilename: null,\n filmstripStatus: \"idle\",\n filmstripGeneratedAt: null,\n filmstripFrames: null,\n filmstripHeight: null,\n })\n .where(eq(files.id, fileId))\n .run();\n\n if (row.pieceId) {\n navigationEmitter.emit(\"refresh_query\", {\n queryKey: \"piece\",\n pieceId: row.pieceId,\n });\n } else {\n navigationEmitter.emit(\"refresh_query\", { queryKey: \"files\" });\n }\n}\n","/**\n * NOTE: This is the in-process page→server callback bus for the Chromium\n * render page. The render page POSTs results back to\n * `/api/export/render-result/...` which calls `resolveRenderJob` here, and\n * errors land via `/api/export/render-error/...` calling `rejectRenderJob`.\n *\n * The job-tracking lifecycle (queue, dedupe, cancel, status, progress) is\n * owned by the generic JobManager (`lib/jobs/manager.ts`) — the\n * `exportRenderRunner` (in `lib/jobs/runners/export-render.ts`) drives this\n * registry as an IPC implementation detail.\n *\n * Both will probably consolidate in a future refactor; for now they're\n * complementary.\n */\nimport { randomUUID } from \"node:crypto\";\nimport type { ExportSettings, Overlay, AudioClip } from \"@/lib/engine/types\";\nimport type { FileRecord } from \"@/lib/db/schema/types\";\n\n/**\n * Payload the `/render` page needs to reconstruct a runtime `Composition`.\n *\n * We deliberately do NOT store a hydrated `Composition` here. Functions (the\n * compiled `scene.draw` callables) don't survive `JSON.stringify`, so anything\n * the render page fetches from `/api/export/render-job/[jobId]` has to be raw\n * data that the client re-hydrates via `buildComposition(...)`.\n */\nexport interface RenderPayload {\n overlays: Overlay[];\n audioClips: AudioClip[];\n width: number;\n height: number;\n fps: number;\n files: FileRecord[];\n /**\n * Total composition frame count (`getCompositionFrames`). Set by the chromium\n * route; the runner uses it to plan chunks. Absent on old persisted jobs →\n * the runner falls back to the single-page path.\n */\n totalFrames?: number;\n /**\n * When present, the render page renders only this sub-range (one chunk of a\n * chunked parallel render). The chunk file starts at t=0. Absent → full\n * composition.\n */\n frameRange?: { startFrame: number; endFrameExclusive: number };\n}\n\nexport interface RenderJobInit {\n pieceId: string;\n payload: RenderPayload;\n settings: ExportSettings;\n /** Overrides the absolute runaway cap (`RENDER_ABSOLUTE_CAP_MS`). Test seam. */\n timeoutMs?: number;\n /** Overrides the sliding stall timeout (`RENDER_STALL_TIMEOUT_MS`). Test seam. */\n stallTimeoutMs?: number;\n /** Optional callback fired when the in-page render bundle reports per-frame\n * progress via /api/export/render-progress. */\n onProgress?: (done: number, total: number) => void;\n}\n\nexport interface RenderJobSuccess {\n tempFilePath: string;\n durationSeconds: number;\n}\n\nexport interface RenderJobHandle {\n jobId: string;\n token: string;\n done: Promise<RenderJobSuccess>;\n}\n\ninterface JobEntry {\n jobId: string;\n token: string;\n pieceId: string;\n payload: RenderPayload;\n settings: ExportSettings;\n resolve: (value: RenderJobSuccess) => void;\n reject: (err: Error) => void;\n /** Sliding stall timer — reset on every progress tick. */\n stallTimer: NodeJS.Timeout;\n /** Runaway backstop — never reset. */\n absoluteTimer: NodeJS.Timeout;\n /** Effective stall duration, so a progress tick can re-arm identically. */\n stallTimeoutMs: number;\n /** Latest progress seen, used to build the stall-timeout message. */\n lastProgress: { done: number; total: number } | null;\n settled: boolean;\n onProgress?: (done: number, total: number) => void;\n}\n\n/**\n * Sliding stall timeout: a render is only killed if NO per-frame progress POST\n * arrives within this window. Armed at `createRenderJob` (covers page boot +\n * hydrate) and reset on every `recordRenderProgress` tick — a healthy render at\n * any fps stays alive indefinitely.\n */\nexport const RENDER_STALL_TIMEOUT_MS = 2 * 60 * 1000;\n\n/**\n * Runaway backstop: never reset. Even a render that keeps reporting progress is\n * killed once total wall time exceeds this cap.\n */\nexport const RENDER_ABSOLUTE_CAP_MS = 60 * 60 * 1000;\n\n/**\n * The registry must survive Next.js dev HMR — the runner (JobManager\n * background work) writes here, and the route handler at\n * `/api/export/render-job/[jobId]` reads from here. In dev, route\n * handlers can be reloaded into a different module instance than the\n * one the runner is holding a reference to, so a plain module-level\n * `Map` shows up empty on the read side and the render page gets a\n * 404. Stash the singleton on globalThis (same trick lib/db/client.ts\n * uses for the DB handle) so all imports of this module resolve to the\n * same Map. Verified by e2e/electron/libi-home-and-export.spec.ts.\n */\nconst globalForRenderJobs = globalThis as unknown as {\n __libi_render_jobs?: Map<string, JobEntry>;\n};\nconst jobs: Map<string, JobEntry> =\n globalForRenderJobs.__libi_render_jobs ?? new Map<string, JobEntry>();\nif (!globalForRenderJobs.__libi_render_jobs) {\n globalForRenderJobs.__libi_render_jobs = jobs;\n}\n\nexport function createRenderJob(init: RenderJobInit): RenderJobHandle {\n const jobId = randomUUID();\n const token = randomUUID();\n let resolve!: (v: RenderJobSuccess) => void;\n let reject!: (e: Error) => void;\n const done = new Promise<RenderJobSuccess>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n const stallTimeoutMs = init.stallTimeoutMs ?? RENDER_STALL_TIMEOUT_MS;\n const absoluteCapMs = init.timeoutMs ?? RENDER_ABSOLUTE_CAP_MS;\n\n // Sliding stall timer — reset on every progress tick (see recordRenderProgress).\n const stallTimer = setTimeout(() => {\n const entry = jobs.get(jobId);\n if (entry && !entry.settled) {\n entry.settled = true;\n clearTimeout(entry.absoluteTimer);\n jobs.delete(jobId);\n const seconds = stallTimeoutMs / 1000;\n const message = entry.lastProgress\n ? `Render job ${jobId} stalled — no progress for ${seconds}s (frame ${entry.lastProgress.done}/${entry.lastProgress.total})`\n : `Render job ${jobId} stalled — no progress reported within ${seconds}s`;\n reject(new Error(message));\n }\n }, stallTimeoutMs);\n\n // Runaway backstop — never reset.\n const absoluteTimer = setTimeout(() => {\n const entry = jobs.get(jobId);\n if (entry && !entry.settled) {\n entry.settled = true;\n clearTimeout(entry.stallTimer);\n jobs.delete(jobId);\n reject(\n new Error(\n `Render job ${jobId} exceeded the ${absoluteCapMs / 60000}-minute absolute cap`,\n ),\n );\n }\n }, absoluteCapMs);\n\n jobs.set(jobId, {\n jobId,\n token,\n pieceId: init.pieceId,\n payload: init.payload,\n settings: init.settings,\n resolve,\n reject,\n stallTimer,\n absoluteTimer,\n stallTimeoutMs,\n lastProgress: null,\n settled: false,\n onProgress: init.onProgress,\n });\n return { jobId, token, done };\n}\n\nexport function getRenderJob(jobId: string, token: string): JobEntry | null {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled || entry.token !== token) return null;\n return entry;\n}\n\n// Note: a timeout firing between getRenderJobTokenByJobId() and resolveRenderJob()\n// is harmless — this function re-checks entry.settled inside the critical section.\n// The worst case is that a driver-timed postback gets a false return value and the\n// client has already received the timeout rejection.\nexport function resolveRenderJob(jobId: string, token: string, value: RenderJobSuccess): boolean {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled || entry.token !== token) return false;\n entry.settled = true;\n clearTimeout(entry.stallTimer);\n clearTimeout(entry.absoluteTimer);\n jobs.delete(jobId);\n entry.resolve(value);\n return true;\n}\n\nexport function rejectRenderJob(jobId: string, token: string, message: string): boolean {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled || entry.token !== token) return false;\n entry.settled = true;\n clearTimeout(entry.stallTimer);\n clearTimeout(entry.absoluteTimer);\n jobs.delete(jobId);\n entry.reject(new Error(message));\n return true;\n}\n\n/** Called by the in-page render bundle to report per-frame progress.\n * Token-validated like the result endpoint. Stashes the latest progress and\n * resets the sliding stall timer so a healthy render never times out. */\nexport function recordRenderProgress(\n jobId: string,\n token: string,\n done: number,\n total: number,\n): void {\n const entry = jobs.get(jobId);\n if (!entry) return;\n if (entry.token !== token) return;\n if (entry.settled) return;\n entry.lastProgress = { done, total };\n clearTimeout(entry.stallTimer);\n entry.stallTimer = setTimeout(() => {\n const current = jobs.get(jobId);\n if (current && !current.settled) {\n current.settled = true;\n clearTimeout(current.absoluteTimer);\n jobs.delete(jobId);\n const seconds = current.stallTimeoutMs / 1000;\n const message = current.lastProgress\n ? `Render job ${jobId} stalled — no progress for ${seconds}s (frame ${current.lastProgress.done}/${current.lastProgress.total})`\n : `Render job ${jobId} stalled — no progress reported within ${seconds}s`;\n current.reject(new Error(message));\n }\n }, entry.stallTimeoutMs);\n entry.onProgress?.(done, total);\n}\n\n/** True while the job exists and has not settled. Driver-side polling seam. */\nexport function isRenderJobPending(jobId: string): boolean {\n const entry = jobs.get(jobId);\n return !!entry && !entry.settled;\n}\n\nexport function __resetRegistryForTests() {\n for (const entry of jobs.values()) {\n clearTimeout(entry.stallTimer);\n clearTimeout(entry.absoluteTimer);\n }\n jobs.clear();\n}\n\n/**\n * Returns the token for an unsettled job, without requiring the caller to know it.\n *\n * Driver-side only — drivers run in the trusted server context and need the token\n * to construct the `/render?jobId=X&token=Y` URL. Also used by tests to settle\n * jobs whose tokens the test didn't generate.\n *\n * Do NOT call this from untrusted surfaces (API handlers, render page). Those\n * must use `getRenderJob(jobId, token)` with an explicit token.\n */\nexport function getRenderJobTokenByJobId(jobId: string): { token: string } | null {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled) return null;\n return { token: entry.token };\n}\n","/**\n * NOTE: This is the page→server IPC bridge for `track-entry.ts` (running\n * inside Playwright Chromium) to post results back to `runFaceTracker` /\n * `runObjectTracker` via HTTP. It is NOT the same as the JobManager\n * (`lib/jobs/manager.ts`) which owns the job lifecycle.\n *\n * The split:\n * - JobManager — runs the runner, owns persistence, checkpoints, cancel.\n * - track-jobs (this file) — bridges the in-browser track-entry's `fetch`\n * calls back to the awaiting Promise inside the server-side runner.\n *\n * Both will probably consolidate in a future refactor; for now they're\n * complementary.\n */\nimport { randomUUID } from \"node:crypto\";\nimport type { TrackMethod, TrackSample } from \"@/lib/tracking/types\";\n\nexport interface TrackPayload {\n fileId: string;\n /** URL the browser will use to load the source video (e.g. /api/files/by-id/{id}/content). */\n fileUrl: string;\n fps: number;\n objectKind: \"face\" | \"object\";\n subjectQuery?: string;\n}\n\nexport interface TrackJobSuccess {\n samples: TrackSample[];\n framerate: number;\n method: TrackMethod;\n}\n\nexport interface TrackJobInit {\n pieceId: string;\n payload: TrackPayload;\n timeoutMs?: number;\n}\n\nexport interface TrackJobHandle {\n jobId: string;\n token: string;\n done: Promise<TrackJobSuccess>;\n}\n\ninterface JobEntry {\n jobId: string;\n token: string;\n pieceId: string;\n payload: TrackPayload;\n resolve: (value: TrackJobSuccess) => void;\n reject: (err: Error) => void;\n timeout: NodeJS.Timeout;\n settled: boolean;\n}\n\nexport const TRACK_REGISTRY_TIMEOUT_MS = 10 * 60 * 1000; // 10 min — tracking can be slow on long videos\n\n/**\n * Module-level state must survive Next.js dev-mode module fragmentation —\n * route handlers and the server-side runner can otherwise resolve to\n * different instances of this file and see different Maps. globalThis\n * caching keeps them sharing one registry.\n */\ndeclare global {\n var __libiTrackJobs: Map<string, JobEntry> | undefined;\n}\nconst jobs: Map<string, JobEntry> = globalThis.__libiTrackJobs ?? new Map<string, JobEntry>();\nglobalThis.__libiTrackJobs = jobs;\n\nexport function createTrackJob(init: TrackJobInit): TrackJobHandle {\n const jobId = randomUUID();\n const token = randomUUID();\n let resolve!: (v: TrackJobSuccess) => void;\n let reject!: (e: Error) => void;\n const done = new Promise<TrackJobSuccess>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n const timeout = setTimeout(() => {\n const entry = jobs.get(jobId);\n if (entry && !entry.settled) {\n entry.settled = true;\n jobs.delete(jobId);\n reject(new Error(`Track job ${jobId} timeout`));\n }\n }, init.timeoutMs ?? TRACK_REGISTRY_TIMEOUT_MS);\n jobs.set(jobId, {\n jobId,\n token,\n pieceId: init.pieceId,\n payload: init.payload,\n resolve,\n reject,\n timeout,\n settled: false,\n });\n return { jobId, token, done };\n}\n\nexport function getTrackJob(jobId: string, token: string): JobEntry | null {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled || entry.token !== token) return null;\n return entry;\n}\n\nexport function resolveTrackJob(\n jobId: string,\n token: string,\n value: TrackJobSuccess,\n): boolean {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled || entry.token !== token) return false;\n entry.settled = true;\n clearTimeout(entry.timeout);\n jobs.delete(jobId);\n entry.resolve(value);\n return true;\n}\n\nexport function rejectTrackJob(jobId: string, token: string, message: string): boolean {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled || entry.token !== token) return false;\n entry.settled = true;\n clearTimeout(entry.timeout);\n jobs.delete(jobId);\n entry.reject(new Error(message));\n return true;\n}\n\nexport function getTrackJobTokenByJobId(jobId: string): { token: string } | null {\n const entry = jobs.get(jobId);\n if (!entry || entry.settled) return null;\n return { token: entry.token };\n}\n\nexport function __resetTrackRegistryForTests() {\n for (const entry of jobs.values()) clearTimeout(entry.timeout);\n jobs.clear();\n}\n","import { eq } from \"drizzle-orm\";\nimport { getDb } from \"@/lib/db/client\";\nimport { mcpServers } from \"@/lib/db/schema/sqlite\";\n\n/**\n * Reads the fal.ai API key from the mcp_servers[\"fal-ai\"] row's env_vars JSON.\n *\n * Returns null when:\n * - the row is missing\n * - env_vars is unset\n * - FAL_KEY is absent, not a string, or empty\n * - env_vars contains malformed JSON\n */\nexport async function readFalApiKey(): Promise<string | null> {\n const db = getDb();\n const [row] = db\n .select({ envVars: mcpServers.envVars })\n .from(mcpServers)\n .where(eq(mcpServers.id, \"fal-ai\"))\n .limit(1)\n .all();\n\n if (!row?.envVars) return null;\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(row.envVars) as Record<string, unknown>;\n } catch {\n return null;\n }\n\n const key = parsed[\"FAL_KEY\"];\n if (typeof key !== \"string\" || key.trim() === \"\") return null;\n return key.trim();\n}\n","/**\n * Shared fal.ai billing-events helper.\n *\n * Both the script-analysis cost-refresh path (script-providers/fal-video-understanding.ts)\n * and the AI-generation cost-refresh path (api/files/.../generation/refresh-cost) hit\n * fal's platform billing API to resolve actual cost-after-completion.\n * One helper, two call sites.\n *\n * Endpoint: https://api.fal.ai/v1/models/billing-events?request_id=<id>&start=<iso>\n * Auth: Authorization: Key <FAL_KEY> — and the key MUST be ADMIN-scoped.\n * API-scoped keys (the kind that runs generations) get 403\n * `authorization_error` here; ADMIN scope is a superset (\"everything\n * in API, plus…\"), so one ADMIN key can serve both generation and\n * billing lookups. Verified live 2026-07-04.\n * Window: `start` DEFAULTS TO 24 HOURS AGO on fal's side — without an\n * explicit `start`, any request older than a day returns zero\n * events and looks forever-pending. Callers pass the generation\n * timestamp; we widen it by an hour of clock-skew margin.\n * Response: { billing_events: [{ cost_estimate_nano_usd: number, ... }] }\n *\n * A literal 0 is \"not yet populated\" (fal occasionally returns the row with\n * cost_estimate_nano_usd === 0 before the real charge lands) → `pending`.\n *\n * Docs: https://fal.ai/docs/platform-apis/v1/models/billing-events.md\n */\n\nimport type { CostFetchOutcome } from \"@/lib/providers/cost-outcome\";\n\nexport const FAL_BILLING_URL = \"https://api.fal.ai/v1/models/billing-events\";\n\n/** User-facing explanation for the permanent 403 case. */\nexport const FAL_ADMIN_SCOPE_HINT =\n \"The configured fal.ai API key can't read billing data — fal's billing API requires an ADMIN-scoped key. Create one at fal.ai → Dashboard → Keys (scope: ADMIN — it runs generations too) and update the FAL_KEY on the fal-ai MCP server in Settings.\";\n\nconst FALLBACK_WINDOW_MS = 90 * 24 * 60 * 60 * 1000; // 90 days\n// Generous on purpose: stored generation timestamps aren't trustworthy —\n// real rows exist whose completedAt is local wall-clock mislabeled as UTC\n// (3h ahead of the billing event's true UTC timestamp). The query is\n// request_id-filtered, so a wide window can't match the wrong event; it only\n// bounds fal's scan.\nconst SKEW_MARGIN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days\n\n/** fal's `start` defaults to 24h ago; compute an explicit window start from\n * the generation timestamp (or a wide fallback when it's missing/invalid). */\nexport function billingWindowStart(\n generatedAt: string | undefined,\n now: number = Date.now(),\n): string {\n const parsed = generatedAt ? Date.parse(generatedAt) : NaN;\n const base = Number.isFinite(parsed) ? parsed : now - FALLBACK_WINDOW_MS;\n return new Date(base - SKEW_MARGIN_MS).toISOString();\n}\n\nexport async function fetchFalCost(args: {\n requestId: string;\n apiKey: string;\n /** ISO timestamp of the generation; drives the billing query's `start`. */\n generatedAt?: string;\n signal?: AbortSignal;\n}): Promise<CostFetchOutcome> {\n const { requestId, apiKey, generatedAt, signal } = args;\n const params = new URLSearchParams({\n request_id: requestId,\n start: billingWindowStart(generatedAt),\n });\n const url = `${FAL_BILLING_URL}?${params.toString()}`;\n\n let resp: Response;\n try {\n resp = await fetch(url, {\n headers: { Authorization: `Key ${apiKey}` },\n signal,\n });\n } catch (err) {\n return {\n kind: \"error\",\n message: `fal billing request failed: ${(err as Error).message}`,\n };\n }\n\n if (resp.status === 401 || resp.status === 403) {\n return { kind: \"forbidden\", message: FAL_ADMIN_SCOPE_HINT };\n }\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n return {\n kind: \"error\",\n status: resp.status,\n message: `fal billing API returned HTTP ${resp.status}: ${body.slice(0, 200)}`,\n };\n }\n\n const body = (await resp.json().catch(() => null)) as {\n billing_events?: Array<{ cost_estimate_nano_usd?: number }>;\n } | null;\n const event = body?.billing_events?.[0];\n if (!event || typeof event.cost_estimate_nano_usd !== \"number\") {\n return { kind: \"pending\" };\n }\n if (event.cost_estimate_nano_usd === 0) {\n return { kind: \"pending\" };\n }\n return { kind: \"resolved\", amountUsd: event.cost_estimate_nano_usd / 1_000_000_000 };\n}\n","import { eq } from \"drizzle-orm\";\nimport { getDb } from \"@/lib/db/client\";\nimport { pieces } from \"@/lib/db/schema/sqlite\";\nimport { getStorage } from \"@/lib/storage\";\nimport { removeAnalysisForPiece } from \"@/lib/analysis/cleanup\";\nimport { navigationEmitter } from \"@/lib/navigation-events\";\n\n/**\n * Fully delete a piece: DB row (files/assets/etc. cascade), on-disk storage\n * directory, and analysis byproducts. Emits refresh_query. Returns false if\n * the piece did not exist.\n */\nexport async function deletePieceCompletely(pieceId: string): Promise<boolean> {\n const db = getDb();\n const [deleted] = await db.delete(pieces).where(eq(pieces.id, pieceId)).returning();\n if (!deleted) return false;\n\n const storage = await getStorage();\n removeAnalysisForPiece(pieceId);\n await storage.deletePieceDir(pieceId);\n\n navigationEmitter.emit(\"refresh_query\", { queryKey: \"pieces\" });\n navigationEmitter.emit(\"refresh_query\", { queryKey: \"piece\", pieceId });\n return true;\n}\n","/**\n * Custom installer for the local tracking engine (Task 11).\n *\n * Two responsibilities, run as one `install()`:\n * 1. `uv sync` the uv-managed Python sidecar project at\n * `mcp/tracking/py/` so onnxruntime / boxmot / opencv / torch are\n * resolved into its `.venv` (creating it on first run).\n * 2. Ensure the tracking model artifacts declared in\n * `mcp/tracking/py/models.json` are present under\n * `~/.libi/models/tracking/` (= `trackingModelsDir()`) — sha-pinned\n * direct downloads plus the locally-exported yoloe11 ONNX.\n *\n * The `CustomInstaller` interface in `mcp/registry/types.ts` is a static\n * `{ verify, install: { command, args } }` spec spawned via `execFile`.\n * That cannot express this multi-step flow, so — exactly like the\n * `yt-dlp-uv` precedent in `mcp/registry/installers.ts` — the spec uses a\n * sentinel `command` (`__TRACKING_PYENV_INSTALL__`) that\n * `DependencyManager.runCustomInstaller` special-cases to call\n * `runTrackingPyenvInstall()` here.\n *\n * Model acquisition handles BOTH kinds declared in models.json\n * (`acquisition`):\n * - `direct-download`: if the dest file is missing OR its sha256\n * differs from models.json, download from `url` and verify sha256\n * (hard-fail on mismatch).\n * - `build-at-install`: the artifact is EXPORTED ON THE USER'S MACHINE\n * from sha-pinned inputs declared in the entry's `build` spec (source\n * .pt + text encoder + CLIP commit archive). The output sha256 is\n * host-specific (the ONNX export embeds non-deterministic metadata), so\n * integrity is anchored on the INPUTS: a `<dest>.build-info.json` marker\n * records the exact inputs + the produced output sha, and the build is\n * skipped iff the dest hashes to the recorded output AND the recorded\n * inputs equal the manifest's current inputs. Building locally (owner\n * decision, 2026-08-01) also avoids libi redistributing an AGPL-derived\n * binary — ultralytics, the YOLOE weights, and the ultralytics/CLIP fork\n * are all AGPL-3.0, while libi is GPL-3.0.\n *\n * This file is server-only (imports `fs`, spawns processes). It is\n * referenced from `installers.ts` (also server-only) so it never reaches\n * the client bundle that imports `bundled.ts` via the Settings UI.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport crypto from \"node:crypto\";\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport { buildUvEnv, trackingVenvDir } from \"@/lib/uv-env/spawn-env\";\nimport { downloadToFileWithStallGuard } from \"@/lib/security/download-verify\";\nimport {\n uvPath,\n sidecarProjectDir,\n trackingModelsDir,\n} from \"@/lib/tracking/engine-deps\";\nimport { serverLogger as logger } from \"@/lib/logger\";\nimport type { CustomInstaller } from \"../types\";\n\nconst pexec = promisify(execFile);\n\n/** Bump when changing the sidecar pin set or model manifest. Mirrors the BundledDependency.pinnedInstallToken date convention. */\n/*\n * Bumped 2026-08-01: yoloe11.onnx moved from reproduce-at-build (manual\n * export, blocked every clean install) to build-at-install (the installer\n * exports it unattended from sha-pinned inputs), and the dead `osnet_reid`\n * ONNX entry was removed (boxmot's runtime PyTorch ReID backend was already\n * the shipped behaviour). The bump makes existing installs run the new\n * model phase once (idempotent: an already-built yoloe11.onnx without a\n * build-info marker is rebuilt once, then skipped on every later run).\n */\nexport const TRACKING_PYENV_TOKEN = \"tracking-pyenv@2026-08-01-yoloe-build-at-install\";\n\n/** Sentinel `command`; DependencyManager.runCustomInstaller dispatches it. */\nexport const TRACKING_PYENV_SENTINEL = \"__TRACKING_PYENV_INSTALL__\";\n\nexport interface TextEncoderSpec {\n /** Path RELATIVE to the models dir (e.g. \".build/mobileclip_blt.ts\"). */\n file: string;\n url: string;\n sha256: string;\n sizeBytes: number;\n}\n\n/** Declared inputs of a `build-at-install` export (models.json `build`). */\nexport interface BuildSpec {\n /** models.json id of the sha-pinned source checkpoint (e.g. \"yoloe_vp\"). */\n sourceModelId: string;\n classes: string[];\n imgsz: number;\n opset: number;\n /** PEP 508 requirement for the export-only CLIP tokenizer package, pinned\n * to an immutable commit-archive URL. Supplied via `uv run --with` so the\n * locked sidecar venv is never mutated. */\n clipRequirement: string;\n textEncoder: TextEncoderSpec;\n}\n\nexport interface ModelEntry {\n id: string;\n file: string;\n url: string;\n sha256: string;\n sizeBytes: number;\n dest: string;\n acquisition: \"direct-download\" | \"build-at-install\";\n build?: BuildSpec;\n}\n\n/** The exact input pins a built artifact was produced from. Persisted in the\n * build-info marker and compared against the manifest to decide rebuilds. */\nexport interface BuildInputs {\n sourcePtSha256: string;\n textEncoderSha256: string;\n clipRequirement: string;\n classes: string[];\n imgsz: number;\n opset: number;\n}\n\nexport interface BuildInfo {\n inputs: BuildInputs;\n output: { sha256: string; sizeBytes: number; ultralytics?: string };\n builtAt: string;\n}\n\nfunction modelsManifestPath(): string {\n return path.join(sidecarProjectDir(), \"models.json\");\n}\n\nfunction readModelsManifest(): ModelEntry[] {\n const p = modelsManifestPath();\n const raw = fs.readFileSync(p, \"utf-8\");\n const parsed = JSON.parse(raw) as ModelEntry[];\n if (!Array.isArray(parsed)) {\n throw new Error(`models.json at ${p} is not a JSON array`);\n }\n return parsed;\n}\n\nfunction sha256File(p: string): string {\n return crypto\n .createHash(\"sha256\")\n .update(fs.readFileSync(p))\n .digest(\"hex\")\n .toLowerCase();\n}\n\n/** The token marker for the synced uv env + provisioned models. */\nfunction tokenMarkerPath(): string {\n return path.join(trackingModelsDir(), \".install-token\");\n}\n\nfunction readToken(): string | null {\n try {\n return fs.readFileSync(tokenMarkerPath(), \"utf-8\").trim();\n } catch {\n return null;\n }\n}\n\nfunction writeToken(): void {\n const p = tokenMarkerPath();\n fs.mkdirSync(path.dirname(p), { recursive: true });\n fs.writeFileSync(p, TRACKING_PYENV_TOKEN, \"utf-8\");\n}\n\n/**\n * Download `url` to `dest`, streaming to disk while hashing (the mobileclip\n * text encoder is ~572MB — never buffer whole files in memory). Verifies the\n * sha256 and renames atomically so a partial download never looks present.\n */\nexport async function downloadVerified(\n url: string,\n sha256: string,\n dest: string,\n label: string,\n): Promise<void> {\n fs.mkdirSync(path.dirname(dest), { recursive: true });\n logger.info(\n { tag: \"tracking-pyenv\", op: \"model_download_start\", id: label, url },\n `downloading ${label}`,\n );\n // Stall-guarded + retrying + HTTPS-only-redirect, streaming to disk.\n //\n // This used to be a bare `fetch(url, { redirect: \"follow\" })`. Measured\n // 2026-08-02 on a real clean install: the 572 MB MobileCLIP encoder stalled\n // mid-body, undici's default body timeout fired as UND_ERR_BODY_TIMEOUT, and\n // the whole tracking provision died with an unhandled stack trace and no\n // retry — the SAME failure #54 fixed for binary deps, on the path that pulls\n // by far the largest artifacts. Buffering is not an option at this size, so\n // this streams and hashes as it writes.\n const { tmpPath, bytesDownloaded, sha256: actual } =\n await downloadToFileWithStallGuard(url, dest, {\n label,\n onRetry: (info) => {\n logger.warn(\n {\n tag: \"tracking-pyenv\",\n op: \"model_download_retry\",\n id: label,\n url,\n attempt: info.attempt,\n maxAttempts: info.maxAttempts,\n bytesDownloaded: info.bytesDownloaded,\n err: info.error,\n },\n `${label} download failed (attempt ${info.attempt}/${info.maxAttempts}), retrying`,\n );\n },\n });\n\n if (actual !== sha256.toLowerCase()) {\n fs.rmSync(tmpPath, { force: true });\n throw new Error(\n `sha256 mismatch for ${label}: expected ${sha256.toLowerCase()}, ` +\n `got ${actual} (downloaded from ${url})`,\n );\n }\n fs.renameSync(tmpPath, dest);\n logger.info(\n {\n tag: \"tracking-pyenv\",\n op: \"model_download_done\",\n id: label,\n dest,\n size: bytesDownloaded,\n },\n `downloaded ${label} (${bytesDownloaded} bytes, sha ok)`,\n );\n}\n\n/**\n * Ensure a single `direct-download` model artifact is on disk.\n * Returns nothing; throws an actionable error on any unrecoverable state.\n * `build-at-install` entries go through {@link ensureBuiltModel} instead.\n */\nexport async function ensureModel(\n entry: ModelEntry,\n modelsDir: string,\n): Promise<void> {\n if (entry.acquisition !== \"direct-download\") {\n throw new Error(\n `ensureModel only handles direct-download entries; ${entry.id} is ` +\n `${entry.acquisition} — route it through ensureBuiltModel`,\n );\n }\n const dest = path.join(modelsDir, entry.dest);\n\n // present-and-matching short-circuits the fetch.\n if (fs.existsSync(dest)) {\n const actual = sha256File(dest);\n if (actual === entry.sha256.toLowerCase()) {\n logger.info(\n { tag: \"tracking-pyenv\", op: \"model_present\", id: entry.id, dest },\n `tracking model ${entry.id} present (sha matches)`,\n );\n return;\n }\n logger.warn(\n {\n tag: \"tracking-pyenv\",\n op: \"model_sha_mismatch\",\n id: entry.id,\n dest,\n expected: entry.sha256.toLowerCase(),\n actual,\n },\n `tracking model ${entry.id} sha mismatch — re-downloading`,\n );\n }\n\n // dest may be nested (e.g. \"matanyone/model.safetensors\") — downloadVerified\n // creates the full parent chain, not just the models root.\n await downloadVerified(entry.url, entry.sha256, dest, entry.id);\n}\n\n/**\n * Resolve the declared build inputs of a `build-at-install` entry against the\n * manifest (pure — exported for unit tests). The result is what gets recorded\n * in the build-info marker and compared to decide whether a rebuild is needed.\n */\nexport function buildInputsOf(\n entry: ModelEntry,\n manifest: ModelEntry[],\n): BuildInputs {\n const build = entry.build;\n if (!build) {\n throw new Error(`model ${entry.id} is build-at-install but has no build spec`);\n }\n const source = manifest.find((m) => m.id === build.sourceModelId);\n if (!source) {\n throw new Error(\n `model ${entry.id} build.sourceModelId \"${build.sourceModelId}\" not in models.json`,\n );\n }\n return {\n sourcePtSha256: source.sha256.toLowerCase(),\n textEncoderSha256: build.textEncoder.sha256.toLowerCase(),\n clipRequirement: build.clipRequirement,\n classes: [...build.classes],\n imgsz: build.imgsz,\n opset: build.opset,\n };\n}\n\nfunction readBuildInfo(destPath: string): BuildInfo | null {\n try {\n return JSON.parse(\n fs.readFileSync(`${destPath}.build-info.json`, \"utf-8\"),\n ) as BuildInfo;\n } catch {\n return null;\n }\n}\n\n/**\n * Is the built artifact at `destPath` current for `expected` inputs?\n * True iff dest exists, the build-info marker parses, the recorded inputs\n * deep-equal the manifest's current inputs, AND the file on disk still hashes\n * to the recorded output sha (guards a corrupted/partial file).\n */\nexport function isBuildCurrent(\n destPath: string,\n expected: BuildInputs,\n): boolean {\n if (!fs.existsSync(destPath)) return false;\n const info = readBuildInfo(destPath);\n if (!info?.inputs || !info.output?.sha256) return false;\n if (JSON.stringify(info.inputs) !== JSON.stringify(expected)) return false;\n return sha256File(destPath) === info.output.sha256.toLowerCase();\n}\n\ntype ExecFn = (\n cmd: string,\n args: string[],\n opts: Record<string, unknown>,\n) => Promise<{ stdout: string; stderr: string }>;\n\n/** Scan stdout lines from the END for the export script's protocol JSON\n * (`{\"type\":\"export\",...}`), skipping ultralytics/uv progress chatter. */\nexport function parseExportResult(\n stdout: string,\n): { ok: boolean; [k: string]: unknown } | null {\n const lines = stdout.split(\"\\n\");\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i].trim();\n if (!line.startsWith(\"{\")) continue;\n try {\n const parsed = JSON.parse(line) as { type?: string; ok?: boolean };\n if (parsed.type === \"export\") {\n return parsed as { ok: boolean; [k: string]: unknown };\n }\n } catch {\n /* not JSON — keep scanning */\n }\n }\n return null;\n}\n\n/**\n * Ensure a `build-at-install` artifact exists and is current, exporting it\n * on this machine when it isn't. All inputs are sha-pinned and verified\n * BEFORE the export runs; the export itself is validated by\n * `export_yoloe11.py` (onnxruntime tensor-shape check) and the result is\n * recorded in `<dest>.build-info.json` so later installs skip the work.\n *\n * `exec` is injectable for hermetic tests; production uses promisified\n * execFile.\n */\nexport async function ensureBuiltModel(\n entry: ModelEntry,\n manifest: ModelEntry[],\n modelsDir: string,\n exec: ExecFn = pexec as unknown as ExecFn,\n): Promise<void> {\n const build = entry.build;\n if (!build) {\n throw new Error(`model ${entry.id} is build-at-install but has no build spec`);\n }\n const dest = path.join(modelsDir, entry.dest);\n const expected = buildInputsOf(entry, manifest);\n\n if (isBuildCurrent(dest, expected)) {\n logger.info(\n { tag: \"tracking-pyenv\", op: \"model_present\", id: entry.id, dest },\n `tracking model ${entry.id} present (build-info current)`,\n );\n return;\n }\n\n // 1. The sha-pinned source checkpoint must already be provisioned (the\n // direct-download phase runs before the build phase).\n const source = manifest.find((m) => m.id === build.sourceModelId)!;\n const sourcePt = path.join(modelsDir, source.dest);\n if (!fs.existsSync(sourcePt)) {\n throw new Error(\n `cannot build ${entry.dest}: source checkpoint ${source.dest} is not ` +\n `provisioned at ${sourcePt} (the direct-download phase must run first)`,\n );\n }\n\n // 2. Ensure the export-only text encoder (sha-verified, cached under the\n // models dir so a rebuild after a token bump never re-downloads 572MB).\n const encoderPath = path.join(modelsDir, build.textEncoder.file);\n const encoderOk =\n fs.existsSync(encoderPath) &&\n sha256File(encoderPath) === build.textEncoder.sha256.toLowerCase();\n if (!encoderOk) {\n await downloadVerified(\n build.textEncoder.url,\n build.textEncoder.sha256,\n encoderPath,\n `${entry.id} text encoder`,\n );\n }\n\n // 3. Run the export inside the locked sidecar env, with the pinned CLIP\n // tokenizer supplied as an ephemeral `--with` overlay (the venv itself\n // is never mutated; uv caches the overlay under <LIBI_HOME>/uv/cache).\n const proj = sidecarProjectDir();\n const args = [\n \"run\",\n \"--locked\",\n \"--with\",\n build.clipRequirement,\n \"python\",\n \"export_yoloe11.py\",\n \"--pt\",\n sourcePt,\n \"--encoder\",\n encoderPath,\n \"--out\",\n dest,\n \"--classes\",\n build.classes.join(\",\"),\n \"--imgsz\",\n String(build.imgsz),\n \"--opset\",\n String(build.opset),\n ];\n logger.info(\n { tag: \"tracking-pyenv\", op: \"model_build_start\", id: entry.id, dest },\n `building tracking model ${entry.id} (local ONNX export)`,\n );\n let stdout = \"\";\n let stderr = \"\";\n try {\n ({ stdout, stderr } = await exec(uvPath(), args, {\n cwd: proj,\n windowsHide: true,\n timeout: 15 * 60_000, // first run resolves the CLIP overlay + exports\n maxBuffer: 32 * 1024 * 1024,\n env: buildUvEnv({\n UV_PROJECT_ENVIRONMENT: trackingVenvDir(),\n NO_COLOR: \"1\",\n }),\n }));\n } catch (e) {\n const err = e as Error & { stdout?: string; stderr?: string };\n const detail = (err.stderr ?? err.stdout ?? err.message ?? \"\").slice(-2000);\n throw new Error(\n `local ONNX export for ${entry.dest} failed: ${err.message}\\n${detail}`,\n );\n }\n const result = parseExportResult(stdout);\n if (!result || !result.ok) {\n const detail = result?.error ?? stderr.slice(-2000);\n throw new Error(\n `local ONNX export for ${entry.dest} did not succeed: ${String(detail)}`,\n );\n }\n if (!fs.existsSync(dest)) {\n throw new Error(\n `local ONNX export for ${entry.dest} reported ok but ${dest} is missing`,\n );\n }\n\n // 4. Record the build so later installs are a cheap hash + compare.\n const outSha = sha256File(dest);\n const info: BuildInfo = {\n inputs: expected,\n output: {\n sha256: outSha,\n sizeBytes: fs.statSync(dest).size,\n ultralytics:\n typeof result.ultralytics === \"string\" ? result.ultralytics : undefined,\n },\n builtAt: new Date().toISOString(),\n };\n fs.writeFileSync(`${dest}.build-info.json`, JSON.stringify(info, null, 2));\n logger.info(\n {\n tag: \"tracking-pyenv\",\n op: \"model_build_done\",\n id: entry.id,\n dest,\n sha256: outSha,\n sizeBytes: info.output.sizeBytes,\n },\n `built tracking model ${entry.id} (local export, inputs pinned)`,\n );\n}\n\n/**\n * The multi-step install. Called by\n * `DependencyManager.runCustomInstaller` when it sees the\n * `__TRACKING_PYENV_INSTALL__` sentinel command.\n */\nexport async function runTrackingPyenvInstall(): Promise<void> {\n const proj = sidecarProjectDir();\n const modelsDir = trackingModelsDir();\n\n // 1. uv sync the sidecar project. cwd-based invocation — the\n // `--project <abs>` form was found unreliable in Task 9 (resolves\n // relative to caller cwd). Mirror that here.\n //\n // UV_PROJECT_ENVIRONMENT (set by buildUvEnv) is what keeps the ~1.2 GB\n // venv OUT of `<proj>/.venv`. That default put it inside the signed\n // `.app`, breaking the code signature and failing on a read-only\n // install. With the env set, `proj` is only ever READ from (pyproject +\n // uv.lock), so the package directory stays read-only — verified against\n // a `chmod 555` project dir.\n const venv = trackingVenvDir();\n logger.info(\n { tag: \"tracking-pyenv\", op: \"uv_sync_start\", proj, venv, uv: uvPath() },\n \"uv sync of tracking sidecar\",\n );\n // `--locked` asserts the shipped uv.lock is already up to date with\n // pyproject.toml. Without it uv may REWRITE uv.lock — another write into the\n // read-only package. It also turns \"someone edited pyproject.toml without\n // re-running `uv lock`\" into a loud install-time error instead of a silent\n // drift between the lockfile we ship and the env we build.\n await pexec(uvPath(), [\"sync\", \"--locked\"], {\n cwd: proj,\n windowsHide: true,\n timeout: 10 * 60_000, // first sync resolves torch + boxmot + opencv\n maxBuffer: 32 * 1024 * 1024,\n // buildUvEnv prepends `~/.libi/bin` so uv-spawned subprocesses (python,\n // pip) find the libi-managed binaries, and pins every uv state location\n // under LIBI_HOME. In a packaged Electron app, bootstrapPath() has\n // already augmented PATH for Homebrew etc., which is inherited here.\n env: buildUvEnv({ UV_PROJECT_ENVIRONMENT: venv, NO_COLOR: \"1\" }),\n });\n logger.info(\n { tag: \"tracking-pyenv\", op: \"uv_sync_done\", proj },\n \"uv sync complete\",\n );\n\n // 2. Ensure model artifacts are on disk: sha-pinned direct downloads\n // first, then any build-at-install exports (which consume the\n // already-provisioned pinned inputs — e.g. yoloe11.onnx is exported\n // locally from the yoloe_vp checkpoint; see ensureBuiltModel).\n //\n // ReID note: there is deliberately NO shipped ReID artifact — on first\n // use boxmot 19 downloads `osnet_x0_25_msmt17.pt` itself and\n // libitrack/associate.py runs its PyTorch backend directly (best-effort,\n // motion-only fallback when offline).\n const manifest = readModelsManifest();\n for (const entry of manifest.filter((e) => e.acquisition === \"direct-download\")) {\n await ensureModel(entry, modelsDir);\n }\n for (const entry of manifest.filter((e) => e.acquisition === \"build-at-install\")) {\n await ensureBuiltModel(entry, manifest, modelsDir);\n }\n\n // 3. Write the install-token marker so resolveStatus() reports installed.\n writeToken();\n logger.info(\n { tag: \"tracking-pyenv\", op: \"install_done\", token: TRACKING_PYENV_TOKEN },\n \"tracking-pyenv install complete\",\n );\n}\n\n/**\n * `CustomInstaller` registered under id `\"tracking-pyenv\"`.\n *\n * `verify()` → returns the models dir path iff the token marker is present\n * AND matches the current token. (We don't re-hash every model on every\n * boot — the token is the cheap re-install lever, same convention as\n * BundledDependency.pinnedInstallToken.)\n *\n * `install` → sentinel command; the real work is `runTrackingPyenvInstall`.\n */\nexport const trackingPyenvInstaller: CustomInstaller & {\n install: { command: string; args: string[]; timeoutMs?: number };\n} = {\n verify: async () => {\n const marker = readToken();\n if (marker === TRACKING_PYENV_TOKEN) {\n return trackingModelsDir();\n }\n return null;\n },\n install: {\n command: TRACKING_PYENV_SENTINEL,\n args: [],\n // uv sync (cold torch/boxmot) + model downloads (incl. the 572MB\n // mobileclip export input) + the local yoloe11 ONNX export.\n timeoutMs: 30 * 60_000,\n },\n};\n\n/** Direct entry point for the dev dry-run / scripts. */\nexport async function install(): Promise<void> {\n await runTrackingPyenvInstall();\n}\n","// lib/effects/registry.ts\nimport type { EffectDef, EffectPhase, LayerKind } from \"./types\";\nimport { BUILTIN_EFFECTS } from \"./builtin\";\n\n/** Custom effects loaded from disk packages. REPLACED wholesale on each refresh\n * (the loader passes the full current disk set) so removal works by re-loading. */\nlet CUSTOM: EffectDef[] = [];\n\n/** Bumped on every registry mutation; lets React consumers subscribe via\n * useSyncExternalStore (see hooks/preview/use-register-custom-effects.ts)\n * instead of mirroring the registry into component state. */\nlet VERSION = 0;\nconst listeners = new Set<() => void>();\n\nfunction notifyRegistryChanged(): void {\n VERSION++;\n for (const l of listeners) l();\n}\n\n/** Subscribe to registry mutations (useSyncExternalStore contract). */\nexport function subscribeEffectsRegistry(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/** Monotonic registry version (useSyncExternalStore snapshot). */\nexport function effectsRegistryVersion(): number {\n return VERSION;\n}\n\n/** Register the full current set of custom effects, replacing any prior set. */\nexport function registerCustomEffects(defs: EffectDef[]): void {\n CUSTOM = defs;\n notifyRegistryChanged();\n}\n\n/** Clear all custom effects (used by test teardown). */\nexport function clearCustomEffects(): void {\n CUSTOM = [];\n notifyRegistryChanged();\n}\n\n/** All registered effects: built-ins first, then customs. A custom id shadows\n * a built-in of the same id (skills-style shadow semantics). */\nfunction allEffects(): EffectDef[] {\n const m = new Map<string, EffectDef>();\n for (const e of BUILTIN_EFFECTS) m.set(e.meta.id, e);\n for (const e of CUSTOM) m.set(e.meta.id, e);\n return [...m.values()];\n}\n\nconst byId = (): Map<string, EffectDef> => {\n const m = new Map<string, EffectDef>();\n for (const e of allEffects()) m.set(e.meta.id, e);\n return m;\n};\n\nexport function findEffect(effectId: string): EffectDef | undefined {\n return byId().get(effectId);\n}\n\nexport function isKnownEffectId(effectId: string): boolean {\n return byId().has(effectId);\n}\n\nexport interface ListEffectsFilter {\n kind?: LayerKind;\n phase?: EffectPhase;\n family?: \"animation\";\n}\n\nexport function listEffects(filter: ListEffectsFilter = {}): EffectDef[] {\n return allEffects().filter((e) => {\n if (filter.kind && !e.meta.supports.includes(filter.kind)) return false;\n if (filter.phase && !e.meta.phases.includes(filter.phase)) return false;\n if (filter.family && e.meta.family !== filter.family) return false;\n return true;\n });\n}\n\n/** Convenience resolver for composeEffects. */\nexport function resolveEffect(effectId: string): EffectDef | undefined {\n return findEffect(effectId);\n}\n","import type { Transform3D, Vec3 } from \"@/lib/engine/types\";\n\n/** The identity 3D transform: no translation, no rotation. */\nexport const IDENTITY_TRANSFORM3D: Transform3D = {\n position: { x: 0, y: 0, z: 0 },\n rotation: { x: 0, y: 0, z: 0 },\n};\n\nfunction vecIsZero(v: Vec3): boolean {\n return v.x === 0 && v.y === 0 && v.z === 0;\n}\n\n/** True only when `t` equals the identity transform. */\nexport function isIdentityTransform3d(t: Transform3D): boolean {\n return vecIsZero(t.position) && vecIsZero(t.rotation);\n}\n\nconst DEG_TO_RAD = Math.PI / 180;\nconst RAD_TO_DEG = 180 / Math.PI;\n\n/** Convert each component from degrees to radians. */\nexport function degToRadVec3(v: Vec3): Vec3 {\n return { x: v.x * DEG_TO_RAD, y: v.y * DEG_TO_RAD, z: v.z * DEG_TO_RAD };\n}\n\n/** Convert each component from radians to degrees. */\nexport function radToDegVec3(v: Vec3): Vec3 {\n return { x: v.x * RAD_TO_DEG, y: v.y * RAD_TO_DEG, z: v.z * RAD_TO_DEG };\n}\n\n/** Wrap an angle (radians) into the range [-PI, PI]. */\nfunction wrap(angle: number): number {\n const twoPi = Math.PI * 2;\n const a = ((angle + Math.PI) % twoPi + twoPi) % twoPi;\n return a - Math.PI;\n}\n\n/**\n * Apply an orbit drag to a transform: horizontal drag (dx) drives yaw\n * (rotation.y), vertical drag (dy) drives pitch (rotation.x). Both wrapped\n * into [-PI, PI]. Position is unchanged. Returns a NEW transform;\n * `base` is not mutated.\n */\nexport function applyOrbitDrag(\n base: Transform3D,\n dxPx: number,\n dyPx: number,\n sensitivity = 0.01,\n): Transform3D {\n return {\n position: { ...base.position },\n rotation: {\n x: wrap(base.rotation.x + dyPx * sensitivity),\n y: wrap(base.rotation.y + dxPx * sensitivity),\n z: base.rotation.z,\n },\n };\n}\n\n/**\n * Vertical drag → pitch (rotation.x). Wrapped into [-PI, PI]. Only rotation.x\n * changes; position and the other rotation axes are unchanged. Returns a\n * NEW transform; `base` is not mutated.\n */\nexport function applyPitchDrag(\n base: Transform3D,\n dyPx: number,\n sensitivity = 0.01,\n): Transform3D {\n return {\n position: { ...base.position },\n rotation: {\n x: wrap(base.rotation.x + dyPx * sensitivity),\n y: base.rotation.y,\n z: base.rotation.z,\n },\n };\n}\n\n/**\n * Horizontal drag → yaw (rotation.y). Wrapped into [-PI, PI]. Only rotation.y\n * changes. Returns a NEW transform; `base` is not mutated.\n */\nexport function applyYawDrag(\n base: Transform3D,\n dxPx: number,\n sensitivity = 0.01,\n): Transform3D {\n return {\n position: { ...base.position },\n rotation: {\n x: base.rotation.x,\n y: wrap(base.rotation.y + dxPx * sensitivity),\n z: base.rotation.z,\n },\n };\n}\n\n/**\n * Drag → roll (rotation.z). Wrapped into [-PI, PI]. Only rotation.z changes.\n * Returns a NEW transform; `base` is not mutated.\n */\nexport function applyRollDrag(\n base: Transform3D,\n dPx: number,\n sensitivity = 0.01,\n): Transform3D {\n return {\n position: { ...base.position },\n rotation: {\n x: base.rotation.x,\n y: base.rotation.y,\n z: wrap(base.rotation.z + dPx * sensitivity),\n },\n };\n}\n\n/**\n * Vertical drag → depth (position.z), in world units (NOT wrapped — depth is\n * unbounded). Default sensitivity is 0.5 (larger than the radian-based rotation\n * helpers): the inspector exposes `transformPosZ` over a ±2000 world-unit range\n * (`components/preview/three-d-fields.tsx`), so 0.5 maps a ~4000px vertical drag\n * to the full range — a sane 1:2 px→unit feel. Only position.z changes. Returns\n * a NEW transform; `base` is not mutated.\n */\nexport function applyDepthDrag(\n base: Transform3D,\n dyPx: number,\n sensitivity = 0.5,\n): Transform3D {\n return {\n position: {\n x: base.position.x,\n y: base.position.y,\n z: base.position.z + dyPx * sensitivity,\n },\n rotation: { ...base.rotation },\n };\n}\n\ntype Axis = \"x\" | \"y\" | \"z\";\n\n/** Immutable setter: returns a copy of `t` with `position[axis] = v`. */\nexport function withPosition(t: Transform3D, axis: Axis, v: number): Transform3D {\n return {\n position: { ...t.position, [axis]: v },\n rotation: { ...t.rotation },\n };\n}\n\n/** Immutable setter: returns a copy of `t` with `rotation[axis] = v`. */\nexport function withRotation(t: Transform3D, axis: Axis, v: number): Transform3D {\n return {\n position: { ...t.position },\n rotation: { ...t.rotation, [axis]: v },\n };\n}\n\n/** Default detent grid (radians): 0, ±45, ±90, ±135, ±180. */\nconst DEFAULT_DETENTS_RAD = [0, 45, 90, 135, 180, -45, -90, -135, -180].map(\n (d) => (d * Math.PI) / 180,\n);\n\n/**\n * Snap an angle (radians) to the nearest detent if it is within `thresholdRad`\n * (default ~5°). Otherwise return the angle unchanged. Pass `thresholdRad = 0`\n * (the Alt-bypass path) to disable snapping entirely. Pure.\n */\nexport function snapAngleToDetents(\n angleRad: number,\n detentsRad: number[] = DEFAULT_DETENTS_RAD,\n thresholdRad = 0.0873,\n): number {\n let best = angleRad;\n let bestDist = thresholdRad;\n for (const d of detentsRad) {\n const dist = Math.abs(angleRad - d);\n if (dist <= bestDist) {\n bestDist = dist;\n best = d;\n }\n }\n return best;\n}\n\n/** Outer \"stays visible\" clamp for `position.z` (world units). Pure.\n * @deprecated Use `clampDepthForKind` for new code — it applies the correct\n * per-kind limit. This overload is retained for callers that already use it. */\nexport function clampDepth(z: number, max = 1800): number {\n return Math.max(-max, Math.min(max, z));\n}\n\n/**\n * Z range for 3D-world overlays (three / text with threeD), in world units.\n * Widened to ±4 so Depth can push content nearly out of sight (far) or right up\n * to the camera (very zoomed-in) — the user-requested expressive range. The\n * billboard camera sits ~6 units back; at +4 the content is close (≈2 units\n * away, big) and at −4 it's distant (≈10 units, small) but still on-screen.\n */\nexport const DEPTH_LIMIT_WORLD = 4;\n\n/**\n * Z range for quad overlay kinds (image / video / code), in pixel-space\n * position.z. Widened to ±500 to match the expanded world range's expressiveness.\n */\nexport const DEPTH_LIMIT_PX = 500;\n\n/**\n * Per-kind safe Z range clamp. World units for three/text (3D-text); pixels\n * for quad kinds (image/video/code). Keeps content on-screen regardless of\n * what the drag or slider emits.\n */\nexport function clampDepthForKind(z: number, kind: string): number {\n const lim = kind === \"three\" || kind === \"text\" ? DEPTH_LIMIT_WORLD : DEPTH_LIMIT_PX;\n return Math.max(-lim, Math.min(lim, z));\n}\n","import type { Transform3D } from \"@/lib/engine/types\";\nimport { IDENTITY_TRANSFORM3D, isIdentityTransform3d } from \"@/lib/overlays/transform3d\";\n\n/** Orientation input: `transform3d` is the ONLY rotation storage; flip is the\n * canonical `flipH`/`flipV` booleans. */\nexport interface OverlayOrientation {\n transform3d?: Transform3D;\n flipH?: boolean;\n flipV?: boolean;\n}\n\n/** Resolved flip state — read from the canonical `flipH`/`flipV` booleans. */\nexport interface ResolvedFlip { flipH: boolean; flipV: boolean; }\n\n/** Flip is the canonical `flipH`/`flipV` booleans. */\nexport function resolveFlip(o: OverlayOrientation): ResolvedFlip {\n return { flipH: !!o.flipH, flipV: !!o.flipV };\n}\n\n/**\n * The single source of truth for an overlay's orientation. `transform3d` is the\n * ONLY rotation authority — in-plane spin lives on `rotation.z`. There is no\n * legacy `rotation` (degrees) field: it was deleted so a spun overlay can never\n * double-count its spin (dragging it used to ADD the legacy field on top of the\n * transform3d z). Flip is NOT part of the transform — it lives on\n * `flipH`/`flipV` and is read via `resolveFlip`. Bare input ⇒ identity.\n */\nexport function resolveOverlayTransform(o: OverlayOrientation): Transform3D {\n return o.transform3d ?? IDENTITY_TRANSFORM3D;\n}\n\n/**\n * Split a resolved transform into its out-of-plane 3D part (rotation.z zeroed)\n * and the in-plane screen-roll (`rotation.z`, radians).\n *\n * `rotation.z` is ALWAYS a 2D screen-roll about the overlay's rect center — it\n * is NEVER a 3D euler component. Applying it as a Canvas2D `ctx.rotate` AFTER\n * the (possibly perspective-projected) content is composited makes the in-plane\n * \"Spin\" render identically — same pivot, same direction (clockwise, the\n * Canvas2D convention) — across every path: planar 2D, the `three` overlay, 3D\n * text, and the spatial textured-quad. The 3D euler fed to three.js / the quad\n * projection uses only x (elevation) and y (angle). This is what keeps Spin\n * consistent (it used to roll CCW for 3D text via `scene.rotation.z`) and keeps\n * a tilted spatial quad's on-screen footprint STABLE while you spin (folding z\n * into the euler made the projected bbox swing wildly).\n */\nexport function splitScreenRoll(t: Transform3D): { spatial: Transform3D; rollRad: number } {\n const rollRad = t.rotation.z;\n if (!rollRad) return { spatial: t, rollRad: 0 };\n return {\n spatial: { position: t.position, rotation: { x: t.rotation.x, y: t.rotation.y, z: 0 } },\n rollRad,\n };\n}\n\n/** Rotate a screen point by `rad` (radians, clockwise) about `center`. */\nexport function rotatePointAround(\n px: number,\n py: number,\n cx: number,\n cy: number,\n rad: number,\n): { x: number; y: number } {\n if (!rad) return { x: px, y: py };\n const cos = Math.cos(rad), sin = Math.sin(rad);\n const dx = px - cx, dy = py - cy;\n return { x: cx + dx * cos - dy * sin, y: cy + dx * sin + dy * cos };\n}\n\nexport type TransformClass = \"identity\" | \"planar\" | \"spatial\";\n\n/**\n * Classify a transform to pick the render path:\n * - identity — no-op.\n * - planar — only in-plane components (z-rotation, x/y-position).\n * Representable as a Canvas2D affine; keeps the cheap path.\n * - spatial — any out-of-plane component (rotation.x/y ≠ 0, or position.z ≠ 0).\n * Needs perspective ⇒ the textured-quad path.\n */\nexport function classifyTransform(t: Transform3D): TransformClass {\n if (isIdentityTransform3d(t)) return \"identity\";\n if (t.rotation.x !== 0 || t.rotation.y !== 0 || t.position.z !== 0) return \"spatial\";\n return \"planar\";\n}\n\nexport interface TransformBox { x: number; y: number; width: number; height: number; }\n\n/** A minimal Canvas2D op (consumed by applyOverlayTransform). */\nexport type Canvas2DOp =\n | { kind: \"translate\"; x: number; y: number }\n | { kind: \"rotate\"; rad: number }\n | { kind: \"scale\"; x: number; y: number };\n\n/**\n * The Canvas2D ops that realize a PLANAR transform (plus flip) about the box center:\n * translate(cx + posX, cy + posY) · rotate(rot.z) · scale(sx, sy) · translate(-cx, -cy)\n * Returns [] only when the transform is identity AND there is no flip.\n * Spatial input ⇒ [] (caller routes to quad path). Legacy values (position 0)\n * reproduce the old center-anchored rotate+flip byte-for-byte.\n */\nexport function planarCanvas2DOps(t: Transform3D, box: TransformBox, flip: ResolvedFlip): Canvas2DOp[] {\n const cls = classifyTransform(t);\n const sx = flip.flipH ? -1 : 1;\n const sy = flip.flipV ? -1 : 1;\n if (cls === \"spatial\") return [];\n if (cls === \"identity\" && sx === 1 && sy === 1) return [];\n const cx = box.x + box.width / 2;\n const cy = box.y + box.height / 2;\n return [\n { kind: \"translate\", x: cx + t.position.x, y: cy + t.position.y },\n { kind: \"rotate\", rad: t.rotation.z },\n { kind: \"scale\", x: sx, y: sy },\n { kind: \"translate\", x: -cx, y: -cy },\n ];\n}\n\n/**\n * Inverse-map a screen point into the overlay's local (pre-transform) space for\n * a PLANAR transform — undoes the ops above in reverse order. Identity/spatial\n * ⇒ the point unchanged (spatial hit-test falls back to the rect upstream).\n */\nexport function inversePlanarPoint(\n t: Transform3D,\n box: TransformBox,\n flip: ResolvedFlip,\n px: number,\n py: number,\n): { x: number; y: number } {\n if (classifyTransform(t) === \"spatial\") return { x: px, y: py };\n const sx = flip.flipH ? -1 : 1;\n const sy = flip.flipV ? -1 : 1;\n const cx = box.x + box.width / 2;\n const cy = box.y + box.height / 2;\n // forward: translate(cx+posX, cy+posY) rotate(z) scale(sx,sy) translate(-cx,-cy)\n // inverse: translate(cx,cy) scale(1/sx,1/sy) rotate(-z) translate(-(cx+posX),-(cy+posY))\n let x = px - (cx + t.position.x);\n let y = py - (cy + t.position.y);\n const cos = Math.cos(-t.rotation.z), sin = Math.sin(-t.rotation.z);\n const rx = x * cos - y * sin;\n const ry = x * sin + y * cos;\n x = rx / sx; y = ry / sy;\n return { x: x + cx, y: y + cy };\n}\n","import { getCurrentPort } from \"@/lib/libi-home\";\n\n/**\n * Lightweight HTTP client for notifying the Next.js server about UI events.\n * All calls are fire-and-forget — silently no-op if the server isn't running.\n */\n\nfunction getServerUrl(): string | null {\n try {\n const port = getCurrentPort();\n return `http://127.0.0.1:${port}`;\n } catch {\n return null;\n }\n}\n\nasync function send(payload: Record<string, unknown>): Promise<void> {\n const url = getServerUrl();\n if (!url) return;\n\n try {\n await fetch(`${url}/api/notify`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(3000),\n });\n } catch {\n // Fire-and-forget — server may not be running.\n }\n}\n\nexport const notify = {\n navigate(event: {\n target: \"piece\" | \"asset\" | \"preview\" | \"storyboard\" | \"folder\";\n /** Required for piece/asset/preview/storyboard/folder targets. */\n pieceId?: string;\n fileId?: string;\n /** Optional id for special targets. */\n id?: string;\n }): void {\n send({ type: \"navigate\", ...event });\n },\n refreshQuery(event: { queryKey: string; pieceId?: string; fileId?: string; trackId?: string }): void {\n send({ type: \"refresh_query\", ...event });\n },\n refreshMcpConfig(): void {\n send({ type: \"refresh_mcp_config\" });\n },\n /** Tell the server that memories or the instruction override changed (files\n * under ~/.libi/). The server regenerates workspace files and restarts\n * sessions; the agent process firing this is about to be killed. */\n instructionsChanged(): void {\n send({ type: \"instructions_changed\" });\n },\n analysisChanged(event: { fileId: string }): void {\n send({ type: \"analysis_changed\", ...event });\n },\n /** Tell the server to navigate the user to Settings (optionally focusing an MCP card). */\n navigateSettings(event: { mcpId?: string }): void {\n send({ type: \"navigate_settings\", mcpId: event.mcpId });\n },\n /** Tell the server to open or switch the right-region panel (onboarding, api-config, etc.). */\n rightRegion(event: {\n mode: \"editor\" | \"onboarding\" | \"api-config\";\n mcpId?: string;\n }): void {\n send({ type: \"right_region\", ...event });\n },\n /** Flash an inspector field for an overlay (guided edit). */\n highlight(event: {\n pieceId: string;\n overlayId: string;\n property: string;\n note?: string;\n }): void {\n send({ type: \"highlight\", ...event });\n },\n /** Flash an effect in the catalog or on a layer's slot (guided edit). */\n highlightEffect(event: {\n pieceId: string;\n target: { kind: \"catalog\"; effectId: string; phase?: \"in\" | \"out\" | \"loop\" } | { kind: \"applied\"; layerId: string; phase: \"in\" | \"out\" | \"loop\" };\n note?: string;\n }): void {\n send({ type: \"highlight_effect\", ...event });\n },\n /** Set a specific overlay's inspector tab/group. */\n setComplexityMode(event: {\n pieceId?: string;\n overlayId?: string;\n mode: \"transform\" | \"style\" | \"text\" | \"3d\" | \"anchors\";\n }): void {\n send({\n type: \"set_complexity_mode\",\n pieceId: event.pieceId,\n overlayId: event.overlayId,\n mode: event.mode,\n });\n },\n /** Forward a job progress tick into the in-process event bus so\n * `SessionEventHandler` can synthesize an `agent-tool-progress` event\n * on the matching tool-call part in the active session. */\n jobProgress(event: {\n jobId: string;\n toolCallId?: string;\n kind: string;\n done: number;\n total: number;\n unit: string;\n etaMs: number | null;\n msSinceProgress?: number | null;\n toolName?: string;\n toolArgs?: unknown;\n progressLabel?: string;\n }): void {\n send({ type: \"job_progress\", ...event });\n },\n};\n\nexport type PushPayload = { title: string; body: string; pieceId?: string; jobId?: string };\nexport type Notifier = {\n isFocused: () => boolean;\n notify: (p: PushPayload) => void;\n};\n\nlet notifier: Notifier | null = null;\n\n/** Bind the runtime notifier. Called from `electron/main.ts` after window\n * creation. In `npx libi --connect-agent` mode this is never called and\n * `pushIfBackgrounded` silently no-ops. */\nexport function bindNotifier(n: Notifier): void {\n notifier = n;\n}\n\n/** Test-only — override notifier inside vitest. */\nexport function __setTestNotifier(n: Notifier | null): void {\n notifier = n;\n}\n\n/** Fire a system notification if the runtime window isn't focused. Used by\n * the job-progress bridge on job completion + failure for runs over\n * ~10 seconds. Caller suppresses based on the user's settings toggle. */\nexport async function pushIfBackgrounded(p: PushPayload): Promise<void> {\n if (!notifier) return;\n if (notifier.isFocused()) return;\n notifier.notify(p);\n}\n","import fs from \"fs\";\nimport path from \"path\";\nimport { spawn } from \"child_process\";\nimport { requireUvBinary } from \"@/lib/uv-path\";\nimport { buildUvEnv } from \"@/lib/uv-env/spawn-env\";\nimport { resolveWhisperModel, whisperModelsDir } from \"@/lib/whisper/models\";\nimport type { ElevenLabsTranscription } from \"@/lib/elevenlabs/transcribe\";\nimport { hashSpec } from \"@/lib/uv-env/hash-spec\";\nimport { isTokenCurrent, writeInstallToken } from \"@/lib/uv-env/install-token\";\nimport { packageRoot } from \"@/lib/runtime/package-root\";\n\nexport const FASTER_WHISPER_VERSION = \"1.1.1\";\n\n/** Pinned Python version for the faster-whisper env. We pin 3.12 to\n * match the rest of libi's uv-managed envs (analyze, generate, TTS)\n * and give every host a deterministic interpreter. 3.12 has full wheel\n * coverage for faster-whisper + its native deps. */\nexport const WHISPER_PYTHON_VERSION = \"3.12\";\n\n/** Single source of truth for the Whisper `--with` list. Consumed by\n * both buildWhisperArgs (what we actually spawn) and\n * whisperEnvSignature() (what the install-token marker hashes). Keep\n * these aligned via this constant so any new dep automatically updates\n * both. Mirrors lib/music/analyze.ts:ANALYZE_WITH_SPECS pattern.\n *\n * `requests` is required because `faster-whisper` 1.1.1 does\n * `import requests` at module load time in `faster_whisper/utils.py`\n * but does NOT declare it in install_requires — it used to get pulled\n * in transitively via `huggingface_hub` < 1.0, but huggingface_hub 1.x\n * dropped `requests` from core deps. Without this explicit pin the env\n * resolves cleanly then crashes at first `from faster_whisper import\n * WhisperModel` with `No module named 'requests'`. Upstream issue:\n * https://github.com/SYSTRAN/faster-whisper/issues — keep the pin\n * until a faster-whisper release declares it directly. */\nexport const WHISPER_WITH_SPECS = [\n `faster-whisper==${FASTER_WHISPER_VERSION}`,\n \"requests\",\n] as const;\n\nexport class WhisperTranscribeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"WhisperTranscribeError\";\n }\n}\n\nfunction whisperScriptPath(): string {\n // cwd is the package root under `npx libi`, but the *user's* project dir\n // under `npx libi --connect-agent` — so fall back to a __dirname-relative\n // path (this file lives at <pkg>/lib/whisper/, script at <pkg>/mcp/whisper/).\n const candidates = [\n path.join(process.cwd(), \"mcp/whisper/transcribe.py\"),\n path.resolve(__dirname, \"../../mcp/whisper/transcribe.py\"),\n // Depth-independent: the compiled mirror (scripts/build-cli.js) puts this\n // module at dist-cli/lib/whisper/, one level deeper than source, so the\n // `../..` above resolves inside dist-cli/ instead of the package root.\n path.join(packageRoot(), \"mcp/whisper/transcribe.py\"),\n ];\n for (const c of candidates) {\n if (fs.existsSync(c)) return c;\n }\n return candidates[0];\n}\n\nfunction resolveUvPath(): string {\n return requireUvBinary(WhisperTranscribeError, \"Whisper\");\n}\n\nexport function buildWhisperArgs(opts: {\n scriptPath: string;\n audioPath?: string;\n model: string;\n downloadRoot: string;\n languageCode?: string;\n downloadOnly?: boolean;\n}): string[] {\n const args = [\n \"run\",\n \"--python\",\n WHISPER_PYTHON_VERSION,\n ...WHISPER_WITH_SPECS.flatMap((s) => [\"--with\", s]),\n \"python\",\n opts.scriptPath,\n ];\n if (!opts.downloadOnly && opts.audioPath) args.push(opts.audioPath);\n args.push(\"--model\", opts.model, \"--download-root\", opts.downloadRoot);\n if (opts.languageCode) args.push(\"--language\", opts.languageCode);\n if (opts.downloadOnly) args.push(\"--download-only\");\n return args;\n}\n\nexport function parseWhisperStdout(stdout: string): ElevenLabsTranscription {\n const trimmed = stdout.trim();\n if (!trimmed)\n throw new WhisperTranscribeError(\"whisper produced empty stdout\");\n let json: unknown;\n try {\n json = JSON.parse(trimmed);\n } catch {\n throw new WhisperTranscribeError(\n `whisper stdout was not JSON: ${trimmed.slice(0, 200)}`,\n );\n }\n if (\n typeof json !== \"object\" ||\n json === null ||\n typeof (json as Record<string, unknown>).text !== \"string\"\n ) {\n throw new WhisperTranscribeError(\n `whisper JSON missing 'text': ${JSON.stringify(json).slice(0, 200)}`,\n );\n }\n const typed = json as ElevenLabsTranscription;\n if (!Array.isArray(typed.words)) typed.words = [];\n return typed;\n}\n\n/** Progress callback: `(filesDone, filesTotal)` parsed from `PROGRESS x/y`\n * stderr markers emitted by the Python download hook. */\ntype ProgressCb = (done: number, total: number) => void;\n\nfunction runUv(\n args: string[],\n timeoutMs: number,\n onProgress?: ProgressCb,\n): Promise<string> {\n const uv = resolveUvPath();\n return new Promise((resolve, reject) => {\n const child = spawn(uv, args, {\n windowsHide: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n env: buildUvEnv(),\n });\n let out = \"\";\n let err = \"\";\n const timer = setTimeout(() => {\n child.kill(\"SIGKILL\");\n reject(\n new WhisperTranscribeError(`whisper timed out after ${timeoutMs}ms`),\n );\n }, timeoutMs);\n child.stdout.on(\"data\", (d) => (out += d.toString()));\n child.stderr.on(\"data\", (d) => {\n const s = d.toString();\n err += s;\n for (const m of s.matchAll(/PROGRESS (\\d+)\\/(\\d+)/g)) {\n onProgress?.(Number(m[1]), Number(m[2]));\n }\n });\n child.on(\"error\", (e) => {\n clearTimeout(timer);\n reject(new WhisperTranscribeError(`uv spawn failed: ${e.message}`));\n });\n child.on(\"close\", (code) => {\n clearTimeout(timer);\n if (code !== 0) {\n reject(\n new WhisperTranscribeError(\n `whisper exited ${code}: ${err.trim().slice(0, 500)}`,\n ),\n );\n return;\n }\n resolve(out);\n });\n });\n}\n\nexport async function transcribeAudio(opts: {\n audioPath: string;\n model?: string;\n languageCode?: string;\n}): Promise<ElevenLabsTranscription> {\n const model = resolveWhisperModel(opts.model);\n const args = buildWhisperArgs({\n scriptPath: whisperScriptPath(),\n audioPath: opts.audioPath,\n model,\n downloadRoot: whisperModelsDir(),\n languageCode: opts.languageCode,\n });\n // 30 min ceiling: long files on CPU + first-run wheel fetch.\n const stdout = await runUv(args, 30 * 60_000);\n const result = parseWhisperStdout(stdout);\n // Warm-env attestation: once the subprocess returned valid output we\n // know the spec resolved end-to-end. Sticky marker so the next call\n // skips needs_install if nothing changed.\n writeWhisperEnvToken();\n return result;\n}\n\n/** Stable signature for the Whisper env spec. Bumping FASTER_WHISPER_VERSION,\n * WHISPER_PYTHON_VERSION, or adding a new --with will invalidate it. */\nexport function whisperEnvSignature(): string {\n return hashSpec(WHISPER_PYTHON_VERSION, WHISPER_WITH_SPECS);\n}\n\nconst WHISPER_ENV_TOKEN = \".libi-whisper-env.install-token\";\n\nexport function isWhisperEnvCurrent(): boolean {\n return isTokenCurrent(WHISPER_ENV_TOKEN, whisperEnvSignature());\n}\n\nexport function writeWhisperEnvToken(): void {\n writeInstallToken(WHISPER_ENV_TOKEN, whisperEnvSignature());\n}\n\nexport async function downloadModel(\n model: string,\n onProgress?: ProgressCb,\n): Promise<void> {\n const args = buildWhisperArgs({\n scriptPath: whisperScriptPath(),\n model: resolveWhisperModel(model),\n downloadRoot: whisperModelsDir(),\n downloadOnly: true,\n });\n // 60 min: large-v3 is ~3 GB. `onProgress` receives per-file ticks from\n // the Python snapshot_download hook.\n await runUv(args, 60 * 60_000, onProgress);\n}\n","import fs from \"fs\";\nimport path from \"path\";\nimport { execFileSync } from \"child_process\";\nimport { getLibiBinDir } from \"@/lib/libi-home\";\nimport { isWindows, exeSuffix } from \"@/lib/platform\";\n\n/**\n * Resolve the `uv` binary path with the same policy as `resolveStatus`\n * + `installYtDlpViaUv` in `mcp/registry/dependency-manager.ts`:\n *\n * 1. `<libi-bin-dir>/uv` — preferred (libi's own install via Category A)\n * 2. system PATH (`which uv` / `where uv`) — covers cases where Category A\n * already treats a PATH-resolved uv as \"installed\" (e.g. Homebrew)\n *\n * Returns the resolved absolute path, or `null` if neither exists. Callers\n * throw their own typed error so the user sees a domain-specific message\n * (Whisper / Kokoro / ACE-Step), but the resolution policy is shared so\n * the three runners can't drift apart.\n */\nexport function resolveUvBinary(): string | null {\n const bundled = path.join(getLibiBinDir(), `uv${exeSuffix()}`);\n if (fs.existsSync(bundled)) return bundled;\n\n const lookup = isWindows() ? \"where\" : \"which\";\n try {\n const stdout = execFileSync(lookup, [\"uv\"], {\n windowsHide: true,\n encoding: \"utf8\",\n timeout: 5_000,\n });\n const candidate = stdout.split(\"\\n\")[0]?.trim();\n if (candidate && fs.existsSync(candidate)) return candidate;\n } catch {\n /* uv not on PATH */\n }\n\n return null;\n}\n\n/**\n * Convenience wrapper for callers that prefer to throw. Pass a domain-specific\n * Error constructor so the user sees \"Whisper requires…\" / \"Local TTS\n * requires…\" / \"Local music requires…\" rather than a generic message.\n */\nexport function requireUvBinary(\n ErrorCtor: new (message: string) => Error,\n featureLabel: string,\n): string {\n const uv = resolveUvBinary();\n if (uv) return uv;\n const expected = path.join(getLibiBinDir(), `uv${exeSuffix()}`);\n throw new ErrorCtor(\n `uv not found at ${expected} or on PATH. ${featureLabel} requires the bundled uv (tier-1 dep).`,\n );\n}\n","import { createHash } from \"crypto\";\n\n/** Deterministic signature for a `uv run --python X --with A --with B …`\n * invocation. We hash the python version + the (sorted) list of pinned\n * dependency specs so any change to either triggers a marker mismatch on\n * the next call. Sorting means a refactor that reorders --with flags\n * WITHOUT semantic change does NOT churn the signature.\n *\n * Output is 16 hex chars (64 bits) — collision-resistant for our scale\n * and short enough to embed in a token filename or log line. */\nexport function hashSpec(pythonVersion: string, withSpecs: readonly string[]): string {\n const parts = [`python:${pythonVersion}`, ...[...withSpecs].sort()];\n return createHash(\"sha256\").update(parts.join(\"\\n\")).digest(\"hex\").slice(0, 16);\n}\n","import fs from \"fs\";\nimport path from \"path\";\nimport { getLibiHome } from \"@/lib/libi-home\";\n\n/** Read a one-line token file under LIBI_HOME. Returns null on any\n * filesystem error (missing file, permission, garbage). */\nexport function readInstallToken(filename: string): string | null {\n try {\n return fs\n .readFileSync(path.join(getLibiHome(), filename), \"utf-8\")\n .trim();\n } catch {\n return null;\n }\n}\n\n/** Write a one-line token file under LIBI_HOME. Creates parent dirs.\n * Single trailing newline so `cat <file>` prints cleanly. */\nexport function writeInstallToken(filename: string, signature: string): void {\n const full = path.join(getLibiHome(), filename);\n fs.mkdirSync(path.dirname(full), { recursive: true });\n fs.writeFileSync(full, signature + \"\\n\");\n}\n\n/** True iff the on-disk token equals the expected signature. */\nexport function isTokenCurrent(filename: string, signature: string): boolean {\n return readInstallToken(filename) === signature;\n}\n","/**\n * Simple in-memory tracking of which piece is currently open in the editor.\n * Used by MCP tools (open piece) and the proxy LRU (open-piece protection)\n * without threading state through every call.\n */\nconst globalForEditor = globalThis as unknown as {\n __openedPieceId?: string | null;\n};\n\nexport function getOpenedPieceId(): string | null {\n return globalForEditor.__openedPieceId ?? null;\n}\n\nexport function setOpenedPieceId(pieceId: string | null): void {\n globalForEditor.__openedPieceId = pieceId;\n}\n","import { join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { getLibiBinDir, getLibiHome } from \"@/lib/libi-home\";\nimport { exeSuffix } from \"@/lib/platform\";\n\nexport function uvPath(): string {\n const p = join(getLibiBinDir(), `uv${exeSuffix()}`);\n return existsSync(p) ? p : \"uv\";\n}\n\nexport function sidecarProjectDir(): string {\n return join(process.cwd(), \"mcp\", \"tracking\", \"py\");\n}\n\nexport function trackingModelsDir(): string {\n return join(getLibiHome(), \"models\", \"tracking\");\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { trackingModelsDir } from \"@/lib/tracking/engine-deps\";\n\nconst TOKEN_MARKER = \".install-token\";\n\n/** True when the tracking-pyenv install token marker exists (the cheap\n * signal the custom installer writes on success). */\nexport function trackingEngineInstalled(): boolean {\n try {\n return fs.existsSync(path.join(trackingModelsDir(), TOKEN_MARKER));\n } catch {\n return false;\n }\n}\n\nexport interface TrackingNotInstalled {\n error: \"tracking_engine_not_installed\";\n data: { hint: string; installPlanPath: string };\n}\n\nexport function trackingNotInstalledError(): TrackingNotInstalled {\n return {\n error: \"tracking_engine_not_installed\",\n data: {\n hint:\n \"The libi-tracking engine is not installed yet. Call libi.get_install_plan \" +\n \"(mcpId 'libi-tracking') and follow it — disclose the ~2 GB / ~10-20 min cost and get \" +\n \"the user's OK, then call libi.install_tracking_engine (it runs the real install \" +\n \"as a background job), then libi.verify_install, then retry.\",\n installPlanPath: \"mcp/bundled-mcps/plans/libi-tracking.md\",\n },\n };\n}\n"],"names":["MEASUREMENT_ID","process","env","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","name","has","toAgentEventId","id","undefined","sanitizeParams","params","out","k","v","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","enabled","clean","db","insert","values","crypto","randomUUID","paramsJson","JSON","stringify","nextAttemptAt","Date","run","n","select","from","all","excess","orderBy","createdAt","limit","map","r","delete","where","drainAnalyticsQueue","deps","now","fetchImpl","fetch","getSettings","markMilestoneOnce","settings","rows","length","sent","failed","dropped","nowMs","due","firstVisitAsked","userId","row","state","sessionStart","firstVisit","parse","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","dropProxyFile","fileId","reason","proxyStatus","proxyFilename","info","event","proxyPath","join","pieceId","unlinkSync","proxyGeneratedAt","emit","queryKey","RENDER_STALL_TIMEOUT_MS","RENDER_ABSOLUTE_CAP_MS","globalForRenderJobs","globalThis","jobs","__libi_render_jobs","Map","createRenderJob","init","jobId","token","resolve","reject","done","Promise","rej","stallTimeoutMs","absoluteCapMs","timeoutMs","stallTimer","setTimeout","entry","get","settled","clearTimeout","absoluteTimer","seconds","message","lastProgress","total","Error","payload","onProgress","getRenderJob","resolveRenderJob","value","rejectRenderJob","recordRenderProgress","current","isRenderJobPending","__resetRegistryForTests","clear","getRenderJobTokenByJobId","TRACK_REGISTRY_TIMEOUT_MS","__libiTrackJobs","createTrackJob","getTrackJob","resolveTrackJob","rejectTrackJob","getTrackJobTokenByJobId","__resetTrackRegistryForTests","readFalApiKey","envVars","parsed","key","trim","FAL_BILLING_URL","FAL_ADMIN_SCOPE_HINT","FALLBACK_WINDOW_MS","SKEW_MARGIN_MS","billingWindowStart","generatedAt","NaN","base","Number","isFinite","toISOString","fetchFalCost","args","requestId","apiKey","URLSearchParams","request_id","start","toString","resp","headers","Authorization","kind","body","text","json","billing_events","cost_estimate_nano_usd","amountUsd","deletePieceCompletely","deleted","returning","storage","deletePieceDir","pexec","TRACKING_PYENV_TOKEN","TRACKING_PYENV_SENTINEL","modelsManifestPath","readModelsManifest","p","raw","readFileSync","Array","isArray","sha256File","createHash","digest","toLowerCase","tokenMarkerPath","readToken","writeToken","mkdirSync","dirname","recursive","writeFileSync","downloadVerified","sha256","dest","label","tmpPath","bytesDownloaded","actual","onRetry","attempt","maxAttempts","error","rmSync","force","renameSync","size","ensureModel","modelsDir","acquisition","existsSync","expected","buildInputsOf","manifest","build","source","find","m","sourceModelId","sourcePtSha256","textEncoderSha256","textEncoder","clipRequirement","classes","imgsz","opset","readBuildInfo","destPath","isBuildCurrent","inputs","output","parseExportResult","stdout","lines","split","i","line","startsWith","type","ensureBuiltModel","exec","sourcePt","encoderPath","file","encoderOk","proj","stderr","cwd","windowsHide","maxBuffer","UV_PROJECT_ENVIRONMENT","NO_COLOR","e","detail","result","outSha","sizeBytes","statSync","ultralytics","builtAt","runTrackingPyenvInstall","venv","uv","filter","trackingPyenvInstaller","verify","marker","install","command","CUSTOM","VERSION","listeners","notifyRegistryChanged","l","subscribeEffectsRegistry","listener","effectsRegistryVersion","registerCustomEffects","defs","clearCustomEffects","allEffects","meta","byId","findEffect","effectId","isKnownEffectId","listEffects","supports","includes","phase","phases","family","resolveEffect","IDENTITY_TRANSFORM3D","position","x","y","z","rotation","vecIsZero","isIdentityTransform3d","t","DEG_TO_RAD","PI","RAD_TO_DEG","degToRadVec3","radToDegVec3","wrap","angle","twoPi","a","applyOrbitDrag","dxPx","dyPx","sensitivity","applyPitchDrag","applyYawDrag","applyRollDrag","dPx","applyDepthDrag","withPosition","axis","withRotation","DEFAULT_DETENTS_RAD","d","snapAngleToDetents","angleRad","detentsRad","thresholdRad","best","bestDist","dist","abs","clampDepth","max","DEPTH_LIMIT_WORLD","DEPTH_LIMIT_PX","clampDepthForKind","lim","getServerUrl","port","send","notify","navigate","refreshQuery","refreshMcpConfig","instructionsChanged","analysisChanged","navigateSettings","mcpId","rightRegion","highlight","highlightEffect","setComplexityMode","overlayId","mode","jobProgress","notifier","bindNotifier","__setTestNotifier","pushIfBackgrounded","isFocused","resolveUvBinary","bundled","lookup","encoding","candidate","requireUvBinary","ErrorCtor","featureLabel","globalForEditor","getOpenedPieceId","__openedPieceId","setOpenedPieceId","uvPath","sidecarProjectDir","trackingModelsDir","TOKEN_MARKER","trackingEngineInstalled","trackingNotInstalledError","data","hint","installPlanPath"],"mappings":"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"}}]
7
7
  }