From 49fe330168768dbcc4e50f0cc2935df13730c588 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Tue, 1 Sep 2026 08:25:09 +0200 Subject: [PATCH] feat: drop the bespoke Anthropic client for ai-kit's Groq -> OpenRouter chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was the one hand-rolled AI client in a fleet where everything else installs ai-kit — needing its own ANTHROPIC_API_KEY, a credential this fleet doesn't otherwise carry. Every deployment only ever has GROQ_API_KEY / OPENROUTER_API_KEY set, so the four callers (form-assist, session-prep, ai-chat, digest) degraded on every single deployment, silently, exactly as designed — which is a working fallback, not a correct one. lib/domain/anthropic.ts -> lib/domain/llm.ts: same callClaude/callLLM signature and null-on-failure contract every caller already handles, now backed by freeChain('SURF') + usableChain + tryChain across Groq and OpenRouter instead of a single vendor's Messages API. Also wires the same createHealthTracker pattern adopted fleet-wide onto /api/health as an informational `llm` field — never gates the 200/503 the deploy pipeline checks, since a dead key can't be fixed by a restart. Does not touch lib/domain/embeddings.ts (OpenAI text-embedding-3-small, needed for real vector embeddings — a different kind of call ai-kit's chat-only chain doesn't cover). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01HVwg8DKHQktxJuHeLM3xpG --- __tests__/ai-chat-rules.test.ts | 2 +- __tests__/ai-forms.test.ts | 2 +- __tests__/check-in-parse.test.ts | 2 +- __tests__/health.test.ts | 8 +- .../admin/clients/[id]/session-prep/route.ts | 6 +- app/api/ai/form-assist/route.ts | 11 ++- app/api/cron/ai-digest/route.ts | 2 +- app/api/health/route.ts | 7 +- lib/constants.ts | 3 +- lib/domain/ai-chat.ts | 18 ++-- lib/domain/anthropic.ts | 57 ------------ lib/domain/check-in-parse.ts | 4 +- lib/domain/digest.ts | 4 +- lib/domain/llm.ts | 93 +++++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 42 ++++++++- pnpm-workspace.yaml | 1 + 17 files changed, 171 insertions(+), 92 deletions(-) delete mode 100644 lib/domain/anthropic.ts create mode 100644 lib/domain/llm.ts diff --git a/__tests__/ai-chat-rules.test.ts b/__tests__/ai-chat-rules.test.ts index eb0aaea..c506dc9 100644 --- a/__tests__/ai-chat-rules.test.ts +++ b/__tests__/ai-chat-rules.test.ts @@ -1,6 +1,6 @@ /** * Unit tests for the rule-based AI response path in lib/domain/ai-chat.ts. - * This is the fallback used when ANTHROPIC_API_KEY is not set. + * This is the fallback used when no chain provider is configured. * ruleBasedResponse is a pure function: no DB, no network. */ import { describe, it, expect, vi } from "vitest"; diff --git a/__tests__/ai-forms.test.ts b/__tests__/ai-forms.test.ts index ee16d18..173efd5 100644 --- a/__tests__/ai-forms.test.ts +++ b/__tests__/ai-forms.test.ts @@ -26,7 +26,7 @@ const field = (name: string) => { const writable = CHECK_IN_FORM.fields.filter((f) => !f.aiExcluded).map((f) => f.name); -/** Stands in for the route's `complete` on a deployment with no ANTHROPIC_API_KEY. */ +/** Stands in for the route's `complete` on a deployment with no chain provider configured. */ const keylessComplete = (instruction: string, intent: "fill" | "refine") => async () => JSON.stringify({ values: keywordFallback(instruction, intent), message: "" }); diff --git a/__tests__/check-in-parse.test.ts b/__tests__/check-in-parse.test.ts index 4e7b143..6d77ad5 100644 --- a/__tests__/check-in-parse.test.ts +++ b/__tests__/check-in-parse.test.ts @@ -1,7 +1,7 @@ /** * Unit tests for the keyword-based check-in parser (lib/domain/check-in-parse.ts). * keywordParse is pure: no DB, no network, no mocks needed. - * This is the fallback path used when ANTHROPIC_API_KEY is not set. + * This is the fallback path used when no chain provider is configured. */ import { describe, it, expect } from "vitest"; import { keywordParse } from "@/lib/domain/check-in-parse"; diff --git a/__tests__/health.test.ts b/__tests__/health.test.ts index fdf1489..468348a 100644 --- a/__tests__/health.test.ts +++ b/__tests__/health.test.ts @@ -17,11 +17,15 @@ beforeEach(() => { }); describe("GET /api/health", () => { - it("returns 200 with { success: true } when the database answers", async () => { + it("returns 200 with { success: true } and the LLM chain's health when the database answers", async () => { execute.mockResolvedValueOnce([{ "?column?": 1 }]); const res = await GET(); expect(res.status).toBe(200); - expect(await res.json()).toEqual({ success: true }); + const body = await res.json(); + expect(body.success).toBe(true); + // Informational only — asserting shape, not a specific status, since + // this must never gate the 200/503 the deploy pipeline checks. + expect(body.llm).toMatchObject({ status: expect.any(String) }); }); it("returns 503 with { success: false } when the database is unreachable", async () => { diff --git a/app/api/admin/clients/[id]/session-prep/route.ts b/app/api/admin/clients/[id]/session-prep/route.ts index 84b30f2..65059e3 100644 --- a/app/api/admin/clients/[id]/session-prep/route.ts +++ b/app/api/admin/clients/[id]/session-prep/route.ts @@ -1,7 +1,7 @@ /** * Pre-session AI prep: generates a clinical summary for a practitioner * before a session with a specific client. - * Uses Anthropic API directly (no SDK). Gracefully degrades when key is absent. + * Uses the fleet's ai-kit provider chain (Groq -> OpenRouter). Gracefully degrades when no chain provider is configured. */ import { formatEnumValue, roundOne } from "@/lib/utils"; import { db } from "@/lib/db"; @@ -18,7 +18,7 @@ import { } from "@/lib/db/schema"; import { eq, desc, and, gte, isNull } from "drizzle-orm"; import type { ProgramPhase } from "@/lib/domain/program"; -import { callClaude } from "@/lib/domain/anthropic"; +import { callLLM } from "@/lib/domain/llm"; import { localDateString, addDaysISO } from "@/lib/utils"; import { computeDailyAdherenceTrend } from "@/lib/domain/techniques"; import { computeCurrentProgramWeek } from "@/lib/domain/check-in"; @@ -140,7 +140,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str activeEnrollment ?? null, ); - const aiSummary = await callClaude({ + const aiSummary = await callLLM({ messages: [ { role: "user", diff --git a/app/api/ai/form-assist/route.ts b/app/api/ai/form-assist/route.ts index ea2bfda..33519a1 100644 --- a/app/api/ai/form-assist/route.ts +++ b/app/api/ai/form-assist/route.ts @@ -11,9 +11,10 @@ * * Why this is hand-written instead of `createFormAssistHandler`: the package's * handler owns the request, and this app needs the raw instruction inside - * `complete` so it can answer from the keyword parser when there is no - * ANTHROPIC_API_KEY. Owning the request here is what keeps the check-in form - * working on a deployment with no model at all — the property the previous + * `complete` so it can answer from the keyword parser when no chain provider + * (GROQ_API_KEY / OPENROUTER_API_KEY) is configured. Owning the request here + * is what keeps the check-in form working on a deployment with no model at + * all — the property the previous * /api/check-in/parse route had and which a straight port would have deleted. */ @@ -23,7 +24,7 @@ import { runFormAssist, type CompleteFn } from "@fleet/ai-forms"; import { requireAuth } from "@/lib/api"; import { AI_FORMS } from "@/lib/config/ai-forms"; import { API_ERR_INVALID_INPUT, FIELD_MAX_MEDIUM } from "@/lib/constants"; -import { callClaude } from "@/lib/domain/anthropic"; +import { callLLM } from "@/lib/domain/llm"; import { keywordFallback } from "@/lib/domain/check-in-parse"; const requestSchema = z.object({ @@ -57,7 +58,7 @@ export async function POST(req: Request) { } const complete: CompleteFn = async ({ system, prompt, maxTokens }) => { - const text = await callClaude({ + const text = await callLLM({ messages: [{ role: "user", content: prompt }], system, maxTokens, diff --git a/app/api/cron/ai-digest/route.ts b/app/api/cron/ai-digest/route.ts index 19e09a0..a25604c 100644 --- a/app/api/cron/ai-digest/route.ts +++ b/app/api/cron/ai-digest/route.ts @@ -3,7 +3,7 @@ * Runs every Sunday at 19:00 CET (after weekly-report at 17:00). * For each client with ≥3 check-ins in the past 7 days, generates an AI * narrative summary and stores it in the most recent check-in's aiInsight field. - * Gracefully degrades when ANTHROPIC_API_KEY is absent. + * Gracefully degrades when no chain provider (GROQ_API_KEY / OPENROUTER_API_KEY) is configured. */ import { NextResponse } from "next/server"; import { db } from "@/lib/db"; diff --git a/app/api/health/route.ts b/app/api/health/route.ts index 867c42c..e24c4a4 100644 --- a/app/api/health/route.ts +++ b/app/api/health/route.ts @@ -3,11 +3,16 @@ export const dynamic = "force-dynamic"; import { NextResponse } from "next/server"; import { sql } from "drizzle-orm"; import { db } from "@/lib/db"; +import { getLLMHealth } from "@/lib/domain/llm"; // Public health check (fleet convention): 200 = the app is up AND its database // answers. Deploy monitoring curls this after every push — a status code is the // contract, the body is for humans. The body never exposes internals; the // detail goes to the logs, where the person fixing it is already looking. +// +// `llm` is informational only — it never flips the status code. A dead +// provider key can't be fixed by a restart, so it must never fail the check +// that triggers one. @see ai-kit's createHealthTracker, adopted fleet-wide. export async function GET() { try { await db.execute(sql`select 1`); @@ -16,5 +21,5 @@ export async function GET() { return NextResponse.json({ success: false, error: "database unreachable" }, { status: 503 }); } - return NextResponse.json({ success: true }); + return NextResponse.json({ success: true, llm: getLLMHealth() }); } diff --git a/lib/constants.ts b/lib/constants.ts index 3d01d93..6d45ffc 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -105,8 +105,7 @@ export const COMPANY_ADDRESS = "Surf Your Life · Zollikerstrasse 183, 8008 Zür // "today" matches what the user sees on their wall clock, not UTC. export const CLINIC_TZ = "Europe/Zurich"; -// AI model identifiers — update here when upgrading models -export const AI_MODEL_FAST = "claude-haiku-4-5-20251001"; +// Embedding model — chat models come from ai-kit's chain, not from here. export const EMBEDDING_MODEL = "text-embedding-3-small"; export const OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings"; diff --git a/lib/domain/ai-chat.ts b/lib/domain/ai-chat.ts index 0ca34d4..b4c5b15 100644 --- a/lib/domain/ai-chat.ts +++ b/lib/domain/ai-chat.ts @@ -1,10 +1,10 @@ /** * AI chat logic for the client portal. * - * When ANTHROPIC_API_KEY is set: calls Claude with check-in context. - * When not set: returns a rule-based response grounded in the client's actual data. + * When GROQ_API_KEY or OPENROUTER_API_KEY is set: calls the chain with + * check-in context. When neither is set: returns a rule-based response + * grounded in the client's actual data. * - * To enable AI: set ANTHROPIC_API_KEY in your environment. * The generateAiReply function handles both paths — no other changes needed. */ @@ -29,7 +29,7 @@ import { } from "@/lib/constants"; import { summariseCheckIns, computeCurrentProgramWeek } from "@/lib/domain/check-in"; export { summariseCheckIns, type CheckInSummaryRow } from "@/lib/domain/check-in"; -import { callClaude } from "@/lib/domain/anthropic"; +import { callLLM } from "@/lib/domain/llm"; import { semanticCheckInSearch } from "@/lib/domain/embeddings"; import { localDateString, addDaysISO } from "@/lib/utils"; import { computeDailyAdherenceTrend } from "@/lib/domain/techniques"; @@ -472,9 +472,9 @@ export function ruleBasedResponse( return `Here's a quick snapshot: over your last ${stats.count} check-ins, your average energy is ${stats.avgEnergy.toFixed(1)}/10${sleepSummary}, and you've had ${stats.pemCount} PEM episode${stats.pemCount === 1 ? "" : "s"}. Ask me about any of these in more detail. ${dataNote}`; } -// ─── AI call (to be enabled when ANTHROPIC_API_KEY is set) ──────────────────── +// ─── AI call (no-op when no chain provider is configured) ───────────────────── -async function callAnthropicApi( +async function callChainChat( userMessage: string, context: BuildContextResult, history: { role: "user" | "assistant"; content: string }[], @@ -548,7 +548,7 @@ ${techniqueLines} Current program: ${programLine}`; - return callClaude({ + return callLLM({ messages: [...history.slice(-AI_CHAT_CONTEXT_WINDOW), { role: "user", content: userMessage }], system: systemPrompt, maxTokens: 500, @@ -572,8 +572,8 @@ export async function generateAiReply( activeEnrollment, } = context; - // Try AI first (no-op until ANTHROPIC_API_KEY is set) - const aiReply = await callAnthropicApi(userMessage, context, history); + // Try AI first (no-op if no chain provider is configured) + const aiReply = await callChainChat(userMessage, context, history); if (aiReply) return aiReply; // Fall back to rule-based response grounded in actual data diff --git a/lib/domain/anthropic.ts b/lib/domain/anthropic.ts deleted file mode 100644 index ac7b167..0000000 --- a/lib/domain/anthropic.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Thin wrapper around the Anthropic Messages API. - * Centralises auth headers, API version, and error handling. - * Returns null on any failure — all callers gracefully degrade. - */ -import { AI_MODEL_FAST } from "@/lib/constants"; - -const ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages"; -const ANTHROPIC_VERSION = "2023-06-01"; - -type Message = { role: "user" | "assistant"; content: string }; - -type CallOptions = { - messages: Message[]; - system?: string; - maxTokens?: number; - model?: string; -}; - -/** - * Call Claude and return the text of the first content block, or null on failure. - * Requires ANTHROPIC_API_KEY to be set — returns null otherwise. - */ -export async function callClaude({ - messages, - system, - maxTokens = 500, - model = AI_MODEL_FAST, -}: CallOptions): Promise { - const apiKey = process.env.ANTHROPIC_API_KEY; - if (!apiKey) return null; - - try { - const body: Record = { - model, - max_tokens: maxTokens, - messages, - }; - if (system) body.system = system; - - const res = await fetch(ANTHROPIC_API_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-api-key": apiKey, - "anthropic-version": ANTHROPIC_VERSION, - }, - body: JSON.stringify(body), - }); - - if (!res.ok) return null; - const data = await res.json(); - return data.content?.[0]?.text ?? null; - } catch { - return null; - } -} diff --git a/lib/domain/check-in-parse.ts b/lib/domain/check-in-parse.ts index 4719a89..c7eeb5c 100644 --- a/lib/domain/check-in-parse.ts +++ b/lib/domain/check-in-parse.ts @@ -2,8 +2,8 @@ * Parse a free-text description of a day into structured check-in fields, * using keyword heuristics and no network call at all. * - * This is what /api/ai/form-assist answers with when ANTHROPIC_API_KEY is not - * set, so the check-in form still fills itself in on a deployment with no + * This is what /api/ai/form-assist answers with when no chain provider is + * configured, so the check-in form still fills itself in on a deployment with no * model. The AI path lives in lib/config/ai-forms.ts — the field list there is * the only description of these fields the model ever sees, which is why there * is no hand-written extraction prompt duplicating the list below. diff --git a/lib/domain/digest.ts b/lib/domain/digest.ts index fcfcb37..a8daf13 100644 --- a/lib/domain/digest.ts +++ b/lib/domain/digest.ts @@ -1,4 +1,4 @@ -import { callClaude } from "@/lib/domain/anthropic"; +import { callLLM } from "@/lib/domain/llm"; type CheckInRow = { createdAt: Date; @@ -63,5 +63,5 @@ Write a concise clinical narrative (3-5 sentences) summarising: Be factual, empathetic, and clinically precise. No bullet points — flowing prose only.`; - return callClaude({ messages: [{ role: "user", content: prompt }], maxTokens: 300 }); + return callLLM({ messages: [{ role: "user", content: prompt }], maxTokens: 300 }); } diff --git a/lib/domain/llm.ts b/lib/domain/llm.ts new file mode 100644 index 0000000..dd927da --- /dev/null +++ b/lib/domain/llm.ts @@ -0,0 +1,93 @@ +/** + * Chat completion via the fleet's shared AI provider chain (Groq -> OpenRouter). + * + * This used to be a bespoke Anthropic-only client, the one hand-rolled + * provider in a fleet where everything else installs `ai-kit`. It needed its + * own `ANTHROPIC_API_KEY` — a credential this fleet doesn't otherwise carry — + * so every caller degraded (silently, by design) on every deployment that + * only ever had GROQ_API_KEY / OPENROUTER_API_KEY set, which is all of them. + * + * Same graceful-degrade contract as before: returns null on any failure + * (no key configured, every vendor refused) rather than throwing, because + * every caller here already treats a null response as "fall back to the + * non-AI path," not as an error to surface. + */ +import { freeChain, usableChain, chainFrom, tryChain, createHealthTracker } from "ai-kit"; + +const health = createHealthTracker({ downAfter: 3 }); + +export function getLLMHealth() { + return health.getHealth(); +} + +type Message = { role: "user" | "assistant"; content: string }; + +type CallOptions = { + messages: Message[]; + system?: string; + maxTokens?: number; + /** Vendor-specific model id. Ignored — model choice comes from the chain. */ + model?: string; +}; + +/** + * Call the chain and return the text of the first choice, or null on + * failure (no provider configured, or every provider in the chain refused). + */ +export async function callLLM({ + messages, + system, + maxTokens = 500, +}: CallOptions): Promise { + const links = usableChain(freeChain("SURF"), process.env); + if (links.length === 0) return null; + + // One link per vendor — a second model at the same vendor draws on the + // same daily budget, so it is not a real fallback. @see ai-kit chain.ts + const seen = new Set(); + const chain = links.filter((link) => { + if (seen.has(link.provider.id)) return false; + seen.add(link.provider.id); + return true; + }); + + try { + return await tryChain(chain, { + health, + attempt: async (link) => { + const [resolved] = chainFrom(undefined, [link]); + const res = await fetch(`${link.provider.baseUrl}/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env[link.provider.keyEnv] ?? ""}`, + }, + body: JSON.stringify({ + model: resolved?.model ?? link.model, + max_tokens: maxTokens, + messages: system ? [{ role: "system", content: system }, ...messages] : messages, + }), + }); + + if (!res.ok) { + // Body goes on the thrown error for the log only — never returned. + const detail = await res.text().catch(() => ""); + throw new Error( + `${link.provider.id} chat failed (${res.status}): ${detail.slice(0, 200)}`, + ); + } + + const body = (await res.json()) as { + choices?: Array<{ message?: { content?: string } }>; + }; + const text = body.choices?.[0]?.message?.content; + if (!text) throw new Error(`${link.provider.id} returned no content`); + return text; + }, + }); + } catch { + // ChainExhaustedError or an empty chain — every caller treats null as + // "fall back," matching the contract this replaces. + return null; + } +} diff --git a/package.json b/package.json index e6b41cb..edfb7b4 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@auth/drizzle-adapter": "^1.11.1", "@fleet/ai-forms": "github:bitbaum/ai-forms#v0.1.0", "@sentry/nextjs": "^10.66.0", + "ai-kit": "github:bitbaum/ai-kit#v0.6.2", "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7317e3d..86a27d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@sentry/nextjs': specifier: ^10.66.0 version: 10.66.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.108.4(esbuild@0.27.7)) + ai-kit: + specifier: github:bitbaum/ai-kit#v0.6.2 + version: https://codeload.github.com/bitbaum/ai-kit/tar.gz/ace11f14d817079ae2bf4cdc010b6daa9faacbec(react@19.2.4) bcryptjs: specifier: ^3.0.3 version: 3.0.3 @@ -2093,6 +2096,25 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + ai-forms@0.1.2: + resolution: {integrity: sha512-agadca4pN0iGlZTlNy1Vo2SmnQB+0dNkrQSDE3wCJYeaVxKDs5KaxiwGj5+GL6mVYcI1xZ8pwnT9kL9WcxnBHA==} + engines: {node: '>=18'} + peerDependencies: + react: '>=18' + peerDependenciesMeta: + react: + optional: true + + ai-kit@https://codeload.github.com/bitbaum/ai-kit/tar.gz/ace11f14d817079ae2bf4cdc010b6daa9faacbec: + resolution: {gitHosted: true, integrity: sha512-WQ963X+XuNATqTSCZqdgsItFgrO3rGc9cn9psLUkdR5BS4hgRpdN55nBXmOZ8j/+811XdcY54OgxTf2gfgMNiw==, tarball: https://codeload.github.com/bitbaum/ai-kit/tar.gz/ace11f14d817079ae2bf4cdc010b6daa9faacbec} + version: 0.6.2 + engines: {node: '>=20'} + peerDependencies: + react: '>=18' + peerDependenciesMeta: + react: + optional: true + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -5742,6 +5764,16 @@ snapshots: transitivePeerDependencies: - supports-color + ai-forms@0.1.2(react@19.2.4): + optionalDependencies: + react: 19.2.4 + + ai-kit@https://codeload.github.com/bitbaum/ai-kit/tar.gz/ace11f14d817079ae2bf4cdc010b6daa9faacbec(react@19.2.4): + dependencies: + ai-forms: 0.1.2(react@19.2.4) + optionalDependencies: + react: 19.2.4 + ajv-formats@2.1.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -6235,7 +6267,7 @@ snapshots: '@next/eslint-plugin-next': 16.2.3 eslint: 10.9.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@10.9.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.1(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.9.1(jiti@2.6.1)))(eslint@10.9.1(jiti@2.6.1)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.1(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@10.9.1(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@10.9.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@10.9.1(jiti@2.6.1)) @@ -6258,7 +6290,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@10.9.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.1(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.9.1(jiti@2.6.1)))(eslint@10.9.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -6273,14 +6305,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.1(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@10.9.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.1(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.1(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.9.1(jiti@2.6.1)))(eslint@10.9.1(jiti@2.6.1)))(eslint@10.9.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.58.1(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3) eslint: 10.9.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@10.9.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.1(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.9.1(jiti@2.6.1)))(eslint@10.9.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -6295,7 +6327,7 @@ snapshots: doctrine: 2.1.0 eslint: 10.9.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.1(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@10.9.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.1(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.1(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.9.1(jiti@2.6.1)))(eslint@10.9.1(jiti@2.6.1)))(eslint@10.9.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 07634f1..2a9179d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,7 @@ allowBuilds: # pnpm requires the exact resolved tarball here, not the bare name — bump # this SHA together with the #vX.Y.Z tag in package.json. "@fleet/ai-forms@https://codeload.github.com/bitbaum/ai-forms/tar.gz/68087895ab9d46d73d03b91ebf5e648b969ebd76": true + "ai-kit@https://codeload.github.com/bitbaum/ai-kit/tar.gz/ace11f14d817079ae2bf4cdc010b6daa9faacbec": true "@parcel/watcher": true "@sentry/cli": true "@swc/core": true