diff --git a/README.md b/README.md index ba90ec42..2425c346 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,12 @@ Built on: ## What you get -- **iMessage in / iMessage out** via Sendblue (with typing indicators and webhook dedup). +- **iMessage in / iMessage out** via Sendblue (with signed inbound requests, typing indicators, and webhook dedup). - **Sendblue CLI integration** — `npm run dev` auto-registers the inbound webhook for you every restart (no re-pasting into the dashboard when free ngrok rotates your URL). - **Dispatcher + workers** pattern: a lean interaction agent decides what to do, spawns focused sub-agents that actually do the work. - **Pure dispatcher** — the interaction agent has only memory + spawn + automation + draft tools. Web access, files, and integrations are explicitly denied to it; sub-agents get `WebSearch` / `WebFetch` / the integrations. - **Tiered memory** (short / long / permanent) with post-turn extraction, decay, and cleaning. -- **Vector search** for recall when you add an embeddings key (Voyage or OpenAI) — falls back to substring. +- **Vector search** for recall with a local BGE-large fallback, or optional Voyage/OpenAI embeddings. - **Memory consolidation** — a daily 3-phase adversarial pipeline (proposer → adversary → judge) that merges duplicates, resolves contradictions, and prunes noise. Uses the configured runtime, with provider-specific model defaults. Runs every 24h by default, also triggerable manually via `POST /consolidate`. - **Automations** — the agent can schedule recurring work from a text ("every morning at 8 summarize my calendar") and push results back to iMessage. - **Draft-and-send** — any external action stages a draft first; the agent only commits when the user confirms. @@ -169,11 +169,38 @@ Public URL: https://.ngrok.app Sendblue webhook: https://.ngrok.app/sendblue/webhook ``` -On free ngrok, **the webhook auto-registers with Sendblue every boot** — no manual paste needed. For stable URLs (ngrok reserved or Cloudflare Tunnel), set the webhook once in the dashboard. +Boop synchronizes the signed Sendblue webhook on every boot, whether the public URL rotates or stays stable. No manual dashboard paste should be needed. Text your Sendblue-provisioned number from a **different** phone. The agent replies. -> **⚠ ngrok free plan gives you a new URL every time.** That means every time you restart `npm run dev`, your Sendblue webhook URL is dead until you paste the new one in. +### Native App + +

+ Boop desktop app icon +

