diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index 45f13e6cd..3d528248b 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -10,6 +10,7 @@ on: type: choice options: - acp + - claude-code - coder - console - database diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c990a8c99..3a76b8ec1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,7 @@ on: push: tags: - 'acp/v*' + - 'claude-code/v*' - 'coder/v*' - 'console/v*' - 'database/v*' diff --git a/README.md b/README.md index 4934a7706..7ab89468d 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ npx skills add iii-hq/iii --all |---|---|---| | [`acp`](acp/) | Rust | Agent Client Protocol surface — stdio JSON-RPC, exposes iii agents as ACP sessions. | | [`harness`](harness/) | Node | TS port of the iii harness stack — bundles `harness` (provider registry + credentials/settings/permissions via the `configuration` worker), `turn-orchestrator`, `approval-gate`, `hook-fanout`, `models-catalog`, the `provider-*` workers, `llm-budget`, and `context-compaction` as one pnpm monorepo. Conversations persist in `session-manager`. See [`harness/README.md`](harness/README.md). | +| [`claude-code`](claude-code/) | Node | Claude Code as an iii worker — `claude::*` runs headless Claude Code turns, mirrors raw messages onto `claude::events`, and streams AgentEvent frames onto `agent::events`. | | [`session-manager`](session-manager/) | Rust | Durable, reactive, branching conversation store — fourteen `session::*` functions plus six trigger types; the transcript backend for `harness` and `console`. See [`session-manager/architecture/`](session-manager/architecture/). | | [`database`](database/) | Rust | PostgreSQL, MySQL, and SQLite client — query, execute, transactions, prepared statements, and change feeds. | | [`iii-directory`](iii-directory/) | Rust | Engine introspection (functions / triggers / workers), workers-registry proxy, and filesystem-backed skill + prompt reader. | diff --git a/claude-code/.gitignore b/claude-code/.gitignore new file mode 100644 index 000000000..43370fa91 --- /dev/null +++ b/claude-code/.gitignore @@ -0,0 +1 @@ +*.tsbuildinfo diff --git a/claude-code/README.md b/claude-code/README.md new file mode 100644 index 000000000..38ca8859d --- /dev/null +++ b/claude-code/README.md @@ -0,0 +1,187 @@ +# claude-code + +Claude Code as an iii worker: the Claude Code API exposed as functions and streams on the iii bus, nothing else. The worker spawns the same `claude` binary the user runs in their terminal, with the same login, the same filesystem, and the same tools (file edits, shell, web). `claude::run` executes one headless turn and returns the result; the raw Claude Code messages mirror verbatim onto the `claude::events` stream, and a translated AgentEvent view lands on `agent::events`, so the iii console, the acp worker, and any sibling worker observe a Claude Code run exactly like a native harness turn. The worker also registers `run::start_and_wait`, the same entrypoint the console and the acp worker drive, so both run Claude Code with no changes. + +## Install + +```bash +iii worker add claude-code +``` + +Requires the `claude` CLI on the host (the Agent SDK shells out to it) and either `ANTHROPIC_API_KEY` in the worker environment or an existing `claude` login. + +## Skills + +Install the `claude-code` agent skill for Claude Code, Cursor, and 30+ other agents: + +```bash +npx skills add iii-hq/workers --skill claude-code +``` + +## Quickstart + +From zero to a Claude Code turn over the bus: + +```bash +curl -fsSL https://install.iii.dev/iii/main/install.sh | sh +iii worker add claude-code +iii # starts the engine + worker +``` + +Then talk to it like any other function: from the console chat, from `iii trigger claude::run`, or from any SDK: + +```ts +import { registerWorker } from 'iii-sdk'; + +const iii = registerWorker('ws://127.0.0.1:49134', { workerName: 'demo' }); + +const res = await iii.trigger({ + function_id: 'claude::run', + payload: { + prompt: 'Add a /health endpoint to server.ts and run the tests', + cwd: '/path/to/repo', + permission_mode: 'acceptEdits', + }, + timeout_ms: 600_000, +}); +// { session_id, claude_session_id, result, stop_reason, usage, total_cost_usd } +``` + +Or straight from the terminal with the `iii trigger` CLI: + +```bash +# one full turn (raise the timeout; the default 30s is too short for agent turns) +iii trigger claude::run --timeout-ms 600000 \ + --json '{"prompt":"add a /health endpoint and run the tests","cwd":"/path/to/repo"}' + +# quick reads use key=value syntax +iii trigger claude::sessions::list +iii trigger claude::status session_id= + +# background turn + control +iii trigger claude::start --json '{"prompt":"...","cwd":"/path/to/repo"}' +iii trigger claude::stop session_id= + +# ask the running engine for a function's description +iii trigger claude::run --help +``` + +A turn from the CLI and the session record it leaves behind: + +![iii trigger claude::run returning the result with usage and cost](assets/cli-run.png) + +![iii trigger claude::status showing the stored session record](assets/cli-status.png) + +![iii trigger claude::run --help printing the published request schema as a parameter table](assets/cli-help.png) + +Call `claude::run` again with the returned `session_id` to continue the same conversation: the worker maps iii session ids to Claude Code session ids in engine state and resumes automatically. + +Two ids come back from every run. `session_id` is the iii session id: the key for `claude::status`, `claude::stop`, resume, and the stream group. `claude_session_id` is Claude Code's internal session id (what the worker passes to the CLI's resume under the hood) — returned for reference, not a lookup key. + +Long turns: use `claude::start` to return immediately, then watch `agent::events` (group_id = your session_id) for `message_complete`, `function_execution_start/end`, and `turn_end` frames. `claude::stop` interrupts a live run, `claude::status` reads a point-in-time view, `claude::sessions::list` enumerates past sessions. + +## Functions + +| Function | Purpose | +| --- | --- | +| `claude::run` | Run one turn, wait, return the final result | +| `claude::start` | Fire-and-forget turn; progress arrives on `agent::events` | +| `claude::stop` | Interrupt a live run | +| `claude::status` | Session state, live flag, usage, cost | +| `claude::sessions::list` | All sessions this worker has run | +| `run::start_and_wait` | Alias for `claude::run` under the entrypoint the console and acp worker drive | + +`claude::run` accepts either a bare `prompt` string or a `messages` array (`[{ role: 'user', content: [{ type: 'text', text }] }]`), plus `model`, `cwd`, `system_prompt`, `append_system_prompt`, `permission_mode`, `allowed_tools`, `disallowed_tools`, and `max_turns` overrides. + +### Raw API pass-through + +The named fields above cover the common path; everything else the Agent SDK accepts goes through the `options` field untouched (camelCase, exactly as in the SDK): + +```jsonc +{ + "prompt": "...", + "options": { + "forkSession": true, + "includePartialMessages": true, + "fallbackModel": "claude-sonnet-4-6", + "addDirs": ["/another/repo"] + } +} +``` + +And the full output side is available raw: every message Claude Code emits (`system/init`, `assistant`, `user`, `result`, and `stream_event` token deltas when `includePartialMessages` is set) is mirrored verbatim onto the `claude::events` stream, group_id = session_id. Consumers that want the exact Claude Code wire format read `claude::events`; consumers that want harness-shaped frames read `agent::events`. Same turn, two views. + +## The agent on the bus + +By default every turn's system prompt carries the iii runtime context: the same engine-grounded rules as the harness identity prompts, retargeted to the `iii` CLI the agent reaches through its shell. The agent discovers capabilities from the live engine instead of memory — `iii trigger engine::functions::list` to find function ids, `iii trigger --help` as the contract before every first call, the registry flow (`directory::registry::workers::list/info`, `worker::add`) when nothing registered fits — plus the calling rules and error-handling discipline that go with them. The matching `Bash(iii *)` allow rule is added automatically so those calls run headless. Local file edits stay on Claude Code's native tools; backend actions go through registered functions. + +```bash +# the agent answers this by querying the live engine itself +iii trigger claude::run --timeout-ms 300000 \ + --json '{"prompt":"List every worker connected to this engine and what each one does.","cwd":"/tmp"}' +``` + +Turn it off per call with `"iii_context": false` or globally in `config.yaml`; a caller-supplied `system_prompt` always wins verbatim and gets nothing appended. + +## Plan mode and permission modes + +`permission_mode` maps straight onto Claude Code's native modes, per turn: + +| Mode | Behavior | +| --- | --- | +| `default` | Claude Code's standard permission prompts (headless: unapproved calls fail) | +| `acceptEdits` | File edits auto-approved; the worker default | +| `plan` | Native plan mode: read-only exploration, produces a plan, refuses edits | +| `bypassPermissions` | Skip all permission checks | + +Plan mode headless behaves like plan mode in the terminal: the turn ends when Claude finishes the plan, and the plan text is the `result` — nothing executes. Because the worker resumes sessions, plan-then-execute is two calls against the same `session_id`: + +```bash +# 1. plan (read-only) +iii trigger claude::run --timeout-ms 600000 \ + --json '{"prompt":"Plan how to add rate limiting to the REST API. Do not implement.","cwd":"/path/to/repo","permission_mode":"plan"}' + +# 2. execute the plan with full context, same conversation +iii trigger claude::run --timeout-ms 600000 \ + --json '{"session_id":"","prompt":"Implement the plan.","permission_mode":"acceptEdits","cwd":"/path/to/repo"}' +``` + +The approval step is whatever sits between the two calls — a human reading the plan, another worker, or a trigger. + +## Configuration + +```yaml +engine_url: ws://127.0.0.1:49134 + +defaults: + model: "" # empty = Claude Code default + permission_mode: acceptEdits # default | acceptEdits | plan | bypassPermissions + max_turns: 50 + cwd: "" # default working directory for runs + +approval_gate: false # route tool permissions through policy::check_permissions +events_stream: agent::events # translated AgentEvent frames +raw_events_stream: claude::events # verbatim Claude Code messages +claude_executable: "" # path to the claude CLI; empty = SDK default resolution +``` + +With `approval_gate: true` and the harness worker installed, every Claude Code tool call is checked against `policy::check_permissions` before it executes, fail-closed when the gate is unreachable, so the same YAML permission rules and console approval flow that govern native harness turns govern Claude Code. + +## Observability + +Every `claude::run` is an ordinary traced invocation on the engine: the trace carries the full input payload (prompt, cwd, caller worker id) and the output (result, stop reason, token usage, cost) as span events, with per-function p50/p95/p99 in the console's trace explorer — no extra instrumentation in the worker. + +![claude::run invocations in the iii console trace explorer, with input and output payloads](assets/console-traces.png) + +## How it maps + +| Claude Code | iii | +| --- | --- | +| SDK `query()` turn | `claude::run` invocation | +| every SDK message, verbatim | `claude::events` stream frame | +| assistant message | `message_complete` frame on `agent::events` | +| tool_use / tool_result | `function_execution_start` / `function_execution_end` frames | +| final result | `turn_end` + `agent_end` frames, function return value | +| session resume | engine state scope `claude_sessions`, keyed by iii session_id | +| permission prompt | `canUseTool` -> `policy::check_permissions` (optional) | +| extra capability | another iii worker on the bus (`shell`, `database`, `storage`, ...) | diff --git a/claude-code/assets/cli-help.png b/claude-code/assets/cli-help.png new file mode 100644 index 000000000..7c457b8e9 Binary files /dev/null and b/claude-code/assets/cli-help.png differ diff --git a/claude-code/assets/cli-run.png b/claude-code/assets/cli-run.png new file mode 100644 index 000000000..62fd4c0cc Binary files /dev/null and b/claude-code/assets/cli-run.png differ diff --git a/claude-code/assets/cli-status.png b/claude-code/assets/cli-status.png new file mode 100644 index 000000000..cf3fb972b Binary files /dev/null and b/claude-code/assets/cli-status.png differ diff --git a/claude-code/assets/console-traces.png b/claude-code/assets/console-traces.png new file mode 100644 index 000000000..29298766a Binary files /dev/null and b/claude-code/assets/console-traces.png differ diff --git a/claude-code/biome.json b/claude-code/biome.json new file mode 100644 index 000000000..29673813a --- /dev/null +++ b/claude-code/biome.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", + "root": false, + "vcs": { "enabled": false, "clientKind": "git" }, + "files": { + "ignoreUnknown": false, + "includes": ["**", "!!**/dist"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "off" + } + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "warn" + }, + "style": { + "useImportType": "off", + "useNodejsImportProtocol": "error" + }, + "complexity": { + "noForEach": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "trailingCommas": "all", + "semicolons": "always" + } + } +} diff --git a/claude-code/config.yaml b/claude-code/config.yaml new file mode 100644 index 000000000..16d88a751 --- /dev/null +++ b/claude-code/config.yaml @@ -0,0 +1,34 @@ +engine_url: ws://127.0.0.1:49134 + +defaults: + model: "" + permission_mode: acceptEdits + max_turns: 50 + cwd: "" + append_system_prompt: "" + allowed_tools: [] + disallowed_tools: [] + +# Route Claude Code permission prompts through policy::check_permissions +# (harness approval gate). Fail-closed when the gate is unreachable. +# Requires the harness worker; leave false for standalone use. +approval_gate: false + +# Stream AgentEvent frames here, grouped by session_id. The console and +# acp worker both read this stream. +events_stream: agent::events + +# Raw Claude Code messages (exact Agent SDK shapes: system/init, assistant, +# user, result, stream_event) mirrored verbatim here, grouped by session_id. +raw_events_stream: claude::events + +# Append the iii runtime context to the system prompt: teaches the agent +# live discovery against the engine catalog through the iii CLI +# (engine::functions::list, `iii trigger --help`). Per-turn override +# via the iii_context payload field; a caller-supplied system_prompt +# always wins and gets nothing appended. +iii_context: true + +# Path to the Claude Code CLI binary. Empty = Agent SDK default resolution; +# set to an absolute path (or "claude") when running the single-file bundle. +claude_executable: "" diff --git a/claude-code/iii-permissions.yaml b/claude-code/iii-permissions.yaml new file mode 100644 index 000000000..7f01c1add --- /dev/null +++ b/claude-code/iii-permissions.yaml @@ -0,0 +1,15 @@ +# Agent permissions for the claude-code worker. +# Spec: docs/sops/new-worker.md § 7. First-match-wins. +# +# claude::run / claude::start (and the run::start_and_wait alias) spawn a full +# Claude Code agent with the host's filesystem and shell — an agent invoking +# those without human approval is a privilege escalation, so they are NOT +# allow-listed and stay at the needs_approval default. Read-only introspection +# is safe to allow. +version: 1 + +rules: + # internal config-reload callback — bus-internal, never agent-callable + - '!claude::on-config-change' + - claude::status + - claude::sessions::list diff --git a/claude-code/iii.worker.yaml b/claude-code/iii.worker.yaml new file mode 100644 index 000000000..1267a1087 --- /dev/null +++ b/claude-code/iii.worker.yaml @@ -0,0 +1,17 @@ +iii: v1 +name: claude-code +language: javascript +deploy: bundle +manifest: package.json +description: Claude Code as an iii worker — claude::* functions run headless Claude Code turns, mirror raw messages onto claude::events, and stream AgentEvent frames onto agent::events. + +runtime: + kind: javascript + +scripts: + install: npm install && npm run build + start: node dist/index.js + +dependencies: + iii-state: "^0.17.0" + iii-stream: "^0.17.0" diff --git a/claude-code/package.json b/claude-code/package.json new file mode 100644 index 000000000..3c91a3b3c --- /dev/null +++ b/claude-code/package.json @@ -0,0 +1,39 @@ +{ + "name": "claude-code", + "version": "0.1.0", + "private": true, + "description": "Claude Code as an iii worker: headless turns over the iii bus, raw messages on claude::events, AgentEvent frames on agent::events.", + "license": "Apache-2.0", + "type": "module", + "engines": { + "node": ">=22" + }, + "packageManager": "pnpm@10.18.2", + "scripts": { + "build": "tsc -b", + "build:bundle": "node scripts/build-bundle.mjs", + "typecheck": "tsc -b --noEmit", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "test": "vitest run", + "start": "node dist/index.js", + "dev": "tsx src/index.ts" + }, + "bin": { + "claude-code": "./dist/index.js" + }, + "dependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.3.173", + "iii-sdk": "^0.19.2", + "yaml": "^2.6.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "@biomejs/biome": "2.4.10", + "@types/node": "^22.10.0", + "esbuild": "^0.25.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + } +} diff --git a/claude-code/pnpm-lock.yaml b/claude-code/pnpm-lock.yaml new file mode 100644 index 000000000..d9399b913 --- /dev/null +++ b/claude-code/pnpm-lock.yaml @@ -0,0 +1,2954 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@anthropic-ai/claude-agent-sdk': + specifier: ^0.3.173 + version: 0.3.173(@anthropic-ai/sdk@0.104.1(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) + iii-sdk: + specifier: ^0.19.2 + version: 0.19.2 + yaml: + specifier: ^2.6.0 + version: 2.9.0 + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@biomejs/biome': + specifier: 2.4.10 + version: 2.4.10 + '@types/node': + specifier: ^22.10.0 + version: 22.19.21 + esbuild: + specifier: ^0.25.0 + version: 0.25.12 + tsx: + specifier: ^4.19.0 + version: 4.22.4 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.6(@types/node@22.19.21)(tsx@4.22.4)(yaml@2.9.0) + +packages: + + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.173': + resolution: {integrity: sha512-McW1toJ4Qdo/i7bHnsiFMm2AtyCiK/5V90WgL7M9ZO9llrJr3riGRdBlRGHvWnS7rKuv5ttj4/SuBkLoL9o75A==} + cpu: [arm64] + os: [darwin] + + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.173': + resolution: {integrity: sha512-m2Oh1pQ69IUg6DSH6n1nAAAtRq+G7J2B/CKTbTfDAEmg5t0lzakZV9l28GZrlP21xl+PIg71dkiJ5u94KYgpRw==} + cpu: [x64] + os: [darwin] + + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.173': + resolution: {integrity: sha512-uu8MnPwFBc9ayFg5c94aHaqJVLS51oHNVxHwud4nK27GliP5WoME3F7pmm1N/Jyy2ry2E2CLNnsAm5l3zemfYA==} + cpu: [arm64] + os: [linux] + + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.173': + resolution: {integrity: sha512-Vb64WJOD2D9tKn/i0ErmLbO6I1187xTwL/kxEANjg4L1FGQnvdYBnZ6J6jU6+x8UgcuIi82imHROQp80gbPW2w==} + cpu: [arm64] + os: [linux] + + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.173': + resolution: {integrity: sha512-ofKxyp/N8+LLSrClt+dJo0laCHDzmBgmN8+Q+zabJ54Jqc1KXee68UsziE7kLCqGbOSY7rsIK9d22T1uQyZkUw==} + cpu: [x64] + os: [linux] + + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.173': + resolution: {integrity: sha512-0l9L5O+Uiw8gq9mu2NqGSYcSmX8RJMAh9GkGacTJqJbkfHCiFxqZmWBWODOt6HP7PTFCV+yMzQK/xJQjnag/jw==} + cpu: [x64] + os: [linux] + + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.173': + resolution: {integrity: sha512-aulIrBYjFDm+S5kl4CxC8A3KUiTDKLHoKV46Mat64E+skGEyBkC7WzZAGbpGKB2y8KZo1WbrwJTGefgyHMpDhw==} + cpu: [arm64] + os: [win32] + + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.173': + resolution: {integrity: sha512-HjGkfDlNLI3Kh9NIUfevJ3SZY8Xv3td4zx5Cz3YE0VPrrjIkRvdEv9K6WTjT94tlS0TUXQS/kFT+NCmoGXuvZQ==} + cpu: [x64] + os: [win32] + + '@anthropic-ai/claude-agent-sdk@0.3.173': + resolution: {integrity: sha512-BsdL223y7vCUJA9uBW9osSrhufvwIT+J94IBkh83v+wjyjoBIwLXREdacFabair70bGNdtkw6cWCaYNThSQg7A==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@anthropic-ai/sdk': '>=0.93.0' + '@modelcontextprotocol/sdk': ^1.29.0 + zod: ^4.0.0 + + '@anthropic-ai/sdk@0.104.1': + resolution: {integrity: sha512-gGACa/+IaiXzRRmF96aOhamoBgapKRBiFWbmmTFP8aMkpaEcuStF+Q61bjo4vPxBM7gqWJNZqsngslRdnLHv0Q==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@2.4.10': + resolution: {integrity: sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.10': + resolution: {integrity: sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.10': + resolution: {integrity: sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.10': + resolution: {integrity: sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.4.10': + resolution: {integrity: sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.4.10': + resolution: {integrity: sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.4.10': + resolution: {integrity: sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.4.10': + resolution: {integrity: sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.10': + resolution: {integrity: sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@iii-dev/observability@0.19.2': + resolution: {integrity: sha512-FsAAzELzRKUA5h8CyLgsB/k0rlN3ZQeg9BVKBsxx8ubF6tNwdLS53bhDFEy9jE7Q5kDgx/AFRVdqB7pUANgChA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@opentelemetry/api-logs@0.57.2': + resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} + engines: {node: '>=14'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@1.30.1': + resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation@0.57.2': + resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.57.2': + resolution: {integrity: sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagator-b3@1.30.1': + resolution: {integrity: sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@1.30.1': + resolution: {integrity: sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@1.30.1': + resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.57.2': + resolution: {integrity: sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@1.30.1': + resolution: {integrity: sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.30.1': + resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@1.30.1': + resolution: {integrity: sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.41.1': + resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} + engines: {node: '>=14'} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.2': + resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + + '@rollup/rollup-android-arm-eabi@4.61.1': + resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.61.1': + resolution: {integrity: sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.61.1': + resolution: {integrity: sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.61.1': + resolution: {integrity: sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.61.1': + resolution: {integrity: sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.61.1': + resolution: {integrity: sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.61.1': + resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.61.1': + resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.61.1': + resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.61.1': + resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.61.1': + resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.61.1': + resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.61.1': + resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.61.1': + resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.61.1': + resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.61.1': + resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.61.1': + resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.61.1': + resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.61.1': + resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.61.1': + resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.61.1': + resolution: {integrity: sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.61.1': + resolution: {integrity: sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.61.1': + resolution: {integrity: sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.61.1': + resolution: {integrity: sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.61.1': + resolution: {integrity: sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==} + cpu: [x64] + os: [win32] + + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@22.19.21': + resolution: {integrity: sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==} + + '@types/shimmer@1.2.0': + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + + '@vitest/expect@3.2.6': + resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==} + + '@vitest/mocker@3.2.6': + resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.6': + resolution: {integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==} + + '@vitest/runner@3.2.6': + resolution: {integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==} + + '@vitest/snapshot@3.2.6': + resolution: {integrity: sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==} + + '@vitest/spy@3.2.6': + resolution: {integrity: sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==} + + '@vitest/utils@3.2.6': + resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hono@4.12.25: + resolution: {integrity: sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + iii-sdk@0.19.2: + resolution: {integrity: sha512-HgMKHxmrOZNxNvJGwMvLsQ7UoifIpv1Av46gCs4vbBjSWDRQiM/wCNsTlZ/QUdSiZOXs6FjIxSLf3kuL1H3bLw==} + + import-in-the-middle@1.15.0: + resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + 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'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pkce-challenge@5.0.1: + 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} + + protobufjs@7.6.3: + resolution: {integrity: sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==} + engines: {node: '>=12.0.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + rollup@4.61.1: + resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==} + 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'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.5: + resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} + 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 + + vitest@3.2.6: + resolution: {integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.6 + '@vitest/ui': 3.2.6 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.173': + optional: true + + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.173': + optional: true + + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.173': + optional: true + + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.173': + optional: true + + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.173': + optional: true + + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.173': + optional: true + + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.173': + optional: true + + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.173': + optional: true + + '@anthropic-ai/claude-agent-sdk@0.3.173(@anthropic-ai/sdk@0.104.1(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': + dependencies: + '@anthropic-ai/sdk': 0.104.1(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + zod: 4.4.3 + optionalDependencies: + '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.173 + '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.173 + '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.173 + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.173 + '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.173 + '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.173 + '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.173 + '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.173 + + '@anthropic-ai/sdk@0.104.1(zod@4.4.3)': + dependencies: + json-schema-to-ts: 3.1.1 + standardwebhooks: 1.0.0 + optionalDependencies: + zod: 4.4.3 + + '@babel/runtime@7.29.7': {} + + '@biomejs/biome@2.4.10': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.10 + '@biomejs/cli-darwin-x64': 2.4.10 + '@biomejs/cli-linux-arm64': 2.4.10 + '@biomejs/cli-linux-arm64-musl': 2.4.10 + '@biomejs/cli-linux-x64': 2.4.10 + '@biomejs/cli-linux-x64-musl': 2.4.10 + '@biomejs/cli-win32-arm64': 2.4.10 + '@biomejs/cli-win32-x64': 2.4.10 + + '@biomejs/cli-darwin-arm64@2.4.10': + optional: true + + '@biomejs/cli-darwin-x64@2.4.10': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.10': + optional: true + + '@biomejs/cli-linux-arm64@2.4.10': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.10': + optional: true + + '@biomejs/cli-linux-x64@2.4.10': + optional: true + + '@biomejs/cli-win32-arm64@2.4.10': + optional: true + + '@biomejs/cli-win32-x64@2.4.10': + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/aix-ppc64@0.28.0': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.28.0': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-arm@0.28.0': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/android-x64@0.28.0': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.28.0': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.28.0': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.28.0': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.28.0': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-arm@0.28.0': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.28.0': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.28.0': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.28.0': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.28.0': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.28.0': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.28.0': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.28.0': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.28.0': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.28.0': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.28.0': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.28.0': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.28.0': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.28.0': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@esbuild/win32-x64@0.28.0': + optional: true + + '@hono/node-server@1.19.14(hono@4.12.25)': + dependencies: + hono: 4.12.25 + + '@iii-dev/observability@0.19.2': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.25) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.0 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.25 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/api-logs@0.57.2': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.57.2 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.15.0 + require-in-the-middle: 7.5.2 + semver: 7.8.4 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-transformer@0.57.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) + protobufjs: 7.6.3 + + '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/propagator-jaeger@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-trace-node@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) + semver: 7.8.4 + + '@opentelemetry/semantic-conventions@1.28.0': {} + + '@opentelemetry/semantic-conventions@1.41.1': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.1': {} + + '@rollup/rollup-android-arm-eabi@4.61.1': + optional: true + + '@rollup/rollup-android-arm64@4.61.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.61.1': + optional: true + + '@rollup/rollup-darwin-x64@4.61.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.61.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.61.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.61.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.61.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.61.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.61.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.61.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.61.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.61.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.61.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.61.1': + optional: true + + '@stablelib/base64@1.0.1': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@22.19.21': + dependencies: + undici-types: 6.21.0 + + '@types/shimmer@1.2.0': {} + + '@vitest/expect@3.2.6': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.6 + '@vitest/utils': 3.2.6 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.6(vite@7.3.5(@types/node@22.19.21)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 3.2.6 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.5(@types/node@22.19.21)(tsx@4.22.4)(yaml@2.9.0) + + '@vitest/pretty-format@3.2.6': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.6': + dependencies: + '@vitest/utils': 3.2.6 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.6': + dependencies: + '@vitest/pretty-format': 3.2.6 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.6': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.6': + dependencies: + '@vitest/pretty-format': 3.2.6 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-import-attributes@1.9.5(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + assertion-error@2.0.1: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + cjs-module-lexer@1.4.3: {} + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + depd@2.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + + escape-html@1.0.3: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + etag@1.8.1: {} + + eventsource-parser@3.1.0: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.1.0 + + expect-type@1.3.0: {} + + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-sha256@1.3.0: {} + + fast-uri@3.1.2: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hono@4.12.25: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + iii-sdk@0.19.2: + dependencies: + '@iii-dev/observability': 0.19.2 + '@opentelemetry/api': 1.9.1 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + import-in-the-middle@1.15.0: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + + inherits@2.0.4: {} + + ip-address@10.2.0: {} + + ipaddr.js@1.9.1: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-promise@4.0.0: {} + + isexe@2.0.0: {} + + jose@6.2.3: {} + + js-tokens@9.0.1: {} + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.7 + ts-algebra: 2.0.0 + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + long@5.3.2: {} + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + module-details-from-path@1.0.4: {} + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + negotiator@1.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parseurl@1.3.3: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-to-regexp@8.4.2: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + pkce-challenge@5.0.1: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + protobufjs@7.6.3: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/node': 22.19.21 + long: 5.3.2 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.15.2: + dependencies: + side-channel: 1.1.1 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + require-from-string@2.0.2: {} + + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rollup@4.61.1: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.61.1 + '@rollup/rollup-android-arm64': 4.61.1 + '@rollup/rollup-darwin-arm64': 4.61.1 + '@rollup/rollup-darwin-x64': 4.61.1 + '@rollup/rollup-freebsd-arm64': 4.61.1 + '@rollup/rollup-freebsd-x64': 4.61.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.61.1 + '@rollup/rollup-linux-arm-musleabihf': 4.61.1 + '@rollup/rollup-linux-arm64-gnu': 4.61.1 + '@rollup/rollup-linux-arm64-musl': 4.61.1 + '@rollup/rollup-linux-loong64-gnu': 4.61.1 + '@rollup/rollup-linux-loong64-musl': 4.61.1 + '@rollup/rollup-linux-ppc64-gnu': 4.61.1 + '@rollup/rollup-linux-ppc64-musl': 4.61.1 + '@rollup/rollup-linux-riscv64-gnu': 4.61.1 + '@rollup/rollup-linux-riscv64-musl': 4.61.1 + '@rollup/rollup-linux-s390x-gnu': 4.61.1 + '@rollup/rollup-linux-x64-gnu': 4.61.1 + '@rollup/rollup-linux-x64-musl': 4.61.1 + '@rollup/rollup-openbsd-x64': 4.61.1 + '@rollup/rollup-openharmony-arm64': 4.61.1 + '@rollup/rollup-win32-arm64-msvc': 4.61.1 + '@rollup/rollup-win32-ia32-msvc': 4.61.1 + '@rollup/rollup-win32-x64-gnu': 4.61.1 + '@rollup/rollup-win32-x64-msvc': 4.61.1 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safer-buffer@2.1.2: {} + + semver@7.8.4: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shimmer@1.2.1: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + + statuses@2.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-preserve-symlinks-flag@1.0.0: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + toidentifier@1.0.1: {} + + ts-algebra@2.0.0: {} + + tsx@4.22.4: + dependencies: + esbuild: 0.28.0 + optionalDependencies: + fsevents: 2.3.3 + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + unpipe@1.0.0: {} + + vary@1.1.2: {} + + vite-node@3.2.4(@types/node@22.19.21)(tsx@4.22.4)(yaml@2.9.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.5(@types/node@22.19.21)(tsx@4.22.4)(yaml@2.9.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.5(@types/node@22.19.21)(tsx@4.22.4)(yaml@2.9.0): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.61.1 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.19.21 + fsevents: 2.3.3 + tsx: 4.22.4 + yaml: 2.9.0 + + vitest@3.2.6(@types/node@22.19.21)(tsx@4.22.4)(yaml@2.9.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.6 + '@vitest/mocker': 3.2.6(vite@7.3.5(@types/node@22.19.21)(tsx@4.22.4)(yaml@2.9.0)) + '@vitest/pretty-format': 3.2.6 + '@vitest/runner': 3.2.6 + '@vitest/snapshot': 3.2.6 + '@vitest/spy': 3.2.6 + '@vitest/utils': 3.2.6 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.5(@types/node@22.19.21)(tsx@4.22.4)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@22.19.21)(tsx@4.22.4)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.21 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrappy@1.0.2: {} + + ws@8.21.0: {} + + yaml@2.9.0: {} + + zod-to-json-schema@3.25.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/claude-code/scripts/build-bundle.mjs b/claude-code/scripts/build-bundle.mjs new file mode 100644 index 000000000..fdb473449 --- /dev/null +++ b/claude-code/scripts/build-bundle.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node + +/** + * Single-file ESM bundle for claude-code (`dist/bundle/index.mjs`). + * + * Mirrors harness/scripts/build-bundle.mjs: + * + * 1. iii-sdk reads its own version at module-init via + * `createRequire(import.meta.url)("../package.json")` + * which resolves relative to the bundle path at runtime. The + * `inlinePackageJson` plugin rewrites that call to a literal object. + * + * 2. `@anthropic-ai/claude-agent-sdk` spawns the Claude Code CLI as a + * subprocess; the `claude_executable` config option (or a `claude` + * binary on PATH) covers the bundled case where the SDK cannot + * resolve its own vendored entrypoint. + */ + +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { build } from 'esbuild'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const root = join(__dirname, '..'); + +/** @type {import('esbuild').Plugin} */ +const inlinePackageJson = { + name: 'iii-inline-sdk-package-json', + setup(b) { + b.onLoad({ filter: /iii-sdk[\\/]dist[\\/]index\.mjs$/ }, async (args) => { + const [source, pkg] = await Promise.all([ + readFile(args.path, 'utf8'), + readFile(join(root, 'node_modules/iii-sdk/package.json'), 'utf8'), + ]); + const { version } = JSON.parse(pkg); + const replaced = source.replace( + /createRequire\(\s*import\.meta\.url\s*\)\s*\(\s*"\.\.\/package\.json"\s*\)/g, + JSON.stringify({ version }), + ); + return { contents: replaced, loader: 'js' }; + }); + }, +}; + +await build({ + entryPoints: [join(root, 'src/index.ts')], + bundle: true, + platform: 'node', + target: 'node22', + format: 'esm', + outfile: join(root, 'dist/bundle/index.mjs'), + legalComments: 'none', + external: ['fsevents'], + banner: { + js: "import{createRequire as __iiiCR}from'module';const require=__iiiCR(import.meta.url);", + }, + define: { + 'process.env.NODE_ENV': '"production"', + }, + plugins: [inlinePackageJson], + logLevel: 'info', +}); diff --git a/claude-code/skills/SKILL.md b/claude-code/skills/SKILL.md new file mode 100644 index 000000000..f1bc44c01 --- /dev/null +++ b/claude-code/skills/SKILL.md @@ -0,0 +1,82 @@ +--- +name: claude-code +description: >- + Run headless Claude Code turns over the iii bus — file edits, shell, and + web against any host directory — with verbatim message streaming, session + resume, and full Agent SDK option pass-through. +--- + +# claude-code + +The claude-code worker exposes the Claude Code API as iii functions. One +`claude::run` call executes one headless Claude Code turn — the same agent +the user runs in their terminal, with the same login, filesystem, and +permission model — in a chosen working directory, and returns the final +result, token usage, and cost. The worker is a pure pass-through: named +payload fields cover the common path, the `options` field forwards any Agent +SDK option verbatim, and every message Claude Code emits mirrors untouched +onto the `claude::events` stream. A translated AgentEvent view lands on +`agent::events`, which is what the iii console and the acp worker render. + +Requires the `claude` CLI on the host with an existing login or +`ANTHROPIC_API_KEY` in the worker environment. When a turn needs a +capability beyond Claude Code itself, add another iii worker to the bus +instead of bolting anything onto this one. + +## When to Use + +- Delegate a whole coding task ("add an endpoint and run the tests") in one + call, instead of orchestrating individual `coder::*` / `shell::*` calls + yourself: `claude::run` with `prompt` and `cwd`. +- Continue a conversation across calls: pass the same `session_id` again and + the worker resumes the underlying Claude Code session with full context. +- Run long jobs without holding the call open: `claude::start` returns + `{session_id, started}` immediately; follow `claude::events` (group_id = + session_id) for raw progress or `agent::events` for the rendered view; + interrupt with `claude::stop`. +- Act on the whole backend: turns carry the iii runtime context by default, + so the agent discovers and calls any registered function through the iii + CLI (engine::functions::list, `iii trigger --help`) with the + matching Bash allow rule pre-set; disable per turn with + `iii_context: false`. +- Plan before touching anything: `permission_mode: "plan"` runs Claude + Code's native plan mode (read-only, returns the plan as the result); + then send "implement the plan" on the same `session_id` with + `permission_mode: "acceptEdits"`. +- Reach past the named payload fields: anything the Agent SDK accepts goes + through `options` unchanged — `{"options": {"forkSession": true, + "includePartialMessages": true}}` — and `includePartialMessages` puts + token-level `stream_event` frames on `claude::events`. + +## Boundaries + +- Spawns the host `claude` CLI per turn — needs Claude Code installed and + authenticated; not available inside a bare container without it. +- Function execution happens inside Claude Code's own permission model + (`permission_mode`, `allowed_tools`, `disallowed_tools`), not the + engine's; set `approval_gate: true` to route every call through + `policy::check_permissions` (fail-closed, needs the harness worker). +- One turn per session at a time: check `claude::status` (`live: true`) + before sending another `claude::run` for the same `session_id`; parallel + runs against one session race on the underlying Claude Code resume. +- `agent::events` carries whole-message frames (`message_complete`, + `function_execution_start/end`, `turn_end`, `agent_end`); token deltas + exist only on `claude::events` and only when `includePartialMessages` is + set. + +## Functions + +- `claude::run` — run one Claude Code turn and wait; accepts `prompt` (or a + `messages` array whose last user entry becomes the prompt), plus `model`, + `cwd`, `permission_mode`, `allowed_tools`, `disallowed_tools`, + `max_turns`, `system_prompt`, `append_system_prompt`, and raw `options`; + returns `{session_id, claude_session_id, result, stop_reason, usage, + total_cost_usd}`. +- `claude::start` — same payload, returns `{session_id, started}` + immediately; progress arrives on the streams. +- `claude::stop` — interrupt the live run for a session. +- `claude::status` — point-in-time session view: live flag, status, turns, + usage, cost. +- `claude::sessions::list` — every session this worker has run. +- `run::start_and_wait` — alias for `claude::run` under the entrypoint the + console and acp worker drive, so both run Claude Code with no changes. diff --git a/claude-code/src/config.ts b/claude-code/src/config.ts new file mode 100644 index 000000000..2cf3dacbe --- /dev/null +++ b/claude-code/src/config.ts @@ -0,0 +1,58 @@ +import { readFile } from 'node:fs/promises'; +import { parse } from 'yaml'; +import { z } from 'zod'; + +const ConfigSchema = z.object({ + engine_url: z.string().default('ws://127.0.0.1:49134'), + defaults: z + .object({ + model: z.string().default(''), + permission_mode: z + .enum(['default', 'acceptEdits', 'plan', 'bypassPermissions']) + .default('acceptEdits'), + max_turns: z.number().int().positive().default(50), + cwd: z.string().default(''), + append_system_prompt: z.string().default(''), + allowed_tools: z.array(z.string()).default([]), + disallowed_tools: z.array(z.string()).default([]), + }) + .prefault({}), + approval_gate: z.boolean().default(false), + events_stream: z.string().default('agent::events'), + raw_events_stream: z.string().default('claude::events'), + iii_context: z.boolean().default(true), + claude_executable: z.string().default(''), +}); + +export type Config = z.infer; + +/** + * The slice managed by the `configuration` worker. `engine_url` is excluded — + * it is bootstrap (needed to reach the configuration worker), so it stays on + * the local seed / `--url` and never hot-reloads. + */ +export const RuntimeConfigSchema = ConfigSchema.omit({ engine_url: true }); +export type RuntimeConfig = z.infer; + +/** JSON Schema published to the configuration worker. */ +export function runtimeJsonSchema(): Record { + return z.toJSONSchema(RuntimeConfigSchema) as Record; +} + +/** The runtime slice of a full config, for use as `initial_value`. */ +export function toRuntime(cfg: Config): RuntimeConfig { + const { engine_url: _drop, ...runtime } = cfg; + return runtime; +} + +export async function loadConfig(path: string): Promise { + let raw: unknown = {}; + try { + raw = parse(await readFile(path, 'utf8')) ?? {}; + } catch (err) { + // a missing config file falls back to defaults; anything else + // (YAML parse error, permissions) must fail the worker fast + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err; + } + return ConfigSchema.parse(raw); +} diff --git a/claude-code/src/configuration.ts b/claude-code/src/configuration.ts new file mode 100644 index 000000000..c3116e6ea --- /dev/null +++ b/claude-code/src/configuration.ts @@ -0,0 +1,74 @@ +/** + * Integration with the built-in `configuration` worker. `config.yaml` is the + * seed installed as `initial_value` on first registration; the live value is + * authoritative thereafter and hot-reloads on `configuration:updated`. + * + * Stream names (`events_stream` / `raw_events_stream`) are read once at boot to + * build the emitters — a change to those needs a restart, like the path-jail + * workers refuse a topology change. Every other field hot-reloads. + */ + +import type { ISdk } from 'iii-sdk'; +import { + type Config, + type RuntimeConfig, + RuntimeConfigSchema, + runtimeJsonSchema, + toRuntime, +} from './config.js'; + +const CONFIG_ID = 'claude-code'; +const CONFIG_FN_ID = 'claude::on-config-change'; +const TIMEOUT_MS = 5_000; + +/** Live snapshot shared with the handlers; `current` is whole-replaced on reload. */ +export type ConfigHolder = { current: Config }; + +export async function registerClaudeConfig(iii: ISdk, seed: Config): Promise { + await iii.trigger({ + function_id: 'configuration::register', + payload: { + id: CONFIG_ID, + name: 'Claude Code', + description: + 'Claude Code worker: per-turn defaults (model, permission mode, max turns, working directory, system-prompt append, allowed/disallowed tools), the agent::events / claude::events stream names, the approval-gate toggle, the claude CLI path, and whether to inject the iii runtime context.', + schema: runtimeJsonSchema(), + initial_value: toRuntime(seed), + }, + timeoutMs: TIMEOUT_MS, + }); +} + +/** Fetch the live runtime config; null when unset/unreachable. */ +export async function fetchRuntime(iii: ISdk): Promise { + try { + const res = await iii.trigger({ + function_id: 'configuration::get', + payload: { id: CONFIG_ID, raw: false }, + timeoutMs: TIMEOUT_MS, + }); + const value = res && typeof res === 'object' ? res.value : null; + if (value == null) return null; + return RuntimeConfigSchema.parse(value); + } catch (err) { + console.warn(`configuration::get failed for ${CONFIG_ID}: ${String(err)}`); + return null; + } +} + +/** + * Register the change handler + bind the `configuration` trigger. `onChange` is + * called once now (reconcile) and on every `configuration:updated`. + */ +export async function bindConfigTrigger(iii: ISdk, onChange: () => Promise): Promise { + await onChange(); + iii.registerFunction(CONFIG_FN_ID, async () => { + await onChange(); + return null; + }); + iii.registerTrigger({ + type: 'configuration', + function_id: CONFIG_FN_ID, + config: { configuration_id: CONFIG_ID, event_types: ['configuration:updated'] }, + }); +} diff --git a/claude-code/src/events.ts b/claude-code/src/events.ts new file mode 100644 index 000000000..19e41d882 --- /dev/null +++ b/claude-code/src/events.ts @@ -0,0 +1,28 @@ +/** + * Emit AgentEvent frames via the engine's stream builtin. Same per-process + * epoch + per-session monotonic sequence scheme as the harness emitter, so + * item_ids never collide across restarts. + */ + +import { randomUUID } from 'node:crypto'; +import type { ISdk } from 'iii-sdk'; +const PROCESS_EPOCH = randomUUID(); +const seqBySession = new Map(); + +export function makeEmitter(iii: ISdk, streamName: string) { + return async function emit(session_id: string, event: unknown): Promise { + const seq = seqBySession.get(session_id) ?? 0; + seqBySession.set(session_id, seq + 1); + const item_id = `${session_id}-${PROCESS_EPOCH}-${seq.toString().padStart(8, '0')}`; + try { + await iii.trigger({ + function_id: 'stream::set', + payload: { stream_name: streamName, group_id: session_id, item_id, data: event }, + }); + } catch (err) { + console.warn(`stream::set failed for ${session_id}: ${String(err)}`); + } + }; +} + +export type Emit = ReturnType; diff --git a/claude-code/src/executable.ts b/claude-code/src/executable.ts new file mode 100644 index 000000000..4ed5b3116 --- /dev/null +++ b/claude-code/src/executable.ts @@ -0,0 +1,26 @@ +/** + * Resolve the Claude Code CLI binary for the Agent SDK. The npm-installed + * SDK ships a native CLI as an optional platform dependency, but the + * single-file bundle (`deploy: bundle`) cannot carry it — so when the + * operator has not pinned `claude_executable` in config.yaml, fall back to + * the `claude` binary on PATH. + */ + +import { accessSync, constants } from 'node:fs'; +import { delimiter, join } from 'node:path'; + +export function resolveClaudeExecutable(configured: string): string { + if (configured) return configured; + const path = process.env.PATH ?? ''; + for (const dir of path.split(delimiter)) { + if (!dir) continue; + const candidate = join(dir, 'claude'); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + // keep scanning + } + } + return ''; +} diff --git a/claude-code/src/iii-prompt.ts b/claude-code/src/iii-prompt.ts new file mode 100644 index 000000000..94d531c3a --- /dev/null +++ b/claude-code/src/iii-prompt.ts @@ -0,0 +1,79 @@ +/** + * iii runtime context appended to the system prompt when `iii_context` is + * enabled. Carries the same engine-grounded rules as the harness identity + * prompts (harness/src/turn-orchestrator/prompt/*), retargeted from the + * `agent_trigger` tool to the `iii` CLI, which this agent reaches through + * its shell. The override rule matches the harness: a caller-supplied + * `system_prompt` wins and nothing is appended to it. + */ + +export const III_CONTEXT_PROMPT = `# iii runtime + +This machine runs an iii engine: a WebSocket-routed worker mesh whose single engine process +holds a live registry of every connected worker, every function those workers expose, and every +trigger bound to them. Every call routes worker -> engine -> worker, so the language, runtime, +and location of a worker are invisible to its callers. The function id is the ONLY contract +between two workers. + +You act on iii ONLY through the \`iii\` CLI on PATH, via your shell: + + iii trigger [key=value ...] [--json ''] [--timeout-ms ] + +Function ids are namespaced with \`::\` (e.g. \`engine::functions::list\`). Simple arguments go +as \`key=value\` pairs; structured payloads go as \`--json\` with a single-quoted JSON OBJECT. + +IMPORTANT: NEVER invent function ids or argument names from memory. Discover them from the live +engine and trust it over memory or this prompt. + +## Discovery + +The live engine is the single source of truth. Ask it — never assume: + +- \`iii trigger engine::functions::list --json '{"search":""}'\` — every function across + all workers; optional filters \`prefix\` / \`search\` / \`worker\`. Use it to FIND a function + id. +- \`iii trigger --help\` — that function's description and request schema, served by + the engine. THIS IS THE API REFERENCE for every call you make. Fetch it BEFORE the first call + to any function; a one-line description from \`list\` is a hint, not the contract. +- \`iii trigger engine::workers::list\` — every connected worker; + \`iii trigger engine::workers::info name=\` — one worker's full surface. +- \`iii trigger engine::triggers::list\` — every trigger TYPE; + \`iii trigger engine::registered-triggers::list\` — every trigger INSTANCE already bound. + +Need a backend capability? Check what is already registered FIRST — it is usually one call +away. When nothing fits, search the public registry before building anything: +\`iii trigger directory::registry::workers::list --json '{"search":""}'\` pages the +published catalogue and \`iii trigger directory::registry::workers::info name=\` returns +one worker's full detail. Say what you are about to install and why, install with +\`iii trigger worker::add --json '{"source":{"kind":"registry","name":""}}'\`, then +confirm the new ids appear via \`engine::functions::list\` with that prefix and fetch each +contract with \`--help\` as usual. + +## Calling rules + +- \`--json\` takes a JSON OBJECT in single quotes: \`--json '{"path":"/tmp"}'\`. Never pass a + JSON-encoded string where the engine expects an object — workers reject it with + \`invalid_arguments\` / \`serialization error\`. +- Long-running functions need \`--timeout-ms\` well above the default 30000. +- Triggers are the engine's push channel: NEVER poll (a loop re-reading a queue, file, or + table) when a trigger type fits — bind a trigger instead. A trigger registration succeeds + even when its type's provider is absent or the config keys are wrong — the binding lands but + never fires — so copy config keys from \`engine::triggers::info\`, not from memory. + +## Error handling + +When a call errors, READ the error and CHANGE something before the next call. NEVER resend the +same function + payload unchanged. \`invalid_arguments\` / \`missing field\` means YOUR payload +is wrong: re-read the contract via \`--help\` and fix the object, keeping the same function. +\`function_not_found\` means the id is wrong: re-check via \`engine::functions::list\`. A +repeating timeout means the approach is wrong, not the arguments: simplify, split the work, or +report the blocker and stop. + +## Boundaries + +- Files in your working directory: use your native tools (read, edit, search). The bus is not + for local file edits. +- Backend actions beyond the working directory — email, databases, storage, queues, schedules, + other services — go through registered iii functions, never ad-hoc processes or foreign + patterns carried in from other ecosystems. If you reach for a tool that is not an iii + function for a backend action, stop and re-check the engine's surface first.`; diff --git a/claude-code/src/index.ts b/claude-code/src/index.ts new file mode 100644 index 000000000..e23758ae1 --- /dev/null +++ b/claude-code/src/index.ts @@ -0,0 +1,71 @@ +#!/usr/bin/env node +/** + * Worker bootstrap: connect, register the configuration schema, fetch the live + * config, register claude::* functions, wait for SIGINT/SIGTERM. Config is + * managed by the `configuration` worker (config.yaml is the seed); the live + * value hot-reloads. Mirrors the binary-worker lifecycle. + */ + +import { parseArgs } from 'node:util'; +import { registerWorker } from 'iii-sdk'; +import { type Config, loadConfig } from './config.js'; +import { + bindConfigTrigger, + type ConfigHolder, + fetchRuntime, + registerClaudeConfig, +} from './configuration.js'; +import { makeEmitter } from './events.js'; +import { resolveClaudeExecutable } from './executable.js'; +import { register } from './run.js'; + +const { values } = parseArgs({ + options: { + config: { type: 'string', default: './config.yaml' }, + url: { type: 'string' }, + }, + strict: false, +}); + +const seed = await loadConfig(String(values.config)); +const url = values.url ? String(values.url) : seed.engine_url; + +const iii = registerWorker(url, { workerName: 'claude-code' }); + +// Best-effort: a configuration-worker hiccup at boot must not stop the worker +// from registering claude::*; it falls back to the seed via fetchRuntime. +try { + await registerClaudeConfig(iii, seed); +} catch (err) { + console.warn(`configuration::register failed; continuing with the seed: ${String(err)}`); +} + +// Live snapshot: start from the seed, then refresh from the configuration +// worker. `claude_executable` is re-resolved on every refresh so a live change +// to it (or an empty value) re-runs the PATH lookup. +const holder: ConfigHolder = { current: seed }; +const refresh = async () => { + const runtime = (await fetchRuntime(iii)) ?? undefined; + const merged: Config = runtime ? { engine_url: seed.engine_url, ...runtime } : { ...seed }; + merged.claude_executable = resolveClaudeExecutable(merged.claude_executable); + holder.current = merged; +}; + +await bindConfigTrigger(iii, refresh); + +// Emitters bind the boot stream names (a stream-name change needs a restart). +const emit = makeEmitter(iii, holder.current.events_stream); +const emitRaw = makeEmitter(iii, holder.current.raw_events_stream); +register(iii, () => holder.current, emit, emitRaw); + +console.log(`claude-code worker connected to ${url}`); + +const shutdown = async () => { + try { + await iii.shutdown?.(); + } finally { + process.exit(0); + } +}; +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); diff --git a/claude-code/src/map.ts b/claude-code/src/map.ts new file mode 100644 index 000000000..4ab162308 --- /dev/null +++ b/claude-code/src/map.ts @@ -0,0 +1,124 @@ +/** + * Map Claude Agent SDK messages to the AgentEvent wire subset. One Claude + * Code "turn" (claude::run call) becomes: + * + * assistant msg -> message_complete (+ function_execution_start per tool_use) + * user msg w/ tool_result -> function_execution_end + * result msg -> turn_end + agent_end + */ + +import type { + AgentMessage, + AssistantMessage, + ContentBlock, + FunctionResultMessage, + Usage, +} from './types.js'; + +type SdkContentBlock = { + type: string; + text?: string; + thinking?: string; + id?: string; + name?: string; + input?: unknown; + tool_use_id?: string; + content?: unknown; + is_error?: boolean; +}; + +export type ToolCallIndex = Map; + +export function toolFunctionId(name: string): string { + return name.startsWith('mcp__') + ? name.replace(/^mcp__/, '').replace(/__/g, '::') + : `claude-code::${name}`; +} + +export function mapAssistantContent( + blocks: SdkContentBlock[], + calls: ToolCallIndex, +): ContentBlock[] { + const out: ContentBlock[] = []; + for (const b of blocks) { + if (b.type === 'text' && b.text) out.push({ type: 'text', text: b.text }); + else if (b.type === 'thinking' && b.thinking) out.push({ type: 'thinking', text: b.thinking }); + else if (b.type === 'tool_use' && b.id && b.name) { + const function_id = toolFunctionId(b.name); + calls.set(b.id, { function_id, started_at: Date.now() }); + out.push({ type: 'function_call', id: b.id, function_id, arguments: b.input ?? {} }); + } + } + return out; +} + +export function mapToolResultContent(raw: unknown): ContentBlock[] { + if (typeof raw === 'string') return [{ type: 'text', text: raw }]; + if (Array.isArray(raw)) { + return raw.map((b) => { + if (typeof b === 'object' && b !== null) { + const blk = b as SdkContentBlock; + return { + type: 'text' as const, + text: blk.type === 'text' ? (blk.text ?? '') : JSON.stringify(blk), + }; + } + return { type: 'text' as const, text: JSON.stringify(b ?? null) }; + }); + } + return [{ type: 'text', text: JSON.stringify(raw ?? null) }]; +} + +export function makeAssistantMessage( + content: ContentBlock[], + model: string, + usage: Usage | null, + stop_reason = 'end', +): AssistantMessage { + return { + role: 'assistant', + content, + stop_reason, + error_message: null, + usage, + model, + provider: 'claude-code', + timestamp: Date.now(), + }; +} + +export function makeFunctionResult( + function_call_id: string, + function_id: string, + content: ContentBlock[], + is_error: boolean, +): FunctionResultMessage { + return { + role: 'function_result', + function_call_id, + function_id, + content, + details: null, + is_error, + timestamp: Date.now(), + }; +} + +export function mapUsage(raw: unknown): Usage | null { + if (typeof raw !== 'object' || raw === null) return null; + const u = raw as Record; + return { + input_tokens: u.input_tokens ?? 0, + output_tokens: u.output_tokens ?? 0, + cache_read_tokens: u.cache_read_input_tokens ?? 0, + cache_write_tokens: u.cache_creation_input_tokens ?? 0, + }; +} + +export function lastAssistant(messages: AgentMessage[]): AgentMessage { + if (messages.length === 0) return makeAssistantMessage([], '', null); + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].role === 'assistant') return messages[i]; + } + return messages[messages.length - 1]; +} diff --git a/claude-code/src/run.ts b/claude-code/src/run.ts new file mode 100644 index 000000000..ab10cc946 --- /dev/null +++ b/claude-code/src/run.ts @@ -0,0 +1,422 @@ +/** + * claude::* function registrations. `claude::run` accepts either a bare + * `prompt` string or the shared agent entrypoint shape (`messages` array of + * role/content-block messages), so anything that can drive + * `run::start_and_wait` — the acp worker, the console, another worker — can + * drive Claude Code unchanged. + */ + +import { randomUUID } from 'node:crypto'; +import { query, type Options, type PermissionResult } from '@anthropic-ai/claude-agent-sdk'; +import type { ISdk } from 'iii-sdk'; +import { z } from 'zod'; +import type { Config } from './config.js'; +import type { Emit } from './events.js'; +import { III_CONTEXT_PROMPT } from './iii-prompt.js'; +import { + lastAssistant, + makeAssistantMessage, + makeFunctionResult, + mapAssistantContent, + mapToolResultContent, + mapUsage, + type ToolCallIndex, +} from './map.js'; +import { listSessions, loadSession, saveSession } from './state.js'; +import type { AgentMessage, FunctionResultMessage, SessionRecord } from './types.js'; + +const ContentBlockSchema = z.object({ type: z.string() }).passthrough(); +const MessageSchema = z.object({ + role: z.string(), + content: z.union([z.string(), z.array(ContentBlockSchema)]), +}); + +const RunPayloadSchema = z.object({ + session_id: z + .string() + .optional() + .describe('iii session id; reuse to resume the same Claude Code conversation'), + prompt: z.string().optional().describe('The user prompt for this turn'), + messages: z + .array(MessageSchema) + .optional() + .describe( + 'Alternative to prompt: role/content messages; the last user entry becomes the prompt', + ), + model: z.string().optional().describe('Model id or alias; empty = Claude Code default'), + cwd: z.string().optional().describe('Working directory the turn runs in'), + system_prompt: z.string().optional().describe('Replace the Claude Code system prompt'), + append_system_prompt: z.string().optional().describe('Append to the default system prompt'), + permission_mode: z + .enum(['default', 'acceptEdits', 'plan', 'bypassPermissions']) + .optional() + .describe('Claude Code permission mode for this turn'), + allowed_tools: z.array(z.string()).optional().describe('Allow rules, e.g. "Bash(git *)"'), + disallowed_tools: z.array(z.string()).optional().describe('Deny rules'), + max_turns: z.number().int().positive().optional().describe('Cap on agentic turns'), + iii_context: z + .boolean() + .optional() + .describe('Append the iii runtime discovery prompt (engine catalog via the iii CLI)'), + timeout_ms: z + .number() + .int() + .positive() + .optional() + .describe('Reserved for callers; not forwarded'), + /** Raw Agent SDK options, spread over everything the worker derives. + * This is the pure pass-through: any Options field the SDK accepts + * (fork_session, include_partial_messages, betas, add dirs, mcp + * servers, ...) goes through untouched, camelCase as in the SDK. */ + options: z + .record(z.string(), z.unknown()) + .optional() + .describe( + 'Raw Agent SDK options forwarded verbatim (camelCase), e.g. forkSession, includePartialMessages', + ), +}); + +export type RunPayload = z.infer; +export { RunPayloadSchema }; + +const SessionIdSchema = z.object({ + session_id: z.string().describe('iii session id returned by claude::run / claude::start'), +}); + +const RUN_REQUEST_FORMAT = z.toJSONSchema(RunPayloadSchema); +const SESSION_ID_FORMAT = z.toJSONSchema(SessionIdSchema); + +type LiveRun = { interrupt: () => Promise }; +const live = new Map(); + +/** Best-effort: flip a session record to `error` so a failed background run + * never leaves it stuck in `working`. Swallows its own failure. */ +async function markSessionError(iii: ISdk, session_id: string): Promise { + try { + const record = await loadSession(iii, session_id); + if (record && record.status === 'working') { + record.status = 'error'; + record.updated_at_ms = Date.now(); + await saveSession(iii, record); + } + } catch (err) { + console.error(`failed to mark session ${session_id} error: ${String(err)}`); + } +} + +export function extractPrompt(payload: RunPayload): string { + if (typeof payload.prompt === 'string') return payload.prompt; + const users = (payload.messages ?? []).filter((m) => m.role === 'user'); + const last = users[users.length - 1]; + if (!last) throw new Error('claude::run requires `prompt` or a user message in `messages`'); + if (typeof last.content === 'string') return last.content; + return last.content + .map((b) => ('text' in b && typeof b.text === 'string' ? b.text : '')) + .filter(Boolean) + .join('\n'); +} + +function gatedCanUseTool(iii: ISdk, session_id: string) { + return async (toolName: string, input: Record): Promise => { + try { + const res = await iii.trigger({ + function_id: 'policy::check_permissions', + payload: { session_id, function_id: `claude-code::${toolName}`, args: input }, + timeoutMs: 5_000, + }); + const decision = res?.decision ?? res?.behavior ?? 'deny'; + if (decision === 'allow') return { behavior: 'allow', updatedInput: input }; + return { behavior: 'deny', message: `denied by approval gate (${decision})` }; + } catch { + return { behavior: 'deny', message: 'approval gate unreachable (fail-closed)' }; + } + }; +} + +function callerOptions(payload: RunPayload, cfg: Config): Partial { + const opts = { ...(payload.options as Partial | undefined) }; + if (cfg.approval_gate) { + // enforcement keys must not shadow the gate + delete (opts as Record).canUseTool; + delete (opts as Record).permissionMode; + } + return opts; +} + +export async function executeRun( + iii: ISdk, + cfg: Config, + emit: Emit, + emitRaw: Emit, + payload: RunPayload, +): Promise> { + const session_id = payload.session_id ?? randomUUID(); + const prompt = extractPrompt(payload); + // One live run per session. Reserve the slot atomically: the has-check and + // the set happen with NO await between them, so two concurrent same-session + // calls cannot both pass (JS is single-threaded). The handle's interrupt is + // a no-op placeholder until the real query exists; everything past here runs + // inside the try/finally below so the slot is always released. + if (live.has(session_id)) { + return { session_id, busy: true, reason: 'a run is already active for this session' }; + } + const handle: LiveRun = { interrupt: async () => {} }; + live.set(session_id, handle); + + try { + return await runReserved(iii, cfg, emit, emitRaw, payload, session_id, prompt, handle); + } finally { + if (live.get(session_id) === handle) live.delete(session_id); + } +} + +async function runReserved( + iii: ISdk, + cfg: Config, + emit: Emit, + emitRaw: Emit, + payload: RunPayload, + session_id: string, + prompt: string, + handle: LiveRun, +): Promise> { + const prior = await loadSession(iii, session_id); + const d = cfg.defaults; + + const record: SessionRecord = prior ?? { + session_id, + claude_session_id: null, + cwd: payload.cwd ?? d.cwd, + model: payload.model ?? d.model, + status: 'working', + turns: 0, + total_cost_usd: 0, + usage: null, + updated_at_ms: Date.now(), + }; + if (payload.cwd) record.cwd = payload.cwd; + if (payload.model) record.model = payload.model; + + const userAppend = payload.append_system_prompt ?? d.append_system_prompt; + const iiiContext = payload.iii_context ?? cfg.iii_context; + const append = [iiiContext ? III_CONTEXT_PROMPT : '', userAppend].filter(Boolean).join('\n\n'); + const options: Options = { + ...(record.model ? { model: record.model } : {}), + ...(record.cwd ? { cwd: record.cwd } : {}), + ...(prior?.claude_session_id ? { resume: prior.claude_session_id } : {}), + maxTurns: payload.max_turns ?? d.max_turns, + // with the approval gate on, the operator wants every tool call routed + // through canUseTool; bypassPermissions skips it, so the gate forces a + // mode that keeps canUseTool authoritative regardless of the caller + permissionMode: cfg.approval_gate ? 'default' : (payload.permission_mode ?? d.permission_mode), + allowedTools: [ + ...(payload.allowed_tools ?? d.allowed_tools), + // the iii context teaches discovery through the CLI; the matching + // allow rule is what lets those calls run headless + ...(iiiContext ? ['Bash(iii *)'] : []), + ], + disallowedTools: payload.disallowed_tools ?? d.disallowed_tools, + systemPrompt: payload.system_prompt + ? payload.system_prompt + : { type: 'preset', preset: 'claude_code', ...(append ? { append } : {}) }, + settingSources: [], + ...(cfg.claude_executable ? { pathToClaudeCodeExecutable: cfg.claude_executable } : {}), + ...callerOptions(payload, cfg), + ...(cfg.approval_gate ? { canUseTool: gatedCanUseTool(iii, session_id) } : {}), + }; + + const q = query({ prompt, options }); + // promote the placeholder to the real interrupt now that the query exists + handle.interrupt = () => q.interrupt(); + + // persist `working` only once the query + live handle exist, so a throw + // during setup never leaves the record stuck in `working` + record.status = 'working'; + record.updated_at_ms = Date.now(); + await saveSession(iii, record); + + const transcript: AgentMessage[] = []; + const pendingResults: FunctionResultMessage[] = []; + const calls: ToolCallIndex = new Map(); + let resultText = ''; + let stopReason = 'end'; + let isError = false; + + try { + for await (const msg of q) { + await emitRaw(session_id, msg); + if (msg.type === 'system' && msg.subtype === 'init') { + record.claude_session_id = msg.session_id; + await saveSession(iii, record); + } else if (msg.type === 'assistant') { + const usage = mapUsage(msg.message.usage); + const content = mapAssistantContent(msg.message.content as never, calls); + const assistant = makeAssistantMessage(content, msg.message.model ?? record.model, usage); + transcript.push(assistant); + await emit(session_id, { type: 'message_complete', message: assistant }); + for (const block of content) { + if (block.type === 'function_call') { + await emit(session_id, { + type: 'function_execution_start', + function_call_id: block.id, + function_id: block.function_id, + args: block.arguments, + }); + } + } + } else if (msg.type === 'user') { + const blocks = Array.isArray(msg.message.content) ? msg.message.content : []; + for (const b of blocks as unknown as Array>) { + if (b.type !== 'tool_result') continue; + const callId = String(b.tool_use_id ?? ''); + const call = calls.get(callId); + const content = mapToolResultContent(b.content); + const fr = makeFunctionResult( + callId, + call?.function_id ?? 'claude-code::unknown', + content, + b.is_error === true, + ); + transcript.push(fr); + pendingResults.push(fr); + await emit(session_id, { + type: 'function_execution_end', + function_call_id: callId, + function_id: fr.function_id, + result: { content, details: null }, + is_error: fr.is_error, + duration_ms: call ? Date.now() - call.started_at : 0, + }); + } + } else if (msg.type === 'result') { + record.claude_session_id = msg.session_id ?? record.claude_session_id; + record.turns += msg.num_turns ?? 1; + record.total_cost_usd += msg.total_cost_usd ?? 0; + record.usage = mapUsage(msg.usage); + isError = msg.is_error === true; + stopReason = msg.subtype === 'success' ? 'end' : msg.subtype; + resultText = 'result' in msg && typeof msg.result === 'string' ? msg.result : ''; + } + } + } catch (err) { + isError = true; + stopReason = 'error'; + resultText = String(err); + } + // slot release is handled by executeRun's finally (covers setup throws too) + + record.status = isError ? 'error' : 'done'; + record.updated_at_ms = Date.now(); + await saveSession(iii, record); + + if (transcript.length === 0) { + transcript.push( + makeAssistantMessage( + [{ type: 'text', text: resultText }], + record.model, + record.usage, + stopReason, + ), + ); + } + await emit(session_id, { + type: 'turn_end', + message: lastAssistant(transcript), + function_results: pendingResults, + }); + await emit(session_id, { type: 'agent_end', messages: transcript }); + + return { + session_id, + claude_session_id: record.claude_session_id, + result: resultText, + stop_reason: stopReason, + is_error: isError, + num_turns: record.turns, + total_cost_usd: record.total_cost_usd, + usage: record.usage, + }; +} + +export function register(iii: ISdk, getCfg: () => Config, emit: Emit, emitRaw: Emit): void { + iii.registerFunction( + 'claude::run', + async (payload: unknown) => + executeRun(iii, getCfg(), emit, emitRaw, RunPayloadSchema.parse(payload ?? {})), + { + description: + 'Run one Claude Code turn and wait for the result. Accepts `prompt` or a `messages` array plus a raw SDK `options` pass-through; streams raw Claude Code messages onto claude::events, AgentEvent frames onto agent::events, and returns {session_id, result, usage, total_cost_usd}.', + request_format: RUN_REQUEST_FORMAT, + }, + ); + + iii.registerFunction( + 'claude::start', + async (payload: unknown) => { + const parsed = RunPayloadSchema.parse(payload ?? {}); + const session_id = parsed.session_id ?? randomUUID(); + void executeRun(iii, getCfg(), emit, emitRaw, { ...parsed, session_id }).catch( + async (err) => { + console.error(`claude::start background run failed for ${session_id}: ${String(err)}`); + // never leave the record stuck in `working`: a failure inside the + // turn's own terminal save lands here, so mark it error best-effort + await markSessionError(iii, session_id); + }, + ); + return { session_id, started: true }; + }, + { + description: + 'Start a Claude Code turn and return immediately; watch agent::events (group_id = session_id) for progress and turn_end.', + request_format: RUN_REQUEST_FORMAT, + }, + ); + + iii.registerFunction( + 'claude::stop', + async (payload: unknown) => { + const { session_id } = SessionIdSchema.parse(payload ?? {}); + const run = live.get(session_id); + if (!run) return { session_id, stopped: false, reason: 'no live run' }; + await run.interrupt(); + return { session_id, stopped: true }; + }, + { + description: 'Interrupt a live Claude Code run for a session.', + request_format: SESSION_ID_FORMAT, + }, + ); + + iii.registerFunction( + 'claude::status', + async (payload: unknown) => { + const { session_id } = SessionIdSchema.parse(payload ?? {}); + const record = await loadSession(iii, session_id); + return { session_id, live: live.has(session_id), record }; + }, + { + description: 'Point-in-time status of a Claude Code session.', + request_format: SESSION_ID_FORMAT, + }, + ); + + iii.registerFunction( + 'claude::sessions::list', + async () => ({ sessions: await listSessions(iii) }), + { + description: 'List every Claude Code session this worker has run.', + request_format: { type: 'object', properties: {} }, + }, + ); + + iii.registerFunction( + 'run::start_and_wait', + async (payload: unknown) => + executeRun(iii, getCfg(), emit, emitRaw, RunPayloadSchema.parse(payload ?? {})), + { + description: + 'Alias for claude::run under the shared agent entrypoint: run a turn for {session_id, messages} and return when it ends.', + request_format: RUN_REQUEST_FORMAT, + }, + ); +} diff --git a/claude-code/src/state.ts b/claude-code/src/state.ts new file mode 100644 index 000000000..f5f423cbb --- /dev/null +++ b/claude-code/src/state.ts @@ -0,0 +1,33 @@ +/** + * Session registry on engine state. Scope `claude_sessions`, key = iii + * session_id. Maps iii sessions to Claude Code session ids so `claude::run` + * with the same session_id resumes the underlying Claude conversation. + */ + +import type { ISdk } from 'iii-sdk'; +import type { SessionRecord } from './types.js'; + +const SCOPE = 'claude_sessions'; + +export async function loadSession(iii: ISdk, session_id: string): Promise { + const res = await iii.trigger({ + function_id: 'state::get', + payload: { scope: SCOPE, key: session_id }, + }); + return res && typeof res === 'object' && 'session_id' in res ? res : null; +} + +export async function saveSession(iii: ISdk, record: SessionRecord): Promise { + await iii.trigger({ + function_id: 'state::set', + payload: { scope: SCOPE, key: record.session_id, value: record }, + }); +} + +export async function listSessions(iii: ISdk): Promise { + const res = await iii.trigger({ + function_id: 'state::list', + payload: { scope: SCOPE }, + }); + return Array.isArray(res) ? res : []; +} diff --git a/claude-code/src/types.ts b/claude-code/src/types.ts new file mode 100644 index 000000000..d1f7c34aa --- /dev/null +++ b/claude-code/src/types.ts @@ -0,0 +1,88 @@ +/** + * Wire types for the AgentEvent subset this worker emits onto the + * `agent::events` stream. Mirrors harness/src/types/* in iii-hq/workers so + * the console and acp worker render Claude Code turns like any other agent worker. + */ + +export type TextContent = { type: 'text'; text: string }; +export type ThinkingContent = { type: 'thinking'; text: string; signature?: string }; +export type FunctionCallContent = { + type: 'function_call'; + id: string; + function_id: string; + arguments: unknown; +}; +export type FunctionResultContent = { + type: 'function_result'; + function_call_id: string; + content: ContentBlock[]; + is_error?: boolean; +}; +export type ContentBlock = + | TextContent + | ThinkingContent + | FunctionCallContent + | FunctionResultContent; + +export type Usage = { + input_tokens: number; + output_tokens: number; + cache_read_tokens?: number; + cache_write_tokens?: number; +}; + +export type AssistantMessage = { + role: 'assistant'; + content: ContentBlock[]; + stop_reason: string; + error_message?: string | null; + usage?: Usage | null; + model: string; + provider: string; + timestamp: number; +}; + +export type UserMessage = { role: 'user'; content: ContentBlock[]; timestamp: number }; + +export type FunctionResultMessage = { + role: 'function_result'; + function_call_id: string; + function_id: string; + content: ContentBlock[]; + details: unknown; + is_error: boolean; + timestamp: number; +}; + +export type AgentMessage = UserMessage | AssistantMessage | FunctionResultMessage; + +export type AgentEvent = + | { type: 'agent_end'; messages: AgentMessage[] } + | { type: 'turn_end'; message: AgentMessage; function_results: FunctionResultMessage[] } + | { type: 'message_complete'; message: AgentMessage; body_streamed?: boolean } + | { + type: 'function_execution_start'; + function_call_id: string; + function_id: string; + args: unknown; + } + | { + type: 'function_execution_end'; + function_call_id: string; + function_id: string; + result: { content: ContentBlock[]; details: unknown }; + is_error: boolean; + duration_ms: number; + }; + +export type SessionRecord = { + session_id: string; + claude_session_id: string | null; + cwd: string; + model: string; + status: 'working' | 'done' | 'error'; + turns: number; + total_cost_usd: number; + usage: Usage | null; + updated_at_ms: number; +}; diff --git a/claude-code/tests/_helpers/fake-iii.ts b/claude-code/tests/_helpers/fake-iii.ts new file mode 100644 index 000000000..ff2ec05c6 --- /dev/null +++ b/claude-code/tests/_helpers/fake-iii.ts @@ -0,0 +1,57 @@ +import { vi } from 'vitest'; +import type { ISdk } from 'iii-sdk'; + +export type TriggerCall = { function_id: string; payload: Record }; + +export type FakeIii = { + iii: ISdk; + calls: TriggerCall[]; + state: Map; + streamFrames: (stream: string) => Array>; + registered: Map Promise>; +}; + +/** + * In-memory stand-in for the engine bus: `state::get/set/list` backed by a + * Map keyed `${scope}/${key}`, `stream::set` recorded as plain calls, and + * `registerFunction` captured so tests can invoke handlers at the same + * unknown boundary the engine uses. + */ +export function fakeIii(): FakeIii { + const calls: TriggerCall[] = []; + const state = new Map(); + const registered = new Map Promise>(); + + const iii = { + trigger: async (req: { function_id: string; payload: Record }) => { + // Clone like the wire would: the live bus serializes payloads, so + // later caller-side mutation must not rewrite recorded calls. + const payload = structuredClone(req.payload); + calls.push({ function_id: req.function_id, payload }); + const { scope, key, value } = payload as { scope?: string; key?: string; value?: unknown }; + if (req.function_id === 'state::set') { + state.set(`${scope}/${key}`, value); + return null; + } + if (req.function_id === 'state::get') return state.get(`${scope}/${key}`) ?? null; + if (req.function_id === 'state::list') { + return [...state.entries()].filter(([k]) => k.startsWith(`${scope}/`)).map(([, v]) => v); + } + return null; + }, + registerFunction: vi.fn((fnId: string, handler: (payload: unknown) => Promise) => { + registered.set(fnId, handler); + }), + } as unknown as ISdk; + + const streamFrames = (stream: string) => + calls + .filter( + (c) => + c.function_id === 'stream::set' && + (c.payload as { stream_name?: string }).stream_name === stream, + ) + .map((c) => c.payload); + + return { iii, calls, state, streamFrames, registered }; +} diff --git a/claude-code/tests/_helpers/fake-query.ts b/claude-code/tests/_helpers/fake-query.ts new file mode 100644 index 000000000..c2c195241 --- /dev/null +++ b/claude-code/tests/_helpers/fake-query.ts @@ -0,0 +1,57 @@ +/** Scripted Agent SDK `query()` replacement: yields a fixed message list and + * records the prompt/options it was called with plus interrupt() calls. */ + +export type QueryCapture = { + prompt?: string; + options?: Record; + interrupted: boolean; +}; + +export function scriptedQuery(messages: Array>, capture: QueryCapture) { + return (args: { prompt: string; options: Record }) => { + capture.prompt = args.prompt; + capture.options = args.options; + return { + async *[Symbol.asyncIterator]() { + yield* messages; + }, + interrupt: async () => { + capture.interrupted = true; + }, + }; + }; +} + +export const initMsg = { type: 'system', subtype: 'init', session_id: 'cs-1' }; + +export const assistantMsg = { + type: 'assistant', + message: { + model: 'claude-opus-4-8', + usage: { input_tokens: 5, output_tokens: 2 }, + content: [ + { type: 'text', text: 'running ls' }, + { type: 'tool_use', id: 'toolu_1', name: 'Bash', input: { command: 'ls' } }, + ], + }, +}; + +export const toolResultMsg = { + type: 'user', + message: { + content: [{ type: 'tool_result', tool_use_id: 'toolu_1', content: 'files', is_error: false }], + }, +}; + +export const resultMsg = { + type: 'result', + subtype: 'success', + session_id: 'cs-1', + num_turns: 2, + total_cost_usd: 0.01, + usage: { input_tokens: 5, output_tokens: 2 }, + result: 'done', + is_error: false, +}; + +export const fullTurn = [initMsg, assistantMsg, toolResultMsg, resultMsg]; diff --git a/claude-code/tests/config.test.ts b/claude-code/tests/config.test.ts new file mode 100644 index 000000000..b4e8910c7 --- /dev/null +++ b/claude-code/tests/config.test.ts @@ -0,0 +1,52 @@ +import { mkdtemp, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { loadConfig } from '../src/config.js'; + +describe('loadConfig', () => { + it('returns full defaults when the file is missing', async () => { + const cfg = await loadConfig('/nonexistent/config.yaml'); + expect(cfg.engine_url).toBe('ws://127.0.0.1:49134'); + expect(cfg.defaults.permission_mode).toBe('acceptEdits'); + expect(cfg.defaults.max_turns).toBe(50); + expect(cfg.approval_gate).toBe(false); + expect(cfg.events_stream).toBe('agent::events'); + expect(cfg.raw_events_stream).toBe('claude::events'); + expect(cfg.iii_context).toBe(true); + expect(cfg.claude_executable).toBe(''); + }); + + it('merges a partial file over defaults', async () => { + const dir = await mkdtemp(join(tmpdir(), 'claude-code-config-')); + const path = join(dir, 'config.yaml'); + await writeFile( + path, + [ + 'engine_url: ws://10.0.0.1:49134', + 'defaults:', + ' permission_mode: plan', + 'approval_gate: true', + ].join('\n'), + ); + const cfg = await loadConfig(path); + expect(cfg.engine_url).toBe('ws://10.0.0.1:49134'); + expect(cfg.defaults.permission_mode).toBe('plan'); + expect(cfg.defaults.max_turns).toBe(50); + expect(cfg.approval_gate).toBe(true); + }); + + it('rethrows YAML parse errors instead of silently using defaults', async () => { + const dir = await mkdtemp(join(tmpdir(), 'claude-code-config-')); + const path = join(dir, 'config.yaml'); + await writeFile(path, 'defaults: [unclosed\n bad: {'); + await expect(loadConfig(path)).rejects.toThrow(); + }); + + it('rejects an invalid permission mode', async () => { + const dir = await mkdtemp(join(tmpdir(), 'claude-code-config-')); + const path = join(dir, 'config.yaml'); + await writeFile(path, 'defaults:\n permission_mode: yolo\n'); + await expect(loadConfig(path)).rejects.toThrow(); + }); +}); diff --git a/claude-code/tests/configuration.test.ts b/claude-code/tests/configuration.test.ts new file mode 100644 index 000000000..e0e1a9fde --- /dev/null +++ b/claude-code/tests/configuration.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; +import { loadConfig, runtimeJsonSchema, toRuntime } from '../src/config.js'; +import { registerClaudeConfig } from '../src/configuration.js'; +import { fakeIii } from './_helpers/fake-iii.js'; + +describe('configuration worker integration', () => { + it('runtime schema excludes the bootstrap engine_url', () => { + const schema = runtimeJsonSchema() as { properties?: Record }; + expect(schema.properties).toBeDefined(); + expect(schema.properties).not.toHaveProperty('engine_url'); + expect(schema.properties).toHaveProperty('defaults'); + expect(schema.properties).toHaveProperty('raw_events_stream'); + }); + + it('toRuntime drops engine_url, keeps the rest', async () => { + const cfg = await loadConfig('/nonexistent/config.yaml'); + const rt = toRuntime(cfg) as Record; + expect(rt).not.toHaveProperty('engine_url'); + expect(rt.raw_events_stream).toBe('claude::events'); + expect(rt.iii_context).toBe(true); + }); + + it('registerClaudeConfig registers the schema with the seed as initial_value', async () => { + const fake = fakeIii(); + const cfg = await loadConfig('/nonexistent/config.yaml'); + await registerClaudeConfig(fake.iii, cfg); + const reg = fake.calls.find((c) => c.function_id === 'configuration::register'); + expect(reg).toBeDefined(); + const payload = reg?.payload as { + id?: string; + schema?: unknown; + initial_value?: Record; + }; + expect(payload.id).toBe('claude-code'); + expect(payload.schema).toBeDefined(); + expect(payload.initial_value).not.toHaveProperty('engine_url'); + expect(payload.initial_value?.iii_context).toBe(true); + }); +}); diff --git a/claude-code/tests/events.test.ts b/claude-code/tests/events.test.ts new file mode 100644 index 000000000..e1286ce24 --- /dev/null +++ b/claude-code/tests/events.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it, vi } from 'vitest'; +import type { ISdk } from 'iii-sdk'; +import { makeEmitter } from '../src/events.js'; +import { fakeIii } from './_helpers/fake-iii.js'; + +describe('makeEmitter', () => { + it('writes stream::set frames with the stream name, group, and event', async () => { + const fake = fakeIii(); + const emit = makeEmitter(fake.iii, 'agent::events'); + await emit('s1', { type: 'agent_end', messages: [] }); + const frames = fake.streamFrames('agent::events'); + expect(frames).toHaveLength(1); + expect(frames[0]).toMatchObject({ + stream_name: 'agent::events', + group_id: 's1', + data: { type: 'agent_end', messages: [] }, + }); + }); + + it('assigns unique, monotonically increasing item_ids per session', async () => { + const fake = fakeIii(); + const emit = makeEmitter(fake.iii, 'agent::events'); + await emit('s-seq', { a: 1 }); + await emit('s-seq', { a: 2 }); + await emit('other', { a: 3 }); + const ids = fake.streamFrames('agent::events').map((f) => String(f.item_id)); + expect(new Set(ids).size).toBe(3); + const [first, second] = ids; + expect(first < second).toBe(true); + expect(first.startsWith('s-seq-')).toBe(true); + }); + + it('swallows stream::set failures instead of failing the turn', async () => { + const iii = { + trigger: vi.fn(async () => { + throw new Error('stream adapter missing'); + }), + } as unknown as ISdk; + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const emit = makeEmitter(iii, 'agent::events'); + await expect(emit('s1', { type: 'turn_end' })).resolves.toBeUndefined(); + expect(warn).toHaveBeenCalled(); + warn.mockRestore(); + }); +}); diff --git a/claude-code/tests/executable.test.ts b/claude-code/tests/executable.test.ts new file mode 100644 index 000000000..c1f37a191 --- /dev/null +++ b/claude-code/tests/executable.test.ts @@ -0,0 +1,34 @@ +import { chmodSync, mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { resolveClaudeExecutable } from '../src/executable.js'; + +describe('resolveClaudeExecutable', () => { + const originalPath = process.env.PATH; + + beforeEach(() => { + process.env.PATH = ''; + }); + + afterEach(() => { + process.env.PATH = originalPath; + }); + + it('returns the configured path untouched', () => { + expect(resolveClaudeExecutable('/opt/claude')).toBe('/opt/claude'); + }); + + it('finds an executable claude on PATH', () => { + const dir = mkdtempSync(join(tmpdir(), 'claude-exe-')); + const bin = join(dir, 'claude'); + writeFileSync(bin, '#!/bin/sh\n'); + chmodSync(bin, 0o755); + process.env.PATH = dir; + expect(resolveClaudeExecutable('')).toBe(bin); + }); + + it('returns empty when nothing is found', () => { + expect(resolveClaudeExecutable('')).toBe(''); + }); +}); diff --git a/claude-code/tests/map.test.ts b/claude-code/tests/map.test.ts new file mode 100644 index 000000000..2dee86565 --- /dev/null +++ b/claude-code/tests/map.test.ts @@ -0,0 +1,151 @@ +import { describe, expect, it } from 'vitest'; +import { + lastAssistant, + makeAssistantMessage, + makeFunctionResult, + mapAssistantContent, + mapToolResultContent, + mapUsage, + type ToolCallIndex, + toolFunctionId, +} from '../src/map.js'; +import type { AgentMessage } from '../src/types.js'; + +describe('toolFunctionId', () => { + it('namespaces built-in Claude Code tools', () => { + expect(toolFunctionId('Bash')).toBe('claude-code::Bash'); + expect(toolFunctionId('Edit')).toBe('claude-code::Edit'); + }); + + it('maps MCP tool names to bus-style ids', () => { + expect(toolFunctionId('mcp__github__create_issue')).toBe('github::create_issue'); + expect(toolFunctionId('mcp__filesystem__read_file')).toBe('filesystem::read_file'); + }); +}); + +describe('mapAssistantContent', () => { + it('maps text, thinking, and tool_use blocks and indexes calls', () => { + const calls: ToolCallIndex = new Map(); + const blocks = [ + { type: 'text', text: 'hello' }, + { type: 'thinking', thinking: 'hmm' }, + { type: 'tool_use', id: 'toolu_1', name: 'Bash', input: { command: 'ls' } }, + ]; + const out = mapAssistantContent(blocks, calls); + expect(out).toEqual([ + { type: 'text', text: 'hello' }, + { type: 'thinking', text: 'hmm' }, + { + type: 'function_call', + id: 'toolu_1', + function_id: 'claude-code::Bash', + arguments: { command: 'ls' }, + }, + ]); + expect(calls.get('toolu_1')?.function_id).toBe('claude-code::Bash'); + }); + + it('drops empty and unknown blocks', () => { + const calls: ToolCallIndex = new Map(); + expect(mapAssistantContent([{ type: 'text' }, { type: 'mystery' }], calls)).toEqual([]); + }); +}); + +describe('mapToolResultContent', () => { + it('wraps strings as one text block', () => { + expect(mapToolResultContent('ok')).toEqual([{ type: 'text', text: 'ok' }]); + }); + + it('flattens block arrays to text', () => { + expect(mapToolResultContent([{ type: 'text', text: 'a' }, { type: 'image' }])).toEqual([ + { type: 'text', text: 'a' }, + { type: 'text', text: '{"type":"image"}' }, + ]); + }); + + it('preserves scalar array entries instead of dropping them', () => { + expect(mapToolResultContent(['plain', 42, null])).toEqual([ + { type: 'text', text: '"plain"' }, + { type: 'text', text: '42' }, + { type: 'text', text: 'null' }, + ]); + }); + + it('stringifies anything else', () => { + expect(mapToolResultContent({ ok: true })).toEqual([{ type: 'text', text: '{"ok":true}' }]); + expect(mapToolResultContent(undefined)).toEqual([{ type: 'text', text: 'null' }]); + }); +}); + +describe('mapUsage', () => { + it('maps SDK usage fields onto the wire shape', () => { + expect( + mapUsage({ + input_tokens: 10, + output_tokens: 5, + cache_read_input_tokens: 100, + cache_creation_input_tokens: 7, + }), + ).toEqual({ + input_tokens: 10, + output_tokens: 5, + cache_read_tokens: 100, + cache_write_tokens: 7, + }); + }); + + it('returns null for non-objects', () => { + expect(mapUsage(null)).toBeNull(); + expect(mapUsage('x')).toBeNull(); + }); + + it('defaults absent cache token fields to 0 instead of undefined', () => { + expect(mapUsage({ input_tokens: 3, output_tokens: 1 })).toEqual({ + input_tokens: 3, + output_tokens: 1, + cache_read_tokens: 0, + cache_write_tokens: 0, + }); + }); +}); + +describe('message constructors', () => { + it('builds an assistant message with provider claude-code', () => { + const msg = makeAssistantMessage([{ type: 'text', text: 'hi' }], 'claude-opus-4-8', null); + expect(msg.role).toBe('assistant'); + expect(msg.provider).toBe('claude-code'); + expect(msg.stop_reason).toBe('end'); + }); + + it('builds a function_result message', () => { + const fr = makeFunctionResult( + 'toolu_1', + 'claude-code::Bash', + [{ type: 'text', text: 'ok' }], + false, + ); + expect(fr.role).toBe('function_result'); + expect(fr.function_call_id).toBe('toolu_1'); + expect(fr.is_error).toBe(false); + }); +}); + +describe('lastAssistant', () => { + it('returns the last assistant message', () => { + const a1 = makeAssistantMessage([{ type: 'text', text: 'one' }], 'm', null); + const a2 = makeAssistantMessage([{ type: 'text', text: 'two' }], 'm', null); + const fr = makeFunctionResult('id', 'fn', [], false); + const messages: AgentMessage[] = [a1, a2, fr]; + expect(lastAssistant(messages)).toBe(a2); + }); + + it('falls back to the final message when no assistant exists', () => { + const fr = makeFunctionResult('id', 'fn', [], false); + expect(lastAssistant([fr])).toBe(fr); + }); + + it('returns a synthetic assistant message for an empty transcript', () => { + const msg = lastAssistant([]); + expect(msg).toMatchObject({ role: 'assistant', content: [] }); + }); +}); diff --git a/claude-code/tests/register.test.ts b/claude-code/tests/register.test.ts new file mode 100644 index 000000000..4ca53adef --- /dev/null +++ b/claude-code/tests/register.test.ts @@ -0,0 +1,200 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@anthropic-ai/claude-agent-sdk', () => ({ query: vi.fn() })); + +import { query } from '@anthropic-ai/claude-agent-sdk'; +import { loadConfig } from '../src/config.js'; +import { makeEmitter } from '../src/events.js'; +import { register } from '../src/run.js'; +import { fakeIii, type FakeIii } from './_helpers/fake-iii.js'; +import { fullTurn, scriptedQuery, type QueryCapture } from './_helpers/fake-query.js'; + +const queryMock = vi.mocked(query); + +async function registeredWorker(): Promise { + const fake = fakeIii(); + const cfg = await loadConfig('/nonexistent/config.yaml'); + const emit = makeEmitter(fake.iii, cfg.events_stream); + const emitRaw = makeEmitter(fake.iii, cfg.raw_events_stream); + register(fake.iii, () => cfg, emit, emitRaw); + return fake; +} + +beforeEach(() => { + queryMock.mockReset(); +}); + +describe('register', () => { + it('registers the full claude::* surface plus the shared entrypoint', async () => { + const fake = await registeredWorker(); + expect([...fake.registered.keys()].sort()).toEqual([ + 'claude::run', + 'claude::sessions::list', + 'claude::start', + 'claude::status', + 'claude::stop', + 'run::start_and_wait', + ]); + }); + + it('claude::run parses at the unknown boundary and runs a turn', async () => { + const fake = await registeredWorker(); + const capture: QueryCapture = { interrupted: false }; + queryMock.mockImplementation(scriptedQuery(fullTurn, capture) as never); + const res = (await fake.registered.get('claude::run')?.({ + prompt: 'hi', + session_id: 's1', + })) as Record; + expect(res.result).toBe('done'); + expect(capture.prompt).toBe('hi'); + }); + + it('claude::run rejects invalid payloads', async () => { + const fake = await registeredWorker(); + await expect( + fake.registered.get('claude::run')?.({ prompt: 'x', permission_mode: 'yolo' }), + ).rejects.toThrow(); + await expect(fake.registered.get('claude::run')?.({ max_turns: -1 })).rejects.toThrow(); + }); + + it('run::start_and_wait shares the claude::run handler behaviour', async () => { + const fake = await registeredWorker(); + const capture: QueryCapture = { interrupted: false }; + queryMock.mockImplementation(scriptedQuery(fullTurn, capture) as never); + const res = (await fake.registered.get('run::start_and_wait')?.({ + session_id: 's1', + messages: [{ role: 'user', content: [{ type: 'text', text: 'go' }] }], + })) as Record; + expect(res.result).toBe('done'); + expect(capture.prompt).toBe('go'); + }); + + it('claude::start returns immediately and the turn lands in the background', async () => { + const fake = await registeredWorker(); + const capture: QueryCapture = { interrupted: false }; + queryMock.mockImplementation(scriptedQuery(fullTurn, capture) as never); + const res = (await fake.registered.get('claude::start')?.({ prompt: 'bg' })) as Record< + string, + unknown + >; + expect(res.started).toBe(true); + expect(typeof res.session_id).toBe('string'); + await vi.waitFor(() => { + const record = fake.state.get(`claude_sessions/${res.session_id}`) as + | { status: string } + | undefined; + expect(record?.status).toBe('done'); + }); + }); + + it('marks the session error when a background run throws mid-stream', async () => { + const fake = await registeredWorker(); + queryMock.mockImplementation((() => ({ + async *[Symbol.asyncIterator]() { + yield { type: 'system', subtype: 'init', session_id: 'cs-1' }; + throw new Error('stream died'); + }, + interrupt: async () => {}, + })) as never); + const res = (await fake.registered.get('claude::start')?.({ + prompt: 'bg', + session_id: 'bg-1', + })) as Record; + expect(res.started).toBe(true); + await vi.waitFor(() => { + const record = fake.state.get('claude_sessions/bg-1') as { status: string } | undefined; + expect(record?.status).toBe('error'); + }); + }); + + it('claude::stop without a live run reports stopped: false', async () => { + const fake = await registeredWorker(); + const res = (await fake.registered.get('claude::stop')?.({ session_id: 'ghost' })) as Record< + string, + unknown + >; + expect(res).toMatchObject({ session_id: 'ghost', stopped: false }); + }); + + it('claude::stop interrupts a live run', async () => { + const fake = await registeredWorker(); + const capture: QueryCapture = { interrupted: false }; + let release: (() => void) | undefined; + const gate = new Promise((r) => { + release = r; + }); + queryMock.mockImplementation(((args: { options: Record }) => { + capture.options = args.options; + return { + async *[Symbol.asyncIterator]() { + yield { type: 'system', subtype: 'init', session_id: 'cs-1' }; + await gate; + }, + interrupt: async () => { + capture.interrupted = true; + release?.(); + }, + }; + }) as never); + + const startRes = (await fake.registered.get('claude::start')?.({ + prompt: 'long', + session_id: 'live-1', + })) as Record; + await vi.waitFor(() => { + expect(fake.state.has('claude_sessions/live-1')).toBe(true); + }); + const stopRes = (await fake.registered.get('claude::stop')?.({ + session_id: String(startRes.session_id), + })) as Record; + expect(stopRes.stopped).toBe(true); + expect(capture.interrupted).toBe(true); + }); + + it('rejects a second run while one is already live for the session', async () => { + const fake = await registeredWorker(); + let release: (() => void) | undefined; + const gate = new Promise((r) => { + release = r; + }); + queryMock.mockImplementation((() => ({ + async *[Symbol.asyncIterator]() { + yield { type: 'system', subtype: 'init', session_id: 'cs-1' }; + await gate; + }, + interrupt: async () => release?.(), + })) as never); + + await fake.registered.get('claude::start')?.({ prompt: 'first', session_id: 'busy-1' }); + await vi.waitFor(() => { + expect(fake.state.has('claude_sessions/busy-1')).toBe(true); + }); + const second = (await fake.registered.get('claude::run')?.({ + prompt: 'second', + session_id: 'busy-1', + })) as Record; + expect(second).toMatchObject({ session_id: 'busy-1', busy: true }); + release?.(); + }); + + it('claude::status reflects the stored record and live flag', async () => { + const fake = await registeredWorker(); + const res = (await fake.registered.get('claude::status')?.({ session_id: 'none' })) as Record< + string, + unknown + >; + expect(res).toMatchObject({ session_id: 'none', live: false, record: null }); + }); + + it('claude::sessions::list returns every stored record', async () => { + const fake = await registeredWorker(); + const capture: QueryCapture = { interrupted: false }; + queryMock.mockImplementation(scriptedQuery(fullTurn, capture) as never); + await fake.registered.get('claude::run')?.({ prompt: 'a', session_id: 's1' }); + await fake.registered.get('claude::run')?.({ prompt: 'b', session_id: 's2' }); + const res = (await fake.registered.get('claude::sessions::list')?.({})) as { + sessions: Array<{ session_id: string }>; + }; + expect(res.sessions.map((s) => s.session_id).sort()).toEqual(['s1', 's2']); + }); +}); diff --git a/claude-code/tests/run-payload.test.ts b/claude-code/tests/run-payload.test.ts new file mode 100644 index 000000000..a49bb253c --- /dev/null +++ b/claude-code/tests/run-payload.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from 'vitest'; +import { extractPrompt, RunPayloadSchema } from '../src/run.js'; + +describe('RunPayloadSchema', () => { + it('accepts a bare prompt', () => { + const p = RunPayloadSchema.parse({ prompt: 'hi' }); + expect(p.prompt).toBe('hi'); + }); + + it('accepts the messages array shape', () => { + const p = RunPayloadSchema.parse({ + session_id: 's1', + messages: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }], + }); + expect(p.messages).toHaveLength(1); + }); + + it('rejects an invalid permission mode', () => { + expect(() => RunPayloadSchema.parse({ prompt: 'x', permission_mode: 'yolo' })).toThrow(); + }); +}); + +describe('extractPrompt', () => { + it('prefers the prompt field', () => { + expect(extractPrompt(RunPayloadSchema.parse({ prompt: 'direct' }))).toBe('direct'); + }); + + it('joins text blocks from the last user message', () => { + const p = RunPayloadSchema.parse({ + messages: [ + { role: 'user', content: [{ type: 'text', text: 'first' }] }, + { role: 'assistant', content: [{ type: 'text', text: 'reply' }] }, + { + role: 'user', + content: [ + { type: 'text', text: 'line one' }, + { type: 'text', text: 'line two' }, + ], + }, + ], + }); + expect(extractPrompt(p)).toBe('line one\nline two'); + }); + + it('accepts plain-string message content', () => { + const p = RunPayloadSchema.parse({ messages: [{ role: 'user', content: 'plain' }] }); + expect(extractPrompt(p)).toBe('plain'); + }); + + it('throws when no user message exists', () => { + const p = RunPayloadSchema.parse({ messages: [{ role: 'assistant', content: 'x' }] }); + expect(() => extractPrompt(p)).toThrow(); + }); +}); diff --git a/claude-code/tests/run.test.ts b/claude-code/tests/run.test.ts new file mode 100644 index 000000000..343605986 --- /dev/null +++ b/claude-code/tests/run.test.ts @@ -0,0 +1,429 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@anthropic-ai/claude-agent-sdk', () => ({ query: vi.fn() })); + +import { query } from '@anthropic-ai/claude-agent-sdk'; +import { loadConfig, type Config } from '../src/config.js'; +import { makeEmitter } from '../src/events.js'; +import { executeRun, RunPayloadSchema } from '../src/run.js'; +import { fakeIii } from './_helpers/fake-iii.js'; +import { fullTurn, scriptedQuery, type QueryCapture } from './_helpers/fake-query.js'; + +const queryMock = vi.mocked(query); + +async function baseConfig(): Promise { + return loadConfig('/nonexistent/config.yaml'); +} + +async function runTurn( + payload: Record, + messages: Array> = fullTurn, + cfgOverrides: Partial = {}, +) { + const fake = fakeIii(); + const cfg = { ...(await baseConfig()), ...cfgOverrides }; + const capture: QueryCapture = { interrupted: false }; + queryMock.mockImplementation(scriptedQuery(messages, capture) as never); + const emit = makeEmitter(fake.iii, cfg.events_stream); + const emitRaw = makeEmitter(fake.iii, cfg.raw_events_stream); + const result = await executeRun(fake.iii, cfg, emit, emitRaw, RunPayloadSchema.parse(payload)); + return { fake, capture, result }; +} + +beforeEach(() => { + queryMock.mockReset(); +}); + +describe('executeRun', () => { + it('releases the live slot even when the working-save rejects (no stuck busy)', async () => { + const fake = fakeIii(); + const cfg = await baseConfig(); + const capture: QueryCapture = { interrupted: false }; + queryMock.mockImplementation(scriptedQuery(fullTurn, capture) as never); + // make every state::set reject — the working-save during setup throws + const realTrigger = fake.iii.trigger.bind(fake.iii); + (fake.iii as { trigger: unknown }).trigger = async (req: { function_id: string }) => { + if (req.function_id === 'state::set') throw new Error('store down'); + return realTrigger(req as never); + }; + const emit = makeEmitter(fake.iii, cfg.events_stream); + await executeRun( + fake.iii, + cfg, + emit, + emit, + RunPayloadSchema.parse({ prompt: 'x', session_id: 'leak-1' }), + ).catch(() => {}); + // slot must be free — a second run is not falsely "busy" + const second = (await executeRun( + fake.iii, + cfg, + emit, + emit, + RunPayloadSchema.parse({ prompt: 'x', session_id: 'leak-1' }), + ).catch(() => ({ busy: undefined }))) as Record; + expect(second.busy).not.toBe(true); + }); + + it('returns the result with mapped usage and cost', async () => { + const { result } = await runTurn({ prompt: 'do it', session_id: 's1' }); + expect(result).toMatchObject({ + session_id: 's1', + claude_session_id: 'cs-1', + result: 'done', + stop_reason: 'end', + is_error: false, + num_turns: 2, + total_cost_usd: 0.01, + usage: { input_tokens: 5, output_tokens: 2 }, + }); + }); + + it('persists the session record working then done with the Claude session id', async () => { + const { fake } = await runTurn({ prompt: 'x', session_id: 's1' }); + const sets = fake.calls.filter( + (c) => + c.function_id === 'state::set' && + (c.payload as { scope?: string }).scope === 'claude_sessions', + ); + const statuses = sets.map((c) => (c.payload.value as { status: string }).status); + expect(statuses[0]).toBe('working'); + expect(statuses[statuses.length - 1]).toBe('done'); + const final = sets[sets.length - 1].payload.value as Record; + expect(final.claude_session_id).toBe('cs-1'); + expect(final.total_cost_usd).toBe(0.01); + }); + + it('mirrors every SDK message verbatim onto the raw stream', async () => { + const { fake } = await runTurn({ prompt: 'x', session_id: 's1' }); + const raw = fake.streamFrames('claude::events').map((f) => f.data); + expect(raw).toEqual(fullTurn); + const groupIds = fake.streamFrames('claude::events').map((f) => f.group_id); + expect(new Set(groupIds)).toEqual(new Set(['s1'])); + }); + + it('emits the translated AgentEvent sequence on agent::events', async () => { + const { fake } = await runTurn({ prompt: 'x', session_id: 's1' }); + const types = fake.streamFrames('agent::events').map((f) => (f.data as { type: string }).type); + expect(types).toEqual([ + 'message_complete', + 'function_execution_start', + 'function_execution_end', + 'turn_end', + 'agent_end', + ]); + const [start, end] = fake + .streamFrames('agent::events') + .map((f) => f.data as Record) + .filter((d) => String(d.type).startsWith('function_execution')); + expect(start).toMatchObject({ + function_call_id: 'toolu_1', + function_id: 'claude-code::Bash', + args: { command: 'ls' }, + }); + expect(end).toMatchObject({ function_call_id: 'toolu_1', is_error: false }); + }); + + it('appends the iii runtime context to the system prompt by default', async () => { + const { capture } = await runTurn({ prompt: 'x', session_id: 's1' }); + const sp = capture.options?.systemPrompt as { type: string; append?: string }; + expect(sp.type).toBe('preset'); + expect(sp.append).toContain('# iii runtime'); + expect(sp.append).toContain('iii trigger engine::functions::list'); + expect(capture.options?.allowedTools).toContain('Bash(iii *)'); + }); + + it('stacks the iii context with a user append', async () => { + const { capture } = await runTurn({ + prompt: 'x', + session_id: 's1', + append_system_prompt: 'house rules', + }); + const sp = capture.options?.systemPrompt as { type: string; append?: string }; + expect(sp.append).toContain('# iii runtime'); + expect(sp.append).toContain('house rules'); + expect(sp.append?.indexOf('# iii runtime')).toBeLessThan( + sp.append?.indexOf('house rules') ?? -1, + ); + }); + + it('config-level iii_context: false disables the block for every turn', async () => { + const { capture } = await runTurn({ prompt: 'x', session_id: 's1' }, fullTurn, { + iii_context: false, + }); + const sp = capture.options?.systemPrompt as { type: string; append?: string }; + expect(sp.append).toBeUndefined(); + expect(capture.options?.allowedTools).not.toContain('Bash(iii *)'); + }); + + it('omits the iii context when disabled per turn and keeps the user append', async () => { + const { capture } = await runTurn({ + prompt: 'x', + session_id: 's1', + iii_context: false, + append_system_prompt: 'house rules', + }); + const sp = capture.options?.systemPrompt as { type: string; append?: string }; + expect(sp.append).toBe('house rules'); + expect(capture.options?.allowedTools).not.toContain('Bash(iii *)'); + }); + + it('a caller-supplied system_prompt wins verbatim with nothing appended', async () => { + const { capture } = await runTurn({ prompt: 'x', session_id: 's1', system_prompt: 'override' }); + expect(capture.options?.systemPrompt).toBe('override'); + }); + + it('passes worker defaults and named fields to query options', async () => { + const { capture } = await runTurn({ + prompt: 'x', + session_id: 's1', + cwd: '/repo', + model: 'claude-sonnet-4-6', + permission_mode: 'plan', + max_turns: 7, + }); + expect(capture.prompt).toBe('x'); + expect(capture.options).toMatchObject({ + cwd: '/repo', + model: 'claude-sonnet-4-6', + permissionMode: 'plan', + maxTurns: 7, + settingSources: [], + }); + }); + + it('forwards raw SDK options verbatim and lets them win over derived fields', async () => { + const { capture } = await runTurn({ + prompt: 'x', + session_id: 's1', + max_turns: 7, + options: { forkSession: true, includePartialMessages: true, maxTurns: 3 }, + }); + expect(capture.options).toMatchObject({ + forkSession: true, + includePartialMessages: true, + maxTurns: 3, + }); + }); + + it('resumes the prior Claude session for a known session_id', async () => { + const fake = fakeIii(); + fake.state.set('claude_sessions/s1', { + session_id: 's1', + claude_session_id: 'cs-prior', + cwd: '/repo', + model: '', + status: 'done', + turns: 1, + total_cost_usd: 0.01, + usage: null, + updated_at_ms: 1, + }); + const cfg = await baseConfig(); + const capture: QueryCapture = { interrupted: false }; + queryMock.mockImplementation(scriptedQuery(fullTurn, capture) as never); + const emit = makeEmitter(fake.iii, cfg.events_stream); + const result = await executeRun( + fake.iii, + cfg, + emit, + emit, + RunPayloadSchema.parse({ prompt: 'again', session_id: 's1' }), + ); + expect(capture.options?.resume).toBe('cs-prior'); + expect(result.num_turns).toBe(3); + }); + + it('honors per-turn cwd and model overrides on a resumed session', async () => { + const fake = fakeIii(); + fake.state.set('claude_sessions/s1', { + session_id: 's1', + claude_session_id: 'cs-prior', + cwd: '/old/repo', + model: 'old-model', + status: 'done', + turns: 1, + total_cost_usd: 0, + usage: null, + updated_at_ms: 1, + }); + const cfg = await baseConfig(); + const capture: QueryCapture = { interrupted: false }; + queryMock.mockImplementation(scriptedQuery(fullTurn, capture) as never); + const emit = makeEmitter(fake.iii, cfg.events_stream); + await executeRun( + fake.iii, + cfg, + emit, + emit, + RunPayloadSchema.parse({ + prompt: 'x', + session_id: 's1', + cwd: '/new/repo', + model: 'new-model', + }), + ); + expect(capture.options).toMatchObject({ + cwd: '/new/repo', + model: 'new-model', + resume: 'cs-prior', + }); + }); + + it('extracts the prompt from the last user message of a messages payload', async () => { + const { capture } = await runTurn({ + session_id: 's1', + messages: [ + { role: 'user', content: [{ type: 'text', text: 'first' }] }, + { role: 'assistant', content: [{ type: 'text', text: 'reply' }] }, + { role: 'user', content: [{ type: 'text', text: 'second' }] }, + ], + }); + expect(capture.prompt).toBe('second'); + }); + + it('marks the record error and still closes the turn when the SDK throws', async () => { + const fake = fakeIii(); + const cfg = await baseConfig(); + queryMock.mockImplementation((() => { + return { + async *[Symbol.asyncIterator]() { + yield { type: 'system', subtype: 'init', session_id: 'cs-1' }; + throw new Error('spawn failed'); + }, + interrupt: async () => {}, + }; + }) as never); + const emit = makeEmitter(fake.iii, cfg.events_stream); + const result = await executeRun( + fake.iii, + cfg, + emit, + emit, + RunPayloadSchema.parse({ prompt: 'x', session_id: 's1' }), + ); + expect(result.is_error).toBe(true); + expect(result.stop_reason).toBe('error'); + expect(String(result.result)).toContain('spawn failed'); + const record = fake.state.get('claude_sessions/s1') as { status: string }; + expect(record.status).toBe('error'); + const types = fake.streamFrames('agent::events').map((f) => (f.data as { type: string }).type); + expect(types).toContain('turn_end'); + expect(types).toContain('agent_end'); + }); + + it('reports non-success result subtypes as the stop reason', async () => { + const turn = [ + { type: 'system', subtype: 'init', session_id: 'cs-1' }, + { ...fullTurn[3], subtype: 'error_max_turns', is_error: true }, + ]; + const { result } = await runTurn({ prompt: 'x', session_id: 's1' }, turn); + expect(result.stop_reason).toBe('error_max_turns'); + expect(result.is_error).toBe(true); + }); + + it('omits the resume option for a fresh session', async () => { + const { capture } = await runTurn({ prompt: 'x', session_id: 'fresh' }); + expect(capture.options).not.toHaveProperty('resume'); + }); +}); + +describe('approval gate', () => { + async function captureCanUseTool(policyReply: () => Promise) { + const fake = fakeIii(); + const cfg = { ...(await baseConfig()), approval_gate: true }; + const originalTrigger = fake.iii.trigger.bind(fake.iii); + (fake.iii as { trigger: unknown }).trigger = async (req: { + function_id: string; + payload: Record; + }) => { + if (req.function_id === 'policy::check_permissions') return policyReply(); + return originalTrigger(req as never); + }; + const capture: QueryCapture = { interrupted: false }; + queryMock.mockImplementation(scriptedQuery(fullTurn, capture) as never); + const emit = makeEmitter(fake.iii, cfg.events_stream); + await executeRun( + fake.iii, + cfg, + emit, + emit, + RunPayloadSchema.parse({ prompt: 'x', session_id: 's1' }), + ); + return capture.options?.canUseTool as ( + tool: string, + input: Record, + ) => Promise<{ behavior: string; message?: string }>; + } + + it('allows when the policy says allow', async () => { + const canUseTool = await captureCanUseTool(async () => ({ decision: 'allow' })); + expect(canUseTool).toBeTypeOf('function'); + await expect(canUseTool('Bash', { command: 'ls' })).resolves.toMatchObject({ + behavior: 'allow', + }); + }); + + it('denies when the policy says deny', async () => { + const canUseTool = await captureCanUseTool(async () => ({ decision: 'deny' })); + await expect(canUseTool('Bash', { command: 'rm' })).resolves.toMatchObject({ + behavior: 'deny', + }); + }); + + it('fails closed when the policy is unreachable', async () => { + const canUseTool = await captureCanUseTool(async () => { + throw new Error('gate down'); + }); + const verdict = await canUseTool('Bash', { command: 'ls' }); + expect(verdict.behavior).toBe('deny'); + expect(verdict.message).toContain('fail-closed'); + }); + + it('does not install canUseTool when the gate is off', async () => { + const { capture } = await runTurn({ prompt: 'x', session_id: 's1' }); + expect(capture.options).not.toHaveProperty('canUseTool'); + }); + + it('caller options cannot shadow the gate', async () => { + const fake = fakeIii(); + const cfg = { ...(await baseConfig()), approval_gate: true }; + const capture: QueryCapture = { interrupted: false }; + queryMock.mockImplementation(scriptedQuery(fullTurn, capture) as never); + const emit = makeEmitter(fake.iii, cfg.events_stream); + await executeRun( + fake.iii, + cfg, + emit, + emit, + RunPayloadSchema.parse({ + prompt: 'x', + session_id: 's1', + options: { canUseTool: 'overridden', permissionMode: 'bypassPermissions' }, + }), + ); + expect(capture.options?.canUseTool).toBeTypeOf('function'); + expect(capture.options?.permissionMode).not.toBe('bypassPermissions'); + }); + + it('the named permission_mode field cannot bypass the gate either', async () => { + const fake = fakeIii(); + const cfg = { ...(await baseConfig()), approval_gate: true }; + const capture: QueryCapture = { interrupted: false }; + queryMock.mockImplementation(scriptedQuery(fullTurn, capture) as never); + const emit = makeEmitter(fake.iii, cfg.events_stream); + await executeRun( + fake.iii, + cfg, + emit, + emit, + RunPayloadSchema.parse({ + prompt: 'x', + session_id: 's1', + permission_mode: 'bypassPermissions', + }), + ); + expect(capture.options?.permissionMode).toBe('default'); + expect(capture.options?.canUseTool).toBeTypeOf('function'); + }); +}); diff --git a/claude-code/tests/state.test.ts b/claude-code/tests/state.test.ts new file mode 100644 index 000000000..0b9567185 --- /dev/null +++ b/claude-code/tests/state.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest'; +import type { ISdk } from 'iii-sdk'; +import { listSessions, loadSession, saveSession } from '../src/state.js'; +import type { SessionRecord } from '../src/types.js'; +import { fakeIii } from './_helpers/fake-iii.js'; + +function record(session_id: string): SessionRecord { + return { + session_id, + claude_session_id: `cs-${session_id}`, + cwd: '/repo', + model: '', + status: 'done', + turns: 1, + total_cost_usd: 0.01, + usage: null, + updated_at_ms: 1, + }; +} + +describe('session state', () => { + it('round-trips a record through scope claude_sessions', async () => { + const fake = fakeIii(); + await saveSession(fake.iii, record('s1')); + const set = fake.calls.find((c) => c.function_id === 'state::set'); + expect(set?.payload).toMatchObject({ scope: 'claude_sessions', key: 's1' }); + await expect(loadSession(fake.iii, 's1')).resolves.toMatchObject({ + session_id: 's1', + claude_session_id: 'cs-s1', + }); + }); + + it('returns null for unknown sessions', async () => { + const fake = fakeIii(); + await expect(loadSession(fake.iii, 'missing')).resolves.toBeNull(); + }); + + it('handles state::get returning the value directly, without an envelope', async () => { + // The engine returns the stored value itself, not {value}; a worker + // built against the envelope shape silently loses resume (caught live). + const iii = { + trigger: async (req: { function_id: string }) => + req.function_id === 'state::get' ? record('s1') : null, + } as unknown as ISdk; + await expect(loadSession(iii, 's1')).resolves.toMatchObject({ session_id: 's1' }); + }); + + it('lists every record and tolerates a non-array reply', async () => { + const fake = fakeIii(); + await saveSession(fake.iii, record('s1')); + await saveSession(fake.iii, record('s2')); + const sessions = await listSessions(fake.iii); + expect(sessions.map((s) => s.session_id).sort()).toEqual(['s1', 's2']); + + const iii = { trigger: async () => null } as unknown as ISdk; + await expect(listSessions(iii)).resolves.toEqual([]); + }); +}); diff --git a/claude-code/tsconfig.json b/claude-code/tsconfig.json new file mode 100644 index 000000000..9a3b09b03 --- /dev/null +++ b/claude-code/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "declaration": false, + "sourceMap": false, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} diff --git a/claude-code/vitest.config.ts b/claude-code/vitest.config.ts new file mode 100644 index 000000000..1c1ef7836 --- /dev/null +++ b/claude-code/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['tests/**/*.test.ts'], + environment: 'node', + globals: false, + testTimeout: 10000, + }, +});