diff --git a/bin/loushy.js b/bin/loushy.js new file mode 100644 index 000000000..487994ef3 --- /dev/null +++ b/bin/loushy.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node +'use strict'; + +const path = require('node:path'); + +async function main() { + const [command, ...rest] = process.argv.slice(2); + + if (command !== 'dev') { + console.error(`loushy: unknown command '${command || ''}'. Usage: loushy dev [--port N]`); + process.exitCode = 1; + return; + } + + const configPath = rest.find((arg) => !arg.startsWith('--')); + const portFlag = rest.find((arg) => arg.startsWith('--port')); + const port = portFlag ? Number(portFlag.split('=')[1] || rest[rest.indexOf(portFlag) + 1]) : 3737; + const hostFlag = rest.find((arg) => arg.startsWith('--host')); + const host = hostFlag ? (hostFlag.split('=')[1] || rest[rest.indexOf(hostFlag) + 1]) : '127.0.0.1'; + + if (!configPath) { + console.error('loushy dev: a config file path is required. Usage: loushy dev [--port N] [--host H]'); + process.exitCode = 1; + return; + } + + const { startDevServer } = require(path.join(__dirname, '..', 'dist', 'cli', 'dev.js')); + + try { + const handle = await startDevServer(path.resolve(configPath), port, host); + console.log(`loushy dev: listening on http://${host}:${handle.port}`); + } catch (error) { + console.error(error && error.message ? error.message : String(error)); + process.exitCode = 1; + } +} + +main(); diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..e90c73924 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,34 @@ +# Examples + +Runnable examples of `@loushy/build-ai-agent` in use. Each directory below +is runnable directly (see its own README for exact instructions); most use +`tsx` and a free mock provider by default, so nothing here needs an API key +to try. + +## [doc-qa](./doc-qa) + +A question-answering agent scoped to a single fixed document (LOU-H10). + +## [research-assistant](./research-assistant) + +A research agent with the built-in `http` tool wired up (LOU-H10). + +## [slack-notifier](./slack-notifier) + +Turns a raw event description into a short, Slack-ready notification (LOU-H10). + +## [support-bot](./support-bot) + +A minimal, empathetic customer-support agent (LOU-H10). + +## [tracing](./tracing) + +Runnable examples of `AgentExecutor.execute()` wired to console and OpenTelemetry trace exporters (LOU-E6). + +## [workflow-router](./workflow-router) + +An agent that classifies an incoming request into a fixed set of categories (LOU-H10). + +--- + +Not a directory, but also worth knowing about: [OpenRouterProvider.examples.ts](./OpenRouterProvider.examples.ts) - runnable snippets showing `OpenRouterProvider` usage (LOU-B6). diff --git a/examples/README.test.ts b/examples/README.test.ts new file mode 100644 index 000000000..9be9dd259 --- /dev/null +++ b/examples/README.test.ts @@ -0,0 +1,27 @@ +/** + * LOU-H11: verifies examples/README.md's `## [dirname](./dirname)` + * headings exactly match the actual directory listing under examples/ - + * no stale, no missing. + */ +import { describe, it, expect } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; + +const EXAMPLES_DIR = __dirname; + +describe('examples/README.md gallery index', () => { + it('has exactly one heading per example directory, and no stale entries', () => { + const dirs = fs + .readdirSync(EXAMPLES_DIR, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + + const readme = fs.readFileSync(path.join(EXAMPLES_DIR, 'README.md'), 'utf8'); + const headings = [...readme.matchAll(/^## \[([^\]]+)\]\(\.\/([^)]+)\)/gm)] + .map((m) => m[2]) + .sort(); + + expect(headings).toEqual(dirs); + }); +}); diff --git a/examples/doc-qa/README.md b/examples/doc-qa/README.md new file mode 100644 index 000000000..5165a41ca --- /dev/null +++ b/examples/doc-qa/README.md @@ -0,0 +1,11 @@ +# doc-qa + +A question-answering agent scoped to a single fixed document (a short +refund policy, inlined in the prompt), built with `createAgent()`. + +Runs against a free local mock provider by default - no API key needed. +Set `OLLAMA_BASE_URL` to run it against a local Ollama instead. + +```bash +npx tsx examples/doc-qa/index.ts +``` diff --git a/examples/doc-qa/index.ts b/examples/doc-qa/index.ts new file mode 100644 index 000000000..d15c8b39f --- /dev/null +++ b/examples/doc-qa/index.ts @@ -0,0 +1,43 @@ +/** + * doc-qa - a question-answering agent scoped to a fixed document, built + * with createAgent() (LOU-H1). + * + * Defaults to a free, local mock provider so it runs with zero setup and + * zero API cost. Set OLLAMA_BASE_URL to run it against a local Ollama + * instead. + * + * Run with: tsx examples/doc-qa/index.ts + */ +import { createAgent } from '../../src/createAgent'; +import { createMockProvider } from '../../src/providers/mock'; +import { resolveProvider } from '../../src/providers/resolveProvider'; + +const DOCUMENT = ` +Loushy Refund Policy: Refunds are available within 30 days of purchase. +Digital goods are non-refundable once downloaded. Contact support with +your order number to request a refund. +`; + +const provider = process.env.OLLAMA_BASE_URL + ? resolveProvider('ollama/llama3') + : createMockProvider({ + responses: ['According to the policy, refunds are available within 30 days of purchase.'], + }); + +const agent = createAgent({ + name: 'doc-qa', + prompt: + `You answer questions using ONLY the following document. If the answer isn't in the ` + + `document, say so.\n\nDocument:\n${DOCUMENT}`, + provider, +}); + +async function main() { + const result = await agent.send('How long do I have to request a refund?'); + console.log(result.text); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/examples/doc-qa/package.json b/examples/doc-qa/package.json new file mode 100644 index 000000000..5fffed946 --- /dev/null +++ b/examples/doc-qa/package.json @@ -0,0 +1,9 @@ +{ + "name": "doc-qa-example", + "private": true, + "version": "0.0.0", + "description": "Document-scoped question-answering agent built with @loushy/build-ai-agent's createAgent()", + "scripts": { + "start": "tsx index.ts" + } +} diff --git a/examples/research-assistant/README.md b/examples/research-assistant/README.md new file mode 100644 index 000000000..88cb35961 --- /dev/null +++ b/examples/research-assistant/README.md @@ -0,0 +1,11 @@ +# research-assistant + +An agent with the built-in `http` tool wired up so it can fetch sources +while researching a question, built with `createAgent()`. + +Runs against a free local mock provider by default - no API key needed. +Set `ANTHROPIC_API_KEY` to run it against real Anthropic instead. + +```bash +npx tsx examples/research-assistant/index.ts +``` diff --git a/examples/research-assistant/index.ts b/examples/research-assistant/index.ts new file mode 100644 index 000000000..d8339c6ab --- /dev/null +++ b/examples/research-assistant/index.ts @@ -0,0 +1,40 @@ +/** + * research-assistant - an agent with the built-in `http` tool wired up, so + * it can fetch pages/APIs while researching a question (LOU-H1's + * createAgent()). + * + * Defaults to a free, local mock provider so it runs with zero setup and + * zero API cost. Set ANTHROPIC_API_KEY to run it against real Anthropic + * instead. + * + * Run with: tsx examples/research-assistant/index.ts + */ +import { createAgent } from '../../src/createAgent'; +import { createMockProvider } from '../../src/providers/mock'; +import { resolveProvider } from '../../src/providers/resolveProvider'; +import { httpTool } from '../../src/tools/built-in/http'; + +const provider = process.env.ANTHROPIC_API_KEY + ? resolveProvider('anthropic/claude-3-5-sonnet-latest') + : createMockProvider({ + responses: ['Based on my research, here is a concise summary of the topic.'], + }); + +const agent = createAgent({ + name: 'research-assistant', + prompt: + 'You are a diligent research assistant. Use the http tool to fetch sources when needed, ' + + 'and always summarize findings concisely with citations.', + provider, + tools: { http: httpTool }, +}); + +async function main() { + const result = await agent.send('Summarize the latest news about renewable energy.'); + console.log(result.text); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/examples/research-assistant/package.json b/examples/research-assistant/package.json new file mode 100644 index 000000000..392656972 --- /dev/null +++ b/examples/research-assistant/package.json @@ -0,0 +1,9 @@ +{ + "name": "research-assistant-example", + "private": true, + "version": "0.0.0", + "description": "Research agent with the http tool wired up, built with @loushy/build-ai-agent's createAgent()", + "scripts": { + "start": "tsx index.ts" + } +} diff --git a/examples/slack-notifier/README.md b/examples/slack-notifier/README.md new file mode 100644 index 000000000..268f1b10b --- /dev/null +++ b/examples/slack-notifier/README.md @@ -0,0 +1,11 @@ +# slack-notifier + +Turns a raw event description into a short, Slack-ready notification +message, built with `createAgent()`. + +Runs against a free local mock provider by default - no API key needed. +Set `OPENAI_API_KEY` to run it against real OpenAI instead. + +```bash +npx tsx examples/slack-notifier/index.ts +``` diff --git a/examples/slack-notifier/index.ts b/examples/slack-notifier/index.ts new file mode 100644 index 000000000..e0003615f --- /dev/null +++ b/examples/slack-notifier/index.ts @@ -0,0 +1,36 @@ +/** + * slack-notifier - an agent that drafts a Slack-ready notification message + * from a raw event description, built with createAgent() (LOU-H1). + * + * Defaults to a free, local mock provider so it runs with zero setup and + * zero API cost. Set OPENAI_API_KEY to run it against real OpenAI instead. + * + * Run with: tsx examples/slack-notifier/index.ts + */ +import { createAgent } from '../../src/createAgent'; +import { createMockProvider } from '../../src/providers/mock'; +import { resolveProvider } from '../../src/providers/resolveProvider'; + +const provider = process.env.OPENAI_API_KEY + ? resolveProvider('openai/gpt-4o-mini') + : createMockProvider({ + responses: [':rotating_light: Deploy to production failed - build #482. cc @on-call'], + }); + +const agent = createAgent({ + name: 'slack-notifier', + prompt: + 'You turn a raw event description into a short, Slack-ready notification message, using ' + + 'Slack emoji shorthand (e.g. :rotating_light:) where it fits.', + provider, +}); + +async function main() { + const result = await agent.send('Deploy to production failed on build #482.'); + console.log(result.text); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/examples/slack-notifier/package.json b/examples/slack-notifier/package.json new file mode 100644 index 000000000..de8c9aab5 --- /dev/null +++ b/examples/slack-notifier/package.json @@ -0,0 +1,9 @@ +{ + "name": "slack-notifier-example", + "private": true, + "version": "0.0.0", + "description": "Drafts a Slack-ready notification from a raw event, built with @loushy/build-ai-agent's createAgent()", + "scripts": { + "start": "tsx index.ts" + } +} diff --git a/examples/support-bot/README.md b/examples/support-bot/README.md new file mode 100644 index 000000000..93271613b --- /dev/null +++ b/examples/support-bot/README.md @@ -0,0 +1,12 @@ +# support-bot + +A minimal customer-support agent built with `createAgent()`. Empathetic, +concise, and always asks for an order number when a customer reports a +problem. + +Runs against a free local mock provider by default - no API key needed. +Set `OPENAI_API_KEY` to run it against real OpenAI instead. + +```bash +npx tsx examples/support-bot/index.ts +``` diff --git a/examples/support-bot/index.ts b/examples/support-bot/index.ts new file mode 100644 index 000000000..65071c832 --- /dev/null +++ b/examples/support-bot/index.ts @@ -0,0 +1,38 @@ +/** + * support-bot - a minimal customer-support agent built with createAgent() + * (LOU-H1). + * + * Defaults to a free, local mock provider so it runs with zero setup and + * zero API cost. Set OPENAI_API_KEY to run it against real OpenAI instead. + * + * Run with: tsx examples/support-bot/index.ts + */ +import { createAgent } from '../../src/createAgent'; +import { createMockProvider } from '../../src/providers/mock'; +import { resolveProvider } from '../../src/providers/resolveProvider'; + +const provider = process.env.OPENAI_API_KEY + ? resolveProvider('openai/gpt-4o-mini') + : createMockProvider({ + responses: [ + "I'm sorry to hear that! Could you tell me your order number so I can look into it?", + ], + }); + +const agent = createAgent({ + name: 'support-bot', + prompt: + 'You are a friendly customer support agent. Be concise, empathetic, and always ask for ' + + 'an order number when a customer reports a problem with an order.', + provider, +}); + +async function main() { + const result = await agent.send('Hi, my order arrived damaged.'); + console.log(result.text); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/examples/support-bot/package.json b/examples/support-bot/package.json new file mode 100644 index 000000000..a45830d11 --- /dev/null +++ b/examples/support-bot/package.json @@ -0,0 +1,9 @@ +{ + "name": "support-bot-example", + "private": true, + "version": "0.0.0", + "description": "Minimal customer-support agent built with @loushy/build-ai-agent's createAgent()", + "scripts": { + "start": "tsx index.ts" + } +} diff --git a/examples/workflow-router/README.md b/examples/workflow-router/README.md new file mode 100644 index 000000000..dcff536e6 --- /dev/null +++ b/examples/workflow-router/README.md @@ -0,0 +1,11 @@ +# workflow-router + +An agent that classifies an incoming request into one of a fixed set of +categories (`billing`/`technical`/`sales`), built with `createAgent()`. + +Runs against a free local mock provider by default - no API key needed. +Set `OPENAI_API_KEY` to run it against real OpenAI instead. + +```bash +npx tsx examples/workflow-router/index.ts +``` diff --git a/examples/workflow-router/index.ts b/examples/workflow-router/index.ts new file mode 100644 index 000000000..94d226020 --- /dev/null +++ b/examples/workflow-router/index.ts @@ -0,0 +1,34 @@ +/** + * workflow-router - an agent whose job is to classify an incoming request + * into one of a fixed set of routes/categories (LOU-H1's createAgent()). + * + * Defaults to a free, local mock provider so it runs with zero setup and + * zero API cost. Set OPENAI_API_KEY to run it against real OpenAI instead. + * + * Run with: tsx examples/workflow-router/index.ts + */ +import { createAgent } from '../../src/createAgent'; +import { createMockProvider } from '../../src/providers/mock'; +import { resolveProvider } from '../../src/providers/resolveProvider'; + +const provider = process.env.OPENAI_API_KEY + ? resolveProvider('openai/gpt-4o-mini') + : createMockProvider({ responses: ['billing'] }); + +const agent = createAgent({ + name: 'workflow-router', + prompt: + 'You are a request router. Given a customer message, reply with exactly one word: ' + + "'billing', 'technical', or 'sales' - whichever category best matches the message.", + provider, +}); + +async function main() { + const result = await agent.send('I was charged twice for my subscription this month.'); + console.log(`Routed to: ${result.text}`); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/examples/workflow-router/package.json b/examples/workflow-router/package.json new file mode 100644 index 000000000..5eb7b517a --- /dev/null +++ b/examples/workflow-router/package.json @@ -0,0 +1,9 @@ +{ + "name": "workflow-router-example", + "private": true, + "version": "0.0.0", + "description": "Request classification/routing agent built with @loushy/build-ai-agent's createAgent()", + "scripts": { + "start": "tsx index.ts" + } +} diff --git a/package-lock.json b/package-lock.json index 03d61291a..bb9579a5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,11 @@ "@modelcontextprotocol/sdk": "^1.30.1", "dockerode": "^5.0.1", "nanoid": "^5.0.7", - "undici": "^8.11.2" + "undici": "^8.11.2", + "yaml": "^2.9.1" + }, + "bin": { + "loushy": "bin/loushy.js" }, "devDependencies": { "@ai-sdk/anthropic": "^0.0.42", @@ -7759,6 +7763,21 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.3", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", diff --git a/package.json b/package.json index 876cae16c..9dc5f095d 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", + "bin": { + "loushy": "bin/loushy.js" + }, "exports": { ".": { "types": "./dist/index.d.ts", @@ -39,12 +42,16 @@ }, "files": [ "dist", + "bin", "README.md", "LICENSE", "CHANGELOG.md", "CONTRIBUTING.md" ], - "sideEffects": false, + "sideEffects": [ + "./src/providers/mock.ts", + "./src/providers/mock.js" + ], "scripts": { "build": "tsup", "dev": "tsup --watch", @@ -55,6 +62,11 @@ "typecheck": "tsc --noEmit", "example:tracing:console": "tsx examples/tracing/run-console.ts", "example:tracing:otel": "tsx examples/tracing/run-otel.ts", + "example:support-bot": "tsx examples/support-bot/index.ts", + "example:research-assistant": "tsx examples/research-assistant/index.ts", + "example:workflow-router": "tsx examples/workflow-router/index.ts", + "example:doc-qa": "tsx examples/doc-qa/index.ts", + "example:slack-notifier": "tsx examples/slack-notifier/index.ts", "prepublishOnly": "npm run build" }, "keywords": [ @@ -104,7 +116,8 @@ "@modelcontextprotocol/sdk": "^1.30.1", "dockerode": "^5.0.1", "nanoid": "^5.0.7", - "undici": "^8.11.2" + "undici": "^8.11.2", + "yaml": "^2.9.1" }, "devDependencies": { "@ai-sdk/anthropic": "^0.0.42", diff --git a/packages/create-loushy-agent/.gitignore b/packages/create-loushy-agent/.gitignore new file mode 100644 index 000000000..f06235c46 --- /dev/null +++ b/packages/create-loushy-agent/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/packages/create-loushy-agent/bin/cli.js b/packages/create-loushy-agent/bin/cli.js new file mode 100644 index 000000000..c2890ee56 --- /dev/null +++ b/packages/create-loushy-agent/bin/cli.js @@ -0,0 +1,14 @@ +#!/usr/bin/env node +'use strict'; + +const { main } = require('../dist/index.js'); + +main(process.argv.slice(2)).then( + (code) => { + process.exitCode = typeof code === 'number' ? code : 0; + }, + (err) => { + console.error(err && err.stack ? err.stack : String(err)); + process.exitCode = 1; + } +); diff --git a/packages/create-loushy-agent/package-lock.json b/packages/create-loushy-agent/package-lock.json new file mode 100644 index 000000000..d8b233dea --- /dev/null +++ b/packages/create-loushy-agent/package-lock.json @@ -0,0 +1,2872 @@ +{ + "name": "create-loushy-agent", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "create-loushy-agent", + "version": "0.1.0", + "dependencies": { + "prompts": "^2.4.2" + }, + "bin": { + "create-loushy-agent": "bin/cli.js" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/prompts": "^2.4.9", + "tsup": "^8.0.0", + "typescript": "^5.0.0", + "vitest": "^1.6.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.5.tgz", + "integrity": "sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.5.tgz", + "integrity": "sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.5.tgz", + "integrity": "sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.5.tgz", + "integrity": "sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.5.tgz", + "integrity": "sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.5.tgz", + "integrity": "sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.5.tgz", + "integrity": "sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.5.tgz", + "integrity": "sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.5.tgz", + "integrity": "sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.5.tgz", + "integrity": "sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.5.tgz", + "integrity": "sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.5.tgz", + "integrity": "sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.5.tgz", + "integrity": "sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.5.tgz", + "integrity": "sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.5.tgz", + "integrity": "sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.5.tgz", + "integrity": "sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.5.tgz", + "integrity": "sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.5.tgz", + "integrity": "sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.5.tgz", + "integrity": "sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.5.tgz", + "integrity": "sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.5.tgz", + "integrity": "sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.5.tgz", + "integrity": "sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.5.tgz", + "integrity": "sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.5.tgz", + "integrity": "sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.5.tgz", + "integrity": "sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prompts": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz", + "integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "kleur": "^3.0.3" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz", + "integrity": "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.5", + "@rollup/rollup-android-arm64": "4.63.5", + "@rollup/rollup-darwin-arm64": "4.63.5", + "@rollup/rollup-darwin-x64": "4.63.5", + "@rollup/rollup-freebsd-arm64": "4.63.5", + "@rollup/rollup-freebsd-x64": "4.63.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.5", + "@rollup/rollup-linux-arm-musleabihf": "4.63.5", + "@rollup/rollup-linux-arm64-gnu": "4.63.5", + "@rollup/rollup-linux-arm64-musl": "4.63.5", + "@rollup/rollup-linux-loong64-gnu": "4.63.5", + "@rollup/rollup-linux-loong64-musl": "4.63.5", + "@rollup/rollup-linux-ppc64-gnu": "4.63.5", + "@rollup/rollup-linux-ppc64-musl": "4.63.5", + "@rollup/rollup-linux-riscv64-gnu": "4.63.5", + "@rollup/rollup-linux-riscv64-musl": "4.63.5", + "@rollup/rollup-linux-s390x-gnu": "4.63.5", + "@rollup/rollup-linux-x64-gnu": "4.63.5", + "@rollup/rollup-linux-x64-musl": "4.63.5", + "@rollup/rollup-openbsd-x64": "4.63.5", + "@rollup/rollup-openharmony-arm64": "4.63.5", + "@rollup/rollup-win32-arm64-msvc": "4.63.5", + "@rollup/rollup-win32-ia32-msvc": "4.63.5", + "@rollup/rollup-win32-x64-gnu": "4.63.5", + "@rollup/rollup-win32-x64-msvc": "4.63.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "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", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/packages/create-loushy-agent/package.json b/packages/create-loushy-agent/package.json new file mode 100644 index 000000000..e2302e1a9 --- /dev/null +++ b/packages/create-loushy-agent/package.json @@ -0,0 +1,29 @@ +{ + "name": "create-loushy-agent", + "version": "0.1.0", + "description": "Scaffold a new @loushy/build-ai-agent project", + "type": "commonjs", + "bin": { + "create-loushy-agent": "bin/cli.js" + }, + "files": [ + "dist", + "bin", + "templates" + ], + "scripts": { + "build": "tsup", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "prompts": "^2.4.2" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/prompts": "^2.4.9", + "tsup": "^8.0.0", + "typescript": "^5.0.0", + "vitest": "^1.6.1" + } +} diff --git a/packages/create-loushy-agent/src/cli.integration.test.ts b/packages/create-loushy-agent/src/cli.integration.test.ts new file mode 100644 index 000000000..627f70789 --- /dev/null +++ b/packages/create-loushy-agent/src/cli.integration.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import path from 'node:path'; +import fs from 'node:fs'; +import os from 'node:os'; + +const CLI = path.join(__dirname, '..', 'bin', 'cli.js'); + +const tempDirs: string[] = []; +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +describe('cli (flag-based non-interactive shortcut)', () => { + it('scaffolds using --name/--provider without touching the interactive prompts', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'create-loushy-agent-cli-')); + tempDirs.push(dir); + + const output = execFileSync( + process.execPath, + [ + CLI, + '--name=test-agent', + '--provider=anthropic', + '--tools=http', + '--yes', + `--dir=${dir}`, + ], + { encoding: 'utf8' } + ); + + expect(output).toContain('Scaffolding test-agent (anthropic)'); + expect(fs.existsSync(path.join(dir, 'package.json'))).toBe(true); + expect(fs.existsSync(path.join(dir, 'src', 'agent.ts'))).toBe(true); + }); +}); diff --git a/packages/create-loushy-agent/src/index.ts b/packages/create-loushy-agent/src/index.ts new file mode 100644 index 000000000..b7c16d068 --- /dev/null +++ b/packages/create-loushy-agent/src/index.ts @@ -0,0 +1,130 @@ +import { parseArgs } from 'node:util'; +import path from 'node:path'; +import { collectAnswers, validateAnswers, AnswerConfig } from './prompts'; +import { generateProject } from './template'; + +const USAGE = `create-loushy-agent - scaffold a new @loushy/build-ai-agent project + +Usage: + create-loushy-agent [options] + +Options: + --name Project name (skips the interactive prompt) + --provider LLM provider: openai | anthropic | ollama + --tools Comma-separated tool list (e.g. "http,github") + --yes, -y Accept flag values / defaults without prompting + --help, -h Show this help message +`; + +export interface ParsedCliArgs { + help: boolean; + yes: boolean; + name?: string; + provider?: string; + tools?: string; + dir?: string; +} + +/** + * Parses argv using Node's built-in util.parseArgs (no new dependency). + * Throws a descriptive error for an unrecognized flag rather than letting + * parseArgs's own generic error surface. + */ +export function parseCliArgs(argv: string[]): ParsedCliArgs { + let values: Record; + try { + ({ values } = parseArgs({ + args: argv, + options: { + help: { type: 'boolean', short: 'h', default: false }, + yes: { type: 'boolean', short: 'y', default: false }, + name: { type: 'string' }, + provider: { type: 'string' }, + tools: { type: 'string' }, + dir: { type: 'string' }, + }, + allowPositionals: true, + strict: true, + })); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + // node:util's parseArgs error message for an unknown flag looks like + // "Unknown option '--xyz'" - normalize to the exact phrasing this + // ticket asks for. + const match = message.match(/Unknown option '(--?[^']+)'/); + if (match) { + throw new CliArgError(`Unknown flag: ${match[1]}`); + } + throw new CliArgError(message); + } + + return { + help: !!values.help, + yes: !!values.yes, + name: values.name as string | undefined, + provider: values.provider as string | undefined, + tools: values.tools as string | undefined, + dir: values.dir as string | undefined, + }; +} + +export class CliArgError extends Error {} + +/** + * CLI entry point. Returns a process exit code. + */ +export async function main(argv: string[]): Promise { + let args: ParsedCliArgs; + try { + args = parseCliArgs(argv); + } catch (error) { + if (error instanceof CliArgError) { + process.stderr.write(error.message + '\n'); + return 1; + } + throw error; + } + + if (args.help) { + process.stdout.write(USAGE); + return 0; + } + + return runScaffold(args); +} + +/** + * Resolves the AnswerConfig either from --yes/flag shortcuts (non-interactive, + * testable via a piped/scripted process) or by running the interactive + * prompts.ts flow. A flag-only invocation ("--yes" or both "--name" and + * "--provider" given) never touches the interactive prompts, which is what + * makes this reliably testable end-to-end without a real TTY. + */ +async function resolveAnswers(args: ParsedCliArgs): Promise { + const useShortcut = args.yes || (!!args.name && !!args.provider); + + if (!useShortcut) { + return collectAnswers(); + } + + return validateAnswers({ + name: args.name || 'my-loushy-agent', + provider: args.provider || 'openai', + tools: args.tools ? args.tools.split(',').map((t) => t.trim()).filter(Boolean) : [], + }); +} + +/** + * Scaffold step: resolve answers, then generate the project on disk. + * --dir overrides the target directory (defaults to ./); used by + * tests to target a temp directory instead of the CLI's own cwd. + */ +async function runScaffold(args: ParsedCliArgs): Promise { + const answers = await resolveAnswers(args); + const targetDir = args.dir || path.join(process.cwd(), answers.name); + + process.stdout.write(`Scaffolding ${answers.name} (${answers.provider}) into ${targetDir}...\n`); + await generateProject(targetDir, answers); + process.stdout.write('Done.\n'); + return 0; +} diff --git a/packages/create-loushy-agent/src/prompts.test.ts b/packages/create-loushy-agent/src/prompts.test.ts new file mode 100644 index 000000000..dadc5260d --- /dev/null +++ b/packages/create-loushy-agent/src/prompts.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import prompts from 'prompts'; +import { collectAnswers, validateAnswers } from './prompts'; + +describe('collectAnswers', () => { + it('collects a name/provider/tools answer set via injected (non-interactive) answers', async () => { + prompts.inject(['my-agent', 'anthropic', ['http', 'github']]); + + const answers = await collectAnswers(); + + expect(answers).toEqual({ + name: 'my-agent', + provider: 'anthropic', + tools: ['http', 'github'], + }); + }); +}); + +describe('validateAnswers', () => { + it('throws naming the invalid provider and the allowed set', () => { + expect(() => + validateAnswers({ name: 'x', provider: 'not-a-provider', tools: [] }) + ).toThrow(/invalid provider 'not-a-provider'.*openai, anthropic, ollama/); + }); + + it('accepts a valid provider', () => { + const result = validateAnswers({ name: 'x', provider: 'openai', tools: [] }); + expect(result.provider).toBe('openai'); + }); +}); diff --git a/packages/create-loushy-agent/src/prompts.ts b/packages/create-loushy-agent/src/prompts.ts new file mode 100644 index 000000000..e95ab5423 --- /dev/null +++ b/packages/create-loushy-agent/src/prompts.ts @@ -0,0 +1,76 @@ +import prompts from 'prompts'; + +export const PROVIDERS = ['openai', 'anthropic', 'ollama'] as const; +export type Provider = (typeof PROVIDERS)[number]; + +export const TOOL_CHOICES = ['http', 'github'] as const; + +export interface AnswerConfig { + name: string; + provider: Provider; + tools: string[]; +} + +/** + * Interactively collects the answers needed to scaffold a project: project + * name (text), provider (select), tools (multiselect). Uses the `prompts` + * library, which supports non-interactive testing via `prompts.inject()` - + * tests call that before invoking collectAnswers() instead of driving a + * real TTY. + */ +export async function collectAnswers(): Promise { + const response = await prompts([ + { + type: 'text', + name: 'name', + message: 'Project name', + initial: 'my-loushy-agent', + }, + { + type: 'select', + name: 'provider', + message: 'LLM provider', + choices: PROVIDERS.map((p) => ({ title: p, value: p })), + initial: 0, + }, + { + type: 'multiselect', + name: 'tools', + message: 'Starter tools', + choices: TOOL_CHOICES.map((t) => ({ title: t, value: t })), + }, + ]); + + return validateAnswers({ + name: response.name, + provider: response.provider, + tools: response.tools || [], + }); +} + +/** + * Validates an AnswerConfig, throwing a clear error naming the invalid + * value and the allowed set. Used both after interactive collection and + * whenever a non-interactive flag override (--provider) supplies a + * provider value directly, bypassing the select prompt's own restricted + * choice list. + */ +export function validateAnswers(answers: { + name: string; + provider: string; + tools: string[]; +}): AnswerConfig { + if (!answers.name || !answers.name.trim()) { + throw new Error('collectAnswers: a project name is required'); + } + if (!(PROVIDERS as readonly string[]).includes(answers.provider)) { + throw new Error( + `collectAnswers: invalid provider '${answers.provider}'. Allowed values: ${PROVIDERS.join(', ')}` + ); + } + return { + name: answers.name.trim(), + provider: answers.provider as Provider, + tools: answers.tools, + }; +} diff --git a/packages/create-loushy-agent/src/template.e2e.test.ts b/packages/create-loushy-agent/src/template.e2e.test.ts new file mode 100644 index 000000000..9111ab82c --- /dev/null +++ b/packages/create-loushy-agent/src/template.e2e.test.ts @@ -0,0 +1,59 @@ +/** + * LOU-H4 real, end-to-end integration test: runs the CLI in a FRESH TEMP + * DIRECTORY (os.tmpdir()+mkdtemp, never inside this worktree), then + * actually runs `npm install && npm run build` inside the generated + * project and asserts both succeed. This is slow (npm install downloads + * real packages) so it's kept in its own file / a generous timeout. + */ +import { describe, it, expect, afterAll } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +const CLI = path.join(__dirname, '..', 'bin', 'cli.js'); + +describe('end-to-end scaffold + install + build', () => { + let dir: string | undefined; + + afterAll(() => { + if (dir) fs.rmSync(dir, { recursive: true, force: true }); + }); + + it( + 'generates an installable, buildable project pinned to the SDK\'s exact own version', + () => { + const base = fs.mkdtempSync(path.join(os.tmpdir(), 'create-loushy-agent-e2e-')); + dir = path.join(base, 'test-agent'); + + execFileSync( + process.execPath, + [CLI, '--name=test-agent', '--provider=openai', '--yes', `--dir=${dir}`], + { encoding: 'utf8' } + ); + + expect(fs.existsSync(path.join(dir, 'package.json'))).toBe(true); + + execFileSync('npm', ['install'], { cwd: dir, encoding: 'utf8', shell: true }); + const buildOutput = execFileSync('npm', ['run', 'build'], { + cwd: dir, + encoding: 'utf8', + shell: true, + }); + expect(buildOutput).toBeDefined(); + + const sdkRootPkg = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', '..', '..', 'package.json'), 'utf8') + ); + const installedPkg = JSON.parse( + fs.readFileSync( + path.join(dir, 'node_modules', '@loushy', 'build-ai-agent', 'package.json'), + 'utf8' + ) + ); + + expect(installedPkg.version).toBe(sdkRootPkg.version); + }, + 180_000 + ); +}); diff --git a/packages/create-loushy-agent/src/template.env.test.ts b/packages/create-loushy-agent/src/template.env.test.ts new file mode 100644 index 000000000..8f191b0e4 --- /dev/null +++ b/packages/create-loushy-agent/src/template.env.test.ts @@ -0,0 +1,45 @@ +/** + * LOU-H5: provider/env-var wiring test. Scaffolds two projects with + * different --provider flags into separate temp dirs and greps each + * generated .env.example for the expected exact var name (matching + * LOU-F8's resolveProvider() env table: OPENAI_API_KEY, ANTHROPIC_API_KEY, + * OLLAMA_BASE_URL). + */ +import { describe, it, expect, afterEach } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { PROVIDER_ENV_VARS } from './template'; + +const CLI = path.join(__dirname, '..', 'bin', 'cli.js'); + +const tempDirs: string[] = []; +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +describe('provider env-var wiring', () => { + it.each([ + ['openai', 'OPENAI_API_KEY'], + ['anthropic', 'ANTHROPIC_API_KEY'], + ['ollama', 'OLLAMA_BASE_URL'], + ])('writes %s -> %s into .env.example', (provider, envVar) => { + expect(PROVIDER_ENV_VARS[provider]).toBe(envVar); + + const dir = fs.mkdtempSync(path.join(os.tmpdir(), `create-loushy-agent-env-${provider}-`)); + tempDirs.push(dir); + const projectDir = path.join(dir, 'proj'); + + execFileSync( + process.execPath, + [CLI, `--name=proj`, `--provider=${provider}`, '--yes', `--dir=${projectDir}`], + { encoding: 'utf8' } + ); + + const envExample = fs.readFileSync(path.join(projectDir, '.env.example'), 'utf8'); + expect(envExample).toMatch(new RegExp(`^${envVar}=`, 'm')); + }); +}); diff --git a/packages/create-loushy-agent/src/template.ts b/packages/create-loushy-agent/src/template.ts new file mode 100644 index 000000000..2e4ffe7da --- /dev/null +++ b/packages/create-loushy-agent/src/template.ts @@ -0,0 +1,171 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { AnswerConfig } from './prompts'; + +/** + * Locates the @loushy/build-ai-agent SDK's own package.json. + * + * This CLI is developed inside the same monorepo/worktree as the SDK + * (packages/create-loushy-agent, with the SDK at the worktree root) and + * the SDK is NOT published to the public npm registry, so a plain semver + * dependency spec in a generated project's package.json would not be + * installable. generateProject() instead packs the SDK's own source tree + * with `npm pack` and copies the resulting tarball into the generated + * project, wiring a `file:./` dependency that installs correctly + * from any directory, including one far outside this worktree. The + * tarball's version always matches the SDK's real package.json version + * exactly, which is what LOU-H4's "pinned version" requirement is about. + */ +function findSdkRoot(): string { + // packages/create-loushy-agent/src -> packages/create-loushy-agent -> packages -> + const candidate = path.resolve(__dirname, '..', '..', '..'); + const pkgPath = path.join(candidate, 'package.json'); + if (!fs.existsSync(pkgPath)) { + throw new Error( + `generateProject: could not locate the @loushy/build-ai-agent SDK root from ${__dirname} (expected package.json at ${pkgPath})` + ); + } + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + if (pkg.name !== '@loushy/build-ai-agent') { + throw new Error( + `generateProject: expected @loushy/build-ai-agent at ${pkgPath}, found '${pkg.name}'` + ); + } + return candidate; +} + +/** Reads the SDK's own pinned version straight from its package.json. */ +export function getPinnedSdkVersion(): string { + const sdkRoot = findSdkRoot(); + const pkg = JSON.parse(fs.readFileSync(path.join(sdkRoot, 'package.json'), 'utf8')); + return pkg.version as string; +} + +/** Env var each provider's generated agent reads its credential from (LOU-H5, matching LOU-F8's resolveProvider table). */ +export const PROVIDER_ENV_VARS: Record = { + openai: 'OPENAI_API_KEY', + anthropic: 'ANTHROPIC_API_KEY', + ollama: 'OLLAMA_BASE_URL', +}; + +function toolImportLine(tool: string): string { + if (tool === 'http') return "import { httpTool } from '@loushy/build-ai-agent/tools';"; + // 'github' (and any other future starter tool) needs real credentials + // (token/owner/repo) that generateProject() has no value for, so it is + // left commented out with a TODO rather than wired to a broken config. + return `// TODO: wire up the '${tool}' tool (needs credentials) - see @loushy/build-ai-agent/tools`; +} + +function toolsObjectSource(tools: string[]): string { + const entries: string[] = []; + if (tools.includes('http')) { + entries.push(' http: httpTool,'); + } + return entries.join('\n'); +} + +/** + * Generates a new @loushy/build-ai-agent project at `dir`: package.json + * (with a real, installable, exact-version-pinned SDK dependency), + * tsconfig.json (mirroring the root project's baseline), src/agent.ts + * (calling createAgent() with the chosen provider/tools), and a + * .env.example naming the right provider env var (LOU-H5). + */ +export async function generateProject(dir: string, answers: AnswerConfig): Promise { + fs.mkdirSync(dir, { recursive: true }); + fs.mkdirSync(path.join(dir, 'src'), { recursive: true }); + + const sdkRoot = findSdkRoot(); + const version = getPinnedSdkVersion(); + + // Pack the SDK's current source tree and copy the tarball alongside the + // generated project so `npm install` works standalone, from any cwd. + // shell:true is required for execFileSync to run the `npm`/`npm.cmd` + // batch script on Windows; every argument here is either a fixed literal + // or a path generateProject() itself computed (never raw user input), + // so this isn't a shell-injection risk in practice. + const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + const packOutput = execFileSync(npmCmd, ['pack', '--pack-destination', dir], { + cwd: sdkRoot, + encoding: 'utf8', + shell: true, + }); + const tarballName = packOutput.trim().split(/\r?\n/).pop()!.trim(); + + const pkgJson = { + name: answers.name, + version: '0.1.0', + private: true, + type: 'commonjs', + scripts: { + build: 'tsc', + start: 'node dist/agent.js', + }, + dependencies: { + '@loushy/build-ai-agent': `file:./${tarballName}`, + }, + devDependencies: { + typescript: '^5.0.0', + '@types/node': '^20.0.0', + }, + }; + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkgJson, null, 2) + '\n'); + + const tsconfig = { + compilerOptions: { + outDir: './dist', + rootDir: './src', + module: 'CommonJS', + moduleResolution: 'node', + target: 'ES2022', + lib: ['ES2022'], + skipLibCheck: true, + strict: true, + esModuleInterop: true, + resolveJsonModule: true, + declaration: false, + types: ['node'], + }, + include: ['src/**/*'], + }; + fs.writeFileSync(path.join(dir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2) + '\n'); + + const envVar = PROVIDER_ENV_VARS[answers.provider]; + fs.writeFileSync( + path.join(dir, '.env.example'), + `# LLM provider credential for '${answers.provider}'\n${envVar}=\n` + ); + + const toolImports = answers.tools.map(toolImportLine).join('\n'); + const toolsSource = toolsObjectSource(answers.tools); + + const agentSource = `import { createAgent } from '@loushy/build-ai-agent'; +import { resolveProvider } from '@loushy/build-ai-agent'; +${toolImports} + +const provider = resolveProvider('${answers.provider}/${defaultModelFor(answers.provider)}'); + +const agent = createAgent({ + prompt: 'You are a helpful assistant built with @loushy/build-ai-agent.', + provider, +${toolsSource ? ` tools: {\n${toolsSource}\n },\n` : ''}}); + +async function main() { + const result = await agent.send(process.argv.slice(2).join(' ') || 'hello'); + console.log(result.text); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); +`; + fs.writeFileSync(path.join(dir, 'src', 'agent.ts'), agentSource); +} + +function defaultModelFor(provider: string): string { + if (provider === 'openai') return 'gpt-4o-mini'; + if (provider === 'anthropic') return 'claude-3-5-sonnet-latest'; + return 'llama3'; +} diff --git a/packages/create-loushy-agent/tsconfig.json b/packages/create-loushy-agent/tsconfig.json new file mode 100644 index 000000000..24013152d --- /dev/null +++ b/packages/create-loushy-agent/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "composite": false, + "incremental": false, + "module": "ESNext", + "moduleResolution": "bundler", + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "skipLibCheck": true, + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] +} diff --git a/packages/create-loushy-agent/tsup.config.ts b/packages/create-loushy-agent/tsup.config.ts new file mode 100644 index 000000000..b59dc2b44 --- /dev/null +++ b/packages/create-loushy-agent/tsup.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'tsup'; + +// Matches the root project's tsup.config.ts style: named entries, dual +// cjs/esm output, declarations, no bundling of node builtins/deps we don't +// own. +export default defineConfig({ + entry: { + index: 'src/index.ts', + }, + format: ['cjs'], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + treeshake: true, + minify: false, + platform: 'node', + external: ['prompts'], +}); diff --git a/packages/create-loushy-agent/vitest.config.ts b/packages/create-loushy-agent/vitest.config.ts new file mode 100644 index 000000000..8e730d505 --- /dev/null +++ b/packages/create-loushy-agent/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, +}); diff --git a/src/agent-types/validators.ts b/src/agent-types/validators.ts index 7026e4a93..3471d00d6 100644 --- a/src/agent-types/validators.ts +++ b/src/agent-types/validators.ts @@ -7,11 +7,15 @@ export function validateAgentConfig(config: Partial): { valid: bool const errors: string[] = []; if (!config.name || config.name.trim() === '') { - errors.push('Agent name is required'); + errors.push( + "Agent name is required. Example: AgentBuilder.create().setName('my-agent')...build()" + ); } if (!config.agentType) { - errors.push('Agent type is required'); + errors.push( + 'Agent type is required. Example: AgentBuilder.create().setType(AgentType.SmartAssistant)...build()' + ); } if (config.agentType && !Object.values(AgentType).includes(config.agentType)) { diff --git a/src/cli/dev-ui/index.html b/src/cli/dev-ui/index.html new file mode 100644 index 000000000..0ba82f1a9 --- /dev/null +++ b/src/cli/dev-ui/index.html @@ -0,0 +1,66 @@ + + + + +loushy dev + + + + +

