diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..8c2d7c2 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,34 @@ +name: Check + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.23.0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install + run: pnpm install --frozen-lockfile + + - name: Check + run: pnpm check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d122355 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +coverage/ +dist/ +.turbo/ +.DS_Store +*.log + diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..f301fed --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +auto-install-peers=false diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..90abee2 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "printWidth": 100, + "semi": true, + "singleQuote": false, + "trailingComma": "all" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8fcc87c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# Contributing + +Cookbook recipes should be small, runnable, and copyable. + +## Recipe Rules + +- Put each recipe in `recipes//`. +- Include `README.md` and `index.ts`. +- Add the recipe to `recipes/manifest.json`. +- Import the SDK as `@openclaw/sdk`, not from OpenClaw monorepo internals. +- Keep recipe code focused on one SDK concept. +- Prefer environment variables for Gateway configuration. +- Add or update tests in `test/recipes.test.ts`. + +## Checks + +Run the full gate before opening a PR: + +```bash +pnpm check +``` + +Until `@openclaw/sdk` is published, tests use `test/shims/openclaw-sdk.ts`. +That shim is only a local validation aid; recipe source should still reflect the +real public SDK API. diff --git a/README.md b/README.md index cfd9939..07c06bf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,72 @@ -# cookbook -Example apps for the OpenClaw SDK +# OpenClaw Cookbook + +Runnable examples for building on the OpenClaw SDK. + +This repository is the public, copyable companion to the SDK. The recipes are +small enough to paste into an app, while the examples show how to compose them +into complete developer workflows. + +## Status + +The SDK package is landing in `openclaw/openclaw` first. Until `@openclaw/sdk` +is published, this repo keeps a tiny test shim so CI can validate recipe shape +without depending on a live Gateway or unpublished package. + +## Quick Start + +```bash +pnpm install +pnpm check +``` + +To run a recipe against a real Gateway, install the SDK once it is published and +set the Gateway connection details: + +```bash +pnpm add @openclaw/sdk +export OPENCLAW_GATEWAY=auto +export OPENCLAW_AGENT_ID=main +pnpm recipe:run-agent -- "Summarize this repository" +``` + +Useful environment variables: + +| Name | Purpose | +| ---------------------- | ------------------------------------------------------------------- | +| `OPENCLAW_GATEWAY` | Gateway URL, or `auto` for local discovery. | +| `OPENCLAW_TOKEN` | Bearer token for protected Gateways. | +| `OPENCLAW_PASSWORD` | Password for protected Gateways. | +| `OPENCLAW_AGENT_ID` | Agent id used by recipes. Defaults to `main`. | +| `OPENCLAW_SESSION_KEY` | Session key for recipes that reuse a conversation. | +| `OPENCLAW_MODEL` | Optional model override, such as `openrouter/deepseek/deepseek-r1`. | + +## Recipes + +| Recipe | What it shows | +| ---------------------------------------------- | --------------------------------------------------- | +| [`run-an-agent`](recipes/run-an-agent) | Start a run and wait for a stable result envelope. | +| [`stream-events`](recipes/stream-events) | Subscribe to normalized SDK events for a run. | +| [`cancel-a-run`](recipes/cancel-a-run) | Cancel active work by run id. | +| [`reuse-session`](recipes/reuse-session) | Create or reuse a session across multiple messages. | +| [`model-status`](recipes/model-status) | Check configured model providers and auth status. | +| [`custom-transport`](recipes/custom-transport) | Test SDK code with an in-memory transport. | + +## Examples + +| Example | What it is | +| ------------------------------- | ----------------------------------------------------- | +| [`node-cli`](examples/node-cli) | A small command-line app that wraps the core recipes. | + +## Repository Scripts + +```bash +pnpm format:check +pnpm typecheck +pnpm test +pnpm docs:check +pnpm check +``` + +The test suite aliases `@openclaw/sdk` to `test/shims/openclaw-sdk.ts`. That +shim exists only for cookbook validation. Recipe source imports `@openclaw/sdk` +directly so copied code matches real SDK usage. diff --git a/examples/node-cli/README.md b/examples/node-cli/README.md new file mode 100644 index 0000000..69d0fc4 --- /dev/null +++ b/examples/node-cli/README.md @@ -0,0 +1,13 @@ +# Node CLI Example + +A small command-line wrapper around the cookbook recipes. + +```bash +pnpm example:node-cli run "Say hello" +pnpm example:node-cli stream "Explain this branch" +pnpm example:node-cli models +pnpm example:node-cli session +``` + +In a real app, keep the recipe functions as library code and make the CLI only +responsible for parsing arguments, output formatting, and exit codes. diff --git a/examples/node-cli/src/index.ts b/examples/node-cli/src/index.ts new file mode 100644 index 0000000..0818e8f --- /dev/null +++ b/examples/node-cli/src/index.ts @@ -0,0 +1,45 @@ +import { cancelRunRecipe } from "../../../recipes/cancel-a-run/index.js"; +import { modelStatusRecipe } from "../../../recipes/model-status/index.js"; +import { reuseSessionRecipe } from "../../../recipes/reuse-session/index.js"; +import { runAgentRecipe } from "../../../recipes/run-an-agent/index.js"; +import { streamEventsRecipe } from "../../../recipes/stream-events/index.js"; + +function usage(): string { + return [ + "Usage: pnpm example:node-cli [prompt]", + "", + "Commands:", + " run Start a run and wait for the result", + " stream Start a run and print normalized event summaries", + " cancel Start a run and cancel it", + " session Reuse a session for two messages", + " models Print model provider/auth status", + ].join("\n"); +} + +async function main(argv: string[]): Promise { + const [command, ...rest] = argv; + const input = rest.join(" ") || undefined; + switch (command) { + case "run": + return await runAgentRecipe({ input }); + case "stream": + return await streamEventsRecipe({ input }); + case "cancel": + return await cancelRunRecipe({ input }); + case "session": + return await reuseSessionRecipe({ firstInput: input }); + case "models": + return await modelStatusRecipe(); + default: + return usage(); + } +} + +try { + const result = await main(process.argv.slice(2)); + console.log(typeof result === "string" ? result : JSON.stringify(result, null, 2)); +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..03e1e50 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "@openclaw/cookbook", + "version": "0.0.0", + "private": true, + "type": "module", + "packageManager": "pnpm@10.23.0", + "scripts": { + "check": "pnpm format:check && pnpm typecheck && pnpm test && pnpm docs:check", + "docs:check": "node scripts/check-docs.mjs", + "example:node-cli": "tsx examples/node-cli/src/index.ts", + "format": "prettier --write .", + "format:check": "prettier --check .", + "recipe:cancel-a-run": "tsx recipes/cancel-a-run/index.ts", + "recipe:custom-transport": "tsx recipes/custom-transport/index.ts", + "recipe:model-status": "tsx recipes/model-status/index.ts", + "recipe:reuse-session": "tsx recipes/reuse-session/index.ts", + "recipe:run-agent": "tsx recipes/run-an-agent/index.ts", + "recipe:stream-events": "tsx recipes/stream-events/index.ts", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "peerDependencies": { + "@openclaw/sdk": "*" + }, + "peerDependenciesMeta": { + "@openclaw/sdk": { + "optional": true + } + }, + "devDependencies": { + "@types/node": "^24.10.1", + "prettier": "^3.7.4", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vitest": "^4.1.5" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..472aab0 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1412 @@ +lockfileVersion: "9.0" + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false + +importers: + .: + devDependencies: + "@types/node": + specifier: ^24.10.1 + version: 24.12.2 + prettier: + specifier: ^3.7.4 + version: 3.8.3 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.1.5 + version: 4.1.5(@types/node@24.12.2)(esbuild@0.27.7)(tsx@4.21.0) + +packages: + "@emnapi/core@1.10.0": + resolution: + { + integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==, + } + + "@emnapi/runtime@1.10.0": + resolution: + { + integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==, + } + + "@emnapi/wasi-threads@1.2.1": + resolution: + { + integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==, + } + + "@esbuild/aix-ppc64@0.27.7": + resolution: + { + integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==, + } + engines: { node: ">=18" } + cpu: [ppc64] + os: [aix] + + "@esbuild/android-arm64@0.27.7": + resolution: + { + integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [android] + + "@esbuild/android-arm@0.27.7": + resolution: + { + integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==, + } + engines: { node: ">=18" } + cpu: [arm] + os: [android] + + "@esbuild/android-x64@0.27.7": + resolution: + { + integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [android] + + "@esbuild/darwin-arm64@0.27.7": + resolution: + { + integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==, + } + engines: { node: ">=18" } + cpu: [arm64] + 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/freebsd-arm64@0.27.7": + resolution: + { + integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==, + } + engines: { node: ">=18" } + cpu: [arm64] + 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/linux-arm64@0.27.7": + resolution: + { + integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [linux] + + "@esbuild/linux-arm@0.27.7": + resolution: + { + integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==, + } + engines: { node: ">=18" } + cpu: [arm] + 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-loong64@0.27.7": + resolution: + { + integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==, + } + engines: { node: ">=18" } + cpu: [loong64] + os: [linux] + + "@esbuild/linux-mips64el@0.27.7": + resolution: + { + integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==, + } + engines: { node: ">=18" } + cpu: [mips64el] + 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-riscv64@0.27.7": + resolution: + { + integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==, + } + engines: { node: ">=18" } + cpu: [riscv64] + os: [linux] + + "@esbuild/linux-s390x@0.27.7": + resolution: + { + integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==, + } + engines: { node: ">=18" } + cpu: [s390x] + os: [linux] + + "@esbuild/linux-x64@0.27.7": + resolution: + { + integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [linux] + + "@esbuild/netbsd-arm64@0.27.7": + resolution: + { + integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [netbsd] + + "@esbuild/netbsd-x64@0.27.7": + resolution: + { + integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [netbsd] + + "@esbuild/openbsd-arm64@0.27.7": + resolution: + { + integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [openbsd] + + "@esbuild/openbsd-x64@0.27.7": + resolution: + { + integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [openbsd] + + "@esbuild/openharmony-arm64@0.27.7": + resolution: + { + integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [openharmony] + + "@esbuild/sunos-x64@0.27.7": + resolution: + { + integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [sunos] + + "@esbuild/win32-arm64@0.27.7": + resolution: + { + integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [win32] + + "@esbuild/win32-ia32@0.27.7": + resolution: + { + integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==, + } + engines: { node: ">=18" } + cpu: [ia32] + os: [win32] + + "@esbuild/win32-x64@0.27.7": + resolution: + { + integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [win32] + + "@jridgewell/sourcemap-codec@1.5.5": + resolution: + { + integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, + } + + "@napi-rs/wasm-runtime@1.1.4": + resolution: + { + integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==, + } + peerDependencies: + "@emnapi/core": ^1.7.1 + "@emnapi/runtime": ^1.7.1 + + "@oxc-project/types@0.127.0": + resolution: + { + integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==, + } + + "@rolldown/binding-android-arm64@1.0.0-rc.17": + resolution: + { + integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [android] + + "@rolldown/binding-darwin-arm64@1.0.0-rc.17": + resolution: + { + integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [darwin] + + "@rolldown/binding-darwin-x64@1.0.0-rc.17": + resolution: + { + integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [darwin] + + "@rolldown/binding-freebsd-x64@1.0.0-rc.17": + resolution: + { + integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [freebsd] + + "@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17": + resolution: + { + integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm] + os: [linux] + + "@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17": + resolution: + { + integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [linux] + + "@rolldown/binding-linux-arm64-musl@1.0.0-rc.17": + resolution: + { + integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [linux] + + "@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17": + resolution: + { + integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [ppc64] + os: [linux] + + "@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17": + resolution: + { + integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [s390x] + os: [linux] + + "@rolldown/binding-linux-x64-gnu@1.0.0-rc.17": + resolution: + { + integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [linux] + + "@rolldown/binding-linux-x64-musl@1.0.0-rc.17": + resolution: + { + integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [linux] + + "@rolldown/binding-openharmony-arm64@1.0.0-rc.17": + resolution: + { + integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [openharmony] + + "@rolldown/binding-wasm32-wasi@1.0.0-rc.17": + resolution: + { + integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [wasm32] + + "@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17": + resolution: + { + integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [win32] + + "@rolldown/binding-win32-x64-msvc@1.0.0-rc.17": + resolution: + { + integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [win32] + + "@rolldown/pluginutils@1.0.0-rc.17": + resolution: + { + integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==, + } + + "@standard-schema/spec@1.1.0": + resolution: + { + integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==, + } + + "@tybys/wasm-util@0.10.1": + resolution: + { + integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==, + } + + "@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.8": + resolution: + { + integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, + } + + "@types/node@24.12.2": + resolution: + { + integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==, + } + + "@vitest/expect@4.1.5": + resolution: + { + integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==, + } + + "@vitest/mocker@4.1.5": + resolution: + { + integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==, + } + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + "@vitest/pretty-format@4.1.5": + resolution: + { + integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==, + } + + "@vitest/runner@4.1.5": + resolution: + { + integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==, + } + + "@vitest/snapshot@4.1.5": + resolution: + { + integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==, + } + + "@vitest/spy@4.1.5": + resolution: + { + integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==, + } + + "@vitest/utils@4.1.5": + resolution: + { + integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==, + } + + assertion-error@2.0.1: + resolution: + { + integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==, + } + engines: { node: ">=12" } + + chai@6.2.2: + resolution: + { + integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==, + } + engines: { node: ">=18" } + + convert-source-map@2.0.0: + resolution: + { + integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, + } + + detect-libc@2.1.2: + resolution: + { + integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, + } + engines: { node: ">=8" } + + es-module-lexer@2.1.0: + resolution: + { + integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==, + } + + esbuild@0.27.7: + resolution: + { + integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==, + } + engines: { node: ">=18" } + hasBin: true + + estree-walker@3.0.3: + resolution: + { + integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, + } + + expect-type@1.3.0: + resolution: + { + integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==, + } + engines: { node: ">=12.0.0" } + + fdir@6.5.0: + resolution: + { + integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, + } + engines: { node: ">=12.0.0" } + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: + { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + os: [darwin] + + get-tsconfig@4.14.0: + resolution: + { + integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==, + } + + lightningcss-android-arm64@1.32.0: + resolution: + { + integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: + { + integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: + { + integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: + { + integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: + { + integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: + { + integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: + { + integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: + { + integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: + { + integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: + { + integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: + { + integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: + { + integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==, + } + engines: { node: ">= 12.0.0" } + + magic-string@0.30.21: + resolution: + { + integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, + } + + nanoid@3.3.11: + resolution: + { + integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==, + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + hasBin: true + + obug@2.1.1: + resolution: + { + integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==, + } + + pathe@2.0.3: + resolution: + { + integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, + } + + 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" } + + postcss@8.5.12: + resolution: + { + integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==, + } + engines: { node: ^10 || ^12 || >=14 } + + prettier@3.8.3: + resolution: + { + integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==, + } + engines: { node: ">=14" } + hasBin: true + + resolve-pkg-maps@1.0.0: + resolution: + { + integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, + } + + rolldown@1.0.0-rc.17: + resolution: + { + integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + hasBin: true + + 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==, + } + + std-env@4.1.0: + resolution: + { + integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==, + } + + tinybench@2.9.0: + resolution: + { + integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==, + } + + tinyexec@1.1.2: + resolution: + { + integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==, + } + engines: { node: ">=18" } + + tinyglobby@0.2.16: + resolution: + { + integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==, + } + engines: { node: ">=12.0.0" } + + tinyrainbow@3.1.0: + resolution: + { + integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==, + } + engines: { node: ">=14.0.0" } + + tslib@2.8.1: + resolution: + { + integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, + } + + tsx@4.21.0: + resolution: + { + integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==, + } + engines: { node: ">=18.0.0" } + hasBin: true + + typescript@5.9.3: + resolution: + { + integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, + } + engines: { node: ">=14.17" } + hasBin: true + + undici-types@7.16.0: + resolution: + { + integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==, + } + + vite@8.0.10: + resolution: + { + integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + hasBin: true + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + "@vitejs/devtools": ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: ">=1.21.0" + less: ^4.0.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 + "@vitejs/devtools": + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + 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@4.1.5: + resolution: + { + integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==, + } + engines: { node: ^20.0.0 || ^22.0.0 || >=24.0.0 } + hasBin: true + peerDependencies: + "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 + "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 4.1.5 + "@vitest/browser-preview": 4.1.5 + "@vitest/browser-webdriverio": 4.1.5 + "@vitest/coverage-istanbul": 4.1.5 + "@vitest/coverage-v8": 4.1.5 + "@vitest/ui": 4.1.5 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@opentelemetry/api": + optional: true + "@types/node": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": + optional: true + "@vitest/coverage-istanbul": + optional: true + "@vitest/coverage-v8": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: + { + integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==, + } + engines: { node: ">=8" } + hasBin: true + +snapshots: + "@emnapi/core@1.10.0": + dependencies: + "@emnapi/wasi-threads": 1.2.1 + tslib: 2.8.1 + optional: true + + "@emnapi/runtime@1.10.0": + dependencies: + tslib: 2.8.1 + optional: true + + "@emnapi/wasi-threads@1.2.1": + dependencies: + tslib: 2.8.1 + optional: true + + "@esbuild/aix-ppc64@0.27.7": + optional: true + + "@esbuild/android-arm64@0.27.7": + optional: true + + "@esbuild/android-arm@0.27.7": + optional: true + + "@esbuild/android-x64@0.27.7": + optional: true + + "@esbuild/darwin-arm64@0.27.7": + optional: true + + "@esbuild/darwin-x64@0.27.7": + optional: true + + "@esbuild/freebsd-arm64@0.27.7": + optional: true + + "@esbuild/freebsd-x64@0.27.7": + optional: true + + "@esbuild/linux-arm64@0.27.7": + optional: true + + "@esbuild/linux-arm@0.27.7": + optional: true + + "@esbuild/linux-ia32@0.27.7": + optional: true + + "@esbuild/linux-loong64@0.27.7": + optional: true + + "@esbuild/linux-mips64el@0.27.7": + optional: true + + "@esbuild/linux-ppc64@0.27.7": + optional: true + + "@esbuild/linux-riscv64@0.27.7": + optional: true + + "@esbuild/linux-s390x@0.27.7": + optional: true + + "@esbuild/linux-x64@0.27.7": + optional: true + + "@esbuild/netbsd-arm64@0.27.7": + optional: true + + "@esbuild/netbsd-x64@0.27.7": + optional: true + + "@esbuild/openbsd-arm64@0.27.7": + optional: true + + "@esbuild/openbsd-x64@0.27.7": + optional: true + + "@esbuild/openharmony-arm64@0.27.7": + optional: true + + "@esbuild/sunos-x64@0.27.7": + optional: true + + "@esbuild/win32-arm64@0.27.7": + optional: true + + "@esbuild/win32-ia32@0.27.7": + optional: true + + "@esbuild/win32-x64@0.27.7": + optional: true + + "@jridgewell/sourcemap-codec@1.5.5": {} + + "@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)": + dependencies: + "@emnapi/core": 1.10.0 + "@emnapi/runtime": 1.10.0 + "@tybys/wasm-util": 0.10.1 + optional: true + + "@oxc-project/types@0.127.0": {} + + "@rolldown/binding-android-arm64@1.0.0-rc.17": + optional: true + + "@rolldown/binding-darwin-arm64@1.0.0-rc.17": + optional: true + + "@rolldown/binding-darwin-x64@1.0.0-rc.17": + optional: true + + "@rolldown/binding-freebsd-x64@1.0.0-rc.17": + optional: true + + "@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17": + optional: true + + "@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17": + optional: true + + "@rolldown/binding-linux-arm64-musl@1.0.0-rc.17": + optional: true + + "@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17": + optional: true + + "@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17": + optional: true + + "@rolldown/binding-linux-x64-gnu@1.0.0-rc.17": + optional: true + + "@rolldown/binding-linux-x64-musl@1.0.0-rc.17": + optional: true + + "@rolldown/binding-openharmony-arm64@1.0.0-rc.17": + optional: true + + "@rolldown/binding-wasm32-wasi@1.0.0-rc.17": + dependencies: + "@emnapi/core": 1.10.0 + "@emnapi/runtime": 1.10.0 + "@napi-rs/wasm-runtime": 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + "@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17": + optional: true + + "@rolldown/binding-win32-x64-msvc@1.0.0-rc.17": + optional: true + + "@rolldown/pluginutils@1.0.0-rc.17": {} + + "@standard-schema/spec@1.1.0": {} + + "@tybys/wasm-util@0.10.1": + dependencies: + tslib: 2.8.1 + optional: true + + "@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.8": {} + + "@types/node@24.12.2": + dependencies: + undici-types: 7.16.0 + + "@vitest/expect@4.1.5": + dependencies: + "@standard-schema/spec": 1.1.0 + "@types/chai": 5.2.3 + "@vitest/spy": 4.1.5 + "@vitest/utils": 4.1.5 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + "@vitest/mocker@4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.27.7)(tsx@4.21.0))": + dependencies: + "@vitest/spy": 4.1.5 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.10(@types/node@24.12.2)(esbuild@0.27.7)(tsx@4.21.0) + + "@vitest/pretty-format@4.1.5": + dependencies: + tinyrainbow: 3.1.0 + + "@vitest/runner@4.1.5": + dependencies: + "@vitest/utils": 4.1.5 + pathe: 2.0.3 + + "@vitest/snapshot@4.1.5": + dependencies: + "@vitest/pretty-format": 4.1.5 + "@vitest/utils": 4.1.5 + magic-string: 0.30.21 + pathe: 2.0.3 + + "@vitest/spy@4.1.5": {} + + "@vitest/utils@4.1.5": + dependencies: + "@vitest/pretty-format": 4.1.5 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + assertion-error@2.0.1: {} + + chai@6.2.2: {} + + convert-source-map@2.0.0: {} + + detect-libc@2.1.2: {} + + es-module-lexer@2.1.0: {} + + 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 + + estree-walker@3.0.3: + dependencies: + "@types/estree": 1.0.8 + + expect-type@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + magic-string@0.30.21: + dependencies: + "@jridgewell/sourcemap-codec": 1.5.5 + + nanoid@3.3.11: {} + + obug@2.1.1: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.12: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier@3.8.3: {} + + resolve-pkg-maps@1.0.0: {} + + rolldown@1.0.0-rc.17: + dependencies: + "@oxc-project/types": 0.127.0 + "@rolldown/pluginutils": 1.0.0-rc.17 + optionalDependencies: + "@rolldown/binding-android-arm64": 1.0.0-rc.17 + "@rolldown/binding-darwin-arm64": 1.0.0-rc.17 + "@rolldown/binding-darwin-x64": 1.0.0-rc.17 + "@rolldown/binding-freebsd-x64": 1.0.0-rc.17 + "@rolldown/binding-linux-arm-gnueabihf": 1.0.0-rc.17 + "@rolldown/binding-linux-arm64-gnu": 1.0.0-rc.17 + "@rolldown/binding-linux-arm64-musl": 1.0.0-rc.17 + "@rolldown/binding-linux-ppc64-gnu": 1.0.0-rc.17 + "@rolldown/binding-linux-s390x-gnu": 1.0.0-rc.17 + "@rolldown/binding-linux-x64-gnu": 1.0.0-rc.17 + "@rolldown/binding-linux-x64-musl": 1.0.0-rc.17 + "@rolldown/binding-openharmony-arm64": 1.0.0-rc.17 + "@rolldown/binding-wasm32-wasi": 1.0.0-rc.17 + "@rolldown/binding-win32-arm64-msvc": 1.0.0-rc.17 + "@rolldown/binding-win32-x64-msvc": 1.0.0-rc.17 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.1.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.1.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + tslib@2.8.1: + optional: true + + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.14.0 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + vite@8.0.10(@types/node@24.12.2)(esbuild@0.27.7)(tsx@4.21.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.12 + rolldown: 1.0.0-rc.17 + tinyglobby: 0.2.16 + optionalDependencies: + "@types/node": 24.12.2 + esbuild: 0.27.7 + fsevents: 2.3.3 + tsx: 4.21.0 + + vitest@4.1.5(@types/node@24.12.2)(esbuild@0.27.7)(tsx@4.21.0): + dependencies: + "@vitest/expect": 4.1.5 + "@vitest/mocker": 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.27.7)(tsx@4.21.0)) + "@vitest/pretty-format": 4.1.5 + "@vitest/runner": 4.1.5 + "@vitest/snapshot": 4.1.5 + "@vitest/spy": 4.1.5 + "@vitest/utils": 4.1.5 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.10(@types/node@24.12.2)(esbuild@0.27.7)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + "@types/node": 24.12.2 + transitivePeerDependencies: + - "@vitejs/devtools" + - esbuild + - jiti + - less + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..ccbac80 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "." diff --git a/recipes/README.md b/recipes/README.md new file mode 100644 index 0000000..533c740 --- /dev/null +++ b/recipes/README.md @@ -0,0 +1,14 @@ +# Recipes + +Recipes are intentionally small. Each one demonstrates one SDK workflow and is +safe to copy into a real app. + +Current recipes are listed in [`manifest.json`](manifest.json). + +## Adding a Recipe + +1. Create `recipes//README.md`. +2. Create `recipes//index.ts`. +3. Add the recipe to `recipes/manifest.json`. +4. Add test coverage in `test/recipes.test.ts`. +5. Run `pnpm check`. diff --git a/recipes/_shared/config.ts b/recipes/_shared/config.ts new file mode 100644 index 0000000..1d8d1e1 --- /dev/null +++ b/recipes/_shared/config.ts @@ -0,0 +1,52 @@ +import { OpenClaw, type OpenClawOptions } from "@openclaw/sdk"; + +export type CookbookConnectionOptions = { + gateway?: string; + token?: string; + password?: string; +}; + +export type RunRecipeOptions = CookbookConnectionOptions & { + agentId?: string; + input?: string; + model?: string; + sessionKey?: string; + timeoutMs?: number; + waitTimeoutMs?: number; +}; + +export function readConnectionOptions(options: CookbookConnectionOptions = {}): OpenClawOptions { + return { + gateway: options.gateway ?? process.env.OPENCLAW_GATEWAY ?? "auto", + token: options.token ?? process.env.OPENCLAW_TOKEN, + password: options.password ?? process.env.OPENCLAW_PASSWORD, + }; +} + +export function createClient(options: CookbookConnectionOptions = {}): OpenClaw { + return new OpenClaw(readConnectionOptions(options)); +} + +export function readAgentId(agentId?: string): string { + return agentId ?? process.env.OPENCLAW_AGENT_ID ?? "main"; +} + +export function readInput(input?: string): string { + return input ?? (process.argv.slice(2).join(" ") || "Say hello from the OpenClaw SDK cookbook."); +} + +export function readSessionKey(sessionKey?: string): string { + return sessionKey ?? process.env.OPENCLAW_SESSION_KEY ?? "cookbook"; +} + +export function readTimeoutMs(value: number | undefined, fallback: number): number { + if (typeof value === "number" && Number.isFinite(value) && value >= 0) { + return Math.floor(value); + } + return fallback; +} + +export function optionalModel(model?: string): { model?: string } { + const resolved = model ?? process.env.OPENCLAW_MODEL; + return resolved ? { model: resolved } : {}; +} diff --git a/recipes/_shared/run-main.ts b/recipes/_shared/run-main.ts new file mode 100644 index 0000000..ac5c722 --- /dev/null +++ b/recipes/_shared/run-main.ts @@ -0,0 +1,19 @@ +import { pathToFileURL } from "node:url"; + +export function isDirectRun(metaUrl: string): boolean { + const entry = process.argv[1]; + return entry ? metaUrl === pathToFileURL(entry).href : false; +} + +export async function runMain(action: () => Promise): Promise { + try { + const result = await action(); + if (result !== undefined) { + console.log(JSON.stringify(result, null, 2)); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(message); + process.exitCode = 1; + } +} diff --git a/recipes/cancel-a-run/README.md b/recipes/cancel-a-run/README.md new file mode 100644 index 0000000..615560c --- /dev/null +++ b/recipes/cancel-a-run/README.md @@ -0,0 +1,11 @@ +# Cancel a Run + +Start a run, cancel it by `runId`, then wait for the Gateway result. This is the +shape to use for UI stop buttons and automation time budgets. + +```bash +OPENCLAW_CANCEL_AFTER_MS=1500 pnpm recipe:cancel-a-run -- "Keep working until cancelled" +``` + +The SDK cancellation path does not require callers to know the session key when +the Gateway can resolve the active run by id. diff --git a/recipes/cancel-a-run/index.ts b/recipes/cancel-a-run/index.ts new file mode 100644 index 0000000..b40ec3b --- /dev/null +++ b/recipes/cancel-a-run/index.ts @@ -0,0 +1,46 @@ +import type { RunResult } from "@openclaw/sdk"; +import { + createClient, + optionalModel, + readAgentId, + readInput, + readSessionKey, + readTimeoutMs, + type RunRecipeOptions, +} from "../_shared/config.js"; +import { isDirectRun, runMain } from "../_shared/run-main.js"; + +export type CancelRunRecipeResult = { + runId: string; + cancelResponse: unknown; + result: RunResult; +}; + +export async function cancelRunRecipe( + options: RunRecipeOptions & { cancelAfterMs?: number } = {}, +): Promise { + const oc = createClient(options); + try { + const run = await oc.runs.create({ + input: readInput(options.input), + agentId: readAgentId(options.agentId), + sessionKey: readSessionKey(options.sessionKey), + timeoutMs: readTimeoutMs(options.timeoutMs, 300_000), + ...optionalModel(options.model), + }); + const cancelAfterMs = readTimeoutMs( + options.cancelAfterMs ?? Number(process.env.OPENCLAW_CANCEL_AFTER_MS), + 1_000, + ); + await new Promise((resolve) => setTimeout(resolve, cancelAfterMs)); + const cancelResponse = await run.cancel(); + const result = await run.wait({ timeoutMs: readTimeoutMs(options.waitTimeoutMs, 30_000) }); + return { runId: run.id, cancelResponse, result }; + } finally { + await oc.close(); + } +} + +if (isDirectRun(import.meta.url)) { + await runMain(() => cancelRunRecipe()); +} diff --git a/recipes/custom-transport/README.md b/recipes/custom-transport/README.md new file mode 100644 index 0000000..b56cb1b --- /dev/null +++ b/recipes/custom-transport/README.md @@ -0,0 +1,11 @@ +# Custom Transport + +Use an in-memory SDK transport to test app code without a real Gateway. + +```bash +pnpm recipe:custom-transport -- "test prompt" +``` + +This pattern is useful for app tests: implement the SDK transport interface, +return canned RPC responses, and assert your app behavior around the SDK +boundary. diff --git a/recipes/custom-transport/index.ts b/recipes/custom-transport/index.ts new file mode 100644 index 0000000..be3ddf0 --- /dev/null +++ b/recipes/custom-transport/index.ts @@ -0,0 +1,55 @@ +import type { GatewayEvent, GatewayRequestOptions, OpenClawTransport } from "@openclaw/sdk"; +import { OpenClaw } from "@openclaw/sdk"; +import { readInput } from "../_shared/config.js"; +import { isDirectRun, runMain } from "../_shared/run-main.js"; + +type RequestCall = { + method: string; + params?: unknown; + options?: GatewayRequestOptions; +}; + +class CookbookTransport implements OpenClawTransport { + readonly calls: RequestCall[] = []; + + async request( + method: string, + params?: unknown, + options?: GatewayRequestOptions, + ): Promise { + this.calls.push({ method, params, options }); + if (method === "agent") { + return { status: "accepted", runId: "cookbook-run" } as T; + } + if (method === "agent.wait") { + return { status: "ok", runId: "cookbook-run", endedAt: Date.now() } as T; + } + throw new Error(`unexpected method: ${method}`); + } + + async *events(): AsyncIterable { + yield { + event: "agent", + payload: { + runId: "cookbook-run", + stream: "lifecycle", + data: { phase: "end" }, + }, + }; + } +} + +export async function customTransportRecipe(input = readInput()): Promise<{ + runId: string; + calls: RequestCall[]; +}> { + const transport = new CookbookTransport(); + const oc = new OpenClaw({ transport }); + const run = await oc.runs.create({ input, idempotencyKey: "cookbook-custom-transport" }); + await run.wait({ timeoutMs: 1_000 }); + return { runId: run.id, calls: transport.calls }; +} + +if (isDirectRun(import.meta.url)) { + await runMain(() => customTransportRecipe()); +} diff --git a/recipes/manifest.json b/recipes/manifest.json new file mode 100644 index 0000000..abf814f --- /dev/null +++ b/recipes/manifest.json @@ -0,0 +1,38 @@ +[ + { + "id": "run-an-agent", + "title": "Run an agent", + "entry": "recipes/run-an-agent/index.ts", + "readme": "recipes/run-an-agent/README.md" + }, + { + "id": "stream-events", + "title": "Stream events", + "entry": "recipes/stream-events/index.ts", + "readme": "recipes/stream-events/README.md" + }, + { + "id": "cancel-a-run", + "title": "Cancel a run", + "entry": "recipes/cancel-a-run/index.ts", + "readme": "recipes/cancel-a-run/README.md" + }, + { + "id": "reuse-session", + "title": "Reuse a session", + "entry": "recipes/reuse-session/index.ts", + "readme": "recipes/reuse-session/README.md" + }, + { + "id": "model-status", + "title": "Model status", + "entry": "recipes/model-status/index.ts", + "readme": "recipes/model-status/README.md" + }, + { + "id": "custom-transport", + "title": "Custom transport", + "entry": "recipes/custom-transport/index.ts", + "readme": "recipes/custom-transport/README.md" + } +] diff --git a/recipes/model-status/README.md b/recipes/model-status/README.md new file mode 100644 index 0000000..4a40028 --- /dev/null +++ b/recipes/model-status/README.md @@ -0,0 +1,10 @@ +# Model Status + +Ask the Gateway for model provider/auth status. + +```bash +pnpm recipe:model-status +``` + +Pass `OPENCLAW_MODEL_STATUS_PROBE=1` to let the Gateway run provider probes when +that is appropriate for your environment. diff --git a/recipes/model-status/index.ts b/recipes/model-status/index.ts new file mode 100644 index 0000000..fa996ce --- /dev/null +++ b/recipes/model-status/index.ts @@ -0,0 +1,18 @@ +import { createClient, type CookbookConnectionOptions } from "../_shared/config.js"; +import { isDirectRun, runMain } from "../_shared/run-main.js"; + +export async function modelStatusRecipe( + options: CookbookConnectionOptions & { probe?: boolean } = {}, +): Promise { + const oc = createClient(options); + try { + const probe = options.probe ?? process.env.OPENCLAW_MODEL_STATUS_PROBE === "1"; + return await oc.models.status({ probe }); + } finally { + await oc.close(); + } +} + +if (isDirectRun(import.meta.url)) { + await runMain(() => modelStatusRecipe()); +} diff --git a/recipes/reuse-session/README.md b/recipes/reuse-session/README.md new file mode 100644 index 0000000..aaedb82 --- /dev/null +++ b/recipes/reuse-session/README.md @@ -0,0 +1,10 @@ +# Reuse a Session + +Create or reuse a session key, send two messages, and wait for both run results. + +```bash +OPENCLAW_SESSION_KEY=cookbook-demo pnpm recipe:reuse-session +``` + +Use this pattern for chat UIs, background workflows, and any app that wants a +stable thread rather than one-off runs. diff --git a/recipes/reuse-session/index.ts b/recipes/reuse-session/index.ts new file mode 100644 index 0000000..5fbd4ee --- /dev/null +++ b/recipes/reuse-session/index.ts @@ -0,0 +1,48 @@ +import type { RunResult } from "@openclaw/sdk"; +import { + createClient, + optionalModel, + readAgentId, + readSessionKey, + readTimeoutMs, + type RunRecipeOptions, +} from "../_shared/config.js"; +import { isDirectRun, runMain } from "../_shared/run-main.js"; + +export type ReuseSessionRecipeResult = { + sessionKey: string; + results: RunResult[]; +}; + +export async function reuseSessionRecipe( + options: RunRecipeOptions & { firstInput?: string; secondInput?: string } = {}, +): Promise { + const oc = createClient(options); + try { + const sessionKey = readSessionKey(options.sessionKey); + const session = await oc.sessions.create({ + key: sessionKey, + agentId: readAgentId(options.agentId), + ...optionalModel(options.model), + }); + const first = await session.send({ + message: options.firstInput ?? "Remember that the cookbook session is working.", + timeoutMs: readTimeoutMs(options.timeoutMs, 60_000), + }); + const second = await session.send({ + message: options.secondInput ?? "What did I ask you to remember?", + timeoutMs: readTimeoutMs(options.timeoutMs, 60_000), + }); + const waitOptions = { timeoutMs: readTimeoutMs(options.waitTimeoutMs, 120_000) }; + return { + sessionKey, + results: [await first.wait(waitOptions), await second.wait(waitOptions)], + }; + } finally { + await oc.close(); + } +} + +if (isDirectRun(import.meta.url)) { + await runMain(() => reuseSessionRecipe()); +} diff --git a/recipes/run-an-agent/README.md b/recipes/run-an-agent/README.md new file mode 100644 index 0000000..10eb391 --- /dev/null +++ b/recipes/run-an-agent/README.md @@ -0,0 +1,11 @@ +# Run an Agent + +Start a run, wait for the Gateway to return a terminal result, and print the +stable SDK result envelope. + +```bash +OPENCLAW_AGENT_ID=main pnpm recipe:run-agent -- "Summarize this repository" +``` + +Use this when you want the simplest request/response shape. For live progress, +use [`stream-events`](../stream-events). diff --git a/recipes/run-an-agent/index.ts b/recipes/run-an-agent/index.ts new file mode 100644 index 0000000..dbc8793 --- /dev/null +++ b/recipes/run-an-agent/index.ts @@ -0,0 +1,31 @@ +import type { RunResult } from "@openclaw/sdk"; +import { + createClient, + optionalModel, + readAgentId, + readInput, + readSessionKey, + readTimeoutMs, + type RunRecipeOptions, +} from "../_shared/config.js"; +import { isDirectRun, runMain } from "../_shared/run-main.js"; + +export async function runAgentRecipe(options: RunRecipeOptions = {}): Promise { + const oc = createClient(options); + try { + const agent = await oc.agents.get(readAgentId(options.agentId)); + const run = await agent.run({ + input: readInput(options.input), + sessionKey: readSessionKey(options.sessionKey), + timeoutMs: readTimeoutMs(options.timeoutMs, 60_000), + ...optionalModel(options.model), + }); + return await run.wait({ timeoutMs: readTimeoutMs(options.waitTimeoutMs, 120_000) }); + } finally { + await oc.close(); + } +} + +if (isDirectRun(import.meta.url)) { + await runMain(() => runAgentRecipe()); +} diff --git a/recipes/stream-events/README.md b/recipes/stream-events/README.md new file mode 100644 index 0000000..3d89235 --- /dev/null +++ b/recipes/stream-events/README.md @@ -0,0 +1,12 @@ +# Stream Events + +Start a run and iterate normalized SDK events until the run reaches a terminal +state. + +```bash +pnpm recipe:stream-events -- "Refactor the current branch and explain the diff" +``` + +The SDK keeps provider-native payloads in `event.raw`, while `event.type` gives +apps a stable UI contract such as `run.started`, `assistant.delta`, or +`run.completed`. diff --git a/recipes/stream-events/index.ts b/recipes/stream-events/index.ts new file mode 100644 index 0000000..f66ad56 --- /dev/null +++ b/recipes/stream-events/index.ts @@ -0,0 +1,48 @@ +import type { OpenClawEvent, OpenClawEventType } from "@openclaw/sdk"; +import { + createClient, + optionalModel, + readAgentId, + readInput, + readSessionKey, + readTimeoutMs, + type RunRecipeOptions, +} from "../_shared/config.js"; +import { isDirectRun, runMain } from "../_shared/run-main.js"; + +const terminalEvents = new Set([ + "run.completed", + "run.failed", + "run.cancelled", + "run.timed_out", +]); + +export async function streamEventsRecipe( + options: RunRecipeOptions = {}, +): Promise>> { + const oc = createClient(options); + try { + const run = await oc.runs.create({ + input: readInput(options.input), + agentId: readAgentId(options.agentId), + sessionKey: readSessionKey(options.sessionKey), + timeoutMs: readTimeoutMs(options.timeoutMs, 60_000), + ...optionalModel(options.model), + }); + + const seen: Array> = []; + for await (const event of run.events()) { + seen.push({ type: event.type, runId: event.runId }); + if (terminalEvents.has(event.type)) { + break; + } + } + return seen; + } finally { + await oc.close(); + } +} + +if (isDirectRun(import.meta.url)) { + await runMain(() => streamEventsRecipe()); +} diff --git a/scripts/check-docs.mjs b/scripts/check-docs.mjs new file mode 100644 index 0000000..3fa6592 --- /dev/null +++ b/scripts/check-docs.mjs @@ -0,0 +1,39 @@ +import fs from "node:fs"; +import path from "node:path"; + +const root = process.cwd(); +const manifestPath = path.join(root, "recipes", "manifest.json"); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +const failures = []; + +for (const recipe of manifest) { + for (const key of ["entry", "readme"]) { + const value = recipe[key]; + if (typeof value !== "string" || !fs.existsSync(path.join(root, value))) { + failures.push(`${recipe.id}: missing ${key} ${value}`); + } + } + const readme = fs.readFileSync(path.join(root, recipe.readme), "utf8"); + if (!readme.includes("```bash")) { + failures.push(`${recipe.id}: README should include a bash command`); + } +} + +const readme = fs.readFileSync(path.join(root, "README.md"), "utf8"); +for (const recipe of manifest) { + const link = `recipes/${recipe.id}`; + if (!readme.includes(link)) { + failures.push(`README missing link to ${link}`); + } +} + +if (!fs.existsSync(path.join(root, "examples", "node-cli", "src", "index.ts"))) { + failures.push("missing node-cli example entry"); +} + +if (failures.length > 0) { + console.error(failures.join("\n")); + process.exitCode = 1; +} else { + console.log(`docs ok: ${manifest.length} recipes`); +} diff --git a/test/recipes.test.ts b/test/recipes.test.ts new file mode 100644 index 0000000..85e841d --- /dev/null +++ b/test/recipes.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import { cancelRunRecipe } from "../recipes/cancel-a-run/index.js"; +import { customTransportRecipe } from "../recipes/custom-transport/index.js"; +import { modelStatusRecipe } from "../recipes/model-status/index.js"; +import { reuseSessionRecipe } from "../recipes/reuse-session/index.js"; +import { runAgentRecipe } from "../recipes/run-an-agent/index.js"; +import { streamEventsRecipe } from "../recipes/stream-events/index.js"; + +describe("cookbook recipes", () => { + it("runs an agent and returns a completed result", async () => { + await expect(runAgentRecipe({ input: "hello" })).resolves.toMatchObject({ + runId: "cookbook-run", + status: "completed", + }); + }); + + it("streams normalized events through a terminal event", async () => { + await expect(streamEventsRecipe({ input: "stream" })).resolves.toEqual([ + { type: "run.started", runId: "cookbook-run" }, + { type: "assistant.delta", runId: "cookbook-run" }, + { type: "run.completed", runId: "cookbook-run" }, + ]); + }); + + it("cancels a run", async () => { + await expect(cancelRunRecipe({ input: "cancel", cancelAfterMs: 0 })).resolves.toMatchObject({ + runId: "cookbook-run", + cancelResponse: { ok: true, status: "aborted" }, + result: { status: "completed" }, + }); + }); + + it("reuses a session for multiple messages", async () => { + const result = await reuseSessionRecipe({ sessionKey: "recipe-test" }); + + expect(result.sessionKey).toBe("recipe-test"); + expect(result.results).toHaveLength(2); + expect(result.results.every((entry) => entry.status === "completed")).toBe(true); + }); + + it("reads model status", async () => { + await expect(modelStatusRecipe()).resolves.toMatchObject({ + providers: [{ id: "openai", authenticated: true }], + }); + }); + + it("runs against a custom transport", async () => { + const result = await customTransportRecipe("transport"); + + expect(result.runId).toBe("cookbook-run"); + expect(result.calls.map((call) => call.method)).toEqual(["agent", "agent.wait"]); + }); +}); diff --git a/test/shims/openclaw-sdk.ts b/test/shims/openclaw-sdk.ts new file mode 100644 index 0000000..b700ede --- /dev/null +++ b/test/shims/openclaw-sdk.ts @@ -0,0 +1,244 @@ +export type GatewayRequestOptions = { + expectFinal?: boolean; + timeoutMs?: number | null; +}; + +export type GatewayEvent = { + event: string; + payload?: unknown; + seq?: number; + stateVersion?: unknown; +}; + +export type OpenClawTransport = { + request( + method: string, + params?: unknown, + options?: GatewayRequestOptions, + ): Promise; + events(filter?: (event: GatewayEvent) => boolean): AsyncIterable; + close?(): Promise | void; +}; + +export type OpenClawOptions = { + gateway?: "auto" | (string & {}); + url?: string; + token?: string; + password?: string; + requestTimeoutMs?: number; + transport?: OpenClawTransport; +}; + +export type RunStatus = "accepted" | "completed" | "failed" | "cancelled" | "timed_out"; +export type RunResult = { + runId: string; + status: RunStatus; + sessionKey?: string; + startedAt?: string | number; + endedAt?: string | number; + raw?: unknown; +}; + +export type OpenClawEventType = + | "run.started" + | "run.completed" + | "run.failed" + | "run.cancelled" + | "run.timed_out" + | "assistant.delta" + | "raw"; + +export type OpenClawEvent = { + version: 1; + id: string; + ts: number; + type: OpenClawEventType; + runId?: string; + data: TData; + raw?: GatewayEvent; +}; + +export type AgentRunParams = { + input: string; + agentId?: string; + model?: string; + sessionKey?: string; + timeoutMs?: number; + idempotencyKey?: string; +}; + +export type SessionCreateParams = { + key?: string; + agentId?: string; + model?: string; +}; + +export type SessionSendParams = { + key?: string; + message: string; + timeoutMs?: number; +}; + +function normalizeEvent(event: GatewayEvent): OpenClawEvent { + const payload = + typeof event.payload === "object" && event.payload !== null + ? (event.payload as Record) + : {}; + const data = + typeof payload.data === "object" && payload.data !== null + ? (payload.data as Record) + : {}; + const phase = typeof data.phase === "string" ? data.phase : undefined; + const stream = typeof payload.stream === "string" ? payload.stream : undefined; + const runId = typeof payload.runId === "string" ? payload.runId : undefined; + const type = + stream === "assistant" + ? "assistant.delta" + : phase === "start" + ? "run.started" + : phase === "end" + ? "run.completed" + : "raw"; + return { + version: 1, + id: `${event.seq ?? "test"}:${event.event}`, + ts: Date.now(), + type, + runId, + data, + raw: event, + }; +} + +class ShimRun { + constructor( + private readonly client: OpenClaw, + readonly id: string, + private readonly sessionKey?: string, + ) {} + + async *events(): AsyncIterable { + if (this.client.transport) { + for await (const event of this.client.transport.events()) { + yield normalizeEvent(event); + } + return; + } + yield { + version: 1, + id: "start", + ts: Date.now(), + type: "run.started", + runId: this.id, + data: {}, + }; + yield { + version: 1, + id: "message", + ts: Date.now(), + type: "assistant.delta", + runId: this.id, + data: { delta: "hello" }, + }; + yield { + version: 1, + id: "end", + ts: Date.now(), + type: "run.completed", + runId: this.id, + data: {}, + }; + } + + async wait(): Promise { + if (this.client.transport) { + const raw = await this.client.transport.request>( + "agent.wait", + { runId: this.id }, + { timeoutMs: null }, + ); + return { + runId: this.id, + status: raw.status === "ok" ? "completed" : "failed", + endedAt: typeof raw.endedAt === "number" ? raw.endedAt : undefined, + raw, + }; + } + return { runId: this.id, status: "completed", sessionKey: this.sessionKey, endedAt: 456 }; + } + + async cancel(): Promise { + return { ok: true, status: "aborted", abortedRunId: this.id }; + } +} + +class ShimAgent { + constructor( + private readonly client: OpenClaw, + readonly id: string, + ) {} + + async run(input: string | Omit): Promise { + const params = + typeof input === "string" ? { input, agentId: this.id } : { ...input, agentId: this.id }; + return await this.client.runs.create(params); + } +} + +class ShimSession { + constructor( + private readonly client: OpenClaw, + readonly key: string, + ) {} + + async send(input: string | Omit): Promise { + const message = typeof input === "string" ? input : input.message; + return await this.client.runs.create({ input: message, sessionKey: this.key }); + } + + async abort(runId?: string): Promise { + return { ok: true, status: runId ? "aborted" : "no-active-run" }; + } +} + +export class OpenClaw { + readonly transport?: OpenClawTransport; + + constructor(options: OpenClawOptions = {}) { + this.transport = options.transport; + } + + readonly agents = { + get: async (id: string) => new ShimAgent(this, id), + }; + + readonly runs = { + create: async (params: AgentRunParams) => { + if (this.transport) { + const raw = await this.transport.request>("agent", params, { + expectFinal: false, + }); + const runId = typeof raw.runId === "string" ? raw.runId : "transport-run"; + return new ShimRun(this, runId, params.sessionKey); + } + return new ShimRun(this, "cookbook-run", params.sessionKey); + }, + wait: async (runId: string) => new ShimRun(this, runId).wait(), + cancel: async (runId: string) => ({ ok: true, abortedRunId: runId, status: "aborted" }), + }; + + readonly sessions = { + create: async (params: SessionCreateParams = {}) => + new ShimSession(this, params.key ?? "cookbook"), + }; + + readonly models = { + status: async () => ({ providers: [{ id: "openai", authenticated: true }] }), + }; + + async close(): Promise { + await this.transport?.close?.(); + } +} + +export { ShimAgent as Agent, ShimRun as Run, ShimSession as Session }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6ff5759 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "declaration": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "types": ["node", "vitest/globals"] + }, + "include": [ + "examples/**/*.ts", + "recipes/**/*.ts", + "scripts/**/*.mjs", + "test/**/*.ts", + "types/**/*.d.ts", + "vitest.config.ts" + ] +} diff --git a/types/openclaw-sdk.d.ts b/types/openclaw-sdk.d.ts new file mode 100644 index 0000000..5b51c02 --- /dev/null +++ b/types/openclaw-sdk.d.ts @@ -0,0 +1,159 @@ +declare module "@openclaw/sdk" { + export type GatewayRequestOptions = { + expectFinal?: boolean; + timeoutMs?: number | null; + }; + + export type GatewayEvent = { + event: string; + payload?: unknown; + seq?: number; + stateVersion?: unknown; + }; + + export type OpenClawTransport = { + request( + method: string, + params?: unknown, + options?: GatewayRequestOptions, + ): Promise; + events(filter?: (event: GatewayEvent) => boolean): AsyncIterable; + close?(): Promise | void; + }; + + export type OpenClawOptions = { + gateway?: "auto" | (string & {}); + url?: string; + token?: string; + password?: string; + requestTimeoutMs?: number; + transport?: OpenClawTransport; + }; + + export type RunStatus = "accepted" | "completed" | "failed" | "cancelled" | "timed_out"; + export type RunTimestamp = string | number; + + export type RunResult = { + runId: string; + status: RunStatus; + sessionId?: string; + sessionKey?: string; + taskId?: string; + startedAt?: RunTimestamp; + endedAt?: RunTimestamp; + error?: { code?: string; message: string; details?: unknown }; + raw?: unknown; + }; + + export type OpenClawEventType = + | "run.created" + | "run.queued" + | "run.started" + | "run.completed" + | "run.failed" + | "run.cancelled" + | "run.timed_out" + | "assistant.delta" + | "assistant.message" + | "thinking.delta" + | "tool.call.started" + | "tool.call.delta" + | "tool.call.completed" + | "tool.call.failed" + | "approval.requested" + | "approval.resolved" + | "question.requested" + | "question.answered" + | "artifact.created" + | "artifact.updated" + | "session.created" + | "session.updated" + | "session.compacted" + | "task.updated" + | "git.branch" + | "git.diff" + | "git.pr" + | "raw"; + + export type OpenClawEvent = { + version: 1; + id: string; + ts: number; + type: OpenClawEventType; + runId?: string; + sessionId?: string; + sessionKey?: string; + taskId?: string; + agentId?: string; + data: TData; + raw?: GatewayEvent; + }; + + export type AgentRunParams = { + input: string; + agentId?: string; + model?: string; + sessionId?: string; + sessionKey?: string; + deliver?: boolean; + timeoutMs?: number; + label?: string; + idempotencyKey?: string; + }; + + export type SessionCreateParams = { + key?: string; + agentId?: string; + label?: string; + model?: string; + parentSessionKey?: string; + task?: string; + message?: string; + }; + + export type SessionSendParams = { + key?: string; + message: string; + thinking?: string; + attachments?: unknown[]; + timeoutMs?: number; + idempotencyKey?: string; + }; + + export class Run { + readonly id: string; + events(filter?: (event: OpenClawEvent) => boolean): AsyncIterable; + wait(options?: { timeoutMs?: number }): Promise; + cancel(): Promise; + } + + export class Agent { + readonly id: string; + run(input: string | Omit): Promise; + } + + export class Session { + readonly key: string; + send(input: string | Omit): Promise; + abort(runId?: string): Promise; + } + + export class OpenClaw { + readonly agents: { + get(id: string): Promise; + }; + readonly runs: { + create(params: AgentRunParams): Promise; + wait(runId: string, options?: { timeoutMs?: number }): Promise; + cancel(runId: string, sessionKey?: string): Promise; + }; + readonly sessions: { + create(params?: SessionCreateParams): Promise; + }; + readonly models: { + status(params?: unknown): Promise; + }; + constructor(options?: OpenClawOptions); + close(): Promise; + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..91b6006 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,13 @@ +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + resolve: { + alias: { + "@openclaw/sdk": fileURLToPath(new URL("./test/shims/openclaw-sdk.ts", import.meta.url)), + }, + }, + test: { + include: ["test/**/*.test.ts"], + }, +});