diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 2cf9bf00..398a93e6 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -92,6 +92,12 @@ {"_type":"issue","id":"foreman-b4m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:42Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:20:51Z","closed_at":"2026-04-18T03:20:51Z","close_reason":"Epic complete: monorepo extracted. packages/agents/ (Hono) + packages/web/ (Next.js). Web builds clean.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-m2m","title":"Monorepo extraction: standalone Mastra/Hono agent server + Next.js frontend","description":"Split the embedded Mastra agent out of Next.js into a standalone Hono server (packages/agents/) that exposes REST + A2A + MCP. Next.js frontend (packages/web/) becomes a thin UI layer. Follows Otaku Solutions canonical architecture.","status":"closed","priority":1,"issue_type":"epic","owner":"admin@otakusolutions.io","created_at":"2026-04-18T02:44:30Z","created_by":"HamChowderr","updated_at":"2026-04-18T03:50:20Z","closed_at":"2026-04-18T03:50:20Z","close_reason":"Epic complete: monorepo extracted, web builds, agent server configured with multi-target deploy.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-0zs","title":"Deploy to Vercel","description":"Env vars set, LibSQL hosted (Turso or self-hosted), domain configured. AC: Production URL responds. Can sign up, connect Zapier, run an action end-to-end.","status":"closed","priority":1,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:28:13Z","created_by":"HamChowderr","updated_at":"2026-04-19T01:41:35Z","closed_at":"2026-04-19T01:41:35Z","close_reason":"Deployed both services. Web frontend on Vercel (foreman-three.vercel.app), agent server on Coolify/Hostinger VPS (foreman-agents.otakusolutions.io:4111). Coolify app UUID: oqshe32xh3v8zva7tt6r4aff. Dockerfile.agents uses multi-stage build with node:22-slim. 14 env vars configured. Using test Clerk keys (prod requires custom domain).","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-sg23","title":"Vercel preview build fails (module_not_found) on alpha branch — align install with CI","description":"PR #17 Vercel preview deploy errors at buildStep with errorCode module_not_found ('cd packages/web \u0026\u0026 npm run build' exit 1), while all 5 GHA gating jobs (incl. web next build via npm ci) are GREEN on the same commit, and a clean local build passes. Install succeeds; .source self-regenerates via withMDX so it is not the cause. Build-log API is 403 (CLI token lacks build-log scope), so the exact module is dashboard-only. Divergence: Vercel installCommand 'npm install' (+ possible stale cached node_modules on a fresh branch) vs GHA 'npm ci'. Non-gating (main production deploys are Ready), does not block merge. Fix: set vercel.json installCommand to 'npm ci' to force a clean lockfile-exact install matching the proven-green CI path.","notes":"FIXED properly: root cause was a duplicated react in the alpha-regenerated lockfile — root node_modules/react 19.2.7 (ecosystem ^19.2.0) + nested packages/web/node_modules/react 19.2.4 (web's exact pin). next (hoisted to root) could not resolve react on Vercel's npm. Fix: root overrides react/react-dom=19.2.7 → clean reinstall dedupes to a SINGLE root react 19.2.7 (no nested copy), verified locally (lockfile single entry + web next build exit 0). Reverted the two wrong guesses (npm ci 3e8a5ec, rm -rf a5bb049); installCommand back to npm install.","status":"in_progress","priority":2,"issue_type":"bug","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T17:14:15Z","created_by":"HamChowderr","updated_at":"2026-06-12T17:59:22Z","started_at":"2026-06-12T17:14:36Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-hlh0","title":"[web] Surface run fired_by/error_message + fix failed-status badge","description":"The agent API (GET /workflows/:id/runs) now returns fired_by/error_message/trigger_id (foreman-2afc), but the web doesn't render them, and there's a pre-existing display bug: the engine writes status 'failed' but workflows-client WorkflowRun.status + the StatusBadge map only know 'error', so failed runs render as 'running'. Fix: (1) WorkflowRun.status -\u003e 'running'|'success'|'failed' + add fired_by/error_message/trigger_id; (2) [id]/page.tsx StatusBadge map key error-\u003efailed + show how each run fired; (3) run detail page show fired_by + error_message. First visible slice of the broader 'surface new backend in UI' work (foreman-tbjb/7am4/ezsm).","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T16:30:49Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:34:48Z","closed_at":"2026-06-12T16:34:48Z","close_reason":"Done (ada01c9): WorkflowRun type gains status=failed + fired_by/error_message/trigger_id; run list shows how each run fired + inline error on failure; run detail shows 'started by' + an error box. Fixed the pre-existing bug where failed runs rendered as 'running' (engine writes 'failed', web only knew 'error'). next build green.","dependencies":[{"issue_id":"foreman-hlh0","depends_on_id":"foreman-2afc","type":"discovered-from","created_at":"2026-06-12T09:30:48Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-tbjb","title":"[signals] notify() is inert — driving processes don't connect the providers","description":"The poll + channel SignalProviders are hosted on the foreman agent for notify(), but the processes that DRIVE them don't reliably CONNECT them, so notify() is a no-op (provider.agent undefined -\u003e notifyOwner returns early). (1) cron-driver-server drives zapierPollProvider.runDuePolls() but never calls getMastra()/builds the foreman agent, so poll notify() never fires. (2) channel: slack/bot.ts builds the agent lazily inside generateReply, AFTER matchAndFireChannelTriggers, so the channel provider isn't connected for trigger-only messages until the process first runs generateReply. (3) even when fired, notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) and no web UI consumes the notification store, so it's not user-visible. Workflow FIRING is unaffected/correct (doesn't need the agent). FIX: have the driving processes construct the agent at startup (cron-driver-server: getMastra() so the provider connects; webhook-server: build foreman at startup) + a web consumer of the notification store / thread to surface it. Until then notify() is scaffolding, not delivered.","notes":"CONNECT FIX DONE (commit 177c281): cron-driver-server + webhook-server now call getMastra() at startup -\u003e foreman agent built in-process -\u003e providers connected (+ notification storage available). Live-verified: cron logs 'poll provider connected: true'. So notify() now FIRES (writes to the notification store) on poll/channel trigger hits. REMAINING (the user-visible half): a web consumer of the notification store / agent thread to surface the notification in chat — bundles with the deferred background-tasks /background-tasks/stream consumer. Also: notify targets synthetic threads (poll:\u003cid\u003e, channel:\u003cch\u003e:\u003croom\u003e) — revisit thread targeting when wiring the UI.","status":"open","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T05:22:51Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:34:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-7am4","title":"[agent] Adopt Mastra background tasks for long-running Zapier tools + web stream consumer","description":"backgroundTasks is enabled and /background-tasks/stream is built, but no tool opts in and nothing consumes the stream. Opt long-running read-only Zapier tools into background:{enabled:true} with a waitTimeoutMs so the agent loop proceeds (non-blocking chat). Add a web consumer of /background-tasks/stream to surface running-\u003edone + injected results. Keep approval-required write/delete tools synchronous (don't disturb the proposal flow). Live-verify on the dev server. 3rd Mastra primitive on its designed surface.","notes":"LIVE-VERIFIED (mastra dev). FINDING: backgrounding the read-only Zapier SDK tools HANGS mastra dev — enabling background injects a _background field into the tool input schema and Studio toJSONSchema over the Zapier zod schemas loops forever (same as lazy-init-zapier hang). mastra build is unaffected (no Studio). Bisected: no-flag binds :4111 in ~12s; flag-on-Zapier hangs. FIX (commit 0a20828): background search_history instead (simple 3-field schema) -\u003e dev binds in ~12s with flag on; /background-tasks/stream serves SSE 200 (manager live). lib/background.ts backgroundIfEnabled() gate, FOREMAN_BACKGROUND_TOOLS=1, OFF by default. REMAINING for full non-blocking UX (modest, search_history-only given the Zapier wall): waitTimeoutMs + web /background-tasks/stream consumer + end-to-end chat verify (LLM round-trip). Zapier tools blocked until alpha fixes toJSONSchema.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T04:38:20Z","created_by":"HamChowderr","updated_at":"2026-06-12T05:05:59Z","started_at":"2026-06-12T04:42:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-ueep","title":"[workflow] Live-verify the poll driver against a real Zapier read action","description":"poll-driver.ts (foreman-ld7s) is unit-tested only. Verify the real run path: bind a type=poll trigger to a real Zapier read action (e.g. gmail/new_email), run cron-driver-server, create a new record, assert exactly one workflow run fires and zero duplicates on re-poll; confirm baseline (first poll fires nothing) and intervalMinutes gating. Per verify-behavior-not-just-logic.","notes":"ORCHESTRATION LIVE-VERIFIED (commit 278246a): tests/live/poll-driver.test.ts, 4/4 green against real local Postgres. Proves attach_trigger(poll) -\u003e runDuePolls -\u003e executeWorkflow -\u003e real workflow_run row (fired_by=poll, trigger_id, status=success) + baseline (first poll no fire, cursor set) + dedup (no double-fire). Only the external Zapier read (runAction) is stubbed — DB was reset, no OAuth-connected app to create records in. REMAINING (thin, user-driven): smoke the literal real Zapier read by connecting an app (gmail) + creating a record while cron-driver-server runs; the SDK network call itself is covered by the tests/sdk live tier. The whole machinery around that call is now proven.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:35:12Z","created_by":"HamChowderr","updated_at":"2026-06-12T06:10:05Z","dependencies":[{"issue_id":"foreman-ueep","depends_on_id":"foreman-ld7s","type":"discovered-from","created_at":"2026-06-11T17:35:12Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-ld7s","title":"[workflow] Un-stub the poll trigger as a Foreman-native poll driver (tickPoll)","description":"Poll trigger type is documented in the migration and accepted by list/detach but hidden by attach-trigger and has no driver. Build src/workflows/poll-driver.ts mirroring cron-driver: read workflow_trigger type=poll, run the Zapier read action via runAction, diff results by config.dedupeKey field against last_dedupe_key cursor, fire executeWorkflow on new records, persist cursor. Un-stub attach_trigger (add poll + pollConfig). Wire into cron-driver-server. Rationale (explored alpha API hands-on): Mastra SignalProvider.poll() skips when zero in-memory subscriptions and notify() requires an in-process agent — it is built for provider-lives-in-agent/notify-thread, not a singleton worker firing DB workflows. SignalProvider will be adopted on the webhook/channel path instead. Keeps door open to Zapier watchTriggerInbox SSE later (iyq6).","status":"closed","priority":2,"issue_type":"feature","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T00:27:10Z","created_by":"HamChowderr","updated_at":"2026-06-12T00:35:07Z","started_at":"2026-06-12T00:27:27Z","closed_at":"2026-06-12T00:35:07Z","close_reason":"Poll driver implemented + unit-tested; full agents suite green (288). Live Zapier integration tracked as follow-up.","dependencies":[{"issue_id":"foreman-ld7s","depends_on_id":"foreman-c63f","type":"discovered-from","created_at":"2026-06-11T17:27:10Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-22c3","title":"Evaluate Mastra signals/signal-providers/notifications vs Foreman's home-grown triggers","description":"Foreman is on @mastra/core 1.41 (post-bump). Mastra now ships native primitives that overlap with Foreman's hand-built trigger/workflow/notification system: (1) SignalProvider (poll OR webhook an external source -\u003e push notifications into agent threads) ~= Foreman's channel-trigger + cron-driver + the stubbed poll trigger (c63f); (2) built-in dedupeKey on notify() directly solves the dedup gap in tv5p (Foreman hand-rolls idempotency); (3) @mastra/github-signals is a production provider and Foreman already has a GitHub channel; (4) background-task suspend/resume for human approval + notification inbox ~= Foreman's proposal/approval flow. Sibling to iyq6 (Zapier-durable eval). Decide adopt-Mastra-native vs keep-own BEFORE building the v8k1 workflow epic. Confirmed code-side: backgroundTasks enabled in mastra/index.ts; pubsub is default in-process EventEmitter (no distributed backend — matters for 2-process + multi-instance); signals/signal-providers not used at all.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T22:33:47Z","created_by":"HamChowderr","updated_at":"2026-06-11T22:33:47Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-rpsr","title":"Fix flaky db-types CI job — 'supabase gen types --local' intermittently errors 'Access token not provided'","description":"CI job 'db types fresh' (.github/workflows/test.yml) is flaky: passes on some commits (4dd20ff, 25b1ed1), fails on others (51e2065, f677794, b1070ae, 040b6a6) with 'Access token not provided' from 'npx supabase gen types typescript --local', producing empty output → diff shows 0a1,4128 (empty vs committed 4128-line database.types.ts). NOT a stale-types problem and NOT caused by the SDK/mastra bump. Root cause: supabase/setup-cli@v1 uses version:latest (unpinned), and the drifted CLI intermittently requires an access token even for --local. Fix options: (1) pin setup-cli to a known-good supabase version; (2) ensure 'supabase start' is fully healthy before gen; (3) set SUPABASE_ACCESS_TOKEN (project avoids secrets). Verify locally by starting Foreman's own supabase (note: a different project's supabase '*_Zillow' was running during diagnosis).","status":"closed","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-11T19:40:46Z","created_by":"HamChowderr","updated_at":"2026-06-11T20:18:52Z","closed_at":"2026-06-11T20:18:52Z","close_reason":"Fixed (379d9b4): added a placeholder SUPABASE_ACCESS_TOKEN to the CI db-types step. The latest supabase CLI demands a token's presence before local type-gen in Linux CI (value unused for --local). All 5 CI jobs green on main (run 27374657799). Also switched scripts to --local --workdir ../.. (cwd-independent) + documented the gotcha in CLAUDE.md.","dependencies":[{"issue_id":"foreman-rpsr","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T12:40:46Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-chxx","title":"Reskin README to myrp-build product-launch style (hero+badges, emoji sections, narrative, FAQ)","description":"Current README is content-complete but dry/dev-doc-toned. Rewrite to mirror hamchowderr/myrp-build README: centered hero + shields.io badges, emoji-prefixed sections split by ---, benefit-led narrative with example-prompt blockquotes, collapsible concrete example, ASCII pipeline diagram, dev-vs-self-hosted comparison table, annotated file-tree architecture, testing-tiers table, FAQ, acknowledgments, license stance. Preserve all factual content (5 agents, 34 Zapier tools, channel statuses, modes, testing tiers, deploy targets, live URLs, MIT license, contact). User-authorized.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T19:17:43Z","created_by":"HamChowderr","updated_at":"2026-06-11T19:28:04Z","closed_at":"2026-06-11T19:28:04Z","close_reason":"README reskinned to myrp-build product-launch style; committed b1070ae, pushed to origin/main, rendered + reviewed in Chrome.","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -100,8 +106,8 @@ {"_type":"issue","id":"foreman-7jn1","title":"[workflow] Decision: per-user Slack isolation sufficient vs build shared group-channel context","description":"Foreman already isolates per Slack user (resourceId per user; slack/bot.ts:58; foreman.ts memory scope resource). Mastra natively supports scope thread|resource but NO shared-group memory, and slack/bot.ts:137 never persists the channel thread to the conversation table. Decide: (A) per-user is enough (lock it), or (B) build shared group context = new channel_conversation table + group memory scope + thread persistence. If B, spawn the feature issue.","acceptance_criteria":"Decision recorded; if B, a scoped feature issue (channel_conversation table + group scope + persist channel thread) is filed.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:18Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:18Z","dependencies":[{"issue_id":"foreman-7jn1","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:17Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-ezsm","title":"[workflow] Build the /workflows UI page (list saved automations + run history)","description":"foreman-v8k1 calls for a UI so users can see saved automations; it does not exist. Build a /workflows page in packages/web listing workflows + triggers + recent runs (status, error_message from the observability issue), using the existing /workflows/* API routes and the shadcn stack. Per-workflow run-history view.","acceptance_criteria":"/workflows lists the user's workflows + triggers; clicking shows run history incl. failed-run errors; reads from existing API routes.","status":"open","priority":2,"issue_type":"feature","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:13Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:13Z","dependencies":[{"issue_id":"foreman-ezsm","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:13Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-8fcs","title":"[workflow] Tests: params.ts substitution + engine step-failure paths","description":"params.ts (extract/substitute/validate) and engine.ts step-failure handling (~80-122) are untested. Add unit tests: substituteParams (partial replace, unknown keys left as-is, nested/arrays), validateParams (missing detected), engine step failure (runAction throws -\u003e failed + error captured), generator-throw cleanup, empty-steps early-exit.","acceptance_criteria":"params + engine error paths covered; npm test green.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:09Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:09Z","dependencies":[{"issue_id":"foreman-8fcs","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:08Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-2afc","title":"[workflow] Run-state \u0026 observability: orphaned 'running' rows on crash; persist error_message + trigger context","description":"engine.ts inserts workflow_run status=running (~line 56) and never sets failed if executeWorkflow throws before completing -\u003e rows orphaned forever. Wrap generator consumption in try/finally at each call site (routes/workflows.ts, channel-trigger.ts, cron-driver.ts) to mark failed with the error. Persist error_message + fired_by(manual/cron/channel) + trigger_id on workflow_run; trigger-fired runs only console.warn today and never appear in run history. Surface in GET /workflows/:id/runs.","acceptance_criteria":"A failed run shows status=failed + error_message within ~1s; cron/channel-fired runs appear in run history with trigger context; no rows stuck in running.","status":"open","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:04Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:31:04Z","dependencies":[{"issue_id":"foreman-2afc","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:03Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-tv5p","title":"[workflow] BUG: trigger dedup/idempotency — channel-trigger has no dedup; cron can double-fire on same-minute restart","description":"channel-trigger.ts (matchAndFireChannelTriggers ~43-79) fires with NO dedup despite an unused workflow_trigger.last_dedupe_key column — a retried webhook = duplicate runs. Add a dedupe key (channel+sender+ts+text hash or delivery id) checked against last_dedupe_key before firing. Separately cron-driver.ts (~164-167) can double-fire if the process restarts mid-minute; add a guard/lock. runOne also swallows errors silently.","acceptance_criteria":"Duplicate webhook =\u003e 1 run not 2 (unit test); a new message =\u003e new run; cron fires once per minute even across a same-minute restart (unit test).","status":"open","priority":2,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:59Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:30:59Z","dependencies":[{"issue_id":"foreman-tv5p","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:30:58Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-2afc","title":"[workflow] Run-state \u0026 observability: orphaned 'running' rows on crash; persist error_message + trigger context","description":"engine.ts inserts workflow_run status=running (~line 56) and never sets failed if executeWorkflow throws before completing -\u003e rows orphaned forever. Wrap generator consumption in try/finally at each call site (routes/workflows.ts, channel-trigger.ts, cron-driver.ts) to mark failed with the error. Persist error_message + fired_by(manual/cron/channel) + trigger_id on workflow_run; trigger-fired runs only console.warn today and never appear in run history. Surface in GET /workflows/:id/runs.","acceptance_criteria":"A failed run shows status=failed + error_message within ~1s; cron/channel-fired runs appear in run history with trigger context; no rows stuck in running.","notes":"Part 1 DONE (4c5da4c): engine try/finally + settled flag = no run stuck in running; 3 unit tests. Part 2 GATED on user go: workflow_run migration (error_message, fired_by, trigger_id), thread trigger context through executeWorkflow + 3 driver sites, surface in GET /workflows/:id/runs, regen db types (needs Foreman local supabase up; a Zillow stack currently holds default ports).","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:31:04Z","created_by":"HamChowderr","updated_at":"2026-06-12T04:25:28Z","closed_at":"2026-06-12T04:25:28Z","close_reason":"Part 1 (4c5da4c): no run stuck in running. Part 2 (c039220): workflow_run migration + error_message/fired_by/trigger_id persisted + surfaced in GET runs. db:types:check clean, migration applies on clean reset, 300 tests green.","dependencies":[{"issue_id":"foreman-2afc","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:31:03Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-tv5p","title":"[workflow] BUG: trigger dedup/idempotency — channel-trigger has no dedup; cron can double-fire on same-minute restart","description":"channel-trigger.ts (matchAndFireChannelTriggers ~43-79) fires with NO dedup despite an unused workflow_trigger.last_dedupe_key column — a retried webhook = duplicate runs. Add a dedupe key (channel+sender+ts+text hash or delivery id) checked against last_dedupe_key before firing. Separately cron-driver.ts (~164-167) can double-fire if the process restarts mid-minute; add a guard/lock. runOne also swallows errors silently.","acceptance_criteria":"Duplicate webhook =\u003e 1 run not 2 (unit test); a new message =\u003e new run; cron fires once per minute even across a same-minute restart (unit test).","status":"closed","priority":2,"issue_type":"bug","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:59Z","created_by":"HamChowderr","updated_at":"2026-06-12T03:29:21Z","started_at":"2026-06-12T03:15:08Z","closed_at":"2026-06-12T03:29:21Z","close_reason":"Channel dedup + cron same-minute atomic claim implemented and unit-tested per acceptance criteria. Error-surfacing portion tracked under 2afc.","dependencies":[{"issue_id":"foreman-tv5p","depends_on_id":"foreman-v8k1","type":"discovered-from","created_at":"2026-06-11T01:30:58Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-wzmx","title":"[aimock] Resolve declared-but-unfixtured MCP/A2A mocks (write fixtures or remove declarations)","description":"aimock.json declares 8 MCP tools + 5 A2A agents but tests/fixtures/aimock/ has no mcp/ or a2a/ fixtures — a declared-vs-real mismatch that 503s under strict mode. Either write minimal fixtures for the tools/agents tests actually exercise, or remove the unused declarations so config reflects reality.","acceptance_criteria":"Every MCP/A2A declaration has a fixture OR is removed; npm test passes under strict mode with no unmatched MCP/A2A requests.","status":"closed","priority":2,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:27Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:42:33Z","started_at":"2026-06-11T21:37:23Z","closed_at":"2026-06-11T21:42:33Z","close_reason":"MCP/A2A declarations made real; orphaned mcp/tools.json folded in and removed","dependencies":[{"issue_id":"foreman-wzmx","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T01:30:27Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-wrrd","title":"[aimock] Enable strict mode and tighten/remove the empty-match catch-all fixture","description":"@copilotkit/aimock supports strict:true (verified config-loader.d.ts:101) so unmatched requests 503 instead of silently matching z-catchall.json (match {}), which currently risks false-green tests. Enable strict in packages/agents/aimock.json and remove/convert z-catchall to a strict error, fixing any tests that relied on the fallback.","acceptance_criteria":"aimock.json has strict:true; z-catchall removed/converted; npm test passes with every test hitting an explicit fixture.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:22Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:43:26Z","closed_at":"2026-06-11T21:43:26Z","close_reason":"strict:true enabled in aimock.json; z-catchall.json deleted; npm test 278 passed/10 skipped under strict; strict 503 verified via probe","dependencies":[{"issue_id":"foreman-wrrd","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T01:30:21Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-xnis","title":"[bump] Surface getProfile as an agent tool; confirm trash option auto-surfaces on table-list tools","description":"Post-bump, surface worthwhile new stable methods. Add getProfile to READ_ONLY in zapier-sdk-tools.ts (agent can answer who-am-I-on-Zapier). Confirm the 0.57 trash option (exclude/include/only) auto-surfaces in listTableRecords/listTableFields tool schemas via the registry (likely no code change). Keep client-credentials mgmt + listTriggers excluded (documented in capability map).","acceptance_criteria":"getProfile in READ_ONLY and callable; trash param present in table-list tool input schema; tests pass.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:17Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:50:31Z","closed_at":"2026-06-11T21:50:31Z","close_reason":"getProfile already surfaced as a read-only tool (in READ_ONLY set, generated via sdk.getRegistry; added 7wks ago in 2092317) — agent can answer who-am-I-on-Zapier, no change needed. 'Trash option': no 'trash' field exists anywhere in @zapier/zapier-sdk@0.69.3; tool params auto-surface from SDK Zod schemas (mechanism verified) but there is nothing to surface — note is stale.","dependencies":[{"issue_id":"foreman-xnis","depends_on_id":"foreman-8ujc","type":"discovered-from","created_at":"2026-06-11T01:30:17Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -208,9 +214,11 @@ {"_type":"issue","id":"foreman-c9y","title":"Set up LibSQL + Drizzle ORM","description":"Single LibSQL database for app + Mastra data. Migrations runnable via script. AC: npm run db:migrate creates BetterAuth tables. Connection string in .env.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:48Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-s46","title":"Install and configure BetterAuth","description":"Email/password auth. Session stored in LibSQL. AC: /sign-up and /sign-in pages work. Authenticated session persists across reloads.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:46Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-5ld","title":"Initialize Next.js 15 project","description":"Scaffold Next.js 15 with App Router, TypeScript, Tailwind. AC: npm run dev serves a blank page at /. Project committed to git under C:\\Users\\HamCh\\code\\foreman.","status":"closed","priority":2,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-04-17T18:25:38Z","created_by":"HamChowderr","updated_at":"2026-04-17T18:28:37Z","closed_at":"2026-04-17T18:28:37Z","close_reason":"Completed in Phase 1 and Phase 2 (T1-T31 done)","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-c8fo","title":"[zapier] Poll-trigger connection resolution: normalizeAppKey + numeric-id/alias vs SDK uuid mismatch","description":"Live e2e (foreman-ueep) surfaced this. A poll trigger config storing a raw Zapier app_key (e.g. 'GitHubCLIAPI') fails the real read: runAction does normalizeAppKey('GitHubCLIAPI')='github', then auto-discover findFirstConnection({app:'github'}) does NOT match the connection whose app_key is 'GitHubCLIAPI' -\u003e no connection -\u003e 'access_token missing'. Separately, resolveConnection expects a numeric connection_id or a registered connection_alias, but sdk.findFirstConnection returns a UUID id (e.g. 0206dbe8-...), so passing that id as config.connection throws 'Unknown connection alias'. Net: for a poll trigger to read for real, the app key must be the normalized form AND the user's connection must be discoverable/aliased under it. CONFIRMED WORKING in isolation: sdk.runAction({app:'GitHubCLIAPI', connection:'\u003cuuid\u003e'}) returns {data:[100 repos]} and the poll driver's extractRecords parses it. So this is connection-resolution plumbing, not poll-driver logic (logic proven by tests/live/poll-driver.test.ts). Decide: normalize app keys at attach time, store the resolved connection id, and/or make auto-discover tolerant of raw app_key suffixes.","notes":"RESOLVED / MISDIAGNOSED earlier. There is NO poll-driver or connection-model bug. Proven: the full real poll fire works end-to-end (real Postgres + real Zapier GitHub read) when the poll config uses the CATALOG app key 'github' — baseline sets cursor from the real read, a new record fires one workflow_run (status=success, fired_by=poll, trigger_id set), and dedup prevents a second. Earlier failures were entirely my test input: I used the raw Zapier app_key 'GitHubCLIAPI', and normalizeAppKey('GitHubCLIAPI')='git-hub' (dashed), which Zapier doesn't match. normalize/resolveConnection/findFirstConnection are all fine. RESIDUAL (latent, low priority, NOT poll-specific): normalizeAppKey turns the raw Zapier key 'GitHubCLIAPI' into 'git-hub' rather than 'github', so feeding a raw Zapier app_key into any runAction breaks. Only matters if a caller uses raw Zapier keys instead of catalog keys; the agent/catalog uses normalized keys. Reduce to P3 / hardening.","status":"closed","priority":3,"issue_type":"bug","owner":"admin@otakusolutions.io","created_at":"2026-06-12T06:29:32Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:18:06Z","closed_at":"2026-06-12T16:18:06Z","close_reason":"FIXED (37e324d): added catalog-backed resolveAppSlug (app_key OR slug -\u003e canonical slug; pass-through unchanged when not in catalog), wired into runAction + workflow-step save, replacing the buggy normalizeAppKey at both call sites. Live-verified: the raw 'GitHubCLIAPI' key (previously broke with 'No current_implementation_id') now fires a real workflow_run (status=success) against real Zapier + real Postgres. 5 unit tests; full suite 308 green.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-m9f6","title":"[channels] Thread real platform message ids into ChannelMessage.dedupeKey","description":"ChannelTriggerSignalProvider dedup falls back to a content hash (channel|room|from|text) when ChannelMessage.dedupeKey is absent, which coalesces a user sending the exact same text back-to-back. Pass each adapter's stable message id as dedupeKey from the ~10 channel-bot call sites (slack/discord/telegram/teams/whatsapp/gchat/github/linear onDirectMessage + onNewMention) where the Chat SDK message exposes one. Removes the false-positive edge; true per-delivery idempotency.","status":"closed","priority":3,"issue_type":"task","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-12T03:31:07Z","created_by":"HamChowderr","updated_at":"2026-06-12T16:46:33Z","started_at":"2026-06-12T16:39:12Z","closed_at":"2026-06-12T16:46:33Z","close_reason":"Threaded normalized Chat SDK message.id into ChannelMessage.dedupeKey across all 8 channel bots (16 call-sites). Provider now dedupes on the real platform message id; content-hash is fallback only. Type-clean, 308 mocked tests green, committed c942a92.","dependencies":[{"issue_id":"foreman-m9f6","depends_on_id":"foreman-tv5p","type":"discovered-from","created_at":"2026-06-11T20:31:07Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-l8ev","title":"Document Zapier SDK auth model in README (client-creds vs userJwt per surface)","description":"Discovered during SDK exploration: the Zapier SDK surfaces need DIFFERENT auth. Trigger inboxes + discovery + runAction accept app-level client-credentials (ZAPIER_CLIENT_ID/SECRET). Durable-workflow endpoints (runDurable/publishWorkflowVersion) require a per-user userJwt (PKCE OAuth token), NOT client-creds. The new zapier-sdk-cli (0.54) login yields client-creds only; ~/.zapierrc deployKey is a legacy 32-char non-JWT (invalid as SDK creds). Document in README: which surfaces need which auth, how Foreman obtains each (env client-creds vs lib/zapier/sdk.ts PKCE per-user token), and the open question for Zapier (server-side path to per-user durable). Come back to this later. See docs/durable-workflow-spike.md + docs/trigger-inbox-spike.md.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T18:39:52Z","created_by":"HamChowderr","updated_at":"2026-06-11T18:39:52Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-z0kn","title":"Port provider-routing generate() test to a PG-enabled tier (storage injection)","description":"provider-swap.test.ts 'returns text' sub-test calls agent.generate() which now needs PostgresStore-backed Memory (foreman migrated LibSQL-\u003e@mastra/pg). It can't run in the no-external-services mocked tier (agents CI job has no PG), so it's it.skip'd. Proper fix: either inject a test storage into createForemanAgent (let tests use an in-memory/file store), or run this sub-test in a PG-enabled tier (test:live or a CI job with a postgres service). Discovered during the catch-up integration of claude/add-multiple-providers.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T16:40:19Z","created_by":"HamChowderr","updated_at":"2026-06-11T16:40:19Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"foreman-elyi","title":"[bump] Surface ZapierApprovalError approval URL to the user (handleSdkError)","description":"Under SDK \u003e=0.50 with approvalMode unset, a server (non-TTY) defaults to 'throw': sdk.runAction throws ZapierApprovalError carrying an approval URL on approval-gated responses. Foreman's handleSdkError (zapier-sdk-tools.ts) currently catches it only via the generic ZapierError catch-all, surfacing .message/.code but DROPPING the approval URL. Add an explicit 'if (err instanceof ZapierApprovalError)' branch that returns the approval URL + approvalStatus so the agent can tell the user how to approve. Non-blocking for the bump (degrades gracefully today). Discovered during foreman-2xdk verification.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T15:10:57Z","created_by":"HamChowderr","updated_at":"2026-06-11T15:10:57Z","dependencies":[{"issue_id":"foreman-elyi","depends_on_id":"foreman-2xdk","type":"discovered-from","created_at":"2026-06-11T08:10:57Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"foreman-elyi","title":"[bump] Surface ZapierApprovalError approval URL to the user (handleSdkError)","description":"Under SDK \u003e=0.50 with approvalMode unset, a server (non-TTY) defaults to 'throw': sdk.runAction throws ZapierApprovalError carrying an approval URL on approval-gated responses. Foreman's handleSdkError (zapier-sdk-tools.ts) currently catches it only via the generic ZapierError catch-all, surfacing .message/.code but DROPPING the approval URL. Add an explicit 'if (err instanceof ZapierApprovalError)' branch that returns the approval URL + approvalStatus so the agent can tell the user how to approve. Non-blocking for the bump (degrades gracefully today). Discovered during foreman-2xdk verification.","status":"closed","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T15:10:57Z","created_by":"HamChowderr","updated_at":"2026-06-11T22:39:56Z","closed_at":"2026-06-11T22:39:56Z","close_reason":"Done: handleSdkError now surfaces ZapierApprovalError.approvalUrl (code APPROVAL_REQUIRED), placed before the generic ZapierError catch-all; handleSdkError exported + 2 unit tests (with/without URL) pass; lint clean. Verified approvalUrl field exists in SDK 0.69.3.","dependencies":[{"issue_id":"foreman-elyi","depends_on_id":"foreman-2xdk","type":"discovered-from","created_at":"2026-06-11T08:10:57Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-d4w9","title":"[aimock] Add a fixture-match coverage check to CI","description":"Nothing verifies fixtures on disk match aimock.json declarations. Add scripts/verify-aimock-coverage.mjs that enumerates declared MCP tools + A2A agents and asserts each has a fixture; wire into .github/workflows/test.yml. Add a short fixture-authoring doc (and why catch-alls are an anti-pattern).","acceptance_criteria":"Coverage script exists, runs in CI, fails on any unmocked declaration; short fixture doc added.","status":"in_progress","priority":3,"issue_type":"chore","assignee":"HamChowderr","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:30:31Z","created_by":"HamChowderr","updated_at":"2026-06-11T21:37:31Z","started_at":"2026-06-11T21:37:31Z","dependencies":[{"issue_id":"foreman-d4w9","depends_on_id":"foreman-9zsp","type":"discovered-from","created_at":"2026-06-11T01:30:31Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-8bh9","title":"[durable] Post-v1 spike: publishWorkflowVersion + runDurable hello-world; evaluate cron-\u003ewatchTriggerInbox SSE","description":"Deferred (post-v1). Prove Zapier durable execution: publish a trivial workflow version, runDurable, poll getDurableRun to terminal, triggerWorkflow via tokenized URL. Separately evaluate replacing the minute-tick cron-driver with watchTriggerInbox SSE for real-time triggers (relates to the trigger-inbox spike). No production wiring.","acceptance_criteria":"Spike doc with call shapes, latency notes, and a recommendation on whether/when to augment. Marked post-v1 backlog.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:29:44Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:29:44Z","dependencies":[{"issue_id":"foreman-8bh9","depends_on_id":"foreman-iyq6","type":"discovered-from","created_at":"2026-06-11T01:29:44Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"foreman-13mw","title":"[trigger-inbox] Confirm long-term stability/sunset of experimental Triggers with Zapier","description":"Access works today (iyq6 notes), but the API is /experimental and the 0.51 changelog mentions a closed-beta notice. Ask Zapier whether trigger-inbox is the go-forward path, the stability/SLA, and any sunset risk. Low priority since we can already build/spike now; this de-risks long-term reliance and is a good Zapier-feedback touchpoint.","acceptance_criteria":"Documented answer from Zapier on stability/roadmap, or a logged attempt + explicit decision to proceed at our own risk.","status":"open","priority":3,"issue_type":"task","owner":"admin@otakusolutions.io","created_at":"2026-06-11T08:29:35Z","created_by":"HamChowderr","updated_at":"2026-06-11T08:29:35Z","dependencies":[{"issue_id":"foreman-13mw","depends_on_id":"foreman-iyq6","type":"discovered-from","created_at":"2026-06-11T01:29:34Z","created_by":"HamChowderr","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..521a9f7c --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/package-lock.json b/package-lock.json index fdf08a33..161cc7ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,13 +71,13 @@ "license": "MIT" }, "node_modules/@ai-sdk/gateway": { - "version": "3.0.127", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.127.tgz", - "integrity": "sha512-Obmw5hmE5x+ccRrMp/Djx5r0rpFVX87YqE6OY06g5fwYlRI30dA84ARfTzX45ivCvkW4eCnBpOVXVWQ/pjH85w==", + "version": "3.0.129", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.129.tgz", + "integrity": "sha512-KEQpZGJuCksc4iFxYtVHeHHG7yH0izGFzJLmRZlriI0hFIzJF9bT2AzJoaTHUI6minlxtP0WKYh84dP18o/Cuw==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "3.0.10", - "@ai-sdk/provider-utils": "4.0.27", + "@ai-sdk/provider-utils": "4.0.29", "@vercel/oidc": "3.2.0" }, "engines": { @@ -100,9 +100,9 @@ } }, "node_modules/@ai-sdk/provider-utils": { - "version": "4.0.27", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.27.tgz", - "integrity": "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw==", + "version": "4.0.29", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.29.tgz", + "integrity": "sha512-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "3.0.10", @@ -191,13 +191,13 @@ } }, "node_modules/@ai-sdk/react": { - "version": "3.0.203", - "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.203.tgz", - "integrity": "sha512-6YcuOw50Ay/q21FXytEaRDXsabEqwpp8YcJ+moEaImyQpd94OKEqFEDs62jLHkK8gW6Xw9nmKNSNpCglsHAJ4g==", + "version": "3.0.205", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.205.tgz", + "integrity": "sha512-qh4H9dUqTJwdr+arKSIrVBHG4o4sYJdZGFUCJfAf9vMe/5IwDRwhq9z5ztpcUrXD3QACO01nuyfufNc0ZoGReA==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider-utils": "4.0.27", - "ai": "6.0.201", + "@ai-sdk/provider-utils": "4.0.29", + "ai": "6.0.203", "swr": "^2.2.5", "throttleit": "2.1.0" }, @@ -870,9 +870,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz", - "integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.0.tgz", + "integrity": "sha512-4kURkd9hAPrdDM3C9n82ycYgx8hvQcW6MjKTEejruj8rK0N8P3OPpdy8BvI8kt3KWY4ycF5XtDOrktetEfhfuw==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -886,20 +886,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.4.16", - "@biomejs/cli-darwin-x64": "2.4.16", - "@biomejs/cli-linux-arm64": "2.4.16", - "@biomejs/cli-linux-arm64-musl": "2.4.16", - "@biomejs/cli-linux-x64": "2.4.16", - "@biomejs/cli-linux-x64-musl": "2.4.16", - "@biomejs/cli-win32-arm64": "2.4.16", - "@biomejs/cli-win32-x64": "2.4.16" + "@biomejs/cli-darwin-arm64": "2.5.0", + "@biomejs/cli-darwin-x64": "2.5.0", + "@biomejs/cli-linux-arm64": "2.5.0", + "@biomejs/cli-linux-arm64-musl": "2.5.0", + "@biomejs/cli-linux-x64": "2.5.0", + "@biomejs/cli-linux-x64-musl": "2.5.0", + "@biomejs/cli-win32-arm64": "2.5.0", + "@biomejs/cli-win32-x64": "2.5.0" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz", - "integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-Mn3Fwi3SA5fgmfCPqmzpWF2DLZnms3BVAhM088nTnGrTZmHS3wwIjcoZPqpXeNgd3DrrLH6xp8vTLIBuJoZiXw==", "cpu": [ "arm64" ], @@ -914,9 +914,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz", - "integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.0.tgz", + "integrity": "sha512-rg3VPL5P8mYro6pqlXYXuJWph21slVp3SZtAqWSrkZs40d2gTzYmHF8E/X1iTID25btmNKltNDJ926sqVBp7DQ==", "cpu": [ "x64" ], @@ -931,9 +931,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz", - "integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.0.tgz", + "integrity": "sha512-tl+LW8fdD96/xdeWtWwc82LIOc5CoY7N2AsogLTp5R4ECErYt+8Jl/N68ezN9vzSiqPTxw6vjcihoLPYKZHrlw==", "cpu": [ "arm64" ], @@ -951,9 +951,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz", - "integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-vQdM4oSGaf7ZNeGO9w5+Y8SBtyser9M6znxYbm7Ec8wInxJu1WiKxFYZW5Auj2d80bcVvefuGGRxoFOE0eee8g==", "cpu": [ "arm64" ], @@ -971,9 +971,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz", - "integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.0.tgz", + "integrity": "sha512-zpEGf4RQbFEh8Vt7OmavLyyOzRbtcE9osCqrS1kfvt8jDvxwhKXLSf7n0ebr/ov0RJ9ssP+lhs6C8a9WwFvrQA==", "cpu": [ "x64" ], @@ -991,9 +991,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz", - "integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-+9hIcMngJ+yGUahXqZuZ8CoWKJE9SAZsFsM3QDvXpNsLbXZ9lqVzgBhOk/jTSYkOA0GLP9eu3teukqpLUojHMg==", "cpu": [ "x64" ], @@ -1011,9 +1011,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz", - "integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.0.tgz", + "integrity": "sha512-jB0wAvTLI4itx5VidqVUejPQFhRUxiZ9l9FvZ26D5fl6t3qme+ZB4PD3bTSeL1vZ8NI2Rx/zj6H9zcESuGHKGw==", "cpu": [ "arm64" ], @@ -1028,9 +1028,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.16.tgz", - "integrity": "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.0.tgz", + "integrity": "sha512-VT/lF+GId+67j8aDfLkxdxNoVApsPSTbyAtB3jJq0IWTrY77WXfbPfpngxq0bA6JCEv/7k8C9qWjDRKRznDlyw==", "cpu": [ "x64" ], @@ -1958,6 +1958,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1979,6 +1980,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2017,9 +2019,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -2033,9 +2035,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -2049,9 +2051,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -2065,9 +2067,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -2081,9 +2083,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -2097,9 +2099,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -2113,9 +2115,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -2129,9 +2131,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -2145,9 +2147,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -2161,9 +2163,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -2177,9 +2179,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -2193,9 +2195,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -2209,9 +2211,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -2225,9 +2227,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -2241,9 +2243,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -2257,9 +2259,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -2273,9 +2275,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -2289,9 +2291,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -2305,9 +2307,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -2321,9 +2323,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -2337,9 +2339,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -2353,9 +2355,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -2369,9 +2371,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -2385,9 +2387,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -2401,9 +2403,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -2417,9 +2419,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -2525,9 +2527,9 @@ "link": true }, "node_modules/@fuma-translate/react": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@fuma-translate/react/-/react-0.0.3.tgz", - "integrity": "sha512-EGaUcCyXM2p1HMV6D/NM1kZjIwCMitO+lmDni7b/J0St8IAZ5cUddhtqPqYyMtFj6Og721B5dXSIN6IDq8wf/g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@fuma-translate/react/-/react-1.0.2.tgz", + "integrity": "sha512-uOiOtBx3nRXR8Nu1GzBf1tApgF1FErDBTHxRIAQeyQdyOoZbrNRN6H4kDCWObY4qyGeGbHydG0DHzgeUgFDMIw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3250,6 +3252,16 @@ } } }, + "node_modules/@inquirer/core/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/@inquirer/core/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3576,18 +3588,6 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -3762,9 +3762,9 @@ "license": "MIT" }, "node_modules/@mastra/ai-sdk": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@mastra/ai-sdk/-/ai-sdk-1.4.4.tgz", - "integrity": "sha512-03Mt0wQLK/80phsEM7P0BilIpXAW776vYxAb74qpBBRkuxP3WtHT2HThWMTqiLEV9v7P1c8iJ99VtLqXas44Gg==", + "version": "1.4.5-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/ai-sdk/-/ai-sdk-1.4.5-alpha.0.tgz", + "integrity": "sha512-lSochIlukdKI+rFxYXFpO7F7xxGme5CP1SOyTY1MwhvnswcQomSQVR5sIK7kYIYHXdHQE6YtJIqoJEfoN6FBKQ==", "license": "Apache-2.0", "engines": { "node": ">=22.13.0" @@ -3775,9 +3775,9 @@ } }, "node_modules/@mastra/core": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/@mastra/core/-/core-1.41.0.tgz", - "integrity": "sha512-A3gV8kdyO3xf4zIFgzUYutVIOhN4595mGoz0IpMrQPuGTYSVTMwSoUhqpKhvyRXt1UYZAOCt88qqI66lscNQtQ==", + "version": "1.42.0-alpha.3", + "resolved": "https://registry.npmjs.org/@mastra/core/-/core-1.42.0-alpha.3.tgz", + "integrity": "sha512-rMO4COXxWcrO2go9+xHZfaUJFG7L4JYPy5QHVI6lRVJsUszQ99ZYJa9upu4mYVR6oXv5DGgIDRMxpysnD7efHQ==", "license": "Apache-2.0", "dependencies": { "@a2a-js/sdk": "~0.3.13", @@ -3829,9 +3829,9 @@ } }, "node_modules/@mastra/deployer": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/@mastra/deployer/-/deployer-1.41.0.tgz", - "integrity": "sha512-85yn+JzYwzKaZsVZEfMIeRW+rjo9vxowB1IEwsntvhvFmfFkKpTUg0t4IPQfnL1G15PyUmMPKI6wM8vwmDvqbg==", + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/@mastra/deployer/-/deployer-1.42.0.tgz", + "integrity": "sha512-iRpCf+gP4OCkGBYfHUKLzARi6+4gFBIK/yVngYveE5f/IuUhIPYRBL1SBoYVDx6r25L9yyLrw7jstAFDggRgHQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -3839,7 +3839,7 @@ "@babel/preset-typescript": "^7.29.7", "@babel/traverse": "^7.29.7", "@hono/node-ws": "^1.3.0", - "@mastra/server": "1.41.0", + "@mastra/server": "1.42.0", "@optimize-lodash/rollup-plugin": "^5.1.0", "@rollup/plugin-alias": "6.0.0", "@rollup/plugin-commonjs": "29.0.2", @@ -3859,7 +3859,7 @@ "rollup": "^4.59.0", "rollup-plugin-esbuild": "^6.2.1", "strip-json-comments": "^5.0.3", - "tinyglobby": "^0.2.16", + "tinyglobby": "^0.2.17", "typescript-paths": "^1.5.2", "ws": "^8.20.0" }, @@ -3871,14 +3871,14 @@ } }, "node_modules/@mastra/deployer-cloudflare": { - "version": "1.1.42", - "resolved": "https://registry.npmjs.org/@mastra/deployer-cloudflare/-/deployer-cloudflare-1.1.42.tgz", - "integrity": "sha512-5Rd1KOaFkqpKW0cdzSibjO3BQpX2iia1GzZJXx1kvcCPShubiXiRXjDHVTeXInLFHxHaOEDQa6TpnzsMAWNMLw==", + "version": "1.1.43-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/deployer-cloudflare/-/deployer-cloudflare-1.1.43-alpha.0.tgz", + "integrity": "sha512-sDssXJ/7BSzKDSf/hlkZdgKb1JeLRvhMzEfE9AcVTazXBE+xycAt1eQD0FtIM3WIpsuAwttI15Fwp6Ohwn+svw==", "license": "Apache-2.0", "optional": true, "dependencies": { "@babel/core": "^7.29.7", - "@mastra/deployer": "^1.41.0", + "@mastra/deployer": "^1.42.0-alpha.0", "@rollup/plugin-virtual": "3.0.2", "cloudflare": "^5.2.0", "rollup": "^4.59.0", @@ -3899,13 +3899,13 @@ } }, "node_modules/@mastra/deployer-vercel": { - "version": "1.1.36", - "resolved": "https://registry.npmjs.org/@mastra/deployer-vercel/-/deployer-vercel-1.1.36.tgz", - "integrity": "sha512-f6GAt7X3374ISTxIpDzyp2XYO1nnqUrcY9PFkdmpMAsCSUN8yHys+TDs1+PSkZJ1JstnBoLkNY9lISzIsQCu1A==", + "version": "1.1.37-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/deployer-vercel/-/deployer-vercel-1.1.37-alpha.0.tgz", + "integrity": "sha512-YtG5YZeINXwJuJrnK0wtGTWl9GP2QYRpN1isNzICbOx+/fQI8+KfHWtMn7hQWsg67SaJshxiMIIfBpM2RqfhaA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@mastra/deployer": "^1.41.0", + "@mastra/deployer": "^1.42.0-alpha.0", "fs-extra": "^11.3.4" }, "engines": { @@ -3959,9 +3959,9 @@ } }, "node_modules/@mastra/duckdb": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@mastra/duckdb/-/duckdb-1.4.1.tgz", - "integrity": "sha512-GPr0SIdDGMtyjV4eVavzx1wsHOw9ILJ2g/dxsvf1hxwo5/UtdZHzwmo//APtXX0oFwzXs286rv3ZuDqojmmCXw==", + "version": "1.4.1-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/duckdb/-/duckdb-1.4.1-alpha.0.tgz", + "integrity": "sha512-/55TAhKJhlMd5sTc3GBGrj7WuA7jEHywDWTloaXIkCPpKjzqHH2RnCpnRHC4dMzvUKtgrAPs8L+R1fUvS2gRyg==", "license": "Apache-2.0", "dependencies": { "@duckdb/node-api": "^1.5.2-r.1" @@ -3974,9 +3974,9 @@ } }, "node_modules/@mastra/editor": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@mastra/editor/-/editor-0.11.1.tgz", - "integrity": "sha512-0EqAXwS26uuHrtTL2mmHx552IOCwKxWAdUPZvqb3PnXlpQZKgO1BeeC1iVO5bRA4WbiN+OYsEvIrLuUwflOU5A==", + "version": "0.11.2-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/editor/-/editor-0.11.2-alpha.0.tgz", + "integrity": "sha512-6RdnmabIHucTkbOG9CvMqNRQFHp8jYiAAhtmm0pKnhPpMHVYwGqAczr+BKZO8cJe8z4kgwkTd7kQzttCDiTsqA==", "dependencies": { "@arcadeai/arcadejs": "^2.3.0", "@composio/core": "^0.6.5", @@ -3997,10 +3997,33 @@ } } }, + "node_modules/@mastra/editor/node_modules/@mastra/memory": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@mastra/memory/-/memory-1.20.2.tgz", + "integrity": "sha512-J999LmWrEniRSz5MiLxtjX+lYwwASPrZI6+oSoW1h01rzODpw+0CMFIIzbVFluteD1BQLk8F/TGI4dkXDS0HHA==", + "license": "Apache-2.0", + "dependencies": { + "@mastra/schema-compat": "1.2.11", + "async-mutex": "^0.5.0", + "diff": "^8.0.3", + "image-size": "^2.0.2", + "json-schema": "^0.4.0", + "lru-cache": "^11.2.7", + "probe-image-size": "^7.2.3", + "tokenx": "^1.3.0", + "xxhash-wasm": "^1.1.0" + }, + "engines": { + "node": ">=22.13.0" + }, + "peerDependencies": { + "@mastra/core": ">=1.4.1-0 <2.0.0-0" + } + }, "node_modules/@mastra/evals": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@mastra/evals/-/evals-1.2.4.tgz", - "integrity": "sha512-9V3Jw71RZeVziCJbNmevNkrADnUP1Ck6FcNQXXGnvgNZXnuB6/ecDkk6IptfG/6Os7GTrIAhnWWYw4A9XarY+Q==", + "version": "1.2.4-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/evals/-/evals-1.2.4-alpha.0.tgz", + "integrity": "sha512-Bi3VpsrNFVHbiQq6dEQttNNGbsbs+nAZnN+ncWKDy4NvWWV98nG9TpC2iaETkW0AZN6nJnp1FZ4AOEml0dDTzg==", "license": "Apache-2.0", "dependencies": { "compromise": "^14.15.0", @@ -4016,9 +4039,9 @@ } }, "node_modules/@mastra/loggers": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@mastra/loggers/-/loggers-1.1.2.tgz", - "integrity": "sha512-1YdWiUzBX14ULWI+5oDX4eIHt8AD1F5nbx98xevybjd/HqKBPmJni/chcngod2sJZfDcx4NyQE87c/s1Cj+ULQ==", + "version": "1.1.2-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/loggers/-/loggers-1.1.2-alpha.0.tgz", + "integrity": "sha512-2R5tpUvWh2JJvgbQHbB8BsL2sc52z0q1b8PzOaj0/HJCs070pV2Gkqk2nBfPs4QPkVHWfMR4ncyqP9LRmbI+pg==", "license": "Apache-2.0", "dependencies": { "pino": "^10.3.1", @@ -4032,9 +4055,9 @@ } }, "node_modules/@mastra/memory": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@mastra/memory/-/memory-1.20.2.tgz", - "integrity": "sha512-J999LmWrEniRSz5MiLxtjX+lYwwASPrZI6+oSoW1h01rzODpw+0CMFIIzbVFluteD1BQLk8F/TGI4dkXDS0HHA==", + "version": "1.20.2-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/memory/-/memory-1.20.2-alpha.0.tgz", + "integrity": "sha512-B/L1DY0qFavrhY6BsHbOkPAPOkQQSfmpx8EgXI1+KLC3zl489LzZ8Zdl6V6c0GsHy2TCVNqT9cpcbfZugSIcvQ==", "license": "Apache-2.0", "dependencies": { "@mastra/schema-compat": "1.2.11", @@ -4055,9 +4078,9 @@ } }, "node_modules/@mastra/observability": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@mastra/observability/-/observability-1.14.1.tgz", - "integrity": "sha512-VKfn3mE1mNFOXgY9Pr9sy5L4q4xLnoLj501gFMOr0teNnRMvAiANVB+p4rnooDfM4i8nzLDG9icM80uMsgXTzQ==", + "version": "1.14.1-alpha.1", + "resolved": "https://registry.npmjs.org/@mastra/observability/-/observability-1.14.1-alpha.1.tgz", + "integrity": "sha512-6tuDV4T7LAtRmpjc9d9txBOhKMD1KQSaKigu+LaoT54HQwnJC+X7YCVDoxfUnP1/35wAaCPCIb36ddVh9Lb68Q==", "license": "Apache-2.0", "engines": { "node": ">=22.13.0" @@ -4068,9 +4091,9 @@ } }, "node_modules/@mastra/pg": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@mastra/pg/-/pg-1.12.1.tgz", - "integrity": "sha512-GTzUcqyoKTXeu+lxdDSOlfBXoxFcsdLX637jZxhiGWqMFtVK+6g6QeLl+5H+rl+W0ceCYVhzOT/N8fzPJBDV/g==", + "version": "1.13.0-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/pg/-/pg-1.13.0-alpha.0.tgz", + "integrity": "sha512-oHnUttVZTAHj2giYhZ82jVrDsDicfdNSdhkyJ8Tz9tEO9DuOKnRNtTIvCdyilXDRKoMEQ7OfFCX1yNQY/Z6k1A==", "license": "Apache-2.0", "dependencies": { "async-mutex": "^0.5.0", @@ -4086,9 +4109,9 @@ } }, "node_modules/@mastra/rag": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mastra/rag/-/rag-2.2.1.tgz", - "integrity": "sha512-Xt24rmypDC3OjoLDEWa6chbBsaqB8h67VX27dtYe/rS+0n+gSU4B/TeRjxKpy11afT0X9xgbnQj7RzZYX2BD9Q==", + "version": "2.2.1-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/rag/-/rag-2.2.1-alpha.0.tgz", + "integrity": "sha512-qbP6leSnKIoBM1ynOXzpdmjX7lPEo91/qbPscdPql0wWI1Rr/rwJIKpehyAKP/wtT5K6paTeAtQl/hYJHSIuqg==", "license": "Apache-2.0", "dependencies": { "@paralleldrive/cuid2": "^2.3.1", @@ -4125,9 +4148,9 @@ } }, "node_modules/@mastra/server": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/@mastra/server/-/server-1.41.0.tgz", - "integrity": "sha512-Dd0k2dJoi/Voe/3KNcUge1+h2SDA7GDHW+p26kXm/4nnBQfzfZPOdc5rne4ZY/FP/MH0g7nFhHtK74z2ut9A7w==", + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/@mastra/server/-/server-1.42.0.tgz", + "integrity": "sha512-3CXP5Mwh2OcFY8QdmPpqVWSJm4QcA9AD+Ln3PsLbBlcrlpzL2+w59SsbMau0sk9NjHyky6QRK29NzRhwG8hwug==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -4142,9 +4165,9 @@ } }, "node_modules/@mastra/voice-openai": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@mastra/voice-openai/-/voice-openai-0.12.2.tgz", - "integrity": "sha512-IICJBIiwda0aKU6d8uGz6OTmhr+09U4MQKAQJ89SSNgjgQXkNx31JQblp/3ffcpI4KSviu3DPiCiLFs7JzLiuw==", + "version": "0.12.2-alpha.0", + "resolved": "https://registry.npmjs.org/@mastra/voice-openai/-/voice-openai-0.12.2-alpha.0.tgz", + "integrity": "sha512-rTYeIYWt0L+h8uAJ1k+a+xogdYfRgsMZ2w1o3VJf2udmlTE6ykPsK6+n/eU/+b9AI3ZDLvyNNSUKFufEgoAMqA==", "license": "Apache-2.0", "dependencies": { "openai": "^5.23.2" @@ -4603,6 +4626,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5131,7 +5155,7 @@ "version": "0.133.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -5202,7 +5226,7 @@ "version": "1.60.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "playwright": "1.60.0" @@ -6818,6 +6842,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6834,6 +6859,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6850,6 +6876,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6866,6 +6893,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6882,6 +6910,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6898,6 +6927,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "glibc" ], @@ -6917,6 +6947,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "musl" ], @@ -6936,6 +6967,7 @@ "cpu": [ "ppc64" ], + "dev": true, "libc": [ "glibc" ], @@ -6955,6 +6987,7 @@ "cpu": [ "s390x" ], + "dev": true, "libc": [ "glibc" ], @@ -6974,6 +7007,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], @@ -6993,6 +7027,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], @@ -7012,6 +7047,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7028,6 +7064,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7046,6 +7083,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7062,6 +7100,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7075,7 +7114,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@rollup/plugin-alias": { @@ -7130,24 +7169,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/@rollup/plugin-commonjs/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -7872,9 +7893,9 @@ } }, "node_modules/@slack/web-api": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.16.0.tgz", - "integrity": "sha512-68SAV77uuGKuhyyaRytX8UijVnqSLsTSKslGXw17cjQYXn+jtNl7gbaEjHgC5x2rhCuFdahBrEC2VCLppbzReg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.17.0.tgz", + "integrity": "sha512-jejr34a8B4L5AS713wOAx1LAqNkW16HVMDEa6sYBvFDc/llUBl8hXaiI4BwF+Al+Sug19Vn2O7iokTVIhVvZ1Q==", "license": "MIT", "dependencies": { "@slack/logger": "^4.0.1", @@ -8106,52 +8127,53 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", - "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.21.0", - "jiti": "^2.6.1", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.0" + "tailwindcss": "4.3.1" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", - "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", - "devOptional": true, + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-x64": "4.3.0", - "@tailwindcss/oxide-freebsd-x64": "4.3.0", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-x64-musl": "4.3.0", - "@tailwindcss/oxide-wasm32-wasi": "4.3.0", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + "@tailwindcss/oxide-android-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-x64": "4.3.1", + "@tailwindcss/oxide-freebsd-x64": "4.3.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-x64-musl": "4.3.1", + "@tailwindcss/oxide-wasm32-wasi": "4.3.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", - "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8162,12 +8184,13 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", - "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8178,12 +8201,13 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", - "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8194,12 +8218,13 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", - "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8210,12 +8235,13 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", - "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8226,12 +8252,13 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", - "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", "cpu": [ "arm64" ], + "dev": true, "libc": [ "glibc" ], @@ -8245,12 +8272,13 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", - "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", "cpu": [ "arm64" ], + "dev": true, "libc": [ "musl" ], @@ -8264,12 +8292,13 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", - "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], @@ -8283,12 +8312,13 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", - "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], @@ -8302,9 +8332,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", - "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -8316,6 +8346,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -8323,7 +8354,7 @@ "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.1", + "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "engines": { @@ -8331,12 +8362,13 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", - "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8347,12 +8379,13 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", - "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8363,38 +8396,17 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.0.tgz", - "integrity": "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.1.tgz", + "integrity": "sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.3.0", - "@tailwindcss/oxide": "4.3.0", - "postcss": "^8.5.10", - "tailwindcss": "4.3.0" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "postcss": "8.5.15", + "tailwindcss": "4.3.1" } }, "node_modules/@testing-library/jest-dom": { @@ -8417,13 +8429,6 @@ "yarn": ">=1" } }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT" - }, "node_modules/@testing-library/react": { "version": "16.3.2", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", @@ -8456,20 +8461,13 @@ "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/@types/babel__traverse": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", @@ -8902,7 +8900,7 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -8912,7 +8910,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -9295,9 +9293,9 @@ } }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -9340,14 +9338,14 @@ } }, "node_modules/ai": { - "version": "6.0.201", - "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.201.tgz", - "integrity": "sha512-iDmXSy7csk+SYaCkTUMwsOE/73pksR1spctV+u+DW97v+R/Cj01gchgCUNVrn9QyNHqOJQuTg+JBVsBsp6lOag==", + "version": "6.0.203", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.203.tgz", + "integrity": "sha512-2Qi1ZPGF/FnlvnRqntVgRbUYGeA5ZKFYwTtgu8rcUzMmddArM/nLsvCW69Ip99B1cop6XHRHl+GCKk9t9B+GDA==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "3.0.127", + "@ai-sdk/gateway": "3.0.129", "@ai-sdk/provider": "3.0.10", - "@ai-sdk/provider-utils": "4.0.27", + "@ai-sdk/provider-utils": "4.0.29", "@opentelemetry/api": "^1.9.0" }, "engines": { @@ -9401,23 +9399,24 @@ } }, "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==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", - "peer": true, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -9508,13 +9507,13 @@ } }, "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" + "engines": { + "node": ">= 0.4" } }, "node_modules/asap": { @@ -9726,12 +9725,13 @@ } }, "node_modules/bare-stream": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.2.tgz", - "integrity": "sha512-2V5j0dOfxv3iIngIWMnW1O6UPXpeoU70HJzUJGVth/+RURhDyq7SEWTD70Y2SkUB0MQonYYWpIX3f85P/I22+Q==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", "dev": true, "license": "Apache-2.0", "dependencies": { + "b4a": "^1.8.1", "streamx": "^2.25.0", "teex": "^1.0.1" }, @@ -9783,9 +9783,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.35", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz", - "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==", + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -9965,19 +9965,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/boxen/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/boxen/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -11232,7 +11219,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/cytoscape": { @@ -12110,17 +12097,16 @@ } }, "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.9.tgz", - "integrity": "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==", + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.10.tgz", + "integrity": "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -12215,9 +12201,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.371", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", - "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==", + "version": "1.5.372", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", "devOptional": true, "license": "ISC" }, @@ -12327,9 +12313,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz", - "integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==", + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12419,9 +12405,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", - "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==", + "version": "1.47.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.1.tgz", + "integrity": "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==", "license": "MIT", "workspaces": [ "docs", @@ -12461,9 +12447,9 @@ } }, "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -12473,32 +12459,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -12957,6 +12943,23 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -13118,16 +13121,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -13299,12 +13302,12 @@ } }, "node_modules/fumadocs-core": { - "version": "16.10.0", - "resolved": "https://registry.npmjs.org/fumadocs-core/-/fumadocs-core-16.10.0.tgz", - "integrity": "sha512-DYAYVh83RCglrJ9eBjXJ0xCMv7yhiUfj3RVHlj2QRv4UuUPhtKkA4K1Tu49qvG1MjZQdTuP8B2A0WLiFijXDgg==", + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/fumadocs-core/-/fumadocs-core-16.10.2.tgz", + "integrity": "sha512-7YMEkUYQYYey+8yzZW47jV3pbctCXsevFY6u4yN9PRwJi/q76QOFfefvzSkbYha3OIudr2C0RGzJxEhzV2a1/g==", "license": "MIT", "dependencies": { - "@fuma-translate/react": "^0.0.3", + "@fuma-translate/react": "^1.0.2", "@orama/orama": "^3.1.18", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", @@ -13573,23 +13576,23 @@ } }, "node_modules/fumadocs-ui": { - "version": "16.10.0", - "resolved": "https://registry.npmjs.org/fumadocs-ui/-/fumadocs-ui-16.10.0.tgz", - "integrity": "sha512-pSqtHX4rxYoALY7j6k32oK3rWmDESkaeUqUJxFiIkl7tCh4NXkkFAPURQSSzPYgy6NWC5qu6ellrbw9LnjWIQg==", + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/fumadocs-ui/-/fumadocs-ui-16.10.2.tgz", + "integrity": "sha512-l8fYpahaLVA73XUktsjTdq6iwG6V0/UGAnN1uuCeVuV4ea/8RDduOo9CcMM62SCup0EnT1zEAk+FGFCdKQ4BhQ==", "license": "MIT", "dependencies": { - "@fuma-translate/react": "^0.0.3", + "@fuma-translate/react": "^1.0.2", "@fumadocs/tailwind": "0.0.5", - "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-direction": "^1.1.1", - "@radix-ui/react-navigation-menu": "^1.2.14", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-presence": "^1.1.5", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-accordion": "^1.2.13", + "@radix-ui/react-collapsible": "^1.1.13", + "@radix-ui/react-dialog": "^1.1.16", + "@radix-ui/react-direction": "^1.1.2", + "@radix-ui/react-navigation-menu": "^1.2.15", + "@radix-ui/react-popover": "^1.1.16", + "@radix-ui/react-presence": "^1.1.6", + "@radix-ui/react-scroll-area": "^1.2.11", + "@radix-ui/react-slot": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.14", "class-variance-authority": "^0.7.1", "lucide-react": "^1.17.0", "motion": "^12.40.0", @@ -13605,7 +13608,7 @@ "@takumi-rs/image-response": "*", "@types/mdx": "*", "@types/react": "*", - "fumadocs-core": "16.10.0", + "fumadocs-core": "16.10.2", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0" @@ -14059,16 +14062,6 @@ "node": ">=8.0.0" } }, - "node_modules/graphql": { - "version": "16.14.2", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", - "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", - "license": "MIT", - "peer": true, - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, "node_modules/gray-matter": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", @@ -15166,7 +15159,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -15590,7 +15583,7 @@ "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "devOptional": true, + "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -15623,6 +15616,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15643,6 +15637,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15663,6 +15658,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15683,6 +15679,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15703,6 +15700,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15723,6 +15721,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "glibc" ], @@ -15746,6 +15745,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "musl" ], @@ -15769,6 +15769,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], @@ -15792,6 +15793,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], @@ -15815,6 +15817,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15835,6 +15838,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -16065,25 +16069,14 @@ "license": "ISC" }, "node_modules/lucide-react": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz", - "integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.18.0.tgz", + "integrity": "sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/magic-bytes.js": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz", @@ -16174,9 +16167,9 @@ } }, "node_modules/mastra": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/mastra/-/mastra-1.12.2.tgz", - "integrity": "sha512-KYMmL4SIDY7WJ9Oc5b4sxrmwUZScGpNoxemCKVb6v6YODdIE+PC2I+/t77tBsJ4NDgUCm4UPGPzVWD9ToDdcUw==", + "version": "1.12.3-alpha.3", + "resolved": "https://registry.npmjs.org/mastra/-/mastra-1.12.3-alpha.3.tgz", + "integrity": "sha512-IsYnqvnE7inOEYmEXIvuc9LrjvJw1rRFoNvBUxYn37reogaUJndWbcTYhgluF3W7odmIBEhqwP2pMnzW2lcVzA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -16184,7 +16177,7 @@ "@babel/types": "^7.29.7", "@clack/prompts": "^1.1.0", "@expo/devcert": "^1.2.1", - "@mastra/deployer": "^1.41.0", + "@mastra/deployer": "^1.42.0-alpha.3", "@mastra/loggers": "^1.1.2", "archiver": "^7.0.1", "commander": "^14.0.3", @@ -16214,6 +16207,23 @@ "@mastra/core": ">=1.34.0-0 <2.0.0-0" } }, + "node_modules/mastra/node_modules/@mastra/loggers": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@mastra/loggers/-/loggers-1.1.2.tgz", + "integrity": "sha512-1YdWiUzBX14ULWI+5oDX4eIHt8AD1F5nbx98xevybjd/HqKBPmJni/chcngod2sJZfDcx4NyQE87c/s1Cj+ULQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "pino": "^10.3.1", + "pino-pretty": "^13.1.3" + }, + "engines": { + "node": ">=22.13.0" + }, + "peerDependencies": { + "@mastra/core": ">=1.0.0-0 <2.0.0-0" + } + }, "node_modules/mastra/node_modules/commander": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", @@ -18017,9 +18027,9 @@ } }, "node_modules/obug": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", - "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "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", @@ -18760,7 +18770,7 @@ "version": "1.60.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.60.0" @@ -18779,7 +18789,7 @@ "version": "1.60.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -18879,12 +18889,12 @@ } }, "node_modules/posthog-node": { - "version": "5.36.17", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.36.17.tgz", - "integrity": "sha512-ed1LT4a9hhiFJizB6XX7dkYYLVPAFHfUpkQSns7BRxoUyhFnvMq15QENKeAOUEKQgPmnaq2I+xNLdAHN0o9eAA==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.37.0.tgz", + "integrity": "sha512-wFwWGcqAqZ1WJRlNNYc92veV83d1lOQcP4Lq0q7Kar9GdZLPpiFYHeudyybYJnjZjkI9v06vLvY/Og5CZIfByg==", "license": "MIT", "dependencies": { - "@posthog/core": "1.32.3" + "@posthog/core": "^1.32.3" }, "engines": { "node": "^20.20.0 || >=22.22.0" @@ -18926,22 +18936,6 @@ "node": ">=10" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "node_modules/pretty-ms": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", @@ -19461,7 +19455,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -19510,7 +19503,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -19518,13 +19510,6 @@ "react": "^19.2.7" } }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT", - "peer": true - }, "node_modules/react-redux": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", @@ -20231,7 +20216,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.133.0", @@ -20424,7 +20409,7 @@ "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -21244,9 +21229,9 @@ } }, "node_modules/streamx": { - "version": "2.27.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.27.0.tgz", - "integrity": "sha512-WZ189TKnHoAokYHvwzaAQMpd55cgUmFIcJFzBSgGcb886jau5DL+XdDhTWV4ps3FLvk+OORp0dLRTPsLZ21CSA==", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", "dev": true, "license": "MIT", "dependencies": { @@ -21301,6 +21286,15 @@ "node": ">=8" } }, + "node_modules/string-width-cjs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -21313,6 +21307,15 @@ "node": ">=8" } }, + "node_modules/string-width/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -21367,16 +21370,13 @@ "node": ">=8" } }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/strip-ansi-cjs/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==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=8" } }, "node_modules/strip-bom-string": { @@ -21591,10 +21591,10 @@ } }, "node_modules/tailwindcss": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", - "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", - "devOptional": true, + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", + "dev": true, "license": "MIT" }, "node_modules/tapable": { @@ -21757,23 +21757,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -21944,7 +21927,7 @@ "version": "4.22.4", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.28.0" @@ -21963,6 +21946,7 @@ "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, @@ -22049,6 +22033,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -22589,7 +22574,7 @@ "version": "8.0.16", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", @@ -22667,6 +22652,7 @@ "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, @@ -22681,7 +22667,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -22989,6 +22975,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -23016,19 +23011,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", @@ -23259,9 +23241,9 @@ } }, "node_modules/zod-from-json-schema": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/zod-from-json-schema/-/zod-from-json-schema-0.5.2.tgz", - "integrity": "sha512-/dNaicfdhJTOuUd4RImbLUE2g5yrSzzDjI/S6C2vO2ecAGZzn9UcRVgtyLSnENSmAOBRiSpUdzDS6fDWX3Z35g==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/zod-from-json-schema/-/zod-from-json-schema-0.5.3.tgz", + "integrity": "sha512-44YFiuq+WHw9YZQAo/Ad0F7o9c/im0Q6cnHI23BsXhEmZtkNn4cD0bljLMMjkfb/EidopPWdsmKI8EvLHX5ZyA==", "license": "MIT", "dependencies": { "zod": "^4.0.17" @@ -23310,17 +23292,17 @@ "@chat-adapter/telegram": "^4.28.1", "@chat-adapter/whatsapp": "^4.28.1", "@hono/node-server": "^1.19.14", - "@mastra/ai-sdk": "^1.4.4", - "@mastra/core": "^1.41.0", - "@mastra/duckdb": "^1.4.1", - "@mastra/editor": "^0.11.1", - "@mastra/evals": "^1.2.4", - "@mastra/loggers": "^1.1.2", - "@mastra/memory": "^1.20.2", - "@mastra/observability": "^1.14.1", - "@mastra/pg": "^1.12.1", - "@mastra/rag": "^2.2.1", - "@mastra/voice-openai": "^0.12.2", + "@mastra/ai-sdk": "1.4.5-alpha.0", + "@mastra/core": "1.42.0-alpha.3", + "@mastra/duckdb": "1.4.1-alpha.0", + "@mastra/editor": "0.11.2-alpha.0", + "@mastra/evals": "1.2.4-alpha.0", + "@mastra/loggers": "1.1.2-alpha.0", + "@mastra/memory": "1.20.2-alpha.0", + "@mastra/observability": "1.14.1-alpha.1", + "@mastra/pg": "1.13.0-alpha.0", + "@mastra/rag": "2.2.1-alpha.0", + "@mastra/voice-openai": "0.12.2-alpha.0", "@supabase/supabase-js": "^2.105.4", "@zapier/zapier-sdk": "^0.69.3", "ai": "^6.0.177", @@ -23346,14 +23328,14 @@ "@copilotkit/aimock": "^1.14.3", "@types/node": "^20", "@zapier/zapier-sdk-cli": "^0.54.3", - "mastra": "^1.12.2", + "mastra": "1.12.3-alpha.3", "typescript": "^5", "vitest": "^4.1.4", "zlib-sync": "^0.1.10" }, "optionalDependencies": { - "@mastra/deployer-cloudflare": "^1.1.42", - "@mastra/deployer-vercel": "^1.1.36" + "@mastra/deployer-cloudflare": "1.1.43-alpha.0", + "@mastra/deployer-vercel": "1.1.37-alpha.0" } }, "packages/web": { @@ -23408,10 +23390,10 @@ "prosemirror-state": "^1.4.3", "prosemirror-view": "^1.34.3", "radix-ui": "^1.4.3", - "react": "19.2.4", + "react": "^19.2.0", "react-data-grid": "7.0.0-beta.47", "react-day-picker": "^10.0.0", - "react-dom": "19.2.4", + "react-dom": "^19.2.0", "react-resizable-panels": "^4.11.0", "recharts": "^3.8.0", "shiki": "^3.23.0", @@ -23460,23 +23442,6 @@ "engines": { "node": "^18 || >=20" } - }, - "packages/web/node_modules/react": { - "version": "19.2.4", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "packages/web/node_modules/react-dom": { - "version": "19.2.4", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } } } } diff --git a/package.json b/package.json index 58b84c43..e0abb3d8 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "esbuild": ">=0.25.0", "zod": "4.4.3", "postcss": "^8.5.10", - "@mastra/core": "1.41.0" + "@mastra/core": "1.42.0-alpha.3" }, "devDependencies": { "@biomejs/biome": "^2.4.12", diff --git a/packages/agents/.npmrc b/packages/agents/.npmrc new file mode 100644 index 00000000..521a9f7c --- /dev/null +++ b/packages/agents/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/packages/agents/package.json b/packages/agents/package.json index f7ec4bd8..f399453b 100644 --- a/packages/agents/package.json +++ b/packages/agents/package.json @@ -47,17 +47,17 @@ "@chat-adapter/telegram": "^4.28.1", "@chat-adapter/whatsapp": "^4.28.1", "@hono/node-server": "^1.19.14", - "@mastra/ai-sdk": "^1.4.4", - "@mastra/core": "^1.41.0", - "@mastra/duckdb": "^1.4.1", - "@mastra/editor": "^0.11.1", - "@mastra/evals": "^1.2.4", - "@mastra/loggers": "^1.1.2", - "@mastra/memory": "^1.20.2", - "@mastra/observability": "^1.14.1", - "@mastra/pg": "^1.12.1", - "@mastra/rag": "^2.2.1", - "@mastra/voice-openai": "^0.12.2", + "@mastra/ai-sdk": "1.4.5-alpha.0", + "@mastra/core": "1.42.0-alpha.3", + "@mastra/duckdb": "1.4.1-alpha.0", + "@mastra/editor": "0.11.2-alpha.0", + "@mastra/evals": "1.2.4-alpha.0", + "@mastra/loggers": "1.1.2-alpha.0", + "@mastra/memory": "1.20.2-alpha.0", + "@mastra/observability": "1.14.1-alpha.1", + "@mastra/pg": "1.13.0-alpha.0", + "@mastra/rag": "2.2.1-alpha.0", + "@mastra/voice-openai": "0.12.2-alpha.0", "@supabase/supabase-js": "^2.105.4", "@zapier/zapier-sdk": "^0.69.3", "ai": "^6.0.177", @@ -80,14 +80,14 @@ "zod": "^4.3.6" }, "optionalDependencies": { - "@mastra/deployer-cloudflare": "^1.1.42", - "@mastra/deployer-vercel": "^1.1.36" + "@mastra/deployer-cloudflare": "1.1.43-alpha.0", + "@mastra/deployer-vercel": "1.1.37-alpha.0" }, "devDependencies": { "@copilotkit/aimock": "^1.14.3", "@types/node": "^20", "@zapier/zapier-sdk-cli": "^0.54.3", - "mastra": "^1.12.2", + "mastra": "1.12.3-alpha.3", "typescript": "^5", "vitest": "^4.1.4", "zlib-sync": "^0.1.10" diff --git a/packages/agents/src/cron-driver-server.ts b/packages/agents/src/cron-driver-server.ts index 5aa63955..d3e1ad08 100644 --- a/packages/agents/src/cron-driver-server.ts +++ b/packages/agents/src/cron-driver-server.ts @@ -1,18 +1,33 @@ /** - * Standalone entrypoint for the cron driver. Run with: + * Standalone entrypoint for the trigger drivers. Run with: * node --env-file=.env.local src/cron-driver-server.ts * * Keeps a long-lived process that ticks once per minute and fires due - * `workflow_trigger` rows. Only run a single instance — there is no - * distributed lock. + * `workflow_trigger` rows: the cron driver (schedule matches) and the poll + * driver (Zapier read actions returning new records). Only run a single + * instance — there is no distributed lock. */ +import { getMastra } from "./mastra"; +import { zapierPollProvider } from "./mastra/signals/zapier-poll-provider"; import { startCronDriver } from "./workflows/cron-driver"; +import { startPollDriver } from "./workflows/poll-driver"; -const stop = startCronDriver(); +// Build the Mastra instance here so the foreman agent is constructed in THIS +// worker process — that connects the SignalProviders (poll/channel) and gives +// them the notification storage notify() needs. Without it, runDuePolls() fires +// workflows fine but notifyOwner() is a silent no-op (no connected agent). The +// provider has no pollInterval, so constructing the agent here doesn't start a +// second poll loop. (foreman notify-wiring) +getMastra(); +console.log(`[trigger-driver] poll provider connected: ${zapierPollProvider.isConnected}`); + +const stopCron = startCronDriver(); +const stopPoll = startPollDriver(); const shutdown = (sig: string) => { - console.log(`[cron-driver] received ${sig}, shutting down`); - stop(); + console.log(`[trigger-driver] received ${sig}, shutting down`); + stopCron(); + stopPoll(); process.exit(0); }; process.on("SIGINT", () => shutdown("SIGINT")); diff --git a/packages/agents/src/discord/bot.ts b/packages/agents/src/discord/bot.ts index 056c8e23..e2468a40 100644 --- a/packages/agents/src/discord/bot.ts +++ b/packages/agents/src/discord/bot.ts @@ -123,6 +123,7 @@ export async function getDiscordBot() { channel: "discord", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -144,6 +145,7 @@ export async function getDiscordBot() { channel: "discord", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/gchat/bot.ts b/packages/agents/src/gchat/bot.ts index 79ba6075..f1e413b6 100644 --- a/packages/agents/src/gchat/bot.ts +++ b/packages/agents/src/gchat/bot.ts @@ -62,6 +62,7 @@ export async function getGoogleChatBot() { channel: "gchat", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -88,6 +89,7 @@ export async function getGoogleChatBot() { channel: "gchat", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/github/bot.ts b/packages/agents/src/github/bot.ts index 120d9cd5..ede14729 100644 --- a/packages/agents/src/github/bot.ts +++ b/packages/agents/src/github/bot.ts @@ -65,6 +65,7 @@ export async function getGitHubBot() { channel: "github", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -91,6 +92,7 @@ export async function getGitHubBot() { channel: "github", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/lib/background.ts b/packages/agents/src/lib/background.ts new file mode 100644 index 00000000..e55154e3 --- /dev/null +++ b/packages/agents/src/lib/background.ts @@ -0,0 +1,22 @@ +/** + * Gated background-execution config for agent tools (foreman-7am4). + * + * Returns `{ background: { enabled: true } }` when FOREMAN_BACKGROUND_TOOLS=1, + * else `{}` — spread into a tool's createTool() config to opt it into Mastra + * background execution. + * + * Apply ONLY to simple-schema custom tools. Live verification showed that + * backgrounding the generated Zapier SDK tools hangs `mastra dev`: enabling + * background makes Mastra inject a `_background` field into the tool's input + * schema, and Studio's `toJSONSchema` introspection over the Zapier tools' + * (pathologically complex) zod schemas then loops forever — the same failure the + * lazy-init-zapier regression test guards. `mastra build` is unaffected (no + * Studio introspection), but the dev playground is. Simple schemas (e.g. + * search_history's 3 fields) inject + introspect fine. + * + * Read at call time so tests can toggle the flag. OFF by default — this changes + * live tool dispatch and is alpha-unverified end-to-end, so it ships inert. + */ +export function backgroundIfEnabled(): { background?: { enabled: true } } { + return process.env.FOREMAN_BACKGROUND_TOOLS === "1" ? { background: { enabled: true } } : {}; +} diff --git a/packages/agents/src/lib/catalog/index.ts b/packages/agents/src/lib/catalog/index.ts index b16824d0..ff138020 100644 --- a/packages/agents/src/lib/catalog/index.ts +++ b/packages/agents/src/lib/catalog/index.ts @@ -1,2 +1,3 @@ +export { resolveAppSlug } from "./resolve"; export { seedCatalog } from "./seed"; export { ensureCatalogIndex, indexAppCatalog, searchAppCatalog } from "./vector"; diff --git a/packages/agents/src/lib/catalog/resolve.ts b/packages/agents/src/lib/catalog/resolve.ts new file mode 100644 index 00000000..9ea77b09 --- /dev/null +++ b/packages/agents/src/lib/catalog/resolve.ts @@ -0,0 +1,37 @@ +import { getSupabase } from "../db"; + +/** + * Resolve any Zapier app identifier to its canonical slug, using the + * `app_catalog` table (seeded from `sdk.listApps`) as the source of truth. + * + * The catalog stores BOTH the Zapier `app_key` (e.g. "GitHubCLIAPI") and the + * display `slug` (e.g. "github"), so it knows the correct mapping. This replaces + * the string-munging `normalizeAppKey`, which can't reliably derive a slug from + * a CamelCase implementation name: "GitHubCLIAPI" → "git-hub" is wrong (the real + * slug is "github"), while "GoogleSheetsV2CLIAPI" → "google-sheets" needs the + * dash. You can't tell those apart from the string alone — only the catalog can. + * (foreman-c8fo) + * + * Accepts either form (app_key or slug, case-insensitive) and returns the slug. + * If the app isn't in the catalog (e.g. an unseeded local DB), the key is + * returned UNCHANGED — the Zapier SDK accepts both raw app_keys and slugs, so + * passing the original through is safe; only the mangled form ever broke. + */ +export async function resolveAppSlug(key: string): Promise { + if (!key) return key; + // App keys/slugs are simple identifiers; guard the PostgREST `or` filter. + if (!/^[\w.-]+$/.test(key)) return key; + + try { + const { data } = await getSupabase() + .from("app_catalog") + .select("slug") + .or(`app_key.ilike.${key},slug.ilike.${key}`) + .limit(1) + .maybeSingle(); + if (data?.slug) return data.slug; + } catch { + // catalog unavailable / not seeded — fall through to pass-through + } + return key; +} diff --git a/packages/agents/src/lib/db/database.types.ts b/packages/agents/src/lib/db/database.types.ts index 9b28c8f1..10fab1be 100644 --- a/packages/agents/src/lib/db/database.types.ts +++ b/packages/agents/src/lib/db/database.types.ts @@ -3429,25 +3429,34 @@ export type Database = { Row: { completed_at: string | null; created_at: string; + error_message: string | null; + fired_by: string | null; id: string; inputs: string; status: string; + trigger_id: string | null; workflow_id: string; }; Insert: { completed_at?: string | null; created_at: string; + error_message?: string | null; + fired_by?: string | null; id: string; inputs: string; status: string; + trigger_id?: string | null; workflow_id: string; }; Update: { completed_at?: string | null; created_at?: string; + error_message?: string | null; + fired_by?: string | null; id?: string; inputs?: string; status?: string; + trigger_id?: string | null; workflow_id?: string; }; Relationships: [ diff --git a/packages/agents/src/lib/workflows/engine.ts b/packages/agents/src/lib/workflows/engine.ts index 3c6f9d48..84958f95 100644 --- a/packages/agents/src/lib/workflows/engine.ts +++ b/packages/agents/src/lib/workflows/engine.ts @@ -15,6 +15,16 @@ export interface WorkflowEvent { missing?: string[]; } +/** How a run started — recorded on workflow_run so trigger-fired runs carry + * context in run history. (foreman-2afc) */ +export type FiredBy = "manual" | "cron" | "channel" | "poll"; + +export interface WorkflowTriggerContext { + firedBy: FiredBy; + /** The workflow_trigger row that fired this run (null for manual runs). */ + triggerId?: string; +} + /** * Execute a saved workflow, yielding SSE-friendly events as each step runs. */ @@ -23,6 +33,7 @@ export async function* executeWorkflow( userId: string, inputs: Record, _orgId?: string, + trigger: WorkflowTriggerContext = { firedBy: "manual" }, ): AsyncGenerator { const supabase = getSupabase(); @@ -59,74 +70,108 @@ export async function* executeWorkflow( inputs: JSON.stringify(inputs), status: "running", created_at: now, + fired_by: trigger.firedBy, + trigger_id: trigger.triggerId ?? null, }); - yield { type: "status", runId, status: "running" }; - - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const template = JSON.parse(step.proposal_template) as Record; - const label = (template.humanLabel as string) ?? `Step ${i + 1}`; - - yield { - type: "step", - runId, - stepIndex: i, - stepId: step.id, - status: "running", - label, - }; - - try { - const rawInputs = (template.inputs ?? {}) as Record; - const resolved = substituteParams(rawInputs, inputs); - - const result = await runAction( - userId, - template.appKey as string, - template.actionType as string, - template.actionKey as string, - resolved, - template.connectionId as string | undefined, - ); - - yield { - type: "step", - runId, - stepIndex: i, - stepId: step.id, - status: "complete", - label, - result, - }; - } catch (err) { - const errorMsg = err instanceof Error ? err.message : String(err); + // Own the run's terminal state in a try/finally so the row can never get + // stuck in 'running'. `settled` flips at each terminal write (step failure or + // success). The finally catches the cases the old code missed: the caller + // abandons the generator mid-stream (for-await break → generator .return()), + // or an unexpected error propagates out of a step. The status yield lives + // inside the try so an abandon at any point after the row exists is covered. + // (foreman-2afc) + let settled = false; + try { + yield { type: "status", runId, status: "running" }; + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const template = JSON.parse(step.proposal_template) as Record; + const label = (template.humanLabel as string) ?? `Step ${i + 1}`; yield { type: "step", runId, stepIndex: i, stepId: step.id, - status: "failed", + status: "running", label, - error: errorMsg, }; - await supabase - .from("workflow_run") - .update({ status: "failed", completed_at: new Date().toISOString() }) - .eq("id", runId); + try { + const rawInputs = (template.inputs ?? {}) as Record; + const resolved = substituteParams(rawInputs, inputs); + + const result = await runAction( + userId, + template.appKey as string, + template.actionType as string, + template.actionKey as string, + resolved, + template.connectionId as string | undefined, + ); + + yield { + type: "step", + runId, + stepIndex: i, + stepId: step.id, + status: "complete", + label, + result, + }; + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + + yield { + type: "step", + runId, + stepIndex: i, + stepId: step.id, + status: "failed", + label, + error: errorMsg, + }; + + await supabase + .from("workflow_run") + .update({ + status: "failed", + completed_at: new Date().toISOString(), + error_message: errorMsg, + }) + .eq("id", runId); + settled = true; + + yield { type: "error", runId, message: errorMsg }; + return; + } + } - yield { type: "error", runId, message: errorMsg }; - return; + // All steps succeeded + await supabase + .from("workflow_run") + .update({ status: "success", completed_at: new Date().toISOString() }) + .eq("id", runId); + settled = true; + + yield { type: "complete", runId, status: "success" }; + } finally { + if (!settled) { + try { + await supabase + .from("workflow_run") + .update({ + status: "failed", + completed_at: new Date().toISOString(), + error_message: + "run did not complete (generator abandoned or aborted before a terminal state)", + }) + .eq("id", runId); + } catch (e) { + console.error(`[engine] failed to mark abandoned run ${runId} failed:`, e); + } } } - - // All steps succeeded - await supabase - .from("workflow_run") - .update({ status: "success", completed_at: new Date().toISOString() }) - .eq("id", runId); - - yield { type: "complete", runId, status: "success" }; } diff --git a/packages/agents/src/lib/workflows/save.ts b/packages/agents/src/lib/workflows/save.ts index 3ecfae54..8fd77fef 100644 --- a/packages/agents/src/lib/workflows/save.ts +++ b/packages/agents/src/lib/workflows/save.ts @@ -1,5 +1,5 @@ +import { resolveAppSlug } from "@/lib/catalog/resolve"; import { getSupabase } from "@/lib/db"; -import { normalizeAppKey } from "@/lib/zapier/normalize"; import { extractParams } from "./params"; /** Heuristic: values that look like they should become parameters. */ @@ -89,7 +89,7 @@ export async function saveWorkflowFromConversation( } const template = { - appKey: normalizeAppKey(p.app_key), + appKey: await resolveAppSlug(p.app_key), actionType: p.action_type, actionKey: p.action_key, humanLabel: p.human_label, diff --git a/packages/agents/src/lib/zapier/execution.ts b/packages/agents/src/lib/zapier/execution.ts index 7fdc64cb..6090f248 100644 --- a/packages/agents/src/lib/zapier/execution.ts +++ b/packages/agents/src/lib/zapier/execution.ts @@ -1,8 +1,8 @@ import { checkCapability } from "../capabilities"; +import { resolveAppSlug } from "../catalog/resolve"; import { runGuardrails } from "../guardrails"; import { resolveConnection } from "./aliases"; import { ZapierActionFailed, ZapierCapabilityDenied } from "./errors"; -import { normalizeAppKey } from "./normalize"; import { getSdkForUser } from "./sdk"; type ActionType = @@ -42,7 +42,10 @@ export async function runAction( inputs: Record, connection?: string, ) { - app = normalizeAppKey(app); + // Resolve to the canonical slug via the catalog (the source of truth). The SDK + // accepts both the raw app_key and the slug; only the old string-munged form + // ("GitHubCLIAPI" → "git-hub") broke. (foreman-c8fo) + app = await resolveAppSlug(app); const capability = ACTION_TYPE_CAPABILITY[actionType] ?? "execute"; await requireCapability(userId, capability); diff --git a/packages/agents/src/linear/bot.ts b/packages/agents/src/linear/bot.ts index 73e62491..e2c44108 100644 --- a/packages/agents/src/linear/bot.ts +++ b/packages/agents/src/linear/bot.ts @@ -63,6 +63,7 @@ export async function getLinearBot() { channel: "linear", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -89,6 +90,7 @@ export async function getLinearBot() { channel: "linear", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/mastra/agents/foreman.ts b/packages/agents/src/mastra/agents/foreman.ts index beea31d9..bf60b787 100644 --- a/packages/agents/src/mastra/agents/foreman.ts +++ b/packages/agents/src/mastra/agents/foreman.ts @@ -17,6 +17,8 @@ import { toolsWithCacheControl, } from "../../lib/providers"; import { generateZapierTools } from "../../lib/zapier-sdk-tools"; +import { channelTriggerProvider } from "../signals/channel-trigger-provider"; +import { zapierPollProvider } from "../signals/zapier-poll-provider"; import { attachTriggerTool } from "../tools/attach-trigger"; import { connectZapierTool } from "../tools/connect-zapier"; import { deleteWorkflowTool } from "../tools/delete-workflow"; @@ -177,6 +179,12 @@ export function createForemanAgent(databaseUrl: string) { stopWhen: stepCountIs(40), }, tools: () => buildForemanTools(), + // Triggers run as Mastra SignalProviders hosted on this agent (agent-hosted + // gives them notify() → events land in the user's thread). Neither sets a + // pollInterval: the poll provider is driven by cron-driver-server on a + // single tick (so it doesn't auto-poll in every process the agent is built + // in); the channel provider is event-driven via the webhook handlers. + signals: [zapierPollProvider, channelTriggerProvider], voice: process.env.OPENAI_API_KEY ? new OpenAIVoice() : undefined, scorers: { relevancy: { diff --git a/packages/agents/src/mastra/signals/channel-trigger-provider.ts b/packages/agents/src/mastra/signals/channel-trigger-provider.ts new file mode 100644 index 00000000..1403e8e2 --- /dev/null +++ b/packages/agents/src/mastra/signals/channel-trigger-provider.ts @@ -0,0 +1,146 @@ +/** + * ChannelTriggerSignalProvider — a Mastra SignalProvider, hosted on the foreman + * agent, that fires saved workflows when an inbound chat message matches a + * `workflow_trigger` of type='channel'. + * + * Like the poll provider, it lives ON the agent (`signals: [...]`) so it's a + * first-class Mastra primitive with `connect()`/`notify()`. It has no + * `pollInterval` — it's event-driven: the channel webhook handlers call + * `handleMessage()` on every inbound message (via the thin + * `matchAndFireChannelTriggers` wrapper in workflows/channel-trigger.ts). + * + * Dedup (fixes foreman-tv5p): a retried webhook re-delivers the same message, + * which previously fired the workflow twice. We derive a delivery key — the + * caller-supplied `dedupeKey` (e.g. the platform message id) when available, + * else a content hash of channel|room|from|text — and skip a trigger whose + * persisted `last_dedupe_key` already equals it. The content-hash fallback + * catches retries (identical content) but would also coalesce a user sending + * the *exact same* text back-to-back; callers that pass a real message id avoid + * that edge entirely (tracked as a follow-up). + * + * On a fresh fire we also drop a notification signal (native `dedupeKey`) into + * the user's thread — additive and best-effort, never blocking the run. + */ + +import { createHash } from "node:crypto"; +import { SignalProvider } from "@mastra/core/signals"; +import { getSupabase } from "@/lib/db"; +import { executeWorkflow } from "@/lib/workflows/engine"; +import type { ChannelMessage } from "@/workflows/channel-trigger"; + +interface ChannelTriggerRow { + id: string; + workflow_id: string; + config: string; + last_dedupe_key: string | null; +} + +/** Strip leading `/` or `!` and trim — used for `match.command`. */ +function normalizeCommand(s: string): string { + return s.trim().replace(/^[/!]\s*/, ""); +} + +/** Stable delivery key for a message: the caller's id if given, else a content + * hash. Identical (retried) deliveries produce the same key. */ +function deliveryKey(msg: ChannelMessage): string { + if (msg.dedupeKey) return msg.dedupeKey; + return createHash("sha1") + .update(`${msg.channel}|${msg.room ?? ""}|${msg.from}|${msg.text}`) + .digest("hex"); +} + +export class ChannelTriggerSignalProvider extends SignalProvider<"channel-trigger"> { + readonly id = "channel-trigger" as const; + readonly name = "Channel Trigger Signals"; + // No pollInterval — event-driven via handleMessage(). + + /** Drop a best-effort notification into the user's thread on a fresh fire. */ + private async notifyOwner(msg: ChannelMessage, userId: string, key: string): Promise { + if (!this.agent) return; + try { + await this.notify( + { + source: "channel-trigger", + kind: msg.channel, + summary: `Workflow triggered by a ${msg.channel} message`, + payload: { from: msg.from, room: msg.room, text: msg.text }, + dedupeKey: key, + }, + { threadId: `channel:${msg.channel}:${msg.room ?? msg.from}`, resourceId: userId }, + ); + } catch (err) { + console.warn(`[channel-trigger] notify failed (${msg.channel}):`, err); + } + } + + /** + * Match an inbound message against channel triggers and fire each match's + * workflow exactly once per delivery. Returns the number of workflows fired. + */ + async handleMessage(msg: ChannelMessage): Promise { + const supabase = getSupabase(); + const { data: rows, error } = await supabase + .from("workflow_trigger") + .select("id, workflow_id, config, last_dedupe_key") + .eq("type", "channel") + .eq("enabled", true); + if (error) { + console.error(`[channel-trigger] fetch failed: ${error.message}`); + return 0; + } + + const key = deliveryKey(msg); + let fired = 0; + for (const row of (rows ?? []) as unknown as ChannelTriggerRow[]) { + let cfg: { channel?: string; match?: { command?: string; from?: string; room?: string } }; + try { + cfg = JSON.parse(row.config); + } catch { + continue; + } + if (cfg.channel !== msg.channel) continue; + const m = cfg.match ?? {}; + if (m.command && normalizeCommand(msg.text) !== normalizeCommand(m.command)) continue; + if (m.from && !new RegExp(m.from).test(msg.from)) continue; + if (m.room && msg.room !== m.room) continue; + + // Dedup: same delivery already fired this trigger → skip (retried webhook). + if (row.last_dedupe_key && row.last_dedupe_key === key) continue; + + const { data: wf } = await supabase + .from("workflow") + .select("user_id") + .eq("id", row.workflow_id) + .maybeSingle(); + if (!wf) continue; + + const userId = wf.user_id as string; + const now = new Date().toISOString(); + // Record the delivery key BEFORE firing so a concurrent retry can't race + // a second run in. last_fired_at doubles as the fire timestamp. + await supabase + .from("workflow_trigger") + .update({ last_dedupe_key: key, last_fired_at: now, updated_at: now }) + .eq("id", row.id); + + try { + for await (const ev of executeWorkflow(row.workflow_id, userId, {}, undefined, { + firedBy: "channel", + triggerId: row.id, + })) { + if (ev.type === "error") { + console.warn(`[channel-trigger] ${row.id} workflow error: ${ev.message}`); + } + } + fired++; + await this.notifyOwner(msg, userId, key); + } catch (err) { + console.error(`[channel-trigger] trigger ${row.id} failed:`, err); + } + } + return fired; + } +} + +/** Singleton hosted on the foreman agent (`signals: [...]`). */ +export const channelTriggerProvider = new ChannelTriggerSignalProvider(); diff --git a/packages/agents/src/mastra/signals/zapier-poll-provider.ts b/packages/agents/src/mastra/signals/zapier-poll-provider.ts new file mode 100644 index 00000000..d393651f --- /dev/null +++ b/packages/agents/src/mastra/signals/zapier-poll-provider.ts @@ -0,0 +1,287 @@ +/** + * ZapierPollSignalProvider — a Mastra SignalProvider that watches Zapier read + * actions and fires saved workflows when new records appear. + * + * This lives ON the foreman agent (`signals: [zapierPollProvider]`), so it's a + * first-class Mastra primitive — not a worker bolted onto the side. Hosting it + * on the agent gives us `notify()`: when a poll turns up new data we drop a + * notification signal (with the framework's native `dedupeKey`) into the user's + * thread, so they *see* "new record → ran your workflow." + * + * Why we DON'T set `pollInterval`: the foreman agent is constructed in every + * Foreman process (the :4111 server AND the :4112 webhook server build it via + * the channel bots). If the provider auto-polled, it would fire in each process + * — the same double-fire the cron driver avoids by running a single dedicated + * instance. Instead we leave `pollInterval` undefined (no framework auto-poll) + * and drive `runDuePolls()` from the single `cron-driver-server` process. One + * Mastra app, the provider lives in the agent, one process ticks it. + * + * Firing stays deterministic: `runDuePolls()` runs the Zapier read via + * `runAction`, diffs results against the persisted `last_dedupe_key` cursor, and + * fires `executeWorkflow` once per new record — no LLM in the hot path. The + * `notify()` call is best-effort and additive (skipped if no agent is connected + * or the notification store is unavailable), so a notification hiccup never + * blocks a workflow run. + * + * Config (workflow_trigger.config for type='poll'): + * { app, action, connection?, inputs?, dedupeKey, intervalMinutes? } + * Reads are assumed newest-first (the Zapier "new X" convention). First poll + * establishes a baseline (sets the cursor, fires nothing) so binding a trigger + * doesn't replay the whole backlog. + */ + +import { SignalProvider } from "@mastra/core/signals"; +import { getSupabase } from "@/lib/db"; +import { executeWorkflow } from "@/lib/workflows/engine"; +import { runAction } from "@/lib/zapier/execution"; + +/** Cap on how many backlog records a single poll will fire, to avoid a flood if + * the cursor falls off the end of the returned page. */ +const MAX_FIRE_PER_POLL = 25; + +interface PollConfig { + app: string; + action: string; + connection?: string; + inputs?: Record; + /** Field on each record that uniquely identifies it (e.g. "id"). */ + dedupeKey: string; + /** How often to poll, in minutes. Defaults to 5. */ + intervalMinutes?: number; +} + +interface PollTriggerRow { + id: string; + workflow_id: string; + config: string; + last_fired_at: string | null; + last_dedupe_key: string | null; +} + +/** Normalize a Zapier read result into an array of record objects. Handles the + * common shapes: a bare array, `{ data: [...] }`, or `{ results: [...] }`. */ +function extractRecords(result: unknown): Record[] { + if (Array.isArray(result)) return result as Record[]; + if (result && typeof result === "object") { + const obj = result as Record; + if (Array.isArray(obj.data)) return obj.data as Record[]; + if (Array.isArray(obj.results)) return obj.results as Record[]; + } + return []; +} + +/** Stable string key for a record, read from the configured dedupeKey field. */ +function recordKey(rec: Record, dedupeKey: string): string | null { + const v = rec[dedupeKey]; + if (v === undefined || v === null) return null; + return String(v); +} + +/** + * Records strictly newer than the cursor, newest-first. Walks the page from the + * top until it hits the cursor key; if the cursor isn't on the page (it scrolled + * off, or this is a fresh page), every record counts as new (capped). + */ +function collectNewRecords( + records: Record[], + dedupeKey: string, + cursorKey: string | null, +): Record[] { + const fresh: Record[] = []; + for (const rec of records) { + if (recordKey(rec, dedupeKey) === cursorKey) break; + fresh.push(rec); + if (fresh.length >= MAX_FIRE_PER_POLL) break; + } + return fresh; +} + +/** Flatten a record's primitive fields to string inputs for {{param}} + * substitution, plus the full record as JSON under `record`. */ +function recordToInputs(rec: Record): Record { + const out: Record = {}; + for (const [k, v] of Object.entries(rec)) { + if (v === null || v === undefined) continue; + if (typeof v === "object") continue; + out[k] = String(v); + } + out.record = JSON.stringify(rec); + return out; +} + +export class ZapierPollSignalProvider extends SignalProvider<"zapier-poll"> { + readonly id = "zapier-poll" as const; + readonly name = "Zapier Poll Signals"; + // No `pollInterval` — see the file header. Driven by cron-driver-server. + + /** + * Drop a notification signal into the workflow owner's thread. Best-effort: + * the workflow fire is the primary action, so a missing agent connection or + * notification-store error must never block it. + */ + private async notifyOwner( + triggerId: string, + cfg: PollConfig, + userId: string, + rec: Record, + key: string | null, + ): Promise { + if (!this.agent) return; // not connected (e.g. unit test, or non-worker process) + try { + await this.notify( + { + source: "zapier-poll", + kind: `${cfg.app}.${cfg.action}`, + summary: `New ${cfg.app} ${cfg.action}${key ? ` (${key})` : ""}`, + payload: rec, + // Native dedup — the same record key won't create a duplicate + // notification even if a re-poll surfaces it again. + dedupeKey: key ?? undefined, + }, + { threadId: `poll:${triggerId}`, resourceId: userId }, + ); + } catch (err) { + console.warn(`[zapier-poll] notify failed for trigger ${triggerId}:`, err); + } + } + + /** + * Process one poll trigger: run its read action, diff against the cursor, fire + * the workflow for each new record, notify the owner's thread, and advance the + * cursor. Returns the number of workflow runs fired. + */ + private async pollOne( + row: PollTriggerRow, + cfg: PollConfig, + userId: string, + nowIso: string, + ): Promise { + const supabase = getSupabase(); + + const result = await runAction( + userId, + cfg.app, + "read", + cfg.action, + cfg.inputs ?? {}, + cfg.connection, + ); + + // A guardrail may demand confirmation — we can't fire autonomously then. + if (result && typeof result === "object" && "__guardrail_confirmation_required" in result) { + console.warn(`[zapier-poll] ${row.id} read needs confirmation — skipping this cycle`); + return 0; + } + + const records = extractRecords(result); + const cursorKey = row.last_dedupe_key ?? null; + const newestKey = records.length ? recordKey(records[0], cfg.dedupeKey) : cursorKey; + + // Advance the cursor + mark polled even when nothing is new, so the interval + // gate and baseline both work off last_fired_at / last_dedupe_key. + await supabase + .from("workflow_trigger") + .update({ last_dedupe_key: newestKey, last_fired_at: nowIso, updated_at: nowIso }) + .eq("id", row.id); + + // First poll: establish the baseline, don't replay the backlog. + if (cursorKey === null) return 0; + + const fresh = collectNewRecords(records, cfg.dedupeKey, cursorKey); + if (fresh.length === 0) return 0; + + let fired = 0; + // Fire oldest-first so the workflow sees records in chronological order. + for (const rec of fresh.reverse()) { + const key = recordKey(rec, cfg.dedupeKey); + try { + for await (const ev of executeWorkflow( + row.workflow_id, + userId, + recordToInputs(rec), + undefined, + { + firedBy: "poll", + triggerId: row.id, + }, + )) { + if (ev.type === "error") { + console.warn(`[zapier-poll] ${row.id} workflow error: ${ev.message}`); + } else if (ev.type === "param_request") { + console.warn( + `[zapier-poll] ${row.id} workflow needs params not in the record: ${ev.missing?.join(", ")}`, + ); + } + } + fired++; + // Additive: surface the fire in the owner's thread via the signal. + await this.notifyOwner(row.id, cfg, userId, rec, key); + } catch (err) { + console.error(`[zapier-poll] ${row.id} fire failed:`, err); + } + } + return fired; + } + + /** + * Run every due poll trigger once. Called by cron-driver-server on its tick. + * Exported behavior mirrors the cron driver's `tickCron`. + */ + async runDuePolls(now: Date = new Date()): Promise<{ fired: number; polled: number }> { + const supabase = getSupabase(); + const nowIso = now.toISOString(); + + const { data: rows, error } = await supabase + .from("workflow_trigger") + .select("id, workflow_id, config, last_fired_at, last_dedupe_key") + .eq("type", "poll") + .eq("enabled", true); + if (error) throw new Error(`zapier-poll: ${error.message}`); + + let fired = 0; + let polled = 0; + for (const row of (rows ?? []) as unknown as PollTriggerRow[]) { + let cfg: PollConfig; + try { + cfg = JSON.parse(row.config); + } catch { + console.warn(`[zapier-poll] trigger ${row.id} has invalid config — skipping`); + continue; + } + if (!cfg.app || !cfg.action || !cfg.dedupeKey) { + console.warn(`[zapier-poll] trigger ${row.id} missing app/action/dedupeKey — skipping`); + continue; + } + + // Interval gate — only hit the Zapier action once per intervalMinutes. + const intervalMs = Math.max(1, cfg.intervalMinutes ?? 5) * 60_000; + if (row.last_fired_at) { + const elapsed = now.getTime() - new Date(row.last_fired_at).getTime(); + if (elapsed < intervalMs) continue; + } + + // Resolve the workflow owner so runAction/executeWorkflow run as that user. + const { data: wf } = await supabase + .from("workflow") + .select("user_id") + .eq("id", row.workflow_id) + .maybeSingle(); + if (!wf) { + console.warn(`[zapier-poll] trigger ${row.id} → workflow ${row.workflow_id} missing`); + continue; + } + + polled++; + try { + fired += await this.pollOne(row, cfg, wf.user_id as string, nowIso); + } catch (err) { + console.error(`[zapier-poll] trigger ${row.id} failed:`, err); + } + } + return { fired, polled }; + } +} + +/** Singleton hosted on the foreman agent (`signals: [...]`) and driven by the + * cron-driver-server worker. */ +export const zapierPollProvider = new ZapierPollSignalProvider(); diff --git a/packages/agents/src/mastra/tools/attach-trigger.ts b/packages/agents/src/mastra/tools/attach-trigger.ts index eff8fe47..a367318f 100644 --- a/packages/agents/src/mastra/tools/attach-trigger.ts +++ b/packages/agents/src/mastra/tools/attach-trigger.ts @@ -33,23 +33,53 @@ const channelConfig = z.object({ .describe("Match criteria for the incoming message."), }); +const pollConfig = z.object({ + app: z.string().min(1).describe("Zapier app key, e.g. 'gmail' or 'google_sheets'."), + action: z + .string() + .min(1) + .describe("The read action key, e.g. 'new_email' or 'new_or_updated_spreadsheet_row'."), + connection: z + .string() + .optional() + .describe("Connection id/alias. Auto-discovered for the app if omitted."), + inputs: z + .record(z.string(), z.unknown()) + .optional() + .describe("Inputs passed to the read action (e.g. { sheet: '...' })."), + dedupeKey: z + .string() + .min(1) + .describe( + "Field on each returned record that uniquely identifies it (e.g. 'id'). " + + "Records are assumed newest-first; new records are those above the last-seen value.", + ), + intervalMinutes: z + .number() + .int() + .min(1) + .optional() + .default(5) + .describe("How often to poll, in minutes (default 5)."), +}); + export const attachTriggerTool = createTool({ id: "attach_trigger", strict: true, description: "Bind a saved workflow to an event source so it fires automatically. " + - "Two trigger types are supported today: 'cron' for scheduled runs (e.g. " + - "every weekday at 9am) and 'channel' for chat-message matches (e.g. when " + - "you DM '!standup' on Slack). The 'poll' type is reserved for future use. " + - "Use after `save_workflow` and after the user confirms the schedule or " + - "match condition.", + "Three trigger types are supported: 'cron' for scheduled runs (e.g. every " + + "weekday at 9am), 'channel' for chat-message matches (e.g. when you DM " + + "'!standup' on Slack), and 'poll' to fire when a Zapier read action returns " + + "a new record (e.g. a new Gmail email or spreadsheet row). Use after " + + "`save_workflow` and after the user confirms the schedule, match, or poll " + + "source.", inputSchema: z.object({ workflowId: z.string().describe("The workflow id (from list_workflows)."), - type: z - .enum(["cron", "channel"]) - .describe("Trigger kind. 'poll' is not yet exposed via the agent."), + type: z.enum(["cron", "channel", "poll"]).describe("Trigger kind."), cron: cronConfig.optional().describe("Required when type='cron'."), channel: channelConfig.optional().describe("Required when type='channel'."), + poll: pollConfig.optional().describe("Required when type='poll'."), enabled: z .boolean() .optional() @@ -58,7 +88,7 @@ export const attachTriggerTool = createTool({ }), outputSchema: z.object({ id: z.string(), - type: z.enum(["cron", "channel"]), + type: z.enum(["cron", "channel", "poll"]), enabled: z.boolean(), }), // Attaching a cron / channel trigger means a workflow may fire without @@ -68,11 +98,11 @@ export const attachTriggerTool = createTool({ const o = output as { id?: string; type?: string }; console.log(`[tool:${toolName}] attached ${o.type} trigger ${o.id?.slice(0, 8)}`); }, - execute: async ({ workflowId, type, cron, channel, enabled }, context) => { + execute: async ({ workflowId, type, cron, channel, poll, enabled }, context) => { const userId = context?.requestContext?.get("userId") as string | undefined; if (!userId) throw new Error("attach_trigger: no userId in request context"); - const config = type === "cron" ? cron : channel; + const config = type === "cron" ? cron : type === "channel" ? channel : poll; if (!config) { throw new Error(`attach_trigger: type='${type}' requires the matching '${type}' field`); } diff --git a/packages/agents/src/mastra/tools/search-history.ts b/packages/agents/src/mastra/tools/search-history.ts index 05c2c09c..93b21373 100644 --- a/packages/agents/src/mastra/tools/search-history.ts +++ b/packages/agents/src/mastra/tools/search-history.ts @@ -1,10 +1,15 @@ import { createTool } from "@mastra/core/tools"; import { z } from "zod"; +import { backgroundIfEnabled } from "@/lib/background"; import { searchActionHistory } from "@/lib/rag"; export const searchHistoryTool = createTool({ id: "search_history", strict: true, + // Opt this read into Mastra background execution when FOREMAN_BACKGROUND_TOOLS=1. + // Simple schema (3 fields) → safe for Studio introspection, unlike the Zapier + // SDK tools whose schemas hang `mastra dev` when background is enabled. + ...backgroundIfEnabled(), description: "Search the user's past action history to find similar actions they've taken before. " + "Use this to recommend actions based on what the user has done previously, or to recall " + diff --git a/packages/agents/src/routes/workflows.ts b/packages/agents/src/routes/workflows.ts index 5b5bda33..b631c889 100644 --- a/packages/agents/src/routes/workflows.ts +++ b/packages/agents/src/routes/workflows.ts @@ -316,6 +316,9 @@ workflows.get("/:id/runs", async (c) => { status: r.status, created_at: r.created_at, completed_at: r.completed_at ?? null, + error_message: r.error_message ?? null, + fired_by: r.fired_by ?? null, + trigger_id: r.trigger_id ?? null, })), ); }); diff --git a/packages/agents/src/slack/bot.ts b/packages/agents/src/slack/bot.ts index d88918c3..41b1ad43 100644 --- a/packages/agents/src/slack/bot.ts +++ b/packages/agents/src/slack/bot.ts @@ -101,6 +101,7 @@ async function _createAndInitBot() { channel: "slack", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -128,6 +129,7 @@ async function _createAndInitBot() { channel: "slack", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/teams/bot.ts b/packages/agents/src/teams/bot.ts index 92d1ccca..ab010a00 100644 --- a/packages/agents/src/teams/bot.ts +++ b/packages/agents/src/teams/bot.ts @@ -62,6 +62,7 @@ export async function getTeamsBot() { channel: "teams", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -88,6 +89,7 @@ export async function getTeamsBot() { channel: "teams", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/telegram/bot.ts b/packages/agents/src/telegram/bot.ts index 76c2b6be..63f463b0 100644 --- a/packages/agents/src/telegram/bot.ts +++ b/packages/agents/src/telegram/bot.ts @@ -92,6 +92,7 @@ export async function getTelegramBot() { channel: "telegram", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -112,6 +113,7 @@ export async function getTelegramBot() { channel: "telegram", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/webhook-server.ts b/packages/agents/src/webhook-server.ts index 6feac3b9..614c8139 100644 --- a/packages/agents/src/webhook-server.ts +++ b/packages/agents/src/webhook-server.ts @@ -4,6 +4,8 @@ import { getGoogleChatBot } from "./gchat/bot"; import { getGitHubBot } from "./github/bot"; import { getiMessageBot } from "./imessage/bot"; import { getLinearBot } from "./linear/bot"; +import { getMastra } from "./mastra"; +import { channelTriggerProvider } from "./mastra/signals/channel-trigger-provider"; import { getSlackBot } from "./slack/bot"; import { getTeamsBot } from "./teams/bot"; import { getTelegramBot } from "./telegram/bot"; @@ -233,6 +235,13 @@ const server = http.createServer(async (req, res) => { server.listen(port, () => { console.log(`Webhook server running on port ${port}`); + // Build the foreman agent up front so the channel SignalProvider is connected + // before the first inbound message — otherwise matchAndFireChannelTriggers + // runs before any generateReply() has lazily built the agent, and the + // channel notify() is a no-op for trigger-only messages. (foreman notify-wiring) + getMastra(); + console.log(`[webhook] channel provider connected: ${channelTriggerProvider.isConnected}`); + // Initialize Slack bot — getSlackBot() handles init + rehydration internally if (process.env.SLACK_SIGNING_SECRET) { getSlackBot() diff --git a/packages/agents/src/whatsapp/bot.ts b/packages/agents/src/whatsapp/bot.ts index 777e96b1..dec0c730 100644 --- a/packages/agents/src/whatsapp/bot.ts +++ b/packages/agents/src/whatsapp/bot.ts @@ -63,6 +63,7 @@ export async function getWhatsAppBot() { channel: "whatsapp", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.channelId, }); if (fired > 0) return; @@ -90,6 +91,7 @@ export async function getWhatsAppBot() { channel: "whatsapp", text: message.text, from: message.author.userId, + dedupeKey: message.id, room: thread.id, }); if (fired > 0) return; diff --git a/packages/agents/src/workflows/channel-trigger.ts b/packages/agents/src/workflows/channel-trigger.ts index f8043830..6280b93a 100644 --- a/packages/agents/src/workflows/channel-trigger.ts +++ b/packages/agents/src/workflows/channel-trigger.ts @@ -1,9 +1,9 @@ /** - * Channel-trigger matcher. Channel webhook handlers (slack/discord/telegram/etc.) - * call `matchAndFireChannelTriggers` on every inbound message. If any - * `workflow_trigger` row of type='channel' matches the channel + criteria, - * the workflow is fired (in the background — the handler still replies - * normally). + * Channel-trigger entrypoint. Channel webhook handlers (slack/discord/telegram/ + * etc.) call `matchAndFireChannelTriggers` on every inbound message. The match + + * fire + dedup logic lives in the ChannelTriggerSignalProvider (a Mastra + * SignalProvider hosted on the foreman agent); this wrapper keeps the existing + * call-site signature stable and delegates to it. * * Match semantics: * - `channel` field MUST equal the inbound channel name. @@ -13,12 +13,11 @@ * - `match.from` (optional): regex on the sender id/handle. * - `match.room` (optional): exact match on room/channel/DM target. * - * All provided criteria must pass — empty match = matches every message - * on that channel (rarely what you want, but supported). + * All provided criteria must pass — empty match = matches every message on that + * channel (rarely what you want, but supported). */ -import { getSupabase } from "@/lib/db"; -import { executeWorkflow } from "@/lib/workflows/engine"; +import { channelTriggerProvider } from "@/mastra/signals/channel-trigger-provider"; export interface ChannelMessage { channel: "slack" | "discord" | "telegram" | "linear" | "github" | "gchat" | "teams" | "whatsapp"; @@ -27,72 +26,17 @@ export interface ChannelMessage { from: string; /** Channel/room/DM target — used by `match.room`. */ room?: string; -} - -interface ChannelTriggerRow { - id: string; - workflow_id: string; - config: string; -} - -/** Strip leading `/` or `!` and trim — used for `match.command`. */ -function normalizeCommand(s: string): string { - return s.trim().replace(/^[/!]\s*/, ""); + /** + * Stable per-delivery key for idempotency (the platform message id). Every + * channel bot passes `message.id` (the normalized Chat SDK `Message.id`), so + * a retried webhook carries the same key and the workflow fires once — while + * two legitimately-repeated identical messages keep distinct ids and both + * fire. When omitted, the provider falls back to a content hash (which would + * coalesce repeated identical text). + */ + dedupeKey?: string; } export async function matchAndFireChannelTriggers(msg: ChannelMessage): Promise { - const supabase = getSupabase(); - const { data: rows, error } = await supabase - .from("workflow_trigger") - .select("id, workflow_id, config") - .eq("type", "channel") - .eq("enabled", true); - if (error) { - console.error(`[channel-trigger] fetch failed: ${error.message}`); - return 0; - } - - let fired = 0; - for (const row of (rows ?? []) as unknown as ChannelTriggerRow[]) { - let cfg: { channel?: string; match?: { command?: string; from?: string; room?: string } }; - try { - cfg = JSON.parse(row.config); - } catch { - continue; - } - if (cfg.channel !== msg.channel) continue; - const m = cfg.match ?? {}; - if (m.command && normalizeCommand(msg.text) !== normalizeCommand(m.command)) continue; - if (m.from && !new RegExp(m.from).test(msg.from)) continue; - if (m.room && msg.room !== m.room) continue; - - const { data: wf } = await supabase - .from("workflow") - .select("user_id") - .eq("id", row.workflow_id) - .maybeSingle(); - if (!wf) continue; - - void runOne(row.workflow_id, wf.user_id as string, row.id); - fired++; - } - return fired; -} - -async function runOne(workflowId: string, userId: string, triggerId: string) { - const supabase = getSupabase(); - const now = new Date().toISOString(); - try { - await supabase - .from("workflow_trigger") - .update({ last_fired_at: now, updated_at: now }) - .eq("id", triggerId); - for await (const ev of executeWorkflow(workflowId, userId, {})) { - if (ev.type === "error") { - console.warn(`[channel-trigger] ${triggerId} workflow error: ${ev.message}`); - } - } - } catch (err) { - console.error(`[channel-trigger] trigger ${triggerId} failed:`, err); - } + return channelTriggerProvider.handleMessage(msg); } diff --git a/packages/agents/src/workflows/cron-driver.ts b/packages/agents/src/workflows/cron-driver.ts index 1c14a0d9..81a48a73 100644 --- a/packages/agents/src/workflows/cron-driver.ts +++ b/packages/agents/src/workflows/cron-driver.ts @@ -160,12 +160,6 @@ export async function tickCron(now: Date = new Date()): Promise<{ fired: number } if (!matches) continue; - // Already fired this minute? Skip. - if (row.last_fired_at) { - const last = minuteFloor(new Date(row.last_fired_at)); - if (last.getTime() >= tickMinute.getTime()) continue; - } - // Find the workflow's owner so executeWorkflow can run with the right userId. const { data: wf } = await supabase .from("workflow") @@ -177,24 +171,38 @@ export async function tickCron(now: Date = new Date()): Promise<{ fired: number continue; } + // Atomic same-minute claim: set last_fired_at=tickIso only if this trigger + // hasn't already fired this minute. The conditional UPDATE is the lock — + // two overlapping ticks (e.g. a process restart mid-minute, or briefly + // co-existing drivers) can't both claim the same minute, so the workflow + // fires exactly once even when last_fired_at hadn't yet been persisted. + const { data: claimed, error: claimErr } = await supabase + .from("workflow_trigger") + .update({ last_fired_at: tickIso, updated_at: tickIso }) + .eq("id", row.id) + .or(`last_fired_at.is.null,last_fired_at.lt.${tickIso}`) + .select("id"); + if (claimErr) { + console.warn(`[cron-driver] trigger ${row.id} claim failed: ${claimErr.message}`); + continue; + } + if (!claimed || claimed.length === 0) continue; // another tick already fired it + // Fire and forget — we don't await full completion to keep the tick fast. - void runOne(row.workflow_id, wf.user_id as string, row.id, tickIso); + void runOne(row.workflow_id, wf.user_id as string, row.id); fired++; } return { fired }; } -async function runOne(workflowId: string, userId: string, triggerId: string, firedAtIso: string) { - const supabase = getSupabase(); +async function runOne(workflowId: string, userId: string, triggerId: string) { + // last_fired_at was already set by the atomic claim in tickCron, so the run is + // dedup'd before we get here. We just execute and surface errors. try { - // Mark fired BEFORE executing so a long-running workflow can't race a - // second tick. last_fired_at is a fingerprint, not a completion marker. - await supabase - .from("workflow_trigger") - .update({ last_fired_at: firedAtIso, updated_at: firedAtIso }) - .eq("id", triggerId); - - for await (const ev of executeWorkflow(workflowId, userId, {})) { + for await (const ev of executeWorkflow(workflowId, userId, {}, undefined, { + firedBy: "cron", + triggerId, + })) { if (ev.type === "error") { console.warn(`[cron-driver] ${triggerId} workflow error: ${ev.message}`); } diff --git a/packages/agents/src/workflows/poll-driver.ts b/packages/agents/src/workflows/poll-driver.ts new file mode 100644 index 00000000..c4302f8f --- /dev/null +++ b/packages/agents/src/workflows/poll-driver.ts @@ -0,0 +1,56 @@ +/** + * Poll driver scheduler — the single-process tick that drives the + * ZapierPollSignalProvider. + * + * The provider itself lives on the foreman agent (a Mastra SignalProvider). It + * deliberately has no `pollInterval` (which would auto-poll in every process the + * agent is built in). This scheduler — run only inside cron-driver-server — is + * the one place that calls `runDuePolls()`, self-aligned to wall-clock minute + * boundaries (a hair after the cron driver so the two don't contend on the same + * trigger-table read). + */ + +import { zapierPollProvider } from "@/mastra/signals/zapier-poll-provider"; + +/** + * Start the poll scheduler. Returns a stop function. First tick lands at the + * next :00 + 750ms. + */ +export function startPollDriver(): () => void { + let stopped = false; + let timer: NodeJS.Timeout | null = null; + let running = false; + + const scheduleNext = () => { + if (stopped) return; + const now = new Date(); + const next = new Date(now); + next.setSeconds(0, 750); + if (next.getTime() <= now.getTime()) next.setMinutes(next.getMinutes() + 1); + const wait = next.getTime() - now.getTime(); + timer = setTimeout(async () => { + if (!running) { + running = true; + try { + const { fired, polled } = await zapierPollProvider.runDuePolls(new Date()); + if (fired > 0) { + console.log(`[poll-driver] fired ${fired} run(s) across ${polled} trigger(s)`); + } + } catch (err) { + console.error("[poll-driver] tick failed:", err); + } finally { + running = false; + } + } + scheduleNext(); + }, wait); + }; + + scheduleNext(); + console.log("[poll-driver] started"); + return () => { + stopped = true; + if (timer) clearTimeout(timer); + console.log("[poll-driver] stopped"); + }; +} diff --git a/packages/agents/tests/live/poll-driver.test.ts b/packages/agents/tests/live/poll-driver.test.ts new file mode 100644 index 00000000..f60ab042 --- /dev/null +++ b/packages/agents/tests/live/poll-driver.test.ts @@ -0,0 +1,198 @@ +/** + * Live integration test for the poll driver (foreman-ueep). + * + * Exercises the full poll-trigger path against REAL local Supabase: + * attach_trigger(type='poll') → zapierPollProvider.runDuePolls() → + * executeWorkflow → workflow_run rows. + * + * The only thing stubbed is the external Zapier read (`runAction`): the local DB + * was reset and there's no OAuth-connected app to create records in, so we + * control what the "read" returns (and stub the workflow's write step so + * executeWorkflow completes). Everything else — cursor persistence, firing, + * dedup, workflow_run inserts with fired_by/trigger_id — is real Postgres. + * + * Auto-skips if Supabase isn't reachable. Run: npm run test:live + */ +import { RequestContext } from "@mastra/core/request-context"; +import { createClient } from "@supabase/supabase-js"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import type { Database } from "@/lib/db/database.types"; + +const SUPABASE_URL = process.env.SUPABASE_URL ?? "http://127.0.0.1:54421"; +const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY ?? ""; + +// Control the Zapier read the poll driver sees; stub the workflow's write step. +let readRecords: Array> = []; +vi.mock("@/lib/zapier/execution", () => ({ + runAction: vi.fn(async (_userId: string, _app: string, actionType: string) => + actionType === "read" ? { data: readRecords } : { ok: true }, + ), + rawFetch: vi.fn(), +})); + +async function supabaseIsReachable(): Promise { + try { + const res = await fetch(`${SUPABASE_URL}/rest/v1/`, { + headers: { apikey: SUPABASE_SERVICE_ROLE_KEY }, + signal: AbortSignal.timeout(2000), + }); + return res.status < 500; + } catch { + return false; + } +} + +function ctx({ userId, conversationId }: { userId: string; conversationId?: string }) { + const entries: [string, string][] = [["userId", userId]]; + if (conversationId) entries.push(["threadId", conversationId]); + return { requestContext: new RequestContext(entries) } as any; +} + +describe("poll driver — live Supabase (foreman-ueep)", () => { + let reachable = false; + let supabase: ReturnType>; + + const testRunId = `poll-${Date.now()}`; + const userId = `${testRunId}-user`; + const conversationId = `${testRunId}-conv`; + let workflowId = ""; + let triggerId = ""; + + async function runsForWorkflow() { + const { data } = await supabase.from("workflow_run").select("*").eq("workflow_id", workflowId); + return data ?? []; + } + async function triggerRow() { + const { data } = await supabase + .from("workflow_trigger") + .select("last_dedupe_key, last_fired_at") + .eq("id", triggerId) + .maybeSingle(); + return data; + } + + beforeAll(async () => { + reachable = await supabaseIsReachable(); + if (!reachable) { + console.warn(`\n⚠ Supabase not reachable at ${SUPABASE_URL}. Skipping live poll tests.\n`); + return; + } + supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY); + const now = new Date().toISOString(); + + await supabase.from("user").insert({ + id: userId, + name: "live-poll-test", + email: `${testRunId}@test.local`, + emailVerified: true, + image: null, + createdAt: now, + updatedAt: now, + }); + await supabase.from("conversation").insert({ + id: conversationId, + user_id: userId, + mastra_thread_id: null, + title: null, + created_at: now, + updated_at: now, + }); + // An executed proposal → save_workflow turns it into a real workflow + step. + await supabase.from("action_proposal").insert({ + id: crypto.randomUUID(), + conversation_id: conversationId, + mastra_run_id: null, + app_key: "slack", + action_type: "write", + action_key: "send_message", + human_label: "slack.send_message", + inputs: JSON.stringify({ channel: "#ops", text: "poll fired" }), + input_schema: "{}", + connection_id: null, + status: "executed", + created_at: now, + updated_at: now, + }); + + const { saveWorkflowTool } = await import("@/mastra/tools/save-workflow"); + const saved = (await saveWorkflowTool.execute!( + { name: "Live poll workflow" } as any, + ctx({ userId, conversationId }), + )) as { workflowId: string }; + workflowId = saved.workflowId; + + const { attachTriggerTool } = await import("@/mastra/tools/attach-trigger"); + const attached = (await attachTriggerTool.execute!( + { + workflowId, + type: "poll", + poll: { app: "gmail", action: "new_email", dedupeKey: "id", intervalMinutes: 5 }, + } as any, + ctx({ userId }), + )) as { id: string; type: string }; + triggerId = attached.id; + expect(attached.type).toBe("poll"); + }); + + afterAll(async () => { + if (!reachable) return; + await supabase.from("workflow_run").delete().eq("workflow_id", workflowId); + await supabase.from("workflow_trigger").delete().eq("workflow_id", workflowId); + await supabase.from("workflow_step").delete().eq("workflow_id", workflowId); + await supabase.from("workflow").delete().eq("id", workflowId); + await supabase.from("action_proposal").delete().eq("conversation_id", conversationId); + await supabase.from("conversation").delete().eq("id", conversationId); + await supabase.from("user").delete().eq("id", userId); + }); + + it("persists the poll trigger config", async ({ skip }) => { + if (!reachable) skip(); + const { data } = await supabase + .from("workflow_trigger") + .select("type, config") + .eq("id", triggerId) + .maybeSingle(); + expect(data?.type).toBe("poll"); + expect(JSON.parse(data?.config as string)).toMatchObject({ + app: "gmail", + action: "new_email", + dedupeKey: "id", + }); + }); + + it("first poll establishes the baseline — fires nothing, sets the cursor", async ({ skip }) => { + if (!reachable) skip(); + readRecords = [{ id: "2" }, { id: "1" }]; + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-06-12T12:00:00Z")); + + expect(out.fired).toBe(0); + expect(await runsForWorkflow()).toHaveLength(0); + expect((await triggerRow())?.last_dedupe_key).toBe("2"); + }); + + it("fires the workflow once for a new record, recording fired_by=poll + trigger_id", async ({ + skip, + }) => { + if (!reachable) skip(); + readRecords = [{ id: "3" }, { id: "2" }, { id: "1" }]; // "3" is new + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-06-12T12:10:00Z")); + + expect(out.fired).toBe(1); + const runs = await runsForWorkflow(); + expect(runs).toHaveLength(1); + expect(runs[0]).toMatchObject({ fired_by: "poll", trigger_id: triggerId, status: "success" }); + expect((await triggerRow())?.last_dedupe_key).toBe("3"); + }); + + it("does not double-fire when no new record appears (dedup)", async ({ skip }) => { + if (!reachable) skip(); + readRecords = [{ id: "3" }, { id: "2" }]; // newest "3" == cursor + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-06-12T12:20:00Z")); + + expect(out.fired).toBe(0); + expect(await runsForWorkflow()).toHaveLength(1); // still just the one + }); +}); diff --git a/packages/agents/tests/unit/background-config.test.ts b/packages/agents/tests/unit/background-config.test.ts new file mode 100644 index 00000000..dc652448 --- /dev/null +++ b/packages/agents/tests/unit/background-config.test.ts @@ -0,0 +1,29 @@ +/** + * Unit tests for backgroundIfEnabled (foreman-7am4) — the env gate that opts a + * tool into Mastra background execution. Off unless FOREMAN_BACKGROUND_TOOLS=1. + */ +import { afterEach, describe, expect, it } from "vitest"; +import { backgroundIfEnabled } from "@/lib/background"; + +const orig = process.env.FOREMAN_BACKGROUND_TOOLS; +afterEach(() => { + if (orig === undefined) delete process.env.FOREMAN_BACKGROUND_TOOLS; + else process.env.FOREMAN_BACKGROUND_TOOLS = orig; +}); + +describe("backgroundIfEnabled", () => { + it("returns a background config when the flag is set to '1'", () => { + process.env.FOREMAN_BACKGROUND_TOOLS = "1"; + expect(backgroundIfEnabled()).toEqual({ background: { enabled: true } }); + }); + + it("is off by default — returns an empty slice", () => { + delete process.env.FOREMAN_BACKGROUND_TOOLS; + expect(backgroundIfEnabled()).toEqual({}); + }); + + it("treats any value other than '1' as off", () => { + process.env.FOREMAN_BACKGROUND_TOOLS = "true"; + expect(backgroundIfEnabled()).toEqual({}); + }); +}); diff --git a/packages/agents/tests/unit/channel-trigger.test.ts b/packages/agents/tests/unit/channel-trigger.test.ts index ec92ad3e..ab2c7646 100644 --- a/packages/agents/tests/unit/channel-trigger.test.ts +++ b/packages/agents/tests/unit/channel-trigger.test.ts @@ -1,11 +1,13 @@ /** - * Unit tests for matchAndFireChannelTriggers. + * Unit tests for matchAndFireChannelTriggers / ChannelTriggerSignalProvider — + * matching, dedup (foreman-tv5p), and the notify() wiring. */ import { beforeEach, describe, expect, it, vi } from "vitest"; type Result = { data: T; error: { message: string } | null }; let triggerListResult: Result = { data: [], error: null }; let workflowSelectResult: Result = { data: null, error: null }; +const updates: { table: string; payload: any }[] = []; function createChain(table: string) { const builder: any = {}; @@ -22,6 +24,7 @@ function createChain(table: string) { const origUpdate = builder.update; builder.update = vi.fn((payload: any) => { builder._verb = "update"; + updates.push({ table, payload }); return origUpdate(payload); }); return builder; @@ -40,28 +43,28 @@ async function* gen(events: unknown[]): AsyncGenerator { for (const e of events) yield e; } +const channelRow = (over: Record = {}) => ({ + id: "t-1", + workflow_id: "wf-1", + config: JSON.stringify({ channel: "slack", match: { command: "!standup" } }), + last_dedupe_key: null, + ...over, +}); + +const lastTriggerUpdate = () => + [...updates].reverse().find((u) => u.table === "workflow_trigger")?.payload; + describe("matchAndFireChannelTriggers", () => { beforeEach(() => { vi.resetAllMocks(); triggerListResult = { data: [], error: null }; workflowSelectResult = { data: { user_id: "user-1" }, error: null }; + updates.length = 0; mockExecuteWorkflow.mockReturnValue(gen([{ type: "complete" }])); }); it("fires when channel + command match", async () => { - triggerListResult = { - data: [ - { - id: "t-1", - workflow_id: "wf-1", - config: JSON.stringify({ - channel: "slack", - match: { command: "!standup" }, - }), - }, - ], - error: null, - }; + triggerListResult = { data: [channelRow()], error: null }; const { matchAndFireChannelTriggers } = await import("@/workflows/channel-trigger"); const fired = await matchAndFireChannelTriggers({ @@ -73,19 +76,7 @@ describe("matchAndFireChannelTriggers", () => { }); it("tolerates leading slashes and whitespace in command match", async () => { - triggerListResult = { - data: [ - { - id: "t-1", - workflow_id: "wf-1", - config: JSON.stringify({ - channel: "slack", - match: { command: "!standup" }, - }), - }, - ], - error: null, - }; + triggerListResult = { data: [channelRow()], error: null }; const { matchAndFireChannelTriggers } = await import("@/workflows/channel-trigger"); const fired = await matchAndFireChannelTriggers({ @@ -97,19 +88,7 @@ describe("matchAndFireChannelTriggers", () => { }); it("skips when channel differs", async () => { - triggerListResult = { - data: [ - { - id: "t-1", - workflow_id: "wf-1", - config: JSON.stringify({ - channel: "slack", - match: { command: "!standup" }, - }), - }, - ], - error: null, - }; + triggerListResult = { data: [channelRow()], error: null }; const { matchAndFireChannelTriggers } = await import("@/workflows/channel-trigger"); const fired = await matchAndFireChannelTriggers({ @@ -123,14 +102,12 @@ describe("matchAndFireChannelTriggers", () => { it("skips when from regex doesn't match", async () => { triggerListResult = { data: [ - { - id: "t-1", - workflow_id: "wf-1", + channelRow({ config: JSON.stringify({ channel: "slack", match: { command: "!standup", from: "^U999" }, }), - }, + }), ], error: null, }; @@ -147,14 +124,12 @@ describe("matchAndFireChannelTriggers", () => { it("skips when room differs", async () => { triggerListResult = { data: [ - { - id: "t-1", - workflow_id: "wf-1", + channelRow({ config: JSON.stringify({ channel: "slack", match: { command: "!standup", room: "#ops" }, }), - }, + }), ], error: null, }; @@ -170,19 +145,7 @@ describe("matchAndFireChannelTriggers", () => { }); it("skips when workflow has been deleted", async () => { - triggerListResult = { - data: [ - { - id: "t-1", - workflow_id: "wf-deleted", - config: JSON.stringify({ - channel: "slack", - match: { command: "!standup" }, - }), - }, - ], - error: null, - }; + triggerListResult = { data: [channelRow({ workflow_id: "wf-deleted" })], error: null }; workflowSelectResult = { data: null, error: null }; const { matchAndFireChannelTriggers } = await import("@/workflows/channel-trigger"); @@ -193,4 +156,73 @@ describe("matchAndFireChannelTriggers", () => { }); expect(fired).toBe(0); }); + + // ─── Dedup (foreman-tv5p) ─────────────────────────────────────────────────── + + it("skips a retried delivery whose dedupeKey already fired the trigger", async () => { + triggerListResult = { data: [channelRow({ last_dedupe_key: "msg-42" })], error: null }; + + const { matchAndFireChannelTriggers } = await import("@/workflows/channel-trigger"); + const fired = await matchAndFireChannelTriggers({ + channel: "slack", + text: "!standup", + from: "U123", + dedupeKey: "msg-42", + }); + expect(fired).toBe(0); + expect(mockExecuteWorkflow).not.toHaveBeenCalled(); + }); + + it("fires a new delivery and records its dedupeKey", async () => { + triggerListResult = { data: [channelRow({ last_dedupe_key: "msg-old" })], error: null }; + + const { matchAndFireChannelTriggers } = await import("@/workflows/channel-trigger"); + const fired = await matchAndFireChannelTriggers({ + channel: "slack", + text: "!standup", + from: "U123", + dedupeKey: "msg-new", + }); + expect(fired).toBe(1); + expect(lastTriggerUpdate()?.last_dedupe_key).toBe("msg-new"); + }); + + it("derives a content-hash dedupeKey when none is supplied (records it)", async () => { + triggerListResult = { data: [channelRow()], error: null }; + + const { matchAndFireChannelTriggers } = await import("@/workflows/channel-trigger"); + const fired = await matchAndFireChannelTriggers({ + channel: "slack", + text: "!standup", + from: "U123", + }); + expect(fired).toBe(1); + // a real (40-char sha1 hex) key was persisted, not undefined + expect(lastTriggerUpdate()?.last_dedupe_key).toMatch(/^[0-9a-f]{40}$/); + }); + + it("notify()s the owner's thread with the native dedupeKey on a fresh fire", async () => { + triggerListResult = { data: [channelRow()], error: null }; + + const { ChannelTriggerSignalProvider } = await import( + "@/mastra/signals/channel-trigger-provider" + ); + const provider = new ChannelTriggerSignalProvider(); + const sendNotificationSignal = vi.fn().mockResolvedValue(undefined); + provider.connect({ sendNotificationSignal } as any); + + const fired = await provider.handleMessage({ + channel: "slack", + text: "!standup", + from: "U123", + room: "#ops", + dedupeKey: "msg-7", + }); + + expect(fired).toBe(1); + expect(sendNotificationSignal).toHaveBeenCalledTimes(1); + const [notification, target] = sendNotificationSignal.mock.calls[0]; + expect(notification).toMatchObject({ source: "channel-trigger", dedupeKey: "msg-7" }); + expect(target).toMatchObject({ threadId: "channel:slack:#ops", resourceId: "user-1" }); + }); }); diff --git a/packages/agents/tests/unit/cron-driver.test.ts b/packages/agents/tests/unit/cron-driver.test.ts index 6b223d14..e07f3d20 100644 --- a/packages/agents/tests/unit/cron-driver.test.ts +++ b/packages/agents/tests/unit/cron-driver.test.ts @@ -8,17 +8,21 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; type Result = { data: T; error: { message: string } | null }; let triggerListResult: Result = { data: [], error: null }; let workflowSelectResult: Result = { data: null, error: null }; +// Result of the atomic same-minute claim (`update ... .select("id")`). A +// non-empty array means the claim succeeded; empty means another tick already +// fired this minute. +let claimResult: Result = { data: [{ id: "trig-1" }], error: null }; const updates: any[] = []; function createChain(table: string) { const builder: any = {}; - for (const m of ["select", "eq", "limit", "update"]) { + for (const m of ["select", "eq", "limit", "update", "or"]) { builder[m] = vi.fn().mockReturnValue(builder); } builder.maybeSingle = vi.fn().mockImplementation(() => Promise.resolve(workflowSelectResult)); // biome-ignore lint/suspicious/noThenProperty: thenable mock builder.then = (resolve: any) => { - if (builder._verb === "update") return resolve({ data: null, error: null }); + if (builder._verb === "update") return resolve(claimResult); if (table === "workflow_trigger") return resolve(triggerListResult); return resolve({ data: null, error: null }); }; @@ -111,6 +115,7 @@ describe("tickCron", () => { vi.resetAllMocks(); triggerListResult = { data: [], error: null }; workflowSelectResult = { data: null, error: null }; + claimResult = { data: [{ id: "trig-1" }], error: null }; updates.length = 0; mockExecuteWorkflow.mockReset(); }); @@ -139,7 +144,7 @@ describe("tickCron", () => { expect(updates.find((u) => u.table === "workflow_trigger")).toBeTruthy(); }); - it("skips a trigger that already fired this minute", async () => { + it("does not double-fire when the atomic claim is lost (same-minute restart)", async () => { triggerListResult = { data: [ { @@ -151,10 +156,16 @@ describe("tickCron", () => { ], error: null, }; + workflowSelectResult = { data: { user_id: "user-1" }, error: null }; + // The conditional UPDATE matches no row — another tick already claimed this + // minute. The driver must NOT fire. + claimResult = { data: [], error: null }; + mockExecuteWorkflow.mockReturnValue(gen([{ type: "complete" }])); const { tickCron } = await import("@/workflows/cron-driver"); const out = await tickCron(new Date("2026-05-10T12:00:30Z")); expect(out.fired).toBe(0); + expect(mockExecuteWorkflow).not.toHaveBeenCalled(); }); it("skips a non-matching schedule", async () => { diff --git a/packages/agents/tests/unit/mastra-agent.test.ts b/packages/agents/tests/unit/mastra-agent.test.ts index f27cfaf1..dfcb4b08 100644 --- a/packages/agents/tests/unit/mastra-agent.test.ts +++ b/packages/agents/tests/unit/mastra-agent.test.ts @@ -43,4 +43,19 @@ describe("Mastra foreman agent", () => { const agent = await createForemanAgent("file:./test-agent.db"); expect(agent.model).toEqual(AGENT_MODELS.foreman); }, 30000); + + it("hosts the trigger SignalProviders and connects them on construction", async () => { + const { createForemanAgent } = await import("@/mastra/agents/foreman"); + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const { channelTriggerProvider } = await import("@/mastra/signals/channel-trigger-provider"); + await createForemanAgent("file:./test-agent.db"); + // The Agent constructor calls provider.connect(this) for each signals entry, + // proving the primitives are live in the agent (not merely defined). + expect(zapierPollProvider.isConnected).toBe(true); + expect(channelTriggerProvider.isConnected).toBe(true); + // Neither sets a pollInterval — the poll provider is driven by + // cron-driver-server, the channel provider is event-driven via webhooks. + expect(zapierPollProvider.pollInterval).toBeUndefined(); + expect(channelTriggerProvider.pollInterval).toBeUndefined(); + }, 30000); }); diff --git a/packages/agents/tests/unit/resolve-app-slug.test.ts b/packages/agents/tests/unit/resolve-app-slug.test.ts new file mode 100644 index 00000000..a95cd0d5 --- /dev/null +++ b/packages/agents/tests/unit/resolve-app-slug.test.ts @@ -0,0 +1,54 @@ +/** + * Unit tests for resolveAppSlug (foreman-c8fo) — catalog-backed app-key + * resolution that replaces the string-munging normalizeAppKey. + */ +import { afterEach, describe, expect, it, vi } from "vitest"; + +let catalogResult: { data: { slug: string } | null } = { data: null }; + +vi.mock("@/lib/db", () => ({ + getSupabase: () => ({ + from: () => { + const b: any = {}; + for (const m of ["select", "or", "limit"]) b[m] = vi.fn().mockReturnValue(b); + b.maybeSingle = vi.fn().mockImplementation(() => Promise.resolve(catalogResult)); + return b; + }, + }), +})); + +afterEach(() => { + catalogResult = { data: null }; +}); + +describe("resolveAppSlug", () => { + it("maps a raw app_key to its catalog slug", async () => { + catalogResult = { data: { slug: "github" } }; + const { resolveAppSlug } = await import("@/lib/catalog/resolve"); + expect(await resolveAppSlug("GitHubCLIAPI")).toBe("github"); + }); + + it("returns the slug unchanged when given a slug", async () => { + catalogResult = { data: { slug: "github" } }; + const { resolveAppSlug } = await import("@/lib/catalog/resolve"); + expect(await resolveAppSlug("github")).toBe("github"); + }); + + it("passes the key through unchanged when the catalog has no match (unseeded)", async () => { + catalogResult = { data: null }; + const { resolveAppSlug } = await import("@/lib/catalog/resolve"); + // The SDK accepts raw app_keys, so passing it through is safe — and crucially + // it does NOT mangle "GitHubCLIAPI" into the broken "git-hub". + expect(await resolveAppSlug("GitHubCLIAPI")).toBe("GitHubCLIAPI"); + }); + + it("passes through non-identifier input without querying", async () => { + const { resolveAppSlug } = await import("@/lib/catalog/resolve"); + expect(await resolveAppSlug("bad,key")).toBe("bad,key"); + }); + + it("handles an empty key", async () => { + const { resolveAppSlug } = await import("@/lib/catalog/resolve"); + expect(await resolveAppSlug("")).toBe(""); + }); +}); diff --git a/packages/agents/tests/unit/workflow-engine.test.ts b/packages/agents/tests/unit/workflow-engine.test.ts new file mode 100644 index 00000000..edc84e7d --- /dev/null +++ b/packages/agents/tests/unit/workflow-engine.test.ts @@ -0,0 +1,130 @@ +/** + * Unit tests for executeWorkflow's run-state lifecycle (foreman-2afc): a run row + * must never get stuck in 'running' — not on step failure, and not when the + * caller abandons the generator mid-stream. runAction is mocked. + */ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +let stepsResult: { data: unknown[] | null; error: unknown } = { data: [], error: null }; +const updates: { table: string; payload: any }[] = []; +const inserts: { table: string; payload: any }[] = []; + +function createChain(table: string) { + const b: any = {}; + for (const m of ["select", "eq", "order", "insert", "update"]) { + b[m] = vi.fn().mockReturnValue(b); + } + // biome-ignore lint/suspicious/noThenProperty: thenable mock + b.then = (resolve: any) => { + if (b._verb === "insert") return resolve({ error: null }); + if (b._verb === "update") return resolve({ error: null }); + if (table === "workflow_step") return resolve(stepsResult); + return resolve({ data: null, error: null }); + }; + const oi = b.insert; + b.insert = vi.fn((p: any) => { + b._verb = "insert"; + inserts.push({ table, payload: p }); + return oi(p); + }); + const ou = b.update; + b.update = vi.fn((p: any) => { + b._verb = "update"; + updates.push({ table, payload: p }); + return ou(p); + }); + return b; +} + +const mockSupabase = { from: vi.fn((t: string) => createChain(t)) }; +vi.mock("@/lib/db", () => ({ getSupabase: () => mockSupabase })); + +const mockRunAction = vi.fn(); +vi.mock("@/lib/zapier/execution", () => ({ + runAction: (...args: unknown[]) => mockRunAction(...args), +})); + +const oneStep = [ + { + id: "s-1", + proposal_template: JSON.stringify({ + humanLabel: "Send", + inputs: {}, + appKey: "gmail", + actionType: "write", + actionKey: "send_email", + }), + }, +]; + +const statuses = () => + updates.filter((u) => u.table === "workflow_run").map((u) => u.payload.status); +const lastRunUpdate = () => [...updates].reverse().find((u) => u.table === "workflow_run")?.payload; +const runInsert = () => inserts.find((i) => i.table === "workflow_run")?.payload; + +describe("executeWorkflow run-state", () => { + beforeEach(() => { + vi.resetAllMocks(); + stepsResult = { data: oneStep, error: null }; + updates.length = 0; + inserts.length = 0; + }); + + it("marks the run failed with the error_message when a step throws", async () => { + mockRunAction.mockRejectedValue(new Error("boom")); + const { executeWorkflow } = await import("@/lib/workflows/engine"); + + const events: string[] = []; + for await (const ev of executeWorkflow("wf-1", "user-1", {})) events.push(ev.type); + + expect(statuses()).toEqual(["failed"]); // exactly one terminal write, not stuck + expect(lastRunUpdate()?.error_message).toBe("boom"); + expect(events).toContain("error"); + }); + + it("marks the run success when all steps complete", async () => { + mockRunAction.mockResolvedValue({ ok: true }); + const { executeWorkflow } = await import("@/lib/workflows/engine"); + + for await (const ev of executeWorkflow("wf-1", "user-1", {})) void ev; + + // success terminal write, and the finally must NOT add a spurious 'failed' + expect(statuses()).toEqual(["success"]); + }); + + it("marks the run failed (with a reason) when the caller abandons the generator", async () => { + // runAction would hang, but the caller breaks before it's reached. + mockRunAction.mockReturnValue(new Promise(() => {})); + const { executeWorkflow } = await import("@/lib/workflows/engine"); + + for await (const ev of executeWorkflow("wf-1", "user-1", {})) { + if (ev.type === "status") break; // abandon — triggers the generator's finally + } + + expect(statuses()).toEqual(["failed"]); // finally rescued the orphaned run + expect(lastRunUpdate()?.error_message).toMatch(/did not complete/); + }); + + it("defaults fired_by='manual' with no trigger id", async () => { + mockRunAction.mockResolvedValue({ ok: true }); + const { executeWorkflow } = await import("@/lib/workflows/engine"); + + for await (const ev of executeWorkflow("wf-1", "user-1", {})) void ev; + + expect(runInsert()).toMatchObject({ fired_by: "manual", trigger_id: null }); + }); + + it("records the trigger context on the run row when fired by a trigger", async () => { + mockRunAction.mockResolvedValue({ ok: true }); + const { executeWorkflow } = await import("@/lib/workflows/engine"); + + for await (const ev of executeWorkflow("wf-1", "user-1", {}, undefined, { + firedBy: "cron", + triggerId: "trig-9", + })) { + void ev; + } + + expect(runInsert()).toMatchObject({ fired_by: "cron", trigger_id: "trig-9" }); + }); +}); diff --git a/packages/agents/tests/unit/zapier-poll-provider.test.ts b/packages/agents/tests/unit/zapier-poll-provider.test.ts new file mode 100644 index 00000000..e6cfcd45 --- /dev/null +++ b/packages/agents/tests/unit/zapier-poll-provider.test.ts @@ -0,0 +1,214 @@ +/** + * Unit tests for the ZapierPollSignalProvider — baseline, diff-by-dedupeKey, + * cursor advance, the interval gate, and the notify() wiring (native dedupeKey + * into the owner's thread). No network: runAction + executeWorkflow are mocked, + * and notify() goes to a stub agent. + */ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// ─── Supabase + engine + zapier mocks ──────────────────────────────────────── + +type Result = { data: T; error: { message: string } | null }; +let triggerListResult: Result = { data: [], error: null }; +let workflowSelectResult: Result = { data: null, error: null }; +const updates: { table: string; payload: any }[] = []; + +function createChain(table: string) { + const builder: any = {}; + for (const m of ["select", "eq", "limit", "update"]) { + builder[m] = vi.fn().mockReturnValue(builder); + } + builder.maybeSingle = vi.fn().mockImplementation(() => Promise.resolve(workflowSelectResult)); + // biome-ignore lint/suspicious/noThenProperty: thenable mock + builder.then = (resolve: any) => { + if (builder._verb === "update") return resolve({ data: null, error: null }); + if (table === "workflow_trigger") return resolve(triggerListResult); + return resolve({ data: null, error: null }); + }; + const origUpdate = builder.update; + builder.update = vi.fn((payload: any) => { + builder._verb = "update"; + updates.push({ table, payload }); + return origUpdate(payload); + }); + return builder; +} + +const mockSupabase = { from: vi.fn((t: string) => createChain(t)) }; +vi.mock("@/lib/db", () => ({ getSupabase: () => mockSupabase })); + +const mockRunAction = vi.fn(); +vi.mock("@/lib/zapier/execution", () => ({ + runAction: (...args: unknown[]) => mockRunAction(...args), +})); + +const mockExecuteWorkflow = vi.fn(); +vi.mock("@/lib/workflows/engine", () => ({ + executeWorkflow: (...args: unknown[]) => mockExecuteWorkflow(...args), +})); + +async function* gen(events: unknown[] = [{ type: "complete" }]): AsyncGenerator { + for (const e of events) yield e; +} + +function pollRow(over: Partial> = {}) { + return { + id: "trig-1", + workflow_id: "wf-1", + config: JSON.stringify({ + app: "gmail", + action: "new_email", + dedupeKey: "id", + intervalMinutes: 5, + }), + last_fired_at: null, + last_dedupe_key: null, + ...over, + }; +} + +const lastTriggerUpdate = () => + [...updates].reverse().find((u) => u.table === "workflow_trigger")?.payload; + +// ─── Tests ─────────────────────────────────────────────────────────────────── + +describe("ZapierPollSignalProvider.runDuePolls", () => { + beforeEach(() => { + vi.resetAllMocks(); + triggerListResult = { data: [], error: null }; + workflowSelectResult = { data: { user_id: "user-1" }, error: null }; + updates.length = 0; + mockExecuteWorkflow.mockReturnValue(gen()); + }); + + it("first poll establishes the baseline — fires nothing, sets the cursor to newest", async () => { + triggerListResult = { data: [pollRow({ last_dedupe_key: null })], error: null }; + mockRunAction.mockResolvedValue({ data: [{ id: "3" }, { id: "2" }, { id: "1" }] }); + + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:00:00Z")); + + expect(out.polled).toBe(1); + expect(out.fired).toBe(0); + expect(mockExecuteWorkflow).not.toHaveBeenCalled(); + expect(lastTriggerUpdate()?.last_dedupe_key).toBe("3"); + }); + + it("fires once per new record above the cursor, oldest-first, then advances", async () => { + triggerListResult = { data: [pollRow({ last_dedupe_key: "1" })], error: null }; + mockRunAction.mockResolvedValue({ data: [{ id: "3" }, { id: "2" }, { id: "1" }] }); + + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:00:00Z")); + + expect(out.fired).toBe(2); + expect(mockExecuteWorkflow).toHaveBeenCalledTimes(2); + expect(mockExecuteWorkflow.mock.calls[0][2]).toMatchObject({ id: "2" }); + expect(mockExecuteWorkflow.mock.calls[1][2]).toMatchObject({ id: "3" }); + expect(lastTriggerUpdate()?.last_dedupe_key).toBe("3"); + }); + + it("fires nothing when the newest record equals the cursor (no new data)", async () => { + triggerListResult = { data: [pollRow({ last_dedupe_key: "3" })], error: null }; + mockRunAction.mockResolvedValue({ data: [{ id: "3" }, { id: "2" }] }); + + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:00:00Z")); + + expect(out.fired).toBe(0); + expect(mockExecuteWorkflow).not.toHaveBeenCalled(); + }); + + it("respects the interval gate — skips a trigger polled too recently", async () => { + triggerListResult = { + data: [pollRow({ last_dedupe_key: "1", last_fired_at: "2026-05-10T12:00:00.000Z" })], + error: null, + }; + mockRunAction.mockResolvedValue({ data: [{ id: "9" }, { id: "1" }] }); + + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:02:00Z")); + + expect(out.polled).toBe(0); + expect(mockRunAction).not.toHaveBeenCalled(); + }); + + it("polls again once the interval has elapsed", async () => { + triggerListResult = { + data: [pollRow({ last_dedupe_key: "1", last_fired_at: "2026-05-10T12:00:00.000Z" })], + error: null, + }; + mockRunAction.mockResolvedValue({ data: [{ id: "9" }, { id: "1" }] }); + + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:06:00Z")); + + expect(out.polled).toBe(1); + expect(out.fired).toBe(1); + }); + + it("skips a trigger missing app/action/dedupeKey", async () => { + triggerListResult = { + data: [pollRow({ config: JSON.stringify({ app: "gmail", action: "new_email" }) })], + error: null, + }; + + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:00:00Z")); + + expect(out.polled).toBe(0); + expect(mockRunAction).not.toHaveBeenCalled(); + }); + + it("does not fire when a guardrail demands confirmation", async () => { + triggerListResult = { data: [pollRow({ last_dedupe_key: "1" })], error: null }; + mockRunAction.mockResolvedValue({ __guardrail_confirmation_required: true }); + + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:00:00Z")); + + expect(out.fired).toBe(0); + expect(mockExecuteWorkflow).not.toHaveBeenCalled(); + }); + + it("handles a bare-array read result (no data wrapper)", async () => { + triggerListResult = { data: [pollRow({ last_dedupe_key: "1" })], error: null }; + mockRunAction.mockResolvedValue([{ id: "2" }, { id: "1" }]); + + const { zapierPollProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const out = await zapierPollProvider.runDuePolls(new Date("2026-05-10T12:00:00Z")); + + expect(out.fired).toBe(1); + expect(mockExecuteWorkflow.mock.calls[0][2]).toMatchObject({ id: "2" }); + }); + + it("notify()s the owner's thread with a native dedupeKey for each new record", async () => { + triggerListResult = { data: [pollRow({ last_dedupe_key: "1" })], error: null }; + mockRunAction.mockResolvedValue({ data: [{ id: "2" }, { id: "1" }] }); + + const { ZapierPollSignalProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const provider = new ZapierPollSignalProvider(); + const sendNotificationSignal = vi.fn().mockResolvedValue(undefined); + // Hosting on an agent is what gives the provider notify() — stub that link. + provider.connect({ sendNotificationSignal } as any); + + const out = await provider.runDuePolls(new Date("2026-05-10T12:00:00Z")); + + expect(out.fired).toBe(1); + expect(sendNotificationSignal).toHaveBeenCalledTimes(1); + const [notification, target] = sendNotificationSignal.mock.calls[0]; + expect(notification).toMatchObject({ source: "zapier-poll", dedupeKey: "2" }); + expect(target).toMatchObject({ threadId: "poll:trig-1", resourceId: "user-1" }); + }); + + it("does not throw when no agent is connected (notify is best-effort)", async () => { + triggerListResult = { data: [pollRow({ last_dedupe_key: "1" })], error: null }; + mockRunAction.mockResolvedValue({ data: [{ id: "2" }, { id: "1" }] }); + + const { ZapierPollSignalProvider } = await import("@/mastra/signals/zapier-poll-provider"); + const provider = new ZapierPollSignalProvider(); // not connected + const out = await provider.runDuePolls(new Date("2026-05-10T12:00:00Z")); + + expect(out.fired).toBe(1); // firing still succeeds without notify + }); +}); diff --git a/packages/web/package.json b/packages/web/package.json index dcf8ca67..e936c01b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -58,10 +58,10 @@ "prosemirror-state": "^1.4.3", "prosemirror-view": "^1.34.3", "radix-ui": "^1.4.3", - "react": "19.2.4", + "react": "^19.2.0", "react-data-grid": "7.0.0-beta.47", "react-day-picker": "^10.0.0", - "react-dom": "19.2.4", + "react-dom": "^19.2.0", "react-resizable-panels": "^4.11.0", "recharts": "^3.8.0", "shiki": "^3.23.0", diff --git a/packages/web/src/app/workflows/[id]/page.tsx b/packages/web/src/app/workflows/[id]/page.tsx index fbf3ccc3..8626ebda 100644 --- a/packages/web/src/app/workflows/[id]/page.tsx +++ b/packages/web/src/app/workflows/[id]/page.tsx @@ -109,6 +109,14 @@ export default async function WorkflowDetailPage({ params }: { params: Promise<{ {new Date(r.created_at).toLocaleString()} + + · {r.fired_by ?? "manual"} + + {r.status === "failed" && r.error_message ? ( + + — {r.error_message} + + ) : null} @@ -138,7 +146,7 @@ function Section({ title, children }: { title: string; children: React.ReactNode function StatusBadge({ status }: { status: WorkflowRun["status"] }) { const map: Record = { success: { bg: "#E6F4EA", fg: "#1B6633", label: "succeeded" }, - error: { bg: "#FCE8E6", fg: "#A61B1B", label: "failed" }, + failed: { bg: "#FCE8E6", fg: "#A61B1B", label: "failed" }, running: { bg: "#FFF1E0", fg: "#7A4A1A", label: "running" }, }; const s = map[status] ?? map.running; diff --git a/packages/web/src/app/workflows/[id]/runs/[runId]/page.tsx b/packages/web/src/app/workflows/[id]/runs/[runId]/page.tsx index 2da66d37..8f59364b 100644 --- a/packages/web/src/app/workflows/[id]/runs/[runId]/page.tsx +++ b/packages/web/src/app/workflows/[id]/runs/[runId]/page.tsx @@ -41,6 +41,8 @@ export default async function RunDetailPage({

status: {run.status} + {" · started by: "} + {run.fired_by ?? "manual"} {duration != null ? ( <> {" · duration: "} @@ -48,6 +50,14 @@ export default async function RunDetailPage({ ) : null}

+ {run.status === "failed" && run.error_message ? ( +

+ {run.error_message} +

+ ) : null}
diff --git a/packages/web/src/lib/workflows-client.ts b/packages/web/src/lib/workflows-client.ts index 1124264c..f3c0e9eb 100644 --- a/packages/web/src/lib/workflows-client.ts +++ b/packages/web/src/lib/workflows-client.ts @@ -37,9 +37,15 @@ export interface WorkflowRun { id: string; workflow_id: string; inputs: Record; - status: "running" | "success" | "error"; + status: "running" | "success" | "failed"; created_at: string; completed_at: string | null; + /** How the run started: 'manual' | 'cron' | 'channel' | 'poll'. */ + fired_by: string | null; + /** Failure reason when status is 'failed'. */ + error_message: string | null; + /** The workflow_trigger that fired this run (null for manual runs). */ + trigger_id: string | null; } export interface WorkflowTrigger { diff --git a/supabase/migrations/20260612000000_workflow_run_observability.sql b/supabase/migrations/20260612000000_workflow_run_observability.sql new file mode 100644 index 00000000..77da845b --- /dev/null +++ b/supabase/migrations/20260612000000_workflow_run_observability.sql @@ -0,0 +1,17 @@ +-- workflow_run observability (foreman-2afc) +-- +-- Persist WHY a run fired and WHY it failed, so trigger-fired runs carry context +-- in run history (previously they only console.warn'd) and failures surface +-- their error instead of just flipping status to 'failed'. +-- +-- error_message — the failure reason when status='failed' +-- fired_by — how the run started: 'manual' | 'cron' | 'channel' | 'poll' +-- trigger_id — the workflow_trigger row that fired it (null for manual runs) + +ALTER TABLE public.workflow_run + ADD COLUMN IF NOT EXISTS error_message text, + ADD COLUMN IF NOT EXISTS fired_by text, + ADD COLUMN IF NOT EXISTS trigger_id text; + +CREATE INDEX IF NOT EXISTS workflow_run_trigger_id_idx + ON public.workflow_run(trigger_id);