loushy dev

+
+
+ + +
+ + + + diff --git a/src/cli/dev.test.ts b/src/cli/dev.test.ts new file mode 100644 index 000000000..b8cee4260 --- /dev/null +++ b/src/cli/dev.test.ts @@ -0,0 +1,205 @@ +import { describe, it, expect, beforeAll, afterEach } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { startDevServer, DevServerHandle } from './dev'; +import { LLMProviderRegistry } from '../providers/llm'; +import { createMockProvider } from '../providers/mock'; + +const MOCK_RESPONSE = 'This is the mock dev-server response.'; + +beforeAll(() => { + LLMProviderRegistry.register('mock', () => + createMockProvider({ responses: [MOCK_RESPONSE] }) + ); +}); + +function writeConfig(dir: string, overrides: Record = {}) { + const configPath = path.join(dir, 'agent.config.json'); + fs.writeFileSync( + configPath, + JSON.stringify({ + name: 'dev-test-agent', + prompt: 'You are a helpful test agent.', + provider: { type: 'mock', model: 'mock-model-1' }, + ...overrides, + }) + ); + return configPath; +} + +let handle: DevServerHandle | undefined; +afterEach(async () => { + if (handle) { + await handle.close(); + handle = undefined; + } +}); + +describe('startDevServer', () => { + it('serves the chat UI on GET / with an id="msg" input', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'loushy-dev-')); + const configPath = writeConfig(dir); + handle = await startDevServer(configPath, 0); + + const res = await fetch(`http://localhost:${addressPort(handle)}/`); + expect(res.status).toBe(200); + expect(res.headers.get('content-type')).toContain('text/html'); + const html = await res.text(); + expect(html).toContain('id="msg"'); + expect(html).toContain('id="send"'); + expect(html).toContain('id="log"'); + }); + + it('a realistic message round-trip via the UI page\'s own /chat call produces a reply', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'loushy-dev-')); + const configPath = writeConfig(dir); + handle = await startDevServer(configPath, 0); + const port = addressPort(handle); + + // Confirm the page itself posts to /chat (what its inline script does), + // then drive that exact same call to confirm the round trip works. + const page = await (await fetch(`http://localhost:${port}/`)).text(); + expect(page).toContain("fetch('/chat'"); + + const res = await fetch(`http://localhost:${port}/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: 'hello from the UI' }), + }); + const json = await res.json(); + expect(json.text).toBe(MOCK_RESPONSE); + }); + + it('responds 200 ok on GET /health', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'loushy-dev-')); + const configPath = writeConfig(dir); + handle = await startDevServer(configPath, 0); + + const res = await fetch(`http://localhost:${addressPort(handle)}/health`); + expect(res.status).toBe(200); + expect(await res.text()).toBe('ok'); + }); + + it('POST /chat returns the mock provider response', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'loushy-dev-')); + const configPath = writeConfig(dir); + handle = await startDevServer(configPath, 0); + + const res = await fetch(`http://localhost:${addressPort(handle)}/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: 'hi' }), + }); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.text).toBe(MOCK_RESPONSE); + }); + + it('rejects a POST /chat body larger than the size cap with 413', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'loushy-dev-')); + const configPath = writeConfig(dir); + handle = await startDevServer(configPath, 0); + const port = addressPort(handle); + + // 1MB cap - send a body comfortably over it. + const oversized = JSON.stringify({ message: 'x'.repeat(2 * 1024 * 1024) }); + + const res = await fetch(`http://localhost:${port}/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: oversized, + }); + expect(res.status).toBe(413); + }); + + it('binds to 127.0.0.1 (localhost-only) by default, not all interfaces', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'loushy-dev-')); + const configPath = writeConfig(dir); + handle = await startDevServer(configPath, 0); + + const addr = handle.server.address(); + expect(addr && typeof addr === 'object' ? addr.address : addr).toBe('127.0.0.1'); + }); + + it('rejects with a clear message when the port is already in use', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'loushy-dev-')); + const configPath = writeConfig(dir); + handle = await startDevServer(configPath, 0); + const port = addressPort(handle); + + await expect(startDevServer(configPath, port)).rejects.toThrow( + new RegExp(`port ${port} is already in use`) + ); + }); +}); + +describe('hot reload (LOU-H8)', () => { + it('picks up an edited prompt without restarting the server/port, and keeps working on an invalid edit', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'loushy-dev-reload-')); + const configPath = writeConfig(dir); + handle = await startDevServer(configPath, 0); + const port = addressPort(handle); + const originalServer = handle.server; + + const first = await ( + await fetch(`http://localhost:${port}/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: 'hi' }), + }) + ).json(); + expect(first.text).toBe(MOCK_RESPONSE); + + // Register a second mock provider variant so the reload is observable. + const UPDATED_RESPONSE = 'Updated after hot reload.'; + LLMProviderRegistry.register('mock', () => + createMockProvider({ responses: [UPDATED_RESPONSE] }) + ); + + fs.writeFileSync( + configPath, + JSON.stringify({ + name: 'dev-test-agent', + prompt: 'You are an UPDATED helpful test agent.', + provider: { type: 'mock', model: 'mock-model-1' }, + }) + ); + + await new Promise((resolve) => setTimeout(resolve, 400)); + + const second = await ( + await fetch(`http://localhost:${port}/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: 'hi again' }), + }) + ).json(); + expect(second.text).toBe(UPDATED_RESPONSE); + + // Same server/port the whole time - no restart happened. + expect(handle.server).toBe(originalServer); + expect(addressPort(handle)).toBe(port); + + // Now write an invalid edit (missing required 'prompt') - the server + // must keep serving the last-good (UPDATED_RESPONSE) config rather + // than crashing. + fs.writeFileSync(configPath, JSON.stringify({ provider: { type: 'mock', model: 'x' } })); + await new Promise((resolve) => setTimeout(resolve, 400)); + + const third = await ( + await fetch(`http://localhost:${port}/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: 'still there?' }), + }) + ).json(); + expect(third.text).toBe(UPDATED_RESPONSE); + }); +}); + +function addressPort(h: DevServerHandle): number { + const addr = h.server.address(); + if (addr && typeof addr === 'object') return addr.port; + return h.port; +} diff --git a/src/cli/dev.ts b/src/cli/dev.ts new file mode 100644 index 000000000..4d6a10faa --- /dev/null +++ b/src/cli/dev.ts @@ -0,0 +1,210 @@ +/** + * `loushy dev` - a local dev server for iterating on an agent (LOU-H6). + * + * Serves: + * GET /health -> 200 'ok' + * GET / -> the minimal chat UI (LOU-H7) + * POST /chat -> { message } in, the agent's real ExecutionResult out + * + * Config loading (LOU-H9): configPath is a declarative agent spec file + * (.yaml/.yml or .json - see src/spec/schema.ts's AgentSpec), loaded and + * zod-validated via loadSpec() and turned into a live agent via + * specToAgent(). This retrofits LOU-H6's original ad-hoc + * {name, prompt, provider, tools} JSON loader now that LOU-H9 (the + * designed-for successor) exists - the two shapes are compatible (a plain + * .json config in the old shape is a valid AgentSpec), so no existing + * configs need to change. + */ +import * as http from 'node:http'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { SimpleAgent } from '../createAgent'; +import { loadSpec } from '../spec/loadSpec'; +import { specToAgent } from '../spec/specToAgent'; + +function loadAgentFromConfig(configPath: string): SimpleAgent { + return specToAgent(loadSpec(configPath)); +} + +export interface DevServerHandle { + server: http.Server; + port: number; + close: () => Promise; +} + +/** Body-size cap for POST /chat, matching common Node.js body-size-limit conventions. */ +const MAX_BODY_BYTES = 1024 * 1024; // 1MB + +class PayloadTooLargeError extends Error {} + +function readBody(req: http.IncomingMessage): Promise { + return new Promise((resolve, reject) => { + let body = ''; + let bytes = 0; + let tooLarge = false; + req.on('data', (chunk) => { + bytes += chunk.length; + if (bytes > MAX_BODY_BYTES) { + // Stop growing `body` once over the cap, but keep draining the + // stream (rather than destroying it or dropping listeners) so the + // client can finish writing and the connection doesn't deadlock - + // we reject once the request actually ends. + tooLarge = true; + return; + } + body += chunk; + }); + req.on('end', () => { + if (tooLarge) { + reject(new PayloadTooLargeError(`Request body exceeds ${MAX_BODY_BYTES} byte limit`)); + return; + } + resolve(body); + }); + req.on('error', reject); + }); +} + +function serveChatUi(res: http.ServerResponse): void { + const uiPath = path.join(__dirname, 'dev-ui', 'index.html'); + const html = fs.readFileSync(uiPath, 'utf8'); + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(html); +} + +/** + * Mutable holder for the currently-loaded agent (LOU-H8), so /chat always + * reads the latest reloaded version without restarting the HTTP server or + * dropping connections. + */ +interface AgentHolder { + agent: SimpleAgent; +} + +async function handleRequest( + req: http.IncomingMessage, + res: http.ServerResponse, + holder: AgentHolder +): Promise { + if (req.method === 'GET' && req.url === '/health') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('ok'); + return; + } + + if (req.method === 'GET' && req.url === '/') { + try { + serveChatUi(res); + } catch { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('dev UI not found'); + } + return; + } + + if (req.method === 'POST' && req.url === '/chat') { + try { + const body = await readBody(req); + const { message } = JSON.parse(body || '{}'); + if (typeof message !== 'string' || !message) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: "Request body must be JSON with a 'message' string" })); + return; + } + + const result = await holder.agent.send(message); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(result)); + } catch (error) { + if (error instanceof PayloadTooLargeError) { + res.writeHead(413, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: error.message })); + return; + } + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: (error as Error).message })); + } + return; + } + + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('not found'); +} + +/** + * Starts the dev server, binding to `host`:`port` (checked for availability + * up front - EADDRINUSE is caught and rejected with a clear, port-naming + * error rather than crashing uncaught). + * + * `host` defaults to '127.0.0.1' (localhost-only) since this is a local dev + * tool and should not be reachable from the network by default. Pass an + * explicit host (e.g. '0.0.0.0') to opt in to LAN access, such as testing + * from a phone on the same network. + * + * Watches configPath (LOU-H8) via fs.watch: on a valid edit, the live + * agent is swapped in-place through the mutable AgentHolder above; on an + * invalid edit (e.g. a JSON syntax error, or a missing required field), + * the error is logged and the previous working agent is kept - the server + * never crashes and never drops the port on a bad config edit. + */ +export async function startDevServer( + configPath: string, + port = 3737, + host = '127.0.0.1' +): Promise { + const holder: AgentHolder = { agent: loadAgentFromConfig(configPath) }; + + const watcher = fs.watch(configPath, { persistent: false }, () => { + try { + holder.agent = loadAgentFromConfig(configPath); + // eslint-disable-next-line no-console + console.log(`[loushy dev] reloaded config from ${configPath}`); + } catch (error) { + // eslint-disable-next-line no-console + console.error( + `[loushy dev] failed to reload ${configPath}, keeping previous config: ${ + (error as Error).message + }` + ); + } + }); + + const server = http.createServer((req, res) => { + handleRequest(req, res, holder).catch((error) => { + // eslint-disable-next-line no-console + console.error('[loushy dev] unhandled request error:', error); + if (!res.headersSent) { + res.writeHead(500); + } + res.end(); + }); + }); + + await new Promise((resolve, reject) => { + const onError = (err: NodeJS.ErrnoException) => { + server.removeListener('listening', onListening); + if (err.code === 'EADDRINUSE') { + reject(new Error(`[loushy dev] port ${port} is already in use. Pass a different port.`)); + } else { + reject(err); + } + }; + const onListening = () => { + server.removeListener('error', onError); + resolve(); + }; + server.once('error', onError); + server.once('listening', onListening); + server.listen(port, host); + }); + + return { + server, + port, + close: () => + new Promise((resolve, reject) => { + watcher.close(); + server.close((err) => (err ? reject(err) : resolve())); + }), + }; +} diff --git a/src/core/AgentBuilder.test.ts b/src/core/AgentBuilder.test.ts index 977c87cce..ed84e0fab 100644 --- a/src/core/AgentBuilder.test.ts +++ b/src/core/AgentBuilder.test.ts @@ -33,6 +33,12 @@ describe('AgentBuilder', () => { }).toThrow('Agent type is required'); }); + it('names the missing field and shows a corrective snippet (LOU-H12)', () => { + expect(() => { + new AgentBuilder().build(); + }).toThrow(/Agent name is required\. Example:.*Agent type is required\. Example:/s); + }); + it('should add tools correctly', () => { const agent = new AgentBuilder() .setType(AgentType.SmartAssistant) diff --git a/src/createAgent.test.ts b/src/createAgent.test.ts new file mode 100644 index 000000000..eed9ab6c5 --- /dev/null +++ b/src/createAgent.test.ts @@ -0,0 +1,27 @@ +/** + * LOU-H1 test: only imports createAgent() and a mock provider - zero other + * Loushy imports - to prove the one-liner surface is self-contained. + */ +import { describe, it, expect } from 'vitest'; +import { createAgent } from './createAgent'; +import { createMockProvider } from './providers/mock'; + +describe('createAgent', () => { + it('sends a message and gets a non-empty response', async () => { + const agent = createAgent({ + prompt: 'You are a helpful assistant.', + provider: createMockProvider({ responses: ['Hello there!'] }), + }); + + const result = await agent.send('hi'); + + expect(result.text).toBeTruthy(); + expect(result.text.length).toBeGreaterThan(0); + }); + + it('throws a guiding error when provider is missing', () => { + expect(() => + createAgent({ prompt: 'x', provider: undefined as any }) + ).toThrow(/provider/i); + }); +}); diff --git a/src/createAgent.ts b/src/createAgent.ts new file mode 100644 index 000000000..7fc3ba241 --- /dev/null +++ b/src/createAgent.ts @@ -0,0 +1,96 @@ +/** + * createAgent() - zero-config, one-liner convenience API (LOU-H1) + * + * Wraps AgentBuilder + ToolRegistry + the real static + * AgentExecutor.execute() behind a minimal `{send}` surface so a caller + * who just wants "prompt in, text out" doesn't have to learn the full + * builder/executor/registry API. + * + * AgentExecutor is a static, instance-free API (`AgentExecutor.execute(options)`, + * never `new AgentExecutor()`). Its ExecuteOptions only strictly requires + * `agent`, `input` and `provider` - repositories (Agent/Session/Result/...) + * are NOT accepted or required by execute() at all, so createAgent() does + * not construct or wire any repository/mock-repository implementation. + * `toolRegistry` is optional too; it's only built when `config.tools` is + * non-empty. + */ + +import { AgentBuilder } from './core/AgentBuilder'; +import { AgentType } from './types'; +import { AgentExecutor, ExecutionResult } from './execution/AgentExecutor'; +import { LLMProvider } from './providers/llm'; +import { ToolRegistry } from './tools/ToolRegistry'; +import { ToolDescriptor } from './types'; + +/** + * Configuration for createAgent(). Tools are keyed by the name the agent + * (and AgentConfig.tools) will refer to them by - createAgent() registers + * each one into a fresh ToolRegistry under that key. + */ +export interface CreateAgentConfig { + /** System prompt for the agent. */ + prompt: string; + /** LLM provider instance (real or mock) used to generate responses. */ + provider: LLMProvider; + /** Optional tools, keyed by the name the agent should call them by. */ + tools?: Record; + /** Optional agent name; defaults to 'agent'. */ + name?: string; + /** Optional maxSteps passed through to AgentExecutor.execute(). */ + maxSteps?: number; +} + +export interface SimpleAgent { + /** Send a single user message and get back the full execution result text. */ + send: (message: string) => Promise; +} + +/** + * Build a ready-to-use agent from a prompt + provider (+ optional tools) in + * one call. Does NOT modify AgentBuilder or AgentExecutor - it is purely a + * thin composition of the existing public APIs. + */ +export function createAgent(config: CreateAgentConfig): SimpleAgent { + if (!config || !config.provider) { + throw new Error( + "createAgent: 'provider' is required. Example: createAgent({ prompt: '...', provider: myProvider })" + ); + } + if (!config.prompt) { + throw new Error( + "createAgent: 'prompt' is required. Example: createAgent({ prompt: 'You are a helpful assistant', provider: myProvider })" + ); + } + + const toolNames = config.tools ? Object.keys(config.tools) : []; + + let toolRegistry: ToolRegistry | undefined; + const toolsConfig: Record = {}; + + if (toolNames.length > 0) { + toolRegistry = new ToolRegistry(); + for (const name of toolNames) { + toolRegistry.register(name, config.tools![name]); + toolsConfig[name] = { tool: name }; + } + } + + const agent = AgentBuilder.create() + .setType(AgentType.SmartAssistant) + .setName(config.name || 'agent') + .setPrompt(config.prompt) + .setTools(toolsConfig) + .build(); + + return { + async send(message: string): Promise { + return AgentExecutor.execute({ + agent, + input: message, + provider: config.provider, + toolRegistry, + maxSteps: config.maxSteps, + }); + }, + }; +} diff --git a/src/execution/AgentExecutor.test.ts b/src/execution/AgentExecutor.test.ts index e27d5988d..f6618c449 100644 --- a/src/execution/AgentExecutor.test.ts +++ b/src/execution/AgentExecutor.test.ts @@ -1141,4 +1141,43 @@ describe('AgentExecutor', () => { expect(result.text).toBeDefined(); }); }); + + describe('guiding runtime error messages (LOU-H12)', () => { + it('throws naming "provider" with a corrective snippet when provider is omitted', async () => { + const agent = AgentBuilder.create() + .setType(AgentType.SmartAssistant) + .setName('Test Agent') + .build(); + + await expect( + AgentExecutor.execute({ + agent, + input: 'Hello', + } as any) + ).rejects.toThrow(/'provider' is required.*Example:/s); + }); + + it('throws naming "agent" with a corrective snippet when agent is omitted', async () => { + await expect( + AgentExecutor.execute({ + input: 'Hello', + provider, + } as any) + ).rejects.toThrow(/'agent' is required.*Example:/s); + }); + + it('throws naming "input" with a corrective snippet when input is omitted', async () => { + const agent = AgentBuilder.create() + .setType(AgentType.SmartAssistant) + .setName('Test Agent') + .build(); + + await expect( + AgentExecutor.execute({ + agent, + provider, + } as any) + ).rejects.toThrow(/'input' is required.*Example:/s); + }); + }); }); diff --git a/src/execution/AgentExecutor.ts b/src/execution/AgentExecutor.ts index fa535d8b9..8473921bd 100644 --- a/src/execution/AgentExecutor.ts +++ b/src/execution/AgentExecutor.ts @@ -198,6 +198,14 @@ export class AgentExecutor { * Execute agent without streaming */ static async execute(options: ExecuteOptions): Promise { + // AgentExecutor is a static, instance-free API - there is no + // constructor to guard these, so execute() is the first and only + // entry point where a missing required option can be caught before it + // fails deep inside runAgentLoop() with a generic "Cannot read + // properties of undefined" error (e.g. `provider.generate(...)` + // throwing because `provider` was never checked). + this.validateExecuteOptions(options); + const { input, exporter } = options; // The entire run is wrapped in a top-level 'agent.run' span (LOU-E5). @@ -731,6 +739,35 @@ export class AgentExecutor { } } + /** + * Guards the options execute() truly cannot run without, throwing a + * clear error naming the missing field plus a corrective one-line code + * snippet - instead of the generic "Cannot read properties of + * undefined" TypeError that would otherwise surface deep inside + * runAgentLoop() (e.g. `provider.generate(...)` when `provider` is + * undefined). + */ + private static validateExecuteOptions(options: ExecuteOptions): void { + if (!options || !options.provider) { + throw new Error( + "AgentExecutor.execute: 'provider' is required. " + + "Example: AgentExecutor.execute({ agent, input, provider: myProvider })" + ); + } + if (!options.agent) { + throw new Error( + "AgentExecutor.execute: 'agent' is required. " + + 'Example: AgentExecutor.execute({ agent: AgentBuilder.create()...build(), input, provider })' + ); + } + if (options.input === undefined || options.input === null) { + throw new Error( + "AgentExecutor.execute: 'input' is required. " + + "Example: AgentExecutor.execute({ agent, input: 'hello', provider })" + ); + } + } + /** * Emit event to callback */ diff --git a/src/index.ts b/src/index.ts index 6e294435d..653fd50c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,3 +46,9 @@ export * from './templates'; // Utils export * from './utils'; + +// createAgent() convenience API (LOU-H1) +export * from './createAgent'; + +// Declarative agent spec file format (LOU-H9) +export * from './spec'; diff --git a/src/providers/index.ts b/src/providers/index.ts index 8f74c9709..e2b305689 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -17,8 +17,16 @@ import { OpenAIProvider, OpenAIProviderConfig } from './OpenAIProvider'; import { OllamaProvider, OllamaProviderConfig } from './OllamaProvider'; import { OpenRouterProvider, OpenRouterProviderConfig } from './OpenRouterProvider'; import { AnthropicProvider, AnthropicProviderConfig } from './AnthropicProvider'; +import { MockLLMProvider, MockProviderConfig } from './mock'; LLMProviderRegistry.register('openai', (config) => new OpenAIProvider(config as OpenAIProviderConfig)); LLMProviderRegistry.register('ollama', (config) => new OllamaProvider(config as OllamaProviderConfig)); LLMProviderRegistry.register('openrouter', (config) => new OpenRouterProvider(config as OpenRouterProviderConfig)); LLMProviderRegistry.register('anthropic', (config) => new AnthropicProvider(config as AnthropicProviderConfig)); +// Zero-config/dev-server support (LOU-H): registered centrally so `loushy dev` +// and starter templates can resolve 'mock' outside of the test suite, where +// individual test files previously registered it themselves. Registration +// here is safe even though some tests also register 'mock' in beforeAll/ +// beforeEach - LLMProviderRegistry.register() just does a Map.set(), so a +// later registration silently overwrites rather than throwing. +LLMProviderRegistry.register('mock', (config) => new MockLLMProvider(config as MockProviderConfig)); diff --git a/src/providers/mock.ts b/src/providers/mock.ts index 649d5ef42..e7c4632e1 100644 --- a/src/providers/mock.ts +++ b/src/providers/mock.ts @@ -6,6 +6,7 @@ import { LLMProvider, LLMProviderConfig, + LLMProviderRegistry, GenerateOptions, GenerateResult, StreamResult, @@ -183,3 +184,30 @@ export class MockLLMProvider implements LLMProvider { export function createMockProvider(config: MockProviderConfig = { name: 'mock' }): MockLLMProvider { return new MockLLMProvider(config); } + +/** + * Self-registers 'mock' into the production LLMProviderRegistry (LOU-H fix). + * + * src/providers/index.ts also registers 'mock' in the same style as the + * real providers (openai/ollama/openrouter/anthropic), for readability and + * for consumers who import the full SDK barrel. But that barrel eagerly + * imports OpenAIProvider/OllamaProvider/OpenRouterProvider, which statically + * `import` their optional peer-dependency SDKs ('@ai-sdk/openai', + * 'ollama-ai-provider', ...) at module scope - fine for consumers who have + * those installed, but not for a bundle like `loushy dev`'s CLI entry + * (dist/cli/dev.js) or LOU-H10's starter templates, which must boot with + * zero API keys/SDKs installed via just the mock provider. + * + * This module (mock.ts) has no external dependencies at all, so it's always + * safe to load eagerly. Registering here too - and having specToAgent.ts + * import this file directly (see src/spec/specToAgent.ts) instead of the + * whole '../providers' barrel - guarantees 'mock' resolves via + * LLMProviderRegistry.create('mock', ...) in every entry point, without + * requiring any of the real provider SDKs to be installed. + * + * Registration is a plain LLMProviderRegistry.register() call (Map.set() + * under the hood), so re-registering 'mock' here after (or before) + * providers/index.ts's own registration is safe - it silently overwrites, + * it does not throw. + */ +LLMProviderRegistry.register('mock', (config) => new MockLLMProvider(config as MockProviderConfig)); diff --git a/src/spec/index.ts b/src/spec/index.ts new file mode 100644 index 000000000..5a21d133e --- /dev/null +++ b/src/spec/index.ts @@ -0,0 +1,3 @@ +export * from './schema'; +export * from './loadSpec'; +export * from './specToAgent'; diff --git a/src/spec/loadSpec.test.ts b/src/spec/loadSpec.test.ts new file mode 100644 index 000000000..df8a48b26 --- /dev/null +++ b/src/spec/loadSpec.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { loadSpec } from './loadSpec'; + +function tmpFile(name: string, content: string): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'loushy-spec-')); + const filePath = path.join(dir, name); + fs.writeFileSync(filePath, content); + return filePath; +} + +describe('loadSpec', () => { + it('loads a .yaml spec file', () => { + const filePath = tmpFile( + 'agent.yaml', + ` +name: yaml-agent +prompt: You are a YAML-defined agent. +provider: + type: mock + model: mock-model-1 +tools: + - http +` + ); + + const spec = loadSpec(filePath); + expect(spec).toEqual({ + name: 'yaml-agent', + prompt: 'You are a YAML-defined agent.', + provider: { type: 'mock', model: 'mock-model-1' }, + tools: ['http'], + }); + }); + + it('loads an equivalent .json spec file', () => { + const filePath = tmpFile( + 'agent.json', + JSON.stringify({ + name: 'json-agent', + prompt: 'You are a JSON-defined agent.', + provider: { type: 'mock', model: 'mock-model-1' }, + tools: ['http'], + }) + ); + + const spec = loadSpec(filePath); + expect(spec.name).toBe('json-agent'); + expect(spec.provider).toEqual({ type: 'mock', model: 'mock-model-1' }); + }); + + it('throws naming the missing field when prompt is absent', () => { + const filePath = tmpFile( + 'invalid.json', + JSON.stringify({ + name: 'incomplete-agent', + provider: { type: 'openai', model: 'gpt-4o-mini' }, + }) + ); + + expect(() => loadSpec(filePath)).toThrow(/'prompt'/); + }); + + it('throws for an unsupported extension', () => { + const filePath = tmpFile('agent.txt', 'not a spec'); + expect(() => loadSpec(filePath)).toThrow(/unsupported extension/); + }); +}); diff --git a/src/spec/loadSpec.ts b/src/spec/loadSpec.ts new file mode 100644 index 000000000..b457260c0 --- /dev/null +++ b/src/spec/loadSpec.ts @@ -0,0 +1,45 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { parse as parseYaml } from 'yaml'; +import { ZodError } from 'zod'; +import { AgentSpec, agentSpecSchema } from './schema'; + +function formatZodError(filePath: string, error: ZodError): string { + const details = error.issues + .map((issue) => { + const field = issue.path.length > 0 ? issue.path.join('.') : '(root)'; + return `'${field}': ${issue.message}`; + }) + .join('; '); + return `loadSpec: '${filePath}' failed validation - ${details}`; +} + +/** + * Loads and validates an agent spec file. Supports both .yaml/.yml (via + * the `yaml` package) and .json (via JSON.parse), chosen by file + * extension. Validated against the zod schema in schema.ts; a missing or + * invalid field throws an Error whose message names the exact field (e.g. + * "'prompt': Required"). + */ +export function loadSpec(filePath: string): AgentSpec { + const raw = fs.readFileSync(filePath, 'utf8'); + const ext = path.extname(filePath).toLowerCase(); + + let data: unknown; + if (ext === '.yaml' || ext === '.yml') { + data = parseYaml(raw); + } else if (ext === '.json') { + data = JSON.parse(raw); + } else { + throw new Error( + `loadSpec: unsupported extension '${ext}' for '${filePath}'. Use .yaml, .yml or .json` + ); + } + + const result = agentSpecSchema.safeParse(data); + if (!result.success) { + throw new Error(formatZodError(filePath, result.error)); + } + + return result.data; +} diff --git a/src/spec/schema.ts b/src/spec/schema.ts new file mode 100644 index 000000000..9ed2c489a --- /dev/null +++ b/src/spec/schema.ts @@ -0,0 +1,42 @@ +/** + * Declarative agent spec file format (LOU-H9). + * + * A plain-data description of an agent - name, prompt, provider, tool + * names - that can be authored as YAML or JSON and turned into a live + * agent via specToAgent(). The TS interface and the zod schema below are + * kept in lockstep by hand (zod is already a project dependency; no + * schema-to-type codegen is set up in this repo). + */ +import { z } from 'zod'; + +export interface AgentSpecProvider { + type: string; + model: string; +} + +export interface AgentSpec { + name: string; + prompt: string; + provider: AgentSpecProvider; + tools?: string[]; +} + +export const agentSpecProviderSchema = z.object({ + type: z.string({ + required_error: "AgentSpec validation failed: missing required field 'provider.type'", + }), + model: z.string({ + required_error: "AgentSpec validation failed: missing required field 'provider.model'", + }), +}); + +export const agentSpecSchema = z.object({ + name: z.string({ + required_error: "AgentSpec validation failed: missing required field 'name'", + }), + prompt: z.string({ + required_error: "AgentSpec validation failed: missing required field 'prompt'", + }), + provider: agentSpecProviderSchema, + tools: z.array(z.string()).optional(), +}); diff --git a/src/spec/specToAgent.test.ts b/src/spec/specToAgent.test.ts new file mode 100644 index 000000000..72e96fb82 --- /dev/null +++ b/src/spec/specToAgent.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { loadSpec } from './loadSpec'; +import { specToAgent } from './specToAgent'; +import { createAgent } from '../createAgent'; +import { LLMProviderRegistry } from '../providers/llm'; +import { createMockProvider, MockLLMProvider } from '../providers/mock'; + +const RESPONSES = ['deterministic response one', 'deterministic response two']; + +beforeAll(() => { + // Registered once so both the spec-built agent and the createAgent()-built + // agent below can each construct their OWN MockLLMProvider instance with + // the same scripted response list, making their behavior directly + // comparable without sharing mutable state between them. + LLMProviderRegistry.register('mock', () => createMockProvider({ responses: RESPONSES })); +}); + +describe('specToAgent', () => { + it('produces behavior identical to an equivalent createAgent()-built agent for the same input', async () => { + const filePath = path.join( + fs.mkdtempSync(path.join(os.tmpdir(), 'loushy-spec-equiv-')), + 'agent.yaml' + ); + fs.writeFileSync( + filePath, + ` +name: equivalence-agent +prompt: You are a deterministic test agent. +provider: + type: mock + model: mock-model-1 +` + ); + + const specAgent = specToAgent(loadSpec(filePath)); + const directAgent = createAgent({ + prompt: 'You are a deterministic test agent.', + provider: new MockLLMProvider({ responses: RESPONSES }), + }); + + const specResult = await specAgent.send('hello'); + const directResult = await directAgent.send('hello'); + + expect(specResult.text).toBe(directResult.text); + expect(specResult.finishReason).toBe(directResult.finishReason); + expect(specResult.toolCalls).toEqual(directResult.toolCalls); + }); + + it('throws naming an unrecognized tool', () => { + expect(() => + specToAgent({ + name: 'x', + prompt: 'x', + provider: { type: 'mock', model: 'm' }, + tools: ['not-a-real-tool'], + }) + ).toThrow(/unknown tool 'not-a-real-tool'/); + }); + + it('throws a guiding error for a credentialed tool (github/jira) with no config field', () => { + expect(() => + specToAgent({ + name: 'x', + prompt: 'x', + provider: { type: 'mock', model: 'm' }, + tools: ['github'], + }) + ).toThrow(/needs credentials/); + }); +}); diff --git a/src/spec/specToAgent.ts b/src/spec/specToAgent.ts new file mode 100644 index 000000000..6fe9369c8 --- /dev/null +++ b/src/spec/specToAgent.ts @@ -0,0 +1,102 @@ +import { AgentSpec } from './schema'; +import { createAgent, SimpleAgent, CreateAgentConfig } from '../createAgent'; +import { resolveProvider } from '../providers/resolveProvider'; +import { LLMProvider, LLMProviderRegistry } from '../providers/llm'; +// Side-effect import: '../providers/mock' self-registers 'mock' into +// LLMProviderRegistry (see the bottom of src/providers/mock.ts for why). +// specToAgent resolves provider types dynamically by string via +// LLMProviderRegistry.create() below, but nothing else in this module's +// dependency graph references a providers module as a VALUE (only as +// types, e.g. LLMProvider), so bundlers (tsup/esbuild) never pull any +// providers module into CLI bundles like dist/cli/dev.js that don't also +// go through src/index.ts. Without this explicit import, `loushy dev` (and +// anything else that loads a spec directly, bypassing the SDK's top-level +// index.ts) never actually registers 'mock' at runtime, even though the +// registration code exists. This imports mock.ts specifically (not the +// whole '../providers' barrel) because mock.ts has no external +// dependencies - importing the full barrel would also eagerly pull in +// OpenAIProvider/OllamaProvider/OpenRouterProvider, whose top-level +// imports of their optional peer-dependency SDKs ('@ai-sdk/openai', +// 'ollama-ai-provider', ...) would then crash `loushy dev` for the exact +// zero-API-key/zero-extra-installs use case this fix exists for. +import '../providers/mock'; +import { httpTool } from '../tools/built-in/http'; +import { currentDateTool } from '../tools/built-in/currentDate'; +import { dayNameTool } from '../tools/built-in/dayName'; +import { ToolDescriptor } from '../types'; + +const REAL_PROVIDER_TYPES = new Set(['openai', 'anthropic', 'ollama', 'openrouter']); + +/** + * Resolves a spec's provider config to an LLMProvider. Real provider types + * go through LOU-F8's resolveProvider() (env-var driven credentials, as + * usual). Any other registered type - notably 'mock', which intentionally + * has no env var and is never part of resolveProvider()'s whitelist - is + * created directly via LLMProviderRegistry. This is what lets `loushy dev` + * (LOU-H6/H8) run end-to-end against a MockLLMProvider in tests without + * real API credentials, while still reusing resolveProvider() verbatim for + * every real provider type. + */ +export function resolveSpecProvider(type: string, model: string): LLMProvider { + if (REAL_PROVIDER_TYPES.has(type.toLowerCase())) { + return resolveProvider(`${type}/${model}`); + } + return LLMProviderRegistry.create(type, { defaultModel: model }); +} + +/** + * Built-in tool names an AgentSpec's `tools` list can reference without + * any extra config. 'github' and 'jira' are deliberately NOT in this map: + * both src/tools/built-in/github.ts and jira.ts export a *factory* + * (createGitHubTools(config)/createJiraTools(config)) that needs real + * credentials (token/owner/repo, or baseUrl/email/apiToken) which a + * spec file has no field for - referencing 'github' or 'jira' in `tools` + * throws a clear, actionable error instead of silently registering a + * tool that would fail at call time. + */ +const RESOLVABLE_BUILT_IN_TOOLS: Record = { + http: httpTool, + 'current-date': currentDateTool, + 'day-name': dayNameTool, +}; + +const CREDENTIALED_TOOLS = new Set(['github', 'jira']); + +function resolveTool(name: string): ToolDescriptor { + const tool = RESOLVABLE_BUILT_IN_TOOLS[name]; + if (tool) return tool; + + if (CREDENTIALED_TOOLS.has(name)) { + throw new Error( + `specToAgent: tool '${name}' needs credentials (see src/tools/built-in/${name}.ts's ` + + `create${name === 'github' ? 'GitHub' : 'Jira'}Tools(config)) that an AgentSpec has no ` + + `field for. Build this agent with createAgent() directly and pass the configured tool instead.` + ); + } + + throw new Error( + `specToAgent: unknown tool '${name}'. Known built-in tools: ${Object.keys( + RESOLVABLE_BUILT_IN_TOOLS + ).join(', ')}` + ); +} + +/** + * Maps a validated AgentSpec to a live agent via createAgent() (LOU-H1), + * resolving each spec tool name against the SDK's real built-in tools. + */ +export function specToAgent(spec: AgentSpec): SimpleAgent { + const provider = resolveSpecProvider(spec.provider.type, spec.provider.model); + + const tools: CreateAgentConfig['tools'] = {}; + for (const name of spec.tools || []) { + tools[name] = resolveTool(name); + } + + return createAgent({ + name: spec.name, + prompt: spec.prompt, + provider, + tools: Object.keys(tools).length > 0 ? tools : undefined, + }); +} diff --git a/tsup.config.ts b/tsup.config.ts index ccd9787cc..ea2e8a48a 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,4 +1,21 @@ import { defineConfig } from 'tsup'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +/** + * tsup only bundles TS/JS entrypoints - src/cli/dev-ui/index.html (LOU-H7) + * is a plain static asset dev.ts reads at runtime relative to its own + * __dirname, so it has to be copied into dist/cli/dev-ui alongside the + * compiled dev.js by hand. + */ +function copyDevUi() { + const src = path.join(__dirname, 'src', 'cli', 'dev-ui'); + const dest = path.join(__dirname, 'dist', 'cli', 'dev-ui'); + fs.mkdirSync(dest, { recursive: true }); + for (const file of fs.readdirSync(src)) { + fs.copyFileSync(path.join(src, file), path.join(dest, file)); + } +} export default defineConfig({ entry: { @@ -9,6 +26,7 @@ export default defineConfig({ 'data/index': 'src/data/index.ts', 'types/index': 'src/types/index.ts', 'data/mocks': 'src/data/mocks.ts', + 'cli/dev': 'src/cli/dev.ts', }, format: ['cjs', 'esm'], dts: true, @@ -18,4 +36,7 @@ export default defineConfig({ treeshake: true, minify: false, external: ['ai', 'zod'], + onSuccess: async () => { + copyDevUi(); + }, });