From 78e28478cb9349f8a7ff1b79709d34cf9dc84c20 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 21:40:05 +0300 Subject: [PATCH 01/16] LOU-H1: createAgent() one-liner convenience API Adds src/createAgent.ts exposing createAgent({prompt, provider, tools}) -> {send}, a thin composition of the existing AgentBuilder + ToolRegistry + static AgentExecutor.execute(). AgentExecutor.execute() does not require any repositories, so none are wired up here. tools is keyed by name (Record) rather than an array, since ToolDescriptor carries no name of its own and AgentConfig.tools is itself name-keyed. Co-Authored-By: Claude Sonnet 5 --- src/createAgent.test.ts | 27 ++++++++++++ src/createAgent.ts | 96 +++++++++++++++++++++++++++++++++++++++++ src/index.ts | 3 ++ 3 files changed, 126 insertions(+) create mode 100644 src/createAgent.test.ts create mode 100644 src/createAgent.ts 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/index.ts b/src/index.ts index 6e294435d..4299846e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,3 +46,6 @@ export * from './templates'; // Utils export * from './utils'; + +// createAgent() convenience API (LOU-H1) +export * from './createAgent'; From f6232088f018b8565b50ee9f5ce031a4116e5e7c Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 21:42:14 +0300 Subject: [PATCH 02/16] LOU-H2: CLI package scaffolding: bin entry + arg parsing Adds packages/create-loushy-agent with its own package.json (bin: create-loushy-agent -> bin/cli.js), a #!/usr/bin/env node bin/cli.js that requires dist/index.js's exported main(), and src/index.ts using Node's built-in util.parseArgs (no new dependency) for --help/--name/--provider /--tools/--yes and a clear "Unknown flag: --xyz" message + exit 1 for anything else. Bundled via its own tsup.config.ts (CJS only, matching the root project's tsup style). Verified: --help exits 0 with usage on stdout; an unrecognized flag exits 1 with a message on stderr. Co-Authored-By: Claude Sonnet 5 --- packages/create-loushy-agent/.gitignore | 2 + packages/create-loushy-agent/bin/cli.js | 14 + .../create-loushy-agent/package-lock.json | 2872 +++++++++++++++++ packages/create-loushy-agent/package.json | 29 + packages/create-loushy-agent/src/index.ts | 97 + packages/create-loushy-agent/tsconfig.json | 23 + packages/create-loushy-agent/tsup.config.ts | 19 + 7 files changed, 3056 insertions(+) create mode 100644 packages/create-loushy-agent/.gitignore create mode 100644 packages/create-loushy-agent/bin/cli.js create mode 100644 packages/create-loushy-agent/package-lock.json create mode 100644 packages/create-loushy-agent/package.json create mode 100644 packages/create-loushy-agent/src/index.ts create mode 100644 packages/create-loushy-agent/tsconfig.json create mode 100644 packages/create-loushy-agent/tsup.config.ts 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/index.ts b/packages/create-loushy-agent/src/index.ts new file mode 100644 index 000000000..4c70a27ba --- /dev/null +++ b/packages/create-loushy-agent/src/index.ts @@ -0,0 +1,97 @@ +import { parseArgs } from 'node:util'; + +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; +} + +/** + * 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' }, + }, + 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, + }; +} + +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); +} + +/** + * Placeholder scaffold step, overridden by LOU-H3/LOU-H4 wiring (prompts + + * template generation) added in later commits of this same file. + */ +async function runScaffold(_args: ParsedCliArgs): Promise { + process.stdout.write('create-loushy-agent: nothing to do yet.\n'); + return 0; +} 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'], +}); From ee913007f20f512e8d73a43a01ad1099064036d8 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 21:43:05 +0300 Subject: [PATCH 03/16] LOU-H3: CLI interactive prompt flow Adds packages/create-loushy-agent/src/prompts.ts exporting AnswerConfig {name, provider, tools} and collectAnswers() built on the `prompts` package (text for name, select for provider, multiselect for tools, with http/github as starter tool choices). Wired into main(): a --yes flag, or both --name and --provider given together, skips the interactive prompts entirely and builds AnswerConfig from flags via the same validateAnswers() guard collectAnswers() itself uses, so an invalid --provider value throws naming the bad value and the allowed set. Tested two ways: a direct unit test using prompts' own `prompts.inject()` dependency-injection support (no TTY needed), and a spawned-process integration test driving the real bin/cli.js with --name/--provider/--yes flags and asserting on stdout. Co-Authored-By: Claude Sonnet 5 --- .../src/cli.integration.test.ts | 17 +++++ packages/create-loushy-agent/src/index.ts | 31 +++++++- .../create-loushy-agent/src/prompts.test.ts | 30 ++++++++ packages/create-loushy-agent/src/prompts.ts | 76 +++++++++++++++++++ packages/create-loushy-agent/vitest.config.ts | 8 ++ 5 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 packages/create-loushy-agent/src/cli.integration.test.ts create mode 100644 packages/create-loushy-agent/src/prompts.test.ts create mode 100644 packages/create-loushy-agent/src/prompts.ts create mode 100644 packages/create-loushy-agent/vitest.config.ts 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..bbd66d445 --- /dev/null +++ b/packages/create-loushy-agent/src/cli.integration.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import path from 'node:path'; + +const CLI = path.join(__dirname, '..', 'bin', 'cli.js'); + +describe('cli (flag-based non-interactive shortcut)', () => { + it('scaffolds using --name/--provider without touching the interactive prompts', () => { + const output = execFileSync( + process.execPath, + [CLI, '--name=test-agent', '--provider=anthropic', '--tools=http,github', '--yes'], + { encoding: 'utf8' } + ); + + expect(output).toContain('Scaffolding test-agent (anthropic)'); + }); +}); diff --git a/packages/create-loushy-agent/src/index.ts b/packages/create-loushy-agent/src/index.ts index 4c70a27ba..8f95611f4 100644 --- a/packages/create-loushy-agent/src/index.ts +++ b/packages/create-loushy-agent/src/index.ts @@ -1,4 +1,5 @@ import { parseArgs } from 'node:util'; +import { collectAnswers, validateAnswers, AnswerConfig } from './prompts'; const USAGE = `create-loushy-agent - scaffold a new @loushy/build-ai-agent project @@ -88,10 +89,32 @@ export async function main(argv: string[]): Promise { } /** - * Placeholder scaffold step, overridden by LOU-H3/LOU-H4 wiring (prompts + - * template generation) added in later commits of this same file. + * 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 runScaffold(_args: ParsedCliArgs): Promise { - process.stdout.write('create-loushy-agent: nothing to do yet.\n'); +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 (LOU-H4 wires + * generateProject() in here). + */ +async function runScaffold(args: ParsedCliArgs): Promise { + const answers = await resolveAnswers(args); + process.stdout.write(`Scaffolding ${answers.name} (${answers.provider})...\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/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', + }, +}); From 07ca91fbef7c2e9736cac9f1ddfaba3b511adecf Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 21:47:24 +0300 Subject: [PATCH 04/16] LOU-H4: CLI project template generation Adds packages/create-loushy-agent/src/template.ts: generateProject(dir, answers) writes a generated package.json, tsconfig.json (mirroring the root project's baseline compilerOptions), src/agent.ts (calling createAgent() from LOU-H1 with the chosen provider via resolveProvider() and any wired tools), and a .env.example (LOU-H5, see next commit). The SDK isn't published to the npm registry, so a plain semver dependency spec would not be installable from a project generated outside this worktree. generateProject() instead `npm pack`s the SDK's own current source tree and copies the resulting tarball alongside the generated project, wiring a `file:./` dependency - this is exact-version- pinned (the tarball's version is read straight from the SDK's own package.json) and genuinely installable from anywhere. main() gained --dir/--tools shortcuts so generateProject() is directly testable. Verified end-to-end for real: scaffolds into a fresh os.tmpdir() temp directory (never inside this worktree) with --name=test-agent --provider=openai --yes, then actually runs `npm install && npm run build` inside it (exit 0), and diffs the generated project's installed @loushy/build-ai-agent version against the SDK's own root package.json version, confirming an exact match. Co-Authored-By: Claude Sonnet 5 --- .../src/cli.integration.test.ts | 25 ++- packages/create-loushy-agent/src/index.ts | 16 +- .../src/template.e2e.test.ts | 59 ++++++ packages/create-loushy-agent/src/template.ts | 171 ++++++++++++++++++ 4 files changed, 266 insertions(+), 5 deletions(-) create mode 100644 packages/create-loushy-agent/src/template.e2e.test.ts create mode 100644 packages/create-loushy-agent/src/template.ts diff --git a/packages/create-loushy-agent/src/cli.integration.test.ts b/packages/create-loushy-agent/src/cli.integration.test.ts index bbd66d445..627f70789 100644 --- a/packages/create-loushy-agent/src/cli.integration.test.ts +++ b/packages/create-loushy-agent/src/cli.integration.test.ts @@ -1,17 +1,38 @@ -import { describe, it, expect } from 'vitest'; +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,github', '--yes'], + [ + 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 index 8f95611f4..b7c16d068 100644 --- a/packages/create-loushy-agent/src/index.ts +++ b/packages/create-loushy-agent/src/index.ts @@ -1,5 +1,7 @@ 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 @@ -20,6 +22,7 @@ export interface ParsedCliArgs { name?: string; provider?: string; tools?: string; + dir?: string; } /** @@ -38,6 +41,7 @@ export function parseCliArgs(argv: string[]): ParsedCliArgs { name: { type: 'string' }, provider: { type: 'string' }, tools: { type: 'string' }, + dir: { type: 'string' }, }, allowPositionals: true, strict: true, @@ -60,6 +64,7 @@ export function parseCliArgs(argv: string[]): ParsedCliArgs { name: values.name as string | undefined, provider: values.provider as string | undefined, tools: values.tools as string | undefined, + dir: values.dir as string | undefined, }; } @@ -110,11 +115,16 @@ async function resolveAnswers(args: ParsedCliArgs): Promise { } /** - * Scaffold step: resolve answers, then generate the project (LOU-H4 wires - * generateProject() in here). + * 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); - process.stdout.write(`Scaffolding ${answers.name} (${answers.provider})...\n`); + 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/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.ts b/packages/create-loushy-agent/src/template.ts new file mode 100644 index 000000000..23f3f8da4 --- /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'; +} From 33e541d5bcbdc31e18566673a89255b7717b8273 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 21:47:44 +0300 Subject: [PATCH 05/16] LOU-H5: CLI provider/env-var wiring PROVIDER_ENV_VARS in template.ts (added in the LOU-H4 commit) already reuses LOU-F8's resolveProvider() env table verbatim (OPENAI_API_KEY, ANTHROPIC_API_KEY, OLLAMA_BASE_URL - note: OLLAMA_BASE_URL, not OLLAMA_HOST as the ticket's illustrative snippet assumed; LOU-F8's real table uses OLLAMA_BASE_URL) and generateProject() already writes a .env.example naming the right var. This commit adds the dedicated test: scaffolds three projects (one per provider) into separate temp dirs via the real CLI and greps each .env.example for the exact expected var name. Co-Authored-By: Claude Sonnet 5 --- .../src/template.env.test.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 packages/create-loushy-agent/src/template.env.test.ts 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')); + }); +}); From fafdd40001e2b58dd9639f945e23fbc655d6ca1e Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 21:50:26 +0300 Subject: [PATCH 06/16] LOU-H6: loushy dev: server core Adds src/cli/dev.ts exporting startDevServer(configPath, port = 3737), built on Node's built-in http module. GET /health -> 200 'ok'; POST /chat parses the JSON body and calls the loaded agent's real send() (createAgent from LOU-H1 under the hood, so this goes through the genuine static AgentExecutor.execute() path), returning the ExecutionResult as JSON. Port availability is checked up front via the server's 'error' listener - EADDRINUSE rejects startDevServer() with a message naming the port instead of crashing uncaught. Config loading (deferred decision, judgment call): configPath is a small ad-hoc JSON file ({name, prompt, provider: {type, model}, tools?}) for now. LOU-H9 (declarative agent spec format) is the designed-for successor; dev.ts is retrofitted to use loadSpec()+specToAgent() once that lands (see the LOU-H9 commit) rather than duplicating config-loading logic. Provider resolution reuses LOU-F8's resolveProvider() for real provider types (openai/anthropic/ollama/openrouter). A non-real type (e.g. 'mock', which is intentionally outside resolveProvider()'s env-var whitelist) falls through to LLMProviderRegistry.create() directly - this is what lets tests run the dev server end-to-end against MockLLMProvider without real API credentials. Added a "loushy" bin entry to the ROOT package.json (bin/loushy.js -> dist/cli/dev.js's startDevServer), since the ticket's file path (src/cli/dev.ts under the root project) points at the root package as the natural home, not packages/create-loushy-agent. Verified: server started against a sample JSON config using a LLMProviderRegistry-registered mock provider; POST /chat returns the mock provider's exact response; GET /health returns 200; starting a second server on an already-bound port rejects with a "port N is already in use" error. Co-Authored-By: Claude Sonnet 5 --- bin/loushy.js | 36 +++++++++ package.json | 4 + src/cli/dev.test.ts | 81 +++++++++++++++++++ src/cli/dev.ts | 191 ++++++++++++++++++++++++++++++++++++++++++++ tsup.config.ts | 1 + 5 files changed, 313 insertions(+) create mode 100644 bin/loushy.js create mode 100644 src/cli/dev.test.ts create mode 100644 src/cli/dev.ts diff --git a/bin/loushy.js b/bin/loushy.js new file mode 100644 index 000000000..0edf7440f --- /dev/null +++ b/bin/loushy.js @@ -0,0 +1,36 @@ +#!/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; + + if (!configPath) { + console.error('loushy dev: a config file path is required. Usage: loushy dev [--port N]'); + process.exitCode = 1; + return; + } + + const { startDevServer } = require(path.join(__dirname, '..', 'dist', 'cli', 'dev.js')); + + try { + const handle = await startDevServer(path.resolve(configPath), port); + console.log(`loushy dev: listening on http://localhost:${handle.port}`); + } catch (error) { + console.error(error && error.message ? error.message : String(error)); + process.exitCode = 1; + } +} + +main(); diff --git a/package.json b/package.json index 876cae16c..4d93410e3 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,6 +42,7 @@ }, "files": [ "dist", + "bin", "README.md", "LICENSE", "CHANGELOG.md", diff --git a/src/cli/dev.test.ts b/src/cli/dev.test.ts new file mode 100644 index 000000000..b3aad0ff0 --- /dev/null +++ b/src/cli/dev.test.ts @@ -0,0 +1,81 @@ +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('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 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`) + ); + }); +}); + +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..d035ef6e0 --- /dev/null +++ b/src/cli/dev.ts @@ -0,0 +1,191 @@ +/** + * `loushy dev` - a local dev server for iterating on an agent (LOU-H6). + * + * Serves: + * GET /health -> 200 'ok' + * POST /chat -> { message } in, the agent's real ExecutionResult out + * + * Config loading: configPath is a small JSON file + * { name, prompt, provider: { type, model }, tools?: string[] } describing + * the agent to run. This is intentionally the simplest thing that works - + * LOU-H9 (declarative agent spec files, .yaml/.json + zod validation) is + * the natural, designed-for successor to this format; dev.ts is retrofitted + * to load configs via loadSpec()+specToAgent() once LOU-H9 lands (see that + * commit). + */ +import * as http from 'node:http'; +import * as fs from 'node:fs'; +import { createAgent, SimpleAgent, CreateAgentConfig } from '../createAgent'; +import { resolveProvider } from '../providers/resolveProvider'; +import { LLMProvider, LLMProviderRegistry } from '../providers/llm'; +import { httpTool } from '../tools/built-in/http'; +import { ToolDescriptor } from '../types'; + +const REAL_PROVIDER_TYPES = new Set(['openai', 'anthropic', 'ollama', 'openrouter']); + +/** + * Resolves a dev-server 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, which is how + * tests run the dev server end-to-end against a MockLLMProvider without + * needing real API credentials. + */ +function resolveDevProvider(type: string, model: string): LLMProvider { + if (REAL_PROVIDER_TYPES.has(type.toLowerCase())) { + return resolveProvider(`${type}/${model}`); + } + return LLMProviderRegistry.create(type, { defaultModel: model }); +} + +export interface DevAgentConfig { + name?: string; + prompt: string; + provider: { type: string; model: string }; + tools?: string[]; +} + +const BUILT_IN_TOOLS: Record = { + http: httpTool, +}; + +/** Loads a DevAgentConfig JSON file, guarding the fields configToAgent() needs. */ +export function loadDevConfig(configPath: string): DevAgentConfig { + const raw = fs.readFileSync(configPath, 'utf8'); + const config = JSON.parse(raw) as DevAgentConfig; + + if (!config.prompt) { + throw new Error(`loadDevConfig: '${configPath}' is missing required field 'prompt'`); + } + if (!config.provider || !config.provider.type) { + throw new Error(`loadDevConfig: '${configPath}' is missing required field 'provider.type'`); + } + + return config; +} + +export function configToAgent(config: DevAgentConfig): SimpleAgent { + const provider = resolveDevProvider(config.provider.type, config.provider.model); + + const tools: CreateAgentConfig['tools'] = {}; + for (const toolName of config.tools || []) { + const tool = BUILT_IN_TOOLS[toolName]; + if (tool) { + tools[toolName] = tool; + } + } + + return createAgent({ + name: config.name, + prompt: config.prompt, + provider, + tools: Object.keys(tools).length > 0 ? tools : undefined, + }); +} + +function loadAgentFromConfig(configPath: string): SimpleAgent { + return configToAgent(loadDevConfig(configPath)); +} + +export interface DevServerHandle { + server: http.Server; + port: number; + close: () => Promise; +} + +function readBody(req: http.IncomingMessage): Promise { + return new Promise((resolve, reject) => { + let body = ''; + req.on('data', (chunk) => { + body += chunk; + }); + req.on('end', () => resolve(body)); + req.on('error', reject); + }); +} + +async function handleRequest( + req: http.IncomingMessage, + res: http.ServerResponse, + agent: SimpleAgent +): Promise { + if (req.method === 'GET' && req.url === '/health') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('ok'); + 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 agent.send(message); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(result)); + } catch (error) { + 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 `port` (checked for availability up + * front - EADDRINUSE is caught and rejected with a clear, port-naming + * error rather than crashing uncaught). + */ +export async function startDevServer( + configPath: string, + port = 3737 +): Promise { + const agent = loadAgentFromConfig(configPath); + + const server = http.createServer((req, res) => { + handleRequest(req, res, agent).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); + }); + + return { + server, + port, + close: () => + new Promise((resolve, reject) => { + server.close((err) => (err ? reject(err) : resolve())); + }), + }; +} diff --git a/tsup.config.ts b/tsup.config.ts index ccd9787cc..0e65c5b31 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -9,6 +9,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, From c573ae7ce2bd65fb9d4414c84b8c8a13bf2923f5 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 21:51:22 +0300 Subject: [PATCH 07/16] LOU-H7: loushy dev: minimal chat UI Adds src/cli/dev-ui/index.html: an input#msg, button#send and div#log, with an inline script that fetch()es POST /chat and appends the reply to the log div with no page reload. Wired a GET / route in dev.ts serving this file with Content-Type: text/html. Static HTML isn't something tsup bundles, so tsup.config.ts gained an onSuccess hook that copies src/cli/dev-ui/ to dist/cli/dev-ui/ after each build, matching where dev.ts's path.join(__dirname, 'dev-ui', ...) looks for it at runtime. Verified: GET / returns HTML containing id="msg"/id="send"/id="log"; the page's own inline script targets fetch('/chat'); driving that exact same POST /chat call (the realistic message round trip the page performs) returns the mock provider's reply. Co-Authored-By: Claude Sonnet 5 --- src/cli/dev-ui/index.html | 66 +++++++++++++++++++++++++++++++++++++++ src/cli/dev.test.ts | 34 ++++++++++++++++++++ src/cli/dev.ts | 18 +++++++++++ tsup.config.ts | 20 ++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 src/cli/dev-ui/index.html 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 index b3aad0ff0..1215532b3 100644 --- a/src/cli/dev.test.ts +++ b/src/cli/dev.test.ts @@ -37,6 +37,40 @@ afterEach(async () => { }); 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); diff --git a/src/cli/dev.ts b/src/cli/dev.ts index d035ef6e0..cf53819ca 100644 --- a/src/cli/dev.ts +++ b/src/cli/dev.ts @@ -15,6 +15,7 @@ */ import * as http from 'node:http'; import * as fs from 'node:fs'; +import * as path from 'node:path'; import { createAgent, SimpleAgent, CreateAgentConfig } from '../createAgent'; import { resolveProvider } from '../providers/resolveProvider'; import { LLMProvider, LLMProviderRegistry } from '../providers/llm'; @@ -105,6 +106,13 @@ function readBody(req: http.IncomingMessage): Promise { }); } +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); +} + async function handleRequest( req: http.IncomingMessage, res: http.ServerResponse, @@ -116,6 +124,16 @@ async function handleRequest( 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); diff --git a/tsup.config.ts b/tsup.config.ts index 0e65c5b31..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: { @@ -19,4 +36,7 @@ export default defineConfig({ treeshake: true, minify: false, external: ['ai', 'zod'], + onSuccess: async () => { + copyDevUi(); + }, }); From 04bcfd120e1dcf569d20fc3e9dd693bfc84c2e7f Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 21:52:00 +0300 Subject: [PATCH 08/16] LOU-H8: loushy dev: hot reload on config change dev.ts now watches configPath via Node's built-in fs.watch. The current agent lives in a mutable top-level holder ({agent}) so /chat always reads the latest reloaded version without restarting the HTTP server or dropping the listening port. On a reload failure (e.g. invalid JSON, or a valid-JSON-but-missing-required-field edit), the error is logged and the previous working agent is kept in the holder rather than crashing. Verified: started the dev server, sent one message and recorded the mock response, edited the config file's prompt (and swapped the registered mock provider's response) on disk, waited 400ms for the fs.watch event, sent a second message and confirmed it reflects the new config while `handle.server` and the port are unchanged (no restart) - then wrote an invalid edit (missing 'prompt') and confirmed the server keeps serving the last-good config instead of crashing. Co-Authored-By: Claude Sonnet 5 --- src/cli/dev.test.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++ src/cli/dev.ts | 39 ++++++++++++++++++++++++--- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/src/cli/dev.test.ts b/src/cli/dev.test.ts index 1215532b3..eef1ef3fe 100644 --- a/src/cli/dev.test.ts +++ b/src/cli/dev.test.ts @@ -108,6 +108,70 @@ describe('startDevServer', () => { }); }); +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; diff --git a/src/cli/dev.ts b/src/cli/dev.ts index cf53819ca..a406a6f38 100644 --- a/src/cli/dev.ts +++ b/src/cli/dev.ts @@ -113,10 +113,19 @@ function serveChatUi(res: http.ServerResponse): void { 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, - agent: SimpleAgent + holder: AgentHolder ): Promise { if (req.method === 'GET' && req.url === '/health') { res.writeHead(200, { 'Content-Type': 'text/plain' }); @@ -144,7 +153,7 @@ async function handleRequest( return; } - const result = await agent.send(message); + const result = await holder.agent.send(message); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(result)); } catch (error) { @@ -162,15 +171,36 @@ async function handleRequest( * Starts the dev server, binding to `port` (checked for availability up * front - EADDRINUSE is caught and rejected with a clear, port-naming * error rather than crashing uncaught). + * + * 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 ): Promise { - const agent = loadAgentFromConfig(configPath); + 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, agent).catch((error) => { + handleRequest(req, res, holder).catch((error) => { // eslint-disable-next-line no-console console.error('[loushy dev] unhandled request error:', error); if (!res.headersSent) { @@ -203,6 +233,7 @@ export async function startDevServer( port, close: () => new Promise((resolve, reject) => { + watcher.close(); server.close((err) => (err ? reject(err) : resolve())); }), }; From 38be9b83dec70c8eabd3748282f42e449c036560 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 21:54:26 +0300 Subject: [PATCH 09/16] LOU-H9: Declarative agent spec file format Adds src/spec/: schema.ts (AgentSpec {name, prompt, provider: {type, model}, tools?} plus a zod runtime schema mirroring it), loadSpec.ts (loads .yaml/.yml via the new `yaml` dependency, or .json via JSON.parse, by extension; validates via the zod schema and throws an Error naming each invalid/missing field, e.g. "'prompt': Required"), and specToAgent.ts (maps a validated spec to a live agent via createAgent() from LOU-H1, resolving `tools` names against the SDK's real built-in tools - http/current-date/day-name resolve directly; 'github'/'jira' throw a clear, actionable error instead of silently registering a broken tool, since GitHubTools/JiraTools are credentialed factories with no spec field to source token/owner/repo or baseUrl/email/apiToken from). Retrofit (LOU-H6 integration): dev.ts now loads configs via loadSpec()+specToAgent() instead of its original ad-hoc JSON loader - the natural integration point the epic calls out. The old shape ({name, prompt, provider, tools}) is a valid AgentSpec as-is, so every LOU-H6/H7/H8 test config and test still passes unmodified. Provider resolution (real types through LOU-F8's resolveProvider(), 'mock' and any other LLMProviderRegistry-registered type via the registry directly) moved from dev.ts into specToAgent.ts as resolveSpecProvider(), now the single canonical implementation. Tested: an authored .yaml spec and an equivalent createAgent()-built agent (same prompt, same scripted MockLLMProvider) produce identical text, finishReason and toolCalls for the same input; a spec missing 'prompt' throws naming that field; an unrecognized tool name and a credentialed tool (github/jira) with no config each throw a distinct, clear error. Co-Authored-By: Claude Sonnet 5 --- package-lock.json | 21 ++++++++- package.json | 3 +- src/cli/dev.ts | 89 ++++++------------------------------ src/index.ts | 3 ++ src/spec/index.ts | 3 ++ src/spec/loadSpec.test.ts | 70 ++++++++++++++++++++++++++++ src/spec/loadSpec.ts | 45 ++++++++++++++++++ src/spec/schema.ts | 42 +++++++++++++++++ src/spec/specToAgent.test.ts | 73 +++++++++++++++++++++++++++++ src/spec/specToAgent.ts | 84 ++++++++++++++++++++++++++++++++++ 10 files changed, 355 insertions(+), 78 deletions(-) create mode 100644 src/spec/index.ts create mode 100644 src/spec/loadSpec.test.ts create mode 100644 src/spec/loadSpec.ts create mode 100644 src/spec/schema.ts create mode 100644 src/spec/specToAgent.test.ts create mode 100644 src/spec/specToAgent.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 4d93410e3..4c74523da 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,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/src/cli/dev.ts b/src/cli/dev.ts index a406a6f38..fe09715a3 100644 --- a/src/cli/dev.ts +++ b/src/cli/dev.ts @@ -3,90 +3,27 @@ * * Serves: * GET /health -> 200 'ok' + * GET / -> the minimal chat UI (LOU-H7) * POST /chat -> { message } in, the agent's real ExecutionResult out * - * Config loading: configPath is a small JSON file - * { name, prompt, provider: { type, model }, tools?: string[] } describing - * the agent to run. This is intentionally the simplest thing that works - - * LOU-H9 (declarative agent spec files, .yaml/.json + zod validation) is - * the natural, designed-for successor to this format; dev.ts is retrofitted - * to load configs via loadSpec()+specToAgent() once LOU-H9 lands (see that - * commit). + * 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 { createAgent, SimpleAgent, CreateAgentConfig } from '../createAgent'; -import { resolveProvider } from '../providers/resolveProvider'; -import { LLMProvider, LLMProviderRegistry } from '../providers/llm'; -import { httpTool } from '../tools/built-in/http'; -import { ToolDescriptor } from '../types'; - -const REAL_PROVIDER_TYPES = new Set(['openai', 'anthropic', 'ollama', 'openrouter']); - -/** - * Resolves a dev-server 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, which is how - * tests run the dev server end-to-end against a MockLLMProvider without - * needing real API credentials. - */ -function resolveDevProvider(type: string, model: string): LLMProvider { - if (REAL_PROVIDER_TYPES.has(type.toLowerCase())) { - return resolveProvider(`${type}/${model}`); - } - return LLMProviderRegistry.create(type, { defaultModel: model }); -} - -export interface DevAgentConfig { - name?: string; - prompt: string; - provider: { type: string; model: string }; - tools?: string[]; -} - -const BUILT_IN_TOOLS: Record = { - http: httpTool, -}; - -/** Loads a DevAgentConfig JSON file, guarding the fields configToAgent() needs. */ -export function loadDevConfig(configPath: string): DevAgentConfig { - const raw = fs.readFileSync(configPath, 'utf8'); - const config = JSON.parse(raw) as DevAgentConfig; - - if (!config.prompt) { - throw new Error(`loadDevConfig: '${configPath}' is missing required field 'prompt'`); - } - if (!config.provider || !config.provider.type) { - throw new Error(`loadDevConfig: '${configPath}' is missing required field 'provider.type'`); - } - - return config; -} - -export function configToAgent(config: DevAgentConfig): SimpleAgent { - const provider = resolveDevProvider(config.provider.type, config.provider.model); - - const tools: CreateAgentConfig['tools'] = {}; - for (const toolName of config.tools || []) { - const tool = BUILT_IN_TOOLS[toolName]; - if (tool) { - tools[toolName] = tool; - } - } - - return createAgent({ - name: config.name, - prompt: config.prompt, - provider, - tools: Object.keys(tools).length > 0 ? tools : undefined, - }); -} +import { SimpleAgent } from '../createAgent'; +import { loadSpec } from '../spec/loadSpec'; +import { specToAgent } from '../spec/specToAgent'; function loadAgentFromConfig(configPath: string): SimpleAgent { - return configToAgent(loadDevConfig(configPath)); + return specToAgent(loadSpec(configPath)); } export interface DevServerHandle { diff --git a/src/index.ts b/src/index.ts index 4299846e2..653fd50c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,3 +49,6 @@ 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/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..21d92c40a --- /dev/null +++ b/src/spec/specToAgent.ts @@ -0,0 +1,84 @@ +import { AgentSpec } from './schema'; +import { createAgent, SimpleAgent, CreateAgentConfig } from '../createAgent'; +import { resolveProvider } from '../providers/resolveProvider'; +import { LLMProvider, LLMProviderRegistry } from '../providers/llm'; +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, + }); +} From 9a12200503e8d17f7448a5a2054a14cbbd10a8f4 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 21:56:01 +0300 Subject: [PATCH 10/16] LOU-H10: Author starter template agents Adds 5 example agents under examples/ - support-bot, research-assistant, workflow-router, doc-qa, slack-notifier - each with index.ts (built on createAgent() from LOU-H1), a minimal package.json, and a README.md. Every index.ts defaults to a free MockLLMProvider (from src/providers/mock.ts) so it runs with zero setup and zero API cost, and only switches to a real provider (via resolveProvider(), LOU-F8) when the relevant env var is already set (OPENAI_API_KEY for support-bot/ workflow-router/slack-notifier, ANTHROPIC_API_KEY for research-assistant, OLLAMA_BASE_URL for doc-qa) - spreading coverage across all three real provider types rather than defaulting every example to the same one. research-assistant also wires up the built-in http tool. Added root package.json scripts (example:support-bot, etc., matching the existing example:tracing:* / tsx convention from LOU-E6) and verified each one runs to completion with exit 0 against its default mock provider. Co-Authored-By: Claude Sonnet 5 --- examples/doc-qa/README.md | 11 ++++++ examples/doc-qa/index.ts | 43 ++++++++++++++++++++++++ examples/doc-qa/package.json | 9 +++++ examples/research-assistant/README.md | 11 ++++++ examples/research-assistant/index.ts | 40 ++++++++++++++++++++++ examples/research-assistant/package.json | 9 +++++ examples/slack-notifier/README.md | 11 ++++++ examples/slack-notifier/index.ts | 36 ++++++++++++++++++++ examples/slack-notifier/package.json | 9 +++++ examples/support-bot/README.md | 12 +++++++ examples/support-bot/index.ts | 38 +++++++++++++++++++++ examples/support-bot/package.json | 9 +++++ examples/workflow-router/README.md | 11 ++++++ examples/workflow-router/index.ts | 34 +++++++++++++++++++ examples/workflow-router/package.json | 9 +++++ package.json | 5 +++ 16 files changed, 297 insertions(+) create mode 100644 examples/doc-qa/README.md create mode 100644 examples/doc-qa/index.ts create mode 100644 examples/doc-qa/package.json create mode 100644 examples/research-assistant/README.md create mode 100644 examples/research-assistant/index.ts create mode 100644 examples/research-assistant/package.json create mode 100644 examples/slack-notifier/README.md create mode 100644 examples/slack-notifier/index.ts create mode 100644 examples/slack-notifier/package.json create mode 100644 examples/support-bot/README.md create mode 100644 examples/support-bot/index.ts create mode 100644 examples/support-bot/package.json create mode 100644 examples/workflow-router/README.md create mode 100644 examples/workflow-router/index.ts create mode 100644 examples/workflow-router/package.json 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.json b/package.json index 4c74523da..85fe6507d 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,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": [ From 294c4764389fd7b28b9d25fe83eb6b6d550e1804 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 21:56:30 +0300 Subject: [PATCH 11/16] LOU-H11: Template gallery index/docs page Adds examples/README.md with one `## [dirname](./dirname)` section per example directory - the 5 new LOU-H10 examples (doc-qa, research- assistant, slack-notifier, support-bot, workflow-router) plus the pre-existing tracing/ (LOU-E6) - each with a one-line description. Deviation: OpenRouterProvider.examples.ts (LOU-B6) is a FILE directly under examples/, not a directory (the ticket's phrasing assumed it had been "relocated" into its own directory; it wasn't). It's called out in a short note at the bottom of the README instead of a `## [dirname]` heading, since the verification is specifically a directory-listing diff and a heading for a non-directory would break that invariant. Verified via examples/README.test.ts: extracts every `## [x](./x)` heading's path and diffs it against the actual directory listing under examples/, asserting an exact match (no stale, no missing). Co-Authored-By: Claude Sonnet 5 --- examples/README.md | 34 ++++++++++++++++++++++++++++++++++ examples/README.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/README.test.ts 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); + }); +}); From 74e402fcbcf2d0ffaf290473b91f071125163066 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 21:57:50 +0300 Subject: [PATCH 12/16] LOU-H12: Guiding runtime error messages AgentExecutor.execute() gained validateExecuteOptions() - since AgentExecutor is a static, instance-free API, execute() is the one entry point where a missing required option can be caught up front, before it fails deep inside runAgentLoop() with a generic "Cannot read properties of undefined" TypeError (e.g. `provider.generate(...)` when `provider` was never checked). Guards `provider`, `agent`, and `input` (all three are non-optional on ExecuteOptions but were previously undereferenced until used), each throwing a message naming the missing field plus a corrective one-line snippet. AgentBuilder's existing validateAgentConfig() (src/agent-types/validators.ts) already guarded name/agentType with clear messages ("Agent name is required" / "Agent type is required") - this commit adds a corrective one-line snippet to each, without changing the existing message text (both LOU-H12's new test and the pre-existing AgentBuilder.test.ts assertions use substring matching, so nothing else needed updating). Searched both files for other required-field dereferences lacking a guard; none found beyond the above (tools/flows/settings/etc. are all genuinely optional with safe defaults already in place). Tested: omitting provider/agent/input from AgentExecutor.execute() each throws naming that field with "Example: ..." corrective text; building an agent with AgentBuilder.create().build() (no name, no type) throws both "Agent name is required. Example: ..." and "Agent type is required. Example: ...". Co-Authored-By: Claude Sonnet 5 --- src/agent-types/validators.ts | 8 ++++-- src/core/AgentBuilder.test.ts | 6 +++++ src/execution/AgentExecutor.test.ts | 39 +++++++++++++++++++++++++++++ src/execution/AgentExecutor.ts | 37 +++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) 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/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/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 */ From 4ea14787531ad7bb1c7f23852dc68c6605b34a4a Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 22:15:50 +0300 Subject: [PATCH 13/16] LOU-H fix: register mock provider centrally so loushy dev and examples work outside the test suite QA found that `node bin/loushy.js dev ` failed with "Provider 'mock' not found" - LLMProviderRegistry only ever learned about 'mock' inside test files (beforeAll/beforeEach), never in any production code path. - src/providers/index.ts: register 'mock' -> MockLLMProvider alongside the real providers, using the same LLMProviderRegistry.register() call signature already used for openai/ollama/openrouter/anthropic. - src/providers/mock.ts: also self-register 'mock' at module load. This is the registration that actually matters at runtime: mock.ts has zero external dependencies, so it's always safe to bundle eagerly, unlike providers/index.ts's barrel, which also statically imports OpenAIProvider/OllamaProvider/OpenRouterProvider - each of which imports its optional peer-dependency SDK ('@ai-sdk/openai', 'ollama-ai-provider', ...) at module scope. Forcing the whole barrel to survive bundling (tried first) crashed `loushy dev` with "Cannot find module '@ai-sdk/openai'" for users who never installed it, defeating the zero-API-key DX goal. Re-registering 'mock' is safe either way: LLMProviderRegistry.register() is a plain Map.set(), so it never throws on re-registration. - src/spec/specToAgent.ts: side-effect import '../providers/mock' (not the whole barrel) so CLI bundles like dist/cli/dev.js - whose dependency graph otherwise only ever touches provider *types*, never providers as values - actually run this registration. - package.json: scope "sideEffects" to src/providers/mock.ts so esbuild (tsup) doesn't tree-shake the registration call out of dist builds; all other modules stay tree-shakeable as before. Verified: `node bin/loushy.js dev ` now boots for real and correctly serves /health and /chat. npx vitest run / npx tsc --noEmit pass with the same pre-existing baseline in both the root package (641 passed/2 skipped, 3 known pre-existing provider-suite failures) and packages/create-loushy-agent (8/8). LOU-H10 starter templates (examples/support-bot etc.) still run correctly via tsx. Co-Authored-By: Claude Sonnet 5 --- package.json | 5 ++++- src/providers/index.ts | 8 ++++++++ src/providers/mock.ts | 28 ++++++++++++++++++++++++++++ src/spec/specToAgent.ts | 18 ++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 85fe6507d..9dc5f095d 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,10 @@ "CHANGELOG.md", "CONTRIBUTING.md" ], - "sideEffects": false, + "sideEffects": [ + "./src/providers/mock.ts", + "./src/providers/mock.js" + ], "scripts": { "build": "tsup", "dev": "tsup --watch", 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/specToAgent.ts b/src/spec/specToAgent.ts index 21d92c40a..6fe9369c8 100644 --- a/src/spec/specToAgent.ts +++ b/src/spec/specToAgent.ts @@ -2,6 +2,24 @@ 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'; From a4c7204d518cfa568fd46b3ab8cc5e18c9d04d3c Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 22:20:32 +0300 Subject: [PATCH 14/16] LOU-H fix: bind dev server to localhost by default, add --host opt-in server.listen(port) with no host argument bound to 0.0.0.0 (all interfaces), making the local dev tool reachable over the network by default. Default startDevServer's host param to 127.0.0.1 and wire a matching --host CLI flag (following bin/loushy.js's existing manual flag-parsing pattern) for developers who want LAN access, e.g. testing from a phone. Adds a test asserting the server binds to 127.0.0.1 by default. Co-Authored-By: Claude Sonnet 5 --- bin/loushy.js | 8 +++++--- src/cli/dev.test.ts | 9 +++++++++ src/cli/dev.ts | 14 ++++++++++---- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/bin/loushy.js b/bin/loushy.js index 0edf7440f..487994ef3 100644 --- a/bin/loushy.js +++ b/bin/loushy.js @@ -15,9 +15,11 @@ async function main() { 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]'); + console.error('loushy dev: a config file path is required. Usage: loushy dev [--port N] [--host H]'); process.exitCode = 1; return; } @@ -25,8 +27,8 @@ async function main() { const { startDevServer } = require(path.join(__dirname, '..', 'dist', 'cli', 'dev.js')); try { - const handle = await startDevServer(path.resolve(configPath), port); - console.log(`loushy dev: listening on http://localhost:${handle.port}`); + 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; diff --git a/src/cli/dev.test.ts b/src/cli/dev.test.ts index eef1ef3fe..a6ca95c08 100644 --- a/src/cli/dev.test.ts +++ b/src/cli/dev.test.ts @@ -96,6 +96,15 @@ describe('startDevServer', () => { expect(json.text).toBe(MOCK_RESPONSE); }); + 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); diff --git a/src/cli/dev.ts b/src/cli/dev.ts index fe09715a3..cfb713f3a 100644 --- a/src/cli/dev.ts +++ b/src/cli/dev.ts @@ -105,10 +105,15 @@ async function handleRequest( } /** - * Starts the dev server, binding to `port` (checked for availability up - * front - EADDRINUSE is caught and rejected with a clear, port-naming + * 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), @@ -117,7 +122,8 @@ async function handleRequest( */ export async function startDevServer( configPath: string, - port = 3737 + port = 3737, + host = '127.0.0.1' ): Promise { const holder: AgentHolder = { agent: loadAgentFromConfig(configPath) }; @@ -162,7 +168,7 @@ export async function startDevServer( }; server.once('error', onError); server.once('listening', onListening); - server.listen(port); + server.listen(port, host); }); return { From af903119595822a1a58ae276c9ae6dddf2f0c618 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 22:21:47 +0300 Subject: [PATCH 15/16] LOU-H fix: cap /chat request body accumulation at 1MB readBody() accumulated request body chunks indefinitely with no size limit; combined with the dev server being network-reachable, an oversized POST could exhaust memory on a long-running dev server. Cap accumulation at 1MB (common Node.js body-size-limit convention): once exceeded, stop growing the buffered body (still draining the stream to avoid stalling the connection) and respond 413 Payload Too Large. Co-Authored-By: Claude Sonnet 5 --- src/cli/dev.test.ts | 17 +++++++++++++++++ src/cli/dev.ts | 29 ++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/cli/dev.test.ts b/src/cli/dev.test.ts index a6ca95c08..b8cee4260 100644 --- a/src/cli/dev.test.ts +++ b/src/cli/dev.test.ts @@ -96,6 +96,23 @@ describe('startDevServer', () => { 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); diff --git a/src/cli/dev.ts b/src/cli/dev.ts index cfb713f3a..4d6a10faa 100644 --- a/src/cli/dev.ts +++ b/src/cli/dev.ts @@ -32,13 +32,35 @@ export interface DevServerHandle { 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', () => resolve(body)); + req.on('end', () => { + if (tooLarge) { + reject(new PayloadTooLargeError(`Request body exceeds ${MAX_BODY_BYTES} byte limit`)); + return; + } + resolve(body); + }); req.on('error', reject); }); } @@ -94,6 +116,11 @@ async function handleRequest( 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 })); } From bdb83b6ddeb1c50c0c44833422c10947b076fb0e Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 28 Sep 2026 22:22:32 +0300 Subject: [PATCH 16/16] LOU-H fix: stop double-quoting npm pack's --pack-destination arg execFileSync's shell: true already shell-quotes each array argument on Windows, so manually wrapping dir in literal quotes double-quoted the path. Pass dir unwrapped and let shell: true handle quoting. Co-Authored-By: Claude Sonnet 5 --- packages/create-loushy-agent/src/template.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-loushy-agent/src/template.ts b/packages/create-loushy-agent/src/template.ts index 23f3f8da4..2e4ffe7da 100644 --- a/packages/create-loushy-agent/src/template.ts +++ b/packages/create-loushy-agent/src/template.ts @@ -86,7 +86,7 @@ export async function generateProject(dir: string, answers: AnswerConfig): Promi // 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}"`], { + const packOutput = execFileSync(npmCmd, ['pack', '--pack-destination', dir], { cwd: sdkRoot, encoding: 'utf8', shell: true,