diff --git a/.gitignore b/.gitignore index c7f75d0..7f9f69e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ dist-test/ # Test output test-results/ + +# VitePress dev cache (build output goes to .vitepress/dist, covered by dist/) +docs/.vitepress/cache/ diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts new file mode 100644 index 0000000..5b37c85 --- /dev/null +++ b/docs/.vitepress/config.ts @@ -0,0 +1,107 @@ +import { defineConfig } from 'vitepress' + +export default defineConfig({ + title: 'GemStack', + description: 'Framework-agnostic tools for building AI applications in Node.', + lang: 'en-US', + ignoreDeadLinks: 'localhostLinks', + + head: [ + ['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }], + ['meta', { name: 'theme-color', content: '#10b981' }], + ], + + themeConfig: { + logo: '/logo.svg', + siteTitle: 'GemStack', + + nav: [ + { text: 'Guide', link: '/guide/', activeMatch: '/guide/' }, + { text: 'Packages', link: '/packages/', activeMatch: '/packages/' }, + { + text: 'Reference', + items: [ + { text: 'Changelog', link: 'https://github.com/gemstack-land/gemstack/releases' }, + { text: 'npm — @gemstack', link: 'https://www.npmjs.com/org/gemstack' }, + ], + }, + ], + + sidebar: { + '/guide/': [ + { + text: 'Introduction', + items: [ + { text: 'What is GemStack?', link: '/guide/' }, + { text: 'Installation', link: '/guide/installation' }, + { text: 'Your First Agent', link: '/guide/first-agent' }, + ], + }, + { + text: 'Packages', + items: [ + { text: 'Overview', link: '/packages/' }, + { text: 'ai-sdk', link: '/packages/ai-sdk/' }, + { text: 'ai-skills', link: '/packages/ai-skills' }, + { text: 'ai-autopilot', link: '/packages/ai-autopilot' }, + { text: 'ai-mcp', link: '/packages/ai-mcp' }, + { text: 'mcp', link: '/packages/mcp' }, + ], + }, + ], + + '/packages/': [ + { + text: 'Overview', + items: [{ text: 'The GemStack family', link: '/packages/' }], + }, + { + text: 'ai-sdk — the agent runtime', + items: [ + { text: 'Overview', link: '/packages/ai-sdk/' }, + { text: 'Agents', link: '/packages/ai-sdk/agents' }, + { text: 'Tools', link: '/packages/ai-sdk/tools' }, + { text: 'Streaming', link: '/packages/ai-sdk/streaming' }, + { text: 'Structured Output', link: '/packages/ai-sdk/structured-output' }, + { text: 'Memory & Persistence', link: '/packages/ai-sdk/memory' }, + { text: 'Vector Stores & RAG', link: '/packages/ai-sdk/rag' }, + { text: 'Providers', link: '/packages/ai-sdk/providers' }, + { text: 'Testing & Evals', link: '/packages/ai-sdk/testing' }, + ], + }, + { + text: 'The family', + items: [ + { text: 'ai-skills', link: '/packages/ai-skills' }, + { text: 'ai-autopilot', link: '/packages/ai-autopilot' }, + { text: 'ai-mcp', link: '/packages/ai-mcp' }, + { text: 'mcp', link: '/packages/mcp' }, + ], + }, + ], + }, + + search: { + provider: 'local', + }, + + socialLinks: [ + { icon: 'github', link: 'https://github.com/gemstack-land/gemstack' }, + ], + + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2026-present GemStack contributors', + }, + + editLink: { + pattern: 'https://github.com/gemstack-land/gemstack/edit/main/docs/:path', + text: 'Edit this page on GitHub', + }, + }, + + markdown: { + theme: { light: 'github-light', dark: 'github-dark' }, + lineNumbers: true, + }, +}) diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css new file mode 100644 index 0000000..9e25ccb --- /dev/null +++ b/docs/.vitepress/theme/custom.css @@ -0,0 +1,25 @@ +:root { + /* GemStack brand — emerald gem */ + --vp-c-brand-1: #0d9488; + --vp-c-brand-2: #10b981; + --vp-c-brand-3: #14b8a6; + --vp-c-brand-soft: rgba(16, 185, 129, 0.14); +} + +.dark { + --vp-c-brand-1: #2dd4bf; + --vp-c-brand-2: #14b8a6; + --vp-c-brand-3: #5eead4; + --vp-c-brand-soft: rgba(16, 185, 129, 0.16); +} + +.VPHero .name .clip { + background: linear-gradient(120deg, #10b981 10%, #06b6d4 90%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.vp-code-group .tabs { + border-bottom-color: var(--vp-c-brand-soft); +} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 0000000..d360e0b --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,9 @@ +import DefaultTheme from 'vitepress/theme' +import './custom.css' +import type { Theme } from 'vitepress' + +const theme: Theme = { + extends: DefaultTheme, +} + +export default theme diff --git a/docs/guide/first-agent.md b/docs/guide/first-agent.md new file mode 100644 index 0000000..a3ed8d8 --- /dev/null +++ b/docs/guide/first-agent.md @@ -0,0 +1,87 @@ +# Your First Agent + +This walkthrough assumes you have [installed](/guide/installation) `@gemstack/ai-sdk` and registered a provider. + +## A one-line prompt + +```ts +import { agent } from '@gemstack/ai-sdk' + +const response = await agent('You are a helpful assistant.') + .prompt('Summarize the transformer architecture in one sentence.') + +console.log(response.text) +``` + +`agent(instructions)` returns an agent; `.prompt(input)` runs it and resolves to an `AgentResponse` with `.text`, `.steps`, `.usage`, and more. + +## Three agent shapes + +Pick whichever reads best at the call site: + +```ts +import { agent, AI, Agent, stepCountIs } from '@gemstack/ai-sdk' + +// Inline, one-off +const r1 = await agent('You summarize text.').prompt('Summarize this...') + +// Facade with the default model +const r2 = await AI.prompt('Hello world') + +// Configured anonymous agent - tools + options together +const r3 = await agent({ + instructions: 'You help find users.', + model: 'anthropic/claude-sonnet-4-6', + tools: [searchTool], +}).prompt('Find all admins') + +// Reusable typed class +class SearchAgent extends Agent { + instructions() { return 'You help find users.' } + model() { return 'anthropic/claude-sonnet-4-6' } + tools() { return [searchTool] } + stopWhen() { return stepCountIs(5) } +} +const r4 = await new SearchAgent().prompt('Find all admins') +``` + +A class is the right shape once an agent has tools, a fixed model, middleware, or memory - everything lives in one place and the type is reusable. + +## Giving the agent a tool + +Tools let the agent call your code. Define one with `toolDefinition(...)`, declare its input with Zod, and attach a `.server()` handler: + +```ts +import { agent, toolDefinition } from '@gemstack/ai-sdk' +import { z } from 'zod' + +const searchTool = toolDefinition({ + name: 'search_users', + description: 'Search users by name or email', + inputSchema: z.object({ + query: z.string().describe('Name or email substring'), + limit: z.number().int().min(1).max(50).default(10), + }), +}).server(async ({ query, limit }) => { + return await db.users.search(query, limit) +}) + +const response = await agent({ + instructions: 'You help find users.', + tools: [searchTool], +}).prompt('Find all admins') +``` + +The agent decides when to call the tool, validates the arguments against `inputSchema` before your handler runs, and feeds the result back to the model. Inspect `response.steps` for the full trace. + +## Where to go next + +| You want to… | Read | +|---|---| +| Understand the agent loop, sub-agents, multi-step runs | [Agents](/packages/ai-sdk/agents) | +| Go deeper on tools, scoped tools, client tools, approval gates | [Tools](/packages/ai-sdk/tools) | +| Stream tokens and tool progress to a UI | [Streaming](/packages/ai-sdk/streaming) | +| Get typed objects back instead of text | [Structured Output](/packages/ai-sdk/structured-output) | +| Persist conversations and give the agent memory | [Memory & Persistence](/packages/ai-sdk/memory) | +| Retrieval-augmented generation over your documents | [Vector Stores & RAG](/packages/ai-sdk/rag) | +| Test agents without hitting a real model | [Testing & Evals](/packages/ai-sdk/testing) | diff --git a/docs/guide/index.md b/docs/guide/index.md new file mode 100644 index 0000000..3f516f1 --- /dev/null +++ b/docs/guide/index.md @@ -0,0 +1,46 @@ +# What is GemStack? + +GemStack is a collection of high-quality, framework-agnostic tools for building AI applications in Node. Each tool is a standalone, well-tested package that works in any Node app and composes cleanly with the others. + +It is shared, community-governed infrastructure built in the open with the [Vike](https://vike.dev) team. Packages join GemStack by *graduating* one at a time - when they prove framework-agnostic value - not by bulk-moving a framework's package set in. + +## The family + +All packages publish under the **`@gemstack/`** scope. + +| Package | What it is | +|---|---| +| [`ai-sdk`](/packages/ai-sdk/) | The agent runtime: providers, the agent loop, tools, streaming, middleware, structured output, memory, and evals. The engine the rest of the family builds on. | +| [`ai-skills`](/packages/ai-skills) | Portable capability bundles: load `SKILL.md` skills (instructions + tools + resources) and compose them onto an agent on demand. | +| [`ai-autopilot`](/packages/ai-autopilot) | Orchestration: a Supervisor that plans, dispatches subagents (bounded concurrency + budget guardrails), and synthesizes the result. | +| [`ai-mcp`](/packages/ai-mcp) | The agent/MCP bridge: consume a remote MCP server's tools as agent tools, and expose an agent as an MCP server. | +| [`mcp`](/packages/mcp) | A standalone framework for *authoring* MCP servers: tools, resources, prompts, decorators, OAuth 2.1, a framework-neutral HTTP handler, and a test client. Agent-agnostic. | + +## How they fit together + +``` +ai-sdk agent runtime (the "verbs") +ai-skills capability bundles (the composable "nouns") -> ai-sdk +ai-autopilot orchestration / autonomy (the "director") -> ai-sdk (+ skills) +ai-mcp agent <-> MCP bridge (the "adapter") -> ai-sdk +----------------------------------------------------------------------------------- +mcp standalone MCP server framework agent-agnostic, not ai-* +``` + +`ai-sdk` is the foundation: it owns the single-agent loop, tools, and streaming. `ai-skills` and `ai-autopilot` build on top of it. `ai-mcp` bridges an agent to the Model Context Protocol. `mcp` stands apart - it is for *authoring* MCP servers and knows nothing about agents. + +## Design principles + +- **Framework-agnostic core.** Every package runs in any `fetch`-capable JS runtime - Node, the browser, Electron, React Native. The agent runtime has zero static `node:*` imports in its main entry, and its only required runtime dependency is `zod`. +- **Neutral contracts, not bundled infrastructure.** Persistence (conversation history, user memory, budgets, suspended runs, generated-file storage) is defined as interfaces you implement against your own database, cache, or object store. In-memory defaults ship for getting started. +- **One way to do a thing.** A single `tool()` shape, a single `Agent` base, a single provider config object - shared across the whole family. +- **Graduated, not dumped.** GemStack grows by promoting packages that earn framework-agnostic standing, with the API settling toward `1.0` in the open. + +## Where these came from + +The AI engine was spun out of Rudder's `@rudderjs/ai` and re-versioned under the GemStack umbrella. The Rudder package now re-exports this engine and adds the Rudder-specific bindings on top (an ORM-backed store set, a `/server` provider, a `make:agent` scaffolder). Those bindings are documented in Rudder's own docs; everything here is the framework-agnostic engine. + +## Next + +- [Installation](/guide/installation) - install the runtime and a provider SDK. +- [Your First Agent](/guide/first-agent) - define and run an agent in a few lines. diff --git a/docs/guide/installation.md b/docs/guide/installation.md new file mode 100644 index 0000000..9848355 --- /dev/null +++ b/docs/guide/installation.md @@ -0,0 +1,62 @@ +# Installation + +The agent runtime lives in [`@gemstack/ai-sdk`](/packages/ai-sdk/). Install it plus the provider SDK(s) you actually use - provider SDKs are optional peers, and each adapter lazy-loads its SDK on first call. + +```bash +pnpm add @gemstack/ai-sdk + +pnpm add @anthropic-ai/sdk # Anthropic (Claude) +pnpm add openai # OpenAI (also OpenRouter / Mistral / DeepSeek / Groq / xAI / Ollama) +pnpm add @google/genai # Google (Gemini) +pnpm add cohere-ai # Cohere (reranking + embeddings) +pnpm add @aws-sdk/client-bedrock-runtime # AWS Bedrock +# ElevenLabs, Voyage, Jina - no extra package needed (direct HTTP) +``` + +The core stands alone: `@gemstack/ai-sdk`'s only required runtime dependency is `zod`. + +## Configure a provider + +Register the providers you want and set a default model. Each provider's `name` (e.g. `anthropic`) becomes the registry key, and model strings are always `provider/model`. + +```ts +import { AiRegistry, AnthropicProvider, OpenAIProvider, OllamaProvider } from '@gemstack/ai-sdk' + +AiRegistry.register(new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! })) +AiRegistry.register(new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY! })) +AiRegistry.register(new OllamaProvider({ baseUrl: 'http://localhost:11434' })) + +AiRegistry.setDefault('anthropic/claude-sonnet-4-6') +``` + +Run this once at startup (an `ai.ts` module you import early, for example). Models are always `provider/model` - a bare model name throws. See [Providers](/packages/ai-sdk/providers) for every adapter and its config. + +> Behind an LLM gateway or proxy? If it is OpenAI- or Anthropic-compatible, set `baseUrl` on the matching provider. If it speaks its own wire format, subclass the gateway adapter from the `@gemstack/ai-sdk/gateway` subpath. + +## Runtime compatibility + +`@gemstack/ai-sdk` works in any `fetch`-capable JS runtime - Node, browser, Electron (main and renderer), React Native. The main entry has zero static `node:*` imports. + +| Import | Runtimes | What's inside | +|---|---|---| +| `@gemstack/ai-sdk` | Node, browser, RN, Electron | Agents, tools, streaming, providers, attachments, structured output | +| `@gemstack/ai-sdk/node` | Node only | `documentFromPath()`, `imageFromPath()`, `transcribeFromPath()` filesystem helpers | +| `@gemstack/ai-sdk/react` | Browser | React bindings (`useAgentRun`) | +| `@gemstack/ai-sdk/eval` | Node | Eval framework (`evalSuite`, metrics, reporters) | +| `@gemstack/ai-sdk/computer-use` | Node | Computer-use tool + executor | + +In a client runtime, use byte-based factories instead of filesystem paths: + +```ts +import { Image } from '@gemstack/ai-sdk' + +const img = Image.fromBase64(cameraBase64, 'image/jpeg') +const url = await Image.fromUrl('https://example.com/photo.jpg') +``` + +> Calling LLM providers directly from a browser or React Native client leaks your API key - use a server-side proxy in production. The main client-side use case is BYOK desktop apps. + +## Next + +- [Your First Agent](/guide/first-agent) - define and run an agent. +- [Packages overview](/packages/) - the whole GemStack family. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..6af7cd7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,41 @@ +--- +layout: home + +hero: + name: "GemStack" + text: "Framework-agnostic tools for building AI applications in Node." + tagline: "An agent runtime, portable skills, multi-agent orchestration, and an MCP toolkit - standalone packages that work in any Node app and compose cleanly with each other." + image: + src: /logo.svg + alt: GemStack + actions: + - theme: brand + text: Get Started + link: /guide/installation + - theme: alt + text: What is GemStack? + link: /guide/ + - theme: alt + text: View on GitHub + link: https://github.com/gemstack-land/gemstack + +features: + - icon: 🧠 + title: Provider-agnostic agent runtime + details: "Define an agent once; swap Anthropic, OpenAI, Google, Ollama, Groq, DeepSeek, xAI, Mistral, and more by changing one config string. Tool calling, streaming, middleware, structured output, and memory included." + - icon: 🧩 + title: Portable capability bundles + details: "Ship a skill as a folder - instructions + tools + resources - and compose it onto an agent on demand. The same SKILL.md shape Claude uses." + - icon: 🎛️ + title: Multi-agent orchestration + details: "A Supervisor that plans, dispatches subagents under bounded concurrency and token budgets, and synthesizes the result. Pluggable plan / workers / synthesize stages." + - icon: 🔌 + title: Model Context Protocol, both directions + details: "Bridge agents to MCP - consume a remote server's tools or expose an agent as a server - and author standalone MCP servers with tools, resources, prompts, and OAuth 2.1." + - icon: 🪶 + title: Zero framework lock-in + details: "Each package works in any fetch-capable JS runtime. The agent runtime's only required dependency is zod; persistence is via neutral contracts you implement against your own infrastructure." + - icon: 💎 + title: Graduated, not dumped + details: "Packages join GemStack one at a time, when they prove framework-agnostic value - built in the open with the Vike team." +--- diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..2e687bc --- /dev/null +++ b/docs/package.json @@ -0,0 +1,15 @@ +{ + "name": "@gemstack/docs", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + }, + "devDependencies": { + "vitepress": "2.0.0-alpha.17", + "vue": "^3.5.29" + } +} diff --git a/docs/packages/ai-autopilot.md b/docs/packages/ai-autopilot.md new file mode 100644 index 0000000..dac1677 --- /dev/null +++ b/docs/packages/ai-autopilot.md @@ -0,0 +1,84 @@ +# @gemstack/ai-autopilot + +Orchestration for [`@gemstack/ai-sdk`](/packages/ai-sdk/) agents: the "director" layer that runs **many** agent runs under a control policy. + +`ai-sdk` owns the single-agent loop and the handoff / subagent primitives. `ai-autopilot` owns orchestrating multiple runs: which agents run, in what order, how their results combine, and when to stop. If a feature is just calling an `ai-sdk` primitive, it belongs in `ai-sdk`; autopilot earns its keep only as the topology and control-policy layer. + +```bash +pnpm add @gemstack/ai-autopilot @gemstack/ai-sdk +``` + +## Supervisor (plan, dispatch, synthesize) + +The first slice is the supervisor/worker topology, the smallest thing clearly more than the primitives: + +1. **Plan** - a planner decomposes the task into subtasks. +2. **Dispatch** - each subtask runs on a worker agent, with bounded concurrency, an optional token budget, and per-subtask error isolation. +3. **Synthesize** - a synthesizer combines the results into the final answer. + +```ts +import { Supervisor, agentPlanner, agentSynthesizer } from '@gemstack/ai-autopilot' + +const supervisor = new Supervisor({ + plan: agentPlanner(plannerAgent), // LLM decomposition + workers: { research: researchAgent, write: writerAgent }, // routed by subtask.worker + synthesize: agentSynthesizer(editorAgent), // LLM synthesis + concurrency: 3, + maxSubtasks: 8, + budget: { maxTotalTokens: 200_000 }, + onEvent: (e) => console.log(e.type), +}) + +const run = await supervisor.run('Draft a launch brief for product X') +console.log(run.text) // synthesized answer +console.log(run.results) // per-subtask outcomes (ok / error / usage) +console.log(run.usage) // aggregate token usage across dispatched subtasks +console.log(run.stoppedEarly) // true if a guardrail trimmed or halted work +``` + +`Supervisor` validates its options at construction (`plan`, `workers`, positive `concurrency` / `maxSubtasks`), and `run()` rejects an empty task, so misconfiguration fails fast with a clear message. + +## Pieces are pluggable + +Each stage is a plain function, so you mix LLM and deterministic logic freely: + +- **`plan`** - a `Planner`: `(task) => Subtask[]`. Use `agentPlanner(agent)` for LLM decomposition, or return a static list (or any hand-rolled logic). +- **`workers`** - a single `Agent` (every subtask runs on it), a `Record` (routed by `subtask.worker`), or a `WorkerRouter` function for full control. +- **`synthesize`** - a `Synthesizer`: `(task, results) => string`. Defaults to `defaultSynthesize` (concatenate the successful results, no LLM call); pass `agentSynthesizer(agent)` for an LLM pass. + +`agentPlanner` and `agentSynthesizer` are the two adapters that turn an `ai-sdk` [agent](/packages/ai-sdk/agents) into a `Planner` / `Synthesizer`; everything else can be ordinary code. + +## Guardrails + +| Guardrail | Default | Effect | +|---|---|---| +| `concurrency` | `4` | Max workers in flight; positive integer. | +| `maxSubtasks` | none | Hard cap. A longer plan is trimmed and `stoppedEarly` is set. Omit for no cap. | +| `budget.maxTotalTokens` | none | Stop dispatching once aggregate dispatch usage crosses the limit. In-flight workers finish (usage can overshoot slightly); remaining subtasks are skipped. Omit for no limit. | + +Two further safety properties hold without configuration: + +- **Error isolation** - a worker that throws becomes an `ok: false` result; siblings continue. +- **Observer safety** - an `onEvent` callback that throws is logged and swallowed; it never aborts the run. + +Progress is reported through `onEvent` as typed `SupervisorEvent`s (`plan`, `plan-trimmed`, `dispatch-start`, `dispatch-result`, `budget-exceeded`, `synthesize`). + +## The run result + +`supervisor.run(task)` resolves to a `SupervisorRun`: + +| Field | Type | Meaning | +|---|---|---| +| `text` | `string` | The synthesized final answer. | +| `plan` | `PlannedSubtask[]` | The plan that was executed (after any guardrail trimming). | +| `results` | `SubtaskResult[]` | One result per dispatched subtask, in plan order. Each carries `text`, `ok`, optional `error`, and `usage`. | +| `usage` | `TokenUsage` | Aggregate token usage across the dispatched subtasks (planning and synthesis spend are not included, since the `Planner` / `Synthesizer` contracts return data, not usage). | +| `stoppedEarly` | `boolean` | True when a guardrail (subtask cap or token budget) stopped work early. | + +## Scope (what's deferred) + +The seed dispatches **autonomous** workers via `agent.prompt()`. A worker that pauses for a client-tool or approval round-trip is reported as a failed subtask. Durable pause/resume across a supervised run (building on `ai-sdk`'s `SubAgentRunStore` and resume primitives) is a deferred adapter, as are other topologies (pipelines, debate) and queue-backed long-running execution. Those land on demand, behind optional seams, not in the core. + +## License + +MIT diff --git a/docs/packages/ai-mcp.md b/docs/packages/ai-mcp.md new file mode 100644 index 0000000..ec104cd --- /dev/null +++ b/docs/packages/ai-mcp.md @@ -0,0 +1,103 @@ +# @gemstack/ai-mcp + +The bridge between [`@gemstack/ai-sdk`](/packages/ai-sdk/) Agents and [Model Context Protocol](https://modelcontextprotocol.io) servers. Two connectors: + +- **`mcpClientTools(transport, opts?)`** - consume a remote MCP server's tools as Agent tools. +- **`mcpServerFromAgent(AgentClass, opts?)`** - expose an Agent as an MCP server that external clients (Claude Desktop, Cursor, etc.) can call. + +This is the **agent bridge** axis of MCP. It depends on `@gemstack/ai-sdk` and is useless without an Agent. It was carved out of `@gemstack/ai-sdk`'s `/mcp` subpath so the optional MCP SDK dependency is declared only by the package that actually needs it. + +## Which MCP package do I use? + +| You want to... | Use | +|---|---| +| Expose an existing Agent, or feed remote MCP tools into one | **`@gemstack/ai-mcp`** (this package) | +| Author an MCP server from scratch (tools / resources / prompts / auth), agent-agnostic | [`@gemstack/mcp`](/packages/mcp) | + +Both can "produce an MCP server", but from different inputs: `mcpServerFromAgent(anAgent)` versus a hand-authored server. That overlap is expected, not duplication. + +## Installation + +```bash +pnpm add @gemstack/ai-mcp @modelcontextprotocol/sdk +``` + +`@modelcontextprotocol/sdk` is an **optional peer dependency**: install it when you use this bridge. `@gemstack/ai-sdk` comes in as a regular dependency. + +## Consume a remote MCP server's tools + +`mcpClientTools` connects to a remote MCP server and returns its tools as `@gemstack/ai-sdk` tools. It accepts three transport shapes: + +```ts +import { mcpClientTools } from '@gemstack/ai-mcp' + +// (a) HTTP - string URL or URL instance (Streamable HTTP transport) +const tools = await mcpClientTools('https://api.example.com/mcp') + +// (b) Local stdio subprocess +const tools = await mcpClientTools({ command: 'npx', args: ['some-mcp-server'] }) + +// (c) Already-connected SDK Client (caller owns lifecycle) +const tools = await mcpClientTools(myClient) +``` + +Spread the result into your Agent's `tools()`: + +```ts +class MyAgent extends Agent { + tools() { return [...tools, ...myOwnTools] } // close() is non-enumerable, so it's not iterated +} +// ... later +await tools.close?.() +``` + +### The `close()` lifecycle + +When this call owns the connection (cases **a** and **b**) the returned array carries a non-enumerable `close()` method; call it when the agent is done so the subprocess or HTTP session shuts down cleanly. When you pass your own `Client` (case **c**) there is no `close()`: you own that lifecycle. + +### Options + +`mcpClientTools(transport, opts)` accepts: + +| Option | Default | Effect | +|---|---|---| +| `filter` | all tools | `(toolName) => boolean` - drop remote tools you don't want to expose. | +| `namePrefix` | `''` | Prefix every tool name, to avoid collisions when wiring several remote servers. | +| `streaming` | `true` | Forward the remote server's `notifications/progress` as `tool-update` chunks during a run. | + +```ts +const tools = await mcpClientTools('https://api.example.com/mcp', { + filter: (name) => !name.startsWith('internal_'), + namePrefix: 'remote_', +}) +``` + +The stdio transport spawn config (`StdioServerSpawn`) takes `command`, optional `args`, `env` (inherited from the parent when omitted), and `cwd`. + +## Expose an Agent as an MCP server + +`mcpServerFromAgent(AgentClass, opts?)` builds an MCP server from one of your [agents](/packages/ai-sdk/agents). Connect it to any MCP transport: + +```ts +import { mcpServerFromAgent } from '@gemstack/ai-mcp' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' + +const server = await mcpServerFromAgent(MyAgent) +await server.connect(new StdioServerTransport()) +``` + +### Exposure modes + +Via `opts.expose`: + +| Mode | What it exposes | Best for | +|---|---|---| +| `'tools'` (default) | One MCP tool per `agent.tools()` entry. | Surfacing an agent's toolbox to other MCP clients. | +| `'agent'` | A single MCP tool that runs the whole agent (`prompt(text) -> text`). | Shipping one agent that any MCP client can call. | +| `'both'` | The individual tools and the agent prompt-tool, side by side. | Both at once. | + +Other options: `name` / `version` (server identity; `name` defaults to `${AgentClass.name}Server`), `instructions` (advertised server instructions, defaulting to the agent's `instructions()`), and `agentToolName` (the prompt-tool's name in `'agent'` / `'both'` mode, defaulting to the agent class name). + +## License + +MIT diff --git a/docs/packages/ai-sdk/agents.md b/docs/packages/ai-sdk/agents.md new file mode 100644 index 0000000..3fbaefb --- /dev/null +++ b/docs/packages/ai-sdk/agents.md @@ -0,0 +1,350 @@ +# Running agents + +An agent is a system prompt plus a model, an optional tool set, and an optional stop condition. You define it once and run it with `.prompt()` (await the full result) or `.stream()` (iterate chunks). This page covers the agent shapes, multi-step loops, sub-agents, suspend/resume across HTTP boundaries, queued background runs, and middleware. For the tool surface itself see [Tools](/packages/ai-sdk/tools); for the stream protocol see [Streaming](/packages/ai-sdk/streaming). + +All examples assume a default provider is registered once at startup: + +```ts +import { AiRegistry, AnthropicProvider } from '@gemstack/ai-sdk' + +AiRegistry.register(new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! })) +AiRegistry.setDefault('anthropic/claude-sonnet-4-6') +``` + +## Three agent shapes + +Pick whichever reads best at the call site: + +```ts +import { agent, AI, Agent, stepCountIs } from '@gemstack/ai-sdk' + +// Inline, one-off +const r1 = await agent('You summarize text.').prompt('Summarize this...') + +// Facade with the default model +const r2 = await AI.prompt('Hello world') + +// Configured anonymous agent - tools + options together +const r3 = await agent({ + instructions: 'You help find users.', + model: 'anthropic/claude-sonnet-4-6', + tools: [searchTool], +}).prompt('Find all admins') + +// Reusable typed class +class SearchAgent extends Agent { + instructions() { return 'You help find users.' } + model() { return 'anthropic/claude-sonnet-4-6' } + tools() { return [searchTool] } + stopWhen() { return stepCountIs(5) } +} +const r4 = await new SearchAgent().prompt('Find all admins') +``` + +The `agent({ ... })` form accepts `instructions`, `model`, `tools`, and `middleware`. Anything beyond that (a stop condition, parallel-tool policy, caching, conversation memory) is declared by subclassing `Agent` and overriding the matching method. + +## Multi-step agents + +By default an agent does one round-trip: prompt, tool calls, final answer. For multi-step reasoning, set a stop condition by overriding `stopWhen()`: + +```ts +import { Agent, stepCountIs } from '@gemstack/ai-sdk' + +class Researcher extends Agent { + instructions() { return 'You research and summarize topics.' } + tools() { return [searchWeb, fetchPage] } + stopWhen() { return stepCountIs(10) } // up to 10 tool-calling rounds +} + +await new Researcher().prompt('Research the transformer architecture.') +``` + +The built-in stop-condition combinators are `stepCountIs(n)` and `hasToolCall(name)`. For anything else, return a plain `StopCondition` predicate, `({ steps }) => boolean`, from `stopWhen()`. Returning an array applies them with OR semantics (the loop stops when any matches). + +## Sub-agents + +A tool's handler can itself invoke another agent. Streaming progress and approval state propagate upstream so the parent agent's UI stays in sync. + +The shortest path is `agent.asTool({ name, description })`, which wraps an agent as a tool the parent can call. The sub-agent runs its own loop end-to-end (its own model, tools, middleware) and returns a single result. + +```ts +const research = new ResearchAgent().asTool({ + name: 'research', + description: 'Research a topic in depth.', +}) + +class Orchestrator extends Agent { + tools() { return [research] } + stopWhen() { return stepCountIs(5) } +} + +await new Orchestrator().prompt('Summarize the transformer paper.') +``` + +Defaults are tuned for the zero-config case: `inputSchema` is `{ prompt: string }` and the parent model only sees `response.text` on its next step. The UI still receives the full `AgentResponse` via the `tool-result` chunk, so dashboards can render rich sub-agent transcripts without bloating the parent's context. + +For a typed input schema, pass `inputSchema` + `prompt`: + +```ts +new ResearchAgent().asTool({ + name: 'research', + description: 'Research a topic in depth.', + inputSchema: z.object({ topic: z.string(), depth: z.enum(['quick', 'deep']) }), + prompt: ({ topic, depth }) => `Research ${topic} at ${depth} depth.`, + modelOutput: (r) => `${r.steps.length} step(s); ${r.text.slice(0, 280)}`, +}) +``` + +### Streaming sub-agent progress + +Pass `streaming: true` to surface inner-agent progress as `tool-update` chunks on the parent's stream. The default projection emits `agent_start` once, `tool_call` per inner tool call, and `agent_done` once when the sub-agent finishes: + +```ts +const research = new ResearchAgent().asTool({ + name: 'research', + description: 'Research a topic in depth.', + streaming: true, +}) + +const { stream } = agent({ tools: [research] }).stream('summarize that paper') +for await (const chunk of stream) { + if (chunk.type === 'tool-update' && chunk.update?.kind === 'tool_call') { + console.log(`subagent calling ${chunk.update.tool}`) + } +} +``` + +For a different cadence (surfacing inner `text-delta` as preview text, or per-step usage), pass a projector: + +```ts +streaming: (chunk) => chunk.type === 'finish' + ? { kind: 'agent_step', step: ++n, tokens: chunk.usage?.totalTokens ?? 0 } + : null +``` + +### Suspend and resume: sub-agents that pause + +A sub-agent's loop pauses in two cases the parent loop has to surface upward: when the model emits a *client* tool call (one with no `.server()` handler) and when a sub-agent's tool with `needsApproval: true` fires. Pass `suspendable: { runStore }` to opt into the propagation protocol; `asTool` handles both pauses symmetrically. + +The run store is a neutral contract. `InMemorySubAgentRunStore` works for tests and single-process dev; `CachedSubAgentRunStore` is an adapter over any `CacheAdapter` you supply (Redis, Memcached, a `Map`, your framework's cache) for cross-process / cross-restart persistence. `@gemstack/ai-sdk` bundles no cache implementation, so you bring the cache: + +```ts +import { CachedSubAgentRunStore } from '@gemstack/ai-sdk' + +const research = new ResearchAgent().asTool({ + name: 'research', + description: 'Research with browser-side tools and approval-gated actions.', + streaming: true, // suspend requires streaming + suspendable: { runStore: new CachedSubAgentRunStore({ cache }) }, +}) +``` + +When the sub-agent pauses, `asTool` snapshots its message history and yields a suspend update plus a control chunk that halts the parent loop. The snapshot's `pauseKind` discriminator tells the host which resume contract applies: + +| Inner `finishReason` | `SubAgentUpdate` emitted | Snapshot `pauseKind` | Parent halts with | +|---|---|---|---| +| `'client_tool_calls'` | `subagent_paused` | `'client_tool'` | `pendingClientToolCalls` | +| `'tool_approval_required'` | `subagent_paused_approval` | `'approval'` | `pendingApprovalToolCall` | + +The host's continuation endpoint resumes via `Agent.resumeAsTool`: + +```ts +import { Agent } from '@gemstack/ai-sdk' + +// Client-tool pause - pass tool results from the browser +const r = await Agent.resumeAsTool(subRunId, browserResults, { + runStore, + agent: rebuiltSubAgent, // host rebuilds the sub-agent context per resume +}) + +// Approval pause - pass the user's decision +const r2 = await Agent.resumeAsTool(subRunId, [], { + runStore, + agent: rebuiltSubAgent, + approvedToolCallIds: ['inner-call-id'], // or rejectedToolCallIds +}) + +if (r.kind === 'completed') { + // feed r.response.text back into the parent's tool result +} else { + // r.kind === 'paused' - r.pauseKind ('client_tool' | 'approval') routes the + // next upstream event; r.toolCall + r.isClientTool are populated for approval + // pauses so renderers can show a fresh approval card. +} +``` + +A resume can pause again on a different kind than it started on (an approval that, once granted, leads the inner agent to emit a client tool call). The `pauseKind` field on `'paused'` returns lets the host route correctly without inspecting the snapshot. Suspend without streaming throws at builder time: silent suspend is a UX trap. + +When an orchestrator dispatches several sub-agents in one parent turn and more than one pauses, `Agent.resumeManyAsTool(requests, { runStore })` resumes them as a batch and aggregates their pending tool calls into a single client round-trip: + +```ts +let batch = await Agent.resumeManyAsTool( + paused.map(p => ({ + subRunId: p.subRunId, + agent: rebuildSubAgent(p), + clientToolResults: resultsBySubRun[p.subRunId], // or approved/rejectedToolCallIds + key: p.subRunId, // echoed back for correlation + })), + { runStore }, +) + +// batch.completed / batch.paused / batch.errors partition the outcomes; +// batch.pendingToolCallIds is the combined set to gather the next round for. +// Re-call with each paused item's NEW subRunId until batch.allCompleted. +``` + +Options: `onError: 'capture'` (default; a bad item becomes a `{ kind: 'error' }` outcome and the rest still resume) or `'throw'`; `concurrency: 'parallel'` (default) or `'serial'`. Pass `streaming` (with `onUpdate`) to keep each resumed sub-agent's progress live rather than freezing its bubble until it completes or pauses again. + +### Standalone run persistence: a top-level `stream()` that pauses + +The sub-agent run store covers pauses *inside* a parent loop. A top-level `agent.stream()` pauses for the same two reasons (a client tool with no handler, or an approval gate) but across an HTTP boundary: the run stops on one request and resumes on the next. Persist the run state between them with `CachedAgentRunStore`, the standalone sibling, also an adapter over a `CacheAdapter` you supply: + +```ts +import { CachedAgentRunStore, newAgentRunId, type AgentRunState } from '@gemstack/ai-sdk' + +const runs = new CachedAgentRunStore({ cache }) + +// First request - the stream pauses on a client tool: +const { stream, response } = agent({ tools: [browserTool] }).stream(input) +for await (const _ of stream) { /* forward chunks to the client */ } +const res = await response + +if (res.pendingClientToolCalls?.length) { + const runId = newAgentRunId() + await runs.store(runId, { + messages: conversationSoFar, // full history to replay + pendingToolCallIds: res.pendingClientToolCalls.map(c => c.id), + stepsSoFar: res.steps.length, + tokensSoFar: res.usage.totalTokens, + meta: { userId }, // opaque, never read by the engine + }) + return { runId, pending: res.pendingClientToolCalls } // hand runId to the client +} +``` + +```ts +// Follow-up request - the client returns tool results for `runId`: +const state = await runs.consume(runId) // atomic single-use: a replayed runId can't read twice +if (!state) throw new Error('run expired or already resumed') + +await agent({ tools: [browserTool] }) + .stream(/* original input */, { messages: [...state.messages, ...toolResultMessages] }) +``` + +`store` / `load` / `consume` are the three operations: `load()` is a non-destructive peek (render a "waiting for approval" view on a GET without burning the run), `consume()` is the atomic read-and-delete you call on the actual resume. `AgentRunState` carries `pauseKind` (`'client_tool'` | `'approval'`) and `pendingApprovalToolCall` so approval pauses round-trip the same way. `newAgentRunId()` mints an unguessable id (a `runId` is a capability handle to a parked conversation). `InMemoryAgentRunStore` is the test / single-process backend. + +### Hand-rolled sub-agent tools + +For full control (a custom progress shape, sub-agent token-deltas as `tool-update` chunks, anything outside the `asTool` envelope), write the wrapping tool by hand: + +```ts +const research = toolDefinition({ + name: 'research', + description: 'Research a topic in depth', + inputSchema: z.object({ topic: z.string() }), +}).server(async ({ topic }) => { + return await new ResearchAgent().prompt(topic) +}) +``` + +For Model Context Protocol bridging (consuming remote MCP tools in an agent, or exposing an agent as an MCP server), see [/packages/ai-mcp](/packages/ai-mcp). For higher-level multi-agent orchestration patterns built on this runtime, see [/packages/ai-autopilot](/packages/ai-autopilot). + +## Chat mentions (`@slug` agent routing) + +In a chat UI where one orchestrator routes to several agents, let users `@` an agent to invoke it explicitly, overriding the orchestrator's own judgment. `@gemstack/ai-sdk/chat-mentions` ships the two reusable pieces: + +```ts +import { parseMentions, buildMentionRoutingRule } from '@gemstack/ai-sdk/chat-mentions' + +const { slugs, cleaned } = parseMentions(userMessage, knownAgentSlugs) +// '@seo audit this' → { slugs: ['seo'], cleaned: 'audit this' } + +const rule = buildMentionRoutingRule(slugs) // null when no mentions +if (rule) systemPrompt += `\n\n${rule}` +// then run the orchestrator with `cleaned` as the user input +``` + +`parseMentions` validates tokens against your known slugs (unknown `@mentions` stay as plain text), dedupes in first-seen order, and strips the matched tokens so the model sees only the cleaned intent. It does not treat `email@host` as a mention. `buildMentionRoutingRule` renders a system-prompt rule forcing the orchestrator to dispatch the mentioned agents in order; pass `{ toolName, argKey }` if your dispatch tool is not the default `run_agent({ agentSlug })`. + +## Queued prompts + +Push an agent run onto a background queue. `agent.queue(input)` returns a `QueuedPromptBuilder` so you can pick a queue, attach success/failure callbacks, and optionally stream progress to a broadcast channel as it runs. + +The queue and broadcast transports are neutral contracts you register once at startup with `configureAiQueue`; `@gemstack/ai-sdk` bundles no queue or broadcast implementation, so you bring your own: + +```ts +import { configureAiQueue } from '@gemstack/ai-sdk' + +configureAiQueue({ + dispatch: (fn) => myQueue.push(fn), // enqueue fn to run on a worker + broadcast: (channel, event, data) => myBus.publish(channel, event, data), // optional +}) +``` + +```ts +// Fire-and-forget background run +await new SupportAgent() + .queue('Help with refund request') + .onQueue('ai') + .send() + +// With success/failure callbacks +await new ResearchAgent() + .queue('Research the latest architecture') + .then(response => console.log('Done:', response.text)) + .catch(error => console.error('Failed:', error)) + .send() +``` + +### Stream progress to a broadcast channel + +Background AI work plus a live UI without polling. Each stream chunk is broadcast to the channel as the job runs; the final response is broadcast as a `done` event. This needs a `broadcast` adapter registered via `configureAiQueue`. + +```ts +await new SupportAgent() + .queue('Help with refund request') + .broadcast(`user.${userId}.support`) + .send() +``` + +Subscribers on `user.${userId}.support` receive: + +- `{ event: 'chunk', data: }` - one per stream chunk (text-delta, tool-call, tool-result, ...) +- `{ event: 'done', data: }` - final result, after the agent loop ends +- `{ event: 'error', data: { message } }` - on failure + +The chunk shape matches the engine's normal `StreamChunk` types, so a frontend can subscribe to the channel and reuse its existing chunk-handling code. Pass `eventPrefix` to namespace events when the channel carries other unrelated messages: + +```ts +.broadcast('shared-channel', { eventPrefix: 'agent.' }) +// emits 'agent.chunk', 'agent.done', 'agent.error' +``` + +## Middleware + +Middleware is an `AiMiddleware` interface: implement only the lifecycle hooks you care about. Hooks include `onConfig`, `onStart`, `onIteration`, `onChunk`, `onBeforeToolCall`, `onAfterToolCall`, `onToolPhaseComplete`, `onUsage`, `onFinish`, `onAbort`, and `onError`. + +```ts +import type { AiMiddleware } from '@gemstack/ai-sdk' + +const logging: AiMiddleware = { + name: 'logging', + onStart(ctx) { console.log(`[ai] ${ctx.model} started`) }, + onUsage(_ctx, u) { console.log(`[ai] ${u.totalTokens} tokens`) }, + onBeforeToolCall(_ctx, name) { + if (name === 'dangerous_tool') return { type: 'skip', result: 'Tool disabled' } + return undefined + }, + onChunk(_ctx, chunk) { return chunk }, // transform, or return null to drop +} + +await agent({ instructions: 'You are helpful.', middleware: [logging] }).prompt('Hello') +``` + +`onBeforeToolCall` can return `{ type: 'skip', result }` to short-circuit a tool, `{ type: 'transformArgs', args }` to rewrite arguments, or `{ type: 'abort', reason }` to stop the loop. For run telemetry without writing middleware, subscribe to the observer registry from `@gemstack/ai-sdk/observers`. + +## Pitfalls + +- **Streaming `response` not resolving.** `await response` only resolves after the `stream` iterator has been fully consumed. Always iterate the stream first, even if you only care about the final result. +- **Bare model names.** `model: 'claude-sonnet-4-6'` throws; it must be `provider/model`. +- **Suspend needs a run store and streaming.** `suspendable` requires `streaming` on `asTool`, and the cache-backed stores require a `CacheAdapter` you supply. diff --git a/docs/packages/ai-sdk/index.md b/docs/packages/ai-sdk/index.md new file mode 100644 index 0000000..5997204 --- /dev/null +++ b/docs/packages/ai-sdk/index.md @@ -0,0 +1,51 @@ +# @gemstack/ai-sdk + +`@gemstack/ai-sdk` is a provider-agnostic agent runtime. Define an agent once, then swap between Anthropic, OpenAI, Google, Ollama, Groq, DeepSeek, xAI, Mistral, Bedrock, and others by changing one model string. The engine handles tool calling, streaming, middleware hooks, structured output, multi-modal attachments, sub-agents, conversation memory, and a test fake. Its only required runtime dependency is `zod`; provider SDKs are optional peers you install per provider. + +```ts +import { AiRegistry, AnthropicProvider, agent } from '@gemstack/ai-sdk' + +AiRegistry.register(new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! })) +AiRegistry.setDefault('anthropic/claude-sonnet-4-6') + +const response = await agent('You are a helpful assistant.') + .prompt('Summarize the transformer architecture in one sentence.') + +console.log(response.text) +``` + +Models are always `provider/model`; a bare model name throws. See [/guide/installation](/guide/installation) for setup and [/guide/first-agent](/guide/first-agent) for a full walkthrough. + +## Subpath exports + +The engine works in any `fetch`-capable JS runtime (Node, browser, Electron, React Native): the main entry has zero static `node:*` imports. + +| Subpath | What it provides | +|---|---| +| `.` | Core: `Agent`, `agent()`, `AI`, tools, streaming, providers, middleware, structured output, run stores, testing fake | +| `./node` | Node-only filesystem helpers for attachments | +| `./observers` | Observer registry (`aiObservers`) for run telemetry | +| `./chat-mentions` | `@slug` agent routing helpers (`parseMentions`, `buildMentionRoutingRule`) | +| `./gateway` | Custom LLM-gateway / proxy adapter helpers | +| `./eval` | Eval framework (`evalSuite`, metrics, reporters) | +| `./computer-use` | Computer-use tool + executor | +| `./react` | React client bindings (`useAgentRun`) | + +The MCP bridge moved out to its own package, [/packages/ai-mcp](/packages/ai-mcp). + +## What's in the box + +- [Running agents](/packages/ai-sdk/agents) - the three agent shapes, multi-step loops, sub-agents, suspend/resume, queued prompts, and middleware. +- [Tools](/packages/ai-sdk/tools) - `toolDefinition().server()`, scoped (multi-capability) tools, client tools, approval gates, and parallel execution. +- [Streaming](/packages/ai-sdk/streaming) - chunk iteration, Server-Sent Events, and the Vercel AI SDK adapter. +- [Structured output](/packages/ai-sdk/structured-output) - typed, schema-validated responses. +- [Memory](/packages/ai-sdk/memory) - conversation persistence and cross-thread user memory behind neutral contracts. +- [RAG](/packages/ai-sdk/rag) - hosted vector stores and `fileSearch` retrieval. +- [Providers](/packages/ai-sdk/providers) - the provider catalog and per-provider configuration. +- [Testing](/packages/ai-sdk/testing) - the `AiFake` programmable mock and the eval harness. + +For higher-level orchestration built on this runtime, see [/packages/ai-autopilot](/packages/ai-autopilot); for Model Context Protocol bridging, see [/packages/ai-mcp](/packages/ai-mcp). + +## Where this came from + +`@gemstack/ai-sdk` is the engine spun out of Rudder's `@rudderjs/ai` (carried forward from the 1.17.x line, renamed and re-versioned under the [GemStack](/packages/) umbrella). `@rudderjs/ai` now re-exports this engine and adds the Rudder-specific bindings on top (the framework service provider, ORM-backed stores, and the `make:agent` / `ai:eval` CLI). Those bindings are not part of this package: everything documented here is what `@gemstack/ai-sdk` itself exports, usable with no framework. diff --git a/docs/packages/ai-sdk/memory.md b/docs/packages/ai-sdk/memory.md new file mode 100644 index 0000000..52b5db3 --- /dev/null +++ b/docs/packages/ai-sdk/memory.md @@ -0,0 +1,271 @@ +# Memory & Persistence + +An agent run is stateless by default: each `prompt()` starts a fresh context. This page covers the three things that make agents feel like they remember across calls, plus prompt caching to keep that context cheap: + +- **Prompt caching** - mark stable parts of the prompt cacheable so providers skip re-billing the unchanged prefix. +- **Conversation persistence** - keep the full message thread so a follow-up turn can pick up where the last one left off. +- **User memory** - persist *facts* about a user that travel across conversations, independent of any single thread. + +The engine ships **neutral contracts plus in-memory defaults**. Both `ConversationStore` and `UserMemory` are interfaces with a Map-backed implementation good for tests and dev; for production you bring your own backend by implementing the interface against your own database, Redis, or external service. The ORM-backed implementations (Prisma / Drizzle / native) live in the Rudder binding `@rudderjs/ai`, not in this engine. + +Every example assumes a provider is registered: + +```ts +import { AiRegistry, AnthropicProvider } from '@gemstack/ai-sdk' + +AiRegistry.register(new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! })) +AiRegistry.setDefault('anthropic/claude-sonnet-4-6') +``` + +See [/guide/installation](/guide/installation) for setup. + +## Prompt caching + +Mark stable parts of the prompt as cacheable. Provider adapters translate the markers to native cache primitives: Anthropic adds `cache_control: { type: 'ephemeral' }` to the last content block of each marked region, OpenAI uses `prompt_cache_key` for routing affinity, and Google translates to `cachedContent` resources via a pluggable registry. Cache hits typically save 50-90% on input tokens. + +Declare cacheable regions with `cacheable()` on the agent class: + +```ts +import { Agent } from '@gemstack/ai-sdk' + +class SupportAgent extends Agent { + instructions() { return LONG_SYSTEM_PROMPT } + tools() { return [/* ... */] } + + cacheable() { + return { instructions: true, tools: true } + } +} +``` + +`messages: N` caches the first N messages (oldest first) - useful for multi-turn conversations where early context is stable: + +```ts +class ChatAgent extends Agent { + cacheable() { return { messages: 4 } } // cache up to message[3] +} +``` + +Per-call override: + +```ts +await agent.prompt('one-off', { cache: false }) // disable for this call +await agent.prompt('different', { cache: { tools: true } }) // replace the agent default +``` + +The full marker shape (`CacheableConfig`): + +| Field | Type | Meaning | +|---|---|---| +| `instructions` | `boolean` | Cache the system instructions. | +| `tools` | `boolean` | Cache the tool definitions. | +| `messages` | `number` | Cache the first N (oldest) messages. | +| `ttl` | `string` | Cache lifetime as a duration string (`'30m'`, `'2h'`, `'1d'`). Default `'1h'`. | + +Google's `cachedContent` resources are stateful and have a configurable TTL - set it via `ttl` (default `'1h'`, max ~24h depending on the model). Anthropic and OpenAI ignore `ttl` because their cache layers do not expose a per-call lifetime knob: + +```ts +class SupportAgent extends Agent { + cacheable() { return { instructions: true, tools: true, ttl: '6h' } } +} +``` + +The Google `cachedContent` registry (`GoogleCacheRegistry`) defaults to an in-process `Map` and warns once on first use. For multi-worker deployments, construct it with your own `CacheAdapter` (the neutral cache contract this engine exports) so cache resources survive across processes and restarts instead of being recreated per worker: + +```ts +import { GoogleCacheRegistry, type CacheAdapter } from '@gemstack/ai-sdk' + +const registry = new GoogleCacheRegistry({ store: myCacheAdapter /* implements CacheAdapter */ }) +``` + +Adapters that do not support caching ignore the markers - the request still runs, uncached. + +## Conversation persistence + +Conversation persistence keeps the full message thread so a follow-up turn resumes the prior context. Register a `ConversationStore`, then call `.forUser(userId)` to start a new conversation or `.continue(conversationId)` to resume one: + +```ts +import { Agent, setConversationStore, MemoryConversationStore } from '@gemstack/ai-sdk' + +setConversationStore(new MemoryConversationStore()) // in-memory default: dev / tests + +const first = await new AssistantAgent().forUser('user-42').prompt('My name is Alice.') +const second = await new AssistantAgent().continue(first.conversationId!).prompt("What's my name?") +// second.text -> 'Your name is Alice.' +``` + +`MemoryConversationStore` is in-process and loses every thread on restart, so it is for tests and dev only. For production, **implement the `ConversationStore` contract against your own database** (Postgres, Redis, an external service). It is five methods: + +```ts +interface ConversationStore { + create(title?: string, meta?: ConversationStoreMeta): Promise + load(conversationId: string): Promise + append(conversationId: string, messages: AiMessage[]): Promise + setTitle(conversationId: string, title: string): Promise + list(userId?: string): Promise + delete?(conversationId: string): Promise // optional +} +``` + +`ConversationStoreMeta` carries `userId`, an optional `agent` thread-segregation key (see [Auto-persist](#auto-persist-conversational) below), and free-form `resourceSlug` / `recordId` fields your backend can index on. + +### Sanitizing loaded history + +A thread interrupted mid-turn (a crash after the assistant row landed but before its tool-result rows) replays into a provider `400`: a dangling `tool_use` on Anthropic, an orphan `role: 'tool'` on OpenAI-compatible providers. `sanitizeConversation()` drops the incomplete turns so any loaded history is replay-safe. It is a pure, idempotent function exported from `@gemstack/ai-sdk`, so your custom store should run loaded history through it in `load()`: + +```ts +import { sanitizeConversation } from '@gemstack/ai-sdk' + +async load(conversationId: string) { + const messages = await this.readFromBackend(conversationId) + return sanitizeConversation(messages) +} +``` + +## Auto-persist (`conversational()`) + +For chat-style agents, threading `forUser()` through every call site is a footgun - forget it once and the conversation silently does not persist. Override `conversational()` on the agent class to auto-load + auto-save without each caller passing the user id: + +```ts +class ChatAgent extends Agent { + conversational() { + return { user: currentUserId } // falsy user -> opt-out + } +} + +await new ChatAgent().prompt('Hi') // auto-loads thread, runs, auto-saves +await new ChatAgent().prompt('Still you?') // resumes the same thread +``` + +Each `(user, agent class)` pair gets its own thread, so a user can talk to a `ChatAgent` and a `SupportAgent` without their histories merging. The segregation key defaults to the class name; override it with `agent: 'custom'` if you ever rename the class. + +Async returns are awaited - useful when the user identity comes from an async lookup: + +```ts +conversational() { return Promise.resolve({ user: await loadUserId() }) } +``` + +For long-running threads, cap loaded history to the last N messages: + +```ts +conversational() { return { user: userId, historyLimit: 50 } } +``` + +Per-call override and explicit-form precedence (high to low): + +1. `agent.forUser(id).prompt()` / `agent.continue(id).prompt()` - explicit always wins. +2. `agent.prompt(input, { conversation: false | { user, id?, historyLimit? } })` - per-call. +3. `agent.conversational()` - class declaration. + +Stores that surface the `agent` meta in `list()` results get the per-class thread separation; stores that ignore it fall back to "always create a new thread", which is the conservative behavior. + +## Validating continuations (`validate`) + +A continuation after a client-tool or approval round-trip carries the prior messages back from the browser, so the server is trusting client-supplied history. Without a guard a caller can rewrite that history (continue another user's thread, an IDOR), forge a `tool` result for a tool the server never ran, or claim an approval that was never pending. + +Pass a `validate` hook through the prompt options. It runs against the server-persisted history just before the agent loop, and throwing rejects the request. `defaultContinuationValidator()` is the built-in gate (prefix equality + tool-result-forgery + approval-forgery): + +```ts +import { defaultContinuationValidator } from '@gemstack/ai-sdk' + +await agent + .continue(conversationId) + .prompt(input, { + messages, // client-supplied continuation + validate: defaultContinuationValidator(), // throws ContinuationValidationError on forgery + }) +``` + +The same hook fires on the auto-persist path (`conversational()`) and on the streaming variant. For custom policy, the lower-level `validateContinuation(persisted, incoming, opts?)` returns a `{ ok, code, reason, index }` verdict you can branch on instead of throwing; `assertValidContinuation(...)` throws on failure. Rejection codes (`ContinuationRejectionCode`) discriminate `not-a-prefix` (history rewrite / IDOR), forged tool results, and forged approvals. Stateless calls (no persistence) never invoke it. The prefix comparison is order-insensitive for nested objects, so a tool-call `arguments` map that came back from storage with its keys reordered (Postgres `jsonb` does this) is not mistaken for a forgery. + +## User memory + +Conversation persistence remembers *messages*. **User memory** persists *facts* - things about a user that should travel across conversations, separate from any single thread. Useful when the agent needs to remember "Alice's project is named Foo" in a brand-new session without replaying the prior history. + +The contract is the `UserMemory` interface; the engine ships one in-memory implementation, `MemoryUserMemory`: + +```ts +import { setUserMemory, MemoryUserMemory } from '@gemstack/ai-sdk' + +setUserMemory(new MemoryUserMemory()) // in-memory default: dev / tests +``` + +`MemoryUserMemory` is Map-backed and uses case-insensitive **token-overlap** recall: the query is tokenized on non-alphanumeric boundaries and any fact sharing a token (>= 3 chars) with the query is returned, in insertion order. It is for tests and dev; for production **implement the `UserMemory` contract against your own database** (and add a semantic backend on top with embeddings - see below). + +### The `UserMemory` interface + +```ts +interface UserMemory { + remember(userId: string, fact: string, opts?: { tags?: string[]; score?: number }): Promise + recall (userId: string, query: string, opts?: { limit?: number; tags?: string[] }): Promise + forget (userId: string, factId: string ): Promise + list (userId: string, opts?: { tags?: string[]; limit?: number }): Promise + forgetAll?(userId: string): Promise // optional GDPR cascade +} +``` + +A `MemoryEntry` is `{ id, userId, fact, tags?, score?, createdAt, updatedAt? }`. The `score` is an optional confidence in `[0, 1]`; auto-extract sets it from the model's self-rating, manual `remember()` calls may omit it, and `recall()` ranking is implementation-defined when scores are absent. + +The interface is intentionally narrow so substring-match, full-text, and vector backends all satisfy it. **Semantic recall** (matching "Where do I deploy?" against "Project Foo lives at fly.io") is just a `UserMemory` implementation whose `recall()` embeds the query with `AI.embed(...)` and ranks facts by cosine similarity - see [Vector Stores & RAG](/packages/ai-sdk/rag) for the embedding surface to build that on. When you wire an external vector store, remember that `forget()` / `forgetAll()` must cascade to it yourself. + +Manual API - drop-in for any agent flow: + +```ts +const mem = new MemoryUserMemory() +await mem.remember('user_123', 'Project name is Foo', { tags: ['project'] }) +const facts = await mem.recall('user_123', 'project') +//=> [{ id: '...', userId: 'user_123', fact: 'Project name is Foo', tags: ['project'], createdAt: ... }] +``` + +### Auto-inject + auto-extract via `Agent.remembers()` + +For the common case - a chat agent that should both pull relevant facts into its system prompt AND distill new facts from each turn - declare `remembers()` on the class. The framework installs the right middleware automatically (it resolves the registered `UserMemory` via `setUserMemory()`): + +```ts +class SupportAgent extends Agent { + remembers() { + return { + user: currentUserId, + inject: 'auto', // recall + prepend per turn + extract: 'auto', // distill new facts per turn + extractWith: 'anthropic/claude-haiku-4-5', // small model for distillation + tags: ['support'], // recall + extract scope + injectLimit: 5, // cap injected facts + injectTokenBudget: 400, // hard token cap; lowest-score drops first + } + } +} +``` + +**Auto-inject** prepends matching facts as a fenced `` block to the system message: + +```text +You are a support agent. + + +- Project Foo deploys to fly.io us-east +- prefers TypeScript strict mode + +``` + +The block is built by querying `mem.recall(spec.user, latestUserText, { limit, tags })` once per turn (the `onStart` middleware), then trimming by `injectTokenBudget` if set. Token budget drops the lowest-score facts first. + +**Auto-extract** runs after each successful turn - the `onFinish` middleware pulls the latest `[user, assistant]` pair, calls the small `extractWith` model with a JSON-mode prompt asking for `{ facts: [{ fact, score, tags? }] }`, filters by confidence threshold (default `0.7`), and writes the survivors via `mem.remember()`. Failures inside auto-extract (network, JSON parse, schema mismatch, store write) are routed through `MemoryExtractOptions.onError` and otherwise swallowed - the parent prompt never breaks because of memory work. Use `MemoryExtractOptions.onExtracted(entries)` for an audit log. + +Per-call escape hatches and precedence (high to low): + +1. `agent.prompt(input, { memory: false })` - disable for this call. +2. `agent.prompt(input, { memory: { user, inject?, extract?, ... } })` - override the spec for this call. +3. `agent.remembers()` - class declaration. + +**Continuation calls** (when `options.messages` is set, e.g. resuming after a client-tool round-trip) skip both inject and extract so the system prompt is not double-augmented and facts are not double-written. + +The two middleware are also exported standalone - `withMemoryInject(spec, opts?)` and `withMemoryExtract(spec, opts?)` - if you want to compose them onto an agent by hand instead of declaring `remembers()`. + +## Pitfalls + +- **Memory poisoning.** Auto-extract trusts the user's own conversation as input - a malicious user can plant adversarial "facts." The default `0.7` confidence threshold is the v1 defense; tighten it for high-risk domains and pair with `MemoryExtractOptions.onExtracted` for an audit log when shipping to production. +- **`forUser()` / `continue()` throw without a store.** Conversation methods need a registered store - call `setConversationStore(...)` before they are used. +- **`remembers()` is a no-op without a store.** Auto-inject and auto-extract resolve the registered `UserMemory` - call `setUserMemory(...)` (or wire your own implementation) first, or nothing is recalled or written. +- **In-memory defaults lose everything on restart.** `MemoryConversationStore` and `MemoryUserMemory` are in-process. Anything you need to survive a restart or share across web processes and workers needs a real backend behind the contract. +- **External vector store cascade.** If a custom `UserMemory` writes vectors to an external store (Pinecone, Weaviate, pgvector), `forget()` / `forgetAll()` only delete the rows you delete - you must implement the cascade to the second store yourself. diff --git a/docs/packages/ai-sdk/providers.md b/docs/packages/ai-sdk/providers.md new file mode 100644 index 0000000..b6bbd47 --- /dev/null +++ b/docs/packages/ai-sdk/providers.md @@ -0,0 +1,257 @@ +# Providers + +`@gemstack/ai-sdk` is provider-agnostic. You write an agent once and switch between Anthropic, OpenAI, Google, and a dozen others by changing one model string. A provider is registered on a neutral registry, and every model reference is a `provider/model` string that the registry resolves to the right adapter. + +This page is the full provider reference: how to register a provider, the config each one takes, the registry name it claims, the SDK peer to install, and which capabilities (chat, embeddings, rerank, image, TTS, STT) each provider actually implements. + +See also: [Installation](/guide/installation), [Agents](/packages/ai-sdk/agents), [Testing & Evals](/packages/ai-sdk/testing). + +## Registering a provider + +Construct a provider with its config and hand it to `AiRegistry.register(...)`, then pick a default model with `AiRegistry.setDefault(...)`: + +```ts +import { AiRegistry, AnthropicProvider } from '@gemstack/ai-sdk' + +AiRegistry.register(new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! })) +AiRegistry.setDefault('anthropic/claude-sonnet-4-6') +``` + +Register as many providers as you use. Each claims its own registry name (the `name` field on the provider), and model strings route to it: + +```ts +import { AiRegistry, AnthropicProvider, OpenAIProvider, OllamaProvider } from '@gemstack/ai-sdk' + +AiRegistry.register(new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! })) +AiRegistry.register(new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY! })) +AiRegistry.register(new OllamaProvider({ baseUrl: 'http://localhost:11434/v1' })) + +AiRegistry.setDefault('anthropic/claude-sonnet-4-6') +``` + +Provider SDKs are optional peers, so you install only the SDK(s) for the providers you register. Each adapter lazy-loads its SDK on first call. + +## Model strings are `provider/model` + +Every model reference is `provider/model`. The part before the slash is the registry name; the part after is passed verbatim to that provider: + +```ts +await agent('You are helpful.').prompt('Hi', { model: 'openai/gpt-4o' }) +await agent('You are helpful.').prompt('Hi', { model: 'anthropic/claude-sonnet-4-6' }) +``` + +A bare model name (`'claude-sonnet-4-6'`) throws. `AiRegistry.parseModelString(...)` is the splitter, and `AiRegistry.resolve(modelString)` returns the resolved adapter if you need it directly. + +## Provider reference table + +| Provider | Registry name | SDK peer | chat | embeddings | rerank | image | TTS | STT | +|---|---|:--:|:--:|:--:|:--:|:--:|:--:|:--:| +| `AnthropicProvider` | `anthropic` | `@anthropic-ai/sdk` | yes | | | | | | +| `OpenAIProvider` | `openai` | `openai` | yes | yes | | yes | yes | yes | +| `GoogleProvider` | `google` | `@google/genai` | yes | yes | | yes | | | +| `OllamaProvider` | `ollama` | `openai` | yes | | | | | | +| `DeepSeekProvider` | `deepseek` | `openai` | yes | | | | | | +| `XaiProvider` | `xai` | `openai` | yes | | | | | | +| `GroqProvider` | `groq` | `openai` | yes | | | | | | +| `MistralProvider` | `mistral` | `openai` | yes | yes | | | | | +| `AzureOpenAIProvider` | `azure` | `openai` | yes | | | | | | +| `CohereProvider` | `cohere` | `cohere-ai` | | yes | yes | | | | +| `JinaProvider` | `jina` | none (HTTP) | | yes | yes | | | | +| `ElevenLabsProvider` | `elevenlabs` | none (HTTP) | | | | | yes | yes | +| `VoyageProvider` | `voyage` | none (HTTP) | | yes | yes | | | | +| `OpenRouterProvider` | `openrouter` | `openai` | yes | | | | | | +| `BedrockProvider` | `bedrock` | `@aws-sdk/client-bedrock-runtime` | yes | | | | | | + +A blank cell means the provider does not implement that capability. Calling `create(...)` on a non-chat provider (Cohere, Jina, ElevenLabs, Voyage) throws with a message pointing you at the capabilities it does support. + +"SDK peer: `openai`" means the provider speaks the OpenAI wire format and reuses the OpenAI adapter, so the `openai` package is the peer to install even though the service is not OpenAI. + +## SDK peers to install + +Install only the SDKs for the providers you register: + +```bash +pnpm add @anthropic-ai/sdk # Anthropic (Claude); also AWS Bedrock Claude +pnpm add openai # OpenAI, plus OpenRouter / Mistral / DeepSeek / Groq / xAI / Ollama / Azure +pnpm add @google/genai # Google (Gemini) +pnpm add cohere-ai # Cohere (reranking + embeddings) +pnpm add @aws-sdk/client-bedrock-runtime # AWS Bedrock +# ElevenLabs (TTS + STT) - no extra package, direct HTTP +# VoyageAI (embeddings + rerank) - no extra package, direct HTTP +# Jina (embeddings + rerank) - no extra package, direct HTTP +``` + +## Provider configs + +Each provider takes a typed config object (its `*Config` type is exported alongside the class). Common shape: `apiKey` plus an optional `baseUrl` to point at a gateway or proxy. + +### AnthropicProvider + +```ts +import { AnthropicProvider, type AnthropicConfig } from '@gemstack/ai-sdk' + +AiRegistry.register(new AnthropicProvider({ + apiKey: process.env.ANTHROPIC_API_KEY!, + baseUrl: undefined, // optional: override https://api.anthropic.com +})) +``` + +`AnthropicConfig`: `apiKey` (required), `baseUrl?`. Chat only. + +### OpenAIProvider + +```ts +import { OpenAIProvider, type OpenAIConfig } from '@gemstack/ai-sdk' + +AiRegistry.register(new OpenAIProvider({ + apiKey: process.env.OPENAI_API_KEY!, + baseUrl: undefined, // optional: override https://api.openai.com/v1 +})) +``` + +`OpenAIConfig`: `apiKey` (required), `baseUrl?`. The widest provider: chat, embeddings, image generation, TTS, and STT. The exported `OpenAIAdapter` is the shared chat adapter that the OpenAI-compatible providers below reuse. + +### GoogleProvider + +```ts +import { GoogleProvider, type GoogleConfig } from '@gemstack/ai-sdk' + +AiRegistry.register(new GoogleProvider({ apiKey: process.env.GOOGLE_API_KEY! })) +``` + +`GoogleConfig`: `apiKey` (required). Chat, embeddings, image generation. Gemini's `cachedContent` resources are backed by `GoogleCacheRegistry` (also exported); construct it with your own `CacheAdapter` and pass it as the second constructor argument for cross-process cache persistence. + +### OllamaProvider + +```ts +import { OllamaProvider, type OllamaConfig } from '@gemstack/ai-sdk' + +AiRegistry.register(new OllamaProvider({ baseUrl: 'http://localhost:11434/v1' })) +``` + +`OllamaConfig`: `baseUrl?` (defaults to `http://localhost:11434/v1`). No API key. Speaks the OpenAI wire format, so the `openai` package is the peer. Chat only. + +### DeepSeekProvider, XaiProvider, GroqProvider + +All three are OpenAI-compatible chat providers with the same config shape: + +```ts +import { DeepSeekProvider, XaiProvider, GroqProvider } from '@gemstack/ai-sdk' + +AiRegistry.register(new DeepSeekProvider({ apiKey: process.env.DEEPSEEK_API_KEY! })) +AiRegistry.register(new XaiProvider({ apiKey: process.env.XAI_API_KEY! })) +AiRegistry.register(new GroqProvider({ apiKey: process.env.GROQ_API_KEY! })) +``` + +Each config: `apiKey` (required), `baseUrl?`. Defaults: DeepSeek `https://api.deepseek.com/v1`, xAI `https://api.x.ai/v1`, Groq `https://api.groq.com/openai/v1`. Peer: `openai`. Chat only. + +### MistralProvider + +```ts +import { MistralProvider, type MistralConfig } from '@gemstack/ai-sdk' + +AiRegistry.register(new MistralProvider({ apiKey: process.env.MISTRAL_API_KEY! })) +``` + +`MistralConfig`: `apiKey` (required), `baseUrl?` (defaults to `https://api.mistral.ai/v1`). Chat (OpenAI-compatible, peer `openai`) plus embeddings. + +### AzureOpenAIProvider + +```ts +import { AzureOpenAIProvider, type AzureOpenAIConfig } from '@gemstack/ai-sdk' + +AiRegistry.register(new AzureOpenAIProvider({ + apiKey: process.env.AZURE_OPENAI_API_KEY!, + baseUrl: 'https://my-resource.openai.azure.com/openai/deployments/my-deployment', +})) +``` + +`AzureOpenAIConfig`: `apiKey` (required), `baseUrl` (required, your Azure deployment endpoint). Peer: `openai`. Chat. + +### CohereProvider + +```ts +import { CohereProvider, type CohereConfig } from '@gemstack/ai-sdk' + +AiRegistry.register(new CohereProvider({ apiKey: process.env.COHERE_API_KEY! })) +``` + +`CohereConfig`: `apiKey` (required). Embeddings and reranking only (no text generation). Peer: `cohere-ai`. + +### JinaProvider + +```ts +import { JinaProvider, type JinaConfig } from '@gemstack/ai-sdk' + +AiRegistry.register(new JinaProvider({ apiKey: process.env.JINA_API_KEY! })) +``` + +`JinaConfig`: `apiKey` (required). Embeddings and reranking only, over direct HTTP (no SDK peer). + +### ElevenLabsProvider + +```ts +import { ElevenLabsProvider, type ElevenLabsConfig } from '@gemstack/ai-sdk' + +AiRegistry.register(new ElevenLabsProvider({ apiKey: process.env.ELEVENLABS_API_KEY! })) +``` + +`ElevenLabsConfig`: `apiKey` (required), `baseUrl?` (override `https://api.elevenlabs.io`), `defaultTtsModelId?` (defaults to `eleven_multilingual_v2`). TTS and STT only, over direct HTTP. The exported `DEFAULT_TTS_MODEL_ID` and `DEFAULT_VOICE_ID` are the fallbacks. + +### VoyageProvider + +```ts +import { VoyageProvider, type VoyageConfig } from '@gemstack/ai-sdk' + +AiRegistry.register(new VoyageProvider({ apiKey: process.env.VOYAGE_API_KEY! })) +``` + +`VoyageConfig`: `apiKey` (required), `baseUrl?` (override `https://api.voyageai.com`), `defaultInputType?` (`'query'` | `'document'`, defaults to `'document'`). Best-in-class embeddings and reranking, over direct HTTP. + +### OpenRouterProvider + +```ts +import { OpenRouterProvider, type OpenRouterConfig } from '@gemstack/ai-sdk' + +AiRegistry.register(new OpenRouterProvider({ + apiKey: process.env.OPENROUTER_API_KEY!, + siteUrl: 'https://my-app.example', // optional: sent as HTTP-Referer + siteName: 'My App', // optional: sent as X-Title +})) +``` + +`OpenRouterConfig`: `apiKey` (required), `baseUrl?` (defaults to `https://openrouter.ai/api/v1`), `siteUrl?`, `siteName?`. OpenAI-compatible, peer `openai`. Chat. + +### BedrockProvider + +```ts +import { BedrockProvider, type BedrockConfig } from '@gemstack/ai-sdk' + +AiRegistry.register(new BedrockProvider({ region: process.env.AWS_REGION ?? 'us-east-1' })) +``` + +`BedrockConfig`: `region` (required), `credentials?` (`{ accessKeyId, secretAccessKey, sessionToken? }`). Prefer the AWS credential chain (env vars, IAM roles) and leave `credentials` unset; set it only for explicit multi-account creds. Peer: `@aws-sdk/client-bedrock-runtime`. Chat; v1 supports Anthropic Claude models on Bedrock (model id starting with `anthropic.`). + +## Gateways and proxies + +Behind an LLM gateway or proxy? If it is OpenAI- or Anthropic-compatible, just set `baseUrl` on the matching provider and you are done: + +```ts +AiRegistry.register(new OpenAIProvider({ + apiKey: process.env.GATEWAY_KEY!, + baseUrl: 'https://gateway.internal/openai/v1', +})) +``` + +If the gateway speaks its own wire format (its own auth scheme and request/response/SSE envelope), reach for the `@gemstack/ai-sdk/gateway` subpath and subclass `HttpGatewayAdapter`. It normalizes an upstream gateway behind the framework's `ProviderAdapter` contract: + +```ts +import { HttpGatewayAdapter, type GatewayRequestContext } from '@gemstack/ai-sdk/gateway' + +class AcmeGatewayAdapter extends HttpGatewayAdapter { + // implement the abstract hooks: build the URL, headers, request body, + // and parse the response / SSE stream into the framework's chunk shape. +} +``` + +The subpath also exports `GatewayAdapterConfig`, `GatewayRequestContext`, and `parseSseStream` (with its `SseEvent` type) for decoding a custom event stream. Wrap your adapter in a small `ProviderFactory` and register it like any other provider. diff --git a/docs/packages/ai-sdk/rag.md b/docs/packages/ai-sdk/rag.md new file mode 100644 index 0000000..761b403 --- /dev/null +++ b/docs/packages/ai-sdk/rag.md @@ -0,0 +1,181 @@ +# Vector Stores & RAG + +Retrieval-augmented generation (RAG) means giving an agent a search tool over a document corpus so it can ground its answers in your own content. `@gemstack/ai-sdk` supports two shapes: + +- **Hosted vector stores** - the provider runs ingestion, chunking, embedding, and search server-side. You upload files, wire the store into an agent, and the model retrieves inline. No tool round-trip, no embedding code. +- **Bring-your-own embeddings + cosine search** - you embed text with `AI.embed(...)`, store the vectors in your own database, and the agent searches them through a tool you supply. More moving parts, full control, no provider lock-in. + +This page also covers the supporting surface: embeddings, embedding caches, and reranking. Provider-specific behavior is kept light here; see [/packages/ai-sdk/providers](/packages/ai-sdk/providers) for which providers implement what. + +Every example assumes a provider is registered: + +```ts +import { AiRegistry, OpenAIProvider } from '@gemstack/ai-sdk' + +AiRegistry.register(new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY! })) +AiRegistry.setDefault('openai/gpt-4o') +``` + +## Hosted vector stores + +`VectorStores` is the façade for managing a provider-hosted store; `fileSearch({ stores })` is the agent tool that searches it. The provider runs ingestion, chunking, embedding, and search; the model invokes the native tool block (OpenAI's `file_search`) and the results land inline in the assistant reply. + +```ts +import { Agent, VectorStores, fileSearch } from '@gemstack/ai-sdk' + +// 1. Create a store and add files (ingest + embed happen provider-side) +const kb = await VectorStores.create('Knowledge Base') // OpenAI by default +await kb.add({ filePath: './report.pdf', attributes: { author: 'Alice', year: 2026 } }) + +// 2. Wire it into an agent +class SupportAgent extends Agent { + model() { return 'openai/gpt-4o' } + tools() { + return [ + fileSearch({ + stores: [kb.id], + where: { author: 'Alice', year: 2026 }, // server-side metadata filter + maxResults: 10, + }), + ] + } +} +``` + +### Managing stores (`VectorStores` / `VectorStore`) + +`VectorStores` is the static façade; each call resolves to a `VectorStore` instance: + +| Call | Returns | Notes | +|---|---|---| +| `VectorStores.create(name, opts?)` | `VectorStore` | `opts` accepts `metadata`, `expiresAfter`, `provider`. | +| `VectorStores.get(id, opts?)` | `VectorStore` | Re-hydrate an existing store by id. | +| `VectorStores.list(opts?)` | `VectorStore[]` | List stores for the provider. | +| `VectorStores.delete(id, opts?)` | `void` | Delete a store (its files are managed separately). | + +On a `VectorStore` instance: + +| Method | Purpose | +|---|---| +| `store.add(opts)` | Attach a file. Pass `fileId`, `filePath`, or `fileBuffer`, plus searchable `attributes` and an optional `chunkingStrategy`. By default waits for ingest + embed to finish (`wait: false` for fire-and-forget). | +| `store.files(opts?)` | List the files attached to the store. | +| `store.remove(fileId)` | Detach a file from the store (does not delete the underlying provider file). | +| `store.delete()` | Delete the store. | + +`store.add(...)` detaching is store-level only; to fully delete the underlying provider file use the [File Manager](#file-management) (`AI.files(provider).delete(id)`). + +### The `fileSearch` tool + +`fileSearch({ stores, where?, maxResults?, name?, description? })` returns a first-class agent tool. On OpenAI the adapter recognizes the tool's provider hint and emits the native `file_search` block, so the search runs server-side and the model never makes a function-call round-trip. + +Metadata filtering uses the `where` option. The sugar form `where: { author: 'Alice', year: 2026 }` is shorthand for the typed OpenAI filter `{ type: 'and', filters: [{ type: 'eq', key: 'author', value: 'Alice' }, { type: 'eq', key: 'year', value: 2026 }] }`. Pass the typed object form directly for `gt` / `lt` / `ne` / `or` operators. The exported `normalizeWhere(where)` performs that lowering and is reused by the adapter (and available to you for tests). `isFileSearchTool(tool)` is a type guard for detecting the tool in a tools array. + +> Hosted file search is provider-specific. A provider that does not recognize the hint sees a plain function-call tool with a `{ query: string }` placeholder schema. To stay portable across hosted and self-hosted, pass a `fallback` (see below) so non-hosted providers run your own cosine search instead. + +## Bring-your-own embeddings + cosine search + +When you do not want a hosted store - to avoid lock-in, keep data on your own infrastructure, or use a local Postgres + pgvector corpus - embed text yourself and search it with a tool you supply. + +### Generating embeddings (`AI.embed`) + +`AI.embed(input, opts?)` returns an `EmbeddingResult` (`{ embeddings: number[][]; usage }`). Pass a string or an array of strings; arrays over 100 inputs are auto-batched: + +```ts +import { AI } from '@gemstack/ai-sdk' + +const { embeddings } = await AI.embed('Project Foo deploys to fly.io', { + model: 'openai/text-embedding-3-small', +}) +//=> embeddings: number[][] (one vector per input) +``` + +Pass `{ cache: true }` to memoize repeated inputs in-process (handy when re-embedding the same query). The provider must implement embeddings; calling `AI.embed` on a provider that does not throws with a clear message (OpenAI, Google, Mistral, Cohere, Voyage, Jina support it). The contract is the `EmbeddingAdapter` interface (`embed(input, model): Promise`), so you can wrap or substitute your own. + +### Caching embeddings (`CachedEmbeddingAdapter`) + +`CachedEmbeddingAdapter` wraps any `EmbeddingAdapter` and caches results by `model:text` key, so repeated inputs skip the provider call (cache hits report zero token usage). `AI.embed({ cache: true })` uses it internally; construct one directly when you manage the adapter yourself: + +```ts +import { CachedEmbeddingAdapter } from '@gemstack/ai-sdk' + +const cached = new CachedEmbeddingAdapter(innerEmbeddingAdapter) +``` + +### Searching your own vectors (`similaritySearch`) + +`similaritySearch({ model, column, embedWith, ... })` is an agent-tool factory: the model emits a natural-language `query`, the tool embeds it with `AI.embed(...)`, runs a vector search over your data, and returns the top rows ranked by similarity. + +It accepts any Model that satisfies the structural `SimilaritySearchModel` interface - the engine calls `model.query()` and the query-builder methods (`whereVectorSimilarTo`, `selectVectorDistance`, `where`, `limit`, `get`) but never imports an ORM package. You bring your own Model with a vector column: + +```ts +import { Agent, similaritySearch } from '@gemstack/ai-sdk' +import { Document } from './app/Models/Document.js' // your own Model, structural fit + +class KnowledgeAgent extends Agent { + tools() { + return [ + similaritySearch({ + model: Document, + column: 'embedding', // vector column on the Model + embedWith: 'openai/text-embedding-3-small', + minSimilarity: 0.7, + limit: 10, + }), + ] + } +} +``` + +Each result is a `SimilarityHit` (`{ row, similarity }`); for the default `cosine` metric, `similarity` is `1 - distance`. + +### Same prompt, hosted or self-hosted (`fallback`) + +`fileSearch({ ..., fallback })` bridges the two worlds: on OpenAI the native `file_search` block runs server-side; on any other provider the tool gains an `execute` that delegates to a `similaritySearch` over your own Model. The agent prompt and tool name stay identical across providers - you change only the registered provider: + +```ts +fileSearch({ + stores: [kb.id], + fallback: { model: Document, column: 'embedding', embedWith: 'openai/text-embedding-3-small' }, +}) +``` + +## Reranking + +Reranking reorders an existing candidate list by relevance to a query - a cheap precision boost after a coarse first-stage retrieval (vector or keyword). `AI.rerank(query, documents, opts?)` returns a `RerankingResult` whose `results` are `{ index, relevanceScore, document }` sorted by relevance: + +```ts +import { AI } from '@gemstack/ai-sdk' + +const { results } = await AI.rerank('how do I reset my password?', candidateChunks, { + model: 'cohere/rerank-v3.5', + topK: 5, +}) +// results[0] -> { index, relevanceScore, document } (most relevant first) +``` + +The fluent builder `Reranker.of(query, documents).model(...).topK(...).rank()` is the same surface if you prefer it. Reranking needs a provider that implements it (Cohere, Voyage, Jina); see [/packages/ai-sdk/providers](/packages/ai-sdk/providers). + +A typical RAG pipeline chains these: `AI.embed` the query, `similaritySearch` (or `fileSearch`) to pull candidates, then `AI.rerank` to tighten the top-K before handing passages to the model. + +## File management + +`AI.files(provider?)` returns a `FileManager` for provider-side file storage - the files hosted stores ingest from, and any other provider file objects: + +| Method | Purpose | +|---|---| +| `files.upload(filePath, opts?)` | Upload a file; `opts.purpose` (e.g. `'assistants'`). Returns `{ id, ... }`. | +| `files.list()` | List uploaded files. | +| `files.retrieve(fileId)` | Fetch file content (not all providers support this). | +| `files.delete(fileId)` | Delete a file. Use this to fully remove a file a vector store only detached. | + +```ts +import { AI } from '@gemstack/ai-sdk' + +const files = AI.files('openai') +const uploaded = await files.upload('./report.pdf', { purpose: 'assistants' }) +await files.delete(uploaded.id) +``` + +## Provider support at a glance + +Hosted file search, embeddings, and reranking are each provider-specific capabilities. Rather than duplicate the matrix here, see [/packages/ai-sdk/providers](/packages/ai-sdk/providers) for which providers implement embeddings, reranking, and hosted vector stores. The portable pattern is: keep the agent's `tools()` and prompt identical, and switch capability by changing the registered provider and model string. diff --git a/docs/packages/ai-sdk/streaming.md b/docs/packages/ai-sdk/streaming.md new file mode 100644 index 0000000..9240036 --- /dev/null +++ b/docs/packages/ai-sdk/streaming.md @@ -0,0 +1,159 @@ +# Streaming + +`agent.stream(...)` runs the same agent loop as [`prompt()`](/packages/ai-sdk/agents) but hands you the tokens as they arrive. It returns an `AgentStreamResponse`: a chunk iterator plus a promise that resolves to the full `AgentResponse` once the loop finishes. + +```ts +import { agent } from '@gemstack/ai-sdk' + +const { stream, response } = agent('You are a helpful assistant.').stream('Tell me a story.') + +for await (const chunk of stream) { + if (chunk.type === 'text-delta') process.stdout.write(chunk.text ?? '') + if (chunk.type === 'tool-call') console.log('Tool called:', chunk.toolCall) + if (chunk.type === 'tool-update') console.log('Progress:', chunk.update) + if (chunk.type === 'tool-result') console.log('Result:', chunk.result) +} + +const final = await response // resolves after the stream has been consumed +``` + +## Chunk shape + +Every value yielded by the stream is a `StreamChunk` discriminated by `chunk.type`: + +| `type` | Carries | Meaning | +|---|---|---| +| `text-delta` | `text` | A slice of assistant text. | +| `tool-call-delta` | `toolCall` (partial), `text`, `toolCallIndex` | Streamed tool-call arguments, before the call is whole. | +| `tool-call` | `toolCall` | A complete tool call the agent decided to make. | +| `tool-update` | `toolCall`, `update` | Per-`yield` progress from a streaming tool (`async function*` handler). Ephemeral: not persisted, not seen by the model on the next step. | +| `tool-result` | `toolCall`, `result` | The value a server-side tool handler returned. | +| `pending-client-tools` | `toolCalls` | Tool calls with no server handler, awaiting a browser round-trip. | +| `pending-approval` | `toolCall`, `isClientTool` | A tool call paused on an approval gate. | +| `handoff` | `handoff` (`{ from, to, message? }`) | Control transferred to another agent. | +| `usage` | `usage` | Token usage. | +| `finish` | `finishReason`, `usage` | The loop ended. | + +`chunk.toolCall` is a `Partial` (`{ id?, name?, arguments? }`), so guard the fields you read. + +## Vercel AI SDK protocol + +For interop with the Vercel AI SDK data-stream wire (the numeric-prefix protocol that `useChat()` reads), convert the chunk iterator: + +```ts +import { agent, toVercelResponse } from '@gemstack/ai-sdk' + +// In a Fetch-style route handler: +const { stream } = agent('You are a helpful assistant.').stream(message) +return toVercelResponse(stream) // text/plain Response, X-Vercel-AI-Data-Stream: v1 +``` + +`toVercelResponse(stream)` wraps a `Response`; `toVercelDataStream(stream)` returns the raw `ReadableStream` if you need to frame the response yourself. Both take the `stream` iterator (`AgentStreamResponse.stream`), not the whole `{ stream, response }` object. + +## Server-Sent Events (named-event protocol) + +When you want a plain `text/event-stream` with self-describing event names (rather than the Vercel numeric-prefix wire), `@gemstack/ai-sdk` ships a matched server/browser pair so the wire vocabulary can never drift between the two ends. Both live in the engine and use only web globals (`ReadableStream`, `Response`, `TextEncoder`), so they run server-side (Node or edge) and in the browser alike. + +### Server + +`toAgentSseResponse(streaming, init?)` projects an `agent.stream()` result onto named events and frames it as a `text/event-stream` `Response`, with the standard no-cache and no-buffering headers set. Because it returns a web `Response`, return it directly from any Fetch-based handler (edge functions, Bun, Deno, Hono, or a Node runtime with a Fetch adapter): + +```ts +import { agent, toAgentSseResponse } from '@gemstack/ai-sdk' + +export async function handler(req: Request): Promise { + const { message } = await req.json() + const streaming = agent('You are a helpful assistant.').stream(message) + return toAgentSseResponse(streaming) // text/event-stream Response +} +``` + +If you need the raw bytes (for example to pipe into a Node `ServerResponse`), use `toAgentSseStream(streaming)`, which returns a `ReadableStream`. + +It emits one named event per loop chunk: `text`, `tool_call`, `tool_update`, `tool_result`, `pending_client_tools`, `tool_approval_required`, `handoff`, then a terminal `complete` event carrying `{ done, finishReason, awaiting, steps, usage }` (or an `error` event if the run throws). `awaiting` is `'client_tools'` or `'approval'` when the loop paused. + +### Browser + +`readAgentStream(response, callbacks?)` decodes those events back into an accumulated `AgentStreamTurn` and fires per-event callbacks: + +```ts +import { readAgentStream } from '@gemstack/ai-sdk' + +const resp = await fetch('/chat', { method: 'POST', body: JSON.stringify({ message }) }) +if (!resp.ok) throw new Error(await resp.text()) // caller owns the error branch + +const turn = await readAgentStream(resp, { + onText: (t) => appendToBubble(t), + onToolCall: (c) => showToolChip(c.tool), +}) + +if (turn.awaiting === 'client_tools') runClientTools(turn.pendingClientTools) +``` + +Pass an already-OK response: `readAgentStream` does not check `resp.ok`, so you own the non-2xx branch (where a rich error body can be read). The resolved `AgentStreamTurn` accumulates `assistantText`, `assistantToolCalls`, `serverToolResults`, `pendingClientTools`, `pendingApproval`, `handoffPath`, `done`, and `awaiting`. Available callbacks are `onText`, `onToolCall`, `onToolUpdate`, `onToolResult`, `onPendingClientTools`, `onToolApprovalRequired`, `onHandoff`, `onComplete`, `onError`, plus `onAppEvent` for any event outside the protocol vocabulary (conversation ids, billing, sub-run fan-out: emit and decode those on your own channel alongside this protocol). + +The reducer is exposed as `applyAgentSseEvent(event, data, turn, callbacks?)` (with `newAgentStreamTurn()` for a fresh turn) so you can unit-test event handling without a live stream. + +## React client (`useAgentRun`) + +`@gemstack/ai-sdk/react` wraps `readAgentStream` in a hook so a component does not hand-roll the same state machine: it drives the stream, accumulates a transcript, tracks status, and surfaces pending client-tool calls and approval gates. React lives behind the subpath (peer `react@>=19.2.0`); the main `@gemstack/ai-sdk` entry stays runtime-agnostic. + +```tsx +import { useAgentRun } from '@gemstack/ai-sdk/react' + +function Chat() { + const { status, outputs, run, pendingApproval, approve, reject } = useAgentRun({ + // The app owns the endpoint + body shape: only your route can rebuild the + // server-side message history from a resume intent. + request: (req, signal) => + fetch('/api/agent', { method: 'POST', body: JSON.stringify(req), signal }), + // Optional: auto-execute client tools in the browser and resume. + clientTools: (call) => runLocalTool(call.name, call.arguments), + }) + + return ( + <> + {outputs.map((o, i) => )} + {pendingApproval && ( + approve(pendingApproval.toolCall.id)} + onNo={() => reject(pendingApproval.toolCall.id)} + /> + )} + + + ) +} +``` + +The hook returns `status` (`'idle'` / `'running'` / `'complete'` / `'error'`), the `outputs` transcript (text, tool calls/results, approval requests, handoffs), `pendingClientTools`, `pendingApproval`, and `error`, plus imperative `run` / `respond` / `approve` / `reject` / `reset`. While paused awaiting client tools (no resolver) or an approval decision, `status` stays `'running'` and the matching `pending*` field is populated until you resume. With a `clientTools` resolver, client-tool pauses auto-resume; approval gates always wait for an explicit `approve` / `reject`. + +The state machine and stream driver are exported framework-free (also from `@gemstack/ai-sdk/react`) for non-React use or tests: `driveAgentRun(req, opts)`, `executeClientTools(calls, resolver)`, and the `appendAgentOutput(outputs, event, data)` transcript reducer. + +## Cancellation + +Pass an `AbortSignal` to cancel an in-flight run. The signal is honored at iteration boundaries and forwarded to the provider adapter so the underlying network request is also cancelled. When the signal aborts, `prompt()` rejects (and `stream()`'s `response` promise rejects) with the signal's reason: + +```ts +const controller = new AbortController() +setTimeout(() => controller.abort(), 5_000) + +try { + await agent('...').prompt('long task', { signal: controller.signal }) +} catch (err) { + // DOMException: This operation was aborted (or TimeoutError for AbortSignal.timeout()) +} + +// Or the standard timeout helper: +await agent('...').prompt('...', { signal: AbortSignal.timeout(10_000) }) +``` + +The same `signal` option works on `stream(...)`: aborting rejects the `response` promise and ends the chunk iterator. In React, `useAgentRun` wires an `AbortController` for you, so `reset()` (or starting a new `run`) aborts any in-flight stream. + +## See also + +- [Agents](/packages/ai-sdk/agents) for `prompt()`, tools, and multi-step loops. +- [Structured Output & Attachments](/packages/ai-sdk/structured-output) for typed results and multi-modal input. +- [Testing](/packages/ai-sdk/testing) for driving streams against the fake. diff --git a/docs/packages/ai-sdk/structured-output.md b/docs/packages/ai-sdk/structured-output.md new file mode 100644 index 0000000..824163b --- /dev/null +++ b/docs/packages/ai-sdk/structured-output.md @@ -0,0 +1,106 @@ +# Structured Output & Attachments + +Two ways to push past plain text: get a typed object back from a run, and send images or documents in. + +## Structured output + +`Output` builds an `OutputWrapper`: a small helper that knows how to (1) instruct the model to emit JSON matching a [Zod](https://zod.dev) schema and (2) parse the model's text back into a validated, typed value. It is a standalone helper, so the flow is explicit: add `output.toSystemPrompt()` to the agent's instructions, then call `output.parse(response.text)` on the result. + +```ts +import { agent, Output } from '@gemstack/ai-sdk' +import { z } from 'zod' + +const output = Output.object({ + schema: z.object({ + sentiment: z.enum(['positive', 'neutral', 'negative']), + score: z.number().min(0).max(1), + }), +}) + +const response = await agent({ + instructions: `Classify the sentiment of the user's message.\n\n${output.toSystemPrompt()}`, +}).prompt('I absolutely love this product!') + +const parsed = output.parse(response.text) +// ^? { sentiment: 'positive' | 'neutral' | 'negative'; score: number } +``` + +`parse(text)` strips an optional ``` ```json ``` markdown fence before parsing, then validates against the Zod schema, so Zod transforms, defaults, and coercion all apply. It throws if the text is not valid JSON or fails schema validation; let that surface (or wrap it) so a malformed model reply is caught rather than silently mistyped. + +### Three output shapes + +| Builder | Returns | Use for | +|---|---|---| +| `Output.object({ schema })` | the object `z.infer` | one structured record | +| `Output.array({ element })` | `z.infer[]` | a list of records | +| `Output.choice({ options })` | one of the literal options | classification into a fixed set | + +```ts +// A list of records +const items = Output.array({ element: z.object({ id: z.number(), title: z.string() }) }) + +// Single-label classification (no JSON: the model replies with one option) +const label = Output.choice({ options: ['bug', 'feature', 'question'] as const }) +const which = label.parse(response.text) // 'bug' | 'feature' | 'question' +``` + +Every wrapper exposes `type` (`'object'` / `'array'` / `'choice'`), the underlying Zod `schema`, `parse(text)`, and `toSystemPrompt()`. `Output.choice` parses the trimmed text directly against a `z.enum`, so its `toSystemPrompt()` asks the model for exactly one option and nothing else. + +## Multi-modal attachments + +Send images and documents alongside a prompt with the `Image` and `Document` classes (exported aliases of `ImageAttachment` and `DocumentAttachment`). Build an attachment, call `.toAttachment()`, and pass the result on the prompt's `attachments` array: + +```ts +import { agent, Image } from '@gemstack/ai-sdk' + +const img = Image.fromBase64(cameraBase64, 'image/jpeg') + +const response = await agent('You describe images.') + .prompt('What is in this photo?', { attachments: [img.toAttachment()] }) +``` + +### Factories + +`Image` and `Document` both build from base64 or a URL; `Document` adds a raw-string factory. The URL factories are async (they fetch the bytes and infer the MIME type from the response). + +```ts +import { Image, Document } from '@gemstack/ai-sdk' + +// Image +const fromB64 = Image.fromBase64(base64, 'image/png') +const fromUrl = await Image.fromUrl('https://example.com/photo.jpg') + +// Document +const fromText = Document.fromString('Quarterly numbers...', 'q3.txt') // text/plain +const docB64 = Document.fromBase64(pdfBase64, 'application/pdf', 'report.pdf') +const docUrl = await Document.fromUrl('https://example.com/report.pdf') +``` + +Each instance offers `.toAttachment()` (for the `attachments` option) and `.toContentPart()` (a `ContentPart` for hand-building a multi-part message). The helpers `attachmentsToContentParts(attachments)` and `getMessageText(content)` are exported for assembling and reading multi-part message content. + +Calling LLM providers directly from a browser or React Native client leaks your API key, so prefer the byte and URL factories on the client and a server-side proxy in production. The main client-side use case is bring-your-own-key desktop apps. + +### Node path helpers + +In a Node runtime, load attachments straight from the filesystem with `@gemstack/ai-sdk/node`. `imageFromPath(path)` and `documentFromPath(path)` read the file, base64-encode it, and infer the MIME type from the extension (`.png`, `.jpg`, `.pdf`, `.md`, `.csv`, and so on): + +```ts +import { agent } from '@gemstack/ai-sdk' +import { imageFromPath, documentFromPath } from '@gemstack/ai-sdk/node' + +const chart = await imageFromPath('./reports/chart.png') +const doc = await documentFromPath('./reports/q3.pdf') + +const response = await agent('You analyze reports.') + .prompt('Summarize the attached report and chart.', { + attachments: [doc.toAttachment(), chart.toAttachment()], + }) +``` + +Both return the same `ImageAttachment` / `DocumentAttachment` instances as the byte factories, so `.toAttachment()` and `.toContentPart()` work identically. The path helpers are Node-only (they use `node:fs`); keep them out of client bundles and use `Image.fromBase64` / `Image.fromUrl` there instead. + +## See also + +- [Agents](/packages/ai-sdk/agents) for the `prompt()` surface and options. +- [Streaming](/packages/ai-sdk/streaming) for token-by-token output. +- [Testing](/packages/ai-sdk/testing) for asserting on structured results against the fake. diff --git a/docs/packages/ai-sdk/testing.md b/docs/packages/ai-sdk/testing.md new file mode 100644 index 0000000..f074bb9 --- /dev/null +++ b/docs/packages/ai-sdk/testing.md @@ -0,0 +1,174 @@ +# Testing & Evals + +`@gemstack/ai-sdk` ships three layers for proving an agent works: + +- **`AiFake`** swaps the registered provider with a programmable mock, so you can test agent wiring with no API key and no network. +- **Observers** let you subscribe to agent lifecycle events for tracing and metrics. +- **The eval framework** (`@gemstack/ai-sdk/eval`) runs a suite of input cases plus assertions against real models, to prove the agent does the right thing, not just that it runs. + +See also: [Agents](/packages/ai-sdk/agents), [Tools](/packages/ai-sdk/tools), [Providers](/packages/ai-sdk/providers). + +## Testing with `AiFake` + +`AiFake.fake()` replaces every registered provider with a mock and sets a default model, so your agent code runs unchanged with no real provider. Call `.restore()` afterward to put the real registry back, so tests do not leak between cases. + +```ts +import { AiFake } from '@gemstack/ai-sdk' + +const fake = AiFake.fake() +fake.respondWith('Mocked response') + +const response = await new MyAgent().prompt('Hello') +// response.text === 'Mocked response' + +fake.restore() +``` + +`respondWith(text)` returns the same text for every call. The default before you set anything is `'fake response'`. + +### Scripting a multi-step loop + +When the agent loops (the model returns tool calls, then text, then more tool calls), script each step with `respondWithSequence(...)`. Step `N` answers the agent's `N`th provider call: + +```ts +fake.respondWithSequence([ + { toolCalls: [{ id: 't1', name: 'lookup', arguments: { id: 42 } }] }, + { text: 'The answer is 42.' }, +]) +``` + +When a step sets `toolCalls`, the `finishReason` defaults to `'tool_calls'`; a text-only step defaults to `'stop'`. Once the sequence is exhausted, later calls fall back to the `respondWith` default. + +### Forcing failures + +`failOnStep(stepIndex, error)` throws on a specific iteration, to exercise failover and error paths. It is independent of the response sequence, so the order in which you call them does not matter: + +```ts +fake.respondWithSequence([ + { toolCalls: [{ id: 't1', name: 'lookup', arguments: {} }] }, + { text: 'recovered' }, +]) +fake.failOnStep(0, new Error('Rate limited')) // first call throws; second succeeds +``` + +### Faking the other capabilities + +The fake also covers the non-chat surfaces, each with a matching `respondWith*`: + +| Method | Fakes | +|---|---| +| `respondWithImage(base64)` | image generation | +| `respondWithAudio(buffer)` | text-to-speech | +| `respondWithTranscription(text)` | speech-to-text | +| `respondWithEmbedding(vectors)` | embeddings | +| `respondWithRanking(results)` | reranking | +| `respondWithFileUpload(result)` | file uploads | +| `respondWithFileSearchResults(opts)` | hosted file-search results | + +### Asserting on what was sent + +After a run, assert on what the agent actually sent the provider: + +```ts +const fake = AiFake.fake() +fake.respondWith('hi') + +await new MyAgent().prompt('Hello there') + +fake.assertPrompted(input => input.includes('Hello')) +fake.restore() +``` + +The assertion helpers each take an optional predicate: `assertPrompted`, `assertNothingPrompted`, `assertImageGenerated`, `assertAudioGenerated`, `assertTranscribed`, `assertEmbedded`, `assertReranked`, and `assertFileUploaded`. + +To make stray prompts a hard error (no ambient `respondWith` default), chain `preventStrayPrompts()` and script every expected call explicitly: + +```ts +const fake = AiFake.fake().preventStrayPrompts() +fake.respondWithSequence([{ text: 'expected reply' }]) +// any call beyond the scripted sequence throws instead of returning a default +``` + +## Observability + +Subscribe to agent lifecycle events through the observer registry on the `@gemstack/ai-sdk/observers` subpath. This is the same surface a tracing or metrics collector hooks into: + +```ts +import { aiObservers } from '@gemstack/ai-sdk/observers' + +const unsubscribe = aiObservers.subscribe(event => { + if (event.kind === 'agent.step.completed') { + console.log(`step ${event.iteration}: ${event.tokens.total} cumulative tokens`) + } + if (event.kind === 'agent.completed') { + console.log(`done in ${event.duration}ms, ${event.steps.length} steps`) + } +}) +``` + +Event kinds: + +- **`agent.step.completed`** fires after each loop iteration, with that step's tools called, finish reason, and cumulative usage. Useful for streaming progress to a UI without waiting for the full run. +- **`agent.completed`** fires once after a successful run, with the full step history and final usage. +- **`agent.failed`** fires once, with `error` set, when the run throws or aborts. +- **`agent.eval.completed`** fires per eval case (see below), so collectors can aggregate pass-rate over time. + +Each step's `toolCalls[]` carries a `duration` field (wall-clock milliseconds spent in the tool handler), so you can attribute latency to specific tools. + +## Evals against real models + +`AiFake` proves the wiring works. **Evals** prove the agent does the right thing on real models. Define a suite of input cases and assertions, then run it. Eval suites use the same `Agent` instances as your app, so there is one source of truth. + +```ts +// evals/support-agent.eval.ts +import { evalSuite, llmJudge, exactMatch, regex } from '@gemstack/ai-sdk/eval' +import { SupportAgent } from '../app/Agents/SupportAgent.js' + +export default evalSuite('SupportAgent', { + agent: () => new SupportAgent(), + cases: [ + { name: 'password reset', input: 'How do I reset my password?', + assert: llmJudge('mentions a password reset link or email') }, + { name: 'price', input: 'How much?', + assert: exactMatch('$99/month') }, + { name: 'support email', input: 'Contact?', + assert: regex(/support@example\.com/) }, + ], +}) +``` + +### Running a suite programmatically + +`evalSuite(...)` returns a suite object; run it with `runSuite(suite)`. It resolves to a `SuiteReport` with per-case pass/fail, score, tokens, cost, and duration. Pair it with a reporter: + +```ts +import { runSuite, reportConsole, reportJson, reportHtml } from '@gemstack/ai-sdk/eval' +import suite from './evals/support-agent.eval.js' + +const report = await runSuite(suite) + +reportConsole(report) // human-readable summary to console +const envelope = reportJson(report) // CI-friendly JSON envelope +const html = reportHtml(report) // self-contained HTML string +``` + +Drive your own pass/fail gate off the report (for example, exit non-zero when `report.failed > 0`) so evals can run in CI without any framework CLI. `reportConsole` returns the report unchanged, so you can chain it inline. + +### Built-in metrics + +An assertion is a `Metric`: `(response, ctx) => MetricResult`, where `MetricResult` is `{ pass, score?, reason? }`. Sync or async both work. The built-ins: + +- `exactMatch(string)` and `regex(RegExp)` are surface checks on `response.text`. +- `llmJudge(criterion, opts?)` uses a small-model judge for fuzzy "did the answer mention X?" assertions. +- `jsonShape(zodSchema)` is a strict structural assertion: it strips code fences and runs zod `safeParse`, surfacing the failing path. +- `semanticMatch(reference, opts?)` embeds the reference and response and compares by cosine similarity against `opts.threshold` (default `0.85`). Requires a registered provider with embeddings. +- `tokenCost(threshold)` passes when `response.usage.totalTokens <= threshold`, to catch prompt-size regressions. +- `compose(...metrics)` runs metrics in order with first-failure short-circuit, for example `compose(jsonShape(Schema), tokenCost(800))`. + +User metrics are first-class: any `(response, ctx) => MetricResult` qualifies. The `ctx` carries the case `input` and `caseName` so a custom metric can log or branch on them. + +### Eval observability + +`runSuite` emits an `agent.eval.completed` observer event after every case (including skipped ones), so a metrics collector subscribed to `aiObservers` can aggregate pass-rate per `(suite, case)` over time, exactly like the lifecycle events above. + +> The `evalSuite` / `runSuite` / metrics / reporters API documented here is the programmatic engine surface. The Rudder framework adds an `ai:eval` CLI on top of it for record/replay and discovery; that command lives in `@rudderjs/ai`, not in this engine. diff --git a/docs/packages/ai-sdk/tools.md b/docs/packages/ai-sdk/tools.md new file mode 100644 index 0000000..0a6af6e --- /dev/null +++ b/docs/packages/ai-sdk/tools.md @@ -0,0 +1,129 @@ +# Tools + +Tools let an agent call your code. Define a tool with `toolDefinition(...)`, declare its input schema with Zod, and attach a `.server()` handler: + +```ts +import { toolDefinition } from '@gemstack/ai-sdk' +import { z } from 'zod' + +const searchTool = toolDefinition({ + name: 'search_users', + description: 'Search users by name or email', + inputSchema: z.object({ + query: z.string().describe('Name or email substring'), + limit: z.number().int().min(1).max(50).default(10), + }), +}).server(async ({ query, limit }) => { + return db.users.search(query, limit) +}) +``` + +The agent decides when to call tools based on the prompt. Tool calls and results both flow through the response: inspect `response.steps` for the full trace, including each call's `duration` (wall-clock ms spent in the handler). + +**Argument validation.** The agent validates each tool call's arguments against `inputSchema` before invoking `.server(...)`, so your handler always receives the parsed value (Zod transforms, defaults, and coercion all apply). When validation fails, the agent feeds an `InvalidToolArgumentsError` back to the model as the tool result so it can correct itself on the next step; your handler never runs with malformed input. + +## Streaming tools + +A `.server()` handler may be a plain async function or an `async function*` generator. Each `yield` surfaces as a `tool-update` chunk on the agent's stream, so a long-running tool can report progress before it returns its final value: + +```ts +const ingest = toolDefinition({ + name: 'ingest', + description: 'Ingest and index a document', + inputSchema: z.object({ url: z.string() }), +}).server(async function* ({ url }) { + yield { status: 'downloading' } + const doc = await fetchDoc(url) + yield { status: 'indexing', pages: doc.pages } + return await index(doc) +}) +``` + +## Parallel execution within a step + +When the model emits more than one tool call in a single step, their `.server()` handlers run concurrently by default. Streamed chunk order is still preserved: tool A's `tool-call -> tool-update* -> tool-result` always precedes B's, so consumers see deterministic sequences regardless of which tool finishes first. Approval gates and `onBeforeToolCall` middleware decisions still resolve serially in tool-call order before any handler runs. Opt out when tools share non-idempotent state: + +```ts +await agent('...').prompt('go', { parallelTools: false }) +``` + +Or per agent class: + +```ts +class CounterAgent extends Agent { + parallelTools() { return false } + // ... +} +``` + +## Client tools + +Omit `.server()` and the tool becomes a *client* tool: the agent loop pauses when the model calls it, surfacing the call as `pendingClientToolCalls` on the response so a browser (or any caller) can execute it and resume. This is how you run a capability that only exists on the client (reading the DOM, a local file picker, device APIs) without leaking it to the server. + +```ts +const pickFile = toolDefinition({ + name: 'pick_file', + description: 'Ask the user to choose a local file', + inputSchema: z.object({ accept: z.string().optional() }), +}) +// no .server() - the loop pauses and reports this call to the caller +``` + +When `response.pendingClientToolCalls` is populated, run the call wherever it belongs, then resume the run with the tool result. The cross-request resume protocol for top-level runs is covered under [Standalone run persistence](/packages/ai-sdk/agents); the streaming SSE wire that carries these pauses is in [Streaming](/packages/ai-sdk/streaming). + +## Approval gates + +Mark a tool `needsApproval: true` to require a human decision before its handler runs. When the model calls it, the loop pauses with a pending-approval signal instead of executing; the caller approves or rejects, and only an approval lets the handler proceed. + +```ts +const refund = toolDefinition({ + name: 'issue_refund', + description: 'Issue a refund to a customer', + inputSchema: z.object({ orderId: z.string(), amount: z.number() }), + needsApproval: true, +}).server(async ({ orderId, amount }) => { + return db.refunds.create(orderId, amount) +}) +``` + +Approval decisions resolve serially in tool-call order, ahead of any parallel handler execution. The pause surfaces on the stream as a pending-approval chunk and on the response so a UI can render a confirm card; see [Streaming](/packages/ai-sdk/streaming) for the wire format and [Running agents](/packages/ai-sdk/agents) for resuming an approval-paused run across an HTTP boundary. + +## Scoped tools: one tool, many capabilities + +Function-calling APIs (OpenAI, DeepSeek, and others) do not reliably honor a top-level `oneOf` in a tool's input schema, so a tool that exposes several distinct capabilities cannot be modeled cleanly as a discriminated union. `scopedTool(...)` collapses N named capability branches into one flat function-call schema with a `sub_tool` discriminator enum, then dispatches to the right branch at call time: + +```ts +import { scopedTool, capability } from '@gemstack/ai-sdk' +import { z } from 'zod' + +const search = scopedTool({ + name: 'search', + description: 'Run a search across one of several engines.', + capabilities: { + web: capability({ + description: 'Web results', + input: z.object({ query: z.string(), page: z.number().optional() }), + handler: async ({ query }) => webSearch(query), + }), + images: capability({ + description: 'Image results', + input: z.object({ query: z.string(), safe: z.boolean() }), + handler: async ({ query, safe }) => imageSearch(query, safe), + }), + }, +}) +``` + +The generated schema is a single object: the discriminator (`sub_tool: 'web' | 'images'`) plus the union of every branch's fields. A field is top-level `required` only when every branch requires it (here `query`); fields that belong to a subset of branches (here `safe`) are optional at the top level and annotated with the capabilities that use them, and the chosen branch's required fields are validated in code before its handler runs. An unknown or disabled discriminator value is rejected with a clear `ScopedToolError` the model can correct on its next step. + +- `capability({ input, handler, description? })` infers each branch's input type so the `handler` parameter is typed without annotation. Handlers may be plain async functions or `async function*` generators (which stream `tool-update` chunks, exactly like `.server()`). +- `discriminator` overrides the field name (default `'sub_tool'`). +- `allow: ['web']` exposes only a subset of declared capabilities: both the enum and the runtime dispatch honor it (per-plan gating). + +`scopedTool(...)` returns a normal server tool, so it drops straight into an agent's `tools()` array alongside `toolDefinition(...)` tools. The lower-level `flattenCapabilities(...)` is exported for inspecting or unit-testing the generated flat plan directly. + +## Pitfalls + +- **Tool handlers throwing.** The agent gets the error message back as the tool result. Catch known errors inside the handler and return a structured failure shape instead of throwing. +- **Non-idempotent parallel tools.** Handlers in one step run concurrently by default. Set `parallelTools: false` when they share mutable state. +- **Client tools need a resume path.** A tool with no `.server()` pauses the loop; the run only completes once the caller returns its result. See [Running agents](/packages/ai-sdk/agents). diff --git a/docs/packages/ai-skills.md b/docs/packages/ai-skills.md new file mode 100644 index 0000000..8f0b7e4 --- /dev/null +++ b/docs/packages/ai-skills.md @@ -0,0 +1,166 @@ +# @gemstack/ai-skills + +Portable capability bundles for [`@gemstack/ai-sdk`](/packages/ai-sdk/) agents. A **skill** is a shippable folder (instructions, tools, and reference files) that you compose onto an `Agent` on demand. It mirrors the Anthropic Agent Skills shape: a skill authored for Claude loads here, and a GemStack skill ships as a plain folder. + +```bash +pnpm add @gemstack/ai-skills @gemstack/ai-sdk +``` + +## What a skill is + +A skill is a directory with one required file and two optional pieces: + +``` +my-skill/ + SKILL.md # YAML frontmatter (name, description, trigger, ...) + markdown instructions + tools.ts # optional: exports @gemstack/ai-sdk tool() objects (loaded compiled, see caveat) + resources/ # optional: reference files +``` + +The skill's instructions become extra system-prompt text, its tools become extra agent tools, and its resources travel alongside as reference material. Skills augment an agent; the agent's own declarations stay authoritative. + +## The skill manifest + +`SKILL.md` is markdown with a YAML frontmatter block, the same convention `@gemstack/ai-sdk` ships in `boost/skills`: + +```markdown +--- +name: refunds +description: Issue and look up customer refunds +trigger: handling a refund request or refund status question +metadata: + author: acme +--- + +# Refunds + +When a customer asks for a refund, look up the order first, then issue the +refund with the `issue_refund` tool. Never refund more than the order total. +``` + +The frontmatter is validated into a `SkillManifest`: + +| Field | Required | Purpose | +|---|---|---| +| `name` | yes | Unique skill id (kebab-case by convention, e.g. `pdf-forms`). | +| `description` | yes | One-line summary, used to decide relevance during discovery. | +| `trigger` | no | Natural-language cue for when to load the skill (progressive disclosure). | +| `skip` | no | When NOT to load it (points at a sibling skill instead). | +| `appliesTo` | no | Free-form hints (package names / globs); documents intent, not enforced. | +| `license` | no | SPDX license id. | +| `metadata` | no | Arbitrary author metadata, passed through untouched. | + +`parseSkillManifest(source)` splits a `SKILL.md` string into its validated `{ manifest, instructions }`; a malformed frontmatter throws a `SkillManifestError`. + +## Authoring `tools.ts` + +A co-located `tools.ts` exports the skill's tools as plain `@gemstack/ai-sdk` `tool()` objects, so there is one tool API across the framework (see [tools](/packages/ai-sdk/tools)): + +```ts +import { toolDefinition } from '@gemstack/ai-sdk' +import { z } from 'zod' + +export const issueRefund = toolDefinition({ + name: 'issue_refund', + description: 'Issue a refund for an order', + inputSchema: z.object({ orderId: z.string(), amount: z.number() }), +}).server(async ({ orderId, amount }) => { + return await refunds.create(orderId, amount) +}) +``` + +> **Compiled-output caveat.** The loader imports the skill's tools module at runtime, so it resolves the *compiled* output (`tools.js` / `tools.mjs` / `tools.cjs`), not `tools.ts`. Author in TypeScript and build the skill folder, or ship the compiled file alongside `SKILL.md`. The `SKILL.md` instructions and `resources/` stay portable as-is; only the typed tools module needs a build step. + +## Composing skills onto an agent + +The ergonomic path is `SkillfulAgent`. You declare your base identity and own tools in the `base*` hooks, and list the skills in `skills()`; skills augment, your own declarations win: + +```ts +import { loadSkill, SkillfulAgent } from '@gemstack/ai-skills' + +const refunds = await loadSkill('./skills/refunds') + +class SupportAgent extends SkillfulAgent { + baseInstructions() { return 'You are a friendly support agent.' } + skills() { return [refunds] } + baseTools() { return [escalateTool] } // wins over a same-named skill tool +} + +const reply = await new SupportAgent().prompt('I want a refund for order #123') +``` + +`SkillfulAgent` exposes four authoring hooks: + +| Hook | Returns | Notes | +|---|---|---| +| `baseInstructions()` | `string` | Your agent's identity. Skill instructions are appended after it. Required. | +| `skills()` | `LoadedSkill[]` | The skills composed onto this agent. Defaults to `[]`. | +| `baseTools()` | `AnyTool[]` | Your own tools, authoritative on a name collision with a skill tool. | +| `baseMiddleware()` | `AiMiddleware[]` | Your own middleware, runs before any skill-contributed middleware. | + +> **Override the `base*` hooks, not `instructions()` / `tools()` / `middleware()`.** Those three are sealed finals on `SkillfulAgent`: they merge your `base*` declarations with the skills. Overriding them directly drops the skill composition. + +Because loading a skill is async (file IO plus importing the tools module) and these hooks are synchronous, load skills once at module init and return the already-loaded objects from `skills()`. + +### Low-level composition + +If you can't extend `SkillfulAgent` (you use the anonymous `agent()` factory, or already extend another base), the same merge is available as plain functions, `composeInstructions` / `composeTools` / `composeMiddleware`: + +```ts +import { Agent } from '@gemstack/ai-sdk' +import { composeInstructions, composeTools } from '@gemstack/ai-skills' + +const skills = [refunds] + +class SupportAgent extends Agent { + instructions() { return composeInstructions('You are a support agent.', skills) } + tools() { return composeTools([escalateTool], skills) } +} +``` + +`SkillfulAgent` is sugar over these. See [agents](/packages/ai-sdk/agents) for the underlying `Agent` base. + +## Loading skills + +`loadSkill(dir, opts?)` reads a skill directory and returns a `LoadedSkill` with its parts ready to compose: + +```ts +import { loadSkill } from '@gemstack/ai-skills' + +const refunds = await loadSkill('./skills/refunds') +refunds.instructions // markdown body (string) +refunds.tools // ai-sdk tool() objects +refunds.resources // [{ name, path }, ...] +``` + +`loadSkills(dirs)` loads several at once. `LoadSkillOptions` includes `loadTools: false` to load instructions and resources without importing (and therefore running) the tools module, and `toolsFile` to point at a non-default tools filename. + +## Discovery (progressive disclosure) + +`SkillRegistry` indexes skills by their cheap frontmatter and loads a skill's full body plus tools only when you ask for it, so you can index hundreds of skills and pay for only the ones you compose: + +```ts +import { SkillRegistry } from '@gemstack/ai-skills' + +const registry = new SkillRegistry() +await registry.discover('./skills') // reads frontmatter only, runs no skill code +registry.list() // [{ manifest, dir }, ...] + +const refunds = await registry.load('refunds') // now imports the compiled tools module +``` + +A malformed or unreadable `SKILL.md` is skipped rather than failing the whole scan; pass `discover(root, { onError })` to observe what was skipped. + +## Trust model + +A skill is code you install or author, like a Vite or ESLint plugin: **loading it runs its code** (the tools module). There is no in-process sandbox (Node's `vm` is not a security boundary). The package keeps the boundary honest instead of pretending to enforce it: + +- **No auto-loading of untrusted directories.** You pass explicit paths to `loadSkill` / `discover`; nothing is scanned implicitly. +- **Surface before compose.** `discover()` reads only frontmatter (no code runs). `loadSkill(dir, { loadTools: false })` loads instructions and resources without importing the tools module. `surface(skill)` returns a `SkillSurface` (instructions size, tool names, resource names) so you can inspect before attaching; `surfaceAll(skills)` does the set. +- **The risky moment stays gated.** Skill tools are ordinary `ai-sdk` tools, so tool execution still flows through the agent's existing approval / middleware flow. + +If you need real isolation, run the app under OS or container isolation, and only load skills from sources you trust. + +## License + +MIT diff --git a/docs/packages/index.md b/docs/packages/index.md new file mode 100644 index 0000000..e7fefc0 --- /dev/null +++ b/docs/packages/index.md @@ -0,0 +1,35 @@ +# The GemStack family + +All packages publish under the **`@gemstack/`** scope (e.g. `npm install @gemstack/ai-sdk`). Each is standalone and framework-agnostic; they compose, but you adopt only what you need. + +| Package | Description | +|---|---| +| [`ai-sdk`](/packages/ai-sdk/) | The agent runtime: providers, the agent loop, tools, streaming, middleware, structured output, memory, and evals. The engine the rest of the AI family builds on. | +| [`ai-skills`](/packages/ai-skills) | Portable capability bundles: load `SKILL.md` skills (instructions + tools + resources) and compose them onto an agent on demand. | +| [`ai-autopilot`](/packages/ai-autopilot) | Orchestration: a Supervisor that plans, dispatches subagents (bounded concurrency + budget guardrails), and synthesizes the result. | +| [`ai-mcp`](/packages/ai-mcp) | The agent/MCP bridge: consume a remote MCP server's tools as agent tools, and expose an agent as an MCP server. | +| [`mcp`](/packages/mcp) | A standalone framework for *authoring* MCP servers: tools, resources, prompts, decorators, OAuth 2.1, a framework-neutral HTTP handler, and a test client. Agent-agnostic. | + +## How they fit together + +``` +ai-sdk agent runtime (the "verbs") +ai-skills capability bundles (the composable "nouns") -> ai-sdk +ai-autopilot orchestration / autonomy (the "director") -> ai-sdk (+ skills) +ai-mcp agent <-> MCP bridge (the "adapter") -> ai-sdk +----------------------------------------------------------------------------------- +mcp standalone MCP server framework agent-agnostic, not ai-* +``` + +### Two MCP packages, two jobs + +`ai-mcp` and `mcp` both touch the Model Context Protocol, but from opposite ends: + +- **`ai-mcp`** is the *agent bridge*. It depends on `ai-sdk` and is useless without an agent: feed a remote MCP server's tools into an agent, or expose an agent as an MCP server. +- **`mcp`** is for *authoring* MCP servers from scratch - tools, resources, prompts, OAuth - and knows nothing about agents. + +Both can "produce an MCP server", but from different inputs (`mcpServerFromAgent(anAgent)` versus a hand-authored server). That overlap is expected, not duplication. + +## Versioning + +Each package versions independently via Changesets. The API is settling toward `1.0` in the open; the AI family currently tracks the `0.x` line while contracts stabilize. See the [releases](https://github.com/gemstack-land/gemstack/releases) for changelogs. diff --git a/docs/packages/mcp.md b/docs/packages/mcp.md new file mode 100644 index 0000000..0539a28 --- /dev/null +++ b/docs/packages/mcp.md @@ -0,0 +1,332 @@ +# @gemstack/mcp + +An agent-agnostic framework for **authoring Model Context Protocol (MCP) servers** in TypeScript: declare tools, resources, and prompts as classes; serve them over a framework-neutral HTTP handler or stdio; protect them with OAuth 2.1. No framework required. + +Once you author an MCP server, an external AI agent (Claude Code, Cursor, Windsurf, any MCP-compatible client) can query your database, kick off jobs, fetch documents, and run domain-specific commands without leaving its chat UI. + +It is standalone and dependency-light: its only runtime dependencies are `@modelcontextprotocol/sdk`, `zod`, and `reflect-metadata`. (It graduated from the mature `@rudderjs/mcp` server framework, re-versioned under the GemStack umbrella.) + +## Which MCP package do I want? + +There are two MCP packages in GemStack, on opposite axes; don't conflate them: + +| Package | Axis | Use it to... | +|---|---|---| +| **`@gemstack/mcp`** (this) | **server authoring** | Build an MCP *server*: hand-author tools/resources/prompts and serve them. Agent-agnostic, depends on no AI runtime. | +| [`@gemstack/ai-mcp`](/packages/ai-mcp) | agent / MCP bridge | Consume remote MCP tools as [`@gemstack/ai-sdk`](/packages/ai-sdk/) Agent tools, or expose a single Agent as an MCP server. Depends on `@gemstack/ai-sdk`. | + +## Install + +```bash +npm install @gemstack/mcp +``` + +`reflect-metadata` must be imported once at your entry point (the decorators rely on it): + +```ts +import 'reflect-metadata' +``` + +## Quick start + +Define a tool and a server. You register the tool **classes**, not instances: + +```ts +import { McpServer, McpTool, McpResponse, Name, Description } from '@gemstack/mcp' +import { z } from 'zod' + +@Description('Echo a message back to the caller') +class EchoTool extends McpTool { + schema() { return z.object({ message: z.string() }) } + async handle(input: { message: string }) { + return McpResponse.text(input.message) + } +} + +@Name('demo') +class DemoServer extends McpServer { + protected tools = [EchoTool] +} +``` + +A tool's name is derived from its class name (kebab-case, minus a trailing `Tool`), so `EchoTool` is `echo` and `CurrentWeatherTool` is `current-weather`. Override `name()` or use `@Name` to set it explicitly. + +## The three primitives + +An MCP server exposes three kinds of capabilities: + +- **Tools** (`McpTool`) - functions the agent calls (most common). +- **Resources** (`McpResource`) - data the agent reads (URIs the agent can fetch). +- **Prompts** (`McpPrompt`) - reusable prompt templates the agent loads. + +A server declares each as an array of classes: + +```ts +import { McpServer, Name } from '@gemstack/mcp' + +@Name('weather') +class WeatherServer extends McpServer { + protected tools = [CurrentWeatherTool, ForecastTool] + protected resources = [LatestReport] + protected prompts = [WeatherSummaryPrompt] +} +``` + +Server identity comes from `@Name`, `@Version`, and `@Instructions` decorators (or by overriding `metadata()`); `version` defaults to `'1.0.0'` and `name` to the class name. + +## Tools with rich input + +Zod schemas drive what the agent sees: + +```ts +@Description('Search posts by query string and tag.') +class SearchPostsTool extends McpTool { + schema() { + return z.object({ + query: z.string().describe('Full-text search query'), + tags: z.array(z.string()).optional().describe('Filter by tags'), + limit: z.number().int().min(1).max(50).default(10), + }) + } + + async handle({ query, tags, limit }) { + const posts = await searchPosts(query, { tags, limit }) + return McpResponse.json(posts) + } +} +``` + +`McpResponse` builds the result shape a tool's `handle()` returns: + +- `McpResponse.text(string)` - a plain-text result. +- `McpResponse.json(data)` - a structured result, serialized as pretty-printed JSON text. +- `McpResponse.error(message)` - an error result (`isError: true`, prefixed with `Error: `). The client sees a failed tool call rather than a thrown exception, so prefer it for expected, user-facing failures (validation, not-found) and reserve throwing for unexpected faults. + +A tool may also declare an optional `outputSchema()` to advertise the structure of its response. + +## Dependency injection - `@Handle` + +A tool / resource / prompt method can ask for dependencies beyond its first argument. Mark the method with `@Handle(...)` and construct the server with a **resolver**: + +```ts +import { McpServer, McpTool, McpResponse, Handle, createResolver } from '@gemstack/mcp' + +class Logger { info(msg: string) { console.log(msg) } } + +class LogTool extends McpTool { + schema() { return z.object({ message: z.string() }) } + @Handle(Logger) + async handle(input: { message: string }, log: Logger) { + log.info(input.message) + return McpResponse.text('logged') + } +} + +class LogServer extends McpServer { protected tools = [LogTool] } + +const resolver = createResolver().register(Logger, new Logger()) +const server = new LogServer({ resolver }) +``` + +The resolver is **instance-scoped**: it is passed at construction and never read off a global. Wire it to any container (Awilix, tsyringe, InversifyJS) with a one-function adapter implementing `McpResolver = { resolve(token): unknown }`: + +```ts +import { createContainer, asValue } from 'awilix' +import type { McpResolver } from '@gemstack/mcp' + +const container = createContainer().register({ logger: asValue(new Logger()) }) +const resolver: McpResolver = { resolve: (token) => container.resolve((token as { name: string }).name) } +new LogServer({ resolver }) +``` + +If a `@Handle` method requests a dependency and no resolver is provided (or the resolver yields `undefined`), the call fails loudly, naming the member and token; it never injects `undefined`. + +> The `@Description` decorator works on classes, and `@Handle` works on `handle()` with explicit tokens. Other method-level decorators that need `design:paramtypes` are unreliable under bundlers such as Vite, so the supported method decorator (`@Handle`) takes its tokens explicitly rather than relying on reflected parameter types. + +## Conditional registration + +Hide a primitive when a feature flag is off, in dev mode, or under any other static condition, via `shouldRegister()`: + +```ts +class ExperimentalTool extends McpTool { + schema() { return z.object({}) } + async handle() { return McpResponse.text('experimental') } + shouldRegister() { return process.env.FEATURE_EXPERIMENTAL === 'true' } +} +``` + +Returning `false` hides the primitive from `tools/list` **and** blocks `tools/call` (returning an "unknown tool" error), so a direct call can't bypass the gate. The same hook works on `McpResource` and `McpPrompt`, and async hooks are supported. The hook runs with no arguments today; per-request gating (auth-scoped tools) is roadmap work. + +## Behavior annotations + +Tools may carry MCP-spec hints that clients use to decide whether to auto-approve, batch, or sandbox a call. Apply them as decorators: + +```ts +import { IsReadOnly, IsDestructive, IsIdempotent, IsOpenWorld } from '@gemstack/mcp' + +@IsReadOnly() @IsIdempotent() class GetUserTool extends McpTool { /* ... */ } +@IsDestructive() @IsOpenWorld() class DeleteFileTool extends McpTool { /* ... */ } +``` + +Both `true` and `false` carry meaning per the spec, so the decorators take an explicit value: `@IsReadOnly()` is `true`, `@IsReadOnly(false)` is `false`, and no decorator omits the hint entirely. The hints are advisory; clients still apply their own policy. + +Resources accept three protocol-level annotations: `@Audience('user' | 'assistant')`, `@Priority(0..1)`, and `@LastModified(string | Date)`. Clients use them to rank and surface resources in their UI. + +## Streaming progress + +For long-running tools, stream progress back to the agent with an async-generator `handle()`: + +```ts +async *handle({ url }) { + yield { progress: 0, message: 'Fetching...' } + const html = await fetchUrl(url) + yield { progress: 50, message: 'Parsing...' } + const text = parseHtml(html) + yield { progress: 100, message: 'Done' } + return McpResponse.text(text) +} +``` + +An `async function*` handler yields `McpToolProgress` objects (`{ progress, total?, message? }`) and returns the final result. The runtime forwards the yields as `notifications/progress` when the calling client supplied a `progressToken`; a streaming tool that runs without one still executes, and the yields are dropped silently. The handler does not take a "send" callback parameter (it mirrors the `@gemstack/ai-sdk` streaming-tool pattern). + +## Resources and prompts + +```ts +import { McpResource, McpPrompt } from '@gemstack/mcp' + +@Description('Latest weather report') +class LatestReport extends McpResource { + uri() { return 'weather://latest' } + async handle() { return await fetchLatestReport() } // returns a plain string +} + +@Description('Compose a weather summary') +class WeatherSummaryPrompt extends McpPrompt { + arguments() { return z.object({ location: z.string() }) } + async handle({ location }) { + return [{ role: 'user' as const, content: `Summarize today's weather in ${location}.` }] + } +} +``` + +A resource's `handle()` returns a plain string (its body); a prompt's `handle()` returns an array of `{ role, content }` messages (`McpPromptMessage[]`). Resources can use URI templates: `weather://location/{city}` is matched against `weather://location/paris` and exposes `{ city: 'paris' }` to `handle(params)`. + +## Exposing the server + +The package ships framework-neutral handlers so you can serve a server over raw `node:http`, any Fetch-style runtime, or stdio, with no framework in the path. + +### Raw `node:http` (and Express / Connect) + +`createMcpHttpHandler` returns a plain `(req, res)` handler over the MCP Streamable HTTP transport: + +```ts +import { createServer } from 'node:http' +import { createMcpHttpHandler } from '@gemstack/mcp' + +const handler = createMcpHttpHandler(new DemoServer()) +createServer((req, res) => { void handler(req, res) }).listen(3000) +``` + +Because it is a `(req, res)` handler, it also mounts on Express or Connect. + +### Fetch / Web (Hono, Vike, edge runtimes) + +For any runtime that speaks the Web Standard `Request` / `Response`, use `createWebRequestHandler` from the `@gemstack/mcp/runtime` subpath; it returns `(request: Request) => Promise`: + +```ts +import { Hono } from 'hono' +import { createWebRequestHandler } from '@gemstack/mcp/runtime' + +const handler = createWebRequestHandler(new DemoServer()) +const app = new Hono() +app.all('/mcp', (c) => handler(c.req.raw)) +``` + +By default each new client gets its own transport (stateful sessions). Pass `sessionIdGenerator: undefined` for **stateless** mode, where a single transport is created lazily and reused for the handler's lifetime. `createMcpHttpHandler` is built on top of this Web handler. + +### stdio + +For a CLI / local server (e.g. spawned by Claude Desktop), use `startStdio` from `@gemstack/mcp/runtime`: + +```ts +import { startStdio } from '@gemstack/mcp/runtime' + +await startStdio(new DemoServer()) +``` + +> **Runnable example.** [`examples/mcp-quickstart`](https://github.com/gemstack-land/gemstack/tree/main/examples/mcp-quickstart) is a complete, framework-neutral server (tool, resource, prompt, `@Handle` DI, OAuth 2.1) served over both `node:http` and Hono, with a CI smoke test and zero framework dependencies. + +## OAuth 2.1 + +Protect a web endpoint with bearer tokens. The core is auth-agnostic: you supply a `verifyToken` that validates the JWT (signature, expiry, revocation) and returns its claims, or `null`/throws when invalid. Back it with any JWT library (`jose` shown here), a token-introspection endpoint, or a framework's auth integration. + +Two pieces work together, and you need **both**: + +1. `oauth2McpMiddleware('/mcp', options)` guards the MCP endpoint and, on failure, returns an RFC 9728 `WWW-Authenticate` challenge. +2. `registerOAuth2Metadata(router, '/mcp', options)` serves the protected-resource metadata document at `/.well-known/oauth-protected-resource/mcp` that the challenge points clients to. Without it, compliant clients can't discover the authorization server. + +```ts +import { oauth2McpMiddleware, registerOAuth2Metadata } from '@gemstack/mcp' +import { createRemoteJWKSet, jwtVerify } from 'jose' + +const JWKS = createRemoteJWKSet(new URL('https://issuer.example.com/.well-known/jwks.json')) + +const options = { + scopes: ['mcp.read'], + scopesSupported: ['mcp.read', 'mcp.write'], + authorizationServers: ['https://issuer.example.com'], + verifyToken: async (jwt: string) => { + try { + const { payload } = await jwtVerify(jwt, JWKS, { audience: 'https://api.example.com/mcp' }) + // map your token's claims onto { sub?, scopes? } + return { sub: payload.sub, scopes: String(payload['scope'] ?? '').split(' ').filter(Boolean) } + } catch { + return null // invalid/expired -> 401 + } + }, +} + +// Express/Connect-style wiring: +app.use('/mcp', oauth2McpMiddleware('/mcp', options)) +registerOAuth2Metadata(app, '/mcp', options) +``` + +On success the verified claims are attached to the request as `req.mcpAuth` (`{ sub?, scopes?, claims }`). A missing or invalid token yields `401 invalid_token`; a valid token missing a required scope yields `403 insufficient_scope`. Match your IdP's token config to the `scopes` you require. + +## Testing + +`McpTestClient` exercises a server's tools, resources, and prompts in-process, with no transport, so assertions run the same dispatch path the HTTP transport uses: + +```ts +import { McpTestClient } from '@gemstack/mcp/testing' + +const client = new McpTestClient(DemoServer) +const result = await client.callTool('echo', { message: 'hi' }) +// result.content[0].text === 'hi' + +// With DI: +const client2 = new McpTestClient(LogServer, { resolver }) +``` + +Beyond `callTool`, the client offers `listTools` / `listResources` / `listPrompts`, `readResource(uri)`, `getPrompt(name, args)`, and assertion helpers (`assertToolExists`, `assertToolCount`, and the resource / prompt equivalents). `callTool` accepts an `onProgress` callback to capture a streaming tool's yields. + +## Observers + +Subscribe to structured tool / resource / prompt events (for tracing or telemetry) via `@gemstack/mcp/observers`. The registry (`mcpObservers`) is a `globalThis` singleton, so state survives module re-evaluation, and each emit is wrapped in a try/catch so an observer error never breaks an MCP server: + +```ts +import { mcpObservers } from '@gemstack/mcp/observers' + +const unsubscribe = mcpObservers.subscribe((event) => { + // event: { kind, serverName, name, input, output, duration, error? } + console.log(event.kind, event.name, event.duration) +}) +``` + +## Authoring utilities + +For custom inspectors or tooling built on the core, the main entry also exports two pure helpers: `zodToJsonSchema(schema)` converts a Zod object to the JSON Schema MCP advertises, and `matchUriTemplate(template, uri)` matches a URI against a `resource://{template}` pattern. + +## License + +MIT diff --git a/docs/public/logo.svg b/docs/public/logo.svg new file mode 100644 index 0000000..f5cef31 --- /dev/null +++ b/docs/public/logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1158c0c..a64c6e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,15 @@ importers: specifier: ^5.4.0 version: 5.9.3 + docs: + devDependencies: + vitepress: + specifier: 2.0.0-alpha.17 + version: 2.0.0-alpha.17(@types/node@20.19.43)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) + vue: + specifier: ^3.5.29 + version: 3.5.39(typescript@5.9.3) + examples/mcp-quickstart: dependencies: '@gemstack/mcp': @@ -269,10 +278,27 @@ packages: resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} engines: {node: '>=18.0.0'} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + '@changesets/apply-release-plan@7.1.1': resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} @@ -328,6 +354,15 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@docsearch/css@4.6.3': + resolution: {integrity: sha512-nlOwcXcsNAptQl4vlL4MA78qNJKO0Qlds5GuBjCoePgkebTXLSf8Qt1oyZ3YBshYupKXG9VRGEsk1zr23d+bzQ==} + + '@docsearch/js@4.6.3': + resolution: {integrity: sha512-qUIX2b4Apew3tv4F0qhmgShsl/Lfw4m6mqv/5/5dWNxwTcDdLMp2s3YwZ+NMGh3IKCg0pBaXm7Q5VdyU5Rj+cQ==} + + '@docsearch/sidepanel-js@4.6.3': + resolution: {integrity: sha512-grGSmvXzG0if+mrzdIKykvpIAuEQ9u0sEJ2eLRRCaQfJvsWqh2C2/aY04bIzWvDh7myi5rvl8D+tUNsVrjYQ3A==} + '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} @@ -499,6 +534,12 @@ packages: peerDependencies: hono: ^4 + '@iconify-json/simple-icons@1.2.87': + resolution: {integrity: sha512-8YciStObhSji3OZFmWAWK6kBujyqO5bLCxeDwLxf3CR3F4PVelq7keC2LBvgTqviWzSTysj5/g4PCFLiAMVGsw==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -508,6 +549,9 @@ packages: '@types/node': optional: true + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -563,6 +607,171 @@ packages: '@protobufjs/utf8@1.1.1': resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@shikijs/core@3.23.0': + resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} + + '@shikijs/engine-javascript@3.23.0': + resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} + + '@shikijs/engine-oniguruma@3.23.0': + resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + + '@shikijs/langs@3.23.0': + resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + + '@shikijs/themes@3.23.0': + resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + + '@shikijs/transformers@3.23.0': + resolution: {integrity: sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==} + + '@shikijs/types@3.23.0': + resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@smithy/core@3.26.0': resolution: {integrity: sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g==} engines: {node: '>=18.0.0'} @@ -632,6 +841,24 @@ packages: cpu: [arm64] os: [win32] + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -644,6 +871,115 @@ packages: '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@ungap/structured-clone@1.3.2': + resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} + + '@vitejs/plugin-vue@6.0.7': + resolution: {integrity: sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.39': + resolution: {integrity: sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==} + + '@vue/compiler-dom@3.5.39': + resolution: {integrity: sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==} + + '@vue/compiler-sfc@3.5.39': + resolution: {integrity: sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==} + + '@vue/compiler-ssr@3.5.39': + resolution: {integrity: sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==} + + '@vue/devtools-api@8.1.4': + resolution: {integrity: sha512-zphdXRe0VxWfUWH2KLcNV4xWUMxjxASyxCjFS/wRHVYfJCMBulq1ce3RHGkYRxghoVBqwCzOw1TWIIfvIWCY1w==} + + '@vue/devtools-kit@8.1.4': + resolution: {integrity: sha512-+GLBwY63hZ46sqTlgXgvd8IcZTpWe0PZzbJgs63ii/8uTYVwg1q9bdIR9xx8ReKcrdWS01RGCbC971jYPvjRCA==} + + '@vue/devtools-shared@8.1.4': + resolution: {integrity: sha512-Earc/zrg0w0Md4KdrvgRR1vC5F17Zn+VzqTNI7PXYXz0fPJDVWENe7fv6NHi6Ja9Sq6Ce6SYUdc2FlXU0OMkjQ==} + + '@vue/reactivity@3.5.39': + resolution: {integrity: sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==} + + '@vue/runtime-core@3.5.39': + resolution: {integrity: sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==} + + '@vue/runtime-dom@3.5.39': + resolution: {integrity: sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==} + + '@vue/server-renderer@3.5.39': + resolution: {integrity: sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==} + peerDependencies: + vue: 3.5.39 + + '@vue/shared@3.5.39': + resolution: {integrity: sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==} + + '@vueuse/core@14.3.0': + resolution: {integrity: sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/integrations@14.3.0': + resolution: {integrity: sha512-76I5FT2ESvCmCaSwapI+a/u/CFtNXmzl9f9lNp1hRtx8vKB8hfiokJr8IvQqcQG5ckGXElyXK516b54ozV3MvA==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 || ^8 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 || ^8 + vue: ^3.5.0 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@14.3.0': + resolution: {integrity: sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==} + + '@vueuse/shared@14.3.0': + resolution: {integrity: sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==} + peerDependencies: + vue: ^3.5.0 + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -698,6 +1034,9 @@ packages: bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + body-parser@2.3.0: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} @@ -727,6 +1066,15 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + chardet@2.2.0: resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} @@ -752,6 +1100,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + content-disposition@1.1.0: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} @@ -808,10 +1159,17 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -834,6 +1192,10 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -863,6 +1225,9 @@ packages: engines: {node: '>=4'} hasBin: true + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -915,6 +1280,15 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -931,6 +1305,9 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + focus-trap@8.2.2: + resolution: {integrity: sha512-qV0g8hRYBqgACcFOH3f9wXc4zPKhr/0z9RI2a6ZijZ72EeBi4g8oBy8zAWuUR1TsMpOzwpUMFvjdasrC41Joug==} + form-data-encoder@4.1.0: resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} engines: {node: '>= 18'} @@ -1022,10 +1399,22 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hono@4.12.27: resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==} engines: {node: '>=16.9.0'} + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -1132,10 +1521,19 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -1148,6 +1546,21 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1168,6 +1581,9 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + minisearch@7.2.0: + resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -1175,6 +1591,11 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -1203,6 +1624,12 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + openai@6.44.0: resolution: {integrity: sha512-09/gH+8jH0RgUwsgWHAaxsKGRT5zVZ95IaJUnqAWj6XejIBmnFRwq2WUIF37VtDEsmGrtPmvCs5+yBSeZGWvkA==} peerDependencies: @@ -1263,6 +1690,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1270,6 +1700,10 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} @@ -1278,6 +1712,10 @@ packages: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -1287,6 +1725,9 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + protobufjs@7.6.4: resolution: {integrity: sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==} engines: {node: '>=12.0.0'} @@ -1328,6 +1769,15 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -1344,6 +1794,11 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -1381,6 +1836,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shiki@3.23.0: + resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} + side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} @@ -1405,6 +1863,13 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} @@ -1421,6 +1886,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1429,10 +1897,17 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + tabbable@6.5.0: + resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1441,6 +1916,9 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} @@ -1468,6 +1946,21 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -1480,6 +1973,75 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitepress@2.0.0-alpha.17: + resolution: {integrity: sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + oxc-minify: '*' + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + oxc-minify: + optional: true + postcss: + optional: true + + vue@3.5.39: + resolution: {integrity: sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -1521,6 +2083,9 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@anthropic-ai/sdk@0.105.0(zod@4.4.3)': @@ -1779,8 +2344,21 @@ snapshots: '@aws/lambda-invoke-store@0.2.4': optional: true + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + '@babel/runtime@7.29.7': {} + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@changesets/apply-release-plan@7.1.1': dependencies: '@changesets/config': 3.1.4 @@ -1924,6 +2502,12 @@ snapshots: human-id: 4.2.0 prettier: 2.8.8 + '@docsearch/css@4.6.3': {} + + '@docsearch/js@4.6.3': {} + + '@docsearch/sidepanel-js@4.6.3': {} + '@esbuild/aix-ppc64@0.28.1': optional: true @@ -2020,6 +2604,12 @@ snapshots: dependencies: hono: 4.12.27 + '@iconify-json/simple-icons@1.2.87': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + '@inquirer/external-editor@1.0.3(@types/node@20.19.43)': dependencies: chardet: 2.2.0 @@ -2027,6 +2617,8 @@ snapshots: optionalDependencies: '@types/node': 20.19.43 + '@jridgewell/sourcemap-codec@1.5.5': {} + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.29.7 @@ -2106,6 +2698,121 @@ snapshots: '@protobufjs/utf8@1.1.1': optional: true + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@shikijs/core@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/themes@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/transformers@3.23.0': + dependencies: + '@shikijs/core': 3.23.0 + '@shikijs/types': 3.23.0 + + '@shikijs/types@3.23.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@smithy/core@3.26.0': dependencies: '@aws-crypto/crc32': 5.2.0 @@ -2184,6 +2891,25 @@ snapshots: '@turbo/windows-arm64@2.9.18': optional: true + '@types/estree@1.0.9': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + '@types/node@12.20.55': {} '@types/node@20.19.43': @@ -2197,6 +2923,106 @@ snapshots: '@types/retry@0.12.0': optional: true + '@types/unist@3.0.3': {} + + '@types/web-bluetooth@0.0.21': {} + + '@ungap/structured-clone@1.3.2': {} + + '@vitejs/plugin-vue@6.0.7(vite@7.3.6(@types/node@20.19.43)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.39(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 7.3.6(@types/node@20.19.43)(tsx@4.22.4)(yaml@2.9.0) + vue: 3.5.39(typescript@5.9.3) + + '@vue/compiler-core@3.5.39': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.39 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.39': + dependencies: + '@vue/compiler-core': 3.5.39 + '@vue/shared': 3.5.39 + + '@vue/compiler-sfc@3.5.39': + dependencies: + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.39 + '@vue/compiler-dom': 3.5.39 + '@vue/compiler-ssr': 3.5.39 + '@vue/shared': 3.5.39 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.15 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.39': + dependencies: + '@vue/compiler-dom': 3.5.39 + '@vue/shared': 3.5.39 + + '@vue/devtools-api@8.1.4': + dependencies: + '@vue/devtools-kit': 8.1.4 + + '@vue/devtools-kit@8.1.4': + dependencies: + '@vue/devtools-shared': 8.1.4 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@8.1.4': {} + + '@vue/reactivity@3.5.39': + dependencies: + '@vue/shared': 3.5.39 + + '@vue/runtime-core@3.5.39': + dependencies: + '@vue/reactivity': 3.5.39 + '@vue/shared': 3.5.39 + + '@vue/runtime-dom@3.5.39': + dependencies: + '@vue/reactivity': 3.5.39 + '@vue/runtime-core': 3.5.39 + '@vue/shared': 3.5.39 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.39(vue@3.5.39(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.39 + '@vue/shared': 3.5.39 + vue: 3.5.39(typescript@5.9.3) + + '@vue/shared@3.5.39': {} + + '@vueuse/core@14.3.0(vue@3.5.39(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.3.0 + '@vueuse/shared': 14.3.0(vue@3.5.39(typescript@5.9.3)) + vue: 3.5.39(typescript@5.9.3) + + '@vueuse/integrations@14.3.0(focus-trap@8.2.2)(vue@3.5.39(typescript@5.9.3))': + dependencies: + '@vueuse/core': 14.3.0(vue@3.5.39(typescript@5.9.3)) + '@vueuse/shared': 14.3.0(vue@3.5.39(typescript@5.9.3)) + vue: 3.5.39(typescript@5.9.3) + optionalDependencies: + focus-trap: 8.2.2 + + '@vueuse/metadata@14.3.0': {} + + '@vueuse/shared@14.3.0(vue@3.5.39(typescript@5.9.3))': + dependencies: + vue: 3.5.39(typescript@5.9.3) + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -2246,6 +3072,8 @@ snapshots: bignumber.js@9.3.1: optional: true + birpc@2.9.0: {} + body-parser@2.3.0: dependencies: bytes: 3.1.2 @@ -2288,6 +3116,12 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + chardet@2.2.0: {} cohere-ai@8.0.0(@aws-crypto/sha256-js@5.2.0)(@smithy/signature-v4@5.5.2): @@ -2307,6 +3141,8 @@ snapshots: delayed-stream: 1.0.0 optional: true + comma-separated-tokens@2.0.3: {} + content-disposition@1.1.0: {} content-type@1.0.5: {} @@ -2348,8 +3184,14 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + detect-indent@6.1.0: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -2374,6 +3216,8 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + entities@7.0.1: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -2423,6 +3267,8 @@ snapshots: esprima@4.0.1: {} + estree-walker@2.0.2: {} + etag@1.8.1: {} event-target-shim@5.0.1: @@ -2499,6 +3345,10 @@ snapshots: dependencies: reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -2525,6 +3375,10 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + focus-trap@8.2.2: + dependencies: + tabbable: 6.5.0 + form-data-encoder@4.1.0: optional: true @@ -2645,8 +3499,30 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hono@4.12.27: {} + hookable@5.5.3: {} + + html-void-elements@3.0.0: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -2753,14 +3629,49 @@ snapshots: long@5.3.2: optional: true + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mark.js@8.11.1: {} + math-intrinsics@1.1.0: {} + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.2 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + media-typer@1.1.0: {} merge-descriptors@2.0.0: {} merge2@1.4.1: {} + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -2780,10 +3691,14 @@ snapshots: dependencies: mime-db: 1.54.0 + minisearch@7.2.0: {} + mri@1.2.0: {} ms@2.1.3: {} + nanoid@3.3.15: {} + negotiator@1.0.0: {} node-domexception@1.0.0: @@ -2808,6 +3723,14 @@ snapshots: dependencies: wrappy: 1.0.2 + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + openai@6.44.0(ws@8.21.0)(zod@4.4.3): optionalDependencies: ws: 8.21.0 @@ -2852,19 +3775,31 @@ snapshots: path-type@4.0.0: {} + perfect-debounce@2.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} + picomatch@4.0.4: {} + pify@4.0.1: {} pkce-challenge@5.0.1: {} + postcss@8.5.15: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prettier@2.8.8: {} process@0.11.10: optional: true + property-information@7.2.0: {} + protobufjs@7.6.4: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -2922,6 +3857,16 @@ snapshots: reflect-metadata@0.2.2: {} + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + require-from-string@2.0.2: {} resolve-from@5.0.0: {} @@ -2931,6 +3876,37 @@ snapshots: reusify@1.1.0: {} + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + router@2.2.0: dependencies: debug: 4.4.3 @@ -2985,6 +3961,17 @@ snapshots: shebang-regex@3.0.0: {} + shiki@3.23.0: + dependencies: + '@shikijs/core': 3.23.0 + '@shikijs/engine-javascript': 3.23.0 + '@shikijs/engine-oniguruma': 3.23.0 + '@shikijs/langs': 3.23.0 + '@shikijs/themes': 3.23.0 + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -3017,6 +4004,10 @@ snapshots: slash@3.0.0: {} + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + spawndamnit@3.0.1: dependencies: cross-spawn: 7.0.6 @@ -3037,20 +4028,34 @@ snapshots: safe-buffer: 5.2.1 optional: true + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 strip-bom@3.0.0: {} + tabbable@6.5.0: {} + term-size@2.2.1: {} + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 toidentifier@1.0.1: {} + trim-lines@3.0.1: {} + ts-algebra@2.0.0: optional: true @@ -3082,12 +4087,117 @@ snapshots: undici-types@6.21.0: {} + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universalify@0.1.2: {} unpipe@1.0.0: {} vary@1.1.2: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@7.3.6(@types/node@20.19.43)(tsx@4.22.4)(yaml@2.9.0): + dependencies: + esbuild: 0.28.1 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.62.2 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 20.19.43 + fsevents: 2.3.3 + tsx: 4.22.4 + yaml: 2.9.0 + + vitepress@2.0.0-alpha.17(@types/node@20.19.43)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0): + dependencies: + '@docsearch/css': 4.6.3 + '@docsearch/js': 4.6.3 + '@docsearch/sidepanel-js': 4.6.3 + '@iconify-json/simple-icons': 1.2.87 + '@shikijs/core': 3.23.0 + '@shikijs/transformers': 3.23.0 + '@shikijs/types': 3.23.0 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 6.0.7(vite@7.3.6(@types/node@20.19.43)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.39(typescript@5.9.3)) + '@vue/devtools-api': 8.1.4 + '@vue/shared': 3.5.39 + '@vueuse/core': 14.3.0(vue@3.5.39(typescript@5.9.3)) + '@vueuse/integrations': 14.3.0(focus-trap@8.2.2)(vue@3.5.39(typescript@5.9.3)) + focus-trap: 8.2.2 + mark.js: 8.11.1 + minisearch: 7.2.0 + shiki: 3.23.0 + vite: 7.3.6(@types/node@20.19.43)(tsx@4.22.4)(yaml@2.9.0) + vue: 3.5.39(typescript@5.9.3) + optionalDependencies: + postcss: 8.5.15 + transitivePeerDependencies: + - '@types/node' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jiti + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - sass + - sass-embedded + - sortablejs + - stylus + - sugarss + - terser + - tsx + - typescript + - universal-cookie + - yaml + + vue@3.5.39(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.39 + '@vue/compiler-sfc': 3.5.39 + '@vue/runtime-dom': 3.5.39 + '@vue/server-renderer': 3.5.39(vue@3.5.39(typescript@5.9.3)) + '@vue/shared': 3.5.39 + optionalDependencies: + typescript: 5.9.3 + web-streams-polyfill@3.3.3: optional: true @@ -3110,3 +4220,5 @@ snapshots: zod: 4.4.3 zod@4.4.3: {} + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b1af21d..1009765 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - packages/* - examples/* + - docs # pnpm 11 prompts before purging node_modules on a store-version change; # disable so non-TTY installs (CI, scripts) don't abort.