From 541f1c17b7caf862799ef652e50f5b69ca855110 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 8 Mar 2026 01:01:43 +0100 Subject: [PATCH] feat(mcps): add kabelfachmann MCP with Kabelhandbuch integration and remove legacy PM2 orchestration --- .gitignore | 6 +- docker-compose.mcps.yml | 18 +- ecosystem.mcps.config.cjs | 48 ---- package.json | 7 +- packages/kabelfachmann-mcp/Dockerfile | 11 + packages/kabelfachmann-mcp/package.json | 31 ++ packages/kabelfachmann-mcp/src/index.ts | 142 +++++++++ packages/kabelfachmann-mcp/src/ingest.ts | 76 +++++ packages/kabelfachmann-mcp/src/llm.ts | 41 +++ packages/kabelfachmann-mcp/src/qdrant.ts | 104 +++++++ packages/kabelfachmann-mcp/src/start.ts | 16 ++ .../kabelfachmann-mcp/test-kabelfachmann.js | 38 +++ packages/kabelfachmann-mcp/tsconfig.json | 15 + .../src/endpoints/generateEndpoints.ts | 271 ++++++++++-------- .../src/endpoints/optimizeEndpoint.ts | 173 ++++++----- pnpm-lock.yaml | 123 +++++++- 16 files changed, 866 insertions(+), 254 deletions(-) delete mode 100644 ecosystem.mcps.config.cjs create mode 100644 packages/kabelfachmann-mcp/Dockerfile create mode 100644 packages/kabelfachmann-mcp/package.json create mode 100644 packages/kabelfachmann-mcp/src/index.ts create mode 100644 packages/kabelfachmann-mcp/src/ingest.ts create mode 100644 packages/kabelfachmann-mcp/src/llm.ts create mode 100644 packages/kabelfachmann-mcp/src/qdrant.ts create mode 100644 packages/kabelfachmann-mcp/src/start.ts create mode 100644 packages/kabelfachmann-mcp/test-kabelfachmann.js create mode 100644 packages/kabelfachmann-mcp/tsconfig.json diff --git a/.gitignore b/.gitignore index e17622f..3870518 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,8 @@ apps/web/out/estimations/ # Memory MCP data/qdrant/ -packages/memory-mcp/models/ \ No newline at end of file +packages/memory-mcp/models/ + +# Kabelfachmann MCP +packages/kabelfachmann-mcp/data/ +packages/kabelfachmann-mcp/models/ \ No newline at end of file diff --git a/docker-compose.mcps.yml b/docker-compose.mcps.yml index f2c598a..c299acf 100644 --- a/docker-compose.mcps.yml +++ b/docker-compose.mcps.yml @@ -3,8 +3,8 @@ services: image: qdrant/qdrant:latest container_name: qdrant-mcp ports: - - "6333:6333" - - "6334:6334" + - "6335:6333" + - "6336:6334" volumes: - ./data/qdrant:/qdrant/storage restart: unless-stopped @@ -85,6 +85,20 @@ services: networks: - mcp-network + kabelfachmann-mcp: + build: + context: ./packages/kabelfachmann-mcp + container_name: kabelfachmann-mcp + env_file: + - .env + ports: + - "3007:3007" + depends_on: + - qdrant + restart: unless-stopped + networks: + - mcp-network + networks: mcp-network: driver: bridge diff --git a/ecosystem.mcps.config.cjs b/ecosystem.mcps.config.cjs deleted file mode 100644 index a7526e0..0000000 --- a/ecosystem.mcps.config.cjs +++ /dev/null @@ -1,48 +0,0 @@ -module.exports = { - apps: [ - { - name: 'gitea-mcp', - script: 'node', - args: 'dist/start.js', - cwd: './packages/gitea-mcp', - watch: false, - }, - { - name: 'memory-mcp', - script: 'node', - args: 'dist/start.js', - cwd: './packages/memory-mcp', - watch: false, - }, - { - name: 'umami-mcp', - script: 'node', - args: 'dist/start.js', - cwd: './packages/umami-mcp', - watch: false, - }, - { - name: 'serpbear-mcp', - script: 'node', - args: 'dist/start.js', - cwd: './packages/serpbear-mcp', - watch: false, - }, - { - name: 'glitchtip-mcp', - script: 'node', - args: 'dist/start.js', - cwd: './packages/glitchtip-mcp', - watch: false, - }, - { - name: 'klz-payload-mcp', - script: 'node', - args: 'dist/start.js', - cwd: './packages/klz-payload-mcp', - watch: false, - }, - ] -}; - - diff --git a/package.json b/package.json index 9220dec..cbc1f1e 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,10 @@ "dev": "pnpm -r dev", "dev:gatekeeper": "bash -c 'trap \"COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml down && COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml up --build --remove-orphans'", "dev:mcps:up": "docker-compose -f docker-compose.mcps.yml up -d", - "dev:mcps:down": "docker-compose -f docker-compose.mcps.yml down && pm2 delete ecosystem.mcps.config.cjs || true", + "dev:mcps:down": "docker-compose -f docker-compose.mcps.yml down", "dev:mcps:watch": "pnpm -r --filter=\"./packages/*-mcp\" exec tsc -w", - "dev:mcps": "npm run dev:mcps:up && pm2 start ecosystem.mcps.config.cjs --watch && npm run dev:mcps:watch", - "start:mcps:run": "pm2 start ecosystem.mcps.config.cjs", - "start:mcps": "npm run dev:mcps:up && npm run start:mcps:run", + "dev:mcps": "npm run dev:mcps:up && npm run dev:mcps:watch", + "start:mcps": "npm run dev:mcps:up", "lint": "pnpm -r --filter='./packages/**' --filter='./apps/**' lint", "test": "pnpm -r test", "changeset": "changeset", diff --git a/packages/kabelfachmann-mcp/Dockerfile b/packages/kabelfachmann-mcp/Dockerfile new file mode 100644 index 0000000..da517e6 --- /dev/null +++ b/packages/kabelfachmann-mcp/Dockerfile @@ -0,0 +1,11 @@ +FROM node:20-slim +WORKDIR /app +COPY package.json ./ + +# Install prod dependencies +RUN npm install --omit=dev --legacy-peer-deps + +COPY ./dist ./dist +COPY ./data ./data + +ENTRYPOINT ["node", "dist/index.js"] diff --git a/packages/kabelfachmann-mcp/package.json b/packages/kabelfachmann-mcp/package.json new file mode 100644 index 0000000..ad14883 --- /dev/null +++ b/packages/kabelfachmann-mcp/package.json @@ -0,0 +1,31 @@ +{ + "name": "@mintel/kabelfachmann-mcp", + "version": "1.0.0", + "description": "Kabelfachmann MCP server", + "main": "dist/index.js", + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "tsx watch src/index.ts", + "ingest": "tsx src/ingest.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.5.0", + "@qdrant/js-client-rest": "^1.12.0", + "@xenova/transformers": "^2.17.2", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "node-fetch": "^3.3.2", + "onnxruntime-node": "^1.14.0", + "pdf-parse": "^1.1.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "@types/node": "^20.14.10", + "@types/pdf-parse": "^1.1.4", + "tsx": "^4.19.1", + "typescript": "^5.5.3" + } +} diff --git a/packages/kabelfachmann-mcp/src/index.ts b/packages/kabelfachmann-mcp/src/index.ts new file mode 100644 index 0000000..b5d1851 --- /dev/null +++ b/packages/kabelfachmann-mcp/src/index.ts @@ -0,0 +1,142 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import express from "express"; +import { z } from "zod"; +import { QdrantMemoryService } from "./qdrant.js"; +import { askOpenRouter } from "./llm.js"; + +async function main() { + const server = new McpServer({ + name: "@mintel/kabelfachmann-mcp", + version: "1.0.0", + }); + + const qdrantService = new QdrantMemoryService(); + + server.tool( + "ask_kabelfachmann", + "Ask the KLZ Kabelfachmann a question about cables based on the KLZ technical handbook. This consultant knows everything about cable specifications, geometries, weights, ampacity (Strombelastbarkeit), and materials.", + { + query: z + .string() + .describe( + "The user's question regarding cables or a specific cable type.", + ), + }, + async (args) => { + console.error(`Kabelfachmann received query: ${args.query}`); + + // Retrieve relevant chunks from the handbook + const results = await qdrantService.retrieveMemory(args.query, 10); + + const contextText = results + .map( + (r) => + `--- Excerpt (Relevance: ${r.score.toFixed(2)}) ---\n${r.content}`, + ) + .join("\n\n"); + + if (!contextText) { + return { + content: [ + { + type: "text", + text: "Der Kabelfachmann konnte keine relevanten Informationen im Handbuch finden.", + }, + ], + }; + } + + const systemPrompt = `Du bist der "KLZ Kabelfachmann" (KLZ Cable Expert), ein professioneller beratender KI-Experte. +Du arbeitest für die Kabeltechnik-Firma "KLZ". +Beantworte die folgende Frage des Nutzers fachlich absolut korrekt und **nur** basierend auf den bereitgestellten Auszügen aus dem KLZ Kabelhandbuch. +Wenn die Information nicht im Kontext enthalten ist, sage höflich, dass dir dazu keine KLZ-Daten vorliegen. Erfinde niemals Spezifikationen oder Daten. +Halte dich relativ knapp und präzise, aber professionell (Siezen). +Hier ist der Kontext aus dem Handbuch: + +${contextText}`; + + try { + const answer = await askOpenRouter(systemPrompt, args.query); + return { + content: [{ type: "text", text: answer }], + }; + } catch (error: any) { + console.error("Error querying OpenRouter:", error); + return { + content: [ + { + type: "text", + text: `Ein Fehler ist bei der KI-Anfrage aufgetreten: ${error.message}`, + }, + ], + isError: true, + }; + } + }, + ); + + const isStdio = process.argv.includes("--stdio"); + + if (isStdio) { + const { StdioServerTransport } = + await import("@modelcontextprotocol/sdk/server/stdio.js"); + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Kabelfachmann MCP server is running on stdio"); + + try { + await qdrantService.initialize(); + } catch (e) { + console.error("Failed to initialize local dependencies:", e); + } + } else { + const app = express(); + let transport: SSEServerTransport | null = null; + + app.get("/sse", async (req, res) => { + console.error("New SSE connection established"); + + if (transport) { + try { + await transport.close(); + } catch (e) { + console.error("Error closing previous transport:", e); + } + } + + transport = new SSEServerTransport("/message", res); + try { + await server.connect(transport); + } catch (e) { + console.error("Failed to connect new transport:", e); + } + }); + + app.post("/message", async (req, res) => { + if (!transport) { + res.status(400).send("No active SSE connection"); + return; + } + await transport.handlePostMessage(req, res); + }); + + const PORT = process.env.KABELFACHMANN_MCP_PORT || 3007; + const HOST = process.env.HOST || "0.0.0.0"; + app.listen(PORT as number, HOST, async () => { + console.error( + `Kabelfachmann MCP server running on http://${HOST}:${PORT}/sse`, + ); + try { + await qdrantService.initialize(); + } catch (e) { + console.error("Failed to initialize local dependencies:", e); + } + }); + } +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/packages/kabelfachmann-mcp/src/ingest.ts b/packages/kabelfachmann-mcp/src/ingest.ts new file mode 100644 index 0000000..e70dc1e --- /dev/null +++ b/packages/kabelfachmann-mcp/src/ingest.ts @@ -0,0 +1,76 @@ +import fs from "fs"; +import path from "path"; +import pdf from "pdf-parse"; +import { QdrantMemoryService } from "./qdrant.js"; + +async function start() { + const qdrant = new QdrantMemoryService( + process.env.QDRANT_URL || "http://localhost:6333", + ); + await qdrant.initialize(); + + const pdfPath = path.join(process.cwd(), "data", "pdf", "kabelhandbuch.pdf"); + console.error(`Reading PDF from ${pdfPath}...`); + + let dataBuffer; + try { + dataBuffer = fs.readFileSync(pdfPath); + } catch (e) { + console.error( + "PDF file not found. Ensure it exists at data/pdf/kabelhandbuch.pdf", + ); + process.exit(1); + } + + const data = await pdf(dataBuffer); + const text = data.text; + + // chunk text + // A simple chunking strategy by paragraph or chunks of ~1000 characters + const paragraphs = text + .split(/\n\s*\n/) + .map((p) => p.trim()) + .filter((p) => p.length > 50); + + let currentChunk = ""; + const chunks: string[] = []; + const MAX_CHUNK_LENGTH = 1500; + + for (const p of paragraphs) { + if (currentChunk.length + p.length > MAX_CHUNK_LENGTH) { + chunks.push(currentChunk); + currentChunk = p; + } else { + currentChunk += (currentChunk.length ? "\n\n" : "") + p; + } + } + if (currentChunk.length > 0) { + chunks.push(currentChunk); + } + + console.error( + `Split PDF into ${chunks.length} chunks. Ingesting to Qdrant...`, + ); + + let successCount = 0; + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + const success = await qdrant.storeMemory(`Handbuch Teil ${i + 1}`, chunk); + if (success) { + successCount++; + } + if ((i + 1) % 10 === 0) { + console.error(`Ingested ${i + 1}/${chunks.length} chunks...`); + } + } + + console.error( + `Ingestion complete! Successfully stored ${successCount}/${chunks.length} chunks.`, + ); + process.exit(0); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/packages/kabelfachmann-mcp/src/llm.ts b/packages/kabelfachmann-mcp/src/llm.ts new file mode 100644 index 0000000..cfb4932 --- /dev/null +++ b/packages/kabelfachmann-mcp/src/llm.ts @@ -0,0 +1,41 @@ +import fetch from "node-fetch"; + +export async function askOpenRouter( + systemPrompt: string, + userPrompt: string, +): Promise { + const apiKey = process.env.OPENROUTER_API_KEY; + if (!apiKey) { + throw new Error("OPENROUTER_API_KEY is not set"); + } + + const response = await fetch( + "https://openrouter.ai/api/v1/chat/completions", + { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "HTTP-Referer": "https://mintel.me", + "X-Title": "Mintel MCP", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "google/gemini-3-flash-preview", + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: userPrompt }, + ], + }), + }, + ); + + if (!response.ok) { + const text = await response.text(); + throw new Error( + `OpenRouter API error: ${response.status} ${response.statusText} - ${text}`, + ); + } + + const data = (await response.json()) as any; + return data.choices[0].message.content; +} diff --git a/packages/kabelfachmann-mcp/src/qdrant.ts b/packages/kabelfachmann-mcp/src/qdrant.ts new file mode 100644 index 0000000..3622eaa --- /dev/null +++ b/packages/kabelfachmann-mcp/src/qdrant.ts @@ -0,0 +1,104 @@ +import { pipeline, env } from "@xenova/transformers"; +import { QdrantClient } from "@qdrant/js-client-rest"; +import crypto from "crypto"; + +env.allowRemoteModels = true; +env.localModelPath = "./models"; + +export class QdrantMemoryService { + private client: QdrantClient; + private collectionName = "kabelfachmann"; + private embedder: any = null; + + constructor( + url: string = process.env.QDRANT_URL || "http://qdrant-mcp:6333", + ) { + this.client = new QdrantClient({ url }); + } + + async initialize() { + console.error("Loading embedding model..."); + this.embedder = await pipeline( + "feature-extraction", + "Xenova/all-MiniLM-L6-v2", + ); + + console.error(`Checking for collection: ${this.collectionName}`); + try { + const collections = await this.client.getCollections(); + const exists = collections.collections.some( + (c) => c.name === this.collectionName, + ); + + if (!exists) { + console.error(`Creating collection: ${this.collectionName}`); + await this.client.createCollection(this.collectionName, { + vectors: { + size: 384, + distance: "Cosine", + }, + }); + console.error("Collection created successfully."); + } + } catch (e) { + console.error("Failed to initialize Qdrant collection:", e); + throw e; + } + } + + private async getEmbedding(text: string): Promise { + if (!this.embedder) { + throw new Error("Embedder not initialized. Call initialize() first."); + } + const output = await this.embedder(text, { + pooling: "mean", + normalize: true, + }); + return Array.from(output.data); + } + + async storeMemory(label: string, content: string): Promise { + try { + const vector = await this.getEmbedding(content); + const id = crypto.randomUUID(); + + await this.client.upsert(this.collectionName, { + wait: true, + points: [ + { + id, + vector, + payload: { label, content, timestamp: new Date().toISOString() }, + }, + ], + }); + return true; + } catch (e) { + console.error("Failed to store memory:", e); + return false; + } + } + + async retrieveMemory( + query: string, + limit: number = 5, + ): Promise> { + try { + const vector = await this.getEmbedding(query); + const searchResults = await this.client.search(this.collectionName, { + vector, + limit, + with_payload: true, + }); + + return searchResults.map((result) => ({ + label: String(result.payload?.label || ""), + content: String(result.payload?.content || ""), + score: result.score, + })); + } catch (e) { + console.error("Failed to retrieve memory:", e); + return []; + } + } +} diff --git a/packages/kabelfachmann-mcp/src/start.ts b/packages/kabelfachmann-mcp/src/start.ts new file mode 100644 index 0000000..df03cef --- /dev/null +++ b/packages/kabelfachmann-mcp/src/start.ts @@ -0,0 +1,16 @@ +import { config } from "dotenv"; +import { resolve } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); + +// Try to load .env.local first (contains credentials usually) +config({ quiet: true, path: resolve(__dirname, "../../../.env.local") }); +// Fallback to .env (contains defaults) +config({ quiet: true, path: resolve(__dirname, "../../../.env") }); + +// Now boot the compiled MCP index +import("./index.js").catch((err) => { + console.error("Failed to start MCP Server:", err); + process.exit(1); +}); diff --git a/packages/kabelfachmann-mcp/test-kabelfachmann.js b/packages/kabelfachmann-mcp/test-kabelfachmann.js new file mode 100644 index 0000000..0dfc14d --- /dev/null +++ b/packages/kabelfachmann-mcp/test-kabelfachmann.js @@ -0,0 +1,38 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; + +async function main() { + console.log("Connecting to Kabelfachmann MCP on localhost:3007/sse..."); + const transport = new SSEClientTransport( + new URL("http://localhost:3007/sse"), + ); + const client = new Client( + { name: "test-client", version: "1.0.0" }, + { capabilities: {} }, + ); + + await client.connect(transport); + console.log("Connected! Requesting tools..."); + + const tools = await client.listTools(); + console.log( + "Available tools:", + tools.tools.map((t) => t.name), + ); + + console.log("Calling ask_kabelfachmann..."); + const result = await client.callTool({ + name: "ask_kabelfachmann", + arguments: { + query: + "Was ist der Mindestbiegeradius von einem NYY-J 5x1,5 Kabel laut Handbuch?", + }, + }); + + console.log("\n--- RESULT ---"); + console.log(JSON.stringify(result, null, 2)); + + process.exit(0); +} + +main().catch(console.error); diff --git a/packages/kabelfachmann-mcp/tsconfig.json b/packages/kabelfachmann-mcp/tsconfig.json new file mode 100644 index 0000000..36cd6f4 --- /dev/null +++ b/packages/kabelfachmann-mcp/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"] +} diff --git a/packages/payload-ai/src/endpoints/generateEndpoints.ts b/packages/payload-ai/src/endpoints/generateEndpoints.ts index bd73f12..bd77b9c 100644 --- a/packages/payload-ai/src/endpoints/generateEndpoints.ts +++ b/packages/payload-ai/src/endpoints/generateEndpoints.ts @@ -4,126 +4,157 @@ import * as path from "node:path"; import * as os from "node:os"; async function getOrchestrator() { - const OPENROUTER_KEY = - process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY; - const REPLICATE_KEY = process.env.REPLICATE_API_KEY; + const OPENROUTER_KEY = + process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY; + const REPLICATE_KEY = process.env.REPLICATE_API_KEY; - if (!OPENROUTER_KEY) { - throw new Error( - "Missing OPENROUTER_API_KEY in .env (Required for AI generation)", - ); - } - - const importDynamic = new Function("modulePath", "return import(modulePath)"); - const { AiBlogPostOrchestrator } = await importDynamic( - "@mintel/content-engine", + if (!OPENROUTER_KEY) { + throw new Error( + "Missing OPENROUTER_API_KEY in .env (Required for AI generation)", ); + } - return new AiBlogPostOrchestrator({ - apiKey: OPENROUTER_KEY, - replicateApiKey: REPLICATE_KEY, - model: "google/gemini-3-flash-preview", - }); + const importDynamic = new Function("modulePath", "return import(modulePath)"); + const { AiBlogPostOrchestrator } = await importDynamic( + "@mintel/content-engine", + ); + + return new AiBlogPostOrchestrator({ + apiKey: OPENROUTER_KEY, + replicateApiKey: REPLICATE_KEY, + model: "google/gemini-3-flash-preview", + }); } export const generateSlugEndpoint = async (req: PayloadRequest) => { + try { + let body: any = {}; try { - const { title, draftContent, oldSlug, instructions } = (await req.json?.() || {}) as any; - const orchestrator = await getOrchestrator(); - const newSlug = await orchestrator.generateSlug( - draftContent, - title, - instructions, - ); - - if (oldSlug && oldSlug !== newSlug) { - await req.payload.create({ - collection: "redirects" as any, - data: { - from: oldSlug, - to: newSlug, - }, - }); - } - - return Response.json({ success: true, slug: newSlug }); - } catch (e: any) { - return Response.json({ success: false, error: e.message }, { status: 500 }); + if (req.body) body = (await req.json?.()) || {}; + } catch { + /* ignore */ } -} + const { title, draftContent, oldSlug, instructions } = body; + const orchestrator = await getOrchestrator(); + const newSlug = await orchestrator.generateSlug( + draftContent, + title, + instructions, + ); + + if (oldSlug && oldSlug !== newSlug) { + await req.payload.create({ + collection: "redirects" as any, + data: { + from: oldSlug, + to: newSlug, + }, + }); + } + + return Response.json({ success: true, slug: newSlug }); + } catch (e: any) { + return Response.json({ success: false, error: e.message }, { status: 500 }); + } +}; export const generateThumbnailEndpoint = async (req: PayloadRequest) => { + try { + let body: any = {}; try { - const { draftContent, title, instructions } = (await req.json?.() || {}) as any; - const OPENROUTER_KEY = - process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY; - const REPLICATE_KEY = process.env.REPLICATE_API_KEY; - - if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY in .env"); - if (!REPLICATE_KEY) throw new Error("Missing REPLICATE_API_KEY in .env"); - - const importDynamic = new Function("modulePath", "return import(modulePath)"); - const { AiBlogPostOrchestrator } = await importDynamic("@mintel/content-engine"); - const { ThumbnailGenerator } = await importDynamic("@mintel/thumbnail-generator"); - - const orchestrator = new AiBlogPostOrchestrator({ - apiKey: OPENROUTER_KEY, - replicateApiKey: REPLICATE_KEY, - model: "google/gemini-3-flash-preview", - }); - - const tg = new ThumbnailGenerator({ replicateApiKey: REPLICATE_KEY }); - - const prompt = await orchestrator.generateVisualPrompt( - draftContent || title || "Technology", - instructions, - ); - - const tmpPath = path.join(os.tmpdir(), `mintel-thumb-${Date.now()}.png`); - await tg.generateImage(prompt, tmpPath); - - const fileData = await fs.readFile(tmpPath); - const stat = await fs.stat(tmpPath); - const fileName = path.basename(tmpPath); - - const newMedia = await req.payload.create({ - collection: "media" as any, - data: { - alt: title ? `Thumbnail for ${title}` : "AI Generated Thumbnail", - }, - file: { - data: fileData, - name: fileName, - mimetype: "image/png", - size: stat.size, - }, - }); - - await fs.unlink(tmpPath).catch(() => { }); - - return Response.json({ success: true, mediaId: newMedia.id }); - } catch (e: any) { - return Response.json({ success: false, error: e.message }, { status: 500 }); + if (req.body) body = (await req.json?.()) || {}; + } catch { + /* ignore */ } -} + const { draftContent, title, instructions } = body; + const OPENROUTER_KEY = + process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY; + const REPLICATE_KEY = process.env.REPLICATE_API_KEY; + + if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY in .env"); + if (!REPLICATE_KEY) throw new Error("Missing REPLICATE_API_KEY in .env"); + + const importDynamic = new Function( + "modulePath", + "return import(modulePath)", + ); + const { AiBlogPostOrchestrator } = await importDynamic( + "@mintel/content-engine", + ); + const { ThumbnailGenerator } = await importDynamic( + "@mintel/thumbnail-generator", + ); + + const orchestrator = new AiBlogPostOrchestrator({ + apiKey: OPENROUTER_KEY, + replicateApiKey: REPLICATE_KEY, + model: "google/gemini-3-flash-preview", + }); + + const tg = new ThumbnailGenerator({ replicateApiKey: REPLICATE_KEY }); + + const prompt = await orchestrator.generateVisualPrompt( + draftContent || title || "Technology", + instructions, + ); + + const tmpPath = path.join(os.tmpdir(), `mintel-thumb-${Date.now()}.png`); + await tg.generateImage(prompt, tmpPath); + + const fileData = await fs.readFile(tmpPath); + const stat = await fs.stat(tmpPath); + const fileName = path.basename(tmpPath); + + const newMedia = await req.payload.create({ + collection: "media" as any, + data: { + alt: title ? `Thumbnail for ${title}` : "AI Generated Thumbnail", + }, + file: { + data: fileData, + name: fileName, + mimetype: "image/png", + size: stat.size, + }, + }); + + await fs.unlink(tmpPath).catch(() => {}); + + return Response.json({ success: true, mediaId: newMedia.id }); + } catch (e: any) { + return Response.json({ success: false, error: e.message }, { status: 500 }); + } +}; export const generateSingleFieldEndpoint = async (req: PayloadRequest) => { + try { + let body: any = {}; try { - const { documentTitle, documentContent, fieldName, fieldDescription, instructions } = (await req.json?.() || {}) as any; + if (req.body) body = (await req.json?.()) || {}; + } catch { + /* ignore */ + } + const { + documentTitle, + documentContent, + fieldName, + fieldDescription, + instructions, + } = body; - const OPENROUTER_KEY = - process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY; - if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY"); + const OPENROUTER_KEY = + process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY; + if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY"); - const contextDocsData = await req.payload.find({ - collection: "context-files" as any, - limit: 100, - }); - const projectContext = contextDocsData.docs - .map((doc: any) => `--- ${doc.filename} ---\n${doc.content}`) - .join("\n\n"); + const contextDocsData = await req.payload.find({ + collection: "context-files" as any, + limit: 100, + }); + const projectContext = contextDocsData.docs + .map((doc: any) => `--- ${doc.filename} ---\n${doc.content}`) + .join("\n\n"); - const prompt = `You are an expert AI assistant perfectly trained for generating exact data values for CMS components. + const prompt = `You are an expert AI assistant perfectly trained for generating exact data values for CMS components. PROJECT STRATEGY & CONTEXT: ${projectContext} @@ -138,21 +169,21 @@ CRITICAL RULES: 3. If the field implies a diagram or flow, output RAW Mermaid.js code. 4. If it's standard text, write professional B2B German. No quotes, no conversational filler.`; - const res = await fetch("https://openrouter.ai/api/v1/chat/completions", { - method: "POST", - headers: { - Authorization: `Bearer ${OPENROUTER_KEY}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: "google/gemini-3-flash-preview", - messages: [{ role: "user", content: prompt }], - }), - }); - const data = await res.json(); - const text = data.choices?.[0]?.message?.content?.trim() || ""; - return Response.json({ success: true, text }); - } catch (e: any) { - return Response.json({ success: false, error: e.message }, { status: 500 }); - } -} + const res = await fetch("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${OPENROUTER_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "google/gemini-3-flash-preview", + messages: [{ role: "user", content: prompt }], + }), + }); + const data = await res.json(); + const text = data.choices?.[0]?.message?.content?.trim() || ""; + return Response.json({ success: true, text }); + } catch (e: any) { + return Response.json({ success: false, error: e.message }, { status: 500 }); + } +}; diff --git a/packages/payload-ai/src/endpoints/optimizeEndpoint.ts b/packages/payload-ai/src/endpoints/optimizeEndpoint.ts index 71e5646..278bbaa 100644 --- a/packages/payload-ai/src/endpoints/optimizeEndpoint.ts +++ b/packages/payload-ai/src/endpoints/optimizeEndpoint.ts @@ -1,75 +1,108 @@ -import { PayloadRequest } from 'payload' +import { PayloadRequest } from "payload"; import { parseMarkdownToLexical } from "../utils/lexicalParser.js"; export const optimizePostEndpoint = async (req: PayloadRequest) => { + try { + let body: any = {}; try { - const { draftContent, instructions } = (await req.json?.() || {}) as { draftContent: string; instructions?: string }; - - if (!draftContent) { - return Response.json({ error: 'Missing draftContent' }, { status: 400 }) - } - - const globalAiSettings = (await req.payload.findGlobal({ slug: "ai-settings" })) as any; - const customSources = - globalAiSettings?.customSources?.map((s: any) => s.sourceName) || []; - - const OPENROUTER_KEY = - process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY; - const REPLICATE_KEY = process.env.REPLICATE_API_KEY; - - if (!OPENROUTER_KEY) { - return Response.json({ error: "OPENROUTER_KEY not found in environment." }, { status: 500 }) - } - - // Dynamically import to avoid bundling it into client components that might accidentally import this file - const importDynamic = new Function("modulePath", "return import(modulePath)"); - const { AiBlogPostOrchestrator } = await importDynamic("@mintel/content-engine"); - - const orchestrator = new AiBlogPostOrchestrator({ - apiKey: OPENROUTER_KEY, - replicateApiKey: REPLICATE_KEY, - model: "google/gemini-3-flash-preview", - }); - - const contextDocsData = await req.payload.find({ - collection: "context-files" as any, - limit: 100, - }); - const projectContext = contextDocsData.docs.map((doc: any) => doc.content); - - const optimizedMarkdown = await orchestrator.optimizeDocument({ - content: draftContent, - projectContext, - availableComponents: [], - instructions, - internalLinks: [], - customSources, - }); - - if (!optimizedMarkdown || typeof optimizedMarkdown !== "string") { - return Response.json({ error: "AI returned invalid markup." }, { status: 500 }) - } - - const blocks = parseMarkdownToLexical(optimizedMarkdown); - - return Response.json({ - success: true, - lexicalAST: { - root: { - type: "root", - format: "", - indent: 0, - version: 1, - children: blocks, - direction: "ltr", - }, - }, - }) - } catch (error: any) { - console.error("Failed to optimize post in endpoint:", error); - return Response.json({ - success: false, - error: error.message || "An unknown error occurred during optimization.", - }, { status: 500 }) + if (req.body) { + // req.json() acts as a method in Next.js/Payload req wrapper + body = (await req.json?.()) || {}; + } + } catch (e) { + // Ignore JSON parse error, body remains empty } -} + + const { draftContent, instructions } = body as { + draftContent?: string; + instructions?: string; + }; + + if (!draftContent) { + return Response.json( + { success: false, error: "Missing draftContent" }, + { status: 400 }, + ); + } + + const globalAiSettings = (await req.payload.findGlobal({ + slug: "ai-settings", + })) as any; + const customSources = + globalAiSettings?.customSources?.map((s: any) => s.sourceName) || []; + + const OPENROUTER_KEY = + process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY; + const REPLICATE_KEY = process.env.REPLICATE_API_KEY; + + if (!OPENROUTER_KEY) { + return Response.json( + { error: "OPENROUTER_KEY not found in environment." }, + { status: 500 }, + ); + } + + // Dynamically import to avoid bundling it into client components that might accidentally import this file + const importDynamic = new Function( + "modulePath", + "return import(modulePath)", + ); + const { AiBlogPostOrchestrator } = await importDynamic( + "@mintel/content-engine", + ); + + const orchestrator = new AiBlogPostOrchestrator({ + apiKey: OPENROUTER_KEY, + replicateApiKey: REPLICATE_KEY, + model: "google/gemini-3-flash-preview", + }); + + const contextDocsData = await req.payload.find({ + collection: "context-files" as any, + limit: 100, + }); + const projectContext = contextDocsData.docs.map((doc: any) => doc.content); + + const optimizedMarkdown = await orchestrator.optimizeDocument({ + content: draftContent, + projectContext, + availableComponents: [], + instructions, + internalLinks: [], + customSources, + }); + + if (!optimizedMarkdown || typeof optimizedMarkdown !== "string") { + return Response.json( + { error: "AI returned invalid markup." }, + { status: 500 }, + ); + } + + const blocks = parseMarkdownToLexical(optimizedMarkdown); + + return Response.json({ + success: true, + lexicalAST: { + root: { + type: "root", + format: "", + indent: 0, + version: 1, + children: blocks, + direction: "ltr", + }, + }, + }); + } catch (error: any) { + console.error("Failed to optimize post in endpoint:", error); + return Response.json( + { + success: false, + error: + error.message || "An unknown error occurred during optimization.", + }, + { status: 500 }, + ); + } +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2f88b5..50c09fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,7 +235,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) packages/content-engine: dependencies: @@ -477,6 +477,52 @@ importers: specifier: ^5.0.0 version: 5.9.3 + packages/kabelfachmann-mcp: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.5.0 + version: 1.27.1(zod@3.25.76) + '@qdrant/js-client-rest': + specifier: ^1.12.0 + version: 1.17.0(typescript@5.9.3) + '@xenova/transformers': + specifier: ^2.17.2 + version: 2.17.2 + dotenv: + specifier: ^17.3.1 + version: 17.3.1 + express: + specifier: ^5.2.1 + version: 5.2.1 + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 + onnxruntime-node: + specifier: ^1.14.0 + version: 1.14.0 + pdf-parse: + specifier: ^1.1.1 + version: 1.1.4 + zod: + specifier: ^3.23.8 + version: 3.25.76 + devDependencies: + '@types/express': + specifier: ^5.0.6 + version: 5.0.6 + '@types/node': + specifier: ^20.14.10 + version: 20.19.33 + '@types/pdf-parse': + specifier: ^1.1.4 + version: 1.1.5 + tsx: + specifier: ^4.19.1 + version: 4.21.0 + typescript: + specifier: ^5.5.3 + version: 5.9.3 + packages/klz-payload-mcp: dependencies: '@modelcontextprotocol/sdk': @@ -537,7 +583,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) packages/meme-generator: dependencies: @@ -596,7 +642,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.3 - version: 2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) + version: 2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) packages/next-config: dependencies: @@ -752,7 +798,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) + version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) packages/page-audit: dependencies: @@ -897,7 +943,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) packages/serpbear-mcp: dependencies: @@ -3457,6 +3503,9 @@ packages: '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/pdf-parse@1.1.5': + resolution: {integrity: sha512-kBfrSXsloMnUJOKi25s3+hRmkycHfLK6A09eRGqF/N8BkQoPUmaCr+q8Cli5FnfohEz/rsv82zAiPz/LXtOGhA==} + '@types/pg-pool@2.0.7': resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==} @@ -4590,6 +4639,10 @@ packages: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-uri-to-buffer@6.0.2: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} @@ -5178,6 +5231,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -5288,6 +5345,10 @@ packages: resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} engines: {node: '>= 12.20'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} @@ -6510,6 +6571,9 @@ packages: engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead + node-ensure@0.0.0: + resolution: {integrity: sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -6519,6 +6583,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -6780,6 +6848,10 @@ packages: peerDependencies: graphql: ^16.8.1 + pdf-parse@1.1.4: + resolution: {integrity: sha512-XRIRcLgk6ZnUbsHsYXExMw+krrPE81hJ6FQPLdBNhhBefqIQKXu/WeTgNBGSwPrfU0v+UCEwn7AoAUOsVKHFvQ==} + engines: {node: '>=6.8.1'} + peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} @@ -8320,6 +8392,10 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + web-streams-polyfill@4.0.0-beta.3: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} engines: {node: '>= 14'} @@ -11311,6 +11387,10 @@ snapshots: '@types/parse-json@4.0.2': {} + '@types/pdf-parse@1.1.5': + dependencies: + '@types/node': 20.19.33 + '@types/pg-pool@2.0.7': dependencies: '@types/pg': 8.15.6 @@ -12529,6 +12609,8 @@ snapshots: dargs@8.1.0: {} + data-uri-to-buffer@4.0.1: {} + data-uri-to-buffer@6.0.2: {} data-urls@5.0.0: @@ -13311,6 +13393,11 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fflate@0.8.2: {} figlet@1.10.0: @@ -13441,6 +13528,10 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 4.0.0-beta.3 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded-parse@2.1.2: {} forwarded@0.2.0: {} @@ -14688,10 +14779,18 @@ snapshots: node-domexception@1.0.0: {} + node-ensure@0.0.0: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-releases@2.0.27: {} normalize-path@3.0.0: {} @@ -15010,6 +15109,10 @@ snapshots: - typescript - utf-8-validate + pdf-parse@1.1.4: + dependencies: + node-ensure: 0.0.0 + peberminta@0.9.0: {} peek-readable@5.4.2: {} @@ -16772,7 +16875,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitest@2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): + vitest@2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): dependencies: '@vitest/expect': 2.1.9 '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.33)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)) @@ -16810,7 +16913,7 @@ snapshots: - supports-color - terser - vitest@2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): + vitest@2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): dependencies: '@vitest/expect': 2.1.9 '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)) @@ -16848,7 +16951,7 @@ snapshots: - supports-color - terser - vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -16890,7 +16993,7 @@ snapshots: - supports-color - terser - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -16993,6 +17096,8 @@ snapshots: dependencies: defaults: 1.0.4 + web-streams-polyfill@3.3.3: {} + web-streams-polyfill@4.0.0-beta.3: {} webidl-conversions@3.0.1: {}