+ +Boop also has an experimental dedicated desktop app for people who want to launch it from the Dock instead of keeping a terminal open. The app starts the same stack as `npm run dev`, embeds the debug dashboard when everything is ready, and gives you start, stop, restart, and server-status controls directly in the app. The dashboard's Connection header is where you can see the running server, Convex, dashboard, tunnel, Sendblue webhook registration, Convex URL, and the Sendblue number people should text. + +**Important:** `npm run setup` does not build or install the desktop app. It configures the checkout you are standing in: it creates or updates `.env.local`, walks through the runtime choice, configures Sendblue, creates or reuses the Convex deployment, generates Convex files, and offers optional local browser support. You still need to run setup at least once before Boop can run. If you want the installed app to run by itself, use `npm run desktop:setup`; that command runs the same interactive setup inside the app's runtime folder. + +```bash +npm run desktop:setup # recommended: setup app runtime, build app, optionally copy to /Applications +npm run desktop:dev # experimental: run the app from this checkout, after npm run setup +npm run desktop:pack # build an unsigned app bundle in dist/mac-arm64 +npm run desktop:dist # build unsigned distributables +``` + +| Command | What happens | Installs the app? | +|---|---|---| +| `npm run setup` | Configures this checkout for terminal/dev use. Writes `.env.local`, configures Sendblue, sets up Convex, generates Convex files, and can install optional browser support. | No | +| `npm run desktop:setup` | Prepares the app runtime folder, runs the same interactive setup there, builds the app, and on macOS offers to copy `Boop.app` to `/Applications`. | Yes, if you accept the `/Applications` prompt | +| `npm run desktop:dev` | Experimental developer runner. Opens the desktop app from this checkout and uses this checkout's setup files. | No | +| `npm run desktop:pack` | Creates an unsigned app bundle under `dist/` for local testing. | No, it only builds | +| `npm run desktop:dist` | Creates unsigned distributable artifacts. | No, it only builds | + +The app keeps secrets and local state out of the app bundle. For an installed macOS app, `.env.local`, `.convex`, generated Convex files, and local data live under `~/Library/Application Support/Boop/runtime`. The app bundle contains the runnable project and the Boop app icon; the runtime folder contains the machine-specific setup. The optional local BGE-large embedding model is not bundled either: setup downloads about 1.3GB into the runtime's `data/` folder when you choose it. Only health checks and provider webhook routes are exposed through the public tunnel; dashboard, browser-control, and local configuration routes remain local to your Mac. + +> **ngrok's free plan gives you a new URL every time.** Boop automatically registers the new Sendblue webhook when `npm run dev` or the desktop app starts, so you should not need to paste it manually. If texts stop arriving, run `npm run sendblue:webhook:check` to compare Sendblue's registration with the active tunnel. > > If you're going to run this for more than a quick demo, **strongly recommend one of:** > - **ngrok paid plan** — gives you a reserved domain that stays the same forever @@ -190,13 +217,14 @@ Text your Sendblue-provisioned number from a **different** phone. The agent repl ## How the Sendblue integration works -Boop uses the [Sendblue CLI](https://github.com/sendblue-api/sendblue-cli) (`@sendblue/cli`) to eliminate almost all manual dashboard work. Three NPM scripts wrap it: +Boop uses the [Sendblue CLI](https://github.com/sendblue-api/sendblue-cli) (`@sendblue/cli`) to eliminate almost all manual dashboard work. These NPM scripts wrap it: | Command | What it does | |---|---| | `npm run setup` | Interactive. Offers to run `sendblue login` / `sendblue setup` and pulls `api_key_id` + `api_secret_key` from `sendblue show-keys` into `.env.local`. | | `npm run sendblue:sync` | Runs `sendblue lines`, parses your provisioned phone number, and writes `SENDBLUE_FROM_NUMBER` to `.env.local` in E.164 format. Run this anytime your number changes or got set wrong. | -| `npm run sendblue:webhook -- ` | Runs `sendblue webhooks list`, removes stale ngrok/tunnel hooks, and adds `` as a `type=receive` inbound webhook. Called automatically by `npm run dev`. | +| `npm run sendblue:webhook -- ` | Uses the Sendblue API to remove stale tunnel hooks, register `` as a `type=receive` inbound webhook, and synchronize its signing secret. Called automatically by `npm run dev`. | +| `npm run sendblue:webhook:check` | Read-only check. Compares the active ngrok tunnel (or `PUBLIC_URL`) and signing secret with the Sendblue receive webhook currently registered. Add `-- ` to check a specific URL. | ### The `npm run dev` lifecycle @@ -212,17 +240,18 @@ Boop uses the [Sendblue CLI](https://github.com/sendblue-api/sendblue-cli) (`@se convex → "Convex functions ready" debug → "Local: http://localhost:5173/" ngrok → tunnel URL visible at http://127.0.0.1:4040 - 4. Auto-register the webhook (FREE ngrok only, not reserved domains): + 4. Synchronize the signed webhook for the active public URL: webhook │ [webhook] removed stale https://old.ngrok-free.app/sendblue/webhook webhook │ [webhook] registered https://new.ngrok-free.app/sendblue/webhook (type=receive) - 5. Show the banner with dashboard + public URL + your Sendblue number. + 5. The desktop app checks that Sendblue has the active webhook registered. + 6. Show the banner with dashboard + public URL + your Sendblue number. ``` The banner will look like: ``` ════════════════════════════════════════════════════════════════════ - Boop is ready — ngrok tunnel is live (webhook auto-registered). + Boop is ready — ngrok tunnel is live. 🐶 Debug dashboard (click me): http://localhost:5173 🌐 Public URL: https://abc123.ngrok-free.app @@ -236,9 +265,9 @@ The banner will look like: | Setup | Auto-register fires? | Why | |---|---|---| | Free ngrok (default) | **Yes**, every boot | URL rotates; dashboard would be stale otherwise | -| Reserved `NGROK_DOMAIN` | No | URL is stable; configure once in Sendblue dashboard | -| Static `PUBLIC_URL` (Cloudflare Tunnel etc.) | No | Same reason | -| `SENDBLUE_AUTO_WEBHOOK=false` | No | Manual opt-out | +| Reserved `NGROK_DOMAIN` | **Yes**, every boot | Keeps the signing secret synchronized without duplicating the stable URL | +| Static `PUBLIC_URL` (Cloudflare Tunnel etc.) | **Yes**, every boot | Keeps the signing secret synchronized without duplicating the stable URL | +| `SENDBLUE_AUTO_WEBHOOK=false` | No | Manual opt-out; run `npm run sendblue:webhook -- ` at least once to configure the required signing secret | ### What you'll see in the server logs during a conversation @@ -267,6 +296,7 @@ The same events are written to Convex (`messages`, `executionAgents`, `agentLogs - **First time / after losing `.env.local`** → `npm run setup` (walks through Sendblue + Convex together) - **Phone number looks wrong in the banner** → `npm run sendblue:sync` +- **Server says healthy but texts do not arrive** → `npm run sendblue:webhook:check` - **Webhook went stale in the dashboard and auto-register is off** → `npm run sendblue:webhook -- https://your-url.example.com/sendblue/webhook` ### Disabling auto-register @@ -442,10 +472,10 @@ It is off by default in two layers: 1. The master Apple data switch must be enabled. 2. Each source must be connected separately: iMessage, Apple Notes, and Apple Reminders. -Turn it on from the debug dashboard: +Turn it on from the debug dashboard, either in the browser during local development or inside the desktop app: -1. Start Boop locally with `npm run dev`. -2. Open `http://localhost:5173`. +1. Start Boop locally with `npm run dev`, or open the Boop desktop app. +2. Open `http://localhost:5173`, or use the embedded dashboard in the desktop app. 3. Go to **Connections → Local Mac**. 4. Click **Connect** only for the sources you want Boop to read. 5. Use **Disconnect** to turn any source off again. @@ -454,7 +484,7 @@ You can also view the overall Apple status from **Settings → Apple data**. Das | Source | Permission | Notes | |---|---|---| -| iMessage / SMS history | Full Disk Access for the terminal, Codex app, or process running `npm run dev` | Reads `~/Library/Messages/chat.db` locally through `/usr/bin/sqlite3`. | +| iMessage / SMS history | Full Disk Access for the app or process running Boop, such as Boop.app for desktop runs | Reads `~/Library/Messages/chat.db` locally through `/usr/bin/sqlite3`. | | Apple Notes | macOS Automation permission for Notes | Uses `/usr/bin/osascript` and exposes search/read tools only. | | Apple Reminders | macOS Automation permission for Reminders | Uses `/usr/bin/osascript` and exposes list tools only. | | Apple Calendar | Optional Apple bridge | Calendar events are not read by the local server path in this repo. | @@ -477,7 +507,7 @@ Boop outsources 3rd-party service integrations to [Composio](https://composio.de COMPOSIO_API_KEY=sk-comp-... ``` 3. `npm run dev`. -4. Open the debug dashboard → **Connections** tab. You'll see a curated list of ~20 cards. For each one: click **Connect**, authenticate on Composio's hosted page, done — Composio ships managed OAuth for every curated toolkit. (If you add a custom toolkit that needs your own OAuth app, the card flips to a "Set up →" state pointing at `platform.composio.dev/auth-configs` — rare, but supported.) +4. Open the debug dashboard → **Connections** tab. The connected and common integrations appear first, followed by the full Composio catalog with each toolkit's available tool count. Click **Connect**, authenticate on Composio's hosted page, and you are done. If a toolkit needs your own OAuth app, its row points to `platform.composio.dev/auth-configs` for the one-time setup. After a successful connect, the agent can use that toolkit immediately — no restart. @@ -507,7 +537,7 @@ Key properties: - **Toolkit slug = integration name.** `spawn_agent(integrations: ["linear"])` works for any toolkit you've connected. Unknown slugs just log a warning and are skipped. - **No tokens on our side.** Every tool call runs through Composio's proxy. If Composio goes down, integrations go down — but your server never holds user OAuth tokens. - **Multi-account per toolkit.** Connect a second Gmail (work + personal) — each gets its own connection row you can alias. The dispatcher picks up all active connections for the slug. -- **Identity resolution.** Connection cards show the real account email (e.g. `user@example.com`) resolved by calling the toolkit's own "who am I" tool through Composio (`GMAIL_GET_PROFILE`, etc.). Alias per connection if you want a friendlier label. +- **Identity resolution.** Connection cards show the connected account identity resolved by calling the toolkit's own "who am I" tool through Composio (`GMAIL_GET_PROFILE`, etc.). Alias per connection if you want a friendlier label. ### Adding toolkits beyond the curated list diff --git a/assets/boop-app-icon.png b/assets/boop-app-icon.png new file mode 100644 index 00000000..20d85cdc Binary files /dev/null and b/assets/boop-app-icon.png differ diff --git a/assets/boop.icns b/assets/boop.icns new file mode 100644 index 00000000..e7f1de43 Binary files /dev/null and b/assets/boop.icns differ diff --git a/convex/agents.ts b/convex/agents.ts index 88506e9a..ccec43cd 100644 --- a/convex/agents.ts +++ b/convex/agents.ts @@ -1,5 +1,8 @@ -import { mutation, query } from "./_generated/server"; +import { mutation, query, type QueryCtx } from "./_generated/server"; import { v } from "convex/values"; +import { DEMO_SCAN_LIMIT, isDemoId, isDemoModeEnabled } from "./demoMode"; + +type AgentStatus = "spawned" | "running" | "completed" | "failed" | "cancelled"; const statusV = v.union( v.literal("spawned"), @@ -77,24 +80,44 @@ export const addLog = mutation({ }, }); -export const list = query({ - args: { status: v.optional(statusV), limit: v.optional(v.number()) }, - handler: async (ctx, args) => { - const limit = args.limit ?? 50; - if (args.status) { - return await ctx.db +async function readAgents( + ctx: QueryCtx, + args: { status?: AgentStatus; limit?: number }, + demoOnly: boolean, +) { + const limit = args.limit ?? 50; + const rows = args.status + ? await ctx.db .query("executionAgents") .withIndex("by_status", (q) => q.eq("status", args.status!)) .order("desc") - .take(limit); - } - return await ctx.db.query("executionAgents").order("desc").take(limit); + .take(DEMO_SCAN_LIMIT) + : await ctx.db.query("executionAgents").order("desc").take(DEMO_SCAN_LIMIT); + return rows.filter((agent) => isDemoId(agent.agentId) === demoOnly).slice(0, limit); +} + +export const list = query({ + args: { + status: v.optional(statusV), + limit: v.optional(v.number()), + }, + handler: async (ctx, args) => readAgents(ctx, args, false), +}); + +export const listForDashboard = query({ + args: { + status: v.optional(statusV), + limit: v.optional(v.number()), + }, + handler: async (ctx, args) => { + return readAgents(ctx, args, await isDemoModeEnabled(ctx)); }, }); export const get = query({ args: { agentId: v.string() }, handler: async (ctx, args) => { + if (isDemoId(args.agentId)) return null; return await ctx.db .query("executionAgents") .withIndex("by_agent_id", (q) => q.eq("agentId", args.agentId)) @@ -105,6 +128,30 @@ export const get = query({ export const getLogs = query({ args: { agentId: v.string(), limit: v.optional(v.number()) }, handler: async (ctx, args) => { + if (isDemoId(args.agentId)) return []; + return await ctx.db + .query("agentLogs") + .withIndex("by_agent", (q) => q.eq("agentId", args.agentId)) + .order("asc") + .take(args.limit ?? 500); + }, +}); + +export const getForDashboard = query({ + args: { agentId: v.string() }, + handler: async (ctx, args) => { + if (isDemoId(args.agentId) !== (await isDemoModeEnabled(ctx))) return null; + return await ctx.db + .query("executionAgents") + .withIndex("by_agent_id", (q) => q.eq("agentId", args.agentId)) + .unique(); + }, +}); + +export const getLogsForDashboard = query({ + args: { agentId: v.string(), limit: v.optional(v.number()) }, + handler: async (ctx, args) => { + if (isDemoId(args.agentId) !== (await isDemoModeEnabled(ctx))) return []; return await ctx.db .query("agentLogs") .withIndex("by_agent", (q) => q.eq("agentId", args.agentId)) diff --git a/convex/automations.ts b/convex/automations.ts index 250eff67..ba8da5bb 100644 --- a/convex/automations.ts +++ b/convex/automations.ts @@ -1,5 +1,6 @@ -import { mutation, query } from "./_generated/server"; +import { mutation, query, type QueryCtx } from "./_generated/server"; import { v } from "convex/values"; +import { DEMO_SCAN_LIMIT, isDemoAutomationRun, isDemoId, isDemoModeEnabled } from "./demoMode"; export const create = mutation({ args: { @@ -27,25 +28,48 @@ export const create = mutation({ }, }); +async function readAutomations( + ctx: QueryCtx, + enabledOnly: boolean | undefined, + demoOnly: boolean, +) { + const rows = enabledOnly + ? await ctx.db + .query("automations") + .withIndex("by_enabled", (q) => q.eq("enabled", true)) + .order("desc") + .take(DEMO_SCAN_LIMIT) + : await ctx.db.query("automations").order("desc").take(DEMO_SCAN_LIMIT); + return rows.filter((automation) => isDemoId(automation.automationId) === demoOnly); +} + export const list = query({ + args: { enabledOnly: v.optional(v.boolean()) }, + handler: async (ctx, args) => readAutomations(ctx, args.enabledOnly, false), +}); + +export const listForDashboard = query({ args: { enabledOnly: v.optional(v.boolean()) }, handler: async (ctx, args) => { - let results; - if (args.enabledOnly) { - results = await ctx.db - .query("automations") - .withIndex("by_enabled", (q) => q.eq("enabled", true)) - .collect(); - } else { - results = await ctx.db.query("automations").order("desc").collect(); - } - return results; + return readAutomations(ctx, args.enabledOnly, await isDemoModeEnabled(ctx)); }, }); export const get = query({ args: { automationId: v.string() }, handler: async (ctx, args) => { + if (isDemoId(args.automationId)) return null; + return await ctx.db + .query("automations") + .withIndex("by_automation_id", (q) => q.eq("automationId", args.automationId)) + .unique(); + }, +}); + +export const getForDashboard = query({ + args: { automationId: v.string() }, + handler: async (ctx, args) => { + if (isDemoId(args.automationId) !== (await isDemoModeEnabled(ctx))) return null; return await ctx.db .query("automations") .withIndex("by_automation_id", (q) => q.eq("automationId", args.automationId)) @@ -141,13 +165,37 @@ export const recentRuns = query({ args: { automationId: v.optional(v.string()), limit: v.optional(v.number()) }, handler: async (ctx, args) => { const limit = args.limit ?? 50; + if (args.automationId && isDemoId(args.automationId)) return []; if (args.automationId) { - return await ctx.db + return (await ctx.db .query("automationRuns") .withIndex("by_automation", (q) => q.eq("automationId", args.automationId!)) .order("desc") - .take(limit); + .take(DEMO_SCAN_LIMIT)) + .filter((run) => !isDemoAutomationRun(run)) + .slice(0, limit); } - return await ctx.db.query("automationRuns").order("desc").take(limit); + const rows = await ctx.db + .query("automationRuns") + .order("desc") + .take(DEMO_SCAN_LIMIT); + return rows.filter((run) => !isDemoAutomationRun(run)).slice(0, limit); + }, +}); + +export const recentRunsForDashboard = query({ + args: { automationId: v.optional(v.string()), limit: v.optional(v.number()) }, + handler: async (ctx, args) => { + const limit = args.limit ?? 50; + const demoOnly = await isDemoModeEnabled(ctx); + if (args.automationId && isDemoId(args.automationId) !== demoOnly) return []; + const rows = args.automationId + ? await ctx.db + .query("automationRuns") + .withIndex("by_automation", (q) => q.eq("automationId", args.automationId!)) + .order("desc") + .take(DEMO_SCAN_LIMIT) + : await ctx.db.query("automationRuns").order("desc").take(DEMO_SCAN_LIMIT); + return rows.filter((run) => isDemoAutomationRun(run) === demoOnly).slice(0, limit); }, }); diff --git a/convex/consolidation.ts b/convex/consolidation.ts index dfe1a6f3..d45b0072 100644 --- a/convex/consolidation.ts +++ b/convex/consolidation.ts @@ -1,5 +1,6 @@ -import { mutation, query } from "./_generated/server"; +import { mutation, query, type QueryCtx } from "./_generated/server"; import { v } from "convex/values"; +import { DEMO_SCAN_LIMIT, isDemoId, isDemoModeEnabled } from "./demoMode"; const statusV = v.union( v.literal("running"), @@ -44,9 +45,23 @@ export const updateRun = mutation({ }, }); +async function readRuns(ctx: QueryCtx, limit: number, demoOnly: boolean) { + const rows = await ctx.db + .query("consolidationRuns") + .order("desc") + .take(DEMO_SCAN_LIMIT); + return rows.filter((run) => isDemoId(run.runId) === demoOnly).slice(0, limit); +} + export const listRuns = query({ + args: { limit: v.optional(v.number()) }, + handler: async (ctx, args) => readRuns(ctx, args.limit ?? 25, false), +}); + +export const listRunsForDashboard = query({ args: { limit: v.optional(v.number()) }, handler: async (ctx, args) => { - return await ctx.db.query("consolidationRuns").order("desc").take(args.limit ?? 25); + const limit = args.limit ?? 25; + return readRuns(ctx, limit, await isDemoModeEnabled(ctx)); }, }); diff --git a/convex/dashboard.ts b/convex/dashboard.ts index f53c4d22..e144afc2 100644 --- a/convex/dashboard.ts +++ b/convex/dashboard.ts @@ -1,4 +1,12 @@ import { query } from "./_generated/server"; +import { + DEMO_SCAN_LIMIT, + isDemoAutomationRun, + isDemoId, + isDemoMessage, + isDemoModeEnabled, + isDemoUsageRecord, +} from "./demoMode"; // Cap per-table scans so a long-lived install doesn't hit Convex's 16,384 // .collect() ceiling and break the dashboard. Metrics reflect the most @@ -8,19 +16,32 @@ const METRICS_SCAN_LIMIT = 5000; export const metrics = query({ args: {}, handler: async (ctx) => { - const [messages, memories, agents, automationRuns, usageRecords] = await Promise.all([ + const demoMode = await isDemoModeEnabled(ctx); + let [messages, memories, agents, automationRuns, usageRecords] = await Promise.all([ ctx.db.query("messages").order("desc").take(METRICS_SCAN_LIMIT), ctx.db.query("memoryRecords").order("desc").take(METRICS_SCAN_LIMIT), ctx.db.query("executionAgents").order("desc").take(METRICS_SCAN_LIMIT), ctx.db.query("automationRuns").order("desc").take(METRICS_SCAN_LIMIT), ctx.db.query("usageRecords").order("desc").take(METRICS_SCAN_LIMIT), ]); + const scanned = { + messages: messages.length, + memories: memories.length, + agents: agents.length, + automationRuns: automationRuns.length, + usageRecords: usageRecords.length, + }; + messages = messages.filter((message) => isDemoMessage(message) === demoMode); + memories = memories.filter((memory) => isDemoId(memory.memoryId) === demoMode); + agents = agents.filter((agent) => isDemoId(agent.agentId) === demoMode); + automationRuns = automationRuns.filter((run) => isDemoAutomationRun(run) === demoMode); + usageRecords = usageRecords.filter((record) => isDemoUsageRecord(record) === demoMode); const truncated = - messages.length === METRICS_SCAN_LIMIT || - memories.length === METRICS_SCAN_LIMIT || - agents.length === METRICS_SCAN_LIMIT || - automationRuns.length === METRICS_SCAN_LIMIT || - usageRecords.length === METRICS_SCAN_LIMIT; + scanned.messages === METRICS_SCAN_LIMIT || + scanned.memories === METRICS_SCAN_LIMIT || + scanned.agents === METRICS_SCAN_LIMIT || + scanned.automationRuns === METRICS_SCAN_LIMIT || + scanned.usageRecords === METRICS_SCAN_LIMIT; const activeMem = memories.filter((m) => m.lifecycle === "active"); @@ -125,15 +146,16 @@ export const metrics = query({ export const imageStorageStats = query({ args: {}, handler: async (ctx) => { + const demoMode = await isDemoModeEnabled(ctx); // Capped scan like the other dashboard queries — unbounded .collect() // throws TransactionTooLargeError when the bandwidth limit is exceeded. const msgs = await ctx.db .query("messages") .order("desc") - .take(METRICS_SCAN_LIMIT); + .take(demoMode ? DEMO_SCAN_LIMIT : METRICS_SCAN_LIMIT); const seen = new Set(); let count = 0; - for (const m of msgs) { + for (const m of msgs.filter((message) => isDemoMessage(message) === demoMode)) { for (const id of m.imageStorageIds ?? []) { if (seen.has(id as unknown as string)) continue; seen.add(id as unknown as string); diff --git a/convex/demo.ts b/convex/demo.ts index 4d7fe399..db2db2a8 100644 --- a/convex/demo.ts +++ b/convex/demo.ts @@ -1,12 +1,11 @@ import { mutation, query, type MutationCtx, type QueryCtx } from "./_generated/server"; import { v } from "convex/values"; +import { DEMO_PREFIX, DEMO_SCAN_LIMIT, DEMO_SETTING_KEY, isDemoId } from "./demoMode"; -const DEMO_PREFIX = "demo:"; -const DEMO_SETTING_KEY = "debug_demo_mode"; -const SCAN_LIMIT = 5000; const MINUTE = 60 * 1000; const HOUR = 60 * MINUTE; const DAY = 24 * HOUR; +const DEMO_EMBEDDING_DIMENSIONS = 1024; type Runtime = "claude" | "codex"; type BillingMode = "api" | "codex-subscription"; @@ -49,6 +48,7 @@ interface AgentTemplate { task: string; result: string; error?: string; + status?: AgentStatus; integrations: string[]; tool: string; query: string; @@ -60,10 +60,7 @@ interface MemoryTemplate { segment: MemorySegment; tier: MemoryTier; importance: number; -} - -function isDemoId(value?: string | null): boolean { - return typeof value === "string" && value.startsWith(DEMO_PREFIX); + graphLabel?: string; } function ago(now: number, days: number, offset = 0): number { @@ -74,6 +71,27 @@ function compactNumber(value: number, precision = 4): number { return Number(value.toFixed(precision)); } +function demoEmbedding(content: string): number[] { + let seed = 2166136261; + for (let index = 0; index < content.length; index += 1) { + seed = Math.imul(seed ^ content.charCodeAt(index), 16777619) >>> 0; + } + + const vector: number[] = []; + let sumSquares = 0; + for (let index = 0; index < DEMO_EMBEDDING_DIMENSIONS; index += 1) { + seed = Math.imul(seed ^ (seed >>> 15), 2246822507) >>> 0; + seed = Math.imul(seed ^ (seed >>> 13), 3266489909) >>> 0; + seed = Math.imul(seed ^ index, 16777619) >>> 0; + const value = (seed / 0xffffffff) * 2 - 1; + vector.push(value); + sumSquares += value * value; + } + + const magnitude = Math.sqrt(sumSquares) || 1; + return vector.map((value) => compactNumber(value / magnitude, 6)); +} + function pick(items: readonly T[], index: number): T { return items[index % items.length]!; } @@ -116,16 +134,16 @@ async function demoCounts(ctx: QueryCtx | MutationCtx): Promise { consolidationRuns, usageRecords, ] = await Promise.all([ - ctx.db.query("conversations").order("desc").take(SCAN_LIMIT), - ctx.db.query("messages").order("desc").take(SCAN_LIMIT), - ctx.db.query("executionAgents").order("desc").take(SCAN_LIMIT), - ctx.db.query("agentLogs").order("desc").take(SCAN_LIMIT), - ctx.db.query("memoryRecords").order("desc").take(SCAN_LIMIT), - ctx.db.query("memoryEvents").order("desc").take(SCAN_LIMIT), - ctx.db.query("automations").order("desc").take(SCAN_LIMIT), - ctx.db.query("automationRuns").order("desc").take(SCAN_LIMIT), - ctx.db.query("consolidationRuns").order("desc").take(SCAN_LIMIT), - ctx.db.query("usageRecords").order("desc").take(SCAN_LIMIT), + ctx.db.query("conversations").order("desc").take(DEMO_SCAN_LIMIT), + ctx.db.query("messages").order("desc").take(DEMO_SCAN_LIMIT), + ctx.db.query("executionAgents").order("desc").take(DEMO_SCAN_LIMIT), + ctx.db.query("agentLogs").order("desc").take(DEMO_SCAN_LIMIT), + ctx.db.query("memoryRecords").order("desc").take(DEMO_SCAN_LIMIT), + ctx.db.query("memoryEvents").order("desc").take(DEMO_SCAN_LIMIT), + ctx.db.query("automations").order("desc").take(DEMO_SCAN_LIMIT), + ctx.db.query("automationRuns").order("desc").take(DEMO_SCAN_LIMIT), + ctx.db.query("consolidationRuns").order("desc").take(DEMO_SCAN_LIMIT), + ctx.db.query("usageRecords").order("desc").take(DEMO_SCAN_LIMIT), ]); return { @@ -162,14 +180,14 @@ async function deleteDemoRows(ctx: MutationCtx): Promise { usageRecords: 0, }; - const agentLogs = await ctx.db.query("agentLogs").order("desc").take(SCAN_LIMIT); + const agentLogs = await ctx.db.query("agentLogs").order("desc").take(DEMO_SCAN_LIMIT); for (const row of agentLogs) { if (!isDemoId(row.agentId)) continue; await ctx.db.delete(row._id); counts.agentLogs += 1; } - const automationRuns = await ctx.db.query("automationRuns").order("desc").take(SCAN_LIMIT); + const automationRuns = await ctx.db.query("automationRuns").order("desc").take(DEMO_SCAN_LIMIT); for (const row of automationRuns) { if (!isDemoId(row.runId) && !isDemoId(row.automationId) && !isDemoId(row.agentId)) { continue; @@ -178,7 +196,7 @@ async function deleteDemoRows(ctx: MutationCtx): Promise { counts.automationRuns += 1; } - const usageRecords = await ctx.db.query("usageRecords").order("desc").take(SCAN_LIMIT); + const usageRecords = await ctx.db.query("usageRecords").order("desc").take(DEMO_SCAN_LIMIT); for (const row of usageRecords) { if (!isDemoId(row.conversationId) && !isDemoId(row.agentId) && !isDemoId(row.runId)) { continue; @@ -187,7 +205,7 @@ async function deleteDemoRows(ctx: MutationCtx): Promise { counts.usageRecords += 1; } - const memoryEvents = await ctx.db.query("memoryEvents").order("desc").take(SCAN_LIMIT); + const memoryEvents = await ctx.db.query("memoryEvents").order("desc").take(DEMO_SCAN_LIMIT); for (const row of memoryEvents) { if (!isDemoId(row.conversationId) && !isDemoId(row.memoryId) && !isDemoId(row.agentId)) { continue; @@ -199,42 +217,42 @@ async function deleteDemoRows(ctx: MutationCtx): Promise { const consolidationRuns = await ctx.db .query("consolidationRuns") .order("desc") - .take(SCAN_LIMIT); + .take(DEMO_SCAN_LIMIT); for (const row of consolidationRuns) { if (!isDemoId(row.runId)) continue; await ctx.db.delete(row._id); counts.consolidationRuns += 1; } - const agents = await ctx.db.query("executionAgents").order("desc").take(SCAN_LIMIT); + const agents = await ctx.db.query("executionAgents").order("desc").take(DEMO_SCAN_LIMIT); for (const row of agents) { if (!isDemoId(row.agentId)) continue; await ctx.db.delete(row._id); counts.agents += 1; } - const memories = await ctx.db.query("memoryRecords").order("desc").take(SCAN_LIMIT); + const memories = await ctx.db.query("memoryRecords").order("desc").take(DEMO_SCAN_LIMIT); for (const row of memories) { if (!isDemoId(row.memoryId)) continue; await ctx.db.delete(row._id); counts.memories += 1; } - const automations = await ctx.db.query("automations").order("desc").take(SCAN_LIMIT); + const automations = await ctx.db.query("automations").order("desc").take(DEMO_SCAN_LIMIT); for (const row of automations) { if (!isDemoId(row.automationId)) continue; await ctx.db.delete(row._id); counts.automations += 1; } - const messages = await ctx.db.query("messages").order("desc").take(SCAN_LIMIT); + const messages = await ctx.db.query("messages").order("desc").take(DEMO_SCAN_LIMIT); for (const row of messages) { if (!isDemoId(row.conversationId) && !isDemoId(row.agentId)) continue; await ctx.db.delete(row._id); counts.messages += 1; } - const conversations = await ctx.db.query("conversations").order("desc").take(SCAN_LIMIT); + const conversations = await ctx.db.query("conversations").order("desc").take(DEMO_SCAN_LIMIT); for (const row of conversations) { if (!isDemoId(row.conversationId)) continue; await ctx.db.delete(row._id); @@ -505,7 +523,7 @@ const agentTemplates: AgentTemplate[] = [ name: "Memory recall trace", task: "Trace which memories were recalled for the calendar scheduling turn.", result: - "Recalled 6 memories: no early Friday meetings, Amie-like dashboard style, concise briefs, and launch-week stakeholders.", + "Recalled 6 memories: no early Friday meetings, compact calendar-first dashboards, concise briefs, and launch-week stakeholders.", integrations: ["boop_memory"], tool: "mcp__boop-memory__recall", query: "calendar scheduling constraints launch week", @@ -591,116 +609,297 @@ const agentTemplates: AgentTemplate[] = [ query: "shipped blockers tomorrow", conversationId: "demo:conversation:launch-week", }, + { + name: "Sub-agent · Inbox evidence pack", + task: "Collect the three email threads that matter for the product review and summarize the decision needed in each one.", + result: + "Found the renewal thread, the escalation thread, and the invoice question. Marked the renewal as the only one that needs a reply before the review.", + status: "completed", + integrations: ["gmail", "boop_memory"], + tool: "mcp__gmail__search_email", + query: "newer_than:48h renewal OR escalation OR invoice", + conversationId: "demo:conversation:morning-brief", + }, + { + name: "Sub-agent · Calendar pressure map", + task: "Check the calendar for conflicts, prep gaps, and movable meetings before the afternoon product review.", + result: + "Mapped two tight windows, found one movable recruiting sync, and preserved a 90-minute writing block before the product review.", + status: "running", + integrations: ["googlecalendar", "boop_memory"], + tool: "mcp__googlecalendar__list_events", + query: "today conflicts prep gaps product review", + conversationId: "demo:conversation:morning-brief", + }, + { + name: "Sub-agent · Launch blocker sweep", + task: "Search Linear and GitHub for launch blockers that changed since yesterday and group them by owner.", + result: + "Grouped six blockers by owner. Two need reproduction notes, one is waiting on review, and three are already in progress.", + status: "completed", + integrations: ["linear", "github"], + tool: "mcp__linear__search_issues", + query: "label:launch-blocker updated_since:yesterday", + conversationId: "demo:conversation:launch-week", + }, + { + name: "Sub-agent · Customer reply polish", + task: "Draft the customer reply from the latest incident window and leave it pending for approval.", + result: + "Prepared a pending draft that acknowledges the missed deadline, explains the retry fix, and promises the next check-in time.", + status: "completed", + integrations: ["gmail", "notion", "linear"], + tool: "mcp__gmail__create_draft", + query: "customer incident retry fix next check-in", + conversationId: "demo:conversation:launch-week", + }, + { + name: "Sub-agent · Notes and receipt sweep", + task: "Search local notes and Drive for open personal-admin items that should not be lost during launch week.", + result: + "Found the package pickup cutoff, the reimbursement packet, and the hotel cancellation deadline. Added them to the afternoon brief.", + status: "completed", + integrations: ["apple-notes", "googledrive", "apple-reminders"], + tool: "mcp__apple-notes__search_notes", + query: "package pickup reimbursement hotel cancellation", + conversationId: "demo:conversation:personal-admin", + }, + { + name: "Product review command center", + task: "Respond to the user's text: 'What needs my attention before the product review?' Delegate inbox, calendar, blocker, draft, and personal-admin checks, then return a concise action plan.", + result: + "Prepared the product review brief: send the renewal reply, move the recruiting sync, review two launch blockers, and handle the package pickup before the evening cutoff.", + status: "completed", + integrations: ["imessage", "gmail", "googlecalendar", "linear", "slack", "boop_memory"], + tool: "mcp__imessage__read_messages", + query: "latest product review attention request", + conversationId: "demo:conversation:morning-brief", + }, ]; const memoryTemplates: MemoryTemplate[] = [ { content: - "The debug dashboard redesign should feel like a calm productivity desktop app, with the left side as the app background and the right side as the main container.", - segment: "project", + "Prefers the recommended next action first, followed by the short reason and any tradeoffs.", + segment: "preference", tier: "permanent", - importance: 0.96, + importance: 0.95, + graphLabel: "Lead with the action", }, { content: - "Use Geist Sans for interface text and Geist Mono for debug values, IDs, costs, and token counts.", + "Default to the home timezone for scheduling, but confirm travel plans in the destination's local time.", segment: "preference", tier: "permanent", - importance: 0.91, + importance: 0.9, + graphLabel: "Timezone-aware scheduling", }, { content: - "The user prefers compact, direct engineering communication without cheerleading or generic filler.", + "Avoid meetings before 9:30 AM on Fridays unless the exception is explicitly approved.", segment: "preference", - tier: "permanent", - importance: 0.93, + tier: "long", + importance: 0.88, + graphLabel: "No early Fridays", }, { content: - "Avoid meetings before 9:30 AM on Fridays unless the user explicitly approves the exception.", + "Keep evenings after 6 PM open unless the request is urgent or the evening is explicitly available.", segment: "preference", tier: "long", - importance: 0.88, + importance: 0.86, + graphLabel: "Protect evenings", }, { content: - "Launch week owners: backend owns webhook retry policy, design owns dashboard QA, and support owns customer follow-up drafts.", - segment: "relationship", - tier: "long", - importance: 0.79, + "Customer reply drafts should be warm, specific, and left pending for review unless sending was pre-approved.", + segment: "preference", + tier: "permanent", + importance: 0.91, + graphLabel: "Review drafts first", }, { content: - "The beta feedback summary should cite source Slack messages and group themes by calendar trust, memory explainability, and connection recovery.", - segment: "project", + "Calendar holds should include location, dial-in link, prep doc, and the decision expected in the meeting.", + segment: "preference", tier: "long", - importance: 0.82, + importance: 0.84, + graphLabel: "Decision-ready meetings", }, { content: - "If an automation writes a customer draft, it should leave the draft pending unless the user has pre-approved that exact workflow.", - segment: "knowledge", - tier: "permanent", + "When comparing options, rank by practical tradeoffs first instead of abstract pros and cons.", + segment: "preference", + tier: "long", importance: 0.87, + graphLabel: "Practical tradeoffs", }, { content: - "The user likes very rounded controls for segmented switches and toggles, but dashboard cards should be slightly less rounded.", - segment: "preference", + "Current launch checklist: pricing FAQ, support macros, webhook recovery notes, billing edge cases, and rollback plan.", + segment: "project", tier: "long", importance: 0.83, + graphLabel: "Launch checklist", }, { content: - "The top-right model badge should show both provider logo and model name, while connection health can live under the logo on the left.", - segment: "project", + "Weekend errands should be grouped by neighborhood so the user does not make extra trips across town.", + segment: "preference", tier: "long", - importance: 0.81, + importance: 0.78, + graphLabel: "Batch errands by area", }, { content: - "When demo mode is enabled, fake data must stay namespaced so it can be removed without touching real user data.", - segment: "knowledge", - tier: "permanent", - importance: 0.9, + "For dinner plans with the partner, aim for around 7 PM and somewhere quiet enough to talk.", + segment: "relationship", + tier: "long", + importance: 0.75, + graphLabel: "Quiet dinners at seven", }, { content: - "For dashboard previews, agents should include detailed tool_use and tool_result logs so the detail screen does not look empty.", - segment: "project", - tier: "long", - importance: 0.84, + "Workout suggestions should protect knees and shoulders and include warmup cues before heavier sets.", + segment: "preference", + tier: "permanent", + importance: 0.82, + graphLabel: "Joint-friendly training", }, { content: - "The user wants Settings, Connections, Memories, Events, Automations, and Consolidation to all share the same visual language.", - segment: "project", + "For travel, prefer nonstop flights, aisle seats, and hotels within walking distance of meetings.", + segment: "preference", tier: "long", - importance: 0.86, + importance: 0.8, + graphLabel: "Nonstop, aisle, walkable", }, ]; const memoryFillers = [ - ["context", "short", "The last dashboard preview was on localhost:5174 and the user was reviewing it in the in-app browser."], - ["project", "short", "The right-side panel should be flush to the top, right, and bottom edges without a visible right gutter."], - ["knowledge", "long", "Tool traces are stored in agentLogs and rendered as a timeline on the agent detail screen."], - ["context", "short", "The connection screen should distinguish connected, stale, and action-required account states."], - ["preference", "long", "Use icons for recognisable actions instead of text-only controls when a familiar symbol is available."], - ["project", "short", "Agent Health should avoid placeholder metrics that do not map to real product concepts."], - ["knowledge", "long", "Memory records can be filtered by tier and segment, and active records power the dashboard memory counts."], - ["relationship", "long", "The product review attendees usually include the requester, the backend owner, the design owner, the support owner, and a rotating support lead."], - ["context", "short", "The latest customer escalation is about delayed webhook retry recovery and needs a short factual reply."], - ["identity", "permanent", "The user is building a personal agent workflow around iMessage, memory, automations, and integrations."], - ["project", "long", "Demo data should include enough automation runs to test completed, failed, and running states."], - ["knowledge", "long", "Consolidation runs store a JSON details blob with proposals, decisions, and applied changes."], - ["preference", "permanent", "The user prefers dense but organized operational dashboards over decorative marketing layouts."], - ["context", "short", "The morning brief should call out urgent email, calendar pressure, open blockers, and pending drafts."], - ["project", "long", "The dashboard uses Google favicon service for integration logos so the app does not need bundled brand assets."], - ["knowledge", "long", "Usage records are append-only rows and drive cost and token summaries in the dashboard charts."], - ["relationship", "long", "The backend owner should be pinged only after the retry reproduction notes are complete."], - ["context", "short", "The hotel hold expires tomorrow evening and should be checked before the end-of-day digest."], + ["identity", "permanent", "Runs a small software team and splits time between product decisions, customer support, and engineering reviews."], + ["identity", "permanent", "Comfortable reading implementation details, but wants the conclusion before the code path."], + ["preference", "permanent", "Status updates should say what changed, what is blocked, who owns it, and the next concrete step."], + ["preference", "long", "Customer-facing copy should sound plainspoken and specific, not polished into generic launch language."], + ["preference", "long", "Code review summaries should lead with risks and file references before nice-to-have cleanup."], + ["preference", "long", "Shopping research should separate the cheapest good option from the best premium option."], + ["preference", "long", "Restaurant picks should take reservations, be quiet enough for conversation, and avoid very late seatings."], + ["preference", "short", "Today's task list should be sorted by urgency, not by project area."], + ["relationship", "long", "The design lead cares most about spacing consistency, mobile screenshots, and whether empty states feel intentional."], + ["relationship", "long", "The support lead wants the customer's exact wording quoted before a reply draft is written."], + ["relationship", "long", "The operations contact wants receipts grouped by month and project before reimbursement is submitted."], + ["relationship", "long", "Send the family group chat travel ETAs only after flight times are confirmed."], + ["relationship", "short", "The launch FAQ owner is waiting on the final pricing answer before publishing the help-center draft."], + ["project", "long", "Launch FAQ still needs answers for pricing, refunds, account deletion, data export, and webhook setup."], + ["project", "long", "Beta feedback digest should group notes into onboarding confusion, notification trust, and missing integrations."], + ["project", "long", "Billing cleanup has three open items: duplicate receipts, seat count mismatch, and tax settings."], + ["project", "short", "Customer escalation reply needs the corrected incident window, mitigation status, and next update time."], + ["project", "short", "Next team update should mention the shipped webhook fix, remaining setup friction, and current support queue volume."], + ["project", "short", "Demo script should show a real before-and-after workflow instead of walking through every feature."], + ["project", "long", "Hiring scorecard should emphasize writing clarity, product judgment, and comfort debugging ambiguous systems."], + ["knowledge", "permanent", "Do not treat absence of a remembered fact as proof it is false; search memory first, then answer carefully."], + ["knowledge", "long", "A draft is not sent until the user explicitly approves it, even when the content looks complete."], + ["knowledge", "long", "For billing or legal commitments, summarize uncertainty and ask for confirmation before acting."], + ["knowledge", "long", "Webhook-dependent workflows need both the active tunnel URL and the provider's registered URL checked."], + ["knowledge", "long", "Travel plans should include date, local timezone, airport transfer, and cancellation deadline."], + ["knowledge", "long", "Recurring automations should report when they skipped work because no new source material appeared."], + ["knowledge", "short", "Focus block ends at 3 PM; do not interrupt it for non-urgent pings."], + ["context", "short", "Morning brief should include urgent email, calendar pressure, open blockers, and pending drafts."], + ["context", "short", "Tomorrow afternoon's calendar hold still needs an agenda and attendee list."], + ["context", "short", "Reimbursement summary is due this week and should include travel, software, and meal receipts."], + ["context", "short", "Latest customer reply draft needs a tone check for defensiveness before it is sent."], + ["context", "short", "Grocery list should avoid ingredients already at home and keep weeknight meals under 30 minutes."], + ["context", "short", "Next workout should be lower impact because the user's knee felt irritated after the last run."], + ["context", "short", "Hotel comparison should prioritize sleep quality, walking distance, and cancellation policy over lobby amenities."], + ["preference", "long", "When presenting calendar conflicts, explain the tradeoff and recommend which meeting to move."], + ["preference", "long", "Financial summaries should include totals, what changed since last time, and anything needing approval."], + ["preference", "long", "Use Markdown tables for compact comparisons, but use prose for final recommendations."], + ["preference", "long", "For long documents, start with the short answer and then include a skim-friendly outline."], + ["preference", "permanent", "Prefer a thoughtful caveat over a confident answer based on stale information."], + ["preference", "long", "Leave a buffer after back-to-back calls before scheduling deep work."], + ["relationship", "long", "Partner likes itinerary summaries with times, neighborhoods, and confirmation numbers hidden unless needed."], + ["relationship", "short", "Contractor is waiting for feedback on the revised statement of work."], + ["relationship", "short", "Finance contact prefers reimbursement packets as one organized PDF."], + ["relationship", "long", "Customer success lead wants escalation summaries to include owner, severity, and promised follow-up time."], + ["relationship", "long", "Editor prefers punchy first lines and fewer abstract claims in public posts."], + ["project", "long", "Next public update should focus on workflow outcomes rather than listing every new setting."], + ["project", "long", "Onboarding checklist should distinguish required setup from optional integrations."], + ["project", "long", "Support macro refresh should remove apologetic filler and add clearer troubleshooting steps."], + ["project", "short", "Bug bash is focused on login recovery, webhook setup, notification copy, and empty states."], + ["project", "short", "Screenshots need to be checked on desktop and mobile before sharing the update."], + ["project", "short", "Launch note should avoid saying 'AI-powered' unless the sentence explains the actual user benefit."], + ["knowledge", "permanent", "Never include private customer details in public release notes or demo screenshots."], + ["knowledge", "long", "If an automation finds sensitive content, summarize categories and ask before quoting details."], + ["knowledge", "long", "Weekly digest should separate shipped work, decisions needed, blocked work, and follow-ups."], + ["context", "short", "Package pickup reminder should fire before the building's evening cutoff."], + ["context", "short", "Next status update should mention what was verified locally and what still needs review."], + ["relationship", "long", "Partner prefers dinner around 7 PM, quiet seating, and no tasting menus on weeknights."], + ["project", "long", "Investor update still needs MRR, churn, hiring, and runway sections checked against source numbers."], + ["preference", "long", "For flights under four hours, prefer nonstop economy aisle over a cheaper connection."], + ["preference", "permanent", "For customer escalations, acknowledge the specific failure first, then explain the fix and next check-in time."], + ["context", "short", "Protect a 90-minute writing block before the afternoon call today."], + ["project", "short", "Renewal reply should acknowledge the missed deadline first, then propose the make-good and next check-in."], ] satisfies Array<[MemorySegment, MemoryTier, string]>; +type DemoMemoryTopic = + | "launch" + | "customer-care" + | "daily-rhythm" + | "people" + | "travel" + | "home-life" + | "wellbeing" + | "principles"; + +const demoMemoryTopicRules: Array<[DemoMemoryTopic, RegExp]> = [ + ["wellbeing", /workout|knee|shoulder|warmup|run|lower impact|health/i], + ["travel", /travel|flight|hotel|airport|itinerary|aisle|nonstop|cancellation/i], + ["home-life", /grocery|errand|package|dinner|restaurant|weekend|meal|neighborhood/i], + [ + "people", + /partner|family|design lead|support lead|operations contact|contractor|finance contact|customer success lead|stakeholder/i, + ], + ["customer-care", /customer|support|escalation|reply|draft|public post|copy|editor/i], + [ + "launch", + /launch|beta|dashboard|onboarding|release|pricing|webhook|bug bash|screenshots?|product|billing|implementation|software team|code review/i, + ], + [ + "daily-rhythm", + /calendar|meeting|schedule|timezone|friday|focus block|deep work|morning brief|status update|weekly digest|automation|evenings? after/i, + ], +]; + +function demoMemoryTopic(row: MemoryTemplate): DemoMemoryTopic { + return demoMemoryTopicRules.find(([, pattern]) => pattern.test(row.content))?.[0] ?? "principles"; +} + +function demoMemoryGraphLabel(row: MemoryTemplate): string { + if (row.graphLabel) return row.graphLabel; + const labelRules: Array<[RegExp, string]> = [ + [/billing cleanup/i, "Billing cleanup"], + [/customer success lead/i, "Escalation handoffs"], + [/grocery list/i, "Fast weeknight meals"], + [/hotel comparison/i, "Walkable, flexible hotels"], + [/weekly digest/i, "Structured weekly digest"], + [/package pickup/i, "Package cutoff"], + [/latest customer reply/i, "Tone-check the draft"], + [/status updates should/i, "Actionable status updates"], + [/beta feedback digest/i, "Beta feedback themes"], + [/launch faq/i, "Launch FAQ"], + ]; + const matchedLabel = labelRules.find(([pattern]) => pattern.test(row.content))?.[1]; + if (matchedLabel) return matchedLabel; + const cleaned = row.content + .replace(/^(the user|user|current|latest|next)\s+/i, "") + .replace(/^(for|when|if)\s+/i, "") + .replace(/[.:;,]+$/g, "") + .trim(); + const words = cleaned.split(/\s+/).filter(Boolean); + const label = words.slice(0, 5).join(" "); + return words.length > 5 ? `${label}...` : label; +} + const automationSeeds = [ { id: "demo:auto:morning-command-center", @@ -790,16 +989,97 @@ async function seedConversations(ctx: MutationCtx, now: number) { return { conversations: conversationSeeds.length, messages: messageCount }; } +type DemoLogRow = { + logType: "thinking" | "tool_use" | "tool_result" | "text" | "error"; + toolName?: string; + accounts?: string[]; + content: string; +}; + +function demoToolForIntegration(integration: string): string { + const normalized = integration.toLowerCase(); + const map: Record = { + gmail: "mcp__gmail__search_email", + googlecalendar: "mcp__googlecalendar__list_events", + linear: "mcp__linear__search_issues", + slack: "mcp__slack__search_messages", + notion: "mcp__notion__search", + github: "mcp__github__search_code", + googledrive: "mcp__googledrive__search", + googledocs: "mcp__googledocs__fetch", + googlesheets: "mcp__googlesheets__read", + imessage: "mcp__imessage__read_messages", + "apple-notes": "mcp__apple-notes__search_notes", + "apple-reminders": "mcp__apple-reminders__list_reminders", + boop_memory: "mcp__boop-memory__recall", + boop_usage: "mcp__boop-usage__summary", + }; + return map[normalized] ?? `mcp__${normalized.replace(/[^a-z0-9]+/g, "_")}__search`; +} + +function demoAccountForIntegration(integration: string): string { + const normalized = integration.toLowerCase(); + const map: Record = { + gmail: "primary_inbox_demo", + googlecalendar: "work_calendar_demo", + linear: "product_workspace_demo", + slack: "team_workspace_demo", + notion: "launch_workspace_demo", + github: "engineering_org_demo", + googledrive: "shared_drive_demo", + googledocs: "shared_drive_demo", + googlesheets: "shared_drive_demo", + imessage: "local_messages_demo", + "apple-notes": "local_notes_demo", + "apple-reminders": "local_reminders_demo", + boop_memory: "boop_memory_demo", + boop_usage: "boop_usage_demo", + }; + return map[normalized] ?? `${normalized.replace(/[^a-z0-9]+/g, "_")}_demo`; +} + +function demoToolResultText(template: AgentTemplate, integration: string, status: AgentStatus) { + if (status === "failed") { + return `Partial ${integration} result returned before the retryable failure. Kept successful evidence attached to the run.`; + } + const displayNames: Record = { + gmail: "Gmail", + googlecalendar: "Google Calendar", + linear: "Linear", + slack: "Slack", + notion: "Notion", + github: "GitHub", + googledrive: "Google Drive", + googledocs: "Google Docs", + googlesheets: "Google Sheets", + imessage: "iMessage", + "apple-notes": "Apple Notes", + "apple-reminders": "Apple Reminders", + boop_memory: "Boop memory", + boop_usage: "Boop usage", + }; + const subject = displayNames[integration] ?? integration.replace(/[_-]+/g, " "); + return `${subject} returned relevant context for "${template.name}". ${template.result}`; +} + +function shouldShowDelegation(template: AgentTemplate, index: number): boolean { + return ( + template.name === "Product review command center" || + template.name.includes("command center") || + (template.integrations.length >= 3 && index % 4 === 0) + ); +} + async function seedAgentsAndLogs(ctx: MutationCtx, now: number) { const statuses: AgentStatus[] = [ "completed", "completed", "running", "completed", - "failed", "completed", "completed", - "cancelled", + "completed", + "completed", "spawned", "completed", "completed", @@ -811,7 +1091,7 @@ async function seedAgentsAndLogs(ctx: MutationCtx, now: number) { const agentId = `demo:agent:${String(index + 1).padStart(2, "0")}`; const runtime: Runtime = index % 3 === 0 || index % 3 === 1 ? "codex" : "claude"; const billingMode: BillingMode = runtime === "codex" ? "codex-subscription" : "api"; - const status = pick(statuses, index); + const status = template.status ?? pick(statuses, index); const isActive = status === "running" || status === "spawned"; const startedAt = isActive ? now - (45 + (index % 6) * 34) * 1000 @@ -861,74 +1141,116 @@ async function seedAgentsAndLogs(ctx: MutationCtx, now: number) { }); const baseLogTime = startedAt + 250; - const account = `${template.integrations[0] ?? "boop"}_demo`; - const toolResult = { - successful: status !== "failed", - source: template.integrations[0] ?? "boop", - results: [ - { - title: template.name, - summary: status === "failed" ? "Partial result before retryable failure" : template.result, - url: `https://example.com/demo/${encodeURIComponent(agentId)}`, - }, - ], - }; + const primaryIntegration = template.integrations[0] ?? "boop_memory"; + const secondaryIntegration = template.integrations[1] ?? primaryIntegration; + const primaryAccount = demoAccountForIntegration(primaryIntegration); + const secondaryAccount = demoAccountForIntegration(secondaryIntegration); + const contextTool = "mcp__boop-memory__recall"; + const secondaryTool = demoToolForIntegration(secondaryIntegration); + const delegationRows: DemoLogRow[] = shouldShowDelegation(template, index) + ? [ + { + logType: "tool_use", + toolName: "spawn_agent", + accounts: ["boop_runtime_demo"], + content: JSON.stringify({ + name: "Delegated evidence sweep", + task: + "Spawn focused sub-agents for inbox evidence, calendar pressure, launch blockers, and customer reply drafting.", + integrations: template.integrations.slice(0, 5), + }), + }, + { + logType: "tool_result", + toolName: "spawn_agent", + accounts: ["boop_runtime_demo"], + content: JSON.stringify({ + successful: true, + data: { + results: + "Spawned sub-agents: Inbox evidence pack, Calendar pressure map, Launch blocker sweep, and Customer reply polish. All returned source-backed summaries.", + }, + spawnedAgents: [ + "demo:agent:29", + "demo:agent:30", + "demo:agent:31", + "demo:agent:32", + ], + }), + }, + ] + : []; - const logRows = [ + const logRows: DemoLogRow[] = [ { logType: "thinking" as const, - content: `Planning ${template.integrations.join(", ")} calls and checking relevant memories for this turn.\n`, + content: `Planning ${template.integrations.join(", ")} calls, checking durable memories, and deciding whether a focused sub-agent should own any part of the work.\n`, }, + { + logType: "tool_use", + toolName: contextTool, + accounts: ["boop_memory_demo"], + content: JSON.stringify({ + query: template.task, + limit: 6, + includeSegments: ["preference", "project", "context"], + }), + }, + { + logType: "tool_result", + toolName: contextTool, + accounts: ["boop_memory_demo"], + content: JSON.stringify({ + successful: true, + data: { + results: + "Recalled: keep drafts pending for approval; lead with the recommended action; protect the 90-minute writing block; group launch blockers by owner.", + }, + }), + }, + ...delegationRows, { logType: "tool_use" as const, toolName: template.tool, - accounts: [account], + accounts: [primaryAccount], content: JSON.stringify({ - tool: template.tool, - args: { query: template.query, limit: 10, demo: true }, + query: template.query, + limit: 10, + account: primaryAccount, }), }, { logType: "tool_result" as const, toolName: template.tool, - accounts: [account], - content: JSON.stringify(toolResult), + accounts: [primaryAccount], + content: JSON.stringify({ + successful: status !== "failed", + data: { + results: demoToolResultText(template, primaryIntegration, status), + }, + source: primaryIntegration, + }), }, { logType: "tool_use" as const, - toolName: pick( - [ - "mcp__boop-memory__recall", - "mcp__notion__search", - "mcp__linear__search_issues", - "mcp__github__search_code", - ], - index, - ), - accounts: ["boop_demo"], + toolName: secondaryTool, + accounts: [secondaryAccount], content: JSON.stringify({ - tool: "context_lookup", - args: { query: template.task, limit: 6, demo: true }, + query: template.task, + limit: 5, + account: secondaryAccount, }), }, { logType: "tool_result" as const, - toolName: pick( - [ - "mcp__boop-memory__recall", - "mcp__notion__search", - "mcp__linear__search_issues", - "mcp__github__search_code", - ], - index, - ), - accounts: ["boop_demo"], + toolName: secondaryTool, + accounts: [secondaryAccount], content: JSON.stringify({ successful: true, - memories: [ - "User prefers concise summaries grouped by owner.", - "Dashboard should use compact rounded panels and realistic operational data.", - ], + data: { + results: demoToolResultText(template, secondaryIntegration, "completed"), + }, + source: secondaryIntegration, }), }, { @@ -973,25 +1295,38 @@ async function seedMemories(ctx: MutationCtx, now: number) { importance: compactNumber(0.54 + (index % 8) * 0.045, 2), })), ]; + const seededRows = Array.from({ length: 72 }, (_, index) => pick(rows, index)); + const topics = seededRows.map((row) => demoMemoryTopic(row)); + const memoryIdsByTopic = new Map(); + topics.forEach((topic, index) => { + const memoryIds = memoryIdsByTopic.get(topic) ?? []; + memoryIds.push(`demo:mem:${String(index + 1).padStart(3, "0")}`); + memoryIdsByTopic.set(topic, memoryIds); + }); let memories = 0; - for (let index = 0; index < 72; index += 1) { - const row = pick(rows, index); + for (let index = 0; index < seededRows.length; index += 1) { + const row = seededRows[index]!; + const topic = topics[index]!; + const topicMemoryIds = memoryIdsByTopic.get(topic) ?? []; + const topicIndex = topicMemoryIds.indexOf(`demo:mem:${String(index + 1).padStart(3, "0")}`); + const relatedMemoryIds = [ + topicMemoryIds[topicIndex - 1], + topicMemoryIds[topicIndex + 1], + ].filter((memoryId): memoryId is string => Boolean(memoryId)); const lifecycle = index % 23 === 0 ? "archived" : index % 31 === 0 ? "pruned" : "active"; await ctx.db.insert("memoryRecords", { memoryId: `demo:mem:${String(index + 1).padStart(3, "0")}`, - content: - index < rows.length - ? row.content - : `${row.content} Demo variation ${index - rows.length + 1} with a different source turn and access pattern.`, + content: row.content, tier: row.tier, segment: row.segment, importance: row.importance, decayRate: compactNumber(0.01 + (index % 7) * 0.006, 3), accessCount: (index * 7) % 29, lastAccessedAt: ago(now, index % 10, (index % 6) * 33 * MINUTE), - sourceTurn: `demo:turn:${index % conversationSeeds.length}-${index % 8}`, + sourceTurn: `demo:turn:${topic}:${index % 4}`, lifecycle, + embedding: demoEmbedding(row.content), supersedes: index % 17 === 0 && index > 0 ? [`demo:mem:${String(index).padStart(3, "0")}`] @@ -1000,6 +1335,11 @@ async function seedMemories(ctx: MutationCtx, now: number) { demo: true, confidence: compactNumber(0.72 + (index % 9) * 0.025, 2), source: pick(["iMessage", "Gmail", "Calendar", "Linear", "Consolidation"], index), + graph: { + topic, + label: demoMemoryGraphLabel(row), + relatedMemoryIds, + }, }), createdAt: ago(now, Math.floor(index / 6), (index % 6) * 41 * MINUTE), }); @@ -1019,29 +1359,26 @@ async function seedMemoryEvents(ctx: MutationCtx, now: number) { "consolidation.proposed", "consolidation.applied", ]; + const eventCopy = [ + "Extracted a scheduling preference from the product-review thread.", + "Recalled launch-week blockers for the command-center agent.", + "Wrote a short-term reminder for the package pickup cutoff.", + "Promoted the draft-approval rule after repeated confirmations.", + "Merged duplicate memories about customer escalation reply tone.", + "Pruned an expired calendar hold after the meeting window passed.", + "Proposed consolidation of overlapping launch checklist memories.", + "Applied memory cleanup and preserved source-linked evidence.", + ]; let memoryEvents = 0; for (let index = 0; index < 128; index += 1) { const memoryId = `demo:mem:${String((index % 72) + 1).padStart(3, "0")}`; + const eventType = pick(eventTypes, index); await ctx.db.insert("memoryEvents", { - eventType: pick(eventTypes, index), + eventType, conversationId: pick(conversationSeeds, index).id, memoryId, agentId: `demo:agent:${String((index % agentTemplates.length) + 1).padStart(2, "0")}`, - data: JSON.stringify({ - demo: true, - memoryId, - reason: pick( - [ - "Matched scheduling preference", - "Deduplicated similar project facts", - "Promoted durable product preference", - "Recalled for launch-week context", - "Pruned short-lived calendar detail", - ], - index, - ), - score: compactNumber(0.61 + (index % 20) * 0.017, 3), - }), + data: `${pick(eventCopy, index)} Score ${compactNumber(0.61 + (index % 20) * 0.017, 3)}.`, createdAt: ago(now, Math.floor(index / 10), (index % 10) * 17 * MINUTE), }); memoryEvents += 1; @@ -1240,7 +1577,7 @@ export const status = query({ seeded: total > 0, counts, total, - scanLimit: SCAN_LIMIT, + scanLimit: DEMO_SCAN_LIMIT, }; }, }); diff --git a/convex/demoMode.ts b/convex/demoMode.ts new file mode 100644 index 00000000..2790e0bd --- /dev/null +++ b/convex/demoMode.ts @@ -0,0 +1,45 @@ +import type { MutationCtx, QueryCtx } from "./_generated/server"; + +export const DEMO_PREFIX = "demo:"; +export const DEMO_SETTING_KEY = "debug_demo_mode"; +export const DEMO_SCAN_LIMIT = 5000; + +export function isDemoId(value?: string | null): boolean { + return typeof value === "string" && value.startsWith(DEMO_PREFIX); +} + +export async function isDemoModeEnabled(ctx: QueryCtx | MutationCtx): Promise { + const row = await ctx.db + .query("settings") + .withIndex("by_key", (q) => q.eq("key", DEMO_SETTING_KEY)) + .unique(); + return row?.value === "true"; +} + +export function isDemoMessage(row: { conversationId?: string | null; agentId?: string | null }) { + return isDemoId(row.conversationId) || isDemoId(row.agentId); +} + +export function isDemoMemoryEvent(row: { + conversationId?: string | null; + memoryId?: string | null; + agentId?: string | null; +}) { + return isDemoId(row.conversationId) || isDemoId(row.memoryId) || isDemoId(row.agentId); +} + +export function isDemoAutomationRun(row: { + runId?: string | null; + automationId?: string | null; + agentId?: string | null; +}) { + return isDemoId(row.runId) || isDemoId(row.automationId) || isDemoId(row.agentId); +} + +export function isDemoUsageRecord(row: { + conversationId?: string | null; + agentId?: string | null; + runId?: string | null; +}) { + return isDemoId(row.conversationId) || isDemoId(row.agentId) || isDemoId(row.runId); +} diff --git a/convex/memoryEvents.ts b/convex/memoryEvents.ts index 90b73bb2..131f1a97 100644 --- a/convex/memoryEvents.ts +++ b/convex/memoryEvents.ts @@ -1,5 +1,6 @@ -import { mutation, query } from "./_generated/server"; +import { mutation, query, type QueryCtx } from "./_generated/server"; import { v } from "convex/values"; +import { DEMO_SCAN_LIMIT, isDemoId, isDemoMemoryEvent, isDemoModeEnabled } from "./demoMode"; export const emit = mutation({ args: { @@ -14,16 +15,47 @@ export const emit = mutation({ }, }); +async function readRecent( + ctx: QueryCtx, + limit: number, + demoOnly: boolean, +) { + const rows = await ctx.db + .query("memoryEvents") + .order("desc") + .take(DEMO_SCAN_LIMIT); + return rows.filter((event) => isDemoMemoryEvent(event) === demoOnly).slice(0, limit); +} + export const recent = query({ + args: { limit: v.optional(v.number()) }, + handler: async (ctx, args) => readRecent(ctx, args.limit ?? 100, false), +}); + +export const recentForDashboard = query({ args: { limit: v.optional(v.number()) }, handler: async (ctx, args) => { - return await ctx.db.query("memoryEvents").order("desc").take(args.limit ?? 100); + const limit = args.limit ?? 100; + return readRecent(ctx, limit, await isDemoModeEnabled(ctx)); }, }); export const byConversation = query({ args: { conversationId: v.string(), limit: v.optional(v.number()) }, handler: async (ctx, args) => { + if (isDemoId(args.conversationId)) return []; + return await ctx.db + .query("memoryEvents") + .withIndex("by_conversation", (q) => q.eq("conversationId", args.conversationId)) + .order("desc") + .take(args.limit ?? 100); + }, +}); + +export const byConversationForDashboard = query({ + args: { conversationId: v.string(), limit: v.optional(v.number()) }, + handler: async (ctx, args) => { + if (isDemoId(args.conversationId) !== (await isDemoModeEnabled(ctx))) return []; return await ctx.db .query("memoryEvents") .withIndex("by_conversation", (q) => q.eq("conversationId", args.conversationId)) diff --git a/convex/memoryRecords.ts b/convex/memoryRecords.ts index 6cd91b22..97a74491 100644 --- a/convex/memoryRecords.ts +++ b/convex/memoryRecords.ts @@ -1,7 +1,8 @@ -import { action, mutation, query } from "./_generated/server"; +import { action, mutation, query, type QueryCtx } from "./_generated/server"; import { v } from "convex/values"; import { api } from "./_generated/api"; -import type { Id } from "./_generated/dataModel"; +import type { Doc, Id } from "./_generated/dataModel"; +import { DEMO_SCAN_LIMIT, isDemoId, isDemoModeEnabled } from "./demoMode"; const tierV = v.union(v.literal("short"), v.literal("long"), v.literal("permanent")); const segmentV = v.union( @@ -15,6 +16,17 @@ const segmentV = v.union( ); const lifecycleV = v.union(v.literal("active"), v.literal("archived"), v.literal("pruned")); +type MemoryTier = "short" | "long" | "permanent"; +type MemorySegment = + | "identity" + | "preference" + | "correction" + | "relationship" + | "project" + | "knowledge" + | "context"; +type MemoryLifecycle = "active" | "archived" | "pruned"; + export const upsert = mutation({ args: { memoryId: v.string(), @@ -94,7 +106,7 @@ export const getByIds = query({ const out = []; for (const id of args.ids) { const r = await ctx.db.get(id); - if (r) out.push(r); + if (r && !isDemoId(r.memoryId)) out.push(r); } return out; }, @@ -102,41 +114,81 @@ export const getByIds = query({ export const vectorSearch = action({ args: { embedding: v.array(v.float64()), limit: v.optional(v.number()) }, - handler: async (ctx, args): Promise; score: number; record: any }>> => { + handler: async ( + ctx, + args, + ): Promise< + Array<{ _id: Id<"memoryRecords">; score: number; record: Doc<"memoryRecords"> }> + > => { + const limit = Math.max(1, Math.min(args.limit ?? 20, 256)); const results = await ctx.vectorSearch("memoryRecords", "by_embedding", { vector: args.embedding, - limit: args.limit ?? 20, + // Demo vectors share the same index. Oversample enough to filter them + // without allowing seeded showcase data into real memory recall. + limit: Math.min(256, limit + 100), filter: (q) => q.eq("lifecycle", "active"), }); const records = await ctx.runQuery(api.memoryRecords.getByIds, { ids: results.map((r) => r._id), }); - const byId = new Map(records.map((r: any) => [r._id, r])); + const byId = new Map(records.map((record) => [record._id, record])); return results .map((r) => ({ _id: r._id, score: r._score, record: byId.get(r._id) })) - .filter((r) => r.record); + .filter( + (result): result is { + _id: Id<"memoryRecords">; + score: number; + record: Doc<"memoryRecords">; + } => Boolean(result.record), + ) + .slice(0, limit); }, }); +type MemoryListArgs = { + tier?: MemoryTier; + segment?: MemorySegment; + lifecycle?: MemoryLifecycle; + limit?: number; +}; + +async function readMemories(ctx: QueryCtx, args: MemoryListArgs, demoOnly: boolean) { + const limit = args.limit ?? 100; + const results = args.tier + ? await ctx.db + .query("memoryRecords") + .withIndex("by_tier", (q) => q.eq("tier", args.tier!)) + .order("desc") + .take(DEMO_SCAN_LIMIT) + : args.segment + ? await ctx.db + .query("memoryRecords") + .withIndex("by_segment", (q) => q.eq("segment", args.segment!)) + .order("desc") + .take(DEMO_SCAN_LIMIT) + : await ctx.db.query("memoryRecords").order("desc").take(DEMO_SCAN_LIMIT); + const lifecycle = args.lifecycle ?? "active"; + return results + .filter((record) => isDemoId(record.memoryId) === demoOnly && record.lifecycle === lifecycle) + .slice(0, limit); +} + +const listArgs = { + tier: v.optional(tierV), + segment: v.optional(segmentV), + lifecycle: v.optional(lifecycleV), + limit: v.optional(v.number()), +}; + export const list = query({ - args: { - tier: v.optional(tierV), - segment: v.optional(segmentV), - lifecycle: v.optional(lifecycleV), - limit: v.optional(v.number()), - }, + args: listArgs, + handler: async (ctx, args) => readMemories(ctx, args, false), +}); + +export const listForDashboard = query({ + args: listArgs, handler: async (ctx, args) => { - const limit = args.limit ?? 100; - let results; - if (args.tier) { - results = await ctx.db.query("memoryRecords").withIndex("by_tier", (q) => q.eq("tier", args.tier!)).order("desc").take(limit * 2); - } else if (args.segment) { - results = await ctx.db.query("memoryRecords").withIndex("by_segment", (q) => q.eq("segment", args.segment!)).order("desc").take(limit * 2); - } else { - results = await ctx.db.query("memoryRecords").order("desc").take(limit * 2); - } - const lifecycle = args.lifecycle ?? "active"; - return results.filter((r) => r.lifecycle === lifecycle).slice(0, limit); + return readMemories(ctx, args, await isDemoModeEnabled(ctx)); }, }); @@ -156,7 +208,7 @@ export const search = query({ .order("desc") .take(500); return active - .filter((m) => m.content.toLowerCase().includes(q)) + .filter((m) => !isDemoId(m.memoryId) && m.content.toLowerCase().includes(q)) .sort((a, b) => b.importance - a.importance) .slice(0, limit); }, @@ -219,11 +271,13 @@ const COUNTS_SCAN_LIMIT = 5000; export const embeddingStats = query({ args: {}, handler: async (ctx) => { - const all = await ctx.db + let all = await ctx.db .query("memoryRecords") .withIndex("by_lifecycle", (q) => q.eq("lifecycle", "active")) .order("desc") .take(COUNTS_SCAN_LIMIT); + const scanned = all.length; + all = all.filter((memory) => !isDemoId(memory.memoryId)); let withEmbedding = 0; let withoutEmbedding = 0; for (const m of all) { @@ -234,7 +288,7 @@ export const embeddingStats = query({ total: all.length, withEmbedding, withoutEmbedding, - truncated: all.length === COUNTS_SCAN_LIMIT, + truncated: scanned === COUNTS_SCAN_LIMIT, }; }, }); @@ -267,7 +321,7 @@ export const listUnembeddedPage = query({ }); return { page: result.page - .filter((m) => !m.embedding || m.embedding.length === 0) + .filter((m) => !isDemoId(m.memoryId) && (!m.embedding || m.embedding.length === 0)) .map((m) => ({ memoryId: m.memoryId, content: m.content })), isDone: result.isDone, continueCursor: result.continueCursor, @@ -297,7 +351,9 @@ export const setEmbedding = mutation({ export const countsByTier = query({ args: {}, handler: async (ctx) => { - const all = await ctx.db.query("memoryRecords").order("desc").take(COUNTS_SCAN_LIMIT); + const demoMode = await isDemoModeEnabled(ctx); + const rows = await ctx.db.query("memoryRecords").order("desc").take(COUNTS_SCAN_LIMIT); + const all = rows.filter((m) => isDemoId(m.memoryId) === demoMode); const active = all.filter((m) => m.lifecycle === "active"); return { short: active.filter((m) => m.tier === "short").length, @@ -305,7 +361,7 @@ export const countsByTier = query({ permanent: active.filter((m) => m.tier === "permanent").length, archived: all.filter((m) => m.lifecycle === "archived").length, pruned: all.filter((m) => m.lifecycle === "pruned").length, - truncated: all.length === COUNTS_SCAN_LIMIT, + truncated: rows.length === COUNTS_SCAN_LIMIT, scanLimit: COUNTS_SCAN_LIMIT, }; }, diff --git a/debug/src/App.tsx b/debug/src/App.tsx index b8ec5bfe..719cc55e 100644 --- a/debug/src/App.tsx +++ b/debug/src/App.tsx @@ -54,6 +54,36 @@ interface AgentSummary { status: string; } +interface DesktopStatus { + state: string; + server: string; + convex: string; + dashboard: string; + tunnel: string; + webhook?: string; + dashboardUrl?: string; + publicUrl?: string; + expectedWebhookUrl?: string; + registeredWebhookUrl?: string; + webhookDetails?: string; + webhookCheckedAt?: string; + convexUrl?: string; + phoneNumber?: string; + runtimeRoot?: string; + lastMessage?: string; +} + +type DesktopAction = + | "start" + | "stop" + | "restart" + | "checkWebhook" + | "openDashboard" + | "showRuntimeFolder" + | "status"; + +const DEMO_PHONE_NUMBER = "+11111111111"; + const NAV_ICONS: Record = { dashboard: DashboardSquare01Icon, agents: MachineRobotIcon, @@ -87,14 +117,18 @@ function getStoredTheme(): Theme { export function App() { const [view, setView] = useState("dashboard"); const [theme, setTheme] = useState(getStoredTheme); - const [runtimeConfig, setRuntimeConfig] = useState(null); + const [runtimeConfig, setRuntimeConfig] = useState< + RuntimeConfigSnapshot | null | undefined + >(undefined); const [changelogOpen, setChangelogOpen] = useState(false); + const [desktopStatus, setDesktopStatus] = useState(null); const { connected } = useSocket(); const counts = useQuery(api.memoryRecords.countsByTier, {}) as | MemoryTierCounts | undefined; - const agents = useQuery(api.agents.list, {}) as AgentSummary[] | undefined; + const agents = useQuery(api.agents.listForDashboard, {}) as AgentSummary[] | undefined; + const demoStatus = useQuery(api.demo.status); const storedRuntime = useQuery(api.settings.get, { key: "runtime" }) as | string | null @@ -121,32 +155,83 @@ export function App() { useEffect(() => { let cancelled = false; - fetch("/api/runtime-config") - .then((res) => { + let timeout: number | undefined; + let attempt = 0; + + async function loadRuntimeConfig() { + try { + const res = await fetch("/api/runtime-config", { cache: "no-store" }); if (!res.ok) throw new Error(`Runtime config fetch failed (${res.status})`); - return res.json() as Promise; - }) - .then((config) => { + const config = (await res.json()) as RuntimeConfigSnapshot; if (!cancelled) setRuntimeConfig(config); - }) - .catch(() => { - if (!cancelled) setRuntimeConfig(null); - }); + } catch { + if (cancelled) return; + setRuntimeConfig(null); + if (attempt < 20) { + const delay = Math.min(750 + attempt * 250, 4000); + attempt += 1; + timeout = window.setTimeout(loadRuntimeConfig, delay); + } + } + } + + loadRuntimeConfig(); return () => { cancelled = true; + if (timeout) window.clearTimeout(timeout); }; - }, [storedRuntime, storedClaudeModel, storedHostedModel]); + }, [ + connected, + desktopStatus?.server, + desktopStatus?.state, + storedRuntime, + storedClaudeModel, + storedHostedModel, + ]); + + useEffect(() => { + function onDesktopStatus(event: MessageEvent) { + if (event.source !== window.parent) return; + const data = event.data; + if (!data || data.type !== "boop-desktop-status") return; + setDesktopStatus(data.status as DesktopStatus); + } + + window.addEventListener("message", onDesktopStatus); + window.parent?.postMessage({ type: "boop-desktop-status-request" }, "*"); + return () => window.removeEventListener("message", onDesktopStatus); + }, []); + + function sendDesktopAction(action: DesktopAction) { + window.parent?.postMessage({ type: "boop-desktop-action", action }, "*"); + } const isDark = theme === "dark"; const currentView = NAV.find((item) => item.id === view)?.label ?? "Dashboard"; + const demoModeEnabled = demoStatus?.enabled ?? false; const storedProvider: RuntimeProvider | null = storedRuntime === "claude" || storedRuntime === "codex" ? storedRuntime : null; - const activeRuntime = runtimeConfig?.runtime ?? storedProvider ?? "claude"; - const providerLabel = activeRuntime === "codex" ? "Codex" : "Claude"; + const activeRuntime = runtimeConfig?.runtime ?? storedProvider ?? (demoModeEnabled ? "codex" : null); + const providerLabel = activeRuntime + ? activeRuntime === "codex" + ? "Codex" + : "Claude" + : "Runtime"; const modelLabel = runtimeConfig?.model ?? - (activeRuntime === "codex" ? storedHostedModel : storedClaudeModel) ?? - "Model unavailable"; + (activeRuntime + ? activeRuntime === "codex" + ? storedHostedModel + : storedClaudeModel + : undefined) ?? + (demoModeEnabled ? "gpt-5.5" : undefined) ?? + (runtimeConfig === undefined ? "Checking..." : "Model unavailable"); + const inDesktopShell = desktopStatus !== null; + const displayDesktopStatus = + desktopStatus && demoModeEnabled + ? { ...desktopStatus, phoneNumber: DEMO_PHONE_NUMBER } + : desktopStatus; + const displayConnected = demoModeEnabled ? true : connected; return (
-
setView("settings")} + className={`desktop-no-drag hidden min-w-0 items-center gap-2 rounded-2xl border px-2.5 py-1.5 text-xs sm:flex ${ isDark ? "border-white/10 bg-white/5 text-zinc-400" : "border-zinc-200 bg-white text-zinc-600" }`} title={`Active model: ${providerLabel} ${modelLabel}`} + aria-label="Open settings" > - + {activeRuntime ? ( + + ) : ( + + )} {providerLabel} {modelLabel} -
+
@@ -309,11 +386,18 @@ export function App() { {view === "dashboard" && } {view === "agents" && } {view === "automations" && } - {view === "memory" && } + {view === "memory" && ( + + )} {view === "events" && } {view === "consolidation" && } {view === "connections" && } - {view === "settings" && } + {view === "settings" && ( + + )}
); } + +function ConnectionHeader({ + connected, + desktopStatus, + isDark, + onAction, +}: { + connected: boolean; + desktopStatus: DesktopStatus | null; + isDark: boolean; + onAction: (action: DesktopAction) => void; +}) { + const [hovered, setHovered] = useState(false); + const [expanded, setExpanded] = useState(false); + const open = hovered || expanded; + const services = desktopStatus + ? [ + { label: "Boop agent", value: desktopStatus.state }, + { label: "Server", value: desktopStatus.server }, + { label: "Convex", value: desktopStatus.convex }, + { label: "Dashboard", value: desktopStatus.dashboard }, + { label: "Tunnel", value: desktopStatus.tunnel }, + { label: "Sendblue webhook", value: desktopStatus.webhook ?? "unknown" }, + ] + : [{ label: "Dashboard socket", value: connected ? "running" : "stopped" }]; + const starting = desktopStatus ? isDesktopStarting(desktopStatus) : false; + const active = desktopStatus ? isDesktopActive(desktopStatus) : connected; + const connectionActions: { label: string; action: DesktopAction }[] = []; + if (desktopStatus) { + if (!active && !starting) connectionActions.push({ label: "Start", action: "start" }); + if (active) connectionActions.push({ label: "Stop", action: "stop" }); + if (active && !starting) connectionActions.push({ label: "Restart", action: "restart" }); + if (desktopStatus.expectedWebhookUrl || desktopStatus.publicUrl) { + connectionActions.push({ label: "Check", action: "checkWebhook" }); + } + } + const webhookState = desktopStatus?.webhook ?? "unknown"; + const webhookProblem = ["mismatch", "missing", "error", "no-tunnel"].includes(webhookState); + const healthy = desktopStatus + ? desktopStatus.state === "running" && connected && !webhookProblem && webhookState === "registered" + : connected; + const statusText = webhookProblem + ? webhookState === "mismatch" + ? "Webhook mismatch" + : webhookState === "missing" + ? "Webhook missing" + : webhookState === "no-tunnel" + ? "No public tunnel" + : "Webhook check failed" + : webhookState === "checking" + ? "Checking webhook" + : healthy + ? "Connection healthy" + : connected + ? "Boop starting" + : "Disconnected"; + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + + + {open && ( +
event.stopPropagation()} + > +
+ Connection +
+
+ {services.map((service) => ( + + ))} +
+ + {connectionActions.length > 0 && ( +
+ {connectionActions.map((action) => ( + onAction(action.action)} + /> + ))} +
+ )} + + {expanded && desktopStatus && ( +
+ + + + + + +
+ )} +
+ )} +
+ ); +} + +function ServiceStatusRow({ + label, + value, + isDark, +}: { + label: string; + value?: string; + isDark: boolean; +}) { + return ( +
+ {label} + + + + {statusLabel(value)} + + +
+ ); +} + +function ConnectionActionButton({ + isDark, + label, + onClick, +}: { + isDark: boolean; + label: string; + onClick: () => void; +}) { + return ( + + ); +} + +function ConnectionDetail({ + label, + value, + isDark, +}: { + label: string; + value?: string; + isDark: boolean; +}) { + return ( +
+
{label}
+
+ {value || "Not set"} +
+
+ ); +} + +function isDesktopStarting(status: DesktopStatus) { + return ( + status.state === "starting" || + status.server === "starting" || + status.convex === "starting" || + status.dashboard === "starting" + ); +} + +function isDesktopActive(status: DesktopStatus) { + return ( + isDesktopStarting(status) || + status.state === "running" || + status.server === "running" || + status.convex === "running" || + status.dashboard === "running" + ); +} + +function statusLabel(value?: string) { + if (!value) return "Unknown"; + return value.charAt(0).toUpperCase() + value.slice(1).replace(/-/g, " "); +} + +function statusDotClass(value?: string) { + if (value === "running" || value === "registered") return "bg-emerald-400"; + if (value === "starting" || value === "checking") return "bg-amber-400"; + if (value === "error" || value === "setup-required" || value === "mismatch" || value === "missing") { + return "bg-rose-400"; + } + if (value === "no-tunnel") return "bg-amber-400"; + if (value === "stopped") return "bg-zinc-500"; + return "bg-zinc-600"; +} diff --git a/debug/src/components/AgentsPanel.tsx b/debug/src/components/AgentsPanel.tsx index f4a52b78..f5b48fb5 100644 --- a/debug/src/components/AgentsPanel.tsx +++ b/debug/src/components/AgentsPanel.tsx @@ -531,7 +531,7 @@ function summarizeResultItem(item: unknown): string { } export function AgentsPanel({ isDark }: { isDark: boolean }) { - const agents = useQuery(api.agents.list, { limit: 60 }); + const agents = useQuery(api.agents.listForDashboard, { limit: 60 }); const [selected, setSelected] = useState(null); const [statusFilter, setStatusFilter] = useState("all"); @@ -703,8 +703,8 @@ function AgentDetail({ onBack: () => void; isDark: boolean; }) { - const agent = useQuery(api.agents.get, { agentId }); - const logs = useQuery(api.agents.getLogs, { agentId, limit: 500 }); + const agent = useQuery(api.agents.getForDashboard, { agentId }); + const logs = useQuery(api.agents.getLogsForDashboard, { agentId, limit: 500 }); const [requestOpen, setRequestOpen] = useState(false); const [responseOpen, setResponseOpen] = useState(false); diff --git a/debug/src/components/AppleSection.tsx b/debug/src/components/AppleSection.tsx index af2185c5..e563e7c6 100644 --- a/debug/src/components/AppleSection.tsx +++ b/debug/src/components/AppleSection.tsx @@ -148,7 +148,7 @@ export function AppleSection({ isDark }: { isDark: boolean }) { } setMessage({ tone: "ok", - text: "Opened Automation settings. Enable Notes or Reminders for Codex, Terminal, or the app running Boop.", + text: "Opened Automation settings. Enable Notes or Reminders for Boop, or for the terminal app running Boop.", }); } catch (err) { setMessage({ tone: "err", text: err instanceof Error ? err.message : String(err) }); diff --git a/debug/src/components/AutomationsPanel.tsx b/debug/src/components/AutomationsPanel.tsx index aa0db364..340a9a32 100644 --- a/debug/src/components/AutomationsPanel.tsx +++ b/debug/src/components/AutomationsPanel.tsx @@ -31,7 +31,7 @@ const STATUS_COLOR: Record = { }; export function AutomationsPanel({ isDark }: { isDark: boolean }) { - const automations = useQuery(api.automations.list, {}); + const automations = useQuery(api.automations.listForDashboard, {}); const setEnabled = useMutation(api.automations.setEnabled); const [selectedId, setSelectedId] = useState(null); @@ -167,8 +167,8 @@ function AutomationDetail({ onBack: () => void; isDark: boolean; }) { - const auto = useQuery(api.automations.get, { automationId }); - const runs = useQuery(api.automations.recentRuns, { automationId, limit: 30 }); + const auto = useQuery(api.automations.getForDashboard, { automationId }); + const runs = useQuery(api.automations.recentRunsForDashboard, { automationId, limit: 30 }); const setEnabled = useMutation(api.automations.setEnabled); const remove = useMutation(api.automations.remove); diff --git a/debug/src/components/ComposioSection.tsx b/debug/src/components/ComposioSection.tsx index 0086e412..8c171463 100644 --- a/debug/src/components/ComposioSection.tsx +++ b/debug/src/components/ComposioSection.tsx @@ -1,4 +1,6 @@ import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; +import { useQuery } from "convex/react"; +import { api } from "../../../convex/_generated/api.js"; import { IntegrationLogo } from "../lib/branding.js"; type AuthMode = "managed" | "byo"; @@ -135,6 +137,280 @@ const COMPOSIO_DASHBOARD_URL = "https://dashboard.composio.dev"; const INTRO_DISMISSED_KEY = "boop:connections:intro-dismissed"; const TOAST_TIMEOUT_MS = 6000; +const DEMO_CREATED_AT = "2026-07-09T14:30:00.000Z"; + +const DEMO_TOOLS_BY_SLUG: Record = { + gmail: [ + { + slug: "GMAIL_SEARCH_EMAILS", + name: "Search emails", + description: "Search recent mail by sender, label, text, or date window.", + }, + { + slug: "GMAIL_FETCH_EMAIL", + name: "Fetch email", + description: "Read the full message and thread context for a selected email.", + }, + { + slug: "GMAIL_CREATE_EMAIL_DRAFT", + name: "Create draft", + description: "Prepare a reply and leave it pending for approval.", + }, + { + slug: "GMAIL_LIST_DRAFTS", + name: "List drafts", + description: "Review pending drafts before anything is sent.", + }, + ], + googlecalendar: [ + { + slug: "GOOGLECALENDAR_LIST_EVENTS", + name: "List events", + description: "Find meetings, holds, conflicts, and free windows.", + }, + { + slug: "GOOGLECALENDAR_CREATE_EVENT", + name: "Create event", + description: "Create a calendar hold with location, dial-in, and notes.", + }, + { + slug: "GOOGLECALENDAR_UPDATE_EVENT", + name: "Update event", + description: "Move, rename, or annotate an existing calendar event.", + }, + ], + slack: [ + { + slug: "SLACK_SEARCH_MESSAGES", + name: "Search messages", + description: "Find source messages across launch, support, and feedback channels.", + }, + { + slug: "SLACK_FETCH_CONVERSATION", + name: "Fetch conversation", + description: "Load surrounding thread context before summarizing.", + }, + { + slug: "SLACK_SEND_MESSAGE", + name: "Send message", + description: "Post a prepared update after explicit approval.", + }, + ], + linear: [ + { + slug: "LINEAR_SEARCH_ISSUES", + name: "Search issues", + description: "Find launch blockers, owners, statuses, and labels.", + }, + { + slug: "LINEAR_CREATE_ISSUE", + name: "Create issue", + description: "File a bug or follow-up with source context attached.", + }, + { + slug: "LINEAR_UPDATE_ISSUE", + name: "Update issue", + description: "Patch status, owner, priority, and comments.", + }, + ], + notion: [ + { + slug: "NOTION_SEARCH", + name: "Search pages", + description: "Find launch notes, briefs, customer docs, and decision logs.", + }, + { + slug: "NOTION_FETCH_PAGE", + name: "Fetch page", + description: "Read a page before citing or updating it.", + }, + { + slug: "NOTION_CREATE_PAGE", + name: "Create page", + description: "Create a structured brief, digest, or handoff note.", + }, + ], + github: [ + { + slug: "GITHUB_SEARCH_CODE", + name: "Search code", + description: "Find implementation references and risky changes.", + }, + { + slug: "GITHUB_LIST_PULL_REQUESTS", + name: "List pull requests", + description: "Review open branches, checks, and pending feedback.", + }, + { + slug: "GITHUB_CREATE_ISSUE", + name: "Create issue", + description: "Capture a bug with reproduction notes and owner.", + }, + ], + googledrive: [ + { + slug: "GOOGLEDRIVE_SEARCH", + name: "Search Drive", + description: "Find source docs, spreadsheets, receipts, and planning files.", + }, + { + slug: "GOOGLEDRIVE_FETCH_FILE", + name: "Fetch file", + description: "Read a selected document before summarizing or linking it.", + }, + ], +}; + +function demoConnection( + slug: string, + alias: string, + label = alias, +): Connection { + return { + id: `demo_${slug}_${alias.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`, + status: "ACTIVE", + alias, + accountLabel: label, + accountEmail: null, + accountName: label, + accountAvatarUrl: null, + createdAt: DEMO_CREATED_AT, + }; +} + +const DEMO_TOOLKITS_RESPONSE: ToolkitsResponse = { + enabled: true, + toolkits: [ + { + slug: "gmail", + displayName: "Gmail", + authMode: "managed", + hasAuthConfig: true, + logoUrl: null, + description: "Priority inbox search, thread reading, and approval-based draft creation.", + toolCount: DEMO_TOOLS_BY_SLUG.gmail.length, + connections: [demoConnection("gmail", "Primary inbox"), demoConnection("gmail", "Support inbox")], + }, + { + slug: "googlecalendar", + displayName: "Google Calendar", + authMode: "managed", + hasAuthConfig: true, + logoUrl: null, + description: "Calendar reads, conflict checks, tentative holds, and meeting updates.", + toolCount: DEMO_TOOLS_BY_SLUG.googlecalendar.length, + connections: [demoConnection("googlecalendar", "Work calendar")], + }, + { + slug: "linear", + displayName: "Linear", + authMode: "managed", + hasAuthConfig: true, + logoUrl: null, + description: "Issue search, blocker sweeps, bug filing, and status updates.", + toolCount: DEMO_TOOLS_BY_SLUG.linear.length, + connections: [demoConnection("linear", "Product workspace")], + }, + { + slug: "slack", + displayName: "Slack", + authMode: "managed", + hasAuthConfig: true, + logoUrl: null, + description: "Source-message search and thread context for support and launch channels.", + toolCount: DEMO_TOOLS_BY_SLUG.slack.length, + connections: [demoConnection("slack", "Team workspace")], + }, + { + slug: "notion", + displayName: "Notion", + authMode: "managed", + hasAuthConfig: true, + logoUrl: null, + description: "Search and write structured briefs, checklists, and decision logs.", + toolCount: DEMO_TOOLS_BY_SLUG.notion.length, + connections: [demoConnection("notion", "Launch workspace")], + }, + { + slug: "github", + displayName: "GitHub", + authMode: "managed", + hasAuthConfig: true, + logoUrl: null, + description: "Code search, pull request checks, and issue creation for engineering follow-up.", + toolCount: DEMO_TOOLS_BY_SLUG.github.length, + connections: [demoConnection("github", "Engineering org")], + }, + { + slug: "googledrive", + displayName: "Google Drive", + authMode: "managed", + hasAuthConfig: true, + logoUrl: null, + description: "Planning docs, sheets, receipts, and launch artifacts.", + toolCount: DEMO_TOOLS_BY_SLUG.googledrive.length, + connections: [demoConnection("googledrive", "Shared Drive")], + }, + ], +}; + +const DEMO_CONNECTED_TOOLKITS_BY_SLUG = new Map( + DEMO_TOOLKITS_RESPONSE.toolkits.map((toolkit) => [toolkit.slug, toolkit]), +); + +function buildDemoToolkitsResponse(catalog: ToolkitsResponse | null): ToolkitsResponse { + if (!catalog || catalog.toolkits.length === 0) return DEMO_TOOLKITS_RESPONSE; + + const bySlug = new Map(); + for (const toolkit of catalog.toolkits) { + const demoToolkit = DEMO_CONNECTED_TOOLKITS_BY_SLUG.get(toolkit.slug); + bySlug.set(toolkit.slug, { + ...toolkit, + authMode: "managed", + hasAuthConfig: true, + connections: demoToolkit?.connections ?? [], + toolCount: toolkit.toolCount ?? demoToolkit?.toolCount ?? null, + }); + } + + for (const toolkit of DEMO_TOOLKITS_RESPONSE.toolkits) { + if (!bySlug.has(toolkit.slug)) bySlug.set(toolkit.slug, toolkit); + } + + return { + enabled: true, + toolkits: [...bySlug.values()].sort((a, b) => { + const aConnected = hasActive(a); + const bConnected = hasActive(b); + if (aConnected !== bConnected) return aConnected ? -1 : 1; + return a.displayName.localeCompare(b.displayName); + }), + }; +} + +const DEMO_APPLE_STATUS: AppleStatus = { + enabled: true, + messagesEnabled: true, + notesEnabled: true, + remindersEnabled: true, + bridge: { + running: true, + source: "desktop-bridge", + port: 45731, + version: "demo", + permissions: { + messages: "granted", + notes: "granted", + reminders: "granted", + }, + error: null, + }, +}; + +const DEMO_EXPANDED_TOOLKITS = Object.fromEntries( + DEMO_TOOLKITS_RESPONSE.toolkits.map((toolkit) => [toolkit.slug, true]), +); + interface ToastState { id: number; message: string; @@ -230,6 +506,8 @@ function IntroCard({ isDark, onDismiss }: { isDark: boolean; onDismiss: () => vo } export function ComposioSection({ isDark }: { isDark: boolean }) { + const demoStatus = useQuery(api.demo.status); + const demoModeEnabled = demoStatus?.enabled ?? false; const [data, setData] = useState(null); const [loaded, setLoaded] = useState(false); const [appleStatus, setAppleStatus] = useState(null); @@ -286,8 +564,12 @@ export function ComposioSection({ isDark }: { isDark: boolean }) { ); const fetchToolkits = useCallback(async () => { + setLoaded(false); try { - const r = await fetch("/api/composio/toolkits"); + const endpoint = demoModeEnabled + ? "/api/composio/toolkits?catalog=all" + : "/api/composio/toolkits"; + const r = await fetch(endpoint); const json = (await r.json()) as ToolkitsResponse; setData(json); } catch { @@ -295,7 +577,7 @@ export function ComposioSection({ isDark }: { isDark: boolean }) { } finally { setLoaded(true); } - }, []); + }, [demoModeEnabled]); const fetchAppleStatus = useCallback(async () => { try { @@ -401,7 +683,7 @@ export function ComposioSection({ isDark }: { isDark: boolean }) { showToast(`Could not open Full Disk Access settings: ${err?.error ?? r.statusText}`); return; } - showToast("Opened Full Disk Access settings. Add Codex, Terminal, or the app running Boop, then restart npm run dev.", "info"); + showToast("Opened Full Disk Access settings. Add Boop, then restart Boop from the Connection header.", "info"); } catch (err) { showToast(`Could not open Full Disk Access settings: ${String(err)}`); } finally { @@ -419,7 +701,7 @@ export function ComposioSection({ isDark }: { isDark: boolean }) { showToast(`Could not open Automation settings: ${err?.error ?? r.statusText}`); return; } - showToast(`Opened Automation settings. Enable ${label} for Codex, Terminal, or the app running Boop.`, "info"); + showToast(`Opened Automation settings. Enable ${label} for Boop.`, "info"); } catch (err) { showToast(`Could not open Automation settings: ${String(err)}`); } finally { @@ -558,25 +840,46 @@ export function ComposioSection({ isDark }: { isDark: boolean }) { ? "border-white/10 bg-[#202024] shadow-black/20" : "border-zinc-200 bg-white shadow-zinc-200/50"; const muted = isDark ? "text-zinc-500" : "text-zinc-400"; + const visibleData = demoModeEnabled ? buildDemoToolkitsResponse(data) : data; + const visibleLoaded = loaded; + const visibleAppleStatus = demoModeEnabled ? DEMO_APPLE_STATUS : appleStatus; + const visibleAppleLoaded = demoModeEnabled ? true : appleLoaded; + const visibleExpanded = demoModeEnabled ? DEMO_EXPANDED_TOOLKITS : expanded; + const visibleToolsBySlug = demoModeEnabled + ? { ...DEMO_TOOLS_BY_SLUG, ...toolsBySlug } + : toolsBySlug; + const handleConnect = demoModeEnabled + ? (slug: string) => showToast(`${slug} is already connected in demo mode.`, "info") + : connect; + const handleDisconnect = demoModeEnabled + ? (_slug: string, _connectionId: string) => + showToast("Demo connections are read-only. Turn off demo mode to manage real accounts.", "info") + : disconnect; + const handleRename = demoModeEnabled + ? async () => { + showToast("Demo account labels are fixed for the recording dataset.", "info"); + return false; + } + : rename; const activeCount = - data?.toolkits.reduce((n, t) => n + t.connections.filter((c) => c.status === "ACTIVE").length, 0) ?? 0; + visibleData?.toolkits.reduce((n, t) => n + t.connections.filter((c) => c.status === "ACTIVE").length, 0) ?? 0; const imessageConnected = - Boolean(appleStatus?.messagesEnabled) && - Boolean(appleStatus?.bridge.running) && - appleStatus?.bridge.permissions?.messages === "granted"; + Boolean(visibleAppleStatus?.messagesEnabled) && + Boolean(visibleAppleStatus?.bridge.running) && + visibleAppleStatus?.bridge.permissions?.messages === "granted"; const notesConnected = - Boolean(appleStatus?.notesEnabled) && - Boolean(appleStatus?.bridge.running) && - appleStatus?.bridge.permissions?.notes === "granted"; + Boolean(visibleAppleStatus?.notesEnabled) && + Boolean(visibleAppleStatus?.bridge.running) && + visibleAppleStatus?.bridge.permissions?.notes === "granted"; const remindersConnected = - Boolean(appleStatus?.remindersEnabled) && - Boolean(appleStatus?.bridge.running) && - appleStatus?.bridge.permissions?.reminders === "granted"; + Boolean(visibleAppleStatus?.remindersEnabled) && + Boolean(visibleAppleStatus?.bridge.running) && + visibleAppleStatus?.bridge.permissions?.reminders === "granted"; const showLocalAppleConnectors = - appleLoaded && - (appleStatus?.bridge.source === "local-server" || - appleStatus?.bridge.source === "desktop-bridge"); + visibleAppleLoaded && + (visibleAppleStatus?.bridge.source === "local-server" || + visibleAppleStatus?.bridge.source === "desktop-bridge"); return (
@@ -589,10 +892,16 @@ export function ComposioSection({ isDark }: { isDark: boolean }) { (remindersConnected ? 1 : 0) } isDark={isDark} - hint={data?.enabled === false ? "Set COMPOSIO_API_KEY in .env.local" : undefined} + hint={ + !demoModeEnabled && visibleData?.enabled === false + ? "Set COMPOSIO_API_KEY in .env.local" + : undefined + } /> - {showIntro && data?.enabled !== false && } + {showIntro && !demoModeEnabled && visibleData?.enabled !== false && ( + + )} {showLocalAppleConnectors && ( toggleAppleSource("messages", enabled)} onRefresh={fetchAppleStatus} onOpenFullDiskAccess={openFullDiskAccess} /> toggleAppleSource("notes", enabled)} onRefresh={fetchAppleStatus} onRequestNotesAccess={requestNotesAccess} onOpenAutomationSettings={() => openAutomationSettings("notes")} /> toggleAppleSource("reminders", enabled)} onRefresh={fetchAppleStatus} onRequestRemindersAccess={requestRemindersAccess} @@ -674,12 +986,12 @@ export function ComposioSection({ isDark }: { isDark: boolean }) { )} - {data?.enabled === false ? ( + {visibleData?.enabled === false ? (
Add COMPOSIO_API_KEY to .env.local and restart the server to connect integrations like Gmail, Slack, GitHub, Linear, Notion, and more. Get a key at{" "} .
- ) : !loaded ? ( + ) : !visibleLoaded ? (
{[1, 2, 3, 4].map((i) => (
@@ -696,7 +1008,7 @@ export function ComposioSection({ isDark }: { isDark: boolean }) {
) : ( (() => { - const toolkits = data?.toolkits ?? []; + const toolkits = visibleData?.toolkits ?? []; const needsSetup = toolkits.filter( (t) => !hasActive(t) && t.authMode === "byo" && !t.hasAuthConfig, ); @@ -705,8 +1017,8 @@ export function ComposioSection({ isDark }: { isDark: boolean }) {
{ready.length > 0 && ( {ready.map((t) => ( @@ -717,11 +1029,12 @@ export function ComposioSection({ isDark }: { isDark: boolean }) { cardBg={cardBg} muted={muted} isDark={isDark} - expanded={!!expanded[t.slug]} - tools={toolsBySlug[t.slug]} - onConnect={connect} - onDisconnect={disconnect} - onRename={rename} + demoMode={demoModeEnabled} + expanded={!!visibleExpanded[t.slug]} + tools={visibleToolsBySlug[t.slug]} + onConnect={handleConnect} + onDisconnect={handleDisconnect} + onRename={handleRename} onToggleTools={toggleTools} /> ))} @@ -741,11 +1054,12 @@ export function ComposioSection({ isDark }: { isDark: boolean }) { cardBg={cardBg} muted={muted} isDark={isDark} - expanded={!!expanded[t.slug]} - tools={toolsBySlug[t.slug]} - onConnect={connect} - onDisconnect={disconnect} - onRename={rename} + demoMode={demoModeEnabled} + expanded={!!visibleExpanded[t.slug]} + tools={visibleToolsBySlug[t.slug]} + onConnect={handleConnect} + onDisconnect={handleDisconnect} + onRename={handleRename} onToggleTools={toggleTools} /> ))} @@ -767,6 +1081,7 @@ function IMessageConnectionCard({ cardBg, muted, isDark, + demoMode, onToggle, onRefresh, onOpenFullDiskAccess, @@ -777,6 +1092,7 @@ function IMessageConnectionCard({ cardBg: string; muted: string; isDark: boolean; + demoMode?: boolean; onToggle: (enabled: boolean) => void; onRefresh: () => void; onOpenFullDiskAccess: () => void; @@ -805,7 +1121,7 @@ function IMessageConnectionCard({

- Reads local Messages history from this Mac. The terminal app running the server needs Full Disk Access. + Reads local Messages history from this Mac. Boop needs Full Disk Access.

@@ -817,38 +1133,42 @@ function IMessageConnectionCard({ )}
-
- - + -
+ ? "bg-zinc-700 text-zinc-400" + : "bg-zinc-200 text-zinc-500" + : enabled + ? isDark + ? "border border-white/10 text-zinc-300 hover:bg-white/5" + : "border border-zinc-200 text-zinc-600 hover:bg-zinc-100" + : isDark + ? "bg-zinc-100 text-zinc-950 hover:bg-white" + : "bg-zinc-950 text-white hover:bg-zinc-800" + }`} + > + {buttonLabel} + + + )} {enabled && !bridge?.running && ( @@ -874,7 +1194,7 @@ function IMessageConnectionCard({ >
- Grant Full Disk Access to the terminal or Codex app running the Boop server, then restart npm run dev. + Grant Full Disk Access to Boop, then restart Boop from the Connection header.
-
- - + -
+ ? "bg-zinc-700 text-zinc-400" + : "bg-zinc-200 text-zinc-500" + : enabled + ? isDark + ? "border border-white/10 text-zinc-300 hover:bg-white/5" + : "border border-zinc-200 text-zinc-600 hover:bg-zinc-100" + : isDark + ? "bg-zinc-100 text-zinc-950 hover:bg-white" + : "bg-zinc-950 text-white hover:bg-zinc-800" + }`} + > + {buttonLabel} + + + )} {enabled && !bridge?.running && ( @@ -1057,6 +1383,7 @@ function AppleRemindersConnectionCard({ cardBg, muted, isDark, + demoMode, onToggle, onRefresh, onRequestRemindersAccess, @@ -1068,6 +1395,7 @@ function AppleRemindersConnectionCard({ cardBg: string; muted: string; isDark: boolean; + demoMode?: boolean; onToggle: (enabled: boolean) => void; onRefresh: () => void; onRequestRemindersAccess: () => void; @@ -1109,38 +1437,42 @@ function AppleRemindersConnectionCard({ )} -
- - + -
+ ? "bg-zinc-700 text-zinc-400" + : "bg-zinc-200 text-zinc-500" + : enabled + ? isDark + ? "border border-white/10 text-zinc-300 hover:bg-white/5" + : "border border-zinc-200 text-zinc-600 hover:bg-zinc-100" + : isDark + ? "bg-zinc-100 text-zinc-950 hover:bg-white" + : "bg-zinc-950 text-white hover:bg-zinc-800" + }`} + > + {buttonLabel} + + + )} {enabled && !bridge?.running && ( @@ -1274,12 +1606,27 @@ function appleRemindersConnectionState(status: AppleStatus | null, loaded: boole return { label: "Permission pending", dotClass: "bg-amber-400", textClass: "text-amber-500" }; } +function DemoReadyPill({ isDark }: { isDark: boolean }) { + return ( + + Ready + + ); +} + function ToolkitCard({ t, busy, cardBg, muted, isDark, + demoMode, expanded, tools, onConnect, @@ -1292,6 +1639,7 @@ function ToolkitCard({ cardBg: string; muted: string; isDark: boolean; + demoMode?: boolean; expanded: boolean; tools: ToolSummary[] | "loading" | "error" | undefined; onConnect: (slug: string) => void; @@ -1346,7 +1694,7 @@ function ToolkitCard({ )} - {!hasConnections && !needsSetup && ( + {!demoMode && !hasConnections && !needsSetup && ( )} - {needsSetup && ( + {!demoMode && needsSetup && ( - {needsSetup && } + {!demoMode && needsSetup && ( + + )} {hasConnections && (
@@ -1387,21 +1737,24 @@ function ToolkitCard({ busy={busy === `${t.slug}:${c.id}`} isDark={isDark} muted={muted} + demoMode={demoMode} onDisconnect={onDisconnect} onRename={onRename} /> ))} - + {!demoMode && ( + + )}
)} @@ -1417,6 +1770,7 @@ function ConnectionRow({ busy, isDark, muted, + demoMode, onDisconnect, onRename, }: { @@ -1426,6 +1780,7 @@ function ConnectionRow({ busy: boolean; isDark: boolean; muted: string; + demoMode?: boolean; onDisconnect: (slug: string, connectionId: string) => void; onRename: (connectionId: string, alias: string) => Promise; }) { @@ -1521,46 +1876,50 @@ function ConnectionRow({
{conn.id}
- {editing ? ( - <> - + {!demoMode && ( + editing ? ( + <> + + + + ) : ( - - ) : ( + ) + )} + {!demoMode && ( )} -
); } diff --git a/debug/src/components/ConsolidationPanel.tsx b/debug/src/components/ConsolidationPanel.tsx index 3f24fb83..ee2fadb4 100644 --- a/debug/src/components/ConsolidationPanel.tsx +++ b/debug/src/components/ConsolidationPanel.tsx @@ -80,7 +80,7 @@ function timeAgo(ts?: number): string { } export function ConsolidationPanel({ isDark }: { isDark: boolean }) { - const runs = useQuery(api.consolidation.listRuns, { limit: 50 }); + const runs = useQuery(api.consolidation.listRunsForDashboard, { limit: 50 }); const [selectedId, setSelectedId] = useState(null); const [livePhases, setLivePhases] = useState>({}); const [triggering, setTriggering] = useState(false); @@ -272,7 +272,7 @@ function ConsolidationDetail({ onBack: () => void; isDark: boolean; }) { - const runs = useQuery(api.consolidation.listRuns, { limit: 80 }); + const runs = useQuery(api.consolidation.listRunsForDashboard, { limit: 80 }); const run = runs?.find((r: any) => r.runId === runId); const [allPhases, setAllPhases] = useState(phases); diff --git a/debug/src/components/EventsPanel.tsx b/debug/src/components/EventsPanel.tsx index 473ffbfd..2b9bbe66 100644 --- a/debug/src/components/EventsPanel.tsx +++ b/debug/src/components/EventsPanel.tsx @@ -13,11 +13,16 @@ const EVENT_COLOR: Record = { "memory.recalled": "bg-sky-500/20 text-sky-400", "memory.extracted": "bg-violet-500/20 text-violet-400", "memory.consolidated": "bg-amber-500/20 text-amber-400", + "memory.promoted": "bg-emerald-500/20 text-emerald-400", + "memory.merged": "bg-cyan-500/20 text-cyan-400", + "memory.pruned": "bg-rose-500/20 text-rose-400", "memory.cleaned": "bg-zinc-500/20 text-zinc-400", + "consolidation.proposed": "bg-amber-500/20 text-amber-400", + "consolidation.applied": "bg-lime-500/20 text-lime-500", }; export function EventsPanel({ isDark }: { isDark: boolean }) { - const events = useQuery(api.memoryEvents.recent, { limit: 200 }); + const events = useQuery(api.memoryEvents.recentForDashboard, { limit: 200 }); const muted = mutedTextClass(isDark); const list = events ?? []; diff --git a/debug/src/components/MemoryGraphView.tsx b/debug/src/components/MemoryGraphView.tsx index 52d4c685..12ff53b6 100644 --- a/debug/src/components/MemoryGraphView.tsx +++ b/debug/src/components/MemoryGraphView.tsx @@ -1,37 +1,19 @@ -import { useMemo, useRef } from "react"; -import ForceGraph2D from "react-force-graph-2d"; - -type MemoryRecord = { - memoryId: string; - content: string; - tier: string; - segment: string; - importance: number; -}; - -type HubNode = { - id: string; - kind: "hub"; - label: string; - memoryCount: number; - dominantSegment: string; -}; - -type MemoryNode = { - id: string; - kind: "memory"; - label: string; - content: string; - segment: string; - tier: string; - importance: number; -}; - -type GraphNode = HubNode | MemoryNode; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import ForceGraph2D, { type ForceGraphMethods } from "react-force-graph-2d"; +import { + buildMemoryGraph, + type GraphLink, + type GraphNode, + type MemoryNode, + type MemoryRecord, + type RootNode, + type TopicNode, +} from "./memoryGraphModel.js"; const SEGMENT_COLORS: Record = { identity: "#f43f5e", preference: "#14b8a6", + correction: "#eab308", relationship: "#ec4899", project: "#f97316", knowledge: "#3b82f6", @@ -43,62 +25,254 @@ function segmentColor(segment: string): string { return SEGMENT_COLORS[segment] ?? DEFAULT_COLOR; } -function humanizeSegment(s: string): string { - return s.charAt(0).toUpperCase() + s.slice(1); +function colorWithAlpha(hex: string, alpha: number): string { + const value = hex.replace("#", ""); + if (value.length !== 6) return hex; + const red = Number.parseInt(value.slice(0, 2), 16); + const green = Number.parseInt(value.slice(2, 4), 16); + const blue = Number.parseInt(value.slice(4, 6), 16); + return `rgba(${red},${green},${blue},${alpha})`; } -function buildGraph(records: MemoryRecord[]) { - const bySegment = new Map(); - for (const r of records) { - const seg = r.segment || "unknown"; - const list = bySegment.get(seg); - if (list) list.push(r); - else bySegment.set(seg, [r]); - } +function roundedRect( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number, + radius: number, +) { + const r = Math.min(radius, width / 2, height / 2); + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + width, y, x + width, y + height, r); + ctx.arcTo(x + width, y + height, x, y + height, r); + ctx.arcTo(x, y + height, x, y, r); + ctx.arcTo(x, y, x + width, y, r); + ctx.closePath(); +} - const nodes: GraphNode[] = []; - const links: { source: string; target: string }[] = []; - - for (const [segment, members] of bySegment) { - const hubId = `hub:${segment}`; - nodes.push({ - id: hubId, - kind: "hub", - label: humanizeSegment(segment), - memoryCount: members.length, - dominantSegment: segment, - }); - for (const m of members) { - nodes.push({ - id: m.memoryId, - kind: "memory", - label: m.content.slice(0, 50) + (m.content.length > 50 ? "…" : ""), - content: m.content, - segment: m.segment, - tier: m.tier, - importance: m.importance, - }); - links.push({ source: hubId, target: m.memoryId }); - } +function drawRootNode(ctx: CanvasRenderingContext2D, node: RootNode, isDark: boolean) { + const width = 104; + const height = 43; + const x = node.fx - width / 2; + const y = node.fy - height / 2; + + ctx.save(); + ctx.shadowColor = isDark ? "rgba(0,0,0,0.35)" : "rgba(15,23,42,0.14)"; + ctx.shadowBlur = 9; + roundedRect(ctx, x, y, width, height, 13); + ctx.fillStyle = isDark ? "#27272a" : "#ffffff"; + ctx.fill(); + ctx.shadowBlur = 0; + ctx.strokeStyle = isDark ? "rgba(255,255,255,0.16)" : "rgba(15,23,42,0.13)"; + ctx.lineWidth = 1; + ctx.stroke(); + + ctx.fillStyle = "#f59e0b"; + ctx.beginPath(); + ctx.arc(x + 17, node.fy, 6, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = isDark ? "#18181b" : "#ffffff"; + ctx.lineWidth = 2; + ctx.stroke(); + + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillStyle = isDark ? "#fafafa" : "#18181b"; + ctx.font = "600 10.5px Geist, ui-sans-serif, system-ui"; + ctx.fillText("Boop memory", x + 29, node.fy - 6); + ctx.fillStyle = isDark ? "#a1a1aa" : "#71717a"; + ctx.font = "7.5px Geist, ui-sans-serif, system-ui"; + ctx.fillText(`${node.memoryCount} memories`, x + 29, node.fy + 8); + ctx.restore(); +} + +function drawTopicNode(ctx: CanvasRenderingContext2D, node: TopicNode, isDark: boolean) { + ctx.save(); + ctx.font = "600 9.5px Geist, ui-sans-serif, system-ui"; + const labelWidth = ctx.measureText(node.label).width; + const countText = String(node.memoryCount); + ctx.font = "600 8px Geist, ui-sans-serif, system-ui"; + const countWidth = ctx.measureText(countText).width; + const width = Math.max(94, labelWidth + countWidth + 38); + const height = 30; + const x = node.fx - width / 2; + const y = node.fy - height / 2; + + ctx.shadowColor = isDark ? "rgba(0,0,0,0.28)" : "rgba(15,23,42,0.1)"; + ctx.shadowBlur = 6; + roundedRect(ctx, x, y, width, height, 9); + ctx.fillStyle = isDark ? "#27272a" : "rgba(255,255,255,0.96)"; + ctx.fill(); + ctx.shadowBlur = 0; + ctx.strokeStyle = colorWithAlpha(node.color, isDark ? 0.65 : 0.45); + ctx.lineWidth = 1.2; + ctx.stroke(); + + ctx.fillStyle = node.color; + roundedRect(ctx, x + 7, y + 7, 4, 16, 2); + ctx.fill(); + + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillStyle = isDark ? "#f4f4f5" : "#27272a"; + ctx.font = "600 9.5px Geist, ui-sans-serif, system-ui"; + ctx.fillText(node.label, x + 18, node.fy); + + const countX = x + width - countWidth - 10; + ctx.fillStyle = isDark ? "#a1a1aa" : "#71717a"; + ctx.font = "600 8px Geist, ui-sans-serif, system-ui"; + ctx.fillText(countText, countX, node.fy); + ctx.restore(); +} + +function drawMemoryNode( + ctx: CanvasRenderingContext2D, + node: MemoryNode, + isDark: boolean, + globalScale: number, +) { + const x = node.x ?? 0; + const y = node.y ?? 0; + const radius = 3.2 + node.importance * 2.2; + const color = segmentColor(node.segment); + + ctx.save(); + ctx.fillStyle = colorWithAlpha(node.topicColor, isDark ? 0.2 : 0.14); + ctx.beginPath(); + ctx.arc(x, y, radius + 3.2, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = isDark ? "#202024" : "#ffffff"; + ctx.lineWidth = 1.2; + ctx.stroke(); + + if (node.tier === "permanent") { + ctx.strokeStyle = colorWithAlpha(color, 0.75); + ctx.lineWidth = 0.8; + ctx.beginPath(); + ctx.arc(x, y, radius + 2, 0, Math.PI * 2); + ctx.stroke(); } - return { nodes, links }; + if (node.showLabel && globalScale >= 0.72) { + ctx.font = "500 7.5px Geist, ui-sans-serif, system-ui"; + const labelWidth = Math.min(104, ctx.measureText(node.label).width + 12); + const labelX = + node.labelSide === "left" + ? x - radius - 4 - labelWidth + : x + radius + 4; + const labelY = y - 8; + roundedRect(ctx, labelX, labelY, labelWidth, 16, 5); + ctx.fillStyle = isDark ? "rgba(39,39,42,0.94)" : "rgba(255,255,255,0.94)"; + ctx.fill(); + ctx.strokeStyle = isDark ? "rgba(255,255,255,0.11)" : "rgba(15,23,42,0.1)"; + ctx.lineWidth = 0.7; + ctx.stroke(); + ctx.fillStyle = isDark ? "#e4e4e7" : "#3f3f46"; + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillText(node.label, labelX + 6, y, labelWidth - 10); + } + ctx.restore(); } export default function MemoryGraphView({ records, isDark, }: { - records: any[]; + records: unknown[]; isDark: boolean; }) { - const graph = useMemo(() => buildGraph(records as MemoryRecord[]), [records]); - const fgRef = useRef(null); + const graph = useMemo(() => buildMemoryGraph(records as MemoryRecord[]), [records]); + const containerRef = useRef(null); + const fgRef = useRef | undefined>(undefined); + const [size, setSize] = useState({ width: 1, height: 1 }); + + const fitGraph = useCallback(() => { + window.setTimeout(() => fgRef.current?.zoomToFit(450, 42), 60); + }, []); + + useEffect(() => { + const element = containerRef.current; + if (!element) return; + let settleTimer: number | undefined; + const updateSize = () => { + const bounds = element.getBoundingClientRect(); + const nextSize = { + width: Math.max(1, Math.floor(bounds.width)), + height: Math.max(1, Math.floor(bounds.height)), + }; + setSize((current) => + current.width === nextSize.width && current.height === nextSize.height + ? current + : nextSize, + ); + + // The desktop app reveals the dashboard iframe after startup. Chromium + // can miss that transition in ResizeObserver, so keep checking until the + // graph has measured a real viewport instead of its 1px fallback. + if (nextSize.width > 1 && nextSize.height > 1 && settleTimer !== undefined) { + window.clearInterval(settleTimer); + settleTimer = undefined; + } + }; + updateSize(); + const observer = new ResizeObserver(updateSize); + observer.observe(element); + const frame = window.requestAnimationFrame(updateSize); + settleTimer = window.setInterval(updateSize, 200); + window.addEventListener("resize", updateSize); + return () => { + observer.disconnect(); + window.cancelAnimationFrame(frame); + if (settleTimer !== undefined) window.clearInterval(settleTimer); + window.removeEventListener("resize", updateSize); + }; + }, []); + + useEffect(() => { + const graphInstance = fgRef.current; + if (!graphInstance) return; + const linkForce = graphInstance.d3Force("link") as + | { + distance: (value: (link: GraphLink) => number) => unknown; + strength: (value: (link: GraphLink) => number) => unknown; + } + | undefined; + linkForce?.distance((link) => { + if (link.kind === "root") return 105; + if (link.kind === "membership") return 52; + if (link.kind === "history") return 36; + return 42; + }); + linkForce?.strength((link) => (link.kind === "root" ? 0.45 : 0.2)); + + const chargeForce = graphInstance.d3Force("charge") as + | { + strength: (value: (node: GraphNode) => number) => unknown; + distanceMax: (value: number) => unknown; + } + | undefined; + chargeForce?.strength((node) => { + if (node.kind === "root") return -180; + if (node.kind === "topic") return -260; + return -24; + }); + chargeForce?.distanceMax(180); + graphInstance.d3ReheatSimulation(); + fitGraph(); + }, [fitGraph, graph, size.height, size.width]); if (records.length === 0) { return (
@@ -108,58 +282,60 @@ export default function MemoryGraphView({ } return ( -
- + ref={fgRef} graphData={graph} + width={size.width} + height={size.height} backgroundColor={isDark ? "#202024" : "#ffffff"} nodeRelSize={4} - linkColor={() => (isDark ? "rgba(148,163,184,0.15)" : "rgba(15,23,42,0.15)")} - linkWidth={1} - nodeLabel={(n: any) => - n.kind === "hub" - ? `
${n.label} — ${n.memoryCount} memories
` - : `
${n.content}
` - } - nodeCanvasObject={(node: any, ctx, globalScale) => { - const isHub = node.kind === "hub"; - const color = isHub - ? segmentColor(node.dominantSegment) - : segmentColor(node.segment); - - if (isHub) { - const radius = Math.max(10, Math.min(30, 8 + Math.log2(node.memoryCount + 1) * 4)); - ctx.fillStyle = color; - ctx.globalAlpha = 0.9; - ctx.beginPath(); - ctx.arc(node.x, node.y, radius, 0, Math.PI * 2); + nodeLabel={(node) => { + if (node.kind === "root") { + return `${node.memoryCount} memories across ${node.topicCount} connected themes`; + } + if (node.kind === "topic") return `${node.label} - ${node.memoryCount} memories`; + return node.content; + }} + nodeCanvasObject={(node, ctx, globalScale) => { + if (node.kind === "root") drawRootNode(ctx, node, isDark); + else if (node.kind === "topic") drawTopicNode(ctx, node, isDark); + else drawMemoryNode(ctx, node, isDark, globalScale); + }} + nodePointerAreaPaint={(node, color, ctx) => { + ctx.fillStyle = color; + if (node.kind === "root") { + roundedRect(ctx, node.fx - 52, node.fy - 22, 104, 44, 13); + ctx.fill(); + } else if (node.kind === "topic") { + roundedRect(ctx, node.fx - 58, node.fy - 16, 116, 32, 9); ctx.fill(); - ctx.globalAlpha = 1; - ctx.fillStyle = "#ffffff"; - ctx.font = `bold ${Math.max(10, 12 / globalScale)}px Geist, ui-sans-serif, system-ui`; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(node.label, node.x, node.y); } else { - const radius = Math.max(3, Math.min(8, 3 + node.importance * 5)); - ctx.fillStyle = color; - ctx.globalAlpha = 0.7; ctx.beginPath(); - ctx.arc(node.x, node.y, radius, 0, Math.PI * 2); + ctx.arc(node.x ?? 0, node.y ?? 0, 9, 0, Math.PI * 2); ctx.fill(); - ctx.globalAlpha = 1; - if (globalScale > 1.5) { - ctx.fillStyle = isDark ? "rgba(244,244,245,0.9)" : "rgba(39,39,42,0.9)"; - ctx.font = `${10 / globalScale}px Geist, ui-sans-serif, system-ui`; - ctx.textAlign = "center"; - ctx.textBaseline = "top"; - ctx.fillText(node.label.slice(0, 40), node.x, node.y + radius + 2); - } } }} - d3AlphaDecay={0.02} - d3VelocityDecay={0.3} - cooldownTicks={100} + linkColor={(link) => { + if (link.kind === "root") return colorWithAlpha(link.color ?? DEFAULT_COLOR, 0.34); + if (link.kind === "membership") { + return colorWithAlpha(link.color ?? DEFAULT_COLOR, isDark ? 0.26 : 0.21); + } + if (link.kind === "history") { + return isDark ? "rgba(250,204,21,0.34)" : "rgba(161,98,7,0.28)"; + } + return isDark ? "rgba(161,161,170,0.19)" : "rgba(71,85,105,0.14)"; + }} + linkWidth={(link) => (link.kind === "root" ? 1.5 : link.kind === "history" ? 1.1 : 0.75)} + linkLineDash={(link) => (link.kind === "history" ? [3, 3] : null)} + linkCurvature={(link) => (link.kind === "affinity" ? 0.08 : 0)} + d3AlphaDecay={0.035} + d3VelocityDecay={0.38} + warmupTicks={80} + cooldownTicks={180} + minZoom={0.65} + maxZoom={6} + onEngineStop={fitGraph} />
); diff --git a/debug/src/components/MemoryPanel.tsx b/debug/src/components/MemoryPanel.tsx index 74868b67..009642aa 100644 --- a/debug/src/components/MemoryPanel.tsx +++ b/debug/src/components/MemoryPanel.tsx @@ -76,7 +76,13 @@ function MemoryImageBadge({ storageId }: { storageId: string }) { ); } -export function MemoryPanel({ isDark }: { isDark: boolean }) { +export function MemoryPanel({ + isDark, + demoMode = false, +}: { + isDark: boolean; + demoMode?: boolean; +}) { const [viewMode, setViewMode] = useState("table"); const [tierFilter, setTierFilter] = useState("all"); const [segmentFilter, setSegmentFilter] = useState("all"); @@ -85,7 +91,7 @@ export function MemoryPanel({ isDark }: { isDark: boolean }) { const [deletingId, setDeletingId] = useState(null); const removeMemory = useMutation(api.memoryRecords.remove); - const records = useQuery(api.memoryRecords.list, { + const records = useQuery(api.memoryRecords.listForDashboard, { tier: tierFilter !== "all" ? (tierFilter as any) : undefined, lifecycle: "active", limit: 500, @@ -131,7 +137,7 @@ export function MemoryPanel({ isDark }: { isDark: boolean }) { stat={{filtered.length}/{allRecords.length}} maxWidth={viewMode === "graph" ? "max-w-none" : "max-w-[1040px]"} > - + {!demoMode && }
{viewMode === "graph" && ( -
+
)} diff --git a/debug/src/components/SettingsPanel.tsx b/debug/src/components/SettingsPanel.tsx index e9d2f3ac..e9d8a6db 100644 --- a/debug/src/components/SettingsPanel.tsx +++ b/debug/src/components/SettingsPanel.tsx @@ -1,5 +1,7 @@ import { useCallback, useEffect, useState, type ReactNode } from "react"; import { useQuery, useMutation } from "convex/react"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Copy01Icon, Message01Icon } from "@hugeicons/core-free-icons"; import { api } from "../../../convex/_generated/api.js"; import { RuntimeProviderBadge, @@ -24,6 +26,11 @@ interface RuntimeConfigSnapshot { billingMode: "api" | "codex-subscription"; } +interface ConnectionConfigSnapshot { + phoneNumber: string; + publicUrl?: string; +} + interface ToggleSetting { kind: "toggle"; key: string; @@ -59,7 +66,8 @@ const SETTINGS: Setting[] = [ }, ]; -const RUNTIME_SETTING_COUNT = SETTINGS.length + 4; +const RUNTIME_SETTING_COUNT = SETTINGS.length + 5; +const DEMO_PHONE_NUMBER = "+11111111111"; const RUNTIME_OPTIONS: Option[] = [ { value: "claude", label: "Claude" }, @@ -141,7 +149,13 @@ async function updateRuntimeConfig( return (await res.json()) as RuntimeConfigSnapshot; } -export function SettingsPanel({ isDark }: { isDark: boolean }) { +export function SettingsPanel({ + isDark, + desktopPhoneNumber, +}: { + isDark: boolean; + desktopPhoneNumber?: string; +}) { return (
@@ -179,6 +193,7 @@ export function SettingsPanel({ isDark }: { isDark: boolean }) {
+ {SETTINGS.map((s) => s.kind === "toggle" ? ( @@ -195,16 +210,23 @@ export function SettingsPanel({ isDark }: { isDark: boolean }) { ); } -function SettingsRuntimeBadge({ isDark }: { isDark: boolean }) { - const storedRuntime = useQuery(api.settings.get, { key: "runtime" }); - const [serverConfig, setServerConfig] = useState(null); +function TextBoopRow({ + isDark, + desktopPhoneNumber, +}: { + isDark: boolean; + desktopPhoneNumber?: string; +}) { + const demoStatus = useQuery(api.demo.status); + const [serverConfig, setServerConfig] = useState(null); + const [copied, setCopied] = useState(false); useEffect(() => { let cancelled = false; - fetch("/api/runtime-config") + fetch("/api/connection-config") .then((res) => { - if (!res.ok) throw new Error(`Runtime config fetch failed (${res.status})`); - return res.json() as Promise; + if (!res.ok) throw new Error(`Connection config fetch failed (${res.status})`); + return res.json() as Promise; }) .then((config) => { if (!cancelled) setServerConfig(config); @@ -215,14 +237,140 @@ function SettingsRuntimeBadge({ isDark }: { isDark: boolean }) { return () => { cancelled = true; }; + }, [desktopPhoneNumber]); + + const demoModeEnabled = demoStatus?.enabled ?? false; + const realPhoneNumber = desktopPhoneNumber || serverConfig?.phoneNumber || ""; + const phoneNumber = demoModeEnabled ? DEMO_PHONE_NUMBER : realPhoneNumber; + const configured = Boolean(phoneNumber); + const debugLine = demoModeEnabled + ? "sendblue.from_number = demo placeholder" + : configured + ? `sendblue.from_number = "${phoneNumber}"` + : "sendblue.from_number = (not configured)"; + + async function copyNumber() { + if (!phoneNumber) return; + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(phoneNumber); + } else { + const input = document.createElement("textarea"); + input.value = phoneNumber; + input.setAttribute("readonly", ""); + input.style.position = "fixed"; + input.style.opacity = "0"; + document.body.appendChild(input); + input.select(); + document.execCommand("copy"); + document.body.removeChild(input); + } + setCopied(true); + window.setTimeout(() => setCopied(false), 1400); + } + + const numberTone = configured + ? isDark + ? "border-emerald-400/20 bg-emerald-400/10 text-emerald-200" + : "border-emerald-200 bg-emerald-50 text-emerald-800" + : isDark + ? "border-amber-400/20 bg-amber-400/10 text-amber-200" + : "border-amber-200 bg-amber-50 text-amber-800"; + const copyButtonTone = isDark + ? "border-white/10 bg-white/5 text-zinc-200 hover:bg-white/10" + : "border-zinc-200 bg-white text-zinc-700 hover:bg-zinc-50"; + + return ( + +
+
+ +
+
+ Text / iMessage this number +
+
+ {configured ? phoneNumber : "Not configured"} +
+
+
+ +
+
+ {configured + ? demoModeEnabled + ? copied + ? "Copied demo placeholder." + : "Demo mode is hiding the real number." + : copied + ? "Copied." + : "Use this as the recipient in Messages." + : "Run setup again or sync Sendblue to configure the receiving number."} +
+
+ } + /> + ); +} + +function SettingsRuntimeBadge({ isDark }: { isDark: boolean }) { + const storedRuntime = useQuery(api.settings.get, { key: "runtime" }); + const [serverConfig, setServerConfig] = useState(null); + + useEffect(() => { + let cancelled = false; + let timeout: number | undefined; + let attempt = 0; + + async function loadRuntimeConfig() { + try { + const res = await fetch("/api/runtime-config", { cache: "no-store" }); + if (!res.ok) throw new Error(`Runtime config fetch failed (${res.status})`); + const config = (await res.json()) as RuntimeConfigSnapshot; + if (!cancelled) setServerConfig(config); + } catch { + if (cancelled) return; + setServerConfig(null); + if (attempt < 20) { + const delay = Math.min(750 + attempt * 250, 4000); + attempt += 1; + timeout = window.setTimeout(loadRuntimeConfig, delay); + } + } + } + + loadRuntimeConfig(); + return () => { + cancelled = true; + if (timeout) window.clearTimeout(timeout); + }; }, [storedRuntime]); - const runtime: RuntimeProvider = - storedRuntime === "claude" || storedRuntime === "codex" - ? storedRuntime - : serverConfig?.runtime ?? "claude"; + const runtime: RuntimeProvider | null = + serverConfig?.runtime ?? + (storedRuntime === "claude" || storedRuntime === "codex" ? storedRuntime : null); - if (storedRuntime === undefined && serverConfig === null) return null; + if (!runtime) return null; return ( (null); const refreshServerConfig = useCallback(async () => { - const res = await fetch("/api/runtime-config"); + const res = await fetch("/api/runtime-config", { cache: "no-store" }); if (!res.ok) throw new Error(`Runtime config fetch failed (${res.status})`); setServerConfig((await res.json()) as RuntimeConfigSnapshot); }, []); useEffect(() => { let cancelled = false; - fetch("/api/runtime-config") - .then((res) => { + let timeout: number | undefined; + let attempt = 0; + + async function loadRuntimeConfig() { + try { + const res = await fetch("/api/runtime-config", { cache: "no-store" }); if (!res.ok) throw new Error(`Runtime config fetch failed (${res.status})`); - return res.json() as Promise; - }) - .then((config) => { + const config = (await res.json()) as RuntimeConfigSnapshot; if (!cancelled) { setServerConfig(config); setError(null); } - }) - .catch((err) => { - if (!cancelled) setError(String(err)); - }); + } catch (err) { + if (cancelled) return; + setError(String(err)); + if (attempt < 20) { + const delay = Math.min(750 + attempt * 250, 4000); + attempt += 1; + timeout = window.setTimeout(loadRuntimeConfig, delay); + } + } + } + + loadRuntimeConfig(); return () => { cancelled = true; + if (timeout) window.clearTimeout(timeout); }; }, [refreshServerConfig, storedRuntime, storedClaudeModel, storedHostedModel, storedHostedEffort]); const runtime: RuntimeChoice = - storedRuntime === "claude" || storedRuntime === "codex" - ? storedRuntime - : serverConfig?.runtime ?? "claude"; + serverConfig?.runtime ?? + (storedRuntime === "claude" || storedRuntime === "codex" ? storedRuntime : "claude"); const activeModelOptions = runtime === "codex" ? CODEX_MODELS : CLAUDE_MODELS; const modelKey = runtime === "codex" ? "codex_model" : "model"; diff --git a/debug/src/components/memoryGraphModel.ts b/debug/src/components/memoryGraphModel.ts new file mode 100644 index 00000000..3705050a --- /dev/null +++ b/debug/src/components/memoryGraphModel.ts @@ -0,0 +1,361 @@ +export type MemoryRecord = { + memoryId: string; + content: string; + tier: string; + segment: string; + importance: number; + accessCount?: number; + supersedes?: string[]; + metadata?: string; +}; + +type TopicDefinition = { + id: string; + label: string; + color: string; + patterns: RegExp[]; +}; + +type GraphMetadata = { + topic?: string; + label?: string; + relatedMemoryIds?: string[]; +}; + +export type RootNode = { + id: "root:memory"; + kind: "root"; + label: string; + memoryCount: number; + topicCount: number; + fx: number; + fy: number; +}; + +export type TopicNode = { + id: string; + kind: "topic"; + label: string; + color: string; + memoryCount: number; + fx: number; + fy: number; +}; + +export type MemoryNode = { + id: string; + kind: "memory"; + label: string; + content: string; + segment: string; + tier: string; + importance: number; + topicId: string; + topicColor: string; + showLabel: boolean; + labelSide: "left" | "right"; + x: number; + y: number; + fx?: number; + fy?: number; +}; + +export type GraphNode = RootNode | TopicNode | MemoryNode; +export type GraphLink = { + source: string; + target: string; + kind: "root" | "membership" | "affinity" | "history"; + color?: string; +}; + +const TOPICS: TopicDefinition[] = [ + { + id: "launch", + label: "Launch & product", + color: "#f97316", + patterns: [ + /launch|beta|dashboard|onboarding|release|pricing|webhook|bug bash|screenshots?/i, + /product|billing|implementation|software team|code review/i, + ], + }, + { + id: "customer-care", + label: "Customer care", + color: "#ec4899", + patterns: [/customer|support|escalation|reply|draft|public post|copy|editor/i], + }, + { + id: "daily-rhythm", + label: "Daily rhythm", + color: "#6366f1", + patterns: [ + /calendar|meeting|schedule|timezone|friday|focus block|deep work/i, + /morning brief|status update|weekly digest|automation|evenings? after/i, + ], + }, + { + id: "people", + label: "People", + color: "#a855f7", + patterns: [ + /partner|family|design lead|support lead|operations contact/i, + /contractor|finance contact|customer success lead|stakeholder/i, + ], + }, + { + id: "travel", + label: "Travel", + color: "#3b82f6", + patterns: [/travel|flight|hotel|airport|itinerary|aisle|nonstop|cancellation/i], + }, + { + id: "home-life", + label: "Home & life", + color: "#14b8a6", + patterns: [/grocery|errand|package|dinner|restaurant|weekend|meal|neighborhood/i], + }, + { + id: "wellbeing", + label: "Wellbeing", + color: "#22c55e", + patterns: [/workout|knee|shoulder|warmup|run|lower impact|health/i], + }, + { + id: "principles", + label: "Principles", + color: "#64748b", + patterns: [/uncertainty|sensitive|private|remembered fact|confirmation|tradeoffs?/i], + }, +]; + +const TOPIC_BY_ID = new Map(TOPICS.map((topic) => [topic.id, topic])); + +function parseGraphMetadata(metadata?: string): GraphMetadata { + if (!metadata) return {}; + try { + const parsed = JSON.parse(metadata) as { graph?: GraphMetadata }; + return parsed.graph ?? {}; + } catch { + return {}; + } +} + +function topicForRecord(record: MemoryRecord, graphMetadata: GraphMetadata): TopicDefinition { + const explicit = graphMetadata.topic ? TOPIC_BY_ID.get(graphMetadata.topic) : undefined; + if (explicit) return explicit; + + const inferred = TOPICS.find((topic) => + topic.patterns.some((pattern) => pattern.test(record.content)), + ); + return inferred ?? TOPIC_BY_ID.get("principles")!; +} + +function compactLabel(content: string): string { + const cleaned = content + .replace(/^(the user|user|current|latest|next)\s+/i, "") + .replace(/^(for|when|if)\s+/i, "") + .replace(/[.:;,]+$/g, "") + .trim(); + const words = cleaned.split(/\s+/).filter(Boolean); + const label = words.slice(0, 5).join(" "); + return words.length > 5 ? `${label}...` : label; +} + +function hashNumber(value: string): number { + let hash = 2166136261; + for (let index = 0; index < value.length; index += 1) { + hash = Math.imul(hash ^ value.charCodeAt(index), 16777619) >>> 0; + } + return hash; +} + +function topicPosition(index: number, count: number) { + const angle = -Math.PI / 2 + (index / count) * Math.PI * 2; + return { + angle, + x: Math.cos(angle) * 330, + y: Math.sin(angle) * 145, + }; +} + +function memoryPosition( + topicX: number, + topicY: number, + topicAngle: number, + memoryId: string, + index: number, +) { + const hash = hashNumber(memoryId); + const ring = Math.floor(index / 7); + const radius = 42 + ring * 15 + (hash % 7); + const angle = index === 0 ? topicAngle : ((hash % 360) * Math.PI) / 180 + index * 0.73; + return { + x: topicX + Math.cos(angle) * radius, + y: topicY + Math.sin(angle) * radius, + }; +} + +export function buildMemoryGraph(records: MemoryRecord[]): { + nodes: GraphNode[]; + links: GraphLink[]; +} { + const prepared = records.map((record) => { + const graphMetadata = parseGraphMetadata(record.metadata); + return { + record, + graphMetadata, + topic: topicForRecord(record, graphMetadata), + }; + }); + + const populatedTopics = TOPICS.filter((topic) => + prepared.some((item) => item.topic.id === topic.id), + ); + const nodes: GraphNode[] = [ + { + id: "root:memory", + kind: "root", + label: "Boop memory", + memoryCount: records.length, + topicCount: populatedTopics.length, + fx: 0, + fy: 0, + }, + ]; + const links: GraphLink[] = []; + const linkKeys = new Set(); + + const addLink = (link: GraphLink) => { + const endpoints = [String(link.source), String(link.target)].sort().join("::"); + const key = `${link.kind}:${endpoints}`; + if (linkKeys.has(key) || link.source === link.target) return; + linkKeys.add(key); + links.push(link); + }; + + const recordsByTopic = new Map(); + for (const item of prepared) { + const members = recordsByTopic.get(item.topic.id) ?? []; + members.push(item); + recordsByTopic.set(item.topic.id, members); + } + + populatedTopics.forEach((topic, topicIndex) => { + const members = [...(recordsByTopic.get(topic.id) ?? [])].sort( + (left, right) => + right.record.importance - left.record.importance || + (right.record.accessCount ?? 0) - (left.record.accessCount ?? 0), + ); + const position = topicPosition(topicIndex, populatedTopics.length); + const topicId = `topic:${topic.id}`; + nodes.push({ + id: topicId, + kind: "topic", + label: topic.label, + color: topic.color, + memoryCount: members.length, + fx: position.x, + fy: position.y, + }); + addLink({ + source: "root:memory", + target: topicId, + kind: "root", + color: topic.color, + }); + + members.forEach((item, memberIndex) => { + const memoryPositionValue = memoryPosition( + position.x, + position.y, + position.angle, + item.record.memoryId, + memberIndex, + ); + nodes.push({ + id: item.record.memoryId, + kind: "memory", + label: item.graphMetadata.label ?? compactLabel(item.record.content), + content: item.record.content, + segment: item.record.segment, + tier: item.record.tier, + importance: item.record.importance, + topicId: topic.id, + topicColor: topic.color, + showLabel: memberIndex === 0, + labelSide: Math.cos(position.angle) < -0.05 ? "left" : "right", + x: memoryPositionValue.x, + y: memoryPositionValue.y, + fx: memberIndex === 0 ? memoryPositionValue.x : undefined, + fy: memberIndex === 0 ? memoryPositionValue.y : undefined, + }); + addLink({ + source: topicId, + target: item.record.memoryId, + kind: "membership", + color: topic.color, + }); + + const next = members[(memberIndex + 1) % members.length]; + if (members.length > 2 && next) { + addLink({ + source: item.record.memoryId, + target: next.record.memoryId, + kind: "affinity", + color: topic.color, + }); + } + + for (const relatedMemoryId of item.graphMetadata.relatedMemoryIds ?? []) { + if (prepared.some((candidate) => candidate.record.memoryId === relatedMemoryId)) { + addLink({ + source: item.record.memoryId, + target: relatedMemoryId, + kind: "affinity", + }); + } + } + + for (const supersededId of item.record.supersedes ?? []) { + if (prepared.some((candidate) => candidate.record.memoryId === supersededId)) { + addLink({ + source: item.record.memoryId, + target: supersededId, + kind: "history", + }); + } + } + }); + }); + + const recordsBySegment = new Map(); + for (const item of prepared) { + const members = recordsBySegment.get(item.record.segment) ?? []; + members.push(item); + recordsBySegment.set(item.record.segment, members); + } + for (const members of recordsBySegment.values()) { + for (let index = 0; index + 3 < members.length; index += 4) { + const source = members[index]; + const target = members[index + 3]; + if (source && target && source.topic.id !== target.topic.id) { + addLink({ + source: source.record.memoryId, + target: target.record.memoryId, + kind: "affinity", + }); + } + } + } + + const paintOrder: Record = { + memory: 0, + topic: 1, + root: 2, + }; + nodes.sort((left, right) => paintOrder[left.kind] - paintOrder[right.kind]); + + return { nodes, links }; +} + +export const MEMORY_GRAPH_TOPICS = TOPICS.map(({ id, label, color }) => ({ id, label, color })); diff --git a/debug/src/styles.css b/debug/src/styles.css index 2947d637..0700a2ff 100644 --- a/debug/src/styles.css +++ b/debug/src/styles.css @@ -27,6 +27,17 @@ select { font-family: inherit; } +button, +a[href], +summary, +label[for], +select, +textarea, +input, +[role="button"] { + -webkit-app-region: no-drag; +} + button:not(:disabled), a[href], summary, @@ -61,6 +72,15 @@ button:not(:disabled):active { transform: scale(0.975); } +.desktop-drag-region { + -webkit-app-region: drag; +} + +.desktop-no-drag, +.desktop-no-drag * { + -webkit-app-region: no-drag; +} + code, kbd, pre, @@ -237,6 +257,7 @@ button.toggle-switch { .view-shell { animation: view-enter 0.2s var(--ease-out-quart); + padding-inline: clamp(1.8rem, 2.88vw, 2.4rem); will-change: transform, opacity; } diff --git a/debug/vite.config.ts b/debug/vite.config.ts index 489d57c0..249388f5 100644 --- a/debug/vite.config.ts +++ b/debug/vite.config.ts @@ -1,10 +1,36 @@ -import { defineConfig, loadEnv } from "vite"; +import { defineConfig, loadEnv, type Plugin } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import path from "node:path"; +import { isTrustedLocalRequest } from "../server/local-access.js"; const PROJECT_ROOT = path.resolve(__dirname, ".."); +function localConnectionConfig(phoneNumber: string): Plugin { + return { + name: "boop-local-connection-config", + configureServer(server) { + server.middlewares.use((req, res, next) => { + const url = new URL(req.url ?? "/", "http://localhost"); + if (req.method !== "GET" || url.pathname !== "/api/connection-config") { + next(); + return; + } + if (!isTrustedLocalRequest(req)) { + res.statusCode = 404; + res.end("Not found"); + return; + } + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + res.setHeader("Cache-Control", "no-store"); + res.setHeader("X-Content-Type-Options", "nosniff"); + res.end(JSON.stringify({ phoneNumber })); + }); + }, + }; +} + export default defineConfig(({ mode }) => { // Load .env.local from the project root (not debug/) so we can read PORT // even though this config file lives under debug/. @@ -14,7 +40,11 @@ export default defineConfig(({ mode }) => { return { root: path.resolve(__dirname), envDir: PROJECT_ROOT, - plugins: [react(), tailwindcss()], + plugins: [ + localConnectionConfig(env.SENDBLUE_FROM_NUMBER ?? ""), + react(), + tailwindcss(), + ], server: { port: 5173, proxy: { diff --git a/electron/main.cjs b/electron/main.cjs new file mode 100644 index 00000000..bb1da442 --- /dev/null +++ b/electron/main.cjs @@ -0,0 +1,844 @@ +const { app, BrowserWindow, Menu, ipcMain, nativeImage, shell } = require("electron"); +const { spawn } = require("node:child_process"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { pathToFileURL } = require("node:url"); + +const isMac = process.platform === "darwin"; +const productName = "Boop"; +const mutableRuntimeItems = [ + ".env", + ".env.local", + ".convex", + "convex/_generated", + "data", + "debug/dist", + "dist", + "node_modules", +]; +const runtimeItems = [ + ".env.example", + "assets", + "convex", + "debug", + "package-lock.json", + "package.json", + "scripts", + "server", + "tsconfig.json", +]; + +let mainWindow; +let boopProcess; +let bootstrapProcess; +let webhookCheckTimer; +let webhookCheckSequence = 0; +let quitting = false; +let intentionalStop = false; +let starting = false; +let runtimeRoot = ""; +let cachedConnectionStatus = null; + +const status = { + state: "stopped", + server: "stopped", + convex: "stopped", + dashboard: "stopped", + tunnel: "unknown", + webhook: "unknown", + dashboardUrl: "http://localhost:5173", + publicUrl: "", + expectedWebhookUrl: "", + registeredWebhookUrl: "", + webhookDetails: "", + webhookCheckedAt: "", + convexUrl: "", + phoneNumber: "", + runtimeRoot: "", + lastMessage: "", +}; + +function desktopDataRoot() { + if (process.platform === "darwin") { + return path.join(os.homedir(), "Library", "Application Support", productName); + } + if (process.platform === "win32") { + return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), productName); + } + return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), productName); +} + +app.setName(productName); +app.setPath("userData", desktopDataRoot()); + +function isInsideMutablePath(relativePath) { + const normalized = relativePath.split(path.sep).join("/"); + if (/^\.env\..+\.local$/.test(normalized)) return true; + return mutableRuntimeItems.some( + (item) => normalized === item || normalized.startsWith(`${item}/`), + ); +} + +function lstatIfExists(value) { + try { + return fs.lstatSync(value); + } catch (error) { + if (error?.code === "ENOENT") return null; + throw error; + } +} + +function copyRuntimeItem(sourceRoot, targetRoot, relativePath) { + if (isInsideMutablePath(relativePath)) return; + + const source = path.join(sourceRoot, relativePath); + const target = path.join(targetRoot, relativePath); + if (!fs.existsSync(source)) return; + + const stat = fs.lstatSync(source); + if (stat.isSymbolicLink()) return; + if (stat.isDirectory()) { + const targetStat = lstatIfExists(target); + if (targetStat && !targetStat.isDirectory()) { + fs.rmSync(target, { force: true, recursive: true }); + } + fs.mkdirSync(target, { recursive: true }); + + const sourceEntries = new Set(fs.readdirSync(source)); + for (const entry of fs.readdirSync(target)) { + const childPath = path.join(relativePath, entry); + if (isInsideMutablePath(childPath) || sourceEntries.has(entry)) continue; + fs.rmSync(path.join(target, entry), { force: true, recursive: true }); + } + for (const entry of fs.readdirSync(source)) { + copyRuntimeItem(sourceRoot, targetRoot, path.join(relativePath, entry)); + } + return; + } + + const targetStat = lstatIfExists(target); + if (targetStat && !targetStat.isFile()) { + fs.rmSync(target, { force: true, recursive: true }); + } + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.copyFileSync(source, target); +} + +function linkNodeModules(sourceRoot, targetRoot) { + const source = path.join(sourceRoot, "node_modules"); + const target = path.join(targetRoot, "node_modules"); + if (!fs.existsSync(source)) return; + try { + const targetStat = fs.lstatSync(target); + if (targetStat.isSymbolicLink()) { + const current = fs.readlinkSync(target); + if (path.resolve(path.dirname(target), current) === source) return; + } + fs.rmSync(target, { force: true, recursive: true }); + } catch { + // Missing target is the normal first-launch path. + } + const type = process.platform === "win32" ? "junction" : "dir"; + fs.symlinkSync(source, target, type); +} + +function preparePackagedRuntime() { + const sourceRoot = app.getAppPath(); + const targetRoot = path.join(app.getPath("userData"), "runtime"); + fs.mkdirSync(targetRoot, { recursive: true }); + + for (const item of runtimeItems) { + copyRuntimeItem(sourceRoot, targetRoot, item); + } + linkNodeModules(sourceRoot, targetRoot); + fs.writeFileSync( + path.join(targetRoot, ".boop-desktop-runtime"), + `source=${sourceRoot}${os.EOL}updated=${new Date().toISOString()}${os.EOL}`, + ); + return targetRoot; +} + +function getRuntimeRoot() { + if (!app.isPackaged) return path.resolve(__dirname, ".."); + return preparePackagedRuntime(); +} + +function getIconPath() { + const root = runtimeRoot || getRuntimeRoot(); + const candidates = [ + path.join(root, "assets", "boop-app-icon.png"), + path.join(root, "assets", "boop.png"), + path.join(root, "debug", "public", "appicon.png"), + ]; + return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0]; +} + +function createIcon(size = 256) { + const image = nativeImage.createFromPath(getIconPath()); + return image.isEmpty() ? nativeImage.createEmpty() : image.resize({ width: size, height: size }); +} + +function writeNodeShim() { + const shimDir = path.join(app.getPath("userData"), "bin"); + fs.mkdirSync(shimDir, { recursive: true }); + + if (process.platform === "win32") { + const shim = path.join(shimDir, "node.cmd"); + fs.writeFileSync( + shim, + `@echo off\r\nset ELECTRON_RUN_AS_NODE=1\r\n"${process.execPath}" %*\r\n`, + ); + return { dir: shimDir, cmd: shim }; + } + + const shim = path.join(shimDir, "node"); + fs.writeFileSync( + shim, + `#!/bin/sh +ELECTRON_RUN_AS_NODE=1 exec "${process.execPath}" "$@" +`, + { mode: 0o755 }, + ); + fs.chmodSync(shim, 0o755); + return { dir: shimDir, cmd: shim }; +} + +function childEnv() { + const nodeShim = writeNodeShim(); + const binDir = path.join(runtimeRoot, "node_modules", ".bin"); + const pathKey = process.platform === "win32" ? "Path" : "PATH"; + const existingPath = process.env[pathKey] || process.env.PATH || ""; + const macCliPaths = + process.platform === "darwin" + ? [ + path.join(os.homedir(), ".local", "bin"), + path.join(os.homedir(), ".npm-global", "bin"), + "/opt/homebrew/bin", + "/opt/homebrew/sbin", + "/usr/local/bin", + "/usr/local/sbin", + "/usr/bin", + "/bin", + "/usr/sbin", + "/sbin", + ] + : []; + return { + ...process.env, + BOOP_DESKTOP: "1", + BOOP_NODE_CMD: nodeShim.cmd, + FORCE_COLOR: "0", + [pathKey]: [nodeShim.dir, binDir, ...macCliPaths, existingPath].filter(Boolean).join(path.delimiter), + }; +} + +const packageBinPaths = { + convex: ["convex", "bin", "main.js"], + tsx: ["tsx", "dist", "cli.mjs"], + vite: ["vite", "bin", "vite.js"], +}; + +function localCommand(name) { + const ext = process.platform === "win32" ? ".cmd" : ""; + const candidate = path.join(runtimeRoot, "node_modules", ".bin", `${name}${ext}`); + if (fs.existsSync(candidate)) return { cmd: candidate, args: [] }; + const packageBin = packageBinPaths[name]; + if (packageBin) { + const script = path.join(runtimeRoot, "node_modules", ...packageBin); + if (fs.existsSync(script)) return { cmd: "node", args: [script] }; + } + return { cmd: name, args: [] }; +} + +function readRuntimeEnv() { + const env = {}; + if (!runtimeRoot) return env; + + for (const filename of [".env", ".env.local"]) { + const file = path.join(runtimeRoot, filename); + if (!fs.existsSync(file)) continue; + + for (const raw of fs.readFileSync(file, "utf8").split(/\r?\n/)) { + const line = raw.trim(); + if (!line || line.startsWith("#")) continue; + const index = line.indexOf("="); + if (index === -1) continue; + + const key = line.slice(0, index).trim(); + let value = line.slice(index + 1).trim(); + value = value.replace(/^['"]|['"]$/g, ""); + env[key] = value; + } + } + + return env; +} + +function readConnectionStatus() { + const env = readRuntimeEnv(); + return { + convexUrl: env.CONVEX_URL || env.VITE_CONVEX_URL || "", + phoneNumber: env.SENDBLUE_FROM_NUMBER || "", + }; +} + +function refreshConnectionStatus() { + cachedConnectionStatus = readConnectionStatus(); + return cachedConnectionStatus; +} + +function connectionStatus() { + return cachedConnectionStatus || refreshConnectionStatus(); +} + +function plainStatus(value) { + return value.charAt(0).toUpperCase() + value.slice(1).replace(/-/g, " "); +} + +function setStatus(partial) { + Object.assign(status, connectionStatus(), partial, { runtimeRoot }); + const ready = + status.server === "running" && + status.convex === "running" && + status.dashboard === "running"; + if (ready && status.state !== "error" && status.state !== "setup-required") { + status.state = "running"; + } + updateMenus(); + for (const window of BrowserWindow.getAllWindows()) { + window.webContents.send("boop-status", status); + } +} + +function webhookUrlFromPublicUrl(publicUrl) { + const trimmed = (publicUrl || "").replace(/\/$/, ""); + if (!trimmed) return ""; + return trimmed.endsWith("/sendblue/webhook") ? trimmed : `${trimmed}/sendblue/webhook`; +} + +function applyWebhookCheck(result) { + setStatus({ + webhook: result.state || (result.ok ? "registered" : "error"), + expectedWebhookUrl: result.expectedWebhookUrl || status.expectedWebhookUrl, + registeredWebhookUrl: result.registeredWebhookUrl || "", + webhookDetails: result.details || "", + webhookCheckedAt: result.checkedAt || new Date().toISOString(), + }); +} + +function runWebhookCheck(expectedUrl, sequence = ++webhookCheckSequence) { + if (!expectedUrl || !runtimeRoot) { + setStatus({ + webhook: "no-tunnel", + expectedWebhookUrl: "", + registeredWebhookUrl: "", + webhookDetails: "No active public URL is available for Sendblue.", + webhookCheckedAt: new Date().toISOString(), + }); + return Promise.resolve(status); + } + + return new Promise((resolve) => { + const script = path.join(runtimeRoot, "scripts", "sendblue-webhook.mjs"); + const child = spawn("node", [script, "--check", "--json", expectedUrl], { + cwd: runtimeRoot, + env: childEnv(), + stdio: ["ignore", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (chunk) => { + stdout += chunk.toString(); + }); + child.stderr.on("data", (chunk) => { + stderr += chunk.toString(); + }); + child.on("error", (error) => { + if (sequence !== webhookCheckSequence) return resolve(status); + setStatus({ + webhook: "error", + expectedWebhookUrl: expectedUrl, + registeredWebhookUrl: "", + webhookDetails: error.message, + webhookCheckedAt: new Date().toISOString(), + }); + resolve(status); + }); + child.on("exit", () => { + if (sequence !== webhookCheckSequence) return resolve(status); + const jsonLine = stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .reverse() + .find((line) => line.startsWith("{") && line.endsWith("}")); + if (!jsonLine) { + setStatus({ + webhook: "error", + expectedWebhookUrl: expectedUrl, + registeredWebhookUrl: "", + webhookDetails: stderr.trim() || stdout.trim() || "Sendblue webhook check did not return JSON.", + webhookCheckedAt: new Date().toISOString(), + }); + return resolve(status); + } + try { + applyWebhookCheck(JSON.parse(jsonLine)); + } catch (error) { + setStatus({ + webhook: "error", + expectedWebhookUrl: expectedUrl, + registeredWebhookUrl: "", + webhookDetails: error instanceof Error ? error.message : String(error), + webhookCheckedAt: new Date().toISOString(), + }); + } + resolve(status); + }); + }); +} + +function scheduleWebhookCheck(publicUrl, delayMs = 1000) { + const expectedUrl = webhookUrlFromPublicUrl(publicUrl); + if (!expectedUrl) return; + const sequence = ++webhookCheckSequence; + clearTimeout(webhookCheckTimer); + setStatus({ + webhook: "checking", + expectedWebhookUrl: expectedUrl, + registeredWebhookUrl: "", + webhookDetails: "Checking Sendblue registration against the active tunnel.", + }); + webhookCheckTimer = setTimeout(() => { + runWebhookCheck(expectedUrl, sequence).catch(() => undefined); + }, delayMs); +} + +function checkSendblueWebhook() { + const expectedUrl = status.expectedWebhookUrl || webhookUrlFromPublicUrl(status.publicUrl); + setStatus({ + webhook: expectedUrl ? "checking" : "no-tunnel", + expectedWebhookUrl: expectedUrl, + registeredWebhookUrl: "", + webhookDetails: expectedUrl + ? "Checking Sendblue registration against the active tunnel." + : "No active public URL is available for Sendblue.", + }); + return runWebhookCheck(expectedUrl); +} + +function showMainWindow() { + if (!mainWindow) return; + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.show(); + mainWindow.focus(); +} + +function loadStatusPage() { + if (!mainWindow) return; + mainWindow.loadFile(path.join(__dirname, "status.html")).catch(() => undefined); + showMainWindow(); +} + +function ensureNativeWindowButtons() { + if (!isMac || !mainWindow) return; + mainWindow.setWindowButtonVisibility(true); + mainWindow.setWindowButtonPosition({ x: 18, y: 18 }); +} + +function createWindow() { + const statusPageUrl = pathToFileURL(path.join(__dirname, "status.html")).href; + mainWindow = new BrowserWindow({ + width: 1180, + height: 780, + minWidth: 920, + minHeight: 620, + title: "Boop", + icon: getIconPath(), + show: false, + backgroundColor: "#101012", + titleBarStyle: isMac ? "hidden" : undefined, + trafficLightPosition: isMac ? { x: 18, y: 18 } : undefined, + acceptFirstMouse: true, + webPreferences: { + preload: path.join(__dirname, "preload.cjs"), + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + }, + }); + + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("https://") || url.startsWith("http://")) { + shell.openExternal(url).catch(() => undefined); + } + return { action: "deny" }; + }); + mainWindow.webContents.on("will-navigate", (event, url) => { + if (url !== statusPageUrl) event.preventDefault(); + }); + + if (isMac) { + ensureNativeWindowButtons(); + mainWindow.on("show", ensureNativeWindowButtons); + mainWindow.on("focus", ensureNativeWindowButtons); + mainWindow.on("blur", ensureNativeWindowButtons); + mainWindow.on("restore", ensureNativeWindowButtons); + } + + mainWindow.once("ready-to-show", () => { + ensureNativeWindowButtons(); + mainWindow.show(); + }); + mainWindow.on("closed", () => { + mainWindow = undefined; + }); + loadStatusPage(); +} + +function openDashboardInWindow() { + if (!mainWindow) createWindow(); + if (mainWindow) { + mainWindow.webContents.send("boop-open-dashboard", status.dashboardUrl); + } + showMainWindow(); +} + +function statusMenuTemplate() { + const canOpenDashboard = status.dashboard === "running" && Boolean(status.dashboardUrl); + const canStop = Boolean(boopProcess || bootstrapProcess || starting); + return [ + { label: `Boop: ${plainStatus(status.state)}`, enabled: false }, + { type: "separator" }, + { label: `Server: ${plainStatus(status.server)}`, enabled: false }, + { label: `Convex: ${plainStatus(status.convex)}`, enabled: false }, + { label: `Dashboard: ${plainStatus(status.dashboard)}`, enabled: false }, + { label: `Tunnel: ${plainStatus(status.tunnel)}`, enabled: false }, + { label: `Sendblue Webhook: ${plainStatus(status.webhook)}`, enabled: false }, + { type: "separator" }, + { + label: "Open Dashboard", + enabled: canOpenDashboard, + click: openDashboardInWindow, + }, + { label: "Show Boop", click: loadStatusPage }, + { + label: "Check Sendblue Webhook", + enabled: Boolean(status.expectedWebhookUrl || status.publicUrl), + click: checkSendblueWebhook, + }, + { label: "Start Boop", enabled: !boopProcess && !starting, click: startBoop }, + { label: "Restart Boop", enabled: !starting, click: restartBoop }, + { label: "Stop Boop", enabled: canStop, click: stopBoop }, + { type: "separator" }, + { label: "Open Runtime Folder", click: () => shell.openPath(runtimeRoot) }, + ]; +} + +function updateMenus() { + const template = [ + ...(isMac + ? [ + { + label: app.name, + submenu: [ + { role: "about" }, + { type: "separator" }, + { role: "hide" }, + { role: "hideOthers" }, + { role: "unhide" }, + { type: "separator" }, + { role: "quit" }, + ], + }, + ] + : []), + { + label: "Status", + submenu: statusMenuTemplate(), + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { type: "separator" }, + { role: "toggleDevTools" }, + ], + }, + { + label: "Window", + submenu: [{ role: "minimize" }, { role: "close" }], + }, + ]; + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +function stripAnsi(value) { + return value.replace(/\x1b\[[0-9;]*m/g, ""); +} + +function ingestLine(line) { + const plain = stripAnsi(line).trim(); + if (!plain) return; + if ( + intentionalStop && + (/^ngrok\s+│/.test(plain) || /obj=tunnels\.session|command_line/.test(plain)) && + /Stopping forwarder|Listener closed|failed to accept connection|accept failed|session closed/.test( + plain, + ) + ) { + return; + } + + const next = { lastMessage: plain }; + let checkWebhookForPublicUrl = ""; + if (/boop-agent server listening on :/.test(plain)) next.server = "running"; + if (/Convex functions ready/.test(plain)) next.convex = "running"; + + const dashboardMatch = + plain.match(/Local:\s+(http:\/\/\S+)/) || + plain.match(/Debug dashboard(?: \(click me\))?:\s+(http:\/\/\S+)/); + if (dashboardMatch) { + next.dashboard = "running"; + next.dashboardUrl = dashboardMatch[1].replace(/\/$/, ""); + } + + const publicMatch = plain.match(/Public URL:\s+(https?:\/\/\S+)/); + if (publicMatch) { + const publicUrl = publicMatch[1].replace(/\/$/, ""); + next.tunnel = "running"; + next.publicUrl = publicUrl; + next.webhook = "checking"; + next.expectedWebhookUrl = webhookUrlFromPublicUrl(publicUrl); + next.registeredWebhookUrl = ""; + next.webhookDetails = "Checking Sendblue registration against the active tunnel."; + checkWebhookForPublicUrl = publicUrl; + } + + if (/ngrok is not installed|No public tunnel configured/.test(plain)) { + next.tunnel = "stopped"; + next.webhook = "no-tunnel"; + next.expectedWebhookUrl = ""; + next.registeredWebhookUrl = ""; + next.webhookDetails = "No public tunnel is running, so Sendblue cannot reach this app."; + } + if (/Convex types haven't been generated/.test(plain)) next.state = "setup-required"; + if (/A child process exited with code|fatal /.test(plain)) next.state = "error"; + + setStatus(next); + if (checkWebhookForPublicUrl) scheduleWebhookCheck(checkWebhookForPublicUrl); +} + +function pipeOutput(stream, shouldIngest = () => true) { + let buffer = ""; + stream.on("data", (chunk) => { + buffer += chunk.toString(); + let index; + while ((index = buffer.indexOf("\n")) !== -1) { + if (shouldIngest()) ingestLine(buffer.slice(0, index)); + buffer = buffer.slice(index + 1); + } + }); +} + +function runBootstrap(command, args) { + return new Promise((resolve, reject) => { + bootstrapProcess = spawn(command, args, { + cwd: runtimeRoot, + env: childEnv(), + stdio: ["ignore", "pipe", "pipe"], + }); + pipeOutput(bootstrapProcess.stdout); + pipeOutput(bootstrapProcess.stderr); + bootstrapProcess.on("error", (error) => { + bootstrapProcess = undefined; + reject(error); + }); + bootstrapProcess.on("exit", (code) => { + bootstrapProcess = undefined; + if (code === 0) { + resolve(); + } else { + reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`)); + } + }); + }); +} + +async function syncConvexRuntime() { + const generatedApi = path.join(runtimeRoot, "convex", "_generated", "api.js"); + const convex = localCommand("convex"); + setStatus({ + state: "starting", + convex: "starting", + lastMessage: fs.existsSync(generatedApi) + ? "Synchronizing Convex functions for this desktop version." + : "Generating Convex files in the desktop runtime folder.", + }); + await runBootstrap(convex.cmd, [...convex.args, "dev", "--once", "--typecheck=disable"]); + refreshConnectionStatus(); +} + +function resetServiceStatuses(state) { + clearTimeout(webhookCheckTimer); + webhookCheckSequence += 1; + refreshConnectionStatus(); + setStatus({ + state, + server: state === "stopped" ? "stopped" : "starting", + convex: state === "stopped" ? "stopped" : "starting", + dashboard: state === "stopped" ? "stopped" : "starting", + tunnel: "unknown", + webhook: "unknown", + publicUrl: "", + expectedWebhookUrl: "", + registeredWebhookUrl: "", + webhookDetails: "", + webhookCheckedAt: "", + lastMessage: "", + }); +} + +async function startBoop() { + if (boopProcess || starting) { + showMainWindow(); + return; + } + + starting = true; + intentionalStop = false; + resetServiceStatuses("starting"); + try { + await syncConvexRuntime(); + } catch (error) { + starting = false; + if (intentionalStop) { + resetServiceStatuses("stopped"); + return; + } + setStatus({ + state: "setup-required", + server: "stopped", + dashboard: "stopped", + lastMessage: error instanceof Error ? error.message : String(error), + }); + return; + } + + const script = path.join(runtimeRoot, "scripts", "dev.mjs"); + boopProcess = spawn("node", [script], { + cwd: runtimeRoot, + env: childEnv(), + stdio: ["ignore", "pipe", "pipe"], + }); + const child = boopProcess; + starting = false; + + pipeOutput(child.stdout, () => boopProcess === child); + pipeOutput(child.stderr, () => boopProcess === child); + child.on("error", (error) => { + if (boopProcess !== child) return; + starting = false; + boopProcess = undefined; + setStatus({ state: "error", lastMessage: error.message }); + }); + child.on("exit", (code) => { + if (boopProcess !== child) return; + starting = false; + boopProcess = undefined; + if (quitting || intentionalStop || status.state === "stopped") return; + if (code === 0 || status.state === "setup-required") { + resetServiceStatuses(status.state === "setup-required" ? "setup-required" : "stopped"); + } else { + setStatus({ + state: "error", + server: "stopped", + convex: "stopped", + dashboard: "stopped", + lastMessage: `Boop exited with code ${code}`, + }); + } + }); +} + +function stopBoop() { + if (bootstrapProcess) { + intentionalStop = true; + bootstrapProcess.kill("SIGTERM"); + bootstrapProcess = undefined; + } + if (!boopProcess) { + starting = false; + resetServiceStatuses("stopped"); + return; + } + const child = boopProcess; + boopProcess = undefined; + starting = false; + intentionalStop = true; + resetServiceStatuses("stopped"); + setStatus({ lastMessage: "Boop is stopped." }); + child.kill("SIGTERM"); +} + +function restartBoop() { + stopBoop(); + setStatus({ + state: "starting", + server: "starting", + convex: "starting", + dashboard: "starting", + tunnel: "unknown", + publicUrl: "", + lastMessage: "Restarting Boop.", + }); + setTimeout(startBoop, 700); +} + +ipcMain.handle("boop:get-status", () => status); +ipcMain.handle("boop:start", async () => { + await startBoop(); + return status; +}); +ipcMain.handle("boop:stop", () => { + stopBoop(); + return status; +}); +ipcMain.handle("boop:restart", () => { + restartBoop(); + return status; +}); +ipcMain.handle("boop:check-webhook", async () => { + await checkSendblueWebhook(); + return status; +}); +ipcMain.handle("boop:open-dashboard", () => { + if (!status.dashboardUrl) return; + openDashboardInWindow(); + return status.dashboardUrl; +}); +ipcMain.handle("boop:show-runtime-folder", () => shell.openPath(runtimeRoot)); + +app.whenReady().then(() => { + runtimeRoot = getRuntimeRoot(); + Object.assign(status, refreshConnectionStatus(), { runtimeRoot }); + if (isMac && app.dock) app.dock.setIcon(createIcon(256)); + + createWindow(); + startBoop(); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + showMainWindow(); + }); +}); + +app.on("before-quit", () => { + quitting = true; + stopBoop(); +}); + +app.on("window-all-closed", () => { + if (!isMac) app.quit(); +}); diff --git a/electron/preload.cjs b/electron/preload.cjs new file mode 100644 index 00000000..f0442c38 --- /dev/null +++ b/electron/preload.cjs @@ -0,0 +1,21 @@ +const { contextBridge, ipcRenderer } = require("electron"); + +contextBridge.exposeInMainWorld("boopDesktop", { + getStatus: () => ipcRenderer.invoke("boop:get-status"), + start: () => ipcRenderer.invoke("boop:start"), + stop: () => ipcRenderer.invoke("boop:stop"), + restart: () => ipcRenderer.invoke("boop:restart"), + checkWebhook: () => ipcRenderer.invoke("boop:check-webhook"), + openDashboard: () => ipcRenderer.invoke("boop:open-dashboard"), + showRuntimeFolder: () => ipcRenderer.invoke("boop:show-runtime-folder"), + onStatus: (callback) => { + const listener = (_event, status) => callback(status); + ipcRenderer.on("boop-status", listener); + return () => ipcRenderer.removeListener("boop-status", listener); + }, + onOpenDashboard: (callback) => { + const listener = (_event, url) => callback(url); + ipcRenderer.on("boop-open-dashboard", listener); + return () => ipcRenderer.removeListener("boop-open-dashboard", listener); + }, +}); diff --git a/electron/status.html b/electron/status.html new file mode 100644 index 00000000..45e39a8c --- /dev/null +++ b/electron/status.html @@ -0,0 +1,191 @@ + + + + + + + Boop + + + +
+
+ + + + +
+
+

Boop is stopped

+
Waiting for Boop.
+
+ + + + +
+
+
+
+
+
+ + + + diff --git a/electron/status.js b/electron/status.js new file mode 100644 index 00000000..21d115d8 --- /dev/null +++ b/electron/status.js @@ -0,0 +1,280 @@ +const fields = { + message: document.getElementById("message"), + runtimeRoot: document.getElementById("runtime-root"), + emptyTitle: document.getElementById("empty-title"), + openDashboard: document.getElementById("open-dashboard"), + start: document.getElementById("start"), + stop: document.getElementById("stop"), + restart: document.getElementById("restart"), + dashboardFrame: document.getElementById("dashboard-frame"), + dashboardEmpty: document.getElementById("dashboard-empty"), +}; +let lastStatus = null; + +function normalizeUrl(value) { + if (!value) return ""; + try { + const url = new URL(value); + const hostname = url.hostname.replace(/^\[|\]$/g, "").toLowerCase(); + const local = + hostname === "localhost" || + hostname === "::1" || + hostname.startsWith("127."); + return url.protocol === "http:" && local ? value.replace(/\/$/, "") : ""; + } catch { + return ""; + } +} + +function dashboardOrigin() { + try { + const url = fields.dashboardFrame.dataset.url || lastStatus?.dashboardUrl || ""; + return normalizeUrl(url) ? new URL(url).origin : ""; + } catch { + return ""; + } +} + +function dashboardStatus(status) { + return { + state: status.state, + server: status.server, + convex: status.convex, + dashboard: status.dashboard, + tunnel: status.tunnel, + webhook: status.webhook, + dashboardUrl: status.dashboardUrl, + publicUrl: status.publicUrl, + expectedWebhookUrl: status.expectedWebhookUrl, + registeredWebhookUrl: status.registeredWebhookUrl, + webhookDetails: status.webhookDetails, + webhookCheckedAt: status.webhookCheckedAt, + convexUrl: status.convexUrl, + phoneNumber: status.phoneNumber, + }; +} + +function postStatusToDashboard(status = lastStatus) { + if (!status || !fields.dashboardFrame.contentWindow) return; + const targetOrigin = dashboardOrigin(); + if (!targetOrigin) return; + fields.dashboardFrame.contentWindow.postMessage( + { + type: "boop-desktop-status", + status: dashboardStatus(status), + }, + targetOrigin, + ); +} + +function showDashboard(url) { + const nextUrl = normalizeUrl(url); + if (!nextUrl) return; + if (fields.dashboardFrame.dataset.url !== nextUrl) { + fields.dashboardFrame.src = nextUrl; + fields.dashboardFrame.dataset.url = nextUrl; + } + fields.dashboardFrame.classList.add("visible"); + fields.dashboardEmpty.classList.add("hidden"); + window.setTimeout(() => postStatusToDashboard(), 100); +} + +function hideDashboard() { + fields.dashboardFrame.classList.remove("visible"); + fields.dashboardEmpty.classList.remove("hidden"); + fields.dashboardFrame.removeAttribute("src"); + fields.dashboardFrame.dataset.url = ""; +} + +function isStarting(status) { + return ( + status.state === "starting" || + status.server === "starting" || + status.convex === "starting" || + status.dashboard === "starting" + ); +} + +function isActive(status) { + return ( + isStarting(status) || + status.state === "running" || + status.server === "running" || + status.convex === "running" || + status.dashboard === "running" + ); +} + +function displayMessage(status) { + const message = status.lastMessage || ""; + if ( + status.state === "stopped" && + (/^ngrok\s+│/.test(message) || /Listener closed|accept failed|session closed/.test(message)) + ) { + return "Boop is stopped."; + } + return message || "Waiting for Boop."; +} + +function render(status) { + lastStatus = status; + const starting = isStarting(status); + const active = isActive(status); + const dashboardReady = status.dashboard === "running" && Boolean(status.dashboardUrl); + + fields.message.textContent = displayMessage(status); + fields.runtimeRoot.textContent = status.runtimeRoot || ""; + fields.openDashboard.disabled = !dashboardReady; + fields.start.disabled = starting || active; + fields.stop.disabled = !active; + fields.restart.disabled = starting; + + fields.emptyTitle.textContent = + status.state === "setup-required" + ? "Setup required" + : starting + ? "Starting Boop" + : status.state === "error" + ? "Boop stopped" + : "Boop is stopped"; + + if (dashboardReady) { + showDashboard(status.dashboardUrl); + } else { + hideDashboard(); + } + + postStatusToDashboard(status); +} + +async function refreshStatus() { + const status = await window.boopDesktop.getStatus(); + render(status); +} + +function optimisticStatus(partial) { + render({ + state: "stopped", + server: "stopped", + convex: "stopped", + dashboard: "stopped", + tunnel: "unknown", + webhook: "unknown", + dashboardUrl: "", + publicUrl: "", + expectedWebhookUrl: "", + registeredWebhookUrl: "", + webhookDetails: "", + webhookCheckedAt: "", + convexUrl: "", + phoneNumber: "", + runtimeRoot: "", + lastMessage: "", + ...(lastStatus || {}), + ...partial, + }); +} + +async function runControl(action, optimistic) { + if (optimistic) optimisticStatus(optimistic); + const status = await window.boopDesktop[action](); + if (status) render(status); + window.setTimeout(refreshStatus, 500); + window.setTimeout(refreshStatus, 1500); +} + +function optimisticForAction(action) { + if (action === "stop") { + return { + state: "stopped", + server: "stopped", + convex: "stopped", + dashboard: "stopped", + tunnel: "stopped", + webhook: "unknown", + dashboardUrl: "", + publicUrl: "", + expectedWebhookUrl: "", + registeredWebhookUrl: "", + webhookDetails: "", + webhookCheckedAt: "", + lastMessage: "Boop is stopped.", + }; + } + + return { + state: "starting", + server: "starting", + convex: "starting", + dashboard: "starting", + tunnel: "unknown", + webhook: "unknown", + expectedWebhookUrl: "", + registeredWebhookUrl: "", + webhookDetails: "", + webhookCheckedAt: "", + lastMessage: action === "restart" ? "Restarting Boop." : "Starting Boop.", + }; +} + +async function handleDashboardAction(action) { + if (action === "status") { + postStatusToDashboard(); + return; + } + if (action === "openDashboard") { + const url = await window.boopDesktop.openDashboard(); + showDashboard(url); + return; + } + if (action === "showRuntimeFolder") { + window.boopDesktop.showRuntimeFolder(); + return; + } + if (action === "checkWebhook") { + const status = await window.boopDesktop.checkWebhook(); + if (status) render(status); + return; + } + if (["start", "stop", "restart"].includes(action)) { + runControl(action, optimisticForAction(action)); + } +} + +refreshStatus(); +window.boopDesktop.onStatus(render); +window.boopDesktop.onOpenDashboard(showDashboard); +fields.dashboardFrame.addEventListener("load", () => postStatusToDashboard()); + +window.addEventListener("message", (event) => { + if (event.source !== fields.dashboardFrame.contentWindow) return; + if (!dashboardOrigin() || event.origin !== dashboardOrigin()) return; + const data = event.data || {}; + if (data.type === "boop-desktop-status-request") { + postStatusToDashboard(); + return; + } + if (data.type === "boop-desktop-action" && typeof data.action === "string") { + handleDashboardAction(data.action); + } +}); + +document.getElementById("open-dashboard").addEventListener("click", async () => { + const url = await window.boopDesktop.openDashboard(); + showDashboard(url); +}); +document.getElementById("restart").addEventListener("click", () => { + runControl("restart", optimisticForAction("restart")); +}); +document.getElementById("start").addEventListener("click", () => { + runControl("start", optimisticForAction("start")); +}); +document.getElementById("stop").addEventListener("click", () => { + runControl("stop", optimisticForAction("stop")); +}); +const runtimeButton = document.getElementById("runtime"); +if (runtimeButton) { + runtimeButton.addEventListener("click", () => { + window.boopDesktop.showRuntimeFolder(); + }); +} diff --git a/package-lock.json b/package-lock.json index 5941f7a1..595503c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,22 +11,30 @@ "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.0", "@composio/claude-agent-sdk": "^0.6.10", + "@composio/client": "^0.1.0-alpha.66", "@composio/core": "^0.6.10", "@hugeicons/core-free-icons": "^1.0.0", "@hugeicons/react": "^1.0.0", "@huggingface/transformers": "^4.2.0", "@modelcontextprotocol/sdk": "^1.0.0", + "@tailwindcss/vite": "^4.0.0", + "@vitejs/plugin-react": "^4.3.0", "convex": "^1.36.1", "cors": "^2.8.5", "croner": "^9.0.0", "dotenv": "^16.4.0", "express": "^5.0.0", + "prompts": "^2.4.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", "react-force-graph-2d": "^1.27.0", + "tailwindcss": "^4.0.0", + "tsx": "^4.19.0", + "vite": "^6.0.0", "ws": "^8.18.0", "zod": "^3.23.0" }, "devDependencies": { - "@tailwindcss/vite": "^4.0.0", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/node": "^22.0.0", @@ -34,16 +42,11 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@types/ws": "^8.5.12", - "@vitejs/plugin-react": "^4.3.0", + "electron": "^43.1.0", + "electron-builder": "^26.0.0", "npm-run-all": "^4.1.5", - "prompts": "^2.4.2", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "tailwindcss": "^4.0.0", - "tsx": "^4.19.0", "typescript": "^5.6.0", - "vite": "^6.0.0", - "vitest": "^2.1.9" + "vitest": "^4.1.10" }, "engines": { "node": ">=20" @@ -75,13 +78,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -90,31 +92,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -134,21 +134,19 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -158,14 +156,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -178,46 +175,42 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -230,64 +223,58 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -300,7 +287,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -316,7 +302,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -329,33 +314,31 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -363,14 +346,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -423,427 +405,851 @@ "zod": ">=3.25.76 <5" } }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.4.tgz", + "integrity": "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", - "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", - "cpu": [ - "ppc64" - ], + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, "engines": { - "node": ">=18" + "node": ">=10.12.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", - "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", - "cpu": [ - "arm" - ], + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", - "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", - "cpu": [ - "arm64" - ], + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", - "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", - "cpu": [ - "x64" - ], + "node_modules/@electron/fuses/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", - "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", - "cpu": [ - "arm64" - ], + "node_modules/@electron/fuses/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" + "node": ">= 10.0.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", - "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", - "cpu": [ - "x64" - ], + "node_modules/@electron/get": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz", + "integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" + }, "engines": { - "node": ">=18" + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", - "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", - "cpu": [ - "arm64" - ], + "node_modules/@electron/get/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], "engines": { - "node": ">=18" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", - "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", - "cpu": [ - "x64" - ], + "node_modules/@electron/get/node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, "license": "MIT", "optional": true, - "os": [ - "freebsd" - ], "engines": { - "node": ">=18" + "node": ">=20.18.1" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", - "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", - "cpu": [ - "arm" - ], + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, "engines": { - "node": ">=18" + "node": ">= 10.0.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", - "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", - "cpu": [ - "arm64" - ], + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", - "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", - "cpu": [ - "ia32" - ], + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", - "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", - "cpu": [ - "loong64" - ], + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">= 10.0.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", - "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", - "cpu": [ - "mips64el" - ], + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", + "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, "engines": { - "node": ">=18" + "node": ">=14.14" } }, - "node_modules/@esbuild/linux-ppc64": { + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", - "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", "cpu": [ "ppc64" ], "license": "MIT", "optional": true, "os": [ - "linux" + "aix" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-riscv64": { + "node_modules/@esbuild/android-arm": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", - "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", "cpu": [ - "riscv64" + "arm" ], "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-s390x": { + "node_modules/@esbuild/android-arm64": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", - "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", "cpu": [ - "s390x" + "arm64" ], "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-x64": { + "node_modules/@esbuild/android-x64": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", - "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/netbsd-arm64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", - "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ - "netbsd" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { + "node_modules/@esbuild/darwin-x64": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", - "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "netbsd" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-arm64": { + "node_modules/@esbuild/freebsd-arm64": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", - "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ - "openbsd" + "freebsd" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { + "node_modules/@esbuild/freebsd-x64": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", - "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "openbsd" + "freebsd" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openharmony-arm64": { + "node_modules/@esbuild/linux-arm": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", - "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", "cpu": [ - "arm64" + "arm" ], "license": "MIT", "optional": true, "os": [ - "openharmony" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { + "node_modules/@esbuild/linux-arm64": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", - "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", "cpu": [ - "x64" + "arm64" ], "license": "MIT", "optional": true, "os": [ - "sunos" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-arm64": { + "node_modules/@esbuild/linux-ia32": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", - "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", "cpu": [ - "arm64" + "ia32" ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-ia32": { + "node_modules/@esbuild/linux-loong64": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", - "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", "cpu": [ - "ia32" + "loong64" ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-x64": { + "node_modules/@esbuild/linux-mips64el": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", - "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", "cpu": [ - "x64" + "mips64el" ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" ], "engines": { "node": ">=18" @@ -1085,6 +1491,44 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-linux-arm": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", @@ -1217,27 +1661,77 @@ "@img/sharp-libvips-linux-x64": "1.0.4" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", "cpu": [ - "wasm32" + "arm64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "libc": [ + "musl" + ], + "license": "Apache-2.0", "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, + "os": [ + "linux" + ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", "cpu": [ @@ -1293,11 +1787,23 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1308,7 +1814,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1319,7 +1824,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1329,20 +1833,96 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", @@ -1383,6 +1963,71 @@ } } }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", + "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "tslib": "^2.8.1", + "webcrypto-core": "^1.9.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1402,19 +2047,18 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -1423,12 +2067,6 @@ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "license": "BSD-3-Clause" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", - "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", - "license": "BSD-3-Clause" - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -1451,7 +2089,6 @@ "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -1461,7 +2098,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1475,7 +2111,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1489,7 +2124,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1503,7 +2137,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1517,7 +2150,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1531,7 +2163,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1545,7 +2176,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1559,7 +2189,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1573,7 +2202,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1587,7 +2215,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1601,7 +2228,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1615,7 +2241,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1629,7 +2254,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1643,7 +2267,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1657,7 +2280,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1671,7 +2293,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1685,7 +2306,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1699,7 +2319,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1713,7 +2332,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1727,7 +2345,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1741,7 +2358,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1755,7 +2371,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1769,7 +2384,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1783,7 +2397,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1797,18 +2410,49 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tailwindcss/node": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", @@ -1824,7 +2468,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 20" @@ -1851,7 +2494,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1868,7 +2510,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1885,7 +2526,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1902,7 +2542,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1919,7 +2558,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1936,7 +2574,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1953,7 +2590,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1970,7 +2606,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1987,7 +2622,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2012,7 +2646,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2029,7 +2662,6 @@ }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { "version": "1.8.1", - "dev": true, "inBundle": true, "license": "MIT", "optional": true, @@ -2040,7 +2672,6 @@ }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { "version": "1.8.1", - "dev": true, "inBundle": true, "license": "MIT", "optional": true, @@ -2050,7 +2681,6 @@ }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { "version": "1.1.0", - "dev": true, "inBundle": true, "license": "MIT", "optional": true, @@ -2060,7 +2690,6 @@ }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { "version": "1.1.1", - "dev": true, "inBundle": true, "license": "MIT", "optional": true, @@ -2076,7 +2705,6 @@ }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { "version": "0.10.1", - "dev": true, "inBundle": true, "license": "MIT", "optional": true, @@ -2086,7 +2714,6 @@ }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { "version": "2.8.1", - "dev": true, "inBundle": true, "license": "0BSD", "optional": true @@ -2098,7 +2725,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2115,7 +2741,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2129,7 +2754,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", - "dev": true, "license": "MIT", "dependencies": { "@tailwindcss/node": "4.2.2", @@ -2150,7 +2774,6 @@ "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", @@ -2164,7 +2787,6 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -2174,7 +2796,6 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -2185,7 +2806,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.2" @@ -2202,6 +2822,30 @@ "@types/node": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -2222,11 +2866,27 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/express": { @@ -2254,6 +2914,23 @@ "@types/send": "*" } }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -2267,6 +2944,23 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", @@ -2321,10 +3015,20 @@ "@types/react": "^19.2.0" } }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2356,7 +3060,6 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.28.0", @@ -2374,91 +3077,138 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -2490,6 +3240,16 @@ "node": ">=12.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", @@ -2523,6 +3283,16 @@ } } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2538,6 +3308,269 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/app-builder-lib": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz", + "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.4", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@noble/hashes": "^2.2.0", + "@peculiar/webcrypto": "^1.7.1", + "@types/fs-extra": "9.0.13", + "ajv": "^8.18.0", + "asn1js": "^3.0.10", + "async-exit-hook": "^2.0.1", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.15.3", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.2.5", + "pkijs": "^3.4.0", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "unzipper": "^0.12.3", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.15.3", + "electron-builder-squirrel-windows": "26.15.3" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/app-builder-lib/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/app-builder-lib/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -2577,6 +3610,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2587,6 +3635,23 @@ "node": ">=12" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -2597,6 +3662,23 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2613,6 +3695,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2620,11 +3709,31 @@ "dev": true, "license": "MIT" }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz", - "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==", + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -2643,6 +3752,13 @@ "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -2686,10 +3802,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", "funding": [ { "type": "opencollective", @@ -2706,10 +3821,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -2719,33 +3834,147 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "node_modules/builder-util": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz", + "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==", "dev": true, "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, "engines": { - "node": ">=8" + "node": ">=14.0.0" } }, - "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "node_modules/builder-util-runtime": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" @@ -2787,10 +4016,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001788", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", - "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", - "dev": true, + "version": "1.0.30001805", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", "funding": [ { "type": "opencollective", @@ -2820,18 +4048,11 @@ } }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -2852,14 +4073,65 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], "license": "MIT", "engines": { - "node": ">= 16" + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/color-convert": { @@ -2880,6 +4152,39 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2913,18 +4218,17 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/convex": { - "version": "1.36.1", - "resolved": "https://registry.npmjs.org/convex/-/convex-1.36.1.tgz", - "integrity": "sha512-NVnwNqU+h8jyPuS0Itvj4MPH9c2yF+tA/RNoSDpCqiLhmYD4+kZxm0dDkVM0QDzz66wem9NqheBb9YQGsHwzBQ==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.42.1.tgz", + "integrity": "sha512-yFKp4xerVuBwQqpuTtvosOk9F/fX0twZs+Xti3oj/MlwPUU90QuhgCYo/I2wvFBAFwXsmx9tXpf2q8ekh1+3ug==", "license": "Apache-2.0", "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0", - "ws": "8.18.0" + "ws": "8.21.0" }, "bin": { "convex": "bin/main.js" @@ -2954,27 +4258,6 @@ } } }, - "node_modules/convex/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2993,6 +4276,13 @@ "node": ">=6.6.0" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -3019,6 +4309,15 @@ "node": ">=18.0" } }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3327,14 +4626,43 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" } }, "node_modules/define-data-property": { @@ -3371,6 +4699,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3395,6 +4733,68 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "license": "MIT" }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dmg-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz", + "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -3407,6 +4807,22 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3421,44 +4837,309 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/electron-to-chromium": { - "version": "1.5.340", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz", - "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==", + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, - "license": "ISC" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" } }, - "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "node_modules/electron": { + "version": "43.1.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-43.1.0.tgz", + "integrity": "sha512-DPfxpQLd4NL3BJ8DBxYAfmLUKKesF5Rx9dQx5FyczAP8bhOPScjHE48GArVeXu68LlAainuwkmQTQvdZwpIIAQ==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" }, "engines": { - "node": ">=10.13.0" + "node": ">= 22.12.0" } }, - "node_modules/error-ex": { - "version": "1.3.4", + "node_modules/electron-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", + "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.15.3", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", + "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", + "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "aws4": "^1.13.2", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "license": "ISC" + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, @@ -3555,9 +5236,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "dev": true, "license": "MIT" }, @@ -3658,7 +5339,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3730,6 +5410,13 @@ "node": ">=12.0.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -3774,12 +5461,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", - "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -3798,9 +5485,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", "funding": [ { "type": "github", @@ -3817,7 +5504,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -3831,6 +5517,39 @@ } } }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -3914,6 +5633,46 @@ "node": ">=12" } }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3932,11 +5691,17 @@ "node": ">= 0.8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -4001,12 +5766,21 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4044,6 +5818,22 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -4066,7 +5856,6 @@ "version": "4.14.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", - "dev": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -4075,6 +5864,28 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/global-agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", @@ -4120,11 +5931,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/guid-typescript": { @@ -4212,9 +6048,9 @@ } }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4224,9 +6060,9 @@ } }, "node_modules/hono": { - "version": "4.12.14", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", - "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", + "version": "4.12.30", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", + "integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -4239,6 +6075,13 @@ "dev": true, "license": "ISC" }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -4259,31 +6102,85 @@ "url": "https://opencollective.com/express" } }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 14" } }, - "node_modules/index-array-by": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz", - "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/index-array-by": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz", + "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", "license": "MIT", "engines": { "node": ">=12" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4315,9 +6212,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -4490,6 +6387,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4711,12 +6618,43 @@ "dev": true, "license": "MIT" }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jerrypick": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz", @@ -4730,7 +6668,6 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -4751,11 +6688,33 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -4764,6 +6723,13 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -4793,7 +6759,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -4802,6 +6767,16 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/kapsule": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", @@ -4814,21 +6789,36 @@ "node": ">=12" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -4861,7 +6851,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -4882,7 +6871,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -4903,7 +6891,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -4924,7 +6911,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -4945,7 +6931,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -4966,7 +6951,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -4987,7 +6971,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5008,7 +6991,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5029,7 +7011,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5050,7 +7031,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5071,7 +7051,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5101,6 +7080,13 @@ "node": ">=4" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash-es": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", @@ -5125,18 +7111,20 @@ "loose-envify": "cli.js" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -5146,7 +7134,6 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -5215,6 +7202,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -5240,6 +7240,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5253,6 +7263,53 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5263,7 +7320,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -5294,13 +7350,112 @@ "dev": true, "license": "MIT" }, - "node_modules/node-releases": { - "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "node_modules/node-abi": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.31.0.tgz", + "integrity": "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true, "license": "MIT" }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -5324,6 +7479,19 @@ "semver": "bin/semver" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-run-all": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", @@ -5542,6 +7710,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -5645,23 +7827,49 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true, "license": "MIT", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", "engines": { @@ -5714,6 +7922,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5754,34 +7972,37 @@ } }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.16" + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" } }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -5822,12 +8043,58 @@ "node": ">=16.20.0" } }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/platform": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", "license": "MIT" }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5842,7 +8109,6 @@ "version": "8.5.10", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -5867,6 +8133,36 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/preact": { "version": "10.29.1", "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", @@ -5892,11 +8188,51 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, "license": "MIT", "dependencies": { "kleur": "^3.0.3", @@ -5917,25 +8253,36 @@ "react-is": "^16.13.1" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "node_modules/protobufjs": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", - "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -5954,6 +8301,17 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/pusher-js": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.5.0.tgz", @@ -5963,13 +8321,34 @@ "tweetnacl": "^1.0.3" } }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -5978,6 +8357,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -6015,7 +8407,6 @@ "version": "19.2.5", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", - "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -6066,12 +8457,24 @@ "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -6087,6 +8490,29 @@ "node": ">=4" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6131,6 +8557,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -6140,6 +8576,24 @@ "node": ">=0.10.0" } }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -6162,16 +8616,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -6193,7 +8691,6 @@ "version": "4.60.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -6270,6 +8767,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -6311,11 +8815,30 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "dev": true, "license": "MIT" }, "node_modules/semver": { @@ -6802,9 +9325,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", "dev": true, "license": "MIT", "engines": { @@ -6815,14 +9338,14 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -6893,23 +9416,62 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, "license": "MIT" }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -6959,6 +9521,16 @@ "dev": true, "license": "MIT" }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -6969,9 +9541,9 @@ } }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -6989,6 +9561,31 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.padend": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", @@ -7067,6 +9664,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -7077,6 +9687,19 @@ "node": ">=4" } }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7106,14 +9729,12 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", - "dev": true, "license": "MIT" }, "node_modules/tapable": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7123,6 +9744,117 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar": { + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -7137,17 +9869,19 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -7160,34 +9894,34 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=14.0.0" } }, - "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": ">=14.14" } }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=14.0.0" + "dependencies": { + "tmp": "^0.2.0" } }, "node_modules/toidentifier": { @@ -7199,18 +9933,27 @@ "node": ">=0.6" } }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "devOptional": true, + "license": "0BSD" }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.27.0", @@ -7369,12 +10112,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -7384,11 +10147,62 @@ "node": ">= 0.8" } }, + "node_modules/unzipper": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", + "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "11.3.1", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/unzipper/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/unzipper/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, "funding": [ { "type": "opencollective", @@ -7415,6 +10229,20 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -7436,10 +10264,9 @@ } }, "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", - "dev": true, + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -7510,1586 +10337,567 @@ } } }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite-node/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite-node/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite-node/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite-node/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vite-node/node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "node_modules/vite/node_modules/@esbuild/linux-ia32": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ - "arm64" + "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "node_modules/vite/node_modules/@esbuild/linux-loong64": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ - "ia32" + "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ - "x64" + "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/vite/node_modules/esbuild": { + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vitest/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ - "loong64" + "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ - "mips64el" + "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ - "ppc64" + "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ - "riscv64" + "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vitest/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ - "s390x" + "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vitest/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ - "x64" + "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ - "x64" + "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "openharmony" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vitest/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vitest/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vitest/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vitest/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "node": ">=18" } }, - "node_modules/vitest/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "hasInstallScript": true, "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vitest/node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" }, "bin": { - "vite": "bin/vite.js" + "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, "@types/node": { "optional": true }, - "less": { + "@vitest/browser-playwright": { "optional": true }, - "lightningcss": { + "@vitest/browser-preview": { "optional": true }, - "sass": { + "@vitest/browser-webdriverio": { "optional": true }, - "sass-embedded": { + "@vitest/coverage-istanbul": { "optional": true }, - "stylus": { + "@vitest/coverage-v8": { "optional": true }, - "sugarss": { + "@vitest/ui": { "optional": true }, - "terser": { + "happy-dom": { + "optional": true + }, + "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, + "node_modules/webcrypto-core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9211,6 +11019,24 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -9218,9 +11044,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -9238,13 +11064,74 @@ } } }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 2644b333..d7f5ccce 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,13 @@ "private": true, "description": "Text-an-agent starter: iMessage your Claude or Codex-backed agent, with sub-agents, memory, automations, and integrations.", "license": "MIT", + "main": "electron/main.cjs", "type": "module", "scripts": { "setup": "tsx scripts/setup.ts", "sendblue:sync": "node scripts/sendblue-sync.mjs", "sendblue:webhook": "node scripts/sendblue-webhook.mjs", + "sendblue:webhook:check": "node scripts/sendblue-webhook.mjs --check", "preflight": "node scripts/preflight.mjs", "dev": "node scripts/dev.mjs", "dev:parallel": "npm-run-all --parallel dev:server dev:convex dev:debug", @@ -16,6 +18,14 @@ "dev:convex": "convex dev", "dev:debug": "vite --config debug/vite.config.ts", "build:debug": "vite build --config debug/vite.config.ts", + "desktop:setup": "node scripts/desktop-setup.mjs", + "desktop:dev": "electron .", + "desktop:pack": "CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --dir", + "desktop:dist": "CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder", + "electron:setup": "npm run desktop:setup", + "electron:dev": "npm run desktop:dev", + "electron:pack": "npm run desktop:pack", + "electron:dist": "npm run desktop:dist", "codex:protocol": "codex app-server generate-ts --out server/runtimes/codex-app-server-protocol --experimental", "start": "npm run preflight && tsx server/index.ts", "deploy:convex": "convex deploy", @@ -26,17 +36,26 @@ "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.0", "@composio/claude-agent-sdk": "^0.6.10", + "@composio/client": "^0.1.0-alpha.66", "@composio/core": "^0.6.10", "@hugeicons/core-free-icons": "^1.0.0", "@hugeicons/react": "^1.0.0", "@huggingface/transformers": "^4.2.0", "@modelcontextprotocol/sdk": "^1.0.0", + "@tailwindcss/vite": "^4.0.0", + "@vitejs/plugin-react": "^4.3.0", "convex": "^1.36.1", "cors": "^2.8.5", "croner": "^9.0.0", "dotenv": "^16.4.0", "express": "^5.0.0", + "prompts": "^2.4.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", "react-force-graph-2d": "^1.27.0", + "tailwindcss": "^4.0.0", + "tsx": "^4.19.0", + "vite": "^6.0.0", "ws": "^8.18.0", "zod": "^3.23.0" }, @@ -44,7 +63,6 @@ "patchright": "^1.59.4" }, "devDependencies": { - "@tailwindcss/vite": "^4.0.0", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/node": "^22.0.0", @@ -52,16 +70,56 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@types/ws": "^8.5.12", - "@vitejs/plugin-react": "^4.3.0", + "electron": "^43.1.0", + "electron-builder": "^26.0.0", "npm-run-all": "^4.1.5", - "prompts": "^2.4.2", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "tailwindcss": "^4.0.0", - "tsx": "^4.19.0", "typescript": "^5.6.0", - "vite": "^6.0.0", - "vitest": "^2.1.9" + "vitest": "^4.1.10" + }, + "build": { + "appId": "com.raroque.boop", + "productName": "Boop", + "artifactName": "${productName}-${version}-${os}-${arch}.${ext}", + "asar": false, + "files": [ + "electron/**", + "assets/**", + "convex/**", + "debug/**", + "scripts/**", + "server/**", + ".env.example", + "package.json", + "package-lock.json", + "tsconfig.json", + "!**/.env", + "!**/.env.local", + "!**/.env.*.local", + "!**/.convex/**", + "!convex/_generated/**", + "!debug/dist/**", + "!data/**", + "!**/*.log", + "!**/.cache/**" + ], + "extraResources": [ + { + "from": "node_modules/sharp/node_modules/@img", + "to": "app/node_modules/sharp/node_modules/@img", + "filter": [ + "sharp-libvips-*/**" + ] + } + ], + "mac": { + "category": "public.app-category.productivity", + "identity": null, + "icon": "assets/boop.icns", + "target": [ + "dmg", + "zip" + ] + } }, "engines": { "node": ">=20" diff --git a/scripts/desktop-setup.mjs b/scripts/desktop-setup.mjs new file mode 100644 index 00000000..57868896 --- /dev/null +++ b/scripts/desktop-setup.mjs @@ -0,0 +1,305 @@ +#!/usr/bin/env node +import { spawn } from "node:child_process"; +import { + copyFileSync, + existsSync, + lstatSync, + mkdirSync, + readdirSync, + readlinkSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { homedir } from "node:os"; +import { delimiter, dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import readline from "node:readline/promises"; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = resolve(here, ".."); +const productName = "Boop"; + +const runtimeItems = [ + ".env.example", + "assets", + "convex", + "debug", + "package-lock.json", + "package.json", + "scripts", + "server", + "tsconfig.json", +]; + +const mutableRuntimeItems = [ + ".env", + ".env.local", + ".env.*.local", + ".convex", + "convex/_generated", + "data", + "debug/dist", + "dist", + "node_modules", +]; + +function runtimeRootForPlatform() { + if (process.platform === "darwin") { + return join(homedir(), "Library", "Application Support", productName, "runtime"); + } + if (process.platform === "win32") { + return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), productName, "runtime"); + } + return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), productName, "runtime"); +} + +const runtimeRoot = runtimeRootForPlatform(); + +function logStep(title) { + console.log(`\n==> ${title}`); +} + +function commandEnv() { + const pathKey = process.platform === "win32" ? "Path" : "PATH"; + const currentPath = process.env[pathKey] ?? process.env.PATH ?? ""; + const pathParts = [ + join(root, "node_modules", ".bin"), + currentPath, + ]; + if (process.platform === "darwin") { + pathParts.push( + join(homedir(), ".local", "bin"), + join(homedir(), ".npm-global", "bin"), + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "/bin", + ); + } + pathParts.push(dirname(process.execPath)); + return { + ...process.env, + [pathKey]: pathParts.filter(Boolean).join(delimiter), + }; +} + +function run(cmd, args, options = {}) { + return new Promise((resolvePromise, reject) => { + const child = spawn(cmd, args, { + cwd: options.cwd ?? root, + env: options.env ?? commandEnv(), + stdio: "inherit", + shell: process.platform === "win32", + }); + child.on("exit", (code) => { + if (code === 0) { + resolvePromise(); + } else { + reject(new Error(`${cmd} ${args.join(" ")} exited with code ${code}`)); + } + }); + child.on("error", reject); + }); +} + +function pathMatchesGlob(relativePath, pattern) { + if (!pattern.includes("*")) return false; + const escaped = pattern + .split("*") + .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) + .join(".*"); + return new RegExp(`^${escaped}$`).test(relativePath); +} + +function isMutableRuntimePath(relativePath) { + return mutableRuntimeItems.some( + (item) => + relativePath === item || + relativePath.startsWith(`${item}/`) || + pathMatchesGlob(relativePath, item), + ); +} + +function lstatIfExists(value) { + try { + return lstatSync(value); + } catch (error) { + if (error?.code === "ENOENT") return null; + throw error; + } +} + +function copyRuntimeItem(sourceRoot, targetRoot, relativePath) { + if (isMutableRuntimePath(relativePath)) return; + + const source = join(sourceRoot, relativePath); + const target = join(targetRoot, relativePath); + if (!existsSync(source)) return; + + const stat = lstatSync(source); + if (stat.isSymbolicLink()) return; + if (stat.isDirectory()) { + const targetStat = lstatIfExists(target); + if (targetStat && !targetStat.isDirectory()) { + rmSync(target, { force: true, recursive: true }); + } + mkdirSync(target, { recursive: true }); + const sourceEntries = new Set(readdirSync(source)); + for (const entry of readdirSync(target)) { + const childPath = join(relativePath, entry); + if (isMutableRuntimePath(childPath) || sourceEntries.has(entry)) continue; + rmSync(join(target, entry), { force: true, recursive: true }); + } + for (const entry of [...sourceEntries].sort()) { + copyRuntimeItem(sourceRoot, targetRoot, join(relativePath, entry)); + } + return; + } + + const targetStat = lstatIfExists(target); + if (targetStat && !targetStat.isFile()) { + rmSync(target, { force: true, recursive: true }); + } + mkdirSync(dirname(target), { recursive: true }); + copyFileSync(source, target); +} + +function prepareRuntimeFiles() { + mkdirSync(runtimeRoot, { recursive: true }); + for (const item of runtimeItems) { + copyRuntimeItem(root, runtimeRoot, item); + } + writeFileSync( + join(runtimeRoot, ".boop-desktop-runtime"), + `source=${root}\nupdated=${new Date().toISOString()}\n`, + ); +} + +function ensureDependencies() { + const requiredBins = [ + join(root, "node_modules", ".bin", process.platform === "win32" ? "tsx.cmd" : "tsx"), + join(root, "node_modules", ".bin", process.platform === "win32" ? "electron-builder.cmd" : "electron-builder"), + ]; + if (requiredBins.every((bin) => existsSync(bin))) return Promise.resolve(); + + logStep("Installing project dependencies"); + return run("npm", ["install"]); +} + +function ensureTemporaryNodeModulesLink() { + const source = join(root, "node_modules"); + const target = join(runtimeRoot, "node_modules"); + if (!existsSync(source)) { + throw new Error("node_modules is missing after npm install"); + } + + let targetExists = true; + try { + lstatSync(target); + } catch { + targetExists = false; + } + + if (!targetExists) { + symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir"); + return true; + } + + try { + if (lstatSync(target).isSymbolicLink()) { + const current = readlinkSync(target); + if (resolve(dirname(target), current) === source) return false; + rmSync(target, { force: true, recursive: true }); + symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir"); + return true; + } + } catch { + rmSync(target, { force: true, recursive: true }); + symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir"); + return true; + } + + return false; +} + +function removeTemporaryNodeModulesLink(created) { + if (!created) return; + rmSync(join(runtimeRoot, "node_modules"), { force: true, recursive: true }); +} + +async function runRuntimeSetup() { + const tsx = join( + root, + "node_modules", + ".bin", + process.platform === "win32" ? "tsx.cmd" : "tsx", + ); + const createdLink = ensureTemporaryNodeModulesLink(); + try { + await run(tsx, ["scripts/setup.ts"], { cwd: runtimeRoot }); + } finally { + removeTemporaryNodeModulesLink(createdLink); + } +} + +async function copyToApplicationsIfWanted(appPath) { + if (process.platform !== "darwin" || !existsSync(appPath)) return; + + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + try { + const answer = await rl.question("\nCopy Boop.app to /Applications now? [Y/n] "); + if (answer.trim().toLowerCase().startsWith("n")) return; + } finally { + rl.close(); + } + + const destination = "/Applications/Boop.app"; + rmSync(destination, { force: true, recursive: true }); + await run("ditto", [appPath, destination], { cwd: root }); + console.log(`\nInstalled: ${destination}`); +} + +async function main() { + console.log(` +Boop desktop setup + +This command prepares the desktop app runtime folder, runs Boop's existing setup +there, then builds the desktop app. Secrets stay in: + ${runtimeRoot} + +They are not copied into the app bundle. +`); + + await ensureDependencies(); + + logStep("Preparing desktop runtime files"); + prepareRuntimeFiles(); + console.log(`Runtime folder: ${runtimeRoot}`); + + logStep("Running Boop setup in the desktop runtime"); + await runRuntimeSetup(); + + logStep("Building the desktop app"); + await run("npm", ["run", "desktop:pack"]); + + const appPath = + process.platform === "darwin" + ? join(root, "dist", process.arch === "arm64" ? "mac-arm64" : "mac", "Boop.app") + : join(root, "dist"); + await copyToApplicationsIfWanted(appPath); + + console.log(` +Done. + +Runtime: ${runtimeRoot} +App: ${appPath} + +Launch Boop from the app bundle, then keep it in the Dock if you want it handy. +`); +} + +main().catch((err) => { + console.error(`\nDesktop setup failed: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); +}); diff --git a/scripts/dev.mjs b/scripts/dev.mjs index e7088d4a..c44d9fb6 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -72,6 +72,28 @@ function hasBinary(name) { }); } +const nodeCmd = process.env.BOOP_NODE_CMD || "node"; + +const packageBinPaths = { + convex: ["convex", "bin", "main.js"], + tsx: ["tsx", "dist", "cli.mjs"], + vite: ["vite", "bin", "vite.js"], +}; + +function localBin(name) { + const ext = process.platform === "win32" ? ".cmd" : ""; + const binPath = resolve(root, "node_modules", ".bin", `${name}${ext}`); + if (existsSync(binPath)) return { cmd: binPath, args: [] }; + + const packageBin = packageBinPaths[name]; + if (packageBin) { + const scriptPath = resolve(root, "node_modules", ...packageBin); + if (existsSync(scriptPath)) return { cmd: nodeCmd, args: [scriptPath] }; + } + + return { cmd: name, args: [] }; +} + // --- color-prefixed child runner ---------------------------------------- const C = { server: "\x1b[36m", @@ -84,6 +106,10 @@ const C = { reset: "\x1b[0m", }; let dashboardUrl = "http://localhost:5173"; +let resolveNgrokOutputUrl; +const ngrokOutputUrlReady = new Promise((resolve) => { + resolveNgrokOutputUrl = resolve; +}); // Vite's http-proxy attaches its own socket error logger that can't be removed // via configure(). EPIPE on WS reconnects is harmless — filter it at the @@ -120,6 +146,12 @@ function run(name, cmd, args, readyPattern) { const localMatch = plain.match(/Local:\s+(http:\/\/\S+)/); if (localMatch) dashboardUrl = localMatch[1].replace(/\/$/, ""); } + if (name === "ngrok") { + const urlMatch = + plain.match(/\burl=(https:\/\/\S+)/) || + plain.match(/Forwarding\s+(https:\/\/\S+)/); + if (urlMatch) resolveNgrokOutputUrl(urlMatch[1].replace(/\/$/, "")); + } if (NOISE_TRIGGERS.some((r) => r.test(plain))) { suppressing = true; @@ -141,25 +173,34 @@ function run(name, cmd, args, readyPattern) { } // --- ngrok URL banner: poll local API after launch ---------------------- +function sleep(ms) { + return new Promise((r) => setTimeout(r, ms)); +} + +async function readNgrokUrl() { + try { + const res = await fetch("http://127.0.0.1:4040/api/tunnels"); + if (res.ok) { + const data = await res.json(); + return data.tunnels?.find((t) => t.proto === "https")?.public_url ?? null; + } + } catch { + /* not up yet */ + } + return null; +} + async function waitForNgrokUrl(timeoutMs = 15000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { - try { - const res = await fetch("http://127.0.0.1:4040/api/tunnels"); - if (res.ok) { - const data = await res.json(); - const https = data.tunnels?.find((t) => t.proto === "https")?.public_url; - if (https) return https; - } - } catch { - /* not up yet */ - } - await new Promise((r) => setTimeout(r, 500)); + const https = await readNgrokUrl(); + if (https) return https; + await sleep(500); } return null; } -function showBanner(url, stable) { +function showBanner(url, stable, webhookSyncState) { const line = "═".repeat(68); const webhook = `${url}/sendblue/webhook`; const from = envVars.SENDBLUE_FROM_NUMBER; @@ -169,15 +210,14 @@ function showBanner(url, stable) { const headline = stable ? `your STABLE public URL is live.` - : `ngrok tunnel is live (webhook auto-registered with Sendblue).`; - const footer = stable - ? `` - : `\n${C.dim} ℹ The inbound webhook above was registered with Sendblue automatically. - Set SENDBLUE_AUTO_WEBHOOK=false in .env.local to disable, or pick a - stable URL (ngrok paid / Cloudflare Tunnel) via \`npm run setup\`.${C.reset}\n`; - const guide = stable - ? `\n → First time? Sendblue dashboard → API Settings → Webhook\n Configuration → add ${webhook} as INBOUND MESSAGE.\n` - : ``; + : `ngrok tunnel is live.`; + const footerMessage = + webhookSyncState === "synchronized" + ? "The inbound webhook above was synchronized with Sendblue automatically." + : webhookSyncState === "disabled" + ? "Automatic Sendblue webhook sync is disabled. Run npm run sendblue:webhook -- after changing the public URL." + : "Sendblue webhook sync failed. Check the webhook log above, then run npm run sendblue:webhook:check."; + const footer = `\n${C.dim} ℹ ${footerMessage}${C.reset}\n`; console.log(` ${C.banner}${line} @@ -186,7 +226,7 @@ ${C.banner}${line} 🐶 Debug dashboard (click me): ${dashboardUrl} 🌐 Public URL: ${url} 📮 Sendblue webhook (inbound): ${webhook} -${fromLine}${guide} +${fromLine} ${line}${C.reset}${footer}`); } @@ -214,27 +254,30 @@ console.log(`\nBoop dev starting on port ${port}. Ctrl-C to stop everything.\n`) // won't collide with startup logs. Silent on the happy path. Not added to // the `children` array because it exits on its own — we don't want its // non-zero exit (which shouldn't happen but hedge anyway) to tear down dev. -run("upstream", "node", ["scripts/check-upstream.mjs"]); +run("upstream", nodeCmd, ["scripts/check-upstream.mjs"]); +const tsxBin = localBin("tsx"); const serverChild = run( "server", - "npx", - ["tsx", "watch", "server/index.ts"], + tsxBin.cmd, + [...tsxBin.args, "watch", "server/index.ts"], /listening on :/, ); convexEnvFile = writeConvexDevEnvFile(); const convexArgs = ["convex", "dev"]; if (convexEnvFile) convexArgs.push("--env-file", convexEnvFile); +const convexBin = localBin("convex"); const convexChild = run( "convex", - "npx", - convexArgs, + convexBin.cmd, + [...convexBin.args, ...convexArgs.slice(1)], /Convex functions ready/, ); +const viteBin = localBin("vite"); const debugChild = run( "debug", - "npx", - ["vite", "--config", "debug/vite.config.ts"], + viteBin.cmd, + [...viteBin.args, "--config", "debug/vite.config.ts"], /Local:\s+http/, ); const children = [serverChild, convexChild, debugChild]; @@ -246,16 +289,19 @@ if (useNgrok && ngrokInstalled) { : ["http", port, "--log=stdout", "--log-format=term", "--log-level=info"]; const ngrokChild = run("ngrok", "ngrok", args); children.push(ngrokChild); - ngrokUrlReady = waitForNgrokUrl().catch(() => null); + ngrokUrlReady = Promise.race([ + ngrokOutputUrlReady, + new Promise((resolve) => setTimeout(() => resolve(null), 10000)), + ]).then((url) => url ?? waitForNgrokUrl().catch(() => null)); } // Wait for all the core services to be ready before printing the banner, // so the URL isn't dangled in front of the user while Convex is still booting. async function autoRegisterWebhook(publicUrl) { - if (envVars.SENDBLUE_AUTO_WEBHOOK === "false") return; + if (envVars.SENDBLUE_AUTO_WEBHOOK === "false") return "disabled"; const webhookUrl = `${publicUrl}/sendblue/webhook`; const prefix = `${C.ngrok}webhook${C.reset} │ `; - const child = spawn("node", ["scripts/sendblue-webhook.mjs", webhookUrl], { + const child = spawn(nodeCmd, ["scripts/sendblue-webhook.mjs", webhookUrl], { cwd: root, env: { ...process.env }, }); @@ -269,14 +315,40 @@ async function autoRegisterWebhook(publicUrl) { if (line.trim()) process.stdout.write(prefix + line + "\n"); } }); - await new Promise((r) => child.on("exit", r)); + const code = await new Promise((resolve) => child.on("exit", resolve)); + return code === 0 ? "synchronized" : "failed"; +} + +let sendblueWebhookRegistrationUrl = ""; +let sendblueWebhookRegistration = Promise.resolve(); +function registerSendblueWebhookOnce(publicUrl) { + if (sendblueWebhookRegistrationUrl === publicUrl) return sendblueWebhookRegistration; + sendblueWebhookRegistrationUrl = publicUrl; + sendblueWebhookRegistration = sendblueWebhookRegistration.then(() => + autoRegisterWebhook(publicUrl), + ); + return sendblueWebhookRegistration; +} + +async function registerSendblueWhenTunnelAppears() { + await sleep(2500); + const start = Date.now(); + while (Date.now() - start < 60000) { + const publicUrl = await readNgrokUrl(); + if (publicUrl) { + await registerSendblueWebhookOnce(publicUrl); + return; + } + await sleep(1000); + } } async function autoRegisterComposioWebhook(publicUrl) { if (envVars.COMPOSIO_AUTO_WEBHOOK === "false") return; if (!envVars.COMPOSIO_API_KEY) return; const prefix = `${C.ngrok}composio${C.reset} │ `; - const child = spawn("npx", ["tsx", "scripts/composio-webhook.ts", publicUrl], { + const tsxBin = localBin("tsx"); + const child = spawn(tsxBin.cmd, [...tsxBin.args, "scripts/composio-webhook.ts", publicUrl], { cwd: root, env: { ...process.env }, }); @@ -293,6 +365,10 @@ async function autoRegisterComposioWebhook(publicUrl) { await new Promise((r) => child.on("exit", r)); } +if (useNgrok && ngrokInstalled && !ngrokDomain) { + registerSendblueWhenTunnelAppears().catch(() => {}); +} + Promise.all([ serverChild.ready, convexChild.ready, @@ -302,23 +378,25 @@ Promise.all([ .then(async ([, , , ngrokUrl]) => { if (useNgrok && ngrokInstalled) { if (ngrokUrl) { - // Only auto-register for ephemeral ngrok URLs. Reserved domains and - // static URLs are already fixed in the Sendblue dashboard. - if (!ngrokDomain) { - await autoRegisterWebhook(ngrokUrl); - } + // Synchronize both the URL and signing secret. This is required even + // for a reserved domain because older dashboard-created webhooks may + // not have Boop's signing secret yet. + const webhookSyncState = await registerSendblueWebhookOnce( + (await readNgrokUrl()) ?? ngrokUrl, + ); // Composio webhook subscription is fully programmatic (PATCHable), // so we can refresh it on every restart regardless of whether the // domain is reserved. await autoRegisterComposioWebhook(ngrokUrl); - showBanner(ngrokUrl, Boolean(ngrokDomain)); + showBanner(ngrokUrl, Boolean(ngrokDomain), webhookSyncState); } else { console.log( `${C.ngrok}ngrok${C.reset} │ could not read tunnel URL from http://127.0.0.1:4040 — check ngrok output above.`, ); } } else if (hasStaticUrl) { - showBanner(publicUrl, true); + const webhookSyncState = await registerSendblueWebhookOnce(publicUrl); + showBanner(publicUrl, true, webhookSyncState); } else { const line = "═".repeat(68); console.log(` diff --git a/scripts/sendblue-sync.mjs b/scripts/sendblue-sync.mjs index 3d68843f..33a39403 100644 --- a/scripts/sendblue-sync.mjs +++ b/scripts/sendblue-sync.mjs @@ -61,7 +61,7 @@ function parsePhones(output) { /* not JSON */ } - // The sendblue CLI formats like "+1 (305) 336-9541". + // The Sendblue CLI formats numbers like "+1 (555) 010-1234". // Process line-by-line so we don't greedily span into e.g. "1 line total". for (const rawLine of clean.split(/\r?\n/)) { const line = rawLine.trim(); diff --git a/scripts/sendblue-webhook.mjs b/scripts/sendblue-webhook.mjs index 7875a70c..0084482e 100644 --- a/scripts/sendblue-webhook.mjs +++ b/scripts/sendblue-webhook.mjs @@ -1,49 +1,182 @@ #!/usr/bin/env node -// Registers (or re-registers) the inbound message webhook with Sendblue via -// their CLI, so free-ngrok users don't have to paste into the dashboard -// every time their tunnel URL rotates. +// Registers (or re-registers) the inbound message webhook with Sendblue, so +// free-ngrok users don't have to paste into the dashboard every time their +// tunnel URL rotates. // // Usage: // node scripts/sendblue-webhook.mjs +// node scripts/sendblue-webhook.mjs --check [public-webhook-url] // // Behavior: -// 1. Runs `sendblue webhooks` to list current inbound hooks. +// 1. Uses the Sendblue API keys in .env.local (falling back to .env) to list current inbound hooks. // 2. Removes any stale *.ngrok-free.app / *.ngrok-free.dev / *.ngrok.app / trycloudflare.com // webhooks of type=receive that don't match the new URL. // 3. Adds the new URL as type=receive (unless already registered). import { spawn } from "node:child_process"; -import { existsSync, readFileSync } from "node:fs"; -import { dirname, resolve } from "node:path"; +import { createHmac } from "node:crypto"; +import { existsSync, readFileSync, realpathSync } from "node:fs"; +import { homedir } from "node:os"; +import { delimiter, dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const here = dirname(fileURLToPath(import.meta.url)); const root = resolve(here, ".."); -const envPath = resolve(root, ".env.local"); +const envPaths = [resolve(root, ".env"), resolve(root, ".env.local")]; +const API_BASE = "https://api.sendblue.com"; +const WEBHOOK_SECRET_CONTEXT = "boop-sendblue-webhook-v1"; -function readEnv() { - if (!existsSync(envPath)) return {}; +export function deriveWebhookSecret(apiSecret) { + return createHmac("sha256", apiSecret).update(WEBHOOK_SECRET_CONTEXT).digest("hex"); +} + +export function parseEnvText(content) { const env = {}; - for (const line of readFileSync(envPath, "utf8").split("\n")) { - const m = line.match(/^([A-Z0-9_]+)=(.*?)(?:\s+#.*)?$/); - if (m) env[m[1]] = m[2].trim(); + for (const raw of content.split(/\r?\n/)) { + const line = raw.trim().replace(/^export\s+/, ""); + if (!line || line.startsWith("#")) continue; + const separator = line.indexOf("="); + if (separator === -1) continue; + + const key = line.slice(0, separator).trim(); + if (!/^[A-Z0-9_]+$/.test(key)) continue; + let value = line.slice(separator + 1).trim(); + const quote = value[0]; + if (quote === '"' || quote === "'") { + const closingQuote = value.lastIndexOf(quote); + value = closingQuote > 0 ? value.slice(1, closingQuote) : value.slice(1); + } else { + value = value.replace(/\s+#.*$/, "").trim(); + } + env[key] = value; } return env; } -function hasBinary(name) { - return new Promise((ok) => { - const p = spawn(process.platform === "win32" ? "where" : "which", [name], { - stdio: "ignore", - }); - p.on("exit", (code) => ok(code === 0)); - p.on("error", () => ok(false)); - }); +export function readEnvFiles(paths = envPaths, baseEnv = process.env) { + const fileEnv = {}; + for (const path of paths) { + if (!existsSync(path)) continue; + Object.assign(fileEnv, parseEnvText(readFileSync(path, "utf8"))); + } + return { ...fileEnv, ...baseEnv }; +} + +function executableNames(name) { + if (process.platform !== "win32") return [name]; + if (/\.(cmd|exe|bat)$/i.test(name)) return [name]; + return [`${name}.cmd`, `${name}.exe`, `${name}.bat`, name]; +} + +function commandSearchDirs() { + const dirs = [ + resolve(root, "node_modules", ".bin"), + ...((process.env.PATH ?? "").split(delimiter).filter(Boolean)), + ]; + if (process.platform === "darwin") { + dirs.push( + resolve(homedir(), ".local", "bin"), + resolve(homedir(), ".npm-global", "bin"), + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "/bin", + ); + } + dirs.push(dirname(process.execPath)); + return [...new Set(dirs)]; +} + +function resolveCommand(name) { + if (name.includes("/") || name.includes("\\")) { + return existsSync(name) ? name : null; + } + for (const dir of commandSearchDirs()) { + for (const executable of executableNames(name)) { + const candidate = resolve(dir, executable); + if (existsSync(candidate)) return candidate; + } + } + return null; +} + +function commandEnv() { + const pathKey = process.platform === "win32" ? "Path" : "PATH"; + const currentPath = process.env[pathKey] ?? process.env.PATH ?? ""; + return { + ...process.env, + [pathKey]: [...commandSearchDirs(), currentPath].filter(Boolean).join(delimiter), + }; +} + +function spawnableNodeCommand() { + const candidates = [ + process.env.BOOP_NODE_CMD, + resolveCommand("node"), + ...(process.platform === "darwin" ? ["/opt/homebrew/bin/node", "/usr/local/bin/node"] : []), + "node", + ].filter(Boolean); + return candidates.find((candidate) => candidate === "node" || existsSync(candidate)) ?? "node"; +} + +function nodeScriptCommand(scriptPath, leading) { + const node = spawnableNodeCommand(); + return { cmd: node, leading: [scriptPath, ...leading] }; +} + +function npmExecCommand(packageName, binName) { + const npm = resolveCommand("npm"); + if (npm) { + try { + const npmCli = realpathSync(npm); + if (npmCli.endsWith(".js")) { + return nodeScriptCommand(npmCli, [ + "exec", + "--yes", + "--package", + packageName, + "--", + binName, + ]); + } + } catch { + /* fall through to spawning npm directly */ + } + return { + cmd: npm, + leading: ["exec", "--yes", "--package", packageName, "--", binName], + }; + } + + const npx = resolveCommand("npx"); + if (npx) { + try { + const npxCli = realpathSync(npx); + if (npxCli.endsWith(".js")) { + return nodeScriptCommand(npxCli, ["-y", packageName]); + } + } catch { + /* fall through to spawning npx directly */ + } + return { cmd: npx, leading: ["-y", packageName] }; + } + + return null; +} + +function sendblueInvoker() { + const sendblue = resolveCommand("sendblue"); + if (sendblue) return { cmd: sendblue, leading: [] }; + + const npmExec = npmExecCommand("@sendblue/cli", "sendblue"); + if (npmExec) return npmExec; + + throw new Error("Could not find sendblue, npx, or npm on PATH."); } function runCapture(cmd, args) { return new Promise((ok, fail) => { - const p = spawn(cmd, args, { cwd: root }); + const p = spawn(cmd, args, { cwd: root, env: commandEnv() }); let out = ""; p.stdout.on("data", (d) => (out += d.toString())); p.stderr.on("data", () => {}); @@ -71,39 +204,227 @@ function parseWebhookLines(output) { const STALE_DOMAIN_RE = /(ngrok-free\.(app|dev)|ngrok\.app|trycloudflare\.com|loca\.lt)/; -async function main() { - const url = process.argv[2]; - if (!url || !/^https?:\/\//.test(url)) { - console.error("Usage: node scripts/sendblue-webhook.mjs "); - process.exit(1); +function normalizeWebhookUrl(value) { + const trimmed = value.replace(/\/$/, ""); + return trimmed.endsWith("/sendblue/webhook") ? trimmed : `${trimmed}/sendblue/webhook`; +} + +async function readActiveTunnelUrl() { + try { + const response = await fetch("http://127.0.0.1:4040/api/tunnels", { + signal: AbortSignal.timeout(3000), + }); + if (!response.ok) return null; + const data = await response.json(); + return data.tunnels?.find((t) => t.proto === "https")?.public_url ?? null; + } catch { + return null; } +} - const env = readEnv(); - if (!env.SENDBLUE_API_KEY || !env.SENDBLUE_API_SECRET) { - console.log("[webhook] skipping — SENDBLUE_API_KEY/SECRET not set in .env.local"); - return; +async function expectedWebhookUrl(env, explicitUrl) { + if (explicitUrl) return normalizeWebhookUrl(explicitUrl); + + const tunnelUrl = await readActiveTunnelUrl(); + if (tunnelUrl) return normalizeWebhookUrl(tunnelUrl); + + const publicUrl = env.PUBLIC_URL || ""; + if (publicUrl && !publicUrl.includes("localhost") && !publicUrl.includes("127.0.0.1")) { + return normalizeWebhookUrl(publicUrl); } - const useGlobal = await hasBinary("sendblue"); - const cmd = useGlobal ? "sendblue" : "npx"; - const leading = useGlobal ? [] : ["-y", "@sendblue/cli"]; + return null; +} - let listing; - try { - listing = await runCapture(cmd, [...leading, "webhooks", "list"]); - } catch (err) { - console.error(`[webhook] couldn't list webhooks (${err.message}). Make sure you've logged in with \`npx @sendblue/cli login\`.`); - return; +function sendblueHeaders(env, json = false) { + return { + ...(json ? { "Content-Type": "application/json" } : {}), + "sb-api-key-id": env.SENDBLUE_API_KEY, + "sb-api-secret-key": env.SENDBLUE_API_SECRET, + }; +} + +async function sendblueJson(env, pathname, options = {}) { + const response = await fetch(`${API_BASE}${pathname}`, { + ...options, + headers: { + ...sendblueHeaders(env, Boolean(options.body)), + ...(options.headers ?? {}), + }, + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + throw new Error(body.error || body.message || `Sendblue API ${response.status}`); + } + return response.json().catch(() => ({})); +} + +export function parseApiWebhookListing(webhooks) { + const hooks = []; + for (const [type, entries] of Object.entries(webhooks || {})) { + if (!Array.isArray(entries)) continue; + for (const entry of entries) { + const url = typeof entry === "string" ? entry : entry?.url; + if (typeof url === "string") hooks.push({ url, type }); + } + } + return { + current: hooks, + globalSecret: typeof webhooks?.globalSecret === "string" ? webhooks.globalSecret : "", + }; +} + +async function apiListWebhooks(env) { + const raw = await sendblueJson(env, "/api/account/webhooks"); + return parseApiWebhookListing(raw.webhooks); +} + +async function apiAddWebhook(env, url) { + const secret = deriveWebhookSecret(env.SENDBLUE_API_SECRET); + await sendblueJson(env, "/api/account/webhooks", { + method: "POST", + body: JSON.stringify({ + webhooks: [{ url, secret }], + globalSecret: secret, + type: "receive", + }), + }); +} + +async function apiRemoveWebhook(env, url) { + await sendblueJson(env, "/api/account/webhooks", { + method: "DELETE", + body: JSON.stringify({ webhooks: [url], type: "receive" }), + }); +} + +export async function resynchronizeWebhookSecret( + url, + listing, + expectedSecret, + removeWebhook, + addWebhook, +) { + const isRegistered = listing.current.some( + (hook) => hook.type === "receive" && hook.url === url, + ); + if (!isRegistered || listing.globalSecret === expectedSecret) return false; + + // Sendblue POST appends, so replace the matching URL instead of duplicating it. + await removeWebhook(url); + await addWebhook(url); + return true; +} + +async function cliListWebhooks() { + const { cmd, leading } = sendblueInvoker(); + const listing = await runCapture(cmd, [...leading, "webhooks", "list"]); + return { + current: parseWebhookLines(listing), + source: "cli", + cmd, + leading, + }; +} + +async function listWebhooks(env) { + if (env.SENDBLUE_API_KEY && env.SENDBLUE_API_SECRET) { + try { + const listing = await apiListWebhooks(env); + return { + ...listing, + source: "api", + signingReady: + listing.globalSecret === deriveWebhookSecret(env.SENDBLUE_API_SECRET), + }; + } catch (err) { + const cli = await cliListWebhooks(); + return { + ...cli, + signingReady: false, + warning: `API list failed (${err.message}); used Sendblue CLI instead.`, + }; + } + } + + const cli = await cliListWebhooks(); + return { + ...cli, + signingReady: false, + warning: "SENDBLUE_API_KEY/SECRET are not set; used Sendblue CLI instead.", + }; +} + +export function webhookCheck(url, current, source, signingReady, warning = "") { + const receive = current.filter((wh) => wh.type === "receive"); + const currentMatches = receive.filter((wh) => wh.url === url); + const staleReceiveWebhooks = receive + .map((wh) => wh.url) + .filter((hookUrl) => hookUrl !== url && STALE_DOMAIN_RE.test(hookUrl)); + const otherReceiveWebhooks = receive + .map((wh) => wh.url) + .filter((hookUrl) => hookUrl !== url && !STALE_DOMAIN_RE.test(hookUrl)); + const state = currentMatches.length > 0 && signingReady + ? "registered" + : currentMatches.length > 0 || staleReceiveWebhooks.length > 0 + ? "mismatch" + : "missing"; + const detailParts = []; + if (currentMatches.length === 0) { + detailParts.push("active tunnel is not registered with Sendblue"); + } else if (currentMatches.length > 1) { + detailParts.push(`active tunnel is registered ${currentMatches.length} times`); + } else { + detailParts.push("active tunnel is registered with Sendblue"); + } + if (!signingReady) { + detailParts.push("webhook signing secret is not synchronized"); + } + if (staleReceiveWebhooks.length) { + detailParts.push(`${staleReceiveWebhooks.length} stale tunnel hook(s) still registered`); + } + if (warning) detailParts.push(warning); + + return { + ok: currentMatches.length > 0 && signingReady, + state, + source, + checkedAt: new Date().toISOString(), + expectedWebhookUrl: url, + registeredWebhookUrl: currentMatches[0]?.url || staleReceiveWebhooks[0] || "", + currentRegistered: currentMatches.length > 0, + currentCount: currentMatches.length, + receiveWebhookCount: receive.length, + staleReceiveWebhooks, + otherReceiveWebhooks, + details: detailParts.join("; "), + }; +} + +function printWebhookCheck(check) { + console.log(`[webhook] expected: ${check.expectedWebhookUrl}`); + if (check.registeredWebhookUrl) { + console.log(`[webhook] registered: ${check.registeredWebhookUrl}`); + } else { + console.log("[webhook] registered: none"); } - const current = parseWebhookLines(listing); + console.log(`[webhook] status: ${check.state}`); + console.log(`[webhook] details: ${check.details}`); +} + +export async function syncWebhooks(url, current, removeWebhook, addWebhook) { + const hasTarget = current.some((wh) => wh.type === "receive" && wh.url === url); // 1. Remove stale ngrok/tunnel URLs so we don't accumulate zombie hooks. + // Keep duplicate copies of the current URL. Sendblue's delete endpoint is + // URL-based and may remove every matching row, which would turn a harmless + // duplicate into a missing webhook during app restart. for (const wh of current) { if (wh.type !== "receive") continue; if (wh.url === url) continue; if (!STALE_DOMAIN_RE.test(wh.url)) continue; try { - await runCapture(cmd, [...leading, "webhooks", "remove", wh.url, "--type", "receive"]); + await removeWebhook(wh.url); console.log(`[webhook] removed stale ${wh.url}`); } catch (err) { console.warn(`[webhook] could not remove ${wh.url}: ${err.message}`); @@ -111,21 +432,130 @@ async function main() { } // 2. Skip if already registered. - if (current.some((w) => w.url === url && w.type === "receive")) { + if (hasTarget) { console.log(`[webhook] already registered: ${url}`); return; } // 3. Register new. try { - await runCapture(cmd, [...leading, "webhooks", "add", url, "--type", "receive"]); + await addWebhook(url); console.log(`[webhook] registered ${url} (type=receive)`); } catch (err) { console.error(`[webhook] failed to register ${url}: ${err.message}`); } } -main().catch((err) => { - console.error(err); - process.exit(1); -}); +async function main() { + const args = process.argv.slice(2); + const checkOnly = args.includes("--check"); + const json = args.includes("--json"); + const urlArg = args.find((arg) => !arg.startsWith("--")); + const env = readEnvFiles(); + const url = checkOnly ? await expectedWebhookUrl(env, urlArg) : urlArg; + + if (checkOnly && !url) { + const result = { + ok: false, + state: "no-tunnel", + source: "none", + checkedAt: new Date().toISOString(), + expectedWebhookUrl: "", + registeredWebhookUrl: "", + currentRegistered: false, + currentCount: 0, + receiveWebhookCount: 0, + staleReceiveWebhooks: [], + otherReceiveWebhooks: [], + details: "No active ngrok tunnel or PUBLIC_URL was found.", + }; + if (json) { + console.log(JSON.stringify(result)); + } else { + printWebhookCheck(result); + } + process.exit(2); + } + + if (!url || !/^https?:\/\//.test(url)) { + console.error("Usage: node scripts/sendblue-webhook.mjs "); + console.error(" or: node scripts/sendblue-webhook.mjs --check [public-webhook-url]"); + process.exit(1); + } + + const webhookUrl = normalizeWebhookUrl(url); + + if (checkOnly) { + try { + const { current, source, signingReady, warning } = await listWebhooks(env); + const result = webhookCheck(webhookUrl, current, source, signingReady, warning); + if (json) { + console.log(JSON.stringify(result)); + } else { + printWebhookCheck(result); + } + process.exit(result.ok ? 0 : 2); + } catch (err) { + const result = { + ok: false, + state: "error", + source: "none", + checkedAt: new Date().toISOString(), + expectedWebhookUrl: webhookUrl, + registeredWebhookUrl: "", + currentRegistered: false, + currentCount: 0, + receiveWebhookCount: 0, + staleReceiveWebhooks: [], + otherReceiveWebhooks: [], + details: err.message, + }; + if (json) { + console.log(JSON.stringify(result)); + } else { + printWebhookCheck(result); + } + process.exit(1); + } + } + + if (env.SENDBLUE_API_KEY && env.SENDBLUE_API_SECRET) { + try { + const listing = await apiListWebhooks(env); + const expectedSecret = deriveWebhookSecret(env.SENDBLUE_API_SECRET); + if (await resynchronizeWebhookSecret( + webhookUrl, + listing, + expectedSecret, + (hookUrl) => apiRemoveWebhook(env, hookUrl), + (hookUrl) => apiAddWebhook(env, hookUrl), + )) { + console.log("[webhook] synchronized Sendblue webhook signing secret"); + } + await syncWebhooks( + webhookUrl, + listing.current, + (hookUrl) => apiRemoveWebhook(env, hookUrl), + (hookUrl) => apiAddWebhook(env, hookUrl), + ); + return; + } catch (err) { + throw new Error(`Sendblue API registration failed: ${err.message}`); + } + } + + throw new Error( + "SENDBLUE_API_KEY and SENDBLUE_API_SECRET are required to register a signed webhook.", + ); +} + +const isDirectExecution = Boolean( + process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url), +); + +if (isDirectExecution) { + main().catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/scripts/setup.ts b/scripts/setup.ts index e7863484..8480aa6a 100644 --- a/scripts/setup.ts +++ b/scripts/setup.ts @@ -1,10 +1,12 @@ #!/usr/bin/env tsx import prompts from "prompts"; import { spawn } from "node:child_process"; -import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { resolve } from "node:path"; +import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { delimiter, dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; -const ROOT = resolve(new URL(".", import.meta.url).pathname, ".."); +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const ENV_PATH = resolve(ROOT, ".env.local"); const EXAMPLE_PATH = resolve(ROOT, ".env.example"); @@ -14,6 +16,11 @@ const DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6"; const DEFAULT_CODEX_MODEL = "gpt-5.5"; const DEFAULT_CODEX_REASONING_EFFORT = "medium"; +interface CommandSpec { + cmd: string; + leading: string[]; +} + const CLAUDE_MODEL_CHOICES = [ { title: "claude-sonnet-4-6 (recommended)", value: "claude-sonnet-4-6" }, { title: "claude-opus-4-6 (slowest, most capable)", value: "claude-opus-4-6" }, @@ -109,6 +116,118 @@ function banner(s: string) { console.log("━".repeat(60)); } +function executableNames(name: string): string[] { + if (process.platform !== "win32") return [name]; + if (/\.(cmd|exe|bat)$/i.test(name)) return [name]; + return [`${name}.cmd`, `${name}.exe`, `${name}.bat`, name]; +} + +function commandSearchDirs(): string[] { + const dirs = [ + resolve(ROOT, "node_modules", ".bin"), + ...((process.env.PATH ?? "").split(delimiter).filter(Boolean)), + ]; + if (process.platform === "darwin") { + dirs.push( + resolve(homedir(), ".local", "bin"), + resolve(homedir(), ".npm-global", "bin"), + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "/bin", + ); + } + dirs.push(dirname(process.execPath)); + return [...new Set(dirs)]; +} + +function resolveCommand(name: string): string | null { + if (name.includes("/") || name.includes("\\")) { + return existsSync(name) ? name : null; + } + for (const dir of commandSearchDirs()) { + for (const executable of executableNames(name)) { + const candidate = resolve(dir, executable); + if (existsSync(candidate)) return candidate; + } + } + return null; +} + +function spawnableNodeCommand(): string { + const candidates = [ + ...(process.platform === "darwin" ? ["/opt/homebrew/bin/node", "/usr/local/bin/node"] : []), + resolveCommand("node"), + "node", + process.execPath, + ].filter((candidate): candidate is string => Boolean(candidate)); + return candidates.find((candidate) => candidate === "node" || existsSync(candidate)) ?? "node"; +} + +function nodeScriptCommand(scriptPath: string, leading: string[]): CommandSpec { + const node = spawnableNodeCommand(); + return { cmd: node, leading: [scriptPath, ...leading] }; +} + +function npmExecCommand(packageName: string, binName: string): CommandSpec | null { + const npm = resolveCommand("npm"); + if (npm) { + try { + const npmCli = realpathSync(npm); + if (npmCli.endsWith(".js")) { + return nodeScriptCommand(npmCli, [ + "exec", + "--yes", + "--package", + packageName, + "--", + binName, + ]); + } + } catch { + /* fall through to spawning npm directly */ + } + return { + cmd: npm, + leading: ["exec", "--yes", "--package", packageName, "--", binName], + }; + } + + const npx = resolveCommand("npx"); + if (npx) { + try { + const npxCli = realpathSync(npx); + if (npxCli.endsWith(".js")) { + return nodeScriptCommand(npxCli, ["-y", packageName]); + } + } catch { + /* fall through to spawning npx directly */ + } + return { cmd: npx, leading: ["-y", packageName] }; + } + + return null; +} + +function commandEnv(): NodeJS.ProcessEnv { + const pathKey = process.platform === "win32" ? "Path" : "PATH"; + const currentPath = process.env[pathKey] ?? process.env.PATH ?? ""; + return { + ...process.env, + [pathKey]: [...commandSearchDirs(), currentPath].filter(Boolean).join(delimiter), + }; +} + +function packageCommand(binName: string, packageName = binName): CommandSpec { + const direct = resolveCommand(binName); + if (direct) return { cmd: direct, leading: [] }; + + const npmExec = npmExecCommand(packageName, binName); + if (npmExec) return npmExec; + + throw new Error(`Could not find ${binName}, npx, or npm on PATH.`); +} + async function runConvexDev(): Promise { // If CONVEX_DEPLOYMENT is already set, `convex dev` reuses that deployment. // Only pass --configure new if this is a first-time setup — otherwise re-running @@ -124,8 +243,9 @@ async function runConvexDev(): Promise { cleanConvexUrlEnv(ENV_PATH); } + const runner = packageCommand("convex", "convex"); console.log( - `\nLaunching \`npx ${args.join(" ")}\` to configure your deployment.`, + `\nLaunching \`${[runner.cmd, ...runner.leading, ...args.slice(1)].join(" ")}\` to configure your deployment.`, ); console.log("Convex will open a browser window if you're not logged in."); if (existing.CONVEX_DEPLOYMENT) { @@ -133,7 +253,11 @@ async function runConvexDev(): Promise { } await new Promise((resolvePromise, reject) => { - const child = spawn("npx", args, { stdio: "inherit", cwd: ROOT }); + const child = spawn(runner.cmd, [...runner.leading, ...args.slice(1)], { + stdio: "inherit", + cwd: ROOT, + env: commandEnv(), + }); child.on("exit", (code) => code === 0 ? resolvePromise() : reject(new Error(`convex dev exited ${code}`)), ); @@ -141,9 +265,10 @@ async function runConvexDev(): Promise { } function hasBinary(name: string): Promise { + if (resolveCommand(name)) return Promise.resolve(true); return new Promise((ok) => { const lookup = process.platform === "win32" ? "where" : "which"; - const child = spawn(lookup, [name], { stdio: "ignore" }); + const child = spawn(lookup, [name], { stdio: "ignore", env: commandEnv() }); child.on("exit", (code) => ok(code === 0)); child.on("error", () => ok(false)); }); @@ -165,7 +290,12 @@ function openInBrowser(url: string): void { function runInherit(cmd: string, args: string[]): Promise { return new Promise((ok, fail) => { - const child = spawn(cmd, args, { stdio: "inherit", cwd: ROOT }); + const resolved = cmd === "node" ? cmd : (resolveCommand(cmd) ?? cmd); + const child = spawn(resolved, args, { + stdio: "inherit", + cwd: ROOT, + env: commandEnv(), + }); child.on("exit", (code) => code === 0 ? ok() : fail(new Error(`${cmd} ${args.join(" ")} exited ${code}`)), ); @@ -173,14 +303,19 @@ function runInherit(cmd: string, args: string[]): Promise { }); } -function runCapture(cmd: string, args: string[]): Promise { +function runCapture(cmd: string, args: string[], options: { echoOutput?: boolean } = {}): Promise { return new Promise((ok, fail) => { - const child = spawn(cmd, args, { stdio: ["inherit", "pipe", "pipe"], cwd: ROOT }); + const resolved = cmd === "node" ? cmd : (resolveCommand(cmd) ?? cmd); + const child = spawn(resolved, args, { + stdio: ["inherit", "pipe", "pipe"], + cwd: ROOT, + env: commandEnv(), + }); let out = ""; child.stdout.on("data", (d) => { const s = d.toString(); out += s; - process.stdout.write(s); + if (options.echoOutput !== false) process.stdout.write(s); }); child.stderr.on("data", (d) => process.stderr.write(d)); child.on("exit", (code) => @@ -190,9 +325,14 @@ function runCapture(cmd: string, args: string[]): Promise { }); } -async function sendblueInvoker(): Promise<{ cmd: string; leading: string[] }> { - if (await hasBinary("sendblue")) return { cmd: "sendblue", leading: [] }; - return { cmd: "npx", leading: ["-y", "@sendblue/cli"] }; +async function sendblueInvoker(): Promise { + const sendblue = resolveCommand("sendblue"); + if (sendblue) return { cmd: sendblue, leading: [] }; + + const npmExec = npmExecCommand("@sendblue/cli", "sendblue"); + if (npmExec) return npmExec; + + throw new Error("Could not find sendblue, npx, or npm on PATH."); } interface SendblueKeys { @@ -258,7 +398,7 @@ function parseSendbluePhones(output: string): string[] { /* not JSON, fall through to text parsing */ } - // `sendblue lines` formats like "+1 (305) 336-9541". + // `sendblue lines` formats numbers like "+1 (555) 010-1234". for (const rawLine of clean.split(/\r?\n/)) { const line = rawLine.trim(); if (!line.startsWith("+")) continue; @@ -312,9 +452,18 @@ async function importSendblueFromCli(): Promise { banner("Sendblue CLI"); try { - await runInherit(cmd, [...leading, account === "setup" ? "setup" : "login"]); + if (account === "setup") { + await runInherit(cmd, [...leading, "setup"]); + } else { + try { + await runCapture(cmd, [...leading, "whoami"], { echoOutput: false }); + console.log("\n✓ Sendblue CLI is already logged in."); + } catch { + await runInherit(cmd, [...leading, "login"]); + } + } console.log("\nFetching your Sendblue keys…\n"); - const output = await runCapture(cmd, [...leading, "show-keys"]); + const output = await runCapture(cmd, [...leading, "show-keys"], { echoOutput: false }); const parsed = parseSendblueKeys(output); if (!parsed.apiKey || !parsed.apiSecret) { console.log( @@ -575,7 +724,7 @@ how you want to generate embeddings: • Local — free, runs in-process via @huggingface/transformers (Xenova/bge-large-en-v1.5, 1024-dim). First run downloads - ~440MB and caches forever. No API key. + ~1.3GB and caches in Boop's local data folder. No API key. • Voyage — paid, ~$0.06/M tokens. Slightly stronger English retrieval. • OpenAI — paid, ~$0.13/M tokens. Comparable to Voyage. @@ -631,13 +780,14 @@ so you can switch later by adding/removing the API key. type: "confirm", name: "preload", message: - "Pre-download the local model now? (~440MB, ~30s on broadband — saves the wait on first recall)", + "Pre-download the local model now? (~1.3GB — saves the wait on first recall)", initial: true, }); if (preload) { console.log("\nDownloading Xenova/bge-large-en-v1.5… (Ctrl+C to skip)\n"); try { - await runInherit("npx", ["tsx", "scripts/preload-embeddings.ts"]); + const tsx = packageCommand("tsx", "tsx"); + await runInherit(tsx.cmd, [...tsx.leading, "scripts/preload-embeddings.ts"]); console.log("✓ Local model cached."); } catch (err) { console.warn( @@ -691,7 +841,8 @@ the browser integration unless you enable it. if (installBrowser) { console.log("\nInstalling Patchright Chrome… (Ctrl+C to skip)\n"); try { - await runInherit("npx", ["-y", "patchright", "install", "chrome"]); + const patchright = packageCommand("patchright", "patchright"); + await runInherit(patchright.cmd, [...patchright.leading, "install", "chrome"]); console.log("✓ Patchright Chrome installed."); } catch (err) { console.warn( diff --git a/server/apple/contacts-local.ts b/server/apple/contacts-local.ts index ee3cf79e..8bebee6b 100644 --- a/server/apple/contacts-local.ts +++ b/server/apple/contacts-local.ts @@ -11,7 +11,7 @@ const SQLITE_MAX_BUFFER = 5 * 1024 * 1024; const CONTACT_MATCH_LIMIT = 50; export const LOCAL_CONTACTS_FULL_DISK_ACCESS_MESSAGE = - "Boop needs Full Disk Access for the terminal app running the server to read Contacts for iMessage contact matching. Open System Settings -> Privacy & Security -> Full Disk Access, add your terminal or Codex app, then restart npm run dev."; + "Boop needs Full Disk Access to read Contacts for iMessage contact matching. In System Settings -> Privacy & Security -> Full Disk Access, add Boop for the desktop app or your terminal for npm run dev, then restart Boop."; interface ContactCandidateRow { id: number; diff --git a/server/apple/messages-local.ts b/server/apple/messages-local.ts index 1f4704e6..579457a3 100644 --- a/server/apple/messages-local.ts +++ b/server/apple/messages-local.ts @@ -15,7 +15,7 @@ export const LOCAL_MESSAGES_UNSUPPORTED_MESSAGE = "Local iMessage reads are only available on macOS."; export const LOCAL_MESSAGES_FULL_DISK_ACCESS_MESSAGE = - "Boop needs Full Disk Access for the terminal app running the server to read Messages. Open System Settings → Privacy & Security → Full Disk Access, add your terminal or Codex app, then restart npm run dev."; + "Boop needs Full Disk Access to read Messages. In System Settings → Privacy & Security → Full Disk Access, add Boop for the desktop app or your terminal for npm run dev, then restart Boop."; export type LocalMessagesPermission = "granted" | "denied" | "notDetermined"; diff --git a/server/apple/notes-local.ts b/server/apple/notes-local.ts index 0a3fa1c5..71737ae0 100644 --- a/server/apple/notes-local.ts +++ b/server/apple/notes-local.ts @@ -12,7 +12,7 @@ export const LOCAL_NOTES_UNSUPPORTED_MESSAGE = "Local Apple Notes reads are only available on macOS."; export const LOCAL_NOTES_ACCESS_MESSAGE = - "Boop needs macOS Automation permission to read Apple Notes. When prompted, allow the app running Boop to control Notes, or open System Settings -> Privacy & Security -> Automation and enable Notes for Codex/Terminal. Access is read-only."; + "Boop needs macOS Automation permission to read Apple Notes. When prompted, allow Boop or the terminal app running npm run dev to control Notes. You can also enable it in System Settings -> Privacy & Security -> Automation. Access is read-only."; export type LocalNotesPermission = "granted" | "denied" | "notDetermined"; diff --git a/server/apple/reminders-local.ts b/server/apple/reminders-local.ts index 82459adc..719e0969 100644 --- a/server/apple/reminders-local.ts +++ b/server/apple/reminders-local.ts @@ -13,7 +13,7 @@ export const LOCAL_REMINDERS_UNSUPPORTED_MESSAGE = "Local Apple Reminders reads are only available on macOS."; export const LOCAL_REMINDERS_ACCESS_MESSAGE = - "Boop needs macOS Automation permission to read Apple Reminders. When prompted, allow the app running Boop to control Reminders, or open System Settings -> Privacy & Security -> Automation and enable Reminders for Codex/Terminal. Access is read-only."; + "Boop needs macOS Automation permission to read Apple Reminders. When prompted, allow Boop or the terminal app running npm run dev to control Reminders. You can also enable it in System Settings -> Privacy & Security -> Automation. Access is read-only."; export type LocalRemindersPermission = "granted" | "denied" | "notDetermined"; diff --git a/server/composio-routes.ts b/server/composio-routes.ts index fa6ef386..3f3b5d7b 100644 --- a/server/composio-routes.ts +++ b/server/composio-routes.ts @@ -23,8 +23,9 @@ export function createComposioRouter(): express.Router { res.json({ enabled: Boolean(getComposio()) }); }); - router.get("/toolkits", async (_req, res) => { + router.get("/toolkits", async (req, res) => { try { + const includeCatalog = req.query.catalog === "all"; const [connected, configured, meta] = await Promise.all([ listConnectedToolkits(), listToolkitSlugsWithAuthConfig(), @@ -68,9 +69,19 @@ export function createComposioRouter(): express.Router { }; }); - const extras = [...connectionsBySlug.entries()] - .filter(([slug]) => !CURATED_TOOLKITS.some((t) => t.slug === slug)) - .map(([slug, conns]) => { + const curatedSlugs = new Set(CURATED_TOOLKITS.map((t) => t.slug)); + const extraSlugs = includeCatalog + ? [...meta.keys()].filter((slug) => !curatedSlugs.has(slug)) + : [...connectionsBySlug.keys()].filter((slug) => !curatedSlugs.has(slug)); + + const extras = extraSlugs + .sort((a, b) => { + const aName = meta.get(a)?.name ?? displayNameFor(a); + const bName = meta.get(b)?.name ?? displayNameFor(b); + return aName.localeCompare(bName); + }) + .map((slug) => { + const conns = connectionsBySlug.get(slug) ?? []; const m = meta.get(slug); // Non-curated toolkit — we don't actually know its auth mode from // here. Infer: if an auth config exists on this account, the user diff --git a/server/composio.ts b/server/composio.ts index 675d46b4..f87f8fab 100644 --- a/server/composio.ts +++ b/server/composio.ts @@ -1,4 +1,5 @@ import { Composio } from "@composio/core"; +import { Composio as ComposioApiClient } from "@composio/client"; import { ClaudeAgentSDKProvider } from "@composio/claude-agent-sdk"; import { createSdkMcpServer, type McpSdkServerConfigWithInstance } from "@anthropic-ai/claude-agent-sdk"; import type { IntegrationModule } from "./integrations/registry.js"; @@ -106,30 +107,67 @@ export interface ToolSummary { description?: string; } +export interface ToolkitCatalogPage { + items: Array<{ + slug: string; + name: string; + meta?: { + logo?: string | null; + description?: string | null; + tools_count?: number; + }; + }>; + nextCursor?: string | null; +} + +export async function collectToolkitCatalog( + fetchPage: (cursor?: string) => Promise, +): Promise> { + const out = new Map(); + const seenCursors = new Set(); + let cursor: string | undefined; + + while (true) { + const page = await fetchPage(cursor); + for (const item of page.items) { + out.set(item.slug, { + slug: item.slug, + name: item.name, + logo: item.meta?.logo ?? undefined, + description: item.meta?.description ?? undefined, + toolsCount: item.meta?.tools_count, + }); + } + + const nextCursor = page.nextCursor ?? undefined; + if (!nextCursor) break; + if (seenCursors.has(nextCursor)) { + throw new Error("Composio toolkit catalog returned a repeated cursor"); + } + seenCursors.add(nextCursor); + cursor = nextCursor; + } + + return out; +} + // Composio's toolkit catalog rarely changes; cache the full list for the life of the process. +const TOOLKIT_META_PAGE_LIMIT = 1000; let toolkitMetaCache: Promise> | null = null; async function fetchAllToolkitMeta(): Promise> { const composio = getComposio(); if (!composio) return new Map(); - const out = new Map(); - const resp = await composio.toolkits.get({ limit: 500 }); - const items = Array.isArray(resp) - ? resp - : ((resp as { items?: unknown[] }).items ?? []); - for (const it of items as Array<{ - slug: string; - name: string; - meta?: { logo?: string; description?: string; toolsCount?: number }; - }>) { - out.set(it.slug, { - slug: it.slug, - name: it.name, - logo: it.meta?.logo, - description: it.meta?.description, - toolsCount: it.meta?.toolsCount, + const apiKey = process.env.COMPOSIO_API_KEY!; + const catalogClient = new ComposioApiClient({ apiKey }); + const out = await collectToolkitCatalog(async (cursor) => { + const page = await catalogClient.toolkits.list({ + limit: TOOLKIT_META_PAGE_LIMIT, + sort_by: "alphabetically", + ...(cursor ? { cursor } : {}), }); - } + return { items: page.items, nextCursor: page.next_cursor }; + }); // Backfill any curated toolkits the list endpoint omitted (e.g. MCP-only // toolkits like granola_mcp that don't appear in the paginated catalog). await Promise.all( diff --git a/server/embeddings.ts b/server/embeddings.ts index 9bd7a6c5..6f86f0a2 100644 --- a/server/embeddings.ts +++ b/server/embeddings.ts @@ -4,17 +4,26 @@ * Convex vector index stays compatible regardless of which provider runs. * * Local fallback ensures `recall()` always works — no API key required. - * First local call downloads ~440MB and caches in ~/.cache/huggingface. + * First local call downloads ~1.3GB and caches under Boop's local data folder. */ import type { FeatureExtractionPipeline } from "@huggingface/transformers"; +import { mkdir } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; const VOYAGE_MODEL = "voyage-3"; const OPENAI_MODEL = "text-embedding-3-large"; const LOCAL_MODEL = "Xenova/bge-large-en-v1.5"; const DIMENSIONS = 1024; +const LOCAL_CACHE_DIR = resolve( + dirname(fileURLToPath(import.meta.url)), + "..", + "data", + "huggingface-cache", +); -// Local pipeline is loaded lazily (model download is ~440MB) and cached +// Local pipeline is loaded lazily (model download is ~1.3GB) and cached // in-process. `loading` dedupes parallel callers during the first load. let extractor: FeatureExtractionPipeline | null = null; let loading: Promise | null = null; @@ -73,8 +82,10 @@ async function getLocalExtractor(): Promise { if (extractor) return extractor; if (loading) return loading; const attempt = (async () => { - const { pipeline } = await import("@huggingface/transformers"); - console.log(`[embeddings] loading local model ${LOCAL_MODEL} (~440MB on first run)…`); + const { env, pipeline } = await import("@huggingface/transformers"); + await mkdir(LOCAL_CACHE_DIR, { recursive: true }); + env.cacheDir = LOCAL_CACHE_DIR; + console.log(`[embeddings] loading local model ${LOCAL_MODEL} (~1.3GB on first run)…`); const start = Date.now(); const ext = await pipeline("feature-extraction", LOCAL_MODEL, { dtype: "fp32", @@ -84,7 +95,7 @@ async function getLocalExtractor(): Promise { return ext; })(); loading = attempt; - // If the load rejects (transient network failure during the 440MB + // If the load rejects (transient network failure during the model // download, etc.) we MUST clear `loading` so the next call re-attempts // instead of replaying the cached rejection forever. Detach the cleanup // from the returned promise via .catch(() => {}) so callers see the diff --git a/server/heartbeat.ts b/server/heartbeat.ts index 0645d7e2..e7b37c11 100644 --- a/server/heartbeat.ts +++ b/server/heartbeat.ts @@ -7,7 +7,10 @@ const STALE_MS = 15 * 60 * 1000; const ORPHANED_MS = 90 * 1000; export async function sweepStaleAgents(): Promise { - const runningInDb = await convex.query(api.agents.list, { status: "running", limit: 100 }); + const runningInDb = await convex.query(api.agents.list, { + status: "running", + limit: 100, + }); const now = Date.now(); const live = new Set(runningAgentIds()); diff --git a/server/index.ts b/server/index.ts index deccc761..ec27cedb 100644 --- a/server/index.ts +++ b/server/index.ts @@ -30,6 +30,7 @@ import { setRuntimeProvider, } from "./runtime-config.js"; import { startImageCleanup } from "./images/clean.js"; +import { isPublicServerRequest, isTrustedLocalRequest } from "./local-access.js"; async function main() { await loadIntegrations(); @@ -55,6 +56,13 @@ async function main() { } const app = express(); + app.use((req, res, next) => { + if (isPublicServerRequest(req) || isTrustedLocalRequest(req)) { + next(); + return; + } + res.status(404).json({ error: "not found" }); + }); app.use(cors()); // Composio webhook receiver must read raw bytes for HMAC verification, so // its body parser is mounted BEFORE the global express.json. Without this @@ -181,7 +189,11 @@ async function main() { const server = createServer(app); const wss = new WebSocketServer({ server, path: "/ws" }); - wss.on("connection", (ws) => { + wss.on("connection", (ws, request) => { + if (!isTrustedLocalRequest(request)) { + ws.close(1008, "local connections only"); + return; + } addClient(ws); ws.send(JSON.stringify({ event: "hello", data: { ok: true }, at: Date.now() })); }); diff --git a/server/interaction-agent.ts b/server/interaction-agent.ts index 6e29bd39..11bbc379 100644 --- a/server/interaction-agent.ts +++ b/server/interaction-agent.ts @@ -187,7 +187,7 @@ optional Apple bridge. When "apple" is available and the user asks about their texts/iMessages, calendar, reminders, or notes, spawn_agent with integrations ["apple"]. If it is not available, tell the user to enable Apple data in Settings. For iMessage, -the terminal or Codex app running Boop needs Full Disk Access on macOS. For +the app or process running Boop needs Full Disk Access on macOS. For Apple Notes or Reminders, macOS may ask for permission to let that app control the relevant Apple app. diff --git a/server/local-access.ts b/server/local-access.ts new file mode 100644 index 00000000..9236e47f --- /dev/null +++ b/server/local-access.ts @@ -0,0 +1,133 @@ +import type { IncomingHttpHeaders, IncomingMessage } from "node:http"; + +type RequestLike = Pick; + +function headerValues(value: string | string[] | undefined): string[] { + return Array.isArray(value) ? value : value ? [value] : []; +} + +function stripQuotes(value: string): string { + const trimmed = value.trim(); + if (trimmed.startsWith('"') && trimmed.endsWith('"')) { + return trimmed.slice(1, -1); + } + return trimmed; +} + +function normalizedAddress(value: string | undefined): string { + if (!value) return ""; + + let address = stripQuotes(value).trim().toLowerCase(); + if (address.startsWith("[")) { + const closingBracket = address.indexOf("]"); + if (closingBracket !== -1) { + address = address.slice(1, closingBracket); + } + } else if (/^\d{1,3}(?:\.\d{1,3}){3}:\d+$/.test(address)) { + address = address.slice(0, address.lastIndexOf(":")); + } + + if (address.startsWith("::ffff:")) { + address = address.slice("::ffff:".length); + } + return address; +} + +export function isLoopbackAddress(value: string | undefined): boolean { + const address = normalizedAddress(value); + return ( + address === "::1" || + address === "0:0:0:0:0:0:0:1" || + /^127(?:\.\d{1,3}){3}$/.test(address) + ); +} + +function isLocalAuthority(value: string | undefined): boolean { + if (!value) return false; + + const authority = stripQuotes(value).trim().toLowerCase(); + if (authority.includes("@")) return false; + try { + const hostname = new URL(`http://${authority}`).hostname.replace(/^\[|\]$/g, ""); + return hostname === "localhost" || hostname === "localhost." || isLoopbackAddress(hostname); + } catch { + return false; + } +} + +function allForwardedAddressesAreLoopback(headers: IncomingHttpHeaders): boolean { + const forwardedFor = headerValues(headers["x-forwarded-for"]).flatMap((value) => + value.split(","), + ); + const singleAddressHeaders = [ + ...headerValues(headers["x-real-ip"]), + ...headerValues(headers["cf-connecting-ip"]), + ...headerValues(headers["true-client-ip"]), + ]; + + return [...forwardedFor, ...singleAddressHeaders].every((value) => + isLoopbackAddress(value.trim()), + ); +} + +function allForwardedHostsAreLocal(headers: IncomingHttpHeaders): boolean { + return headerValues(headers["x-forwarded-host"]) + .flatMap((value) => value.split(",")) + .every((value) => isLocalAuthority(value.trim())); +} + +function forwardedHeaderIsLocal(headers: IncomingHttpHeaders): boolean { + for (const value of headerValues(headers.forwarded)) { + for (const entry of value.split(",")) { + for (const parameter of entry.split(";")) { + const separator = parameter.indexOf("="); + if (separator === -1) continue; + const key = parameter.slice(0, separator).trim().toLowerCase(); + const parameterValue = parameter.slice(separator + 1).trim(); + if (key === "for" && !isLoopbackAddress(parameterValue)) return false; + if (key === "host" && !isLocalAuthority(parameterValue)) return false; + } + } + } + return true; +} + +function hasTrustedOrigin(headers: IncomingHttpHeaders): boolean { + const origins = headerValues(headers.origin); + if (origins.length === 0) return true; + + return origins.every((origin) => { + try { + return isLocalAuthority(new URL(origin).host); + } catch { + return false; + } + }); +} + +export function isTrustedLocalRequest(request: RequestLike): boolean { + return ( + isLoopbackAddress(request.socket.remoteAddress) && + isLocalAuthority(request.headers.host) && + hasTrustedOrigin(request.headers) && + allForwardedAddressesAreLoopback(request.headers) && + allForwardedHostsAreLocal(request.headers) && + forwardedHeaderIsLocal(request.headers) + ); +} + +export function isPublicServerRequest(request: RequestLike): boolean { + let pathname: string; + try { + pathname = new URL(request.url ?? "/", "http://localhost").pathname; + } catch { + return false; + } + + const normalizedPath = pathname.replace(/\/+$/, "") || "/"; + return ( + (request.method === "GET" && normalizedPath === "/health") || + (request.method === "POST" && normalizedPath === "/sendblue/webhook") || + (request.method === "POST" && normalizedPath === "/composio/webhook") + ); +} diff --git a/server/scripted-demo-replies.ts b/server/scripted-demo-replies.ts new file mode 100644 index 00000000..3c7d64c1 --- /dev/null +++ b/server/scripted-demo-replies.ts @@ -0,0 +1,162 @@ +import { api } from "../convex/_generated/api.js"; +import { convex } from "./convex-client.js"; +import { broadcast } from "./broadcast.js"; +import { currentPage, launchLocalBrowser } from "./browser/launcher.js"; +import { redactPhoneNumbers } from "./privacy.js"; + +const DEMO_MODE_SETTING_KEY = "debug_demo_mode"; +const WATER_BOTTLE_PROMPT = "what was that water bottle brand my mom texted me about"; +const LINKEDIN_LOGIN_URL = "https://www.linkedin.com/login/en-us/"; + +export function matchesWaterBottleDemoPrompt(content: string): boolean { + return normalizeDemoPrompt(content) === WATER_BOTTLE_PROMPT; +} + +export function matchesLinkedInDemoPrompt(content: string): boolean { + const prompt = normalizeDemoPrompt(content); + return ( + /^(?:(?:can|could|would) you |please )?check (?:my )?linkedin(?: messages)?(?: using (?:the )?browser)?$/.test( + prompt, + ) || + /^(?:(?:can|could|would) you |please )?use (?:the )?browser to check (?:my )?linkedin(?: messages)?$/.test( + prompt, + ) + ); +} + +function normalizeDemoPrompt(content: string): string { + return content + .trim() + .toLowerCase() + .replace(/[’‘]/g, "'") + .replace(/[“”]/g, '"') + .replace(/\s+/g, " ") + .replace(/[?.!]+$/g, ""); +} + +function randomDemoTurnId(): string { + return `demo_turn_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; +} + +function wait(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export async function openLinkedInLoginForDemo(): Promise { + await launchLocalBrowser({ forceVisible: true }); + const page = await currentPage(); + await page.goto("about:blank"); + await page.context().addCookies([ + { + name: "lang", + value: "v=2&lang=en-us", + domain: ".linkedin.com", + path: "/", + secure: true, + sameSite: "Lax", + }, + ]); + await page.goto(LINKEDIN_LOGIN_URL, { + waitUntil: "domcontentloaded", + timeout: 30_000, + }); +} + +async function demoModeEnabled(): Promise { + try { + const value = await convex.query(api.settings.get, { key: DEMO_MODE_SETTING_KEY }); + return value === "true"; + } catch (err) { + console.error("[demo-script] failed to read demo mode setting", err); + return false; + } +} + +type ScriptedDemoReplyDeps = { + sendImessage: (toNumber: string, text: string) => Promise; + sendTypingIndicator: (toNumber: string) => Promise; +}; + +type ScriptedDemoReplyOpts = { + conversationId: string; + content: string; + fromNumber: string; + turnTag: string; +}; + +export async function maybeHandleScriptedDemoReply( + opts: ScriptedDemoReplyOpts, + deps: ScriptedDemoReplyDeps, +): Promise { + const demo = matchesWaterBottleDemoPrompt(opts.content) + ? "water-bottle" + : matchesLinkedInDemoPrompt(opts.content) + ? "linkedin-login" + : null; + if (!demo) return false; + if (!(await demoModeEnabled())) return false; + + const turnId = randomDemoTurnId(); + const log = (message: string) => console.log(`[turn ${opts.turnTag}] [demo-script] ${message}`); + + await convex.mutation(api.messages.send, { + conversationId: opts.conversationId, + role: "user", + content: opts.content, + turnId, + }); + broadcast("user_message", { + conversationId: opts.conversationId, + content: opts.content, + }); + + const sendStep = async (content: string): Promise => { + const text = redactPhoneNumbers(content.trim()); + if (!text) return; + await deps.sendImessage(opts.fromNumber, text); + await convex.mutation(api.messages.send, { + conversationId: opts.conversationId, + role: "assistant", + content: text, + turnId, + }); + broadcast("assistant_message", { + conversationId: opts.conversationId, + content: text, + }); + log(`→ ${JSON.stringify(text)}`); + }; + + if (demo === "water-bottle") { + log("matched water bottle demo prompt"); + await deps.sendTypingIndicator(opts.fromNumber); + await wait(150); + await sendStep("Searching iMessage for the thread from your mom..."); + + await deps.sendTypingIndicator(opts.fromNumber); + await wait(1800); + await sendStep("It was the LARQ bottle."); + return true; + } + + log("matched LinkedIn browser demo prompt"); + await deps.sendTypingIndicator(opts.fromNumber); + await wait(250); + await sendStep("I'll go check it."); + + await deps.sendTypingIndicator(opts.fromNumber); + try { + await Promise.all([ + openLinkedInLoginForDemo(), + wait(1400), + ]); + log("opened visible LinkedIn login page"); + } catch (err) { + console.error(`[turn ${opts.turnTag}] [demo-script] failed to open LinkedIn login`, err); + } + await sendStep( + "I tried using the browser, but I need you to log in. Please log in and then, when you're done, let me know.", + ); + + return true; +} diff --git a/server/sendblue-webhook-auth.ts b/server/sendblue-webhook-auth.ts new file mode 100644 index 00000000..e9115281 --- /dev/null +++ b/server/sendblue-webhook-auth.ts @@ -0,0 +1,18 @@ +import { createHmac, timingSafeEqual } from "node:crypto"; + +const WEBHOOK_SECRET_CONTEXT = "boop-sendblue-webhook-v1"; + +export function deriveSendblueWebhookSecret(apiSecret: string): string { + return createHmac("sha256", apiSecret).update(WEBHOOK_SECRET_CONTEXT).digest("hex"); +} + +export function verifySendblueWebhookSecret( + received: string | undefined, + apiSecret = process.env.SENDBLUE_API_SECRET, +): boolean { + if (!received || !apiSecret) return false; + + const expected = Buffer.from(deriveSendblueWebhookSecret(apiSecret)); + const actual = Buffer.from(received); + return actual.length === expected.length && timingSafeEqual(actual, expected); +} diff --git a/server/sendblue.ts b/server/sendblue.ts index b3c9a800..0aaf3af5 100644 --- a/server/sendblue.ts +++ b/server/sendblue.ts @@ -5,10 +5,26 @@ import { handleUserMessage } from "./interaction-agent.js"; import { broadcast } from "./broadcast.js"; import { validateImageHeader, MAX_IMAGE_BYTES, type ImageMediaType } from "./images/mime.js"; import { redactContactHandle, redactPhoneNumbers } from "./privacy.js"; +import { maybeHandleScriptedDemoReply } from "./scripted-demo-replies.js"; +import { verifySendblueWebhookSecret } from "./sendblue-webhook-auth.js"; const API_BASE = "https://api.sendblue.com/api"; const MAX_CHUNK = 2900; +export function extractSendblueMediaUrls( + mediaUrl: unknown, + mediaUrls: unknown, +): string[] { + const urls = new Set(); + if (Array.isArray(mediaUrls)) { + for (const value of mediaUrls) { + if (typeof value === "string" && value.trim()) urls.add(value.trim()); + } + } + if (typeof mediaUrl === "string" && mediaUrl.trim()) urls.add(mediaUrl.trim()); + return [...urls]; +} + function stripMarkdown(text: string): string { return text .replace(/```[\s\S]*?```/g, (m) => m.replace(/```\w*\n?|```/g, "")) @@ -82,7 +98,9 @@ export async function sendImessage(toNumber: string, text: string): Promise ""); - console.error(`[sendblue] send failed ${res.status}: ${body}`); + console.error( + `[sendblue] send failed ${res.status}: ${redactPhoneNumbers(body).slice(0, 500)}`, + ); if (body.includes("missing required parameter") && body.includes("from_number")) { console.error( `[sendblue] → Set SENDBLUE_FROM_NUMBER in .env.local to your Sendblue-provisioned number and restart the server.`, @@ -206,16 +224,14 @@ export function createSendblueRouter(): express.Router { const router = express.Router(); router.post("/webhook", async (req, res) => { + if (!verifySendblueWebhookSecret(req.get("sb-signing-secret"))) { + res.status(401).json({ error: "invalid webhook signature" }); + return; + } + const { content, from_number, is_outbound, message_handle, media_url, media_urls } = req.body ?? {}; - const rawUrls: string[] = []; - if (Array.isArray(media_urls)) { - for (const u of media_urls) { - if (typeof u === "string" && u.length > 0) rawUrls.push(u); - } - } else if (typeof media_url === "string" && media_url.length > 0) { - rawUrls.push(media_url); - } + const rawUrls = extractSendblueMediaUrls(media_url, media_urls); if (is_outbound || !from_number || (!content && rawUrls.length === 0)) { res.json({ ok: true, skipped: true }); return; @@ -250,6 +266,20 @@ export function createSendblueRouter(): express.Router { broadcast("message_in", { conversationId, content, from_number, handle: message_handle }); res.json({ ok: true }); + if ( + await maybeHandleScriptedDemoReply( + { + conversationId, + content: textForLog, + fromNumber: from_number, + turnTag, + }, + { sendImessage, sendTypingIndicator }, + ) + ) { + return; + } + const stopTyping = startTypingLoop(from_number); try { const reply = await handleUserMessage({ diff --git a/test/composio-catalog.test.ts b/test/composio-catalog.test.ts new file mode 100644 index 00000000..3daa9175 --- /dev/null +++ b/test/composio-catalog.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it, vi } from "vitest"; +import { collectToolkitCatalog } from "../server/composio.js"; + +describe("Composio toolkit catalog pagination", () => { + it("collects every page and maps API metadata", async () => { + const fetchPage = vi.fn(async (cursor?: string) => { + if (!cursor) { + return { + items: [ + { + slug: "alpha", + name: "Alpha", + meta: { + logo: "https://example.test/alpha.png", + description: "First toolkit", + tools_count: 12, + }, + }, + ], + nextCursor: "page-2", + }; + } + return { + items: [ + { + slug: "beta", + name: "Beta", + meta: { tools_count: 7 }, + }, + ], + nextCursor: null, + }; + }); + + const catalog = await collectToolkitCatalog(fetchPage); + + expect(fetchPage).toHaveBeenNthCalledWith(1, undefined); + expect(fetchPage).toHaveBeenNthCalledWith(2, "page-2"); + expect([...catalog.keys()]).toEqual(["alpha", "beta"]); + expect(catalog.get("alpha")).toMatchObject({ + description: "First toolkit", + toolsCount: 12, + }); + expect(catalog.get("beta")?.toolsCount).toBe(7); + }); + + it("rejects a repeated cursor instead of looping forever", async () => { + const fetchPage = vi.fn(async () => ({ items: [], nextCursor: "same-page" })); + + await expect(collectToolkitCatalog(fetchPage)).rejects.toThrow("repeated cursor"); + expect(fetchPage).toHaveBeenCalledTimes(2); + }); +}); diff --git a/test/local-access.test.ts b/test/local-access.test.ts new file mode 100644 index 00000000..c6cff571 --- /dev/null +++ b/test/local-access.test.ts @@ -0,0 +1,106 @@ +import type { IncomingHttpHeaders } from "node:http"; +import { describe, expect, it } from "vitest"; +import { + isLoopbackAddress, + isPublicServerRequest, + isTrustedLocalRequest, +} from "../server/local-access.js"; + +function request({ + headers = {}, + method = "GET", + remoteAddress = "127.0.0.1", + url = "/runtime-config", +}: { + headers?: IncomingHttpHeaders; + method?: string; + remoteAddress?: string; + url?: string; +} = {}) { + return { + headers: { host: "localhost:3456", ...headers }, + method, + socket: { remoteAddress }, + url, + } as Parameters[0]; +} + +describe("local server access", () => { + it("recognizes IPv4, IPv6, and mapped loopback addresses", () => { + expect(isLoopbackAddress("127.0.0.1")).toBe(true); + expect(isLoopbackAddress("127.9.8.7")).toBe(true); + expect(isLoopbackAddress("::1")).toBe(true); + expect(isLoopbackAddress("::ffff:127.0.0.1")).toBe(true); + expect(isLoopbackAddress("192.168.1.20")).toBe(false); + expect(isLoopbackAddress("203.0.113.10")).toBe(false); + }); + + it("allows direct local and Vite-proxied requests", () => { + expect(isTrustedLocalRequest(request())).toBe(true); + expect( + isTrustedLocalRequest( + request({ + headers: { + host: "localhost:5173", + origin: "http://localhost:5173", + }, + remoteAddress: "::1", + }), + ), + ).toBe(true); + }); + + it("rejects tunnel, LAN, DNS-rebinding, and cross-origin requests", () => { + expect( + isTrustedLocalRequest( + request({ headers: { "x-forwarded-for": "203.0.113.10" } }), + ), + ).toBe(false); + expect(isTrustedLocalRequest(request({ remoteAddress: "192.168.1.20" }))).toBe(false); + expect( + isTrustedLocalRequest(request({ headers: { host: "example.com" } })), + ).toBe(false); + expect( + isTrustedLocalRequest(request({ headers: { host: "example.com@localhost" } })), + ).toBe(false); + expect( + isTrustedLocalRequest( + request({ headers: { origin: "https://example.com" } }), + ), + ).toBe(false); + }); + + it("rejects mixed or spoofed forwarding chains", () => { + expect( + isTrustedLocalRequest( + request({ headers: { "x-forwarded-for": "127.0.0.1, 203.0.113.10" } }), + ), + ).toBe(false); + expect( + isTrustedLocalRequest( + request({ headers: { "x-forwarded-host": "localhost, example.com" } }), + ), + ).toBe(false); + expect( + isTrustedLocalRequest( + request({ headers: { forwarded: "for=127.0.0.1;host=example.com" } }), + ), + ).toBe(false); + }); + + it("exposes only health and provider webhooks publicly", () => { + expect(isPublicServerRequest(request({ url: "/health?source=desktop" }))).toBe(true); + expect( + isPublicServerRequest(request({ method: "POST", url: "/sendblue/webhook/" })), + ).toBe(true); + expect( + isPublicServerRequest(request({ method: "POST", url: "/composio/webhook" })), + ).toBe(true); + expect(isPublicServerRequest(request({ method: "POST", url: "/chat" }))).toBe(false); + expect(isPublicServerRequest(request({ url: "/runtime-config" }))).toBe(false); + expect(isPublicServerRequest(request({ url: "/composio/toolkits" }))).toBe(false); + expect(isPublicServerRequest(request({ method: "GET", url: "/sendblue/webhook" }))).toBe( + false, + ); + }); +}); diff --git a/test/memory-graph-model.test.ts b/test/memory-graph-model.test.ts new file mode 100644 index 00000000..c796625a --- /dev/null +++ b/test/memory-graph-model.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from "vitest"; +import { + buildMemoryGraph, + type GraphLink, + type MemoryRecord, +} from "../debug/src/components/memoryGraphModel.js"; + +function memory( + memoryId: string, + topic: string, + content: string, + overrides: Partial = {}, +): MemoryRecord { + return { + memoryId, + content, + tier: "long", + segment: "preference", + importance: 0.8, + accessCount: 4, + metadata: JSON.stringify({ graph: { topic, label: content, relatedMemoryIds: [] } }), + ...overrides, + }; +} + +function endpoints(link: GraphLink): [string, string] { + return [String(link.source), String(link.target)]; +} + +describe("memory graph model", () => { + it("builds a connected topic map with affinity and history edges", () => { + const graph = buildMemoryGraph([ + memory("memory:1", "launch", "Launch checklist", { + metadata: JSON.stringify({ + graph: { + topic: "launch", + label: "Launch checklist", + relatedMemoryIds: ["memory:3"], + }, + }), + }), + memory("memory:2", "launch", "Beta feedback"), + memory("memory:3", "daily-rhythm", "Protect focus time", { + supersedes: ["memory:4"], + }), + memory("memory:4", "daily-rhythm", "Old focus-time rule"), + ]); + + expect(graph.nodes.filter((node) => node.kind === "root")).toHaveLength(1); + expect(graph.nodes.filter((node) => node.kind === "topic")).toHaveLength(2); + expect(graph.nodes.filter((node) => node.kind === "memory")).toHaveLength(4); + expect(graph.links.filter((link) => link.kind === "membership")).toHaveLength(4); + expect(graph.links.some((link) => link.kind === "affinity")).toBe(true); + expect(graph.links.some((link) => link.kind === "history")).toBe(true); + + const adjacency = new Map>(); + for (const link of graph.links) { + const [source, target] = endpoints(link); + if (!adjacency.has(source)) adjacency.set(source, new Set()); + if (!adjacency.has(target)) adjacency.set(target, new Set()); + adjacency.get(source)!.add(target); + adjacency.get(target)!.add(source); + } + const visited = new Set(["root:memory"]); + const queue = ["root:memory"]; + while (queue.length > 0) { + const current = queue.shift()!; + for (const neighbor of adjacency.get(current) ?? []) { + if (visited.has(neighbor)) continue; + visited.add(neighbor); + queue.push(neighbor); + } + } + + expect(visited.size).toBe(graph.nodes.length); + }); + + it("infers a useful topic when real memory metadata has no graph hints", () => { + const graph = buildMemoryGraph([ + memory("memory:travel", "unknown", "Prefer nonstop flights and an aisle seat", { + metadata: undefined, + }), + ]); + + expect(graph.nodes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ kind: "topic", id: "topic:travel", label: "Travel" }), + ]), + ); + }); +}); diff --git a/test/scripted-demo-replies.test.ts b/test/scripted-demo-replies.test.ts new file mode 100644 index 00000000..ced880b4 --- /dev/null +++ b/test/scripted-demo-replies.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { + matchesLinkedInDemoPrompt, + matchesWaterBottleDemoPrompt, +} from "../server/scripted-demo-replies.js"; + +describe("scripted demo replies", () => { + it("matches the private water-bottle demo prompt with normal texting punctuation", () => { + expect( + matchesWaterBottleDemoPrompt("What was that water bottle brand my mom texted me about?"), + ).toBe(true); + expect( + matchesWaterBottleDemoPrompt( + " what was that water bottle brand my mom texted me about!!! ", + ), + ).toBe(true); + }); + + it("does not intercept unrelated messages", () => { + expect(matchesWaterBottleDemoPrompt("what water bottle should I buy?")).toBe(false); + expect(matchesWaterBottleDemoPrompt("what did my mom text me about?")).toBe(false); + }); + + it("matches natural LinkedIn browser demo prompts", () => { + expect(matchesLinkedInDemoPrompt("Check my LinkedIn")).toBe(true); + expect(matchesLinkedInDemoPrompt("Check my LinkedIn messages using the browser.")).toBe(true); + expect(matchesLinkedInDemoPrompt("Can you use the browser to check my LinkedIn messages?")).toBe( + true, + ); + }); + + it("does not intercept unrelated LinkedIn messages", () => { + expect(matchesLinkedInDemoPrompt("Write a LinkedIn post for me")).toBe(false); + expect(matchesLinkedInDemoPrompt("Who messaged me?")).toBe(false); + }); +}); diff --git a/test/sendblue-media.test.ts b/test/sendblue-media.test.ts new file mode 100644 index 00000000..32bec869 --- /dev/null +++ b/test/sendblue-media.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { extractSendblueMediaUrls } from "../server/sendblue.js"; + +describe("Sendblue media payloads", () => { + it("keeps the legacy media_url when media_urls is an empty array", () => { + expect( + extractSendblueMediaUrls("https://cdn.example/image.png", []), + ).toEqual(["https://cdn.example/image.png"]); + }); + + it("combines and deduplicates both supported media fields", () => { + expect( + extractSendblueMediaUrls("https://cdn.example/one.png", [ + "https://cdn.example/one.png", + "https://cdn.example/two.jpg", + ]), + ).toEqual([ + "https://cdn.example/one.png", + "https://cdn.example/two.jpg", + ]); + }); +}); diff --git a/test/sendblue-webhook-auth.test.ts b/test/sendblue-webhook-auth.test.ts new file mode 100644 index 00000000..66e380a6 --- /dev/null +++ b/test/sendblue-webhook-auth.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it } from "vitest"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + deriveWebhookSecret, + parseEnvText, + parseApiWebhookListing, + readEnvFiles, + resynchronizeWebhookSecret, + webhookCheck, +} from "../scripts/sendblue-webhook.mjs"; +import { + deriveSendblueWebhookSecret, + verifySendblueWebhookSecret, +} from "../server/sendblue-webhook-auth.js"; + +describe("Sendblue webhook authentication", () => { + it("parses quoted values without treating inline hashes as comments", () => { + expect( + parseEnvText(` + SENDBLUE_API_KEY="quoted-key" + SENDBLUE_API_SECRET='secret#inside' # trailing comment + PUBLIC_URL=https://example.test # trailing comment + `), + ).toEqual({ + SENDBLUE_API_KEY: "quoted-key", + SENDBLUE_API_SECRET: "secret#inside", + PUBLIC_URL: "https://example.test", + }); + }); + + it("loads .env before .env.local and lets explicit process values win", () => { + const root = mkdtempSync(join(tmpdir(), "boop-sendblue-env-")); + try { + const fallback = join(root, ".env"); + const local = join(root, ".env.local"); + writeFileSync(fallback, "SENDBLUE_API_KEY=fallback\nPUBLIC_URL=https://fallback.test\n"); + writeFileSync(local, "SENDBLUE_API_KEY=local\nSENDBLUE_API_SECRET=local-secret\n"); + + expect( + readEnvFiles([fallback, local], { SENDBLUE_API_SECRET: "process-secret" }), + ).toMatchObject({ + SENDBLUE_API_KEY: "local", + SENDBLUE_API_SECRET: "process-secret", + PUBLIC_URL: "https://fallback.test", + }); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("derives the same scoped secret in registration and request handling", () => { + const apiSecret = "test-api-secret-not-real"; + expect(deriveWebhookSecret(apiSecret)).toBe(deriveSendblueWebhookSecret(apiSecret)); + }); + + it("accepts only the expected signing header", () => { + const apiSecret = "test-api-secret-not-real"; + const signingSecret = deriveSendblueWebhookSecret(apiSecret); + + expect(verifySendblueWebhookSecret(signingSecret, apiSecret)).toBe(true); + expect(verifySendblueWebhookSecret("wrong", apiSecret)).toBe(false); + expect(verifySendblueWebhookSecret(undefined, apiSecret)).toBe(false); + expect(verifySendblueWebhookSecret(signingSecret, "")).toBe(false); + }); + + it("parses URL and object webhook entries without treating the global secret as a URL", () => { + expect( + parseApiWebhookListing({ + receive: [ + "https://first.example/sendblue/webhook", + { url: "https://second.example/sendblue/webhook", secret: "hidden" }, + ], + globalSecret: "hidden", + }), + ).toEqual({ + current: [ + { type: "receive", url: "https://first.example/sendblue/webhook" }, + { type: "receive", url: "https://second.example/sendblue/webhook" }, + ], + globalSecret: "hidden", + }); + }); + + it("does not report a registered webhook as healthy until signing is synchronized", () => { + const url = "https://active.example/sendblue/webhook"; + const result = webhookCheck(url, [{ type: "receive", url }], "api", false); + + expect(result.ok).toBe(false); + expect(result.state).toBe("mismatch"); + expect(result.details).toContain("signing secret is not synchronized"); + }); + + it("replaces a registered webhook before re-adding it with a new signing secret", async () => { + const url = "https://active.example/sendblue/webhook"; + const operations: string[] = []; + + const changed = await resynchronizeWebhookSecret( + url, + { current: [{ type: "receive", url }], globalSecret: "old-secret" }, + "new-secret", + async (hookUrl: string) => { + operations.push(`remove:${hookUrl}`); + }, + async (hookUrl: string) => { + operations.push(`add:${hookUrl}`); + }, + ); + + expect(changed).toBe(true); + expect(operations).toEqual([`remove:${url}`, `add:${url}`]); + }); + + it("leaves a registered webhook untouched when its signing secret already matches", async () => { + const url = "https://active.example/sendblue/webhook"; + const operations: string[] = []; + + const changed = await resynchronizeWebhookSecret( + url, + { current: [{ type: "receive", url }], globalSecret: "current-secret" }, + "current-secret", + async () => { + operations.push("remove"); + }, + async () => { + operations.push("add"); + }, + ); + + expect(changed).toBe(false); + expect(operations).toEqual([]); + }); +}); diff --git a/test/sendblue-webhook-sync.test.ts b/test/sendblue-webhook-sync.test.ts new file mode 100644 index 00000000..6108f852 --- /dev/null +++ b/test/sendblue-webhook-sync.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it, vi } from "vitest"; +import { syncWebhooks } from "../scripts/sendblue-webhook.mjs"; + +describe("Sendblue webhook synchronization", () => { + it("keeps duplicate active hooks while removing stale tunnel URLs", async () => { + const active = "https://active.ngrok-free.app/sendblue/webhook"; + const stale = "https://stale.ngrok-free.app/sendblue/webhook"; + const removeWebhook = vi.fn(async () => undefined); + const addWebhook = vi.fn(async () => undefined); + + await syncWebhooks( + active, + [ + { type: "receive", url: active }, + { type: "receive", url: active }, + { type: "receive", url: stale }, + ], + removeWebhook, + addWebhook, + ); + + expect(removeWebhook).toHaveBeenCalledOnce(); + expect(removeWebhook).toHaveBeenCalledWith(stale); + expect(addWebhook).not.toHaveBeenCalled(); + }); + + it("registers the active hook when it is missing", async () => { + const active = "https://active.ngrok-free.app/sendblue/webhook"; + const addWebhook = vi.fn(async () => undefined); + + await syncWebhooks(active, [], vi.fn(async () => undefined), addWebhook); + + expect(addWebhook).toHaveBeenCalledOnce(); + expect(addWebhook).toHaveBeenCalledWith(active); + }); +});