@nagellabs/libi 0.1.8 → 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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +5 -5
- package/.next/diagnostics/route-bundle-stats.json +16 -16
- package/.next/externals-manifest.json +1 -1
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/(app)/editor/page/build-manifest.json +2 -2
- package/.next/server/app/(app)/instructions/page/build-manifest.json +2 -2
- package/.next/server/app/(app)/mcps-skills/page/build-manifest.json +2 -2
- package/.next/server/app/(app)/mcps-skills/skills/[name]/page/build-manifest.json +2 -2
- package/.next/server/app/(app)/mcps-skills/skills/new/page/build-manifest.json +2 -2
- package/.next/server/app/(app)/settings/page/build-manifest.json +2 -2
- package/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/server/app/api/agent/cancel/route.js +8 -8
- package/.next/server/app/api/agent/cancel/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/detect/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/dispatch/route.js +5 -5
- package/.next/server/app/api/agent/dispatch/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/events/route.js +11 -11
- package/.next/server/app/api/agent/events/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/messages/route.js +7 -7
- package/.next/server/app/api/agent/messages/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/providers/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/send/route.js +7 -7
- package/.next/server/app/api/agent/send/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/start/route.js +7 -7
- package/.next/server/app/api/agent/start/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/stop/route.js +8 -8
- package/.next/server/app/api/agent/stop/route.js.nft.json +1 -1
- package/.next/server/app/api/agents/[agentId]/install/route.js +4 -4
- package/.next/server/app/api/agents/[agentId]/install/route.js.nft.json +1 -1
- package/.next/server/app/api/codex/connect/route.js.nft.json +1 -1
- package/.next/server/app/api/db/resolve/route.js +9 -9
- package/.next/server/app/api/db/resolve/route.js.nft.json +1 -1
- package/.next/server/app/api/export/chromium/route.js +5 -5
- package/.next/server/app/api/export/chromium/route.js.nft.json +1 -1
- package/.next/server/app/api/export/route.js +4 -4
- package/.next/server/app/api/export/route.js.nft.json +1 -1
- package/.next/server/app/api/files/by-id/[fileId]/filmstrip-status/route.js +4 -4
- package/.next/server/app/api/files/by-id/[fileId]/filmstrip-status/route.js.nft.json +1 -1
- package/.next/server/app/api/files/by-id/[fileId]/rename/route.js +7 -7
- package/.next/server/app/api/files/by-id/[fileId]/rename/route.js.nft.json +1 -1
- package/.next/server/app/api/folders/[folderId]/duplicate/route.js +6 -6
- package/.next/server/app/api/folders/[folderId]/duplicate/route.js.nft.json +1 -1
- package/.next/server/app/api/install-from-url/route.js +10 -10
- package/.next/server/app/api/install-from-url/route.js.nft.json +1 -1
- package/.next/server/app/api/instructions/override/route.js.nft.json +1 -1
- package/.next/server/app/api/jobs/[id]/events/route.js +8 -8
- package/.next/server/app/api/jobs/[id]/events/route.js.nft.json +1 -1
- package/.next/server/app/api/jobs/[id]/retry/route.js +4 -4
- package/.next/server/app/api/jobs/[id]/retry/route.js.nft.json +1 -1
- package/.next/server/app/api/jobs/[id]/route.js +4 -4
- package/.next/server/app/api/jobs/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/jobs/[id]/status/route.js +3 -3
- package/.next/server/app/api/jobs/[id]/status/route.js.nft.json +1 -1
- package/.next/server/app/api/jobs/route.js +9 -9
- package/.next/server/app/api/jobs/route.js.nft.json +1 -1
- package/.next/server/app/api/notify/route.js.nft.json +1 -1
- package/.next/server/app/api/pieces/[pieceId]/duplicate/route.js +4 -4
- package/.next/server/app/api/pieces/[pieceId]/duplicate/route.js.nft.json +1 -1
- package/.next/server/app/api/pieces/[pieceId]/tracks/[trackId]/manual-anchors/apply-deletes/route.js +9 -9
- package/.next/server/app/api/pieces/[pieceId]/tracks/[trackId]/manual-anchors/apply-deletes/route.js.nft.json +1 -1
- package/.next/server/app/api/pieces/[pieceId]/tracks/[trackId]/manual-anchors/route.js +9 -9
- package/.next/server/app/api/pieces/[pieceId]/tracks/[trackId]/manual-anchors/route.js.nft.json +1 -1
- package/.next/server/app/api/render/frames/route.js +7 -7
- package/.next/server/app/api/render/frames/route.js.nft.json +1 -1
- package/.next/server/app/api/runtime/update/route.js +5 -5
- package/.next/server/app/api/runtime/update/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[sessionId]/activate/route.js +8 -8
- package/.next/server/app/api/sessions/[sessionId]/activate/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[sessionId]/context/route.js +8 -8
- package/.next/server/app/api/sessions/[sessionId]/context/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[sessionId]/deactivate/route.js +8 -8
- package/.next/server/app/api/sessions/[sessionId]/deactivate/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[sessionId]/model/route.js +7 -7
- package/.next/server/app/api/sessions/[sessionId]/model/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[sessionId]/permission/route.js +7 -7
- package/.next/server/app/api/sessions/[sessionId]/permission/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/permission-modes/route.js +7 -7
- package/.next/server/app/api/sessions/permission-modes/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js +7 -7
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/settings/mcp-servers/[id]/dependencies/route.js +7 -7
- package/.next/server/app/api/settings/mcp-servers/[id]/dependencies/route.js.nft.json +1 -1
- package/.next/server/app/api/settings/mcp-servers/[id]/install-plan/route.js +5 -5
- package/.next/server/app/api/settings/mcp-servers/[id]/install-plan/route.js.nft.json +1 -1
- package/.next/server/app/api/settings/mcp-servers/[id]/retry/route.js +7 -7
- package/.next/server/app/api/settings/mcp-servers/[id]/retry/route.js.nft.json +1 -1
- package/.next/server/app/api/settings/mcp-servers/[id]/retry-dep/route.js +7 -7
- package/.next/server/app/api/settings/mcp-servers/[id]/retry-dep/route.js.nft.json +1 -1
- package/.next/server/app/api/settings/mcp-servers/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/settings/mcp-servers/resync/route.js +7 -7
- package/.next/server/app/api/settings/mcp-servers/resync/route.js.nft.json +1 -1
- package/.next/server/app/api/settings/memories/route.js.nft.json +1 -1
- package/.next/server/app/api/skill-eval/configure/route.js +8 -8
- package/.next/server/app/api/skill-eval/configure/route.js.nft.json +1 -1
- package/.next/server/app/api/terminal/sessions/route.js +2 -2
- package/.next/server/app/api/terminal/ws-info/route.js.nft.json +1 -1
- package/.next/server/app/api/tracking/run/route.js +4 -4
- package/.next/server/app/api/tracking/run/route.js.nft.json +1 -1
- package/.next/server/app/page/build-manifest.json +2 -2
- package/.next/server/chunks/[root-of-the-server]__04thdbt._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__04thdbt._.js.map +2 -2
- package/.next/server/chunks/[root-of-the-server]__05bfu4m._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__05bfu4m._.js.map +2 -2
- package/.next/server/chunks/[root-of-the-server]__09qyvq1._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__09qyvq1._.js.map +2 -2
- package/.next/server/chunks/[root-of-the-server]__0gnut6f._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0gnut6f._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0l_-iy7._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0l_-iy7._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0s0b6ty._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0s0b6ty._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0sj68ta._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0sj68ta._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__10oxitf._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__10oxitf._.js.map +2 -2
- package/.next/server/chunks/[root-of-the-server]__11.cp_g._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__11.cp_g._.js.map +2 -2
- package/.next/server/chunks/_04i643i._.js +3 -3
- package/.next/server/chunks/_0_t4b-f._.js +4 -4
- package/.next/server/chunks/_0gc3hkv._.js +3 -3
- package/.next/server/chunks/_0hainf0._.js +4 -4
- package/.next/server/chunks/_0jrwb4i._.js +3 -3
- package/.next/server/chunks/_0ms~zo_._.js +3 -3
- package/.next/server/chunks/_0ou5g0c._.js +4 -4
- package/.next/server/chunks/_0ovzm6d._.js +4 -4
- package/.next/server/chunks/_0pwx1vo._.js +3 -3
- package/.next/server/chunks/_0snzqta._.js +4 -4
- package/.next/server/chunks/lib_08x49mw._.js +3 -3
- package/.next/server/chunks/lib_runtime_runtime-prune_ts_0eaw~-q._.js +3 -3
- package/.next/server/chunks/node_modules_003ody_._.js +3 -3
- package/.next/server/chunks/ssr/_0675jux._.js +3 -3
- package/.next/server/instrumentation.js.nft.json +1 -1
- package/.next/server/middleware-build-manifest.js +5 -5
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{0c4f164pt1cw~.js → 0rgnvt2t.fexk.js} +3 -3
- package/.next/static/chunks/{turbopack-07eje62zh5_9e.js → turbopack-06tait4frhc5a.js} +3 -3
- package/.next/trace-build +1 -1
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/dist-cli/BUILD_INFO.json +6 -6
- package/dist-cli/lib/analytics/config.js +1 -1
- package/dist-cli/lib/install/next-externals.js +6 -1
- package/dist-cli/lib/onboarding/piece/v1/index.js +5 -1
- package/dist-cli/lib/server/next-server.js +12 -2
- package/lib/analytics/config.ts +15 -2
- package/lib/install/next-externals.ts +36 -1
- package/lib/onboarding/piece/v1/index.ts +5 -1
- package/lib/server/next-server.ts +29 -4
- package/package.json +1 -1
- /package/.next/static/{fX0xUAoWQHHhMjl4jpDOL → 05vWgPQtQKJ6EuLHg8nZ3}/_buildManifest.js +0 -0
- /package/.next/static/{fX0xUAoWQHHhMjl4jpDOL → 05vWgPQtQKJ6EuLHg8nZ3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{fX0xUAoWQHHhMjl4jpDOL → 05vWgPQtQKJ6EuLHg8nZ3}/_ssgManifest.js +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": [],
|
|
4
|
-
"debugId": "
|
|
4
|
+
"debugId": "582cc164-d340-885e-1b8a-766994ab4929",
|
|
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/test-mode.ts","../../../lib/shell-path-cache.ts","../../../lib/uv-env/spawn-env.ts","../../../lib/runtime/compiled-entry.ts","../../../lib/runtime/package-root.ts","../../../mcp/registry/local-bin-resolver.ts","../../../lib/security/secret-scrub.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 * Centralized test-mode detection. Returns true when the user launched libi\n * with `LIBI_TEST_MODE=1 npx libi`. Used by:\n * - lib/mcp-config.ts to swap fal-ai's transport to the fake-fal stdio\n * server (mcp/dev/fake-fal/) so the agent uses real fal-ai tool names\n * but gets deterministic placeholder outputs at zero cost\n * - mcp/instructions.ts for the TEST MODE banner\n *\n * Read at every call site rather than cached so test code can stub the env\n * via vi.stubEnv() without rebuilding modules.\n */\nexport function isTestMode(): boolean {\n const v = process.env.LIBI_TEST_MODE;\n return v === \"1\" || v === \"true\";\n}\n","/**\n * The login-shell PATH cache written by `electron/path-bootstrap.ts`.\n *\n * A GUI-launched Electron app inherits a minimal PATH — not the one the user's\n * shell profile builds. `path-bootstrap.ts` resolves the real login-shell PATH\n * once at startup and caches it at `<LIBI_HOME>/shell-path-cache.json`; this is\n * the reader every consumer shares.\n *\n * **Never re-probe the login shell to refresh this.** `path-bootstrap.ts`\n * documents a packaged boot that hung for 10+ minutes inside a TCC-blocked\n * `/bin/zsh` which no timeout could kill. Reading the cache is free; respawning\n * the shell is not, and has already cost a hang in the field.\n *\n * A cache written under a different `$SHELL` is still better evidence about the\n * user's environment than this process's own PATH, so it is read leniently and\n * an absent/corrupt cache degrades to an empty list rather than throwing.\n */\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport { getLibiHome } from \"@/lib/libi-home\";\nimport { isWindows } from \"@/lib/platform\";\n\n/** PATH list separator for this platform. */\nexport function pathSeparator(): string {\n return isWindows() ? \";\" : \":\";\n}\n\n/**\n * PATH entries from the login-shell cache, or `[]` when there is none.\n *\n * Absent is normal, not an error: only the Electron shell writes this file, so\n * an `npx @nagellabs/libi` run has no cache at all. In that case the process's\n * OWN PATH is the honest source — the user launched it from their shell.\n */\nexport function cachedShellPathDirs(): string[] {\n try {\n const raw = fs.readFileSync(\n path.join(getLibiHome(), \"shell-path-cache.json\"),\n \"utf8\",\n );\n const parsed = JSON.parse(raw) as { path?: unknown };\n if (typeof parsed.path !== \"string\") return [];\n return parsed.path.split(pathSeparator()).filter(Boolean);\n } catch {\n return [];\n }\n}\n\n/**\n * Directories a user-invoked command would be searched in, most-honest source\n * first: the login-shell cache (what the user's own terminal sees), then this\n * process's PATH as a fallback.\n */\nexport function userCommandSearchDirs(): string[] {\n const own = (process.env.PATH ?? \"\").split(pathSeparator()).filter(Boolean);\n return [...cachedShellPathDirs(), ...own];\n}\n","/**\n * The ONE place that decides where uv-managed state lives, and the ONE way a\n * uv subprocess is spawned.\n *\n * ## Why this exists\n *\n * Left to its defaults, `uv` writes to the *user's* home, not libi's:\n *\n * - `~/.cache/uv` — wheels, sdists, git checkouts, and the\n * ephemeral `uv run --with …` environments.\n * Measured at 2.6 GB on a dev machine, almost\n * all of it torch pulled in by libi's whisper /\n * Kokoro / ACE-Step runners.\n * - `~/.local/share/uv` — managed CPython interpreters + `uv tool`\n * installs (yt-dlp).\n * - `<project>/.venv` — the project environment for `uv sync`.\n *\n * The last one is the dangerous default. The tracking sidecar's project dir is\n * `mcp/tracking/py/` INSIDE the shipped package, so `uv sync` planted a 1.2 GB\n * venv inside the signed `.app`\n * (`Resources/libi-bundle/node_modules/@nagellabs/libi/mcp/tracking/py/.venv`)\n * — breaking the code signature and failing outright on a read-only install.\n * The packaged app's own directory must be treated as READ-ONLY.\n *\n * The other two are not a signing break, but they leak multi-gigabyte,\n * libi-created state into the user's personal uv cache where it cannot be\n * attributed to libi and survives uninstalling it.\n *\n * So: every uv invocation in libi routes through `buildUvEnv()`, which pins all\n * four locations under `<LIBI_HOME>/uv/`. Because the paths are computed from\n * `getLibiHome()` alone, dev, the packaged Electron app, and `npx\n * @nagellabs/libi` all resolve through this single code path with no\n * per-environment branching.\n *\n * ## Sharing across worktrees\n *\n * A git worktree gets its own `LIBI_HOME` (`~/.libi/worktrees/<name>/`), which\n * would naively mean re-downloading gigabytes of torch per worktree.\n * `lib/dev/worktree-bootstrap.ts` already solves exactly this for `bin`,\n * `models` and `node_modules` by symlinking them to the canonical `~/.libi`,\n * and `\"uv\"` is registered in that same `SHARED_LINKS` list. So there is one uv\n * state tree per machine, libi owns it, and `rm -rf ~/.libi` reclaims all of\n * it — without inventing a second sharing mechanism.\n */\n\nimport path from \"node:path\";\nimport { getLibiBinDir, getLibiHome } from \"@/lib/libi-home\";\nimport { isWindows } from \"@/lib/platform\";\n\n/** Root of every uv-managed artifact libi creates. */\nexport function uvStateRoot(): string {\n return path.join(getLibiHome(), \"uv\");\n}\n\n/** `UV_CACHE_DIR` — wheels, sdists, and `uv run --with` ephemeral envs. */\nexport function uvCacheDir(): string {\n return path.join(uvStateRoot(), \"cache\");\n}\n\n/** `UV_PYTHON_INSTALL_DIR` — uv-managed CPython interpreters. */\nexport function uvPythonInstallDir(): string {\n return path.join(uvStateRoot(), \"python\");\n}\n\n/** `UV_TOOL_DIR` — `uv tool install` root (yt-dlp). */\nexport function uvToolDir(): string {\n return path.join(uvStateRoot(), \"tools\");\n}\n\n/** `UV_TOOL_BIN_DIR` — entry points written by `uv tool install`. */\nexport function uvToolBinDir(): string {\n return path.join(uvStateRoot(), \"tools-bin\");\n}\n\n/**\n * `PYTHONPYCACHEPREFIX` — where CPython writes `.pyc` files.\n *\n * Python's default is a `__pycache__/` directory NEXT TO THE SOURCE. Every\n * Python entry point libi runs (`mcp/tracking/py/track_runner.py`,\n * `mcp/whisper/transcribe.py`, `mcp/tts/synthesize.py`, `mcp/music/*.py`)\n * lives in the shipped package, so the default silently writes into the signed\n * `.app` — the same defect class as the venv, just smaller. The `__pycache__`\n * exclusion in `package.json`'s `files` list is evidence these were being\n * created in-tree.\n *\n * Redirecting (rather than disabling via PYTHONDONTWRITEBYTECODE) keeps the\n * bytecode cache, so a large dependency tree like torch is not recompiled on\n * every run.\n */\nexport function pythonPycachePrefix(): string {\n return path.join(uvStateRoot(), \"pycache\");\n}\n\n/**\n * `YOLO_CONFIG_DIR` — ultralytics' settings/assets directory.\n *\n * Ultralytics (loaded by the tracking sidecar's YOLOE detector) falls back\n * through `~/Library/Application Support/Ultralytics` → `/tmp/Ultralytics` →\n * `Path.cwd()/Ultralytics`, and its `weights_dir` / `runs_dir` defaults are\n * cwd-RELATIVE. The sidecar's cwd is the shipped package, so an asset download\n * or a config-dir fallback would write into the signed `.app`. No such write\n * fires today (libi passes absolute model paths), which makes this latent\n * rather than active — pinned here so it stays that way.\n */\nexport function yoloConfigDir(): string {\n return path.join(uvStateRoot(), \"ultralytics\");\n}\n\n/**\n * `UV_PROJECT_ENVIRONMENT` for the tracking sidecar (`mcp/tracking/py`).\n *\n * This is the venv that used to land inside the package. It is keyed by\n * project, not by caller, so `uv sync` (the installer) and `uv run` (the\n * runners + selftest) all address the same environment.\n */\nexport function trackingVenvDir(): string {\n return path.join(uvStateRoot(), \"envs\", \"tracking\");\n}\n\n/**\n * The `UV_*` variables that redirect uv away from the user's home.\n *\n * These deliberately OVERRIDE any inherited value: a stray `UV_CACHE_DIR` in\n * the user's shell must not silently relocate libi's state and reintroduce the\n * drift this module exists to remove.\n */\nexport function uvEnvVars(): Record<string, string> {\n return {\n UV_CACHE_DIR: uvCacheDir(),\n UV_PYTHON_INSTALL_DIR: uvPythonInstallDir(),\n UV_TOOL_DIR: uvToolDir(),\n UV_TOOL_BIN_DIR: uvToolBinDir(),\n };\n}\n\n/**\n * Build the environment for a uv subprocess.\n *\n * Inherits `process.env` (uv and the Python it spawns need HOME, TMPDIR, LANG,\n * proxy vars, …), prepends `<LIBI_HOME>/bin` to PATH so uv-spawned children\n * find libi's ffmpeg/uv/node, then pins the `UV_*` locations.\n *\n * `extra` is layered last so a caller can add job-specific variables\n * (`UV_PROJECT_ENVIRONMENT`, `LIBI_TRACK_MODELS`, `NO_COLOR`, …).\n *\n * ALL uv spawns must go through this — see\n * `__tests__/unit/uv-env/install-path-invariants.test.ts`, which fails the\n * build if a new `uv` spawn site forgets it.\n */\nexport function buildUvEnv(\n extra: Record<string, string> = {},\n): NodeJS.ProcessEnv {\n // Built by mutating a spread of `process.env` rather than assembled as one\n // object literal. `NodeJS.ProcessEnv` is augmented in this project to\n // REQUIRE `NODE_ENV`, so a fresh literal — however complete at runtime —\n // cannot satisfy the type, which is why this used to end in an\n // `as NodeJS.ProcessEnv` that did not actually typecheck. Spreading\n // `process.env` carries that required key through, so no cast is needed and\n // nothing is silenced.\n const env: NodeJS.ProcessEnv = { ...process.env };\n\n // Drop non-string values so the result is a clean string map, matching what\n // every spawn call site expects to hand to child_process.\n for (const key of Object.keys(env)) {\n if (typeof env[key] !== \"string\") delete env[key];\n }\n\n const pathSep = isWindows() ? \";\" : \":\";\n env.PATH = `${getLibiBinDir()}${pathSep}${process.env.PATH ?? \"\"}`;\n\n Object.assign(env, uvEnvVars(), {\n // Keeps CPython from writing bytecode next to the shipped .py sources.\n PYTHONPYCACHEPREFIX: pythonPycachePrefix(),\n // Harmless to processes that never load ultralytics; set on the shared\n // seam rather than the three tracking spawns so it cannot drift.\n YOLO_CONFIG_DIR: yoloConfigDir(),\n });\n Object.assign(env, extra);\n\n return env;\n}\n","/**\n * Dual-mode resolution for the child processes libi spawns from its own tree\n * (the core MCP server, the standalone tracking MCP).\n *\n * ## The two modes\n *\n * **Source + tsx** — a dev checkout and the packaged Electron app both run\n * libi's `.ts` directly through tsx. Nothing here changes that.\n *\n * **Compiled** — an `npm install` / `npx @nagellabs/libi` copy CANNOT. tsx\n * applies the tsconfig `paths` matcher only when the importing file's own path\n * has no `node_modules` segment (verified in `node_modules/tsx/dist/register-*.cjs`),\n * and an installed package is entirely under one — so every `@/…` import\n * throws MODULE_NOT_FOUND before a single line of libi runs. The packaged\n * Electron app is only exempt by accident of layout (`Contents/Resources/app/`\n * has no such segment), which is why this stayed invisible.\n * `scripts/build-cli.js` pre-resolves those specifiers into `dist-cli/`; this\n * module is what points the spawns at it.\n *\n * ## Why the switch is \"am I under node_modules\", not \"does dist-cli exist\"\n *\n * Preferring the compiled tree whenever it happens to be present would mean a\n * dev who once ran `npm run build:cli` silently starts debugging STALE\n * compiled JS instead of their working tree — the drift trap. The\n * node_modules test is the exact condition that breaks tsx, so it selects\n * compiled output precisely when source cannot work, and never otherwise.\n */\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport { resolveNodeCommand } from \"@/lib/runtime/node-runtime\";\n\n/** Directory holding the compiled mirror, relative to the package root. */\nexport const COMPILED_DIR = \"dist-cli\";\n\n/**\n * True when this copy of libi lives under a `node_modules` directory — i.e.\n * it was installed as a dependency, and tsx will refuse to resolve its `@/…`\n * imports.\n */\nexport function isInstalledUnderNodeModules(root: string): boolean {\n return root.split(path.sep).includes(\"node_modules\");\n}\n\n/** `mcp/index.ts` → `<root>/dist-cli/mcp/index.js`. */\nexport function compiledEntryPath(root: string, sourceRel: string): string {\n return path.join(root, COMPILED_DIR, sourceRel.replace(/\\.tsx?$/, \".js\"));\n}\n\nexport interface ResolvedEntrySpawn {\n command: string;\n args: string[];\n mode: \"tsx\" | \"compiled\";\n}\n\n/**\n * Resolve how to spawn one of libi's own entry points, or `null` when neither\n * mode is available (callers throw their own diagnostic — the two call sites\n * word it differently and both messages are load-bearing).\n *\n * `sourceRel` is the entry's path relative to the package root, e.g.\n * `mcp/index.ts`.\n *\n * `command` is `resolveNodeCommand()` in both modes — never `process.execPath`:\n * under Electron `electronFuses.runAsNode: false` makes the packaged binary\n * ignore `ELECTRON_RUN_AS_NODE`, so spawning it launches a second full Libi\n * GUI instead of a Node child.\n */\nexport function resolveEntrySpawn(\n projectRoot: string,\n sourceRel: string,\n): ResolvedEntrySpawn | null {\n const entryPoint = path.join(projectRoot, sourceRel);\n const tsxCli = path.join(projectRoot, \"node_modules\", \"tsx\", \"dist\", \"cli.mjs\");\n\n if (\n !isInstalledUnderNodeModules(projectRoot) &&\n fs.existsSync(entryPoint) &&\n fs.existsSync(tsxCli)\n ) {\n return {\n command: resolveNodeCommand(),\n args: [tsxCli, \"--tsconfig\", path.join(projectRoot, \"tsconfig.json\"), entryPoint],\n mode: \"tsx\",\n };\n }\n\n const compiled = compiledEntryPath(projectRoot, sourceRel);\n if (fs.existsSync(compiled)) {\n return { command: resolveNodeCommand(), args: [compiled], mode: \"compiled\" };\n }\n\n return null;\n}\n\n/** Shared tail for both call sites' \"neither mode resolved\" diagnostics. */\nexport function entryResolutionDiagnostic(projectRoot: string, sourceRel: string): string {\n return (\n `expected either ${path.join(projectRoot, sourceRel)} + ` +\n `${path.join(projectRoot, \"node_modules\", \"tsx\", \"dist\", \"cli.mjs\")} (source mode), or ` +\n `${compiledEntryPath(projectRoot, sourceRel)} (compiled mode, built by \\`npm run build:cli\\` ` +\n `and shipped in the npm tarball). Check electron-builder.yml's files allowlist for a packaged ` +\n `build, or package.json's files allowlist for an npm install.`\n );\n}\n","/**\n * Where is libi's own package root, from any module inside it?\n *\n * Several modules resolve package-relative data files (`mcp/whisper/*.py`,\n * `mcp/templates/instructions.md`, the `projectRoot` the production server\n * chdirs to) from their OWN `__dirname` rather than `process.cwd()` — cwd is\n * the user's launch directory under `npx @nagellabs/libi`, and the agent\n * workspace (`~/.libi/agent/`) for the MCP child, so it cannot be trusted.\n *\n * Hardcoding the hop count (`path.resolve(__dirname, \"..\", \"..\")`) makes that\n * arithmetic depend on where the module physically sits — which is no longer\n * a constant: `scripts/build-cli.js` emits a compiled mirror of `lib/` and\n * `mcp/` under `dist-cli/`, one level deeper than the source tree, for the\n * npm-installed run mode (see that script's header for why compiling is\n * required at all). A `../..` that is right in dev is off by one there.\n *\n * Walking up to the nearest `package.json` is depth-independent and correct in\n * every run mode: dev checkout, compiled `dist-cli/`, packaged Electron\n * (`Contents/Resources/app/`), and an installed\n * `node_modules/@nagellabs/libi/`. There is no `package.json` inside\n * `dist-cli/`, `lib/` or `mcp/`, so the first hit is always the real root.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\n/** Max ancestors to inspect. Deep enough for any real layout, bounded so a\n * pathological path can't spin. */\nconst MAX_DEPTH = 12;\n\n/**\n * Nearest ancestor of `fromDir` (inclusive) containing a `package.json`, or\n * `null` when there is none.\n *\n * Returns null — rather than guessing — under a Turbopack-bundled build,\n * where `__dirname` is the build-time placeholder `/ROOT/...` that does not\n * exist at runtime. Callers pick their own fallback for that case.\n */\nexport function findPackageRoot(fromDir: string): string | null {\n let dir = fromDir;\n for (let i = 0; i < MAX_DEPTH; i++) {\n try {\n if (fs.statSync(path.join(dir, \"package.json\")).isFile()) return dir;\n } catch {\n /* keep walking */\n }\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return null;\n}\n\n/**\n * libi's package root, falling back to `process.cwd()` when the walk finds\n * nothing (the Turbopack-bundled case — both entry points normalise cwd to the\n * app root before anything reaches here).\n */\nexport function packageRoot(fromDir: string = __dirname): string {\n return findPackageRoot(fromDir) ?? process.cwd();\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { getLibiNodeModulesRoot } from \"@/lib/libi-home\";\nimport { resolveNodeCommand } from \"@/lib/runtime/node-runtime\";\nimport { adapterBinFileNames } from \"@/lib/agents/runtime-install\";\nimport { packageRoot } from \"@/lib/runtime/package-root\";\nimport {\n entryResolutionDiagnostic,\n resolveEntrySpawn,\n} from \"@/lib/runtime/compiled-entry\";\nimport type { BundledMcpDef } from \"./types\";\n\nexport interface ResolvedSpawn {\n command: string;\n args: string[];\n /** \"local\" when the pinned package is installed under ~/.libi/node_modules/,\n * \"in-repo\" when the def is an in-repo MCP and we resolved its tsx-direct\n * entry from the source tree,\n * \"fallback\" when we couldn't find it and we're falling back to the def's\n * `command`/`args` (typically `npx`). */\n source: \"local\" | \"in-repo\" | \"fallback\";\n}\n\n/**\n * Resolve the spawn command for a bundled MCP.\n *\n * For defs that declare `npmPackage` + `pinnedVersion`, prefers the locally\n * installed copy under `~/.libi/node_modules/`. This is the bullet-proof\n * path — no `npx` registry roundtrip, no npm-lock contention with parallel\n * spawns, sub-100 ms cold start.\n *\n * Falls back to the def's own `command`/`args` (e.g. `npx -y <pkg>@<ver>`)\n * when:\n * - the def has no `npmPackage` (e.g. uvx- or HTTP-based MCPs)\n * - the install hasn't happened yet (fresh boot before Category B's\n * bundled-install step completes)\n * - the installed version doesn't match the pinned version (drift —\n * bundled-install will reconcile on the next run)\n *\n * The `binName` defaults to the last segment of `npmPackage`. The package's\n * `bin` field is consulted via the bin shim that npm/pnpm writes to\n * `node_modules/.bin/<binName>` at install time, so the resolver doesn't\n * need to read package.json itself.\n */\nexport function resolveBundledSpawn(def: BundledMcpDef): ResolvedSpawn {\n const fallback: ResolvedSpawn = {\n command: def.command,\n args: def.args ?? [],\n source: \"fallback\",\n };\n\n // In-repo MCPs (e.g. libi-tracking) ship inside the libi package itself —\n // they are NOT separate npm installs, and their DB-row fallback is\n // `npx libi serve-mcp-*`, which fails in dev because `libi` isn't on PATH\n // and is a latent security exposure in a packaged build (see below). When\n // the source tree + tsx are present, resolve the tsx-direct entry so EVERY\n // spawn path (prober, diagnose, session) launches it correctly — exactly\n // how the core libi server's tsx entry is used everywhere.\n //\n // tsx is resolved as `node_modules/tsx/dist/cli.mjs` — the ACTUAL package's\n // CLI entry point — run via `resolveNodeCommand()` (the libi-managed\n // `<LIBI_HOME>/bin/node` when present, else the bare name), NOT\n // `node_modules/.bin/tsx`.\n // electron-builder copies NO `.bin/` directories into the packaged app\n // (verified against the built bundle), so the bin shim this used to\n // resolve is always absent there even though `node_modules/tsx` itself is\n // present. Mirrors `buildLibiEntry()` in `lib/mcp-config.ts` exactly —\n // read that function's doc comment for the full rationale (including why\n // `command` is `\"node\"` and never `process.execPath`).\n if (def.inRepoEntry) {\n // libi's own package root, resolved by walking up from THIS module's\n // `__dirname` — NOT `process.cwd()`.\n //\n // cwd is libi's package root only in the Next.js SERVER process (a dev\n // checkout, `lib/cli/studio.ts`'s production chdir, `electron/main.ts`'s\n // chdir to the runtime root). But two of this function's three call sites\n // run in the libi MCP CHILD: `libi.diagnose_mcp`\n // (`mcp/bundled-mcps/diagnose.ts`) and the install/probe path\n // (`mcp/registry/server-prober.ts`, reached from\n // `mcp/bundled-mcps/install-tools.ts`). The ACP adapter spawns that child\n // with cwd = the AGENT WORKSPACE (`~/.libi/agent/`) — see the same note in\n // `mcp/bundled-mcps/install-tools.ts` and `mcp/registry/spawn-env.ts` —\n // which holds no `mcp/`, no `node_modules/tsx`, and no `dist-cli/`, so\n // every in-repo resolution from those paths threw the diagnostic below\n // instead of returning a spawn command.\n //\n // `packageRoot()` is depth-independent (dev tree, the compiled `dist-cli/`\n // mirror one level deeper, packaged Electron, and an installed\n // `node_modules/@nagellabs/libi/`) and falls back to `process.cwd()` only\n // under a Turbopack-bundled build, where `__dirname` is a placeholder —\n // that is exactly the Next.js server, where cwd is already correct.\n const projectRoot = packageRoot();\n // Dual mode, identical to `buildLibiEntry()`: source+tsx in dev and in the\n // packaged Electron app; the compiled `dist-cli/` twin when libi is\n // installed under node_modules, where tsx refuses to resolve `@/…` at all.\n // See lib/runtime/compiled-entry.ts.\n const resolved = resolveEntrySpawn(projectRoot, def.inRepoEntry);\n if (resolved) {\n return { command: resolved.command, args: resolved.args, source: \"in-repo\" };\n }\n // There is deliberately no `npx libi ...` fallback here, for the same\n // reason `buildLibiEntry()` has none: this repo is `private: true` and\n // unpublished, and the `libi` name on the public npm registry belongs to\n // an unrelated third party with no `bin` field today. `buildSpawnEnv()`\n // hands an MCP child the ENTIRE process env (`mcp/registry/spawn-env.ts`),\n // so silently shelling out to a name libi doesn't own is a latent\n // security exposure independent of whether the fallback happens to\n // \"work\" today. `mcp/` + `node_modules/tsx` are both guaranteed to exist\n // (dev tree, or the packaged app's `files` allowlist — electron-builder.yml\n // / package.json), so a missing entry point/tsx CLI here means a broken\n // build — fail loudly with a diagnostic instead of reaching for that\n // fallback. This function is called from the settings/ACP spawn path AND\n // from `libi.diagnose_mcp` (mcp/bundled-mcps/diagnose.ts) and the\n // install/probe path (mcp/registry/server-prober.ts), so closing the gap\n // here closes it everywhere.\n throw new Error(\n `resolveBundledSpawn: could not resolve the in-repo MCP entry point for \"${def.id}\" — ` +\n `${entryResolutionDiagnostic(projectRoot, def.inRepoEntry)} ` +\n `Deliberately not falling back to \\`${def.command}${(def.args ?? []).length ? \" \" + (def.args ?? []).join(\" \") : \"\"}\\`: ` +\n `this repo is unpublished and the public \\`libi\\` npm package belongs to an unrelated third party.`,\n );\n }\n\n if (!def.npmPackage || !def.pinnedVersion) return fallback;\n\n const root = getLibiNodeModulesRoot();\n const binName = def.binName ?? lastSegment(def.npmPackage);\n // `resolveBinIn`, not `path.join(.bin, binName)`. On Windows npm's cmd-shim\n // writes THREE files per bin: an extensionless `<name>` that is a BASH\n // script, plus `<name>.cmd` and `<name>.ps1`. The extensionless one exists,\n // so a bare existsSync passes and we would hand `spawn()` a bash script it\n // cannot execute (ENOEXEC) — and the `def.command` fallback is a bare `npx`,\n // which is `npx.cmd` on Windows and equally unspawnable without a shell. So\n // the failure was silent and total: every npm-backed bundled MCP down, with\n // a resolver that believed it had found the binary.\n // Windows-only widening. `adapterBinFileNames` supplies the shim names\n // (`.cmd`/`.exe` on win32, the bare name elsewhere); the EXISTENCE check is\n // kept exactly as it was, deliberately. `resolveBinIn` — the sibling helper\n // that shares the name list — additionally requires X_OK on Unix, and\n // swapping to it here would have quietly tightened a shipped code path from\n // \"exists\" to \"exists and is executable\" on mac and Linux. That is a\n // different change, with its own risk, and it is not this one.\n const binDirPath = path.join(root, \"node_modules\", \".bin\");\n const binPath =\n adapterBinFileNames(binName, process.platform)\n .map((f) => path.join(binDirPath, f))\n .find((c) => fs.existsSync(c)) ?? null;\n const pkgJsonPath = path.join(\n root,\n \"node_modules\",\n ...def.npmPackage.split(\"/\"),\n \"package.json\",\n );\n\n let installedVersion: string | null = null;\n try {\n const raw = fs.readFileSync(pkgJsonPath, \"utf-8\");\n const parsed: unknown = JSON.parse(raw);\n if (\n parsed &&\n typeof parsed === \"object\" &&\n \"version\" in parsed &&\n typeof (parsed as { version: unknown }).version === \"string\"\n ) {\n installedVersion = (parsed as { version: string }).version;\n }\n } catch {\n return fallback;\n }\n\n if (installedVersion !== def.pinnedVersion) return fallback;\n if (!binPath) return fallback;\n\n return { command: binPath, args: [], source: \"local\" };\n}\n\nfunction lastSegment(npmPackage: string): string {\n const trimmed = npmPackage.startsWith(\"@\")\n ? npmPackage.split(\"/\").slice(1).join(\"/\")\n : npmPackage;\n return trimmed.split(\"/\").pop() ?? npmPackage;\n}\n","/**\n * Replace every occurrence of each configured secret in `text` with a mask, so\n * captured subprocess output (e.g. an MCP child's stderr) can be persisted /\n * surfaced without leaking API keys. Pure + allocation-cheap; safe on empty\n * inputs.\n *\n * @param text Arbitrary captured text (may be empty).\n * @param secrets Secret values to redact (empty / falsy entries are ignored).\n */\nexport function scrubSecrets(text: string, secrets: string[]): string {\n if (!text) return text;\n let out = text;\n for (const secret of secrets) {\n if (!secret) continue;\n // split/join replaces ALL occurrences without regex-escaping the secret.\n out = out.split(secret).join(\"••••\");\n }\n return out;\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","isTestMode","LIBI_TEST_MODE","pathSeparator","cachedShellPathDirs","raw","readFileSync","join","parsed","path","split","filter","Boolean","userCommandSearchDirs","own","PATH","uvStateRoot","uvCacheDir","uvPythonInstallDir","uvToolDir","uvToolBinDir","pythonPycachePrefix","yoloConfigDir","trackingVenvDir","uvEnvVars","UV_CACHE_DIR","UV_PYTHON_INSTALL_DIR","UV_TOOL_DIR","UV_TOOL_BIN_DIR","buildUvEnv","extra","key","keys","pathSep","assign","PYTHONPYCACHEPREFIX","YOLO_CONFIG_DIR","COMPILED_DIR","isInstalledUnderNodeModules","root","sep","includes","compiledEntryPath","sourceRel","replace","resolveEntrySpawn","projectRoot","entryPoint","tsxCli","existsSync","command","args","mode","compiled","entryResolutionDiagnostic","MAX_DEPTH","findPackageRoot","fromDir","dir","i","statSync","isFile","parent","dirname","packageRoot","cwd","resolveBundledSpawn","def","fallback","source","inRepoEntry","resolved","Error","npmPackage","pinnedVersion","binName","lastSegment","binDirPath","binPath","platform","f","find","c","pkgJsonPath","installedVersion","version","trimmed","startsWith","pop","scrubSecrets","text","secrets","secret"],"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/test-mode.ts","../../../lib/shell-path-cache.ts","../../../lib/uv-env/spawn-env.ts","../../../lib/runtime/compiled-entry.ts","../../../lib/runtime/package-root.ts","../../../mcp/registry/local-bin-resolver.ts","../../../lib/security/secret-scrub.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 * Centralized test-mode detection. Returns true when the user launched libi\n * with `LIBI_TEST_MODE=1 npx libi`. Used by:\n * - lib/mcp-config.ts to swap fal-ai's transport to the fake-fal stdio\n * server (mcp/dev/fake-fal/) so the agent uses real fal-ai tool names\n * but gets deterministic placeholder outputs at zero cost\n * - mcp/instructions.ts for the TEST MODE banner\n *\n * Read at every call site rather than cached so test code can stub the env\n * via vi.stubEnv() without rebuilding modules.\n */\nexport function isTestMode(): boolean {\n const v = process.env.LIBI_TEST_MODE;\n return v === \"1\" || v === \"true\";\n}\n","/**\n * The login-shell PATH cache written by `electron/path-bootstrap.ts`.\n *\n * A GUI-launched Electron app inherits a minimal PATH — not the one the user's\n * shell profile builds. `path-bootstrap.ts` resolves the real login-shell PATH\n * once at startup and caches it at `<LIBI_HOME>/shell-path-cache.json`; this is\n * the reader every consumer shares.\n *\n * **Never re-probe the login shell to refresh this.** `path-bootstrap.ts`\n * documents a packaged boot that hung for 10+ minutes inside a TCC-blocked\n * `/bin/zsh` which no timeout could kill. Reading the cache is free; respawning\n * the shell is not, and has already cost a hang in the field.\n *\n * A cache written under a different `$SHELL` is still better evidence about the\n * user's environment than this process's own PATH, so it is read leniently and\n * an absent/corrupt cache degrades to an empty list rather than throwing.\n */\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport { getLibiHome } from \"@/lib/libi-home\";\nimport { isWindows } from \"@/lib/platform\";\n\n/** PATH list separator for this platform. */\nexport function pathSeparator(): string {\n return isWindows() ? \";\" : \":\";\n}\n\n/**\n * PATH entries from the login-shell cache, or `[]` when there is none.\n *\n * Absent is normal, not an error: only the Electron shell writes this file, so\n * an `npx @nagellabs/libi` run has no cache at all. In that case the process's\n * OWN PATH is the honest source — the user launched it from their shell.\n */\nexport function cachedShellPathDirs(): string[] {\n try {\n const raw = fs.readFileSync(\n path.join(getLibiHome(), \"shell-path-cache.json\"),\n \"utf8\",\n );\n const parsed = JSON.parse(raw) as { path?: unknown };\n if (typeof parsed.path !== \"string\") return [];\n return parsed.path.split(pathSeparator()).filter(Boolean);\n } catch {\n return [];\n }\n}\n\n/**\n * Directories a user-invoked command would be searched in, most-honest source\n * first: the login-shell cache (what the user's own terminal sees), then this\n * process's PATH as a fallback.\n */\nexport function userCommandSearchDirs(): string[] {\n const own = (process.env.PATH ?? \"\").split(pathSeparator()).filter(Boolean);\n return [...cachedShellPathDirs(), ...own];\n}\n","/**\n * The ONE place that decides where uv-managed state lives, and the ONE way a\n * uv subprocess is spawned.\n *\n * ## Why this exists\n *\n * Left to its defaults, `uv` writes to the *user's* home, not libi's:\n *\n * - `~/.cache/uv` — wheels, sdists, git checkouts, and the\n * ephemeral `uv run --with …` environments.\n * Measured at 2.6 GB on a dev machine, almost\n * all of it torch pulled in by libi's whisper /\n * Kokoro / ACE-Step runners.\n * - `~/.local/share/uv` — managed CPython interpreters + `uv tool`\n * installs (yt-dlp).\n * - `<project>/.venv` — the project environment for `uv sync`.\n *\n * The last one is the dangerous default. The tracking sidecar's project dir is\n * `mcp/tracking/py/` INSIDE the shipped package, so `uv sync` planted a 1.2 GB\n * venv inside the signed `.app`\n * (`Resources/libi-bundle/node_modules/@nagellabs/libi/mcp/tracking/py/.venv`)\n * — breaking the code signature and failing outright on a read-only install.\n * The packaged app's own directory must be treated as READ-ONLY.\n *\n * The other two are not a signing break, but they leak multi-gigabyte,\n * libi-created state into the user's personal uv cache where it cannot be\n * attributed to libi and survives uninstalling it.\n *\n * So: every uv invocation in libi routes through `buildUvEnv()`, which pins all\n * four locations under `<LIBI_HOME>/uv/`. Because the paths are computed from\n * `getLibiHome()` alone, dev, the packaged Electron app, and `npx\n * @nagellabs/libi` all resolve through this single code path with no\n * per-environment branching.\n *\n * ## Sharing across worktrees\n *\n * A git worktree gets its own `LIBI_HOME` (`~/.libi/worktrees/<name>/`), which\n * would naively mean re-downloading gigabytes of torch per worktree.\n * `lib/dev/worktree-bootstrap.ts` already solves exactly this for `bin`,\n * `models` and `node_modules` by symlinking them to the canonical `~/.libi`,\n * and `\"uv\"` is registered in that same `SHARED_LINKS` list. So there is one uv\n * state tree per machine, libi owns it, and `rm -rf ~/.libi` reclaims all of\n * it — without inventing a second sharing mechanism.\n */\n\nimport path from \"node:path\";\nimport { getLibiBinDir, getLibiHome } from \"@/lib/libi-home\";\nimport { isWindows } from \"@/lib/platform\";\n\n/** Root of every uv-managed artifact libi creates. */\nexport function uvStateRoot(): string {\n return path.join(getLibiHome(), \"uv\");\n}\n\n/** `UV_CACHE_DIR` — wheels, sdists, and `uv run --with` ephemeral envs. */\nexport function uvCacheDir(): string {\n return path.join(uvStateRoot(), \"cache\");\n}\n\n/** `UV_PYTHON_INSTALL_DIR` — uv-managed CPython interpreters. */\nexport function uvPythonInstallDir(): string {\n return path.join(uvStateRoot(), \"python\");\n}\n\n/** `UV_TOOL_DIR` — `uv tool install` root (yt-dlp). */\nexport function uvToolDir(): string {\n return path.join(uvStateRoot(), \"tools\");\n}\n\n/** `UV_TOOL_BIN_DIR` — entry points written by `uv tool install`. */\nexport function uvToolBinDir(): string {\n return path.join(uvStateRoot(), \"tools-bin\");\n}\n\n/**\n * `PYTHONPYCACHEPREFIX` — where CPython writes `.pyc` files.\n *\n * Python's default is a `__pycache__/` directory NEXT TO THE SOURCE. Every\n * Python entry point libi runs (`mcp/tracking/py/track_runner.py`,\n * `mcp/whisper/transcribe.py`, `mcp/tts/synthesize.py`, `mcp/music/*.py`)\n * lives in the shipped package, so the default silently writes into the signed\n * `.app` — the same defect class as the venv, just smaller. The `__pycache__`\n * exclusion in `package.json`'s `files` list is evidence these were being\n * created in-tree.\n *\n * Redirecting (rather than disabling via PYTHONDONTWRITEBYTECODE) keeps the\n * bytecode cache, so a large dependency tree like torch is not recompiled on\n * every run.\n */\nexport function pythonPycachePrefix(): string {\n return path.join(uvStateRoot(), \"pycache\");\n}\n\n/**\n * `YOLO_CONFIG_DIR` — ultralytics' settings/assets directory.\n *\n * Ultralytics (loaded by the tracking sidecar's YOLOE detector) falls back\n * through `~/Library/Application Support/Ultralytics` → `/tmp/Ultralytics` →\n * `Path.cwd()/Ultralytics`, and its `weights_dir` / `runs_dir` defaults are\n * cwd-RELATIVE. The sidecar's cwd is the shipped package, so an asset download\n * or a config-dir fallback would write into the signed `.app`. No such write\n * fires today (libi passes absolute model paths), which makes this latent\n * rather than active — pinned here so it stays that way.\n */\nexport function yoloConfigDir(): string {\n return path.join(uvStateRoot(), \"ultralytics\");\n}\n\n/**\n * `UV_PROJECT_ENVIRONMENT` for the tracking sidecar (`mcp/tracking/py`).\n *\n * This is the venv that used to land inside the package. It is keyed by\n * project, not by caller, so `uv sync` (the installer) and `uv run` (the\n * runners + selftest) all address the same environment.\n */\nexport function trackingVenvDir(): string {\n return path.join(uvStateRoot(), \"envs\", \"tracking\");\n}\n\n/**\n * The `UV_*` variables that redirect uv away from the user's home.\n *\n * These deliberately OVERRIDE any inherited value: a stray `UV_CACHE_DIR` in\n * the user's shell must not silently relocate libi's state and reintroduce the\n * drift this module exists to remove.\n */\nexport function uvEnvVars(): Record<string, string> {\n return {\n UV_CACHE_DIR: uvCacheDir(),\n UV_PYTHON_INSTALL_DIR: uvPythonInstallDir(),\n UV_TOOL_DIR: uvToolDir(),\n UV_TOOL_BIN_DIR: uvToolBinDir(),\n };\n}\n\n/**\n * Build the environment for a uv subprocess.\n *\n * Inherits `process.env` (uv and the Python it spawns need HOME, TMPDIR, LANG,\n * proxy vars, …), prepends `<LIBI_HOME>/bin` to PATH so uv-spawned children\n * find libi's ffmpeg/uv/node, then pins the `UV_*` locations.\n *\n * `extra` is layered last so a caller can add job-specific variables\n * (`UV_PROJECT_ENVIRONMENT`, `LIBI_TRACK_MODELS`, `NO_COLOR`, …).\n *\n * ALL uv spawns must go through this — see\n * `__tests__/unit/uv-env/install-path-invariants.test.ts`, which fails the\n * build if a new `uv` spawn site forgets it.\n */\nexport function buildUvEnv(\n extra: Record<string, string> = {},\n): NodeJS.ProcessEnv {\n // Built by mutating a spread of `process.env` rather than assembled as one\n // object literal. `NodeJS.ProcessEnv` is augmented in this project to\n // REQUIRE `NODE_ENV`, so a fresh literal — however complete at runtime —\n // cannot satisfy the type, which is why this used to end in an\n // `as NodeJS.ProcessEnv` that did not actually typecheck. Spreading\n // `process.env` carries that required key through, so no cast is needed and\n // nothing is silenced.\n const env: NodeJS.ProcessEnv = { ...process.env };\n\n // Drop non-string values so the result is a clean string map, matching what\n // every spawn call site expects to hand to child_process.\n for (const key of Object.keys(env)) {\n if (typeof env[key] !== \"string\") delete env[key];\n }\n\n const pathSep = isWindows() ? \";\" : \":\";\n env.PATH = `${getLibiBinDir()}${pathSep}${process.env.PATH ?? \"\"}`;\n\n Object.assign(env, uvEnvVars(), {\n // Keeps CPython from writing bytecode next to the shipped .py sources.\n PYTHONPYCACHEPREFIX: pythonPycachePrefix(),\n // Harmless to processes that never load ultralytics; set on the shared\n // seam rather than the three tracking spawns so it cannot drift.\n YOLO_CONFIG_DIR: yoloConfigDir(),\n });\n Object.assign(env, extra);\n\n return env;\n}\n","/**\n * Dual-mode resolution for the child processes libi spawns from its own tree\n * (the core MCP server, the standalone tracking MCP).\n *\n * ## The two modes\n *\n * **Source + tsx** — a dev checkout and the packaged Electron app both run\n * libi's `.ts` directly through tsx. Nothing here changes that.\n *\n * **Compiled** — an `npm install` / `npx @nagellabs/libi` copy CANNOT. tsx\n * applies the tsconfig `paths` matcher only when the importing file's own path\n * has no `node_modules` segment (verified in `node_modules/tsx/dist/register-*.cjs`),\n * and an installed package is entirely under one — so every `@/…` import\n * throws MODULE_NOT_FOUND before a single line of libi runs. The packaged\n * Electron app is only exempt by accident of layout (`Contents/Resources/app/`\n * has no such segment), which is why this stayed invisible.\n * `scripts/build-cli.js` pre-resolves those specifiers into `dist-cli/`; this\n * module is what points the spawns at it.\n *\n * ## Why the switch is \"am I under node_modules\", not \"does dist-cli exist\"\n *\n * Preferring the compiled tree whenever it happens to be present would mean a\n * dev who once ran `npm run build:cli` silently starts debugging STALE\n * compiled JS instead of their working tree — the drift trap. The\n * node_modules test is the exact condition that breaks tsx, so it selects\n * compiled output precisely when source cannot work, and never otherwise.\n */\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport { resolveNodeCommand } from \"@/lib/runtime/node-runtime\";\n\n/** Directory holding the compiled mirror, relative to the package root. */\nexport const COMPILED_DIR = \"dist-cli\";\n\n/**\n * True when this copy of libi lives under a `node_modules` directory — i.e.\n * it was installed as a dependency, and tsx will refuse to resolve its `@/…`\n * imports.\n */\nexport function isInstalledUnderNodeModules(root: string): boolean {\n return root.split(path.sep).includes(\"node_modules\");\n}\n\n/** `mcp/index.ts` → `<root>/dist-cli/mcp/index.js`. */\nexport function compiledEntryPath(root: string, sourceRel: string): string {\n return path.join(root, COMPILED_DIR, sourceRel.replace(/\\.tsx?$/, \".js\"));\n}\n\nexport interface ResolvedEntrySpawn {\n command: string;\n args: string[];\n mode: \"tsx\" | \"compiled\";\n}\n\n/**\n * Resolve how to spawn one of libi's own entry points, or `null` when neither\n * mode is available (callers throw their own diagnostic — the two call sites\n * word it differently and both messages are load-bearing).\n *\n * `sourceRel` is the entry's path relative to the package root, e.g.\n * `mcp/index.ts`.\n *\n * `command` is `resolveNodeCommand()` in both modes — never `process.execPath`:\n * under Electron `electronFuses.runAsNode: false` makes the packaged binary\n * ignore `ELECTRON_RUN_AS_NODE`, so spawning it launches a second full Libi\n * GUI instead of a Node child.\n */\nexport function resolveEntrySpawn(\n projectRoot: string,\n sourceRel: string,\n): ResolvedEntrySpawn | null {\n const entryPoint = path.join(projectRoot, sourceRel);\n const tsxCli = path.join(projectRoot, \"node_modules\", \"tsx\", \"dist\", \"cli.mjs\");\n\n if (\n !isInstalledUnderNodeModules(projectRoot) &&\n fs.existsSync(entryPoint) &&\n fs.existsSync(tsxCli)\n ) {\n return {\n command: resolveNodeCommand(),\n args: [tsxCli, \"--tsconfig\", path.join(projectRoot, \"tsconfig.json\"), entryPoint],\n mode: \"tsx\",\n };\n }\n\n const compiled = compiledEntryPath(projectRoot, sourceRel);\n if (fs.existsSync(compiled)) {\n return { command: resolveNodeCommand(), args: [compiled], mode: \"compiled\" };\n }\n\n return null;\n}\n\n/** Shared tail for both call sites' \"neither mode resolved\" diagnostics. */\nexport function entryResolutionDiagnostic(projectRoot: string, sourceRel: string): string {\n return (\n `expected either ${path.join(projectRoot, sourceRel)} + ` +\n `${path.join(projectRoot, \"node_modules\", \"tsx\", \"dist\", \"cli.mjs\")} (source mode), or ` +\n `${compiledEntryPath(projectRoot, sourceRel)} (compiled mode, built by \\`npm run build:cli\\` ` +\n `and shipped in the npm tarball). Check electron-builder.yml's files allowlist for a packaged ` +\n `build, or package.json's files allowlist for an npm install.`\n );\n}\n","/**\n * Where is libi's own package root, from any module inside it?\n *\n * Several modules resolve package-relative data files (`mcp/whisper/*.py`,\n * `mcp/templates/instructions.md`, the `projectRoot` the production server\n * chdirs to) from their OWN `__dirname` rather than `process.cwd()` — cwd is\n * the user's launch directory under `npx @nagellabs/libi`, and the agent\n * workspace (`~/.libi/agent/`) for the MCP child, so it cannot be trusted.\n *\n * Hardcoding the hop count (`path.resolve(__dirname, \"..\", \"..\")`) makes that\n * arithmetic depend on where the module physically sits — which is no longer\n * a constant: `scripts/build-cli.js` emits a compiled mirror of `lib/` and\n * `mcp/` under `dist-cli/`, one level deeper than the source tree, for the\n * npm-installed run mode (see that script's header for why compiling is\n * required at all). A `../..` that is right in dev is off by one there.\n *\n * Walking up to the nearest `package.json` is depth-independent and correct in\n * every run mode: dev checkout, compiled `dist-cli/`, packaged Electron\n * (`Contents/Resources/app/`), and an installed\n * `node_modules/@nagellabs/libi/`. There is no `package.json` inside\n * `dist-cli/`, `lib/` or `mcp/`, so the first hit is always the real root.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\n/** Max ancestors to inspect. Deep enough for any real layout, bounded so a\n * pathological path can't spin. */\nconst MAX_DEPTH = 12;\n\n/**\n * Nearest ancestor of `fromDir` (inclusive) containing a `package.json`, or\n * `null` when there is none.\n *\n * Returns null — rather than guessing — under a Turbopack-bundled build,\n * where `__dirname` is the build-time placeholder `/ROOT/...` that does not\n * exist at runtime. Callers pick their own fallback for that case.\n */\nexport function findPackageRoot(fromDir: string): string | null {\n let dir = fromDir;\n for (let i = 0; i < MAX_DEPTH; i++) {\n try {\n if (fs.statSync(path.join(dir, \"package.json\")).isFile()) return dir;\n } catch {\n /* keep walking */\n }\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return null;\n}\n\n/**\n * libi's package root, falling back to `process.cwd()` when the walk finds\n * nothing (the Turbopack-bundled case — both entry points normalise cwd to the\n * app root before anything reaches here).\n */\nexport function packageRoot(fromDir: string = __dirname): string {\n return findPackageRoot(fromDir) ?? process.cwd();\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { getLibiNodeModulesRoot } from \"@/lib/libi-home\";\nimport { resolveNodeCommand } from \"@/lib/runtime/node-runtime\";\nimport { adapterBinFileNames } from \"@/lib/agents/runtime-install\";\nimport { packageRoot } from \"@/lib/runtime/package-root\";\nimport {\n entryResolutionDiagnostic,\n resolveEntrySpawn,\n} from \"@/lib/runtime/compiled-entry\";\nimport type { BundledMcpDef } from \"./types\";\n\nexport interface ResolvedSpawn {\n command: string;\n args: string[];\n /** \"local\" when the pinned package is installed under ~/.libi/node_modules/,\n * \"in-repo\" when the def is an in-repo MCP and we resolved its tsx-direct\n * entry from the source tree,\n * \"fallback\" when we couldn't find it and we're falling back to the def's\n * `command`/`args` (typically `npx`). */\n source: \"local\" | \"in-repo\" | \"fallback\";\n}\n\n/**\n * Resolve the spawn command for a bundled MCP.\n *\n * For defs that declare `npmPackage` + `pinnedVersion`, prefers the locally\n * installed copy under `~/.libi/node_modules/`. This is the bullet-proof\n * path — no `npx` registry roundtrip, no npm-lock contention with parallel\n * spawns, sub-100 ms cold start.\n *\n * Falls back to the def's own `command`/`args` (e.g. `npx -y <pkg>@<ver>`)\n * when:\n * - the def has no `npmPackage` (e.g. uvx- or HTTP-based MCPs)\n * - the install hasn't happened yet (fresh boot before Category B's\n * bundled-install step completes)\n * - the installed version doesn't match the pinned version (drift —\n * bundled-install will reconcile on the next run)\n *\n * The `binName` defaults to the last segment of `npmPackage`. The package's\n * `bin` field is consulted via the bin shim that npm/pnpm writes to\n * `node_modules/.bin/<binName>` at install time, so the resolver doesn't\n * need to read package.json itself.\n */\nexport function resolveBundledSpawn(def: BundledMcpDef): ResolvedSpawn {\n const fallback: ResolvedSpawn = {\n command: def.command,\n args: def.args ?? [],\n source: \"fallback\",\n };\n\n // In-repo MCPs (e.g. libi-tracking) ship inside the libi package itself —\n // they are NOT separate npm installs, and their DB-row fallback is\n // `npx libi serve-mcp-*`, which fails in dev because `libi` isn't on PATH\n // and is a latent security exposure in a packaged build (see below). When\n // the source tree + tsx are present, resolve the tsx-direct entry so EVERY\n // spawn path (prober, diagnose, session) launches it correctly — exactly\n // how the core libi server's tsx entry is used everywhere.\n //\n // tsx is resolved as `node_modules/tsx/dist/cli.mjs` — the ACTUAL package's\n // CLI entry point — run via `resolveNodeCommand()` (the libi-managed\n // `<LIBI_HOME>/bin/node` when present, else the bare name), NOT\n // `node_modules/.bin/tsx`.\n // electron-builder copies NO `.bin/` directories into the packaged app\n // (verified against the built bundle), so the bin shim this used to\n // resolve is always absent there even though `node_modules/tsx` itself is\n // present. Mirrors `buildLibiEntry()` in `lib/mcp-config.ts` exactly —\n // read that function's doc comment for the full rationale (including why\n // `command` is `\"node\"` and never `process.execPath`).\n if (def.inRepoEntry) {\n // libi's own package root, resolved by walking up from THIS module's\n // `__dirname` — NOT `process.cwd()`.\n //\n // cwd is libi's package root only in the Next.js SERVER process (a dev\n // checkout, `lib/cli/studio.ts`'s production chdir, `electron/main.ts`'s\n // chdir to the runtime root). But two of this function's three call sites\n // run in the libi MCP CHILD: `libi.diagnose_mcp`\n // (`mcp/bundled-mcps/diagnose.ts`) and the install/probe path\n // (`mcp/registry/server-prober.ts`, reached from\n // `mcp/bundled-mcps/install-tools.ts`). The ACP adapter spawns that child\n // with cwd = the AGENT WORKSPACE (`~/.libi/agent/`) — see the same note in\n // `mcp/bundled-mcps/install-tools.ts` and `mcp/registry/spawn-env.ts` —\n // which holds no `mcp/`, no `node_modules/tsx`, and no `dist-cli/`, so\n // every in-repo resolution from those paths threw the diagnostic below\n // instead of returning a spawn command.\n //\n // `packageRoot()` is depth-independent (dev tree, the compiled `dist-cli/`\n // mirror one level deeper, packaged Electron, and an installed\n // `node_modules/@nagellabs/libi/`) and falls back to `process.cwd()` only\n // under a Turbopack-bundled build, where `__dirname` is a placeholder —\n // that is exactly the Next.js server, where cwd is already correct.\n const projectRoot = packageRoot();\n // Dual mode, identical to `buildLibiEntry()`: source+tsx in dev and in the\n // packaged Electron app; the compiled `dist-cli/` twin when libi is\n // installed under node_modules, where tsx refuses to resolve `@/…` at all.\n // See lib/runtime/compiled-entry.ts.\n const resolved = resolveEntrySpawn(projectRoot, def.inRepoEntry);\n if (resolved) {\n return { command: resolved.command, args: resolved.args, source: \"in-repo\" };\n }\n // There is deliberately no `npx libi ...` fallback here, for the same\n // reason `buildLibiEntry()` has none: this repo is `private: true` and\n // unpublished, and the `libi` name on the public npm registry belongs to\n // an unrelated third party with no `bin` field today. `buildSpawnEnv()`\n // hands an MCP child the ENTIRE process env (`mcp/registry/spawn-env.ts`),\n // so silently shelling out to a name libi doesn't own is a latent\n // security exposure independent of whether the fallback happens to\n // \"work\" today. `mcp/` + `node_modules/tsx` are both guaranteed to exist\n // (dev tree, or the packaged app's `files` allowlist — electron-builder.yml\n // / package.json), so a missing entry point/tsx CLI here means a broken\n // build — fail loudly with a diagnostic instead of reaching for that\n // fallback. This function is called from the settings/ACP spawn path AND\n // from `libi.diagnose_mcp` (mcp/bundled-mcps/diagnose.ts) and the\n // install/probe path (mcp/registry/server-prober.ts), so closing the gap\n // here closes it everywhere.\n throw new Error(\n `resolveBundledSpawn: could not resolve the in-repo MCP entry point for \"${def.id}\" — ` +\n `${entryResolutionDiagnostic(projectRoot, def.inRepoEntry)} ` +\n `Deliberately not falling back to \\`${def.command}${(def.args ?? []).length ? \" \" + (def.args ?? []).join(\" \") : \"\"}\\`: ` +\n `this repo is unpublished and the public \\`libi\\` npm package belongs to an unrelated third party.`,\n );\n }\n\n if (!def.npmPackage || !def.pinnedVersion) return fallback;\n\n const root = getLibiNodeModulesRoot();\n const binName = def.binName ?? lastSegment(def.npmPackage);\n // `resolveBinIn`, not `path.join(.bin, binName)`. On Windows npm's cmd-shim\n // writes THREE files per bin: an extensionless `<name>` that is a BASH\n // script, plus `<name>.cmd` and `<name>.ps1`. The extensionless one exists,\n // so a bare existsSync passes and we would hand `spawn()` a bash script it\n // cannot execute (ENOEXEC) — and the `def.command` fallback is a bare `npx`,\n // which is `npx.cmd` on Windows and equally unspawnable without a shell. So\n // the failure was silent and total: every npm-backed bundled MCP down, with\n // a resolver that believed it had found the binary.\n // Windows-only widening. `adapterBinFileNames` supplies the shim names\n // (`.cmd`/`.exe` on win32, the bare name elsewhere); the EXISTENCE check is\n // kept exactly as it was, deliberately. `resolveBinIn` — the sibling helper\n // that shares the name list — additionally requires X_OK on Unix, and\n // swapping to it here would have quietly tightened a shipped code path from\n // \"exists\" to \"exists and is executable\" on mac and Linux. That is a\n // different change, with its own risk, and it is not this one.\n const binDirPath = path.join(root, \"node_modules\", \".bin\");\n const binPath =\n adapterBinFileNames(binName, process.platform)\n .map((f) => path.join(binDirPath, f))\n .find((c) => fs.existsSync(c)) ?? null;\n const pkgJsonPath = path.join(\n root,\n \"node_modules\",\n ...def.npmPackage.split(\"/\"),\n \"package.json\",\n );\n\n let installedVersion: string | null = null;\n try {\n const raw = fs.readFileSync(pkgJsonPath, \"utf-8\");\n const parsed: unknown = JSON.parse(raw);\n if (\n parsed &&\n typeof parsed === \"object\" &&\n \"version\" in parsed &&\n typeof (parsed as { version: unknown }).version === \"string\"\n ) {\n installedVersion = (parsed as { version: string }).version;\n }\n } catch {\n return fallback;\n }\n\n if (installedVersion !== def.pinnedVersion) return fallback;\n if (!binPath) return fallback;\n\n return { command: binPath, args: [], source: \"local\" };\n}\n\nfunction lastSegment(npmPackage: string): string {\n const trimmed = npmPackage.startsWith(\"@\")\n ? npmPackage.split(\"/\").slice(1).join(\"/\")\n : npmPackage;\n return trimmed.split(\"/\").pop() ?? npmPackage;\n}\n","/**\n * Replace every occurrence of each configured secret in `text` with a mask, so\n * captured subprocess output (e.g. an MCP child's stderr) can be persisted /\n * surfaced without leaking API keys. Pure + allocation-cheap; safe on empty\n * inputs.\n *\n * @param text Arbitrary captured text (may be empty).\n * @param secrets Secret values to redact (empty / falsy entries are ignored).\n */\nexport function scrubSecrets(text: string, secrets: string[]): string {\n if (!text) return text;\n let out = text;\n for (const secret of secrets) {\n if (!secret) continue;\n // split/join replaces ALL occurrences without regex-escaping the secret.\n out = out.split(secret).join(\"••••\");\n }\n return out;\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","isTestMode","LIBI_TEST_MODE","pathSeparator","cachedShellPathDirs","raw","readFileSync","join","parsed","path","split","filter","Boolean","userCommandSearchDirs","own","PATH","uvStateRoot","uvCacheDir","uvPythonInstallDir","uvToolDir","uvToolBinDir","pythonPycachePrefix","yoloConfigDir","trackingVenvDir","uvEnvVars","UV_CACHE_DIR","UV_PYTHON_INSTALL_DIR","UV_TOOL_DIR","UV_TOOL_BIN_DIR","buildUvEnv","extra","key","keys","pathSep","assign","PYTHONPYCACHEPREFIX","YOLO_CONFIG_DIR","COMPILED_DIR","isInstalledUnderNodeModules","root","sep","includes","compiledEntryPath","sourceRel","replace","resolveEntrySpawn","projectRoot","entryPoint","tsxCli","existsSync","command","args","mode","compiled","entryResolutionDiagnostic","MAX_DEPTH","findPackageRoot","fromDir","dir","i","statSync","isFile","parent","dirname","packageRoot","cwd","resolveBundledSpawn","def","fallback","source","inRepoEntry","resolved","Error","npmPackage","pinnedVersion","binName","lastSegment","binDirPath","binPath","platform","f","find","c","pkgJsonPath","installedVersion","version","trimmed","startsWith","pop","scrubSecrets","text","secrets","secret"],"mappings":"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"}}]
|
|
7
7
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
;!function(){try { var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof global?global:"undefined"!=typeof window?window:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&((e._debugIds|| (e._debugIds={}))[n]="
|
|
2
|
-
module.exports=[38657,e=>{"use strict";var t;process.env.NEXT_PUBLIC_LIBI_GA4_MEASUREMENT_ID;let r="1"!==(t=process.env).LIBI_ANALYTICS_DISABLED&&"1"===t.NEXT_PUBLIC_LIBI_ANALYTICS;process.env.NODE_ENV;let i="1"===process.env.LIBI_ANALYTICS_DISABLED;e.s(["ANALYTICS_ENABLED",0,r,"ANALYTICS_KILL_SWITCH",0,i])},260338,e=>{"use strict";e.i(156325),e.i(844988);var t=e.i(518899);e.s(["count",0,function(e){return t.sql`count(${e||t.sql.raw("*")})`.mapWith(Number)}])},884724,e=>{"use strict";let t=new Set(["first_launch","first_export","persona_selected","agent_connected","onboarding_piece_built","persona_prompt_shown","agent_connect_shown","agent_setup_started","agent_setup_command_copied","agent_install_completed","agent_install_failed","agent_sign_in_opened","first_message_sent","piece_created","export_started","export_completed","agent_message_sent","file_uploaded","terminal_session_started","mcp_server_added","codex_connect_toggled","runtime_update_installed","version_restore","overlay_added","overlay_edited","preset_applied","effect_applied","effect_previewed","text_3d_used","overlay_3d_used","chat_slash_command","context_meter_opened","page_view","page_engagement","tool_used","pro_waitlist_joined","announcement_shown","analytics_opt_out","analytics_opt_in"]);e.s(["isEventName",0,function(e){return t.has(e)},"sanitizeParams",0,function(e){if(!e)return{};let t={};for(let[r,i]of Object.entries(e))null!=i&&("string"==typeof i?t[r]=i.slice(0,100):"number"==typeof i||"boolean"==typeof i?t[r]=i:t[r]=String(i).slice(0,100));return t},"toAgentEventId",0,function(e){return"claude-code"===e||"codex"===e?e:void 0}])},991858,e=>{"use strict";var t=e.i(349129),r=e.i(260338),i=e.i(875225),n=e.i(190793),o=e.i(478181),s=e.i(884724),a=e.i(883978),l=e.i(38657),d=e.i(692855);e.i(640911),e.s(["trackServerEvent",0,function(e,u){try{!function(e,u){try{if((0,d.isTestMode)()||l.ANALYTICS_KILL_SWITCH||!(0,a.getAnalyticsSettings)().enabled)return;let c=(0,s.sanitizeParams)(u),p=(0,n.getDb)();p.insert(o.analyticsQueue).values({id:crypto.randomUUID(),name:e,paramsJson:JSON.stringify(c),nextAttemptAt:new Date(0)}).run();let[{n:f}]=p.select({n:(0,r.count)()}).from(o.analyticsQueue).all();if(f>500){let e=p.select({id:o.analyticsQueue.id}).from(o.analyticsQueue).orderBy((0,t.asc)(o.analyticsQueue.createdAt)).limit(f-500).all().map(e=>e.id);p.delete(o.analyticsQueue).where((0,i.inArray)(o.analyticsQueue.id,e)).run()}}catch{}}(e,u)}catch{}}],991858)},727195,74199,e=>{"use strict";var t=e.i(902157),r=e.i(750227),i=e.i(875225),n=e.i(190793),o=e.i(478181),s=e.i(270958),a=e.i(640911),l=e.i(452183);e.s(["dropProxyFile",0,function(e,d="user"){let u=(0,n.getDb)(),[c]=u.select().from(o.files).where((0,i.eq)(o.files.id,e)).limit(1).all();if(c&&("idle"!==c.proxyStatus||c.proxyFilename)){if(a.proxyLogger.info({fileId:e,event:"drop",reason:d},"proxy.drop"),c.proxyFilename){let e=r.default.join((0,s.getLibiStorageDir)(),c.pieceId??"_global",c.proxyFilename);try{t.default.unlinkSync(e)}catch{}}u.update(o.files).set({proxyFilename:null,proxyStatus:"idle",proxyGeneratedAt:null}).where((0,i.eq)(o.files.id,e)).run(),c.pieceId?l.navigationEmitter.emit("refresh_query",{queryKey:"piece",pieceId:c.pieceId}):l.navigationEmitter.emit("refresh_query",{queryKey:"files"})}}],727195),e.s(["dropFilmstripFile",0,function(e,d="user"){let u=(0,n.getDb)(),[c]=u.select().from(o.files).where((0,i.eq)(o.files.id,e)).limit(1).all();if(c&&("idle"!==c.filmstripStatus||c.filmstripFilename)){if(a.proxyLogger.info({fileId:e,event:"drop",reason:d,tag:"filmstrip"},"filmstrip.drop"),c.filmstripFilename){let e=r.default.join((0,s.getLibiStorageDir)(),c.pieceId??"_global",c.filmstripFilename);try{t.default.unlinkSync(e)}catch{}}u.update(o.files).set({filmstripFilename:null,filmstripStatus:"idle",filmstripGeneratedAt:null,filmstripFrames:null,filmstripHeight:null}).where((0,i.eq)(o.files.id,e)).run(),c.pieceId?l.navigationEmitter.emit("refresh_query",{queryKey:"piece",pieceId:c.pieceId}):l.navigationEmitter.emit("refresh_query",{queryKey:"files"})}}],74199)},795995,e=>{"use strict";var t=e.i(666680);let r=globalThis,i=r.__libi_render_jobs??new Map;r.__libi_render_jobs||(r.__libi_render_jobs=i),e.s(["RENDER_ABSOLUTE_CAP_MS",0,36e5,"createRenderJob",0,function(e){let r,n,o=(0,t.randomUUID)(),s=(0,t.randomUUID)(),a=new Promise((e,t)=>{r=e,n=t}),l=e.stallTimeoutMs??12e4,d=e.timeoutMs??36e5,u=setTimeout(()=>{let e=i.get(o);if(e&&!e.settled){e.settled=!0,clearTimeout(e.absoluteTimer),i.delete(o);let t=l/1e3,r=e.lastProgress?`Render job ${o} stalled — no progress for ${t}s (frame ${e.lastProgress.done}/${e.lastProgress.total})`:`Render job ${o} stalled — no progress reported within ${t}s`;n(Error(r))}},l),c=setTimeout(()=>{let e=i.get(o);e&&!e.settled&&(e.settled=!0,clearTimeout(e.stallTimer),i.delete(o),n(Error(`Render job ${o} exceeded the ${d/6e4}-minute absolute cap`)))},d);return i.set(o,{jobId:o,token:s,pieceId:e.pieceId,payload:e.payload,settings:e.settings,resolve:r,reject:n,stallTimer:u,absoluteTimer:c,stallTimeoutMs:l,lastProgress:null,settled:!1,onProgress:e.onProgress}),{jobId:o,token:s,done:a}},"getRenderJob",0,function(e,t){let r=i.get(e);return!r||r.settled||r.token!==t?null:r},"getRenderJobTokenByJobId",0,function(e){let t=i.get(e);return!t||t.settled?null:{token:t.token}},"isRenderJobPending",0,function(e){let t=i.get(e);return!!t&&!t.settled},"recordRenderProgress",0,function(e,t,r,n){let o=i.get(e);!o||o.token!==t||o.settled||(o.lastProgress={done:r,total:n},clearTimeout(o.stallTimer),o.stallTimer=setTimeout(()=>{let t=i.get(e);if(t&&!t.settled){t.settled=!0,clearTimeout(t.absoluteTimer),i.delete(e);let r=t.stallTimeoutMs/1e3,n=t.lastProgress?`Render job ${e} stalled — no progress for ${r}s (frame ${t.lastProgress.done}/${t.lastProgress.total})`:`Render job ${e} stalled — no progress reported within ${r}s`;t.reject(Error(n))}},o.stallTimeoutMs),o.onProgress?.(r,n))},"rejectRenderJob",0,function(e,t,r){let n=i.get(e);return!!n&&!n.settled&&n.token===t&&(n.settled=!0,clearTimeout(n.stallTimer),clearTimeout(n.absoluteTimer),i.delete(e),n.reject(Error(r)),!0)},"resolveRenderJob",0,function(e,t,r){let n=i.get(e);return!!n&&!n.settled&&n.token===t&&(n.settled=!0,clearTimeout(n.stallTimer),clearTimeout(n.absoluteTimer),i.delete(e),n.resolve(r),!0)}])},816423,e=>{"use strict";var t=e.i(666680);let r=globalThis.__libiTrackJobs??new Map;globalThis.__libiTrackJobs=r,e.s(["createTrackJob",0,function(e){let i,n,o=(0,t.randomUUID)(),s=(0,t.randomUUID)(),a=new Promise((e,t)=>{i=e,n=t}),l=setTimeout(()=>{let e=r.get(o);e&&!e.settled&&(e.settled=!0,r.delete(o),n(Error(`Track job ${o} timeout`)))},e.timeoutMs??6e5);return r.set(o,{jobId:o,token:s,pieceId:e.pieceId,payload:e.payload,resolve:i,reject:n,timeout:l,settled:!1}),{jobId:o,token:s,done:a}},"getTrackJob",0,function(e,t){let i=r.get(e);return!i||i.settled||i.token!==t?null:i},"rejectTrackJob",0,function(e,t,i){let n=r.get(e);return!!n&&!n.settled&&n.token===t&&(n.settled=!0,clearTimeout(n.timeout),r.delete(e),n.reject(Error(i)),!0)},"resolveTrackJob",0,function(e,t,i){let n=r.get(e);return!!n&&!n.settled&&n.token===t&&(n.settled=!0,clearTimeout(n.timeout),r.delete(e),n.resolve(i),!0)}])},478500,(e,t,r)=>{t.exports=e.x("node:async_hooks",()=>require("node:async_hooks"))},81111,(e,t,r)=>{t.exports=e.x("node:stream",()=>require("node:stream"))},761095,(e,t,r)=>{t.exports=e.x("node:net",()=>require("node:net"))},347299,(e,t,r)=>{t.exports=e.x("node:http",()=>require("node:http"))},677652,(e,t,r)=>{t.exports=e.x("node:diagnostics_channel",()=>require("node:diagnostics_channel"))},685560,(e,t,r)=>{t.exports=e.x("node:tls",()=>require("node:tls"))},727028,(e,t,r)=>{t.exports=e.x("node:zlib",()=>require("node:zlib"))},925127,(e,t,r)=>{t.exports=e.x("node:worker_threads",()=>require("node:worker_threads"))},857764,(e,t,r)=>{t.exports=e.x("node:url",()=>require("node:url"))},254799,(e,t,r)=>{t.exports=e.x("crypto",()=>require("crypto"))},524836,(e,t,r)=>{t.exports=e.x("https",()=>require("https"))},495879,(e,t,r)=>{t.exports=e.x("node:dns",()=>require("node:dns"))},57328,(e,t,r)=>{t.exports=e.x("node:assert",()=>require("node:assert"))},483324,(e,t,r)=>{t.exports=e.x("node:querystring",()=>require("node:querystring"))},951615,(e,t,r)=>{t.exports=e.x("node:buffer",()=>require("node:buffer"))},874096,(e,t,r)=>{t.exports=e.x("node:perf_hooks",()=>require("node:perf_hooks"))},107417,(e,t,r)=>{t.exports=e.x("node:util/types",()=>require("node:util/types"))},246479,(e,t,r)=>{t.exports=e.x("node:http2",()=>require("node:http2"))},241293,(e,t,r)=>{t.exports=e.x("node:console",()=>require("node:console"))},26910,(e,t,r)=>{t.exports=e.x("node:timers",()=>require("node:timers"))},236302,e=>{"use strict";var t=e.i(875225),r=e.i(190793),i=e.i(478181);async function n(){let e,[n]=(0,r.getDb)().select({envVars:i.mcpServers.envVars}).from(i.mcpServers).where((0,t.eq)(i.mcpServers.id,"fal-ai")).limit(1).all();if(!n?.envVars)return null;try{e=JSON.parse(n.envVars)}catch{return null}let o=e.FAL_KEY;return"string"!=typeof o||""===o.trim()?null:o.trim()}e.s(["readFalApiKey",0,n])},163635,e=>{"use strict";async function t(e){let t,{requestId:r,apiKey:i,generatedAt:n,signal:o}=e,s=new URLSearchParams({request_id:r,start:function(e,t=Date.now()){let r=e?Date.parse(e):NaN;return new Date((Number.isFinite(r)?r:t-7776e6)-6048e5).toISOString()}(n)}),a=`https://api.fal.ai/v1/models/billing-events?${s.toString()}`;try{t=await fetch(a,{headers:{Authorization:`Key ${i}`},signal:o})}catch(e){return{kind:"error",message:`fal billing request failed: ${e.message}`}}if(401===t.status||403===t.status)return{kind:"forbidden",message:"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."};if(!t.ok){let e=await t.text().catch(()=>"");return{kind:"error",status:t.status,message:`fal billing API returned HTTP ${t.status}: ${e.slice(0,200)}`}}let l=await t.json().catch(()=>null),d=l?.billing_events?.[0];return d&&"number"==typeof d.cost_estimate_nano_usd&&0!==d.cost_estimate_nano_usd?{kind:"resolved",amountUsd:d.cost_estimate_nano_usd/1e9}:{kind:"pending"}}e.s(["fetchFalCost",0,t])},791180,e=>{"use strict";var t=e.i(875225),r=e.i(190793),i=e.i(478181),n=e.i(124569),o=e.i(589024),s=e.i(452183);async function a(e){let a=(0,r.getDb)(),[l]=await a.delete(i.pieces).where((0,t.eq)(i.pieces.id,e)).returning();if(!l)return!1;let d=await (0,n.getStorage)();return(0,o.removeAnalysisForPiece)(e),await d.deletePieceDir(e),s.navigationEmitter.emit("refresh_query",{queryKey:"pieces"}),s.navigationEmitter.emit("refresh_query",{queryKey:"piece",pieceId:e}),!0}e.s(["deletePieceCompletely",0,a])},577833,e=>{"use strict";var t=e.i(902157),r=e.i(750227),i=e.i(666680),n=e.i(874533),o=e.i(812057),s=e.i(470072),a=e.i(153708),l=e.i(695687),d=e.i(640911);let u=(0,o.promisify)(n.execFile),c="tracking-pyenv@2026-08-01-yoloe-build-at-install";function p(e){return i.default.createHash("sha256").update(t.default.readFileSync(e)).digest("hex").toLowerCase()}function f(){return r.default.join((0,l.trackingModelsDir)(),".install-token")}async function g(e,i,n,o){t.default.mkdirSync(r.default.dirname(n),{recursive:!0}),d.serverLogger.info({tag:"tracking-pyenv",op:"model_download_start",id:o,url:e},`downloading ${o}`);let{tmpPath:s,bytesDownloaded:l,sha256:u}=await (0,a.downloadToFileWithStallGuard)(e,n,{label:o,onRetry:t=>{d.serverLogger.warn({tag:"tracking-pyenv",op:"model_download_retry",id:o,url:e,attempt:t.attempt,maxAttempts:t.maxAttempts,bytesDownloaded:t.bytesDownloaded,err:t.error},`${o} download failed (attempt ${t.attempt}/${t.maxAttempts}), retrying`)}});if(u!==i.toLowerCase())throw t.default.rmSync(s,{force:!0}),Error(`sha256 mismatch for ${o}: expected ${i.toLowerCase()}, got ${u} (downloaded from ${e})`);t.default.renameSync(s,n),d.serverLogger.info({tag:"tracking-pyenv",op:"model_download_done",id:o,dest:n,size:l},`downloaded ${o} (${l} bytes, sha ok)`)}async function m(e,i){if("direct-download"!==e.acquisition)throw Error(`ensureModel only handles direct-download entries; ${e.id} is ${e.acquisition} — route it through ensureBuiltModel`);let n=r.default.join(i,e.dest);if(t.default.existsSync(n)){let t=p(n);if(t===e.sha256.toLowerCase())return void d.serverLogger.info({tag:"tracking-pyenv",op:"model_present",id:e.id,dest:n},`tracking model ${e.id} present (sha matches)`);d.serverLogger.warn({tag:"tracking-pyenv",op:"model_sha_mismatch",id:e.id,dest:n,expected:e.sha256.toLowerCase(),actual:t},`tracking model ${e.id} sha mismatch — re-downloading`)}await g(e.url,e.sha256,n,e.id)}async function y(e,i,n,o=u){let a=e.build;if(!a)throw Error(`model ${e.id} is build-at-install but has no build spec`);let c=r.default.join(n,e.dest),f=function(e,t){let r=e.build;if(!r)throw Error(`model ${e.id} is build-at-install but has no build spec`);let i=t.find(e=>e.id===r.sourceModelId);if(!i)throw Error(`model ${e.id} build.sourceModelId "${r.sourceModelId}" not in models.json`);return{sourcePtSha256:i.sha256.toLowerCase(),textEncoderSha256:r.textEncoder.sha256.toLowerCase(),clipRequirement:r.clipRequirement,classes:[...r.classes],imgsz:r.imgsz,opset:r.opset}}(e,i);if(function(e,r){if(!t.default.existsSync(e))return!1;let i=function(e){try{return JSON.parse(t.default.readFileSync(`${e}.build-info.json`,"utf-8"))}catch{return null}}(e);return!!i?.inputs&&!!i.output?.sha256&&JSON.stringify(i.inputs)===JSON.stringify(r)&&p(e)===i.output.sha256.toLowerCase()}(c,f))return void d.serverLogger.info({tag:"tracking-pyenv",op:"model_present",id:e.id,dest:c},`tracking model ${e.id} present (build-info current)`);let m=i.find(e=>e.id===a.sourceModelId),h=r.default.join(n,m.dest);if(!t.default.existsSync(h))throw Error(`cannot build ${e.dest}: source checkpoint ${m.dest} is not provisioned at ${h} (the direct-download phase must run first)`);let _=r.default.join(n,a.textEncoder.file);t.default.existsSync(_)&&p(_)===a.textEncoder.sha256.toLowerCase()||await g(a.textEncoder.url,a.textEncoder.sha256,_,`${e.id} text encoder`);let w=(0,l.sidecarProjectDir)(),v=["run","--locked","--with",a.clipRequirement,"python","export_yoloe11.py","--pt",h,"--encoder",_,"--out",c,"--classes",a.classes.join(","),"--imgsz",String(a.imgsz),"--opset",String(a.opset)];d.serverLogger.info({tag:"tracking-pyenv",op:"model_build_start",id:e.id,dest:c},`building tracking model ${e.id} (local ONNX export)`);let b="",x="";try{({stdout:b,stderr:x}=await o((0,l.uvPath)(),v,{cwd:w,windowsHide:!0,timeout:9e5,maxBuffer:0x2000000,env:(0,s.buildUvEnv)({UV_PROJECT_ENVIRONMENT:(0,s.trackingVenvDir)(),NO_COLOR:"1"})}))}catch(r){let t=(r.stderr??r.stdout??r.message??"").slice(-2e3);throw Error(`local ONNX export for ${e.dest} failed: ${r.message}
|
|
1
|
+
;!function(){try { var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof global?global:"undefined"!=typeof window?window:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&((e._debugIds|| (e._debugIds={}))[n]="f91c0519-5818-07c6-1b9f-059db6049a81")}catch(e){}}();
|
|
2
|
+
module.exports=[38657,e=>{"use strict";var t,r;process.env.NEXT_PUBLIC_LIBI_GA4_MEASUREMENT_ID;let i="1"!==(t=process.env).LIBI_ANALYTICS_DISABLED&&"1"===t.NEXT_PUBLIC_LIBI_ANALYTICS;"1"===(r=process.env).LIBI_ANALYTICS_DEBUG||r.NODE_ENV;let n="1"===process.env.LIBI_ANALYTICS_DISABLED;e.s(["ANALYTICS_ENABLED",0,i,"ANALYTICS_KILL_SWITCH",0,n])},260338,e=>{"use strict";e.i(156325),e.i(844988);var t=e.i(518899);e.s(["count",0,function(e){return t.sql`count(${e||t.sql.raw("*")})`.mapWith(Number)}])},884724,e=>{"use strict";let t=new Set(["first_launch","first_export","persona_selected","agent_connected","onboarding_piece_built","persona_prompt_shown","agent_connect_shown","agent_setup_started","agent_setup_command_copied","agent_install_completed","agent_install_failed","agent_sign_in_opened","first_message_sent","piece_created","export_started","export_completed","agent_message_sent","file_uploaded","terminal_session_started","mcp_server_added","codex_connect_toggled","runtime_update_installed","version_restore","overlay_added","overlay_edited","preset_applied","effect_applied","effect_previewed","text_3d_used","overlay_3d_used","chat_slash_command","context_meter_opened","page_view","page_engagement","tool_used","pro_waitlist_joined","announcement_shown","analytics_opt_out","analytics_opt_in"]);e.s(["isEventName",0,function(e){return t.has(e)},"sanitizeParams",0,function(e){if(!e)return{};let t={};for(let[r,i]of Object.entries(e))null!=i&&("string"==typeof i?t[r]=i.slice(0,100):"number"==typeof i||"boolean"==typeof i?t[r]=i:t[r]=String(i).slice(0,100));return t},"toAgentEventId",0,function(e){return"claude-code"===e||"codex"===e?e:void 0}])},991858,e=>{"use strict";var t=e.i(349129),r=e.i(260338),i=e.i(875225),n=e.i(190793),o=e.i(478181),s=e.i(884724),a=e.i(883978),l=e.i(38657),d=e.i(692855);e.i(640911),e.s(["trackServerEvent",0,function(e,u){try{!function(e,u){try{if((0,d.isTestMode)()||l.ANALYTICS_KILL_SWITCH||!(0,a.getAnalyticsSettings)().enabled)return;let c=(0,s.sanitizeParams)(u),p=(0,n.getDb)();p.insert(o.analyticsQueue).values({id:crypto.randomUUID(),name:e,paramsJson:JSON.stringify(c),nextAttemptAt:new Date(0)}).run();let[{n:f}]=p.select({n:(0,r.count)()}).from(o.analyticsQueue).all();if(f>500){let e=p.select({id:o.analyticsQueue.id}).from(o.analyticsQueue).orderBy((0,t.asc)(o.analyticsQueue.createdAt)).limit(f-500).all().map(e=>e.id);p.delete(o.analyticsQueue).where((0,i.inArray)(o.analyticsQueue.id,e)).run()}}catch{}}(e,u)}catch{}}],991858)},727195,74199,e=>{"use strict";var t=e.i(902157),r=e.i(750227),i=e.i(875225),n=e.i(190793),o=e.i(478181),s=e.i(270958),a=e.i(640911),l=e.i(452183);e.s(["dropProxyFile",0,function(e,d="user"){let u=(0,n.getDb)(),[c]=u.select().from(o.files).where((0,i.eq)(o.files.id,e)).limit(1).all();if(c&&("idle"!==c.proxyStatus||c.proxyFilename)){if(a.proxyLogger.info({fileId:e,event:"drop",reason:d},"proxy.drop"),c.proxyFilename){let e=r.default.join((0,s.getLibiStorageDir)(),c.pieceId??"_global",c.proxyFilename);try{t.default.unlinkSync(e)}catch{}}u.update(o.files).set({proxyFilename:null,proxyStatus:"idle",proxyGeneratedAt:null}).where((0,i.eq)(o.files.id,e)).run(),c.pieceId?l.navigationEmitter.emit("refresh_query",{queryKey:"piece",pieceId:c.pieceId}):l.navigationEmitter.emit("refresh_query",{queryKey:"files"})}}],727195),e.s(["dropFilmstripFile",0,function(e,d="user"){let u=(0,n.getDb)(),[c]=u.select().from(o.files).where((0,i.eq)(o.files.id,e)).limit(1).all();if(c&&("idle"!==c.filmstripStatus||c.filmstripFilename)){if(a.proxyLogger.info({fileId:e,event:"drop",reason:d,tag:"filmstrip"},"filmstrip.drop"),c.filmstripFilename){let e=r.default.join((0,s.getLibiStorageDir)(),c.pieceId??"_global",c.filmstripFilename);try{t.default.unlinkSync(e)}catch{}}u.update(o.files).set({filmstripFilename:null,filmstripStatus:"idle",filmstripGeneratedAt:null,filmstripFrames:null,filmstripHeight:null}).where((0,i.eq)(o.files.id,e)).run(),c.pieceId?l.navigationEmitter.emit("refresh_query",{queryKey:"piece",pieceId:c.pieceId}):l.navigationEmitter.emit("refresh_query",{queryKey:"files"})}}],74199)},795995,e=>{"use strict";var t=e.i(666680);let r=globalThis,i=r.__libi_render_jobs??new Map;r.__libi_render_jobs||(r.__libi_render_jobs=i),e.s(["RENDER_ABSOLUTE_CAP_MS",0,36e5,"createRenderJob",0,function(e){let r,n,o=(0,t.randomUUID)(),s=(0,t.randomUUID)(),a=new Promise((e,t)=>{r=e,n=t}),l=e.stallTimeoutMs??12e4,d=e.timeoutMs??36e5,u=setTimeout(()=>{let e=i.get(o);if(e&&!e.settled){e.settled=!0,clearTimeout(e.absoluteTimer),i.delete(o);let t=l/1e3,r=e.lastProgress?`Render job ${o} stalled — no progress for ${t}s (frame ${e.lastProgress.done}/${e.lastProgress.total})`:`Render job ${o} stalled — no progress reported within ${t}s`;n(Error(r))}},l),c=setTimeout(()=>{let e=i.get(o);e&&!e.settled&&(e.settled=!0,clearTimeout(e.stallTimer),i.delete(o),n(Error(`Render job ${o} exceeded the ${d/6e4}-minute absolute cap`)))},d);return i.set(o,{jobId:o,token:s,pieceId:e.pieceId,payload:e.payload,settings:e.settings,resolve:r,reject:n,stallTimer:u,absoluteTimer:c,stallTimeoutMs:l,lastProgress:null,settled:!1,onProgress:e.onProgress}),{jobId:o,token:s,done:a}},"getRenderJob",0,function(e,t){let r=i.get(e);return!r||r.settled||r.token!==t?null:r},"getRenderJobTokenByJobId",0,function(e){let t=i.get(e);return!t||t.settled?null:{token:t.token}},"isRenderJobPending",0,function(e){let t=i.get(e);return!!t&&!t.settled},"recordRenderProgress",0,function(e,t,r,n){let o=i.get(e);!o||o.token!==t||o.settled||(o.lastProgress={done:r,total:n},clearTimeout(o.stallTimer),o.stallTimer=setTimeout(()=>{let t=i.get(e);if(t&&!t.settled){t.settled=!0,clearTimeout(t.absoluteTimer),i.delete(e);let r=t.stallTimeoutMs/1e3,n=t.lastProgress?`Render job ${e} stalled — no progress for ${r}s (frame ${t.lastProgress.done}/${t.lastProgress.total})`:`Render job ${e} stalled — no progress reported within ${r}s`;t.reject(Error(n))}},o.stallTimeoutMs),o.onProgress?.(r,n))},"rejectRenderJob",0,function(e,t,r){let n=i.get(e);return!!n&&!n.settled&&n.token===t&&(n.settled=!0,clearTimeout(n.stallTimer),clearTimeout(n.absoluteTimer),i.delete(e),n.reject(Error(r)),!0)},"resolveRenderJob",0,function(e,t,r){let n=i.get(e);return!!n&&!n.settled&&n.token===t&&(n.settled=!0,clearTimeout(n.stallTimer),clearTimeout(n.absoluteTimer),i.delete(e),n.resolve(r),!0)}])},816423,e=>{"use strict";var t=e.i(666680);let r=globalThis.__libiTrackJobs??new Map;globalThis.__libiTrackJobs=r,e.s(["createTrackJob",0,function(e){let i,n,o=(0,t.randomUUID)(),s=(0,t.randomUUID)(),a=new Promise((e,t)=>{i=e,n=t}),l=setTimeout(()=>{let e=r.get(o);e&&!e.settled&&(e.settled=!0,r.delete(o),n(Error(`Track job ${o} timeout`)))},e.timeoutMs??6e5);return r.set(o,{jobId:o,token:s,pieceId:e.pieceId,payload:e.payload,resolve:i,reject:n,timeout:l,settled:!1}),{jobId:o,token:s,done:a}},"getTrackJob",0,function(e,t){let i=r.get(e);return!i||i.settled||i.token!==t?null:i},"rejectTrackJob",0,function(e,t,i){let n=r.get(e);return!!n&&!n.settled&&n.token===t&&(n.settled=!0,clearTimeout(n.timeout),r.delete(e),n.reject(Error(i)),!0)},"resolveTrackJob",0,function(e,t,i){let n=r.get(e);return!!n&&!n.settled&&n.token===t&&(n.settled=!0,clearTimeout(n.timeout),r.delete(e),n.resolve(i),!0)}])},478500,(e,t,r)=>{t.exports=e.x("node:async_hooks",()=>require("node:async_hooks"))},81111,(e,t,r)=>{t.exports=e.x("node:stream",()=>require("node:stream"))},761095,(e,t,r)=>{t.exports=e.x("node:net",()=>require("node:net"))},347299,(e,t,r)=>{t.exports=e.x("node:http",()=>require("node:http"))},677652,(e,t,r)=>{t.exports=e.x("node:diagnostics_channel",()=>require("node:diagnostics_channel"))},685560,(e,t,r)=>{t.exports=e.x("node:tls",()=>require("node:tls"))},727028,(e,t,r)=>{t.exports=e.x("node:zlib",()=>require("node:zlib"))},925127,(e,t,r)=>{t.exports=e.x("node:worker_threads",()=>require("node:worker_threads"))},857764,(e,t,r)=>{t.exports=e.x("node:url",()=>require("node:url"))},254799,(e,t,r)=>{t.exports=e.x("crypto",()=>require("crypto"))},524836,(e,t,r)=>{t.exports=e.x("https",()=>require("https"))},495879,(e,t,r)=>{t.exports=e.x("node:dns",()=>require("node:dns"))},57328,(e,t,r)=>{t.exports=e.x("node:assert",()=>require("node:assert"))},483324,(e,t,r)=>{t.exports=e.x("node:querystring",()=>require("node:querystring"))},951615,(e,t,r)=>{t.exports=e.x("node:buffer",()=>require("node:buffer"))},874096,(e,t,r)=>{t.exports=e.x("node:perf_hooks",()=>require("node:perf_hooks"))},107417,(e,t,r)=>{t.exports=e.x("node:util/types",()=>require("node:util/types"))},246479,(e,t,r)=>{t.exports=e.x("node:http2",()=>require("node:http2"))},241293,(e,t,r)=>{t.exports=e.x("node:console",()=>require("node:console"))},26910,(e,t,r)=>{t.exports=e.x("node:timers",()=>require("node:timers"))},236302,e=>{"use strict";var t=e.i(875225),r=e.i(190793),i=e.i(478181);async function n(){let e,[n]=(0,r.getDb)().select({envVars:i.mcpServers.envVars}).from(i.mcpServers).where((0,t.eq)(i.mcpServers.id,"fal-ai")).limit(1).all();if(!n?.envVars)return null;try{e=JSON.parse(n.envVars)}catch{return null}let o=e.FAL_KEY;return"string"!=typeof o||""===o.trim()?null:o.trim()}e.s(["readFalApiKey",0,n])},163635,e=>{"use strict";async function t(e){let t,{requestId:r,apiKey:i,generatedAt:n,signal:o}=e,s=new URLSearchParams({request_id:r,start:function(e,t=Date.now()){let r=e?Date.parse(e):NaN;return new Date((Number.isFinite(r)?r:t-7776e6)-6048e5).toISOString()}(n)}),a=`https://api.fal.ai/v1/models/billing-events?${s.toString()}`;try{t=await fetch(a,{headers:{Authorization:`Key ${i}`},signal:o})}catch(e){return{kind:"error",message:`fal billing request failed: ${e.message}`}}if(401===t.status||403===t.status)return{kind:"forbidden",message:"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."};if(!t.ok){let e=await t.text().catch(()=>"");return{kind:"error",status:t.status,message:`fal billing API returned HTTP ${t.status}: ${e.slice(0,200)}`}}let l=await t.json().catch(()=>null),d=l?.billing_events?.[0];return d&&"number"==typeof d.cost_estimate_nano_usd&&0!==d.cost_estimate_nano_usd?{kind:"resolved",amountUsd:d.cost_estimate_nano_usd/1e9}:{kind:"pending"}}e.s(["fetchFalCost",0,t])},791180,e=>{"use strict";var t=e.i(875225),r=e.i(190793),i=e.i(478181),n=e.i(124569),o=e.i(589024),s=e.i(452183);async function a(e){let a=(0,r.getDb)(),[l]=await a.delete(i.pieces).where((0,t.eq)(i.pieces.id,e)).returning();if(!l)return!1;let d=await (0,n.getStorage)();return(0,o.removeAnalysisForPiece)(e),await d.deletePieceDir(e),s.navigationEmitter.emit("refresh_query",{queryKey:"pieces"}),s.navigationEmitter.emit("refresh_query",{queryKey:"piece",pieceId:e}),!0}e.s(["deletePieceCompletely",0,a])},577833,e=>{"use strict";var t=e.i(902157),r=e.i(750227),i=e.i(666680),n=e.i(874533),o=e.i(812057),s=e.i(470072),a=e.i(153708),l=e.i(695687),d=e.i(640911);let u=(0,o.promisify)(n.execFile),c="tracking-pyenv@2026-08-01-yoloe-build-at-install";function p(e){return i.default.createHash("sha256").update(t.default.readFileSync(e)).digest("hex").toLowerCase()}function f(){return r.default.join((0,l.trackingModelsDir)(),".install-token")}async function g(e,i,n,o){t.default.mkdirSync(r.default.dirname(n),{recursive:!0}),d.serverLogger.info({tag:"tracking-pyenv",op:"model_download_start",id:o,url:e},`downloading ${o}`);let{tmpPath:s,bytesDownloaded:l,sha256:u}=await (0,a.downloadToFileWithStallGuard)(e,n,{label:o,onRetry:t=>{d.serverLogger.warn({tag:"tracking-pyenv",op:"model_download_retry",id:o,url:e,attempt:t.attempt,maxAttempts:t.maxAttempts,bytesDownloaded:t.bytesDownloaded,err:t.error},`${o} download failed (attempt ${t.attempt}/${t.maxAttempts}), retrying`)}});if(u!==i.toLowerCase())throw t.default.rmSync(s,{force:!0}),Error(`sha256 mismatch for ${o}: expected ${i.toLowerCase()}, got ${u} (downloaded from ${e})`);t.default.renameSync(s,n),d.serverLogger.info({tag:"tracking-pyenv",op:"model_download_done",id:o,dest:n,size:l},`downloaded ${o} (${l} bytes, sha ok)`)}async function m(e,i){if("direct-download"!==e.acquisition)throw Error(`ensureModel only handles direct-download entries; ${e.id} is ${e.acquisition} — route it through ensureBuiltModel`);let n=r.default.join(i,e.dest);if(t.default.existsSync(n)){let t=p(n);if(t===e.sha256.toLowerCase())return void d.serverLogger.info({tag:"tracking-pyenv",op:"model_present",id:e.id,dest:n},`tracking model ${e.id} present (sha matches)`);d.serverLogger.warn({tag:"tracking-pyenv",op:"model_sha_mismatch",id:e.id,dest:n,expected:e.sha256.toLowerCase(),actual:t},`tracking model ${e.id} sha mismatch — re-downloading`)}await g(e.url,e.sha256,n,e.id)}async function y(e,i,n,o=u){let a=e.build;if(!a)throw Error(`model ${e.id} is build-at-install but has no build spec`);let c=r.default.join(n,e.dest),f=function(e,t){let r=e.build;if(!r)throw Error(`model ${e.id} is build-at-install but has no build spec`);let i=t.find(e=>e.id===r.sourceModelId);if(!i)throw Error(`model ${e.id} build.sourceModelId "${r.sourceModelId}" not in models.json`);return{sourcePtSha256:i.sha256.toLowerCase(),textEncoderSha256:r.textEncoder.sha256.toLowerCase(),clipRequirement:r.clipRequirement,classes:[...r.classes],imgsz:r.imgsz,opset:r.opset}}(e,i);if(function(e,r){if(!t.default.existsSync(e))return!1;let i=function(e){try{return JSON.parse(t.default.readFileSync(`${e}.build-info.json`,"utf-8"))}catch{return null}}(e);return!!i?.inputs&&!!i.output?.sha256&&JSON.stringify(i.inputs)===JSON.stringify(r)&&p(e)===i.output.sha256.toLowerCase()}(c,f))return void d.serverLogger.info({tag:"tracking-pyenv",op:"model_present",id:e.id,dest:c},`tracking model ${e.id} present (build-info current)`);let m=i.find(e=>e.id===a.sourceModelId),h=r.default.join(n,m.dest);if(!t.default.existsSync(h))throw Error(`cannot build ${e.dest}: source checkpoint ${m.dest} is not provisioned at ${h} (the direct-download phase must run first)`);let _=r.default.join(n,a.textEncoder.file);t.default.existsSync(_)&&p(_)===a.textEncoder.sha256.toLowerCase()||await g(a.textEncoder.url,a.textEncoder.sha256,_,`${e.id} text encoder`);let w=(0,l.sidecarProjectDir)(),v=["run","--locked","--with",a.clipRequirement,"python","export_yoloe11.py","--pt",h,"--encoder",_,"--out",c,"--classes",a.classes.join(","),"--imgsz",String(a.imgsz),"--opset",String(a.opset)];d.serverLogger.info({tag:"tracking-pyenv",op:"model_build_start",id:e.id,dest:c},`building tracking model ${e.id} (local ONNX export)`);let b="",x="";try{({stdout:b,stderr:x}=await o((0,l.uvPath)(),v,{cwd:w,windowsHide:!0,timeout:9e5,maxBuffer:0x2000000,env:(0,s.buildUvEnv)({UV_PROJECT_ENVIRONMENT:(0,s.trackingVenvDir)(),NO_COLOR:"1"})}))}catch(r){let t=(r.stderr??r.stdout??r.message??"").slice(-2e3);throw Error(`local ONNX export for ${e.dest} failed: ${r.message}
|
|
3
3
|
${t}`)}let S=function(e){let t=e.split("\n");for(let e=t.length-1;e>=0;e--){let r=t[e].trim();if(r.startsWith("{"))try{let e=JSON.parse(r);if("export"===e.type)return e}catch{}}return null}(b);if(!S||!S.ok){let t=S?.error??x.slice(-2e3);throw Error(`local ONNX export for ${e.dest} did not succeed: ${String(t)}`)}if(!t.default.existsSync(c))throw Error(`local ONNX export for ${e.dest} reported ok but ${c} is missing`);let k=p(c),I={inputs:f,output:{sha256:k,sizeBytes:t.default.statSync(c).size,ultralytics:"string"==typeof S.ultralytics?S.ultralytics:void 0},builtAt:new Date().toISOString()};t.default.writeFileSync(`${c}.build-info.json`,JSON.stringify(I,null,2)),d.serverLogger.info({tag:"tracking-pyenv",op:"model_build_done",id:e.id,dest:c,sha256:k,sizeBytes:I.output.sizeBytes},`built tracking model ${e.id} (local export, inputs pinned)`)}async function h(){let e,i=(0,l.sidecarProjectDir)(),n=(0,l.trackingModelsDir)(),o=(0,s.trackingVenvDir)();d.serverLogger.info({tag:"tracking-pyenv",op:"uv_sync_start",proj:i,venv:o,uv:(0,l.uvPath)()},"uv sync of tracking sidecar"),await u((0,l.uvPath)(),["sync","--locked"],{cwd:i,windowsHide:!0,timeout:6e5,maxBuffer:0x2000000,env:(0,s.buildUvEnv)({UV_PROJECT_ENVIRONMENT:o,NO_COLOR:"1"})}),d.serverLogger.info({tag:"tracking-pyenv",op:"uv_sync_done",proj:i},"uv sync complete");let a=function(){let e=r.default.join((0,l.sidecarProjectDir)(),"models.json"),i=JSON.parse(t.default.readFileSync(e,"utf-8"));if(!Array.isArray(i))throw Error(`models.json at ${e} is not a JSON array`);return i}();for(let e of a.filter(e=>"direct-download"===e.acquisition))await m(e,n);for(let e of a.filter(e=>"build-at-install"===e.acquisition))await y(e,a,n);e=f(),t.default.mkdirSync(r.default.dirname(e),{recursive:!0}),t.default.writeFileSync(e,c,"utf-8"),d.serverLogger.info({tag:"tracking-pyenv",op:"install_done",token:c},"tracking-pyenv install complete")}e.s(["runTrackingPyenvInstall",0,h,"trackingPyenvInstaller",0,{verify:async()=>(function(){try{return t.default.readFileSync(f(),"utf-8").trim()}catch{return null}})()===c?(0,l.trackingModelsDir)():null,install:{command:"__TRACKING_PYENV_INSTALL__",args:[],timeoutMs:18e5}}])},772104,e=>{"use strict";var t=e.i(313345);let r=[],i=0,n=new Set;function o(){let e=new Map;for(let r of t.BUILTIN_EFFECTS)e.set(r.meta.id,r);for(let t of r)e.set(t.meta.id,t);return[...e.values()]}function s(e){return(()=>{let e=new Map;for(let t of o())e.set(t.meta.id,t);return e})().get(e)}e.s(["findEffect",0,s,"listEffects",0,function(e={}){return o().filter(t=>(!e.kind||!!t.meta.supports.includes(e.kind))&&(!e.phase||!!t.meta.phases.includes(e.phase))&&(!e.family||t.meta.family===e.family))},"registerCustomEffects",0,function(e){for(let t of(r=e,i++,n))t()},"resolveEffect",0,function(e){return s(e)}])},855972,51882,e=>{"use strict";let t={position:{x:0,y:0,z:0},rotation:{x:0,y:0,z:0}};function r(e){return 0===e.x&&0===e.y&&0===e.z}function i(e){return r(e.position)&&r(e.rotation)}[0,45,90,135,180,-45,-90,-135,-180].map(e=>e*Math.PI/180),e.s(["IDENTITY_TRANSFORM3D",0,t,"isIdentityTransform3d",0,i],51882),e.s(["classifyTransform",0,function(e){return i(e)?"identity":0!==e.rotation.x||0!==e.rotation.y||0!==e.position.z?"spatial":"planar"},"resolveFlip",0,function(e){return{flipH:!!e.flipH,flipV:!!e.flipV}},"resolveOverlayTransform",0,function(e){return e.transform3d??t}],855972)},702220,e=>{"use strict";var t=e.i(270958);async function r(e){let r=function(){try{let e=(0,t.getCurrentPort)();return`http://127.0.0.1:${e}`}catch{return null}}();if(r)try{await fetch(`${r}/api/notify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e),signal:AbortSignal.timeout(3e3)})}catch{}}async function i(e){}e.s(["notify",0,{navigate(e){r({type:"navigate",...e})},refreshQuery(e){r({type:"refresh_query",...e})},refreshMcpConfig(){r({type:"refresh_mcp_config"})},instructionsChanged(){r({type:"instructions_changed"})},analysisChanged(e){r({type:"analysis_changed",...e})},navigateSettings(e){r({type:"navigate_settings",mcpId:e.mcpId})},rightRegion(e){r({type:"right_region",...e})},highlight(e){r({type:"highlight",...e})},highlightEffect(e){r({type:"highlight_effect",...e})},setComplexityMode(e){r({type:"set_complexity_mode",pieceId:e.pieceId,overlayId:e.overlayId,mode:e.mode})},jobProgress(e){r({type:"job_progress",...e})}},"pushIfBackgrounded",0,i])},720659,346385,775497,799261,e=>{"use strict";var t=e.i(522734),r=e.i(814747),i=e.i(233405),n=e.i(270958),o=e.i(246387);function s(e,s){let a=function(){let e=r.default.join((0,n.getLibiBinDir)(),`uv${(0,o.exeSuffix)()}`);if(t.default.existsSync(e))return e;let s=(0,o.isWindows)()?"where":"which";try{let e=(0,i.execFileSync)(s,["uv"],{windowsHide:!0,encoding:"utf8",timeout:5e3}),r=e.split("\n")[0]?.trim();if(r&&t.default.existsSync(r))return r}catch{}return null}();if(a)return a;let l=r.default.join((0,n.getLibiBinDir)(),`uv${(0,o.exeSuffix)()}`);throw new e(`uv not found at ${l} or on PATH. ${s} requires the bundled uv (tier-1 dep).`)}e.s(["requireUvBinary",0,s],346385);var a=e.i(470072),l=e.i(332879),d=e.i(254799);function u(e,t){let r=[`python:${e}`,...[...t].sort()];return(0,d.createHash)("sha256").update(r.join("\n")).digest("hex").slice(0,16)}function c(e,i){let o=r.default.join((0,n.getLibiHome)(),e);t.default.mkdirSync(r.default.dirname(o),{recursive:!0}),t.default.writeFileSync(o,i+"\n")}function p(e,i){return function(e){try{return t.default.readFileSync(r.default.join((0,n.getLibiHome)(),e),"utf-8").trim()}catch{return null}}(e)===i}e.s(["hashSpec",0,u],775497),e.s(["isTokenCurrent",0,p,"writeInstallToken",0,c],799261);var f=e.i(961288);let g="3.12",m=["faster-whisper==1.1.1","requests"];class y extends Error{constructor(e){super(e),this.name="WhisperTranscribeError"}}function h(){let e=[r.default.join(process.cwd(),"mcp/whisper/transcribe.py"),r.default.resolve("/ROOT/lib/whisper","../../mcp/whisper/transcribe.py"),r.default.join((0,f.packageRoot)(),"mcp/whisper/transcribe.py")];for(let r of e)if(t.default.existsSync(r))return r;return e[0]}function _(e){let t=["run","--python",g,...m.flatMap(e=>["--with",e]),"python",e.scriptPath];return!e.downloadOnly&&e.audioPath&&t.push(e.audioPath),t.push("--model",e.model,"--download-root",e.downloadRoot),e.languageCode&&t.push("--language",e.languageCode),e.downloadOnly&&t.push("--download-only"),t}function w(e,t,r){let n=s(y,"Whisper");return new Promise((o,s)=>{let l=(0,i.spawn)(n,e,{windowsHide:!0,stdio:["ignore","pipe","pipe"],env:(0,a.buildUvEnv)()}),d="",u="",c=setTimeout(()=>{l.kill("SIGKILL"),s(new y(`whisper timed out after ${t}ms`))},t);l.stdout.on("data",e=>d+=e.toString()),l.stderr.on("data",e=>{let t=e.toString();for(let e of(u+=t,t.matchAll(/PROGRESS (\d+)\/(\d+)/g)))r?.(Number(e[1]),Number(e[2]))}),l.on("error",e=>{clearTimeout(c),s(new y(`uv spawn failed: ${e.message}`))}),l.on("close",e=>{(clearTimeout(c),0!==e)?s(new y(`whisper exited ${e}: ${u.trim().slice(0,500)}`)):o(d)})})}async function v(e){let t=(0,l.resolveWhisperModel)(e.model),r=_({scriptPath:h(),audioPath:e.audioPath,model:t,downloadRoot:(0,l.whisperModelsDir)(),languageCode:e.languageCode}),i=function(e){let t,r=e.trim();if(!r)throw new y("whisper produced empty stdout");try{t=JSON.parse(r)}catch{throw new y(`whisper stdout was not JSON: ${r.slice(0,200)}`)}if("object"!=typeof t||null===t||"string"!=typeof t.text)throw new y(`whisper JSON missing 'text': ${JSON.stringify(t).slice(0,200)}`);let i=t;return Array.isArray(i.words)||(i.words=[]),i}(await w(r,18e5));return x(),i}let b=".libi-whisper-env.install-token";function x(){c(b,u(g,m))}async function S(e,t){let r=_({scriptPath:h(),model:(0,l.resolveWhisperModel)(e),downloadRoot:(0,l.whisperModelsDir)(),downloadOnly:!0});await w(r,36e5,t)}e.s(["WHISPER_PYTHON_VERSION",0,g,"WHISPER_WITH_SPECS",0,m,"downloadModel",0,S,"isWhisperEnvCurrent",0,function(){return p(b,u(g,m))},"transcribeAudio",0,v,"writeWhisperEnvToken",0,x],720659)},723663,e=>{"use strict";let t=globalThis;e.s(["getOpenedPieceId",0,function(){return t.__openedPieceId??null},"setOpenedPieceId",0,function(e){t.__openedPieceId=e}])},695687,e=>{"use strict";var t=e.i(750227),r=e.i(902157),i=e.i(270958),n=e.i(246387);e.s(["sidecarProjectDir",0,function(){return(0,t.join)(process.cwd(),"mcp","tracking","py")},"trackingModelsDir",0,function(){return(0,t.join)((0,i.getLibiHome)(),"models","tracking")},"uvPath",0,function(){let e=(0,t.join)((0,i.getLibiBinDir)(),`uv${(0,n.exeSuffix)()}`);return(0,r.existsSync)(e)?e:"uv"}])},632484,e=>{"use strict";var t=e.i(902157),r=e.i(750227),i=e.i(695687);e.s(["trackingEngineInstalled",0,function(){try{return t.default.existsSync(r.default.join((0,i.trackingModelsDir)(),".install-token"))}catch{return!1}},"trackingNotInstalledError",0,function(){return{error:"tracking_engine_not_installed",data:{hint:"The libi-tracking engine is not installed yet. Call libi.get_install_plan (mcpId 'libi-tracking') and follow it — disclose the ~2 GB / ~10-20 min cost and get the user's OK, then call libi.install_tracking_engine (it runs the real install as a background job), then libi.verify_install, then retry.",installPlanPath:"mcp/bundled-mcps/plans/libi-tracking.md"}}}])}];
|
|
4
4
|
|
|
5
|
-
//# debugId=
|
|
5
|
+
//# debugId=f91c0519-5818-07c6-1b9f-059db6049a81
|
|
6
6
|
//# sourceMappingURL=%5Broot-of-the-server%5D__09qyvq1._.js.map
|